Completed
Push — master ( 970e11...6694cf )
by
unknown
20:31 queued 14s
created
lib/private/legacy/OC_Helper.php 1 patch
Indentation   +503 added lines, -503 removed lines patch added patch discarded remove patch
@@ -31,510 +31,510 @@
 block discarded – undo
31 31
  * }
32 32
  */
33 33
 class OC_Helper {
34
-	private static $templateManager;
35
-	private static ?ICacheFactory $cacheFactory = null;
36
-	private static ?bool $quotaIncludeExternalStorage = null;
37
-
38
-	/**
39
-	 * Make a human file size
40
-	 * @param int|float $bytes file size in bytes
41
-	 * @return string a human readable file size
42
-	 * @deprecated 4.0.0 replaced with \OCP\Util::humanFileSize
43
-	 *
44
-	 * Makes 2048 to 2 kB.
45
-	 */
46
-	public static function humanFileSize(int|float $bytes): string {
47
-		return \OCP\Util::humanFileSize($bytes);
48
-	}
49
-
50
-	/**
51
-	 * Make a computer file size
52
-	 * @param string $str file size in human readable format
53
-	 * @return false|int|float a file size in bytes
54
-	 * @deprecated 4.0.0 Use \OCP\Util::computerFileSize
55
-	 *
56
-	 * Makes 2kB to 2048.
57
-	 *
58
-	 * Inspired by: https://www.php.net/manual/en/function.filesize.php#92418
59
-	 */
60
-	public static function computerFileSize(string $str): false|int|float {
61
-		return \OCP\Util::computerFileSize($str);
62
-	}
63
-
64
-	/**
65
-	 * Recursive copying of folders
66
-	 * @param string $src source folder
67
-	 * @param string $dest target folder
68
-	 * @return void
69
-	 * @deprecated 32.0.0 - use \OCP\Files\Folder::copy
70
-	 */
71
-	public static function copyr($src, $dest) {
72
-		if (!file_exists($src)) {
73
-			return;
74
-		}
75
-
76
-		if (is_dir($src)) {
77
-			if (!is_dir($dest)) {
78
-				mkdir($dest);
79
-			}
80
-			$files = scandir($src);
81
-			foreach ($files as $file) {
82
-				if ($file != '.' && $file != '..') {
83
-					self::copyr("$src/$file", "$dest/$file");
84
-				}
85
-			}
86
-		} else {
87
-			$validator = \OCP\Server::get(FilenameValidator::class);
88
-			if (!$validator->isForbidden($src)) {
89
-				copy($src, $dest);
90
-			}
91
-		}
92
-	}
93
-
94
-	/**
95
-	 * Recursive deletion of folders
96
-	 * @param string $dir path to the folder
97
-	 * @param bool $deleteSelf if set to false only the content of the folder will be deleted
98
-	 * @return bool
99
-	 * @deprecated 5.0.0 use \OCP\Files::rmdirr instead
100
-	 */
101
-	public static function rmdirr($dir, $deleteSelf = true) {
102
-		return \OCP\Files::rmdirr($dir, $deleteSelf);
103
-	}
104
-
105
-	/**
106
-	 * @deprecated 18.0.0
107
-	 * @return \OC\Files\Type\TemplateManager
108
-	 */
109
-	public static function getFileTemplateManager() {
110
-		if (!self::$templateManager) {
111
-			self::$templateManager = new \OC\Files\Type\TemplateManager();
112
-		}
113
-		return self::$templateManager;
114
-	}
115
-
116
-	/**
117
-	 * detect if a given program is found in the search PATH
118
-	 *
119
-	 * @param string $name
120
-	 * @param bool $path
121
-	 * @internal param string $program name
122
-	 * @internal param string $optional search path, defaults to $PATH
123
-	 * @return bool true if executable program found in path
124
-	 * @deprecated 32.0.0 use the \OCP\IBinaryFinder
125
-	 */
126
-	public static function canExecute($name, $path = false) {
127
-		// path defaults to PATH from environment if not set
128
-		if ($path === false) {
129
-			$path = getenv('PATH');
130
-		}
131
-		// we look for an executable file of that name
132
-		$exts = [''];
133
-		$check_fn = 'is_executable';
134
-		// Default check will be done with $path directories :
135
-		$dirs = explode(PATH_SEPARATOR, (string)$path);
136
-		// WARNING : We have to check if open_basedir is enabled :
137
-		$obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir');
138
-		if ($obd != 'none') {
139
-			$obd_values = explode(PATH_SEPARATOR, $obd);
140
-			if (count($obd_values) > 0 and $obd_values[0]) {
141
-				// open_basedir is in effect !
142
-				// We need to check if the program is in one of these dirs :
143
-				$dirs = $obd_values;
144
-			}
145
-		}
146
-		foreach ($dirs as $dir) {
147
-			foreach ($exts as $ext) {
148
-				if ($check_fn("$dir/$name" . $ext)) {
149
-					return true;
150
-				}
151
-			}
152
-		}
153
-		return false;
154
-	}
155
-
156
-	/**
157
-	 * copy the contents of one stream to another
158
-	 *
159
-	 * @param resource $source
160
-	 * @param resource $target
161
-	 * @return array the number of bytes copied and result
162
-	 */
163
-	public static function streamCopy($source, $target) {
164
-		if (!$source or !$target) {
165
-			return [0, false];
166
-		}
167
-		$bufSize = 8192;
168
-		$result = true;
169
-		$count = 0;
170
-		while (!feof($source)) {
171
-			$buf = fread($source, $bufSize);
172
-			$bytesWritten = fwrite($target, $buf);
173
-			if ($bytesWritten !== false) {
174
-				$count += $bytesWritten;
175
-			}
176
-			// note: strlen is expensive so only use it when necessary,
177
-			// on the last block
178
-			if ($bytesWritten === false
179
-				|| ($bytesWritten < $bufSize && $bytesWritten < strlen($buf))
180
-			) {
181
-				// write error, could be disk full ?
182
-				$result = false;
183
-				break;
184
-			}
185
-		}
186
-		return [$count, $result];
187
-	}
188
-
189
-	/**
190
-	 * Adds a suffix to the name in case the file exists
191
-	 *
192
-	 * @param string $path
193
-	 * @param string $filename
194
-	 * @return string
195
-	 */
196
-	public static function buildNotExistingFileName($path, $filename) {
197
-		$view = \OC\Files\Filesystem::getView();
198
-		return self::buildNotExistingFileNameForView($path, $filename, $view);
199
-	}
200
-
201
-	/**
202
-	 * Adds a suffix to the name in case the file exists
203
-	 *
204
-	 * @param string $path
205
-	 * @param string $filename
206
-	 * @return string
207
-	 */
208
-	public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) {
209
-		if ($path === '/') {
210
-			$path = '';
211
-		}
212
-		if ($pos = strrpos($filename, '.')) {
213
-			$name = substr($filename, 0, $pos);
214
-			$ext = substr($filename, $pos);
215
-		} else {
216
-			$name = $filename;
217
-			$ext = '';
218
-		}
219
-
220
-		$newpath = $path . '/' . $filename;
221
-		if ($view->file_exists($newpath)) {
222
-			if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) {
223
-				//Replace the last "(number)" with "(number+1)"
224
-				$last_match = count($matches[0]) - 1;
225
-				$counter = $matches[1][$last_match][0] + 1;
226
-				$offset = $matches[0][$last_match][1];
227
-				$match_length = strlen($matches[0][$last_match][0]);
228
-			} else {
229
-				$counter = 2;
230
-				$match_length = 0;
231
-				$offset = false;
232
-			}
233
-			do {
234
-				if ($offset) {
235
-					//Replace the last "(number)" with "(number+1)"
236
-					$newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length);
237
-				} else {
238
-					$newname = $name . ' (' . $counter . ')';
239
-				}
240
-				$newpath = $path . '/' . $newname . $ext;
241
-				$counter++;
242
-			} while ($view->file_exists($newpath));
243
-		}
244
-
245
-		return $newpath;
246
-	}
247
-
248
-	/**
249
-	 * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is.
250
-	 * Based on https://www.php.net/manual/en/function.array-change-key-case.php#107715
251
-	 *
252
-	 * @param array $input The array to work on
253
-	 * @param int $case Either MB_CASE_UPPER or MB_CASE_LOWER (default)
254
-	 * @param string $encoding The encoding parameter is the character encoding. Defaults to UTF-8
255
-	 * @return array
256
-	 * @deprecated 4.5.0 use \OCP\Util::mb_array_change_key_case instead
257
-	 */
258
-	public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') {
259
-		return \OCP\Util::mb_array_change_key_case($input, $case, $encoding);
260
-	}
261
-
262
-	/**
263
-	 * Performs a search in a nested array.
264
-	 * Taken from https://www.php.net/manual/en/function.array-search.php#97645
265
-	 *
266
-	 * @param array $haystack the array to be searched
267
-	 * @param string $needle the search string
268
-	 * @param mixed $index optional, only search this key name
269
-	 * @return mixed the key of the matching field, otherwise false
270
-	 * @deprecated 4.5.0 - use \OCP\Util::recursiveArraySearch
271
-	 */
272
-	public static function recursiveArraySearch($haystack, $needle, $index = null) {
273
-		return \OCP\Util::recursiveArraySearch($haystack, $needle, $index);
274
-	}
275
-
276
-	/**
277
-	 * calculates the maximum upload size respecting system settings, free space and user quota
278
-	 *
279
-	 * @param string $dir the current folder where the user currently operates
280
-	 * @param int|float $freeSpace the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly
281
-	 * @return int|float number of bytes representing
282
-	 * @deprecated 5.0.0 - use \OCP\Util::maxUploadFilesize
283
-	 */
284
-	public static function maxUploadFilesize($dir, $freeSpace = null) {
285
-		return \OCP\Util::maxUploadFilesize($dir, $freeSpace);
286
-	}
287
-
288
-	/**
289
-	 * Calculate free space left within user quota
290
-	 *
291
-	 * @param string $dir the current folder where the user currently operates
292
-	 * @return int|float number of bytes representing
293
-	 * @deprecated 7.0.0 - use \OCP\Util::freeSpace
294
-	 */
295
-	public static function freeSpace($dir) {
296
-		return \OCP\Util::freeSpace($dir);
297
-	}
298
-
299
-	/**
300
-	 * Calculate PHP upload limit
301
-	 *
302
-	 * @return int|float PHP upload file size limit
303
-	 * @deprecated 7.0.0 - use \OCP\Util::uploadLimit
304
-	 */
305
-	public static function uploadLimit() {
306
-		return \OCP\Util::uploadLimit();
307
-	}
308
-
309
-	/**
310
-	 * Checks if a function is available
311
-	 *
312
-	 * @deprecated 25.0.0 use \OCP\Util::isFunctionEnabled instead
313
-	 */
314
-	public static function is_function_enabled(string $function_name): bool {
315
-		return \OCP\Util::isFunctionEnabled($function_name);
316
-	}
317
-
318
-	/**
319
-	 * Try to find a program
320
-	 * @deprecated 25.0.0 Use \OC\BinaryFinder directly
321
-	 */
322
-	public static function findBinaryPath(string $program): ?string {
323
-		$result = \OCP\Server::get(IBinaryFinder::class)->findBinaryPath($program);
324
-		return $result !== false ? $result : null;
325
-	}
326
-
327
-	/**
328
-	 * Calculate the disc space for the given path
329
-	 *
330
-	 * BEWARE: this requires that Util::setupFS() was called
331
-	 * already !
332
-	 *
333
-	 * @param string $path
334
-	 * @param \OCP\Files\FileInfo $rootInfo (optional)
335
-	 * @param bool $includeMountPoints whether to include mount points in the size calculation
336
-	 * @param bool $useCache whether to use the cached quota values
337
-	 * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
338
-	 * @return StorageInfo
339
-	 * @throws \OCP\Files\NotFoundException
340
-	 */
341
-	public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) {
342
-		if (!self::$cacheFactory) {
343
-			self::$cacheFactory = \OC::$server->get(ICacheFactory::class);
344
-		}
345
-		$memcache = self::$cacheFactory->createLocal('storage_info');
346
-
347
-		// return storage info without adding mount points
348
-		if (self::$quotaIncludeExternalStorage === null) {
349
-			self::$quotaIncludeExternalStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false);
350
-		}
351
-
352
-		$view = Filesystem::getView();
353
-		if (!$view) {
354
-			throw new \OCP\Files\NotFoundException();
355
-		}
356
-		$fullPath = Filesystem::normalizePath($view->getAbsolutePath($path));
357
-
358
-		$cacheKey = $fullPath . '::' . ($includeMountPoints ? 'include' : 'exclude');
359
-		if ($useCache) {
360
-			$cached = $memcache->get($cacheKey);
361
-			if ($cached) {
362
-				return $cached;
363
-			}
364
-		}
365
-
366
-		if (!$rootInfo) {
367
-			$rootInfo = \OC\Files\Filesystem::getFileInfo($path, self::$quotaIncludeExternalStorage ? 'ext' : false);
368
-		}
369
-		if (!$rootInfo instanceof \OCP\Files\FileInfo) {
370
-			throw new \OCP\Files\NotFoundException('The root directory of the user\'s files is missing');
371
-		}
372
-		$used = $rootInfo->getSize($includeMountPoints);
373
-		if ($used < 0) {
374
-			$used = 0.0;
375
-		}
376
-		/** @var int|float $quota */
377
-		$quota = \OCP\Files\FileInfo::SPACE_UNLIMITED;
378
-		$mount = $rootInfo->getMountPoint();
379
-		$storage = $mount->getStorage();
380
-		$sourceStorage = $storage;
381
-		if ($storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
382
-			self::$quotaIncludeExternalStorage = false;
383
-		}
384
-		if (self::$quotaIncludeExternalStorage) {
385
-			if ($storage->instanceOfStorage('\OC\Files\Storage\Home')
386
-				|| $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage')
387
-			) {
388
-				/** @var \OC\Files\Storage\Home $storage */
389
-				$user = $storage->getUser();
390
-			} else {
391
-				$user = \OC::$server->getUserSession()->getUser();
392
-			}
393
-			$quota = OC_Util::getUserQuota($user);
394
-			if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) {
395
-				// always get free space / total space from root + mount points
396
-				return self::getGlobalStorageInfo($quota, $user, $mount);
397
-			}
398
-		}
399
-
400
-		// TODO: need a better way to get total space from storage
401
-		if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) {
402
-			/** @var \OC\Files\Storage\Wrapper\Quota $storage */
403
-			$quota = $sourceStorage->getQuota();
404
-		}
405
-		try {
406
-			$free = $sourceStorage->free_space($rootInfo->getInternalPath());
407
-			if (is_bool($free)) {
408
-				$free = 0.0;
409
-			}
410
-		} catch (\Exception $e) {
411
-			if ($path === '') {
412
-				throw $e;
413
-			}
414
-			/** @var LoggerInterface $logger */
415
-			$logger = \OC::$server->get(LoggerInterface::class);
416
-			$logger->warning('Error while getting quota info, using root quota', ['exception' => $e]);
417
-			$rootInfo = self::getStorageInfo('');
418
-			$memcache->set($cacheKey, $rootInfo, 5 * 60);
419
-			return $rootInfo;
420
-		}
421
-		if ($free >= 0) {
422
-			$total = $free + $used;
423
-		} else {
424
-			$total = $free; //either unknown or unlimited
425
-		}
426
-		if ($total > 0) {
427
-			if ($quota > 0 && $total > $quota) {
428
-				$total = $quota;
429
-			}
430
-			// prevent division by zero or error codes (negative values)
431
-			$relative = round(($used / $total) * 10000) / 100;
432
-		} else {
433
-			$relative = 0;
434
-		}
435
-
436
-		/*
34
+    private static $templateManager;
35
+    private static ?ICacheFactory $cacheFactory = null;
36
+    private static ?bool $quotaIncludeExternalStorage = null;
37
+
38
+    /**
39
+     * Make a human file size
40
+     * @param int|float $bytes file size in bytes
41
+     * @return string a human readable file size
42
+     * @deprecated 4.0.0 replaced with \OCP\Util::humanFileSize
43
+     *
44
+     * Makes 2048 to 2 kB.
45
+     */
46
+    public static function humanFileSize(int|float $bytes): string {
47
+        return \OCP\Util::humanFileSize($bytes);
48
+    }
49
+
50
+    /**
51
+     * Make a computer file size
52
+     * @param string $str file size in human readable format
53
+     * @return false|int|float a file size in bytes
54
+     * @deprecated 4.0.0 Use \OCP\Util::computerFileSize
55
+     *
56
+     * Makes 2kB to 2048.
57
+     *
58
+     * Inspired by: https://www.php.net/manual/en/function.filesize.php#92418
59
+     */
60
+    public static function computerFileSize(string $str): false|int|float {
61
+        return \OCP\Util::computerFileSize($str);
62
+    }
63
+
64
+    /**
65
+     * Recursive copying of folders
66
+     * @param string $src source folder
67
+     * @param string $dest target folder
68
+     * @return void
69
+     * @deprecated 32.0.0 - use \OCP\Files\Folder::copy
70
+     */
71
+    public static function copyr($src, $dest) {
72
+        if (!file_exists($src)) {
73
+            return;
74
+        }
75
+
76
+        if (is_dir($src)) {
77
+            if (!is_dir($dest)) {
78
+                mkdir($dest);
79
+            }
80
+            $files = scandir($src);
81
+            foreach ($files as $file) {
82
+                if ($file != '.' && $file != '..') {
83
+                    self::copyr("$src/$file", "$dest/$file");
84
+                }
85
+            }
86
+        } else {
87
+            $validator = \OCP\Server::get(FilenameValidator::class);
88
+            if (!$validator->isForbidden($src)) {
89
+                copy($src, $dest);
90
+            }
91
+        }
92
+    }
93
+
94
+    /**
95
+     * Recursive deletion of folders
96
+     * @param string $dir path to the folder
97
+     * @param bool $deleteSelf if set to false only the content of the folder will be deleted
98
+     * @return bool
99
+     * @deprecated 5.0.0 use \OCP\Files::rmdirr instead
100
+     */
101
+    public static function rmdirr($dir, $deleteSelf = true) {
102
+        return \OCP\Files::rmdirr($dir, $deleteSelf);
103
+    }
104
+
105
+    /**
106
+     * @deprecated 18.0.0
107
+     * @return \OC\Files\Type\TemplateManager
108
+     */
109
+    public static function getFileTemplateManager() {
110
+        if (!self::$templateManager) {
111
+            self::$templateManager = new \OC\Files\Type\TemplateManager();
112
+        }
113
+        return self::$templateManager;
114
+    }
115
+
116
+    /**
117
+     * detect if a given program is found in the search PATH
118
+     *
119
+     * @param string $name
120
+     * @param bool $path
121
+     * @internal param string $program name
122
+     * @internal param string $optional search path, defaults to $PATH
123
+     * @return bool true if executable program found in path
124
+     * @deprecated 32.0.0 use the \OCP\IBinaryFinder
125
+     */
126
+    public static function canExecute($name, $path = false) {
127
+        // path defaults to PATH from environment if not set
128
+        if ($path === false) {
129
+            $path = getenv('PATH');
130
+        }
131
+        // we look for an executable file of that name
132
+        $exts = [''];
133
+        $check_fn = 'is_executable';
134
+        // Default check will be done with $path directories :
135
+        $dirs = explode(PATH_SEPARATOR, (string)$path);
136
+        // WARNING : We have to check if open_basedir is enabled :
137
+        $obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir');
138
+        if ($obd != 'none') {
139
+            $obd_values = explode(PATH_SEPARATOR, $obd);
140
+            if (count($obd_values) > 0 and $obd_values[0]) {
141
+                // open_basedir is in effect !
142
+                // We need to check if the program is in one of these dirs :
143
+                $dirs = $obd_values;
144
+            }
145
+        }
146
+        foreach ($dirs as $dir) {
147
+            foreach ($exts as $ext) {
148
+                if ($check_fn("$dir/$name" . $ext)) {
149
+                    return true;
150
+                }
151
+            }
152
+        }
153
+        return false;
154
+    }
155
+
156
+    /**
157
+     * copy the contents of one stream to another
158
+     *
159
+     * @param resource $source
160
+     * @param resource $target
161
+     * @return array the number of bytes copied and result
162
+     */
163
+    public static function streamCopy($source, $target) {
164
+        if (!$source or !$target) {
165
+            return [0, false];
166
+        }
167
+        $bufSize = 8192;
168
+        $result = true;
169
+        $count = 0;
170
+        while (!feof($source)) {
171
+            $buf = fread($source, $bufSize);
172
+            $bytesWritten = fwrite($target, $buf);
173
+            if ($bytesWritten !== false) {
174
+                $count += $bytesWritten;
175
+            }
176
+            // note: strlen is expensive so only use it when necessary,
177
+            // on the last block
178
+            if ($bytesWritten === false
179
+                || ($bytesWritten < $bufSize && $bytesWritten < strlen($buf))
180
+            ) {
181
+                // write error, could be disk full ?
182
+                $result = false;
183
+                break;
184
+            }
185
+        }
186
+        return [$count, $result];
187
+    }
188
+
189
+    /**
190
+     * Adds a suffix to the name in case the file exists
191
+     *
192
+     * @param string $path
193
+     * @param string $filename
194
+     * @return string
195
+     */
196
+    public static function buildNotExistingFileName($path, $filename) {
197
+        $view = \OC\Files\Filesystem::getView();
198
+        return self::buildNotExistingFileNameForView($path, $filename, $view);
199
+    }
200
+
201
+    /**
202
+     * Adds a suffix to the name in case the file exists
203
+     *
204
+     * @param string $path
205
+     * @param string $filename
206
+     * @return string
207
+     */
208
+    public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) {
209
+        if ($path === '/') {
210
+            $path = '';
211
+        }
212
+        if ($pos = strrpos($filename, '.')) {
213
+            $name = substr($filename, 0, $pos);
214
+            $ext = substr($filename, $pos);
215
+        } else {
216
+            $name = $filename;
217
+            $ext = '';
218
+        }
219
+
220
+        $newpath = $path . '/' . $filename;
221
+        if ($view->file_exists($newpath)) {
222
+            if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) {
223
+                //Replace the last "(number)" with "(number+1)"
224
+                $last_match = count($matches[0]) - 1;
225
+                $counter = $matches[1][$last_match][0] + 1;
226
+                $offset = $matches[0][$last_match][1];
227
+                $match_length = strlen($matches[0][$last_match][0]);
228
+            } else {
229
+                $counter = 2;
230
+                $match_length = 0;
231
+                $offset = false;
232
+            }
233
+            do {
234
+                if ($offset) {
235
+                    //Replace the last "(number)" with "(number+1)"
236
+                    $newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length);
237
+                } else {
238
+                    $newname = $name . ' (' . $counter . ')';
239
+                }
240
+                $newpath = $path . '/' . $newname . $ext;
241
+                $counter++;
242
+            } while ($view->file_exists($newpath));
243
+        }
244
+
245
+        return $newpath;
246
+    }
247
+
248
+    /**
249
+     * Returns an array with all keys from input lowercased or uppercased. Numbered indices are left as is.
250
+     * Based on https://www.php.net/manual/en/function.array-change-key-case.php#107715
251
+     *
252
+     * @param array $input The array to work on
253
+     * @param int $case Either MB_CASE_UPPER or MB_CASE_LOWER (default)
254
+     * @param string $encoding The encoding parameter is the character encoding. Defaults to UTF-8
255
+     * @return array
256
+     * @deprecated 4.5.0 use \OCP\Util::mb_array_change_key_case instead
257
+     */
258
+    public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') {
259
+        return \OCP\Util::mb_array_change_key_case($input, $case, $encoding);
260
+    }
261
+
262
+    /**
263
+     * Performs a search in a nested array.
264
+     * Taken from https://www.php.net/manual/en/function.array-search.php#97645
265
+     *
266
+     * @param array $haystack the array to be searched
267
+     * @param string $needle the search string
268
+     * @param mixed $index optional, only search this key name
269
+     * @return mixed the key of the matching field, otherwise false
270
+     * @deprecated 4.5.0 - use \OCP\Util::recursiveArraySearch
271
+     */
272
+    public static function recursiveArraySearch($haystack, $needle, $index = null) {
273
+        return \OCP\Util::recursiveArraySearch($haystack, $needle, $index);
274
+    }
275
+
276
+    /**
277
+     * calculates the maximum upload size respecting system settings, free space and user quota
278
+     *
279
+     * @param string $dir the current folder where the user currently operates
280
+     * @param int|float $freeSpace the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly
281
+     * @return int|float number of bytes representing
282
+     * @deprecated 5.0.0 - use \OCP\Util::maxUploadFilesize
283
+     */
284
+    public static function maxUploadFilesize($dir, $freeSpace = null) {
285
+        return \OCP\Util::maxUploadFilesize($dir, $freeSpace);
286
+    }
287
+
288
+    /**
289
+     * Calculate free space left within user quota
290
+     *
291
+     * @param string $dir the current folder where the user currently operates
292
+     * @return int|float number of bytes representing
293
+     * @deprecated 7.0.0 - use \OCP\Util::freeSpace
294
+     */
295
+    public static function freeSpace($dir) {
296
+        return \OCP\Util::freeSpace($dir);
297
+    }
298
+
299
+    /**
300
+     * Calculate PHP upload limit
301
+     *
302
+     * @return int|float PHP upload file size limit
303
+     * @deprecated 7.0.0 - use \OCP\Util::uploadLimit
304
+     */
305
+    public static function uploadLimit() {
306
+        return \OCP\Util::uploadLimit();
307
+    }
308
+
309
+    /**
310
+     * Checks if a function is available
311
+     *
312
+     * @deprecated 25.0.0 use \OCP\Util::isFunctionEnabled instead
313
+     */
314
+    public static function is_function_enabled(string $function_name): bool {
315
+        return \OCP\Util::isFunctionEnabled($function_name);
316
+    }
317
+
318
+    /**
319
+     * Try to find a program
320
+     * @deprecated 25.0.0 Use \OC\BinaryFinder directly
321
+     */
322
+    public static function findBinaryPath(string $program): ?string {
323
+        $result = \OCP\Server::get(IBinaryFinder::class)->findBinaryPath($program);
324
+        return $result !== false ? $result : null;
325
+    }
326
+
327
+    /**
328
+     * Calculate the disc space for the given path
329
+     *
330
+     * BEWARE: this requires that Util::setupFS() was called
331
+     * already !
332
+     *
333
+     * @param string $path
334
+     * @param \OCP\Files\FileInfo $rootInfo (optional)
335
+     * @param bool $includeMountPoints whether to include mount points in the size calculation
336
+     * @param bool $useCache whether to use the cached quota values
337
+     * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
338
+     * @return StorageInfo
339
+     * @throws \OCP\Files\NotFoundException
340
+     */
341
+    public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) {
342
+        if (!self::$cacheFactory) {
343
+            self::$cacheFactory = \OC::$server->get(ICacheFactory::class);
344
+        }
345
+        $memcache = self::$cacheFactory->createLocal('storage_info');
346
+
347
+        // return storage info without adding mount points
348
+        if (self::$quotaIncludeExternalStorage === null) {
349
+            self::$quotaIncludeExternalStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false);
350
+        }
351
+
352
+        $view = Filesystem::getView();
353
+        if (!$view) {
354
+            throw new \OCP\Files\NotFoundException();
355
+        }
356
+        $fullPath = Filesystem::normalizePath($view->getAbsolutePath($path));
357
+
358
+        $cacheKey = $fullPath . '::' . ($includeMountPoints ? 'include' : 'exclude');
359
+        if ($useCache) {
360
+            $cached = $memcache->get($cacheKey);
361
+            if ($cached) {
362
+                return $cached;
363
+            }
364
+        }
365
+
366
+        if (!$rootInfo) {
367
+            $rootInfo = \OC\Files\Filesystem::getFileInfo($path, self::$quotaIncludeExternalStorage ? 'ext' : false);
368
+        }
369
+        if (!$rootInfo instanceof \OCP\Files\FileInfo) {
370
+            throw new \OCP\Files\NotFoundException('The root directory of the user\'s files is missing');
371
+        }
372
+        $used = $rootInfo->getSize($includeMountPoints);
373
+        if ($used < 0) {
374
+            $used = 0.0;
375
+        }
376
+        /** @var int|float $quota */
377
+        $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED;
378
+        $mount = $rootInfo->getMountPoint();
379
+        $storage = $mount->getStorage();
380
+        $sourceStorage = $storage;
381
+        if ($storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
382
+            self::$quotaIncludeExternalStorage = false;
383
+        }
384
+        if (self::$quotaIncludeExternalStorage) {
385
+            if ($storage->instanceOfStorage('\OC\Files\Storage\Home')
386
+                || $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage')
387
+            ) {
388
+                /** @var \OC\Files\Storage\Home $storage */
389
+                $user = $storage->getUser();
390
+            } else {
391
+                $user = \OC::$server->getUserSession()->getUser();
392
+            }
393
+            $quota = OC_Util::getUserQuota($user);
394
+            if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) {
395
+                // always get free space / total space from root + mount points
396
+                return self::getGlobalStorageInfo($quota, $user, $mount);
397
+            }
398
+        }
399
+
400
+        // TODO: need a better way to get total space from storage
401
+        if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) {
402
+            /** @var \OC\Files\Storage\Wrapper\Quota $storage */
403
+            $quota = $sourceStorage->getQuota();
404
+        }
405
+        try {
406
+            $free = $sourceStorage->free_space($rootInfo->getInternalPath());
407
+            if (is_bool($free)) {
408
+                $free = 0.0;
409
+            }
410
+        } catch (\Exception $e) {
411
+            if ($path === '') {
412
+                throw $e;
413
+            }
414
+            /** @var LoggerInterface $logger */
415
+            $logger = \OC::$server->get(LoggerInterface::class);
416
+            $logger->warning('Error while getting quota info, using root quota', ['exception' => $e]);
417
+            $rootInfo = self::getStorageInfo('');
418
+            $memcache->set($cacheKey, $rootInfo, 5 * 60);
419
+            return $rootInfo;
420
+        }
421
+        if ($free >= 0) {
422
+            $total = $free + $used;
423
+        } else {
424
+            $total = $free; //either unknown or unlimited
425
+        }
426
+        if ($total > 0) {
427
+            if ($quota > 0 && $total > $quota) {
428
+                $total = $quota;
429
+            }
430
+            // prevent division by zero or error codes (negative values)
431
+            $relative = round(($used / $total) * 10000) / 100;
432
+        } else {
433
+            $relative = 0;
434
+        }
435
+
436
+        /*
437 437
 		 * \OCA\Files_Sharing\External\Storage returns the cloud ID as the owner for the storage.
438 438
 		 * It is unnecessary to query the user manager for the display name, as it won't have this information.
439 439
 		 */
