mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c44535990b | ||
|
b8d5b83eee | ||
|
00da2dedc3 | ||
|
4ce1de079e | ||
|
6b9c125de4 | ||
|
2c4f44e808 | ||
|
2b694731ad | ||
|
7e34535e62 | ||
|
0bb831fe88 | ||
|
42a32ce142 | ||
|
23f58b7bf4 | ||
|
4e9101fded | ||
|
9c700c77a8 | ||
|
cb1f674332 | ||
|
6823d94ffb |
18 changed files with 1627 additions and 1292 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.17.0
|
||||
1.17.1
|
||||
|
|
|
@ -128,6 +128,8 @@ const PLACEHOLDERS = [
|
|||
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
||||
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
||||
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
||||
['[[BARCODE_C93]]', 'Code 93 barcode linking to this element'],
|
||||
['[[BARCODE_DATAMATRIX]]', 'Datamatrix code linking to this element'],
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -69,6 +69,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
|||
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
||||
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
||||
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
||||
'Code 93 barcode linking to this element': 'Code 93 Barcode verknüpft mit diesem Element',
|
||||
'Datamatrix code linking to this element': 'Datamatrix Code verknüpft mit diesem Element',
|
||||
|
||||
'Location ID': 'Lagerort ID',
|
||||
'Name': 'Name',
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"omines/datatables-bundle": "^0.9.1",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
|
|
887
composer.lock
generated
887
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -52,6 +52,11 @@ server {
|
|||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Set Content-Security-Policy for svg files, to block embedded javascript in there
|
||||
location ~* \.svg$ {
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';";
|
||||
}
|
||||
|
||||
error_log /var/log/nginx/parts.error.log;
|
||||
access_log /var/log/nginx/parts.access.log;
|
||||
|
|
|
@ -127,9 +127,6 @@ You must create an organization there and create a "Production app". Most settin
|
|||
grant access to the "Product Information" API.
|
||||
You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below).
|
||||
|
||||
**Attention**: Currently only the "Product Information V3 (Deprecated)" is supported by Part-DB.
|
||||
Using "Product Information V4" will not work.
|
||||
|
||||
The following env configuration options are available:
|
||||
|
||||
* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory)
|
||||
|
|
|
@ -118,3 +118,10 @@ DirectoryIndex index.php
|
|||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 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\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:sanitize-svg', "Sanitize uploaded SVG files.")]
|
||||
class SanitizeSVGAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly AttachmentSubmitHandler $attachmentSubmitHandler, ?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command allows to sanitize SVG files uploaded via attachments. This happens automatically since version 1.17.1, this command is intended to be used for older files.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->info('This command will sanitize all uploaded SVG files. This is only required if you have uploaded (untrusted) SVG files before version 1.17.1. If you are running a newer version, you don\'t need to run this command (again).');
|
||||
if (!$io->confirm('Do you want to continue?', false)) {
|
||||
$io->success('Command aborted.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
|
||||
//Finding all attachments with svg files
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('a')
|
||||
->from(Attachment::class, 'a')
|
||||
->where('a.internal_path LIKE :pattern ESCAPE \'#\'')
|
||||
->orWhere('a.original_filename LIKE :pattern ESCAPE \'#\'')
|
||||
->setParameter('pattern', '%.svg');
|
||||
|
||||
$attachments = $qb->getQuery()->getResult();
|
||||
$io->note('Found '.count($attachments).' attachments with SVG files.');
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No SVG files found.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
$io->progressStart(count($attachments));
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$io->note('Sanitizing attachment '.$attachment->getId().' ('.($attachment->getFilename() ?? '???').')');
|
||||
$this->attachmentSubmitHandler->sanitizeSVGAttachment($attachment);
|
||||
$io->progressAdvance();
|
||||
|
||||
}
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success('Sanitization finished. All SVG files have been sanitized.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -318,6 +318,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
|
|||
return new ArrayCollection();
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
return $this->children ?? new ArrayCollection();
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class AttachmentSubmitHandler
|
|||
'htpasswd', ''];
|
||||
|
||||
public function __construct(protected AttachmentPathResolver $pathResolver, protected bool $allow_attachments_downloads,
|
||||
protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes,
|
||||
protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes, protected readonly SVGSanitizer $SVGSanitizer,
|
||||
protected FileTypeFilterTools $filterTools, /**
|
||||
* @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to
|
||||
*/
|
||||
|
@ -214,6 +214,9 @@ class AttachmentSubmitHandler
|
|||
//Move the attachment files to secure location (and back) if needed
|
||||
$this->moveFile($attachment, $secure_attachment);
|
||||
|
||||
//Sanitize the SVG if needed
|
||||
$this->sanitizeSVGAttachment($attachment);
|
||||
|
||||
//Rename blacklisted (unsecure) files to a better extension
|
||||
$this->renameBlacklistedExtensions($attachment);
|
||||
|
||||
|
@ -498,4 +501,32 @@ class AttachmentSubmitHandler
|
|||
|
||||
return $this->max_upload_size_bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG file, if the attachment is an internal SVG file.
|
||||
* @param Attachment $attachment
|
||||
* @return Attachment
|
||||
*/
|
||||
public function sanitizeSVGAttachment(Attachment $attachment): Attachment
|
||||
{
|
||||
//We can not do anything on builtins or external ressources
|
||||
if ($attachment->isBuiltIn() || !$attachment->hasInternal()) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
//Resolve the path to the file
|
||||
$path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath());
|
||||
|
||||
//Check if the file exists
|
||||
if (!file_exists($path)) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
//Check if the file is an SVG
|
||||
if ($attachment->getExtension() === "svg") {
|
||||
$this->SVGSanitizer->sanitizeFile($path);
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
}
|
||||
|
|
58
src/Services/Attachments/SVGSanitizer.php
Normal file
58
src/Services/Attachments/SVGSanitizer.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 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\Services\Attachments;
|
||||
|
||||
use Rhukster\DomSanitizer\DOMSanitizer;
|
||||
|
||||
class SVGSanitizer
|
||||
{
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG string by removing any potentially harmful content (like inline scripts).
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public function sanitizeString(string $input): string
|
||||
{
|
||||
return (new DOMSanitizer(DOMSanitizer::SVG))->sanitize($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG file by removing any potentially harmful content (like inline scripts).
|
||||
* The sanitized content is written back to the file.
|
||||
* @param string $filepath
|
||||
*/
|
||||
public function sanitizeFile(string $filepath): void
|
||||
{
|
||||
//Open the file and read the content
|
||||
$content = file_get_contents($filepath);
|
||||
if ($content === false) {
|
||||
throw new \RuntimeException('Could not read file: ' . $filepath);
|
||||
}
|
||||
//Sanitize the content
|
||||
$sanitizedContent = $this->sanitizeString($content);
|
||||
//Write the sanitized content back to the file
|
||||
file_put_contents($filepath, $sanitizedContent);
|
||||
}
|
||||
}
|
|
@ -49,8 +49,8 @@ class PollinProvider implements InfoProviderInterface
|
|||
{
|
||||
return [
|
||||
'name' => 'Pollin',
|
||||
'description' => 'Webscrapping from pollin.de to get part information',
|
||||
'url' => 'https://www.reichelt.de/',
|
||||
'description' => 'Webscraping from pollin.de to get part information',
|
||||
'url' => 'https://www.pollin.de/',
|
||||
'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1'
|
||||
];
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ class ReicheltProvider implements InfoProviderInterface
|
|||
{
|
||||
return [
|
||||
'name' => 'Reichelt',
|
||||
'description' => 'Webscrapping from reichelt.com to get part information',
|
||||
'description' => 'Webscraping from reichelt.com to get part information',
|
||||
'url' => 'https://www.reichelt.com/',
|
||||
'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1'
|
||||
];
|
||||
|
|
|
@ -63,12 +63,24 @@ final class BarcodeProvider implements PlaceholderProviderInterface
|
|||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_DATAMATRIX]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::DATAMATRIX);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C39]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE39);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C93]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE93);
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C128]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType(BarcodeType::CODE128);
|
||||
|
|
|
@ -12346,5 +12346,29 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
|
|||
<target>Visualizza la versione esterna</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="X9HUFrv" name="part.table.actions.error">
|
||||
<segment state="translated">
|
||||
<source>part.table.actions.error</source>
|
||||
<target>Si sono verificati %count% errori durante l'esecuzione dell'azione:</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id=".ppbsNn" name="part.table.actions.error_detail">
|
||||
<segment state="translated">
|
||||
<source>part.table.actions.error_detail</source>
|
||||
<target>%part_name% (ID: %part_id%): %message%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="4wpp6h." name="part_list.action.action.change_location">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.action.change_location</source>
|
||||
<target>Cambia posizione (solo per componenti con stock singolo)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="9_9I.m4" name="parts.table.action_handler.error.part_lots_multiple">
|
||||
<segment state="translated">
|
||||
<source>parts.table.action_handler.error.part_lots_multiple</source>
|
||||
<target>Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="fr">
|
||||
<file id="security.en">
|
||||
<unit id="aazoCks" name="user.login_error.user_disabled">
|
||||
<unit id="GrLNa9P" name="user.login_error.user_disabled">
|
||||
<segment state="translated">
|
||||
<source>user.login_error.user_disabled</source>
|
||||
<target>Votre compte est désactivé ! Contactez un administrateur si vous pensez que c'est une erreur.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="IFQ5XrG" name="saml.error.cannot_login_local_user_per_saml">
|
||||
<segment state="translated">
|
||||
<source>saml.error.cannot_login_local_user_per_saml</source>
|
||||
<target>Impossible de se connecter via le SSO (Single Sign On) !</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="wOYPZmb" name="saml.error.cannot_login_saml_user_locally">
|
||||
<segment state="translated">
|
||||
<source>saml.error.cannot_login_saml_user_locally</source>
|
||||
<target>Vous ne pouvez pas utiliser l'authentification par mot de passe ! Veuillez utiliser le SSO!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue