Passed
Push — master ( a74705...2ba34c )
by Morris
13:54 queued 10s
created
lib/private/Archive/TAR.php 1 patch
Indentation   +335 added lines, -335 removed lines patch added patch discarded remove patch
@@ -35,360 +35,360 @@
 block discarded – undo
35 35
 use Icewind\Streams\CallbackWrapper;
36 36
 
37 37
 class TAR extends Archive {
38
-	public const PLAIN = 0;
39
-	public const GZIP = 1;
40
-	public const BZIP = 2;
38
+    public const PLAIN = 0;
39
+    public const GZIP = 1;
40
+    public const BZIP = 2;
41 41
 
42
-	private $fileList;
43
-	private $cachedHeaders;
42
+    private $fileList;
43
+    private $cachedHeaders;
44 44
 
45
-	/**
46
-	 * @var \Archive_Tar tar
47
-	 */
48
-	private $tar = null;
49
-	private $path;
45
+    /**
46
+     * @var \Archive_Tar tar
47
+     */
48
+    private $tar = null;
49
+    private $path;
50 50
 
51
-	/**
52
-	 * @param string $source
53
-	 */
54
-	public function __construct($source) {
55
-		$types = [null, 'gz', 'bz2'];
56
-		$this->path = $source;
57
-		$this->tar = new \Archive_Tar($source, $types[self::getTarType($source)]);
58
-	}
51
+    /**
52
+     * @param string $source
53
+     */
54
+    public function __construct($source) {
55
+        $types = [null, 'gz', 'bz2'];
56
+        $this->path = $source;
57
+        $this->tar = new \Archive_Tar($source, $types[self::getTarType($source)]);
58
+    }
59 59
 
60
-	/**
61
-	 * try to detect the type of tar compression
62
-	 *
63
-	 * @param string $file
64
-	 * @return integer
65
-	 */
66
-	public static function getTarType($file) {
67
-		if (strpos($file, '.')) {
68
-			$extension = substr($file, strrpos($file, '.'));
69
-			switch ($extension) {
70
-				case '.gz':
71
-				case '.tgz':
72
-					return self::GZIP;
73
-				case '.bz':
74
-				case '.bz2':
75
-					return self::BZIP;
76
-				case '.tar':
77
-					return self::PLAIN;
78
-				default:
79
-					return self::PLAIN;
80
-			}
81
-		} else {
82
-			return self::PLAIN;
83
-		}
84
-	}
60
+    /**
61
+     * try to detect the type of tar compression
62
+     *
63
+     * @param string $file
64
+     * @return integer
65
+     */
66
+    public static function getTarType($file) {
67
+        if (strpos($file, '.')) {
68
+            $extension = substr($file, strrpos($file, '.'));
69
+            switch ($extension) {
70
+                case '.gz':
71
+                case '.tgz':
72
+                    return self::GZIP;
73
+                case '.bz':
74
+                case '.bz2':
75
+                    return self::BZIP;
76
+                case '.tar':
77
+                    return self::PLAIN;
78
+                default:
79
+                    return self::PLAIN;
80
+            }
81
+        } else {
82
+            return self::PLAIN;
83
+        }
84
+    }
85 85
 
86
-	/**
87
-	 * add an empty folder to the archive
88
-	 *
89
-	 * @param string $path
90
-	 * @return bool
91
-	 */
92
-	public function addFolder($path) {
93
-		$tmpBase = \OC::$server->getTempManager()->getTemporaryFolder();
94
-		$path = rtrim($path, '/') . '/';
95
-		if ($this->fileExists($path)) {
96
-			return false;
97
-		}
98
-		$parts = explode('/', $path);
99
-		$folder = $tmpBase;
100
-		foreach ($parts as $part) {
101
-			$folder .= '/' . $part;
102
-			if (!is_dir($folder)) {
103
-				mkdir($folder);
104
-			}
105
-		}
106
-		$result = $this->tar->addModify([$tmpBase . $path], '', $tmpBase);
107
-		rmdir($tmpBase . $path);
108
-		$this->fileList = false;
109
-		$this->cachedHeaders = false;
110
-		return $result;
111
-	}
86
+    /**
87
+     * add an empty folder to the archive
88
+     *
89
+     * @param string $path
90
+     * @return bool
91
+     */
92
+    public function addFolder($path) {
93
+        $tmpBase = \OC::$server->getTempManager()->getTemporaryFolder();
94
+        $path = rtrim($path, '/') . '/';
95
+        if ($this->fileExists($path)) {
96
+            return false;
97
+        }
98
+        $parts = explode('/', $path);
99
+        $folder = $tmpBase;
100
+        foreach ($parts as $part) {
101
+            $folder .= '/' . $part;
102
+            if (!is_dir($folder)) {
103
+                mkdir($folder);
104
+            }
105
+        }
106
+        $result = $this->tar->addModify([$tmpBase . $path], '', $tmpBase);
107
+        rmdir($tmpBase . $path);
108
+        $this->fileList = false;
109
+        $this->cachedHeaders = false;
110
+        return $result;
111
+    }
112 112
 
113
-	/**
114
-	 * add a file to the archive
115
-	 *
116
-	 * @param string $path
117
-	 * @param string $source either a local file or string data
118
-	 * @return bool
119
-	 */
120
-	public function addFile($path, $source = '') {
121
-		if ($this->fileExists($path)) {
122
-			$this->remove($path);
123
-		}
124
-		if ($source and $source[0] == '/' and file_exists($source)) {
125
-			$source = file_get_contents($source);
126
-		}
127
-		$result = $this->tar->addString($path, $source);
128
-		$this->fileList = false;
129
-		$this->cachedHeaders = false;
130
-		return $result;
131
-	}
113
+    /**
114
+     * add a file to the archive
115
+     *
116
+     * @param string $path
117
+     * @param string $source either a local file or string data
118
+     * @return bool
119
+     */
120
+    public function addFile($path, $source = '') {
121
+        if ($this->fileExists($path)) {
122
+            $this->remove($path);
123
+        }
124
+        if ($source and $source[0] == '/' and file_exists($source)) {
125
+            $source = file_get_contents($source);
126
+        }
127
+        $result = $this->tar->addString($path, $source);
128
+        $this->fileList = false;
129
+        $this->cachedHeaders = false;
130
+        return $result;
131
+    }
132 132
 
133
-	/**
134
-	 * rename a file or folder in the archive
135
-	 *
136
-	 * @param string $source
137
-	 * @param string $dest
138
-	 * @return bool
139
-	 */
140
-	public function rename($source, $dest) {
141
-		//no proper way to delete, rename entire archive, rename file and remake archive
142
-		$tmp = \OC::$server->getTempManager()->getTemporaryFolder();
143
-		$this->tar->extract($tmp);
144
-		rename($tmp . $source, $tmp . $dest);
145
-		$this->tar = null;
146
-		unlink($this->path);
147
-		$types = [null, 'gz', 'bz'];
148
-		$this->tar = new \Archive_Tar($this->path, $types[self::getTarType($this->path)]);
149
-		$this->tar->createModify([$tmp], '', $tmp . '/');
150
-		$this->fileList = false;
151
-		$this->cachedHeaders = false;
152
-		return true;
153
-	}
133
+    /**
134
+     * rename a file or folder in the archive
135
+     *
136
+     * @param string $source
137
+     * @param string $dest
138
+     * @return bool
139
+     */
140
+    public function rename($source, $dest) {
141
+        //no proper way to delete, rename entire archive, rename file and remake archive
142
+        $tmp = \OC::$server->getTempManager()->getTemporaryFolder();
143
+        $this->tar->extract($tmp);
144
+        rename($tmp . $source, $tmp . $dest);
145
+        $this->tar = null;
146
+        unlink($this->path);
147
+        $types = [null, 'gz', 'bz'];
148
+        $this->tar = new \Archive_Tar($this->path, $types[self::getTarType($this->path)]);
149
+        $this->tar->createModify([$tmp], '', $tmp . '/');
150
+        $this->fileList = false;
151
+        $this->cachedHeaders = false;
152
+        return true;
153
+    }
154 154
 
155
-	/**
156
-	 * @param string $file
157
-	 */
158
-	private function getHeader($file) {
159
-		if (!$this->cachedHeaders) {
160
-			$this->cachedHeaders = $this->tar->listContent();
161
-		}
162
-		foreach ($this->cachedHeaders as $header) {
163
-			if ($file == $header['filename']
164
-				or $file . '/' == $header['filename']
165
-				or '/' . $file . '/' == $header['filename']
166
-				or '/' . $file == $header['filename']
167
-			) {
168
-				return $header;
169
-			}
170
-		}
171
-		return null;
172
-	}
155
+    /**
156
+     * @param string $file
157
+     */
158
+    private function getHeader($file) {
159
+        if (!$this->cachedHeaders) {
160
+            $this->cachedHeaders = $this->tar->listContent();
161
+        }
162
+        foreach ($this->cachedHeaders as $header) {
163
+            if ($file == $header['filename']
164
+                or $file . '/' == $header['filename']
165
+                or '/' . $file . '/' == $header['filename']
166
+                or '/' . $file == $header['filename']
167
+            ) {
168
+                return $header;
169
+            }
170
+        }
171
+        return null;
172
+    }
173 173
 
174
-	/**
175
-	 * get the uncompressed size of a file in the archive
176
-	 *
177
-	 * @param string $path
178
-	 * @return int
179
-	 */
180
-	public function filesize($path) {
181
-		$stat = $this->getHeader($path);
182
-		return $stat['size'];
183
-	}
174
+    /**
175
+     * get the uncompressed size of a file in the archive
176
+     *
177
+     * @param string $path
178
+     * @return int
179
+     */
180
+    public function filesize($path) {
181
+        $stat = $this->getHeader($path);
182
+        return $stat['size'];
183
+    }
184 184
 
185
-	/**
186
-	 * get the last modified time of a file in the archive
187
-	 *
188
-	 * @param string $path
189
-	 * @return int
190
-	 */
191
-	public function mtime($path) {
192
-		$stat = $this->getHeader($path);
193
-		return $stat['mtime'];
194
-	}
185
+    /**
186
+     * get the last modified time of a file in the archive
187
+     *
188
+     * @param string $path
189
+     * @return int
190
+     */
191
+    public function mtime($path) {
192
+        $stat = $this->getHeader($path);
193
+        return $stat['mtime'];
194
+    }
195 195
 
196
-	/**
197
-	 * get the files in a folder
198
-	 *
199
-	 * @param string $path
200
-	 * @return array
201
-	 */
202
-	public function getFolder($path) {
203
-		$files = $this->getFiles();
204
-		$folderContent = [];
205
-		$pathLength = strlen($path);
206
-		foreach ($files as $file) {
207
-			if ($file[0] == '/') {
208
-				$file = substr($file, 1);
209
-			}
210
-			if (substr($file, 0, $pathLength) == $path and $file != $path) {
211
-				$result = substr($file, $pathLength);
212
-				if ($pos = strpos($result, '/')) {
213
-					$result = substr($result, 0, $pos + 1);
214
-				}
215
-				if (array_search($result, $folderContent) === false) {
216
-					$folderContent[] = $result;
217
-				}
218
-			}
219
-		}
220
-		return $folderContent;
221
-	}
196
+    /**
197
+     * get the files in a folder
198
+     *
199
+     * @param string $path
200
+     * @return array
201
+     */
202
+    public function getFolder($path) {
203
+        $files = $this->getFiles();
204
+        $folderContent = [];
205
+        $pathLength = strlen($path);
206
+        foreach ($files as $file) {
207
+            if ($file[0] == '/') {
208
+                $file = substr($file, 1);
209
+            }
210
+            if (substr($file, 0, $pathLength) == $path and $file != $path) {
211
+                $result = substr($file, $pathLength);
212
+                if ($pos = strpos($result, '/')) {
213
+                    $result = substr($result, 0, $pos + 1);
214
+                }
215
+                if (array_search($result, $folderContent) === false) {
216
+                    $folderContent[] = $result;
217
+                }
218
+            }
219
+        }
220
+        return $folderContent;
221
+    }
222 222
 
223
-	/**
224
-	 * get all files in the archive
225
-	 *
226
-	 * @return array
227
-	 */
228
-	public function getFiles() {
229
-		if ($this->fileList) {
230
-			return $this->fileList;
231
-		}
232
-		if (!$this->cachedHeaders) {
233
-			$this->cachedHeaders = $this->tar->listContent();
234
-		}
235
-		$files = [];
236
-		foreach ($this->cachedHeaders as $header) {
237
-			$files[] = $header['filename'];
238
-		}
239
-		$this->fileList = $files;
240
-		return $files;
241
-	}
223
+    /**
224
+     * get all files in the archive
225
+     *
226
+     * @return array
227
+     */
228
+    public function getFiles() {
229
+        if ($this->fileList) {
230
+            return $this->fileList;
231
+        }
232
+        if (!$this->cachedHeaders) {
233
+            $this->cachedHeaders = $this->tar->listContent();
234
+        }
235
+        $files = [];
236
+        foreach ($this->cachedHeaders as $header) {
237
+            $files[] = $header['filename'];
238
+        }
239
+        $this->fileList = $files;
240
+        return $files;
241
+    }
242 242
 
243
-	/**
244
-	 * get the content of a file
245
-	 *
246
-	 * @param string $path
247
-	 * @return string
248
-	 */
249
-	public function getFile($path) {
250
-		return $this->tar->extractInString($path);
251
-	}
243
+    /**
244
+     * get the content of a file
245
+     *
246
+     * @param string $path
247
+     * @return string
248
+     */
249
+    public function getFile($path) {
250
+        return $this->tar->extractInString($path);
251
+    }
252 252
 
253
-	/**
254
-	 * extract a single file from the archive
255
-	 *
256
-	 * @param string $path
257
-	 * @param string $dest
258
-	 * @return bool
259
-	 */
260
-	public function extractFile($path, $dest) {
261
-		$tmp = \OC::$server->getTempManager()->getTemporaryFolder();
262
-		if (!$this->fileExists($path)) {
263
-			return false;
264
-		}
265
-		if ($this->fileExists('/' . $path)) {
266
-			$success = $this->tar->extractList(['/' . $path], $tmp);
267
-		} else {
268
-			$success = $this->tar->extractList([$path], $tmp);
269
-		}
270
-		if ($success) {
271
-			rename($tmp . $path, $dest);
272
-		}
273
-		\OCP\Files::rmdirr($tmp);
274
-		return $success;
275
-	}
253
+    /**
254
+     * extract a single file from the archive
255
+     *
256
+     * @param string $path
257
+     * @param string $dest
258
+     * @return bool
259
+     */
260
+    public function extractFile($path, $dest) {
261
+        $tmp = \OC::$server->getTempManager()->getTemporaryFolder();
262
+        if (!$this->fileExists($path)) {
263
+            return false;
264
+        }
265
+        if ($this->fileExists('/' . $path)) {
266
+            $success = $this->tar->extractList(['/' . $path], $tmp);
267
+        } else {
268
+            $success = $this->tar->extractList([$path], $tmp);
269
+        }
270
+        if ($success) {
271
+            rename($tmp . $path, $dest);
272
+        }
273
+        \OCP\Files::rmdirr($tmp);
274
+        return $success;
275
+    }
276 276
 
277
-	/**
278
-	 * extract the archive
279
-	 *
280
-	 * @param string $dest
281
-	 * @return bool
282
-	 */
283
-	public function extract($dest) {
284
-		return $this->tar->extract($dest);
285
-	}
277
+    /**
278
+     * extract the archive
279
+     *
280
+     * @param string $dest
281
+     * @return bool
282
+     */
283
+    public function extract($dest) {
284
+        return $this->tar->extract($dest);
285
+    }
286 286
 
287
-	/**
288
-	 * check if a file or folder exists in the archive
289
-	 *
290
-	 * @param string $path
291
-	 * @return bool
292
-	 */
293
-	public function fileExists($path) {
294
-		$files = $this->getFiles();
295
-		if ((array_search($path, $files) !== false) or (array_search($path . '/', $files) !== false)) {
296
-			return true;
297
-		} else {
298
-			$folderPath = rtrim($path, '/') . '/';
299
-			$pathLength = strlen($folderPath);
300
-			foreach ($files as $file) {
301
-				if (strlen($file) > $pathLength and substr($file, 0, $pathLength) == $folderPath) {
302
-					return true;
303
-				}
304
-			}
305
-		}
306
-		if ($path[0] != '/') { //not all programs agree on the use of a leading /
307
-			return $this->fileExists('/' . $path);
308
-		} else {
309
-			return false;
310
-		}
311
-	}
287
+    /**
288
+     * check if a file or folder exists in the archive
289
+     *
290
+     * @param string $path
291
+     * @return bool
292
+     */
293
+    public function fileExists($path) {
294
+        $files = $this->getFiles();
295
+        if ((array_search($path, $files) !== false) or (array_search($path . '/', $files) !== false)) {
296
+            return true;
297
+        } else {
298
+            $folderPath = rtrim($path, '/') . '/';
299
+            $pathLength = strlen($folderPath);
300
+            foreach ($files as $file) {
301
+                if (strlen($file) > $pathLength and substr($file, 0, $pathLength) == $folderPath) {
302
+                    return true;
303
+                }
304
+            }
305
+        }
306
+        if ($path[0] != '/') { //not all programs agree on the use of a leading /
307
+            return $this->fileExists('/' . $path);
308
+        } else {
309
+            return false;
310
+        }
311
+    }
312 312
 
313
-	/**
314
-	 * remove a file or folder from the archive
315
-	 *
316
-	 * @param string $path
317
-	 * @return bool
318
-	 */
319
-	public function remove($path) {
320
-		if (!$this->fileExists($path)) {
321
-			return false;
322
-		}
323
-		$this->fileList = false;
324
-		$this->cachedHeaders = false;
325
-		//no proper way to delete, extract entire archive, delete file and remake archive
326
-		$tmp = \OC::$server->getTempManager()->getTemporaryFolder();
327
-		$this->tar->extract($tmp);
328
-		\OCP\Files::rmdirr($tmp . $path);
329
-		$this->tar = null;
330
-		unlink($this->path);
331
-		$this->reopen();
332
-		$this->tar->createModify([$tmp], '', $tmp);
333
-		return true;
334
-	}
313
+    /**
314
+     * remove a file or folder from the archive
315
+     *
316
+     * @param string $path
317
+     * @return bool
318
+     */
319
+    public function remove($path) {
320
+        if (!$this->fileExists($path)) {
321
+            return false;
322
+        }
323
+        $this->fileList = false;
324
+        $this->cachedHeaders = false;
325
+        //no proper way to delete, extract entire archive, delete file and remake archive
326
+        $tmp = \OC::$server->getTempManager()->getTemporaryFolder();
327
+        $this->tar->extract($tmp);
328
+        \OCP\Files::rmdirr($tmp . $path);
329
+        $this->tar = null;
330
+        unlink($this->path);
331
+        $this->reopen();
332
+        $this->tar->createModify([$tmp], '', $tmp);
333
+        return true;
334
+    }
335 335
 
336
-	/**
337
-	 * get a file handler
338
-	 *
339
-	 * @param string $path
340
-	 * @param string $mode
341
-	 * @return resource
342
-	 */
343
-	public function getStream($path, $mode) {
344
-		if (strrpos($path, '.') !== false) {
345
-			$ext = substr($path, strrpos($path, '.'));
346
-		} else {
347
-			$ext = '';
348
-		}
349
-		$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
350
-		if ($this->fileExists($path)) {
351
-			$this->extractFile($path, $tmpFile);
352
-		} elseif ($mode == 'r' or $mode == 'rb') {
353
-			return false;
354
-		}
355
-		if ($mode == 'r' or $mode == 'rb') {
356
-			return fopen($tmpFile, $mode);
357
-		} else {
358
-			$handle = fopen($tmpFile, $mode);
359
-			return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
360
-				$this->writeBack($tmpFile, $path);
361
-			});
362
-		}
363
-	}
336
+    /**
337
+     * get a file handler
338
+     *
339
+     * @param string $path
340
+     * @param string $mode
341
+     * @return resource
342
+     */
343
+    public function getStream($path, $mode) {
344
+        if (strrpos($path, '.') !== false) {
345
+            $ext = substr($path, strrpos($path, '.'));
346
+        } else {
347
+            $ext = '';
348
+        }
349
+        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
350
+        if ($this->fileExists($path)) {
351
+            $this->extractFile($path, $tmpFile);
352
+        } elseif ($mode == 'r' or $mode == 'rb') {
353
+            return false;
354
+        }
355
+        if ($mode == 'r' or $mode == 'rb') {
356
+            return fopen($tmpFile, $mode);
357
+        } else {
358
+            $handle = fopen($tmpFile, $mode);
359
+            return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
360
+                $this->writeBack($tmpFile, $path);
361
+            });
362
+        }
363
+    }
364 364
 