440
-		$isRemoteShare = $storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class);
441
-
442
-		$ownerId = $storage->getOwner($path);
443
-		$ownerDisplayName = '';
444
-
445
-		if ($isRemoteShare === false && $ownerId !== false) {
446
-			$ownerDisplayName = \OC::$server->getUserManager()->getDisplayName($ownerId) ?? '';
447
-		}
448
-
449
-		if (substr_count($mount->getMountPoint(), '/') < 3) {
450
-			$mountPoint = '';
451
-		} else {
452
-			[,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
453
-		}
454
-
455
-		$info = [
456
-			'free' => $free,
457
-			'used' => $used,
458
-			'quota' => $quota,
459
-			'total' => $total,
460
-			'relative' => $relative,
461
-			'owner' => $ownerId,
462
-			'ownerDisplayName' => $ownerDisplayName,
463
-			'mountType' => $mount->getMountType(),
464
-			'mountPoint' => trim($mountPoint, '/'),
465
-		];
466
-
467
-		if ($isRemoteShare === false && $ownerId !== false && $path === '/') {
468
-			// If path is root, store this as last known quota usage for this user
469
-			\OCP\Server::get(\OCP\IConfig::class)->setUserValue($ownerId, 'files', 'lastSeenQuotaUsage', (string)$relative);
470
-		}
471
-
472
-		$memcache->set($cacheKey, $info, 5 * 60);
473
-
474
-		return $info;
475
-	}
476
-
477
-	/**
478
-	 * Get storage info including all mount points and quota
479
-	 *
480
-	 * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
481
-	 * @return StorageInfo
482
-	 */
483
-	private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMountPoint $mount): array {
484
-		$rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext');
485
-		/** @var int|float $used */
486
-		$used = $rootInfo['size'];
487
-		if ($used < 0) {
488
-			$used = 0.0;
489
-		}
490
-
491
-		$total = $quota;
492
-		/** @var int|float $free */
493
-		$free = $quota - $used;
494
-
495
-		if ($total > 0) {
496
-			if ($quota > 0 && $total > $quota) {
497
-				$total = $quota;
498
-			}
499
-			// prevent division by zero or error codes (negative values)
500
-			$relative = round(($used / $total) * 10000) / 100;
501
-		} else {
502
-			$relative = 0.0;
503
-		}
504
-
505
-		if (substr_count($mount->getMountPoint(), '/') < 3) {
506
-			$mountPoint = '';
507
-		} else {
508
-			[,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
509
-		}
510
-
511
-		return [
512
-			'free' => $free,
513
-			'used' => $used,
514
-			'total' => $total,
515
-			'relative' => $relative,
516
-			'quota' => $quota,
517
-			'owner' => $user->getUID(),
518
-			'ownerDisplayName' => $user->getDisplayName(),
519
-			'mountType' => $mount->getMountType(),
520
-			'mountPoint' => trim($mountPoint, '/'),
521
-		];
522
-	}
523
-
524
-	public static function clearStorageInfo(string $absolutePath): void {
525
-		/** @var ICacheFactory $cacheFactory */
526
-		$cacheFactory = \OC::$server->get(ICacheFactory::class);
527
-		$memcache = $cacheFactory->createLocal('storage_info');
528
-		$cacheKeyPrefix = Filesystem::normalizePath($absolutePath) . '::';
529
-		$memcache->remove($cacheKeyPrefix . 'include');
530
-		$memcache->remove($cacheKeyPrefix . 'exclude');
531
-	}
532
-
533
-	/**
534
-	 * Returns whether the config file is set manually to read-only
535
-	 * @return bool
536
-	 */
537
-	public static function isReadOnlyConfigEnabled() {
538
-		return \OC::$server->getConfig()->getSystemValueBool('config_is_read_only', false);
539
-	}
440
+        $isRemoteShare = $storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class);
441
+
442
+        $ownerId = $storage->getOwner($path);
443
+        $ownerDisplayName = '';
444
+
445
+        if ($isRemoteShare === false && $ownerId !== false) {
446
+            $ownerDisplayName = \OC::$server->getUserManager()->getDisplayName($ownerId) ?? '';
447
+        }
448
+
449
+        if (substr_count($mount->getMountPoint(), '/') < 3) {
450
+            $mountPoint = '';
451
+        } else {
452
+            [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
453
+        }
454
+
455
+        $info = [
456
+            'free' => $free,
457
+            'used' => $used,
458
+            'quota' => $quota,
459
+            'total' => $total,
460
+            'relative' => $relative,
461
+            'owner' => $ownerId,
462
+            'ownerDisplayName' => $ownerDisplayName,
463
+            'mountType' => $mount->getMountType(),
464
+            'mountPoint' => trim($mountPoint, '/'),
465
+        ];
466
+
467
+        if ($isRemoteShare === false && $ownerId !== false && $path === '/') {
468
+            // If path is root, store this as last known quota usage for this user
469
+            \OCP\Server::get(\OCP\IConfig::class)->setUserValue($ownerId, 'files', 'lastSeenQuotaUsage', (string)$relative);
470
+        }
471
+
472
+        $memcache->set($cacheKey, $info, 5 * 60);
473
+
474
+        return $info;
475
+    }
476
+
477
+    /**
478
+     * Get storage info including all mount points and quota
479
+     *
480
+     * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct
481
+     * @return StorageInfo
482
+     */
483
+    private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMountPoint $mount): array {
484
+        $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext');
485
+        /** @var int|float $used */
486
+        $used = $rootInfo['size'];
487
+        if ($used < 0) {
488
+            $used = 0.0;
489
+        }
490
+
491
+        $total = $quota;
492
+        /** @var int|float $free */
493
+        $free = $quota - $used;
494
+
495
+        if ($total > 0) {
496
+            if ($quota > 0 && $total > $quota) {
497
+                $total = $quota;
498
+            }
499
+            // prevent division by zero or error codes (negative values)
500
+            $relative = round(($used / $total) * 10000) / 100;
501
+        } else {
502
+            $relative = 0.0;
503
+        }
504
+
505
+        if (substr_count($mount->getMountPoint(), '/') < 3) {
506
+            $mountPoint = '';
507
+        } else {
508
+            [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4);
509
+        }
510
+
511
+        return [
512
+            'free' => $free,
513
+            'used' => $used,
514
+            'total' => $total,
515
+            'relative' => $relative,
516
+            'quota' => $quota,
517
+            'owner' => $user->getUID(),
518
+            'ownerDisplayName' => $user->getDisplayName(),
519
+            'mountType' => $mount->getMountType(),
520
+            'mountPoint' => trim($mountPoint, '/'),
521
+        ];
522
+    }
523
+
524
+    public static function clearStorageInfo(string $absolutePath): void {
525
+        /** @var ICacheFactory $cacheFactory */
526
+        $cacheFactory = \OC::$server->get(ICacheFactory::class);
527
+        $memcache = $cacheFactory->createLocal('storage_info');
528
+        $cacheKeyPrefix = Filesystem::normalizePath($absolutePath) . '::';
529
+        $memcache->remove($cacheKeyPrefix . 'include');
530
+        $memcache->remove($cacheKeyPrefix . 'exclude');
531
+    }
532
+
533
+    /**
534
+     * Returns whether the config file is set manually to read-only
535
+     * @return bool
536
+     */
537
+    public static function isReadOnlyConfigEnabled() {
538
+        return \OC::$server->getConfig()->getSystemValueBool('config_is_read_only', false);
539
+    }
540 540
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 1 patch
Indentation   +592 added lines, -592 removed lines patch added patch discarded remove patch
@@ -33,596 +33,596 @@
 block discarded – undo
