Passed
Push — master ( 63e7ac...1a3bb2 )
by Joas
17:16 queued 14s
created
apps/updatenotification/lib/Notification/Notifier.php 1 patch
Indentation   +163 added lines, -163 removed lines patch added patch discarded remove patch
@@ -40,167 +40,167 @@
 block discarded – undo
40 40
 use OCP\Util;
41 41
 
42 42
 class Notifier implements INotifier {
43
-	/** @var IURLGenerator */
44
-	protected $url;
45
-
46
-	/** @var IConfig */
47
-	protected $config;
48
-
49
-	/** @var IManager */
50
-	protected $notificationManager;
51
-
52
-	/** @var IFactory */
53
-	protected $l10NFactory;
54
-
55
-	/** @var IUserSession */
56
-	protected $userSession;
57
-
58
-	/** @var IGroupManager */
59
-	protected $groupManager;
60
-
61
-	/** @var string[] */
62
-	protected $appVersions;
63
-
64
-	/**
65
-	 * Notifier constructor.
66
-	 *
67
-	 * @param IURLGenerator $url
68
-	 * @param IConfig $config
69
-	 * @param IManager $notificationManager
70
-	 * @param IFactory $l10NFactory
71
-	 * @param IUserSession $userSession
72
-	 * @param IGroupManager $groupManager
73
-	 */
74
-	public function __construct(IURLGenerator $url, IConfig $config, IManager $notificationManager, IFactory $l10NFactory, IUserSession $userSession, IGroupManager $groupManager) {
75
-		$this->url = $url;
76
-		$this->notificationManager = $notificationManager;
77
-		$this->config = $config;
78
-		$this->l10NFactory = $l10NFactory;
79
-		$this->userSession = $userSession;
80
-		$this->groupManager = $groupManager;
81
-		$this->appVersions = $this->getAppVersions();
82
-	}
83
-
84
-	/**
85
-	 * Identifier of the notifier, only use [a-z0-9_]
86
-	 *
87
-	 * @return string
88
-	 * @since 17.0.0
89
-	 */
90
-	public function getID(): string {
91
-		return 'updatenotification';
92
-	}
93
-
94
-	/**
95
-	 * Human readable name describing the notifier
96
-	 *
97
-	 * @return string
98
-	 * @since 17.0.0
99
-	 */
100
-	public function getName(): string {
101
-		return $this->l10NFactory->get('updatenotification')->t('Update notifications');
102
-	}
103
-
104
-	/**
105
-	 * @param INotification $notification
106
-	 * @param string $languageCode The code of the language that should be used to prepare the notification
107
-	 * @return INotification
108
-	 * @throws \InvalidArgumentException When the notification was not prepared by a notifier
109
-	 * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted
110
-	 * @since 9.0.0
111
-	 */
112
-	public function prepare(INotification $notification, string $languageCode): INotification {
113
-		if ($notification->getApp() !== 'updatenotification') {
114
-			throw new \InvalidArgumentException('Unknown app id');
115
-		}
116
-
117
-		$l = $this->l10NFactory->get('updatenotification', $languageCode);
118
-		if ($notification->getSubject() === 'connection_error') {
119
-			$errors = (int) $this->config->getAppValue('updatenotification', 'update_check_errors', '0');
120
-			if ($errors === 0) {
121
-				$this->notificationManager->markProcessed($notification);
122
-				throw new \InvalidArgumentException('Update checked worked again');
123
-			}
124
-
125
-			$notification->setParsedSubject($l->t('The update server could not be reached since %d days to check for new updates.', [$errors]))
126
-				->setParsedMessage($l->t('Please check the Nextcloud and server log files for errors.'));
127
-		} elseif ($notification->getObjectType() === 'core') {
128
-			$this->updateAlreadyInstalledCheck($notification, $this->getCoreVersions());
129
-
130
-			$parameters = $notification->getSubjectParameters();
131
-			$notification->setParsedSubject($l->t('Update to %1$s is available.', [$parameters['version']]))
132
-				->setRichSubject($l->t('Update to {serverAndVersion} is available.'), [
133
-					'serverAndVersion' => [
134
-						'type' => 'highlight',
135
-						'id' => $notification->getObjectType(),
136
-						'name' => $parameters['version'],
137
-					]
138
-				]);
139
-
140
-			if ($this->isAdmin()) {
141
-				$notification->setLink($this->url->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'overview']) . '#version');
142
-			}
143
-		} else {
144
-			$appInfo = $this->getAppInfo($notification->getObjectType(), $languageCode);
145
-			$appName = ($appInfo === null) ? $notification->getObjectType() : $appInfo['name'];
146
-
147
-			if (isset($this->appVersions[$notification->getObjectType()])) {
148
-				$this->updateAlreadyInstalledCheck($notification, $this->appVersions[$notification->getObjectType()]);
149
-			}
150
-
151
-			$notification->setRichSubject($l->t('Update for {app} to version %s is available.', [$notification->getObjectId()]), [
152
-				'app' => [
153
-					'type' => 'app',
154
-					'id' => $notification->getObjectType(),
155
-					'name' => $appName,
156
-				]
157
-			]);
158
-
159
-			if ($this->isAdmin()) {
160
-				$notification->setLink($this->url->linkToRouteAbsolute('settings.AppSettings.viewApps', ['category' => 'updates']) . '#app-' . $notification->getObjectType());
161
-			}
162
-		}
163
-
164
-		$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('updatenotification', 'notification.svg')));
165
-
166
-		return $notification;
167
-	}
168
-
169
-	/**
170
-	 * Remove the notification and prevent rendering, when the update is installed
171
-	 *
172
-	 * @param INotification $notification
173
-	 * @param string $installedVersion
174
-	 * @throws AlreadyProcessedException When the update is already installed
175
-	 */
176
-	protected function updateAlreadyInstalledCheck(INotification $notification, $installedVersion) {
177
-		if (version_compare($notification->getObjectId(), $installedVersion, '<=')) {
178
-			throw new AlreadyProcessedException();
179
-		}
180
-	}
181
-
182
-	/**
183
-	 * @return bool
184
-	 */
185
-	protected function isAdmin(): bool {
186
-		$user = $this->userSession->getUser();
187
-
188
-		if ($user instanceof IUser) {
189
-			return $this->groupManager->isAdmin($user->getUID());
190
-		}
191
-
192
-		return false;
193
-	}
194
-
195
-	protected function getCoreVersions(): string {
196
-		return implode('.', Util::getVersion());
197
-	}
198
-
199
-	protected function getAppVersions(): array {
200
-		return \OC_App::getAppVersions();
201
-	}
202
-
203
-	protected function getAppInfo($appId, $languageCode) {
204
-		return \OCP\Server::get(IAppManager::class)->getAppInfo($appId, false, $languageCode);
205
-	}
43
+    /** @var IURLGenerator */
44
+    protected $url;
45
+
46
+    /** @var IConfig */
47
+    protected $config;
48
+
49
+    /** @var IManager */
50
+    protected $notificationManager;
51
+
52
+    /** @var IFactory */
53
+    protected $l10NFactory;
54
+
55
+    /** @var IUserSession */
56
+    protected $userSession;
57
+
58
+    /** @var IGroupManager */
59
+    protected $groupManager;
60
+
61
+    /** @var string[] */
62
+    protected $appVersions;
63
+
64
+    /**
65
+     * Notifier constructor.
66
+     *
67
+     * @param IURLGenerator $url
68
+     * @param IConfig $config
69
+     * @param IManager $notificationManager
70
+     * @param IFactory $l10NFactory
71
+     * @param IUserSession $userSession
72
+     * @param IGroupManager $groupManager
73
+     */
74
+    public function __construct(IURLGenerator $url, IConfig $config, IManager $notificationManager, IFactory $l10NFactory, IUserSession $userSession, IGroupManager $groupManager) {
75
+        $this->url = $url;
76
+        $this->notificationManager = $notificationManager;
77
+        $this->config = $config;
78
+        $this->l10NFactory = $l10NFactory;
79
+        $this->userSession = $userSession;
80
+        $this->groupManager = $groupManager;
81
+        $this->appVersions = $this->getAppVersions();
82
+    }
83
+
84
+    /**
85
+     * Identifier of the notifier, only use [a-z0-9_]
86
+     *
87
+     * @return string
88
+     * @since 17.0.0
89
+     */
90
+    public function getID(): string {
91
+        return 'updatenotification';
92
+    }
93
+
94
+    /**
95
+     * Human readable name describing the notifier
96
+     *
97
+     * @return string
98
+     * @since 17.0.0
99
+     */
100
+    public function getName(): string {
101
+        return $this->l10NFactory->get('updatenotification')->t('Update notifications');
102
+    }
103
+
104
+    /**
105
+     * @param INotification $notification
106
+     * @param string $languageCode The code of the language that should be used to prepare the notification
107
+     * @return INotification
108
+     * @throws \InvalidArgumentException When the notification was not prepared by a notifier
109
+     * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted
110
+     * @since 9.0.0
111
+     */
112
+    public function prepare(INotification $notification, string $languageCode): INotification {
113
+        if ($notification->getApp() !== 'updatenotification') {
114
+            throw new \InvalidArgumentException('Unknown app id');
115
+        }
116
+
117
+        $l = $this->l10NFactory->get('updatenotification', $languageCode);
118
+        if ($notification->getSubject() === 'connection_error') {
119
+            $errors = (int) $this->config->getAppValue('updatenotification', 'update_check_errors', '0');
120
+            if ($errors === 0) {
121
+                $this->notificationManager->markProcessed($notification);
122
+                throw new \InvalidArgumentException('Update checked worked again');
123
+            }
124
+
125
+            $notification->setParsedSubject($l->t('The update server could not be reached since %d days to check for new updates.', [$errors]))
126
+                ->setParsedMessage($l->t('Please check the Nextcloud and server log files for errors.'));
127
+        } elseif ($notification->getObjectType() === 'core') {
128
+            $this->updateAlreadyInstalledCheck($notification, $this->getCoreVersions());
129
+
130
+            $parameters = $notification->getSubjectParameters();
131
+            $notification->setParsedSubject($l->t('Update to %1$s is available.', [$parameters['version']]))
132
+                ->setRichSubject($l->t('Update to {serverAndVersion} is available.'), [
133
+                    'serverAndVersion' => [
134
+                        'type' => 'highlight',
135
+                        'id' => $notification->getObjectType(),
136
+                        'name' => $parameters['version'],
137
+                    ]
138
+                ]);
139
+
140
+            if ($this->isAdmin()) {
141
+                $notification->setLink($this->url->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'overview']) . '#version');
142
+            }
143
+        } else {
144
+            $appInfo = $this->getAppInfo($notification->getObjectType(), $languageCode);
145
+            $appName = ($appInfo === null) ? $notification->getObjectType() : $appInfo['name'];
146
+
147
+            if (isset($this->appVersions[$notification->getObjectType()])) {
148
+                $this->updateAlreadyInstalledCheck($notification, $this->appVersions[$notification->getObjectType()]);
149
+            }
150
+
151
+            $notification->setRichSubject($l->t('Update for {app} to version %s is available.', [$notification->getObjectId()]), [
152
+                'app' => [
153
+                    'type' => 'app',
154
+                    'id' => $notification->getObjectType(),
155
+                    'name' => $appName,
156
+                ]
157
+            ]);
158
+
159
+            if ($this->isAdmin()) {
160
+                $notification->setLink($this->url->linkToRouteAbsolute('settings.AppSettings.viewApps', ['category' => 'updates']) . '#app-' . $notification->getObjectType());
161
+            }
162
+        }
163
+
164
+        $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('updatenotification', 'notification.svg')));
165
+
166
+        return $notification;
167
+    }
168
+
169
+    /**
170
+     * Remove the notification and prevent rendering, when the update is installed
171
+     *
172
+     * @param INotification $notification
173
+     * @param string $installedVersion
174
+     * @throws AlreadyProcessedException When the update is already installed
175
+     */
176
+    protected function updateAlreadyInstalledCheck(INotification $notification, $installedVersion) {
177
+        if (version_compare($notification->getObjectId(), $installedVersion, '<=')) {
178
+            throw new AlreadyProcessedException();
179
+        }
180
+    }
181
+
182
+    /**
183
+     * @return bool
184
+     */
185
+    protected function isAdmin(): bool {
186
+        $user = $this->userSession->getUser();
187
+
188
+        if ($user instanceof IUser) {
189
+            return $this->groupManager->isAdmin($user->getUID());
190
+        }
191
+
192
+        return false;
193
+    }
194
+
195
+    protected function getCoreVersions(): string {
196
+        return implode('.', Util::getVersion());
197
+    }
198
+
199
+    protected function getAppVersions(): array {
200
+        return \OC_App::getAppVersions();
201
+    }
202
+
203
+    protected function getAppInfo($appId, $languageCode) {
204
+        return \OCP\Server::get(IAppManager::class)->getAppInfo($appId, false, $languageCode);
205
+    }
206 206
 }
Please login to merge, or discard this patch.
core/Command/App/Install.php 2 patches
Indentation   +58 added lines, -58 removed lines patch added patch discarded remove patch
@@ -36,69 +36,69 @@
 block discarded – undo
36 36
 use Symfony\Component\Console\Output\OutputInterface;
37 37
 
38 38
 class Install extends Command {
39
-	protected function configure() {
40
-		$this
41
-			->setName('app:install')
42
-			->setDescription('install an app')
43
-			->addArgument(
44
-				'app-id',
45
-				InputArgument::REQUIRED,
46
-				'install the specified app'
47
-			)
48
-			->addOption(
49
-				'keep-disabled',
50
-				null,
51
-				InputOption::VALUE_NONE,
52
-				'don\'t enable the app afterwards'
53
-			)
54
-			->addOption(
55
-				'force',
56
-				'f',
57
-				InputOption::VALUE_NONE,
58
-				'install the app regardless of the Nextcloud version requirement'
59
-			)
60
-			->addOption(
61
-				'allow-unstable',
62
-				null,
63
-				InputOption::VALUE_NONE,
64
-				'allow installing an unstable releases'
65
-			)
66
-		;
67
-	}
39
+    protected function configure() {
40
+        $this
41
+            ->setName('app:install')
42
+            ->setDescription('install an app')
43
+            ->addArgument(
44
+                'app-id',
45
+                InputArgument::REQUIRED,
46
+                'install the specified app'
47
+            )
48
+            ->addOption(
49
+                'keep-disabled',
50
+                null,
51
+                InputOption::VALUE_NONE,
52
+                'don\'t enable the app afterwards'
53
+            )
54
+            ->addOption(
55
+                'force',
56
+                'f',
57
+                InputOption::VALUE_NONE,
58
+                'install the app regardless of the Nextcloud version requirement'
59
+            )
60
+            ->addOption(
61
+                'allow-unstable',
62
+                null,
63
+                InputOption::VALUE_NONE,
64
+                'allow installing an unstable releases'
65
+            )
66
+        ;
67
+    }
68 68
 
69
-	protected function execute(InputInterface $input, OutputInterface $output): int {
70
-		$appId = $input->getArgument('app-id');
71
-		$forceEnable = (bool) $input->getOption('force');
69
+    protected function execute(InputInterface $input, OutputInterface $output): int {
70
+        $appId = $input->getArgument('app-id');
71
+        $forceEnable = (bool) $input->getOption('force');
72 72
 
73
-		if (\OC_App::getAppPath($appId)) {
74
-			$output->writeln($appId . ' already installed');
75
-			return 1;
76
-		}
73
+        if (\OC_App::getAppPath($appId)) {
74
+            $output->writeln($appId . ' already installed');
75
+            return 1;
76
+        }
77 77
 
78
-		try {
79
-			/** @var Installer $installer */
80
-			$installer = \OC::$server->query(Installer::class);
81
-			$installer->downloadApp($appId, $input->getOption('allow-unstable'));
82
-			$result = $installer->installApp($appId, $forceEnable);
83
-		} catch (\Exception $e) {
84
-			$output->writeln('Error: ' . $e->getMessage());
85
-			return 1;
86
-		}
78
+        try {
79
+            /** @var Installer $installer */
80
+            $installer = \OC::$server->query(Installer::class);
81
+            $installer->downloadApp($appId, $input->getOption('allow-unstable'));
82
+            $result = $installer->installApp($appId, $forceEnable);
83
+        } catch (\Exception $e) {
84
+            $output->writeln('Error: ' . $e->getMessage());
85
+            return 1;
86
+        }
87 87
 
88
-		if ($result === false) {
89
-			$output->writeln($appId . ' couldn\'t be installed');
90
-			return 1;
91
-		}
88
+        if ($result === false) {
89
+            $output->writeln($appId . ' couldn\'t be installed');
90
+            return 1;
91
+        }
92 92
 
93
-		$appVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId);
94
-		$output->writeln($appId . ' ' . $appVersion . ' installed');
93
+        $appVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId);
94
+        $output->writeln($appId . ' ' . $appVersion . ' installed');
95 95
 
96
-		if (!$input->getOption('keep-disabled')) {
97
-			$appClass = new \OC_App();
98
-			$appClass->enable($appId);
99
-			$output->writeln($appId . ' enabled');
100
-		}
96
+        if (!$input->getOption('keep-disabled')) {
97
+            $appClass = new \OC_App();
98
+            $appClass->enable($appId);
99
+            $output->writeln($appId . ' enabled');
100
+        }
101 101
 
102
-		return 0;
103
-	}
102
+        return 0;
103
+    }
104 104
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -71,7 +71,7 @@  discard block
 block discarded – undo
71 71
 		$forceEnable = (bool) $input->getOption('force');
72 72
 
73 73
 		if (\OC_App::getAppPath($appId)) {
74
-			$output->writeln($appId . ' already installed');
74
+			$output->writeln($appId.' already installed');
75 75
 			return 1;
76 76
 		}
77 77
 
@@ -81,22 +81,22 @@  discard block
 block discarded – undo
81 81
 			$installer->downloadApp($appId, $input->getOption('allow-unstable'));
82 82
 			$result = $installer->installApp($appId, $forceEnable);
83 83
 		} catch (\Exception $e) {
84
-			$output->writeln('Error: ' . $e->getMessage());
84
+			$output->writeln('Error: '.$e->getMessage());
85 85
 			return 1;
86 86
 		}
87 87
 
88 88
 		if ($result === false) {
89
-			$output->writeln($appId . ' couldn\'t be installed');
89
+			$output->writeln($appId.' couldn\'t be installed');
90 90
 			return 1;
91 91
 		}
92 92
 
93 93
 		$appVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId);
94
-		$output->writeln($appId . ' ' . $appVersion . ' installed');
94
+		$output->writeln($appId.' '.$appVersion.' installed');
95 95
 
96 96
 		if (!$input->getOption('keep-disabled')) {
97 97
 			$appClass = new \OC_App();
98 98
 			$appClass->enable($appId);
99
-			$output->writeln($appId . ' enabled');
99
+			$output->writeln($appId.' enabled');
100 100
 		}
101 101
 
102 102
 		return 0;
Please login to merge, or discard this patch.
core/Command/App/Remove.php 2 patches
Indentation   +91 added lines, -91 removed lines patch added patch discarded remove patch
@@ -39,107 +39,107 @@
 block discarded – undo
39 39
 use Throwable;
40 40
 
41 41
 class Remove extends Command implements CompletionAwareInterface {
42
-	protected IAppManager $manager;
43
-	private Installer $installer;
44
-	private LoggerInterface $logger;
42
+    protected IAppManager $manager;
43
+    private Installer $installer;
44
+    private LoggerInterface $logger;
45 45
 
46
-	public function __construct(IAppManager $manager, Installer $installer, LoggerInterface $logger) {
47
-		parent::__construct();
48
-		$this->manager = $manager;
49
-		$this->installer = $installer;
50
-		$this->logger = $logger;
51
-	}
46
+    public function __construct(IAppManager $manager, Installer $installer, LoggerInterface $logger) {
47
+        parent::__construct();
48
+        $this->manager = $manager;
49
+        $this->installer = $installer;
50
+        $this->logger = $logger;
51
+    }
52 52
 
53
-	protected function configure() {
54
-		$this
55
-			->setName('app:remove')
56
-			->setDescription('remove an app')
57
-			->addArgument(
58
-				'app-id',
59
-				InputArgument::REQUIRED,
60
-				'remove the specified app'
61
-			)
62
-			->addOption(
63
-				'keep-data',
64
-				null,
65
-				InputOption::VALUE_NONE,
66
-				'keep app data and do not remove them'
67
-			);
68
-	}
53
+    protected function configure() {
54
+        $this
55
+            ->setName('app:remove')
56
+            ->setDescription('remove an app')
57
+            ->addArgument(
58
+                'app-id',
59
+                InputArgument::REQUIRED,
60
+                'remove the specified app'
61
+            )
62
+            ->addOption(
63
+                'keep-data',
64
+                null,
65
+                InputOption::VALUE_NONE,
66
+                'keep app data and do not remove them'
67
+            );
68
+    }
69 69
 
70
-	protected function execute(InputInterface $input, OutputInterface $output): int {
71
-		$appId = $input->getArgument('app-id');
70
+    protected function execute(InputInterface $input, OutputInterface $output): int {
71
+        $appId = $input->getArgument('app-id');
72 72
 
73
-		// Check if the app is installed
74
-		if (!\OC_App::getAppPath($appId)) {
75
-			$output->writeln($appId . ' is not installed');
76
-			return 1;
77
-		}
73
+        // Check if the app is installed
74
+        if (!\OC_App::getAppPath($appId)) {
75
+            $output->writeln($appId . ' is not installed');
76
+            return 1;
77
+        }
78 78
 
79
-		// Removing shipped apps is not possible, therefore we pre-check that
80
-		// before trying to remove it
81
-		if ($this->manager->isShipped($appId)) {
82
-			$output->writeln($appId . ' could not be removed as it is a shipped app');
83
-			return 1;
84
-		}
79
+        // Removing shipped apps is not possible, therefore we pre-check that
80
+        // before trying to remove it
81
+        if ($this->manager->isShipped($appId)) {
82
+            $output->writeln($appId . ' could not be removed as it is a shipped app');
83
+            return 1;
84
+        }
85 85
 
86
-		// If we want to keep the data of the app, we simply don't disable it here.
87
-		// App uninstall tasks are being executed when disabled. More info: PR #11627.
88
-		if (!$input->getOption('keep-data')) {
89
-			try {
90
-				$this->manager->disableApp($appId);
91
-				$output->writeln($appId . ' disabled');
92
-			} catch (Throwable $e) {
93
-				$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
94
-				$this->logger->error($e->getMessage(), [
95
-					'app' => 'CLI',
96
-					'exception' => $e,
97
-				]);
98
-				return 1;
99
-			}
100
-		}
86
+        // If we want to keep the data of the app, we simply don't disable it here.
87
+        // App uninstall tasks are being executed when disabled. More info: PR #11627.
88
+        if (!$input->getOption('keep-data')) {
89
+            try {
90
+                $this->manager->disableApp($appId);
91
+                $output->writeln($appId . ' disabled');
92
+            } catch (Throwable $e) {
93
+                $output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
94
+                $this->logger->error($e->getMessage(), [
95
+                    'app' => 'CLI',
96
+                    'exception' => $e,
97
+                ]);
98
+                return 1;
99
+            }
100
+        }
101 101
 
102
-		// Let's try to remove the app...
103
-		try {
104
-			$result = $this->installer->removeApp($appId);
105
-		} catch (Throwable $e) {
106
-			$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
107
-			$this->logger->error($e->getMessage(), [
108
-				'app' => 'CLI',
109
-				'exception' => $e,
110
-			]);
111
-			return 1;
112
-		}
102
+        // Let's try to remove the app...
103
+        try {
104
+            $result = $this->installer->removeApp($appId);
105
+        } catch (Throwable $e) {
106
+            $output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
107
+            $this->logger->error($e->getMessage(), [
108
+                'app' => 'CLI',
109
+                'exception' => $e,
110
+            ]);
111
+            return 1;
112
+        }
113 113
 
114
-		if ($result === false) {
115
-			$output->writeln($appId . ' could not be removed');
116
-			return 1;
117
-		}
114
+        if ($result === false) {
115
+            $output->writeln($appId . ' could not be removed');
116
+            return 1;
117
+        }
118 118
 
119
-		$appVersion = $this->manager->getAppVersion($appId);
120
-		$output->writeln($appId . ' ' . $appVersion . ' removed');
119
+        $appVersion = $this->manager->getAppVersion($appId);
120
+        $output->writeln($appId . ' ' . $appVersion . ' removed');
121 121
 
122
-		return 0;
123
-	}
122
+        return 0;
123
+    }
124 124
 
125
-	/**
126
-	 * @param string $optionName
127
-	 * @param CompletionContext $context
128
-	 * @return string[]
129
-	 */
130
-	public function completeOptionValues($optionName, CompletionContext $context) {
131
-		return [];
132
-	}
125
+    /**
126
+     * @param string $optionName
127
+     * @param CompletionContext $context
128
+     * @return string[]
129
+     */
130
+    public function completeOptionValues($optionName, CompletionContext $context) {
131
+        return [];
132
+    }
133 133
 
134
-	/**
135
-	 * @param string $argumentName
136
-	 * @param CompletionContext $context
137
-	 * @return string[]
138
-	 */
139
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
140
-		if ($argumentName === 'app-id') {
141
-			return \OC_App::getAllApps();
142
-		}
143
-		return [];
144
-	}
134
+    /**
135
+     * @param string $argumentName
136
+     * @param CompletionContext $context
137
+     * @return string[]
138
+     */
139
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
140
+        if ($argumentName === 'app-id') {
141
+            return \OC_App::getAllApps();
142
+        }
143
+        return [];
144
+    }
145 145
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -72,14 +72,14 @@  discard block
 block discarded – undo