365
-	/**
366
-	 * write back temporary files
367
-	 */
368
-	public function writeBack($tmpFile, $path) {
369
-		$this->addFile($path, $tmpFile);
370
-		unlink($tmpFile);
371
-	}
365
+    /**
366
+     * write back temporary files
367
+     */
368
+    public function writeBack($tmpFile, $path) {
369
+        $this->addFile($path, $tmpFile);
370
+        unlink($tmpFile);
371
+    }
372 372
 
373
-	/**
374
-	 * reopen the archive to ensure everything is written
375
-	 */
376
-	private function reopen() {
377
-		if ($this->tar) {
378
-			$this->tar->_close();
379
-			$this->tar = null;
380
-		}
381
-		$types = [null, 'gz', 'bz'];
382
-		$this->tar = new \Archive_Tar($this->path, $types[self::getTarType($this->path)]);
383
-	}
373
+    /**
374
+     * reopen the archive to ensure everything is written
375
+     */
376
+    private function reopen() {
377
+        if ($this->tar) {
378
+            $this->tar->_close();
379
+            $this->tar = null;
380
+        }
381
+        $types = [null, 'gz', 'bz'];
382
+        $this->tar = new \Archive_Tar($this->path, $types[self::getTarType($this->path)]);
383
+    }
384 384
 
