Completed
Push — master ( e6780c...89b6ee )
by Morris
33:24 queued 16:36
created

addToCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright 2018, Georg Ehrke <[email protected]>
4
 *
5
 * @author Georg Ehrke <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\DAV\BackgroundJob;
25
26
use OC\BackgroundJob\TimedJob;
27
use OCA\DAV\CalDAV\CalDavBackend;
28
use OCP\Calendar\BackendTemporarilyUnavailableException;
29
use OCP\Calendar\Resource\IManager as IResourceManager;
30
use OCP\Calendar\Resource\IResource;
31
use OCP\Calendar\Room\IManager as IRoomManager;
32
use OCP\Calendar\Room\IRoom;
33
use OCP\IDBConnection;
34
35
class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
36
37
	/** @var IResourceManager */
38
	private $resourceManager;
39
40
	/** @var IRoomManager */
41
	private $roomManager;
42
43
	/** @var IDBConnection */
44
	private $db;
45
46
	/** @var CalDavBackend */
47
	private $calDavBackend;
48
49
	/** @var string */
50
	private $resourceDbTable;
51
52
	/** @var string */
53
	private $resourcePrincipalUri;
54
55
	/** @var string */
56
	private $roomDbTable;
57
58
	/** @var string */
59
	private $roomPrincipalUri;
60
61
	/**
62
	 * UpdateCalendarResourcesRoomsBackgroundJob constructor.
63
	 *
64
	 * @param IResourceManager $resourceManager
65
	 * @param IRoomManager $roomManager
66
	 * @param IDBConnection $dbConnection
67
	 * @param CalDavBackend $calDavBackend
68
	 */
69
	public function __construct(IResourceManager $resourceManager, IRoomManager $roomManager,
70
								IDBConnection $dbConnection, CalDavBackend $calDavBackend) {
71
		$this->resourceManager = $resourceManager;
72
		$this->roomManager = $roomManager;
73
		$this->db = $dbConnection;
74
		$this->calDavBackend = $calDavBackend;
75
		$this->resourceDbTable = 'calendar_resources_cache';
76
		$this->resourcePrincipalUri = 'principals/calendar-resources';
77
		$this->roomDbTable = 'calendar_rooms_cache';
78
		$this->roomPrincipalUri = 'principals/calendar-rooms';
79
80
		// run once an hour
81
		$this->setInterval(60 * 60);
82
	}
83
84
	/**
85
	 * @param $argument
86
	 */
87
	public function run($argument) {
88
		$this->runResources();
89
		$this->runRooms();
90
	}
91
92
	/**
93
	 * run timed job for resources
94
	 */
95 View Code Duplication
	private function runResources() {
96
		$resourceBackends = $this->resourceManager->getBackends();
97
		$cachedResources = $this->getCached($this->resourceDbTable);
98
		$cachedResourceIds = $this->getCachedResourceIds($cachedResources);
99
100
		$remoteResourceIds = [];
101
		foreach($resourceBackends as $resourceBackend) {
102
			try {
103
				$remoteResourceIds[$resourceBackend->getBackendIdentifier()] =
104
					$resourceBackend->listAllResources();
105
			} catch(BackendTemporarilyUnavailableException $ex) {
106
				// If the backend is temporarily unavailable
107
				// ignore this backend in this execution
108
				unset($cachedResourceIds[$resourceBackend->getBackendIdentifier()]);
109
			}
110
		}
111
112
		$sortedResources = $this->sortByNewDeletedExisting($cachedResourceIds, $remoteResourceIds);
113
114
		foreach($sortedResources['new'] as $backendId => $newResources) {
115
			foreach ($newResources as $newResource) {
116
				$resource = $this->resourceManager->getBackend($backendId)
117
					->getResource($newResource);
118
				$this->addToCache($this->resourceDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $this->resourceManager->...tResource($newResource) on line 116 can be null; however, OCA\DAV\BackgroundJob\Up...groundJob::addToCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
119
			}
120
		}
121
		foreach($sortedResources['deleted'] as $backendId => $deletedResources) {
122
			foreach ($deletedResources as $deletedResource) {
123
				$this->deleteFromCache($this->resourceDbTable,
124
					$this->resourcePrincipalUri, $backendId, $deletedResource);
125
			}
126
		}
127
		foreach($sortedResources['edited'] as $backendId => $editedResources) {
128
			foreach ($editedResources as $editedResource) {
129
				$resource = $this->resourceManager->getBackend($backendId)
130
					->getResource($editedResource);
131
				$this->updateCache($this->resourceDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $this->resourceManager->...source($editedResource) on line 129 can be null; however, OCA\DAV\BackgroundJob\Up...roundJob::updateCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
132
			}
133
		}
134
	}
135
136
	/**
137
	 * run timed job for rooms
138
	 */
139 View Code Duplication
	private function runRooms() {
140
		$roomBackends = $this->roomManager->getBackends();
141
		$cachedRooms = $this->getCached($this->roomDbTable);
142
		$cachedRoomIds = $this->getCachedRoomIds($cachedRooms);
143
144
		$remoteRoomIds = [];
145
		foreach($roomBackends as $roomBackend) {
146
			try {
147
				$remoteRoomIds[$roomBackend->getBackendIdentifier()] =
148
					$roomBackend->listAllRooms();
149
			} catch(BackendTemporarilyUnavailableException $ex) {
150
				// If the backend is temporarily unavailable
151
				// ignore this backend in this execution
152
				unset($cachedRoomIds[$roomBackend->getBackendIdentifier()]);
153
			}
154
		}
155
156
		$sortedRooms = $this->sortByNewDeletedExisting($cachedRoomIds, $remoteRoomIds);
157
158
		foreach($sortedRooms['new'] as $backendId => $newRooms) {
159
			foreach ($newRooms as $newRoom) {
160
				$resource = $this->roomManager->getBackend($backendId)
161
					->getRoom($newRoom);
162
				$this->addToCache($this->roomDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $this->roomManager->getB...dId)->getRoom($newRoom) on line 160 can be null; however, OCA\DAV\BackgroundJob\Up...groundJob::addToCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
163
			}
164
		}
165
		foreach($sortedRooms['deleted'] as $backendId => $deletedRooms) {
166
			foreach ($deletedRooms as $deletedRoom) {
167
				$this->deleteFromCache($this->roomDbTable,
168
					$this->roomPrincipalUri, $backendId, $deletedRoom);
169
			}
170
		}
171
		foreach($sortedRooms['edited'] as $backendId => $editedRooms) {
172
			foreach ($editedRooms as $editedRoom) {
173
				$resource = $this->roomManager->getBackend($backendId)
174
					->getRoom($editedRoom);
175
				$this->updateCache($this->roomDbTable, $resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by $this->roomManager->getB...)->getRoom($editedRoom) on line 173 can be null; however, OCA\DAV\BackgroundJob\Up...roundJob::updateCache() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
176
			}
177
		}
178
	}
179
180
	/**
181
	 * get cached db rows for resources / rooms
182
	 * @param string $tableName
183
	 * @return array
184
	 */
185
	private function getCached($tableName):array {
186
		$query = $this->db->getQueryBuilder();
187
		$query->select('*')->from($tableName);
188
189
		$rows = [];
190
		$stmt = $query->execute();
191
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
192
			$rows[] = $row;
193
		}
194
195
		return $rows;
196
	}
197
198
	/**
199
	 * @param array $cachedResources
200
	 * @return array
201
	 */
202 View Code Duplication
	private function getCachedResourceIds(array $cachedResources):array {
203
		$cachedResourceIds = [];
204
		foreach ($cachedResources as $cachedResource) {
205
			if (!isset($cachedResourceIds[$cachedResource['backend_id']])) {
206
				$cachedResourceIds[$cachedResource['backend_id']] = [];
207
			}
208
209
			$cachedResourceIds[$cachedResource['backend_id']][] =
210
				$cachedResource['resource_id'];
211
		}
212
213
		return $cachedResourceIds;
214
	}
215
216
	/**
217
	 * @param array $cachedRooms
218
	 * @return array
219
	 */
220 View Code Duplication
	private function getCachedRoomIds(array $cachedRooms):array {
221
		$cachedRoomIds = [];
222
		foreach ($cachedRooms as $cachedRoom) {
223
			if (!isset($cachedRoomIds[$cachedRoom['backend_id']])) {
224
				$cachedRoomIds[$cachedRoom['backend_id']] = [];
225
			}
226
227
			$cachedRoomIds[$cachedRoom['backend_id']][] =
228
				$cachedRoom['resource_id'];
229
		}
230
231
		return $cachedRoomIds;
232
	}
233
234
	/**
235
	 * sort list of ids by whether they appear only in the backend /
236
	 * only in the cache / in both
237
	 *
238
	 * @param array $cached
239
	 * @param array $remote
240
	 * @return array
241
	 */
242
	private function sortByNewDeletedExisting(array $cached, array $remote):array {
243
		$sorted = [
244
			'new' => [],
245
			'deleted' => [],
246
			'edited' => [],
247
		];
248
249
		$backendIds = array_merge(array_keys($cached), array_keys($remote));
250
		foreach($backendIds as $backendId) {
251
			if (!isset($cached[$backendId])) {
252
				$sorted['new'][$backendId] = $remote[$backendId];
253
			} elseif (!isset($remote[$backendId])) {
254
				$sorted['deleted'][$backendId] = $cached[$backendId];
255
			} else {
256
				$sorted['new'][$backendId] = array_diff($remote[$backendId], $cached[$backendId]);
257
				$sorted['deleted'][$backendId] = array_diff($cached[$backendId], $remote[$backendId]);
258
				$sorted['edited'][$backendId] = array_intersect($remote[$backendId], $cached[$backendId]);
259
			}
260
		}
261
262
		return $sorted;
263
	}
264
265
	/**
266
	 * add entry to cache that exists remotely but not yet in cache
267
	 *
268
	 * @param string $table
269
	 * @param IResource|IRoom $remote
270
	 */
271
	private function addToCache($table, $remote) {
272
		$query = $this->db->getQueryBuilder();
273
		$query->insert($table)
274
			->values([
275
				'backend_id' => $query->createNamedParameter($remote->getBackend()->getBackendIdentifier()),
276
				'resource_id' => $query->createNamedParameter($remote->getId()),
277
				'email' => $query->createNamedParameter($remote->getEMail()),
278
				'displayname' => $query->createNamedParameter($remote->getDisplayName()),
279
				'group_restrictions' => $query->createNamedParameter(
280
					$this->serializeGroupRestrictions(
281
						$remote->getGroupRestrictions()
282
					))
283
			])
284
			->execute();
285
	}
286
287
	/**
288
	 * delete entry from cache that does not exist anymore remotely
289
	 *
290
	 * @param string $table
291
	 * @param string $principalUri
292
	 * @param string $backendId
293
	 * @param string $resourceId
294
	 */
295
	private function deleteFromCache($table, $principalUri, $backendId, $resourceId) {
296
		$query = $this->db->getQueryBuilder();
297
		$query->delete($table)
298
			->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
299
			->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)))
