From 49ee9131d00f531cbcfdfd841db5096d1b128e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 27 Mar 2025 20:59:22 +0100 Subject: [PATCH 1/6] Use composer/ca-bundle instead of system CA for element14 provider This is a workaround for debian systems, where the required root CA is missing as trusted CA in the system CAs. This fixes issue #891 and #866 --- composer.json | 2 +- composer.lock | 2 +- .../Providers/Element14Provider.php | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 3b2904d6..c725b235 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "api-platform/core": "^3.1", "beberlei/doctrineextensions": "^1.2", "brick/math": "0.12.1 as 0.11.0", - "composer/ca-bundle": "^1.3", + "composer/ca-bundle": "^1.5", "composer/package-versions-deprecated": "^1.11.99.5", "doctrine/data-fixtures": "^2.0.0", "doctrine/dbal": "^4.0.0", diff --git a/composer.lock b/composer.lock index cf422f6b..12abd82f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "75643d42e05fce4684644d375bff2d0a", + "content-hash": "7fb73581b0074c5a79afb3ffa614ed8e", "packages": [ { "name": "amphp/amp", diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php index eb1d4675..b942b929 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use Composer\CaBundle\CaBundle; use Symfony\Contracts\HttpClient\HttpClientInterface; class Element14Provider implements InfoProviderInterface @@ -43,9 +44,19 @@ class Element14Provider implements InfoProviderInterface private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant', 'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode']; - public function __construct(private readonly HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id) - { + private readonly HttpClientInterface $element14Client; + public function __construct(HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id) + { + /* We use the mozilla CA from the composer ca bundle directly, as some debian systems seems to have problems + * with the SSL.COM CA, element14 uses. See https://github.com/Part-DB/Part-DB-server/issues/866 + * + * This is a workaround until the issue is resolved in debian (or never). + * As this only affects this provider, this should have no negative impact and the CA bundle is still secure. + */ + $this->element14Client = $element14Client->withOptions([ + 'cafile' => CaBundle::getBundledCaBundlePath(), + ]); } public function getProviderInfo(): array From 7275db27e7728d10e34d248b8e93741843a3c2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 27 Mar 2025 21:06:50 +0100 Subject: [PATCH 2/6] Manually filter mouser search results to fix the edgecase, that the API returned multiple results for an exact part number This fixes issue #888 and issue #616 --- .../InfoProviderSystem/Providers/MouserProvider.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php index c36fab66..90bad263 100644 --- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -94,6 +94,7 @@ class MouserProvider implements InfoProviderInterface From the startingRecord, the number of records specified will be returned up to the end of the recordset. This is useful for paging through the complete recordset of parts matching keyword. + searchOptions string Optional. If not provided, the default is None. @@ -176,11 +177,16 @@ class MouserProvider implements InfoProviderInterface throw new \RuntimeException('No part found with ID '.$id); } + //Manually filter out the part with the correct ID + $tmp = array_filter($tmp, fn(PartDetailDTO $part) => $part->provider_id === $id); + if (count($tmp) === 0) { + throw new \RuntimeException('No part found with ID '.$id); + } if (count($tmp) > 1) { - throw new \RuntimeException('Multiple parts found with ID '.$id . ' ('.count($tmp).' found). This is basically a bug in Mousers API response. See issue #616.'); + throw new \RuntimeException('Multiple parts found with ID '.$id); } - return $tmp[0]; + return reset($tmp); } public function getCapabilities(): array From d3b225771c9297b46f9e1f955ddf5da0f98809c0 Mon Sep 17 00:00:00 2001 From: Daniel Carrasco <1249741+Danixu@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:26:18 +0100 Subject: [PATCH 3/6] Modified the DigiKey Provider to works with the V4 API (#875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modified the DigiKey Provider to works with the V4 API * Correclty apply the MarketPlaceFilter option to digikey v4 API * Show the packe type (Tape&Reel, Box, etc.) as footprint in digikey provider search --------- Co-authored-by: Jan Böhmer --- .../Providers/DigikeyProvider.php | 117 +++++++++++------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index d8e93321..1eb2a219 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -108,12 +108,15 @@ class DigikeyProvider implements InfoProviderInterface { $request = [ 'Keywords' => $keyword, - 'RecordCount' => 50, - 'RecordStartPosition' => 0, - 'ExcludeMarketPlaceProducts' => 'true', + 'Limit' => 50, + 'Offset' => 0, + 'FilterOptionsRequest' => [ + 'MarketPlaceFilter' => 'ExcludeMarketPlace', + ], ]; - $response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [ + //$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [ + $response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [ 'json' => $request, 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) ]); @@ -124,18 +127,21 @@ class DigikeyProvider implements InfoProviderInterface $result = []; $products = $response_array['Products']; foreach ($products as $product) { - $result[] = new SearchResultDTO( - provider_key: $this->getProviderKey(), - provider_id: $product['DigiKeyPartNumber'], - name: $product['ManufacturerPartNumber'], - description: $product['DetailedDescription'] ?? $product['ProductDescription'], - category: $this->getCategoryString($product), - manufacturer: $product['Manufacturer']['Value'] ?? null, - mpn: $product['ManufacturerPartNumber'], - preview_image_url: $product['PrimaryPhoto'] ?? null, - manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']), - provider_url: $product['ProductUrl'], - ); + foreach ($product['ProductVariations'] as $variation) { + $result[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $variation['DigiKeyProductNumber'], + name: $product['ManufacturerProductNumber'], + description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], + category: $this->getCategoryString($product), + manufacturer: $product['Manufacturer']['Name'] ?? null, + mpn: $product['ManufacturerProductNumber'], + preview_image_url: $product['PhotoUrl'] ?? null, + manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']), + provider_url: $product['ProductUrl'], + footprint: $variation['PackageType']['Name'], //Use the footprint field, to show the user the package type (Tape & Reel, etc., as digikey has many different package types) + ); + } } return $result; @@ -143,32 +149,42 @@ class DigikeyProvider implements InfoProviderInterface public function getDetails(string $id): PartDetailDTO { - $response = $this->digikeyClient->request('GET', '/Search/v3/Products/' . urlencode($id), [ + $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [ 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) ]); - $product = $response->toArray(); + $response_array = $response->toArray(); + $product = $response_array['Product']; $footprint = null; $parameters = $this->parametersToDTOs($product['Parameters'] ?? [], $footprint); - $media = $this->mediaToDTOs($product['MediaLinks']); + $media = $this->mediaToDTOs($id); + + // Get the price_breaks of the selected variation + $price_breaks = []; + foreach ($product['ProductVariations'] as $variation) { + if ($variation['DigiKeyProductNumber'] == $id) { + $price_breaks = $variation['StandardPricing'] ?? []; + break; + } + } return new PartDetailDTO( provider_key: $this->getProviderKey(), - provider_id: $product['DigiKeyPartNumber'], - name: $product['ManufacturerPartNumber'], - description: $product['DetailedDescription'] ?? $product['ProductDescription'], + provider_id: $id, + name: $product['ManufacturerProductNumber'], + description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], category: $this->getCategoryString($product), - manufacturer: $product['Manufacturer']['Value'] ?? null, - mpn: $product['ManufacturerPartNumber'], - preview_image_url: $product['PrimaryPhoto'] ?? null, - manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']), + manufacturer: $product['Manufacturer']['Name'] ?? null, + mpn: $product['ManufacturerProductNumber'], + preview_image_url: $product['PhotoUrl'] ?? null, + manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']), provider_url: $product['ProductUrl'], footprint: $footprint, datasheets: $media['datasheets'], images: $media['images'], parameters: $parameters, - vendor_infos: $this->pricingToDTOs($product['StandardPricing'] ?? [], $product['DigiKeyPartNumber'], $product['ProductUrl']), + vendor_infos: $this->pricingToDTOs($price_breaks, $id, $product['ProductUrl']), ); } @@ -177,28 +193,35 @@ class DigikeyProvider implements InfoProviderInterface * @param string|null $dk_status * @return ManufacturingStatus|null */ - private function productStatusToManufacturingStatus(?string $dk_status): ?ManufacturingStatus + private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus { + // The V4 can use strings to get the status, but if you have changed the PROVIDER_DIGIKEY_LANGUAGE it will not match. + // Using the Id instead which should be fixed. + // + // The API is not well documented and the ID are not there yet, so were extracted using "trial and error". + // The 'Preliminary' id was not found in several categories so I was unable to extract it. Disabled for now. return match ($dk_status) { null => null, - 'Active' => ManufacturingStatus::ACTIVE, - 'Obsolete' => ManufacturingStatus::DISCONTINUED, - 'Discontinued at Digi-Key', 'Last Time Buy' => ManufacturingStatus::EOL, - 'Not For New Designs' => ManufacturingStatus::NRFND, - 'Preliminary' => ManufacturingStatus::ANNOUNCED, + 0 => ManufacturingStatus::ACTIVE, + 1 => ManufacturingStatus::DISCONTINUED, + 2, 4 => ManufacturingStatus::EOL, + 7 => ManufacturingStatus::NRFND, + //'Preliminary' => ManufacturingStatus::ANNOUNCED, default => ManufacturingStatus::NOT_SET, }; } private function getCategoryString(array $product): string { - $category = $product['Category']['Value']; - $sub_category = $product['Family']['Value']; + $category = $product['Category']['Name']; + $sub_category = current($product['Category']['ChildCategories']); - //Replace the ' - ' category separator with ' -> ' - $sub_category = str_replace(' - ', ' -> ', $sub_category); + if ($sub_category) { + //Replace the ' - ' category separator with ' -> ' + $category = $category . ' -> ' . str_replace(' - ', ' -> ', $sub_category["Name"]); + } - return $category . ' -> ' . $sub_category; + return $category; } /** @@ -215,18 +238,18 @@ class DigikeyProvider implements InfoProviderInterface foreach ($parameters as $parameter) { if ($parameter['ParameterId'] === 1291) { //Meaning "Manufacturer given footprint" - $footprint_name = $parameter['Value']; + $footprint_name = $parameter['ValueText']; } - if (in_array(trim((string) $parameter['Value']), ['', '-'], true)) { + if (in_array(trim((string) $parameter['ValueText']), ['', '-'], true)) { continue; } //If the parameter was marked as text only, then we do not try to parse it as a numerical value if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) { - $results[] = new ParameterDTO(name: $parameter['Parameter'], value_text: $parameter['Value']); + $results[] = new ParameterDTO(name: $parameter['ParameterText'], value_text: $parameter['ValueText']); } else { //Otherwise try to parse it as a numerical value - $results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']); + $results[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterText'], $parameter['ValueText']); } } @@ -258,12 +281,18 @@ class DigikeyProvider implements InfoProviderInterface * @return FileDTO[][] * @phpstan-return array */ - private function mediaToDTOs(array $media_links): array + private function mediaToDTOs(string $id): array { $datasheets = []; $images = []; - foreach ($media_links as $media_link) { + $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/media', [ + 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) + ]); + + $media_array = $response->toArray(); + + foreach ($media_array['MediaLinks'] as $media_link) { $file = new FileDTO(url: $media_link['Url'], name: $media_link['Title']); switch ($media_link['MediaType']) { From 1daf6f01f4d776464f3a53a1029b22ca52f0ccda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 27 Mar 2025 21:40:51 +0100 Subject: [PATCH 4/6] Fixed error 500 if internal attachment path was not resolvable to an URL This fixes issue #898 --- src/Services/EntityURLGenerator.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index c66b5fd9..78db06f0 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -156,8 +156,10 @@ class EntityURLGenerator public function viewURL(Attachment $entity): string { - if ($entity->hasInternal()) { - return $this->attachmentURLGenerator->getInternalViewURL($entity); + //If the underlying file path is invalid, null gets returned, which is not allowed here. + //We still have the chance to use an external path, if it is set. + if ($entity->hasInternal() && ($url = $this->attachmentURLGenerator->getInternalViewURL($entity)) !== null) { + return $url; } if($entity->hasExternal()) { From 059a9683db3626bfa6ac4ab346f11241c10b2ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 27 Mar 2025 21:47:52 +0100 Subject: [PATCH 5/6] Fixed problem that global_theme setting was not respected This fixes issue #880 --- templates/base.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html.twig b/templates/base.html.twig index 3b4cebba..e7c641c6 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -38,7 +38,7 @@ - {% if not app.user.theme is defined %} + {% if not app.user.theme is defined or app.user.theme is null %} {% set theme = global_theme %} {% else %} {% set theme = app.user.theme %} From 03e1105a8efcc3495ac34e9b60f68173b1ea37b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 27 Mar 2025 23:11:49 +0100 Subject: [PATCH 6/6] Fixed phpstan issues --- src/Services/InfoProviderSystem/Providers/DigikeyProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index 1eb2a219..b20368ce 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -190,7 +190,7 @@ class DigikeyProvider implements InfoProviderInterface /** * Converts the product status from the Digikey API to the manufacturing status used in Part-DB - * @param string|null $dk_status + * @param int|null $dk_status * @return ManufacturingStatus|null */ private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus @@ -277,7 +277,7 @@ class DigikeyProvider implements InfoProviderInterface } /** - * @param array $media_links + * @param string $id The Digikey product number, to get the media for * @return FileDTO[][] * @phpstan-return array */