72 72
 
73 73
 		// Check if the app is installed
74 74
 		if (!\OC_App::getAppPath($appId)) {
75
-			$output->writeln($appId . ' is not installed');
75
+			$output->writeln($appId.' is not installed');
76 76
 			return 1;
77 77
 		}
78 78
 
79 79
 		// Removing shipped apps is not possible, therefore we pre-check that
80 80
 		// before trying to remove it
81 81
 		if ($this->manager->isShipped($appId)) {
82
-			$output->writeln($appId . ' could not be removed as it is a shipped app');
82
+			$output->writeln($appId.' could not be removed as it is a shipped app');
83 83
 			return 1;
84 84
 		}
85 85
 
@@ -88,9 +88,9 @@  discard block
 block discarded – undo
88 88
 		if (!$input->getOption('keep-data')) {
89 89
 			try {
90 90
 				$this->manager->disableApp($appId);
91
-				$output->writeln($appId . ' disabled');
91
+				$output->writeln($appId.' disabled');
92 92
 			} catch (Throwable $e) {
93
-				$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
93
+				$output->writeln('<error>Error: '.$e->getMessage().'</error>');
94 94
 				$this->logger->error($e->getMessage(), [
95 95
 					'app' => 'CLI',
96 96
 					'exception' => $e,
@@ -103,7 +103,7 @@  discard block
 block discarded – undo
103 103
 		try {
104 104
 			$result = $this->installer->removeApp($appId);
105 105
 		} catch (Throwable $e) {
106
-			$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
106
+			$output->writeln('<error>Error: '.$e->getMessage().'</error>');
107 107
 			$this->logger->error($e->getMessage(), [
108 108
 				'app' => 'CLI',
109 109
 				'exception' => $e,
@@ -112,12 +112,12 @@  discard block
 block discarded – undo
112 112
 		}
113 113
 
114 114
 		if ($result === false) {
115
-			$output->writeln($appId . ' could not be removed');
115
+			$output->writeln($appId.' could not be removed');
116 116
 			return 1;
117 117
 		}
118 118
 
119 119
 		$appVersion = $this->manager->getAppVersion($appId);
120
-		$output->writeln($appId . ' ' . $appVersion . ' removed');
120
+		$output->writeln($appId.' '.$appVersion.' removed');
121 121
 
122 122
 		return 0;
123 123
 	}
Please login to merge, or discard this patch.
core/Command/App/Disable.php 2 patches
Indentation   +56 added lines, -56 removed lines patch added patch discarded remove patch
@@ -33,69 +33,69 @@
 block discarded – undo
33 33
 use Symfony\Component\Console\Output\OutputInterface;
34 34
 
35 35
 class Disable extends Command implements CompletionAwareInterface {
36
-	protected IAppManager $appManager;
37
-	protected int $exitCode = 0;
36
+    protected IAppManager $appManager;
37
+    protected int $exitCode = 0;
38 38
 
39
-	public function __construct(IAppManager $appManager) {
40
-		parent::__construct();
41
-		$this->appManager = $appManager;
42
-	}
39
+    public function __construct(IAppManager $appManager) {
40
+        parent::__construct();
41
+        $this->appManager = $appManager;
42
+    }
43 43
 
44
-	protected function configure(): void {
45
-		$this
46
-			->setName('app:disable')
47
-			->setDescription('disable an app')
48
-			->addArgument(
49
-				'app-id',
50
-				InputArgument::REQUIRED | InputArgument::IS_ARRAY,
51
-				'disable the specified app'
52
-			);
53
-	}
44
+    protected function configure(): void {
45
+        $this
46
+            ->setName('app:disable')
47
+            ->setDescription('disable an app')
48
+            ->addArgument(
49
+                'app-id',
50
+                InputArgument::REQUIRED | InputArgument::IS_ARRAY,
51
+                'disable the specified app'
52
+            );
53
+    }
54 54
 
55
-	protected function execute(InputInterface $input, OutputInterface $output): int {
56
-		$appIds = $input->getArgument('app-id');
55
+    protected function execute(InputInterface $input, OutputInterface $output): int {
56
+        $appIds = $input->getArgument('app-id');
57 57
 
58
-		foreach ($appIds as $appId) {
59
-			$this->disableApp($appId, $output);
60
-		}
58
+        foreach ($appIds as $appId) {
59
+            $this->disableApp($appId, $output);
60
+        }
61 61
 
62
-		return $this->exitCode;
63
-	}
62
+        return $this->exitCode;
63
+    }
64 64
 
65
-	private function disableApp(string $appId, OutputInterface $output): void {
66
-		if ($this->appManager->isInstalled($appId) === false) {
67
-			$output->writeln('No such app enabled: ' . $appId);
68
-			return;
69
-		}
65
+    private function disableApp(string $appId, OutputInterface $output): void {
66
+        if ($this->appManager->isInstalled($appId) === false) {
67
+            $output->writeln('No such app enabled: ' . $appId);
68
+            return;
69
+        }
70 70
 
71
-		try {
72
-			$this->appManager->disableApp($appId);
73
-			$appVersion = $this->appManager->getAppVersion($appId);
74
-			$output->writeln($appId . ' ' . $appVersion . ' disabled');
75
-		} catch (\Exception $e) {
76
-			$output->writeln($e->getMessage());
77
-			$this->exitCode = 2;
78
-		}
79
-	}
71
+        try {
72
+            $this->appManager->disableApp($appId);
73
+            $appVersion = $this->appManager->getAppVersion($appId);
74
+            $output->writeln($appId . ' ' . $appVersion . ' disabled');
75
+        } catch (\Exception $e) {
76
+            $output->writeln($e->getMessage());
77
+            $this->exitCode = 2;
78
+        }
79
+    }
80 80
 
81
-	/**
82
-	 * @param string $optionName
83
-	 * @param CompletionContext $context
84
-	 * @return string[]
85
-	 */
86
-	public function completeOptionValues($optionName, CompletionContext $context) {
87
-		return [];
88
-	}
81
+    /**
82
+     * @param string $optionName
83
+     * @param CompletionContext $context
84
+     * @return string[]
85
+     */
86
+    public function completeOptionValues($optionName, CompletionContext $context) {
87
+        return [];
88
+    }
89 89
 
90
-	/**
91
-	 * @param string $argumentName
92
-	 * @param CompletionContext $context
93
-	 * @return string[]
94
-	 */
95
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
96
-		if ($argumentName === 'app-id') {
97
-			return array_diff(\OC_App::getEnabledApps(true, true), $this->appManager->getAlwaysEnabledApps());
98
-		}
99
-		return [];
100
-	}
90
+    /**
91
+     * @param string $argumentName
92
+     * @param CompletionContext $context
93
+     * @return string[]
94
+     */
95
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
96
+        if ($argumentName === 'app-id') {
97
+            return array_diff(\OC_App::getEnabledApps(true, true), $this->appManager->getAlwaysEnabledApps());
98
+        }
99
+        return [];
100
+    }
101 101
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -64,14 +64,14 @@
 block discarded – undo
64 64
 
65 65
 	private function disableApp(string $appId, OutputInterface $output): void {
66 66
 		if ($this->appManager->isInstalled($appId) === false) {
67
-			$output->writeln('No such app enabled: ' . $appId);
67
+			$output->writeln('No such app enabled: '.$appId);
68 68
 			return;
69 69
 		}
70 70
 
71 71
 		try {
72 72
 			$this->appManager->disableApp($appId);
73 73
 			$appVersion = $this->appManager->getAppVersion($appId);
74
-			$output->writeln($appId . ' ' . $appVersion . ' disabled');
74
+			$output->writeln($appId.' '.$appVersion.' disabled');
75 75
 		} catch (\Exception $e) {
76 76
 			$output->writeln($e->getMessage());
77 77
 			$this->exitCode = 2;
Please login to merge, or discard this patch.
core/Command/App/Enable.php 1 patch
Indentation   +129 added lines, -129 removed lines patch added patch discarded remove patch
@@ -39,133 +39,133 @@
 block discarded – undo
39 39
 use Symfony\Component\Console\Output\OutputInterface;
40 40
 
41 41
 class Enable extends Command implements CompletionAwareInterface {
42
-	protected IAppManager $appManager;
43
-	protected IGroupManager $groupManager;
44
-	protected int $exitCode = 0;
45
-
46
-	public function __construct(IAppManager $appManager, IGroupManager $groupManager) {
47
-		parent::__construct();
48
-		$this->appManager = $appManager;
49
-		$this->groupManager = $groupManager;
50
-	}
51
-
52
-	protected function configure(): void {
53
-		$this
54
-			->setName('app:enable')
55
-			->setDescription('enable an app')
56
-			->addArgument(
57
-				'app-id',
58
-				InputArgument::REQUIRED | InputArgument::IS_ARRAY,
59
-				'enable the specified app'
60
-			)
61
-			->addOption(
62
-				'groups',
63
-				'g',
64
-				InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
65
-				'enable the app only for a list of groups'
66
-			)
67
-			->addOption(
68
-				'force',
69
-				'f',
70
-				InputOption::VALUE_NONE,
71
-				'enable the app regardless of the Nextcloud version requirement'
72
-			);
73
-	}
74
-
75
-	protected function execute(InputInterface $input, OutputInterface $output): int {
76
-		$appIds = $input->getArgument('app-id');
77
-		$groups = $this->resolveGroupIds($input->getOption('groups'));
78
-		$forceEnable = (bool) $input->getOption('force');
79
-
80
-		foreach ($appIds as $appId) {
81
-			$this->enableApp($appId, $groups, $forceEnable, $output);
82
-		}
83
-
84
-		return $this->exitCode;
85
-	}
86
-
87
-	/**
88
-	 * @param string $appId
89
-	 * @param array $groupIds
90
-	 * @param bool $forceEnable
91
-	 * @param OutputInterface $output
92
-	 */
93
-	private function enableApp(string $appId, array $groupIds, bool $forceEnable, OutputInterface $output): void {
94
-		$groupNames = array_map(function (IGroup $group) {
95
-			return $group->getDisplayName();
96
-		}, $groupIds);
97
-
98
-		if ($this->appManager->isInstalled($appId) && $groupIds === []) {
99
-			$output->writeln($appId . ' already enabled');
100
-			return;
101
-		}
102
-
103
-		try {
104
-			/** @var Installer $installer */
105
-			$installer = \OC::$server->query(Installer::class);
106
-
107
-			if (false === $installer->isDownloaded($appId)) {
108
-				$installer->downloadApp($appId);
109
-			}
110
-
111
-			$installer->installApp($appId, $forceEnable);
112
-			$appVersion = $this->appManager->getAppVersion($appId);
113
-
114
-			if ($groupIds === []) {
115
-				$this->appManager->enableApp($appId, $forceEnable);
116
-				$output->writeln($appId . ' ' . $appVersion . ' enabled');
117
-			} else {
118
-				$this->appManager->enableAppForGroups($appId, $groupIds, $forceEnable);
119
-				$output->writeln($appId . ' ' . $appVersion . ' enabled for groups: ' . implode(', ', $groupNames));
120
-			}
121
-		} catch (AppPathNotFoundException $e) {
122
-			$output->writeln($appId . ' not found');
123
-			$this->exitCode = 1;
124
-		} catch (\Exception $e) {
125
-			$output->writeln($e->getMessage());
126
-			$this->exitCode = 1;
127
-		}
128
-	}
129
-
130
-	/**
131
-	 * @param array $groupIds
132
-	 * @return array
133
-	 */
134
-	private function resolveGroupIds(array $groupIds): array {
135
-		$groups = [];
136
-		foreach ($groupIds as $groupId) {
137
-			$group = $this->groupManager->get($groupId);
138
-			if ($group instanceof IGroup) {
139
-				$groups[] = $group;
140
-			}
141
-		}
142
-		return $groups;
143
-	}
144
-
145
-	/**
146
-	 * @param string $optionName
147
-	 * @param CompletionContext $context
148
-	 * @return string[]
149
-	 */
150
-	public function completeOptionValues($optionName, CompletionContext $context) {
151
-		if ($optionName === 'groups') {
152
-			return array_map(function (IGroup $group) {
153
-				return $group->getGID();
154
-			}, $this->groupManager->search($context->getCurrentWord()));
155
-		}
156
-		return [];
157
-	}
158
-
159
-	/**
160
-	 * @param string $argumentName
161
-	 * @param CompletionContext $context
162
-	 * @return string[]
163
-	 */
164
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
165
-		if ($argumentName === 'app-id') {
166
-			$allApps = \OC_App::getAllApps();
167
-			return array_diff($allApps, \OC_App::getEnabledApps(true, true));
168
-		}
169
-		return [];
170
-	}
42
+    protected IAppManager $appManager;
43
+    protected IGroupManager $groupManager;
44
+    protected int $exitCode = 0;
45
+
46
+    public function __construct(IAppManager $appManager, IGroupManager $groupManager) {
47
+        parent::__construct();
48
+        $this->appManager = $appManager;
49
+        $this->groupManager = $groupManager;
50
+    }
51
+
52
+    protected function configure(): void {
53
+        $this
54
+            ->setName('app:enable')
55
+            ->setDescription('enable an app')
56
+            ->addArgument(
57
+                'app-id',
58
+                InputArgument::REQUIRED | InputArgument::IS_ARRAY,
59
+                'enable the specified app'
60
+            )
61
+            ->addOption(
62
+                'groups',
63
+                'g',
64
+                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
65
+                'enable the app only for a list of groups'
66
+            )
67
+            ->addOption(
68
+                'force',
69
+                'f',
70
+                InputOption::VALUE_NONE,
71
+                'enable the app regardless of the Nextcloud version requirement'
72
+            );
73
+    }
74
+
75
+    protected function execute(InputInterface $input, OutputInterface $output): int {
76
+        $appIds = $input->getArgument('app-id');
77
+        $groups = $this->resolveGroupIds($input->getOption('groups'));
78
+        $forceEnable = (bool) $input->getOption('force');
79
+
80
+        foreach ($appIds as $appId) {
81
+            $this->enableApp($appId, $groups, $forceEnable, $output);
82
+        }
83
+
84
+        return $this->exitCode;
85
+    }
86
+
87
+    /**
88
+     * @param string $appId
89
+     * @param array $groupIds
90
+     * @param bool $forceEnable
91
+     * @param OutputInterface $output
92
+     */
93
+    private function enableApp(string $appId, array $groupIds, bool $forceEnable, OutputInterface $output): void {
94
+        $groupNames = array_map(function (IGroup $group) {
95
+            return $group->getDisplayName();
96
+        }, $groupIds);
97
+
98
+        if ($this->appManager->isInstalled($appId) && $groupIds === []) {
99
+            $output->writeln($appId . ' already enabled');
100
+            return;
101
+        }
102
+
103
+        try {
104
+            /** @var Installer $installer */
105
+            $installer = \OC::$server->query(Installer::class);
106
+
107
+            if (false === $installer->isDownloaded($appId)) {
108
+                $installer->downloadApp($appId);
109
+            }
110
+
111
+            $installer->installApp($appId, $forceEnable);
112
+            $appVersion = $this->appManager->getAppVersion($appId);
113
+
114
+            if ($groupIds === []) {
115
+                $this->appManager->enableApp($appId, $forceEnable);
116
+                $output->writeln($appId . ' ' . $appVersion . ' enabled');
117
+            } else {
118
+                $this->appManager->enableAppForGroups($appId, $groupIds, $forceEnable);
119
+                $output->writeln($appId . ' ' . $appVersion . ' enabled for groups: ' . implode(', ', $groupNames));
120
+            }
121
+        } catch (AppPathNotFoundException $e) {
122
+            $output->writeln($appId . ' not found');
123
+            $this->exitCode = 1;
124
+        } catch (\Exception $e) {
125
+            $output->writeln($e->getMessage());
126
+            $this->exitCode = 1;
127
+        }
128
+    }
129
+
130
+    /**
131
+     * @param array $groupIds
132
+     * @return array
133
+     */
134
+    private function resolveGroupIds(array $groupIds): array {
135
+        $groups = [];
136
+        foreach ($groupIds as $groupId) {
137
+            $group = $this->groupManager->get($groupId);
138
+            if ($group instanceof IGroup) {
139
+                $groups[] = $group;
140
+            }
141
+        }
142
+        return $groups;
143
+    }
144
+
145
+    /**
146
+     * @param string $optionName
147
+     * @param CompletionContext $context
148
+     * @return string[]
149
+     */
150
+    public function completeOptionValues($optionName, CompletionContext $context) {
151
+        if ($optionName === 'groups') {
152
+            return array_map(function (IGroup $group) {
153
+                return $group->getGID();
154
+            }, $this->groupManager->search($context->getCurrentWord()));
155
+        }
156
+        return [];
157
+    }
158
+
159
+    /**
160
+     * @param string $argumentName
161
+     * @param CompletionContext $context
162
+     * @return string[]
163
+     */
164
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
165
+        if ($argumentName === 'app-id') {
166
+            $allApps = \OC_App::getAllApps();
167
+            return array_diff($allApps, \OC_App::getEnabledApps(true, true));
168
+        }
169
+        return [];
170
+    }
171 171
 }
Please login to merge, or discard this patch.
lib/private/Updater.php 1 patch
Indentation   +468 added lines, -468 removed lines patch added patch discarded remove patch
@@ -73,472 +73,472 @@
 block discarded – undo
73 73
  *  - failure(string $message)
74 74
  */
75 75
 class Updater extends BasicEmitter {
76
-	/** @var LoggerInterface */
77
-	private $log;
78
-
79
-	/** @var IConfig */
80
-	private $config;
81
-
82
-	/** @var Checker */
83
-	private $checker;
84
-
85
-	/** @var Installer */
86
-	private $installer;
87
-
88
-	private $logLevelNames = [
89
-		0 => 'Debug',
90
-		1 => 'Info',
91
-		2 => 'Warning',
92
-		3 => 'Error',
93
-		4 => 'Fatal',
94
-	];
95
-
96
-	public function __construct(IConfig $config,
97
-								Checker $checker,
98
-								?LoggerInterface $log,
99
-								Installer $installer) {
100
-		$this->log = $log;
101
-		$this->config = $config;
102
-		$this->checker = $checker;
103
-		$this->installer = $installer;
104
-	}
105
-
106
-	/**
107
-	 * runs the update actions in maintenance mode, does not upgrade the source files
108
-	 * except the main .htaccess file
109
-	 *
110
-	 * @return bool true if the operation succeeded, false otherwise
111
-	 */
112
-	public function upgrade(): bool {
113
-		$this->logAllEvents();
114
-
115
-		$logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN);
116
-		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
117
-		$this->config->setSystemValue('loglevel', ILogger::DEBUG);
118
-
119
-		$wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance');
120
-
121
-		if (!$wasMaintenanceModeEnabled) {
122
-			$this->config->setSystemValue('maintenance', true);
123
-			$this->emit('\OC\Updater', 'maintenanceEnabled');
124
-		}
125
-
126
-		// Clear CAN_INSTALL file if not on git
127
-		if (\OC_Util::getChannel() !== 'git' && is_file(\OC::$configDir.'/CAN_INSTALL')) {
128
-			if (!unlink(\OC::$configDir . '/CAN_INSTALL')) {
129
-				$this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.');
130
-			}
131
-		}
132
-
133
-		$installedVersion = $this->config->getSystemValueString('version', '0.0.0');
134
-		$currentVersion = implode('.', \OCP\Util::getVersion());
135
-
136
-		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
137
-
138
-		$success = true;
139
-		try {
140
-			$this->doUpgrade($currentVersion, $installedVersion);
141
-		} catch (HintException $exception) {
142
-			$this->log->error($exception->getMessage(), [
143
-				'exception' => $exception,
144
-			]);
145
-			$this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' .$exception->getHint()]);
146
-			$success = false;
147
-		} catch (\Exception $exception) {
148
-			$this->log->error($exception->getMessage(), [
149
-				'exception' => $exception,
150
-			]);
151
-			$this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' .$exception->getMessage()]);
152
-			$success = false;
153
-		}
154
-
155
-		$this->emit('\OC\Updater', 'updateEnd', [$success]);
156
-
157
-		if (!$wasMaintenanceModeEnabled && $success) {
158
-			$this->config->setSystemValue('maintenance', false);
159
-			$this->emit('\OC\Updater', 'maintenanceDisabled');
160
-		} else {
161
-			$this->emit('\OC\Updater', 'maintenanceActive');
162
-		}
163
-
164
-		$this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
165
-		$this->config->setSystemValue('loglevel', $logLevel);
166
-		$this->config->setSystemValue('installed', true);
167
-
168
-		return $success;
169
-	}
170
-
171
-	/**
172
-	 * Return version from which this version is allowed to upgrade from
173
-	 *
174
-	 * @return array allowed previous versions per vendor
175
-	 */
176
-	private function getAllowedPreviousVersions(): array {
177
-		// this should really be a JSON file
178
-		require \OC::$SERVERROOT . '/version.php';
179
-		/** @var array $OC_VersionCanBeUpgradedFrom */
180
-		return $OC_VersionCanBeUpgradedFrom;
181
-	}
182
-
183
-	/**
184
-	 * Return vendor from which this version was published
185
-	 *
186
-	 * @return string Get the vendor
187
-	 */
188
-	private function getVendor(): string {
189
-		// this should really be a JSON file
190
-		require \OC::$SERVERROOT . '/version.php';
191
-		/** @var string $vendor */
192
-		return (string) $vendor;
193
-	}
194
-
195
-	/**
196
-	 * Whether an upgrade to a specified version is possible
197
-	 * @param string $oldVersion
198
-	 * @param string $newVersion
199
-	 * @param array $allowedPreviousVersions
200
-	 * @return bool
201
-	 */
202
-	public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool {
203
-		$version = explode('.', $oldVersion);
204
-		$majorMinor = $version[0] . '.' . $version[1];
205
-
206
-		$currentVendor = $this->config->getAppValue('core', 'vendor', '');
207
-
208
-		// Vendor was not set correctly on install, so we have to white-list known versions
209
-		if ($currentVendor === '' && (
210
-			isset($allowedPreviousVersions['owncloud'][$oldVersion]) ||
211
-			isset($allowedPreviousVersions['owncloud'][$majorMinor])
212
-		)) {
213
-			$currentVendor = 'owncloud';
214
-			$this->config->setAppValue('core', 'vendor', $currentVendor);
215
-		}
216
-
217
-		if ($currentVendor === 'nextcloud') {
218
-			return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
219
-				&& (version_compare($oldVersion, $newVersion, '<=') ||
220
-					$this->config->getSystemValueBool('debug', false));
221
-		}
222
-
223
-		// Check if the instance can be migrated
224
-		return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) ||
225
-			isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
226
-	}
227
-
228
-	/**
229
-	 * runs the update actions in maintenance mode, does not upgrade the source files
230
-	 * except the main .htaccess file
231
-	 *
232
-	 * @param string $currentVersion current version to upgrade to
233
-	 * @param string $installedVersion previous version from which to upgrade from
234
-	 *
235
-	 * @throws \Exception
236
-	 */
237
-	private function doUpgrade(string $currentVersion, string $installedVersion): void {
238
-		// Stop update if the update is over several major versions
239
-		$allowedPreviousVersions = $this->getAllowedPreviousVersions();
240
-		if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
241
-			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
242
-		}
243
-
244
-		// Update .htaccess files
245
-		try {
246
-			Setup::updateHtaccess();
247
-			Setup::protectDataDirectory();
248
-		} catch (\Exception $e) {
249
-			throw new \Exception($e->getMessage());
250
-		}
251
-
252
-		// create empty file in data dir, so we can later find
253
-		// out that this is indeed an ownCloud data directory
254
-		// (in case it didn't exist before)
255
-		file_put_contents($this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
256
-
257
-		// pre-upgrade repairs
258
-		$repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->get(LoggerInterface::class));
259
-		$repair->run();
260
-
261
-		$this->doCoreUpgrade();
262
-
263
-		try {
264
-			// TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
265
-			Setup::installBackgroundJobs();
266
-		} catch (\Exception $e) {
267
-			throw new \Exception($e->getMessage());
268
-		}
269
-
270
-		// update all shipped apps
271
-		$this->checkAppsRequirements();
272
-		$this->doAppUpgrade();
273
-
274
-		// Update the appfetchers version so it downloads the correct list from the appstore
275
-		\OC::$server->getAppFetcher()->setVersion($currentVersion);
276
-
277
-		/** @var AppManager $appManager */
278
-		$appManager = \OC::$server->getAppManager();
279
-
280
-		// upgrade appstore apps
281
-		$this->upgradeAppStoreApps($appManager->getInstalledApps());
282
-		$autoDisabledApps = $appManager->getAutoDisabledApps();
283
-		if (!empty($autoDisabledApps)) {
284
-			$this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
285
-		}
286
-
287
-		// install new shipped apps on upgrade
288
-		$errors = Installer::installShippedApps(true);
289
-		foreach ($errors as $appId => $exception) {
290
-			/** @var \Exception $exception */
291
-			$this->log->error($exception->getMessage(), [
292
-				'exception' => $exception,
293
-				'app' => $appId,
294
-			]);
295
-			$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
296
-		}
297
-
298
-		// post-upgrade repairs
299
-		$repair = new Repair(Repair::getRepairSteps(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->get(LoggerInterface::class));
300
-		$repair->run();
301
-
302
-		//Invalidate update feed
303
-		$this->config->setAppValue('core', 'lastupdatedat', '0');
304
-
305
-		// Check for code integrity if not disabled
306
-		if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
307
-			$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
308
-			$this->checker->runInstanceVerification();
309
-			$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
310
-		}
311
-
312
-		// only set the final version if everything went well
313
-		$this->config->setSystemValue('version', implode('.', Util::getVersion()));
314
-		$this->config->setAppValue('core', 'vendor', $this->getVendor());
315
-	}
316
-
317
-	protected function doCoreUpgrade(): void {
318
-		$this->emit('\OC\Updater', 'dbUpgradeBefore');
319
-
320
-		// execute core migrations
321
-		$ms = new MigrationService('core', \OC::$server->get(Connection::class));
322
-		$ms->migrate();
323
-
324
-		$this->emit('\OC\Updater', 'dbUpgrade');
325
-	}
326
-
327
-	/**
328
-	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
329
-	 * (types authentication, filesystem, logging, in that order) afterwards.
330
-	 *
331
-	 * @throws NeedsUpdateException
332
-	 */
333
-	protected function doAppUpgrade(): void {
334
-		$apps = \OC_App::getEnabledApps();
335
-		$priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging'];
336
-		$pseudoOtherType = 'other';
337
-		$stacks = [$pseudoOtherType => []];
338
-
339
-		foreach ($apps as $appId) {
340
-			$priorityType = false;
341
-			foreach ($priorityTypes as $type) {
342
-				if (!isset($stacks[$type])) {
343
-					$stacks[$type] = [];
344
-				}
345
-				if (\OC_App::isType($appId, [$type])) {
346
-					$stacks[$type][] = $appId;
347
-					$priorityType = true;
348
-					break;
349
-				}
350
-			}
351
-			if (!$priorityType) {
352
-				$stacks[$pseudoOtherType][] = $appId;
353
-			}
354
-		}
355
-		foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
356
-			$stack = $stacks[$type];
357
-			foreach ($stack as $appId) {
358
-				if (\OC_App::shouldUpgrade($appId)) {
359
-					$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
360
-					\OC_App::updateApp($appId);
361
-					$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
362
-				}
363
-				if ($type !== $pseudoOtherType) {
364
-					// load authentication, filesystem and logging apps after
365
-					// upgrading them. Other apps my need to rely on modifying
366
-					// user and/or filesystem aspects.
367
-					\OC_App::loadApp($appId);
368
-				}
369
-			}
370
-		}
371
-	}
372
-
373
-	/**
374
-	 * check if the current enabled apps are compatible with the current
375
-	 * ownCloud version. disable them if not.
376
-	 * This is important if you upgrade ownCloud and have non ported 3rd
377
-	 * party apps installed.
378
-	 *
379
-	 * @throws \Exception
380
-	 */
381
-	private function checkAppsRequirements(): void {
382
-		$isCoreUpgrade = $this->isCodeUpgrade();
383
-		$apps = OC_App::getEnabledApps();
384
-		$version = implode('.', Util::getVersion());
385
-		$appManager = \OC::$server->getAppManager();
386
-		foreach ($apps as $app) {
387
-			// check if the app is compatible with this version of Nextcloud
388
-			$info = $appManager->getAppInfo($app);
389
-			if ($info === null || !OC_App::isAppCompatible($version, $info)) {
390
-				if ($appManager->isShipped($app)) {
391
-					throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
392
-				}
393
-				$appManager->disableApp($app, true);
394
-				$this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
395
-			}
396
-		}
397
-	}
398
-
399
-	/**
400
-	 * @return bool
401
-	 */
402
-	private function isCodeUpgrade(): bool {
403
-		$installedVersion = $this->config->getSystemValueString('version', '0.0.0');
404
-		$currentVersion = implode('.', Util::getVersion());
405
-		if (version_compare($currentVersion, $installedVersion, '>')) {
406
-			return true;
407
-		}
408
-		return false;
409
-	}
410
-
411
-	/**
412
-	 * @param array $apps
413
-	 * @param array $previousEnableStates
414
-	 * @throws \Exception
415
-	 */
416
-	private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void {
417
-		foreach ($apps as $app) {
418
-			try {
419
-				$this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]);
420
-				if ($this->installer->isUpdateAvailable($app)) {
421
-					$this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
422
-					$this->installer->updateAppstoreApp($app);
423
-				}
424
-				$this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
425
-
426
-				if (!empty($previousEnableStates)) {
427
-					$ocApp = new \OC_App();
428
-					if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
429
-						$ocApp->enable($app, $previousEnableStates[$app]);
430
-					} else {
431
-						$ocApp->enable($app);
432
-					}
433
-				}
434
-			} catch (\Exception $ex) {
435
-				$this->log->error($ex->getMessage(), [
436
-					'exception' => $ex,
437
-				]);
438
-			}
439
-		}
440
-	}
441
-
442
-	private function logAllEvents(): void {
443
-		$log = $this->log;
444
-
445
-		/** @var IEventDispatcher $dispatcher */
446
-		$dispatcher = \OC::$server->get(IEventDispatcher::class);
447
-		$dispatcher->addListener(
448
-			MigratorExecuteSqlEvent::class,
449
-			function (MigratorExecuteSqlEvent $event) use ($log): void {
450
-				$log->info(get_class($event).': ' . $event->getSql() . ' (' . $event->getCurrentStep() . ' of ' . $event->getMaxStep() . ')', ['app' => 'updater']);
451
-			}
452
-		);
453
-
454
-		$repairListener = function (Event $event) use ($log): void {
455
-			if ($event instanceof RepairStartEvent) {
456
-				$log->info(get_class($event).': Starting ... ' . $event->getMaxStep() .  ' (' . $event->getCurrentStepName() . ')', ['app' => 'updater']);
457
-			} elseif ($event instanceof RepairAdvanceEvent) {
458
-				$desc = $event->getDescription();
459
-				if (empty($desc)) {
460
-					$desc = '';
461
-				}
462
-				$log->info(get_class($event).': ' . $desc . ' (' . $event->getIncrement() . ')', ['app' => 'updater']);
463
-			} elseif ($event instanceof RepairFinishEvent) {
464
-				$log->info(get_class($event), ['app' => 'updater']);
465
-			} elseif ($event instanceof RepairStepEvent) {
466
-				$log->info(get_class($event).': Repair step: ' . $event->getStepName(), ['app' => 'updater']);
467
-			} elseif ($event instanceof RepairInfoEvent) {
468
-				$log->info(get_class($event).': Repair info: ' . $event->getMessage(), ['app' => 'updater']);
469
-			} elseif ($event instanceof RepairWarningEvent) {
470
-				$log->warning(get_class($event).': Repair warning: ' . $event->getMessage(), ['app' => 'updater']);
471
-			} elseif ($event instanceof RepairErrorEvent) {
472
-				$log->error(get_class($event).': Repair error: ' . $event->getMessage(), ['app' => 'updater']);
473
-			}
474
-		};
475
-
476
-		$dispatcher->addListener(RepairStartEvent::class, $repairListener);
477
-		$dispatcher->addListener(RepairAdvanceEvent::class, $repairListener);
478
-		$dispatcher->addListener(RepairFinishEvent::class, $repairListener);
479
-		$dispatcher->addListener(RepairStepEvent::class, $repairListener);
480
-		$dispatcher->addListener(RepairInfoEvent::class, $repairListener);
481
-		$dispatcher->addListener(RepairWarningEvent::class, $repairListener);
482
-		$dispatcher->addListener(RepairErrorEvent::class, $repairListener);
483
-
484
-
485
-		$this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) {
486
-			$log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']);
487
-		});
488
-		$this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) {
489
-			$log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']);
490
-		});
491
-		$this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) {
492
-			$log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']);
493
-		});
494
-		$this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) {
495
-			if ($success) {
496
-				$log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']);
497
-			} else {
498
-				$log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']);
499
-			}
500
-		});
501
-		$this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) {
502
-			$log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']);
503
-		});
504
-		$this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) {
505
-			$log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']);
506
-		});
507
-		$this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) {
508
-			$log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']);
509
-		});
510
-		$this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) {
511
-			$log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']);
512
-		});
513
-		$this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) {
514
-			$log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']);
515
-		});
516
-		$this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) {
517
-			$log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']);
518
-		});
519
-		$this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) {
520
-			$log->info('\OC\Updater::appSimulateUpdate: Checking whether the database schema for <' . $app . '> can be updated (this can take a long time depending on the database size)', ['app' => 'updater']);
521
-		});
522
-		$this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) {
523
-			$log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']);
524
-		});
525
-		$this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) {
526
-			$log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']);
527
-		});
528
-		$this->listen('\OC\Updater', 'failure', function ($message) use ($log) {
529
-			$log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']);
530
-		});
531
-		$this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) {
532
-			$log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']);
533
-		});
534
-		$this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) {
535
-			$log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']);
536
-		});
537
-		$this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) {
538
-			$log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']);
539
-		});
540
-		$this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) {
541
-			$log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']);
542
-		});
543
-	}
76
+    /** @var LoggerInterface */
77
+    private $log;
78
+
79
+    /** @var IConfig */
80
+    private $config;
81
+
82
+    /** @var Checker */
83
+    private $checker;
84
+
85
+    /** @var Installer */
86
+    private $installer;
87
+
88
+    private $logLevelNames = [
89
+        0 => 'Debug',
90
+        1 => 'Info',
91
+        2 => 'Warning',
92
+        3 => 'Error',
93
+        4 => 'Fatal',
94
+    ];
95
+
96
+    public function __construct(IConfig $config,
97
+                                Checker $checker,
98
+                                ?LoggerInterface $log,
99
+                                Installer $installer) {
100
+        $this->log = $log;
101
+        $this->config = $config;
102
+        $this->checker = $checker;
103
+        $this->installer = $installer;
104
+    }
105
+
106
+    /**
107
+     * runs the update actions in maintenance mode, does not upgrade the source files
108
+     * except the main .htaccess file
109
+     *
110
+     * @return bool true if the operation succeeded, false otherwise
111
+     */
112
+    public function upgrade(): bool {
113
+        $this->logAllEvents();
114
+
115
+        $logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN);
116
+        $this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
117
+        $this->config->setSystemValue('loglevel', ILogger::DEBUG);
118
+
119
+        $wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance');
120
+
121
+        if (!$wasMaintenanceModeEnabled) {
122
+            $this->config->setSystemValue('maintenance', true);
123
+            $this->emit('\OC\Updater', 'maintenanceEnabled');
124
+        }
125
+
126
+        // Clear CAN_INSTALL file if not on git
127
+        if (\OC_Util::getChannel() !== 'git' && is_file(\OC::$configDir.'/CAN_INSTALL')) {
128
+            if (!unlink(\OC::$configDir . '/CAN_INSTALL')) {
129
+                $this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.');
130
+            }
131
+        }
132
+
133
+        $installedVersion = $this->config->getSystemValueString('version', '0.0.0');
134
+        $currentVersion = implode('.', \OCP\Util::getVersion());
135
+
136
+        $this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
137
+
138
+        $success = true;
139
+        try {
140
+            $this->doUpgrade($currentVersion, $installedVersion);
141
+        } catch (HintException $exception) {
142
+            $this->log->error($exception->getMessage(), [
143
+                'exception' => $exception,
144
+            ]);
145
+            $this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' .$exception->getHint()]);
146
+            $success = false;
147
+        } catch (\Exception $exception) {
148
+            $this->log->error($exception->getMessage(), [
149
+                'exception' => $exception,
150
+            ]);
151
+            $this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' .$exception->getMessage()]);
152
+            $success = false;
153
+        }
154
+
155
+        $this->emit('\OC\Updater', 'updateEnd', [$success]);
156
+
157
+        if (!$wasMaintenanceModeEnabled && $success) {
158
+            $this->config->setSystemValue('maintenance', false);
159
+            $this->emit('\OC\Updater', 'maintenanceDisabled');
160
+        } else {
161
+            $this->emit('\OC\Updater', 'maintenanceActive');
162
+        }
163
+
164
+        $this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
165
+        $this->config->setSystemValue('loglevel', $logLevel);
166
+        $this->config->setSystemValue('installed', true);
167
+
168
+        return $success;
169
+    }
170
+
171
+    /**
172
+     * Return version from which this version is allowed to upgrade from
173
+     *
174
+     * @return array allowed previous versions per vendor
175
+     */
176
+    private function getAllowedPreviousVersions(): array {
177
+        // this should really be a JSON file
178
+        require \OC::$SERVERROOT . '/version.php';
179
+        /** @var array $OC_VersionCanBeUpgradedFrom */
180
+        return $OC_VersionCanBeUpgradedFrom;
181
+    }
182
+
183
+    /**
184
+     * Return vendor from which this version was published
185
+     *
186
+     * @return string Get the vendor
187
+     */
188
+    private function getVendor(): string {
189
+        // this should really be a JSON file
190
+        require \OC::$SERVERROOT . '/version.php';
191
+        /** @var string $vendor */
192
+        return (string) $vendor;
193
+    }
194
+
195
+    /**
196
+     * Whether an upgrade to a specified version is possible
197
+     * @param string $oldVersion
198
+     * @param string $newVersion
199
+     * @param array $allowedPreviousVersions
200
+     * @return bool
201
+     */
202
+    public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool {
203
+        $version = explode('.', $oldVersion);
204
+        $majorMinor = $version[0] . '.' . $version[1];
205
+
206
+        $currentVendor = $this->config->getAppValue('core', 'vendor', '');
207
+
208
+        // Vendor was not set correctly on install, so we have to white-list known versions
209
+        if ($currentVendor === '' && (
210
+            isset($allowedPreviousVersions['owncloud'][$oldVersion]) ||
211
+            isset($allowedPreviousVersions['owncloud'][$majorMinor])
212
+        )) {
213
+            $currentVendor = 'owncloud';
214
+            $this->config->setAppValue('core', 'vendor', $currentVendor);
215
+        }
216
+
217
+        if ($currentVendor === 'nextcloud') {
218
+            return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
219
+                && (version_compare($oldVersion, $newVersion, '<=') ||
220
+                    $this->config->getSystemValueBool('debug', false));
221
+        }
222
+
223
+        // Check if the instance can be migrated
224
+        return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) ||
225
+            isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
226
+    }
227
+
228
+    /**
229
+     * runs the update actions in maintenance mode, does not upgrade the source files
230
+     * except the main .htaccess file
231
+     *
232
+     * @param string $currentVersion current version to upgrade to
233
+     * @param string $installedVersion previous version from which to upgrade from
234
+     *
235
+     * @throws \Exception
236
+     */
237
+    private function doUpgrade(string $currentVersion, string $installedVersion): void {
238
+        // Stop update if the update is over several major versions
239
+        $allowedPreviousVersions = $this->getAllowedPreviousVersions();
240
+        if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
241
+            throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
242
+        }
243
+
244
+        // Update .htaccess files
245
+        try {
246
+            Setup::updateHtaccess();
247
+            Setup::protectDataDirectory();
248
+        } catch (\Exception $e) {
249
+            throw new \Exception($e->getMessage());
250
+        }
251
+
252
+        // create empty file in data dir, so we can later find
253
+        // out that this is indeed an ownCloud data directory
254
+        // (in case it didn't exist before)
255
+        file_put_contents($this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
256
+
257
+        // pre-upgrade repairs
258
+        $repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->get(LoggerInterface::class));
259
+        $repair->run();
260
+
261
+        $this->doCoreUpgrade();
262
+
263
+        try {
264
+            // TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
265
+            Setup::installBackgroundJobs();
266
+        } catch (\Exception $e) {
267
+            throw new \Exception($e->getMessage());
268
+        }
269
+
270
+        // update all shipped apps
271
+        $this->checkAppsRequirements();
272
+        $this->doAppUpgrade();
273
+
274
+        // Update the appfetchers version so it downloads the correct list from the appstore
275
+        \OC::$server->getAppFetcher()->setVersion($currentVersion);
276
+
277
+        /** @var AppManager $appManager */
278
+        $appManager = \OC::$server->getAppManager();
279
+
280
+        // upgrade appstore apps
281
+        $this->upgradeAppStoreApps($appManager->getInstalledApps());
282
+        $autoDisabledApps = $appManager->getAutoDisabledApps();
283
+        if (!empty($autoDisabledApps)) {
284
+            $this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
285
+        }
286
+
287
+        // install new shipped apps on upgrade
288
+        $errors = Installer::installShippedApps(true);
289
+        foreach ($errors as $appId => $exception) {
290
+            /** @var \Exception $exception */
291
+            $this->log->error($exception->getMessage(), [
292
+                'exception' => $exception,
293
+                'app' => $appId,
294
+            ]);
295
+            $this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
296
+        }
297
+
298
+        // post-upgrade repairs
299
+        $repair = new Repair(Repair::getRepairSteps(), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->get(LoggerInterface::class));
300
+        $repair->run();
301
+
302
+        //Invalidate update feed
303
+        $this->config->setAppValue('core', 'lastupdatedat', '0');
304
+
305
+        // Check for code integrity if not disabled
306
+        if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
307
+            $this->emit('\OC\Updater', 'startCheckCodeIntegrity');
308
+            $this->checker->runInstanceVerification();
309
+            $this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
310
+        }
311
+
312
+        // only set the final version if everything went well
313
+        $this->config->setSystemValue('version', implode('.', Util::getVersion()));
314
+        $this->config->setAppValue('core', 'vendor', $this->getVendor());
315
+    }
316
+
317
+    protected function doCoreUpgrade(): void {
318
+        $this->emit('\OC\Updater', 'dbUpgradeBefore');
319
+
320
+        // execute core migrations
321
+        $ms = new MigrationService('core', \OC::$server->get(Connection::class));
322
+        $ms->migrate();
323
+
324
+        $this->emit('\OC\Updater', 'dbUpgrade');
325
+    }
326
+
327
+    /**
328
+     * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
329
+     * (types authentication, filesystem, logging, in that order) afterwards.
330
+     *
331
+     * @throws NeedsUpdateException
332
+     */
333
+    protected function doAppUpgrade(): void {
334
+        $apps = \OC_App::getEnabledApps();
335
+        $priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging'];
336
+        $pseudoOtherType = 'other';
337
+        $stacks = [$pseudoOtherType => []];
338
+
339
+        foreach ($apps as $appId) {
340
+            $priorityType = false;
341
+            foreach ($priorityTypes as $type) {
342
+                if (!isset($stacks[$type])) {
343
+                    $stacks[$type] = [];
344
+                }
345
+                if (\OC_App::isType($appId, [$type])) {
346
+                    $stacks[$type][] = $appId;
347
+                    $priorityType = true;
348
+                    break;
349
+                }
350
+            }
351
+            if (!$priorityType) {
352
+                $stacks[$pseudoOtherType][] = $appId;
353
+            }
354
+        }
355
+        foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
356
+            $stack = $stacks[$type];
357
+            foreach ($stack as $appId) {
358
+                if (\OC_App::shouldUpgrade($appId)) {
359
+                    $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
360
+                    \OC_App::updateApp($appId);
361
+                    $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
362
+                }
363
+                if ($type !== $pseudoOtherType) {
364
+                    // load authentication, filesystem and logging apps after
365
+                    // upgrading them. Other apps my need to rely on modifying
366
+                    // user and/or filesystem aspects.
367
+                    \OC_App::loadApp($appId);
368
+                }
369
+            }
370
+        }
371
+    }
372
+
373
+    /**
374
+     * check if the current enabled apps are compatible with the current
375
+     * ownCloud version. disable them if not.
376
+     * This is important if you upgrade ownCloud and have non ported 3rd
377
+     * party apps installed.
378
+     *
379
+     * @throws \Exception
380
+     */
381
+    private function checkAppsRequirements(): void {
382
+        $isCoreUpgrade = $this->isCodeUpgrade();
383
+        $apps = OC_App::getEnabledApps();
384
+        $version = implode('.', Util::getVersion());
385
+        $appManager = \OC::$server->getAppManager();
386
+        foreach ($apps as $app) {
387
+            // check if the app is compatible with this version of Nextcloud
388
+            $info = $appManager->getAppInfo($app);
389
+            if ($info === null || !OC_App::isAppCompatible($version, $info)) {
390
+                if ($appManager->isShipped($app)) {
391
+                    throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
392
+                }
393
+                $appManager->disableApp($app, true);
394
+                $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
395
+            }
396
+        }
397
+    }
398
+
399
+    /**
400
+     * @return bool
401
+     */
402
+    private function isCodeUpgrade(): bool {
403
+        $installedVersion = $this->config->getSystemValueString('version', '0.0.0');
404
+        $currentVersion = implode('.', Util::getVersion());
405
+        if (version_compare($currentVersion, $installedVersion, '>')) {
406
+            return true;
407
+        }
408
+        return false;
409
+    }
410
+
411
+    /**
412
+     * @param array $apps
413
+     * @param array $previousEnableStates
414
+     * @throws \Exception
415
+     */
416
+    private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void {
417
+        foreach ($apps as $app) {
418
+            try {
419
+                $this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]);
420
+                if ($this->installer->isUpdateAvailable($app)) {
421
+                    $this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
422
+                    $this->installer->updateAppstoreApp($app);
423
+                }
424
+                $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
425
+
426
+                if (!empty($previousEnableStates)) {
427
+                    $ocApp = new \OC_App();
428
+                    if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
429
+                        $ocApp->enable($app, $previousEnableStates[$app]);
430
+                    } else {
431
+                        $ocApp->enable($app);
432
+                    }
433
+                }
434
+            } catch (\Exception $ex) {
435
+                $this->log->error($ex->getMessage(), [
436
+                    'exception' => $ex,
437
+                ]);
438
+            }
439
+        }
440
+    }
441
+
442
+    private function logAllEvents(): void {
443
+        $log = $this->log;
444
+
445
+        /** @var IEventDispatcher $dispatcher */
446
+        $dispatcher = \OC::$server->get(IEventDispatcher::class);
447
+        $dispatcher->addListener(
448
+            MigratorExecuteSqlEvent::class,
449
+            function (MigratorExecuteSqlEvent $event) use ($log): void {
450
+                $log->info(get_class($event).': ' . $event->getSql() . ' (' . $event->getCurrentStep() . ' of ' . $event->getMaxStep() . ')', ['app' => 'updater']);
451
+            }
452
+        );
453
+
454
+        $repairListener = function (Event $event) use ($log): void {
455
+            if ($event instanceof RepairStartEvent) {
456
+                $log->info(get_class($event).': Starting ... ' . $event->getMaxStep() .  ' (' . $event->getCurrentStepName() . ')', ['app' => 'updater']);
457
+            } elseif ($event instanceof RepairAdvanceEvent) {
458
+                $desc = $event->getDescription();
459
+                if (empty($desc)) {
460
+                    $desc = '';
461
+                }
462
+                $log->info(get_class($event).': ' . $desc . ' (' . $event->getIncrement() . ')', ['app' => 'updater']);
463
+            } elseif ($event instanceof RepairFinishEvent) {
464
+                $log->info(get_class($event), ['app' => 'updater']);
465
+            } elseif ($event instanceof RepairStepEvent) {
466
+                $log->info(get_class($event).': Repair step: ' . $event->getStepName(), ['app' => 'updater']);
467
+            } elseif ($event instanceof RepairInfoEvent) {
468
+                $log->info(get_class($event).': Repair info: ' . $event->getMessage(), ['app' => 'updater']);
469
+            } elseif ($event instanceof RepairWarningEvent) {
470
+                $log->warning(get_class($event).': Repair warning: ' . $event->getMessage(), ['app' => 'updater']);
471
+            } elseif ($event instanceof RepairErrorEvent) {
472
+                $log->error(get_class($event).': Repair error: ' . $event->getMessage(), ['app' => 'updater']);
473
+            }
474
+        };
475
+
476
+        $dispatcher->addListener(RepairStartEvent::class, $repairListener);
477
+        $dispatcher->addListener(RepairAdvanceEvent::class, $repairListener);
478
+        $dispatcher->addListener(RepairFinishEvent::class, $repairListener);
479
+        $dispatcher->addListener(RepairStepEvent::class, $repairListener);
480
+        $dispatcher->addListener(RepairInfoEvent::class, $repairListener);
481
+        $dispatcher->addListener(RepairWarningEvent::class, $repairListener);
482
+        $dispatcher->addListener(RepairErrorEvent::class, $repairListener);
483
+
484
+
485
+        $this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) {
486
+            $log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']);
487
+        });
488
+        $this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) {
489
+            $log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']);
490
+        });
491
+        $this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) {
492
+            $log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']);
493
+        });
494
+        $this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) {
495
+            if ($success) {
496
+                $log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']);
497
+            } else {
498
+                $log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']);
499
+            }
500
+        });
501
+        $this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) {
502
+            $log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']);
503
+        });
504
+        $this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) {
505
+            $log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']);
506
+        });
507
+        $this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) {
508
+            $log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']);
509
+        });
510
+        $this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) {
511
+            $log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']);
512
+        });
513
+        $this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) {
514
+            $log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']);
515
+        });
516
+        $this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) {
517
+            $log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']);
518
+        });
519
+        $this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) {
520
+            $log->info('\OC\Updater::appSimulateUpdate: Checking whether the database schema for <' . $app . '> can be updated (this can take a long time depending on the database size)', ['app' => 'updater']);
521
+        });
522
+        $this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) {
523
+            $log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']);
524
+        });
525
+        $this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) {
526
+            $log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']);
527
+        });
528
+        $this->listen('\OC\Updater', 'failure', function ($message) use ($log) {
529
+            $log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']);
530
+        });
531
+        $this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) {
532
+            $log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']);
533
+        });
534
+        $this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) {
535
+            $log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']);
536
+        });
537
+        $this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) {
538
+            $log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']);
539
+        });
540
+        $this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) {
541
+            $log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']);
542
+        });
543
+    }
544 544
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 2 patches
Indentation   +568 added lines, -568 removed lines patch added patch discarded remove patch
@@ -60,572 +60,572 @@
 block discarded – undo
