Passed
Push — master ( 9bdf0e...8b8917 )
by Roeland
14:23 queued 10s
created

WeatherStatusService::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 9
dl 0
loc 21
rs 9.8666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2020, Julien Veyssier
7
 *
8
 * @author Julien Veyssier <[email protected]>
9
 *
10
 * @license AGPL-3.0
11
 *
12
 * This code is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License, version 3,
14
 * as published by the Free Software Foundation.
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, version 3,
22
 * along with this program. If not, see <http://www.gnu.org/licenses/>
23
 *
24
 */
25
26
namespace OCA\WeatherStatus\Service;
27
28
use OCP\IConfig;
29
use OCP\IL10N;
30
use OCP\App\IAppManager;
31
use OCP\Accounts\IAccountManager;
32
use OCP\Accounts\PropertyDoesNotExistException;
33
use OCP\IUserManager;
34
use OCP\Http\Client\IClientService;
35
use OCP\Http\Client\IClient;
36
use OCP\ICacheFactory;
37
use OCP\ICache;
38
use OCP\ILogger;
39
40
use OCA\WeatherStatus\AppInfo\Application;
41
42
/**
43
 * Class WeatherStatusService
44
 *
45
 * @package OCA\WeatherStatus\Service
46
 */
47
class WeatherStatusService {
48
	public const MODE_BROWSER_LOCATION = 1;
49
	public const MODE_MANUAL_LOCATION = 2;
50
51
	/** @var IClientService */
52
	private $clientService;
53
54
	/** @var IClient */
55
	private $client;
56
57
	/** @var IConfig */
58
	private $config;
59
60
	/** @var IL10N */
61
	private $l10n;
62
63
	/** @var ILogger */
64
	private $logger;
65
66
	/** @var IAccountManager */
67
	private $accountManager;
68
69
	/** @var IUserManager */
70
	private $userManager;
71
72
	/** @var IAppManager */
73
	private $appManager;
74
75
	/** @var ICacheFactory */
76
	private $cacheFactory;
0 ignored issues
show
introduced by
The private property $cacheFactory is not used, and could be removed.
Loading history...
77
78
	/** @var ICache */
79
	private $cache;
80
81
	/** @var string */
82
	private $userId;
83
84
	/** @var string */
85
	private $version;
86
87
	/**
88
	 * WeatherStatusService constructor
89
	 *
90
	 * @param IClientService $clientService
91
	 * @param IConfig $config
92
	 * @param IL10N $l10n
93
	 * @param ILogger $logger
94
	 * @param IAccountManager $accountManager
95
	 * @param IUserManager $userManager
96
	 * @param IAppManager $appManager
97
	 * @param ICacheFactory $cacheFactory
98
	 * @param string $userId
99
	 */
100
	public function __construct(IClientService $clientService,
101
								IConfig $config,
102
								IL10N $l10n,
103
								ILogger $logger,
104
								IAccountManager $accountManager,
105
								IUserManager $userManager,
106
								IAppManager $appManager,
107
								ICacheFactory $cacheFactory,
108
								?string $userId) {
109
		$this->config = $config;
110
		$this->userId = $userId;
111
		$this->l10n = $l10n;
112
		$this->logger = $logger;
113
		$this->accountManager = $accountManager;
114
		$this->userManager = $userManager;
115
		$this->appManager = $appManager;
116
		$this->version = $appManager->getAppVersion(Application::APP_ID);
117
		$this->clientService = $clientService;
118
		$this->client = $clientService->newClient();
119
		if ($cacheFactory->isAvailable()) {
120
			$this->cache = $cacheFactory->createDistributed();
121
		}
122
	}
123
124
	/**
125
	 * Change the weather status mode. There are currently 2 modes:
126
	 * - ask the browser
127
	 * - use the user defined address
128
	 * @param int $mode New mode
129
	 * @return array success state
130
	 */
131
	public function setMode(int $mode): array {
132
		$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval($mode));
133
		return ['success' => true];
134
	}
