Compare commits

...

10 commits

Author SHA1 Message Date
Jan Böhmer
7054c51490 Started documenting the upgrade process
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
2025-08-15 01:04:20 +02:00
Jan Böhmer
808af0d3cd Fixed phpstan issue 2025-08-15 00:52:19 +02:00
Jan Böhmer
b14fc0e22a Only use inline style for commonmark parser 2025-08-15 00:09:28 +02:00
Jan Böhmer
f7259a118b Removed invalid reference to parsedown 2025-08-14 22:56:40 +02:00
Jan Böhmer
be60c4363c Replaced parsedown with the newer league/commonmark library 2025-08-14 22:56:20 +02:00
Jan Böhmer
631db7df31 Fixed postgresql migrations 2025-08-14 22:37:40 +02:00
Jan Böhmer
781ea45633 Removed ArrayType which is not necessary anymore with the new webauthn lib 2025-08-14 18:47:17 +02:00
Jan Böhmer
0eee161630 Use new webauthn library for 2FA 2025-08-14 18:46:10 +02:00
Jan Böhmer
7a1b9b8ce1 Updated dependencies 2025-08-13 16:13:25 +02:00
Jan Böhmer
3fcb5ce82e Merge branch 'master' into v2 2025-08-13 16:07:20 +02:00
19 changed files with 1653 additions and 1229 deletions

View file

@ -13,8 +13,8 @@
"ext-mbstring": "*",
"amphp/http-client": "^5.1",
"api-platform/doctrine-orm": "^4.1",
"api-platform/symfony": "^4.0.0",
"api-platform/json-api": "^4.0.0",
"api-platform/symfony": "^4.0.0",
"beberlei/doctrineextensions": "^1.2",
"brick/math": "^0.13.1",
"composer/ca-bundle": "^1.5",
@ -25,16 +25,16 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^3.2.0",
"dompdf/dompdf": "^v3.0.0",
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
"florianv/swap-bundle": "dev-master",
"gregwar/captcha-bundle": "^2.1.0",
"hshn/base64-encoded-file": "^5.0",
"jbtronics/2fa-webauthn": "^v2.2.0",
"jbtronics/2fa-webauthn": "^3.0.0",
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
"jbtronics/settings-bundle": "^v2.6.0",
"jfcherng/php-diff": "^6.14",
"knpuniversity/oauth2-client-bundle": "^2.15",
"league/commonmark": "^2.7",
"league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
@ -95,7 +95,7 @@
"twig/intl-extra": "^3.8",
"twig/markdown-extra": "^3.8",
"twig/string-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0"
"web-auth/webauthn-symfony-bundle": "^5.0.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^v8.0.0",

1075
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -32,5 +32,5 @@ return [
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
\ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
];

View file

@ -25,10 +25,6 @@ doctrine:
tinyint:
class: App\Doctrine\Types\TinyIntType
# This was removed in doctrine/orm 4.0 but we need it for the WebauthnKey entity
array:
class: App\Doctrine\Types\ArrayType
schema_filter: ~^(?!internal)~
# Only enable this when needed
profiling_collect_backtrace: false

View file

@ -13,7 +13,7 @@ security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
pattern: ^/(_(profiler|wdt)|css|images|js|\.well-known)/
security: false
main:
provider: app_user_provider

27
docs/upgrade/1_to_2.md Normal file
View file

@ -0,0 +1,27 @@
---
layout: default
title: Upgrade from Part-DB 1.x to 2.x
nav_order: 1
has_children: false
---
# Upgrade from Part-DB 1.x to 2.x
Part-DB 2.0 is a major release that changes a lot of things internally, but it is still compatible with Part-DB 1.x.
Depending on your preferences, you will have to do some changes to your Part-DB installation, this document will guide
you through the upgrade process.
## New requirements
*If you are running Part-DB inside a docker container, you can skip this section, as the new requirements are already
fulfilled by the official Part-DB docker image.*
Part-DB 2.0 requires at least PHP 8.2 (newer versions are recommended). So if your existing Part-DB installation is still
running PHP 8.1, you will have to upgrade your PHP version first.
The minimum required version of node.js is now 20.0 or newer, so if you are using 18.0, you will have to upgrade it too.
Most distributions should have the possibility to get backports for PHP 8.4 and modern nodejs, so you should be able to
easily upgrade your system to the new requirements. Otherwise, you can use the official Part-DB docker image, which
ships all required dependencies and is always up to date with the latest requirements, so that you do not have to worry
about the requirements at all.

