diff options
Diffstat (limited to 'apps/updatenotification/lib')
17 files changed, 917 insertions, 608 deletions
diff --git a/apps/updatenotification/lib/AppInfo/Application.php b/apps/updatenotification/lib/AppInfo/Application.php index 3290b88dcf9..2a1678da5db 100644 --- a/apps/updatenotification/lib/AppInfo/Application.php +++ b/apps/updatenotification/lib/AppInfo/Application.php @@ -3,64 +3,55 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UpdateNotification\AppInfo; +use OCA\UpdateNotification\Listener\AppUpdateEventListener; +use OCA\UpdateNotification\Listener\BeforeTemplateRenderedEventListener; +use OCA\UpdateNotification\Notification\AppUpdateNotifier; use OCA\UpdateNotification\Notification\Notifier; use OCA\UpdateNotification\UpdateChecker; +use OCP\App\Events\AppUpdateEvent; use OCP\App\IAppManager; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; -use OCP\AppFramework\IAppContainer; -use OCP\AppFramework\QueryException; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\IConfig; use OCP\IGroupManager; -use OCP\ILogger; use OCP\IUser; use OCP\IUserSession; use OCP\Util; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; class Application extends App implements IBootstrap { + public const APP_NAME = 'updatenotification'; + public function __construct() { - parent::__construct('updatenotification', []); + parent::__construct(self::APP_NAME, []); } public function register(IRegistrationContext $context): void { $context->registerNotifierService(Notifier::class); + $context->registerNotifierService(AppUpdateNotifier::class); + + $context->registerEventListener(AppUpdateEvent::class, AppUpdateEventListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedEventListener::class); } public function boot(IBootContext $context): void { $context->injectFn(function (IConfig $config, - IUserSession $userSession, - IAppManager $appManager, - IGroupManager $groupManager, - IAppContainer $appContainer, - ILogger $logger) { + IUserSession $userSession, + IAppManager $appManager, + IGroupManager $groupManager, + ContainerInterface $container, + LoggerInterface $logger, + ): void { if ($config->getSystemValue('updatechecker', true) !== true) { // Updater check is disabled return; @@ -72,18 +63,18 @@ class Application extends App implements IBootstrap { return; } - if (!$appManager->isEnabledForUser('notifications') && - $groupManager->isAdmin($user->getUID())) { + if (!$appManager->isEnabledForUser('notifications') + && $groupManager->isAdmin($user->getUID())) { try { - $updateChecker = $appContainer->get(UpdateChecker::class); - } catch (QueryException $e) { - $logger->logException($e); + $updateChecker = $container->get(UpdateChecker::class); + } catch (ContainerExceptionInterface $e) { + $logger->error($e->getMessage(), ['exception' => $e]); return; } if ($updateChecker->getUpdateState() !== []) { - Util::addScript('updatenotification', 'legacy-notification'); - \OC_Hook::connect('\OCP\Config', 'js', $updateChecker, 'populateJavaScriptVariables'); + Util::addScript(self::APP_NAME, 'update-notification-legacy'); + $updateChecker->setInitialState(); } } }); diff --git a/apps/updatenotification/lib/BackgroundJob/AppUpdatedNotifications.php b/apps/updatenotification/lib/BackgroundJob/AppUpdatedNotifications.php new file mode 100644 index 00000000000..049390546ed --- /dev/null +++ b/apps/updatenotification/lib/BackgroundJob/AppUpdatedNotifications.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification\BackgroundJob; + +use OCA\UpdateNotification\AppInfo\Application; +use OCA\UpdateNotification\Manager; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IAppConfig; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use Psr\Log\LoggerInterface; + +class AppUpdatedNotifications extends QueuedJob { + public function __construct( + ITimeFactory $time, + private IConfig $config, + private IAppConfig $appConfig, + private IManager $notificationManager, + private IUserManager $userManager, + private IAppManager $appManager, + private LoggerInterface $logger, + private Manager $manager, + ) { + parent::__construct($time); + } + + /** + * @param array{appId: string, timestamp: int} $argument + */ + protected function run(mixed $argument): void { + $appId = $argument['appId']; + $timestamp = $argument['timestamp']; + $dateTime = $this->time->getDateTime(); + $dateTime->setTimestamp($timestamp); + + $this->logger->debug( + 'Running background job to create app update notifications for "' . $appId . '"', + [ + 'app' => Application::APP_NAME, + ], + ); + + if ($this->manager->getChangelogFile($appId, 'en') === null) { + $this->logger->debug('Skipping app updated notification - no changelog provided'); + return; + } + + $this->stopPreviousNotifications($appId); + + // Create new notifications + $notification = $this->notificationManager->createNotification(); + $notification->setApp(Application::APP_NAME) + ->setDateTime($dateTime) + ->setSubject('app_updated', [$appId]) + ->setObject('app_updated', $appId); + + $this->notifyUsers($appId, $notification); + } + + /** + * Stop all previous notifications users might not have dismissed until now + * @param string $appId The app to stop update notifications for + */ + private function stopPreviousNotifications(string $appId): void { + $notification = $this->notificationManager->createNotification(); + $notification->setApp(Application::APP_NAME) + ->setObject('app_updated', $appId); + $this->notificationManager->markProcessed($notification); + } + + /** + * Notify all users for which the updated app is enabled + */ + private function notifyUsers(string $appId, INotification $notification): void { + $guestsEnabled = $this->appConfig->getAppValueBool('app_updated.notify_guests', false) && class_exists('\OCA\Guests\UserBackend'); + + $isDefer = $this->notificationManager->defer(); + + // Notify all seen users about the app update + $this->userManager->callForSeenUsers(function (IUser $user) use ($guestsEnabled, $appId, $notification): void { + if (!$guestsEnabled && ($user->getBackendClassName() === '\OCA\Guests\UserBackend')) { + return; + } + + if (!$this->appManager->isEnabledForUser($appId, $user)) { + return; + } + + $notification->setUser($user->getUID()); + $this->notificationManager->notify($notification); + }); + + // If we enabled the defer we call the flush + if ($isDefer) { + $this->notificationManager->flush(); + } + } +} diff --git a/apps/updatenotification/lib/BackgroundJob/ResetToken.php b/apps/updatenotification/lib/BackgroundJob/ResetToken.php new file mode 100644 index 00000000000..35543ce5247 --- /dev/null +++ b/apps/updatenotification/lib/BackgroundJob/ResetToken.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\UpdateNotification\BackgroundJob; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; +use OCP\IConfig; + +/** + * Deletes the updater secret after if it is older than 48h + */ +class ResetToken extends TimedJob { + + /** + * @param IConfig $config + * @param ITimeFactory $timeFactory + */ + public function __construct( + ITimeFactory $time, + private IConfig $config, + private IAppConfig $appConfig, + ) { + parent::__construct($time); + // Run all 10 minutes + parent::setInterval(60 * 10); + } + + /** + * @param $argument + */ + protected function run($argument) { + if ($this->config->getSystemValueBool('config_is_read_only')) { + return; + } + + $secretCreated = $this->appConfig->getValueInt('core', 'updater.secret.created', $this->time->getTime()); + // Delete old tokens after 2 days + if ($secretCreated >= 172800) { + $this->config->deleteSystemValue('updater.secret'); + } + } +} diff --git a/apps/updatenotification/lib/Notification/BackgroundJob.php b/apps/updatenotification/lib/BackgroundJob/UpdateAvailableNotifications.php index e7dc193df6c..8879bb0c223 100644 --- a/apps/updatenotification/lib/Notification/BackgroundJob.php +++ b/apps/updatenotification/lib/BackgroundJob/UpdateAvailableNotifications.php @@ -3,81 +3,58 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ -namespace OCA\UpdateNotification\Notification; +namespace OCA\UpdateNotification\BackgroundJob; -use OC\BackgroundJob\TimedJob; use OC\Installer; use OC\Updater\VersionCheck; +use OCA\UpdateNotification\AppInfo\Application; use OCP\App\IAppManager; -use OCP\Http\Client\IClientService; +use OCP\AppFramework\Services\IAppConfig; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\Notification\IManager; +use OCP\ServerVersion; -class BackgroundJob extends TimedJob { - protected $connectionNotifications = [3, 7, 14, 30]; +class UpdateAvailableNotifications extends TimedJob { - /** @var IConfig */ - protected $config; - - /** @var IManager */ - protected $notificationManager; - - /** @var IGroupManager */ - protected $groupManager; - - /** @var IAppManager */ - protected $appManager; - - /** @var IClientService */ - protected $client; - - /** @var Installer */ - protected $installer; - - /** @var string[] */ - protected $users; - - public function __construct(IConfig $config, - IManager $notificationManager, - IGroupManager $groupManager, - IAppManager $appManager, - IClientService $client, - Installer $installer) { + /** + * Numbers of failed updater connection to report error as notification. + * @var list<int> + */ + protected const CONNECTION_NOTIFICATIONS = [3, 7, 14, 30]; + + /** @var ?string[] */ + protected $users = null; + + public function __construct( + ITimeFactory $timeFactory, + protected ServerVersion $serverVersion, + protected IConfig $config, + protected IAppConfig $appConfig, + protected IManager $notificationManager, + protected IGroupManager $groupManager, + protected IAppManager $appManager, + protected Installer $installer, + protected VersionCheck $versionCheck, + ) { + parent::__construct($timeFactory); // Run once a day $this->setInterval(60 * 60 * 24); - - $this->config = $config; - $this->notificationManager = $notificationManager; - $this->groupManager = $groupManager; - $this->appManager = $appManager; - $this->client = $client; - $this->installer = $installer; + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } protected function run($argument) { + // Do not check for updates if not connected to the internet + if (!$this->config->getSystemValueBool('has_internet_connection', true)) { + return; + } + if (\OC::$CLI && !$this->config->getSystemValueBool('debug', false)) { try { // Jitter the pinging of the updater server and the appstore a bit. @@ -93,26 +70,29 @@ class BackgroundJob extends TimedJob { } /** - * Check for ownCloud update + * Check for Nextcloud server update */ - protected function checkCoreUpdate() { - if (\in_array($this->getChannel(), ['daily', 'git'], true)) { - // "These aren't the update channels you're looking for." - Ben Obi-Wan Kenobi + protected function checkCoreUpdate(): void { + if (!$this->config->getSystemValueBool('updatechecker', true)) { + // update checker is disabled so no core update check! return; } - $updater = $this->createVersionCheck(); + if (\in_array($this->serverVersion->getChannel(), ['daily', 'git'], true)) { + // "These aren't the update channels you're looking for." - Ben Obi-Wan Kenobi + return; + } - $status = $updater->check(); + $status = $this->versionCheck->check(); if ($status === false) { - $errors = 1 + (int) $this->config->getAppValue('updatenotification', 'update_check_errors', 0); - $this->config->setAppValue('updatenotification', 'update_check_errors', $errors); + $errors = 1 + $this->appConfig->getAppValueInt('update_check_errors', 0); + $this->appConfig->setAppValueInt('update_check_errors', $errors); - if (\in_array($errors, $this->connectionNotifications, true)) { + if (\in_array($errors, self::CONNECTION_NOTIFICATIONS, true)) { $this->sendErrorNotifications($errors); } } elseif (\is_array($status)) { - $this->config->setAppValue('updatenotification', 'update_check_errors', 0); + $this->appConfig->setAppValueInt('update_check_errors', 0); $this->clearErrorNotifications(); if (isset($status['version'])) { @@ -125,14 +105,14 @@ class BackgroundJob extends TimedJob { * Send a message to the admin when the update server could not be reached * @param int $numDays */ - protected function sendErrorNotifications($numDays) { + protected function sendErrorNotifications($numDays): void { $this->clearErrorNotifications(); $notification = $this->notificationManager->createNotification(); try { - $notification->setApp('updatenotification') + $notification->setApp(Application::APP_NAME) ->setDateTime(new \DateTime()) - ->setObject('updatenotification', 'error') + ->setObject(Application::APP_NAME, 'error') ->setSubject('connection_error', ['days' => $numDays]); foreach ($this->getUsersToNotify() as $uid) { @@ -147,12 +127,12 @@ class BackgroundJob extends TimedJob { /** * Remove error notifications again */ - protected function clearErrorNotifications() { + protected function clearErrorNotifications(): void { $notification = $this->notificationManager->createNotification(); try { - $notification->setApp('updatenotification') + $notification->setApp(Application::APP_NAME) ->setSubject('connection_error') - ->setObject('updatenotification', 'error'); + ->setObject(Application::APP_NAME, 'error'); } catch (\InvalidArgumentException $e) { return; } @@ -162,8 +142,8 @@ class BackgroundJob extends TimedJob { /** * Check all installed apps for updates */ - protected function checkAppUpdates() { - $apps = $this->appManager->getInstalledApps(); + protected function checkAppUpdates(): void { + $apps = $this->appManager->getEnabledApps(); foreach ($apps as $app) { $update = $this->isUpdateAvailable($app); if ($update !== false) { @@ -179,21 +159,21 @@ class BackgroundJob extends TimedJob { * @param string $version * @param string $visibleVersion */ - protected function createNotifications($app, $version, $visibleVersion = '') { - $lastNotification = $this->config->getAppValue('updatenotification', $app, false); + protected function createNotifications($app, $version, $visibleVersion = ''): void { + $lastNotification = $this->appConfig->getAppValueString($app, ''); if ($lastNotification === $version) { // We already notified about this update return; } - if ($lastNotification !== false) { + if ($lastNotification !== '') { // Delete old updates $this->deleteOutdatedNotifications($app, $lastNotification); } $notification = $this->notificationManager->createNotification(); try { - $notification->setApp('updatenotification') + $notification->setApp(Application::APP_NAME) ->setDateTime(new \DateTime()) ->setObject($app, $version); @@ -211,7 +191,7 @@ class BackgroundJob extends TimedJob { return; } - $this->config->setAppValue('updatenotification', $app, $version); + $this->appConfig->setAppValueString($app, $version); } /** @@ -222,19 +202,18 @@ class BackgroundJob extends TimedJob { return $this->users; } - $notifyGroups = (array) json_decode($this->config->getAppValue('updatenotification', 'notify_groups', '["admin"]'), true); + $notifyGroups = $this->appConfig->getAppValueArray('notify_groups', ['admin']); $this->users = []; foreach ($notifyGroups as $group) { $groupToNotify = $this->groupManager->get($group); if ($groupToNotify instanceof IGroup) { foreach ($groupToNotify->getUsers() as $user) { - $this->users[$user->getUID()] = true; + $this->users[] = $user->getUID(); } } } - $this->users = array_keys($this->users); - + $this->users = array_values(array_unique($this->users)); return $this->users; } @@ -244,35 +223,18 @@ class BackgroundJob extends TimedJob { * @param string $app * @param string $version */ - protected function deleteOutdatedNotifications($app, $version) { + protected function deleteOutdatedNotifications($app, $version): void { $notification = $this->notificationManager->createNotification(); try { - $notification->setApp('updatenotification') + $notification->setApp(Application::APP_NAME) ->setObject($app, $version); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { return; } $this->notificationManager->markProcessed($notification); } /** - * @return VersionCheck - */ - protected function createVersionCheck(): VersionCheck { - return new VersionCheck( - $this->client, - $this->config - ); - } - - /** - * @return string - */ - protected function getChannel(): string { - return \OC_Util::getChannel(); - } - - /** * @param string $app * @return string|false */ diff --git a/apps/updatenotification/lib/Command/Check.php b/apps/updatenotification/lib/Command/Check.php index 7126a6b50b0..d93e4935012 100644 --- a/apps/updatenotification/lib/Command/Check.php +++ b/apps/updatenotification/lib/Command/Check.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Tobia De Koninck (tobia@ledfan.be) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Tobia De Koninck <LEDfan@users.noreply.github.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UpdateNotification\Command; @@ -37,26 +17,12 @@ use Symfony\Component\Console\Output\OutputInterface; class Check extends Command { - /** - * @var Installer $installer - */ - private $installer; - - /** - * @var AppManager $appManager - */ - private $appManager; - - /** - * @var UpdateChecker $updateChecker - */ - private $updateChecker; - - public function __construct(AppManager $appManager, UpdateChecker $updateChecker, Installer $installer) { + public function __construct( + private AppManager $appManager, + private UpdateChecker $updateChecker, + private Installer $installer, + ) { parent::__construct(); - $this->installer = $installer; - $this->appManager = $appManager; - $this->updateChecker = $updateChecker; } protected function configure(): void { @@ -72,13 +38,13 @@ class Check extends Command { // Server $r = $this->updateChecker->getUpdateState(); if (isset($r['updateAvailable']) && $r['updateAvailable']) { - $output->writeln($r['updateVersionString'] . ' is available. Get more information on how to update at '. $r['updateLink'] . '.'); + $output->writeln($r['updateVersionString'] . ' is available. Get more information on how to update at ' . $r['updateLink'] . '.'); $updatesAvailableCount += 1; } // Apps - $apps = $this->appManager->getInstalledApps(); + $apps = $this->appManager->getEnabledApps(); foreach ($apps as $app) { $update = $this->installer->isUpdateAvailable($app); if ($update !== false) { diff --git a/apps/updatenotification/lib/Controller/APIController.php b/apps/updatenotification/lib/Controller/APIController.php index 9d5d1c2d764..4360d814dd2 100644 --- a/apps/updatenotification/lib/Controller/APIController.php +++ b/apps/updatenotification/lib/Controller/APIController.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UpdateNotification\Controller; use OC\App\AppStore\Fetcher\AppFetcher; +use OCA\UpdateNotification\Manager; +use OCA\UpdateNotification\ResponseDefinitions; use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Http; @@ -37,45 +21,50 @@ use OCP\IRequest; use OCP\IUserSession; use OCP\L10N\IFactory; +/** + * @psalm-import-type UpdateNotificationApp from ResponseDefinitions + */ class APIController extends OCSController { - /** @var IConfig */ - protected $config; - - /** @var IAppManager */ - protected $appManager; - - /** @var AppFetcher */ - protected $appFetcher; - - /** @var IFactory */ - protected $l10nFactory; + protected ?string $language = null; - /** @var IUserSession */ - protected $userSession; - - /** @var string */ - protected $language; - - public function __construct(string $appName, - IRequest $request, - IConfig $config, - IAppManager $appManager, - AppFetcher $appFetcher, - IFactory $l10nFactory, - IUserSession $userSession) { + /** + * List of apps that were in the appstore but are now shipped and don't have + * a compatible update available. + * + * @var array<string, int> + */ + protected array $appsShippedInFutureVersion = [ + 'bruteforcesettings' => 25, + 'suspicious_login' => 25, + 'twofactor_totp' => 25, + 'files_downloadlimit' => 29, + 'twofactor_nextcloud_notification' => 30, + 'app_api' => 30, + ]; + + public function __construct( + string $appName, + IRequest $request, + protected IConfig $config, + protected IAppManager $appManager, + protected AppFetcher $appFetcher, + protected IFactory $l10nFactory, + protected IUserSession $userSession, + protected Manager $manager, + ) { parent::__construct($appName, $request); - - $this->config = $config; - $this->appManager = $appManager; - $this->appFetcher = $appFetcher; - $this->l10nFactory = $l10nFactory; - $this->userSession = $userSession; } /** - * @param string $newVersion - * @return DataResponse + * List available updates for apps + * + * @param string $newVersion Server version to check updates for + * + * @return DataResponse<Http::STATUS_OK, array{missing: list<UpdateNotificationApp>, available: list<UpdateNotificationApp>}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{appstore_disabled: bool, already_on_latest?: bool}, array{}> + * + * 200: Apps returned + * 404: New versions not found */ public function getAppList(string $newVersion): DataResponse { if (!$this->config->getSystemValue('appstoreenabled', true)) { @@ -85,14 +74,14 @@ class APIController extends OCSController { } // Get list of installed custom apps - $installedApps = $this->appManager->getInstalledApps(); + $installedApps = $this->appManager->getEnabledApps(); $installedApps = array_filter($installedApps, function ($app) { try { $this->appManager->getAppPath($app); } catch (AppPathNotFoundException $e) { return false; } - return !$this->appManager->isShipped($app); + return !$this->appManager->isShipped($app) && !isset($this->appsShippedInFutureVersion[$app]); }); if (empty($installedApps)) { @@ -105,7 +94,7 @@ class APIController extends OCSController { $this->appFetcher->setVersion($newVersion, 'future-apps.json', false); // Apps available on the app store for that version - $availableApps = array_map(static function (array $app) { + $availableApps = array_map(static function (array $app): string { return $app['id']; }, $this->appFetcher->get()); @@ -116,7 +105,14 @@ class APIController extends OCSController { ], Http::STATUS_NOT_FOUND); } - $this->language = $this->l10nFactory->getUserLanguage($this->userSession->getUser()); + // Ignore apps that are deployed from git + $installedApps = array_filter($installedApps, function (string $appId) { + try { + return !file_exists($this->appManager->getAppPath($appId) . '/.git'); + } catch (AppPathNotFoundException $e) { + return true; + } + }); $missing = array_diff($installedApps, $availableApps); $missing = array_map([$this, 'getAppDetails'], $missing); @@ -136,13 +132,66 @@ class APIController extends OCSController { * Get translated app name * * @param string $appId - * @return string[] + * @return UpdateNotificationApp */ protected function getAppDetails(string $appId): array { $app = $this->appManager->getAppInfo($appId, false, $this->language); + $name = $app['name'] ?? $appId; return [ 'appId' => $appId, - 'appName' => $app['name'] ?? $appId, + 'appName' => $name, ]; } + + protected function getLanguage(): string { + if ($this->language === null) { + $this->language = $this->l10nFactory->getUserLanguage($this->userSession->getUser()); + } + return $this->language; + } + + /** + * Get changelog entry for an app + * + * @param string $appId App to search changelog entry for + * @param string|null $version The version to search the changelog entry for (defaults to the latest installed) + * + * @return DataResponse<Http::STATUS_OK, array{appName: string, content: string, version: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{}, array{}> + * + * 200: Changelog entry returned + * 400: The `version` parameter is not a valid version format + * 404: No changelog found + */ + public function getAppChangelogEntry(string $appId, ?string $version = null): DataResponse { + $version = $version ?? $this->appManager->getAppVersion($appId); + // handle pre-release versions + $matches = []; + $result = preg_match('/^(\d+\.\d+(\.\d+)?)/', $version, $matches); + if ($result === false || $result === 0) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + $shortVersion = $matches[0]; + + $changes = $this->manager->getChangelog($appId, $shortVersion); + + if ($changes === null) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + // Remove version headline + /** @var string[] */ + $changes = explode("\n", $changes, 2); + $changes = trim(end($changes)); + + // Get app info for localized app name + $info = $this->appManager->getAppInfo($appId) ?? []; + /** @var string */ + $appName = $info['name'] ?? $appId; + + return new DataResponse([ + 'appName' => $appName, + 'content' => $changes, + 'version' => $version, + ]); + } } diff --git a/apps/updatenotification/lib/Controller/AdminController.php b/apps/updatenotification/lib/Controller/AdminController.php index b13ba66efd5..26745948890 100644 --- a/apps/updatenotification/lib/Controller/AdminController.php +++ b/apps/updatenotification/lib/Controller/AdminController.php @@ -3,36 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\UpdateNotification\Controller; -use OCA\UpdateNotification\ResetTokenBackgroundJob; +use OCA\UpdateNotification\BackgroundJob\ResetToken; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; @@ -40,43 +23,22 @@ use OCP\Security\ISecureRandom; use OCP\Util; class AdminController extends Controller { - /** @var IJobList */ - private $jobList; - /** @var ISecureRandom */ - private $secureRandom; - /** @var IConfig */ - private $config; - /** @var ITimeFactory */ - private $timeFactory; - /** @var IL10N */ - private $l10n; - /** - * @param string $appName - * @param IRequest $request - * @param IJobList $jobList - * @param ISecureRandom $secureRandom - * @param IConfig $config - * @param ITimeFactory $timeFactory - * @param IL10N $l10n - */ - public function __construct($appName, - IRequest $request, - IJobList $jobList, - ISecureRandom $secureRandom, - IConfig $config, - ITimeFactory $timeFactory, - IL10N $l10n) { + public function __construct( + string $appName, + IRequest $request, + private IJobList $jobList, + private ISecureRandom $secureRandom, + private IConfig $config, + private IAppConfig $appConfig, + private ITimeFactory $timeFactory, + private IL10N $l10n, + ) { parent::__construct($appName, $request); - $this->jobList = $jobList; - $this->secureRandom = $secureRandom; - $this->config = $config; - $this->timeFactory = $timeFactory; - $this->l10n = $l10n; } - private function isUpdaterEnabled() { - return !$this->config->getSystemValue('upgrade.disable-web', false); + private function isUpdaterEnabled(): bool { + return !$this->config->getSystemValueBool('upgrade.disable-web'); } /** @@ -85,7 +47,7 @@ class AdminController extends Controller { */ public function setChannel(string $channel): DataResponse { Util::setChannel($channel); - $this->config->setAppValue('core', 'lastupdatedat', 0); + $this->appConfig->setValueInt('core', 'lastupdatedat', 0); return new DataResponse(['status' => 'success', 'data' => ['message' => $this->l10n->t('Channel updated')]]); } @@ -98,8 +60,8 @@ class AdminController extends Controller { } // Create a new job and store the creation date - $this->jobList->add(ResetTokenBackgroundJob::class); - $this->config->setAppValue('core', 'updater.secret.created', $this->timeFactory->getTime()); + $this->jobList->add(ResetToken::class); + $this->appConfig->setValueInt('core', 'updater.secret.created', $this->timeFactory->getTime()); // Create a new token $newToken = $this->secureRandom->generate(64); diff --git a/apps/updatenotification/lib/Controller/ChangelogController.php b/apps/updatenotification/lib/Controller/ChangelogController.php new file mode 100644 index 00000000000..a274ed3d2b2 --- /dev/null +++ b/apps/updatenotification/lib/Controller/ChangelogController.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UpdateNotification\Controller; + +use OCA\UpdateNotification\Manager; +use OCP\App\IAppManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IRequest; +use OCP\Util; + +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] +class ChangelogController extends Controller { + + public function __construct( + string $appName, + IRequest $request, + private Manager $manager, + private IAppManager $appManager, + private IInitialState $initialState, + ) { + parent::__construct($appName, $request); + } + + /** + * This page is only used for clients not support showing the app changelog feature in-app and thus need to show it on a dedicated page. + * @param string $app App to show the changelog for + * @param string|null $version Version entry to show (defaults to latest installed) + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function showChangelog(string $app, ?string $version = null): TemplateResponse { + $version = $version ?? $this->appManager->getAppVersion($app); + $appInfo = $this->appManager->getAppInfo($app) ?? []; + $appName = $appInfo['name'] ?? $app; + + $changes = $this->manager->getChangelog($app, $version) ?? ''; + // Remove version headline + /** @var string[] */ + $changes = explode("\n", $changes, 2); + $changes = trim(end($changes)); + + $this->initialState->provideInitialState('changelog', [ + 'appName' => $appName, + 'appVersion' => $version, + 'text' => $changes, + ]); + + Util::addScript($this->appName, 'view-changelog-page'); + return new TemplateResponse($this->appName, 'empty'); + } +} diff --git a/apps/updatenotification/lib/Listener/AppUpdateEventListener.php b/apps/updatenotification/lib/Listener/AppUpdateEventListener.php new file mode 100644 index 00000000000..49a2506d913 --- /dev/null +++ b/apps/updatenotification/lib/Listener/AppUpdateEventListener.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification\Listener; + +use OCA\UpdateNotification\AppInfo\Application; +use OCA\UpdateNotification\BackgroundJob\AppUpdatedNotifications; +use OCP\App\Events\AppUpdateEvent; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** @template-implements IEventListener<AppUpdateEvent> */ +class AppUpdateEventListener implements IEventListener { + + public function __construct( + private IJobList $jobList, + private LoggerInterface $logger, + private IAppConfig $appConfig, + ) { + } + + /** + * @param AppUpdateEvent $event + */ + public function handle(Event $event): void { + if (!($event instanceof AppUpdateEvent)) { + return; + } + + if (!$this->appConfig->getValueBool(Application::APP_NAME, 'app_updated.enabled', true)) { + return; + } + + foreach ($this->jobList->getJobsIterator(AppUpdatedNotifications::class, null, 0) as $job) { + // Remove waiting notification jobs for this app + if ($job->getArgument()['appId'] === $event->getAppId()) { + $this->jobList->remove($job); + } + } + + $this->jobList->add(AppUpdatedNotifications::class, [ + 'appId' => $event->getAppId(), + 'timestamp' => time(), + ]); + + $this->logger->debug( + 'Scheduled app update notification for "' . $event->getAppId() . '"', + [ + 'app' => Application::APP_NAME, + ], + ); + } +} diff --git a/apps/updatenotification/lib/Listener/BeforeTemplateRenderedEventListener.php b/apps/updatenotification/lib/Listener/BeforeTemplateRenderedEventListener.php new file mode 100644 index 00000000000..974734a76f4 --- /dev/null +++ b/apps/updatenotification/lib/Listener/BeforeTemplateRenderedEventListener.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification\Listener; + +use OCA\UpdateNotification\AppInfo\Application; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IAppConfig; +use OCP\Util; +use Psr\Log\LoggerInterface; + +/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */ +class BeforeTemplateRenderedEventListener implements IEventListener { + + public function __construct( + private IAppManager $appManager, + private LoggerInterface $logger, + private IAppConfig $appConfig, + ) { + } + + /** + * @param BeforeTemplateRenderedEvent $event + */ + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + return; + } + + if (!$this->appConfig->getValueBool(Application::APP_NAME, 'app_updated.enabled', true)) { + return; + } + + // Only handle logged in users + if (!$event->isLoggedIn()) { + return; + } + + // Ignore when notifications are disabled + if (!$this->appManager->isEnabledForUser('notifications')) { + return; + } + + Util::addInitScript(Application::APP_NAME, 'init'); + } +} diff --git a/apps/updatenotification/lib/Manager.php b/apps/updatenotification/lib/Manager.php new file mode 100644 index 00000000000..ebc1c83a9b4 --- /dev/null +++ b/apps/updatenotification/lib/Manager.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification; + +use OCP\App\IAppManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; + +class Manager { + + private ?IUser $currentUser; + + public function __construct( + IUserSession $currentSession, + private IAppManager $appManager, + private IFactory $l10NFactory, + private LoggerInterface $logger, + ) { + $this->currentUser = $currentSession->getUser(); + } + + /** + * Get the changelog entry for the given appId + * @param string $appId The app for which to query the entry + * @param string $version The version for which to query the changelog entry + * @param ?string $languageCode The language in which to query the changelog (defaults to current user language and fallsback to English) + * @return string|null Either the changelog entry or null if no changelog is found + */ + public function getChangelog(string $appId, string $version, ?string $languageCode = null): ?string { + if ($languageCode === null) { + $languageCode = $this->l10NFactory->getUserLanguage($this->currentUser); + } + + $path = $this->getChangelogFile($appId, $languageCode); + if ($path === null) { + $this->logger->debug('No changelog file found for app ' . $appId . ' and language code ' . $languageCode); + return null; + } + + $changes = $this->retrieveChangelogEntry($path, $version); + return $changes; + } + + /** + * Get the changelog file in the requested language or fallback to English + * @param string $appId The app to load the changelog for + * @param string $languageCode The language code to search + * @return string|null Either the file path or null if not found + */ + public function getChangelogFile(string $appId, string $languageCode): ?string { + try { + $appPath = $this->appManager->getAppPath($appId); + $files = ["CHANGELOG.$languageCode.md", 'CHANGELOG.en.md']; + foreach ($files as $file) { + $path = $appPath . '/' . $file; + if (is_file($path)) { + return $path; + } + } + } catch (\Throwable $e) { + // ignore and return null below + } + return null; + } + + /** + * Retrieve a log entry from the changelog + * @param string $path The path to the changelog file + * @param string $version The version to query (make sure to only pass in "{major}.{minor}(.{patch}" format) + */ + protected function retrieveChangelogEntry(string $path, string $version): ?string { + $matches = []; + $content = file_get_contents($path); + if ($content === false) { + $this->logger->debug('Could not open changelog file', ['file-path' => $path]); + return null; + } + + $result = preg_match_all('/^## (?:\[)?(?:v)?(\d+\.\d+(\.\d+)?)/m', $content, $matches, PREG_OFFSET_CAPTURE); + if ($result === false || $result === 0) { + $this->logger->debug('No entries in changelog found', ['file_path' => $path]); + return null; + } + + // Get the key of the match that equals the requested version + $index = array_key_first( + // Get the array containing the match that equals the requested version, keys are preserved so: [1 => '1.2.4'] + array_filter( + // This is the array of the versions found, like ['1.2.3', '1.2.4'] + $matches[1], + // Callback to filter only version that matches the requested version + fn (array $match) => version_compare($match[0], $version, '=='), + ) + ); + + if ($index === null) { + $this->logger->debug('No changelog entry for version ' . $version . ' found', ['file_path' => $path]); + return null; + } + + $offsetChangelogEntry = $matches[0][$index][1]; + // Length of the changelog entry (offset of next match - own offset) or null if the whole rest should be considered + $lengthChangelogEntry = $index < ($result - 1) ? ($matches[0][$index + 1][1] - $offsetChangelogEntry) : null; + return substr($content, $offsetChangelogEntry, $lengthChangelogEntry); + } +} diff --git a/apps/updatenotification/lib/Notification/AppUpdateNotifier.php b/apps/updatenotification/lib/Notification/AppUpdateNotifier.php new file mode 100644 index 00000000000..353ca883aba --- /dev/null +++ b/apps/updatenotification/lib/Notification/AppUpdateNotifier.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification\Notification; + +use OCA\UpdateNotification\AppInfo\Application; +use OCP\App\IAppManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Notification\AlreadyProcessedException; +use OCP\Notification\IAction; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; +use Psr\Log\LoggerInterface; + +class AppUpdateNotifier implements INotifier { + + public function __construct( + private IFactory $l10nFactory, + private INotificationManager $notificationManager, + private IUserManager $userManager, + private IURLGenerator $urlGenerator, + private IAppManager $appManager, + private LoggerInterface $logger, + ) { + } + + public function getID(): string { + return 'updatenotification_app_updated'; + } + + /** + * Human readable name describing the notifier + */ + public function getName(): string { + return $this->l10nFactory->get(Application::APP_NAME)->t('App updated'); + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + * @throws UnknownNotificationException When the notification was not prepared by a notifier + * @throws AlreadyProcessedException When the app is no longer known + */ + public function prepare(INotification $notification, string $languageCode): INotification { + if ($notification->getApp() !== Application::APP_NAME) { + throw new UnknownNotificationException('Unknown app'); + } + + if ($notification->getSubject() !== 'app_updated') { + throw new UnknownNotificationException('Unknown subject'); + } + + $appId = $notification->getSubjectParameters()[0]; + $appInfo = $this->appManager->getAppInfo($appId, lang:$languageCode); + if ($appInfo === null) { + throw new AlreadyProcessedException(); + } + + // Prepare translation factory for requested language + $l = $this->l10nFactory->get(Application::APP_NAME, $languageCode); + + $icon = $this->appManager->getAppIcon($appId, true); + if ($icon === null) { + $icon = $this->urlGenerator->imagePath('core', 'actions/change.svg'); + } + + $action = $notification->createAction(); + $action + ->setLabel($l->t('See what\'s new')) + ->setParsedLabel($l->t('See what\'s new')) + ->setLink($this->urlGenerator->linkToRouteAbsolute('updatenotification.Changelog.showChangelog', ['app' => $appId, 'version' => $this->appManager->getAppVersion($appId)]), IAction::TYPE_WEB); + + $notification + ->setIcon($this->urlGenerator->getAbsoluteURL($icon)) + ->addParsedAction($action) + ->setRichSubject( + $l->t('{app} updated to version {version}'), + [ + 'app' => [ + 'type' => 'app', + 'id' => $appId, + 'name' => $appInfo['name'], + ], + 'version' => [ + 'type' => 'highlight', + 'id' => $appId, + 'name' => $appInfo['version'], + ], + ], + ); + + return $notification; + } +} diff --git a/apps/updatenotification/lib/Notification/Notifier.php b/apps/updatenotification/lib/Notification/Notifier.php index bfbcc203480..787675bd98d 100644 --- a/apps/updatenotification/lib/Notification/Notifier.php +++ b/apps/updatenotification/lib/Notification/Notifier.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\UpdateNotification\Notification; -use OCP\IConfig; +use OCA\UpdateNotification\AppInfo\Application; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IAppConfig; use OCP\IGroupManager; use OCP\IURLGenerator; use OCP\IUser; @@ -36,49 +20,27 @@ use OCP\Notification\AlreadyProcessedException; use OCP\Notification\IManager; use OCP\Notification\INotification; use OCP\Notification\INotifier; -use OCP\Util; +use OCP\Notification\UnknownNotificationException; +use OCP\ServerVersion; class Notifier implements INotifier { - - /** @var IURLGenerator */ - protected $url; - - /** @var IConfig */ - protected $config; - - /** @var IManager */ - protected $notificationManager; - - /** @var IFactory */ - protected $l10NFactory; - - /** @var IUserSession */ - protected $userSession; - - /** @var IGroupManager */ - protected $groupManager; - /** @var string[] */ protected $appVersions; /** * Notifier constructor. - * - * @param IURLGenerator $url - * @param IConfig $config - * @param IManager $notificationManager - * @param IFactory $l10NFactory - * @param IUserSession $userSession - * @param IGroupManager $groupManager */ - public function __construct(IURLGenerator $url, IConfig $config, IManager $notificationManager, IFactory $l10NFactory, IUserSession $userSession, IGroupManager $groupManager) { - $this->url = $url; - $this->notificationManager = $notificationManager; - $this->config = $config; - $this->l10NFactory = $l10NFactory; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->appVersions = $this->getAppVersions(); + public function __construct( + protected IURLGenerator $url, + protected IAppConfig $appConfig, + protected IManager $notificationManager, + protected IFactory $l10NFactory, + protected IUserSession $userSession, + protected IGroupManager $groupManager, + protected IAppManager $appManager, + protected ServerVersion $serverVersion, + ) { + $this->appVersions = $this->appManager->getAppInstalledVersions(); } /** @@ -88,7 +50,7 @@ class Notifier implements INotifier { * @since 17.0.0 */ public function getID(): string { - return 'updatenotification'; + return Application::APP_NAME; } /** @@ -98,51 +60,60 @@ class Notifier implements INotifier { * @since 17.0.0 */ public function getName(): string { - return $this->l10NFactory->get('updatenotification')->t('Update notifications'); + return $this->l10NFactory->get(Application::APP_NAME)->t('Update notifications'); } /** * @param INotification $notification * @param string $languageCode The code of the language that should be used to prepare the notification * @return INotification - * @throws \InvalidArgumentException When the notification was not prepared by a notifier + * @throws UnknownNotificationException When the notification was not prepared by a notifier * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted * @since 9.0.0 */ public function prepare(INotification $notification, string $languageCode): INotification { - if ($notification->getApp() !== 'updatenotification') { - throw new \InvalidArgumentException('Unknown app id'); + if ($notification->getApp() !== Application::APP_NAME) { + throw new UnknownNotificationException('Unknown app id'); } - $l = $this->l10NFactory->get('updatenotification', $languageCode); + if ($notification->getSubject() !== 'update_available' && $notification->getSubject() !== 'connection_error') { + throw new UnknownNotificationException('Unknown subject'); + } + + $l = $this->l10NFactory->get(Application::APP_NAME, $languageCode); if ($notification->getSubject() === 'connection_error') { - $errors = (int) $this->config->getAppValue('updatenotification', 'update_check_errors', 0); + $errors = $this->appConfig->getAppValueInt('update_check_errors', 0); if ($errors === 0) { - $this->notificationManager->markProcessed($notification); - throw new \InvalidArgumentException('Update checked worked again'); + throw new AlreadyProcessedException(); } $notification->setParsedSubject($l->t('The update server could not be reached since %d days to check for new updates.', [$errors])) ->setParsedMessage($l->t('Please check the Nextcloud and server log files for errors.')); - } elseif ($notification->getObjectType() === 'core') { - $this->updateAlreadyInstalledCheck($notification, $this->getCoreVersions()); + } else { + if ($notification->getObjectType() === 'core') { + $this->updateAlreadyInstalledCheck($notification, $this->getCoreVersions()); - $parameters = $notification->getSubjectParameters(); - $notification->setParsedSubject($l->t('Update to %1$s is available.', [$parameters['version']])); + $parameters = $notification->getSubjectParameters(); + $notification->setRichSubject($l->t('Update to {serverAndVersion} is available.'), [ + 'serverAndVersion' => [ + 'type' => 'highlight', + 'id' => $notification->getObjectType(), + 'name' => $parameters['version'], + ] + ]); - if ($this->isAdmin()) { - $notification->setLink($this->url->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'overview']) . '#version'); - } - } else { - $appInfo = $this->getAppInfo($notification->getObjectType(), $languageCode); - $appName = ($appInfo === null) ? $notification->getObjectType() : $appInfo['name']; + if ($this->isAdmin()) { + $notification->setLink($this->url->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'overview']) . '#version'); + } + } else { + $appInfo = $this->getAppInfo($notification->getObjectType(), $languageCode); + $appName = ($appInfo === null) ? $notification->getObjectType() : $appInfo['name']; - if (isset($this->appVersions[$notification->getObjectType()])) { - $this->updateAlreadyInstalledCheck($notification, $this->appVersions[$notification->getObjectType()]); - } + if (isset($this->appVersions[$notification->getObjectType()])) { + $this->updateAlreadyInstalledCheck($notification, $this->appVersions[$notification->getObjectType()]); + } - $notification->setParsedSubject($l->t('Update for %1$s to version %2$s is available.', [$appName, $notification->getObjectId()])) - ->setRichSubject($l->t('Update for {app} to version %s is available.', [$notification->getObjectId()]), [ + $notification->setRichSubject($l->t('Update for {app} to version %s is available.', [$notification->getObjectId()]), [ 'app' => [ 'type' => 'app', 'id' => $notification->getObjectType(), @@ -150,12 +121,13 @@ class Notifier implements INotifier { ] ]); - if ($this->isAdmin()) { - $notification->setLink($this->url->linkToRouteAbsolute('settings.AppSettings.viewApps', ['category' => 'updates']) . '#app-' . $notification->getObjectType()); + if ($this->isAdmin()) { + $notification->setLink($this->url->linkToRouteAbsolute('settings.AppSettings.viewApps', ['category' => 'updates']) . '#app-' . $notification->getObjectType()); + } } } - $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('updatenotification', 'notification.svg'))); + $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath(Application::APP_NAME, 'notification.svg'))); return $notification; } @@ -167,15 +139,12 @@ class Notifier implements INotifier { * @param string $installedVersion * @throws AlreadyProcessedException When the update is already installed */ - protected function updateAlreadyInstalledCheck(INotification $notification, $installedVersion) { + protected function updateAlreadyInstalledCheck(INotification $notification, $installedVersion): void { if (version_compare($notification->getObjectId(), $installedVersion, '<=')) { throw new AlreadyProcessedException(); } } - /** - * @return bool - */ protected function isAdmin(): bool { $user = $this->userSession->getUser(); @@ -187,14 +156,10 @@ class Notifier implements INotifier { } protected function getCoreVersions(): string { - return implode('.', Util::getVersion()); - } - - protected function getAppVersions(): array { - return \OC_App::getAppVersions(); + return implode('.', $this->serverVersion->getVersion()); } - protected function getAppInfo($appId, $languageCode) { - return \OC_App::getAppInfo($appId, false, $languageCode); + protected function getAppInfo(string $appId, ?string $languageCode): ?array { + return $this->appManager->getAppInfo($appId, false, $languageCode); } } diff --git a/apps/updatenotification/lib/ResetTokenBackgroundJob.php b/apps/updatenotification/lib/ResetTokenBackgroundJob.php deleted file mode 100644 index 96a50c5ff7f..00000000000 --- a/apps/updatenotification/lib/ResetTokenBackgroundJob.php +++ /dev/null @@ -1,66 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\UpdateNotification; - -use OC\BackgroundJob\TimedJob; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\IConfig; - -/** - * Class ResetTokenBackgroundJob deletes any configured token all 24 hours for - * - * - * @package OCA\UpdateNotification - */ -class ResetTokenBackgroundJob extends TimedJob { - /** @var IConfig */ - private $config; - /** @var ITimeFactory */ - private $timeFactory; - - /** - * @param IConfig $config - * @param ITimeFactory $timeFactory - */ - public function __construct(IConfig $config, - ITimeFactory $timeFactory) { - // Run all 10 minutes - $this->setInterval(60 * 10); - $this->config = $config; - $this->timeFactory = $timeFactory; - } - - /** - * @param $argument - */ - protected function run($argument) { - // Delete old tokens after 2 days - if ($this->timeFactory->getTime() - $this->config->getAppValue('core', 'updater.secret.created', $this->timeFactory->getTime()) >= 172800) { - $this->config->deleteSystemValue('updater.secret'); - } - } -} diff --git a/apps/updatenotification/lib/ResponseDefinitions.php b/apps/updatenotification/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..754787ea2a7 --- /dev/null +++ b/apps/updatenotification/lib/ResponseDefinitions.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UpdateNotification; + +/** + * @psalm-type UpdateNotificationApp = array{ + * appId: string, + * appName: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/updatenotification/lib/Settings/Admin.php b/apps/updatenotification/lib/Settings/Admin.php index b8062efd81f..22228f1bccc 100644 --- a/apps/updatenotification/lib/Settings/Admin.php +++ b/apps/updatenotification/lib/Settings/Admin.php @@ -3,89 +3,44 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\UpdateNotification\Settings; -use OC\User\Backend; -use OCP\User\Backend\ICountUsersBackend; +use OCA\UpdateNotification\AppInfo\Application; use OCA\UpdateNotification\UpdateChecker; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IGroupManager; +use OCP\IUserManager; use OCP\L10N\IFactory; +use OCP\ServerVersion; use OCP\Settings\ISettings; use OCP\Support\Subscription\IRegistry; -use OCP\Util; -use OCP\IUserManager; use Psr\Log\LoggerInterface; class Admin implements ISettings { - /** @var IConfig */ - private $config; - /** @var UpdateChecker */ - private $updateChecker; - /** @var IGroupManager */ - private $groupManager; - /** @var IDateTimeFormatter */ - private $dateTimeFormatter; - /** @var IFactory */ - private $l10nFactory; - /** @var IRegistry */ - private $subscriptionRegistry; - /** @var IUserManager */ - private $userManager; - /** @var LoggerInterface */ - private $logger; - public function __construct( - IConfig $config, - UpdateChecker $updateChecker, - IGroupManager $groupManager, - IDateTimeFormatter $dateTimeFormatter, - IFactory $l10nFactory, - IRegistry $subscriptionRegistry, - IUserManager $userManager, - LoggerInterface $logger + private IConfig $config, + private IAppConfig $appConfig, + private UpdateChecker $updateChecker, + private IGroupManager $groupManager, + private IDateTimeFormatter $dateTimeFormatter, + private IFactory $l10nFactory, + private IRegistry $subscriptionRegistry, + private IUserManager $userManager, + private LoggerInterface $logger, + private IInitialState $initialState, + private ServerVersion $serverVersion, ) { - $this->config = $config; - $this->updateChecker = $updateChecker; - $this->groupManager = $groupManager; - $this->dateTimeFormatter = $dateTimeFormatter; - $this->l10nFactory = $l10nFactory; - $this->subscriptionRegistry = $subscriptionRegistry; - $this->userManager = $userManager; - $this->logger = $logger; } - /** - * @return TemplateResponse - */ public function getForm(): TemplateResponse { - $lastUpdateCheckTimestamp = $this->config->getAppValue('core', 'lastupdatedat'); + $lastUpdateCheckTimestamp = $this->appConfig->getValueInt('core', 'lastupdatedat'); $lastUpdateCheck = $this->dateTimeFormatter->formatDateTime($lastUpdateCheckTimestamp); $channels = [ @@ -94,14 +49,14 @@ class Admin implements ISettings { 'stable', 'production', ]; - $currentChannel = Util::getChannel(); + $currentChannel = $this->serverVersion->getChannel(); if ($currentChannel === 'git') { $channels[] = 'git'; } $updateState = $this->updateChecker->getUpdateState(); - $notifyGroups = json_decode($this->config->getAppValue('updatenotification', 'notify_groups', '["admin"]'), true); + $notifyGroups = $this->appConfig->getValueArray(Application::APP_NAME, 'notify_groups', ['admin']); $defaultUpdateServerURL = 'https://updates.nextcloud.com/updater_server/'; $updateServerURL = $this->config->getSystemValue('updater.server.url', $defaultUpdateServerURL); @@ -131,12 +86,9 @@ class Admin implements ISettings { 'notifyGroups' => $this->getSelectedGroups($notifyGroups), 'hasValidSubscription' => $hasValidSubscription, ]; + $this->initialState->provideInitialState('data', $params); - $params = [ - 'json' => json_encode($params), - ]; - - return new TemplateResponse('updatenotification', 'admin', $params, ''); + return new TemplateResponse('updatenotification', 'admin', [], ''); } protected function filterChanges(array $changes): array { @@ -162,8 +114,8 @@ class Admin implements ISettings { } /** - * @param array $groupIds - * @return array + * @param string[] $groupIds + * @return list<array{id: string, displayname: string}> */ protected function getSelectedGroups(array $groupIds): array { $result = []; @@ -174,51 +126,26 @@ class Admin implements ISettings { continue; } - $result[] = ['value' => $group->getGID(), 'label' => $group->getDisplayName()]; + $result[] = ['id' => $group->getGID(), 'displayname' => $group->getDisplayName()]; } return $result; } - /** - * @return string the section ID, e.g. 'sharing' - */ - public function getSection(): string { + public function getSection(): ?string { + if (!$this->config->getSystemValueBool('updatechecker', true)) { + // update checker is disabled so we do not show the section at all + return null; + } + return 'overview'; } - /** - * @return int whether the form should be rather on the top or bottom of - * the admin section. The forms are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. - * - * E.g.: 70 - */ public function getPriority(): int { return 11; } private function isWebUpdaterRecommended(): bool { - return $this->getUserCount() < 100; - } - - /** - * @see https://github.com/nextcloud/server/blob/39494fbf794d982f6f6551c984e6ca4c4e947d01/lib/private/Support/Subscription/Registry.php#L188-L216 implementation reference - */ - private function getUserCount(): int { - $userCount = 0; - $backends = $this->userManager->getBackends(); - foreach ($backends as $backend) { - // TODO: change below to 'if ($backend instanceof ICountUsersBackend) {' - if ($backend->implementsActions(Backend::COUNT_USERS)) { - /** @var ICountUsersBackend $backend */ - $backendUsers = $backend->countUsers(); - if ($backendUsers !== false) { - $userCount += $backendUsers; - } - } - } - - return $userCount; + return (int)$this->userManager->countUsersTotal(100) < 100; } } diff --git a/apps/updatenotification/lib/UpdateChecker.php b/apps/updatenotification/lib/UpdateChecker.php index 40bf784e605..b206ba4a3e4 100644 --- a/apps/updatenotification/lib/UpdateChecker.php +++ b/apps/updatenotification/lib/UpdateChecker.php @@ -3,46 +3,23 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\UpdateNotification; use OC\Updater\ChangesCheck; use OC\Updater\VersionCheck; +use OCP\AppFramework\Services\IInitialState; class UpdateChecker { - /** @var VersionCheck */ - private $updater; - /** @var ChangesCheck */ - private $changesCheck; - /** - * @param VersionCheck $updater - */ - public function __construct(VersionCheck $updater, ChangesCheck $changesCheck) { - $this->updater = $updater; - $this->changesCheck = $changesCheck; + public function __construct( + private VersionCheck $updater, + private ChangesCheck $changesCheck, + private IInitialState $initialState, + ) { } /** @@ -79,13 +56,17 @@ class UpdateChecker { } /** - * @param array $data + * Provide update information as initial state */ - public function populateJavaScriptVariables(array $data) { - $data['array']['oc_updateState'] = json_encode([ - 'updateAvailable' => true, - 'updateVersion' => $this->getUpdateState()['updateVersionString'], - 'updateLink' => $this->getUpdateState()['updateLink'] ?? '', + public function setInitialState(): void { + $updateState = $this->getUpdateState(); + if (empty($updateState)) { + return; + } + + $this->initialState->provideInitialState('updateState', [ + 'updateVersion' => $updateState['updateVersionString'], + 'updateLink' => $updateState['updateLink'] ?? '', ]); } } |