60 60
  * This class provides the functionality needed to install, update and remove apps
61 61
  */
62 62
 class Installer {
63
-	/** @var AppFetcher */
64
-	private $appFetcher;
65
-	/** @var IClientService */
66
-	private $clientService;
67
-	/** @var ITempManager */
68
-	private $tempManager;
69
-	/** @var LoggerInterface */
70
-	private $logger;
71
-	/** @var IConfig */
72
-	private $config;
73
-	/** @var array - for caching the result of app fetcher */
74
-	private $apps = null;
75
-	/** @var bool|null - for caching the result of the ready status */
76
-	private $isInstanceReadyForUpdates = null;
77
-	/** @var bool */
78
-	private $isCLI;
79
-
80
-	public function __construct(
81
-		AppFetcher $appFetcher,
82
-		IClientService $clientService,
83
-		ITempManager $tempManager,
84
-		LoggerInterface $logger,
85
-		IConfig $config,
86
-		bool $isCLI
87
-	) {
88
-		$this->appFetcher = $appFetcher;
89
-		$this->clientService = $clientService;
90
-		$this->tempManager = $tempManager;
91
-		$this->logger = $logger;
92
-		$this->config = $config;
93
-		$this->isCLI = $isCLI;
94
-	}
95
-
96
-	/**
97
-	 * Installs an app that is located in one of the app folders already
98
-	 *
99
-	 * @param string $appId App to install
100
-	 * @param bool $forceEnable
101
-	 * @throws \Exception
102
-	 * @return string app ID
103
-	 */
104
-	public function installApp(string $appId, bool $forceEnable = false): string {
105
-		$app = \OC_App::findAppInDirectories($appId);
106
-		if ($app === false) {
107
-			throw new \Exception('App not found in any app directory');
108
-		}
109
-
110
-		$basedir = $app['path'].'/'.$appId;
111
-
112
-		if (is_file($basedir . '/appinfo/database.xml')) {
113
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
114
-		}
115
-
116
-		$l = \OC::$server->getL10N('core');
117
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($basedir . '/appinfo/info.xml', true, $l->getLanguageCode());
118
-
119
-		if (!is_array($info)) {
120
-			throw new \Exception(
121
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
122
-					[$appId]
123
-				)
124
-			);
125
-		}
126
-
127
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
128
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
129
-
130
-		$version = implode('.', \OCP\Util::getVersion());
131
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
132
-			throw new \Exception(
133
-				// TODO $l
134
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
135
-					[$info['name']]
136
-				)
137
-			);
138
-		}
139
-
140
-		// check for required dependencies
141
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
142
-		/** @var Coordinator $coordinator */
143
-		$coordinator = \OC::$server->get(Coordinator::class);
144
-		$coordinator->runLazyRegistration($appId);
145
-		\OC_App::registerAutoloading($appId, $basedir);
146
-
147
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
148
-		if ($previousVersion) {
149
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
150
-		}
151
-
152
-		//install the database
153
-		$ms = new MigrationService($info['id'], \OC::$server->get(Connection::class));
154
-		$ms->migrate('latest', !$previousVersion);
155
-
156
-		if ($previousVersion) {
157
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
158
-		}
159
-
160
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
161
-
162
-		//run appinfo/install.php
163
-		self::includeAppScript($basedir . '/appinfo/install.php');
164
-
165
-		OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
166
-
167
-		//set the installed version
168
-		\OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
169
-		\OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
170
-
171
-		//set remote/public handlers
172
-		foreach ($info['remote'] as $name => $path) {
173
-			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
174
-		}
175
-		foreach ($info['public'] as $name => $path) {
176
-			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
177
-		}
178
-
179
-		OC_App::setAppTypes($info['id']);
180
-
181
-		return $info['id'];
182
-	}
183
-
184
-	/**
185
-	 * Updates the specified app from the appstore
186
-	 *
187
-	 * @param string $appId
188
-	 * @param bool [$allowUnstable] Allow unstable releases
189
-	 * @return bool
190
-	 */
191
-	public function updateAppstoreApp($appId, $allowUnstable = false) {
192
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
193
-			try {
194
-				$this->downloadApp($appId, $allowUnstable);
195
-			} catch (\Exception $e) {
196
-				$this->logger->error($e->getMessage(), [
197
-					'exception' => $e,
198
-				]);
199
-				return false;
200
-			}
201
-			return OC_App::updateApp($appId);
202
-		}
203
-
204
-		return false;
205
-	}
206
-
207
-	/**
208
-	 * Split the certificate file in individual certs
209
-	 *
210
-	 * @param string $cert
211
-	 * @return string[]
212
-	 */
213
-	private function splitCerts(string $cert): array {
214
-		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
215
-
216
-		return $matches[0];
217
-	}
218
-
219
-	/**
220
-	 * Downloads an app and puts it into the app directory
221
-	 *
222
-	 * @param string $appId
223
-	 * @param bool [$allowUnstable]
224
-	 *
225
-	 * @throws \Exception If the installation was not successful
226
-	 */
227
-	public function downloadApp($appId, $allowUnstable = false) {
228
-		$appId = strtolower($appId);
229
-
230
-		$apps = $this->appFetcher->get($allowUnstable);
231
-		foreach ($apps as $app) {
232
-			if ($app['id'] === $appId) {
233
-				// Load the certificate
234
-				$certificate = new X509();
235
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
236
-				$rootCrts = $this->splitCerts($rootCrt);
237
-				foreach ($rootCrts as $rootCrt) {
238
-					$certificate->loadCA($rootCrt);
239
-				}
240
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
241
-
242
-				// Verify if the certificate has been revoked
243
-				$crl = new X509();
244
-				foreach ($rootCrts as $rootCrt) {
245
-					$crl->loadCA($rootCrt);
246
-				}
247
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
248
-				if ($crl->validateSignature() !== true) {
249
-					throw new \Exception('Could not validate CRL signature');
250
-				}
251
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
252
-				$revoked = $crl->getRevoked($csn);
253
-				if ($revoked !== false) {
254
-					throw new \Exception(
255
-						sprintf(
256
-							'Certificate "%s" has been revoked',
257
-							$csn
258
-						)
259
-					);
260
-				}
261
-
262
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
263
-				if ($certificate->validateSignature() !== true) {
264
-					throw new \Exception(
265
-						sprintf(
266
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
267
-							$appId
268
-						)
269
-					);
270
-				}
271
-
272
-				// Verify if the certificate is issued for the requested app id
273
-				$certInfo = openssl_x509_parse($app['certificate']);
274
-				if (!isset($certInfo['subject']['CN'])) {
275
-					throw new \Exception(
276
-						sprintf(
277
-							'App with id %s has a cert with no CN',
278
-							$appId
279
-						)
280
-					);
281
-				}
282
-				if ($certInfo['subject']['CN'] !== $appId) {
283
-					throw new \Exception(
284
-						sprintf(
285
-							'App with id %s has a cert issued to %s',
286
-							$appId,
287
-							$certInfo['subject']['CN']
288
-						)
289
-					);
290
-				}
291
-
292
-				// Download the release
293
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
294
-				$timeout = $this->isCLI ? 0 : 120;
295
-				$client = $this->clientService->newClient();
296
-				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
297
-
298
-				// Check if the signature actually matches the downloaded content
299
-				$certificate = openssl_get_publickey($app['certificate']);
300
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
301
-				// PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
302
-				if ((PHP_VERSION_ID < 80000)) {
303
-					openssl_free_key($certificate);
304
-				}
305
-
306
-				if ($verified === true) {
307
-					// Seems to match, let's proceed
308
-					$extractDir = $this->tempManager->getTemporaryFolder();
309
-					$archive = new TAR($tempFile);
310
-
311
-					if (!$archive->extract($extractDir)) {
312
-						$errorMessage = 'Could not extract app ' . $appId;
313
-
314
-						$archiveError = $archive->getError();
315
-						if ($archiveError instanceof \PEAR_Error) {
316
-							$errorMessage .= ': ' . $archiveError->getMessage();
317
-						}
318
-
319
-						throw new \Exception($errorMessage);
320
-					}
321
-					$allFiles = scandir($extractDir);
322
-					$folders = array_diff($allFiles, ['.', '..']);
323
-					$folders = array_values($folders);
324
-
325
-					if (count($folders) > 1) {
326
-						throw new \Exception(
327
-							sprintf(
328
-								'Extracted app %s has more than 1 folder',
329
-								$appId
330
-							)
331
-						);
332
-					}
333
-
334
-					// Check if appinfo/info.xml has the same app ID as well
335
-					if ((PHP_VERSION_ID < 80000)) {
336
-						$loadEntities = libxml_disable_entity_loader(false);
337
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
338
-						libxml_disable_entity_loader($loadEntities);
339
-					} else {
340
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
341
-					}
342
-					if ((string)$xml->id !== $appId) {
343
-						throw new \Exception(
344
-							sprintf(
345
-								'App for id %s has a wrong app ID in info.xml: %s',
346
-								$appId,
347
-								(string)$xml->id
348
-							)
349
-						);
350
-					}
351
-
352
-					// Check if the version is lower than before
353
-					$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
354
-					$newVersion = (string)$xml->version;
355
-					if (version_compare($currentVersion, $newVersion) === 1) {
356
-						throw new \Exception(
357
-							sprintf(
358
-								'App for id %s has version %s and tried to update to lower version %s',
359
-								$appId,
360
-								$currentVersion,
361
-								$newVersion
362
-							)
363
-						);
364
-					}
365
-
366
-					$baseDir = OC_App::getInstallPath() . '/' . $appId;
367
-					// Remove old app with the ID if existent
368
-					OC_Helper::rmdirr($baseDir);
369
-					// Move to app folder
370
-					if (@mkdir($baseDir)) {
371
-						$extractDir .= '/' . $folders[0];
372
-						OC_Helper::copyr($extractDir, $baseDir);
373
-					}
374
-					OC_Helper::copyr($extractDir, $baseDir);
375
-					OC_Helper::rmdirr($extractDir);
376
-					return;
377
-				}
378
-				// Signature does not match
379
-				throw new \Exception(
380
-					sprintf(
381
-						'App with id %s has invalid signature',
382
-						$appId
383
-					)
384
-				);
385
-			}
386
-		}
387
-
388
-		throw new \Exception(
389
-			sprintf(
390
-				'Could not download app %s',
391
-				$appId
392
-			)
393
-		);
394
-	}
395
-
396
-	/**
397
-	 * Check if an update for the app is available
398
-	 *
399
-	 * @param string $appId
400
-	 * @param bool $allowUnstable
401
-	 * @return string|false false or the version number of the update
402
-	 */
403
-	public function isUpdateAvailable($appId, $allowUnstable = false) {
404
-		if ($this->isInstanceReadyForUpdates === null) {
405
-			$installPath = OC_App::getInstallPath();
406
-			if ($installPath === false || $installPath === null) {
407
-				$this->isInstanceReadyForUpdates = false;
408
-			} else {
409
-				$this->isInstanceReadyForUpdates = true;
410
-			}
411
-		}
412
-
413
-		if ($this->isInstanceReadyForUpdates === false) {
414
-			return false;
415
-		}
416
-
417
-		if ($this->isInstalledFromGit($appId) === true) {
418
-			return false;
419
-		}
420
-
421
-		if ($this->apps === null) {
422
-			$this->apps = $this->appFetcher->get($allowUnstable);
423
-		}
424
-
425
-		foreach ($this->apps as $app) {
426
-			if ($app['id'] === $appId) {
427
-				$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
428
-
429
-				if (!isset($app['releases'][0]['version'])) {
430
-					return false;
431
-				}
432
-				$newestVersion = $app['releases'][0]['version'];
433
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
434
-					return $newestVersion;
435
-				} else {
436
-					return false;
437
-				}
438
-			}
439
-		}
440
-
441
-		return false;
442
-	}
443
-
444
-	/**
445
-	 * Check if app has been installed from git
446
-	 * @param string $name name of the application to remove
447
-	 * @return boolean
448
-	 *
449
-	 * The function will check if the path contains a .git folder
450
-	 */
451
-	private function isInstalledFromGit($appId) {
452
-		$app = \OC_App::findAppInDirectories($appId);
453
-		if ($app === false) {
454
-			return false;
455
-		}
456
-		$basedir = $app['path'].'/'.$appId;
457
-		return file_exists($basedir.'/.git/');
458
-	}
459
-
460
-	/**
461
-	 * Check if app is already downloaded
462
-	 * @param string $name name of the application to remove
463
-	 * @return boolean
464
-	 *
465
-	 * The function will check if the app is already downloaded in the apps repository
466
-	 */
467
-	public function isDownloaded($name) {
468
-		foreach (\OC::$APPSROOTS as $dir) {
469
-			$dirToTest = $dir['path'];
470
-			$dirToTest .= '/';
471
-			$dirToTest .= $name;
472
-			$dirToTest .= '/';
473
-
474
-			if (is_dir($dirToTest)) {
475
-				return true;
476
-			}
477
-		}
478
-
479
-		return false;
480
-	}
481
-
482
-	/**
483
-	 * Removes an app
484
-	 * @param string $appId ID of the application to remove
485
-	 * @return boolean
486
-	 *
487
-	 *
488
-	 * This function works as follows
489
-	 *   -# call uninstall repair steps
490
-	 *   -# removing the files
491
-	 *
492
-	 * The function will not delete preferences, tables and the configuration,
493
-	 * this has to be done by the function oc_app_uninstall().
494
-	 */
495
-	public function removeApp($appId) {
496
-		if ($this->isDownloaded($appId)) {
497
-			if (\OC::$server->getAppManager()->isShipped($appId)) {
498
-				return false;
499
-			}
500
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
501
-			OC_Helper::rmdirr($appDir);
502
-			return true;
503
-		} else {
504
-			$this->logger->error('can\'t remove app '.$appId.'. It is not installed.');
505
-
506
-			return false;
507
-		}
508
-	}
509
-
510
-	/**
511
-	 * Installs the app within the bundle and marks the bundle as installed
512
-	 *
513
-	 * @param Bundle $bundle
514
-	 * @throws \Exception If app could not get installed
515
-	 */
516
-	public function installAppBundle(Bundle $bundle) {
517
-		$appIds = $bundle->getAppIdentifiers();
518
-		foreach ($appIds as $appId) {
519
-			if (!$this->isDownloaded($appId)) {
520
-				$this->downloadApp($appId);
521
-			}
522
-			$this->installApp($appId);
523
-			$app = new OC_App();
524
-			$app->enable($appId);
525
-		}
526
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
527
-		$bundles[] = $bundle->getIdentifier();
528
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
529
-	}
530
-
531
-	/**
532
-	 * Installs shipped apps
533
-	 *
534
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
535
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
536
-	 *                         working ownCloud at the end instead of an aborted update.
537
-	 * @return array Array of error messages (appid => Exception)
538
-	 */
539
-	public static function installShippedApps($softErrors = false) {
540
-		$appManager = \OC::$server->getAppManager();
541
-		$config = \OC::$server->getConfig();
542
-		$errors = [];
543
-		foreach (\OC::$APPSROOTS as $app_dir) {
544
-			if ($dir = opendir($app_dir['path'])) {
545
-				while (false !== ($filename = readdir($dir))) {
546
-					if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
547
-						if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
548
-							if ($config->getAppValue($filename, "installed_version", null) === null) {
549
-								$enabled = $appManager->isDefaultEnabled($filename);
550
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
551
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
552
-									if ($softErrors) {
553
-										try {
554
-											Installer::installShippedApp($filename);
555
-										} catch (HintException $e) {
556
-											if ($e->getPrevious() instanceof TableExistsException) {
557
-												$errors[$filename] = $e;
558
-												continue;
559
-											}
560
-											throw $e;
561
-										}
562
-									} else {
563
-										Installer::installShippedApp($filename);
564
-									}
565
-									$config->setAppValue($filename, 'enabled', 'yes');
566
-								}
567
-							}
568
-						}
569
-					}
570
-				}
571
-				closedir($dir);
572
-			}
573
-		}
574
-
575
-		return $errors;
576
-	}
577
-
578
-	/**
579
-	 * install an app already placed in the app folder
580
-	 * @param string $app id of the app to install
581
-	 * @return integer
582
-	 */
583
-	public static function installShippedApp($app) {
584
-		//install the database
585
-		$appPath = OC_App::getAppPath($app);
586
-		\OC_App::registerAutoloading($app, $appPath);
587
-
588
-		$config = \OC::$server->getConfig();
589
-
590
-		$ms = new MigrationService($app, \OC::$server->get(Connection::class));
591
-		$previousVersion = $config->getAppValue($app, 'installed_version', false);
592
-		$ms->migrate('latest', !$previousVersion);
593
-
594
-		//run appinfo/install.php
595
-		self::includeAppScript("$appPath/appinfo/install.php");
596
-
597
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
598
-		if (is_null($info)) {
599
-			return false;
600
-		}
601
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
602
-
603
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
604
-
605
-		$config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
606
-		if (array_key_exists('ocsid', $info)) {
607
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
608
-		}
609
-
610
-		//set remote/public handlers
611
-		foreach ($info['remote'] as $name => $path) {
612
-			$config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
613
-		}
614
-		foreach ($info['public'] as $name => $path) {
615
-			$config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
616
-		}
617
-
618
-		OC_App::setAppTypes($info['id']);
619
-
620
-		return $info['id'];
621
-	}
622
-
623
-	/**
624
-	 * @param string $script
625
-	 */
626
-	private static function includeAppScript($script) {
627
-		if (file_exists($script)) {
628
-			include $script;
629
-		}
630
-	}
63
+    /** @var AppFetcher */
64
+    private $appFetcher;
65
+    /** @var IClientService */
66
+    private $clientService;
67
+    /** @var ITempManager */
68
+    private $tempManager;
69
+    /** @var LoggerInterface */
70
+    private $logger;
71
+    /** @var IConfig */
72
+    private $config;
73
+    /** @var array - for caching the result of app fetcher */
74
+    private $apps = null;
75
+    /** @var bool|null - for caching the result of the ready status */
76
+    private $isInstanceReadyForUpdates = null;
77
+    /** @var bool */
78
+    private $isCLI;
79
+
80
+    public function __construct(
81
+        AppFetcher $appFetcher,
82
+        IClientService $clientService,
83
+        ITempManager $tempManager,
84
+        LoggerInterface $logger,
85
+        IConfig $config,
86
+        bool $isCLI
87
+    ) {
88
+        $this->appFetcher = $appFetcher;
89
+        $this->clientService = $clientService;
90
+        $this->tempManager = $tempManager;
91
+        $this->logger = $logger;
92
+        $this->config = $config;
93
+        $this->isCLI = $isCLI;
94
+    }
95
+
96
+    /**
97
+     * Installs an app that is located in one of the app folders already
98
+     *
99
+     * @param string $appId App to install
100
+     * @param bool $forceEnable
101
+     * @throws \Exception
102
+     * @return string app ID
103
+     */
104
+    public function installApp(string $appId, bool $forceEnable = false): string {
105
+        $app = \OC_App::findAppInDirectories($appId);
106
+        if ($app === false) {
107
+            throw new \Exception('App not found in any app directory');
108
+        }
109
+
110
+        $basedir = $app['path'].'/'.$appId;
111
+
112
+        if (is_file($basedir . '/appinfo/database.xml')) {
113
+            throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
114
+        }
115
+
116
+        $l = \OC::$server->getL10N('core');
117
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfo($basedir . '/appinfo/info.xml', true, $l->getLanguageCode());
118
+
119
+        if (!is_array($info)) {
120
+            throw new \Exception(
121
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
122
+                    [$appId]
123
+                )
124
+            );
125
+        }
126
+
127
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
128
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
129
+
130
+        $version = implode('.', \OCP\Util::getVersion());
131
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
132
+            throw new \Exception(
133
+                // TODO $l
134
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
135
+                    [$info['name']]
136
+                )
137
+            );
138
+        }
139
+
140
+        // check for required dependencies
141
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
142
+        /** @var Coordinator $coordinator */
143
+        $coordinator = \OC::$server->get(Coordinator::class);
144
+        $coordinator->runLazyRegistration($appId);
145
+        \OC_App::registerAutoloading($appId, $basedir);
146
+
147
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
148
+        if ($previousVersion) {
149
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
150
+        }
151
+
152
+        //install the database
153
+        $ms = new MigrationService($info['id'], \OC::$server->get(Connection::class));
154
+        $ms->migrate('latest', !$previousVersion);
155
+
156
+        if ($previousVersion) {
157
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
158
+        }
159
+
160
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
161
+
162
+        //run appinfo/install.php
163
+        self::includeAppScript($basedir . '/appinfo/install.php');
164
+
165
+        OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
166
+
167
+        //set the installed version
168
+        \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
169
+        \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
170
+
171
+        //set remote/public handlers
172
+        foreach ($info['remote'] as $name => $path) {
173
+            \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
174
+        }
175
+        foreach ($info['public'] as $name => $path) {
176
+            \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
177
+        }
178
+
179
+        OC_App::setAppTypes($info['id']);
180
+
181
+        return $info['id'];
182
+    }
183
+
184
+    /**
185
+     * Updates the specified app from the appstore
186
+     *
187
+     * @param string $appId
188
+     * @param bool [$allowUnstable] Allow unstable releases
189
+     * @return bool
190
+     */
191
+    public function updateAppstoreApp($appId, $allowUnstable = false) {
192
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
193
+            try {
194
+                $this->downloadApp($appId, $allowUnstable);
195
+            } catch (\Exception $e) {
196
+                $this->logger->error($e->getMessage(), [
197
+                    'exception' => $e,
198
+                ]);
199
+                return false;
200
+            }
201
+            return OC_App::updateApp($appId);
202
+        }
203
+
204
+        return false;
205
+    }
206
+
207
+    /**
208
+     * Split the certificate file in individual certs
209
+     *
210
+     * @param string $cert
211
+     * @return string[]
212
+     */
213
+    private function splitCerts(string $cert): array {
214
+        preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
215
+
216
+        return $matches[0];
217
+    }
218
+
219
+    /**
220
+     * Downloads an app and puts it into the app directory
221
+     *
222
+     * @param string $appId
223
+     * @param bool [$allowUnstable]
224
+     *
225
+     * @throws \Exception If the installation was not successful
226
+     */
227
+    public function downloadApp($appId, $allowUnstable = false) {
228
+        $appId = strtolower($appId);
229
+
230
+        $apps = $this->appFetcher->get($allowUnstable);
231
+        foreach ($apps as $app) {
232
+            if ($app['id'] === $appId) {
233
+                // Load the certificate
234
+                $certificate = new X509();
235
+                $rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
236
+                $rootCrts = $this->splitCerts($rootCrt);
237
+                foreach ($rootCrts as $rootCrt) {
238
+                    $certificate->loadCA($rootCrt);
239
+                }
240
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
241
+
242
+                // Verify if the certificate has been revoked
243
+                $crl = new X509();
244
+                foreach ($rootCrts as $rootCrt) {
245
+                    $crl->loadCA($rootCrt);
246
+                }
247
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
248
+                if ($crl->validateSignature() !== true) {
249
+                    throw new \Exception('Could not validate CRL signature');
250
+                }
251
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
252
+                $revoked = $crl->getRevoked($csn);
253
+                if ($revoked !== false) {
254
+                    throw new \Exception(
255
+                        sprintf(
256
+                            'Certificate "%s" has been revoked',
257
+                            $csn
258
+                        )
259
+                    );
260
+                }
261
+
262
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
263
+                if ($certificate->validateSignature() !== true) {
264
+                    throw new \Exception(
265
+                        sprintf(
266
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
267
+                            $appId
268
+                        )
269
+                    );
270
+                }
271
+
272
+                // Verify if the certificate is issued for the requested app id
273
+                $certInfo = openssl_x509_parse($app['certificate']);
274
+                if (!isset($certInfo['subject']['CN'])) {
275
+                    throw new \Exception(
276
+                        sprintf(
277
+                            'App with id %s has a cert with no CN',
278
+                            $appId
279
+                        )
280
+                    );
281
+                }
282
+                if ($certInfo['subject']['CN'] !== $appId) {
283
+                    throw new \Exception(
284
+                        sprintf(
285
+                            'App with id %s has a cert issued to %s',
286
+                            $appId,
287
+                            $certInfo['subject']['CN']
288
+                        )
289
+                    );
290
+                }
291
+
292
+                // Download the release
293
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
294
+                $timeout = $this->isCLI ? 0 : 120;
295
+                $client = $this->clientService->newClient();
296
+                $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
297
+
298
+                // Check if the signature actually matches the downloaded content
299
+                $certificate = openssl_get_publickey($app['certificate']);
300
+                $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
301
+                // PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
302
+                if ((PHP_VERSION_ID < 80000)) {
303
+                    openssl_free_key($certificate);
304
+                }
305
+
306
+                if ($verified === true) {
307
+                    // Seems to match, let's proceed
308
+                    $extractDir = $this->tempManager->getTemporaryFolder();
309
+                    $archive = new TAR($tempFile);
310
+
311
+                    if (!$archive->extract($extractDir)) {
312
+                        $errorMessage = 'Could not extract app ' . $appId;
313
+
314
+                        $archiveError = $archive->getError();
315
+                        if ($archiveError instanceof \PEAR_Error) {
316
+                            $errorMessage .= ': ' . $archiveError->getMessage();
317
+                        }
318
+
319
+                        throw new \Exception($errorMessage);
320
+                    }
321
+                    $allFiles = scandir($extractDir);
322
+                    $folders = array_diff($allFiles, ['.', '..']);
323
+                    $folders = array_values($folders);
324
+
325
+                    if (count($folders) > 1) {
326
+                        throw new \Exception(
327
+                            sprintf(
328
+                                'Extracted app %s has more than 1 folder',
329
+                                $appId
330
+                            )
331
+                        );
332
+                    }
333
+
334
+                    // Check if appinfo/info.xml has the same app ID as well
335
+                    if ((PHP_VERSION_ID < 80000)) {
336
+                        $loadEntities = libxml_disable_entity_loader(false);
337
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
338
+                        libxml_disable_entity_loader($loadEntities);
339
+                    } else {
340
+                        $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
341
+                    }
342
+                    if ((string)$xml->id !== $appId) {
343
+                        throw new \Exception(
344
+                            sprintf(
345
+                                'App for id %s has a wrong app ID in info.xml: %s',
346
+                                $appId,
347
+                                (string)$xml->id
348
+                            )
349
+                        );
350
+                    }
351
+
352
+                    // Check if the version is lower than before
353
+                    $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
354
+                    $newVersion = (string)$xml->version;
355
+                    if (version_compare($currentVersion, $newVersion) === 1) {
356
+                        throw new \Exception(
357
+                            sprintf(
358
+                                'App for id %s has version %s and tried to update to lower version %s',
359
+                                $appId,
360
+                                $currentVersion,
361
+                                $newVersion
362
+                            )
363
+                        );
364
+                    }
365
+
366
+                    $baseDir = OC_App::getInstallPath() . '/' . $appId;
367
+                    // Remove old app with the ID if existent
368
+                    OC_Helper::rmdirr($baseDir);
369
+                    // Move to app folder
370
+                    if (@mkdir($baseDir)) {
371
+                        $extractDir .= '/' . $folders[0];
372
+                        OC_Helper::copyr($extractDir, $baseDir);
373
+                    }
374
+                    OC_Helper::copyr($extractDir, $baseDir);
375
+                    OC_Helper::rmdirr($extractDir);
376
+                    return;
377
+                }
378
+                // Signature does not match
379
+                throw new \Exception(
380
+                    sprintf(
381
+                        'App with id %s has invalid signature',
382
+                        $appId
383
+                    )
384
+                );
385
+            }
386
+        }
387
+
388
+        throw new \Exception(
389
+            sprintf(
390
+                'Could not download app %s',
391
+                $appId
392
+            )
393
+        );
394
+    }
395
+
396
+    /**
397
+     * Check if an update for the app is available
398
+     *
399
+     * @param string $appId
400
+     * @param bool $allowUnstable
401
+     * @return string|false false or the version number of the update
402
+     */
403
+    public function isUpdateAvailable($appId, $allowUnstable = false) {
404
+        if ($this->isInstanceReadyForUpdates === null) {
405
+            $installPath = OC_App::getInstallPath();
406
+            if ($installPath === false || $installPath === null) {
407
+                $this->isInstanceReadyForUpdates = false;
408
+            } else {
409
+                $this->isInstanceReadyForUpdates = true;
410
+            }
411
+        }
412
+
413
+        if ($this->isInstanceReadyForUpdates === false) {
414
+            return false;
415
+        }
416
+
417
+        if ($this->isInstalledFromGit($appId) === true) {
418
+            return false;
419
+        }
420
+
421
+        if ($this->apps === null) {
422
+            $this->apps = $this->appFetcher->get($allowUnstable);
423
+        }
424
+
425
+        foreach ($this->apps as $app) {
426
+            if ($app['id'] === $appId) {
427
+                $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
428
+
429
+                if (!isset($app['releases'][0]['version'])) {
430
+                    return false;
431
+                }
432
+                $newestVersion = $app['releases'][0]['version'];
433
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
434
+                    return $newestVersion;
435
+                } else {
436
+                    return false;
437
+                }
438
+            }
439
+        }
440
+
441
+        return false;
442
+    }
443
+
444
+    /**
445
+     * Check if app has been installed from git
446
+     * @param string $name name of the application to remove
447
+     * @return boolean
448
+     *
449
+     * The function will check if the path contains a .git folder
450
+     */
451
+    private function isInstalledFromGit($appId) {
452
+        $app = \OC_App::findAppInDirectories($appId);
453
+        if ($app === false) {
454
+            return false;
455
+        }
456
+        $basedir = $app['path'].'/'.$appId;
457
+        return file_exists($basedir.'/.git/');
458
+    }
459
+
460
+    /**
461
+     * Check if app is already downloaded
462
+     * @param string $name name of the application to remove
463
+     * @return boolean
464
+     *
465
+     * The function will check if the app is already downloaded in the apps repository
466
+     */
467
+    public function isDownloaded($name) {
468
+        foreach (\OC::$APPSROOTS as $dir) {
469
+            $dirToTest = $dir['path'];
470
+            $dirToTest .= '/';
471
+            $dirToTest .= $name;
472
+            $dirToTest .= '/';
473
+
474
+            if (is_dir($dirToTest)) {
475
+                return true;
476
+            }
477
+        }
478
+
479
+        return false;
480
+    }
481
+
482
+    /**
483
+     * Removes an app
484
+     * @param string $appId ID of the application to remove
485
+     * @return boolean
486
+     *
487
+     *
488
+     * This function works as follows
489
+     *   -# call uninstall repair steps
490
+     *   -# removing the files
491
+     *
492
+     * The function will not delete preferences, tables and the configuration,
493
+     * this has to be done by the function oc_app_uninstall().
494
+     */
495
+    public function removeApp($appId) {
496
+        if ($this->isDownloaded($appId)) {
497
+            if (\OC::$server->getAppManager()->isShipped($appId)) {
498
+                return false;
499
+            }
500
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
501
+            OC_Helper::rmdirr($appDir);
502
+            return true;
503
+        } else {
504
+            $this->logger->error('can\'t remove app '.$appId.'. It is not installed.');
505
+
506
+            return false;
507
+        }
508
+    }
509
+
510
+    /**
511
+     * Installs the app within the bundle and marks the bundle as installed
512
+     *
513
+     * @param Bundle $bundle
514
+     * @throws \Exception If app could not get installed
515
+     */
516
+    public function installAppBundle(Bundle $bundle) {
517
+        $appIds = $bundle->getAppIdentifiers();
518
+        foreach ($appIds as $appId) {
519
+            if (!$this->isDownloaded($appId)) {
520
+                $this->downloadApp($appId);
521
+            }
522
+            $this->installApp($appId);
523
+            $app = new OC_App();
524
+            $app->enable($appId);
525
+        }
526
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
527
+        $bundles[] = $bundle->getIdentifier();
528
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
529
+    }
530
+
531
+    /**
532
+     * Installs shipped apps
533
+     *
534
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
535
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
536
+     *                         working ownCloud at the end instead of an aborted update.
537
+     * @return array Array of error messages (appid => Exception)
538
+     */
539
+    public static function installShippedApps($softErrors = false) {
540
+        $appManager = \OC::$server->getAppManager();
541
+        $config = \OC::$server->getConfig();
542
+        $errors = [];
543
+        foreach (\OC::$APPSROOTS as $app_dir) {
544
+            if ($dir = opendir($app_dir['path'])) {
545
+                while (false !== ($filename = readdir($dir))) {
546
+                    if ($filename[0] !== '.' and is_dir($app_dir['path']."/$filename")) {
547
+                        if (file_exists($app_dir['path']."/$filename/appinfo/info.xml")) {
548
+                            if ($config->getAppValue($filename, "installed_version", null) === null) {
549
+                                $enabled = $appManager->isDefaultEnabled($filename);
550
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
551
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
552
+                                    if ($softErrors) {
553
+                                        try {
554
+                                            Installer::installShippedApp($filename);
555
+                                        } catch (HintException $e) {
556
+                                            if ($e->getPrevious() instanceof TableExistsException) {
557
+                                                $errors[$filename] = $e;
558
+                                                continue;
559
+                                            }
560
+                                            throw $e;
561
+                                        }
562
+                                    } else {
563
+                                        Installer::installShippedApp($filename);
564
+                                    }
565
+                                    $config->setAppValue($filename, 'enabled', 'yes');
566
+                                }
567
+                            }
568
+                        }
569
+                    }
570
+                }
571
+                closedir($dir);
572
+            }
573
+        }
574
+
575
+        return $errors;
576
+    }
577
+
578
+    /**
579
+     * install an app already placed in the app folder
580
+     * @param string $app id of the app to install
581
+     * @return integer
582
+     */
583
+    public static function installShippedApp($app) {
584
+        //install the database
585
+        $appPath = OC_App::getAppPath($app);
586
+        \OC_App::registerAutoloading($app, $appPath);
587
+
588
+        $config = \OC::$server->getConfig();
589
+
590
+        $ms = new MigrationService($app, \OC::$server->get(Connection::class));
591
+        $previousVersion = $config->getAppValue($app, 'installed_version', false);
592
+        $ms->migrate('latest', !$previousVersion);
593
+
594
+        //run appinfo/install.php
595
+        self::includeAppScript("$appPath/appinfo/install.php");
596
+
597
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
598
+        if (is_null($info)) {
599
+            return false;
600
+        }
601
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
602
+
603
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
604
+
605
+        $config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
606
+        if (array_key_exists('ocsid', $info)) {
607
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
608
+        }
609
+
610
+        //set remote/public handlers
611
+        foreach ($info['remote'] as $name => $path) {
612
+            $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
613
+        }
614
+        foreach ($info['public'] as $name => $path) {
615
+            $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
616
+        }
617
+
618
+        OC_App::setAppTypes($info['id']);
619
+
620
+        return $info['id'];
621
+    }
622
+
623
+    /**
624
+     * @param string $script
625
+     */
626
+    private static function includeAppScript($script) {
627
+        if (file_exists($script)) {
628
+            include $script;
629
+        }
630
+    }
631 631
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -109,12 +109,12 @@  discard block
 block discarded – undo