9
docs/upgrade/index.md Normal file
View file

@ -0,0 +1,9 @@
---
layout: default
title: Upgrade
nav_order: 7
has_children: true
---
This section provides information on how to upgrade Part-DB to the latest version.
This is intended for major release upgrades, where requirements or things changes significantly.

View file

@ -2,6 +2,7 @@
layout: default
title: Upgrade from legacy Part-DB version (<1.0)
nav_order: 100
redirect_from: /upgrade_legacy
---
# Upgrade from legacy Part-DB version

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20250813214628 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Migrate webauthn_keys transports and other_ui fields to JSON type';
}
public function convertArrayToJson(): void
{
$connection = $this->connection;
$rows = $connection->fetchAllAssociative('SELECT id, transports, other_ui FROM webauthn_keys');
foreach ($rows as $row) {
$id = $row['id'];
$new_transports = json_encode(unserialize($row['transports'], ['allowed_classes' => false]),
JSON_THROW_ON_ERROR);
$new_other_ui = json_encode(unserialize($row['other_ui'], ['allowed_classes' => false]),
JSON_THROW_ON_ERROR);
$connection->executeStatement(
'UPDATE webauthn_keys SET transports = :transports, other_ui = :other_ui WHERE id = :id',
[
'transports' => $new_transports,
'other_ui' => $new_other_ui,
'id' => $id,
]
);
}
}
public function mySQLUp(Schema $schema): void
{
$this->convertArrayToJson();
$this->addSql('ALTER TABLE webauthn_keys CHANGE transports transports JSON NOT NULL, CHANGE other_ui other_ui JSON DEFAULT NULL');
}
public function mySQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE webauthn_keys CHANGE transports transports LONGTEXT NOT NULL, CHANGE other_ui other_ui LONGTEXT DEFAULT NULL');
}
public function sqLiteUp(Schema $schema): void
{
//As there is no JSON type in SQLite, we only need to convert the data.
$this->convertArrayToJson();
}
public function sqLiteDown(Schema $schema): void
{
//Nothing to do here, as SQLite does not support JSON type and we are not changing the column type.
}
public function postgreSQLUp(Schema $schema): void
{
$this->convertArrayToJson();
$this->addSql('ALTER TABLE webauthn_keys ALTER transports TYPE JSON USING transports::JSON');
$this->addSql('ALTER TABLE webauthn_keys ALTER other_ui TYPE JSON USING other_ui::JSON');
}
public function postgreSQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE webauthn_keys ALTER transports TYPE TEXT');
$this->addSql('ALTER TABLE webauthn_keys ALTER other_ui TYPE TEXT');
}
}

View file

@ -1,116 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* 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 <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Doctrine\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\SerializationFailed;
use Doctrine\DBAL\Types\Type;
use Doctrine\Deprecations\Deprecation;
use function is_resource;
use function restore_error_handler;
use function serialize;
use function set_error_handler;
use function stream_get_contents;
use function unserialize;
use const E_DEPRECATED;
use const E_USER_DEPRECATED;
/**
* This class is taken from doctrine ORM 3.8. https://github.com/doctrine/dbal/blob/3.8.x/src/Types/ArrayType.php
*
* It was removed in doctrine ORM 4.0. However, we require it for backward compatibility with WebauthnKey.
* Therefore, we manually added it here as a custom type as a forward compatibility layer.
*/
class ArrayType extends Type
{
/**
* {@inheritDoc}
*/
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getClobTypeDeclarationSQL($column);
}
/**
* {@inheritDoc}
*/
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string
{
return serialize($value);
}
/**
* {@inheritDoc}
*/
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
{
if ($value === null) {
return null;
}
$value = is_resource($value) ? stream_get_contents($value) : $value;
set_error_handler(function (int $code, string $message): bool {
if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) {
return false;
}
//Change to original code. Use SerializationFailed instead of ConversionException.
throw new SerializationFailed("Serialization failed (Code $code): " . $message);
});
try {
//Change to original code. Use false for allowed_classes, to avoid unsafe unserialization of objects.
return unserialize($value, ['allowed_classes' => false]);
} finally {
restore_error_handler();
}
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
return "array";
}
/**
* {@inheritDoc}
*
* @deprecated
*/
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5509',
'%s is deprecated.',
__METHOD__,
);
return true;
}
}

View file

