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 */ 25namespace OCA\DAV\BackgroundJob; 26 27use OC\BackgroundJob\TimedJob; 28use OCA\DAV\CalDAV\CalDavBackend; 29use OCP\Calendar\BackendTemporarilyUnavailableException; 30use OCP\Calendar\IMetadataProvider; 31use OCP\Calendar\Resource\IBackend as IResourceBackend; 32use OCP\Calendar\Resource\IManager as IResourceManager; 33use OCP\Calendar\Resource\IResource; 34use OCP\Calendar\Room\IManager as IRoomManager; 35use OCP\Calendar\Room\IRoom; 36use OCP\IDBConnection; 37 38class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { 39 40 /** @var IResourceManager */ 41 private $resourceManager; 42 43 /** @var IRoomManager */ 44 private $roomManager; 45 46 /** @var IDBConnection */ 47 private $dbConnection; 48 49 /** @var CalDavBackend */ 50 private $calDavBackend; 51 52 /** 53 * UpdateCalendarResourcesRoomsBackgroundJob constructor. 54 * 55 * @param IResourceManager $resourceManager 56 * @param IRoomManager $roomManager 57 * @param IDBConnection $dbConnection 58 * @param CalDavBackend $calDavBackend 59 */ 60 public function __construct(IResourceManager $resourceManager, 61 IRoomManager $roomManager, 62 IDBConnection $dbConnection, 63 CalDavBackend $calDavBackend) { 64 $this->resourceManager = $resourceManager; 65 $this->roomManager = $roomManager; 66 $this->dbConnection = $dbConnection; 67 $this->calDavBackend = $calDavBackend; 68 69 // run once an hour 70 $this->setInterval(60 * 60); 71 } 72 73 /** 74 * @param $argument 75 */ 76 public function run($argument):void { 77 $this->runForBackend( 78 $this->resourceManager, 79 'calendar_resources', 80 'calendar_resources_md', 81 'resource_id', 82 'principals/calendar-resources' 83 ); 84 $this->runForBackend( 85 $this->roomManager, 86 'calendar_rooms', 87 'calendar_rooms_md', 88 'room_id', 89 'principals/calendar-rooms' 90 ); 91 } 92 93 /** 94 * Run background-job for one specific backendManager 95 * either ResourceManager or RoomManager 96 * 97 * @param IResourceManager|IRoomManager $backendManager 98 * @param string $dbTable 99 * @param string $dbTableMetadata 100 * @param string $foreignKey 101 * @param string $principalPrefix 102 */ 103 private function runForBackend($backendManager, 104 string $dbTable, 105 string $dbTableMetadata, 106 string $foreignKey, 107 string $principalPrefix):void { 108 $backends = $backendManager->getBackends(); 109 110 foreach ($backends as $backend) { 111 $backendId = $backend->getBackendIdentifier(); 112 113 try { 114 if ($backend instanceof IResourceBackend) { 115 $list = $backend->listAllResources(); 116 } else { 117 $list = $backend->listAllRooms(); 118 } 119 } catch (BackendTemporarilyUnavailableException $ex) { 120 continue; 121 } 122 123 $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); 124 $newIds = array_diff($list, $cachedList); 125 $deletedIds = array_diff($cachedList, $list); 126 $editedIds = array_intersect($list, $cachedList); 127 128 foreach ($newIds as $newId) { 129 try { 130 if ($backend instanceof IResourceBackend) { 131 $resource = $backend->getResource($newId); 132 } else { 133 $resource = $backend->getRoom($newId); 134 } 135 136 $metadata = []; 137 if ($resource instanceof IMetadataProvider) { 138 $metadata = $this->getAllMetadataOfBackend($resource); 139 } 140 } catch (BackendTemporarilyUnavailableException $ex) { 141 continue; 142 } 143 144 $id = $this->addToCache($dbTable, $backendId, $resource); 145 $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); 146 // we don't create the calendar here, it is created lazily 147 // when an event is actually scheduled with this resource / room 148 } 149 150 foreach ($deletedIds as $deletedId) { 151 $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); 152 $this->deleteFromCache($dbTable, $id); 153 $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); 154 155 $principalName = implode('-', [$backendId, $deletedId]); 156 $this->deleteCalendarDataForResource($principalPrefix, $principalName); 157 } 158 159 foreach ($editedIds as $editedId) { 160 $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); 161 162 try { 163 if ($backend instanceof IResourceBackend) { 164 $resource = $backend->getResource($editedId); 165 } else { 166 $resource = $backend->getRoom($editedId); 167 } 168 169 $metadata = []; 170 if ($resource instanceof IMetadataProvider) { 171 $metadata = $this->getAllMetadataOfBackend($resource); 172 } 173 } catch (BackendTemporarilyUnavailableException $ex) { 174 continue; 175 } 176 177 $this->updateCache($dbTable, $id, $resource); 178 179 if ($resource instanceof IMetadataProvider) { 180 $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); 181 $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); 182 } 183 } 184 } 185 } 186 187 /** 188 * add entry to cache that exists remotely but not yet in cache 189 * 190 * @param string $table 191 * @param string $backendId 192 * @param IResource|IRoom $remote 193 * @return int Insert id 194 */ 195 private function addToCache(string $table, 196 string $backendId, 197 $remote):int { 198 $query = $this->dbConnection->getQueryBuilder(); 199 $query->insert($table) 200 ->values([ 201 'backend_id' => $query->createNamedParameter($backendId), 202 'resource_id' => $query->createNamedParameter($remote->getId()), 203 'email' => $query->createNamedParameter($remote->getEMail()), 204 'displayname' => $query->createNamedParameter($remote->getDisplayName()), 205 'group_restrictions' => $query->createNamedParameter( 206 $this->serializeGroupRestrictions( 207 $remote->getGroupRestrictions() 208 )) 209 ]) 210 ->execute(); 211 return $query->getLastInsertId(); 212 } 213 214 /** 215 * @param string $table 216 * @param string $foreignKey 217 * @param int $foreignId 218 * @param array $metadata 219 */ 220 private function addMetadataToCache(string $table, 221 string $foreignKey, 222 int $foreignId, 223 array $metadata):void { 224 foreach ($metadata as $key => $value) { 225 $query = $this->dbConnection->getQueryBuilder(); 226 $query->insert($table) 227 ->values([ 228 $foreignKey => $query->createNamedParameter($foreignId), 229 'key' => $query->createNamedParameter($key), 230 'value' => $query->createNamedParameter($value), 231 ]) 232 ->execute(); 233 } 234 } 235 236 /** 237 * delete entry from cache that does not exist anymore remotely 238 * 239 * @param string $table 240 * @param int $id 241 */ 242 private function deleteFromCache(string $table, 243 int $id):void { 244 $query = $this->dbConnection->getQueryBuilder(); 245 $query->delete($table) 246 ->where($query->expr()->eq('id', $query->createNamedParameter($id))) 247 ->execute(); 248 } 249 250 /** 251 * @param string $table 252 * @param string $foreignKey 253 * @param int $id 254 */ 255 private function deleteMetadataFromCache(string $table, 256 string $foreignKey, 257 int $id):void { 258 $query = $this->dbConnection->getQueryBuilder(); 259 $query->delete($table) 260 ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) 261 ->execute(); 262 } 263 264 /** 265 * update an existing entry in cache 266 * 267 * @param string $table 268 * @param int $id 269 * @param IResource|IRoom $remote 270 */ 271 private function updateCache(string $table, 272 int $id, 273 $remote):void { 274 $query = $this->dbConnection->getQueryBuilder(); 275 $query->update($table) 276 ->set('email', $query->createNamedParameter($remote->getEMail())) 277 ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) 278 ->set('group_restrictions', $query->createNamedParameter( 279 $this->serializeGroupRestrictions( 280 $remote->getGroupRestrictions() 281 ))) 282 ->where($query->expr()->eq('id', $query->createNamedParameter($id))) 283 ->execute(); 284 } 285 286 /** 287 * @param string $dbTable 288 * @param string $foreignKey 289 * @param int $id 290 * @param array $metadata 291 * @param array $cachedMetadata 292 */ 293 private function updateMetadataCache(string $dbTable, 294 string $foreignKey, 295 int $id, 296 array $metadata, 297 array $cachedMetadata):void { 298 $newMetadata = array_diff_key($metadata, $cachedMetadata); 299 $deletedMetadata = array_diff_key($cachedMetadata, $metadata); 300 301 foreach ($newMetadata as $key => $value) { 302 $query = $this->dbConnection->getQueryBuilder(); 303 $query->insert($dbTable) 304 ->values([ 305 $foreignKey => $query->createNamedParameter($id), 306 'key' => $query->createNamedParameter($key), 307 'value' => $query->createNamedParameter($value), 308 ]) 309 ->execute(); 310 } 311 312 foreach ($deletedMetadata as $key => $value) { 313 $query = $this->dbConnection->getQueryBuilder(); 314 $query->delete($dbTable) 315 ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) 316 ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) 317 ->execute(); 318 } 319 320 $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); 321 foreach ($existingKeys as $existingKey) { 322 if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { 323 $query = $this->dbConnection->getQueryBuilder(); 324 $query->update($dbTable) 325 ->set('value', $query->createNamedParameter($metadata[$existingKey])) 326 ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) 327 ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) 328 ->execute(); 329 } 330 } 331 } 332 333 /** 334 * serialize array of group restrictions to store them in database 335 * 336 * @param array $groups 337 * @return string 338 */ 339 private function serializeGroupRestrictions(array $groups):string { 340 return \json_encode($groups); 341 } 342 343 /** 344 * Gets all metadata of a backend 345 * 346 * @param IResource|IRoom $resource 347 * @return array 348 */ 349 private function getAllMetadataOfBackend($resource):array { 350 if (!($resource instanceof IMetadataProvider)) { 351 return []; 352 } 353 354 $keys = $resource->getAllAvailableMetadataKeys(); 355 $metadata = []; 356 foreach ($keys as $key) { 357 $metadata[$key] = $resource->getMetadataForKey($key); 358 } 359 360 return $metadata; 361 } 362 363 /** 364 * @param string $table 365 * @param string $foreignKey 366 * @param int $id 367 * @return array 368 */ 369 private function getAllMetadataOfCache(string $table, 370 string $foreignKey, 371 int $id):array { 372 $query = $this->dbConnection->getQueryBuilder(); 373 $query->select(['key', 'value']) 374 ->from($table) 375 ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); 376 $stmt = $query->execute(); 377 $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); 378 379 $metadata = []; 380 foreach ($rows as $row) { 381 $metadata[$row['key']] = $row['value']; 382 } 383 384 return $metadata; 385 } 386 387 /** 388 * Gets all cached rooms / resources by backend 389 * 390 * @param $tableName 391 * @param $backendId 392 * @return array 393 */ 394 private function getAllCachedByBackend(string $tableName, 395 string $backendId):array { 396 $query = $this->dbConnection->getQueryBuilder(); 397 $query->select('resource_id') 398 ->from($tableName) 399 ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); 400 $stmt = $query->execute(); 401 402 return array_map(function ($row): string { 403 return $row['resource_id']; 404 }, $stmt->fetchAll()); 405 } 406 407 /** 408 * @param $principalPrefix 409 * @param $principalUri 410 */ 411 private function deleteCalendarDataForResource(string $principalPrefix, 412 string $principalUri):void { 413 $calendar = $this->calDavBackend->getCalendarByUri( 414 implode('/', [$principalPrefix, $principalUri]), 415 CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); 416 417 if ($calendar !== null) { 418 $this->calDavBackend->deleteCalendar( 419 $calendar['id'], 420 true // Because this wasn't deleted by a user 421 ); 422 } 423 } 424 425 /** 426 * @param $table 427 * @param $backendId 428 * @param $resourceId 429 * @return int 430 */ 431 private function getIdForBackendAndResource(string $table, 432 string $backendId, 433 string $resourceId):int { 434 $query = $this->dbConnection->getQueryBuilder(); 435 $query->select('id') 436 ->from($table) 437 ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) 438 ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); 439 $stmt = $query->execute(); 440 441 return $stmt->fetch()['id']; 442 } 443} 444