Passed
Push — master ( 6d21e0...1f4c02 )
by Morris
14:24 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 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 12
c 1
b 0
f 0
nc 2
nop 9
dl 0
loc 21
rs 9.8666

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
	 * Try to use the address set in user personal settings as weather location
138
	 *
139
	 * @return array with success state and address information
140
	 */
141
	public function usePersonalAddress(): array {
142
		$account = $this->accountManager->getAccount($this->userManager->get($this->userId));
143
		try {
144
			$address = $account->getProperty('address')->getValue();
145
		} catch (PropertyDoesNotExistException $e) {
146
			return ['success' => false];
147
		}
148
		if ($address === '') {
149
			return ['success' => false];
150
		}
151
		return $this->setAddress($address);
152
	}
153
154
	/**
155
	 * Set address and resolve it to get coordinates
156
	 * or directly set coordinates and get address with reverse geocoding
157
	 *
158
	 * @param string|null $address Any approximative or exact address
159
	 * @param float|null $lat Latitude in decimal degree format
160
	 * @param float|null $lon Longitude in decimal degree format
161
	 * @return array with success state and address information
162
	 */
163
	public function setLocation(?string $address, ?float $lat, ?float $lon): array {
164
		if (!is_null($lat) && !is_null($lon)) {
165
			// store coordinates
166
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($lat));
167
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($lon));
168
			// resolve and store formatted address
169
			$address = $this->resolveLocation($lat, $lon);
170
			$address = $address ? $address : $this->l10n->t('Unknown address');
171
			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $address);
172
			// get and store altitude
173
			$altitude = $this->getAltitude($lat, $lon);
174
			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
175
			return [
176
				'address' => $address,
177
				'success' => true,
178
			];
179
		} elseif ($address) {
180
			return $this->setAddress($address);
181
		} else {
182
			return ['success' => false];
183
		}
184
	}
185
186
	/**
187
	 * Provide address information from coordinates
188
	 *
189
	 * @param float $lat Latitude in decimal degree format
190
	 * @param float $lon Longitude in decimal degree format
191
	 */
192
	private function resolveLocation(float $lat, float $lon): ?string {
193
		$params = [
194
			'lat' => number_format($lat, 2),
195
			'lon' => number_format($lon, 2),
196
			'addressdetails' => 1,
197
			'format' => 'json',
198
		];
199
		$url = 'https://nominatim.openstreetmap.org/reverse';
200
		$result = $this->requestJSON($url, $params);
201
		return $this->formatOsmAddress($result);
202
	}
203
204
	/**
205
	 * Get altitude from coordinates
206
	 *
207
	 * @param float $lat Latitude in decimal degree format
208
	 * @param float $lon Longitude in decimal degree format
209
	 * @return float altitude in meter
210
	 */
211
	private function getAltitude(float $lat, float $lon): float {
212
		$params = [
213
			'locations' => $lat . ',' . $lon,
214
		];
215
		$url = 'https://api.opentopodata.org/v1/srtm30m';
216
		$result = $this->requestJSON($url, $params);
217
		$altitude = 0;
218
		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...
219
			&& is_array($result['results'][0]) && isset($result['results'][0]['elevation'])) {
220
			$altitude = floatval($result['results'][0]['elevation']);
221
		}
222
		return $altitude;
223
	}
224
225
	/**
226
	 * @return string Formatted address from JSON nominatim result
227
	 */
228
	private function formatOsmAddress(array $json): ?string {
229
		if (isset($json['address']) && isset($json['display_name'])) {
230
			$jsonAddr = $json['address'];
231
			$cityAddress = '';
232
			// priority : city, town, village, municipality
233
			if (isset($jsonAddr['city'])) {
234
				$cityAddress .= $jsonAddr['city'];
235
			} elseif (isset($jsonAddr['town'])) {
236
				$cityAddress .= $jsonAddr['town'];
237
			} elseif (isset($jsonAddr['village'])) {
238
				$cityAddress .= $jsonAddr['village'];
239
			} elseif (isset($jsonAddr['municipality'])) {
240
				$cityAddress .= $jsonAddr['municipality'];
241
			} else {
242
				return $json['display_name'];
243
			}
244
			// post code
245
			if (isset($jsonAddr['postcode'])) {
246
				$cityAddress .= ', ' . $jsonAddr['postcode'];
247
			}
248
			// country
249
			if (isset($jsonAddr['country'])) {
250
				$cityAddress .= ', ' . $jsonAddr['country'];
251
				return $cityAddress;
252
			} else {
253
				return $json['display_name'];
254
			}
255
		} elseif (isset($json['display_name'])) {
256
			return $json['display_name'];
257
		}
258
		return null;
259
	}
260
261
	/**
262
	 * Set address and resolve it to get coordinates
263
	 *
264
	 * @param string $address Any approximative or exact address
265
	 * @return array with success state and address information (coordinates and formatted address)
266
	 */
267
	public function setAddress(string $address): array {
268
		$addressInfo = $this->searchForAddress($address);
269
		if (isset($addressInfo['display_name']) && isset($addressInfo['lat']) && isset($addressInfo['lon'])) {
270
			$formattedAddress = $this->formatOsmAddress($addressInfo);
271
			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $formattedAddress);
272
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($addressInfo['lat']));
273
			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($addressInfo['lon']));
274
			$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval(self::MODE_MANUAL_LOCATION));
275
			// get and store altitude
276
			$altitude = $this->getAltitude(floatval($addressInfo['lat']), floatval($addressInfo['lon']));
