mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 05:39:33 +00:00
Merge branch 'Part-DB:master' into Buerklin-provider
This commit is contained in:
commit
739237b770
40 changed files with 929 additions and 530 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.15.2
|
||||
1.16.0
|
||||
|
|
|
|||
11
composer.lock
generated
11
composer.lock
generated
|
|
@ -16590,12 +16590,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||
"reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c"
|
||||
"reference": "23b2141a1db97b4e3278510ed9e74a16361619b1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/70eb886a27427421cf1bd612067810c9fb1cbb5c",
|
||||
"reference": "70eb886a27427421cf1bd612067810c9fb1cbb5c",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/23b2141a1db97b4e3278510ed9e74a16361619b1",
|
||||
"reference": "23b2141a1db97b4e3278510ed9e74a16361619b1",
|
||||
"shasum": ""
|
||||
},
|
||||
"conflict": {
|
||||
|
|
@ -16934,6 +16934,7 @@
|
|||
"league/commonmark": "<2.6",
|
||||
"league/flysystem": "<1.1.4|>=2,<2.1.1",
|
||||
"league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3",
|
||||
"leantime/leantime": "<3.3",
|
||||
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
|
||||
"libreform/libreform": ">=2,<=2.0.8",
|
||||
"librenms/librenms": "<2017.08.18",
|
||||
|
|
@ -16958,7 +16959,7 @@
|
|||
"mantisbt/mantisbt": "<=2.26.3",
|
||||
"marcwillmann/turn": "<0.3.3",
|
||||
"matyhtf/framework": "<3.0.6",
|
||||
"mautic/core": "<4.4.13|>=5,<5.1.1",
|
||||
"mautic/core": "<4.4.13|>=5.0.0.0-alpha,<5.1.1",
|
||||
"mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1",
|
||||
"maximebf/debugbar": "<1.19",
|
||||
"mdanter/ecc": "<2",
|
||||
|
|
@ -17457,7 +17458,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-18T20:05:22+00:00"
|
||||
"time": "2025-02-21T23:05:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
|
|
|||
|
|
@ -34,4 +34,8 @@ api_platform:
|
|||
|
||||
keep_legacy_inflector: false
|
||||
# Need to be true, or some tests will fail
|
||||
use_symfony_listeners: true
|
||||
use_symfony_listeners: true
|
||||
|
||||
serializer:
|
||||
# Change this to false later, to remove the hydra prefix on the API
|
||||
hydra_prefix: true
|
||||
83
migrations/Version20250220215048.php
Normal file
83
migrations/Version20250220215048.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
final class Version20250220215048 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Split $path property for attachments into $internal_path and $external_path';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
//Create the new columns as nullable (that is easier modifying them)
|
||||
$this->addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL, ADD external_path VARCHAR(255) DEFAULT NULL');
|
||||
|
||||
//Copy the data from path to external_path and remove the path column
|
||||
$this->addSql('UPDATE attachments SET external_path=path');
|
||||
$this->addSql('ALTER TABLE attachments DROP path');
|
||||
|
||||
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL');
|
||||
$this->addSql('ALTER TABLE attachments DROP internal_path');
|
||||
$this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path');
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
//We can use the same SQL for PostgreSQL as for MySQL
|
||||
$this->mySQLUp($schema);
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
//We can use the same SQL for PostgreSQL as for MySQL
|
||||
$this->mySQLDown($schema);
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__attachments AS SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM attachments');
|
||||
$this->addSql('DROP TABLE attachments');
|
||||
$this->addSql('CREATE TABLE attachments (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type_id INTEGER NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, show_in_table BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INTEGER NOT NULL, internal_path VARCHAR(255) DEFAULT NULL, external_path VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES attachment_types (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO attachments (id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, external_path) SELECT id, type_id, original_filename, show_in_table, name, last_modified, datetime_added, class_name, element_id, path FROM __temp__attachments');
|
||||
$this->addSql('DROP TABLE __temp__attachments');
|
||||
$this->addSql('CREATE INDEX attachment_element_idx ON attachments (class_name, element_id)');
|
||||
$this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)');
|
||||
$this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)');
|
||||
$this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)');
|
||||
$this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON attachments (type_id)');
|
||||
$this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON attachments (element_id)');
|
||||
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
//Reuse the MySQL down migration:
|
||||
$this->mySQLDown($schema);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
31
migrations/Version20250222165240.php
Normal file
31
migrations/Version20250222165240.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250222165240 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrate the old attachment class discriminator values from legacy Part-DB to the modern format, so that there is just one unified value';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
//Change the old discriminator values to the new ones
|
||||
$this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'");
|
||||
$this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
//No down required, as the new format can also be read by older Part-DB version
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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\ApiPlatform;
|
||||
|
||||
use ApiPlatform\JsonSchema\Schema;
|
||||
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the properties given by DocumentedAPIProperty attributes on the classes to the schema.
|
||||
*/
|
||||
#[AsDecorator('api_platform.json_schema.schema_factory')]
|
||||
class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterface
|
||||
{
|
||||
|
||||
public function __construct(private readonly SchemaFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildSchema(
|
||||
string $className,
|
||||
string $format = 'json',
|
||||
string $type = Schema::TYPE_OUTPUT,
|
||||
?Operation $operation = null,
|
||||
?Schema $schema = null,
|
||||
?array $serializerContext = null,
|
||||
bool $forceCollection = false
|
||||
): Schema {
|
||||
|
||||
|
||||
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
|
||||
|
||||
//Check if there is are DocumentedAPIProperty attributes on the class
|
||||
$reflectionClass = new \ReflectionClass($className);
|
||||
$attributes = $reflectionClass->getAttributes(DocumentedAPIProperty::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var DocumentedAPIProperty $api_property */
|
||||
$api_property = $attribute->newInstance();
|
||||
$this->addPropertyToSchema($schema, $api_property->schemaName, $api_property->property,
|
||||
$api_property, $serializerContext ?? [], $format);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function addPropertyToSchema(Schema $schema, string $definitionName, string $normalizedPropertyName, DocumentedAPIProperty $propertyMetadata, array $serializerContext, string $format): void
|
||||
{
|
||||
$version = $schema->getVersion();
|
||||
$swagger = Schema::VERSION_SWAGGER === $version;
|
||||
|
||||
$propertySchema = [];
|
||||
|
||||
if (false === $propertyMetadata->writeable) {
|
||||
$propertySchema['readOnly'] = true;
|
||||
}
|
||||
if (!$swagger && false === $propertyMetadata->readable) {
|
||||
$propertySchema['writeOnly'] = true;
|
||||
}
|
||||
if (null !== $description = $propertyMetadata->description) {
|
||||
$propertySchema['description'] = $description;
|
||||
}
|
||||
|
||||
$deprecationReason = $propertyMetadata->deprecationReason;
|
||||
|
||||
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
||||
if (!$swagger && null !== $deprecationReason) {
|
||||
$propertySchema['deprecated'] = true;
|
||||
}
|
||||
|
||||
if (!empty($default = $propertyMetadata->default)) {
|
||||
if ($default instanceof \BackedEnum) {
|
||||
$default = $default->value;
|
||||
}
|
||||
$propertySchema['default'] = $default;
|
||||
}
|
||||
|
||||
if (!empty($example = $propertyMetadata->example)) {
|
||||
$propertySchema['example'] = $example;
|
||||
}
|
||||
|
||||
if (!isset($propertySchema['example']) && isset($propertySchema['default'])) {
|
||||
$propertySchema['example'] = $propertySchema['default'];
|
||||
}
|
||||
|
||||
$propertySchema['type'] = $propertyMetadata->type;
|
||||
$propertySchema['nullable'] = $propertyMetadata->nullable;
|
||||
|
||||
$propertySchema = new \ArrayObject($propertySchema);
|
||||
|
||||
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -21,7 +21,9 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
|
||||
/**
|
||||
* When this attribute is applied to a class, an property will be added to the API documentation using the given parameters.
|
||||
|
|
@ -64,4 +66,55 @@ final class DocumentedAPIProperty
|
|||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAPIProperty(bool $use_swagger = false): ApiProperty
|
||||
{
|
||||
$openApiContext = [];
|
||||
|
||||
if (false === $this->writeable) {
|
||||
$openApiContext['readOnly'] = true;
|
||||
}
|
||||
if (!$use_swagger && false === $this->readable) {
|
||||
$openApiContext['writeOnly'] = true;
|
||||
}
|
||||
if (null !== $description = $this->description) {
|
||||
$openApiContext['description'] = $description;
|
||||
}
|
||||
|
||||
$deprecationReason = $this->deprecationReason;
|
||||
|
||||
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
||||
if (!$use_swagger && null !== $deprecationReason) {
|
||||
$openApiContext['deprecated'] = true;
|
||||
}
|
||||
|
||||
if (!empty($default = $this->default)) {
|
||||
if ($default instanceof \BackedEnum) {
|
||||
$default = $default->value;
|
||||
}
|
||||
$openApiContext['default'] = $default;
|
||||
}
|
||||
|
||||
if (!empty($example = $this->example)) {
|
||||
$openApiContext['example'] = $example;
|
||||
}
|
||||
|
||||
if (!isset($openApiContext['example']) && isset($openApiContext['default'])) {
|
||||
$openApiContext['example'] = $openApiContext['default'];
|
||||
}
|
||||
|
||||
$openApiContext['type'] = $this->type;
|
||||
$openApiContext['nullable'] = $this->nullable;
|
||||
|
||||
|
||||
|
||||
return new ApiProperty(
|
||||
description: $this->description,
|
||||
readable: $this->readable,
|
||||
writable: $this->writeable,
|
||||
openapiContext: $openApiContext,
|
||||
types: $this->type,
|
||||
property: $this->property
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<?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\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata
|
||||
* which then get picked up by the openapi schema generator
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.metadata_factory')]
|
||||
class PropertyMetadataFactory implements PropertyMetadataFactoryInterface
|
||||
{
|
||||
public function __construct(private PropertyMetadataFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
|
||||
{
|
||||
$metadata = $this->decorated->create($resourceClass, $property, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (!class_exists($resourceClass)) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$refClass = new ReflectionClass($resourceClass);
|
||||
$attributes = $refClass->getAttributes(DocumentedAPIProperty::class);
|
||||
|
||||
//Look for the DocumentedAPIProperty attribute with the given property name
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var DocumentedAPIProperty $api_property */
|
||||
$api_property = $attribute->newInstance();
|
||||
//If attribute not matches the property name, skip it
|
||||
if ($api_property->property !== $property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Return the virtual property
|
||||
return $api_property->toAPIProperty();
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?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\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
||||
use ApiPlatform\Metadata\Property\PropertyNameCollection;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection
|
||||
* which then get picked up by the openapi schema generator
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
|
||||
class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
|
||||
{
|
||||
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, array $options = []): PropertyNameCollection
|
||||
{
|
||||
// Get the default properties from the decorated service
|
||||
$propertyNames = $this->decorated->create($resourceClass, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
if (!class_exists($resourceClass)) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
$properties = iterator_to_array($propertyNames);
|
||||
|
||||
$refClass = new ReflectionClass($resourceClass);
|
||||
|
||||
foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) {
|
||||
/** @var DocumentedAPIProperty $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$properties[] = $instance->property;
|
||||
}
|
||||
|
||||
return new PropertyNameCollection($properties);
|
||||
}
|
||||
}
|
||||
|
|
@ -92,12 +92,6 @@ class EntityFilterHelper
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
|
|||
|
|
@ -67,12 +67,6 @@ final class LikeFilter extends AbstractFilter
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
|
|||
|
|
@ -89,12 +89,6 @@ final class TagFilter extends AbstractFilter
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter for tags of a part',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
|
|||
77
src/ApiPlatform/NormalizePropertyNameCollectionFactory.php
Normal file
77
src/ApiPlatform/NormalizePropertyNameCollectionFactory.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?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\ApiPlatform;
|
||||
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
||||
use ApiPlatform\Metadata\Property\PropertyNameCollection;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* This decorator removes all camelCase property names from the property name collection, if a snake_case version exists.
|
||||
* This is a fix for https://github.com/Part-DB/Part-DB-server/issues/862, as the openapi schema generator wrongly collects
|
||||
* both camelCase and snake_case property names, which leads to duplicate properties in the schema.
|
||||
* This seems to come from the fact that the openapi schema generator uses no serializerContext, which seems then to collect
|
||||
* the getters too...
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
|
||||
class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
|
||||
{
|
||||
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, array $options = []): PropertyNameCollection
|
||||
{
|
||||
// Get the default properties from the decorated service
|
||||
$propertyNames = $this->decorated->create($resourceClass, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
//If we are not in the jsonapi generator (which sets no serializer groups), return the property names as is
|
||||
if (isset($options['serializer_groups'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
//Remove all camelCase property names from the collection, if a snake_case version exists
|
||||
$properties = iterator_to_array($propertyNames);
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (str_contains($property, '_')) {
|
||||
$camelized = u($property)->camel()->toString();
|
||||
|
||||
//If the camelized version exists, remove it from the collection
|
||||
$index = array_search($camelized, $properties, true);
|
||||
if ($index !== false) {
|
||||
unset($properties[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PropertyNameCollection($properties);
|
||||
}
|
||||
}
|
||||
|
|
@ -51,15 +51,15 @@ class AttachmentFileController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('show_private', $attachment);
|
||||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
if (!$attachment->hasInternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
if (!$helper->isInternalFileExisting($attachment)) {
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||
$response = new BinaryFileResponse($file_path);
|
||||
|
||||
//Set header content disposition, so that the file will be downloaded
|
||||
|
|
@ -80,15 +80,15 @@ class AttachmentFileController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('show_private', $attachment);
|
||||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
if (!$attachment->hasInternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
if (!$helper->isInternalFileExisting($attachment)) {
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||
$response = new BinaryFileResponse($file_path);
|
||||
|
||||
//Set header content disposition, so that the file will be downloaded
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Test2');
|
||||
$attachment->setPath('invalid');
|
||||
$attachment->setInternalPath('invalid');
|
||||
$attachment->setShowInTable(true);
|
||||
$attachment->setAttachmentType($manager->find(AttachmentType::class, 1));
|
||||
$part->addAttachment($attachment);
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
{
|
||||
$dataTable->add('dont_matter', RowClassColumn::class, [
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
//Mark attachments with missing files yellow
|
||||
if(!$this->attachmentHelper->isFileExisting($context)){
|
||||
//Mark attachments yellow which have an internal file linked that doesn't exist
|
||||
if($context->hasInternal() && !$this->attachmentHelper->isInternalFileExisting($context)){
|
||||
return 'table-warning';
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +64,8 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
'className' => 'no-colvis',
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
if ($context->isPicture()
|
||||
&& !$context->isExternal()
|
||||
&& $this->attachmentHelper->isFileExisting($context)) {
|
||||
&& $this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
|
||||
$title = htmlspecialchars($context->getName());
|
||||
if ($context->getFilename()) {
|
||||
$title .= ' ('.htmlspecialchars($context->getFilename()).')';
|
||||
|
|
@ -93,26 +93,6 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
$dataTable->add('name', TextColumn::class, [
|
||||
'label' => 'attachment.edit.name',
|
||||
'orderField' => 'NATSORT(attachment.name)',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
//Link to external source
|
||||
if ($context->isExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external">%s</a>',
|
||||
htmlspecialchars((string) $context->getURL()),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->attachmentHelper->isFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||
$this->entityURLGenerator->viewURL($context),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('attachment_type', TextColumn::class, [
|
||||
|
|
@ -136,25 +116,60 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
),
|
||||
]);
|
||||
|
||||
$dataTable->add('filename', TextColumn::class, [
|
||||
'label' => $this->translator->trans('attachment.table.filename'),
|
||||
$dataTable->add('internal_link', TextColumn::class, [
|
||||
'label' => 'attachment.table.internal_file',
|
||||
'propertyPath' => 'filename',
|
||||
'orderField' => 'NATSORT(attachment.original_filename)',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||
$this->entityURLGenerator->viewURL($context),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
]);
|
||||
|
||||
$dataTable->add('external_link', TextColumn::class, [
|
||||
'label' => 'attachment.table.external_link',
|
||||
'propertyPath' => 'host',
|
||||
'orderField' => 'attachment.external_path',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($context->hasExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars($value),
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
]);
|
||||
|
||||
$dataTable->add('filesize', TextColumn::class, [
|
||||
'label' => $this->translator->trans('attachment.table.filesize'),
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($context->isExternal()) {
|
||||
if (!$context->hasInternal()) {
|
||||
return sprintf(
|
||||
'<span class="badge bg-primary">
|
||||
<i class="fas fa-globe fa-fw"></i>%s
|
||||
</span>',
|
||||
$this->translator->trans('attachment.external')
|
||||
$this->translator->trans('attachment.external_only')
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->attachmentHelper->isFileExisting($context)) {
|
||||
return $this->attachmentHelper->getHumanFileSize($context);
|
||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<span class="badge bg-secondary">
|
||||
<i class="fas fa-hdd fa-fw"></i> %s
|
||||
</span>',
|
||||
$this->attachmentHelper->getHumanFileSize($context)
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class AttachmentFilter implements FilterInterface
|
|||
public readonly DateTimeConstraint $lastModified;
|
||||
public readonly DateTimeConstraint $addedDate;
|
||||
|
||||
public readonly TextConstraint $originalFileName;
|
||||
public readonly TextConstraint $externalLink;
|
||||
|
||||
|
||||
public function __construct(NodesListBuilder $nodesListBuilder)
|
||||
{
|
||||
|
|
@ -55,6 +58,9 @@ class AttachmentFilter implements FilterInterface
|
|||
$this->lastModified = new DateTimeConstraint('attachment.lastModified');
|
||||
$this->addedDate = new DateTimeConstraint('attachment.addedDate');
|
||||
$this->showInTable = new BooleanConstraint('attachment.show_in_table');
|
||||
$this->originalFileName = new TextConstraint('attachment.original_filename');
|
||||
$this->externalLink = new TextConstraint('attachment.external_path');
|
||||
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
|
|
|
|||
|
|
@ -33,23 +33,24 @@ use ApiPlatform\Metadata\Get;
|
|||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\ApiPlatform\DocumentedAPIProperty;
|
||||
use App\ApiPlatform\DocumentedAPIProperties\DocumentedAPIProperty;
|
||||
use App\ApiPlatform\Filter\EntityFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\ApiPlatform\HandleAttachmentsUploadsProcessor;
|
||||
use App\Repository\AttachmentRepository;
|
||||
use App\EntityListeners\AttachmentDeleteListener;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\EntityListeners\AttachmentDeleteListener;
|
||||
use App\Repository\AttachmentRepository;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
use function in_array;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Class Attachment.
|
||||
|
|
@ -78,11 +79,16 @@ use LogicException;
|
|||
denormalizationContext: ['groups' => ['attachment:write', 'attachment:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
processor: HandleAttachmentsUploadsProcessor::class,
|
||||
)]
|
||||
#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'media_url', type: 'string', nullable: true,
|
||||
description: 'The URL to the file, where the attachment file can be downloaded. This can be an internal or external URL.',
|
||||
example: '/media/part/2/bc547-6508afa5a79c8.pdf')]
|
||||
#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true,
|
||||
description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.')]
|
||||
//This property is added by the denormalizer in order to resolve the placeholder
|
||||
#[DocumentedAPIProperty(
|
||||
schemaName: 'Attachment-Read', property: 'internal_path', type: 'string', nullable: false,
|
||||
description: 'The URL to the internally saved copy of the file, if one exists',
|
||||
example: '/media/part/2/bc547-6508afa5a79c8.pdf'
|
||||
)]
|
||||
#[DocumentedAPIProperty(
|
||||
schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true,
|
||||
description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.'
|
||||
)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name"])]
|
||||
#[ApiFilter(EntityFilter::class, properties: ["attachment_type"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
|
|
@ -91,8 +97,8 @@ use LogicException;
|
|||
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
|
||||
abstract class Attachment extends AbstractNamedDBElement
|
||||
{
|
||||
private const ORM_DISCRIMINATOR_MAP = ['PartDB\Part' => PartAttachment::class, 'Part' => PartAttachment::class,
|
||||
'PartDB\Device' => ProjectAttachment::class, 'Device' => ProjectAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class,
|
||||
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class,
|
||||
'AttachmentType' => AttachmentTypeAttachment::class,
|
||||
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
|
||||
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
|
||||
'Storelocation' => StorageLocationAttachment::class, 'Supplier' => SupplierAttachment::class,
|
||||
|
|
@ -119,10 +125,6 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
*/
|
||||
final public const MODEL_EXTS = ['x3d'];
|
||||
|
||||
/**
|
||||
* When the path begins with one of the placeholders.
|
||||
*/
|
||||
final public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%'];
|
||||
|
||||
/**
|
||||
* @var array placeholders for attachments which using built in files
|
||||
|
|
@ -152,10 +154,21 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
protected ?string $original_filename = null;
|
||||
|
||||
/**
|
||||
* @var string The path to the file relative to a placeholder path like %MEDIA%
|
||||
* @var string|null If a copy of the file is stored internally, the path to the file relative to a placeholder
|
||||
* path like %MEDIA%
|
||||
*/
|
||||
#[ORM\Column(name: 'path', type: Types::STRING)]
|
||||
protected string $path = '';
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
protected ?string $internal_path = null;
|
||||
|
||||
|
||||
/**
|
||||
* @var string|null The path to the external source if the file is stored externally or was downloaded from an
|
||||
* external source. Null if there is no external source.
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['attachment:read'])]
|
||||
#[ApiProperty(example: 'http://example.com/image.jpg')]
|
||||
protected ?string $external_path = null;
|
||||
|
||||
/**
|
||||
* @var string the name of this element
|
||||
|
|
@ -237,7 +250,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
|
||||
/**
|
||||
* Check if this attachment is a picture (analyse the file's extension).
|
||||
* If the link is external, it is assumed that this is true.
|
||||
* If the link is only external and doesn't contain an extension, it is assumed that this is true.
|
||||
*
|
||||
* @return bool * true if the file extension is a picture extension
|
||||
* * otherwise false
|
||||
|
|
@ -245,54 +258,67 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[Groups(['attachment:read'])]
|
||||
public function isPicture(): bool
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if($this->hasInternal()){
|
||||
|
||||
$extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
|
||||
}
|
||||
if ($this->hasExternal()) {
|
||||
//Check if we can extract a file extension from the URL
|
||||
$extension = pathinfo(parse_url($this->path, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
|
||||
$extension = pathinfo(parse_url($this->getExternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
|
||||
|
||||
//If no extension is found or it is known picture extension, we assume that this is a picture extension
|
||||
return $extension === '' || in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
}
|
||||
|
||||
$extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
//File doesn't have an internal, nor an external copy. This shouldn't happen, but it certainly isn't a picture...
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this attachment is a 3D model and therefore can be directly shown to user.
|
||||
* If the attachment is external, false is returned (3D Models must be internal).
|
||||
* If no internal copy exists, false is returned (3D Models must be internal).
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
#[SerializedName('3d_model')]
|
||||
public function is3DModel(): bool
|
||||
{
|
||||
//We just assume that 3D Models are internally saved, otherwise we get problems loading them.
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
|
||||
$extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::MODEL_EXTS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the attachment file is externally saved (the database saves an URL).
|
||||
* Checks if this attachment has a path to an external file
|
||||
*
|
||||
* @return bool true, if the file is saved externally
|
||||
* @return bool true, if there is a path to an external file
|
||||
* @phpstan-assert-if-true non-empty-string $this->external_path
|
||||
* @phpstan-assert-if-true non-empty-string $this->getExternalPath())
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
public function isExternal(): bool
|
||||
public function hasExternal(): bool
|
||||
{
|
||||
//When path is empty, this attachment can not be external
|
||||
if ($this->path === '') {
|
||||
return false;
|
||||
}
|
||||
return $this->external_path !== null && $this->external_path !== '';
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $this->path);
|
||||
|
||||
return !in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), true);
|
||||
/**
|
||||
* Checks if this attachment has a path to an internal file.
|
||||
* Does not check if the file exists.
|
||||
*
|
||||
* @return bool true, if there is a path to an internal file
|
||||
* @phpstan-assert-if-true non-empty-string $this->internal_path
|
||||
* @phpstan-assert-if-true non-empty-string $this->getInternalPath())
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
public function hasInternal(): bool
|
||||
{
|
||||
return $this->internal_path !== null && $this->internal_path !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -305,8 +331,12 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[SerializedName('private')]
|
||||
public function isSecure(): bool
|
||||
{
|
||||
if ($this->internal_path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $this->path);
|
||||
$tmp = explode('/', $this->internal_path);
|
||||
|
||||
return '%SECURE%' === $tmp[0];
|
||||
}
|
||||
|
|
@ -320,7 +350,11 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[Groups(['attachment:read'])]
|
||||
public function isBuiltIn(): bool
|
||||
{
|
||||
return static::checkIfBuiltin($this->path);
|
||||
if ($this->internal_path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return static::checkIfBuiltin($this->internal_path);
|
||||
}
|
||||
|
||||
/********************************************************************************
|
||||
|
|
@ -332,13 +366,13 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/**
|
||||
* Returns the extension of the file referenced via the attachment.
|
||||
* For a path like %BASE/path/foo.bar, bar will be returned.
|
||||
* If this attachment is external null is returned.
|
||||
* If this attachment is only external null is returned.
|
||||
*
|
||||
* @return string|null the file extension in lower case
|
||||
*/
|
||||
public function getExtension(): ?string
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +380,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
|
||||
return strtolower(pathinfo($this->getInternalPath(), PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -361,52 +395,54 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* The URL to the external file, or the path to the built-in file.
|
||||
* The URL to the external file, or the path to the built-in file, but not paths to uploaded files.
|
||||
* Returns null, if the file is not external (and not builtin).
|
||||
* The output of this function is such, that no changes occur when it is fed back into setURL().
|
||||
* Required for the Attachment form field.
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
#[SerializedName('url')]
|
||||
public function getURL(): ?string
|
||||
{
|
||||
if (!$this->isExternal() && !$this->isBuiltIn()) {
|
||||
return null;
|
||||
if($this->hasExternal()){
|
||||
return $this->getExternalPath();
|
||||
}
|
||||
|
||||
return $this->path;
|
||||
if($this->isBuiltIn()){
|
||||
return $this->getInternalPath();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hostname where the external file is stored.
|
||||
* Returns null, if the file is not external.
|
||||
* Returns null, if there is no external path.
|
||||
*/
|
||||
public function getHost(): ?string
|
||||
{
|
||||
if (!$this->isExternal()) {
|
||||
if (!$this->hasExternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parse_url((string) $this->getURL(), PHP_URL_HOST);
|
||||
return parse_url($this->getExternalPath(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filepath, relative to %BASE%.
|
||||
*
|
||||
* @return string A string like %BASE/path/foo.bar
|
||||
*/
|
||||
public function getPath(): string
|
||||
public function getInternalPath(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
return $this->internal_path;
|
||||
}
|
||||
|
||||
public function getExternalPath(): ?string
|
||||
{
|
||||
return $this->external_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename of the attachment.
|
||||
* For a path like %BASE/path/foo.bar, foo.bar will be returned.
|
||||
*
|
||||
* If the path is a URL (can be checked via isExternal()), null will be returned.
|
||||
* If there is no internal copy of the file, null will be returned.
|
||||
*/
|
||||
public function getFilename(): ?string
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +451,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
return $this->original_filename;
|
||||
}
|
||||
|
||||
return pathinfo($this->getPath(), PATHINFO_BASENAME);
|
||||
return pathinfo($this->getInternalPath(), PATHINFO_BASENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -488,15 +524,12 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the filepath (with relative placeholder) for this attachment.
|
||||
*
|
||||
* @param string $path the new filepath of the attachment
|
||||
*
|
||||
* @return Attachment
|
||||
* Sets the path to a file hosted internally. If you set this path to a file that was not downloaded from the
|
||||
* external source in external_path, make sure to reset external_path.
|
||||
*/
|
||||
public function setPath(string $path): self
|
||||
public function setInternalPath(?string $internal_path): self
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->internal_path = $internal_path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -512,34 +545,60 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the url associated with this attachment.
|
||||
* If the url is empty nothing is changed, to not override the file path.
|
||||
*
|
||||
* @return Attachment
|
||||
* Sets up the paths using a user provided string which might contain an external path or a builtin path. Allows
|
||||
* resetting the external path if an internal path exists. Resets any other paths if a (nonempty) new path is set.
|
||||
*/
|
||||
#[Groups(['attachment:write'])]
|
||||
#[SerializedName('url')]
|
||||
#[ApiProperty(description: 'Set the path of the attachment here.
|
||||
Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty
|
||||
string if the attachment has an internal file associated and you\'d like to reset the external source.
|
||||
If you set a new (nonempty) file path any associated internal file will be removed!')]
|
||||
public function setURL(?string $url): self
|
||||
{
|
||||
//Do nothing if the URL is empty
|
||||
if ($url === null || $url === '') {
|
||||
//Don't allow the user to set an empty external path if the internal path is empty already
|
||||
if (($url === null || $url === "") && !$this->hasInternal()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$url = trim($url);
|
||||
//Escape spaces in URL
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
//Only set if the URL is not empty
|
||||
if ($url !== '') {
|
||||
if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) {
|
||||
throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
|
||||
}
|
||||
|
||||
$this->path = $url;
|
||||
//Reset internal filename
|
||||
$this->original_filename = null;
|
||||
//The URL field can also contain the special builtin internal paths, so we need to distinguish here
|
||||
if ($this::checkIfBuiltin($url)) {
|
||||
$this->setInternalPath($url);
|
||||
//make sure the external path isn't still pointing to something unrelated
|
||||
$this->setExternalPath(null);
|
||||
} else {
|
||||
$this->setExternalPath($url);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the path to a file hosted on an external server. Setting the external path to a (nonempty) value different
|
||||
* from the the old one _clears_ the internal path, so that the external path reflects where any associated internal
|
||||
* file came from.
|
||||
*/
|
||||
public function setExternalPath(?string $external_path): self
|
||||
{
|
||||
//If we only clear the external path, don't reset the internal path, since that could be confusing
|
||||
if($external_path === null || $external_path === '') {
|
||||
$this->external_path = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$external_path = trim($external_path);
|
||||
//Escape spaces in URL
|
||||
$external_path = str_replace(' ', '%20', $external_path);
|
||||
|
||||
if($this->external_path === $external_path) {
|
||||
//Nothing changed, nothing to do
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->external_path = $external_path;
|
||||
$this->internal_path = null;
|
||||
//Reset internal filename
|
||||
$this->original_filename = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -551,12 +610,17 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/**
|
||||
* Checks if the given path is a path to a builtin resource.
|
||||
*
|
||||
* @param string $path The path that should be checked
|
||||
* @param string|null $path The path that should be checked
|
||||
*
|
||||
* @return bool true if the path is pointing to a builtin resource
|
||||
*/
|
||||
public static function checkIfBuiltin(string $path): bool
|
||||
public static function checkIfBuiltin(?string $path): bool
|
||||
{
|
||||
//An empty path can't be a builtin
|
||||
if ($path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $path);
|
||||
//Builtins must have a %PLACEHOLDER% construction
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ class AttachmentDeleteListener
|
|||
#[PreUpdate]
|
||||
public function preUpdateHandler(Attachment $attachment, PreUpdateEventArgs $event): void
|
||||
{
|
||||
if ($event->hasChangedField('path')) {
|
||||
$old_path = $event->getOldValue('path');
|
||||
if ($event->hasChangedField('internal_path')) {
|
||||
$old_path = $event->getOldValue('internal_path');
|
||||
|
||||
//Dont delete file if the attachment uses a builtin ressource:
|
||||
if (Attachment::checkIfBuiltin($old_path)) {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,15 @@ class AttachmentFilterType extends AbstractType
|
|||
'label' => 'attachment.edit.show_in_table'
|
||||
]);
|
||||
|
||||
$builder->add('originalFileName', TextConstraintType::class, [
|
||||
'label' => 'attachment.file_name'
|
||||
]);
|
||||
|
||||
$builder->add('externalLink', TextConstraintType::class, [
|
||||
'label' => 'attachment.table.external_link'
|
||||
]);
|
||||
|
||||
|
||||
$builder->add('lastModified', DateTimeConstraintType::class, [
|
||||
'label' => 'lastModified'
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class AttachmentRepository extends DBElementRepository
|
|||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->where('attachment.path LIKE :like ESCAPE \'#\'');
|
||||
->where('attachment.internal_path LIKE :like ESCAPE \'#\'');
|
||||
$qb->setParameter('like', '#%SECURE#%%');
|
||||
$query = $qb->getQuery();
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ class AttachmentRepository extends DBElementRepository
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the count of all external attachments (attachments only containing a URL).
|
||||
* Gets the count of all external attachments (attachments containing an external path).
|
||||
*
|
||||
* @throws NoResultException
|
||||
* @throws NonUniqueResultException
|
||||
|
|
@ -75,17 +75,15 @@ class AttachmentRepository extends DBElementRepository
|
|||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->where('ILIKE(attachment.path, :http) = TRUE')
|
||||
->orWhere('ILIKE(attachment.path, :https) = TRUE');
|
||||
$qb->setParameter('http', 'http://%');
|
||||
$qb->setParameter('https', 'https://%');
|
||||
->andWhere('attaachment.internal_path IS NULL')
|
||||
->where('attachment.external_path IS NOT NULL');
|
||||
$query = $qb->getQuery();
|
||||
|
||||
return (int) $query->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of all attachments where a user uploaded a file.
|
||||
* Gets the count of all attachments where a user uploaded a file or a file was downloaded from an external source.
|
||||
*
|
||||
* @throws NoResultException
|
||||
* @throws NonUniqueResultException
|
||||
|
|
@ -94,9 +92,9 @@ class AttachmentRepository extends DBElementRepository
|
|||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->where('attachment.path LIKE :base ESCAPE \'#\'')
|
||||
->orWhere('attachment.path LIKE :media ESCAPE \'#\'')
|
||||
->orWhere('attachment.path LIKE :secure ESCAPE \'#\'');
|
||||
->where('attachment.internal_path LIKE :base ESCAPE \'#\'')
|
||||
->orWhere('attachment.internal_path LIKE :media ESCAPE \'#\'')
|
||||
->orWhere('attachment.internal_path LIKE :secure ESCAPE \'#\'');
|
||||
$qb->setParameter('secure', '#%SECURE#%%');
|
||||
$qb->setParameter('base', '#%BASE#%%');
|
||||
$qb->setParameter('media', '#%MEDIA#%%');
|
||||
|
|
|
|||
|
|
@ -52,11 +52,15 @@ class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterf
|
|||
$context[self::ALREADY_CALLED] = true;
|
||||
|
||||
$data = $this->normalizer->normalize($object, $format, $context);
|
||||
$data['internal_path'] = $this->attachmentURLGenerator->getInternalViewURL($object);
|
||||
|
||||
$data['media_url'] = $this->attachmentURLGenerator->getViewURL($object);
|
||||
//Add thumbnail url if the attachment is a picture
|
||||
$data['thumbnail_url'] = $object->isPicture() ? $this->attachmentURLGenerator->getThumbnailURL($object) : null;
|
||||
|
||||
//For backwards compatibility reasons
|
||||
//Deprecated: Use internal_path and external_path instead
|
||||
$data['media_url'] = $data['internal_path'] ?? $object->getExternalPath();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -244,12 +244,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;
|
||||
}
|
||||
|
|
@ -267,7 +267,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));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -275,17 +275,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;
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +295,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;
|
||||
}
|
||||
|
|
@ -319,7 +319,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;
|
||||
}
|
||||
|
|
@ -329,7 +329,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
|
||||
{
|
||||
|
|
@ -338,7 +338,7 @@ 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);
|
||||
|
|
@ -399,7 +399,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!');
|
||||
}
|
||||
|
|
@ -427,7 +427,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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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()]);
|
||||
|
|
|
|||
|
|
@ -247,7 +247,8 @@ trait EntityMergerHelperTrait
|
|||
{
|
||||
return $this->mergeCollections($target, $other, 'attachments', fn(Attachment $t, Attachment $o): bool => $t->getName() === $o->getName()
|
||||
&& $t->getAttachmentType() === $o->getAttachmentType()
|
||||
&& $t->getPath() === $o->getPath());
|
||||
&& $t->getExternalPath() === $o->getExternalPath()
|
||||
&& $t->getInternalPath() === $o->getInternalPath());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -156,25 +156,32 @@ class EntityURLGenerator
|
|||
|
||||
public function viewURL(Attachment $entity): string
|
||||
{
|
||||
if ($entity->isExternal()) { //For external attachments, return the link to external path
|
||||
return $entity->getURL() ?? throw new \RuntimeException('External attachment has no URL!');
|
||||
if ($entity->hasInternal()) {
|
||||
return $this->attachmentURLGenerator->getInternalViewURL($entity);
|
||||
}
|
||||
//return $this->urlGenerator->generate('attachment_view', ['id' => $entity->getID()]);
|
||||
return $this->attachmentURLGenerator->getViewURL($entity) ?? '';
|
||||
|
||||
if($entity->hasExternal()) {
|
||||
return $entity->getExternalPath();
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Attachment has no internal nor external path!');
|
||||
}
|
||||
|
||||
public function downloadURL($entity): string
|
||||
{
|
||||
if ($entity instanceof Attachment) {
|
||||
if ($entity->isExternal()) { //For external attachments, return the link to external path
|
||||
return $entity->getURL() ?? throw new \RuntimeException('External attachment has no URL!');
|
||||
}
|
||||
|
||||
return $this->attachmentURLGenerator->getDownloadURL($entity);
|
||||
if (!($entity instanceof Attachment)) {
|
||||
throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class));
|
||||
}
|
||||
|
||||
//Otherwise throw an error
|
||||
throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class));
|
||||
if ($entity->hasInternal()) {
|
||||
return $this->attachmentURLGenerator->getInternalDownloadURL($entity);
|
||||
}
|
||||
|
||||
if($entity->hasExternal()) {
|
||||
return $entity->getExternalPath();
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Attachment has not internal or external path!');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ trait PKImportHelperTrait
|
|||
//Next comes the filename plus extension
|
||||
$path .= '/'.$attachment_row['filename'].'.'.$attachment_row['extension'];
|
||||
|
||||
$attachment->setPath($path);
|
||||
$attachment->setInternalPath($path);
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
{
|
||||
|
||||
private const ENDPOINT_URL = 'https://api.element14.com/catalog/products';
|
||||
private const API_VERSION_NUMBER = '1.2';
|
||||
private const API_VERSION_NUMBER = '1.4';
|
||||
private const NUMBER_OF_RESULTS = 20;
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'Farnell';
|
||||
|
|
@ -83,7 +83,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
'resultsSettings.responseGroup' => 'large',
|
||||
'callInfo.apiKey' => $this->api_key,
|
||||
'callInfo.responseDataFormat' => 'json',
|
||||
'callInfo.version' => self::API_VERSION_NUMBER,
|
||||
'versionNumber' => self::API_VERSION_NUMBER,
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
@ -107,21 +107,18 @@ class Element14Provider implements InfoProviderInterface
|
|||
mpn: $product['translatedManufacturerPartNumber'],
|
||||
preview_image_url: $this->toImageUrl($product['image'] ?? null),
|
||||
manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['releaseStatusCode'] ?? null),
|
||||
provider_url: $this->generateProductURL($product['sku']),
|
||||
provider_url: $product['productURL'],
|
||||
notes: $product['productOverview']['description'] ?? null,
|
||||
datasheets: $this->parseDataSheets($product['datasheets'] ?? null),
|
||||
parameters: $this->attributesToParameters($product['attributes'] ?? null),
|
||||
vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [])
|
||||
vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [], $product['productURL']),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function generateProductURL($sku): string
|
||||
{
|
||||
return 'https://' . $this->store_id . '/' . $sku;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $datasheets
|
||||
* @return FileDTO[]|null Array of FileDTOs
|
||||
|
|
@ -161,7 +158,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
* @param array $prices
|
||||
* @return array
|
||||
*/
|
||||
private function pricesToVendorInfo(string $sku, array $prices): array
|
||||
private function pricesToVendorInfo(string $sku, array $prices, string $product_url): array
|
||||
{
|
||||
$price_dtos = [];
|
||||
|
||||
|
|
@ -179,7 +176,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
distributor_name: self::DISTRIBUTOR_NAME,
|
||||
order_number: $sku,
|
||||
prices: $price_dtos,
|
||||
product_url: $this->generateProductURL($sku)
|
||||
product_url: $product_url
|
||||
)
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ final class SandboxedTwigFactory
|
|||
'getFullPath', 'getPathArray', 'getSubelements', 'getChildren', 'isNotSelectable', ],
|
||||
AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite', 'getAutoProductUrl'],
|
||||
AttachmentContainingDBElement::class => ['getAttachments', 'getMasterPictureAttachment'],
|
||||
Attachment::class => ['isPicture', 'is3DModel', 'isExternal', 'isSecure', 'isBuiltIn', 'getExtension',
|
||||
'getElement', 'getURL', 'getHost', 'getFilename', 'getAttachmentType', 'getShowInTable', ],
|
||||
Attachment::class => ['isPicture', 'is3DModel', 'hasExternal', 'hasInternal', 'isSecure', 'isBuiltIn', 'getExtension',
|
||||
'getElement', 'getExternalPath', 'getHost', 'getFilename', 'getAttachmentType', 'getShowInTable'],
|
||||
AbstractParameter::class => ['getFormattedValue', 'getGroup', 'getSymbol', 'getValueMin', 'getValueMax',
|
||||
'getValueTypical', 'getUnit', 'getValueText', ],
|
||||
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
{{ form_row(filterForm.attachmentType) }}
|
||||
{{ form_row(filterForm.targetType) }}
|
||||
{{ form_row(filterForm.showInTable) }}
|
||||
{{ form_row(filterForm.originalFileName) }}
|
||||
{{ form_row(filterForm.externalLink) }}
|
||||
{{ form_row(filterForm.lastModified) }}
|
||||
{{ form_row(filterForm.addedDate) }}
|
||||
{{ form_row(filterForm.dbId) }}
|
||||
|
|
|
|||
|
|
@ -152,35 +152,32 @@
|
|||
</button>
|
||||
|
||||
{% set attach = form.vars.value %}
|
||||
{# @var \App\Entity\Attachments\Attachment attach #}
|
||||
|
||||
{% if attach is not null %}
|
||||
{% if attachment_manager.fileExisting(attach) %}
|
||||
{% if not attach.external %}
|
||||
<br><br>
|
||||
<h6>
|
||||
{% if not attach.hasInternal() and attach.external %}
|
||||
<div class="mt-2">
|
||||
<span class="badge bg-primary">
|
||||
<i class="fas fa-fw {{ ext_to_fa_icon(attach.extension) }}"></i> {{ attach.filename }}
|
||||
<i class="fas fa-fw fa-globe"></i> {% trans %}attachment.external_only{% endtrans %}
|
||||
</span>
|
||||
<br>
|
||||
<span class="badge bg-secondary">
|
||||
</div>
|
||||
{% elseif attachment_manager.isInternalFileExisting(attach) %}
|
||||
<div class="mt-2">
|
||||
<div class="badge bg-primary mt-2" title="{{ attach.filename }}">
|
||||
<i class="fas fa-fw {{ ext_to_fa_icon(attach.extension) }}"></i> {{ attach.filename|u.truncate(25, ' ...') }}
|
||||
</div>
|
||||
<br>
|
||||
<div class="badge bg-secondary mt-1">
|
||||
<i class="fas fa-hdd fa-fw"></i> {{ attachment_manager.humanFileSize(attach) }}
|
||||
</span>
|
||||
</h6>
|
||||
{% else %}
|
||||
<br><br>
|
||||
<h6>
|
||||
<span class="badge bg-primary">
|
||||
<i class="fas fa-fw fa-globe"></i> {% trans %}attachment.external{% endtrans %}
|
||||
</span>
|
||||
</h6>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if attach.secure %}
|
||||
<h6>
|
||||
<div>
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-fw fa-shield-alt"></i> {% trans %}attachment.secure{% endtrans %}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if attach.secure and not is_granted('show_private', attach) %}
|
||||
|
|
@ -190,16 +187,21 @@
|
|||
<img class="img-fluid img-thumbnail thumbnail-sm" src="{{ attachment_thumbnail(attach, 'thumbnail_md') }}" alt="{% trans %}attachment.preview.alt{% endtrans %}" />
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ entity_url(attach, 'file_view') }}" rel="noopener" target="_blank" data-turbo="false" class="link-external">{% trans %}attachment.view{% endtrans %}</a>
|
||||
<a href="{{ entity_url(attach, 'file_view') }}" rel="noopener" target="_blank" data-turbo="false" class="link-external">{% trans %}attachment.view_local{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<br><br>
|
||||
<h6>
|
||||
<div class="mt-2">
|
||||
<span class="badge bg-warning">
|
||||
<i class="fas fa-exclamation-circle fa-fw"></i> {% trans %}attachment.file_not_found{% endtrans %}
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if attach.external %}
|
||||
<div>
|
||||
<a href="{{ attach.externalPath }}" rel="noopener" target="_blank" data-turbo="false" class="link-external"
|
||||
title="{% trans with {"%host%": attach.host} %}attachment.view_external.view_at{% endtrans %}">{% trans %}attachment.view_external{% endtrans %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,18 +24,16 @@
|
|||
<td class="align-middle">{{ attachment.name }}</td>
|
||||
<td class="align-middle">{{ attachment.attachmentType.fullPath }}</td>
|
||||
<td class="align-middle">
|
||||
{% if attachment.external %}
|
||||
<a href="{{ attachment.uRL }}" rel="noopener" target="_blank" class="link-external">{{ attachment.host }}</a>
|
||||
{% else %}
|
||||
{% if attachment.hasInternal() %}
|
||||
{{ attachment.filename }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle h6">
|
||||
{% if attachment.external %}
|
||||
{% if not attachment.hasInternal() %}
|
||||
<span class="badge bg-primary">
|
||||
<i class="fas fa-fw fa-globe"></i> {% trans %}attachment.external{% endtrans %}
|
||||
<i class="fas fa-fw fa-globe"></i> {% trans %}attachment.external_only{% endtrans %}
|
||||
</span>
|
||||
{% elseif attachment_manager.fileExisting(attachment) %}
|
||||
{% elseif attachment_manager.internalFileExisting(attachment) %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fas fa-hdd fa-fw"></i> {{ attachment_manager.humanFileSize(attachment) }}
|
||||
</span>
|
||||
|
|
@ -58,14 +56,19 @@
|
|||
</td>
|
||||
|
||||
<td><div class="btn-group" role="group" aria-label="">
|
||||
<a {% if attachment_manager.fileExisting(attachment) %}href="{{ entity_url(attachment, 'file_view') }}"{% endif %} target="_blank"
|
||||
class="btn btn-secondary {% if not attachment_manager.fileExisting(attachment) or (attachment.secure and not is_granted("show_private", attachment)) %}disabled{% endif %}"
|
||||
data-turbo="false" title="{% trans %}attachment.view{% endtrans %}" rel="noopener">
|
||||
<a {% if attachment.hasExternal() %}href="{{ attachment.externalPath }}"{% endif %} target="_blank"
|
||||
class="btn btn-secondary {% if not attachment.hasExternal() %}disabled{% endif %}"
|
||||
data-turbo="false" title="{% trans with {"%host%": attachment.host} %}attachment.view_external.view_at{% endtrans %}" rel="noopener">
|
||||
<i class="fas fa-globe fa-fw"></i>
|
||||
</a>
|
||||
<a {% if attachment_manager.isInternalFileExisting(attachment) %}href="{{ entity_url(attachment, 'file_view') }}"{% endif %} target="_blank"
|
||||
class="btn btn-secondary {% if not attachment_manager.isInternalFileExisting(attachment) or (attachment.secure and not is_granted("show_private", attachment)) %}disabled{% endif %}"
|
||||
data-turbo="false" title="{% trans %}attachment.view_local{% endtrans %}" rel="noopener">
|
||||
<i class="fas fa-eye fa-fw"></i>
|
||||
</a>
|
||||
<a {% if attachment_manager.fileExisting(attachment) %}href="{{ entity_url(attachment, 'file_download') }}"{% endif %} data-turbo="false"
|
||||
class="btn btn-secondary {% if not attachment_manager.fileExisting(attachment) or (attachment.secure and not is_granted("show_private", attachment)) %}disabled{% endif %}"
|
||||
title="{% trans %}attachment.download{% endtrans %}">
|
||||
<a {% if attachment_manager.isInternalFileExisting(attachment) %}href="{{ entity_url(attachment, 'file_download') }}"{% endif %} data-turbo="false"
|
||||
class="btn btn-secondary {% if not attachment_manager.isInternalFileExisting(attachment) or (attachment.secure and not is_granted("show_private", attachment)) %}disabled{% endif %}"
|
||||
title="{% trans %}attachment.download_local{% endtrans %}">
|
||||
<i class="fas fa-download fa-fw"></i>
|
||||
</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class AttachmentsEndpointTest extends AuthenticatedApiTestCase
|
|||
//Attachment must be set (not null)
|
||||
$array = json_decode($response->getContent(), true);
|
||||
|
||||
self::assertNotNull($array['media_url']);
|
||||
self::assertNotNull($array['internal_path']);
|
||||
|
||||
//Attachment must be private
|
||||
self::assertJsonContains([
|
||||
|
|
|
|||
|
|
@ -59,14 +59,15 @@ class AttachmentTest extends TestCase
|
|||
|
||||
$this->assertNull($attachment->getAttachmentType());
|
||||
$this->assertFalse($attachment->isPicture());
|
||||
$this->assertFalse($attachment->isExternal());
|
||||
$this->assertFalse($attachment->hasExternal());
|
||||
$this->assertFalse($attachment->hasInternal());
|
||||
$this->assertFalse($attachment->isSecure());
|
||||
$this->assertFalse($attachment->isBuiltIn());
|
||||
$this->assertFalse($attachment->is3DModel());
|
||||
$this->assertFalse($attachment->getShowInTable());
|
||||
$this->assertEmpty($attachment->getPath());
|
||||
$this->assertEmpty($attachment->getInternalPath());
|
||||
$this->assertEmpty($attachment->getExternalPath());
|
||||
$this->assertEmpty($attachment->getName());
|
||||
$this->assertEmpty($attachment->getURL());
|
||||
$this->assertEmpty($attachment->getExtension());
|
||||
$this->assertNull($attachment->getElement());
|
||||
$this->assertEmpty($attachment->getFilename());
|
||||
|
|
@ -119,82 +120,63 @@ class AttachmentTest extends TestCase
|
|||
$attachment->setElement($element);
|
||||
}
|
||||
|
||||
public function externalDataProvider(): \Iterator
|
||||
{
|
||||
yield ['', false];
|
||||
yield ['%MEDIA%/foo/bar.txt', false];
|
||||
yield ['%BASE%/foo/bar.jpg', false];
|
||||
yield ['%FOOTPRINTS%/foo/bar.jpg', false];
|
||||
yield ['%FOOTPRINTS3D%/foo/bar.jpg', false];
|
||||
yield ['%SECURE%/test.txt', false];
|
||||
yield ['%test%/foo/bar.ghp', true];
|
||||
yield ['foo%MEDIA%/foo.jpg', true];
|
||||
yield ['foo%MEDIA%/%BASE%foo.jpg', true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider externalDataProvider
|
||||
*/
|
||||
public function testIsExternal($path, $expected): void
|
||||
public static function extensionDataProvider(): \Iterator
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->assertSame($expected, $attachment->isExternal());
|
||||
}
|
||||
|
||||
public function extensionDataProvider(): \Iterator
|
||||
{
|
||||
yield ['%MEDIA%/foo/bar.txt', null, 'txt'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', null, 'jpeg'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', 'test.txt', 'txt'];
|
||||
yield ['%MEDIA%/foo/bar', null, ''];
|
||||
yield ['%MEDIA%/foo.bar', 'bar', ''];
|
||||
yield ['http://google.de', null, null];
|
||||
yield ['https://foo.bar', null, null];
|
||||
yield ['https://foo.bar/test.jpeg', null, null];
|
||||
yield ['test', null, null];
|
||||
yield ['test.txt', null, null];
|
||||
yield ['%MEDIA%/foo/bar.txt', 'http://google.de', null, 'txt'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', 'https://foo.bar', null, 'jpeg'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', null, 'test.txt', 'txt'];
|
||||
yield ['%MEDIA%/foo/bar', 'https://foo.bar/test.jpeg', null, ''];
|
||||
yield ['%MEDIA%/foo.bar', 'test.txt', 'bar', ''];
|
||||
yield [null, 'http://google.de', null, null];
|
||||
yield [null, 'https://foo.bar', null, null];
|
||||
yield [null, ',https://foo.bar/test.jpeg', null, null];
|
||||
yield [null, 'test', null, null];
|
||||
yield [null, 'test.txt', null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider extensionDataProvider
|
||||
*/
|
||||
public function testGetExtension($path, $originalFilename, $expected): void
|
||||
public function testGetExtension(?string $internal_path, ?string $external_path, ?string $originalFilename, ?string $expected): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->setProtectedProperty($attachment, 'internal_path', $internal_path);
|
||||
$this->setProtectedProperty($attachment, 'external_path', $external_path);
|
||||
$this->setProtectedProperty($attachment, 'original_filename', $originalFilename);
|
||||
$this->assertSame($expected, $attachment->getExtension());
|
||||
}
|
||||
|
||||
public function pictureDataProvider(): \Iterator
|
||||
public static function pictureDataProvider(): \Iterator
|
||||
{
|
||||
yield ['%MEDIA%/foo/bar.txt', false];
|
||||
yield ['https://test.de/picture.jpeg', true];
|
||||
yield ['https://test.de/picture.png?test=fdsj&width=34', true];
|
||||
yield ['https://invalid.invalid/file.txt', false];
|
||||
yield ['http://infsf.inda/file.zip?test', false];
|
||||
yield ['https://test.de', true];
|
||||
yield ['https://invalid.com/invalid/pic', true];
|
||||
yield ['%MEDIA%/foo/bar.jpeg', true];
|
||||
yield ['%MEDIA%/foo/bar.webp', true];
|
||||
yield ['%MEDIA%/foo', false];
|
||||
yield ['%SECURE%/foo.txt/test', false];
|
||||
yield [null, '%MEDIA%/foo/bar.txt', false];
|
||||
yield [null, 'https://test.de/picture.jpeg', true];
|
||||
yield [null, 'https://test.de/picture.png?test=fdsj&width=34', true];
|
||||
yield [null, 'https://invalid.invalid/file.txt', false];
|
||||
yield [null, 'http://infsf.inda/file.zip?test', false];
|
||||
yield [null, 'https://test.de', true];
|
||||
yield [null, 'https://invalid.com/invalid/pic', true];
|
||||
yield ['%MEDIA%/foo/bar.jpeg', 'https://invalid.invalid/file.txt', true];
|
||||
yield ['%MEDIA%/foo/bar.webp', '', true];
|
||||
yield ['%MEDIA%/foo', '', false];
|
||||
yield ['%SECURE%/foo.txt/test', 'https://test.de/picture.jpeg', false];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider pictureDataProvider
|
||||
*/
|
||||
public function testIsPicture($path, $expected): void
|
||||
public function testIsPicture(?string $internal_path, ?string $external_path, bool $expected): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->setProtectedProperty($attachment, 'internal_path', $internal_path);
|
||||
$this->setProtectedProperty($attachment, 'external_path', $external_path);
|
||||
$this->assertSame($expected, $attachment->isPicture());
|
||||
}
|
||||
|
||||
public function builtinDataProvider(): \Iterator
|
||||
public static function builtinDataProvider(): \Iterator
|
||||
{
|
||||
yield ['', false];
|
||||
yield [null, false];
|
||||
yield ['%MEDIA%/foo/bar.txt', false];
|
||||
yield ['%BASE%/foo/bar.txt', false];
|
||||
yield ['/', false];
|
||||
|
|
@ -205,14 +187,14 @@ class AttachmentTest extends TestCase
|
|||
/**
|
||||
* @dataProvider builtinDataProvider
|
||||
*/
|
||||
public function testIsBuiltIn($path, $expected): void
|
||||
public function testIsBuiltIn(?string $path, $expected): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->setProtectedProperty($attachment, 'internal_path', $path);
|
||||
$this->assertSame($expected, $attachment->isBuiltIn());
|
||||
}
|
||||
|
||||
public function hostDataProvider(): \Iterator
|
||||
public static function hostDataProvider(): \Iterator
|
||||
{
|
||||
yield ['%MEDIA%/foo/bar.txt', null];
|
||||
yield ['https://www.google.de/test.txt', 'www.google.de'];
|
||||
|
|
@ -222,55 +204,60 @@ class AttachmentTest extends TestCase
|
|||
/**
|
||||
* @dataProvider hostDataProvider
|
||||
*/
|
||||
public function testGetHost($path, $expected): void
|
||||
public function testGetHost(?string $path, ?string $expected): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->setProtectedProperty($attachment, 'external_path', $path);
|
||||
$this->assertSame($expected, $attachment->getHost());
|
||||
}
|
||||
|
||||
public function filenameProvider(): \Iterator
|
||||
public static function filenameProvider(): \Iterator
|
||||
{
|
||||
yield ['%MEDIA%/foo/bar.txt', null, 'bar.txt'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', 'test.txt', 'test.txt'];
|
||||
yield ['https://www.google.de/test.txt', null, null];
|
||||
yield ['%MEDIA%/foo/bar.txt', 'https://www.google.de/test.txt', null, 'bar.txt'];
|
||||
yield ['%MEDIA%/foo/bar.JPeg', 'https://www.google.de/foo.txt', 'test.txt', 'test.txt'];
|
||||
yield ['', 'https://www.google.de/test.txt', null, null];
|
||||
yield [null, 'https://www.google.de/test.txt', null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider filenameProvider
|
||||
*/
|
||||
public function testGetFilename($path, $original_filename, $expected): void
|
||||
public function testGetFilename(?string $internal_path, ?string $external_path, ?string $original_filename, ?string $expected): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
$this->setProtectedProperty($attachment, 'path', $path);
|
||||
$this->setProtectedProperty($attachment, 'internal_path', $internal_path);
|
||||
$this->setProtectedProperty($attachment, 'external_path', $external_path);
|
||||
$this->setProtectedProperty($attachment, 'original_filename', $original_filename);
|
||||
$this->assertSame($expected, $attachment->getFilename());
|
||||
}
|
||||
|
||||
public function testSetURL(): void
|
||||
public function testSetExternalPath(): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
|
||||
//Set URL
|
||||
$attachment->setURL('https://google.de');
|
||||
$this->assertSame('https://google.de', $attachment->getURL());
|
||||
$attachment->setExternalPath('https://google.de');
|
||||
$this->assertSame('https://google.de', $attachment->getExternalPath());
|
||||
|
||||
//Ensure that an empty url does not overwrite the existing one
|
||||
$attachment->setPath('%MEDIA%/foo/bar.txt');
|
||||
$attachment->setURL(' ');
|
||||
$this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getPath());
|
||||
//Ensure that changing the external path does reset the internal one
|
||||
$attachment->setInternalPath('%MEDIA%/foo/bar.txt');
|
||||
$attachment->setExternalPath('https://example.de');
|
||||
$this->assertSame(null, $attachment->getInternalPath());
|
||||
|
||||
//Ensure that setting the same value to the external path again doesn't reset the internal one
|
||||
$attachment->setExternalPath('https://google.de');
|
||||
$attachment->setInternalPath('%MEDIA%/foo/bar.txt');
|
||||
$attachment->setExternalPath('https://google.de');
|
||||
$this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getInternalPath());
|
||||
|
||||
//Ensure that resetting the external path doesn't reset the internal one
|
||||
$attachment->setInternalPath('%MEDIA%/foo/bar.txt');
|
||||
$attachment->setExternalPath('');
|
||||
$this->assertSame('%MEDIA%/foo/bar.txt', $attachment->getInternalPath());
|
||||
|
||||
//Ensure that spaces get replaced by %20
|
||||
$attachment->setURL('https://google.de/test file.txt');
|
||||
$this->assertSame('https://google.de/test%20file.txt', $attachment->getURL());
|
||||
}
|
||||
|
||||
public function testSetURLForbiddenURL(): void
|
||||
{
|
||||
$attachment = new PartAttachment();
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$attachment->setURL('%MEDIA%/foo/bar.txt');
|
||||
$attachment->setExternalPath('https://example.de/test file.txt');
|
||||
$this->assertSame('https://example.de/test%20file.txt', $attachment->getExternalPath());
|
||||
}
|
||||
|
||||
public function testIsURL(): void
|
||||
|
|
|
|||
|
|
@ -779,18 +779,10 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs
|
|||
<target>Löschen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="W80Gv6o" name="attachment.external">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\_attachments.html.twig:41</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:38</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:35</note>
|
||||
<note category="file-source" priority="1">Part-DB1\src\DataTables\AttachmentDataTable.php:159</note>
|
||||
<note priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:38</note>
|
||||
<note priority="1">Part-DB1\src\DataTables\AttachmentDataTable.php:159</note>
|
||||
</notes>
|
||||
<unit id="FtktoBj" name="attachment.external_only">
|
||||
<segment state="translated">
|
||||
<source>attachment.external</source>
|
||||
<target>Extern</target>
|
||||
<source>attachment.external_only</source>
|
||||
<target>Nur Extern</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="JES0hrm" name="attachment.preview.alt">
|
||||
|
|
@ -805,7 +797,7 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs
|
|||
<target>Thumbnail des Dateianhanges</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="fCQby7u" name="attachment.view">
|
||||
<unit id="I_HDnsL" name="attachment.view_local">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\_attachments.html.twig:52</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:50</note>
|
||||
|
|
@ -815,8 +807,8 @@ Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müs
|
|||
<note priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:45</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<source>attachment.view</source>
|
||||
<target>Anzeigen</target>
|
||||
<source>attachment.view_local</source>
|
||||
<target>Lokale Datei anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="mEHEYM6" name="attachment.file_not_found">
|
||||
|
|
@ -2118,14 +2110,14 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
|
|||
<target>Vorschaubild</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="O2kBcDz" name="attachment.download">
|
||||
<unit id="Uuy6Ntl" name="attachment.download_local">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:67</note>
|
||||
<note priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:50</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<source>attachment.download</source>
|
||||
<target>Herunterladen</target>
|
||||
<source>attachment.download_local</source>
|
||||
<target>Lokale Datei downloaden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="mPK9Iyq" name="user.creating_user">
|
||||
|
|
@ -12319,5 +12311,35 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Profil gespeichert!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8C9ijHM" name="entity.export.flash.error.no_entities">
|
||||
<segment state="translated">
|
||||
<source>entity.export.flash.error.no_entities</source>
|
||||
<target>Es gibt keine Entitäten zu exportieren!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="0B3_rob" name="attachment.table.internal_file">
|
||||
<segment state="translated">
|
||||
<source>attachment.table.internal_file</source>
|
||||
<target>Interne Datei</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="uhfLnkB" name="attachment.table.external_link">
|
||||
<segment state="translated">
|
||||
<source>attachment.table.external_link</source>
|
||||
<target>Externer link</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2WKNZAm" name="attachment.view_external.view_at">
|
||||
<segment state="translated">
|
||||
<source>attachment.view_external.view_at</source>
|
||||
<target>Auf %host% anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nwO78O_" name="attachment.view_external">
|
||||
<segment state="translated">
|
||||
<source>attachment.view_external</source>
|
||||
<target>Externe Version anzeigen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
|
|
@ -780,18 +780,10 @@ The user will have to set up all two-factor authentication methods again and pri
|
|||
<target>Delete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="W80Gv6o" name="attachment.external">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\_attachments.html.twig:41</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:38</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:35</note>
|
||||
<note category="file-source" priority="1">Part-DB1\src\DataTables\AttachmentDataTable.php:159</note>
|
||||
<note priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:38</note>
|
||||
<note priority="1">Part-DB1\src\DataTables\AttachmentDataTable.php:159</note>
|
||||
</notes>
|
||||
<unit id="FtktoBj" name="attachment.external_only">
|
||||
<segment state="translated">
|
||||
<source>attachment.external</source>
|
||||
<target>External</target>
|
||||
<source>attachment.external_only</source>
|
||||
<target>External only</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="JES0hrm" name="attachment.preview.alt">
|
||||
|
|
@ -806,7 +798,7 @@ The user will have to set up all two-factor authentication methods again and pri
|
|||
<target>Attachment thumbnail</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="fCQby7u" name="attachment.view">
|
||||
<unit id="I_HDnsL" name="attachment.view_local">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\_attachments.html.twig:52</note>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\edit\_attachments.html.twig:50</note>
|
||||
|
|
@ -816,8 +808,8 @@ The user will have to set up all two-factor authentication methods again and pri
|
|||
<note priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:45</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<source>attachment.view</source>
|
||||
<target>View</target>
|
||||
<source>attachment.view_local</source>
|
||||
<target>View Local Copy</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="mEHEYM6" name="attachment.file_not_found">
|
||||
|
|
@ -2119,14 +2111,14 @@ Sub elements will be moved upwards.</target>
|
|||
<target>Preview picture</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="O2kBcDz" name="attachment.download">
|
||||
<unit id="Uuy6Ntl" name="attachment.download_local">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:67</note>
|
||||
<note priority="1">Part-DB1\templates\Parts\info\_attachments_info.html.twig:50</note>
|
||||
</notes>
|
||||
<segment state="translated">
|
||||
<source>attachment.download</source>
|
||||
<target>Download</target>
|
||||
<source>attachment.download_local</source>
|
||||
<target>Download Local Copy</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="mPK9Iyq" name="user.creating_user">
|
||||
|
|
@ -12329,5 +12321,29 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>There are no entities to export!</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="0B3_rob" name="attachment.table.internal_file">
|
||||
<segment state="translated">
|
||||
<source>attachment.table.internal_file</source>
|
||||
<target>Internal file</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="uhfLnkB" name="attachment.table.external_link">
|
||||
<segment state="translated">
|
||||
<source>attachment.table.external_link</source>
|
||||
<target>External link</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2WKNZAm" name="attachment.view_external.view_at">
|
||||
<segment state="translated">
|
||||
<source>attachment.view_external.view_at</source>
|
||||
<target>View at %host%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nwO78O_" name="attachment.view_external">
|
||||
<segment state="translated">
|
||||
<source>attachment.view_external</source>
|
||||
<target>View external version</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
28
yarn.lock
28
yarn.lock
|
|
@ -2085,9 +2085,9 @@
|
|||
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
|
||||
|
||||
"@types/node@*":
|
||||
version "22.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a"
|
||||
integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==
|
||||
version "22.13.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.5.tgz#23add1d71acddab2c6a4d31db89c0f98d330b511"
|
||||
integrity sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
|
|
@ -2629,7 +2629,7 @@ cacache@^15.0.5:
|
|||
tar "^6.0.2"
|
||||
unique-filename "^1.1.1"
|
||||
|
||||
call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1:
|
||||
call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
|
|
@ -3501,9 +3501,9 @@ duplexer@^0.1.2:
|
|||
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
|
||||
|
||||
electron-to-chromium@^1.5.73:
|
||||
version "1.5.102"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz#81a452ace8e2c3fa7fba904ea4fed25052c53d3f"
|
||||
integrity sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==
|
||||
version "1.5.103"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz#3d02025bc16e96e5edb3ed3ffa2538a11ae682dc"
|
||||
integrity sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
|
|
@ -3639,7 +3639,7 @@ es-module-lexer@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21"
|
||||
integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==
|
||||
|
||||
es-object-atoms@^1.0.0:
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
|
|
@ -3933,16 +3933,16 @@ get-caller-file@^2.0.1:
|
|||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
|
||||
integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.0.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.0"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue