Completed
Pull Request — master (#362)
by Maxence
01:53
created

GSUpstreamService::broadcastEvent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php declare(strict_types=1);
2
3
4
/**
5
 * Circles - Bring cloud-users closer together.
6
 *
7
 * This file is licensed under the Affero General Public License version 3 or
8
 * later. See the COPYING file.
9
 *
10
 * @author Maxence Lange <[email protected]>
11
 * @copyright 2017
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
30
namespace OCA\Circles\Service;
31
32
33
use daita\MySmallPhpTools\Exceptions\RequestContentException;
34
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
35
use daita\MySmallPhpTools\Exceptions\RequestResultNotJsonException;
36
use daita\MySmallPhpTools\Exceptions\RequestResultSizeException;
37
use daita\MySmallPhpTools\Exceptions\RequestServerException;
38
use daita\MySmallPhpTools\Model\Request;
39
use daita\MySmallPhpTools\Model\SimpleDataStore;
40
use daita\MySmallPhpTools\Traits\TArrayTools;
41
use daita\MySmallPhpTools\Traits\TRequest;
42
use Exception;
43
use OCA\Circles\Db\CirclesRequest;
44
use OCA\Circles\Db\GSEventsRequest;
45
use OCA\Circles\Exceptions\GlobalScaleEventException;
46
use OCA\Circles\Exceptions\GSStatusException;
47
use OCA\Circles\Exceptions\JsonException;
48
use OCA\Circles\Exceptions\ModelException;
49
use OCA\Circles\GlobalScale\CircleStatus;
50
use OCA\Circles\Model\Circle;
51
use OCA\Circles\Model\GlobalScale\GSEvent;
52
use OCA\Circles\Model\GlobalScale\GSWrapper;
53
use OCP\IURLGenerator;
54
55
56
/**
57
 * Class GSUpstreamService
58
 *
59
 * @package OCA\Circles\Service
60
 */
61
class GSUpstreamService {
62
63
64
	use TRequest;
65
	use TArrayTools;
66
67
68
	/** @var string */
69
	private $userId = '';
70
71
	/** @var IURLGenerator */
72
	private $urlGenerator;
73
74
	/** @var GSEventsRequest */
75
	private $gsEventsRequest;
76
77
	/** @var CirclesRequest */
78
	private $circlesRequest;
79
80
	/** @var GlobalScaleService */
81
	private $globalScaleService;
82
83
	/** @var ConfigService */
84
	private $configService;
85
86
	/** @var MiscService */
87
	private $miscService;
88
89
90
	/**
91
	 * GSUpstreamService constructor.
92
	 *
93
	 * @param $userId
94
	 * @param IURLGenerator $urlGenerator
95
	 * @param GSEventsRequest $gsEventsRequest
96
	 * @param CirclesRequest $circlesRequest
97
	 * @param GlobalScaleService $globalScaleService
98
	 * @param ConfigService $configService
99
	 * @param MiscService $miscService
100
	 */
101
	public function __construct(
102
		$userId,
103
		IURLGenerator $urlGenerator,
104
		GSEventsRequest $gsEventsRequest,
105
		CirclesRequest $circlesRequest,
106
		GlobalScaleService $globalScaleService,
107
		ConfigService $configService,
108
		MiscService $miscService
109
	) {
110
		$this->userId = $userId;
111
		$this->urlGenerator = $urlGenerator;
112
		$this->gsEventsRequest = $gsEventsRequest;
113
		$this->circlesRequest = $circlesRequest;
114
		$this->globalScaleService = $globalScaleService;
115
		$this->configService = $configService;
116
		$this->miscService = $miscService;
117
	}
118
119
120
	/**
121
	 * @param GSEvent $event
122
	 *
123
	 * @throws Exception
124
	 */
125
	public function newEvent(GSEvent $event) {
126
		try {
127
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
128
129
			$this->fillEvent($event);
130
			if ($this->isLocalEvent($event)) {
131
				$gs->verify($event, true);
132
				$gs->manage($event);
133
134
				$this->globalScaleService->asyncBroadcast($event);
135
			} else {
136
				$gs->verify($event); // needed ? as we check event on the 'master' of the circle
137
				$this->confirmEvent($event);
138
				$gs->manage($event);
139
			}
140
		} catch (Exception $e) {
141
			$this->miscService->log(
142
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
143
			);
144
			throw $e;
145
		}
146
	}
147
148
149
	/**
150
	 * @param GSWrapper $wrapper
151
	 * @param string $protocol
152
	 */
153
	public function broadcastWrapper(GSWrapper $wrapper, string $protocol): void {
154
		$status = GSWrapper::STATUS_FAILED;
155
156
		try {
157
			$this->broadcastEvent($wrapper->getEvent(), $wrapper->getInstance(), $protocol);
158
			$status = GSWrapper::STATUS_DONE;
159
		} catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
160
		}
161
162
		if ($wrapper->getSeverity() === GSEvent::SEVERITY_HIGH) {
163
			$wrapper->setStatus($status);
164
		} else {
165
			$wrapper->setStatus(GSWrapper::STATUS_OVER);
166
		}
167
168
		$this->gsEventsRequest->update($wrapper);
169
	}
170
171
172
	/**
173
	 * @param GSEvent $event
174
	 * @param string $instance
175
	 * @param string $protocol
176
	 *
177
	 * @throws RequestContentException
178
	 * @throws RequestNetworkException
179
	 * @throws RequestResultNotJsonException
180
	 * @throws RequestResultSizeException
181
	 * @throws RequestServerException
182
	 */
183
	public function broadcastEvent(GSEvent $event, string $instance, string $protocol = ''): void {
184
		$this->signEvent($event);
185
186
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
187
		$request = new Request($path, Request::TYPE_POST);
188
189
		$protocols = ['https', 'http'];
190
		if ($protocol !== '') {
191
			$protocols = [$protocol];
192
		}
193
194
		$request->setProtocols($protocols);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
195
		$request->setDataSerialize($event);
196
197
		$request->setAddress($instance);
198
199
		$data = $this->retrieveJson($request);
200
		$event->setResult(new SimpleDataStore($this->getArray('result', $data, [])));
201
	}
202
203
204
	/**
205
	 * @param GSEvent $event
206
	 *
207
	 * @throws RequestContentException
208
	 * @throws RequestNetworkException
209
	 * @throws RequestResultSizeException
210
	 * @throws RequestServerException
211
	 * @throws RequestResultNotJsonException
212
	 * @throws GlobalScaleEventException
213
	 */
214
	public function confirmEvent(GSEvent $event): void {
215
		$this->signEvent($event);
216
217
		$circle = $event->getCircle();
218
		$owner = $circle->getOwner();
219
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
220
221
		$request = new Request($path, Request::TYPE_POST);
222
		$request->setProtocols([$_SERVER['REQUEST_SCHEME']]);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
223
		$request->setAddressFromUrl($owner->getInstance());
224
		$request->setDataSerialize($event);
225
226
		$result = $this->retrieveJson($request);
227
		$this->miscService->log('result ' . json_encode($result));
228
		if ($this->getInt('status', $result) === 0) {
229
			throw new GlobalScaleEventException($this->get('error', $result));
230
		}
231
	}
232
233
234
	/**
235
	 * @param GSEvent $event
236
	 *
237
	 * @throws GSStatusException
238
	 */
239
	private function fillEvent(GSEvent $event): void {
240
		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
241
			return;
242
		}
243
244
		$event->setSource($this->configService->getLocalCloudId());
245
	}