277
			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
278
			return [
279
				'lat' => $addressInfo['lat'],
280
				'lon' => $addressInfo['lon'],
281
				'address' => $formattedAddress,
282
				'success' => true,
283
			];
284
		} else {
285
			return ['success' => false];
286
		}
287
	}
288
289
	/**
290
	 * Ask nominatim information about an unformatted address
291
	 *
292
	 * @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...
293
	 * @return array Full Nominatim result for the given address
294
	 */
295
	private function searchForAddress(string $address): array {
296
		$params = [
297
			'format' => 'json',
298
			'addressdetails' => '1',
299
			'extratags' => '1',
300
			'namedetails' => '1',
301
			'limit' => '1',
302
		];
303
		$url = 'https://nominatim.openstreetmap.org/search/' . $address;
304
		$results = $this->requestJSON($url, $params);
305
		if (count($results) > 0) {
306
			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...
307
		}
308
		return ['error' => $this->l10n->t('No result.')];
309
	}
310
311
	/**
312
	 * Get stored user location
313
	 *
314
	 * @return array which contains coordinates, formatted address and current weather status mode
315
	 */
316
	public function getLocation(): array {
317
		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
318
		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
319
		$address = $this->config->getUserValue($this->userId, Application::APP_ID, 'address', '');
320
		$mode = $this->config->getUserValue($this->userId, Application::APP_ID, 'mode', self::MODE_MANUAL_LOCATION);
321
		return [
322
			'lat' => $lat,
323
			'lon' => $lon,
324
			'address' => $address,
325
			'mode' => intval($mode),
326
		];
327
	}
328
329
	/**
330
	 * Get forecast for current location
331
	 *
332
	 * @return array which contains success state and filtered forecast data
333
	 */
334
	public function getForecast(): array {
335
		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
336
		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
337
		$alt = $this->config->getUserValue($this->userId, Application::APP_ID, 'altitude', '');
338
		if (!is_numeric($alt)) {
339
			$alt = 0;
340
		}
341
		if (is_numeric($lat) && is_numeric($lon)) {
342
			return $this->forecastRequest(floatval($lat), floatval($lon), floatval($alt));
343
		} else {
344
			return ['success' => false];
345
		}
346
	}
347
348
	/**
349
	 * Actually make the request to the forecast service
350
	 *
351
	 * @param float $lat Latitude of requested forecast, in decimal degree format
352
	 * @param float $lon Longitude of requested forecast, in decimal degree format
353
	 * @param float $altitude Altitude of requested forecast, in meter
354
	 * @param int $nbValues Number of forecast values (hours)
355
	 * @return array Filtered forecast data
356
	 */
357
	private function forecastRequest(float $lat, float $lon, float $altitude, int $nbValues = 10): array {
358
		$params = [
359
			'lat' => number_format($lat, 2),
360
			'lon' => number_format($lon, 2),
361
			'altitude' => $altitude,
362
		];
363
		$url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact';
364
		$weather = $this->requestJSON($url, $params);
365
		if (isset($weather['properties']) && isset($weather['properties']['timeseries']) && is_array($weather['properties']['timeseries'])) {
366
			return array_slice($weather['properties']['timeseries'], 0, $nbValues);
367
		}
368
		return ['error' => $this->l10n->t('Malformed JSON data.')];
369
	}
370
371
	/**
372
	 * Make a HTTP GET request and parse JSON result.
373
	 * Request results are cached until the 'Expires' response header says so
374
	 *
375
	 * @param string $url Base URL to query
376
	 * @param array $params GET parameters
377
	 * @return array which contains the error message or the parsed JSON result
378
	 */
379
	private function requestJSON(string $url, array $params = []): array {
380
		if (isset($this->cache)) {
381
			$cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params));
382
			if ($this->cache->hasKey($cacheKey)) {
383
				return $this->cache->get($cacheKey);
384
			}
385
		}
386
		try {
387
			$options = [
388
				'headers' => [
389
					'User-Agent' => 'NextcloudWeatherStatus/' . $this->version . ' nextcloud.com'
390
				],
391
			];
392
393
			$reqUrl = $url;
394
			if (count($params) > 0) {
395
				$paramsContent = http_build_query($params);
396
				$reqUrl = $url . '?' . $paramsContent;
397
			}
398
399
			$response = $this->client->get($reqUrl, $options);
400
			$body = $response->getBody();
401
			$headers = $response->getHeaders();
402
			$respCode = $response->getStatusCode();
403
404
			if ($respCode >= 400) {
405
				return ['error' => $this->l10n->t('Error')];
406
			} else {
407
				$json = json_decode($body, true);
408
				if (isset($this->cache)) {
409
					// default cache duration is one hour
410
					$cacheDuration = 60 * 60;
411
					if (isset($headers['Expires']) && count($headers['Expires']) > 0) {
412
						// if the Expires response header is set, use it to define cache duration
413
						$expireTs = (new \Datetime($headers['Expires'][0]))->getTimestamp();
414
						$nowTs = (new \Datetime())->getTimestamp();
415
						$duration = $expireTs - $nowTs;
416
						if ($duration > $cacheDuration) {
417
							$cacheDuration = $duration;
418
						}
419
					}
420
					$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...
421
				}
422
				return $json;
423
			}
424
		} catch (\Exception $e) {
425
			$this->logger->warning($url . 'API error : ' . $e, ['app' => Application::APP_ID]);
426
			return ['error' => $e->getMessage()];
427
		}
428
	}
429
}
430