Passed
Push — master ( 90c0e9...e44f27 )
by Roeland
16:59 queued 10s
created
lib/public/Http/Client/IClient.php 1 patch
Indentation   +169 added lines, -169 removed lines patch added patch discarded remove patch
@@ -34,178 +34,178 @@
 block discarded – undo
34 34
  */
35 35
 interface IClient {
36 36
 
37
-	/**
38
-	 * Sends a GET request
39
-	 * @param string $uri
40
-	 * @param array $options Array such as
41
-	 *              'query' => [
42
-	 *                  'field' => 'abc',
43
-	 *                  'other_field' => '123',
44
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
45
-	 *              ],
46
-	 *              'headers' => [
47
-	 *                  'foo' => 'bar',
48
-	 *              ],
49
-	 *              'cookies' => ['
50
-	 *                  'foo' => 'bar',
51
-	 *              ],
52
-	 *              'allow_redirects' => [
53
-	 *                   'max'       => 10,  // allow at most 10 redirects.
54
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
55
-	 *                   'referer'   => true,     // add a Referer header
56
-	 *                   'protocols' => ['https'] // only allow https URLs
57
-	 *              ],
58
-	 *              'sink' => '/path/to/file', // save to a file or a stream
59
-	 *              'verify' => true, // bool or string to CA file
60
-	 *              'debug' => true,
61
-	 * @return IResponse
62
-	 * @throws \Exception If the request could not get completed
63
-	 * @since 8.1.0
64
-	 */
65
-	public function get(string $uri, array $options = []): IResponse;
37
+    /**
38
+     * Sends a GET request
39
+     * @param string $uri
40
+     * @param array $options Array such as
41
+     *              'query' => [
42
+     *                  'field' => 'abc',
43
+     *                  'other_field' => '123',
44
+     *                  'file_name' => fopen('/path/to/file', 'r'),
45
+     *              ],
46
+     *              'headers' => [
47
+     *                  'foo' => 'bar',
48
+     *              ],
49
+     *              'cookies' => ['
50
+     *                  'foo' => 'bar',
51
+     *              ],
52
+     *              'allow_redirects' => [
53
+     *                   'max'       => 10,  // allow at most 10 redirects.
54
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
55
+     *                   'referer'   => true,     // add a Referer header
56
+     *                   'protocols' => ['https'] // only allow https URLs
57
+     *              ],
58
+     *              'sink' => '/path/to/file', // save to a file or a stream
59
+     *              'verify' => true, // bool or string to CA file
60
+     *              'debug' => true,
61
+     * @return IResponse
62
+     * @throws \Exception If the request could not get completed
63
+     * @since 8.1.0
64
+     */
65
+    public function get(string $uri, array $options = []): IResponse;
66 66
 
67
-	/**
68
-	 * Sends a HEAD request
69
-	 * @param string $uri
70
-	 * @param array $options Array such as
71
-	 *              'headers' => [
72
-	 *                  'foo' => 'bar',
73
-	 *              ],
74
-	 *              'cookies' => ['
75
-	 *                  'foo' => 'bar',
76
-	 *              ],
77
-	 *              'allow_redirects' => [
78
-	 *                   'max'       => 10,  // allow at most 10 redirects.
79
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
80
-	 *                   'referer'   => true,     // add a Referer header
81
-	 *                   'protocols' => ['https'] // only allow https URLs
82
-	 *              ],
83
-	 *              'sink' => '/path/to/file', // save to a file or a stream
84
-	 *              'verify' => true, // bool or string to CA file
85
-	 *              'debug' => true,
86
-	 * @return IResponse
87
-	 * @throws \Exception If the request could not get completed
88
-	 * @since 8.1.0
89
-	 */
90
-	public function head(string $uri, array $options = []): IResponse;
67
+    /**
68
+     * Sends a HEAD request
69
+     * @param string $uri
70
+     * @param array $options Array such as
71
+     *              'headers' => [
72
+     *                  'foo' => 'bar',
73
+     *              ],
74
+     *              'cookies' => ['
75
+     *                  'foo' => 'bar',
76
+     *              ],
77
+     *              'allow_redirects' => [
78
+     *                   'max'       => 10,  // allow at most 10 redirects.
79
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
80
+     *                   'referer'   => true,     // add a Referer header
81
+     *                   'protocols' => ['https'] // only allow https URLs
82
+     *              ],
83
+     *              'sink' => '/path/to/file', // save to a file or a stream
84
+     *              'verify' => true, // bool or string to CA file
85
+     *              'debug' => true,
86
+     * @return IResponse
87
+     * @throws \Exception If the request could not get completed
88
+     * @since 8.1.0
89
+     */
90
+    public function head(string $uri, array $options = []): IResponse;
91 91
 
92
-	/**
93
-	 * Sends a POST request
94
-	 * @param string $uri
95
-	 * @param array $options Array such as
96
-	 *              'body' => [
97
-	 *                  'field' => 'abc',
98
-	 *                  'other_field' => '123',
99
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
100
-	 *              ],
101
-	 *              'headers' => [
102
-	 *                  'foo' => 'bar',
103
-	 *              ],
104
-	 *              'cookies' => ['
105
-	 *                  'foo' => 'bar',
106
-	 *              ],
107
-	 *              'allow_redirects' => [
108
-	 *                   'max'       => 10,  // allow at most 10 redirects.
109
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
110
-	 *                   'referer'   => true,     // add a Referer header
111
-	 *                   'protocols' => ['https'] // only allow https URLs
112
-	 *              ],
113
-	 *              'sink' => '/path/to/file', // save to a file or a stream
114
-	 *              'verify' => true, // bool or string to CA file
115
-	 *              'debug' => true,
116
-	 * @return IResponse
117
-	 * @throws \Exception If the request could not get completed
118
-	 * @since 8.1.0
119
-	 */
120
-	public function post(string $uri, array $options = []): IResponse;
92
+    /**
93
+     * Sends a POST request
94
+     * @param string $uri
95
+     * @param array $options Array such as
96
+     *              'body' => [
97
+     *                  'field' => 'abc',
98
+     *                  'other_field' => '123',
99
+     *                  'file_name' => fopen('/path/to/file', 'r'),
100
+     *              ],
101
+     *              'headers' => [
102
+     *                  'foo' => 'bar',
103
+     *              ],
104
+     *              'cookies' => ['
105
+     *                  'foo' => 'bar',
106
+     *              ],
107
+     *              'allow_redirects' => [
108
+     *                   'max'       => 10,  // allow at most 10 redirects.
109
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
110
+     *                   'referer'   => true,     // add a Referer header
111
+     *                   'protocols' => ['https'] // only allow https URLs
112
+     *              ],
113
+     *              'sink' => '/path/to/file', // save to a file or a stream
114
+     *              'verify' => true, // bool or string to CA file
115
+     *              'debug' => true,
116
+     * @return IResponse
117
+     * @throws \Exception If the request could not get completed
118
+     * @since 8.1.0
119
+     */
120
+    public function post(string $uri, array $options = []): IResponse;
121 121
 
122
-	/**
123
-	 * Sends a PUT request
124
-	 * @param string $uri
125
-	 * @param array $options Array such as
126
-	 *              'body' => [
127
-	 *                  'field' => 'abc',
128
-	 *                  'other_field' => '123',
129
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
130
-	 *              ],
131
-	 *              'headers' => [
132
-	 *                  'foo' => 'bar',
133
-	 *              ],
134
-	 *              'cookies' => ['
135
-	 *                  'foo' => 'bar',
136
-	 *              ],
137
-	 *              'allow_redirects' => [
138
-	 *                   'max'       => 10,  // allow at most 10 redirects.
139
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
140
-	 *                   'referer'   => true,     // add a Referer header
141
-	 *                   'protocols' => ['https'] // only allow https URLs
142
-	 *              ],
143
-	 *              'sink' => '/path/to/file', // save to a file or a stream
144
-	 *              'verify' => true, // bool or string to CA file
145
-	 *              'debug' => true,
146
-	 * @return IResponse
147
-	 * @throws \Exception If the request could not get completed
148
-	 * @since 8.1.0
149
-	 */
150
-	public function put(string $uri, array $options = []): IResponse;
122
+    /**
123
+     * Sends a PUT request
124
+     * @param string $uri
125
+     * @param array $options Array such as
126
+     *              'body' => [
127
+     *                  'field' => 'abc',
128
+     *                  'other_field' => '123',
129
+     *                  'file_name' => fopen('/path/to/file', 'r'),
130
+     *              ],
131
+     *              'headers' => [
132
+     *                  'foo' => 'bar',
133
+     *              ],
134
+     *              'cookies' => ['
135
+     *                  'foo' => 'bar',
136
+     *              ],
137
+     *              'allow_redirects' => [
138
+     *                   'max'       => 10,  // allow at most 10 redirects.
139
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
140
+     *                   'referer'   => true,     // add a Referer header
141
+     *                   'protocols' => ['https'] // only allow https URLs
142
+     *              ],
143
+     *              'sink' => '/path/to/file', // save to a file or a stream
144
+     *              'verify' => true, // bool or string to CA file
145
+     *              'debug' => true,
146
+     * @return IResponse
147
+     * @throws \Exception If the request could not get completed
148
+     * @since 8.1.0
149
+     */
150
+    public function put(string $uri, array $options = []): IResponse;
151 151
 
152
-	/**
153
-	 * Sends a DELETE request
154
-	 * @param string $uri
155
-	 * @param array $options Array such as
156
-	 *              'body' => [
157
-	 *                  'field' => 'abc',
158
-	 *                  'other_field' => '123',
159
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
160
-	 *              ],
161
-	 *              'headers' => [
162
-	 *                  'foo' => 'bar',
163
-	 *              ],
164
-	 *              'cookies' => ['
165
-	 *                  'foo' => 'bar',
166
-	 *              ],
167
-	 *              'allow_redirects' => [
168
-	 *                   'max'       => 10,  // allow at most 10 redirects.
169
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
170
-	 *                   'referer'   => true,     // add a Referer header
171
-	 *                   'protocols' => ['https'] // only allow https URLs
172
-	 *              ],
173
-	 *              'sink' => '/path/to/file', // save to a file or a stream
174
-	 *              'verify' => true, // bool or string to CA file
175
-	 *              'debug' => true,
176
-	 * @return IResponse
177
-	 * @throws \Exception If the request could not get completed
178
-	 * @since 8.1.0
179
-	 */
180
-	public function delete(string $uri, array $options = []): IResponse;
152
+    /**
153
+     * Sends a DELETE request
154
+     * @param string $uri
155
+     * @param array $options Array such as
156
+     *              'body' => [
157
+     *                  'field' => 'abc',
158
+     *                  'other_field' => '123',
159
+     *                  'file_name' => fopen('/path/to/file', 'r'),
160
+     *              ],
161
+     *              'headers' => [
162
+     *                  'foo' => 'bar',
163
+     *              ],
164
+     *              'cookies' => ['
165
+     *                  'foo' => 'bar',
166
+     *              ],
167
+     *              'allow_redirects' => [
168
+     *                   'max'       => 10,  // allow at most 10 redirects.
169
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
170
+     *                   'referer'   => true,     // add a Referer header
171
+     *                   'protocols' => ['https'] // only allow https URLs
172
+     *              ],
173
+     *              'sink' => '/path/to/file', // save to a file or a stream
174
+     *              'verify' => true, // bool or string to CA file
175
+     *              'debug' => true,
176
+     * @return IResponse
177
+     * @throws \Exception If the request could not get completed
178
+     * @since 8.1.0
179
+     */
180
+    public function delete(string $uri, array $options = []): IResponse;
181 181
 
182
-	/**
183
-	 * Sends a options request
184
-	 * @param string $uri
185
-	 * @param array $options Array such as
186
-	 *              'body' => [
187
-	 *                  'field' => 'abc',
188
-	 *                  'other_field' => '123',
189
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
190
-	 *              ],
191
-	 *              'headers' => [
192
-	 *                  'foo' => 'bar',
193
-	 *              ],
194
-	 *              'cookies' => ['
195
-	 *                  'foo' => 'bar',
196
-	 *              ],
197
-	 *              'allow_redirects' => [
198
-	 *                   'max'       => 10,  // allow at most 10 redirects.
199
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
200
-	 *                   'referer'   => true,     // add a Referer header
201
-	 *                   'protocols' => ['https'] // only allow https URLs
202
-	 *              ],
203
-	 *              'sink' => '/path/to/file', // save to a file or a stream
204
-	 *              'verify' => true, // bool or string to CA file
205
-	 *              'debug' => true,
206
-	 * @return IResponse
207
-	 * @throws \Exception If the request could not get completed
208
-	 * @since 8.1.0
209
-	 */
210
-	public function options(string $uri, array $options = []): IResponse;
182
+    /**
183
+     * Sends a options request
184
+     * @param string $uri
185
+     * @param array $options Array such as
186
+     *              'body' => [
187
+     *                  'field' => 'abc',
188
+     *                  'other_field' => '123',
189
+     *                  'file_name' => fopen('/path/to/file', 'r'),
190
+     *              ],
191
+     *              'headers' => [
192
+     *                  'foo' => 'bar',
193
+     *              ],
194
+     *              'cookies' => ['
195
+     *                  'foo' => 'bar',
196
+     *              ],
197
+     *              'allow_redirects' => [
198
+     *                   'max'       => 10,  // allow at most 10 redirects.
199
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
200
+     *                   'referer'   => true,     // add a Referer header
201
+     *                   'protocols' => ['https'] // only allow https URLs
202
+     *              ],
203
+     *              'sink' => '/path/to/file', // save to a file or a stream
204
+     *              'verify' => true, // bool or string to CA file
205
+     *              'debug' => true,
206
+     * @return IResponse
207
+     * @throws \Exception If the request could not get completed
208
+     * @since 8.1.0
209
+     */
210
+    public function options(string $uri, array $options = []): IResponse;
211 211
 }
