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/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()) { diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index d8e93321..b20368ce 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,62 +149,79 @@ 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']), ); } /** * 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(?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']); } } @@ -254,16 +277,22 @@ 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 */ - 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']) { 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 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 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 %}