mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-01 14:04:30 +02:00
Merge branch 'master' into settings-bundle
This commit is contained in:
commit
3e657a7cac
305 changed files with 7543 additions and 4274 deletions
|
@ -139,12 +139,12 @@ class AttachmentPathResolver
|
|||
}
|
||||
|
||||
//If we have now have a placeholder left, the string is invalid:
|
||||
if (preg_match('#%\w+%#', $placeholder_path)) {
|
||||
if (preg_match('#%\w+%#', (string) $placeholder_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Path is invalid if path is directory traversal
|
||||
if (str_contains($placeholder_path, '..')) {
|
||||
if (str_contains((string) $placeholder_path, '..')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -183,7 +183,7 @@ class AttachmentPathResolver
|
|||
}
|
||||
|
||||
//If the new string does not begin with a placeholder, it is invalid
|
||||
if (!preg_match('#^%\w+%#', $real_path)) {
|
||||
if (!preg_match('#^%\w+%#', (string) $real_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -302,7 +302,7 @@ class AttachmentSubmitHandler
|
|||
return $attachment;
|
||||
}
|
||||
|
||||
$filename = basename($old_path);
|
||||
$filename = basename((string) $old_path);
|
||||
//If the basename is not one of the new unique on, we have to save the old filename
|
||||
if (!preg_match('#\w+-\w{13}\.#', $filename)) {
|
||||
//Save filename to attachment field
|
||||
|
@ -380,7 +380,7 @@ class AttachmentSubmitHandler
|
|||
|
||||
//If we don't know filename yet, try to determine it out of url
|
||||
if ('' === $filename) {
|
||||
$filename = basename(parse_url($url, PHP_URL_PATH));
|
||||
$filename = basename(parse_url((string) $url, PHP_URL_PATH));
|
||||
}
|
||||
|
||||
//Set original file
|
||||
|
|
|
@ -46,11 +46,10 @@ class ElementCacheTagGenerator
|
|||
{
|
||||
//Ensure that the given element is a class name
|
||||
if (is_object($element)) {
|
||||
$element = get_class($element);
|
||||
} else { //And that the class exists
|
||||
if (!class_exists($element)) {
|
||||
throw new \InvalidArgumentException("The given class '$element' does not exist!");
|
||||
}
|
||||
$element = $element::class;
|
||||
} elseif (!class_exists($element)) {
|
||||
//And that the class exists
|
||||
throw new \InvalidArgumentException("The given class '$element' does not exist!");
|
||||
}
|
||||
|
||||
//Check if the tag is already cached
|
||||
|
|
|
@ -134,7 +134,7 @@ class KiCadHelper
|
|||
|
||||
if ($this->category_depth >= 0) {
|
||||
//Ensure that the category is set
|
||||
if (!$category) {
|
||||
if ($category === null) {
|
||||
throw new NotFoundHttpException('Category must be set, if category_depth is greater than 1!');
|
||||
}
|
||||
|
||||
|
@ -196,25 +196,25 @@ class KiCadHelper
|
|||
|
||||
//Add basic fields
|
||||
$result["fields"]["description"] = $this->createField($part->getDescription());
|
||||
if ($part->getCategory()) {
|
||||
if ($part->getCategory() !== null) {
|
||||
$result["fields"]["Category"] = $this->createField($part->getCategory()->getFullPath('/'));
|
||||
}
|
||||
if ($part->getManufacturer()) {
|
||||
if ($part->getManufacturer() !== null) {
|
||||
$result["fields"]["Manufacturer"] = $this->createField($part->getManufacturer()->getName());
|
||||
}
|
||||
if ($part->getManufacturerProductNumber() !== "") {
|
||||
$result['fields']["MPN"] = $this->createField($part->getManufacturerProductNumber());
|
||||
}
|
||||
if ($part->getManufacturingStatus()) {
|
||||
if ($part->getManufacturingStatus() !== null) {
|
||||
$result["fields"]["Manufacturing Status"] = $this->createField(
|
||||
//Always use the english translation
|
||||
$this->translator->trans($part->getManufacturingStatus()->toTranslationKey(), locale: 'en')
|
||||
);
|
||||
}
|
||||
if ($part->getFootprint()) {
|
||||
if ($part->getFootprint() !== null) {
|
||||
$result["fields"]["Part-DB Footprint"] = $this->createField($part->getFootprint()->getName());
|
||||
}
|
||||
if ($part->getPartUnit()) {
|
||||
if ($part->getPartUnit() !== null) {
|
||||
$unit = $part->getPartUnit()->getName();
|
||||
if ($part->getPartUnit()->getUnit() !== "") {
|
||||
$unit .= ' ('.$part->getPartUnit()->getUnit().')';
|
||||
|
@ -225,7 +225,7 @@ class KiCadHelper
|
|||
$result["fields"]["Mass"] = $this->createField($part->getMass() . ' g');
|
||||
}
|
||||
$result["fields"]["Part-DB ID"] = $this->createField($part->getId());
|
||||
if (!empty($part->getIpn())) {
|
||||
if ($part->getIpn() !== null && $part->getIpn() !== '' && $part->getIpn() !== '0') {
|
||||
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
|
||||
}
|
||||
|
||||
|
@ -308,14 +308,9 @@ class KiCadHelper
|
|||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//And on the footprint
|
||||
if ($part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Otherwise the part should be not visible
|
||||
return false;
|
||||
return $part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -69,7 +69,7 @@ class EntityMerger
|
|||
{
|
||||
$merger = $this->findMergerForObject($target, $other, $context);
|
||||
if ($merger === null) {
|
||||
throw new \RuntimeException('No merger found for merging '.get_class($other).' into '.get_class($target));
|
||||
throw new \RuntimeException('No merger found for merging '.$other::class.' into '.$target::class);
|
||||
}
|
||||
return $merger->merge($target, $other, $context);
|
||||
}
|
||||
|
|
|
@ -85,9 +85,7 @@ trait EntityMergerHelperTrait
|
|||
protected function useOtherValueIfNotNull(object $target, object $other, string $field): object
|
||||
{
|
||||
return $this->useCallback(
|
||||
function ($target_value, $other_value) {
|
||||
return $target_value ?? $other_value;
|
||||
},
|
||||
fn($target_value, $other_value) => $target_value ?? $other_value,
|
||||
$target,
|
||||
$other,
|
||||
$field
|
||||
|
@ -106,9 +104,7 @@ trait EntityMergerHelperTrait
|
|||
protected function useOtherValueIfNotEmtpy(object $target, object $other, string $field): object
|
||||
{
|
||||
return $this->useCallback(
|
||||
function ($target_value, $other_value) {
|
||||
return empty($target_value) ? $other_value : $target_value;
|
||||
},
|
||||
fn($target_value, $other_value) => empty($target_value) ? $other_value : $target_value,
|
||||
$target,
|
||||
$other,
|
||||
$field
|
||||
|
@ -126,9 +122,7 @@ trait EntityMergerHelperTrait
|
|||
protected function useLargerValue(object $target, object $other, string $field): object
|
||||
{
|
||||
return $this->useCallback(
|
||||
function ($target_value, $other_value) {
|
||||
return max($target_value, $other_value);
|
||||
},
|
||||
fn($target_value, $other_value) => max($target_value, $other_value),
|
||||
$target,
|
||||
$other,
|
||||
$field
|
||||
|
@ -146,9 +140,7 @@ trait EntityMergerHelperTrait
|
|||
protected function useSmallerValue(object $target, object $other, string $field): object
|
||||
{
|
||||
return $this->useCallback(
|
||||
function ($target_value, $other_value) {
|
||||
return min($target_value, $other_value);
|
||||
},
|
||||
fn($target_value, $other_value) => min($target_value, $other_value),
|
||||
$target,
|
||||
$other,
|
||||
$field
|
||||
|
@ -166,9 +158,7 @@ trait EntityMergerHelperTrait
|
|||
protected function useTrueValue(object $target, object $other, string $field): object
|
||||
{
|
||||
return $this->useCallback(
|
||||
function (bool $target_value, bool $other_value): bool {
|
||||
return $target_value || $other_value;
|
||||
},
|
||||
fn(bool $target_value, bool $other_value): bool => $target_value || $other_value,
|
||||
$target,
|
||||
$other,
|
||||
$field
|
||||
|
@ -232,10 +222,8 @@ trait EntityMergerHelperTrait
|
|||
continue 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($target_collection->contains($item)) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($target_collection->contains($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clones[] = clone $item;
|
||||
|
@ -257,11 +245,9 @@ trait EntityMergerHelperTrait
|
|||
*/
|
||||
protected function mergeAttachments(AttachmentContainingDBElement $target, AttachmentContainingDBElement $other): object
|
||||
{
|
||||
return $this->mergeCollections($target, $other, 'attachments', function (Attachment $t, Attachment $o): bool {
|
||||
return $t->getName() === $o->getName()
|
||||
&& $t->getAttachmentType() === $o->getAttachmentType()
|
||||
&& $t->getPath() === $o->getPath();
|
||||
});
|
||||
return $this->mergeCollections($target, $other, 'attachments', fn(Attachment $t, Attachment $o): bool => $t->getName() === $o->getName()
|
||||
&& $t->getAttachmentType() === $o->getAttachmentType()
|
||||
&& $t->getPath() === $o->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -272,16 +258,14 @@ trait EntityMergerHelperTrait
|
|||
*/
|
||||
protected function mergeParameters(AbstractStructuralDBElement|Part $target, AbstractStructuralDBElement|Part $other): object
|
||||
{
|
||||
return $this->mergeCollections($target, $other, 'parameters', function (AbstractParameter $t, AbstractParameter $o): bool {
|
||||
return $t->getName() === $o->getName()
|
||||
&& $t->getSymbol() === $o->getSymbol()
|
||||
&& $t->getUnit() === $o->getUnit()
|
||||
&& $t->getValueMax() === $o->getValueMax()
|
||||
&& $t->getValueMin() === $o->getValueMin()
|
||||
&& $t->getValueTypical() === $o->getValueTypical()
|
||||
&& $t->getValueText() === $o->getValueText()
|
||||
&& $t->getGroup() === $o->getGroup();
|
||||
});
|
||||
return $this->mergeCollections($target, $other, 'parameters', fn(AbstractParameter $t, AbstractParameter $o): bool => $t->getName() === $o->getName()
|
||||
&& $t->getSymbol() === $o->getSymbol()
|
||||
&& $t->getUnit() === $o->getUnit()
|
||||
&& $t->getValueMax() === $o->getValueMax()
|
||||
&& $t->getValueMin() === $o->getValueMin()
|
||||
&& $t->getValueTypical() === $o->getValueTypical()
|
||||
&& $t->getValueText() === $o->getValueText()
|
||||
&& $t->getGroup() === $o->getGroup());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -33,6 +33,7 @@ use App\Entity\PriceInformations\Orderdetail;
|
|||
* This class merges two parts together.
|
||||
*
|
||||
* @implements EntityMergerInterface<Part>
|
||||
* @see \App\Tests\Services\EntityMergers\Mergers\PartMergerTest
|
||||
*/
|
||||
class PartMerger implements EntityMergerInterface
|
||||
{
|
||||
|
@ -99,7 +100,7 @@ class PartMerger implements EntityMergerInterface
|
|||
return $target;
|
||||
}
|
||||
|
||||
private static function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool {
|
||||
private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool {
|
||||
//We compare the translation keys, as it contains info about the type and other type info
|
||||
return $t->getOther() === $o->getOther()
|
||||
&& $t->getTypeTranslationKey() === $o->getTypeTranslationKey();
|
||||
|
@ -117,7 +118,7 @@ class PartMerger implements EntityMergerInterface
|
|||
$this->mergeParameters($target, $other);
|
||||
|
||||
//Merge the associations
|
||||
$this->mergeCollections($target, $other, 'associated_parts_as_owner', self::comparePartAssociations(...));
|
||||
$this->mergeCollections($target, $other, 'associated_parts_as_owner', $this->comparePartAssociations(...));
|
||||
|
||||
//We have to recreate the associations towards the other part, as they are not created by the merger
|
||||
foreach ($other->getAssociatedPartsAsOther() as $association) {
|
||||
|
@ -131,7 +132,7 @@ class PartMerger implements EntityMergerInterface
|
|||
}
|
||||
//Ensure that the association is not already present
|
||||
foreach ($owner->getAssociatedPartsAsOwner() as $existing_association) {
|
||||
if (self::comparePartAssociations($existing_association, $clone)) {
|
||||
if ($this->comparePartAssociations($existing_association, $clone)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -360,11 +360,7 @@ class EntityURLGenerator
|
|||
*/
|
||||
protected function mapToController(array $map, string|AbstractDBElement $entity): string
|
||||
{
|
||||
if (is_string($entity)) { //If a class name was already passed, then use it directly
|
||||
$class = $entity;
|
||||
} else { //Otherwise get the class name from the entity
|
||||
$class = $entity::class;
|
||||
}
|
||||
$class = is_string($entity) ? $entity : $entity::class;
|
||||
|
||||
//Check if we have an direct mapping for the given class
|
||||
if (!array_key_exists($class, $map)) {
|
||||
|
|
|
@ -23,9 +23,13 @@ declare(strict_types=1);
|
|||
namespace App\Services\ImportExportSystem;
|
||||
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Helpers\FilenameSanatizer;
|
||||
use App\Serializer\APIPlatform\SkippableItemNormalizer;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use function is_array;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
@ -97,10 +101,27 @@ class EntityExporter
|
|||
'csv_delimiter' => $options['csv_delimiter'],
|
||||
'xml_root_node_name' => 'PartDBExport',
|
||||
'partdb_export' => true,
|
||||
//Skip the item normalizer, so that we dont get IRIs in the output
|
||||
SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true,
|
||||
//Handle circular references
|
||||
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function handleCircularReference(object $object, string $format, array $context): string
|
||||
{
|
||||
if ($object instanceof AbstractStructuralDBElement) {
|
||||
return $object->getFullPath("->");
|
||||
} elseif ($object instanceof AbstractNamedDBElement) {
|
||||
return $object->getName();
|
||||
} elseif ($object instanceof \Stringable) {
|
||||
return $object->__toString();
|
||||
}
|
||||
|
||||
throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports an Entity or an array of entities to multiple file formats.
|
||||
*
|
||||
|
|
|
@ -29,6 +29,7 @@ use App\Entity\Parts\Part;
|
|||
use App\Repository\StructuralDBElementRepository;
|
||||
use App\Serializer\APIPlatform\SkippableItemNormalizer;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use function count;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
|
@ -57,6 +58,7 @@ class EntityImporter
|
|||
* @phpstan-param class-string<T> $class_name
|
||||
* @param AbstractStructuralDBElement|null $parent the element which will be used as parent element for new elements
|
||||
* @param array $errors an associative array containing all validation errors
|
||||
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
*
|
||||
* @return AbstractNamedDBElement[] An array containing all valid imported entities (with the type $class_name)
|
||||
* @return T[]
|
||||
|
@ -152,6 +154,7 @@ class EntityImporter
|
|||
* @param string $data The serialized data which should be imported
|
||||
* @param array $options The options for the import process
|
||||
* @param array $errors An array which will be filled with the validation errors, if any occurs during import
|
||||
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
* @return array An array containing all valid imported entities
|
||||
*/
|
||||
public function importString(string $data, array $options = [], array &$errors = []): array
|
||||
|
@ -218,6 +221,10 @@ class EntityImporter
|
|||
if (count($tmp) > 0) { //Log validation errors to global log.
|
||||
$name = $entity instanceof AbstractStructuralDBElement ? $entity->getFullPath() : $entity->getName();
|
||||
|
||||
if (trim($name) === '') {
|
||||
$name = 'Row ' . (string) $key;
|
||||
}
|
||||
|
||||
$errors[$name] = [
|
||||
'violations' => $tmp,
|
||||
'entity' => $entity,
|
||||
|
@ -264,7 +271,7 @@ class EntityImporter
|
|||
* @param array $options options for the import process
|
||||
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
|
||||
*
|
||||
* @return array<string, ConstraintViolationList> An associative array containing an ConstraintViolationList and the entity name as key are returned,
|
||||
* @return array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> An associative array containing an ConstraintViolationList and the entity name as key are returned,
|
||||
* if an error happened during validation. When everything was successfully, the array should be empty.
|
||||
*/
|
||||
public function importFileAndPersistToDB(File $file, array $options = [], array &$entities = []): array
|
||||
|
@ -296,6 +303,7 @@ class EntityImporter
|
|||
*
|
||||
* @param File $file the file that should be used for importing
|
||||
* @param array $options options for the import process
|
||||
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
*
|
||||
* @return array an array containing the deserialized elements
|
||||
*/
|
||||
|
|
|
@ -30,7 +30,7 @@ use App\Entity\Base\AbstractDBElement;
|
|||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
|
||||
/**
|
||||
|
@ -212,7 +212,7 @@ trait PKImportHelperTrait
|
|||
$id = (int) $id;
|
||||
|
||||
$metadata = $this->em->getClassMetadata($element::class);
|
||||
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE);
|
||||
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
|
||||
$metadata->setIdGenerator(new AssignedGenerator());
|
||||
$metadata->setIdentifierValues($element, ['id' => $id]);
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ trait PKImportHelperTrait
|
|||
protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str): void
|
||||
{
|
||||
if ($datetime_str !== null && $datetime_str !== '' && $datetime_str !== '0000-00-00 00:00:00') {
|
||||
$date = new \DateTime($datetime_str);
|
||||
$date = new \DateTimeImmutable($datetime_str);
|
||||
} else {
|
||||
$date = null; //Null means "now" at persist time
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ class PKOptionalImporter
|
|||
//All imported users get assigned to the "PartKeepr Users" group
|
||||
$group_users = $this->em->find(Group::class, 3);
|
||||
$group = $this->em->getRepository(Group::class)->findOneBy(['name' => 'PartKeepr Users', 'parent' => $group_users]);
|
||||
if (!$group) {
|
||||
if ($group === null) {
|
||||
$group = new Group();
|
||||
$group->setName('PartKeepr Users');
|
||||
$group->setParent($group_users);
|
||||
|
|
|
@ -218,7 +218,7 @@ class PKPartImporter
|
|||
'iso_code' => $currency_iso_code,
|
||||
]);
|
||||
|
||||
if (!$currency) {
|
||||
if ($currency === null) {
|
||||
$currency = new Currency();
|
||||
$currency->setIsoCode($currency_iso_code);
|
||||
$currency->setName(Currencies::getName($currency_iso_code));
|
||||
|
@ -265,7 +265,7 @@ class PKPartImporter
|
|||
]);
|
||||
|
||||
//When no orderdetail exists, create one
|
||||
if (!$orderdetail) {
|
||||
if ($orderdetail === null) {
|
||||
$orderdetail = new Orderdetail();
|
||||
$orderdetail->setSupplier($supplier);
|
||||
$orderdetail->setSupplierpartnr($spn);
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace App\Services\InfoProviderSystem\DTOs;
|
|||
/**
|
||||
* This DTO represents a file that can be downloaded from a URL.
|
||||
* This could be a datasheet, a 3D model, a picture or similar.
|
||||
* @see \App\Tests\Services\InfoProviderSystem\DTOs\FileDTOTest
|
||||
*/
|
||||
class FileDTO
|
||||
{
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace App\Services\InfoProviderSystem\DTOs;
|
|||
/**
|
||||
* This DTO represents a parameter of a part (similar to the AbstractParameter entity).
|
||||
* This could be a voltage, a current, a temperature or similar.
|
||||
* @see \App\Tests\Services\InfoProviderSystem\DTOs\ParameterDTOTest
|
||||
*/
|
||||
class ParameterDTO
|
||||
{
|
||||
|
@ -76,7 +77,7 @@ class ParameterDTO
|
|||
$parts = preg_split('/\s*(\.{3}|~)\s*/', $value);
|
||||
if (count($parts) === 2) {
|
||||
//Try to extract number and unit from value (allow leading +)
|
||||
if (empty($unit)) {
|
||||
if ($unit === null || trim($unit) === '') {
|
||||
[$number, $unit] = self::splitIntoValueAndUnit(ltrim($parts[0], " +")) ?? [$parts[0], null];
|
||||
} else {
|
||||
$number = $parts[0];
|
||||
|
|
|
@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\DTOs;
|
|||
|
||||
/**
|
||||
* This DTO represents a purchase information for a part (supplier name, order number and prices).
|
||||
* @see \App\Tests\Services\InfoProviderSystem\DTOs\PurchaseInfoDTOTest
|
||||
*/
|
||||
class PurchaseInfoDTO
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ use App\Entity\Parts\ManufacturingStatus;
|
|||
|
||||
/**
|
||||
* This DTO represents a search result for a part.
|
||||
* @see \App\Tests\Services\InfoProviderSystem\DTOs\SearchResultDTOTest
|
||||
*/
|
||||
class SearchResultDTO
|
||||
{
|
||||
|
|
|
@ -45,6 +45,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
|
||||
/**
|
||||
* This class converts DTOs to entities which can be persisted in the DB
|
||||
* @see \App\Tests\Services\InfoProviderSystem\DTOtoEntityConverterTest
|
||||
*/
|
||||
final class DTOtoEntityConverter
|
||||
{
|
||||
|
@ -127,7 +128,7 @@ final class DTOtoEntityConverter
|
|||
$entity->setAttachmentType($type);
|
||||
|
||||
//If no name is given, try to extract the name from the URL
|
||||
if (empty($dto->name)) {
|
||||
if ($dto->name === null || $dto->name === '' || $dto->name === '0') {
|
||||
$entity->setName($this->getAttachmentNameFromURL($dto->url));
|
||||
} else {
|
||||
$entity->setName($dto->name);
|
||||
|
|
|
@ -27,6 +27,7 @@ use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
|||
|
||||
/**
|
||||
* This class keeps track of all registered info providers and allows to find them by their key
|
||||
* @see \App\Tests\Services\InfoProviderSystem\ProviderRegistryTest
|
||||
*/
|
||||
final class ProviderRegistry
|
||||
{
|
||||
|
|
|
@ -93,7 +93,7 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
public function isActive(): bool
|
||||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
return !empty($this->clientId) && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return $this->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
|
@ -210,7 +210,7 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
$footprint_name = $parameter['Value'];
|
||||
}
|
||||
|
||||
if (in_array(trim($parameter['Value']), array('', '-'), true)) {
|
||||
if (in_array(trim((string) $parameter['Value']), ['', '-'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class Element14Provider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return !empty($this->settings->apiKey);
|
||||
return $this->settings->storeId !== null && $this->settings->apiKey !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -36,7 +36,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||
class LCSCProvider implements InfoProviderInterface
|
||||
{
|
||||
|
||||
private const ENDPOINT_URL = 'https://wmsc.lcsc.com/wmsc';
|
||||
private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm';
|
||||
|
||||
public const DISTRIBUTOR_NAME = 'LCSC';
|
||||
|
||||
|
@ -97,7 +97,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
*/
|
||||
private function getRealDatasheetUrl(?string $url): string
|
||||
{
|
||||
if (!empty($url) && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
|
||||
if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
|
||||
$response = $this->lcscClient->request('GET', $url, [
|
||||
'headers' => [
|
||||
'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
|
||||
|
@ -140,7 +140,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
// LCSC does not display LCSC codes in the search, instead taking you directly to the
|
||||
// detailed product listing. It does so utilizing a product tip field.
|
||||
// If product tip exists and there are no products in the product list try a detail query
|
||||
if (count($products) === 0 && !($tipProductCode === null)) {
|
||||
if (count($products) === 0 && $tipProductCode !== null) {
|
||||
$result[] = $this->queryDetail($tipProductCode);
|
||||
}
|
||||
|
||||
|
@ -175,11 +175,11 @@ class LCSCProvider implements InfoProviderInterface
|
|||
{
|
||||
// Get product images in advance
|
||||
$product_images = $this->getProductImages($product['productImages'] ?? null);
|
||||
$product['productImageUrl'] = $product['productImageUrl'] ?? null;
|
||||
$product['productImageUrl'] ??= null;
|
||||
|
||||
// If the product does not have a product image but otherwise has attached images, use the first one.
|
||||
if (count($product_images) > 0) {
|
||||
$product['productImageUrl'] = $product['productImageUrl'] ?? $product_images[0]->url;
|
||||
$product['productImageUrl'] ??= $product_images[0]->url;
|
||||
}
|
||||
|
||||
// LCSC puts HTML in footprints and descriptions sometimes randomly
|
||||
|
@ -322,7 +322,7 @@ class LCSCProvider implements InfoProviderInterface
|
|||
foreach ($attributes as $attribute) {
|
||||
|
||||
//Skip this attribute if it's empty
|
||||
if (in_array(trim($attribute['paramValueEn']), array('', '-'), true)) {
|
||||
if (in_array(trim((string) $attribute['paramValueEn']), ['', '-'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return !empty($this->settings->apiKey);
|
||||
return $this->settings->apiKey !== '' && $this->settings->apiKey !== null;
|
||||
}
|
||||
|
||||
public function searchByKeyword(string $keyword): array
|
||||
|
@ -175,7 +175,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
}
|
||||
|
||||
if (count($tmp) > 1) {
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id);
|
||||
throw new \RuntimeException('Multiple parts found with ID '.$id . ' ('.count($tmp).' found). This is basically a bug in Mousers API response. See issue #616.');
|
||||
}
|
||||
|
||||
return $tmp[0];
|
||||
|
@ -209,6 +209,12 @@ class MouserProvider implements InfoProviderInterface
|
|||
$result = [];
|
||||
foreach ($products as $product) {
|
||||
|
||||
//Check if we have a valid product number. We assume that a product number, must have at least 4 characters
|
||||
//Otherwise filter it out
|
||||
if (strlen($product['MouserPartNumber']) < 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Check if we have a mass field available
|
||||
$mass = null;
|
||||
if (isset($product['UnitWeightKg']['UnitWeight'])) {
|
||||
|
@ -245,7 +251,7 @@ class MouserProvider implements InfoProviderInterface
|
|||
|
||||
private function parseDataSheets(?string $sheetUrl, ?string $sheetName): ?array
|
||||
{
|
||||
if (empty($sheetUrl)) {
|
||||
if ($sheetUrl === null || $sheetUrl === '' || $sheetUrl === '0') {
|
||||
return null;
|
||||
}
|
||||
$result = [];
|
||||
|
|
|
@ -183,7 +183,7 @@ class OctopartProvider implements InfoProviderInterface
|
|||
{
|
||||
//The client ID has to be set and a token has to be available (user clicked connect)
|
||||
//return /*!empty($this->clientId) && */ $this->authTokenManager->hasToken(self::OAUTH_APP_NAME);
|
||||
return !empty($this->clientId) && !empty($this->secret);
|
||||
return $this->clientId !== '' && $this->secret !== '';
|
||||
}
|
||||
|
||||
private function mapLifeCycleStatus(?string $value): ?ManufacturingStatus
|
||||
|
@ -243,11 +243,14 @@ class OctopartProvider implements InfoProviderInterface
|
|||
//If we encounter the mass spec, we save it for later
|
||||
if ($spec['attribute']['shortname'] === "weight") {
|
||||
$mass = (float) $spec['siValue'];
|
||||
} else if ($spec['attribute']['shortname'] === "case_package") { //Package
|
||||
} elseif ($spec['attribute']['shortname'] === "case_package") {
|
||||
//Package
|
||||
$package = $spec['value'];
|
||||
} else if ($spec['attribute']['shortname'] === "numberofpins") { //Pin Count
|
||||
} elseif ($spec['attribute']['shortname'] === "numberofpins") {
|
||||
//Pin Count
|
||||
$pinCount = $spec['value'];
|
||||
} else if ($spec['attribute']['shortname'] === "lifecyclestatus") { //LifeCycleStatus
|
||||
} elseif ($spec['attribute']['shortname'] === "lifecyclestatus") {
|
||||
//LifeCycleStatus
|
||||
$mStatus = $this->mapLifeCycleStatus($spec['value']);
|
||||
}
|
||||
|
||||
|
@ -295,7 +298,7 @@ class OctopartProvider implements InfoProviderInterface
|
|||
$category = null;
|
||||
if (!empty($part['category']['name'])) {
|
||||
$category = implode(' -> ', array_map(static fn($c) => $c['name'], $part['category']['ancestors'] ?? []));
|
||||
if (!empty($category)) {
|
||||
if ($category !== '' && $category !== '0') {
|
||||
$category .= ' -> ';
|
||||
}
|
||||
$category .= $part['category']['name'];
|
||||
|
|
|
@ -78,7 +78,7 @@ class TMEProvider implements InfoProviderInterface
|
|||
$result[] = new SearchResultDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['Symbol'],
|
||||
name: !empty($product['OriginalSymbol']) ? $product['OriginalSymbol'] : $product['Symbol'],
|
||||
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
|
||||
description: $product['Description'],
|
||||
category: $product['Category'],
|
||||
manufacturer: $product['Producer'],
|
||||
|
@ -114,7 +114,7 @@ class TMEProvider implements InfoProviderInterface
|
|||
return new PartDetailDTO(
|
||||
provider_key: $this->getProviderKey(),
|
||||
provider_id: $product['Symbol'],
|
||||
name: !empty($product['OriginalSymbol']) ? $product['OriginalSymbol'] : $product['Symbol'],
|
||||
name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'],
|
||||
description: $product['Description'],
|
||||
category: $product['Category'],
|
||||
manufacturer: $product['Producer'],
|
||||
|
|
|
@ -28,6 +28,7 @@ use Com\Tecnick\Barcode\Barcode;
|
|||
|
||||
/**
|
||||
* This function is used to generate barcodes of various types using arbitrary (text) content.
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeHelperTest
|
||||
*/
|
||||
class BarcodeHelper
|
||||
{
|
||||
|
@ -66,7 +67,7 @@ class BarcodeHelper
|
|||
{
|
||||
$svg = $this->barcodeAsSVG($content, $type);
|
||||
$base64 = $this->dataUri($svg, 'image/svg+xml');
|
||||
$alt_text = $alt_text ?? $content;
|
||||
$alt_text ??= $content;
|
||||
|
||||
return '<img src="'.$base64.'" width="'.$width.'" style="min-height: 25px;" alt="'.$alt_text.'"/>';
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeNormalizerTest
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeScanHelperTest
|
||||
*/
|
||||
final class BarcodeScanHelper
|
||||
{
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\LabelSystem;
|
||||
|
||||
use Dompdf\Dompdf;
|
||||
|
@ -36,10 +38,8 @@ class DompdfFactory implements DompdfFactoryInterface
|
|||
|
||||
private function createDirectoryIfNotExisting(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
if (!mkdir($concurrentDirectory = $path, 0777, true) && !is_dir($concurrentDirectory)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
|
||||
}
|
||||
if (!is_dir($path) && (!mkdir($concurrentDirectory = $path, 0777, true) && !is_dir($concurrentDirectory))) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,4 +51,4 @@ class DompdfFactory implements DompdfFactoryInterface
|
|||
'tempDir' => $this->tmpDirectory,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ use App\Services\LabelSystem\Barcodes\BarcodeHelper;
|
|||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\LabelSystem\BarcodeGeneratorTest
|
||||
* @see \App\Tests\Services\LabelSystem\LabelBarcodeGeneratorTest
|
||||
*/
|
||||
final class LabelBarcodeGenerator
|
||||
{
|
||||
|
|
|
@ -97,7 +97,7 @@ final class LabelExampleElementsGenerator
|
|||
|
||||
$lot->setDescription('Example Lot');
|
||||
$lot->setComment('Lot comment');
|
||||
$lot->setExpirationDate(new DateTime('+1 days'));
|
||||
$lot->setExpirationDate(new \DateTimeImmutable('+1 day'));
|
||||
$lot->setStorageLocation($this->getStructuralData(StorageLocation::class));
|
||||
$lot->setAmount(123);
|
||||
$lot->setOwner($this->getUser());
|
||||
|
|
|
@ -86,7 +86,7 @@ final class GlobalProviders implements PlaceholderProviderInterface
|
|||
return 'anonymous';
|
||||
}
|
||||
|
||||
$now = new DateTime();
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
if ('[[DATETIME]]' === $placeholder) {
|
||||
$formatter = IntlDateFormatter::create(
|
||||
|
|
|
@ -119,7 +119,7 @@ final class PartProvider implements PlaceholderProviderInterface
|
|||
}
|
||||
|
||||
if ('[[DESCRIPTION_T]]' === $placeholder) {
|
||||
return strip_tags($parsedown->line($part->getDescription()));
|
||||
return strip_tags((string) $parsedown->line($part->getDescription()));
|
||||
}
|
||||
|
||||
if ('[[COMMENT]]' === $placeholder) {
|
||||
|
@ -127,7 +127,7 @@ final class PartProvider implements PlaceholderProviderInterface
|
|||
}
|
||||
|
||||
if ('[[COMMENT_T]]' === $placeholder) {
|
||||
return strip_tags($parsedown->line($part->getComment()));
|
||||
return strip_tags((string) $parsedown->line($part->getComment()));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -52,7 +52,7 @@ final class StructuralDBElementProvider implements PlaceholderProviderInterface
|
|||
return $label_target->getComment();
|
||||
}
|
||||
if ('[[COMMENT_T]]' === $placeholder) {
|
||||
return strip_tags($label_target->getComment());
|
||||
return strip_tags((string) $label_target->getComment());
|
||||
}
|
||||
if ('[[FULL_PATH]]' === $placeholder) {
|
||||
return $label_target->getFullPath();
|
||||
|
|
|
@ -42,7 +42,6 @@ declare(strict_types=1);
|
|||
namespace App\Services\LabelSystem\PlaceholderProviders;
|
||||
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use DateTime;
|
||||
use IntlDateFormatter;
|
||||
use Locale;
|
||||
|
||||
|
@ -57,11 +56,11 @@ final class TimestampableElementProvider implements PlaceholderProviderInterface
|
|||
$formatter = new IntlDateFormatter(Locale::getDefault(), IntlDateFormatter::SHORT, IntlDateFormatter::SHORT);
|
||||
|
||||
if ('[[LAST_MODIFIED]]' === $placeholder) {
|
||||
return $formatter->format($label_target->getLastModified() ?? new DateTime());
|
||||
return $formatter->format($label_target->getLastModified() ?? new \DateTimeImmutable());
|
||||
}
|
||||
|
||||
if ('[[CREATION_DATE]]' === $placeholder) {
|
||||
return $formatter->format($label_target->getAddedDate() ?? new DateTime());
|
||||
return $formatter->format($label_target->getAddedDate() ?? new \DateTimeImmutable());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\LogSystem;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
|
|
@ -136,7 +136,7 @@ class LogDataFormatter
|
|||
}
|
||||
|
||||
try {
|
||||
$dateTime = new \DateTime($date, new \DateTimeZone($timezone));
|
||||
$dateTime = new \DateTimeImmutable($date, new \DateTimeZone($timezone));
|
||||
} catch (\Exception) {
|
||||
return '<i>unknown DateTime format</i>';
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ class LogEntryExtraFormatter
|
|||
}
|
||||
|
||||
if ($context instanceof LogWithCommentInterface && $context->hasComment()) {
|
||||
$array[] = htmlspecialchars($context->getComment());
|
||||
$array[] = htmlspecialchars((string) $context->getComment());
|
||||
}
|
||||
|
||||
if ($context instanceof ElementCreatedLogEntry && $context->hasCreationInstockValue()) {
|
||||
|
@ -193,7 +193,7 @@ class LogEntryExtraFormatter
|
|||
htmlspecialchars($this->elementTypeNameGenerator->getLocalizedTypeLabel(PartLot::class))
|
||||
.' ' . $context->getMoveToTargetID();
|
||||
}
|
||||
if ($context->getActionTimestamp()) {
|
||||
if ($context->getActionTimestamp() !== null) {
|
||||
$formatter = new \IntlDateFormatter($this->translator->getLocale(), \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT);
|
||||
$array['log.part_stock_changed.timestamp'] = $formatter->format($context->getActionTimestamp());
|
||||
}
|
||||
|
|
|
@ -34,12 +34,14 @@ use App\Repository\LogEntryRepository;
|
|||
use Brick\Math\BigDecimal;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
|
||||
class TimeTravel
|
||||
{
|
||||
|
@ -54,8 +56,11 @@ class TimeTravel
|
|||
/**
|
||||
* Undeletes the element with the given ID.
|
||||
*
|
||||
* @template T of AbstractDBElement
|
||||
* @param string $class The class name of the element that should be undeleted
|
||||
* @phpstan-param class-string<T> $class
|
||||
* @param int $id the ID of the element that should be undeleted
|
||||
* @phpstan-return T
|
||||
*/
|
||||
public function undeleteEntity(string $class, int $id): AbstractDBElement
|
||||
{
|
||||
|
@ -136,16 +141,16 @@ class TimeTravel
|
|||
|
||||
//Revert many-to-one association (one element in property)
|
||||
if (
|
||||
ClassMetadataInfo::MANY_TO_ONE === $mapping['type']
|
||||
|| ClassMetadataInfo::ONE_TO_ONE === $mapping['type']
|
||||
ClassMetadata::MANY_TO_ONE === $mapping['type']
|
||||
|| ClassMetadata::ONE_TO_ONE === $mapping['type']
|
||||
) {
|
||||
$target_element = $this->getField($element, $field);
|
||||
if (null !== $target_element && $element->getLastModified() > $timestamp) {
|
||||
$this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements);
|
||||
}
|
||||
} elseif ( //Revert *_TO_MANY associations (collection properties)
|
||||
(ClassMetadataInfo::MANY_TO_MANY === $mapping['type']
|
||||
|| ClassMetadataInfo::ONE_TO_MANY === $mapping['type'])
|
||||
(ClassMetadata::MANY_TO_MANY === $mapping['type']
|
||||
|| ClassMetadata::ONE_TO_MANY === $mapping['type'])
|
||||
&& !$mapping['isOwningSide']
|
||||
) {
|
||||
$target_elements = $this->getField($element, $field);
|
||||
|
@ -171,17 +176,26 @@ class TimeTravel
|
|||
/**
|
||||
* This function decodes the array which is created during the json_encode of a datetime object and returns a DateTime object.
|
||||
* @param array $input
|
||||
* @return DateTime
|
||||
* @return \DateTimeInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
private function dateTimeDecode(?array $input): ?\DateTime
|
||||
private function dateTimeDecode(?array $input, string $doctrineType): ?\DateTimeInterface
|
||||
{
|
||||
//Allow null values
|
||||
if ($input === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new \DateTime($input['date'], new \DateTimeZone($input['timezone']));
|
||||
//Mutable types
|
||||
if (in_array($doctrineType, [Types::DATETIME_MUTABLE, Types::DATE_MUTABLE], true)) {
|
||||
return new \DateTime($input['date'], new \DateTimeZone($input['timezone']));
|
||||
}
|
||||
//Immutable types
|
||||
if (in_array($doctrineType, [Types::DATETIME_IMMUTABLE, Types::DATE_IMMUTABLE], true)) {
|
||||
return new \DateTimeImmutable($input['date'], new \DateTimeZone($input['timezone']));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('The given doctrine type is not a datetime type!');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,12 +218,14 @@ class TimeTravel
|
|||
foreach ($old_data as $field => $data) {
|
||||
if ($metadata->hasField($field)) {
|
||||
//We need to convert the string to a BigDecimal first
|
||||
if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)['type'])) {
|
||||
if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) {
|
||||
$data = BigDecimal::of($data);
|
||||
}
|
||||
|
||||
if (!$data instanceof DateTime && ('datetime' === $metadata->getFieldMapping($field)['type'])) {
|
||||
$data = $this->dateTimeDecode($data);
|
||||
if (!$data instanceof \DateTimeInterface
|
||||
&& (in_array($metadata->getFieldMapping($field)->type,
|
||||
[Types::DATETIME_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATE_MUTABLE, Types::DATETIME_IMMUTABLE], true))) {
|
||||
$data = $this->dateTimeDecode($data, $metadata->getFieldMapping($field)->type);
|
||||
}
|
||||
|
||||
$this->setField($element, $field, $data);
|
||||
|
@ -219,7 +235,7 @@ class TimeTravel
|
|||
$target_class = $mapping['targetEntity'];
|
||||
//Try to extract the old ID:
|
||||
if (is_array($data) && isset($data['@id'])) {
|
||||
$entity = $this->em->getPartialReference($target_class, $data['@id']);
|
||||
$entity = $this->em->getReference($target_class, $data['@id']);
|
||||
$this->setField($element, $field, $entity);
|
||||
}
|
||||
}
|
||||
|
@ -241,8 +257,20 @@ class TimeTravel
|
|||
*/
|
||||
protected function setField(AbstractDBElement $element, string $field, mixed $new_value): void
|
||||
{
|
||||
$reflection = new ReflectionClass($element::class);
|
||||
$property = $reflection->getProperty($field);
|
||||
//If the field name contains a dot, it is a embeddedable object and we need to split the field name
|
||||
if (str_contains($field, '.')) {
|
||||
[$embedded, $embedded_field] = explode('.', $field);
|
||||
|
||||
$elementClass = new ReflectionClass($element::class);
|
||||
$property = $elementClass->getProperty($embedded);
|
||||
$embeddedClass = $property->getValue($element);
|
||||
|
||||
$embeddedReflection = new ReflectionClass($embeddedClass::class);
|
||||
$property = $embeddedReflection->getProperty($embedded_field);
|
||||
} else {
|
||||
$reflection = new ReflectionClass($element::class);
|
||||
$property = $reflection->getProperty($field);
|
||||
}
|
||||
|
||||
//Check if the property is an BackedEnum, then convert the int or float value to an enum instance
|
||||
if ((is_string($new_value) || is_int($new_value))
|
||||
|
|
|
@ -25,7 +25,8 @@ namespace App\Services\Misc;
|
|||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
|
@ -50,10 +51,14 @@ class DBInfoHelper
|
|||
return 'mysql';
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) {
|
||||
return 'sqlite';
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
|
||||
return 'postgresql';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -67,10 +72,14 @@ class DBInfoHelper
|
|||
return $this->connection->fetchOne('SELECT VERSION()');
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) {
|
||||
return $this->connection->fetchOne('SELECT sqlite_version()');
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
|
||||
return $this->connection->fetchOne('SELECT version()');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -89,7 +98,7 @@ class DBInfoHelper
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) {
|
||||
try {
|
||||
return (int) $this->connection->fetchOne('SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();');
|
||||
} catch (Exception) {
|
||||
|
@ -97,6 +106,14 @@ class DBInfoHelper
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
|
||||
try {
|
||||
return (int) $this->connection->fetchOne('SELECT pg_database_size(current_database())');
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -121,9 +138,17 @@ class DBInfoHelper
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
|
||||
if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) {
|
||||
return 'sqlite';
|
||||
}
|
||||
|
||||
if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) {
|
||||
try {
|
||||
return $this->connection->fetchOne('SELECT current_user');
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -47,11 +47,10 @@ final class OAuthTokenManager
|
|||
$tokenEntity = $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]);
|
||||
|
||||
//If the token was already existing, we just replace it with the new one
|
||||
if ($tokenEntity) {
|
||||
if ($tokenEntity !== null) {
|
||||
$tokenEntity->replaceWithNewToken($token);
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
$this->entityManager->flush($tokenEntity);
|
||||
$this->entityManager->flush();
|
||||
|
||||
//We are done
|
||||
return $tokenEntity;
|
||||
|
@ -60,8 +59,8 @@ final class OAuthTokenManager
|
|||
//If the token was not existing, we create a new one
|
||||
$tokenEntity = OAuthToken::fromAccessToken($token, $app_name);
|
||||
$this->entityManager->persist($tokenEntity);
|
||||
//@phpstan-ignore-next-line
|
||||
$this->entityManager->flush($tokenEntity);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $tokenEntity;
|
||||
}
|
||||
|
@ -97,7 +96,7 @@ final class OAuthTokenManager
|
|||
{
|
||||
$token = $this->getToken($app_name);
|
||||
|
||||
if (!$token) {
|
||||
if ($token === null) {
|
||||
throw new \RuntimeException('No token was saved yet for '.$app_name);
|
||||
}
|
||||
|
||||
|
@ -113,9 +112,7 @@ final class OAuthTokenManager
|
|||
|
||||
//Persist the token
|
||||
$token->replaceWithNewToken($new_token);
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
$this->entityManager->flush($token);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
@ -131,7 +128,7 @@ final class OAuthTokenManager
|
|||
$token = $this->getToken($app_name);
|
||||
|
||||
//If the token is not existing, we return null
|
||||
if (!$token) {
|
||||
if ($token === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ final class SidebarTreeUpdater
|
|||
//This tag and therfore this whole cache gets cleared by TreeCacheInvalidationListener when a structural element is changed
|
||||
$item->tag('sidebar_tree_update');
|
||||
|
||||
return new \DateTime();
|
||||
return new \DateTimeImmutable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,8 +59,7 @@ class PasswordResetManager
|
|||
$user->setPwResetToken($this->passwordEncoder->hash($unencrypted_token));
|
||||
|
||||
//Determine the expiration datetime of
|
||||
$expiration_date = new DateTime();
|
||||
$expiration_date->add(date_interval_create_from_date_string('1 day'));
|
||||
$expiration_date = new \DateTimeImmutable("+1 day");
|
||||
$user->setPwResetExpires($expiration_date);
|
||||
|
||||
if ($user->getEmail() !== null && $user->getEmail() !== '') {
|
||||
|
@ -105,7 +104,7 @@ class PasswordResetManager
|
|||
}
|
||||
|
||||
//Check if token is expired yet
|
||||
if ($user->getPwResetExpires() < new DateTime()) {
|
||||
if ($user->getPwResetExpires() < new \DateTimeImmutable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -119,7 +118,7 @@ class PasswordResetManager
|
|||
|
||||
//Remove token
|
||||
$user->setPwResetToken(null);
|
||||
$user->setPwResetExpires(new DateTime());
|
||||
$user->setPwResetExpires(new \DateTimeImmutable());
|
||||
|
||||
//Save to DB
|
||||
$this->em->flush();
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* 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/>.
|
||||
*/
|
||||
|
||||
namespace App\Services\UserSystem\TFA;
|
||||
|
||||
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
|
||||
|
@ -67,4 +69,4 @@ class DecoratedGoogleAuthenticator implements GoogleAuthenticatorInterface
|
|||
{
|
||||
return $this->inner->generateSecret();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ use App\Security\ApiTokenAuthenticatedToken;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Services\UserSystem\VoterHelperTest
|
||||
*/
|
||||
final class VoterHelper
|
||||
{
|
||||
private readonly UserRepository $userRepository;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue