diff --git a/config/permissions.yaml b/config/permissions.yaml index d00e1e77..cf363100 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -251,6 +251,8 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co label: "perm.server_infos" manage_oauth_tokens: label: "Manage OAuth tokens" + show_updates: + label: "perm.system.show_available_updates" attachments: label: "perm.part.attachments" diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index 212363ff..dc728465 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -25,6 +25,7 @@ namespace App\Controller; use App\DataTables\LogDataTable; use App\Entity\Parts\Part; use App\Services\Misc\GitVersionInfo; +use App\Services\System\UpdateAvailableManager; use Doctrine\ORM\EntityManagerInterface; use const DIRECTORY_SEPARATOR; use Omines\DataTablesBundle\DataTableFactory; @@ -62,7 +63,8 @@ class HomepageController extends AbstractController } #[Route(path: '/', name: 'homepage')] - public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager): Response + public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager, + UpdateAvailableManager $updateAvailableManager): Response { if ($this->isGranted('@tools.lastActivity')) { $table = $this->dataTable->createFromType( @@ -97,6 +99,9 @@ class HomepageController extends AbstractController 'git_commit' => $versionInfo->getGitCommitHash(), 'show_first_steps' => $show_first_steps, 'datatable' => $table, + 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), + 'new_version' => $updateAvailableManager->getLatestVersionString(), + 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), ]); } } diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index c14e1a13..e0626e7b 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -28,6 +28,7 @@ use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Misc\GitVersionInfo; use App\Services\Misc\DBInfoHelper; +use App\Services\System\UpdateAvailableManager; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -47,7 +48,7 @@ class ToolsController extends AbstractController #[Route(path: '/server_infos', name: 'tools_server_infos')] public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, - AttachmentSubmitHandler $attachmentSubmitHandler): Response + AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response { $this->denyAccessUnlessGranted('@system.server_infos'); @@ -90,6 +91,11 @@ class ToolsController extends AbstractController 'db_size' => $DBInfoHelper->getDatabaseSize(), 'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown', 'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown', + + //New version section + 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), + 'new_version' => $updateAvailableManager->getLatestVersionString(), + 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), ]); } diff --git a/src/Entity/UserSystem/PermissionData.php b/src/Entity/UserSystem/PermissionData.php index 01bb2416..38f4b774 100644 --- a/src/Entity/UserSystem/PermissionData.php +++ b/src/Entity/UserSystem/PermissionData.php @@ -43,7 +43,7 @@ final class PermissionData implements \JsonSerializable /** * The current schema version of the permission data */ - public const CURRENT_SCHEMA_VERSION = 2; + public const CURRENT_SCHEMA_VERSION = 3; /** * Creates a new Permission Data Instance using the given data. diff --git a/src/Helpers/TrinaryLogicHelper.php b/src/Helpers/TrinaryLogicHelper.php new file mode 100644 index 00000000..54ab9bf8 --- /dev/null +++ b/src/Helpers/TrinaryLogicHelper.php @@ -0,0 +1,106 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Helpers; + +/** + * Helper functions for logic operations with trinary logic. + * True and false are represented as classical boolean values, undefined is represented as null. + */ +class TrinaryLogicHelper +{ + + /** + * Implements the trinary logic NOT. + * @param bool|null $a + * @return bool|null + */ + public static function not(?bool $a): ?bool + { + if ($a === null) { + return null; + } + return !$a; + } + + + /** + * Returns the trinary logic OR of the given parameters. At least one parameter is required. + * @param bool|null ...$args + * @return bool|null + */ + public static function or(?bool ...$args): ?bool + { + if (count($args) === 0) { + throw new \LogicException('At least one parameter is required.'); + } + + // The trinary or is the maximum of the integer representation of the parameters. + return self::intToBool( + max(array_map(self::boolToInt(...), $args)) + ); + } + + /** + * Returns the trinary logic AND of the given parameters. At least one parameter is required. + * @param bool|null ...$args + * @return bool|null + */ + public static function and(?bool ...$args): ?bool + { + if (count($args) === 0) { + throw new \LogicException('At least one parameter is required.'); + } + + // The trinary and is the minimum of the integer representation of the parameters. + return self::intToBool( + min(array_map(self::boolToInt(...), $args)) + ); + } + + /** + * Convert the trinary bool to an integer, where true is 1, false is -1 and null is 0. + * @param bool|null $a + * @return int + */ + private static function boolToInt(?bool $a): int + { + if ($a === null) { + return 0; + } + return $a ? 1 : -1; + } + + /** + * Convert the integer to a trinary bool, where 1 is true, -1 is false and 0 is null. + * @param int $a + * @return bool|null + */ + private static function intToBool(int $a): ?bool + { + if ($a === 0) { + return null; + } + return $a > 0; + } +} \ No newline at end of file diff --git a/src/Services/System/UpdateAvailableManager.php b/src/Services/System/UpdateAvailableManager.php new file mode 100644 index 00000000..29da8cd4 --- /dev/null +++ b/src/Services/System/UpdateAvailableManager.php @@ -0,0 +1,108 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Version\Version; + +/** + * This class checks if a new version of Part-DB is available. + */ +class UpdateAvailableManager +{ + + private const API_URL = 'https://api.github.com/repos/Part-DB/Part-DB-server/releases/latest'; + private const CACHE_KEY = 'uam_latest_version'; + private const CACHE_TTL = 60 * 60 * 24 * 2; // 2 day + + public function __construct(private readonly HttpClientInterface $httpClient, private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager) + { + + } + + /** + * Gets the latest version of Part-DB as string (e.g. "1.2.3"). + * This value is cached for 2 days. + * @return string + */ + public function getLatestVersionString(): string + { + return $this->getLatestVersionInfo()['version']; + } + + /** + * Gets the latest version of Part-DB as Version object. + */ + public function getLatestVersion(): Version + { + return Version::fromString($this->getLatestVersionString()); + } + + /** + * Gets the URL to the latest version of Part-DB on GitHub. + * @return string + */ + public function getLatestVersionUrl(): string + { + return $this->getLatestVersionInfo()['url']; + } + + /** + * Checks if a new version of Part-DB is available. This value is cached for 2 days. + * @return bool + */ + public function isUpdateAvailable(): bool + { + $latestVersion = $this->getLatestVersion(); + $currentVersion = $this->versionManager->getVersion(); + + return $latestVersion->isGreaterThan($currentVersion); + } + + /** + * Get the latest version info. The value is cached for 2 days. + * @return array + * @phpstan-return array{version: string} + */ + private function getLatestVersionInfo(): array + { + return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) { + $item->expiresAfter(self::CACHE_TTL); + $response = $this->httpClient->request('GET', self::API_URL); + $result = $response->toArray(); + $tag_name = $result['tag_name']; + + // Remove the leading 'v' from the tag name + $version = substr($tag_name, 1); + + return [ + 'version' => $version, + 'url' => $result['html_url'], + ]; + }); + } +} \ No newline at end of file diff --git a/src/Services/UserSystem/PermissionPresetsHelper.php b/src/Services/UserSystem/PermissionPresetsHelper.php index ea2391f7..eeb80f61 100644 --- a/src/Services/UserSystem/PermissionPresetsHelper.php +++ b/src/Services/UserSystem/PermissionPresetsHelper.php @@ -107,6 +107,8 @@ class PermissionPresetsHelper //Allow to manage Oauth tokens $this->permissionResolver->setPermission($perm_holder, 'system', 'manage_oauth_tokens', PermissionData::ALLOW); + //Allow to show updates + $this->permissionResolver->setPermission($perm_holder, 'system', 'show_updates', PermissionData::ALLOW); } diff --git a/src/Services/UserSystem/PermissionSchemaUpdater.php b/src/Services/UserSystem/PermissionSchemaUpdater.php index 5fb08182..104800dc 100644 --- a/src/Services/UserSystem/PermissionSchemaUpdater.php +++ b/src/Services/UserSystem/PermissionSchemaUpdater.php @@ -25,6 +25,7 @@ namespace App\Services\UserSystem; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\PermissionData; use App\Entity\UserSystem\User; +use App\Helpers\TrinaryLogicHelper; use App\Security\Interfaces\HasPermissionsInterface; /** @@ -138,4 +139,22 @@ class PermissionSchemaUpdater $holder->getPermissions()->removePermission('devices'); } } + + private function upgradeSchemaToVersion3(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection + { + $permissions = $holder->getPermissions(); + + //If the system.show_updates permission is not defined yet, set it to true, if the user can view server info, server logs or edit users or groups + if (!$permissions->isPermissionSet('system', 'show_updates')) { + + $new_value = TrinaryLogicHelper::or( + $permissions->getPermissionValue('system', 'server_infos'), + $permissions->getPermissionValue('system', 'show_logs'), + $permissions->getPermissionValue('users', 'edit'), + $permissions->getPermissionValue('groups', 'edit') + ); + + $permissions->setPermissionValue('system', 'show_updates', $new_value); + } + } } diff --git a/templates/components/new_version.macro.html.twig b/templates/components/new_version.macro.html.twig new file mode 100644 index 00000000..f8bc1e2e --- /dev/null +++ b/templates/components/new_version.macro.html.twig @@ -0,0 +1,9 @@ +{% macro new_version_alert(is_available, new_version, new_version_url) %} + {% if is_available %} + + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/templates/homepage.html.twig b/templates/homepage.html.twig index 8352e2de..138257c7 100644 --- a/templates/homepage.html.twig +++ b/templates/homepage.html.twig @@ -1,6 +1,13 @@ {% extends "base.html.twig" %} +{% import "components/new_version.macro.html.twig" as nv %} + {% block content %} + + {% if is_granted('@system.show_updates') %} + {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} + {% endif %} +

{{ partdb_title }}

diff --git a/templates/tools/server_infos/server_infos.html.twig b/templates/tools/server_infos/server_infos.html.twig index fd3d2759..e4e02d32 100644 --- a/templates/tools/server_infos/server_infos.html.twig +++ b/templates/tools/server_infos/server_infos.html.twig @@ -1,4 +1,5 @@ {% extends "main_card.html.twig" %} +{% import "components/new_version.macro.html.twig" as nv %} {% block title %}{% trans %}tools.server_infos.title{% endtrans %}{% endblock %} @@ -6,6 +7,12 @@ {% trans %}tools.server_infos.title{% endtrans %} {% endblock %} +{% block before_card %} + {% if is_granted('@system.show_updates') %} + {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} + {% endif %} +{% endblock %} + {% block card_content %}