Please login to merge, or discard this patch.
lib/private/Http/Client/Client.php 1 patch
Indentation   +363 added lines, -363 removed lines patch added patch discarded remove patch
@@ -49,367 +49,367 @@
 block discarded – undo
49 49
  * @package OC\Http
50 50
  */
51 51
 class Client implements IClient {
52
-	/** @var GuzzleClient */
53
-	private $client;
54
-	/** @var IConfig */
55
-	private $config;
56
-	/** @var ILogger */
57
-	private $logger;
58
-	/** @var ICertificateManager */
59
-	private $certificateManager;
60
-
61
-	public function __construct(
62
-		IConfig $config,
63
-		ILogger $logger,
64
-		ICertificateManager $certificateManager,
65
-		GuzzleClient $client
66
-	) {
67
-		$this->config = $config;
68
-		$this->logger = $logger;
69
-		$this->client = $client;
70
-		$this->certificateManager = $certificateManager;
71
-	}
72
-
73
-	private function buildRequestOptions(array $options): array {
74
-		$proxy = $this->getProxyUri();
75
-
76
-		$defaults = [
77
-			RequestOptions::VERIFY => $this->getCertBundle(),
78
-			RequestOptions::TIMEOUT => 30,
79
-		];
80
-
81
-		// Only add RequestOptions::PROXY if Nextcloud is explicitly
82
-		// configured to use a proxy. This is needed in order not to override
83
-		// Guzzle default values.
84
-		if ($proxy !== null) {
85
-			$defaults[RequestOptions::PROXY] = $proxy;
86
-		}
87
-
88
-		$options = array_merge($defaults, $options);
89
-
90
-		if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
91
-			$options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
92
-		}
93
-
94
-		if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
95
-			$options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
96
-		}
97
-
98
-		// Fallback for save_to
99
-		if (isset($options['save_to'])) {
100
-			$options['sink'] = $options['save_to'];
101
-			unset($options['save_to']);
102
-		}
103
-
104
-		return $options;
105
-	}
106
-
107
-	private function getCertBundle(): string {
108
-		// If the instance is not yet setup we need to use the static path as
109
-		// $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
110
-		// a view
111
-		if ($this->config->getSystemValue('installed', false) === false) {
112
-			return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
113
-		}
114
-
115
-		return $this->certificateManager->getAbsoluteBundlePath();
116
-	}
117
-
118
-	/**
119
-	 * Returns a null or an associative array specifiying the proxy URI for
120
-	 * 'http' and 'https' schemes, in addition to a 'no' key value pair
121
-	 * providing a list of host names that should not be proxied to.
122
-	 *
123
-	 * @return array|null
124
-	 *
125
-	 * The return array looks like:
126
-	 * [
127
-	 *   'http' => 'username:[email protected]',
128
-	 *   'https' => 'username:[email protected]',
129
-	 *   'no' => ['foo.com', 'bar.com']
130
-	 * ]
131
-	 *
132
-	 */
133
-	private function getProxyUri(): ?array {
134
-		$proxyHost = $this->config->getSystemValue('proxy', '');
135
-
136
-		if ($proxyHost === '' || $proxyHost === null) {
137
-			return null;
138
-		}
139
-
140
-		$proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
141
-		if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
142
-			$proxyHost = $proxyUserPwd . '@' . $proxyHost;
143
-		}
144
-
145
-		$proxy = [
146
-			'http' => $proxyHost,
147
-			'https' => $proxyHost,
148
-		];
149
-
150
-		$proxyExclude = $this->config->getSystemValue('proxyexclude', []);
151
-		if ($proxyExclude !== [] && $proxyExclude !== null) {
152
-			$proxy['no'] = $proxyExclude;
153
-		}
154
-
155
-		return $proxy;
156
-	}
157
-
158
-	protected function preventLocalAddress(string $uri, array $options): void {
159
-		if (($options['nextcloud']['allow_local_address'] ?? false) ||
160
-			$this->config->getSystemValueBool('allow_local_remote_servers', false)) {
161
-			return;
162
-		}
163
-
164
-		$host = parse_url($uri, PHP_URL_HOST);
165
-		if ($host === false || $host === null) {
166
-			$this->logger->warning("Could not detect any host in $uri");
167
-			throw new LocalServerException('Could not detect any host');
168
-		}
169
-
170
-		$host = strtolower($host);
171
-		// Remove brackets from IPv6 addresses
172
-		if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
173
-			$host = substr($host, 1, -1);
174
-		}
175
-
176
-		// Disallow localhost and local network
177
-		if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
178
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
179
-			throw new LocalServerException('Host violates local access rules');
180
-		}
181
-
182
-		// Disallow hostname only
183
-		if (substr_count($host, '.') === 0) {
184
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
185
-			throw new LocalServerException('Host violates local access rules');
186
-		}
187
-
188
-		if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
189
-			$this->logger->warning("Host $host was not connected to because it violates local access rules");
190
-			throw new LocalServerException('Host violates local access rules');
191
-		}
192
-
193
-		// Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
194
-		if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
195
-			$delimiter = strrpos($host, ':'); // Get last colon
196
-			$ipv4Address = substr($host, $delimiter + 1);
197
-
198
-			if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
199
-				$this->logger->warning("Host $host was not connected to because it violates local access rules");
200
-				throw new LocalServerException('Host violates local access rules');
201
-			}
202
-		}
203
-	}
204
-
205
-	/**
206
-	 * Sends a GET request
207
-	 *
208
-	 * @param string $uri
209
-	 * @param array $options Array such as
210
-	 *              'query' => [
211
-	 *                  'field' => 'abc',
212
-	 *                  'other_field' => '123',
213
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
214
-	 *              ],
215
-	 *              'headers' => [
216
-	 *                  'foo' => 'bar',
217
-	 *              ],
218
-	 *              'cookies' => ['
219
-	 *                  'foo' => 'bar',
220
-	 *              ],
221
-	 *              'allow_redirects' => [
222
-	 *                   'max'       => 10,  // allow at most 10 redirects.
223
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
224
-	 *                   'referer'   => true,     // add a Referer header
225
-	 *                   'protocols' => ['https'] // only allow https URLs
226
-	 *              ],
227
-	 *              'sink' => '/path/to/file', // save to a file or a stream
228
-	 *              'verify' => true, // bool or string to CA file
229
-	 *              'debug' => true,
230
-	 *              'timeout' => 5,
231
-	 * @return IResponse
232
-	 * @throws \Exception If the request could not get completed
233
-	 */
234
-	public function get(string $uri, array $options = []): IResponse {
235
-		$this->preventLocalAddress($uri, $options);
236
-		$response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
237
-		$isStream = isset($options['stream']) && $options['stream'];
238
-		return new Response($response, $isStream);
239
-	}
240
-
241
-	/**
242
-	 * Sends a HEAD request
243
-	 *
244
-	 * @param string $uri
245
-	 * @param array $options Array such as
246
-	 *              'headers' => [
247
-	 *                  'foo' => 'bar',
248
-	 *              ],
249
-	 *              'cookies' => ['
250
-	 *                  'foo' => 'bar',
251
-	 *              ],
252
-	 *              'allow_redirects' => [
253
-	 *                   'max'       => 10,  // allow at most 10 redirects.
254
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
255
-	 *                   'referer'   => true,     // add a Referer header
256
-	 *                   'protocols' => ['https'] // only allow https URLs
257
-	 *              ],
258
-	 *              'sink' => '/path/to/file', // save to a file or a stream
259
-	 *              'verify' => true, // bool or string to CA file
260
-	 *              'debug' => true,
261
-	 *              'timeout' => 5,
262
-	 * @return IResponse
263
-	 * @throws \Exception If the request could not get completed
264
-	 */
265
-	public function head(string $uri, array $options = []): IResponse {
266
-		$this->preventLocalAddress($uri, $options);
267
-		$response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
268
-		return new Response($response);
269
-	}
270
-
271
-	/**
272
-	 * Sends a POST request
273
-	 *
274
-	 * @param string $uri
275
-	 * @param array $options Array such as
276
-	 *              'body' => [
277
-	 *                  'field' => 'abc',
278
-	 *                  'other_field' => '123',
279
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
280
-	 *              ],
281
-	 *              'headers' => [
282
-	 *                  'foo' => 'bar',
283
-	 *              ],
284
-	 *              'cookies' => ['
285
-	 *                  'foo' => 'bar',
286
-	 *              ],
287
-	 *              'allow_redirects' => [
288
-	 *                   'max'       => 10,  // allow at most 10 redirects.
289
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
290
-	 *                   'referer'   => true,     // add a Referer header
291
-	 *                   'protocols' => ['https'] // only allow https URLs
292
-	 *              ],
293
-	 *              'sink' => '/path/to/file', // save to a file or a stream
294
-	 *              'verify' => true, // bool or string to CA file
295
-	 *              'debug' => true,
296
-	 *              'timeout' => 5,
297
-	 * @return IResponse
298
-	 * @throws \Exception If the request could not get completed
299
-	 */
300
-	public function post(string $uri, array $options = []): IResponse {
301
-		$this->preventLocalAddress($uri, $options);
302
-
303
-		if (isset($options['body']) && is_array($options['body'])) {
304
-			$options['form_params'] = $options['body'];
305
-			unset($options['body']);
306
-		}
307
-		$response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
308
-		return new Response($response);
309
-	}
310
-
311
-	/**
312
-	 * Sends a PUT request
313
-	 *
314
-	 * @param string $uri
315
-	 * @param array $options Array such as
316
-	 *              'body' => [
317
-	 *                  'field' => 'abc',
318
-	 *                  'other_field' => '123',
319
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
320
-	 *              ],
321
-	 *              'headers' => [
322
-	 *                  'foo' => 'bar',
323
-	 *              ],
324
-	 *              'cookies' => ['
325
-	 *                  'foo' => 'bar',
326
-	 *              ],
327
-	 *              'allow_redirects' => [
328
-	 *                   'max'       => 10,  // allow at most 10 redirects.
329
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
330
-	 *                   'referer'   => true,     // add a Referer header
331
-	 *                   'protocols' => ['https'] // only allow https URLs
332
-	 *              ],
333
-	 *              'sink' => '/path/to/file', // save to a file or a stream
334
-	 *              'verify' => true, // bool or string to CA file
335
-	 *              'debug' => true,
336
-	 *              'timeout' => 5,
337
-	 * @return IResponse
338
-	 * @throws \Exception If the request could not get completed
339
-	 */
340
-	public function put(string $uri, array $options = []): IResponse {
341
-		$this->preventLocalAddress($uri, $options);
342
-		$response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
343
-		return new Response($response);
344
-	}
345
-
346
-	/**
347
-	 * Sends a DELETE request
348
-	 *
349
-	 * @param string $uri
350
-	 * @param array $options Array such as
351
-	 *              'body' => [
352
-	 *                  'field' => 'abc',
353
-	 *                  'other_field' => '123',
354
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
355
-	 *              ],
356
-	 *              'headers' => [
357
-	 *                  'foo' => 'bar',
358
-	 *              ],
359
-	 *              'cookies' => ['
360
-	 *                  'foo' => 'bar',
361
-	 *              ],
362
-	 *              'allow_redirects' => [
363
-	 *                   'max'       => 10,  // allow at most 10 redirects.
364
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
365
-	 *                   'referer'   => true,     // add a Referer header
366
-	 *                   'protocols' => ['https'] // only allow https URLs
367
-	 *              ],
368
-	 *              'sink' => '/path/to/file', // save to a file or a stream
369
-	 *              'verify' => true, // bool or string to CA file
370
-	 *              'debug' => true,
371
-	 *              'timeout' => 5,
372
-	 * @return IResponse
373
-	 * @throws \Exception If the request could not get completed
374
-	 */
375
-	public function delete(string $uri, array $options = []): IResponse {
376
-		$this->preventLocalAddress($uri, $options);
377
-		$response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
378
-		return new Response($response);
379
-	}
380
-
381
-	/**
382
-	 * Sends a options request
383
-	 *
384
-	 * @param string $uri
385
-	 * @param array $options Array such as
386
-	 *              'body' => [
387
-	 *                  'field' => 'abc',
388
-	 *                  'other_field' => '123',
389
-	 *                  'file_name' => fopen('/path/to/file', 'r'),
390
-	 *              ],
391
-	 *              'headers' => [
392
-	 *                  'foo' => 'bar',
393
-	 *              ],
394
-	 *              'cookies' => ['
395
-	 *                  'foo' => 'bar',
396
-	 *              ],
397
-	 *              'allow_redirects' => [
398
-	 *                   'max'       => 10,  // allow at most 10 redirects.
399
-	 *                   'strict'    => true,     // use "strict" RFC compliant redirects.
400
-	 *                   'referer'   => true,     // add a Referer header
401
-	 *                   'protocols' => ['https'] // only allow https URLs
402
-	 *              ],
403
-	 *              'sink' => '/path/to/file', // save to a file or a stream
404
-	 *              'verify' => true, // bool or string to CA file
405
-	 *              'debug' => true,
406
-	 *              'timeout' => 5,
407
-	 * @return IResponse
408
-	 * @throws \Exception If the request could not get completed
409
-	 */
410
-	public function options(string $uri, array $options = []): IResponse {
411
-		$this->preventLocalAddress($uri, $options);
412
-		$response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
413
-		return new Response($response);
414
-	}
52
+    /** @var GuzzleClient */
53
+    private $client;
54
+    /** @var IConfig */
55
+    private $config;
56
+    /** @var ILogger */
57
+    private $logger;
58
+    /** @var ICertificateManager */
59
+    private $certificateManager;
60
+
61
+    public function __construct(
62
+        IConfig $config,
63
+        ILogger $logger,
64
+        ICertificateManager $certificateManager,
65
+        GuzzleClient $client
66
+    ) {
67
+        $this->config = $config;
68
+        $this->logger = $logger;
69
+        $this->client = $client;
70
+        $this->certificateManager = $certificateManager;
71
+    }
72
+
73
+    private function buildRequestOptions(array $options): array {
74
+        $proxy = $this->getProxyUri();
75
+
76
+        $defaults = [
77
+            RequestOptions::VERIFY => $this->getCertBundle(),
78
+            RequestOptions::TIMEOUT => 30,
79
+        ];
80
+
81
+        // Only add RequestOptions::PROXY if Nextcloud is explicitly
82
+        // configured to use a proxy. This is needed in order not to override
83
+        // Guzzle default values.
84
+        if ($proxy !== null) {
85
+            $defaults[RequestOptions::PROXY] = $proxy;
86
+        }
87
+
88
+        $options = array_merge($defaults, $options);
89
+
90
+        if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
91
+            $options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
92
+        }
93
+
94
+        if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
95
+            $options[RequestOptions::HEADERS]['Accept-Encoding'] = 'gzip';
96
+        }
97
+
98
+        // Fallback for save_to
99
+        if (isset($options['save_to'])) {
100
+            $options['sink'] = $options['save_to'];
101
+            unset($options['save_to']);
102
+        }
103
+
104
+        return $options;
105
+    }
106
+
107
+    private function getCertBundle(): string {
108
+        // If the instance is not yet setup we need to use the static path as
109
+        // $this->certificateManager->getAbsoluteBundlePath() tries to instantiate
110
+        // a view
111
+        if ($this->config->getSystemValue('installed', false) === false) {
112
+            return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
113
+        }
114
+
115
+        return $this->certificateManager->getAbsoluteBundlePath();
116
+    }
117
+
118
+    /**
119
+     * Returns a null or an associative array specifiying the proxy URI for
120
+     * 'http' and 'https' schemes, in addition to a 'no' key value pair
121
+     * providing a list of host names that should not be proxied to.
122
+     *
123
+     * @return array|null
124
+     *
125
+     * The return array looks like:
126
+     * [
127
+     *   'http' => 'username:[email protected]',
128
+     *   'https' => 'username:[email protected]',
129
+     *   'no' => ['foo.com', 'bar.com']
130
+     * ]
131
+     *
132
+     */
133
+    private function getProxyUri(): ?array {
134
+        $proxyHost = $this->config->getSystemValue('proxy', '');
135
+
136
+        if ($proxyHost === '' || $proxyHost === null) {
137
+            return null;
138
+        }
139
+
140
+        $proxyUserPwd = $this->config->getSystemValue('proxyuserpwd', '');
141
+        if ($proxyUserPwd !== '' && $proxyUserPwd !== null) {
142
+            $proxyHost = $proxyUserPwd . '@' . $proxyHost;
143
+        }
144
+
145
+        $proxy = [
146
+            'http' => $proxyHost,
147
+            'https' => $proxyHost,
148
+        ];
149
+
150
+        $proxyExclude = $this->config->getSystemValue('proxyexclude', []);
151
+        if ($proxyExclude !== [] && $proxyExclude !== null) {
152
+            $proxy['no'] = $proxyExclude;
153
+        }
154
+
155
+        return $proxy;
156
+    }
157
+
158
+    protected function preventLocalAddress(string $uri, array $options): void {
159
+        if (($options['nextcloud']['allow_local_address'] ?? false) ||
160
+            $this->config->getSystemValueBool('allow_local_remote_servers', false)) {
161
+            return;
162
+        }
163
+
164
+        $host = parse_url($uri, PHP_URL_HOST);
165
+        if ($host === false || $host === null) {
166
+            $this->logger->warning("Could not detect any host in $uri");
167
+            throw new LocalServerException('Could not detect any host');
168
+        }
169
+
170
+        $host = strtolower($host);
171
+        // Remove brackets from IPv6 addresses
172
+        if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
173
+            $host = substr($host, 1, -1);
174
+        }
175
+
176
+        // Disallow localhost and local network
177
+        if ($host === 'localhost' || substr($host, -6) === '.local' || substr($host, -10) === '.localhost') {
178
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
179
+            throw new LocalServerException('Host violates local access rules');
180
+        }
181
+
182
+        // Disallow hostname only
183
+        if (substr_count($host, '.') === 0) {
184
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
185
+            throw new LocalServerException('Host violates local access rules');
186
+        }
187
+
188
+        if ((bool)filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
189
+            $this->logger->warning("Host $host was not connected to because it violates local access rules");
190
+            throw new LocalServerException('Host violates local access rules');
191
+        }
192
+
193
+        // Also check for IPv6 IPv4 nesting, because that's not covered by filter_var
194
+        if ((bool)filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && substr_count($host, '.') > 0) {
195
+            $delimiter = strrpos($host, ':'); // Get last colon
196
+            $ipv4Address = substr($host, $delimiter + 1);
197
+
198
+            if (!filter_var($ipv4Address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
199
+                $this->logger->warning("Host $host was not connected to because it violates local access rules");
200
+                throw new LocalServerException('Host violates local access rules');
201
+            }
202
+        }
203
+    }
204
+
205
+    /**
206
+     * Sends a GET request
207
+     *
208
+     * @param string $uri
209
+     * @param array $options Array such as
210
+     *              'query' => [
211
+     *                  'field' => 'abc',
212
+     *                  'other_field' => '123',
213
+     *                  'file_name' => fopen('/path/to/file', 'r'),
214
+     *              ],
215
+     *              'headers' => [
216
+     *                  'foo' => 'bar',
217
+     *              ],
218
+     *              'cookies' => ['
219
+     *                  'foo' => 'bar',
220
+     *              ],
221
+     *              'allow_redirects' => [
222
+     *                   'max'       => 10,  // allow at most 10 redirects.
223
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
224
+     *                   'referer'   => true,     // add a Referer header
225
+     *                   'protocols' => ['https'] // only allow https URLs
226
+     *              ],
227
+     *              'sink' => '/path/to/file', // save to a file or a stream
228
+     *              'verify' => true, // bool or string to CA file
229
+     *              'debug' => true,
230
+     *              'timeout' => 5,
231
+     * @return IResponse
232
+     * @throws \Exception If the request could not get completed
233
+     */
234
+    public function get(string $uri, array $options = []): IResponse {
235
+        $this->preventLocalAddress($uri, $options);
236
+        $response = $this->client->request('get', $uri, $this->buildRequestOptions($options));
237
+        $isStream = isset($options['stream']) && $options['stream'];
238
+        return new Response($response, $isStream);
239
+    }
240
+
241
+    /**
242
+     * Sends a HEAD request
243
+     *
244
+     * @param string $uri
245
+     * @param array $options Array such as
246
+     *              'headers' => [
247
+     *                  'foo' => 'bar',
248
+     *              ],
249
+     *              'cookies' => ['
250
+     *                  'foo' => 'bar',
251
+     *              ],
252
+     *              'allow_redirects' => [
253
+     *                   'max'       => 10,  // allow at most 10 redirects.
254
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
255
+     *                   'referer'   => true,     // add a Referer header
256
+     *                   'protocols' => ['https'] // only allow https URLs
257
+     *              ],
258
+     *              'sink' => '/path/to/file', // save to a file or a stream
259
+     *              'verify' => true, // bool or string to CA file
260
+     *              'debug' => true,
261
+     *              'timeout' => 5,
262
+     * @return IResponse
263
+     * @throws \Exception If the request could not get completed
264
+     */
265
+    public function head(string $uri, array $options = []): IResponse {
266
+        $this->preventLocalAddress($uri, $options);
267
+        $response = $this->client->request('head', $uri, $this->buildRequestOptions($options));
268
+        return new Response($response);
269
+    }
270
+
271
+    /**
272
+     * Sends a POST request
273
+     *
274
+     * @param string $uri
275
+     * @param array $options Array such as
276
+     *              'body' => [
277
+     *                  'field' => 'abc',
278
+     *                  'other_field' => '123',
279
+     *                  'file_name' => fopen('/path/to/file', 'r'),
280
+     *              ],
281
+     *              'headers' => [
282
+     *                  'foo' => 'bar',
283
+     *              ],
284
+     *              'cookies' => ['
285
+     *                  'foo' => 'bar',
286
+     *              ],
287
+     *              'allow_redirects' => [
288
+     *                   'max'       => 10,  // allow at most 10 redirects.
289
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
290
+     *                   'referer'   => true,     // add a Referer header
291
+     *                   'protocols' => ['https'] // only allow https URLs
292
+     *              ],
293
+     *              'sink' => '/path/to/file', // save to a file or a stream
294
+     *              'verify' => true, // bool or string to CA file
295
+     *              'debug' => true,
296
+     *              'timeout' => 5,
297
+     * @return IResponse
298
+     * @throws \Exception If the request could not get completed
299
+     */
300
+    public function post(string $uri, array $options = []): IResponse {
301
+        $this->preventLocalAddress($uri, $options);
302
+
303
+        if (isset($options['body']) && is_array($options['body'])) {
304
+            $options['form_params'] = $options['body'];
305
+            unset($options['body']);
306
+        }
307
+        $response = $this->client->request('post', $uri, $this->buildRequestOptions($options));
308
+        return new Response($response);
309
+    }
310
+
311
+    /**
312
+     * Sends a PUT request
313
+     *
314
+     * @param string $uri
315
+     * @param array $options Array such as
316
+     *              'body' => [
317
+     *                  'field' => 'abc',
318
+     *                  'other_field' => '123',
319
+     *                  'file_name' => fopen('/path/to/file', 'r'),
320
+     *              ],
321
+     *              'headers' => [
322
+     *                  'foo' => 'bar',
323
+     *              ],
324
+     *              'cookies' => ['
325
+     *                  'foo' => 'bar',
326
+     *              ],
327
+     *              'allow_redirects' => [
328
+     *                   'max'       => 10,  // allow at most 10 redirects.
329
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
330
+     *                   'referer'   => true,     // add a Referer header
331
+     *                   'protocols' => ['https'] // only allow https URLs
332
+     *              ],
333
+     *              'sink' => '/path/to/file', // save to a file or a stream
334
+     *              'verify' => true, // bool or string to CA file
335
+     *              'debug' => true,
336
+     *              'timeout' => 5,
337
+     * @return IResponse
338
+     * @throws \Exception If the request could not get completed
339
+     */
340
+    public function put(string $uri, array $options = []): IResponse {
341
+        $this->preventLocalAddress($uri, $options);
342
+        $response = $this->client->request('put', $uri, $this->buildRequestOptions($options));
343
+        return new Response($response);
344
+    }
345
+
346
+    /**
347
+     * Sends a DELETE request
348
+     *
349
+     * @param string $uri
350
+     * @param array $options Array such as
351
+     *              'body' => [
352
+     *                  'field' => 'abc',
353
+     *                  'other_field' => '123',
354
+     *                  'file_name' => fopen('/path/to/file', 'r'),
355
+     *              ],
356
+     *              'headers' => [
357
+     *                  'foo' => 'bar',
358
+     *              ],
359
+     *              'cookies' => ['
360
+     *                  'foo' => 'bar',
361
+     *              ],
362
+     *              'allow_redirects' => [
363
+     *                   'max'       => 10,  // allow at most 10 redirects.
364
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
365
+     *                   'referer'   => true,     // add a Referer header
366
+     *                   'protocols' => ['https'] // only allow https URLs
367
+     *              ],
368
+     *              'sink' => '/path/to/file', // save to a file or a stream
369
+     *              'verify' => true, // bool or string to CA file
370
+     *              'debug' => true,
371
+     *              'timeout' => 5,
372
+     * @return IResponse
373
+     * @throws \Exception If the request could not get completed
374
+     */
375
+    public function delete(string $uri, array $options = []): IResponse {
376
+        $this->preventLocalAddress($uri, $options);
377
+        $response = $this->client->request('delete', $uri, $this->buildRequestOptions($options));
378
+        return new Response($response);
379
+    }
380
+
381
+    /**
382
+     * Sends a options request
383
+     *
384
+     * @param string $uri
385
+     * @param array $options Array such as
386
+     *              'body' => [
387
+     *                  'field' => 'abc',
388
+     *                  'other_field' => '123',
389
+     *                  'file_name' => fopen('/path/to/file', 'r'),
390
+     *              ],
391
+     *              'headers' => [
392
+     *                  'foo' => 'bar',
393
+     *              ],
394
+     *              'cookies' => ['
395
+     *                  'foo' => 'bar',
396
+     *              ],
397
+     *              'allow_redirects' => [
398
+     *                   'max'       => 10,  // allow at most 10 redirects.
399
+     *                   'strict'    => true,     // use "strict" RFC compliant redirects.
400
+     *                   'referer'   => true,     // add a Referer header
401
+     *                   'protocols' => ['https'] // only allow https URLs
402
+     *              ],
403
+     *              'sink' => '/path/to/file', // save to a file or a stream
404
+     *              'verify' => true, // bool or string to CA file
405
+     *              'debug' => true,
406
+     *              'timeout' => 5,
407
+     * @return IResponse
408
+     * @throws \Exception If the request could not get completed
409
+     */
410
+    public function options(string $uri, array $options = []): IResponse {
411
+        $this->preventLocalAddress($uri, $options);
412
+        $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
413
+        return new Response($response);
414
+    }
415 415
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 1 patch
Indentation   +578 added lines, -578 removed lines patch added patch discarded remove patch
@@ -59,582 +59,582 @@
 block discarded – undo
