Passed
Push — master ( f11659...ffd76d )
by Roeland
14:01
created

Fetcher::fetch()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 28
nc 12
nop 2
dl 0
loc 44
rs 8.5386
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Lukas Reschke <[email protected]>
4
 *
5
 * @author Daniel Kesselberg <[email protected]>
6
 * @author Georg Ehrke <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author John Molakvoæ (skjnldsv) <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Steffen Lindner <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
31
namespace OC\App\AppStore\Fetcher;
32
33
use GuzzleHttp\Exception\ConnectException;
34
use OC\Files\AppData\Factory;
35
use OCP\AppFramework\Http;
36
use OCP\AppFramework\Utility\ITimeFactory;
37
use OCP\Files\IAppData;
38
use OCP\Files\NotFoundException;
39
use OCP\Http\Client\IClientService;
40
use OCP\IConfig;
41
use OCP\ILogger;
42
43
abstract class Fetcher {
44
	public const INVALIDATE_AFTER_SECONDS = 3600;
45
	public const RETRY_AFTER_FAILURE_SECONDS = 300;
46
47
	/** @var IAppData */
48
	protected $appData;
49
	/** @var IClientService */
50
	protected $clientService;
51
	/** @var ITimeFactory */
52
	protected $timeFactory;
53
	/** @var IConfig */
54
	protected $config;
55
	/** @var Ilogger */
56
	protected $logger;
57
	/** @var string */
58
	protected $fileName;
59
	/** @var string */
60
	protected $endpointName;
61
	/** @var string */
62
	protected $version;
63
	/** @var string */
64
	protected $channel;
65
66
	/**
67
	 * @param Factory $appDataFactory
68
	 * @param IClientService $clientService
69
	 * @param ITimeFactory $timeFactory
70
	 * @param IConfig $config
71
	 * @param ILogger $logger
72
	 */
73
	public function __construct(Factory $appDataFactory,
74
								IClientService $clientService,
75
								ITimeFactory $timeFactory,
76
								IConfig $config,
77
								ILogger $logger) {
78
		$this->appData = $appDataFactory->get('appstore');
79
		$this->clientService = $clientService;
80
		$this->timeFactory = $timeFactory;
81
		$this->config = $config;
82
		$this->logger = $logger;
83
	}
84
85
	/**
86
	 * Fetches the response from the server
87
	 *
88
	 * @param string $ETag
89
	 * @param string $content
90
	 *
91
	 * @return array
92
	 */
93
	protected function fetch($ETag, $content) {
94
		$appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
95
		if ((int)$this->config->getAppValue('settings', 'appstore-fetcher-lastFailure', '0') > time() - self::RETRY_AFTER_FAILURE_SECONDS) {
96
			return [];
97
		}
98
99
		if (!$appstoreenabled) {
100
			return [];
101
		}
102
103
		$options = [
104
			'timeout' => 60,
105
		];
106
107
		if ($ETag !== '') {
108
			$options['headers'] = [
109
				'If-None-Match' => $ETag,
110
			];
111
		}
112
113
		$client = $this->clientService->newClient();
114
		try {
115
			$response = $client->get($this->getEndpoint(), $options);
116
		} catch (ConnectException $e) {
117
			$this->config->setAppValue('settings', 'appstore-fetcher-lastFailure', (string)time());
118
			throw $e;
119
		}
120
121
		$responseJson = [];
122
		if ($response->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
123
			$responseJson['data'] = json_decode($content, true);
124
		} else {
125
			$responseJson['data'] = json_decode($response->getBody(), true);
126
			$ETag = $response->getHeader('ETag');
127
		}
128
		$this->config->deleteAppValue('settings', 'appstore-fetcher-lastFailure');
129
130
		$responseJson['timestamp'] = $this->timeFactory->getTime();
131
		$responseJson['ncversion'] = $this->getVersion();
132
		if ($ETag !== '') {
133
			$responseJson['ETag'] = $ETag;
134
		}
135
136
		return $responseJson;
137
	}
138
139
	/**
140
	 * Returns the array with the categories on the appstore server
141
	 *
142
	 * @param bool [$allowUnstable] Allow unstable releases
0 ignored issues
show
Documentation Bug introduced by
The doc comment [$allowUnstable] at position 0 could not be parsed: Unknown type name '[' at position 0 in [$allowUnstable].
Loading history...
143
	 * @return array
144
	 */
145
	public function get($allowUnstable = false) {
146
		$appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
147
		$internetavailable = $this->config->getSystemValue('has_internet_connection', true);
148
149
		if (!$appstoreenabled || !$internetavailable) {
150
			return [];
151
		}
152
153
		$rootFolder = $this->appData->getFolder('/');
154
155
		$ETag = '';
156
		$content = '';
157
158
		try {
159
			// File does already exists
160
			$file = $rootFolder->getFile($this->fileName);
161
			$jsonBlob = json_decode($file->getContent(), true);
162
163
			// Always get latests apps info if $allowUnstable
164
			if (!$allowUnstable && is_array($jsonBlob)) {
165
166
				// No caching when the version has been updated
167
				if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) {
168
169
					// If the timestamp is older than 3600 seconds request the files new
170
					if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) {
171
						return $jsonBlob['data'];
172
					}
173
174
					if (isset($jsonBlob['ETag'])) {
175
						$ETag = $jsonBlob['ETag'];
176
						$content = json_encode($jsonBlob['data']);
177
					}
178
				}
179
			}
180
		} catch (NotFoundException $e) {
181
			// File does not already exists
182
			$file = $rootFolder->newFile($this->fileName);
183
		}
184
185
		// Refresh the file content
186
		try {
187
			$responseJson = $this->fetch($ETag, $content, $allowUnstable);
0 ignored issues
show
Unused Code introduced by
The call to OC\App\AppStore\Fetcher\Fetcher::fetch() has too many arguments starting with $allowUnstable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

187
			/** @scrutinizer ignore-call */ 
188
   $responseJson = $this->fetch($ETag, $content, $allowUnstable);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
188
189
			if (empty($responseJson)) {
190
				return [];
191
			}
192
193
			// Don't store the apps request file
194
			if ($allowUnstable) {
195
				return $responseJson['data'];
196
			}
197
198
			$file->putContent(json_encode($responseJson));
199
			return json_decode($file->getContent(), true)['data'];
200
		} catch (ConnectException $e) {
201
			$this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
202
			return [];
203
		} catch (\Exception $e) {
204
			$this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

204
			$this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => /** @scrutinizer ignore-deprecated */ ILogger::WARN]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
205
			return [];
206
		}
207
	}
208
209
	/**
210
	 * Get the currently Nextcloud version
211
	 * @return string
212
	 */
213
	protected function getVersion() {
214
		if ($this->version === null) {
215
			$this->version = $this->config->getSystemValue('version', '0.0.0');
216
		}
217
		return $this->version;
218
	}
219
220
	/**
221
	 * Set the current Nextcloud version
222
	 * @param string $version
223
	 */
224
	public function setVersion(string $version) {
225
		$this->version = $version;
226
	}
227
228
	/**
229
	 * Get the currently Nextcloud update channel
230
	 * @return string
231
	 */
232
	protected function getChannel() {
233
		if ($this->channel === null) {
234
			$this->channel = \OC_Util::getChannel();
235
		}
236
		return $this->channel;
237
	}
238
239
	/**
240
	 * Set the current Nextcloud update channel
241
	 * @param string $channel
242
	 */
243
	public function setChannel(string $channel) {
244
		$this->channel = $channel;
245
	}
246
247
	protected function getEndpoint(): string {
248
		return $this->config->getSystemValue('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName;
249
	}
250
}
251