300
			->execute();
301
302
		$calendar = $this->calDavBackend->getCalendarByUri($principalUri, implode('-', [$backendId, $resourceId]));
303
		if ($calendar !== null) {
304
			$this->calDavBackend->deleteCalendar($calendar['id']);
305
		}
306
	}
307
308
	/**
309
	 * update an existing entry in cache
310
	 *
311
	 * @param string $table
312
	 * @param IResource|IRoom $remote
313
	 */
314
	private function updateCache($table, $remote) {
315
		$query = $this->db->getQueryBuilder();
316
		$query->update($table)
317
			->set('email', $query->createNamedParameter($remote->getEMail()))
318
			->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
319
			->set('group_restrictions', $query->createNamedParameter(
320
				$this->serializeGroupRestrictions(
321
					$remote->getGroupRestrictions()
322
				)))
323
			->where($query->expr()->eq('backend_id', $query->createNamedParameter($remote->getBackend()->getBackendIdentifier())))
324
			->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($remote->getId())))
325
			->execute();
326
	}
327
328
	/**
329
	 * serialize array of group restrictions to store them in database
330
	 *
331
	 * @param array $groups
332
	 * @return string
333
	 */
334
	private function serializeGroupRestrictions(array $groups):string {
335
		return \json_encode($groups);
336
	}
337
}
338