246
247
248
	/**
249
	 * @param GSEvent $event
250
	 */
251
	private function signEvent(GSevent $event) {
252
		$event->setKey($this->globalScaleService->getKey());
253
	}
254
255
256
	/**
257
	 * We check that the event can be managed/checked locally or if the owner of the circle belongs to
258
	 * an other instance of Nextcloud
259
	 *
260
	 * @param GSEvent $event
261
	 *asyncBroadcast
262
	 *
263
	 * @return bool
264
	 */
265
	private function isLocalEvent(GSEvent $event): bool {
266
		if ($event->isLocal()) {
267
			return true;
268
		}
269
270
		$circle = $event->getCircle();
271
		$owner = $circle->getOwner();
272
		if ($owner->getInstance() === ''
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $owner->getInstan...->getTrustedDomains());.
Loading history...
273
			|| in_array(
274
				$owner->getInstance(), $this->configService->getTrustedDomains()
275
			)) {
276
			return true;
277
		}
278
279
		return false;
280
	}
281
282
283
	/**
284
	 * @param string $token
285
	 *
286
	 * @return GSWrapper[]
287
	 * @throws JsonException
288
	 * @throws ModelException
289
	 */
290
	public function getEventsByToken(string $token): array {
291
		return $this->gsEventsRequest->getByToken($token);
292
	}