135
136
	/**
137
	 * Get favorites list
138
	 * @param array $favorites
139
	 * @return array success state
140
	 */
141
	public function getFavorites(): array {
142
		$favoritesJson = $this->config->getUserValue($this->userId, Application::APP_ID, 'favorites', '');
143
		return json_decode($favoritesJson, true) ?: [];
144
	}
145
146
	/**
147
	 * Set favorites list
148
	 * @param array $favorites
149
	 * @return array success state
150
	 */
151
	public function setFavorites(array $favorites): array {
152
		$this->config->setUserValue($this->userId, Application::APP_ID, 'favorites', json_encode($favorites));
153
		return ['success' => true];
154
	}
155
156
	/**
157
	 * Try to use the address set in user personal settings as weather location
158
	 *
159
	 * @return array with success state and address information
160
	 */
161
	public function usePersonalAddress(): array {
162
		$account = $this->accountManager->getAccount($this->userManager->get($this->userId));
163
		try {
164
			$address = $account->getProperty('address')->getValue();
165
		} catch (PropertyDoesNotExistException $e) {
166
			return ['success' => false];
167
		}
168
		if ($address === '') {
169
			return ['success' => false];
170
		}
171
		return $this->setAddress($address);
172
	}
173
174
	/**
175
	 * Set address and resolve it to get coordinates
176
	 * or directly set coordinates and get address with reverse geocoding
177
	 *
178
	 * @param string|null $address Any approximative or exact address
179
	 * @param float|null $lat Latitude in decimal degree format
180
	 * @param float|null $lon Longitude in decimal degree format
181
	 * @return array with success state and address information
182
	 */
183
	public function setLocation(?string $address, ?float $lat, ?float $lon): array {
184
		if (!is_null($lat) && !is_null($lon)) {
185
			// store coordinates
186
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($lat));
187
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($lon));
188
			// resolve and store formatted address
189
			$address = $this->resolveLocation($lat, $lon);
190
			$address = $address ? $address : $this->l10n->t('Unknown address');
191
			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $address);
192
			// get and store altitude
193
			$altitude = $this->getAltitude($lat, $lon);
194
			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
195
			return [
196
				'address' => $address,
197
				'success' => true,
198
			];
199
		} elseif ($address) {
200
			return $this->setAddress($address);
201
		} else {
202
			return ['success' => false];
203
		}
204
	}
205
206
	/**
207
	 * Provide address information from coordinates
208
	 *
209
	 * @param float $lat Latitude in decimal degree format
210
	 * @param float $lon Longitude in decimal degree format
211
	 */
212
	private function resolveLocation(float $lat, float $lon): ?string {
213
		$params = [
214
			'lat' => number_format($lat, 2),
215
			'lon' => number_format($lon, 2),
216
			'addressdetails' => 1,
217
			'format' => 'json',
218
		];
219
		$url = 'https://nominatim.openstreetmap.org/reverse';
220
		$result = $this->requestJSON($url, $params);
221
		return $this->formatOsmAddress($result);
222
	}
223
224
	/**
225
	 * Get altitude from coordinates
226
	 *
227
	 * @param float $lat Latitude in decimal degree format
228
	 * @param float $lon Longitude in decimal degree format
229
	 * @return float altitude in meter
230
	 */
231
	private function getAltitude(float $lat, float $lon): float {
232
		$params = [
233
			'locations' => $lat . ',' . $lon,
234
		];
235
		$url = 'https://api.opentopodata.org/v1/srtm30m';
236
		$result = $this->requestJSON($url, $params);
237
		$altitude = 0;
238
		if (isset($result['results']) && is_array($result['results']) && count($result['results']) > 0
0 ignored issues
show
introduced by
The condition is_array($result['results']) is always false.
Loading history...
239
			&& is_array($result['results'][0]) && isset($result['results'][0]['elevation'])) {
240
			$altitude = floatval($result['results'][0]['elevation']);
241
		}
242
		return $altitude;
243
	}
244
245
	/**
246
	 * @return string Formatted address from JSON nominatim result
247
	 */