109 109
 
110 110
 		$basedir = $app['path'].'/'.$appId;
111 111
 
112
-		if (is_file($basedir . '/appinfo/database.xml')) {
113
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
112
+		if (is_file($basedir.'/appinfo/database.xml')) {
113
+			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in '.$appId);
114 114
 		}
115 115
 
116 116
 		$l = \OC::$server->getL10N('core');
117
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($basedir . '/appinfo/info.xml', true, $l->getLanguageCode());
117
+		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($basedir.'/appinfo/info.xml', true, $l->getLanguageCode());
118 118
 
119 119
 		if (!is_array($info)) {
120 120
 			throw new \Exception(
@@ -160,7 +160,7 @@  discard block
 block discarded – undo
160 160
 		\OC_App::setupBackgroundJobs($info['background-jobs']);
161 161
 
162 162
 		//run appinfo/install.php
163
-		self::includeAppScript($basedir . '/appinfo/install.php');
163
+		self::includeAppScript($basedir.'/appinfo/install.php');
164 164
 
165 165
 		OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
166 166
 
@@ -232,7 +232,7 @@  discard block
 block discarded – undo
232 232
 			if ($app['id'] === $appId) {
233 233
 				// Load the certificate
234 234
 				$certificate = new X509();
235
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
235
+				$rootCrt = file_get_contents(__DIR__.'/../../resources/codesigning/root.crt');
236 236
 				$rootCrts = $this->splitCerts($rootCrt);
237 237
 				foreach ($rootCrts as $rootCrt) {
238 238
 					$certificate->loadCA($rootCrt);
@@ -244,7 +244,7 @@  discard block
 block discarded – undo
244 244
 				foreach ($rootCrts as $rootCrt) {
245 245
 					$crl->loadCA($rootCrt);
246 246
 				}
247
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
247
+				$crl->loadCRL(file_get_contents(__DIR__.'/../../resources/codesigning/root.crl'));
248 248
 				if ($crl->validateSignature() !== true) {
249 249
 					throw new \Exception('Could not validate CRL signature');
250 250
 				}
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
 
298 298
 				// Check if the signature actually matches the downloaded content
299 299
 				$certificate = openssl_get_publickey($app['certificate']);
300
-				$verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
300
+				$verified = (bool) openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
301 301
 				// PHP 8+ deprecates openssl_free_key and automatically destroys the key instance when it goes out of scope
302 302
 				if ((PHP_VERSION_ID < 80000)) {
303 303
 					openssl_free_key($certificate);
@@ -309,11 +309,11 @@  discard block
 block discarded – undo
309 309
 					$archive = new TAR($tempFile);
310 310
 
311 311
 					if (!$archive->extract($extractDir)) {
312
-						$errorMessage = 'Could not extract app ' . $appId;
312
+						$errorMessage = 'Could not extract app '.$appId;
313 313
 
314 314
 						$archiveError = $archive->getError();
315 315
 						if ($archiveError instanceof \PEAR_Error) {
316
-							$errorMessage .= ': ' . $archiveError->getMessage();
316
+							$errorMessage .= ': '.$archiveError->getMessage();
317 317
 						}
318 318
 
319 319
 						throw new \Exception($errorMessage);
@@ -334,24 +334,24 @@  discard block
 block discarded – undo
334 334
 					// Check if appinfo/info.xml has the same app ID as well
335 335
 					if ((PHP_VERSION_ID < 80000)) {
336 336
 						$loadEntities = libxml_disable_entity_loader(false);
337
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
337
+						$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
338 338
 						libxml_disable_entity_loader($loadEntities);
339 339
 					} else {
340
-						$xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
340
+						$xml = simplexml_load_file($extractDir.'/'.$folders[0].'/appinfo/info.xml');
341 341
 					}
342
-					if ((string)$xml->id !== $appId) {
342
+					if ((string) $xml->id !== $appId) {
343 343
 						throw new \Exception(
344 344
 							sprintf(
345 345
 								'App for id %s has a wrong app ID in info.xml: %s',
346 346
 								$appId,
347
-								(string)$xml->id
347
+								(string) $xml->id
348 348
 							)
349 349
 						);
350 350
 					}
351 351
 
352 352
 					// Check if the version is lower than before
353 353
 					$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
354
-					$newVersion = (string)$xml->version;
354
+					$newVersion = (string) $xml->version;
355 355
 					if (version_compare($currentVersion, $newVersion) === 1) {
356 356
 						throw new \Exception(
357 357
 							sprintf(
@@ -363,12 +363,12 @@  discard block
 block discarded – undo
363 363
 						);
364 364
 					}
365 365
 
366
-					$baseDir = OC_App::getInstallPath() . '/' . $appId;
366
+					$baseDir = OC_App::getInstallPath().'/'.$appId;
367 367
 					// Remove old app with the ID if existent
368 368
 					OC_Helper::rmdirr($baseDir);
369 369
 					// Move to app folder
370 370
 					if (@mkdir($baseDir)) {
371
-						$extractDir .= '/' . $folders[0];
371
+						$extractDir .= '/'.$folders[0];
372 372
 						OC_Helper::copyr($extractDir, $baseDir);
373 373
 					}
374 374
 					OC_Helper::copyr($extractDir, $baseDir);
@@ -497,7 +497,7 @@  discard block
 block discarded – undo
497 497
 			if (\OC::$server->getAppManager()->isShipped($appId)) {
498 498
 				return false;
499 499
 			}
500
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
500
+			$appDir = OC_App::getInstallPath().'/'.$appId;
501 501
 			OC_Helper::rmdirr($appDir);
502 502
 			return true;
503 503
 		} else {
Please login to merge, or discard this patch.
lib/private/AppFramework/App.php 2 patches
Indentation   +209 added lines, -209 removed lines patch added patch discarded remove patch
@@ -52,218 +52,218 @@
 block discarded – undo
52 52
  * Handles all the dependency injection, controllers and output flow
53 53
  */
54 54
 class App {
55
-	/** @var string[] */
56
-	private static $nameSpaceCache = [];
57
-
58
-	/**
59
-	 * Turns an app id into a namespace by either reading the appinfo.xml's
60
-	 * namespace tag or uppercasing the appid's first letter
61
-	 * @param string $appId the app id
62
-	 * @param string $topNamespace the namespace which should be prepended to
63
-	 * the transformed app id, defaults to OCA\
64
-	 * @return string the starting namespace for the app
65
-	 */
66
-	public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string {
67
-		// Hit the cache!
68
-		if (isset(self::$nameSpaceCache[$appId])) {
69
-			return $topNamespace . self::$nameSpaceCache[$appId];
70
-		}
71
-
72
-		$appInfo = \OCP\Server::get(IAppManager::class)->getAppInfo($appId);
73
-		if (isset($appInfo['namespace'])) {
74
-			self::$nameSpaceCache[$appId] = trim($appInfo['namespace']);
75
-		} else {
76
-			if ($appId !== 'spreed') {
77
-				// if the tag is not found, fall back to uppercasing the first letter
78
-				self::$nameSpaceCache[$appId] = ucfirst($appId);
79
-			} else {
80
-				// For the Talk app (appid spreed) the above fallback doesn't work.
81
-				// This leads to a problem when trying to install it freshly,
82
-				// because the apps namespace is already registered before the
83
-				// app is downloaded from the appstore, because of the hackish
84
-				// global route index.php/call/{token} which is registered via
85
-				// the core/routes.php so it does not have the app namespace.
86
-				// @ref https://github.com/nextcloud/server/pull/19433
87
-				self::$nameSpaceCache[$appId] = 'Talk';
88
-			}
89
-		}
90
-
91
-		return $topNamespace . self::$nameSpaceCache[$appId];
92
-	}
93
-
94
-	public static function getAppIdForClass(string $className, string $topNamespace = 'OCA\\'): ?string {
95
-		if (strpos($className, $topNamespace) !== 0) {
96
-			return null;
97
-		}
98
-
99
-		foreach (self::$nameSpaceCache as $appId => $namespace) {
100
-			if (strpos($className, $topNamespace . $namespace . '\\') === 0) {
101
-				return $appId;
102
-			}
103
-		}
104
-
105
-		return null;
106
-	}
107
-
108
-
109
-	/**
110
-	 * Shortcut for calling a controller method and printing the result
111
-	 *
112
-	 * @param string $controllerName the name of the controller under which it is
113
-	 *                               stored in the DI container
114
-	 * @param string $methodName the method that you want to call
115
-	 * @param DIContainer $container an instance of a pimple container.
116
-	 * @param array $urlParams list of URL parameters (optional)
117
-	 * @throws HintException
118
-	 */
119
-	public static function main(string $controllerName, string $methodName, DIContainer $container, array $urlParams = null) {
120
-		/** @var IProfiler $profiler */
121
-		$profiler = $container->get(IProfiler::class);
122
-		$eventLogger = $container->get(IEventLogger::class);
123
-		// Disable profiler on the profiler UI
124
-		$profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.'));
125
-		if ($profiler->isEnabled()) {
126
-			\OC::$server->get(IEventLogger::class)->activate();
127
-			$profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName));
128
-		}
129
-
130
-		$eventLogger->start('app:controller:params', 'Gather controller parameters');
131
-
132
-		if (!is_null($urlParams)) {
133
-			/** @var Request $request */
134
-			$request = $container->get(IRequest::class);
135
-			$request->setUrlParameters($urlParams);
136
-		} elseif (isset($container['urlParams']) && !is_null($container['urlParams'])) {
137
-			/** @var Request $request */
138
-			$request = $container->get(IRequest::class);
139
-			$request->setUrlParameters($container['urlParams']);
140
-		}
141
-		$appName = $container['AppName'];
142
-
143
-		$eventLogger->end('app:controller:params');
144
-
145
-		$eventLogger->start('app:controller:load', 'Load app controller');
146
-
147
-		// first try $controllerName then go for \OCA\AppName\Controller\$controllerName
148
-		try {
149
-			$controller = $container->get($controllerName);
150
-		} catch (QueryException $e) {
151
-			if (strpos($controllerName, '\\Controller\\') !== false) {
152
-				// This is from a global registered app route that is not enabled.
153
-				[/*OC(A)*/, $app, /* Controller/Name*/] = explode('\\', $controllerName, 3);
154
-				throw new HintException('App ' . strtolower($app) . ' is not enabled');
155
-			}
156
-
157
-			if ($appName === 'core') {
158
-				$appNameSpace = 'OC\\Core';
159
-			} else {
160
-				$appNameSpace = self::buildAppNamespace($appName);
161
-			}
162
-			$controllerName = $appNameSpace . '\\Controller\\' . $controllerName;
163
-			$controller = $container->query($controllerName);
164
-		}
165
-
166
-		$eventLogger->end('app:controller:load');
167
-
168
-		$eventLogger->start('app:controller:dispatcher', 'Initialize dispatcher and pre-middleware');
169
-
170
-		// initialize the dispatcher and run all the middleware before the controller
171
-		/** @var Dispatcher $dispatcher */
172
-		$dispatcher = $container['Dispatcher'];
173
-
174
-		$eventLogger->end('app:controller:dispatcher');
175
-
176
-		$eventLogger->start('app:controller:run', 'Run app controller');
177
-
178
-		[
179
-			$httpHeaders,
180
-			$responseHeaders,
181
-			$responseCookies,
182
-			$output,
183
-			$response
184
-		] = $dispatcher->dispatch($controller, $methodName);
185
-
186
-		$eventLogger->end('app:controller:run');
187
-
188
-		$io = $container[IOutput::class];
189
-
190
-		if ($profiler->isEnabled()) {
191
-			$eventLogger->end('runtime');
192
-			$profile = $profiler->collect($container->get(IRequest::class), $response);
193
-			$profiler->saveProfile($profile);
194
-			$io->setHeader('X-Debug-Token:' . $profile->getToken());
195
-			$io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"');
196
-		}
197
-
198
-		if (!is_null($httpHeaders)) {
199
-			$io->setHeader($httpHeaders);
200
-		}
201
-
202
-		foreach ($responseHeaders as $name => $value) {
203
-			$io->setHeader($name . ': ' . $value);
204
-		}
205
-
206
-		foreach ($responseCookies as $name => $value) {
207
-			$expireDate = null;
208
-			if ($value['expireDate'] instanceof \DateTime) {
209
-				$expireDate = $value['expireDate']->getTimestamp();
210
-			}
211
-			$sameSite = $value['sameSite'] ?? 'Lax';
212
-
213
-			$io->setCookie(
214
-				$name,
215
-				$value['value'],
216
-				$expireDate,
217
-				$container->getServer()->getWebRoot(),
218
-				null,
219
-				$container->getServer()->getRequest()->getServerProtocol() === 'https',
220
-				true,
221
-				$sameSite
222
-			);
223
-		}
224
-
225
-		/*
55
+    /** @var string[] */
56
+    private static $nameSpaceCache = [];
57
+
58
+    /**
59
+     * Turns an app id into a namespace by either reading the appinfo.xml's
60
+     * namespace tag or uppercasing the appid's first letter
61
+     * @param string $appId the app id
62
+     * @param string $topNamespace the namespace which should be prepended to
63
+     * the transformed app id, defaults to OCA\
64
+     * @return string the starting namespace for the app
65
+     */
66
+    public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string {
67
+        // Hit the cache!
68
+        if (isset(self::$nameSpaceCache[$appId])) {
69
+            return $topNamespace . self::$nameSpaceCache[$appId];
70
+        }
71
+
72
+        $appInfo = \OCP\Server::get(IAppManager::class)->getAppInfo($appId);
73
+        if (isset($appInfo['namespace'])) {
74
+            self::$nameSpaceCache[$appId] = trim($appInfo['namespace']);
75
+        } else {
76
+            if ($appId !== 'spreed') {
77
+                // if the tag is not found, fall back to uppercasing the first letter
78
+                self::$nameSpaceCache[$appId] = ucfirst($appId);
79
+            } else {
80
+                // For the Talk app (appid spreed) the above fallback doesn't work.
81
+                // This leads to a problem when trying to install it freshly,
82
+                // because the apps namespace is already registered before the
83
+                // app is downloaded from the appstore, because of the hackish
84
+                // global route index.php/call/{token} which is registered via
85
+                // the core/routes.php so it does not have the app namespace.
86
+                // @ref https://github.com/nextcloud/server/pull/19433
87
+                self::$nameSpaceCache[$appId] = 'Talk';
88
+            }
89
+        }
90
+
91
+        return $topNamespace . self::$nameSpaceCache[$appId];
92
+    }
93
+
94
+    public static function getAppIdForClass(string $className, string $topNamespace = 'OCA\\'): ?string {
95
+        if (strpos($className, $topNamespace) !== 0) {
96
+            return null;
97
+        }
98
+
99
+        foreach (self::$nameSpaceCache as $appId => $namespace) {
100
+            if (strpos($className, $topNamespace . $namespace . '\\') === 0) {
101
+                return $appId;
102
+            }
103
+        }
104
+
105
+        return null;
106
+    }
107
+
108
+
109
+    /**
110
+     * Shortcut for calling a controller method and printing the result
111
+     *
112
+     * @param string $controllerName the name of the controller under which it is
113
+     *                               stored in the DI container
114
+     * @param string $methodName the method that you want to call
115
+     * @param DIContainer $container an instance of a pimple container.
116
+     * @param array $urlParams list of URL parameters (optional)
117
+     * @throws HintException
118
+     */
119
+    public static function main(string $controllerName, string $methodName, DIContainer $container, array $urlParams = null) {
120
+        /** @var IProfiler $profiler */
121
+        $profiler = $container->get(IProfiler::class);
122
+        $eventLogger = $container->get(IEventLogger::class);
123
+        // Disable profiler on the profiler UI
124
+        $profiler->setEnabled($profiler->isEnabled() && !is_null($urlParams) && isset($urlParams['_route']) && !str_starts_with($urlParams['_route'], 'profiler.'));
125
+        if ($profiler->isEnabled()) {
126
+            \OC::$server->get(IEventLogger::class)->activate();
127
+            $profiler->add(new RoutingDataCollector($container['AppName'], $controllerName, $methodName));
128
+        }
129
+
130
+        $eventLogger->start('app:controller:params', 'Gather controller parameters');
131
+
132
+        if (!is_null($urlParams)) {
133
+            /** @var Request $request */
134
+            $request = $container->get(IRequest::class);
135
+            $request->setUrlParameters($urlParams);
136
+        } elseif (isset($container['urlParams']) && !is_null($container['urlParams'])) {
137
+            /** @var Request $request */
138
+            $request = $container->get(IRequest::class);
139
+            $request->setUrlParameters($container['urlParams']);
140
+        }
141
+        $appName = $container['AppName'];
142
+
143
+        $eventLogger->end('app:controller:params');
144
+
145
+        $eventLogger->start('app:controller:load', 'Load app controller');
146
+
147
+        // first try $controllerName then go for \OCA\AppName\Controller\$controllerName
148
+        try {
149
+            $controller = $container->get($controllerName);
150
+        } catch (QueryException $e) {
151
+            if (strpos($controllerName, '\\Controller\\') !== false) {
152
+                // This is from a global registered app route that is not enabled.
153
+                [/*OC(A)*/, $app, /* Controller/Name*/] = explode('\\', $controllerName, 3);
154
+                throw new HintException('App ' . strtolower($app) . ' is not enabled');
155
+            }
156
+
157
+            if ($appName === 'core') {
158
+                $appNameSpace = 'OC\\Core';
159
+            } else {
160
+                $appNameSpace = self::buildAppNamespace($appName);
161
+            }
162
+            $controllerName = $appNameSpace . '\\Controller\\' . $controllerName;
163
+            $controller = $container->query($controllerName);
164
+        }
165
+
166
+        $eventLogger->end('app:controller:load');
167
+
168
+        $eventLogger->start('app:controller:dispatcher', 'Initialize dispatcher and pre-middleware');
169
+
170
+        // initialize the dispatcher and run all the middleware before the controller
171
+        /** @var Dispatcher $dispatcher */
172
+        $dispatcher = $container['Dispatcher'];
173
+
174
+        $eventLogger->end('app:controller:dispatcher');
175
+
176
+        $eventLogger->start('app:controller:run', 'Run app controller');
177
+
178
+        [
179
+            $httpHeaders,
180
+            $responseHeaders,
181
+            $responseCookies,
182
+            $output,
183
+            $response
184
+        ] = $dispatcher->dispatch($controller, $methodName);
185
+
186
+        $eventLogger->end('app:controller:run');
187
+
188
+        $io = $container[IOutput::class];
189
+
190
+        if ($profiler->isEnabled()) {
191
+            $eventLogger->end('runtime');
192
+            $profile = $profiler->collect($container->get(IRequest::class), $response);
193
+            $profiler->saveProfile($profile);
194
+            $io->setHeader('X-Debug-Token:' . $profile->getToken());
195
+            $io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"');
196
+        }
197
+
198
+        if (!is_null($httpHeaders)) {
199
+            $io->setHeader($httpHeaders);
200
+        }
201
+
202
+        foreach ($responseHeaders as $name => $value) {
203
+            $io->setHeader($name . ': ' . $value);
204
+        }
205
+
206
+        foreach ($responseCookies as $name => $value) {
207
+            $expireDate = null;
208
+            if ($value['expireDate'] instanceof \DateTime) {
209
+                $expireDate = $value['expireDate']->getTimestamp();
210
+            }
211
+            $sameSite = $value['sameSite'] ?? 'Lax';
212
+
213
+            $io->setCookie(
214
+                $name,
215
+                $value['value'],
216
+                $expireDate,
217
+                $container->getServer()->getWebRoot(),
218
+                null,
219
+                $container->getServer()->getRequest()->getServerProtocol() === 'https',
220
+                true,
221
+                $sameSite
222
+            );
223
+        }
224
+
225
+        /*
226 226
 		 * Status 204 does not have a body and no Content Length
227 227
 		 * Status 304 does not have a body and does not need a Content Length
228 228
 		 * https://tools.ietf.org/html/rfc7230#section-3.3
229 229
 		 * https://tools.ietf.org/html/rfc7230#section-3.3.2
230 230
 		 */
231
-		$emptyResponse = false;
232
-		if (preg_match('/^HTTP\/\d\.\d (\d{3}) .*$/', $httpHeaders, $matches)) {
233
-			$status = (int)$matches[1];
234
-			if ($status === Http::STATUS_NO_CONTENT || $status === Http::STATUS_NOT_MODIFIED) {
235
-				$emptyResponse = true;
236
-			}
237
-		}
238
-
239
-		if (!$emptyResponse) {
240
-			if ($response instanceof ICallbackResponse) {
241
-				$response->callback($io);
242
-			} elseif (!is_null($output)) {
243
-				$io->setHeader('Content-Length: ' . strlen($output));
244
-				$io->setOutput($output);
245
-			}
246
-		}
247
-	}
248
-
249
-	/**
250
-	 * Shortcut for calling a controller method and printing the result.
251
-	 * Similar to App:main except that no headers will be sent.
252
-	 *
253
-	 * @param string $controllerName the name of the controller under which it is
254
-	 *                               stored in the DI container
255
-	 * @param string $methodName the method that you want to call
256
-	 * @param array $urlParams an array with variables extracted from the routes
257
-	 * @param DIContainer $container an instance of a pimple container.
258
-	 */
259
-	public static function part(string $controllerName, string $methodName, array $urlParams,
260
-								DIContainer $container) {
261
-		$container['urlParams'] = $urlParams;
262
-		$controller = $container[$controllerName];
263
-
264
-		$dispatcher = $container['Dispatcher'];
265
-
266
-		[, , $output] = $dispatcher->dispatch($controller, $methodName);
267
-		return $output;
268
-	}
231
+        $emptyResponse = false;
232
+        if (preg_match('/^HTTP\/\d\.\d (\d{3}) .*$/', $httpHeaders, $matches)) {
233
+            $status = (int)$matches[1];
234
+            if ($status === Http::STATUS_NO_CONTENT || $status === Http::STATUS_NOT_MODIFIED) {
235
+                $emptyResponse = true;
236
+            }
237
+        }
238
+
239
+        if (!$emptyResponse) {
240
+            if ($response instanceof ICallbackResponse) {
241
+                $response->callback($io);
242
+            } elseif (!is_null($output)) {
243
+                $io->setHeader('Content-Length: ' . strlen($output));
244
+                $io->setOutput($output);
245
+            }
246
+        }
247
+    }
248
+
249
+    /**
250
+     * Shortcut for calling a controller method and printing the result.
251
+     * Similar to App:main except that no headers will be sent.
252
+     *
253
+     * @param string $controllerName the name of the controller under which it is
254
+     *                               stored in the DI container
255
+     * @param string $methodName the method that you want to call
256
+     * @param array $urlParams an array with variables extracted from the routes
257
+     * @param DIContainer $container an instance of a pimple container.
258
+     */
259
+    public static function part(string $controllerName, string $methodName, array $urlParams,
260
+                                DIContainer $container) {
261
+        $container['urlParams'] = $urlParams;
262
+        $controller = $container[$controllerName];
263
+
264
+        $dispatcher = $container['Dispatcher'];
265
+
266
+        [, , $output] = $dispatcher->dispatch($controller, $methodName);
267
+        return $output;
268
+    }
269 269
 }
Please login to merge, or discard this patch.
Spacing   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -66,7 +66,7 @@  discard block
 block discarded – undo
66 66
 	public static function buildAppNamespace(string $appId, string $topNamespace = 'OCA\\'): string {
67 67
 		// Hit the cache!
68 68
 		if (isset(self::$nameSpaceCache[$appId])) {
69
-			return $topNamespace . self::$nameSpaceCache[$appId];
69
+			return $topNamespace.self::$nameSpaceCache[$appId];
70 70
 		}
71 71
 
72 72
 		$appInfo = \OCP\Server::get(IAppManager::class)->getAppInfo($appId);
@@ -88,7 +88,7 @@  discard block
 block discarded – undo
88 88
 			}
89 89
 		}
90 90
 
91
-		return $topNamespace . self::$nameSpaceCache[$appId];
91
+		return $topNamespace.self::$nameSpaceCache[$appId];
92 92
 	}
93 93
 
94 94
 	public static function getAppIdForClass(string $className, string $topNamespace = 'OCA\\'): ?string {
@@ -97,7 +97,7 @@  discard block
 block discarded – undo
97 97
 		}
98 98
 
99 99
 		foreach (self::$nameSpaceCache as $appId => $namespace) {
100
-			if (strpos($className, $topNamespace . $namespace . '\\') === 0) {
100
+			if (strpos($className, $topNamespace.$namespace.'\\') === 0) {
101 101
 				return $appId;
102 102
 			}
103 103
 		}
@@ -151,7 +151,7 @@  discard block
 block discarded – undo
151 151
 			if (strpos($controllerName, '\\Controller\\') !== false) {
152 152
 				// This is from a global registered app route that is not enabled.
153 153
 				[/*OC(A)*/, $app, /* Controller/Name*/] = explode('\\', $controllerName, 3);
154
-				throw new HintException('App ' . strtolower($app) . ' is not enabled');
154
+				throw new HintException('App '.strtolower($app).' is not enabled');
155 155
 			}
156 156
 
157 157
 			if ($appName === 'core') {
@@ -159,7 +159,7 @@  discard block
 block discarded – undo
159 159
 			} else {
160 160
 				$appNameSpace = self::buildAppNamespace($appName);
161 161
 			}
162
-			$controllerName = $appNameSpace . '\\Controller\\' . $controllerName;
162
+			$controllerName = $appNameSpace.'\\Controller\\'.$controllerName;
163 163
 			$controller = $container->query($controllerName);
164 164
 		}
165 165
 
@@ -191,8 +191,8 @@  discard block
 block discarded – undo
191 191
 			$eventLogger->end('runtime');
192 192
 			$profile = $profiler->collect($container->get(IRequest::class), $response);
193 193
 			$profiler->saveProfile($profile);
194
-			$io->setHeader('X-Debug-Token:' . $profile->getToken());
195
-			$io->setHeader('Server-Timing: token;desc="' . $profile->getToken() . '"');
194
+			$io->setHeader('X-Debug-Token:'.$profile->getToken());
195
+			$io->setHeader('Server-Timing: token;desc="'.$profile->getToken().'"');
196 196
 		}
197 197
 
198 198
 		if (!is_null($httpHeaders)) {
@@ -200,7 +200,7 @@  discard block
 block discarded – undo
200 200
 		}
201 201
 
202 202
 		foreach ($responseHeaders as $name => $value) {
203
-			$io->setHeader($name . ': ' . $value);
203
+			$io->setHeader($name.': '.$value);
204 204
 		}
205 205
 
206 206
 		foreach ($responseCookies as $name => $value) {
@@ -230,7 +230,7 @@  discard block
 block discarded – undo
230 230
 		 */
231 231
 		$emptyResponse = false;
232 232
 		if (preg_match('/^HTTP\/\d\.\d (\d{3}) .*$/', $httpHeaders, $matches)) {
233
-			$status = (int)$matches[1];
233
+			$status = (int) $matches[1];
234 234
 			if ($status === Http::STATUS_NO_CONTENT || $status === Http::STATUS_NOT_MODIFIED) {
235 235
 				$emptyResponse = true;
236 236
 			}
@@ -240,7 +240,7 @@  discard block
 block discarded – undo
240 240
 			if ($response instanceof ICallbackResponse) {
241 241
 				$response->callback($io);
242 242
 			} elseif (!is_null($output)) {
243
-				$io->setHeader('Content-Length: ' . strlen($output));
243
+				$io->setHeader('Content-Length: '.strlen($output));
244 244
 				$io->setOutput($output);
245 245
 			}
246 246
 		}
@@ -263,7 +263,7 @@  discard block
 block discarded – undo
263 263
 
264 264
 		$dispatcher = $container['Dispatcher'];
265 265
 
266
-		[, , $output] = $dispatcher->dispatch($controller, $methodName);
266
+		[,, $output] = $dispatcher->dispatch($controller, $methodName);
267 267
 		return $output;
268 268
 	}
269 269
 }
Please login to merge, or discard this patch.
lib/private/legacy/OC_App.php 2 patches
Indentation   +899 added lines, -899 removed lines patch added patch discarded remove patch
@@ -73,903 +73,903 @@
 block discarded – undo
73 73
  * upgrading and removing apps.
74 74
  */
75 75
 class OC_App {
76
-	private static $adminForms = [];
77
-	private static $personalForms = [];
78
-	private static $altLogin = [];
79
-	private static $alreadyRegistered = [];
80
-	public const supportedApp = 300;
81
-	public const officialApp = 200;
82
-
83
-	/**
84
-	 * clean the appId
85
-	 *
86
-	 * @psalm-taint-escape file
87
-	 * @psalm-taint-escape include
88
-	 * @psalm-taint-escape html
89
-	 * @psalm-taint-escape has_quotes
90
-	 *
91
-	 * @param string $app AppId that needs to be cleaned
92
-	 * @return string
93
-	 */
94
-	public static function cleanAppId(string $app): string {
95
-		return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app);
96
-	}
97
-
98
-	/**
99
-	 * Check if an app is loaded
100
-	 *
101
-	 * @param string $app
102
-	 * @return bool
103
-	 * @deprecated 27.0.0 use IAppManager::isAppLoaded
104
-	 */
105
-	public static function isAppLoaded(string $app): bool {
106
-		return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
107
-	}
108
-
109
-	/**
110
-	 * loads all apps
111
-	 *
112
-	 * @param string[] $types
113
-	 * @return bool
114
-	 *
115
-	 * This function walks through the ownCloud directory and loads all apps
116
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
117
-	 * exists.
118
-	 *
119
-	 * if $types is set to non-empty array, only apps of those types will be loaded
120
-	 */
121
-	public static function loadApps(array $types = []): bool {
122
-		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
123
-			// This should be done before calling this method so that appmanager can be used
124
-			return false;
125
-		}
126
-		return \OC::$server->get(IAppManager::class)->loadApps($types);
127
-	}
128
-
129
-	/**
130
-	 * load a single app
131
-	 *
132
-	 * @param string $app
133
-	 * @throws Exception
134
-	 * @deprecated 27.0.0 use IAppManager::loadApp
135
-	 */
136
-	public static function loadApp(string $app): void {
137
-		\OC::$server->get(IAppManager::class)->loadApp($app);
138
-	}
139
-
140
-	/**
141
-	 * @internal
142
-	 * @param string $app
143
-	 * @param string $path
144
-	 * @param bool $force
145
-	 */
146
-	public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
-		$key = $app . '-' . $path;
148
-		if (!$force && isset(self::$alreadyRegistered[$key])) {
149
-			return;
150
-		}
151
-
152
-		self::$alreadyRegistered[$key] = true;
153
-
154
-		// Register on PSR-4 composer autoloader
155
-		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156
-		\OC::$server->registerNamespace($app, $appNamespace);
157
-
158
-		if (file_exists($path . '/composer/autoload.php')) {
159
-			require_once $path . '/composer/autoload.php';
160
-		} else {
161
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
162
-		}
163
-
164
-		// Register Test namespace only when testing
165
-		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
167
-		}
168
-	}
169
-
170
-	/**
171
-	 * check if an app is of a specific type
172
-	 *
173
-	 * @param string $app
174
-	 * @param array $types
175
-	 * @return bool
176
-	 * @deprecated 27.0.0 use IAppManager::isType
177
-	 */
178
-	public static function isType(string $app, array $types): bool {
179
-		return \OC::$server->get(IAppManager::class)->isType($app, $types);
180
-	}
181
-
182
-	/**
183
-	 * read app types from info.xml and cache them in the database
184
-	 */
185
-	public static function setAppTypes(string $app) {
186
-		$appManager = \OC::$server->getAppManager();
187
-		$appData = $appManager->getAppInfo($app);
188
-		if (!is_array($appData)) {
189
-			return;
190
-		}
191
-
192
-		if (isset($appData['types'])) {
193
-			$appTypes = implode(',', $appData['types']);
194
-		} else {
195
-			$appTypes = '';
196
-			$appData['types'] = [];
197
-		}
198
-
199
-		$config = \OC::$server->getConfig();
200
-		$config->setAppValue($app, 'types', $appTypes);
201
-
202
-		if ($appManager->hasProtectedAppType($appData['types'])) {
203
-			$enabled = $config->getAppValue($app, 'enabled', 'yes');
204
-			if ($enabled !== 'yes' && $enabled !== 'no') {
205
-				$config->setAppValue($app, 'enabled', 'yes');
206
-			}
207
-		}
208
-	}
209
-
210
-	/**
211
-	 * Returns apps enabled for the current user.
212
-	 *
213
-	 * @param bool $forceRefresh whether to refresh the cache
214
-	 * @param bool $all whether to return apps for all users, not only the
215
-	 * currently logged in one
216
-	 * @return string[]
217
-	 */
218
-	public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
219
-		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
220
-			return [];
221
-		}
222
-		// in incognito mode or when logged out, $user will be false,
223
-		// which is also the case during an upgrade
224
-		$appManager = \OC::$server->getAppManager();
225
-		if ($all) {
226
-			$user = null;
227
-		} else {
228
-			$user = \OC::$server->getUserSession()->getUser();
229
-		}
230
-
231
-		if (is_null($user)) {
232
-			$apps = $appManager->getInstalledApps();
233
-		} else {
234
-			$apps = $appManager->getEnabledAppsForUser($user);
235
-		}
236
-		$apps = array_filter($apps, function ($app) {
237
-			return $app !== 'files';//we add this manually
238
-		});
239
-		sort($apps);
240
-		array_unshift($apps, 'files');
241
-		return $apps;
242
-	}
243
-
244
-	/**
245
-	 * enables an app
246
-	 *
247
-	 * @param string $appId
248
-	 * @param array $groups (optional) when set, only these groups will have access to the app
249
-	 * @throws \Exception
250
-	 * @return void
251
-	 *
252
-	 * This function set an app as enabled in appconfig.
253
-	 */
254
-	public function enable(string $appId,
255
-						   array $groups = []) {
256
-		// Check if app is already downloaded
257
-		/** @var Installer $installer */
258
-		$installer = \OC::$server->query(Installer::class);
259
-		$isDownloaded = $installer->isDownloaded($appId);
260
-
261
-		if (!$isDownloaded) {
262
-			$installer->downloadApp($appId);
263
-		}
264
-
265
-		$installer->installApp($appId);
266
-
267
-		$appManager = \OC::$server->getAppManager();
268
-		if ($groups !== []) {
269
-			$groupManager = \OC::$server->getGroupManager();
270
-			$groupsList = [];
271
-			foreach ($groups as $group) {
272
-				$groupItem = $groupManager->get($group);
273
-				if ($groupItem instanceof \OCP\IGroup) {
274
-					$groupsList[] = $groupManager->get($group);
275
-				}
276
-			}
277
-			$appManager->enableAppForGroups($appId, $groupsList);
278
-		} else {
279
-			$appManager->enableApp($appId);
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * Get the path where to install apps
285
-	 *
286
-	 * @return string|false
287
-	 */
288
-	public static function getInstallPath() {
289
-		foreach (OC::$APPSROOTS as $dir) {
290
-			if (isset($dir['writable']) && $dir['writable'] === true) {
291
-				return $dir['path'];
292
-			}
293
-		}
294
-
295
-		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR);
296
-		return null;
297
-	}
298
-
299
-
300
-	/**
301
-	 * search for an app in all app-directories
302
-	 *
303
-	 * @param string $appId
304
-	 * @param bool $ignoreCache ignore cache and rebuild it
305
-	 * @return false|string
306
-	 */
307
-	public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
308
-		$sanitizedAppId = self::cleanAppId($appId);
309
-		if ($sanitizedAppId !== $appId) {
310
-			return false;
311
-		}
312
-		static $app_dir = [];
313
-
314
-		if (isset($app_dir[$appId]) && !$ignoreCache) {
315
-			return $app_dir[$appId];
316
-		}
317
-
318
-		$possibleApps = [];
319
-		foreach (OC::$APPSROOTS as $dir) {
320
-			if (file_exists($dir['path'] . '/' . $appId)) {
321
-				$possibleApps[] = $dir;
322
-			}
323
-		}
324
-
325
-		if (empty($possibleApps)) {
326
-			return false;
327
-		} elseif (count($possibleApps) === 1) {
328
-			$dir = array_shift($possibleApps);
329
-			$app_dir[$appId] = $dir;
330
-			return $dir;
331
-		} else {
332
-			$versionToLoad = [];
333
-			foreach ($possibleApps as $possibleApp) {
334
-				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
335
-				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336
-					$versionToLoad = [
337
-						'dir' => $possibleApp,
338
-						'version' => $version,
339
-					];
340
-				}
341
-			}
342
-			$app_dir[$appId] = $versionToLoad['dir'];
343
-			return $versionToLoad['dir'];
344
-			//TODO - write test
345
-		}
346
-	}
347
-
348
-	/**
349
-	 * Get the directory for the given app.
350
-	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
351
-	 *
352
-	 * @psalm-taint-specialize
353
-	 *
354
-	 * @param string $appId
355
-	 * @param bool $refreshAppPath should be set to true only during install/upgrade
356
-	 * @return string|false
357
-	 * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath()
358
-	 */
359
-	public static function getAppPath(string $appId, bool $refreshAppPath = false) {
360
-		if ($appId === null || trim($appId) === '') {
361
-			return false;
362
-		}
363
-
364
-		if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
-			return $dir['path'] . '/' . $appId;
366
-		}
367
-		return false;
368
-	}
369
-
370
-	/**
371
-	 * Get the path for the given app on the access
372
-	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
373
-	 *
374
-	 * @param string $appId
375
-	 * @return string|false
376
-	 * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
377
-	 */
378
-	public static function getAppWebPath(string $appId) {
379
-		if (($dir = self::findAppInDirectories($appId)) != false) {
380
-			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
381
-		}
382
-		return false;
383
-	}
384
-
385
-	/**
386
-	 * get app's version based on it's path
387
-	 *
388
-	 * @param string $path
389
-	 * @return string
390
-	 */
391
-	public static function getAppVersionByPath(string $path): string {
392
-		$infoFile = $path . '/appinfo/info.xml';
393
-		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394
-		return isset($appData['version']) ? $appData['version'] : '';
395
-	}
396
-
397
-	/**
398
-	 * get the id of loaded app
399
-	 *
400
-	 * @return string
401
-	 */
402
-	public static function getCurrentApp(): string {
403
-		if (\OC::$CLI) {
404
-			return '';
405
-		}
406
-
407
-		$request = \OC::$server->getRequest();
408
-		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
409
-		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
410
-		if (empty($topFolder)) {
411
-			try {
412
-				$path_info = $request->getPathInfo();
413
-			} catch (Exception $e) {
414
-				// Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
415
-				\OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
416
-				return '';
417
-			}
418
-			if ($path_info) {
419
-				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
420
-			}
421
-		}
422
-		if ($topFolder == 'apps') {
423
-			$length = strlen($topFolder);
424
-			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
425
-		} else {
426
-			return $topFolder;
427
-		}
428
-	}
429
-
430
-	/**
431
-	 * @param string $type
432
-	 * @return array
433
-	 */
434
-	public static function getForms(string $type): array {
435
-		$forms = [];
436
-		switch ($type) {
437
-			case 'admin':
438
-				$source = self::$adminForms;
439
-				break;
440
-			case 'personal':
441
-				$source = self::$personalForms;
442
-				break;
443
-			default:
444
-				return [];
445
-		}
446
-		foreach ($source as $form) {
447
-			$forms[] = include $form;
448
-		}
449
-		return $forms;
450
-	}
451
-
452
-	/**
453
-	 * @param array $entry
454
-	 * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface
455
-	 */
456
-	public static function registerLogIn(array $entry) {
457
-		\OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface');
458
-		self::$altLogin[] = $entry;
459
-	}
460
-
461
-	/**
462
-	 * @return array
463
-	 */
464
-	public static function getAlternativeLogIns(): array {
465
-		/** @var Coordinator $bootstrapCoordinator */
466
-		$bootstrapCoordinator = \OC::$server->query(Coordinator::class);
467
-
468
-		foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
469
-			if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
470
-				\OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
471
-					'option' => $registration->getService(),
472
-					'interface' => IAlternativeLogin::class,
473
-					'app' => $registration->getAppId(),
474
-				]);
475
-				continue;
476
-			}
477
-
478
-			try {
479
-				/** @var IAlternativeLogin $provider */
480
-				$provider = \OC::$server->query($registration->getService());
481
-			} catch (QueryException $e) {
482
-				\OC::$server->getLogger()->logException($e, [
483
-					'message' => 'Alternative login option {option} can not be initialised.',
484
-					'option' => $registration->getService(),
485
-					'app' => $registration->getAppId(),
486
-				]);
487
-			}
488
-
489
-			try {
490
-				$provider->load();
491
-
492
-				self::$altLogin[] = [
493
-					'name' => $provider->getLabel(),
494
-					'href' => $provider->getLink(),
495
-					'class' => $provider->getClass(),
496
-				];
497
-			} catch (Throwable $e) {
498
-				\OC::$server->getLogger()->logException($e, [
499
-					'message' => 'Alternative login option {option} had an error while loading.',
500
-					'option' => $registration->getService(),
501
-					'app' => $registration->getAppId(),
502
-				]);
503
-			}
504
-		}
505
-
506
-		return self::$altLogin;
507
-	}
508
-
509
-	/**
510
-	 * get a list of all apps in the apps folder
511
-	 *
512
-	 * @return string[] an array of app names (string IDs)
513
-	 * @todo: change the name of this method to getInstalledApps, which is more accurate
514
-	 */
515
-	public static function getAllApps(): array {
516
-		$apps = [];
517
-
518
-		foreach (OC::$APPSROOTS as $apps_dir) {
519
-			if (!is_readable($apps_dir['path'])) {
520
-				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
521
-				continue;
522
-			}
523
-			$dh = opendir($apps_dir['path']);
524
-
525
-			if (is_resource($dh)) {
526
-				while (($file = readdir($dh)) !== false) {
527
-					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
528
-						$apps[] = $file;
529
-					}
530
-				}
531
-			}
532
-		}
533
-
534
-		$apps = array_unique($apps);
535
-
536
-		return $apps;
537
-	}
538
-
539
-	/**
540
-	 * List all supported apps
541
-	 *
542
-	 * @return array
543
-	 */
544
-	public function getSupportedApps(): array {
545
-		/** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
546
-		$subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
547
-		$supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
548
-		return $supportedApps;
549
-	}
550
-
551
-	/**
552
-	 * List all apps, this is used in apps.php
553
-	 *
554
-	 * @return array
555
-	 */
556
-	public function listAllApps(): array {
557
-		$installedApps = OC_App::getAllApps();
558
-
559
-		$appManager = \OC::$server->getAppManager();
560
-		//we don't want to show configuration for these
561
-		$blacklist = $appManager->getAlwaysEnabledApps();
562
-		$appList = [];
563
-		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
564
-		$urlGenerator = \OC::$server->getURLGenerator();
565
-		$supportedApps = $this->getSupportedApps();
566
-
567
-		foreach ($installedApps as $app) {
568
-			if (array_search($app, $blacklist) === false) {
569
-				$info = $appManager->getAppInfo($app, false, $langCode);
570
-				if (!is_array($info)) {
571
-					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
572
-					continue;
573
-				}
574
-
575
-				if (!isset($info['name'])) {
576
-					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
577
-					continue;
578
-				}
579
-
580
-				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
581
-				$info['groups'] = null;
582
-				if ($enabled === 'yes') {
583
-					$active = true;
584
-				} elseif ($enabled === 'no') {
585
-					$active = false;
586
-				} else {
587
-					$active = true;
588
-					$info['groups'] = $enabled;
589
-				}
590
-
591
-				$info['active'] = $active;
592
-
593
-				if ($appManager->isShipped($app)) {
594
-					$info['internal'] = true;
595
-					$info['level'] = self::officialApp;
596
-					$info['removable'] = false;
597
-				} else {
598
-					$info['internal'] = false;
599
-					$info['removable'] = true;
600
-				}
601
-
602
-				if (in_array($app, $supportedApps)) {
603
-					$info['level'] = self::supportedApp;
604
-				}
605
-
606
-				$appPath = self::getAppPath($app);
607
-				if ($appPath !== false) {
608
-					$appIcon = $appPath . '/img/' . $app . '.svg';
609
-					if (file_exists($appIcon)) {
610
-						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
611
-						$info['previewAsIcon'] = true;
612
-					} else {
613
-						$appIcon = $appPath . '/img/app.svg';
614
-						if (file_exists($appIcon)) {
615
-							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616
-							$info['previewAsIcon'] = true;
617
-						}
618
-					}
619
-				}
620
-				// fix documentation
621
-				if (isset($info['documentation']) && is_array($info['documentation'])) {
622
-					foreach ($info['documentation'] as $key => $url) {
623
-						// If it is not an absolute URL we assume it is a key
624
-						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
625
-						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
626
-							$url = $urlGenerator->linkToDocs($url);
627
-						}
628
-
629
-						$info['documentation'][$key] = $url;
630
-					}
631
-				}
632
-
633
-				$info['version'] = $appManager->getAppVersion($app);
634
-				$appList[] = $info;
635
-			}
636
-		}
637
-
638
-		return $appList;
639
-	}
640
-
641
-	public static function shouldUpgrade(string $app): bool {
642
-		$versions = self::getAppVersions();
643
-		$currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
644
-		if ($currentVersion && isset($versions[$app])) {
645
-			$installedVersion = $versions[$app];
646
-			if (!version_compare($currentVersion, $installedVersion, '=')) {
647
-				return true;
648
-			}
649
-		}
650
-		return false;
651
-	}
652
-
653
-	/**
654
-	 * Adjust the number of version parts of $version1 to match
655
-	 * the number of version parts of $version2.
656
-	 *
657
-	 * @param string $version1 version to adjust
658
-	 * @param string $version2 version to take the number of parts from
659
-	 * @return string shortened $version1
660
-	 */
661
-	private static function adjustVersionParts(string $version1, string $version2): string {
662
-		$version1 = explode('.', $version1);
663
-		$version2 = explode('.', $version2);
664
-		// reduce $version1 to match the number of parts in $version2
665
-		while (count($version1) > count($version2)) {
666
-			array_pop($version1);
667
-		}
668
-		// if $version1 does not have enough parts, add some
669
-		while (count($version1) < count($version2)) {
670
-			$version1[] = '0';
671
-		}
672
-		return implode('.', $version1);
673
-	}
674
-
675
-	/**
676
-	 * Check whether the current ownCloud version matches the given
677
-	 * application's version requirements.
678
-	 *
679
-	 * The comparison is made based on the number of parts that the
680
-	 * app info version has. For example for ownCloud 6.0.3 if the
681
-	 * app info version is expecting version 6.0, the comparison is
682
-	 * made on the first two parts of the ownCloud version.
683
-	 * This means that it's possible to specify "requiremin" => 6
684
-	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
685
-	 *
686
-	 * @param string $ocVersion ownCloud version to check against
687
-	 * @param array $appInfo app info (from xml)
688
-	 *
689
-	 * @return boolean true if compatible, otherwise false
690
-	 */
691
-	public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
692
-		$requireMin = '';
693
-		$requireMax = '';
694
-		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
695
-			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
696
-		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
697
-			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
698
-		} elseif (isset($appInfo['requiremin'])) {
699
-			$requireMin = $appInfo['requiremin'];
700
-		} elseif (isset($appInfo['require'])) {
701
-			$requireMin = $appInfo['require'];
702
-		}
703
-
704
-		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
705
-			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
706
-		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
707
-			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
708
-		} elseif (isset($appInfo['requiremax'])) {
709
-			$requireMax = $appInfo['requiremax'];
710
-		}
711
-
712
-		if (!empty($requireMin)
713
-			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
714
-		) {
715
-			return false;
716
-		}
717
-
718
-		if (!$ignoreMax && !empty($requireMax)
719
-			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
720
-		) {
721
-			return false;
722
-		}
723
-
724
-		return true;
725
-	}
726
-
727
-	/**
728
-	 * get the installed version of all apps
729
-	 */
730
-	public static function getAppVersions() {
731
-		static $versions;
732
-
733
-		if (!$versions) {
734
-			$appConfig = \OC::$server->getAppConfig();
735
-			$versions = $appConfig->getValues(false, 'installed_version');
736
-		}
737
-		return $versions;
738
-	}
739
-
740
-	/**
741
-	 * update the database for the app and call the update script
742
-	 *
743
-	 * @param string $appId
744
-	 * @return bool
745
-	 */
746
-	public static function updateApp(string $appId): bool {
747
-		// for apps distributed with core, we refresh app path in case the downloaded version
748
-		// have been installed in custom apps and not in the default path
749
-		$appPath = self::getAppPath($appId, true);
750
-		if ($appPath === false) {
751
-			return false;
752
-		}
753
-
754
-		if (is_file($appPath . '/appinfo/database.xml')) {
755
-			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
756
-			return false;
757
-		}
758
-
759
-		\OC::$server->getAppManager()->clearAppsCache();
760
-		$l = \OC::$server->getL10N('core');
761
-		$appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
762
-
763
-		$ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
764
-		$ignoreMax = in_array($appId, $ignoreMaxApps, true);
765
-		\OC_App::checkAppDependencies(
766
-			\OC::$server->getConfig(),
767
-			$l,
768
-			$appData,
769
-			$ignoreMax
770
-		);
771
-
772
-		self::registerAutoloading($appId, $appPath, true);
773
-		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
774
-
775
-		$ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
776
-		$ms->migrate();
777
-
778
-		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
779
-		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
780
-		// update appversion in app manager
781
-		\OC::$server->getAppManager()->clearAppsCache();
782
-		\OC::$server->getAppManager()->getAppVersion($appId, false);
783
-
784
-		self::setupBackgroundJobs($appData['background-jobs']);
785
-
786
-		//set remote/public handlers
787
-		if (array_key_exists('ocsid', $appData)) {
788
-			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
789
-		} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
790
-			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791
-		}
792
-		foreach ($appData['remote'] as $name => $path) {
793
-			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
794
-		}
795
-		foreach ($appData['public'] as $name => $path) {
796
-			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
797
-		}
798
-
799
-		self::setAppTypes($appId);
800
-
801
-		$version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
802
-		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
803
-
804
-		\OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
805
-		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
806
-			ManagerEvent::EVENT_APP_UPDATE, $appId
807
-		));
808
-
809
-		return true;
810
-	}
811
-
812
-	/**
813
-	 * @param string $appId
814
-	 * @param string[] $steps
815
-	 * @throws \OC\NeedsUpdateException
816
-	 */
817
-	public static function executeRepairSteps(string $appId, array $steps) {
818
-		if (empty($steps)) {
819
-			return;
820
-		}
821
-		// load the app
822
-		self::loadApp($appId);
823
-
824
-		$dispatcher = \OC::$server->get(IEventDispatcher::class);
825
-
826
-		// load the steps
827
-		$r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
828
-		foreach ($steps as $step) {
829
-			try {
830
-				$r->addStep($step);
831
-			} catch (Exception $ex) {
832
-				$dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
833
-				\OC::$server->getLogger()->logException($ex);
834
-			}
835
-		}
836
-		// run the steps
837
-		$r->run();
838
-	}
839
-
840
-	public static function setupBackgroundJobs(array $jobs) {
841
-		$queue = \OC::$server->getJobList();
842
-		foreach ($jobs as $job) {
843
-			$queue->add($job);
844
-		}
845
-	}
846
-
847
-	/**
848
-	 * @param string $appId
849
-	 * @param string[] $steps
850
-	 */
851
-	private static function setupLiveMigrations(string $appId, array $steps) {
852
-		$queue = \OC::$server->getJobList();
853
-		foreach ($steps as $step) {
854
-			$queue->add('OC\Migration\BackgroundRepair', [
855
-				'app' => $appId,
856
-				'step' => $step]);
857
-		}
858
-	}
859
-
860
-	/**
861
-	 * @param string $appId
862
-	 * @return \OC\Files\View|false
863
-	 */
864
-	public static function getStorage(string $appId) {
865
-		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866
-			if (\OC::$server->getUserSession()->isLoggedIn()) {
867
-				$view = new \OC\Files\View('/' . OC_User::getUser());
868
-				if (!$view->file_exists($appId)) {
869
-					$view->mkdir($appId);
870
-				}
871
-				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
872
-			} else {
873
-				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
874
-				return false;
875
-			}
876
-		} else {
877
-			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
878
-			return false;
879
-		}
880
-	}
881
-
882
-	protected static function findBestL10NOption(array $options, string $lang): string {
883
-		// only a single option
884
-		if (isset($options['@value'])) {
885
-			return $options['@value'];
886
-		}
887
-
888
-		$fallback = $similarLangFallback = $englishFallback = false;
889
-
890
-		$lang = strtolower($lang);
891
-		$similarLang = $lang;
892
-		if (strpos($similarLang, '_')) {
893
-			// For "de_DE" we want to find "de" and the other way around
894
-			$similarLang = substr($lang, 0, strpos($lang, '_'));
895
-		}
896
-
897
-		foreach ($options as $option) {
898
-			if (is_array($option)) {
899
-				if ($fallback === false) {
900
-					$fallback = $option['@value'];
901
-				}
902
-
903
-				if (!isset($option['@attributes']['lang'])) {
904
-					continue;
905
-				}
906
-
907
-				$attributeLang = strtolower($option['@attributes']['lang']);
908
-				if ($attributeLang === $lang) {
909
-					return $option['@value'];
910
-				}
911
-
912
-				if ($attributeLang === $similarLang) {
913
-					$similarLangFallback = $option['@value'];
914
-				} elseif (strpos($attributeLang, $similarLang . '_') === 0) {
915
-					if ($similarLangFallback === false) {
916
-						$similarLangFallback = $option['@value'];
917
-					}
918
-				}
919
-			} else {
920
-				$englishFallback = $option;
921
-			}
922
-		}
923
-
924
-		if ($similarLangFallback !== false) {
925
-			return $similarLangFallback;
926
-		} elseif ($englishFallback !== false) {
927
-			return $englishFallback;
928
-		}
929
-		return (string) $fallback;
930
-	}
931
-
932
-	/**
933
-	 * parses the app data array and enhanced the 'description' value
934
-	 *
935
-	 * @param array $data the app data
936
-	 * @param string $lang
937
-	 * @return array improved app data
938
-	 */
939
-	public static function parseAppInfo(array $data, $lang = null): array {
940
-		if ($lang && isset($data['name']) && is_array($data['name'])) {
941
-			$data['name'] = self::findBestL10NOption($data['name'], $lang);
942
-		}
943
-		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
944
-			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
945
-		}
946
-		if ($lang && isset($data['description']) && is_array($data['description'])) {
947
-			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
948
-		} elseif (isset($data['description']) && is_string($data['description'])) {
949
-			$data['description'] = trim($data['description']);
950
-		} else {
951
-			$data['description'] = '';
952
-		}
953
-
954
-		return $data;
955
-	}
956
-
957
-	/**
958
-	 * @param \OCP\IConfig $config
959
-	 * @param \OCP\IL10N $l
960
-	 * @param array $info
961
-	 * @throws \Exception
962
-	 */
963
-	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
964
-		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
965
-		$missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
966
-		if (!empty($missing)) {
967
-			$missingMsg = implode(PHP_EOL, $missing);
968
-			throw new \Exception(
969
-				$l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
970
-					[$info['name'], $missingMsg]
971
-				)
972
-			);
973
-		}
974
-	}
76
+    private static $adminForms = [];
77
+    private static $personalForms = [];
78
+    private static $altLogin = [];
79
+    private static $alreadyRegistered = [];
80
+    public const supportedApp = 300;
81
+    public const officialApp = 200;
82
+
83
+    /**
84
+     * clean the appId
85
+     *
86
+     * @psalm-taint-escape file
87
+     * @psalm-taint-escape include
88
+     * @psalm-taint-escape html
89
+     * @psalm-taint-escape has_quotes
90
+     *
91
+     * @param string $app AppId that needs to be cleaned
92
+     * @return string
93
+     */
94
+    public static function cleanAppId(string $app): string {
95
+        return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app);
96
+    }
97
+
98
+    /**
99
+     * Check if an app is loaded
100
+     *
101
+     * @param string $app
102
+     * @return bool
103
+     * @deprecated 27.0.0 use IAppManager::isAppLoaded
104
+     */
105
+    public static function isAppLoaded(string $app): bool {
106
+        return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
107
+    }
108
+
109
+    /**
110
+     * loads all apps
111
+     *
112
+     * @param string[] $types
113
+     * @return bool
114
+     *
115
+     * This function walks through the ownCloud directory and loads all apps
116
+     * it can find. A directory contains an app if the file /appinfo/info.xml
117
+     * exists.
118
+     *
119
+     * if $types is set to non-empty array, only apps of those types will be loaded
120
+     */
121
+    public static function loadApps(array $types = []): bool {
122
+        if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
123
+            // This should be done before calling this method so that appmanager can be used
124
+            return false;
125
+        }
126
+        return \OC::$server->get(IAppManager::class)->loadApps($types);
127
+    }
128
+
129
+    /**
130
+     * load a single app
131
+     *
132
+     * @param string $app
133
+     * @throws Exception
134
+     * @deprecated 27.0.0 use IAppManager::loadApp
135
+     */
136
+    public static function loadApp(string $app): void {
137
+        \OC::$server->get(IAppManager::class)->loadApp($app);
138
+    }
139
+
140
+    /**
141
+     * @internal
142
+     * @param string $app
143
+     * @param string $path
144
+     * @param bool $force
145
+     */
146
+    public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
+        $key = $app . '-' . $path;
148
+        if (!$force && isset(self::$alreadyRegistered[$key])) {
149
+            return;
150
+        }
151
+
152
+        self::$alreadyRegistered[$key] = true;
153
+
154
+        // Register on PSR-4 composer autoloader
155
+        $appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156
+        \OC::$server->registerNamespace($app, $appNamespace);
157
+
158
+        if (file_exists($path . '/composer/autoload.php')) {
159
+            require_once $path . '/composer/autoload.php';
160
+        } else {
161
+            \OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
162
+        }
163
+
164
+        // Register Test namespace only when testing
165
+        if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
+            \OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
167
+        }
168
+    }
169
+
170
+    /**
171
+     * check if an app is of a specific type
172
+     *
173
+     * @param string $app
174
+     * @param array $types
175
+     * @return bool
176
+     * @deprecated 27.0.0 use IAppManager::isType
177
+     */
178
+    public static function isType(string $app, array $types): bool {
179
+        return \OC::$server->get(IAppManager::class)->isType($app, $types);
180
+    }
181
+
182
+    /**
183
+     * read app types from info.xml and cache them in the database
184
+     */
185
+    public static function setAppTypes(string $app) {
186
+        $appManager = \OC::$server->getAppManager();
187
+        $appData = $appManager->getAppInfo($app);
188
+        if (!is_array($appData)) {
189
+            return;
190
+        }
191
+
192
+        if (isset($appData['types'])) {
193
+            $appTypes = implode(',', $appData['types']);
194
+        } else {
195
+            $appTypes = '';
196
+            $appData['types'] = [];
197
+        }
198
+
199
+        $config = \OC::$server->getConfig();
200
+        $config->setAppValue($app, 'types', $appTypes);
201
+
202
+        if ($appManager->hasProtectedAppType($appData['types'])) {
203
+            $enabled = $config->getAppValue($app, 'enabled', 'yes');
204
+            if ($enabled !== 'yes' && $enabled !== 'no') {
205
+                $config->setAppValue($app, 'enabled', 'yes');
206
+            }
207
+        }
208
+    }
209
+
210
+    /**
211
+     * Returns apps enabled for the current user.
212
+     *
213
+     * @param bool $forceRefresh whether to refresh the cache
214
+     * @param bool $all whether to return apps for all users, not only the
215
+     * currently logged in one
216
+     * @return string[]
217
+     */
218
+    public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
219
+        if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
220
+            return [];
221
+        }
222
+        // in incognito mode or when logged out, $user will be false,
223
+        // which is also the case during an upgrade
224
+        $appManager = \OC::$server->getAppManager();
225
+        if ($all) {
226
+            $user = null;
227
+        } else {
228
+            $user = \OC::$server->getUserSession()->getUser();
229
+        }
230
+
231
+        if (is_null($user)) {
232
+            $apps = $appManager->getInstalledApps();
233
+        } else {
234
+            $apps = $appManager->getEnabledAppsForUser($user);
235
+        }
236
+        $apps = array_filter($apps, function ($app) {
237
+            return $app !== 'files';//we add this manually
238
+        });
239
+        sort($apps);
240
+        array_unshift($apps, 'files');
241
+        return $apps;
242
+    }
243
+
244
+    /**
245
+     * enables an app
246
+     *
247
+     * @param string $appId
248
+     * @param array $groups (optional) when set, only these groups will have access to the app
249
+     * @throws \Exception
250
+     * @return void
251
+     *
252
+     * This function set an app as enabled in appconfig.
253
+     */
254
+    public function enable(string $appId,
255
+                            array $groups = []) {
256
+        // Check if app is already downloaded
257
+        /** @var Installer $installer */
258
+        $installer = \OC::$server->query(Installer::class);
259
+        $isDownloaded = $installer->isDownloaded($appId);
260
+
261
+        if (!$isDownloaded) {
262
+            $installer->downloadApp($appId);
263
+        }
264
+
265
+        $installer->installApp($appId);
266
+
267
+        $appManager = \OC::$server->getAppManager();
268
+        if ($groups !== []) {
269
+            $groupManager = \OC::$server->getGroupManager();
270
+            $groupsList = [];
271
+            foreach ($groups as $group) {
272
+                $groupItem = $groupManager->get($group);
273
+                if ($groupItem instanceof \OCP\IGroup) {
274
+                    $groupsList[] = $groupManager->get($group);
275
+                }
276
+            }
277
+            $appManager->enableAppForGroups($appId, $groupsList);
278
+        } else {
279
+            $appManager->enableApp($appId);
280
+        }
281
+    }
282
+
283
+    /**
284
+     * Get the path where to install apps
285
+     *
286
+     * @return string|false
287
+     */
288
+    public static function getInstallPath() {
289
+        foreach (OC::$APPSROOTS as $dir) {
290
+            if (isset($dir['writable']) && $dir['writable'] === true) {
291
+                return $dir['path'];
292
+            }
293
+        }
294
+
295
+        \OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR);
296
+        return null;
297
+    }
298
+
299
+
300
+    /**
301
+     * search for an app in all app-directories
302
+     *
303
+     * @param string $appId
304
+     * @param bool $ignoreCache ignore cache and rebuild it
305
+     * @return false|string
306
+     */
307
+    public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
308
+        $sanitizedAppId = self::cleanAppId($appId);
309
+        if ($sanitizedAppId !== $appId) {
310
+            return false;
311
+        }
312
+        static $app_dir = [];
313
+
314
+        if (isset($app_dir[$appId]) && !$ignoreCache) {
315
+            return $app_dir[$appId];
316
+        }
317
+
318
+        $possibleApps = [];
319
+        foreach (OC::$APPSROOTS as $dir) {
320
+            if (file_exists($dir['path'] . '/' . $appId)) {
321
+                $possibleApps[] = $dir;
322
+            }
323
+        }
324
+
325
+        if (empty($possibleApps)) {
326
+            return false;
327
+        } elseif (count($possibleApps) === 1) {
328
+            $dir = array_shift($possibleApps);
329
+            $app_dir[$appId] = $dir;
330
+            return $dir;
331
+        } else {
332
+            $versionToLoad = [];
333
+            foreach ($possibleApps as $possibleApp) {
334
+                $version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
335
+                if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336
+                    $versionToLoad = [
337
+                        'dir' => $possibleApp,
338
+                        'version' => $version,
339
+                    ];
340
+                }
341
+            }
342
+            $app_dir[$appId] = $versionToLoad['dir'];
343
+            return $versionToLoad['dir'];
344
+            //TODO - write test
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Get the directory for the given app.
350
+     * If the app is defined in multiple directories, the first one is taken. (false if not found)
351
+     *
352
+     * @psalm-taint-specialize
353
+     *
354
+     * @param string $appId
355
+     * @param bool $refreshAppPath should be set to true only during install/upgrade
356
+     * @return string|false
357
+     * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath()
358
+     */
359
+    public static function getAppPath(string $appId, bool $refreshAppPath = false) {
360
+        if ($appId === null || trim($appId) === '') {
361
+            return false;
362
+        }
363
+
364
+        if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
+            return $dir['path'] . '/' . $appId;
366
+        }
367
+        return false;
368
+    }
369
+
370
+    /**
371
+     * Get the path for the given app on the access
372
+     * If the app is defined in multiple directories, the first one is taken. (false if not found)
373
+     *
374
+     * @param string $appId
375
+     * @return string|false
376
+     * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
377
+     */
378
+    public static function getAppWebPath(string $appId) {
379
+        if (($dir = self::findAppInDirectories($appId)) != false) {
380
+            return OC::$WEBROOT . $dir['url'] . '/' . $appId;
381
+        }
382
+        return false;
383
+    }
384
+
385
+    /**
386
+     * get app's version based on it's path
387
+     *
388
+     * @param string $path
389
+     * @return string
390
+     */
391
+    public static function getAppVersionByPath(string $path): string {
392
+        $infoFile = $path . '/appinfo/info.xml';
393
+        $appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394
+        return isset($appData['version']) ? $appData['version'] : '';
395
+    }
396
+
397
+    /**
398
+     * get the id of loaded app
399
+     *
400
+     * @return string
401
+     */
402
+    public static function getCurrentApp(): string {
403
+        if (\OC::$CLI) {
404
+            return '';
405
+        }
406
+
407
+        $request = \OC::$server->getRequest();
408
+        $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
409
+        $topFolder = substr($script, 0, strpos($script, '/') ?: 0);
410
+        if (empty($topFolder)) {
411
+            try {
412
+                $path_info = $request->getPathInfo();
413
+            } catch (Exception $e) {
414
+                // Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
415
+                \OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
416
+                return '';
417
+            }
418
+            if ($path_info) {
419
+                $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
420
+            }
421
+        }
422
+        if ($topFolder == 'apps') {
423
+            $length = strlen($topFolder);
424
+            return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
425
+        } else {
426
+            return $topFolder;
427
+        }
428
+    }
429
+
430
+    /**
431
+     * @param string $type
432
+     * @return array
433
+     */
434
+    public static function getForms(string $type): array {
435
+        $forms = [];
436
+        switch ($type) {
437
+            case 'admin':
438
+                $source = self::$adminForms;
439
+                break;
440
+            case 'personal':
441
+                $source = self::$personalForms;
442
+                break;
443
+            default:
444
+                return [];
445
+        }
446
+        foreach ($source as $form) {
447
+            $forms[] = include $form;
448
+        }
449
+        return $forms;
450
+    }
451
+
452
+    /**
453
+     * @param array $entry
454
+     * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface
455
+     */
456
+    public static function registerLogIn(array $entry) {
457
+        \OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface');
458
+        self::$altLogin[] = $entry;
459
+    }
460
+
461
+    /**
462
+     * @return array
463
+     */
464
+    public static function getAlternativeLogIns(): array {
465
+        /** @var Coordinator $bootstrapCoordinator */
466
+        $bootstrapCoordinator = \OC::$server->query(Coordinator::class);
467
+
468
+        foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
469
+            if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
470
+                \OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
471
+                    'option' => $registration->getService(),
472
+                    'interface' => IAlternativeLogin::class,
473
+                    'app' => $registration->getAppId(),
474
+                ]);
475
+                continue;
476
+            }
477
+
478
+            try {
479
+                /** @var IAlternativeLogin $provider */
480
+                $provider = \OC::$server->query($registration->getService());
481
+            } catch (QueryException $e) {
482
+                \OC::$server->getLogger()->logException($e, [
483
+                    'message' => 'Alternative login option {option} can not be initialised.',
484
+                    'option' => $registration->getService(),
485
+                    'app' => $registration->getAppId(),
486
+                ]);
487
+            }
488
+
489
+            try {
490
+                $provider->load();
491
+
492
+                self::$altLogin[] = [
493
+                    'name' => $provider->getLabel(),
494
+                    'href' => $provider->getLink(),
495
+                    'class' => $provider->getClass(),
496
+                ];
497
+            } catch (Throwable $e) {
498
+                \OC::$server->getLogger()->logException($e, [
499
+                    'message' => 'Alternative login option {option} had an error while loading.',
500
+                    'option' => $registration->getService(),
501
+                    'app' => $registration->getAppId(),
502
+                ]);
503
+            }
504
+        }
505
+
506
+        return self::$altLogin;
507
+    }
508
+
509
+    /**
510
+     * get a list of all apps in the apps folder
511
+     *
512
+     * @return string[] an array of app names (string IDs)
513
+     * @todo: change the name of this method to getInstalledApps, which is more accurate
514
+     */
515
+    public static function getAllApps(): array {
516
+        $apps = [];
517
+
518
+        foreach (OC::$APPSROOTS as $apps_dir) {
519
+            if (!is_readable($apps_dir['path'])) {
520
+                \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
521
+                continue;
522
+            }
523
+            $dh = opendir($apps_dir['path']);
524
+
525
+            if (is_resource($dh)) {
526
+                while (($file = readdir($dh)) !== false) {
527
+                    if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
528
+                        $apps[] = $file;
529
+                    }
530
+                }
531
+            }
532
+        }
533
+
534
+        $apps = array_unique($apps);
535
+
536
+        return $apps;
537
+    }
538
+
539
+    /**
540
+     * List all supported apps
541
+     *
542
+     * @return array
543
+     */
544
+    public function getSupportedApps(): array {
545
+        /** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
546
+        $subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
547
+        $supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
548
+        return $supportedApps;
549
+    }
550
+
551
+    /**
552
+     * List all apps, this is used in apps.php
553
+     *
554
+     * @return array
555
+     */
556
+    public function listAllApps(): array {
557
+        $installedApps = OC_App::getAllApps();
558
+
559
+        $appManager = \OC::$server->getAppManager();
560
+        //we don't want to show configuration for these
561
+        $blacklist = $appManager->getAlwaysEnabledApps();
562
+        $appList = [];
563
+        $langCode = \OC::$server->getL10N('core')->getLanguageCode();
564
+        $urlGenerator = \OC::$server->getURLGenerator();
565
+        $supportedApps = $this->getSupportedApps();
566
+
567
+        foreach ($installedApps as $app) {
568
+            if (array_search($app, $blacklist) === false) {
569
+                $info = $appManager->getAppInfo($app, false, $langCode);
570
+                if (!is_array($info)) {
571
+                    \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
572
+                    continue;
573
+                }
574
+
575
+                if (!isset($info['name'])) {
576
+                    \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
577
+                    continue;
578
+                }
579
+
580
+                $enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
581
+                $info['groups'] = null;
582
+                if ($enabled === 'yes') {
583
+                    $active = true;
584
+                } elseif ($enabled === 'no') {
585
+                    $active = false;
586
+                } else {
587
+                    $active = true;
588
+                    $info['groups'] = $enabled;
589
+                }
590
+
591
+                $info['active'] = $active;
592
+
593
+                if ($appManager->isShipped($app)) {
594
+                    $info['internal'] = true;
595
+                    $info['level'] = self::officialApp;
596
+                    $info['removable'] = false;
597
+                } else {
598
+                    $info['internal'] = false;
599
+                    $info['removable'] = true;
600
+                }
601
+
602
+                if (in_array($app, $supportedApps)) {
603
+                    $info['level'] = self::supportedApp;
604
+                }
605
+
606
+                $appPath = self::getAppPath($app);
607
+                if ($appPath !== false) {
608
+                    $appIcon = $appPath . '/img/' . $app . '.svg';
609
+                    if (file_exists($appIcon)) {
610
+                        $info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
611
+                        $info['previewAsIcon'] = true;
612
+                    } else {
613
+                        $appIcon = $appPath . '/img/app.svg';
614
+                        if (file_exists($appIcon)) {
615
+                            $info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616
+                            $info['previewAsIcon'] = true;
617
+                        }
618
+                    }
619
+                }
620
+                // fix documentation
621
+                if (isset($info['documentation']) && is_array($info['documentation'])) {
622
+                    foreach ($info['documentation'] as $key => $url) {
623
+                        // If it is not an absolute URL we assume it is a key
624
+                        // i.e. admin-ldap will get converted to go.php?to=admin-ldap
625
+                        if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
626
+                            $url = $urlGenerator->linkToDocs($url);
627
+                        }
628
+
629
+                        $info['documentation'][$key] = $url;
630
+                    }
631
+                }
632
+
633
+                $info['version'] = $appManager->getAppVersion($app);
634
+                $appList[] = $info;
635
+            }
636
+        }
637
+
638
+        return $appList;
639
+    }
640
+
641
+    public static function shouldUpgrade(string $app): bool {
642
+        $versions = self::getAppVersions();
643
+        $currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
644
+        if ($currentVersion && isset($versions[$app])) {
645
+            $installedVersion = $versions[$app];
646
+            if (!version_compare($currentVersion, $installedVersion, '=')) {
647
+                return true;
648
+            }
649
+        }
650
+        return false;
651
+    }
652
+
653
+    /**
654
+     * Adjust the number of version parts of $version1 to match
655
+     * the number of version parts of $version2.
656
+     *
657
+     * @param string $version1 version to adjust
658
+     * @param string $version2 version to take the number of parts from
659
+     * @return string shortened $version1
660
+     */
661
+    private static function adjustVersionParts(string $version1, string $version2): string {
662
+        $version1 = explode('.', $version1);
663
+        $version2 = explode('.', $version2);
664
+        // reduce $version1 to match the number of parts in $version2
665
+        while (count($version1) > count($version2)) {
666
+            array_pop($version1);
667
+        }
668
+        // if $version1 does not have enough parts, add some
669
+        while (count($version1) < count($version2)) {
670
+            $version1[] = '0';
671
+        }
672
+        return implode('.', $version1);
673
+    }
674
+
675
+    /**
676
+     * Check whether the current ownCloud version matches the given
677
+     * application's version requirements.
678
+     *
679
+     * The comparison is made based on the number of parts that the
680
+     * app info version has. For example for ownCloud 6.0.3 if the
681
+     * app info version is expecting version 6.0, the comparison is
682
+     * made on the first two parts of the ownCloud version.
683
+     * This means that it's possible to specify "requiremin" => 6
684
+     * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
685
+     *
686
+     * @param string $ocVersion ownCloud version to check against
687
+     * @param array $appInfo app info (from xml)
688
+     *
689
+     * @return boolean true if compatible, otherwise false
690
+     */
691
+    public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
692
+        $requireMin = '';
693
+        $requireMax = '';
694
+        if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
695
+            $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
696
+        } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
697
+            $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
698
+        } elseif (isset($appInfo['requiremin'])) {
699
+            $requireMin = $appInfo['requiremin'];
700
+        } elseif (isset($appInfo['require'])) {
701
+            $requireMin = $appInfo['require'];
702
+        }
703
+
704
+        if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
705
+            $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
706
+        } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
707
+            $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
708
+        } elseif (isset($appInfo['requiremax'])) {
709
+            $requireMax = $appInfo['requiremax'];
710
+        }
711
+
712
+        if (!empty($requireMin)
713
+            && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
714
+        ) {
715
+            return false;
716
+        }
717
+
718
+        if (!$ignoreMax && !empty($requireMax)
719
+            && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
720
+        ) {
721
+            return false;
722
+        }
723
+
724
+        return true;
725
+    }
726
+
727
+    /**
728
+     * get the installed version of all apps
729
+     */
730
+    public static function getAppVersions() {
731
+        static $versions;
732
+
733
+        if (!$versions) {
734
+            $appConfig = \OC::$server->getAppConfig();
735
+            $versions = $appConfig->getValues(false, 'installed_version');
736
+        }
737
+        return $versions;
738
+    }
739
+
740
+    /**
741
+     * update the database for the app and call the update script
742
+     *
743
+     * @param string $appId
744
+     * @return bool
745
+     */
746
+    public static function updateApp(string $appId): bool {
747
+        // for apps distributed with core, we refresh app path in case the downloaded version
748
+        // have been installed in custom apps and not in the default path
749
+        $appPath = self::getAppPath($appId, true);
750
+        if ($appPath === false) {
751
+            return false;
752
+        }
753
+
754
+        if (is_file($appPath . '/appinfo/database.xml')) {
755
+            \OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
756
+            return false;
757
+        }
758
+
759
+        \OC::$server->getAppManager()->clearAppsCache();
760
+        $l = \OC::$server->getL10N('core');
761
+        $appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
762
+
763
+        $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
764
+        $ignoreMax = in_array($appId, $ignoreMaxApps, true);
765
+        \OC_App::checkAppDependencies(
766
+            \OC::$server->getConfig(),
767
+            $l,
768
+            $appData,
769
+            $ignoreMax
770
+        );
771
+
772
+        self::registerAutoloading($appId, $appPath, true);
773
+        self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
774
+
775
+        $ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
776
+        $ms->migrate();
777
+
778
+        self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
779
+        self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
780
+        // update appversion in app manager
781
+        \OC::$server->getAppManager()->clearAppsCache();
782
+        \OC::$server->getAppManager()->getAppVersion($appId, false);
783
+
784
+        self::setupBackgroundJobs($appData['background-jobs']);
785
+
786
+        //set remote/public handlers
787
+        if (array_key_exists('ocsid', $appData)) {
788
+            \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
789
+        } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
790
+            \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791
+        }
792
+        foreach ($appData['remote'] as $name => $path) {
793
+            \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
794
+        }
795
+        foreach ($appData['public'] as $name => $path) {
796
+            \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
797
+        }
798
+
799
+        self::setAppTypes($appId);
800
+
801
+        $version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
802
+        \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
803
+
804
+        \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
805
+        \OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
806
+            ManagerEvent::EVENT_APP_UPDATE, $appId
807
+        ));
808
+
809
+        return true;
810
+    }
811
+
812
+    /**
813
+     * @param string $appId
814
+     * @param string[] $steps
815
+     * @throws \OC\NeedsUpdateException
816
+     */
817
+    public static function executeRepairSteps(string $appId, array $steps) {
818
+        if (empty($steps)) {
819
+            return;
820
+        }
821
+        // load the app
822
+        self::loadApp($appId);
823
+
824
+        $dispatcher = \OC::$server->get(IEventDispatcher::class);
825
+
826
+        // load the steps
827
+        $r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
828
+        foreach ($steps as $step) {
829
+            try {
830
+                $r->addStep($step);
831
+            } catch (Exception $ex) {
832
+                $dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
833
+                \OC::$server->getLogger()->logException($ex);
834
+            }
835
+        }
836
+        // run the steps
837
+        $r->run();
838
+    }
839
+
840
+    public static function setupBackgroundJobs(array $jobs) {
841
+        $queue = \OC::$server->getJobList();
842
+        foreach ($jobs as $job) {
843
+            $queue->add($job);
844
+        }
845
+    }
846
+
847
+    /**
848
+     * @param string $appId
849
+     * @param string[] $steps
850
+     */
851
+    private static function setupLiveMigrations(string $appId, array $steps) {
852
+        $queue = \OC::$server->getJobList();
853
+        foreach ($steps as $step) {
854
+            $queue->add('OC\Migration\BackgroundRepair', [
855
+                'app' => $appId,
856
+                'step' => $step]);
857
+        }
858
+    }
859
+
860
+    /**
861
+     * @param string $appId
862
+     * @return \OC\Files\View|false
863
+     */
864
+    public static function getStorage(string $appId) {
865
+        if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866
+            if (\OC::$server->getUserSession()->isLoggedIn()) {
867
+                $view = new \OC\Files\View('/' . OC_User::getUser());
868
+                if (!$view->file_exists($appId)) {
869
+                    $view->mkdir($appId);
870
+                }
871
+                return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
872
+            } else {
873
+                \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
874
+                return false;
875
+            }
876
+        } else {
877
+            \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
878
+            return false;
879
+        }
880
+    }
881
+
882
+    protected static function findBestL10NOption(array $options, string $lang): string {
883
+        // only a single option
884
+        if (isset($options['@value'])) {
885
+            return $options['@value'];
886
+        }
887
+
888
+        $fallback = $similarLangFallback = $englishFallback = false;
889
+
890
+        $lang = strtolower($lang);
891
+        $similarLang = $lang;
892
+        if (strpos($similarLang, '_')) {
893
+            // For "de_DE" we want to find "de" and the other way around
894
+            $similarLang = substr($lang, 0, strpos($lang, '_'));
895
+        }
896
+
897
+        foreach ($options as $option) {
898
+            if (is_array($option)) {
899
+                if ($fallback === false) {
900
+                    $fallback = $option['@value'];
901
+                }
902
+
903
+                if (!isset($option['@attributes']['lang'])) {
904
+                    continue;
905
+                }
906
+
907
+                $attributeLang = strtolower($option['@attributes']['lang']);
908
+                if ($attributeLang === $lang) {
909
+                    return $option['@value'];
910
+                }
911
+
912
+                if ($attributeLang === $similarLang) {
913
+                    $similarLangFallback = $option['@value'];
914
+                } elseif (strpos($attributeLang, $similarLang . '_') === 0) {
915
+                    if ($similarLangFallback === false) {
916
+                        $similarLangFallback = $option['@value'];
917
+                    }
918
+                }
919
+            } else {
920
+                $englishFallback = $option;
921
+            }
922
+        }
923
+
924
+        if ($similarLangFallback !== false) {
925
+            return $similarLangFallback;
926
+        } elseif ($englishFallback !== false) {
927
+            return $englishFallback;
928
+        }
929
+        return (string) $fallback;
930
+    }
931
+
932
+    /**
933
+     * parses the app data array and enhanced the 'description' value
934
+     *
935
+     * @param array $data the app data
936
+     * @param string $lang
937
+     * @return array improved app data
938
+     */
939
+    public static function parseAppInfo(array $data, $lang = null): array {
940
+        if ($lang && isset($data['name']) && is_array($data['name'])) {
941
+            $data['name'] = self::findBestL10NOption($data['name'], $lang);
942
+        }
943
+        if ($lang && isset($data['summary']) && is_array($data['summary'])) {
944
+            $data['summary'] = self::findBestL10NOption($data['summary'], $lang);
945
+        }
946
+        if ($lang && isset($data['description']) && is_array($data['description'])) {
947
+            $data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
948
+        } elseif (isset($data['description']) && is_string($data['description'])) {
949
+            $data['description'] = trim($data['description']);
950
+        } else {
951
+            $data['description'] = '';
952
+        }
953
+
954
+        return $data;
955
+    }
956
+
957
+    /**
958
+     * @param \OCP\IConfig $config
959
+     * @param \OCP\IL10N $l
960
+     * @param array $info
961
+     * @throws \Exception
962
+     */
963
+    public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
964
+        $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
965
+        $missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
966
+        if (!empty($missing)) {
967
+            $missingMsg = implode(PHP_EOL, $missing);
968
+            throw new \Exception(
969
+                $l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
970
+                    [$info['name'], $missingMsg]
971
+                )
972
+            );
973
+        }
974
+    }
975 975
 }