293
294
295
	/**
296
	 * @param array $circles
297
	 *
298
	 * @throws GSStatusException
299
	 */
300
	public function syncCircles(array $circles): void {
301
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
302
		$event->setSource($this->configService->getLocalCloudId());
303
		$event->setData(new SimpleDataStore($circles));
304
305
		foreach ($this->globalScaleService->getInstances() as $instance) {
306
			try {
307
				$this->broadcastEvent($event, $instance);
308
			} catch (RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException | RequestResultNotJsonException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
309
			}
310
		}
311
	}
312
313
314
	/**
315
	 * @param Circle $circle
316
	 *
317
	 * @return bool
318
	 * @throws GSStatusException
319
	 */
320
	public function confirmCircleStatus(Circle $circle): bool {
321
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
322
		$event->setSource($this->configService->getLocalCloudId());
323
		$event->setCircle($circle);
324
325
		$this->signEvent($event);
326
327
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
328
		$request = new Request($path, Request::TYPE_POST);
329
330
		$request->setProtocols(['https', 'http']);
0 ignored issues
show
Bug introduced by
The method setProtocols() does not exist on daita\MySmallPhpTools\Model\Request. Did you maybe mean setProtocol()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
331
		$request->setDataSerialize($event);
332
333
		$requestIssue = false;
334
		$notFound = false;
335
		$foundWithNoOwner = false;
336
		foreach ($this->globalScaleService->getInstances() as $instance) {
337
			$request->setAddress($instance);
338
339
			try {
340
				$result = $this->retrieveJson($request);
341
				$this->miscService->log('result: ' . json_encode($result));
342
				if ($this->getInt('status', $result, 0) !== 1) {
343
					throw new RequestContentException('result status is not good');
344
				}
345
346
				$status = $this->getInt('success.data.status', $result);
347
348
				// if error, we assume the circle might still exist.
349
				if ($status === CircleStatus::STATUS_ERROR) {
350
					return true;
351
				}
352
353
				if ($status === CircleStatus::STATUS_OK) {
354
					return true;
355
				}
356
357
				// TODO: check the data.supposedOwner entry.
358
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
359
					$foundWithNoOwner = true;
360
				}
361
362
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
363
					$notFound = true;
364
				}
365
366
			} catch (RequestContentException
367
			| RequestNetworkException
368
			| RequestResultNotJsonException
369
			| RequestResultSizeException
370
			| RequestServerException $e) {
371
				$requestIssue = true;
372
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
373
				continue;
374
			}
375
		}
376
377
		// if no request issue, we can imagine that the instance that owns the circle is down.
378
		// We'll wait for more information (cf request exceptions management);
379
		if ($requestIssue) {
380
			return true;
381
		}
382
383
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
384
		if ($notFound && !$foundWithNoOwner) {
385
			return false;
386
		}
387
388
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
389
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
390
		if (!$notFound && $foundWithNoOwner) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !$notFound && $foundWithNoOwner;.
Loading history...
391
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
392
			return true;
393
		}
394
395
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
396
		return false;
397
	}
398
399
400
	/**
401
	 * @param string $token
402
	 */
403
	public function manageResults(string $token): void {
404
		try {
405
			$wrappers = $this->gsEventsRequest->getByToken($token);
406
		} catch (JsonException | ModelException $e) {
407
			return;
408
		}
409
410
		$event = null;
411
		$events = [];
412
		foreach ($wrappers as $wrapper) {
413
			if ($wrapper->getStatus() !== GSWrapper::STATUS_DONE) {
414
				return;
415
			}
416
417
			$events[$wrapper->getInstance()] = $event = $wrapper->getEvent();
418
		}
419
420
		if ($event === null) {
421
			return;
422
		}
423
424
		try {
425
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
426
			$gs->result($events);
427
		} catch (GlobalScaleEventException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
428
		}
429
	}
430
431
432
	/**
433
	 *
434
	 */
435
	public function deprecatedEvents(): void {
436
437
	}
438
439
}
440
441