Completed
Pull Request — master (#362)
by Maxence
02:21
created

GSUpstreamService::broadcastEvent()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
cc 4
nc 6
nop 2
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\Exceptions\TokenDoesNotExistException;
50
use OCA\Circles\GlobalScale\CircleStatus;
51
use OCA\Circles\Model\Circle;
52
use OCA\Circles\Model\GlobalScale\GSEvent;
53
use OCA\Circles\Model\GlobalScale\GSWrapper;
54
use OCP\IURLGenerator;
55
56
57
/**
58
 * Class GSUpstreamService
59
 *
60
 * @package OCA\Circles\Service
61
 */
62
class GSUpstreamService {
63
64
65
	use TRequest;
66
	use TArrayTools;
67
68
69
	/** @var string */
70
	private $userId = '';
71
72
	/** @var IURLGenerator */
73
	private $urlGenerator;
74
75
	/** @var GSEventsRequest */
76
	private $gsEventsRequest;
77
78
	/** @var CirclesRequest */
79
	private $circlesRequest;
80
81
	/** @var GlobalScaleService */
82
	private $globalScaleService;
83
84
	/** @var ConfigService */
85
	private $configService;
86
87
	/** @var MiscService */
88
	private $miscService;
89
90
91
	/**
92
	 * GSUpstreamService constructor.
93
	 *
94
	 * @param $userId
95
	 * @param IURLGenerator $urlGenerator
96
	 * @param GSEventsRequest $gsEventsRequest
97
	 * @param CirclesRequest $circlesRequest
98
	 * @param GlobalScaleService $globalScaleService
99
	 * @param ConfigService $configService
100
	 * @param MiscService $miscService
101
	 */
102
	public function __construct(
103
		$userId,
104
		IURLGenerator $urlGenerator,
105
		GSEventsRequest $gsEventsRequest,
106
		CirclesRequest $circlesRequest,
107
		GlobalScaleService $globalScaleService,
108
		ConfigService $configService,
109
		MiscService $miscService
110
	) {
111
		$this->userId = $userId;
112
		$this->urlGenerator = $urlGenerator;
113
		$this->gsEventsRequest = $gsEventsRequest;
114
		$this->circlesRequest = $circlesRequest;
115
		$this->globalScaleService = $globalScaleService;
116
		$this->configService = $configService;
117
		$this->miscService = $miscService;
118
	}
119
120
121
	/**
122
	 * @param GSEvent $event
123
	 *
124
	 * @throws Exception
125
	 */
126
	public function newEvent(GSEvent $event) {
127
		try {
128
			$gs = $this->globalScaleService->getGlobalScaleEvent($event);
129
//		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
130
//			return true;
131
//		}
132
133
			$this->fillEvent($event);
134
			if ($this->isLocalEvent($event)) {
135
				$gs->verify($event, true);
136
				$gs->manage($event);
137
138
				$this->globalScaleService->asyncBroadcast($event);
139
			} else {
140
				$gs->verify($event);
141
				$this->confirmEvent($event);
142
				$gs->manage($event);
143
			}
144
		} catch (Exception $e) {
145
			$this->miscService->log(
146
				get_class($e) . ' on new event: ' . $e->getMessage() . ' - ' . json_encode($event), 1
147
			);
148
			throw $e;
149
		}
150
	}
151
152
153
	/**
154
	 * @param string $protocol
155
	 * @param GSEvent $event
156
	 *
157
	 * @throws GSStatusException
158
	 */
159
	public function broadcastEvent(GSEvent $event, string $protocol = ''): void {
160
		$this->signEvent($event);
161
162
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.broadcast');
163
		$request = new Request($path, Request::TYPE_POST);
164
165
		if ($protocol === '') {
166
			// TODO: test https first, then http
167
			$protocol = 'http';
168
		}
169
		$request->setProtocol($protocol);
170
		$request->setDataSerialize($event);
171
172
		foreach ($this->getInstances() as $instance) {
173
			$request->setAddress($instance);
174
175
			try {
176
				$this->doRequest($request);
177
			} catch
178
			(RequestContentException | RequestNetworkException | RequestResultSizeException | RequestServerException $e) {
179
				// TODO: queue request
180
			}
181
		}
182
183
	}
184
185
186
	/**
187
	 * @param GSEvent $event
188
	 *
189
	 * @throws RequestContentException
190
	 * @throws RequestNetworkException
191
	 * @throws RequestResultSizeException
192
	 * @throws RequestServerException
193
	 * @throws RequestResultNotJsonException
194
	 * @throws GlobalScaleEventException
195
	 */
196
	public function confirmEvent(GSEvent $event): void {
197
		$this->signEvent($event);
198
199
		$circle = $event->getCircle();
200
		$owner = $circle->getOwner();
201
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.event');
202
203
		$request = new Request($path, Request::TYPE_POST);
204
		$request->setProtocol($_SERVER['REQUEST_SCHEME']);
205
		$request->setAddressFromUrl($owner->getInstance());
206
		$request->setDataSerialize($event);
207
208
		$result = $this->retrieveJson($request);
209
		$this->miscService->log('result ' . json_encode($result));
210
		if ($this->getInt('status', $result) === 0) {
211
			throw new GlobalScaleEventException($this->get('error', $result));
212
		}
213
	}
214
215
216
	/**
217
	 * @param GSEvent $event
218
	 *
219
	 * @throws GSStatusException
220
	 */
221
	private function fillEvent(GSEvent $event): void {
222
		if (!$this->configService->getGSStatus(ConfigService::GS_ENABLED)) {
223
			return;
224
		}
225
226
		$event->setSource($this->configService->getLocalCloudId());
227
	}
228
229
	/**
230
	 * @param bool $all
231
	 *
232
	 * @return array
233
	 * @throws GSStatusException
234
	 */
235
	public function getInstances(bool $all = false): array {
236
		/** @var string $lookup */
237
		$lookup = $this->configService->getGSStatus(ConfigService::GS_LOOKUP);
238
239
		$request = new Request('/instances', Request::TYPE_GET);
240
		$request->setAddressFromUrl($lookup);
241
242
		try {
243
			$instances = $this->retrieveJson($request);
244
		} catch (
245
		RequestContentException |
246
		RequestNetworkException |
247
		RequestResultSizeException |
248
		RequestServerException |
249
		RequestResultNotJsonException $e
250
		) {
251
			$this->miscService->log('Issue while retrieving instances from lookup: ' . $e->getMessage());
252
253
			return [];
254
		}
255
256
		if ($all) {
257
			return $instances;
258
		}
259
260
		return array_diff($instances, $this->configService->getTrustedDomains());
261
	}
262
263
264
	/**
265
	 * @param GSEvent $event
266
	 */
267
	private function signEvent(GSevent $event) {
268
		$event->setKey($this->globalScaleService->getKey());
269
	}
270
271
272
	/**
273
	 * @param GSEvent $event
274
	 *asyncBroadcast
275
	 *
276
	 * @return bool
277
	 */
278
	private function isLocalEvent(GSEvent $event): bool {
279
		if ($event->isLocal()) {
280
			return true;
281
		}
282
283
		$circle = $event->getCircle();
284
		$owner = $circle->getOwner();
285
		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...
286
			|| in_array(
287
				$owner->getInstance(), $this->configService->getTrustedDomains()
288
			)) {
289
			return true;
290
		}
291
292
		return false;
293
	}
294
295
296
	/**
297
	 * @param string $token
298
	 *
299
	 * @return GSWrapper
300
	 * @throws JsonException
301
	 * @throws ModelException
302
	 * @throws TokenDoesNotExistException
303
	 */
304
	public function getEventByToken(string $token): GSWrapper {
305
		return $this->gsEventsRequest->getByToken($token);
306
	}
307
308
309
	/**
310
	 * @param array $circles
311
	 *
312
	 * @throws GSStatusException
313
	 */
314
	public function syncCircles(array $circles): void {
315
		$event = new GSEvent(GSEvent::GLOBAL_SYNC, true);
316
		$event->setSource($this->configService->getLocalCloudId());
317
		$event->setData(new SimpleDataStore($circles));
318
319
		$this->broadcastEvent($event);
320
	}
321
322
323
	/**
324
	 * @param Circle $circle
325
	 *
326
	 * @return bool
327
	 * @throws GSStatusException
328
	 */
329
	public function confirmCircleStatus(Circle $circle): bool {
330
		$event = new GSEvent(GSEvent::CIRCLE_STATUS, true);
331
		$event->setSource($this->configService->getLocalCloudId());
332
		$event->setCircle($circle);
333
334
		$this->signEvent($event);
335
336
		$path = $this->urlGenerator->linkToRoute('circles.GlobalScale.status');
337
		$request = new Request($path, Request::TYPE_POST);
338
339
		// TODO: test https first, then http
340
		$protocol = 'http';
341
		$request->setProtocol($protocol);
342
		$request->setDataSerialize($event);
343
344
		$requestIssue = false;
345
		$notFound = false;
346
		$foundWithNoOwner = false;
347
		foreach ($this->getInstances() as $instance) {
348
			$request->setAddress($instance);
349
350
			try {
351
				$result = $this->retrieveJson($request);
352
				$this->miscService->log('result: ' . json_encode($result));
353
				if ($this->getInt('status', $result, 0) !== 1) {
354
					throw new RequestContentException('result status is not good');
355
				}
356
357
				$status = $this->getInt('success.data.status', $result);
358
359
				// if error, we assume the circle might still exist.
360
				if ($status === CircleStatus::STATUS_ERROR) {
361
					return true;
362
				}
363
364
				if ($status === CircleStatus::STATUS_OK) {
365
					return true;
366
				}
367
368
				// TODO: check the data.supposedOwner entry.
369
				if ($status === CircleStatus::STATUS_NOT_OWNER) {
370
					$foundWithNoOwner = true;
371
				}
372
373
				if ($status === CircleStatus::STATUS_NOT_FOUND) {
374
					$notFound = true;
375
				}
376
377
			} catch (RequestContentException
378
			| RequestNetworkException
379
			| RequestResultNotJsonException
380
			| RequestResultSizeException
381
			| RequestServerException $e) {
382
				$requestIssue = true;
383
				// TODO: log instances that have network issue, after too many tries (7d), remove this circle.
384
				continue;
385
			}
386
		}
387
388
		// if no request issue, we can imagine that the instance that owns the circle is down.
389
		// We'll wait for more information (cf request exceptions management);
390
		if ($requestIssue) {
391
			return true;
392
		}
393
394
		// circle were not found in any other instances, we can easily says that the circle does not exists anymore
395
		if ($notFound && !$foundWithNoOwner) {
396
			return false;
397
		}
398
399
		// circle were found everywhere but with no owner on every instance. we need to assign a new owner.
400
		// This should be done by checking admin rights. if no admin rights, let's assume that circle should be removed.
401
		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...
402
			// TODO: assign a new owner and check that when changing owner, we do check that the destination instance is updated FOR SURE!
403
			 return true;
404
		}
405
406
		// some instances returned notFound, some returned circle with no owner. let's assume the circle is deprecated.
407
		return false;
408
	}
409
410
}
411
412