Merge branch 'master' into settings-bundle

This commit is contained in:
Jan Böhmer 2025-06-15 18:39:49 +02:00
commit 442457f11b
131 changed files with 12759 additions and 6750 deletions

View file

@ -44,35 +44,31 @@ class AttachmentManager
*
* @param Attachment $attachment The attachment for which the file should be generated
*
* @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is external or has
* @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is only external or has
* invalid file.
*/
public function attachmentToFile(Attachment $attachment): ?SplFileInfo
{
if ($attachment->isExternal() || !$this->isFileExisting($attachment)) {
if (!$this->isInternalFileExisting($attachment)) {
return null;
}
return new SplFileInfo($this->toAbsoluteFilePath($attachment));
return new SplFileInfo($this->toAbsoluteInternalFilePath($attachment));
}
/**
* Returns the absolute filepath of the attachment. Null is returned, if the attachment is externally saved,
* or is not existing.
* Returns the absolute filepath to the internal copy of the attachment. Null is returned, if the attachment is
* only externally saved, or is not existing.
*
* @param Attachment $attachment The attachment for which the filepath should be determined
*/
public function toAbsoluteFilePath(Attachment $attachment): ?string
public function toAbsoluteInternalFilePath(Attachment $attachment): ?string
{
if ($attachment->getPath() === '') {
if (!$attachment->hasInternal()){
return null;
}
if ($attachment->isExternal()) {
return null;
}
$path = $this->pathResolver->placeholderToRealPath($attachment->getPath());
$path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath());
//realpath does not work with null as argument
if (null === $path) {
@ -89,8 +85,8 @@ class AttachmentManager
}
/**
* Checks if the file in this attachement is existing. This works for files on the HDD, and for URLs
* (it's not checked if the ressource behind the URL is really existing, so for every external attachment true is returned).
* Checks if the file in this attachment is existing. This works for files on the HDD, and for URLs
* (it's not checked if the resource behind the URL is really existing, so for every external attachment true is returned).
*
* @param Attachment $attachment The attachment for which the existence should be checked
*
@ -98,15 +94,23 @@ class AttachmentManager
*/
public function isFileExisting(Attachment $attachment): bool
{
if ($attachment->getPath() === '') {
return false;
}
if ($attachment->isExternal()) {
if($attachment->hasExternal()){
return true;
}
return $this->isInternalFileExisting($attachment);
}
$absolute_path = $this->toAbsoluteFilePath($attachment);
/**
* Checks if the internal file in this attachment is existing. Returns false if the attachment doesn't have an
* internal file.
*
* @param Attachment $attachment The attachment for which the existence should be checked
*
* @return bool true if the file is existing
*/
public function isInternalFileExisting(Attachment $attachment): bool
{
$absolute_path = $this->toAbsoluteInternalFilePath($attachment);
if (null === $absolute_path) {
return false;
@ -117,21 +121,17 @@ class AttachmentManager
/**
* Returns the filesize of the attachments in bytes.
* For external attachments or not existing attachments, null is returned.
* For purely external attachments or inexistent attachments, null is returned.
*
* @param Attachment $attachment the filesize for which the filesize should be calculated
*/
public function getFileSize(Attachment $attachment): ?int
{
if ($attachment->isExternal()) {
if (!$this->isInternalFileExisting($attachment)) {
return null;
}
if (!$this->isFileExisting($attachment)) {
return null;
}
$tmp = filesize($this->toAbsoluteFilePath($attachment));
$tmp = filesize($this->toAbsoluteInternalFilePath($attachment));
return false !== $tmp ? $tmp : null;
}

View file

@ -115,12 +115,16 @@ class AttachmentPathResolver
* Converts an relative placeholder filepath (with %MEDIA% or older %BASE%) to an absolute filepath on disk.
* The directory separator is always /. Relative pathes are not realy possible (.. is striped).
*
* @param string $placeholder_path the filepath with placeholder for which the real path should be determined
* @param string|null $placeholder_path the filepath with placeholder for which the real path should be determined
*
* @return string|null The absolute real path of the file, or null if the placeholder path is invalid
*/
public function placeholderToRealPath(string $placeholder_path): ?string
public function placeholderToRealPath(?string $placeholder_path): ?string
{
if (null === $placeholder_path) {
return null;
}
//The new attachments use %MEDIA% as placeholders, which is the directory set in media_directory
//Older path entries are given via %BASE% which was the project root

View file

@ -55,7 +55,7 @@ class AttachmentReverseSearch
$repo = $this->em->getRepository(Attachment::class);
return $repo->findBy([
'path' => [$relative_path_new, $relative_path_old],
'internal_path' => [$relative_path_new, $relative_path_old],
]);
}

View file

@ -71,6 +71,7 @@ class AttachmentSubmitHandler
protected MimeTypesInterface $mimeTypes,
protected FileTypeFilterTools $filterTools,
protected AttachmentsSettings $settings,
protected readonly SVGSanitizer $SVGSanitizer,
)
{
//The mapping used to determine which folder will be used for an attachment type
@ -209,13 +210,16 @@ class AttachmentSubmitHandler
if ($file instanceof UploadedFile) {
$this->upload($attachment, $file, $secure_attachment);
} elseif ($upload->downloadUrl && $attachment->isExternal()) {
} elseif ($upload->downloadUrl && $attachment->hasExternal()) {
$this->downloadURL($attachment, $secure_attachment);
}
//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);
@ -246,12 +250,12 @@ class AttachmentSubmitHandler
protected function renameBlacklistedExtensions(Attachment $attachment): Attachment
{
//We can not do anything on builtins or external ressources
if ($attachment->isBuiltIn() || $attachment->isExternal()) {
if ($attachment->isBuiltIn() || !$attachment->hasInternal()) {
return $attachment;
}
//Determine the old filepath
$old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath());
$old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath());
if ($old_path === null || $old_path === '' || !file_exists($old_path)) {
return $attachment;
}
@ -269,7 +273,7 @@ class AttachmentSubmitHandler
$fs->rename($old_path, $new_path);
//Update the attachment
$attachment->setPath($this->pathResolver->realPathToPlaceholder($new_path));
$attachment->setInternalPath($this->pathResolver->realPathToPlaceholder($new_path));
}
@ -277,17 +281,17 @@ class AttachmentSubmitHandler
}
/**
* Move the given attachment to secure location (or back to public folder) if needed.
* Move the internal copy of the given attachment to a secure location (or back to public folder) if needed.
*
* @param Attachment $attachment the attachment for which the file should be moved
* @param bool $secure_location this value determines, if the attachment is moved to the secure or public folder
*
* @return Attachment The attachment with the updated filepath
* @return Attachment The attachment with the updated internal filepath
*/
protected function moveFile(Attachment $attachment, bool $secure_location): Attachment
{
//We can not do anything on builtins or external ressources
if ($attachment->isBuiltIn() || $attachment->isExternal()) {
if ($attachment->isBuiltIn() || !$attachment->hasInternal()) {
return $attachment;
}
@ -297,7 +301,7 @@ class AttachmentSubmitHandler
}
//Determine the old filepath
$old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath());
$old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath());
if (!file_exists($old_path)) {
return $attachment;
}
@ -321,7 +325,7 @@ class AttachmentSubmitHandler
//Save info to attachment entity
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
$attachment->setPath($new_path);
$attachment->setInternalPath($new_path);
return $attachment;
}
@ -331,7 +335,7 @@ class AttachmentSubmitHandler
*
* @param bool $secureAttachment True if the file should be moved to the secure attachment storage
*
* @return Attachment The attachment with the new filepath
* @return Attachment The attachment with the downloaded copy
*/
protected function downloadURL(Attachment $attachment, bool $secureAttachment): Attachment
{
@ -340,16 +344,35 @@ class AttachmentSubmitHandler
throw new RuntimeException('Download of attachments is not allowed!');
}
$url = $attachment->getURL();
$url = $attachment->getExternalPath();
$fs = new Filesystem();
$attachment_folder = $this->generateAttachmentPath($attachment, $secureAttachment);
$tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp');
try {
$response = $this->httpClient->request('GET', $url, [
$opts = [
'buffer' => false,
]);
//Use user-agent and other headers to make the server think we are a browser
'headers' => [
"sec-ch-ua" => "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
"sec-ch-ua-mobile" => "?0",
"sec-ch-ua-platform" => "\"Windows\"",
"user-agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
"sec-fetch-site" => "none",
"sec-fetch-mode" => "navigate",
],
];
$response = $this->httpClient->request('GET', $url, $opts);
//Digikey wants TLSv1.3, so try again with that if we get a 403
if ($response->getStatusCode() === 403) {
$opts['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
$response = $this->httpClient->request('GET', $url, $opts);
}
# if you have these changes and downloads still fail, check if it's due to an unknown certificate. Curl by
# default uses the systems ca store and that doesn't contain all the intermediate certificates needed to
# verify the leafs
if (200 !== $response->getStatusCode()) {
throw new AttachmentDownloadException('Status code: '.$response->getStatusCode());
@ -401,7 +424,7 @@ class AttachmentSubmitHandler
//Make our file path relative to %BASE%
$new_path = $this->pathResolver->realPathToPlaceholder($new_path);
//Save the path to the attachment
$attachment->setPath($new_path);
$attachment->setInternalPath($new_path);
} catch (TransportExceptionInterface) {
throw new AttachmentDownloadException('Transport error!');
}
@ -429,7 +452,9 @@ class AttachmentSubmitHandler
//Make our file path relative to %BASE%
$file_path = $this->pathResolver->realPathToPlaceholder($file_path);
//Save the path to the attachment
$attachment->setPath($file_path);
$attachment->setInternalPath($file_path);
//reset any external paths the attachment might have had
$attachment->setExternalPath(null);
//And save original filename
$attachment->setFilename($file->getClientOriginalName());
@ -479,4 +504,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;
}
}

View file

@ -92,9 +92,9 @@ class AttachmentURLGenerator
* Returns a URL under which the attachment file can be viewed.
* @return string|null The URL or null if the attachment file is not existing
*/
public function getViewURL(Attachment $attachment): ?string
public function getInternalViewURL(Attachment $attachment): ?string
{
$absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment);
$absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment);
if (null === $absolute_path) {
return null;
}
@ -111,6 +111,7 @@ 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
*/
public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string
@ -119,11 +120,14 @@ class AttachmentURLGenerator
throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!');
}
if ($attachment->isExternal() && ($attachment->getURL() !== null && $attachment->getURL() !== '')) {
return $attachment->getURL();
if (!$attachment->hasInternal()){
if($attachment->hasExternal()) {
return $attachment->getExternalPath();
}
return null;
}
$absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment);
$absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment);
if (null === $absolute_path) {
return null;
}
@ -137,7 +141,7 @@ class AttachmentURLGenerator
//GD can not work with SVG, so serve it directly...
//We can not use getExtension here, because it uses the original filename and not the real extension
//Instead we use the logic, which is also used to determine if the attachment is a picture
$extension = pathinfo(parse_url($attachment->getPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
$extension = pathinfo(parse_url($attachment->getInternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
if ('svg' === $extension) {
return $this->assets->getUrl($asset_path);
}
@ -157,7 +161,7 @@ class AttachmentURLGenerator
/**
* Returns a download link to the file associated with the attachment.
*/
public function getDownloadURL(Attachment $attachment): string
public function getInternalDownloadURL(Attachment $attachment): string
{
//Redirect always to download controller, which sets the correct headers for downloading:
return $this->urlGenerator->generate('attachment_download', ['id' => $attachment->getID()]);

View 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);
}
}