@ -100,16 +100,19 @@ class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampable
public static function fromRegistration(BasePublicKeyCredentialSource $registration): self
{
return new self(
$registration->getPublicKeyCredentialId(),
$registration->getType(),
$registration->getTransports(),
$registration->getAttestationType(),
$registration->getTrustPath(),
$registration->getAaguid(),
$registration->getCredentialPublicKey(),
$registration->getUserHandle(),
$registration->getCounter(),
$registration->getOtherUI()
publicKeyCredentialId: $registration->publicKeyCredentialId,
type: $registration->type,
transports: $registration->transports,
attestationType: $registration->attestationType,
trustPath: $registration->trustPath,
aaguid: $registration->aaguid,
credentialPublicKey: $registration->credentialPublicKey,
userHandle: $registration->userHandle,
counter: $registration->counter,
otherUI: $registration->otherUI,
backupEligible: $registration->backupEligible,
backupStatus: $registration->backupStatus,
uvInitialized: $registration->uvInitialized,
);
}
}

View file

@ -33,6 +33,7 @@ use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Webauthn\PublicKeyCredential;
/**
* This class decorates the Webauthn TwoFactorProvider and adds additional logic which allows us to set a last used date
@ -88,10 +89,12 @@ class WebauthnKeyLastUseTwoFactorProvider implements TwoFactorProviderInterface
private function getWebauthnKeyFromCode(string $authenticationCode): ?WebauthnKey
{
$publicKeyCredentialLoader = $this->webauthnProvider->getPublicKeyCredentialLoader();
$serializer = $this->webauthnProvider->getWebauthnSerializer();
//Try to load the public key credential from the code
$publicKeyCredential = $publicKeyCredentialLoader->load($authenticationCode);
$publicKeyCredential = $serializer->deserialize($authenticationCode, PublicKeyCredential::class, 'json', [
'json_decode_options' => JSON_THROW_ON_ERROR
]);
//Find the credential source for the given credential id
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($publicKeyCredential->rawId);
@ -103,4 +106,4 @@ class WebauthnKeyLastUseTwoFactorProvider implements TwoFactorProviderInterface
return $publicKeyCredentialSource;
}
}
}

View file

@ -112,12 +112,12 @@ class AttachmentURLGenerator
/**
* Returns a URL to a thumbnail of the attachment file.
* For external files the original URL is returned.
* @return string|null The URL or null if the attachment file is not existing
* @return string|null The URL or null if the attachment file is not existing or is invalid
*/
public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string
{
if (!$attachment->isPicture()) {
throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!');
return null;
}
if (!$attachment->hasInternal()){

View file

@ -57,6 +57,7 @@ class EntityImporter
/**
* Creates many entries at once, based on a (text) list of name.
* The created entities are not persisted to database yet, so you have to do it yourself.
* It returns all entities in the hierachy chain (even if they are already persisted).
*
* @template T of AbstractNamedDBElement
* @param string $lines The list of names seperated by \n
@ -132,32 +133,38 @@ class EntityImporter
//We can only use the getNewEntityFromPath function, if the repository is a StructuralDBElementRepository
if ($repo instanceof StructuralDBElementRepository) {
$entities = $repo->getNewEntityFromPath($new_path);
$entity = end($entities);
if ($entity === false) {
if ($entities === []) {
throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!');
}
} else { //Otherwise just create a new entity
$entity = new $class_name;
$entity->setName($name);
$entities = [$entity];
}
//Validate entity
$tmp = $this->validator->validate($entity);
//If no error occured, write entry to DB:
if (0 === count($tmp)) {
$valid_entities[] = $entity;
} else { //Otherwise log error
$errors[] = [
'entity' => $entity,
'violations' => $tmp,
];
foreach ($entities as $entity) {
$tmp = $this->validator->validate($entity);
//If no error occured, write entry to DB:
if (0 === count($tmp)) {
$valid_entities[] = $entity;
} else { //Otherwise log error
$errors[] = [
'entity' => $entity,
'violations' => $tmp,
];
}
}
$last_element = $entity;
$last_element = end($entities);
if ($last_element === false) {
$last_element = null;
}
}
return $valid_entities;
//Only return objects once
return array_values(array_unique($valid_entities));
}
/**

View file

@ -46,7 +46,9 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Part;
use App\Services\Formatters\SIFormatter;
use Parsedown;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
use League\CommonMark\MarkdownConverter;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -54,8 +56,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
final class PartProvider implements PlaceholderProviderInterface
{
private readonly MarkdownConverter $inlineConverter;
public function __construct(private readonly SIFormatter $siFormatter, private readonly TranslatorInterface $translator)
{
$environment = new Environment();
$environment->addExtension(new InlinesOnlyExtension());
$this->inlineConverter = new MarkdownConverter($environment);
}
public function replace(string $placeholder, object $part, array $options = []): ?string
@ -112,22 +119,20 @@ final class PartProvider implements PlaceholderProviderInterface
return $this->translator->trans($part->getManufacturingStatus()->toTranslationKey());
}
$parsedown = new Parsedown();
if ('[[DESCRIPTION]]' === $placeholder) {
return $parsedown->line($part->getDescription());
return trim($this->inlineConverter->convert($part->getDescription())->getContent());
}
if ('[[DESCRIPTION_T]]' === $placeholder) {
return strip_tags((string) $parsedown->line($part->getDescription()));
return trim(strip_tags($this->inlineConverter->convert($part->getDescription())->getContent()));
}
if ('[[COMMENT]]' === $placeholder) {
return $parsedown->line($part->getComment());
return trim($this->inlineConverter->convert($part->getComment())->getContent());
}
if ('[[COMMENT_T]]' === $placeholder) {
return strip_tags((string) $parsedown->line($part->getComment()));
return trim(strip_tags($this->inlineConverter->convert($part->getComment())->getContent()));
}
return null;

View file

@ -133,9 +133,6 @@
"ekino/phpstan-banned-code": {
"version": "v0.3.1"
},
"erusev/parsedown": {
"version": "1.7.4"
},
"florianv/exchanger": {
"version": "1.4.1"
},

View file

@ -75,8 +75,8 @@ class EntityImporterTest extends WebTestCase
$em = self::getContainer()->get(EntityManagerInterface::class);
$parent = $em->find(AttachmentType::class, 1);
$results = $this->service->massCreation($lines, AttachmentType::class, $parent, $errors);
$this->assertCount(3, $results);
$this->assertSame($parent, $results[0]->getParent());
$this->assertCount(4, $results);
$this->assertSame("Test 1", $results[1]->getName());
//Test for addition of existing elements
$errors = [];
@ -113,6 +113,31 @@ EOT;
}
public function testMassCreationArrow(): void
{
$input = <<<EOT
Test1 -> Test1.1
Test1 -> Test1.2
Test2 -> Test2.1
Test1
Test1.3
EOT;
$errors = [];
$results = $this->service->massCreation($input, AttachmentType::class, null, $errors);
//We have 6 elements, and 0 errors
$this->assertCount(0, $errors);
$this->assertCount(6, $results);
$this->assertEquals('Test1', $results[0]->getName());
$this->assertEquals('Test1.1', $results[1]->getName());
$this->assertEquals('Test1.2', $results[2]->getName());
$this->assertEquals('Test2', $results[3]->getName());
$this->assertEquals('Test2.1', $results[4]->getName());
$this->assertEquals('Test1.3', $results[5]->getName());
}
public function testMassCreationNested(): void
{
$input = <<<EOT
@ -132,15 +157,15 @@ EOT;
//We have 7 elements, and 0 errors
$this->assertCount(0, $errors);
$this->assertCount(7, $results);
$this->assertCount(8, $results);
$element1 = $results[0];
$element11 = $results[1];
$element111 = $results[2];
$element112 = $results[3];
$element12 = $results[4];
$element121 = $results[5];
$element2 = $results[6];
$element1 = $results[1];
$element11 = $results[2];
$element111 = $results[3];
$element112 = $results[4];
$element12 = $results[5];
$element121 = $results[6];
$element2 = $results[7];
$this->assertSame('Test 1', $element1->getName());
$this->assertSame('Test 1.1', $element11->getName());

View file

@ -7157,12 +7157,15 @@ Exampletown</target>
</notes>
<segment state="translated">
<source>mass_creation.lines.placeholder</source>
<target>Element 1
<target><![CDATA[Element 1
Element 1.1
Element 1.1.1
Element 1.2
Element 2
Element 3</target>
Element 3
Element 1 -> Element 1.1
Element 1 -> Element 1.2]]></target>
</segment>
</unit>
<unit id="TWSqPFi" name="entity.mass_creation.btn">

1420
yarn.lock

File diff suppressed because it is too large Load diff