diff options
Diffstat (limited to 'apps/updatenotification/lib')
18 files changed, 1622 insertions, 143 deletions
diff --git a/apps/updatenotification/lib/AppInfo/Application.php b/apps/updatenotification/lib/AppInfo/Application.php new file mode 100644 index 00000000000..2a1678da5db --- /dev/null +++ b/apps/updatenotification/lib/AppInfo/Application.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +/** + * 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\Http\Events\BeforeTemplateRenderedEvent; +use OCP\IConfig; +use OCP\IGroupManager; +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(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, + ContainerInterface $container, + LoggerInterface $logger, + ): void { + if ($config->getSystemValue('updatechecker', true) !== true) { + // Updater check is disabled + return; + } + + $user = $userSession->getUser(); + if (!$user instanceof IUser) { + // Nothing to do for guests + return; + } + + if (!$appManager->isEnabledForUser('notifications') + && $groupManager->isAdmin($user->getUID())) { + try { + $updateChecker = $container->get(UpdateChecker::class); + } catch (ContainerExceptionInterface $e) { + $logger->error($e->getMessage(), ['exception' => $e]); + return; + } + + if ($updateChecker->getUpdateState() !== []) { + 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/BackgroundJob/UpdateAvailableNotifications.php b/apps/updatenotification/lib/BackgroundJob/UpdateAvailableNotifications.php new file mode 100644 index 00000000000..8879bb0c223 --- /dev/null +++ b/apps/updatenotification/lib/BackgroundJob/UpdateAvailableNotifications.php @@ -0,0 +1,244 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\UpdateNotification\BackgroundJob; + +use OC\Installer; +use OC\Updater\VersionCheck; +use OCA\UpdateNotification\AppInfo\Application; +use OCP\App\IAppManager; +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 UpdateAvailableNotifications extends TimedJob { + + /** + * 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->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. + // Otherwise all Nextcloud installations are pinging the servers + // in one of 288 + sleep(random_int(1, 180)); + } catch (\Exception $e) { + } + } + + $this->checkCoreUpdate(); + $this->checkAppUpdates(); + } + + /** + * Check for Nextcloud server update + */ + protected function checkCoreUpdate(): void { + if (!$this->config->getSystemValueBool('updatechecker', true)) { + // update checker is disabled so no core update check! + return; + } + + 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 = $this->versionCheck->check(); + if ($status === false) { + $errors = 1 + $this->appConfig->getAppValueInt('update_check_errors', 0); + $this->appConfig->setAppValueInt('update_check_errors', $errors); + + if (\in_array($errors, self::CONNECTION_NOTIFICATIONS, true)) { + $this->sendErrorNotifications($errors); + } + } elseif (\is_array($status)) { + $this->appConfig->setAppValueInt('update_check_errors', 0); + $this->clearErrorNotifications(); + + if (isset($status['version'])) { + $this->createNotifications('core', $status['version'], $status['versionstring']); + } + } + } + + /** + * Send a message to the admin when the update server could not be reached + * @param int $numDays + */ + protected function sendErrorNotifications($numDays): void { + $this->clearErrorNotifications(); + + $notification = $this->notificationManager->createNotification(); + try { + $notification->setApp(Application::APP_NAME) + ->setDateTime(new \DateTime()) + ->setObject(Application::APP_NAME, 'error') + ->setSubject('connection_error', ['days' => $numDays]); + + foreach ($this->getUsersToNotify() as $uid) { + $notification->setUser($uid); + $this->notificationManager->notify($notification); + } + } catch (\InvalidArgumentException $e) { + return; + } + } + + /** + * Remove error notifications again + */ + protected function clearErrorNotifications(): void { + $notification = $this->notificationManager->createNotification(); + try { + $notification->setApp(Application::APP_NAME) + ->setSubject('connection_error') + ->setObject(Application::APP_NAME, 'error'); + } catch (\InvalidArgumentException $e) { + return; + } + $this->notificationManager->markProcessed($notification); + } + + /** + * Check all installed apps for updates + */ + protected function checkAppUpdates(): void { + $apps = $this->appManager->getEnabledApps(); + foreach ($apps as $app) { + $update = $this->isUpdateAvailable($app); + if ($update !== false) { + $this->createNotifications($app, $update); + } + } + } + + /** + * Create notifications for this app version + * + * @param string $app + * @param string $version + * @param string $visibleVersion + */ + protected function createNotifications($app, $version, $visibleVersion = ''): void { + $lastNotification = $this->appConfig->getAppValueString($app, ''); + if ($lastNotification === $version) { + // We already notified about this update + return; + } + + if ($lastNotification !== '') { + // Delete old updates + $this->deleteOutdatedNotifications($app, $lastNotification); + } + + $notification = $this->notificationManager->createNotification(); + try { + $notification->setApp(Application::APP_NAME) + ->setDateTime(new \DateTime()) + ->setObject($app, $version); + + if ($visibleVersion !== '') { + $notification->setSubject('update_available', ['version' => $visibleVersion]); + } else { + $notification->setSubject('update_available'); + } + + foreach ($this->getUsersToNotify() as $uid) { + $notification->setUser($uid); + $this->notificationManager->notify($notification); + } + } catch (\InvalidArgumentException $e) { + return; + } + + $this->appConfig->setAppValueString($app, $version); + } + + /** + * @return string[] + */ + protected function getUsersToNotify(): array { + if ($this->users !== null) { + return $this->users; + } + + $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(); + } + } + } + + $this->users = array_values(array_unique($this->users)); + return $this->users; + } + + /** + * Delete notifications for old updates + * + * @param string $app + * @param string $version + */ + protected function deleteOutdatedNotifications($app, $version): void { + $notification = $this->notificationManager->createNotification(); + try { + $notification->setApp(Application::APP_NAME) + ->setObject($app, $version); + } catch (\InvalidArgumentException) { + return; + } + $this->notificationManager->markProcessed($notification); + } + + /** + * @param string $app + * @return string|false + */ + protected function isUpdateAvailable($app) { + return $this->installer->isUpdateAvailable($app); + } +} diff --git a/apps/updatenotification/lib/Command/Check.php b/apps/updatenotification/lib/Command/Check.php new file mode 100644 index 00000000000..d93e4935012 --- /dev/null +++ b/apps/updatenotification/lib/Command/Check.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UpdateNotification\Command; + +use OC\App\AppManager; +use OC\Installer; +use OCA\UpdateNotification\UpdateChecker; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Check extends Command { + + public function __construct( + private AppManager $appManager, + private UpdateChecker $updateChecker, + private Installer $installer, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('update:check') + ->setDescription('Check for server and app updates') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $updatesAvailableCount = 0; + + // 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'] . '.'); + $updatesAvailableCount += 1; + } + + + // Apps + $apps = $this->appManager->getEnabledApps(); + foreach ($apps as $app) { + $update = $this->installer->isUpdateAvailable($app); + if ($update !== false) { + $output->writeln('Update for ' . $app . ' to version ' . $update . ' is available.'); + $updatesAvailableCount += 1; + } + } + + // Report summary + if ($updatesAvailableCount === 0) { + $output->writeln('<info>Everything up to date</info>'); + } elseif ($updatesAvailableCount === 1) { + $output->writeln('<comment>1 update available</comment>'); + } else { + $output->writeln('<comment>' . $updatesAvailableCount . ' updates available</comment>'); + } + + return 0; + } +} diff --git a/apps/updatenotification/lib/Controller/APIController.php b/apps/updatenotification/lib/Controller/APIController.php new file mode 100644 index 00000000000..4360d814dd2 --- /dev/null +++ b/apps/updatenotification/lib/Controller/APIController.php @@ -0,0 +1,197 @@ +<?php + +declare(strict_types=1); + +/** + * 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; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\L10N\IFactory; + +/** + * @psalm-import-type UpdateNotificationApp from ResponseDefinitions + */ +class APIController extends OCSController { + + protected ?string $language = null; + + /** + * 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); + } + + /** + * 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)) { + return new DataResponse([ + 'appstore_disabled' => true, + ], Http::STATUS_NOT_FOUND); + } + + // Get list of installed custom apps + $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) && !isset($this->appsShippedInFutureVersion[$app]); + }); + + if (empty($installedApps)) { + return new DataResponse([ + 'missing' => [], + 'available' => [], + ]); + } + + $this->appFetcher->setVersion($newVersion, 'future-apps.json', false); + + // Apps available on the app store for that version + $availableApps = array_map(static function (array $app): string { + return $app['id']; + }, $this->appFetcher->get()); + + if (empty($availableApps)) { + return new DataResponse([ + 'appstore_disabled' => false, + 'already_on_latest' => false, + ], Http::STATUS_NOT_FOUND); + } + + // 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); + sort($missing); + + $available = array_intersect($installedApps, $availableApps); + $available = array_map([$this, 'getAppDetails'], $available); + sort($available); + + return new DataResponse([ + 'missing' => $missing, + 'available' => $available, + ]); + } + + /** + * Get translated app name + * + * @param string $appId + * @return UpdateNotificationApp + */ + protected function getAppDetails(string $appId): array { + $app = $this->appManager->getAppInfo($appId, false, $this->language); + $name = $app['name'] ?? $appId; + return [ + 'appId' => $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 new file mode 100644 index 00000000000..26745948890 --- /dev/null +++ b/apps/updatenotification/lib/Controller/AdminController.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * 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\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; +use OCP\Security\ISecureRandom; +use OCP\Util; + +class AdminController extends Controller { + + 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); + } + + private function isUpdaterEnabled(): bool { + return !$this->config->getSystemValueBool('upgrade.disable-web'); + } + + /** + * @param string $channel + * @return DataResponse + */ + public function setChannel(string $channel): DataResponse { + Util::setChannel($channel); + $this->appConfig->setValueInt('core', 'lastupdatedat', 0); + return new DataResponse(['status' => 'success', 'data' => ['message' => $this->l10n->t('Channel updated')]]); + } + + /** + * @return DataResponse + */ + public function createCredentials(): DataResponse { + if (!$this->isUpdaterEnabled()) { + return new DataResponse(['status' => 'error', 'message' => $this->l10n->t('Web updater is disabled')], Http::STATUS_FORBIDDEN); + } + + // Create a new job and store the creation date + $this->jobList->add(ResetToken::class); + $this->appConfig->setValueInt('core', 'updater.secret.created', $this->timeFactory->getTime()); + + // Create a new token + $newToken = $this->secureRandom->generate(64); + $this->config->setSystemValue('updater.secret', password_hash($newToken, PASSWORD_DEFAULT)); + + return new DataResponse($newToken); + } +} 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 new file mode 100644 index 00000000000..787675bd98d --- /dev/null +++ b/apps/updatenotification/lib/Notification/Notifier.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\UpdateNotification\Notification; + +use OCA\UpdateNotification\AppInfo\Application; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IAppConfig; +use OCP\IGroupManager; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Notification\AlreadyProcessedException; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; +use OCP\ServerVersion; + +class Notifier implements INotifier { + /** @var string[] */ + protected $appVersions; + + /** + * Notifier constructor. + */ + 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(); + } + + /** + * Identifier of the notifier, only use [a-z0-9_] + * + * @return string + * @since 17.0.0 + */ + public function getID(): string { + return Application::APP_NAME; + } + + /** + * Human readable name describing the notifier + * + * @return string + * @since 17.0.0 + */ + public function getName(): string { + 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 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() !== Application::APP_NAME) { + throw new UnknownNotificationException('Unknown app id'); + } + + 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 = $this->appConfig->getAppValueInt('update_check_errors', 0); + if ($errors === 0) { + 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.')); + } else { + if ($notification->getObjectType() === 'core') { + $this->updateAlreadyInstalledCheck($notification, $this->getCoreVersions()); + + $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 (isset($this->appVersions[$notification->getObjectType()])) { + $this->updateAlreadyInstalledCheck($notification, $this->appVersions[$notification->getObjectType()]); + } + + $notification->setRichSubject($l->t('Update for {app} to version %s is available.', [$notification->getObjectId()]), [ + 'app' => [ + 'type' => 'app', + 'id' => $notification->getObjectType(), + 'name' => $appName, + ] + ]); + + if ($this->isAdmin()) { + $notification->setLink($this->url->linkToRouteAbsolute('settings.AppSettings.viewApps', ['category' => 'updates']) . '#app-' . $notification->getObjectType()); + } + } + } + + $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath(Application::APP_NAME, 'notification.svg'))); + + return $notification; + } + + /** + * Remove the notification and prevent rendering, when the update is installed + * + * @param INotification $notification + * @param string $installedVersion + * @throws AlreadyProcessedException When the update is already installed + */ + protected function updateAlreadyInstalledCheck(INotification $notification, $installedVersion): void { + if (version_compare($notification->getObjectId(), $installedVersion, '<=')) { + throw new AlreadyProcessedException(); + } + } + + protected function isAdmin(): bool { + $user = $this->userSession->getUser(); + + if ($user instanceof IUser) { + return $this->groupManager->isAdmin($user->getUID()); + } + + return false; + } + + protected function getCoreVersions(): string { + return implode('.', $this->serverVersion->getVersion()); + } + + protected function getAppInfo(string $appId, ?string $languageCode): ?array { + return $this->appManager->getAppInfo($appId, false, $languageCode); + } +} 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 new file mode 100644 index 00000000000..22228f1bccc --- /dev/null +++ b/apps/updatenotification/lib/Settings/Admin.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\UpdateNotification\Settings; + +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 Psr\Log\LoggerInterface; + +class Admin implements ISettings { + public function __construct( + 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, + ) { + } + + public function getForm(): TemplateResponse { + $lastUpdateCheckTimestamp = $this->appConfig->getValueInt('core', 'lastupdatedat'); + $lastUpdateCheck = $this->dateTimeFormatter->formatDateTime($lastUpdateCheckTimestamp); + + $channels = [ + 'daily', + 'beta', + 'stable', + 'production', + ]; + $currentChannel = $this->serverVersion->getChannel(); + if ($currentChannel === 'git') { + $channels[] = 'git'; + } + + $updateState = $this->updateChecker->getUpdateState(); + + $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); + $defaultCustomerUpdateServerURLPrefix = 'https://updates.nextcloud.com/customers/'; + + $isDefaultUpdateServerURL = $updateServerURL === $defaultUpdateServerURL + || strpos($updateServerURL, $defaultCustomerUpdateServerURLPrefix) === 0; + + $hasValidSubscription = $this->subscriptionRegistry->delegateHasValidSubscription(); + + $params = [ + 'isNewVersionAvailable' => !empty($updateState['updateAvailable']), + 'isUpdateChecked' => $lastUpdateCheckTimestamp > 0, + 'lastChecked' => $lastUpdateCheck, + 'currentChannel' => $currentChannel, + 'channels' => $channels, + 'newVersion' => empty($updateState['updateVersion']) ? '' : $updateState['updateVersion'], + 'newVersionString' => empty($updateState['updateVersionString']) ? '' : $updateState['updateVersionString'], + 'downloadLink' => empty($updateState['downloadLink']) ? '' : $updateState['downloadLink'], + 'changes' => $this->filterChanges($updateState['changes'] ?? []), + 'webUpdaterEnabled' => !$this->config->getSystemValue('upgrade.disable-web', false), + 'isWebUpdaterRecommended' => $this->isWebUpdaterRecommended(), + 'updaterEnabled' => empty($updateState['updaterEnabled']) ? false : $updateState['updaterEnabled'], + 'versionIsEol' => empty($updateState['versionIsEol']) ? false : $updateState['versionIsEol'], + 'isDefaultUpdateServerURL' => $isDefaultUpdateServerURL, + 'updateServerURL' => $updateServerURL, + 'notifyGroups' => $this->getSelectedGroups($notifyGroups), + 'hasValidSubscription' => $hasValidSubscription, + ]; + $this->initialState->provideInitialState('data', $params); + + return new TemplateResponse('updatenotification', 'admin', [], ''); + } + + protected function filterChanges(array $changes): array { + $filtered = []; + if (isset($changes['changelogURL'])) { + $filtered['changelogURL'] = $changes['changelogURL']; + } + if (!isset($changes['whatsNew'])) { + return $filtered; + } + + $iterator = $this->l10nFactory->getLanguageIterator(); + do { + $lang = $iterator->current(); + if (isset($changes['whatsNew'][$lang])) { + $filtered['whatsNew'] = $changes['whatsNew'][$lang]; + return $filtered; + } + $iterator->next(); + } while ($lang !== 'en' && $iterator->valid()); + + return $filtered; + } + + /** + * @param string[] $groupIds + * @return list<array{id: string, displayname: string}> + */ + protected function getSelectedGroups(array $groupIds): array { + $result = []; + foreach ($groupIds as $groupId) { + $group = $this->groupManager->get($groupId); + + if ($group === null) { + continue; + } + + $result[] = ['id' => $group->getGID(), 'displayname' => $group->getDisplayName()]; + } + + return $result; + } + + 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'; + } + + public function getPriority(): int { + return 11; + } + + private function isWebUpdaterRecommended(): bool { + return (int)$this->userManager->countUsersTotal(100) < 100; + } +} diff --git a/apps/updatenotification/lib/UpdateChecker.php b/apps/updatenotification/lib/UpdateChecker.php new file mode 100644 index 00000000000..b206ba4a3e4 --- /dev/null +++ b/apps/updatenotification/lib/UpdateChecker.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * 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 { + + public function __construct( + private VersionCheck $updater, + private ChangesCheck $changesCheck, + private IInitialState $initialState, + ) { + } + + /** + * @return array + */ + public function getUpdateState(): array { + $data = $this->updater->check(); + $result = []; + + if (isset($data['version']) && $data['version'] !== '' && $data['version'] !== []) { + $result['updateAvailable'] = true; + $result['updateVersion'] = $data['version']; + $result['updateVersionString'] = $data['versionstring']; + $result['updaterEnabled'] = $data['autoupdater'] === '1'; + $result['versionIsEol'] = $data['eol'] === '1'; + if (strpos($data['web'], 'https://') === 0) { + $result['updateLink'] = $data['web']; + } + if (strpos($data['url'], 'https://') === 0) { + $result['downloadLink'] = $data['url']; + } + if (strpos($data['changes'], 'https://') === 0) { + try { + $result['changes'] = $this->changesCheck->check($data['changes'], $data['version']); + } catch (\Exception $e) { + // no info, not a problem + } + } + + return $result; + } + + return []; + } + + /** + * Provide update information as initial state + */ + public function setInitialState(): void { + $updateState = $this->getUpdateState(); + if (empty($updateState)) { + return; + } + + $this->initialState->provideInitialState('updateState', [ + 'updateVersion' => $updateState['updateVersionString'], + 'updateLink' => $updateState['updateLink'] ?? '', + ]); + } +} diff --git a/apps/updatenotification/lib/resettokenbackgroundjob.php b/apps/updatenotification/lib/resettokenbackgroundjob.php deleted file mode 100644 index 61bd9fc0490..00000000000 --- a/apps/updatenotification/lib/resettokenbackgroundjob.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @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\AppFramework\Utility\TimeFactory; -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|null $config - * @param ITimeFactory|null $timeFactory - */ - public function __construct(IConfig $config = null, - ITimeFactory $timeFactory = null) { - // Run all 10 minutes - $this->setInterval(60 * 10); - - if ($config instanceof IConfig && $timeFactory instanceof ITimeFactory) { - $this->config = $config; - $this->timeFactory = $timeFactory; - } else { - $this->fixDIForJobs(); - } - } - - /** - * DI for jobs - */ - private function fixDIForJobs() { - $this->config = \OC::$server->getConfig(); - $this->timeFactory = new 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/updatechecker.php b/apps/updatenotification/lib/updatechecker.php deleted file mode 100644 index 965e21617e7..00000000000 --- a/apps/updatenotification/lib/updatechecker.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @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\Updater; - -class UpdateChecker { - /** @var Updater */ - private $updater; - - /** - * @param Updater $updater - */ - public function __construct(Updater $updater) { - $this->updater = $updater; - } - - /** - * @return array - */ - public function getUpdateState() { - $data = $this->updater->check(); - $result = []; - - if(isset($data['version']) && $data['version'] !== '' && $data['version'] !== []) { - $result['updateAvailable'] = true; - $result['updateVersion'] = $data['versionstring']; - if(substr($data['web'], 0, 8) === 'https://') { - $result['updateLink'] = $data['web']; - } - - return $result; - } - - return []; - } - - /** - * @param array $data - */ - public function getJavaScript(array $data) { - $data['array']['oc_updateState'] = json_encode([ - 'updateAvailable' => true, - 'updateVersion' => $this->getUpdateState()['updateVersion'], - 'updateLink' => isset($this->getUpdateState()['updateLink']) ? $this->getUpdateState()['updateLink'] : '', - ]); - } -} |