You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UpdateCalendarResourcesRoomsBackgroundJob.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <?php
  2. /**
  3. * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Georg Ehrke <oc.list@georgehrke.com>
  7. * @author Roeland Jago Douma <roeland@famdouma.nl>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OCA\DAV\BackgroundJob;
  26. use OC\BackgroundJob\TimedJob;
  27. use OCA\DAV\CalDAV\CalDavBackend;
  28. use OCP\Calendar\BackendTemporarilyUnavailableException;
  29. use OCP\Calendar\IMetadataProvider;
  30. use OCP\Calendar\Resource\IBackend as IResourceBackend;
  31. use OCP\Calendar\Resource\IManager as IResourceManager;
  32. use OCP\Calendar\Resource\IResource;
  33. use OCP\Calendar\Room\IManager as IRoomManager;
  34. use OCP\Calendar\Room\IRoom;
  35. use OCP\IDBConnection;
  36. class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
  37. /** @var IResourceManager */
  38. private $resourceManager;
  39. /** @var IRoomManager */
  40. private $roomManager;
  41. /** @var IDBConnection */
  42. private $dbConnection;
  43. /** @var CalDavBackend */
  44. private $calDavBackend;
  45. /**
  46. * UpdateCalendarResourcesRoomsBackgroundJob constructor.
  47. *
  48. * @param IResourceManager $resourceManager
  49. * @param IRoomManager $roomManager
  50. * @param IDBConnection $dbConnection
  51. * @param CalDavBackend $calDavBackend
  52. */
  53. public function __construct(IResourceManager $resourceManager,
  54. IRoomManager $roomManager,
  55. IDBConnection $dbConnection,
  56. CalDavBackend $calDavBackend) {
  57. $this->resourceManager = $resourceManager;
  58. $this->roomManager = $roomManager;
  59. $this->dbConnection = $dbConnection;
  60. $this->calDavBackend = $calDavBackend;
  61. // run once an hour
  62. $this->setInterval(60 * 60);
  63. }
  64. /**
  65. * @param $argument
  66. */
  67. public function run($argument):void {
  68. $this->runForBackend(
  69. $this->resourceManager,
  70. 'calendar_resources',
  71. 'calendar_resources_md',
  72. 'resource_id',
  73. 'principals/calendar-resources'
  74. );
  75. $this->runForBackend(
  76. $this->roomManager,
  77. 'calendar_rooms',
  78. 'calendar_rooms_md',
  79. 'room_id',
  80. 'principals/calendar-rooms'
  81. );
  82. }
  83. /**
  84. * Run background-job for one specific backendManager
  85. * either ResourceManager or RoomManager
  86. *
  87. * @param IResourceManager|IRoomManager $backendManager
  88. * @param string $dbTable
  89. * @param string $dbTableMetadata
  90. * @param string $foreignKey
  91. * @param string $principalPrefix
  92. */
  93. private function runForBackend($backendManager,
  94. string $dbTable,
  95. string $dbTableMetadata,
  96. string $foreignKey,
  97. string $principalPrefix):void {
  98. $backends = $backendManager->getBackends();
  99. foreach ($backends as $backend) {
  100. $backendId = $backend->getBackendIdentifier();
  101. try {
  102. if ($backend instanceof IResourceBackend) {
  103. $list = $backend->listAllResources();
  104. } else {
  105. $list = $backend->listAllRooms();
  106. }
  107. } catch (BackendTemporarilyUnavailableException $ex) {
  108. continue;
  109. }
  110. $cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
  111. $newIds = array_diff($list, $cachedList);
  112. $deletedIds = array_diff($cachedList, $list);
  113. $editedIds = array_intersect($list, $cachedList);
  114. foreach ($newIds as $newId) {
  115. try {
  116. if ($backend instanceof IResourceBackend) {
  117. $resource = $backend->getResource($newId);
  118. } else {
  119. $resource = $backend->getRoom($newId);
  120. }
  121. $metadata = [];
  122. if ($resource instanceof IMetadataProvider) {
  123. $metadata = $this->getAllMetadataOfBackend($resource);
  124. }
  125. } catch (BackendTemporarilyUnavailableException $ex) {
  126. continue;
  127. }
  128. $id = $this->addToCache($dbTable, $backendId, $resource);
  129. $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
  130. // we don't create the calendar here, it is created lazily
  131. // when an event is actually scheduled with this resource / room
  132. }
  133. foreach ($deletedIds as $deletedId) {
  134. $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
  135. $this->deleteFromCache($dbTable, $id);
  136. $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
  137. $principalName = implode('-', [$backendId, $deletedId]);
  138. $this->deleteCalendarDataForResource($principalPrefix, $principalName);
  139. }
  140. foreach ($editedIds as $editedId) {
  141. $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
  142. try {
  143. if ($backend instanceof IResourceBackend) {
  144. $resource = $backend->getResource($editedId);
  145. } else {
  146. $resource = $backend->getRoom($editedId);
  147. }
  148. $metadata = [];
  149. if ($resource instanceof IMetadataProvider) {
  150. $metadata = $this->getAllMetadataOfBackend($resource);
  151. }
  152. } catch (BackendTemporarilyUnavailableException $ex) {
  153. continue;
  154. }
  155. $this->updateCache($dbTable, $id, $resource);
  156. if ($resource instanceof IMetadataProvider) {
  157. $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
  158. $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
  159. }
  160. }
  161. }
  162. }
  163. /**
  164. * add entry to cache that exists remotely but not yet in cache
  165. *
  166. * @param string $table
  167. * @param string $backendId
  168. * @param IResource|IRoom $remote
  169. * @return int Insert id
  170. */
  171. private function addToCache(string $table,
  172. string $backendId,
  173. $remote):int {
  174. $query = $this->dbConnection->getQueryBuilder();
  175. $query->insert($table)
  176. ->values([
  177. 'backend_id' => $query->createNamedParameter($backendId),
  178. 'resource_id' => $query->createNamedParameter($remote->getId()),
  179. 'email' => $query->createNamedParameter($remote->getEMail()),
  180. 'displayname' => $query->createNamedParameter($remote->getDisplayName()),
  181. 'group_restrictions' => $query->createNamedParameter(
  182. $this->serializeGroupRestrictions(
  183. $remote->getGroupRestrictions()
  184. ))
  185. ])
  186. ->execute();
  187. return $query->getLastInsertId();
  188. }
  189. /**
  190. * @param string $table
  191. * @param string $foreignKey
  192. * @param int $foreignId
  193. * @param array $metadata
  194. */
  195. private function addMetadataToCache(string $table,
  196. string $foreignKey,
  197. int $foreignId,
  198. array $metadata):void {
  199. foreach ($metadata as $key => $value) {
  200. $query = $this->dbConnection->getQueryBuilder();
  201. $query->insert($table)
  202. ->values([
  203. $foreignKey => $query->createNamedParameter($foreignId),
  204. 'key' => $query->createNamedParameter($key),
  205. 'value' => $query->createNamedParameter($value),
  206. ])
  207. ->execute();
  208. }
  209. }
  210. /**
  211. * delete entry from cache that does not exist anymore remotely
  212. *
  213. * @param string $table
  214. * @param int $id
  215. */
  216. private function deleteFromCache(string $table,
  217. int $id):void {
  218. $query = $this->dbConnection->getQueryBuilder();
  219. $query->delete($table)
  220. ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
  221. ->execute();
  222. }
  223. /**
  224. * @param string $table
  225. * @param string $foreignKey
  226. * @param int $id
  227. */
  228. private function deleteMetadataFromCache(string $table,
  229. string $foreignKey,
  230. int $id):void {
  231. $query = $this->dbConnection->getQueryBuilder();
  232. $query->delete($table)
  233. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  234. ->execute();
  235. }
  236. /**
  237. * update an existing entry in cache
  238. *
  239. * @param string $table
  240. * @param int $id
  241. * @param IResource|IRoom $remote
  242. */
  243. private function updateCache(string $table,
  244. int $id,
  245. $remote):void {
  246. $query = $this->dbConnection->getQueryBuilder();
  247. $query->update($table)
  248. ->set('email', $query->createNamedParameter($remote->getEMail()))
  249. ->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
  250. ->set('group_restrictions', $query->createNamedParameter(
  251. $this->serializeGroupRestrictions(
  252. $remote->getGroupRestrictions()
  253. )))
  254. ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
  255. ->execute();
  256. }
  257. /**
  258. * @param string $dbTable
  259. * @param string $foreignKey
  260. * @param int $id
  261. * @param array $metadata
  262. * @param array $cachedMetadata
  263. */
  264. private function updateMetadataCache(string $dbTable,
  265. string $foreignKey,
  266. int $id,
  267. array $metadata,
  268. array $cachedMetadata):void {
  269. $newMetadata = array_diff_key($metadata, $cachedMetadata);
  270. $deletedMetadata = array_diff_key($cachedMetadata, $metadata);
  271. foreach ($newMetadata as $key => $value) {
  272. $query = $this->dbConnection->getQueryBuilder();
  273. $query->insert($dbTable)
  274. ->values([
  275. $foreignKey => $query->createNamedParameter($id),
  276. 'key' => $query->createNamedParameter($key),
  277. 'value' => $query->createNamedParameter($value),
  278. ])
  279. ->execute();
  280. }
  281. foreach ($deletedMetadata as $key => $value) {
  282. $query = $this->dbConnection->getQueryBuilder();
  283. $query->delete($dbTable)
  284. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  285. ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
  286. ->execute();
  287. }
  288. $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
  289. foreach ($existingKeys as $existingKey) {
  290. if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
  291. $query = $this->dbConnection->getQueryBuilder();
  292. $query->update($dbTable)
  293. ->set('value', $query->createNamedParameter($metadata[$existingKey]))
  294. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
  295. ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
  296. ->execute();
  297. }
  298. }
  299. }
  300. /**
  301. * serialize array of group restrictions to store them in database
  302. *
  303. * @param array $groups
  304. * @return string
  305. */
  306. private function serializeGroupRestrictions(array $groups):string {
  307. return \json_encode($groups);
  308. }
  309. /**
  310. * Gets all metadata of a backend
  311. *
  312. * @param IResource|IRoom $resource
  313. * @return array
  314. */
  315. private function getAllMetadataOfBackend($resource):array {
  316. if (!($resource instanceof IMetadataProvider)) {
  317. return [];
  318. }
  319. $keys = $resource->getAllAvailableMetadataKeys();
  320. $metadata = [];
  321. foreach ($keys as $key) {
  322. $metadata[$key] = $resource->getMetadataForKey($key);
  323. }
  324. return $metadata;
  325. }
  326. /**
  327. * @param string $table
  328. * @param string $foreignKey
  329. * @param int $id
  330. * @return array
  331. */
  332. private function getAllMetadataOfCache(string $table,
  333. string $foreignKey,
  334. int $id):array {
  335. $query = $this->dbConnection->getQueryBuilder();
  336. $query->select(['key', 'value'])
  337. ->from($table)
  338. ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
  339. $stmt = $query->execute();
  340. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
  341. $metadata = [];
  342. foreach ($rows as $row) {
  343. $metadata[$row['key']] = $row['value'];
  344. }
  345. return $metadata;
  346. }
  347. /**
  348. * Gets all cached rooms / resources by backend
  349. *
  350. * @param $tableName
  351. * @param $backendId
  352. * @return array
  353. */
  354. private function getAllCachedByBackend(string $tableName,
  355. string $backendId):array {
  356. $query = $this->dbConnection->getQueryBuilder();
  357. $query->select('resource_id')
  358. ->from($tableName)
  359. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
  360. $stmt = $query->execute();
  361. return array_map(function ($row): string {
  362. return $row['resource_id'];
  363. }, $stmt->fetchAll());
  364. }
  365. /**
  366. * @param $principalPrefix
  367. * @param $principalUri
  368. */
  369. private function deleteCalendarDataForResource(string $principalPrefix,
  370. string $principalUri):void {
  371. $calendar = $this->calDavBackend->getCalendarByUri(
  372. implode('/', [$principalPrefix, $principalUri]),
  373. CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
  374. if ($calendar !== null) {
  375. $this->calDavBackend->deleteCalendar(
  376. $calendar['id'],
  377. true // Because this wasn't deleted by a user
  378. );
  379. }
  380. }
  381. /**
  382. * @param $table
  383. * @param $backendId
  384. * @param $resourceId
  385. * @return int
  386. */
  387. private function getIdForBackendAndResource(string $table,
  388. string $backendId,
  389. string $resourceId):int {
  390. $query = $this->dbConnection->getQueryBuilder();
  391. $query->select('id')
  392. ->from($table)
  393. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  394. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  395. $stmt = $query->execute();
  396. return $stmt->fetch()['id'];
  397. }
  398. }