Please login to merge, or discard this patch.
Spacing   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -144,7 +144,7 @@  discard block
 block discarded – undo
144 144
 	 * @param bool $force
145 145
 	 */
146 146
 	public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
-		$key = $app . '-' . $path;
147
+		$key = $app.'-'.$path;
148 148
 		if (!$force && isset(self::$alreadyRegistered[$key])) {
149 149
 			return;
150 150
 		}
@@ -155,15 +155,15 @@  discard block
 block discarded – undo
155 155
 		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156 156
 		\OC::$server->registerNamespace($app, $appNamespace);
157 157
 
158
-		if (file_exists($path . '/composer/autoload.php')) {
159
-			require_once $path . '/composer/autoload.php';
158
+		if (file_exists($path.'/composer/autoload.php')) {
159
+			require_once $path.'/composer/autoload.php';
160 160
 		} else {
161
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
161
+			\OC::$composerAutoloader->addPsr4($appNamespace.'\\', $path.'/lib/', true);
162 162
 		}
163 163
 
164 164
 		// Register Test namespace only when testing
165 165
 		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
-			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
166
+			\OC::$composerAutoloader->addPsr4($appNamespace.'\\Tests\\', $path.'/tests/', true);
167 167
 		}
168 168
 	}
169 169
 
@@ -233,8 +233,8 @@  discard block
 block discarded – undo