385
-	/**
386
-	 * Get error object from archive_tar.
387
-	 */
388
-	public function getError(): ?\PEAR_Error {
389
-		if ($this->tar instanceof \Archive_Tar && $this->tar->error_object instanceof \PEAR_Error) {
390
-			return $this->tar->error_object;
391
-		}
392
-		return null;
393
-	}
385
+    /**
386
+     * Get error object from archive_tar.
387
+     */
388
+    public function getError(): ?\PEAR_Error {
389
+        if ($this->tar instanceof \Archive_Tar && $this->tar->error_object instanceof \PEAR_Error) {
390
+            return $this->tar->error_object;
391
+        }
392
+        return null;
393
+    }
394 394
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 2 patches
Indentation   +575 added lines, -575 removed lines patch added patch discarded remove patch
@@ -56,579 +56,579 @@
 block discarded – undo
56 56
  * This class provides the functionality needed to install, update and remove apps
57 57
  */
58 58
 class Installer {
59
-	/** @var AppFetcher */
60
-	private $appFetcher;
61
-	/** @var IClientService */
62
-	private $clientService;
63
-	/** @var ITempManager */
64
-	private $tempManager;
65
-	/** @var ILogger */
66
-	private $logger;
67
-	/** @var IConfig */
68
-	private $config;
69
-	/** @var array - for caching the result of app fetcher */
70
-	private $apps = null;
71
-	/** @var bool|null - for caching the result of the ready status */
72
-	private $isInstanceReadyForUpdates = null;
73
-	/** @var bool */
74
-	private $isCLI;
75
-
76
-	/**
77
-	 * @param AppFetcher $appFetcher
78
-	 * @param IClientService $clientService
79
-	 * @param ITempManager $tempManager
80
-	 * @param ILogger $logger
81
-	 * @param IConfig $config
82
-	 */
83
-	public function __construct(
84
-		AppFetcher $appFetcher,
85
-		IClientService $clientService,
86
-		ITempManager $tempManager,
87
-		ILogger $logger,
88
-		IConfig $config,
89
-		bool $isCLI
90
-	) {
91
-		$this->appFetcher = $appFetcher;
92
-		$this->clientService = $clientService;
93
-		$this->tempManager = $tempManager;
94
-		$this->logger = $logger;
95
-		$this->config = $config;
96
-		$this->isCLI = $isCLI;
97
-	}
98
-
99
-	/**
100
-	 * Installs an app that is located in one of the app folders already
101
-	 *
102
-	 * @param string $appId App to install
103
-	 * @param bool $forceEnable
104
-	 * @throws \Exception
105
-	 * @return string app ID
106
-	 */
107
-	public function installApp(string $appId, bool $forceEnable = false): string {
108
-		$app = \OC_App::findAppInDirectories($appId);
109
-		if ($app === false) {
110
-			throw new \Exception('App not found in any app directory');
111
-		}
112
-
113
-		$basedir = $app['path'].'/'.$appId;
114
-		$info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
115
-
116
-		$l = \OC::$server->getL10N('core');
117
-
118
-		if (!is_array($info)) {
119
-			throw new \Exception(
120
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
121
-					[$appId]
122
-				)
123
-			);
124
-		}
125
-
126
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
127
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
128
-
129
-		$version = implode('.', \OCP\Util::getVersion());
130
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
131
-			throw new \Exception(
132
-				// TODO $l
133
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
134
-					[$info['name']]
135
-				)
136
-			);
137
-		}
138
-
139
-		// check for required dependencies
140
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
141
-		\OC_App::registerAutoloading($appId, $basedir);
142
-
143
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
144
-		if ($previousVersion) {
145
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
146
-		}
147
-
148
-		//install the database
149
-		if (is_file($basedir.'/appinfo/database.xml')) {
150
-			if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
151
-				OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
152
-			} else {
153
-				OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
154
-			}
155
-		} else {
156
-			$ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
157
-			$ms->migrate();
158
-		}
159
-		if ($previousVersion) {
160
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
161
-		}
162
-
163
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
164
-
165
-		//run appinfo/install.php
166
-		self::includeAppScript($basedir . '/appinfo/install.php');
167
-
168
-		$appData = OC_App::getAppInfo($appId);
169
-		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
170
-
171
-		//set the installed version
172
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
173
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
174
-
175
-		//set remote/public handlers
176
-		foreach ($info['remote'] as $name => $path) {
177
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
178
-		}
179
-		foreach ($info['public'] as $name => $path) {
180
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
181
-		}
182
-
183
-		OC_App::setAppTypes($info['id']);
184
-
185
-		return $info['id'];
186
-	}
187
-
188
-	/**
189
-	 * Updates the specified app from the appstore
190
-	 *
191
-	 * @param string $appId
192
-	 * @param bool [$allowUnstable] Allow unstable releases
193
-	 * @return bool
194
-	 */
195
-	public function updateAppstoreApp($appId, $allowUnstable = false) {
196
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
197
-			try {
198
-				$this->downloadApp($appId, $allowUnstable);
199
-			} catch (\Exception $e) {
200
-				$this->logger->logException($e, [
201
-					'level' => ILogger::ERROR,
202
-					'app' => 'core',
203
-				]);
204
-				return false;
205
-			}
206
-			return OC_App::updateApp($appId);
207
-		}
208
-
209
-		return false;
210
-	}
211
-
212
-	/**
213
-	 * Downloads an app and puts it into the app directory
214
-	 *
215
-	 * @param string $appId
216
-	 * @param bool [$allowUnstable]
217
-	 *
218
-	 * @throws \Exception If the installation was not successful
219
-	 */
220
-	public function downloadApp($appId, $allowUnstable = false) {
221
-		$appId = strtolower($appId);
222
-
223
-		$apps = $this->appFetcher->get($allowUnstable);
224
-		foreach ($apps as $app) {
225
-			if ($app['id'] === $appId) {
226
-				// Load the certificate
227
-				$certificate = new X509();
228
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
229
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
230
-
231
-				// Verify if the certificate has been revoked
232
-				$crl = new X509();
233
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
234
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
235
-				if ($crl->validateSignature() !== true) {
236
-					throw new \Exception('Could not validate CRL signature');
237
-				}
238
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
239
-				$revoked = $crl->getRevoked($csn);
240
-				if ($revoked !== false) {
241
-					throw new \Exception(
242
-						sprintf(
243
-							'Certificate "%s" has been revoked',
244
-							$csn
245
-						)
246
-					);
247
-				}
248
-
249
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
250
-				if ($certificate->validateSignature() !== true) {
251
-					throw new \Exception(
252
-						sprintf(
253
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
254
-							$appId
255
-						)
256
-					);
257
-				}
258
-
259
-				// Verify if the certificate is issued for the requested app id
260
-				$certInfo = openssl_x509_parse($app['certificate']);
261
-				if (!isset($certInfo['subject']['CN'])) {
262
-					throw new \Exception(
263
-						sprintf(
264
-							'App with id %s has a cert with no CN',
265
-							$appId
266
-						)
267
-					);
268
-				}
269
-				if ($certInfo['subject']['CN'] !== $appId) {
270
-					throw new \Exception(
271
-						sprintf(
272
-							'App with id %s has a cert issued to %s',
273
-							$appId,
274
-							$certInfo['subject']['CN']
275
-						)
276
-					);
277
-				}
278
-
279
-				// Download the release
280
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
281
-				$timeout = $this->isCLI ? 0 : 120;
282
-				$client = $this->clientService->newClient();
283
-				$client->get($app['releases'][0]['download'], ['save_to' => $tempFile, 'timeout' => $timeout]);
284
-
285
-				// Check if the signature actually matches the downloaded content
286
-				$certificate = openssl_get_publickey($app['certificate']);
287
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
288
-				openssl_free_key($certificate);
289
-
290
-				if ($verified === true) {
291
-					// Seems to match, let's proceed
292
-					$extractDir = $this->tempManager->getTemporaryFolder();
293
-					$archive = new TAR($tempFile);
294
-
295
-					if ($archive) {
296
-						if (!$archive->extract($extractDir)) {
297
-							$errorMessage = 'Could not extract app ' . $appId;
298
-
299
-							$archiveError = $archive->getError();
300
-							if ($archiveError instanceof \PEAR_Error) {
301
-								$errorMessage .= ': ' . $archiveError->getMessage();
302
-							}
303
-
304
-							throw new \Exception($errorMessage);
305
-						}
306
-						$allFiles = scandir($extractDir);
307
-						$folders = array_diff($allFiles, ['.', '..']);
308
-						$folders = array_values($folders);
309
-
310
-						if (count($folders) > 1) {
311
-							throw new \Exception(
312
-								sprintf(
313
-									'Extracted app %s has more than 1 folder',
314
-									$appId
315
-								)
316
-							);
317
-						}
318
-
319
-						// Check if appinfo/info.xml has the same app ID as well
320
-						$loadEntities = libxml_disable_entity_loader(false);
321
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
322
-						libxml_disable_entity_loader($loadEntities);
323
-						if ((string)$xml->id !== $appId) {
324
-							throw new \Exception(
325
-								sprintf(
326
-									'App for id %s has a wrong app ID in info.xml: %s',
327
-									$appId,
328
-									(string)$xml->id
329
-								)
330
-							);
331
-						}
332
-
333
-						// Check if the version is lower than before
334
-						$currentVersion = OC_App::getAppVersion($appId);
335
-						$newVersion = (string)$xml->version;
336
-						if (version_compare($currentVersion, $newVersion) === 1) {
337
-							throw new \Exception(
338
-								sprintf(
339
-									'App for id %s has version %s and tried to update to lower version %s',
340
-									$appId,
341
-									$currentVersion,
342
-									$newVersion
343
-								)
344
-							);
345
-						}
346
-
347
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
348
-						// Remove old app with the ID if existent
349
-						OC_Helper::rmdirr($baseDir);
350
-						// Move to app folder
351
-						if (@mkdir($baseDir)) {
352
-							$extractDir .= '/' . $folders[0];
353
-							OC_Helper::copyr($extractDir, $baseDir);
354
-						}
355
-						OC_Helper::copyr($extractDir, $baseDir);
356
-						OC_Helper::rmdirr($extractDir);
357
-						return;
358
-					} else {
359
-						throw new \Exception(
360
-							sprintf(
361
-								'Could not extract app with ID %s to %s',
362
-								$appId,
363
-								$extractDir
364
-							)
365
-						);
366
-					}
367
-				} else {
368
-					// Signature does not match
369
-					throw new \Exception(
370
-						sprintf(
371
-							'App with id %s has invalid signature',
372
-							$appId
373
-						)
374
-					);
375
-				}
376
-			}
377
-		}
378
-
379
-		throw new \Exception(
380
-			sprintf(
381
-				'Could not download app %s',
382
-				$appId
383
-			)
384
-		);
385
-	}
386
-
387
-	/**
388
-	 * Check if an update for the app is available
389
-	 *
390
-	 * @param string $appId
391
-	 * @param bool $allowUnstable
392
-	 * @return string|false false or the version number of the update
393
-	 */
394
-	public function isUpdateAvailable($appId, $allowUnstable = false) {
395
-		if ($this->isInstanceReadyForUpdates === null) {
396
-			$installPath = OC_App::getInstallPath();
397
-			if ($installPath === false || $installPath === null) {
398
-				$this->isInstanceReadyForUpdates = false;
399
-			} else {
400
-				$this->isInstanceReadyForUpdates = true;
401
-			}
402
-		}
403
-
404
-		if ($this->isInstanceReadyForUpdates === false) {
405
-			return false;
406
-		}
407
-
408
-		if ($this->isInstalledFromGit($appId) === true) {
409
-			return false;
410
-		}
411
-
412
-		if ($this->apps === null) {
413
-			$this->apps = $this->appFetcher->get($allowUnstable);
414
-		}
415
-
416
-		foreach ($this->apps as $app) {
417
-			if ($app['id'] === $appId) {
418
-				$currentVersion = OC_App::getAppVersion($appId);
419
-
420
-				if (!isset($app['releases'][0]['version'])) {
421
-					return false;
422
-				}
423
-				$newestVersion = $app['releases'][0]['version'];
424
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
425
-					return $newestVersion;
426
-				} else {
427
-					return false;
428
-				}
429
-			}
430
-		}
431
-
432
-		return false;
433
-	}
434
-
435
-	/**
436
-	 * Check if app has been installed from git
437
-	 * @param string $name name of the application to remove
438
-	 * @return boolean
439
-	 *
440
-	 * The function will check if the path contains a .git folder
441
-	 */
442
-	private function isInstalledFromGit($appId) {
443
-		$app = \OC_App::findAppInDirectories($appId);
444
-		if ($app === false) {
445
-			return false;
446
-		}
447
-		$basedir = $app['path'].'/'.$appId;
448
-		return file_exists($basedir.'/.git/');
449
-	}
450
-
451
-	/**
452
-	 * Check if app is already downloaded
453
-	 * @param string $name name of the application to remove
454
-	 * @return boolean
455
-	 *
456
-	 * The function will check if the app is already downloaded in the apps repository
457
-	 */
458
-	public function isDownloaded($name) {
459
-		foreach (\OC::$APPSROOTS as $dir) {
460
-			$dirToTest = $dir['path'];
461
-			$dirToTest .= '/';
462
-			$dirToTest .= $name;
463
-			$dirToTest .= '/';
464
-
465
-			if (is_dir($dirToTest)) {
466
-				return true;
467
-			}
468
-		}
469
-
470
-		return false;
471
-	}
472
-
473
-	/**
474
-	 * Removes an app
475
-	 * @param string $appId ID of the application to remove
476
-	 * @return boolean
477
-	 *
478
-	 *
479
-	 * This function works as follows
480
-	 *   -# call uninstall repair steps
481
-	 *   -# removing the files
482
-	 *
483
-	 * The function will not delete preferences, tables and the configuration,
484
-	 * this has to be done by the function oc_app_uninstall().
485
-	 */
486
-	public function removeApp($appId) {
487
-		if ($this->isDownloaded($appId)) {
488
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
489
-				return false;
490
-			}
491
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
492
-			OC_Helper::rmdirr($appDir);
493
-			return true;
494
-		} else {
495
-			\OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
496
-
497
-			return false;
498
-		}
499
-	}
500
-
501
-	/**
502
-	 * Installs the app within the bundle and marks the bundle as installed
503
-	 *
504
-	 * @param Bundle $bundle
505
-	 * @throws \Exception If app could not get installed
506
-	 */
507
-	public function installAppBundle(Bundle $bundle) {
508
-		$appIds = $bundle->getAppIdentifiers();
509
-		foreach ($appIds as $appId) {
510
-			if (!$this->isDownloaded($appId)) {
511
-				$this->downloadApp($appId);
512
-			}
513
-			$this->installApp($appId);
514
-			$app = new OC_App();
515
-			$app->enable($appId);
516
-		}
517
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
518
-		$bundles[] = $bundle->getIdentifier();
519
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
520
-	}
521
-
522
-	/**
523
-	 * Installs shipped apps
524
-	 *
525
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
526
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
527
-	 *                         working ownCloud at the end instead of an aborted update.
528
-	 * @return array Array of error messages (appid => Exception)
529
-	 */
530
-	public static function installShippedApps($softErrors = false) {
531
-		$appManager = \OC::$server->getAppManager();
532
-		$config = \OC::$server->getConfig();
533
-		$errors = [];
534
-		foreach (\OC::$APPSROOTS as $app_dir) {
535
-			if ($dir = opendir($app_dir['path'])) {
536
-				while (false !== ($filename = readdir($dir))) {
537
-					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
538
-						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
539
-							if ($config->getAppValue($filename, "installed_version", null) === null) {
540
-								$info = OC_App::getAppInfo($filename);
541
-								$enabled = isset($info['default_enable']);
542
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
543
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
544
-									if ($softErrors) {
545
-										try {
546
-											Installer::installShippedApp($filename);
547
-										} catch (HintException $e) {
548
-											if ($e->getPrevious() instanceof TableExistsException) {
549
-												$errors[$filename] = $e;
550
-												continue;
551
-											}
552
-											throw $e;
553
-										}
554
-									} else {
555
-										Installer::installShippedApp($filename);
556
-									}
557
-									$config->setAppValue($filename, 'enabled', 'yes');
558
-								}
559
-							}
560
-						}
561
-					}
562
-				}
563
-				closedir($dir);
564
-			}
565
-		}
566
-
567
-		return $errors;
568
-	}
569
-
570
-	/**
571
-	 * install an app already placed in the app folder
572
-	 * @param string $app id of the app to install
573
-	 * @return integer
574
-	 */
575
-	public static function installShippedApp($app) {
576
-		//install the database
577
-		$appPath = OC_App::getAppPath($app);
578
-		\OC_App::registerAutoloading($app, $appPath);
579
-
580
-		if (is_file("$appPath/appinfo/database.xml")) {
581
-			try {
582
-				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
583
-			} catch (TableExistsException $e) {
584
-				throw new HintException(
585
-					'Failed to enable app ' . $app,
586
-					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
587
-					0, $e
588
-				);
589
-			}
590
-		} else {
591
-			$ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
592
-			$ms->migrate();
593
-		}
594
-
595
-		//run appinfo/install.php
596
-		self::includeAppScript("$appPath/appinfo/install.php");
597
-
598
-		$info = OC_App::getAppInfo($app);
599
-		if (is_null($info)) {
600
-			return false;
601
-		}
602
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
603
-
604
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
605
-
606
-		$config = \OC::$server->getConfig();
607
-
608
-		$config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
609
-		if (array_key_exists('ocsid', $info)) {
610
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
611
-		}
612
-
613
-		//set remote/public handlers
614
-		foreach ($info['remote'] as $name => $path) {
615
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
616
-		}
617
-		foreach ($info['public'] as $name => $path) {
618
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
619
-		}
620
-
621
-		OC_App::setAppTypes($info['id']);
622
-
623
-		return $info['id'];
624
-	}
625
-
626
-	/**
627
-	 * @param string $script
628
-	 */
629
-	private static function includeAppScript($script) {
630
-		if (file_exists($script)) {
631
-			include $script;
632
-		}
633
-	}
59
+    /** @var AppFetcher */
60
+    private $appFetcher;
61
+    /** @var IClientService */
62
+    private $clientService;
63
+    /** @var ITempManager */
64
+    private $tempManager;
65
+    /** @var ILogger */
66
+    private $logger;
67
+    /** @var IConfig */
68
+    private $config;
69
+    /** @var array - for caching the result of app fetcher */
70
+    private $apps = null;
71
+    /** @var bool|null - for caching the result of the ready status */
72
+    private $isInstanceReadyForUpdates = null;
73
+    /** @var bool */
74
+    private $isCLI;
75
+
76
+    /**
77
+     * @param AppFetcher $appFetcher
78
+     * @param IClientService $clientService
79
+     * @param ITempManager $tempManager
80
+     * @param ILogger $logger
81
+     * @param IConfig $config
82
+     */
83
+    public function __construct(
84
+        AppFetcher $appFetcher,
85
+        IClientService $clientService,
86
+        ITempManager $tempManager,
87
+        ILogger $logger,
88
+        IConfig $config,
89
+        bool $isCLI
90
+    ) {
91
+        $this->appFetcher = $appFetcher;
92
+        $this->clientService = $clientService;
93
+        $this->tempManager = $tempManager;
94
+        $this->logger = $logger;
95
+        $this->config = $config;
96
+        $this->isCLI = $isCLI;
97
+    }
98
+
99
+    /**
100
+     * Installs an app that is located in one of the app folders already
101
+     *
102
+     * @param string $appId App to install
103
+     * @param bool $forceEnable
104
+     * @throws \Exception
105
+     * @return string app ID
106
+     */
107
+    public function installApp(string $appId, bool $forceEnable = false): string {
108
+        $app = \OC_App::findAppInDirectories($appId);
109
+        if ($app === false) {
110
+            throw new \Exception('App not found in any app directory');
111
+        }
112
+
113
+        $basedir = $app['path'].'/'.$appId;
114
+        $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
115
+
116
+        $l = \OC::$server->getL10N('core');
117
+
118
+        if (!is_array($info)) {
119
+            throw new \Exception(
120
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
121
+                    [$appId]
122
+                )
123
+            );
124
+        }
125
+
126
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
127
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
128
+
129
+        $version = implode('.', \OCP\Util::getVersion());
130
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
131
+            throw new \Exception(
132
+                // TODO $l
133
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
134
+                    [$info['name']]
135
+                )
136
+            );
137
+        }
138
+
139
+        // check for required dependencies
140
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
141
+        \OC_App::registerAutoloading($appId, $basedir);
142
+
143
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
144
+        if ($previousVersion) {
145
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
146
+        }
147
+
148
+        //install the database
149
+        if (is_file($basedir.'/appinfo/database.xml')) {
150
+            if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
151
+                OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
152
+            } else {
153
+                OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
154
+            }
155
+        } else {
156
+            $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
157
+            $ms->migrate();
158
+        }
159
+        if ($previousVersion) {
160
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
161
+        }
162
+
163
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
164
+
165
+        //run appinfo/install.php
166
+        self::includeAppScript($basedir . '/appinfo/install.php');
167
+
168
+        $appData = OC_App::getAppInfo($appId);
169
+        OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
170
+
171
+        //set the installed version
172
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
173
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
174
+
175
+        //set remote/public handlers
176
+        foreach ($info['remote'] as $name => $path) {
177
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
178
+        }
179
+        foreach ($info['public'] as $name => $path) {
180
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
181
+        }
182
+
183
+        OC_App::setAppTypes($info['id']);
184
+
185
+        return $info['id'];
186
+    }
187
+
188
+    /**
189
+     * Updates the specified app from the appstore
190
+     *
191
+     * @param string $appId
192
+     * @param bool [$allowUnstable] Allow unstable releases
193
+     * @return bool
194
+     */
195
+    public function updateAppstoreApp($appId, $allowUnstable = false) {
196
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
197
+            try {
198
+                $this->downloadApp($appId, $allowUnstable);
199
+            } catch (\Exception $e) {
200
+                $this->logger->logException($e, [
201
+                    'level' => ILogger::ERROR,
202
+                    'app' => 'core',
203
+                ]);
204
+                return false;
205
+            }
206
+            return OC_App::updateApp($appId);
207
+        }
208
+
209
+        return false;
210
+    }
211
+
212
+    /**
213
+     * Downloads an app and puts it into the app directory
214
+     *
215
+     * @param string $appId
216
+     * @param bool [$allowUnstable]
217
+     *
218
+     * @throws \Exception If the installation was not successful
219
+     */
220
+    public function downloadApp($appId, $allowUnstable = false) {
221
+        $appId = strtolower($appId);
222
+
223
+        $apps = $this->appFetcher->get($allowUnstable);
224
+        foreach ($apps as $app) {
225
+            if ($app['id'] === $appId) {
226
+                // Load the certificate
227
+                $certificate = new X509();
228
+                $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
229
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
230
+
231
+                // Verify if the certificate has been revoked
232
+                $crl = new X509();
233
+                $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
234
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
235
+                if ($crl->validateSignature() !== true) {
236
+                    throw new \Exception('Could not validate CRL signature');
237
+                }
238
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
239
+                $revoked = $crl->getRevoked($csn);
240
+                if ($revoked !== false) {
241
+                    throw new \Exception(
242
+                        sprintf(
243
+                            'Certificate "%s" has been revoked',
244
+                            $csn
245
+                        )
246
+                    );
247
+                }
248
+
249
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
250
+                if ($certificate->validateSignature() !== true) {
251
+                    throw new \Exception(
252
+                        sprintf(
253
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
254
+                            $appId
255
+                        )
256
+                    );
257
+                }
258
+
259
+                // Verify if the certificate is issued for the requested app id
260
+                $certInfo = openssl_x509_parse($app['certificate']);
261
+                if (!isset($certInfo['subject']['CN'])) {
262
+                    throw new \Exception(
263
+                        sprintf(
264
+                            'App with id %s has a cert with no CN',
265
+                            $appId
266
+                        )
267
+                    );
268
+                }
269
+                if ($certInfo['subject']['CN'] !== $appId) {
270
+                    throw new \Exception(
271
+                        sprintf(
272
+                            'App with id %s has a cert issued to %s',
273
+                            $appId,
274
+                            $certInfo['subject']['CN']
275
+                        )
276
+                    );
277
+                }
278
+
279
+                // Download the release
280
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
281
+                $timeout = $this->isCLI ? 0 : 120;
282
+                $client = $this->clientService->newClient();
283
+                $client->get($app['releases'][0]['download'], ['save_to' => $tempFile, 'timeout' => $timeout]);
284
+
285
+                // Check if the signature actually matches the downloaded content
286
+                $certificate = openssl_get_publickey($app['certificate']);
287
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
288
+                openssl_free_key($certificate);
289
+
290
+                if ($verified === true) {
291
+                    // Seems to match, let's proceed
292
+                    $extractDir = $this->tempManager->getTemporaryFolder();
293
+                    $archive = new TAR($tempFile);
294
+
295
+                    if ($archive) {
296
+                        if (!$archive->extract($extractDir)) {
297
+                            $errorMessage = 'Could not extract app ' . $appId;
298
+
299
+                            $archiveError = $archive->getError();
300
+                            if ($archiveError instanceof \PEAR_Error) {
301
+                                $errorMessage .= ': ' . $archiveError->getMessage();
302
+                            }
303
+
304
+                            throw new \Exception($errorMessage);
305
+                        }
306
+                        $allFiles = scandir($extractDir);
307
+                        $folders = array_diff($allFiles, ['.', '..']);
308
+                        $folders = array_values($folders);
309
+
310
+                        if (count($folders) > 1) {
311
+                            throw new \Exception(
312
+                                sprintf(
313
+                                    'Extracted app %s has more than 1 folder',
314
+                                    $appId
315
+                                )
316
+                            );
317
+                        }
318
+
319
+                        // Check if appinfo/info.xml has the same app ID as well
320
+                        $loadEntities = libxml_disable_entity_loader(false);
321
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
322
+                        libxml_disable_entity_loader($loadEntities);
323
+                        if ((string)$xml->id !== $appId) {
324
+                            throw new \Exception(
325
+                                sprintf(
326
+                                    'App for id %s has a wrong app ID in info.xml: %s',
327
+                                    $appId,
328
+                                    (string)$xml->id
329
+                                )
330
+                            );
331
+                        }
332
+
333
+                        // Check if the version is lower than before
334
+                        $currentVersion = OC_App::getAppVersion($appId);
335
+                        $newVersion = (string)$xml->version;
336
+                        if (version_compare($currentVersion, $newVersion) === 1) {
337
+                            throw new \Exception(
338
+                                sprintf(
339
+                                    'App for id %s has version %s and tried to update to lower version %s',
340
+                                    $appId,
341
+                                    $currentVersion,
342
+                                    $newVersion
343
+                                )
344
+                            );
345
+                        }
346
+
347
+                        $baseDir = OC_App::getInstallPath() . '/' . $appId;
348
+                        // Remove old app with the ID if existent
349
+                        OC_Helper::rmdirr($baseDir);
350
+                        // Move to app folder
351
+                        if (@mkdir($baseDir)) {
352
+                            $extractDir .= '/' . $folders[0];
353
+                            OC_Helper::copyr($extractDir, $baseDir);
354
+                        }
355
+                        OC_Helper::copyr($extractDir, $baseDir);
356
+                        OC_Helper::rmdirr($extractDir);
357
+                        return;
358
+                    } else {
359
+                        throw new \Exception(
360
+                            sprintf(
361
+                                'Could not extract app with ID %s to %s',
362
+                                $appId,
363
+                                $extractDir
364
+                            )
365
+                        );
366
+                    }
367
+                } else {
368
+                    // Signature does not match
369
+                    throw new \Exception(
370
+                        sprintf(
371
+                            'App with id %s has invalid signature',
372
+                            $appId
373
+                        )
374
+                    );
375
+                }
376
+            }
377
+        }
378
+
379
+        throw new \Exception(
380
+            sprintf(
381
+                'Could not download app %s',
382
+                $appId
383
+            )
384
+        );
385
+    }
386
+
387
+    /**
388
+     * Check if an update for the app is available
389
+     *
390
+     * @param string $appId
391
+     * @param bool $allowUnstable
392
+     * @return string|false false or the version number of the update
393
+     */
394
+    public function isUpdateAvailable($appId, $allowUnstable = false) {
395
+        if ($this->isInstanceReadyForUpdates === null) {
396
+            $installPath = OC_App::getInstallPath();
397
+            if ($installPath === false || $installPath === null) {
398
+                $this->isInstanceReadyForUpdates = false;
399
+            } else {
400
+                $this->isInstanceReadyForUpdates = true;
401
+            }
402
+        }
403
+
404
+        if ($this->isInstanceReadyForUpdates === false) {
405
+            return false;
406
+        }
407
+
408
+        if ($this->isInstalledFromGit($appId) === true) {
409
+            return false;
410
+        }
411
+
412
+        if ($this->apps === null) {
413
+            $this->apps = $this->appFetcher->get($allowUnstable);
414
+        }
415
+
416
+        foreach ($this->apps as $app) {
417
+            if ($app['id'] === $appId) {
418
+                $currentVersion = OC_App::getAppVersion($appId);
419
+
420
+                if (!isset($app['releases'][0]['version'])) {
421
+                    return false;
422
+                }
423
+                $newestVersion = $app['releases'][0]['version'];
424
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
425
+                    return $newestVersion;
426
+                } else {
427
+                    return false;
428
+                }
429
+            }
430
+        }
431
+
432
+        return false;
433
+    }
434
+
435
+    /**
436
+     * Check if app has been installed from git
437
+     * @param string $name name of the application to remove
438
+     * @return boolean
439
+     *
440
+     * The function will check if the path contains a .git folder
441
+     */
442
+    private function isInstalledFromGit($appId) {
443
+        $app = \OC_App::findAppInDirectories($appId);
444
+        if ($app === false) {
445
+            return false;
446
+        }
447
+        $basedir = $app['path'].'/'.$appId;
448
+        return file_exists($basedir.'/.git/');
449
+    }
450
+
451
+    /**
452
+     * Check if app is already downloaded
453
+     * @param string $name name of the application to remove
454
+     * @return boolean
455
+     *
456
+     * The function will check if the app is already downloaded in the apps repository
457
+     */
458
+    public function isDownloaded($name) {
459
+        foreach (\OC::$APPSROOTS as $dir) {
460
+            $dirToTest = $dir['path'];
461
+            $dirToTest .= '/';
462
+            $dirToTest .= $name;
463
+            $dirToTest .= '/';
464
+
465
+            if (is_dir($dirToTest)) {
466
+                return true;
467
+            }
468
+        }
469
+
470
+        return false;
471
+    }
472
+
473
+    /**
474
+     * Removes an app
475
+     * @param string $appId ID of the application to remove
476
+     * @return boolean
477
+     *
478
+     *
479
+     * This function works as follows
480
+     *   -# call uninstall repair steps
481
+     *   -# removing the files
482
+     *
483
+     * The function will not delete preferences, tables and the configuration,
484
+     * this has to be done by the function oc_app_uninstall().
485
+     */
486
+    public function removeApp($appId) {
487
+        if ($this->isDownloaded($appId)) {
488
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
489
+                return false;
490
+            }
491
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
492
+            OC_Helper::rmdirr($appDir);
493
+            return true;
494
+        } else {
495
+            \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', ILogger::ERROR);
496
+
497
+            return false;
498
+        }
499
+    }
500
+
501
+    /**
502
+     * Installs the app within the bundle and marks the bundle as installed
503
+     *
504
+     * @param Bundle $bundle
505
+     * @throws \Exception If app could not get installed
506
+     */
507
+    public function installAppBundle(Bundle $bundle) {
508
+        $appIds = $bundle->getAppIdentifiers();
509
+        foreach ($appIds as $appId) {
510
+            if (!$this->isDownloaded($appId)) {
511
+                $this->downloadApp($appId);
512
+            }
513
+            $this->installApp($appId);
514
+            $app = new OC_App();
515
+            $app->enable($appId);
516
+        }
517
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
518
+        $bundles[] = $bundle->getIdentifier();
519
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
520
+    }
521
+
522
+    /**
523
+     * Installs shipped apps
524
+     *
525
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
526
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
527
+     *                         working ownCloud at the end instead of an aborted update.
528
+     * @return array Array of error messages (appid => Exception)
529
+     */
530
+    public static function installShippedApps($softErrors = false) {
531
+        $appManager = \OC::$server->getAppManager();
532
+        $config = \OC::$server->getConfig();
533
+        $errors = [];
534
+        foreach (\OC::$APPSROOTS as $app_dir) {
535
+            if ($dir = opendir($app_dir['path'])) {
536
+                while (false !== ($filename = readdir($dir))) {
537
+                    if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
538
+                        if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
539
+                            if ($config->getAppValue($filename, "installed_version", null) === null) {
540
+                                $info = OC_App::getAppInfo($filename);
541
+                                $enabled = isset($info['default_enable']);
542
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
543
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
544
+                                    if ($softErrors) {
545
+                                        try {
546
+                                            Installer::installShippedApp($filename);
547
+                                        } catch (HintException $e) {
548
+                                            if ($e->getPrevious() instanceof TableExistsException) {
549
+                                                $errors[$filename] = $e;
550
+                                                continue;
551
+                                            }
552
+                                            throw $e;
553
+                                        }
554
+                                    } else {
555
+                                        Installer::installShippedApp($filename);
556
+                                    }
557
+                                    $config->setAppValue($filename, 'enabled', 'yes');
558
+                                }
559
+                            }
560
+                        }
561
+                    }
562
+                }
563
+                closedir($dir);
564
+            }
565
+        }
566
+
567
+        return $errors;
568
+    }
569
+
570
+    /**
571
+     * install an app already placed in the app folder
572
+     * @param string $app id of the app to install
573
+     * @return integer
574
+     */
575
+    public static function installShippedApp($app) {
576
+        //install the database
577
+        $appPath = OC_App::getAppPath($app);
578
+        \OC_App::registerAutoloading($app, $appPath);
579
+
580
+        if (is_file("$appPath/appinfo/database.xml")) {
581
+            try {
582
+                OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
583
+            } catch (TableExistsException $e) {
584
+                throw new HintException(
585
+                    'Failed to enable app ' . $app,
586
+                    'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
587
+                    0, $e
588
+                );
589
+            }
590
+        } else {
591
+            $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
592
+            $ms->migrate();
593
+        }
594
+
595
+        //run appinfo/install.php
596
+        self::includeAppScript("$appPath/appinfo/install.php");
597
+
598
+        $info = OC_App::getAppInfo($app);
599
+        if (is_null($info)) {
600
+            return false;
601
+        }
602
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
603
+
604
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
605
+
606
+        $config = \OC::$server->getConfig();
607
+
608
+        $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
609
+        if (array_key_exists('ocsid', $info)) {
610
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
611
+        }
612
+
613
+        //set remote/public handlers
614
+        foreach ($info['remote'] as $name => $path) {
615
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
616
+        }
617
+        foreach ($info['public'] as $name => $path) {
618
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
619
+        }
620
+
621
+        OC_App::setAppTypes($info['id']);
622
+
623
+        return $info['id'];
624
+    }
625
+
626
+    /**
627
+     * @param string $script
628
+     */
629
+    private static function includeAppScript($script) {
630
+        if (file_exists($script)) {
631
+            include $script;
632
+        }
633
+    }
634 634
 }