59 59
  * This class provides the functionality needed to install, update and remove apps
60 60
  */
61 61
 class Installer {
62
-	/** @var AppFetcher */
63
-	private $appFetcher;
64
-	/** @var IClientService */
65
-	private $clientService;
66
-	/** @var ITempManager */
67
-	private $tempManager;
68
-	/** @var ILogger */
69
-	private $logger;
70
-	/** @var IConfig */
71
-	private $config;
72
-	/** @var array - for caching the result of app fetcher */
73
-	private $apps = null;
74
-	/** @var bool|null - for caching the result of the ready status */
75
-	private $isInstanceReadyForUpdates = null;
76
-	/** @var bool */
77
-	private $isCLI;
78
-
79
-	/**
80
-	 * @param AppFetcher $appFetcher
81
-	 * @param IClientService $clientService
82
-	 * @param ITempManager $tempManager
83
-	 * @param ILogger $logger
84
-	 * @param IConfig $config
85
-	 */
86
-	public function __construct(
87
-		AppFetcher $appFetcher,
88
-		IClientService $clientService,
89
-		ITempManager $tempManager,
90
-		ILogger $logger,
91
-		IConfig $config,
92
-		bool $isCLI
93
-	) {
94
-		$this->appFetcher = $appFetcher;
95
-		$this->clientService = $clientService;
96
-		$this->tempManager = $tempManager;
97
-		$this->logger = $logger;
98
-		$this->config = $config;
99
-		$this->isCLI = $isCLI;
100
-	}
101
-
102
-	/**
103
-	 * Installs an app that is located in one of the app folders already
104
-	 *
105
-	 * @param string $appId App to install
106
-	 * @param bool $forceEnable
107
-	 * @throws \Exception
108
-	 * @return string app ID
109
-	 */
110
-	public function installApp(string $appId, bool $forceEnable = false): string {
111
-		$app = \OC_App::findAppInDirectories($appId);
112
-		if ($app === false) {
113
-			throw new \Exception('App not found in any app directory');
114
-		}
115
-
116
-		$basedir = $app['path'].'/'.$appId;
117
-		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
118
-
119
-		$l = \OC::$server->getL10N('core');
120
-
121
-		if (!is_array($info)) {
122
-			throw new \Exception(
123
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
124
-					[$appId]
125
-				)
126
-			);
127
-		}
128
-
129
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
130
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
131
-
132
-		$version = implode('.', \OCP\Util::getVersion());
133
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
134
-			throw new \Exception(
135
-				// TODO $l
136
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
137
-					[$info['name']]
138
-				)
139
-			);
140
-		}
141
-
142
-		// check for required dependencies
143
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
144
-		/** @var Coordinator $coordinator */
145
-		$coordinator = \OC::$server->get(Coordinator::class);
146
-		$coordinator->runLazyRegistration($appId);
147
-		\OC_App::registerAutoloading($appId, $basedir);
148
-
149
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
150
-		if ($previousVersion) {
151
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
152
-		}
153
-
154
-		//install the database
155
-		if (is_file($basedir.'/appinfo/database.xml')) {
156
-			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
157
-				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
158
-			} else {
159
-				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
160
-			}
161
-		} else {
162
-			$ms = new \OC\DB\MigrationService($info['id'], \OC::$server->get(Connection::class));
163
-			$ms->migrate('latest', true);
164
-		}
165
-		if ($previousVersion) {
166
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
167
-		}
168
-
169
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
170
-
171
-		//run appinfo/install.php
172
-		self::includeAppScript($basedir . '/appinfo/install.php');
173
-
174
-		$appData = OC_App::getAppInfo($appId);
175
-		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
176
-
177
-		//set the installed version
178
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
179
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
180
-
181
-		//set remote/public handlers
182
-		foreach ($info['remote'] as $name => $path) {
183
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
184
-		}
185
-		foreach ($info['public'] as $name => $path) {
186
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
187
-		}
188
-
189
-		OC_App::setAppTypes($info['id']);
190
-
191
-		return $info['id'];
192
-	}
193
-
194
-	/**
195
-	 * Updates the specified app from the appstore
196
-	 *
197
-	 * @param string $appId
198
-	 * @param bool [$allowUnstable] Allow unstable releases
199
-	 * @return bool
200
-	 */
201
-	public function updateAppstoreApp($appId, $allowUnstable = false) {
202
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
203
-			try {
204
-				$this->downloadApp($appId, $allowUnstable);
205
-			} catch (\Exception $e) {
206
-				$this->logger->logException($e, [
207
-					'level' => ILogger::ERROR,
208
-					'app' => 'core',
209
-				]);
210
-				return false;
211
-			}
212
-			return OC_App::updateApp($appId);
213
-		}
214
-
215
-		return false;
216
-	}
217
-
218
-	/**
219
-	 * Downloads an app and puts it into the app directory
220
-	 *
221
-	 * @param string $appId
222
-	 * @param bool [$allowUnstable]
223
-	 *
224
-	 * @throws \Exception If the installation was not successful
225
-	 */
226
-	public function downloadApp($appId, $allowUnstable = false) {
227
-		$appId = strtolower($appId);
228
-
229
-		$apps = $this->appFetcher->get($allowUnstable);
230
-		foreach ($apps as $app) {
231
-			if ($app['id'] === $appId) {
232
-				// Load the certificate
233
-				$certificate = new X509();
234
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
235
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
236
-
237
-				// Verify if the certificate has been revoked
238
-				$crl = new X509();
239
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
240
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
241
-				if ($crl->validateSignature() !== true) {
242
-					throw new \Exception('Could not validate CRL signature');
243
-				}
244
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
245
-				$revoked = $crl->getRevoked($csn);
246
-				if ($revoked !== false) {
247
-					throw new \Exception(
248
-						sprintf(
249
-							'Certificate "%s" has been revoked',
250
-							$csn
251
-						)
252
-					);
253
-				}
254
-
255
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
256
-				if ($certificate->validateSignature() !== true) {
257
-					throw new \Exception(
258
-						sprintf(
259
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
260
-							$appId
261
-						)
262
-					);
263
-				}
264
-
265
-				// Verify if the certificate is issued for the requested app id
266
-				$certInfo = openssl_x509_parse($app['certificate']);
267
-				if (!isset($certInfo['subject']['CN'])) {
268
-					throw new \Exception(
269
-						sprintf(
270
-							'App with id %s has a cert with no CN',
271
-							$appId
272
-						)
273
-					);
274
-				}
275
-				if ($certInfo['subject']['CN'] !== $appId) {
276
-					throw new \Exception(
277
-						sprintf(
278
-							'App with id %s has a cert issued to %s',
279
-							$appId,
280
-							$certInfo['subject']['CN']
281
-						)
282
-					);
283
-				}
284
-
285
-				// Download the release
286
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
287
-				$timeout = $this->isCLI ? 0 : 120;
288
-				$client = $this->clientService->newClient();
289
-				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
290
-
291
-				// Check if the signature actually matches the downloaded content
292
-				$certificate = openssl_get_publickey($app['certificate']);
293
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
294
-				openssl_free_key($certificate);
295
-
296
-				if ($verified === true) {
297
-					// Seems to match, let's proceed
298
-					$extractDir = $this->tempManager->getTemporaryFolder();
299
-					$archive = new TAR($tempFile);
300
-
301
-					if ($archive) {
302
-						if (!$archive->extract($extractDir)) {
303
-							$errorMessage = 'Could not extract app ' . $appId;
304
-
305
-							$archiveError = $archive->getError();
306
-							if ($archiveError instanceof \PEAR_Error) {
307
-								$errorMessage .= ': ' . $archiveError->getMessage();
308
-							}
309
-
310
-							throw new \Exception($errorMessage);
311
-						}
312
-						$allFiles = scandir($extractDir);
313
-						$folders = array_diff($allFiles, ['.', '..']);
314
-						$folders = array_values($folders);
315
-
316
-						if (count($folders) > 1) {
317
-							throw new \Exception(
318
-								sprintf(
319
-									'Extracted app %s has more than 1 folder',
320
-									$appId
321
-								)
322
-							);
323
-						}
324
-
325
-						// Check if appinfo/info.xml has the same app ID as well
326
-						$loadEntities = libxml_disable_entity_loader(false);
327
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
328
-						libxml_disable_entity_loader($loadEntities);
329
-						if ((string)$xml->id !== $appId) {
330
-							throw new \Exception(
331
-								sprintf(
332
-									'App for id %s has a wrong app ID in info.xml: %s',
333
-									$appId,
334
-									(string)$xml->id
335
-								)
336
-							);
337
-						}
338
-
339
-						// Check if the version is lower than before
340
-						$currentVersion = OC_App::getAppVersion($appId);
341
-						$newVersion = (string)$xml->version;
342
-						if (version_compare($currentVersion, $newVersion) === 1) {
343
-							throw new \Exception(
344
-								sprintf(
345
-									'App for id %s has version %s and tried to update to lower version %s',
346
-									$appId,
347
-									$currentVersion,
348
-									$newVersion
349
-								)
350
-							);
351
-						}
352
-
353
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
354
-						// Remove old app with the ID if existent
355
-						OC_Helper::rmdirr($baseDir);
356
-						// Move to app folder
357
-						if (@mkdir($baseDir)) {
358
-							$extractDir .= '/' . $folders[0];
359
-							OC_Helper::copyr($extractDir, $baseDir);
360
-						}
361
-						OC_Helper::copyr($extractDir, $baseDir);
362
-						OC_Helper::rmdirr($extractDir);
363
-						return;
364
-					} else {
365
-						throw new \Exception(
366
-							sprintf(
367
-								'Could not extract app with ID %s to %s',
368
-								$appId,
369
-								$extractDir
370
-							)
371
-						);
372
-					}
373
-				} else {
374
-					// Signature does not match
375
-					throw new \Exception(
376
-						sprintf(
377
-							'App with id %s has invalid signature',
378
-							$appId
379
-						)
380
-					);
381
-				}
382
-			}
383
-		}
384
-
385
-		throw new \Exception(
386
-			sprintf(
387
-				'Could not download app %s',
388
-				$appId
389
-			)
390
-		);
391
-	}
392
-
393
-	/**
394
-	 * Check if an update for the app is available
395
-	 *
396
-	 * @param string $appId
397
-	 * @param bool $allowUnstable
398
-	 * @return string|false false or the version number of the update
399
-	 */
400
-	public function isUpdateAvailable($appId, $allowUnstable = false) {
401
-		if ($this->isInstanceReadyForUpdates === null) {
402
-			$installPath = OC_App::getInstallPath();
403
-			if ($installPath === false || $installPath === null) {
404
-				$this->isInstanceReadyForUpdates = false;
405
-			} else {
406
-				$this->isInstanceReadyForUpdates = true;
407
-			}
408
-		}
409
-
410
-		if ($this->isInstanceReadyForUpdates === false) {
411
-			return false;
412
-		}
413
-
414
-		if ($this->isInstalledFromGit($appId) === true) {
415
-			return false;
416
-		}
417
-
418
-		if ($this->apps === null) {
419
-			$this->apps = $this->appFetcher->get($allowUnstable);
420
-		}
421
-
422
-		foreach ($this->apps as $app) {
423
-			if ($app['id'] === $appId) {
424
-				$currentVersion = OC_App::getAppVersion($appId);
425
-
426
-				if (!isset($app['releases'][0]['version'])) {
427
-					return false;
428
-				}
429
-				$newestVersion = $app['releases'][0]['version'];
430
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
431
-					return $newestVersion;
432
-				} else {
433
-					return false;
434
-				}
435
-			}
436
-		}
437
-
438
-		return false;
439
-	}
440
-
441
-	/**
442
-	 * Check if app has been installed from git
443
-	 * @param string $name name of the application to remove
444
-	 * @return boolean
445
-	 *
446
-	 * The function will check if the path contains a .git folder
447
-	 */
448
-	private function isInstalledFromGit($appId) {
449
-		$app = \OC_App::findAppInDirectories($appId);
450
-		if ($app === false) {
451
-			return false;
452
-		}
453
-		$basedir = $app['path'].'/'.$appId;
454
-		return file_exists($basedir.'/.git/');
455
-	}
456
-
457
-	/**
458
-	 * Check if app is already downloaded
459
-	 * @param string $name name of the application to remove
460
-	 * @return boolean
461
-	 *
462
-	 * The function will check if the app is already downloaded in the apps repository
463
-	 */
464
-	public function isDownloaded($name) {
465
-		foreach (\OC::$APPSROOTS as $dir) {
466
-			$dirToTest = $dir['path'];
467
-			$dirToTest .= '/';
468
-			$dirToTest .= $name;
469
-			$dirToTest .= '/';
470
-
471
-			if (is_dir($dirToTest)) {
472
-				return true;
473
-			}
474
-		}
475
-
476
-		return false;
477
-	}
478
-
479
-	/**
480
-	 * Removes an app
481
-	 * @param string $appId ID of the application to remove
482
-	 * @return boolean
483
-	 *
484
-	 *
485
-	 * This function works as follows
486
-	 *   -# call uninstall repair steps
487
-	 *   -# removing the files
488
-	 *
489
-	 * The function will not delete preferences, tables and the configuration,
490
-	 * this has to be done by the function oc_app_uninstall().
491
-	 */
492
-	public function removeApp($appId) {
493
-		if ($this->isDownloaded($appId)) {
494
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
495
-				return false;
496
-			}
497
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
498
-			OC_Helper::rmdirr($appDir);
499
-			return true;
500
-		} else {
501
-			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
502
-
503
-			return false;
504
-		}
505
-	}
506
-
507
-	/**
508
-	 * Installs the app within the bundle and marks the bundle as installed
509
-	 *
510
-	 * @param Bundle $bundle
511
-	 * @throws \Exception If app could not get installed
512
-	 */
513
-	public function installAppBundle(Bundle $bundle) {
514
-		$appIds = $bundle->getAppIdentifiers();
515
-		foreach ($appIds as $appId) {
516
-			if (!$this->isDownloaded($appId)) {
517
-				$this->downloadApp($appId);
518
-			}
519
-			$this->installApp($appId);
520
-			$app = new OC_App();
521
-			$app->enable($appId);
522
-		}
523
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
524
-		$bundles[] = $bundle->getIdentifier();
525
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
526
-	}
527
-
528
-	/**
529
-	 * Installs shipped apps
530
-	 *
531
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
532
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
533
-	 *                         working ownCloud at the end instead of an aborted update.
534
-	 * @return array Array of error messages (appid => Exception)
535
-	 */
536
-	public static function installShippedApps($softErrors = false) {
537
-		$appManager = \OC::$server->getAppManager();
538
-		$config = \OC::$server->getConfig();
539
-		$errors = [];
540
-		foreach (\OC::$APPSROOTS as $app_dir) {
541
-			if ($dir = opendir($app_dir['path'])) {
542
-				while (false !== ($filename = readdir($dir))) {
543
-					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
544
-						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
545
-							if ($config->getAppValue($filename, "installed_version", null) === null) {
546
-								$info = OC_App::getAppInfo($filename);
547
-								$enabled = isset($info['default_enable']);
548
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
549
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
550
-									if ($softErrors) {
551
-										try {
552
-											Installer::installShippedApp($filename);
553
-										} catch (HintException $e) {
554
-											if ($e->getPrevious() instanceof TableExistsException) {
555
-												$errors[$filename] = $e;
556
-												continue;
557
-											}
558
-											throw $e;
559
-										}
560
-									} else {
561
-										Installer::installShippedApp($filename);
562
-									}
563
-									$config->setAppValue($filename, 'enabled', 'yes');
564
-								}
565
-							}
566
-						}
567
-					}
568
-				}
569
-				closedir($dir);
570
-			}
571
-		}
572
-
573
-		return $errors;
574
-	}
575
-
576
-	/**
577
-	 * install an app already placed in the app folder
578
-	 * @param string $app id of the app to install
579
-	 * @return integer
580
-	 */
581
-	public static function installShippedApp($app) {
582
-		//install the database
583
-		$appPath = OC_App::getAppPath($app);
584
-		\OC_App::registerAutoloading($app, $appPath);
585
-
586
-		if (is_file("$appPath/appinfo/database.xml")) {
587
-			try {
588
-				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
589
-			} catch (TableExistsException $e) {
590
-				throw new HintException(
591
-					'Failed to enable app ' . $app,
592
-					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
593
-					0, $e
594
-				);
595
-			}
596
-		} else {
597
-			$ms = new \OC\DB\MigrationService($app, \OC::$server->get(Connection::class));
598
-			$ms->migrate('latest', true);
599
-		}
600
-
601
-		//run appinfo/install.php
602
-		self::includeAppScript("$appPath/appinfo/install.php");
603
-
604
-		$info = OC_App::getAppInfo($app);
605
-		if (is_null($info)) {
606
-			return false;
607
-		}
608
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
609
-
610
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
611
-
612
-		$config = \OC::$server->getConfig();
613
-
614
-		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
615
-		if (array_key_exists('ocsid', $info)) {
616
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
617
-		}
618
-
619
-		//set remote/public handlers
620
-		foreach ($info['remote'] as $name => $path) {
621
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
622
-		}
623
-		foreach ($info['public'] as $name => $path) {
624
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
625
-		}
626
-
627
-		OC_App::setAppTypes($info['id']);
628
-
629
-		return $info['id'];
630
-	}
631
-
632
-	/**
633
-	 * @param string $script
634
-	 */
635
-	private static function includeAppScript($script) {
636
-		if (file_exists($script)) {
637
-			include $script;
638
-		}
639
-	}
62
+    /** @var AppFetcher */
63
+    private $appFetcher;
64
+    /** @var IClientService */
65
+    private $clientService;
66
+    /** @var ITempManager */
67
+    private $tempManager;
68
+    /** @var ILogger */
69
+    private $logger;
70
+    /** @var IConfig */
71
+    private $config;
72
+    /** @var array - for caching the result of app fetcher */
73
+    private $apps = null;
74
+    /** @var bool|null - for caching the result of the ready status */
75
+    private $isInstanceReadyForUpdates = null;
76
+    /** @var bool */
77
+    private $isCLI;
78
+
79
+    /**
80
+     * @param AppFetcher $appFetcher
81
+     * @param IClientService $clientService
82
+     * @param ITempManager $tempManager
83
+     * @param ILogger $logger
84
+     * @param IConfig $config
85
+     */
86
+    public function __construct(
87
+        AppFetcher $appFetcher,
88
+        IClientService $clientService,
89
+        ITempManager $tempManager,
90
+        ILogger $logger,
91
+        IConfig $config,
92
+        bool $isCLI
93
+    ) {
94
+        $this->appFetcher = $appFetcher;
95
+        $this->clientService = $clientService;
96
+        $this->tempManager = $tempManager;
97
+        $this->logger = $logger;
98
+        $this->config = $config;
99
+        $this->isCLI = $isCLI;
100
+    }
101
+
102
+    /**
103
+     * Installs an app that is located in one of the app folders already
104
+     *
105
+     * @param string $appId App to install
106
+     * @param bool $forceEnable
107
+     * @throws \Exception
108
+     * @return string app ID
109
+     */
110
+    public function installApp(string $appId, bool $forceEnable = false): string {
111
+        $app = \OC_App::findAppInDirectories($appId);
112
+        if ($app === false) {
113
+            throw new \Exception('App not found in any app directory');
114
+        }
115
+
116
+        $basedir = $app['path'].'/'.$appId;
117
+        $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
118
+
119
+        $l = \OC::$server->getL10N('core');
120
+
121
+        if (!is_array($info)) {
122
+            throw new \Exception(
123
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
124
+                    [$appId]
125
+                )
126
+            );
127
+        }
128
+
129
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
130
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
131
+
132
+        $version = implode('.', \OCP\Util::getVersion());
133
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
134
+            throw new \Exception(
135
+                // TODO $l
136
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
137
+                    [$info['name']]
138
+                )
139
+            );
140
+        }
141
+
142
+        // check for required dependencies
143
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
144
+        /** @var Coordinator $coordinator */
145
+        $coordinator = \OC::$server->get(Coordinator::class);
146
+        $coordinator->runLazyRegistration($appId);
147
+        \OC_App::registerAutoloading($appId, $basedir);
148
+
149
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
150
+        if ($previousVersion) {
151
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
152
+        }
153
+
154
+        //install the database
155
+        if (is_file($basedir.'/appinfo/database.xml')) {
156
+            if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
157
+                OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
158
+            } else {
159
+                OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
160
+            }
161
+        } else {
162
+            $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->get(Connection::class));
163
+            $ms->migrate('latest', true);
164
+        }
165
+        if ($previousVersion) {
166
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
167
+        }
168
+
169
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
170
+
171
+        //run appinfo/install.php
172
+        self::includeAppScript($basedir . '/appinfo/install.php');
173
+
174
+        $appData = OC_App::getAppInfo($appId);
175
+        OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
176
+
177
+        //set the installed version
178
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
179
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
180
+
181
+        //set remote/public handlers
182
+        foreach ($info['remote'] as $name => $path) {
183
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
184
+        }
185
+        foreach ($info['public'] as $name => $path) {
186
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
187
+        }
188
+
189
+        OC_App::setAppTypes($info['id']);
190
+
191
+        return $info['id'];
192
+    }
193
+
194
+    /**
195
+     * Updates the specified app from the appstore
196
+     *
197
+     * @param string $appId
198
+     * @param bool [$allowUnstable] Allow unstable releases
199
+     * @return bool
200
+     */
201
+    public function updateAppstoreApp($appId, $allowUnstable = false) {
202
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
203
+            try {
204
+                $this->downloadApp($appId, $allowUnstable);
205
+            } catch (\Exception $e) {
206
+                $this->logger->logException($e, [
207
+                    'level' => ILogger::ERROR,
208
+                    'app' => 'core',
209
+                ]);
210
+                return false;
211
+            }
212
+            return OC_App::updateApp($appId);
213
+        }
214
+
215
+        return false;
216
+    }
217
+
218
+    /**
219
+     * Downloads an app and puts it into the app directory
220
+     *
221
+     * @param string $appId
222
+     * @param bool [$allowUnstable]
223
+     *
224
+     * @throws \Exception If the installation was not successful
225
+     */
226
+    public function downloadApp($appId, $allowUnstable = false) {
227
+        $appId = strtolower($appId);
228
+
229
+        $apps = $this->appFetcher->get($allowUnstable);
230
+        foreach ($apps as $app) {
231
+            if ($app['id'] === $appId) {
232
+                // Load the certificate
233
+                $certificate = new X509();
234
+                $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
235
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
236
+
237
+                // Verify if the certificate has been revoked
238
+                $crl = new X509();
239
+                $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
240
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
241
+                if ($crl->validateSignature() !== true) {
242
+                    throw new \Exception('Could not validate CRL signature');
243
+                }
244
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
245
+                $revoked = $crl->getRevoked($csn);
246
+                if ($revoked !== false) {
247
+                    throw new \Exception(
248
+                        sprintf(
249
+                            'Certificate "%s" has been revoked',
250
+                            $csn
251
+                        )
252
+                    );
253
+                }
254
+
255
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
256
+                if ($certificate->validateSignature() !== true) {
257
+                    throw new \Exception(
258
+                        sprintf(
259
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
260
+                            $appId
261
+                        )
262
+                    );
263
+                }
264
+
265
+                // Verify if the certificate is issued for the requested app id
266
+                $certInfo = openssl_x509_parse($app['certificate']);
267
+                if (!isset($certInfo['subject']['CN'])) {
268
+                    throw new \Exception(
269
+                        sprintf(
270
+                            'App with id %s has a cert with no CN',
271
+                            $appId
272
+                        )
273
+                    );
274
+                }
275
+                if ($certInfo['subject']['CN'] !== $appId) {
276
+                    throw new \Exception(
277
+                        sprintf(
278
+                            'App with id %s has a cert issued to %s',
279
+                            $appId,
280
+                            $certInfo['subject']['CN']
281
+                        )
282
+                    );
283
+                }
284
+
285
+                // Download the release
286
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
287
+                $timeout = $this->isCLI ? 0 : 120;
288
+                $client = $this->clientService->newClient();
289
+                $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
290
+
291
+                // Check if the signature actually matches the downloaded content
292
+                $certificate = openssl_get_publickey($app['certificate']);
293
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
294
+                openssl_free_key($certificate);
295
+
296
+                if ($verified === true) {
297
+                    // Seems to match, let's proceed
298
+                    $extractDir = $this->tempManager->getTemporaryFolder();
299
+                    $archive = new TAR($tempFile);
300
+
301
+                    if ($archive) {
302
+                        if (!$archive->extract($extractDir)) {
303
+                            $errorMessage = 'Could not extract app ' . $appId;
304
+
305
+                            $archiveError = $archive->getError();
306
+                            if ($archiveError instanceof \PEAR_Error) {
307
+                                $errorMessage .= ': ' . $archiveError->getMessage();
308
+                            }
309
+
310
+                            throw new \Exception($errorMessage);
311
+                        }
312
+                        $allFiles = scandir($extractDir);
313
+                        $folders = array_diff($allFiles, ['.', '..']);
314
+                        $folders = array_values($folders);
315
+
316
+                        if (count($folders) > 1) {
317
+                            throw new \Exception(
318
+                                sprintf(
319
+                                    'Extracted app %s has more than 1 folder',
320
+                                    $appId
321
+                                )
322
+                            );
323
+                        }
324
+
325
+                        // Check if appinfo/info.xml has the same app ID as well
326
+                        $loadEntities = libxml_disable_entity_loader(false);
327
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
328
+                        libxml_disable_entity_loader($loadEntities);
329
+                        if ((string)$xml->id !== $appId) {
330
+                            throw new \Exception(
331
+                                sprintf(
332
+                                    'App for id %s has a wrong app ID in info.xml: %s',
333
+                                    $appId,
334
+                                    (string)$xml->id
335
+                                )
336
+                            );
337
+                        }
338
+
339
+                        // Check if the version is lower than before
340
+                        $currentVersion = OC_App::getAppVersion($appId);
341
+                        $newVersion = (string)$xml->version;
342
+                        if (version_compare($currentVersion, $newVersion) === 1) {
343
+                            throw new \Exception(
344
+                                sprintf(
345
+                                    'App for id %s has version %s and tried to update to lower version %s',
346
+                                    $appId,
347
+                                    $currentVersion,
348
+                                    $newVersion
349
+                                )
350
+                            );
351
+                        }
352
+
353
+                        $baseDir = OC_App::getInstallPath() . '/' . $appId;
354
+                        // Remove old app with the ID if existent
355
+                        OC_Helper::rmdirr($baseDir);
356
+                        // Move to app folder
357
+                        if (@mkdir($baseDir)) {
358
+                            $extractDir .= '/' . $folders[0];
359
+                            OC_Helper::copyr($extractDir, $baseDir);
360
+                        }
361
+                        OC_Helper::copyr($extractDir, $baseDir);
362
+                        OC_Helper::rmdirr($extractDir);
363
+                        return;
364
+                    } else {
365
+                        throw new \Exception(
366
+                            sprintf(
367
+                                'Could not extract app with ID %s to %s',
368
+                                $appId,
369
+                                $extractDir
370
+                            )
371
+                        );
372
+                    }
373
+                } else {
374
+                    // Signature does not match
375
+                    throw new \Exception(
376
+                        sprintf(
377
+                            'App with id %s has invalid signature',
378
+                            $appId
379
+                        )
380
+                    );
381
+                }
382
+            }
383
+        }
384
+
385
+        throw new \Exception(
386
+            sprintf(
387
+                'Could not download app %s',
388
+                $appId
389
+            )
390
+        );
391
+    }
392
+
393
+    /**
394
+     * Check if an update for the app is available
395
+     *
396
+     * @param string $appId
397
+     * @param bool $allowUnstable
398
+     * @return string|false false or the version number of the update
399
+     */
400
+    public function isUpdateAvailable($appId, $allowUnstable = false) {
401
+        if ($this->isInstanceReadyForUpdates === null) {
402
+            $installPath = OC_App::getInstallPath();
403
+            if ($installPath === false || $installPath === null) {
404
+                $this->isInstanceReadyForUpdates = false;
405
+            } else {
406
+                $this->isInstanceReadyForUpdates = true;
407
+            }
408
+        }
409
+
410
+        if ($this->isInstanceReadyForUpdates === false) {
411
+            return false;
412
+        }
413
+
414
+        if ($this->isInstalledFromGit($appId) === true) {
415
+            return false;
416
+        }
417
+
418
+        if ($this->apps === null) {
419
+            $this->apps = $this->appFetcher->get($allowUnstable);
420
+        }
421
+
422
+        foreach ($this->apps as $app) {
423
+            if ($app['id'] === $appId) {
424
+                $currentVersion = OC_App::getAppVersion($appId);
425
+
426
+                if (!isset($app['releases'][0]['version'])) {
427
+                    return false;
428
+                }
429
+                $newestVersion = $app['releases'][0]['version'];
430
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
431
+                    return $newestVersion;
432
+                } else {
433
+                    return false;
434
+                }
435
+            }
436
+        }
437
+
438
+        return false;
439
+    }
440
+
441
+    /**
442
+     * Check if app has been installed from git
443
+     * @param string $name name of the application to remove
444
+     * @return boolean
445
+     *
446
+     * The function will check if the path contains a .git folder
447
+     */
448
+    private function isInstalledFromGit($appId) {
449
+        $app = \OC_App::findAppInDirectories($appId);
450
+        if ($app === false) {
451
+            return false;
452
+        }
453
+        $basedir = $app['path'].'/'.$appId;
454
+        return file_exists($basedir.'/.git/');
455
+    }
456
+
457
+    /**
458
+     * Check if app is already downloaded
459
+     * @param string $name name of the application to remove
460
+     * @return boolean
461
+     *
462
+     * The function will check if the app is already downloaded in the apps repository
463
+     */
464
+    public function isDownloaded($name) {
465
+        foreach (\OC::$APPSROOTS as $dir) {
466
+            $dirToTest = $dir['path'];
467
+            $dirToTest .= '/';
468
+            $dirToTest .= $name;
469
+            $dirToTest .= '/';
470
+
471
+            if (is_dir($dirToTest)) {
472
+                return true;
473
+            }
474
+        }
475
+
476
+        return false;
477
+    }
478
+
479
+    /**
480
+     * Removes an app
481
+     * @param string $appId ID of the application to remove
482
+     * @return boolean
483
+     *
484
+     *
485
+     * This function works as follows
486
+     *   -# call uninstall repair steps
487
+     *   -# removing the files
488
+     *
489
+     * The function will not delete preferences, tables and the configuration,
490
+     * this has to be done by the function oc_app_uninstall().
491
+     */
492
+    public function removeApp($appId) {
493
+        if ($this->isDownloaded($appId)) {
494
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
495
+                return false;
496
+            }
497
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
498
+            OC_Helper::rmdirr($appDir);
499
+            return true;
500
+        } else {
501
+            \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
502
+
503
+            return false;
504
+        }
505
+    }
506
+
507
+    /**
508
+     * Installs the app within the bundle and marks the bundle as installed
509
+     *
510
+     * @param Bundle $bundle
511
+     * @throws \Exception If app could not get installed
512
+     */
513
+    public function installAppBundle(Bundle $bundle) {
514
+        $appIds = $bundle->getAppIdentifiers();
515
+        foreach ($appIds as $appId) {
516
+            if (!$this->isDownloaded($appId)) {
517
+                $this->downloadApp($appId);
518
+            }
519
+            $this->installApp($appId);
520
+            $app = new OC_App();
521
+            $app->enable($appId);
522
+        }
523
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
524
+        $bundles[] = $bundle->getIdentifier();
525
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
526
+    }
527
+
528
+    /**
529
+     * Installs shipped apps
530
+     *
531
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
532
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
533
+     *                         working ownCloud at the end instead of an aborted update.
534
+     * @return array Array of error messages (appid => Exception)
535
+     */
536
+    public static function installShippedApps($softErrors = false) {
537
+        $appManager = \OC::$server->getAppManager();
538
+        $config = \OC::$server->getConfig();
539
+        $errors = [];
540
+        foreach (\OC::$APPSROOTS as $app_dir) {
541
+            if ($dir = opendir($app_dir['path'])) {
542
+                while (false !== ($filename = readdir($dir))) {
543
+                    if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
544
+                        if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
545
+                            if ($config->getAppValue($filename, "installed_version", null) === null) {
546
+                                $info = OC_App::getAppInfo($filename);
547
+                                $enabled = isset($info['default_enable']);
548
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
549
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
550
+                                    if ($softErrors) {
551
+                                        try {
552
+                                            Installer::installShippedApp($filename);
553
+                                        } catch (HintException $e) {
554
+                                            if ($e->getPrevious() instanceof TableExistsException) {
555
+                                                $errors[$filename] = $e;
556
+                                                continue;
557
+                                            }
558
+                                            throw $e;
559
+                                        }
560
+                                    } else {
561
+                                        Installer::installShippedApp($filename);
562
+                                    }
563
+                                    $config->setAppValue($filename, 'enabled', 'yes');
564
+                                }
565
+                            }
566
+                        }
567
+                    }
568
+                }
569
+                closedir($dir);
570
+            }
571
+        }
572
+
573
+        return $errors;
574
+    }
575
+
576
+    /**
577
+     * install an app already placed in the app folder
578
+     * @param string $app id of the app to install
579
+     * @return integer
580
+     */
581
+    public static function installShippedApp($app) {
582
+        //install the database
583
+        $appPath = OC_App::getAppPath($app);
584
+        \OC_App::registerAutoloading($app, $appPath);
585
+
586
+        if (is_file("$appPath/appinfo/database.xml")) {
587
+            try {
588
+                OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
589
+            } catch (TableExistsException $e) {
590
+                throw new HintException(
591
+                    'Failed to enable app ' . $app,
592
+                    'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
593
+                    0, $e
594
+                );
595
+            }
596
+        } else {
597
+            $ms = new \OC\DB\MigrationService($app, \OC::$server->get(Connection::class));
598
+            $ms->migrate('latest', true);
599
+        }
600
+
601
+        //run appinfo/install.php
602
+        self::includeAppScript("$appPath/appinfo/install.php");
603
+
604
+        $info = OC_App::getAppInfo($app);
605
+        if (is_null($info)) {
606
+            return false;
607
+        }
608
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
609
+
610
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
611
+
612
+        $config = \OC::$server->getConfig();
613
+
614
+        $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
615
+        if (array_key_exists('ocsid', $info)) {
616
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
617
+        }
618
+
619
+        //set remote/public handlers
620
+        foreach ($info['remote'] as $name => $path) {
621
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
622
+        }
623
+        foreach ($info['public'] as $name => $path) {
624
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
625
+        }
626
+
627
+        OC_App::setAppTypes($info['id']);
628
+
629
+        return $info['id'];
630
+    }
631
+
632
+    /**
633
+     * @param string $script
634
+     */
635
+    private static function includeAppScript($script) {
636
+        if (file_exists($script)) {
637
+            include $script;
638
+        }
639
+    }
640 640
 }
Please login to merge, or discard this patch.