248
	private function formatOsmAddress(array $json): ?string {
249
		if (isset($json['address']) && isset($json['display_name'])) {
250
			$jsonAddr = $json['address'];
251
			$cityAddress = '';
252
			// priority : city, town, village, municipality
253
			if (isset($jsonAddr['city'])) {
254
				$cityAddress .= $jsonAddr['city'];
255
			} elseif (isset($jsonAddr['town'])) {
256
				$cityAddress .= $jsonAddr['town'];
257
			} elseif (isset($jsonAddr['village'])) {
258
				$cityAddress .= $jsonAddr['village'];
259
			} elseif (isset($jsonAddr['municipality'])) {
260
				$cityAddress .= $jsonAddr['municipality'];
261
			} else {
262
				return $json['display_name'];
263
			}
264
			// post code
265
			if (isset($jsonAddr['postcode'])) {
266
				$cityAddress .= ', ' . $jsonAddr['postcode'];
267
			}
268
			// country
269
			if (isset($jsonAddr['country'])) {
270
				$cityAddress .= ', ' . $jsonAddr['country'];
271
				return $cityAddress;
272
			} else {
273
				return $json['display_name'];
274
			}
275
		} elseif (isset($json['display_name'])) {
276
			return $json['display_name'];
277
		}
278
		return null;
279
	}
280
281
	/**
282
	 * Set address and resolve it to get coordinates
283
	 *
284
	 * @param string $address Any approximative or exact address
285
	 * @return array with success state and address information (coordinates and formatted address)
286
	 */
287
	public function setAddress(string $address): array {
288
		$addressInfo = $this->searchForAddress($address);
289
		if (isset($addressInfo['display_name']) && isset($addressInfo['lat']) && isset($addressInfo['lon'])) {
290
			$formattedAddress = $this->formatOsmAddress($addressInfo);
291
			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $formattedAddress);
292
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($addressInfo['lat']));
293
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($addressInfo['lon']));
294
			$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval(self::MODE_MANUAL_LOCATION));
295
			// get and store altitude
296
			$altitude = $this->getAltitude(floatval($addressInfo['lat']), floatval($addressInfo['lon']));
297
			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
298
			return [
299
				'lat' => $addressInfo['lat'],
300
				'lon' => $addressInfo['lon'],
301
				'address' => $formattedAddress,
302
				'success' => true,
303
			];
304
		} else {
305
			return ['success' => false];
306
		}
307
	}
308
309
	/**
310
	 * Ask nominatim information about an unformatted address
311
	 *
312
	 * @param string Unformatted address
0 ignored issues
show
Bug introduced by
The type OCA\WeatherStatus\Service\Unformatted was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
313
	 * @return array Full Nominatim result for the given address
314
	 */
315
	private function searchForAddress(string $address): array {
316
		$params = [
317
			'format' => 'json',
318
			'addressdetails' => '1',
319
			'extratags' => '1',
320
			'namedetails' => '1',
321
			'limit' => '1',
322
		];
323
		$url = 'https://nominatim.openstreetmap.org/search/' . $address;
324
		$results = $this->requestJSON($url, $params);
325
		if (count($results) > 0) {
326
			return $results[0];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $results[0] returns the type string which is incompatible with the type-hinted return array.
Loading history...
327
		}
328
		return ['error' => $this->l10n->t('No result.')];
329
	}
330
331
	/**
332
	 * Get stored user location
333
	 *
334
	 * @return array which contains coordinates, formatted address and current weather status mode
335
	 */
336
	public function getLocation(): array {
337
		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
338
		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
339
		$address = $this->config->getUserValue($this->userId, Application::APP_ID, 'address', '');
340
		$mode = $this->config->getUserValue($this->userId, Application::APP_ID, 'mode', self::MODE_MANUAL_LOCATION);
341
		return [
342
			'lat' => $lat,
343
			'lon' => $lon,
344
			'address' => $address,
345
			'mode' => intval($mode),
346
		];
347
	}
