ResourcesRoomsUpdater.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Calendar;
  8. use OCA\DAV\CalDAV\CalDavBackend;
  9. use OCP\Calendar\BackendTemporarilyUnavailableException;
  10. use OCP\Calendar\IMetadataProvider;
  11. use OCP\Calendar\Resource\IBackend as IResourceBackend;
  12. use OCP\Calendar\Resource\IManager as IResourceManager;
  13. use OCP\Calendar\Resource\IResource;
  14. use OCP\Calendar\Room\IManager as IRoomManager;
  15. use OCP\Calendar\Room\IRoom;
  16. use OCP\IDBConnection;
  17. use Psr\Container\ContainerInterface;
  18. class ResourcesRoomsUpdater {
  19. public function __construct(
  20. private ContainerInterface $container,
  21. private IDBConnection $dbConnection,
  22. private CalDavBackend $calDavBackend,
  23. ) {
  24. }
  25. /**
  26. * Update resource cache from backends
  27. */
  28. public function updateResources(): void {
  29. $this->updateFromBackend(
  30. $this->container->get(IResourceManager::class),
  31. 'calendar_resources',
  32. 'calendar_resources_md',
  33. 'resource_id',
  34. 'principals/calendar-resources'
  35. );
  36. }
  37. /**
  38. * Update room cache from backends
  39. */
  40. public function updateRooms(): void {
  41. $this->updateFromBackend(
  42. $this->container->get(IRoomManager::class),
  43. 'calendar_rooms',
  44. 'calendar_rooms_md',
  45. 'room_id',
  46. 'principals/calendar-rooms'
  47. );
  48. }
  49. /**
  50. * Update cache from one specific backend manager, either ResourceManager or RoomManager
  51. *
  52. * @param IResourceManager|IRoomManager $backendManager
  53. */
  54. private function updateFromBackend($backendManager,
  55. string $dbTable,
  56. string $dbTableMetadata,
  57. string $foreignKey,
  58. string $principalPrefix): void {
  59. $backends = $backendManager->getBackends();
  60. foreach ($backends as $backend) {
  61. $backendId = $backend->getBackendIdentifier();
  62. try {
  63. if ($backend instanceof IResourceBackend) {
  64. $list = $backend->listAllResources();
  65. } else {
  66. $list = $backend->listAllRooms();
  67. }
  68. } catch (BackendTemporarilyUnavailableException $ex) {
  69. continue;
  70. }
  71. $cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
  72. $newIds = array_diff($list, $cachedList);
  73. $deletedIds = array_diff($cachedList, $list);
  74. $editedIds = array_intersect($list, $cachedList);
  75. foreach ($newIds as $newId) {
  76. try {
  77. if ($backend instanceof IResourceBackend) {
  78. $resource = $backend->getResource($newId);
  79. } else {
  80. $resource = $backend->getRoom($newId);
  81. }
  82. $metadata = [];
  83. if ($resource instanceof IMetadataProvider) {
  84. $metadata = $this->getAllMetadataOfBackend($resource);
  85. }
  86. } catch (BackendTemporarilyUnavailableException $ex) {
  87. continue;
  88. }
  89. $id = $this->addToCache($dbTable, $backendId, $resource);
  90. $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
  91. // we don't create the calendar here, it is created lazily
  92. // when an event is actually scheduled with this resource / room
  93. }
  94. foreach ($deletedIds as $deletedId) {
  95. $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
  96. $this->deleteFromCache($dbTable, $id);
  97. $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
  98. $principalName = implode('-', [$backendId, $deletedId]);
  99. $this->deleteCalendarDataForResource($principalPrefix, $principalName);
  100. }
  101. foreach ($editedIds as $editedId) {
  102. $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
  103. try {
  104. if ($backend instanceof IResourceBackend) {
  105. $resource = $backend->getResource($editedId);
  106. } else {
  107. $resource = $backend->getRoom($editedId);
  108. }
  109. $metadata = [];
  110. if ($resource instanceof IMetadataProvider) {
  111. $metadata = $this->getAllMetadataOfBackend($resource);
  112. }
  113. } catch (BackendTemporarilyUnavailableException $ex) {
  114. continue;
  115. }
  116. $this->updateCache($dbTable, $id, $resource);
  117. if ($resource instanceof IMetadataProvider) {
  118. $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
  119. $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
  120. }
  121. }
  122. }
  123. }
  124. /**
  125. * add entry to cache that exists remotely but not yet in cache
  126. *
  127. * @param string $table
  128. * @param string $backendId
  129. * @param IResource|IRoom $remote
  130. *
  131. * @return int Insert id
  132. */
  133. private function addToCache(string $table,
  134. string $backendId,
  135. $remote): int {
  136. $query = $this->dbConnection->getQueryBuilder();
  137. $query->insert($table)
  138. ->values([
  139. 'backend_id' => $query->createNamedParameter($backendId),
  140. 'resource_id' => $query->createNamedParameter($remote->getId()),
  141. 'email' => $query->createNamedParameter($remote->getEMail()),
  142. 'displayname' => $query->createNamedParameter($remote->getDisplayName()),
  143. 'group_restrictions' => $query->createNamedParameter(
  144. $this->serializeGroupRestrictions(
  145. $remote->getGroupRestrictions()
  146. ))
  147. ])
  148. ->executeStatement();
  149. return $query->getLastInsertId();
  150. }
  151. /**
  152. * @param string $table
  153. * @param string $foreignKey
  154. * @param int $foreignId
  155. * @param array $metadata
  156. */
  157. private function addMetadataToCache(string $table,
  158. string $foreignKey,
  159. int $foreignId,
  160. array $metadata): void {
  161. foreach ($metadata as $key => $value) {
  162. $query = $this->dbConnection->getQueryBuilder();
  163. $query->insert($table)
  164. ->values([
  165. $foreignKey => $query->createNamedParameter($foreignId),
  166. 'key' => $query->createNamedParameter($key),
  167. 'value' => $query->createNamedParameter($value),
  168. ])
  169. ->executeStatement();
  170. }
  171. }
  172. /**
  173. * delete entry from cache that does not exist anymore remotely
  174. *
  175. * @param string $table
  176. * @param int $id
  177. */
  178. private function deleteFromCache(string $table,
  179. int $id): void {
  180. $query = $this->dbConnection->getQueryBuilder();
  181. $query->delete($table)
  182. ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
  183. ->executeStatement();
  184. }
  185. /**
  186. * @param string $table
  187. * @param string $foreignKey
  188. * @param int $id
  189. */
  190. private function deleteMetadataFromCache(string $table,
  191. string $foreignKey,
  192. int $id): void {
  193. $query = $this->dbConnection->getQueryBuilder();
  194. $query->delete($table)
  195. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  196. ->executeStatement();
  197. }
  198. /**
  199. * update an existing entry in cache
  200. *
  201. * @param string $table
  202. * @param int $id
  203. * @param IResource|IRoom $remote
  204. */
  205. private function updateCache(string $table,
  206. int $id,
  207. $remote): void {
  208. $query = $this->dbConnection->getQueryBuilder();
  209. $query->update($table)
  210. ->set('email', $query->createNamedParameter($remote->getEMail()))
  211. ->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
  212. ->set('group_restrictions', $query->createNamedParameter(
  213. $this->serializeGroupRestrictions(
  214. $remote->getGroupRestrictions()
  215. )))
  216. ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
  217. ->executeStatement();
  218. }
  219. /**
  220. * @param string $dbTable
  221. * @param string $foreignKey
  222. * @param int $id
  223. * @param array $metadata
  224. * @param array $cachedMetadata
  225. */
  226. private function updateMetadataCache(string $dbTable,
  227. string $foreignKey,
  228. int $id,
  229. array $metadata,
  230. array $cachedMetadata): void {
  231. $newMetadata = array_diff_key($metadata, $cachedMetadata);
  232. $deletedMetadata = array_diff_key($cachedMetadata, $metadata);
  233. foreach ($newMetadata as $key => $value) {
  234. $query = $this->dbConnection->getQueryBuilder();
  235. $query->insert($dbTable)
  236. ->values([
  237. $foreignKey => $query->createNamedParameter($id),
  238. 'key' => $query->createNamedParameter($key),
  239. 'value' => $query->createNamedParameter($value),
  240. ])
  241. ->executeStatement();
  242. }
  243. foreach ($deletedMetadata as $key => $value) {
  244. $query = $this->dbConnection->getQueryBuilder();
  245. $query->delete($dbTable)
  246. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  247. ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
  248. ->executeStatement();
  249. }
  250. $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
  251. foreach ($existingKeys as $existingKey) {
  252. if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
  253. $query = $this->dbConnection->getQueryBuilder();
  254. $query->update($dbTable)
  255. ->set('value', $query->createNamedParameter($metadata[$existingKey]))
  256. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  257. ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
  258. ->executeStatement();
  259. }
  260. }
  261. }
  262. /**
  263. * serialize array of group restrictions to store them in database
  264. *
  265. * @param array $groups
  266. *
  267. * @return string
  268. */
  269. private function serializeGroupRestrictions(array $groups): string {
  270. return \json_encode($groups, JSON_THROW_ON_ERROR);
  271. }
  272. /**
  273. * Gets all metadata of a backend
  274. *
  275. * @param IResource|IRoom $resource
  276. *
  277. * @return array
  278. */
  279. private function getAllMetadataOfBackend($resource): array {
  280. if (!($resource instanceof IMetadataProvider)) {
  281. return [];
  282. }
  283. $keys = $resource->getAllAvailableMetadataKeys();
  284. $metadata = [];
  285. foreach ($keys as $key) {
  286. $metadata[$key] = $resource->getMetadataForKey($key);
  287. }
  288. return $metadata;
  289. }
  290. /**
  291. * @param string $table
  292. * @param string $foreignKey
  293. * @param int $id
  294. *
  295. * @return array
  296. */
  297. private function getAllMetadataOfCache(string $table,
  298. string $foreignKey,
  299. int $id): array {
  300. $query = $this->dbConnection->getQueryBuilder();
  301. $query->select(['key', 'value'])
  302. ->from($table)
  303. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
  304. $result = $query->executeQuery();
  305. $rows = $result->fetchAll();
  306. $result->closeCursor();
  307. $metadata = [];
  308. foreach ($rows as $row) {
  309. $metadata[$row['key']] = $row['value'];
  310. }
  311. return $metadata;
  312. }
  313. /**
  314. * Gets all cached rooms / resources by backend
  315. *
  316. * @param $tableName
  317. * @param $backendId
  318. *
  319. * @return array
  320. */
  321. private function getAllCachedByBackend(string $tableName,
  322. string $backendId): array {
  323. $query = $this->dbConnection->getQueryBuilder();
  324. $query->select('resource_id')
  325. ->from($tableName)
  326. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
  327. $result = $query->executeQuery();
  328. $rows = $result->fetchAll();
  329. $result->closeCursor();
  330. return array_map(function ($row): string {
  331. return $row['resource_id'];
  332. }, $rows);
  333. }
  334. /**
  335. * @param $principalPrefix
  336. * @param $principalUri
  337. */
  338. private function deleteCalendarDataForResource(string $principalPrefix,
  339. string $principalUri): void {
  340. $calendar = $this->calDavBackend->getCalendarByUri(
  341. implode('/', [$principalPrefix, $principalUri]),
  342. CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
  343. if ($calendar !== null) {
  344. $this->calDavBackend->deleteCalendar(
  345. $calendar['id'],
  346. true // Because this wasn't deleted by a user
  347. );
  348. }
  349. }
  350. /**
  351. * @param $table
  352. * @param $backendId
  353. * @param $resourceId
  354. *
  355. * @return int
  356. */
  357. private function getIdForBackendAndResource(string $table,
  358. string $backendId,
  359. string $resourceId): int {
  360. $query = $this->dbConnection->getQueryBuilder();
  361. $query->select('id')
  362. ->from($table)
  363. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  364. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  365. $result = $query->executeQuery();
  366. $id = (int)$result->fetchOne();
  367. $result->closeCursor();
  368. return $id;
  369. }
  370. }