33 33
  * This class provides the functionality needed to install, update and remove apps
34 34
  */
35 35
 class Installer {
36
-	private ?bool $isInstanceReadyForUpdates = null;
37
-	private ?array $apps = null;
38
-
39
-	public function __construct(
40
-		private AppFetcher $appFetcher,
41
-		private IClientService $clientService,
42
-		private ITempManager $tempManager,
43
-		private LoggerInterface $logger,
44
-		private IConfig $config,
45
-		private bool $isCLI,
46
-	) {
47
-	}
48
-
49
-	/**
50
-	 * Installs an app that is located in one of the app folders already
51
-	 *
52
-	 * @param string $appId App to install
53
-	 * @param bool $forceEnable
54
-	 * @throws \Exception
55
-	 * @return string app ID
56
-	 */
57
-	public function installApp(string $appId, bool $forceEnable = false): string {
58
-		$app = \OC_App::findAppInDirectories($appId);
59
-		if ($app === false) {
60
-			throw new \Exception('App not found in any app directory');
61
-		}
62
-
63
-		$basedir = $app['path'] . '/' . $appId;
64
-
65
-		if (is_file($basedir . '/appinfo/database.xml')) {
66
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
67
-		}
68
-
69
-		$l = \OCP\Util::getL10N('core');
70
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode());
71
-
72
-		if (!is_array($info)) {
73
-			throw new \Exception(
74
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
75
-					[$appId]
76
-				)
77
-			);
78
-		}
79
-
80
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
81
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
82
-
83
-		$version = implode('.', \OCP\Util::getVersion());
84
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
85
-			throw new \Exception(
86
-				// TODO $l
87
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
88
-					[$info['name']]
89
-				)
90
-			);
91
-		}
92
-
93
-		// check for required dependencies
94
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
95
-		/** @var Coordinator $coordinator */
96
-		$coordinator = \OC::$server->get(Coordinator::class);
97
-		$coordinator->runLazyRegistration($appId);
98
-		\OC_App::registerAutoloading($appId, $basedir);
99
-
100
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
101
-		if ($previousVersion) {
102
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
103
-		}
104
-
105
-		//install the database
106
-		$ms = new MigrationService($info['id'], \OCP\Server::get(Connection::class));
107
-		$ms->migrate('latest', !$previousVersion);
108
-
109
-		if ($previousVersion) {
110
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
111
-		}
112
-
113
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
114
-
115
-		//run appinfo/install.php
116
-		self::includeAppScript($basedir . '/appinfo/install.php');
117
-
118
-		OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
119
-
120
-		$config = \OCP\Server::get(IConfig::class);
121
-		//set the installed version
122
-		$config->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
123
-		$config->setAppValue($info['id'], 'enabled', 'no');
124
-
125
-		//set remote/public handlers
126
-		foreach ($info['remote'] as $name => $path) {
127
-			$config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path);
128
-		}
129
-		foreach ($info['public'] as $name => $path) {
130
-			$config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path);
131
-		}
132
-
133
-		OC_App::setAppTypes($info['id']);
134
-
135
-		return $info['id'];
136
-	}
137
-
138
-	/**
139
-	 * Updates the specified app from the appstore
140
-	 *
141
-	 * @param bool $allowUnstable Allow unstable releases
142
-	 */
143
-	public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool {
144
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
145
-			try {
146
-				$this->downloadApp($appId, $allowUnstable);
147
-			} catch (\Exception $e) {
148
-				$this->logger->error($e->getMessage(), [
149
-					'exception' => $e,
150
-				]);
151
-				return false;
152
-			}
153
-			return OC_App::updateApp($appId);
154
-		}
155
-
156
-		return false;
157
-	}
158
-
159
-	/**
160
-	 * Split the certificate file in individual certs
161
-	 *
162
-	 * @param string $cert
163
-	 * @return string[]
164
-	 */
165
-	private function splitCerts(string $cert): array {
166
-		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
167
-
168
-		return $matches[0];
169
-	}
170
-
171
-	/**
172
-	 * Downloads an app and puts it into the app directory
173
-	 *
174
-	 * @param string $appId
175
-	 * @param bool [$allowUnstable]
176
-	 *
177
-	 * @throws \Exception If the installation was not successful
178
-	 */
179
-	public function downloadApp(string $appId, bool $allowUnstable = false): void {
180
-		$appId = strtolower($appId);
181
-
182
-		$apps = $this->appFetcher->get($allowUnstable);
183
-		foreach ($apps as $app) {
184
-			if ($app['id'] === $appId) {
185
-				// Load the certificate
186
-				$certificate = new X509();
187
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
188
-				$rootCrts = $this->splitCerts($rootCrt);
189
-				foreach ($rootCrts as $rootCrt) {
190
-					$certificate->loadCA($rootCrt);
191
-				}
192
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
193
-
194
-				// Verify if the certificate has been revoked
195
-				$crl = new X509();
196
-				foreach ($rootCrts as $rootCrt) {
197
-					$crl->loadCA($rootCrt);
198
-				}
199
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
200
-				if ($crl->validateSignature() !== true) {
201
-					throw new \Exception('Could not validate CRL signature');
202
-				}
203
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
204
-				$revoked = $crl->getRevoked($csn);
205
-				if ($revoked !== false) {
206
-					throw new \Exception(
207
-						sprintf(
208
-							'Certificate "%s" has been revoked',
209
-							$csn
210
-						)
211
-					);
212
-				}
213
-
214
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
215
-				if ($certificate->validateSignature() !== true) {
216
-					throw new \Exception(
217
-						sprintf(
218
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
219
-							$appId
220
-						)
221
-					);
222
-				}
223
-
224
-				// Verify if the certificate is issued for the requested app id
225
-				$certInfo = openssl_x509_parse($app['certificate']);
226
-				if (!isset($certInfo['subject']['CN'])) {
227
-					throw new \Exception(
228
-						sprintf(
229
-							'App with id %s has a cert with no CN',
230
-							$appId
231
-						)
232
-					);
233
-				}
234
-				if ($certInfo['subject']['CN'] !== $appId) {
235
-					throw new \Exception(
236
-						sprintf(
237
-							'App with id %s has a cert issued to %s',
238
-							$appId,
239
-							$certInfo['subject']['CN']
240
-						)
241
-					);
242
-				}
243
-
244
-				// Download the release
245
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
246
-				if ($tempFile === false) {
247
-					throw new \RuntimeException('Could not create temporary file for downloading app archive.');
248
-				}
249
-
250
-				$timeout = $this->isCLI ? 0 : 120;
251
-				$client = $this->clientService->newClient();
252
-				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
253
-
254
-				// Check if the signature actually matches the downloaded content
255
-				$certificate = openssl_get_publickey($app['certificate']);
256
-				$verified = openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512) === 1;
257
-
258
-				if ($verified === true) {
259
-					// Seems to match, let's proceed
260
-					$extractDir = $this->tempManager->getTemporaryFolder();
261
-					if ($extractDir === false) {
262
-						throw new \RuntimeException('Could not create temporary directory for unpacking app.');
263
-					}
264
-
265
-					$archive = new TAR($tempFile);
266
-					if (!$archive->extract($extractDir)) {
267
-						$errorMessage = 'Could not extract app ' . $appId;
268
-
269
-						$archiveError = $archive->getError();
270
-						if ($archiveError instanceof \PEAR_Error) {
271
-							$errorMessage .= ': ' . $archiveError->getMessage();
272
-						}
273
-
274
-						throw new \Exception($errorMessage);
275
-					}
276
-					$allFiles = scandir($extractDir);
277
-					$folders = array_diff($allFiles, ['.', '..']);
278
-					$folders = array_values($folders);
279
-
280
-					if (count($folders) < 1) {
281
-						throw new \Exception(
282
-							sprintf(
283
-								'Extracted app %s has no folders',
284
-								$appId
285
-							)
286
-						);
287
-					}
288
-
289
-					if (count($folders) > 1) {
290
-						throw new \Exception(
291
-							sprintf(
292
-								'Extracted app %s has more than 1 folder',
293
-								$appId
294
-							)
295
-						);
296
-					}
297
-
298
-					// Check if appinfo/info.xml has the same app ID as well
299
-					$xml = simplexml_load_string(file_get_contents($extractDir . '/' . $folders[0] . '/appinfo/info.xml'));
300
-
301
-					if ($xml === false) {
302
-						throw new \Exception(
303
-							sprintf(
304
-								'Failed to load info.xml for app id %s',
305
-								$appId,
306
-							)
307
-						);
308
-					}
309
-
310
-					if ((string)$xml->id !== $appId) {
311
-						throw new \Exception(
312
-							sprintf(
313
-								'App for id %s has a wrong app ID in info.xml: %s',
314
-								$appId,
315
-								(string)$xml->id
316
-							)
317
-						);
318
-					}
319
-
320
-					// Check if the version is lower than before
321
-					$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
322
-					$newVersion = (string)$xml->version;
323
-					if (version_compare($currentVersion, $newVersion) === 1) {
324
-						throw new \Exception(
325
-							sprintf(
326
-								'App for id %s has version %s and tried to update to lower version %s',
327
-								$appId,
328
-								$currentVersion,
329
-								$newVersion
330
-							)
331
-						);
332
-					}
333
-
334
-					$baseDir = OC_App::getInstallPath() . '/' . $appId;
335
-					// Remove old app with the ID if existent
336
-					Files::rmdirr($baseDir);
337
-					// Move to app folder
338
-					if (@mkdir($baseDir)) {
339
-						$extractDir .= '/' . $folders[0];
340
-					}
341
-					// otherwise we just copy the outer directory
342
-					$this->copyRecursive($extractDir, $baseDir);
343
-					Files::rmdirr($extractDir);
344
-					return;
345
-				}
346
-				// Signature does not match
347
-				throw new \Exception(
348
-					sprintf(
349
-						'App with id %s has invalid signature',
350
-						$appId
351
-					)
352
-				);
353
-			}
354
-		}
355
-
356
-		throw new \Exception(
357
-			sprintf(
358
-				'Could not download app %s',
359
-				$appId
360
-			)
361
-		);
362
-	}
363
-
364
-	/**
365
-	 * Check if an update for the app is available
366
-	 *
367
-	 * @param string $appId
368
-	 * @param bool $allowUnstable
369
-	 * @return string|false false or the version number of the update
370
-	 */
371
-	public function isUpdateAvailable($appId, $allowUnstable = false): string|false {
372
-		if ($this->isInstanceReadyForUpdates === null) {
373
-			$installPath = OC_App::getInstallPath();
374
-			if ($installPath === null) {
375
-				$this->isInstanceReadyForUpdates = false;
376
-			} else {
377
-				$this->isInstanceReadyForUpdates = true;
378
-			}
379
-		}
380
-
381
-		if ($this->isInstanceReadyForUpdates === false) {
382
-			return false;
383
-		}
384
-
385
-		if ($this->isInstalledFromGit($appId) === true) {
386
-			return false;
387
-		}
388
-
389
-		if ($this->apps === null) {
390
-			$this->apps = $this->appFetcher->get($allowUnstable);
391
-		}
392
-
393
-		foreach ($this->apps as $app) {
394
-			if ($app['id'] === $appId) {
395
-				$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
396
-
397
-				if (!isset($app['releases'][0]['version'])) {
398
-					return false;
399
-				}
400
-				$newestVersion = $app['releases'][0]['version'];
401
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
402
-					return $newestVersion;
403
-				} else {
404
-					return false;
405
-				}
406
-			}
407
-		}
408
-
409
-		return false;
410
-	}
411
-
412
-	/**
413
-	 * Check if app has been installed from git
414
-	 *
415
-	 * The function will check if the path contains a .git folder
416
-	 */
417
-	private function isInstalledFromGit(string $appId): bool {
418
-		$app = \OC_App::findAppInDirectories($appId);
419
-		if ($app === false) {
420
-			return false;
421
-		}
422
-		$basedir = $app['path'] . '/' . $appId;
423
-		return file_exists($basedir . '/.git/');
424
-	}
425
-
426
-	/**
427
-	 * Check if app is already downloaded
428
-	 *
429
-	 * The function will check if the app is already downloaded in the apps repository
430
-	 */
431
-	public function isDownloaded(string $name): bool {
432
-		foreach (\OC::$APPSROOTS as $dir) {
433
-			$dirToTest = $dir['path'];
434
-			$dirToTest .= '/';
435
-			$dirToTest .= $name;
436
-			$dirToTest .= '/';
437
-
438
-			if (is_dir($dirToTest)) {
439
-				return true;
440
-			}
441
-		}
442
-
443
-		return false;
444
-	}
445
-
446
-	/**
447
-	 * Removes an app
448
-	 *
449
-	 * This function works as follows
450
-	 *   -# call uninstall repair steps
451
-	 *   -# removing the files
452
-	 *
453
-	 * The function will not delete preferences, tables and the configuration,
454
-	 * this has to be done by the function oc_app_uninstall().
455
-	 */
456
-	public function removeApp(string $appId): bool {
457
-		if ($this->isDownloaded($appId)) {
458
-			if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) {
459
-				return false;
460
-			}
461
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
462
-			Files::rmdirr($appDir);
463
-			return true;
464
-		} else {
465
-			$this->logger->error('can\'t remove app ' . $appId . '. It is not installed.');
466
-
467
-			return false;
468
-		}
469
-	}
470
-
471
-	/**
472
-	 * Installs the app within the bundle and marks the bundle as installed
473
-	 *
474
-	 * @throws \Exception If app could not get installed
475
-	 */
476
-	public function installAppBundle(Bundle $bundle): void {
477
-		$appIds = $bundle->getAppIdentifiers();
478
-		foreach ($appIds as $appId) {
479
-			if (!$this->isDownloaded($appId)) {
480
-				$this->downloadApp($appId);
481
-			}
482
-			$this->installApp($appId);
483
-			$app = new OC_App();
484
-			$app->enable($appId);
485
-		}
486
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
487
-		$bundles[] = $bundle->getIdentifier();
488
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
489
-	}
490
-
491
-	/**
492
-	 * Installs shipped apps
493
-	 *
494
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
495
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
496
-	 *                         working ownCloud at the end instead of an aborted update.
497
-	 * @return array Array of error messages (appid => Exception)
498
-	 */
499
-	public static function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array {
500
-		if ($output instanceof IOutput) {
501
-			$output->debug('Installing shipped apps');
502
-		}
503
-		$appManager = \OCP\Server::get(IAppManager::class);
504
-		$config = \OCP\Server::get(IConfig::class);
505
-		$errors = [];
506
-		foreach (\OC::$APPSROOTS as $app_dir) {
507
-			if ($dir = opendir($app_dir['path'])) {
508
-				while (false !== ($filename = readdir($dir))) {
509
-					if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
510
-						if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
511
-							if ($config->getAppValue($filename, 'installed_version', null) === null) {
512
-								$enabled = $appManager->isDefaultEnabled($filename);
513
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
514
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
515
-									if ($softErrors) {
516
-										try {
517
-											Installer::installShippedApp($filename, $output);
518
-										} catch (HintException $e) {
519
-											if ($e->getPrevious() instanceof TableExistsException) {
520
-												$errors[$filename] = $e;
521
-												continue;
522
-											}
523
-											throw $e;
524
-										}
525
-									} else {
526
-										Installer::installShippedApp($filename, $output);
527
-									}
528
-									$config->setAppValue($filename, 'enabled', 'yes');
529
-								}
530
-							}
531
-						}
532
-					}
533
-				}
534
-				closedir($dir);
535
-			}
536
-		}
537
-
538
-		return $errors;
539
-	}
540
-
541
-	/**
542
-	 * install an app already placed in the app folder
543
-	 */
544
-	public static function installShippedApp(string $app, ?IOutput $output = null): string|false {
545
-		if ($output instanceof IOutput) {
546
-			$output->debug('Installing ' . $app);
547
-		}
548
-
549
-		$appManager = \OCP\Server::get(IAppManager::class);
550
-		$config = \OCP\Server::get(IConfig::class);
551
-
552
-		$appPath = $appManager->getAppPath($app);
553
-		\OC_App::registerAutoloading($app, $appPath);
554
-
555
-		$ms = new MigrationService($app, \OCP\Server::get(Connection::class));
556
-		if ($output instanceof IOutput) {
557
-			$ms->setOutput($output);
558
-		}
559
-		$previousVersion = $config->getAppValue($app, 'installed_version', false);
560
-		$ms->migrate('latest', !$previousVersion);
561
-
562
-		//run appinfo/install.php
563
-		self::includeAppScript("$appPath/appinfo/install.php");
564
-
565
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
566
-		if (is_null($info)) {
567
-			return false;
568
-		}
569
-		if ($output instanceof IOutput) {
570
-			$output->debug('Registering tasks of ' . $app);
571
-		}
572
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
573
-
574
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
575
-
576
-		$config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
577
-		if (array_key_exists('ocsid', $info)) {
578
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
579
-		}
580
-
581
-		//set remote/public handlers
582
-		foreach ($info['remote'] as $name => $path) {
583
-			$config->setAppValue('core', 'remote_' . $name, $app . '/' . $path);
584
-		}
585
-		foreach ($info['public'] as $name => $path) {
586
-			$config->setAppValue('core', 'public_' . $name, $app . '/' . $path);
587
-		}
588
-
589
-		OC_App::setAppTypes($info['id']);
590
-
591
-		return $info['id'];
592
-	}
593
-
594
-	private static function includeAppScript(string $script): void {
595
-		if (file_exists($script)) {
596
-			include $script;
597
-		}
598
-	}
599
-
600
-	/**
601
-	 * Recursive copying of local folders.
602
-	 *
603
-	 * @param string $src source folder
604
-	 * @param string $dest target folder
605
-	 */
606
-	private function copyRecursive(string $src, string $dest): void {
607
-		if (!file_exists($src)) {
608
-			return;
609
-		}
610
-
611
-		if (is_dir($src)) {
612
-			if (!is_dir($dest)) {
613
-				mkdir($dest);
614
-			}
615
-			$files = scandir($src);
616
-			foreach ($files as $file) {
617
-				if ($file != '.' && $file != '..') {
618
-					$this->copyRecursive("$src/$file", "$dest/$file");
619
-				}
620
-			}
621
-		} else {
622
-			$validator = Server::get(FilenameValidator::class);
623
-			if (!$validator->isForbidden($src)) {
624
-				copy($src, $dest);
625
-			}
626
-		}
627
-	}
36
+    private ?bool $isInstanceReadyForUpdates = null;
37
+    private ?array $apps = null;
38
+
39
+    public function __construct(
40
+        private AppFetcher $appFetcher,
41
+        private IClientService $clientService,
42
+        private ITempManager $tempManager,
43
+        private LoggerInterface $logger,
44
+        private IConfig $config,
45
+        private bool $isCLI,
46
+    ) {
47
+    }
48
+
49
+    /**
50
+     * Installs an app that is located in one of the app folders already
51
+     *
52
+     * @param string $appId App to install
53
+     * @param bool $forceEnable
54
+     * @throws \Exception
55
+     * @return string app ID
56
+     */
57
+    public function installApp(string $appId, bool $forceEnable = false): string {
58
+        $app = \OC_App::findAppInDirectories($appId);
59
+        if ($app === false) {
60
+            throw new \Exception('App not found in any app directory');
61
+        }
62
+
63
+        $basedir = $app['path'] . '/' . $appId;
64
+
65
+        if (is_file($basedir . '/appinfo/database.xml')) {
66
+            throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
67
+        }
68
+
69
+        $l = \OCP\Util::getL10N('core');
70
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode());
71
+
72
+        if (!is_array($info)) {
73
+            throw new \Exception(
74
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
75
+                    [$appId]
76
+                )
77
+            );
78
+        }
79
+
80
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
81
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
82
+
83
+        $version = implode('.', \OCP\Util::getVersion());
84
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
85
+            throw new \Exception(
86
+                // TODO $l
87
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
88
+                    [$info['name']]
89
+                )
90
+            );
91
+        }
92
+
93
+        // check for required dependencies
94
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
95
+        /** @var Coordinator $coordinator */
96
+        $coordinator = \OC::$server->get(Coordinator::class);
97
+        $coordinator->runLazyRegistration($appId);
98
+        \OC_App::registerAutoloading($appId, $basedir);
99
+
100
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
101
+        if ($previousVersion) {
102
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
103
+        }
104
+
105
+        //install the database
106
+        $ms = new MigrationService($info['id'], \OCP\Server::get(Connection::class));
107
+        $ms->migrate('latest', !$previousVersion);
108
+
109
+        if ($previousVersion) {
110
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
111
+        }
112
+
113
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
114
+
115
+        //run appinfo/install.php
116
+        self::includeAppScript($basedir . '/appinfo/install.php');
117
+
118
+        OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
119
+
120
+        $config = \OCP\Server::get(IConfig::class);
121
+        //set the installed version
122
+        $config->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
123
+        $config->setAppValue($info['id'], 'enabled', 'no');
124
+
125
+        //set remote/public handlers
126
+        foreach ($info['remote'] as $name => $path) {
127
+            $config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path);
128
+        }
129
+        foreach ($info['public'] as $name => $path) {
130
+            $config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path);
131
+        }
132
+
133
+        OC_App::setAppTypes($info['id']);
134
+
135
+        return $info['id'];
136
+    }
137
+
138
+    /**
139
+     * Updates the specified app from the appstore
140
+     *
141
+     * @param bool $allowUnstable Allow unstable releases
142
+     */
143
+    public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool {
144
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
145
+            try {
146
+                $this->downloadApp($appId, $allowUnstable);
147
+            } catch (\Exception $e) {
148
+                $this->logger->error($e->getMessage(), [
149
+                    'exception' => $e,
150
+                ]);
151
+                return false;
152
+            }
153
+            return OC_App::updateApp($appId);
154
+        }
155
+
156
+        return false;
157
+    }
158
+
159
+    /**
160
+     * Split the certificate file in individual certs
161
+     *
162
+     * @param string $cert
163
+     * @return string[]
164
+     */
165
+    private function splitCerts(string $cert): array {
166
+        preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
167
+
168
+        return $matches[0];
169
+    }
170
+
171
+    /**
172
+     * Downloads an app and puts it into the app directory
173
+     *
174
+     * @param string $appId
175
+     * @param bool [$allowUnstable]
176
+     *
177
+     * @throws \Exception If the installation was not successful
178
+     */
179
+    public function downloadApp(string $appId, bool $allowUnstable = false): void {
180
+        $appId = strtolower($appId);
181
+
182
+        $apps = $this->appFetcher->get($allowUnstable);
183
+        foreach ($apps as $app) {
184
+            if ($app['id'] === $appId) {
185
+                // Load the certificate
186
+                $certificate = new X509();
187
+                $rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
188
+                $rootCrts = $this->splitCerts($rootCrt);
189
+                foreach ($rootCrts as $rootCrt) {
190
+                    $certificate->loadCA($rootCrt);
191
+                }
192
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
193
+
194
+                // Verify if the certificate has been revoked
195
+                $crl = new X509();
196
+                foreach ($rootCrts as $rootCrt) {
197
+                    $crl->loadCA($rootCrt);
198
+                }
199
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
200
+                if ($crl->validateSignature() !== true) {
201
+                    throw new \Exception('Could not validate CRL signature');
202
+                }
203
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
204
+                $revoked = $crl->getRevoked($csn);
205
+                if ($revoked !== false) {
206
+                    throw new \Exception(
207
+                        sprintf(
208
+                            'Certificate "%s" has been revoked',
209
+                            $csn
210
+                        )
211
+                    );
212
+                }
213
+
214
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
215
+                if ($certificate->validateSignature() !== true) {
216
+                    throw new \Exception(
217
+                        sprintf(
218
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
219
+                            $appId
220
+                        )
221
+                    );
222
+                }
223
+
224
+                // Verify if the certificate is issued for the requested app id
225
+                $certInfo = openssl_x509_parse($app['certificate']);
226
+                if (!isset($certInfo['subject']['CN'])) {
227
+                    throw new \Exception(
228
+                        sprintf(
229
+                            'App with id %s has a cert with no CN',
230
+                            $appId
231
+                        )
232
+                    );
233
+                }
234
+                if ($certInfo['subject']['CN'] !== $appId) {
235
+                    throw new \Exception(
236
+                        sprintf(
237
+                            'App with id %s has a cert issued to %s',
238
+                            $appId,
239
+                            $certInfo['subject']['CN']
240
+                        )
241
+                    );
242
+                }
243
+
244
+                // Download the release
245
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
246
+                if ($tempFile === false) {
247
+                    throw new \RuntimeException('Could not create temporary file for downloading app archive.');
248
+                }
249
+
250
+                $timeout = $this->isCLI ? 0 : 120;
251
+                $client = $this->clientService->newClient();
252
+                $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
253
+
254
+                // Check if the signature actually matches the downloaded content
255
+                $certificate = openssl_get_publickey($app['certificate']);
256
+                $verified = openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512) === 1;
257
+
258
+                if ($verified === true) {
259
+                    // Seems to match, let's proceed
260
+                    $extractDir = $this->tempManager->getTemporaryFolder();
261
+                    if ($extractDir === false) {
262
+                        throw new \RuntimeException('Could not create temporary directory for unpacking app.');
263
+                    }
264
+
265
+                    $archive = new TAR($tempFile);
266
+                    if (!$archive->extract($extractDir)) {
267
+                        $errorMessage = 'Could not extract app ' . $appId;
268
+
269
+                        $archiveError = $archive->getError();
270
+                        if ($archiveError instanceof \PEAR_Error) {
271
+                            $errorMessage .= ': ' . $archiveError->getMessage();
272
+                        }
273
+
274
+                        throw new \Exception($errorMessage);
275
+                    }
276
+                    $allFiles = scandir($extractDir);
277
+                    $folders = array_diff($allFiles, ['.', '..']);
278
+                    $folders = array_values($folders);
279
+
280
+                    if (count($folders) < 1) {
281
+                        throw new \Exception(
282
+                            sprintf(
283
+                                'Extracted app %s has no folders',
284
+                                $appId
285
+                            )
286
+                        );
287
+                    }
288
+
289
+                    if (count($folders) > 1) {
290
+                        throw new \Exception(
291
+                            sprintf(
292
+                                'Extracted app %s has more than 1 folder',
293
+                                $appId
294
+                            )
295
+                        );
296
+                    }
297
+
298
+                    // Check if appinfo/info.xml has the same app ID as well
299
+                    $xml = simplexml_load_string(file_get_contents($extractDir . '/' . $folders[0] . '/appinfo/info.xml'));
300
+
301
+                    if ($xml === false) {
302
+                        throw new \Exception(
303
+                            sprintf(
304
+                                'Failed to load info.xml for app id %s',
305
+                                $appId,
306
+                            )
307
+                        );
308
+                    }
309
+
310
+                    if ((string)$xml->id !== $appId) {
311
+                        throw new \Exception(
312
+                            sprintf(
313
+                                'App for id %s has a wrong app ID in info.xml: %s',
314
+                                $appId,
315
+                                (string)$xml->id
316
+                            )
317
+                        );
318
+                    }
319
+
320
+                    // Check if the version is lower than before
321
+                    $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
322
+                    $newVersion = (string)$xml->version;
323
+                    if (version_compare($currentVersion, $newVersion) === 1) {
324
+                        throw new \Exception(
325
+                            sprintf(
326
+                                'App for id %s has version %s and tried to update to lower version %s',
327
+                                $appId,
328
+                                $currentVersion,
329
+                                $newVersion
330
+                            )
331
+                        );
332
+                    }
333
+
334
+                    $baseDir = OC_App::getInstallPath() . '/' . $appId;
335
+                    // Remove old app with the ID if existent
336
+                    Files::rmdirr($baseDir);
337
+                    // Move to app folder
338
+                    if (@mkdir($baseDir)) {
339
+                        $extractDir .= '/' . $folders[0];
340
+                    }
341
+                    // otherwise we just copy the outer directory
342
+                    $this->copyRecursive($extractDir, $baseDir);
343
+                    Files::rmdirr($extractDir);
344
+                    return;
345
+                }
346
+                // Signature does not match
347
+                throw new \Exception(
348
+                    sprintf(
349
+                        'App with id %s has invalid signature',
350
+                        $appId
351
+                    )
352
+                );
353
+            }
354
+        }
355
+
356
+        throw new \Exception(
357
+            sprintf(
358
+                'Could not download app %s',
359
+                $appId
360
+            )
361
+        );
362
+    }
363
+
364
+    /**
365
+     * Check if an update for the app is available
366
+     *
367
+     * @param string $appId
368
+     * @param bool $allowUnstable
369
+     * @return string|false false or the version number of the update
370
+     */
371
+    public function isUpdateAvailable($appId, $allowUnstable = false): string|false {
372
+        if ($this->isInstanceReadyForUpdates === null) {
373
+            $installPath = OC_App::getInstallPath();
374
+            if ($installPath === null) {
375
+                $this->isInstanceReadyForUpdates = false;
376
+            } else {
377
+                $this->isInstanceReadyForUpdates = true;
378
+            }
379
+        }
380
+
381
+        if ($this->isInstanceReadyForUpdates === false) {
382
+            return false;
383
+        }
384
+
385
+        if ($this->isInstalledFromGit($appId) === true) {
386
+            return false;
387
+        }
388
+
389
+        if ($this->apps === null) {
390
+            $this->apps = $this->appFetcher->get($allowUnstable);
391
+        }
392
+
393
+        foreach ($this->apps as $app) {
394
+            if ($app['id'] === $appId) {
395
+                $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
396
+
397
+                if (!isset($app['releases'][0]['version'])) {
398
+                    return false;
399
+                }
400
+                $newestVersion = $app['releases'][0]['version'];
401
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
402
+                    return $newestVersion;
403
+                } else {
404
+                    return false;
405
+                }
406
+            }
407
+        }
408
+
409
+        return false;
410
+    }
411
+
412
+    /**
413
+     * Check if app has been installed from git
414
+     *
415
+     * The function will check if the path contains a .git folder
416
+     */
417
+    private function isInstalledFromGit(string $appId): bool {
418
+        $app = \OC_App::findAppInDirectories($appId);
419
+        if ($app === false) {
420
+            return false;
421
+        }
422
+        $basedir = $app['path'] . '/' . $appId;
423
+        return file_exists($basedir . '/.git/');
424
+    }
425
+
426
+    /**
427
+     * Check if app is already downloaded
428
+     *
429
+     * The function will check if the app is already downloaded in the apps repository
430
+     */
431
+    public function isDownloaded(string $name): bool {
432
+        foreach (\OC::$APPSROOTS as $dir) {
433
+            $dirToTest = $dir['path'];
434
+            $dirToTest .= '/';
435
+            $dirToTest .= $name;
436
+            $dirToTest .= '/';
437
+
438
+            if (is_dir($dirToTest)) {
439
+                return true;
440
+            }
441
+        }
442
+
443
+        return false;
444
+    }
445
+
446
+    /**
447
+     * Removes an app
448
+     *
449
+     * This function works as follows
450
+     *   -# call uninstall repair steps
451
+     *   -# removing the files
452
+     *
453
+     * The function will not delete preferences, tables and the configuration,
454
+     * this has to be done by the function oc_app_uninstall().
455
+     */
456
+    public function removeApp(string $appId): bool {
457
+        if ($this->isDownloaded($appId)) {
458
+            if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) {
459
+                return false;
460
+            }
461
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
462
+            Files::rmdirr($appDir);
463
+            return true;
464
+        } else {
465
+            $this->logger->error('can\'t remove app ' . $appId . '. It is not installed.');
466
+
467
+            return false;
468
+        }
469
+    }
470
+
471
+    /**
472
+     * Installs the app within the bundle and marks the bundle as installed
473
+     *
474
+     * @throws \Exception If app could not get installed
475
+     */
476
+    public function installAppBundle(Bundle $bundle): void {
477
+        $appIds = $bundle->getAppIdentifiers();
478
+        foreach ($appIds as $appId) {
479
+            if (!$this->isDownloaded($appId)) {
480
+                $this->downloadApp($appId);
481
+            }
482
+            $this->installApp($appId);
483
+            $app = new OC_App();
484
+            $app->enable($appId);
485
+        }
486
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
487
+        $bundles[] = $bundle->getIdentifier();
488
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
489
+    }
490
+
491
+    /**
492
+     * Installs shipped apps
493
+     *
494
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
495
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
496
+     *                         working ownCloud at the end instead of an aborted update.
497
+     * @return array Array of error messages (appid => Exception)
498
+     */
499
+    public static function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array {
500
+        if ($output instanceof IOutput) {
501
+            $output->debug('Installing shipped apps');
502
+        }
503
+        $appManager = \OCP\Server::get(IAppManager::class);
504
+        $config = \OCP\Server::get(IConfig::class);
505
+        $errors = [];
506
+        foreach (\OC::$APPSROOTS as $app_dir) {
507
+            if ($dir = opendir($app_dir['path'])) {
508
+                while (false !== ($filename = readdir($dir))) {
509
+                    if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
510
+                        if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
511
+                            if ($config->getAppValue($filename, 'installed_version', null) === null) {
512
+                                $enabled = $appManager->isDefaultEnabled($filename);
513
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
514
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
515
+                                    if ($softErrors) {
516
+                                        try {
517
+                                            Installer::installShippedApp($filename, $output);
518
+                                        } catch (HintException $e) {
519
+                                            if ($e->getPrevious() instanceof TableExistsException) {
520
+                                                $errors[$filename] = $e;
521
+                                                continue;
522
+                                            }
523
+                                            throw $e;
524
+                                        }
525
+                                    } else {
526
+                                        Installer::installShippedApp($filename, $output);
527
+                                    }
528
+                                    $config->setAppValue($filename, 'enabled', 'yes');
529
+                                }
530
+                            }
531
+                        }
532
+                    }
533
+                }
534
+                closedir($dir);
535
+            }
536
+        }
537
+
538
+        return $errors;
539
+    }
540
+
541
+    /**
542
+     * install an app already placed in the app folder
543
+     */
544
+    public static function installShippedApp(string $app, ?IOutput $output = null): string|false {
545
+        if ($output instanceof IOutput) {
546
+            $output->debug('Installing ' . $app);
547
+        }
548
+
549
+        $appManager = \OCP\Server::get(IAppManager::class);
550
+        $config = \OCP\Server::get(IConfig::class);
551
+
552
+        $appPath = $appManager->getAppPath($app);
553
+        \OC_App::registerAutoloading($app, $appPath);
554
+
555
+        $ms = new MigrationService($app, \OCP\Server::get(Connection::class));
556
+        if ($output instanceof IOutput) {
557
+            $ms->setOutput($output);
558
+        }
559
+        $previousVersion = $config->getAppValue($app, 'installed_version', false);
560
+        $ms->migrate('latest', !$previousVersion);
561
+
562
+        //run appinfo/install.php
563
+        self::includeAppScript("$appPath/appinfo/install.php");
564
+
565
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
566
+        if (is_null($info)) {
567
+            return false;
568
+        }
569
+        if ($output instanceof IOutput) {
570
+            $output->debug('Registering tasks of ' . $app);
571
+        }
572
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
573
+
574
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
575
+
576
+        $config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
577
+        if (array_key_exists('ocsid', $info)) {
578
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
579
+        }
580
+
581
+        //set remote/public handlers
582
+        foreach ($info['remote'] as $name => $path) {
583
+            $config->setAppValue('core', 'remote_' . $name, $app . '/' . $path);
584
+        }
585
+        foreach ($info['public'] as $name => $path) {
586
+            $config->setAppValue('core', 'public_' . $name, $app . '/' . $path);
587
+        }
588
+
589
+        OC_App::setAppTypes($info['id']);
590
+
591
+        return $info['id'];
592
+    }
593
+
594
+    private static function includeAppScript(string $script): void {
595
+        if (file_exists($script)) {
596
+            include $script;
597
+        }
598
+    }
599
+
600
+    /**
601
+     * Recursive copying of local folders.
602
+     *
603
+     * @param string $src source folder
604
+     * @param string $dest target folder
605
+     */
606
+    private function copyRecursive(string $src, string $dest): void {
607
+        if (!file_exists($src)) {
608
+            return;
609
+        }
610
+
611
+        if (is_dir($src)) {
612
+            if (!is_dir($dest)) {
613
+                mkdir($dest);
614
+            }
615
+            $files = scandir($src);
616
+            foreach ($files as $file) {
617
+                if ($file != '.' && $file != '..') {
618
+                    $this->copyRecursive("$src/$file", "$dest/$file");
619
+                }
620
+            }
621
+        } else {
622
+            $validator = Server::get(FilenameValidator::class);
623
+            if (!$validator->isForbidden($src)) {
624
+                copy($src, $dest);
625
+            }
626
+        }
627
+    }
628 628
 }
Please login to merge, or discard this patch.