Please login to merge, or discard this patch.
Spacing   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -163,7 +163,7 @@  discard block
 block discarded – undo
163 163
 		\OC_App::setupBackgroundJobs($info['background-jobs']);
164 164
 
165 165
 		//run appinfo/install.php
166
-		self::includeAppScript($basedir . '/appinfo/install.php');
166
+		self::includeAppScript($basedir.'/appinfo/install.php');
167 167
 
168 168
 		$appData = OC_App::getAppInfo($appId);
169 169
 		OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
@@ -225,13 +225,13 @@  discard block
 block discarded – undo
225 225
 			if ($app['id'] === $appId) {
226 226
 				// Load the certificate
227 227
 				$certificate = new X509();
228
-				$certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
228
+				$certificate->loadCA(file_get_contents(__DIR__.'/../../resources/codesigning/root.crt'));
229 229
 				$loadedCertificate = $certificate->loadX509($app['certificate']);
230 230
 
231 231
 				// Verify if the certificate has been revoked
232 232
 				$crl = new X509();
233
-				$crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
234
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
233
+				$crl->loadCA(file_get_contents(__DIR__.'/../../resources/codesigning/root.crt'));
234
+				$crl->loadCRL(file_get_contents(__DIR__.'/../../resources/codesigning/root.crl'));
235 235
 				if ($crl->validateSignature() !== true) {
236 236
 					throw new \Exception('Could not validate CRL signature');
237 237
 				}
@@ -284,7 +284,7 @@  discard block
 block discarded – undo
284 284
 
285 285
 				// Check if the signature actually matches the downloaded content
286 286
 				$certificate = openssl_get_publickey($app['certificate']);
287
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
287
+				$verified = (bool) openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
288 288
 				openssl_free_key($certificate);
289 289
 
290 290
 				if ($verified === true) {
@@ -294,11 +294,11 @@  discard block
 block discarded – undo
294 294
 
295 295
 					if ($archive) {
296 296
 						if (!$archive->extract($extractDir)) {
297
-							$errorMessage = 'Could not extract app ' . $appId;
297
+							$errorMessage = 'Could not extract app '.$appId;
298 298
 
299 299
 							$archiveError = $archive->getError();
300 300
 							if ($archiveError instanceof \PEAR_Error) {
301
-								$errorMessage .= ': ' . $archiveError->getMessage();
301
+								$errorMessage .= ': '.$archiveError->getMessage();
302 302
 							}
303 303
 
304 304
 							throw new \Exception($errorMessage);
@@ -318,21 +318,21 @@  discard block
 block discarded – undo
318 318
 
319 319
 						// Check if appinfo/info.xml has the same app ID as well
320 320
 						$loadEntities = libxml_disable_entity_loader(false);
321
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
321
+						$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
322 322
 						libxml_disable_entity_loader($loadEntities);
323
-						if ((string)$xml->id !== $appId) {
323
+						if ((string) $xml->id !== $appId) {
324 324
 							throw new \Exception(
325 325
 								sprintf(
326 326
 									'App for id %s has a wrong app ID in info.xml: %s',
327 327
 									$appId,
328
-									(string)$xml->id
328
+									(string) $xml->id
329 329
 								)
330 330
 							);
331 331
 						}
332 332
 
333 333
 						// Check if the version is lower than before
334 334
 						$currentVersion = OC_App::getAppVersion($appId);
335
-						$newVersion = (string)$xml->version;
335
+						$newVersion = (string) $xml->version;
336 336
 						if (version_compare($currentVersion, $newVersion) === 1) {
337 337
 							throw new \Exception(
338 338
 								sprintf(
@@ -344,12 +344,12 @@  discard block
 block discarded – undo
344 344
 							);
345 345
 						}
346 346
 
347
-						$baseDir = OC_App::getInstallPath() . '/' . $appId;
347
+						$baseDir = OC_App::getInstallPath().'/'.$appId;
348 348
 						// Remove old app with the ID if existent
349 349
 						OC_Helper::rmdirr($baseDir);
350 350
 						// Move to app folder
351 351
 						if (@mkdir($baseDir)) {
352
-							$extractDir .= '/' . $folders[0];
352
+							$extractDir .= '/'.$folders[0];
353 353
 							OC_Helper::copyr($extractDir, $baseDir);
354 354
 						}
355 355
 						OC_Helper::copyr($extractDir, $baseDir);
@@ -488,7 +488,7 @@  discard block
 block discarded – undo
488 488
 			if (\OC::$server->getAppManager()->isShipped($appId)) {
489 489
 				return false;
490 490
 			}
491
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
491
+			$appDir = OC_App::getInstallPath().'/'.$appId;
492 492
 			OC_Helper::rmdirr($appDir);
493 493
 			return true;
494 494
 		} else {
@@ -582,7 +582,7 @@  discard block
 block discarded – undo
582 582
 				OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
583 583
 			} catch (TableExistsException $e) {
584 584
 				throw new HintException(
585
-					'Failed to enable app ' . $app,
585
+					'Failed to enable app '.$app,
586 586
 					'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
587 587
 					0, $e
588 588
 				);
Please login to merge, or discard this patch.