348
349
	/**
350
	 * Get forecast for current location
351
	 *
352
	 * @return array which contains success state and filtered forecast data
353
	 */
354
	public function getForecast(): array {
355
		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
356
		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
357
		$alt = $this->config->getUserValue($this->userId, Application::APP_ID, 'altitude', '');
358
		if (!is_numeric($alt)) {
359
			$alt = 0;
360
		}
361
		if (is_numeric($lat) && is_numeric($lon)) {
362
			return $this->forecastRequest(floatval($lat), floatval($lon), floatval($alt));
363
		} else {
364
			return ['success' => false];
365
		}
366
	}
367
368
	/**
369
	 * Actually make the request to the forecast service
370
	 *
371
	 * @param float $lat Latitude of requested forecast, in decimal degree format
372
	 * @param float $lon Longitude of requested forecast, in decimal degree format
373
	 * @param float $altitude Altitude of requested forecast, in meter
374
	 * @param int $nbValues Number of forecast values (hours)
375
	 * @return array Filtered forecast data
376
	 */
377
	private function forecastRequest(float $lat, float $lon, float $altitude, int $nbValues = 10): array {
378
		$params = [
379
			'lat' => number_format($lat, 2),
380
			'lon' => number_format($lon, 2),
381
			'altitude' => $altitude,
382
		];
383
		$url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact';
384
		$weather = $this->requestJSON($url, $params);
385
		if (isset($weather['properties']) && isset($weather['properties']['timeseries']) && is_array($weather['properties']['timeseries'])) {
386
			return array_slice($weather['properties']['timeseries'], 0, $nbValues);
387
		}
388
		return ['error' => $this->l10n->t('Malformed JSON data.')];
389
	}
390
391
	/**
392
	 * Make a HTTP GET request and parse JSON result.
393
	 * Request results are cached until the 'Expires' response header says so
394
	 *
395
	 * @param string $url Base URL to query
396
	 * @param array $params GET parameters
397
	 * @return array which contains the error message or the parsed JSON result
398
	 */
399
	private function requestJSON(string $url, array $params = []): array {
400
		if (isset($this->cache)) {
401
			$cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params));
402
			if ($this->cache->hasKey($cacheKey)) {
403
				return $this->cache->get($cacheKey);
404
			}
405
		}
406
		try {
407
			$options = [
408
				'headers' => [
409
					'User-Agent' => 'NextcloudWeatherStatus/' . $this->version . ' nextcloud.com'
410
				],
411
			];
412
413
			$reqUrl = $url;
414
			if (count($params) > 0) {
415
				$paramsContent = http_build_query($params);
416
				$reqUrl = $url . '?' . $paramsContent;
417
			}
418
419
			$response = $this->client->get($reqUrl, $options);
420
			$body = $response->getBody();
421
			$headers = $response->getHeaders();
422
			$respCode = $response->getStatusCode();
423
424
			if ($respCode >= 400) {
425
				return ['error' => $this->l10n->t('Error')];
426
			} else {
427
				$json = json_decode($body, true);
428
				if (isset($this->cache)) {
429
					// default cache duration is one hour
430
					$cacheDuration = 60 * 60;
431
					if (isset($headers['Expires']) && count($headers['Expires']) > 0) {
432
						// if the Expires response header is set, use it to define cache duration
433
						$expireTs = (new \Datetime($headers['Expires'][0]))->getTimestamp();
434
						$nowTs = (new \Datetime())->getTimestamp();
435
						$duration = $expireTs - $nowTs;
436
						if ($duration > $cacheDuration) {
437
							$cacheDuration = $duration;
438
						}
439
					}
440
					$this->cache->set($cacheKey, $json, $cacheDuration);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cacheKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
441
				}
442
				return $json;
443
			}
444
		} catch (\Exception $e) {
445
			$this->logger->warning($url . 'API error : ' . $e, ['app' => Application::APP_ID]);
446
			return ['error' => $e->getMessage()];
447
		}
448
	}
449
}
450