233 233
 		} else {
234 234
 			$apps = $appManager->getEnabledAppsForUser($user);
235 235
 		}
236
-		$apps = array_filter($apps, function ($app) {
237
-			return $app !== 'files';//we add this manually
236
+		$apps = array_filter($apps, function($app) {
237
+			return $app !== 'files'; //we add this manually
238 238
 		});
239 239
 		sort($apps);
240 240
 		array_unshift($apps, 'files');
@@ -317,7 +317,7 @@  discard block
 block discarded – undo
317 317
 
318 318
 		$possibleApps = [];
319 319
 		foreach (OC::$APPSROOTS as $dir) {
320
-			if (file_exists($dir['path'] . '/' . $appId)) {
320
+			if (file_exists($dir['path'].'/'.$appId)) {
321 321
 				$possibleApps[] = $dir;
322 322
 			}
323 323
 		}
@@ -331,7 +331,7 @@  discard block
 block discarded – undo
331 331
 		} else {
332 332
 			$versionToLoad = [];
333 333
 			foreach ($possibleApps as $possibleApp) {
334
-				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
334
+				$version = self::getAppVersionByPath($possibleApp['path'].'/'.$appId);
335 335
 				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336 336
 					$versionToLoad = [
337 337
 						'dir' => $possibleApp,
@@ -362,7 +362,7 @@  discard block
 block discarded – undo
362 362
 		}
363 363
 
364 364
 		if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
365
-			return $dir['path'] . '/' . $appId;
365
+			return $dir['path'].'/'.$appId;
366 366
 		}
367 367
 		return false;
368 368
 	}
@@ -377,7 +377,7 @@  discard block
 block discarded – undo
377 377
 	 */
378 378
 	public static function getAppWebPath(string $appId) {
379 379
 		if (($dir = self::findAppInDirectories($appId)) != false) {
380
-			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
380
+			return OC::$WEBROOT.$dir['url'].'/'.$appId;
381 381
 		}
382 382
 		return false;
383 383
 	}
@@ -389,7 +389,7 @@  discard block
 block discarded – undo
389 389
 	 * @return string
390 390
 	 */
391 391
 	public static function getAppVersionByPath(string $path): string {
392
-		$infoFile = $path . '/appinfo/info.xml';
392
+		$infoFile = $path.'/appinfo/info.xml';
393 393
 		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394 394
 		return isset($appData['version']) ? $appData['version'] : '';
395 395
 	}
@@ -517,14 +517,14 @@  discard block
 block discarded – undo
517 517
 
518 518
 		foreach (OC::$APPSROOTS as $apps_dir) {
519 519
 			if (!is_readable($apps_dir['path'])) {
520
-				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
520
+				\OCP\Util::writeLog('core', 'unable to read app folder : '.$apps_dir['path'], ILogger::WARN);
521 521
 				continue;
522 522
 			}
523 523
 			$dh = opendir($apps_dir['path']);
524 524
 
525 525
 			if (is_resource($dh)) {
526 526
 				while (($file = readdir($dh)) !== false) {
527
-					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
527
+					if ($file[0] != '.' and is_dir($apps_dir['path'].'/'.$file) and is_file($apps_dir['path'].'/'.$file.'/appinfo/info.xml')) {
528 528
 						$apps[] = $file;
529 529
 					}
530 530
 				}
@@ -568,12 +568,12 @@  discard block
 block discarded – undo
568 568
 			if (array_search($app, $blacklist) === false) {
569 569
 				$info = $appManager->getAppInfo($app, false, $langCode);
570 570
 				if (!is_array($info)) {
571
-					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
571
+					\OCP\Util::writeLog('core', 'Could not read app info file for app "'.$app.'"', ILogger::ERROR);
572 572
 					continue;
573 573
 				}
574 574
 
575 575
 				if (!isset($info['name'])) {
576
-					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
576
+					\OCP\Util::writeLog('core', 'App id "'.$app.'" has no name in appinfo', ILogger::ERROR);
577 577
 					continue;
578 578
 				}
579 579
 
@@ -605,12 +605,12 @@  discard block
 block discarded – undo
605 605
 
606 606
 				$appPath = self::getAppPath($app);
607 607
 				if ($appPath !== false) {
608
-					$appIcon = $appPath . '/img/' . $app . '.svg';
608
+					$appIcon = $appPath.'/img/'.$app.'.svg';
609 609
 					if (file_exists($appIcon)) {
610
-						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
610
+						$info['preview'] = $urlGenerator->imagePath($app, $app.'.svg');
611 611
 						$info['previewAsIcon'] = true;
612 612
 					} else {
613
-						$appIcon = $appPath . '/img/app.svg';
613
+						$appIcon = $appPath.'/img/app.svg';
614 614
 						if (file_exists($appIcon)) {
615 615
 							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616 616
 							$info['previewAsIcon'] = true;
@@ -751,8 +751,8 @@  discard block
 block discarded – undo
751 751
 			return false;
752 752
 		}
753 753
 
754
-		if (is_file($appPath . '/appinfo/database.xml')) {
755
-			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
754
+		if (is_file($appPath.'/appinfo/database.xml')) {
755
+			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in '.$appId);
756 756
 			return false;
757 757
 		}
758 758
 
@@ -790,10 +790,10 @@  discard block
 block discarded – undo
790 790
 			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791 791
 		}
792 792
 		foreach ($appData['remote'] as $name => $path) {
793
-			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
793
+			\OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $appId.'/'.$path);
794 794
 		}
795 795
 		foreach ($appData['public'] as $name => $path) {
796
-			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
796
+			\OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $appId.'/'.$path);
797 797
 		}
798 798
 
799 799
 		self::setAppTypes($appId);
@@ -864,17 +864,17 @@  discard block
 block discarded – undo
864 864
 	public static function getStorage(string $appId) {
865 865
 		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866 866
 			if (\OC::$server->getUserSession()->isLoggedIn()) {
867
-				$view = new \OC\Files\View('/' . OC_User::getUser());
867
+				$view = new \OC\Files\View('/'.OC_User::getUser());
868 868
 				if (!$view->file_exists($appId)) {
869 869
 					$view->mkdir($appId);
870 870
 				}
871
-				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
871
+				return new \OC\Files\View('/'.OC_User::getUser().'/'.$appId);
872 872
 			} else {
873
-				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
873
+				\OCP\Util::writeLog('core', 'Can\'t get app storage, app '.$appId.', user not logged in', ILogger::ERROR);
874 874
 				return false;
875 875
 			}
876 876
 		} else {
877
-			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
877
+			\OCP\Util::writeLog('core', 'Can\'t get app storage, app '.$appId.' not enabled', ILogger::ERROR);
878 878
 			return false;
879 879
 		}
880 880
 	}
@@ -911,7 +911,7 @@  discard block
 block discarded – undo
911 911
 
912 912
 				if ($attributeLang === $similarLang) {
913 913
 					$similarLangFallback = $option['@value'];
914
-				} elseif (strpos($attributeLang, $similarLang . '_') === 0) {
914
+				} elseif (strpos($attributeLang, $similarLang.'_') === 0) {
915 915
 					if ($similarLangFallback === false) {
916 916
 						$similarLangFallback = $option['@value'];
917 917
 					}
Please login to merge, or discard this patch.