Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PISHPS-329 UrlParsingService - matching trackign code from tracking url #798

Closed
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@
<argument key="$envAppUrl">%env(default::APP_URL)%</argument>
</service>

<service id="Kiener\MolliePayments\Service\TrackingInfoStructFactory"/>
<service id="Kiener\MolliePayments\Service\UrlParsingService"/>

<service id="Kiener\MolliePayments\Service\TrackingInfoStructFactory">
<argument type="service" id="Kiener\MolliePayments\Service\UrlParsingService"/>
</service>


<service id="Kiener\MolliePayments\Subscriber\KernelSubscriber">
Expand Down
4 changes: 3 additions & 1 deletion src/Resources/config/services/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@

<service id="Kiener\MolliePayments\Service\MollieApi\PriceCalculator"/>

<service id="Kiener\MolliePayments\Service\MollieApi\LineItemDataExtractor"/>
<service id="Kiener\MolliePayments\Service\MollieApi\LineItemDataExtractor">
<argument type="service" id="Kiener\MolliePayments\Service\UrlParsingService"/>
</service>


<service id="Kiener\MolliePayments\Service\Order\OrderStatusUpdater">
Expand Down
46 changes: 11 additions & 35 deletions src/Service/MollieApi/LineItemDataExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Kiener\MolliePayments\Service\MollieApi;

use Kiener\MolliePayments\Service\UrlParsingService;
use Kiener\MolliePayments\Struct\LineItemExtraData;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Content\Media\MediaEntity;
Expand All @@ -13,6 +14,15 @@

class LineItemDataExtractor
{
/**
* @var UrlParsingService
*/
private $urlParsingService;
public function __construct(UrlParsingService $urlParsingService)
{
$this->urlParsingService = $urlParsingService;
}

public function extractExtraData(OrderLineItemEntity $lineItem): LineItemExtraData
{
$product = $lineItem->getProduct();
Expand All @@ -30,7 +40,7 @@ public function extractExtraData(OrderLineItemEntity $lineItem): LineItemExtraDa
&& $medias->first()->getMedia() instanceof MediaEntity
) {
$url = $medias->first()->getMedia()->getUrl();
$url = $this->encodePathAndQuery($url);
$url = $this->urlParsingService->encodePathAndQuery($url);
$extraData->setImageUrl($url);
}

Expand All @@ -43,38 +53,4 @@ public function extractExtraData(OrderLineItemEntity $lineItem): LineItemExtraDa

return $extraData;
}

private function encodePathAndQuery(string $fullUrl):string
{
$urlParts = parse_url($fullUrl);

$scheme = isset($urlParts['scheme']) ? $urlParts['scheme'] . '://' : '';

$host = isset($urlParts['host']) ? $urlParts['host'] : '';

$port = isset($urlParts['port']) ? ':' . $urlParts['port'] : '';

$user = isset($urlParts['user']) ? $urlParts['user'] : '';

$pass = isset($urlParts['pass']) ? ':' . $urlParts['pass'] : '';

$pass = ($user || $pass) ? "$pass@" : '';

$path = isset($urlParts['path']) ? $urlParts['path'] : '';

if (mb_strlen($path) > 0) {
$pathParts = explode('/', $path);
array_walk($pathParts, function (&$pathPart) {
$pathPart = rawurlencode($pathPart);
});
$path = implode('/', $pathParts);
}

$query = isset($urlParts['query']) ? '?' . $urlParts['query'] : '';


$fragment = isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '';

return trim($scheme.$user.$pass.$host.$port.$path.$query.$fragment);
}
}
24 changes: 17 additions & 7 deletions src/Service/TrackingInfoStructFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ class TrackingInfoStructFactory
{
use StringTrait;

/**
* @var UrlParsingService
*/
private $urlParsingService;

public function __construct(UrlParsingService $urlParsingService)
{
$this->urlParsingService = $urlParsingService;
}


/**
* Mollie throws an error with length >= 100
Expand Down Expand Up @@ -91,6 +101,11 @@ private function createInfoStruct(string $trackingCarrier, string $trackingCode,
throw new \InvalidArgumentException('Missing Argument for Tracking Code!');
}

// determine if the provided tracking code is actually a tracking URL
if (empty($trackingUrl) === true || $this->urlParsingService->isUrl($trackingCode)) {
[$trackingCode, $trackingUrl] = $this->urlParsingService->parseTrackingCodeFromUrl($trackingCode);
}

# we just have to completely remove those codes, so that no tracking happens, but a shipping works.
# still, if we find multiple codes (because separators exist), then we use the first one only
if (mb_strlen($trackingCode) > self::MAX_TRACKING_CODE_LENGTH) {
Expand All @@ -114,13 +129,8 @@ private function createInfoStruct(string $trackingCarrier, string $trackingCode,

$trackingUrl = trim(sprintf($trackingUrl, $trackingCode));

if (filter_var($trackingUrl, FILTER_VALIDATE_URL) === false) {
$trackingUrl = '';
}

# following characters are not allowed in the tracking URL {,},<,>,#
if (preg_match_all('/[{}<>#]/m', $trackingUrl)) {
$trackingUrl = '';
if ($this->urlParsingService->isUrl($trackingUrl) === false) {
return new ShipmentTrackingInfoStruct($trackingCarrier, $trackingCode, '');
}

return new ShipmentTrackingInfoStruct($trackingCarrier, $trackingCode, $trackingUrl);
Expand Down
131 changes: 131 additions & 0 deletions src/Service/UrlParsingService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);

namespace Kiener\MolliePayments\Service;

class UrlParsingService
{
/**
* Checks if a given string is a valid URL.
*
* @param string $value The string to be checked.
* @return bool True if the string is a valid URL, false otherwise.
*/
public function isUrl(string $value): bool
{
return filter_var($value, FILTER_VALIDATE_URL) !== false;
}

/**
* Parses the tracking code from a given URL.
*
* This method searches for tracking codes in the URL in the following formats:
* - As a query parameter (e.g., ?code=12345)
* - As a path segment (e.g., /code/12345/)
* - As a hash fragment (e.g., #code=12345)
*
* @param string $value The URL to be parsed.
* @return array{0: string, 1: string} An array where:
* - Index 0 contains the parsed tracking code (if found), or an empty string if no code is found.
* - Index 1 contains the original URL.
*/
public function parseTrackingCodeFromUrl(string $value): array
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that can be dangerous, since we dont know for each provier the tracking code. i would rather suggest extract url and put query parameters as code (if code contains URL)

{
// Case 1: Query parameter
if ((bool)preg_match('#(code|shipment|track|tracking)=([a-zA-Z0-9]+)#i', $value, $matches)) {
return [$matches[2], $value];
}

// Case 2: Path-based tracking
if ((bool)preg_match('#/(code|shipment|track|tracking)/([a-zA-Z0-9]+)/#i', $value, $matches)) {
return [$matches[2], $value];
}

// Case 3: Hash-based tracking
if ((bool)preg_match('#\#(code|shipment|track|tracking)=([a-zA-Z0-9]+)#i', $value, $matches)) {
return [$matches[2], $value];
}

// could not determine code
return ['', $value];
}

public function encodePathAndQuery(string $fullUrl):string
{
$urlParts = parse_url($fullUrl);

$scheme = isset($urlParts['scheme']) ? $urlParts['scheme'] . '://' : '';

$host = isset($urlParts['host']) ? $urlParts['host'] : '';

$port = isset($urlParts['port']) ? ':' . $urlParts['port'] : '';

$user = isset($urlParts['user']) ? $urlParts['user'] : '';

$pass = isset($urlParts['pass']) ? ':' . $urlParts['pass'] : '';

$pass = ($user || $pass) ? "$pass@" : '';

$path = isset($urlParts['path']) ? $urlParts['path'] : '';

if (mb_strlen($path) > 0) {
$pathParts = explode('/', $path);
array_walk($pathParts, function (&$pathPart) {
$pathPart = rawurlencode($pathPart);
});
$path = implode('/', $pathParts);
}

$query = '';
if (isset($urlParts['query'])) {
$urlParts['query'] = $this->sanitizeQuery(explode('&', $urlParts['query']));
$query = '?' . implode('&', $urlParts['query']);
}


$fragment = isset($urlParts['fragment']) ? '#' . rawurlencode($urlParts['fragment']) : '';

return trim($scheme.$user.$pass.$host.$port.$path.$query.$fragment);
}

/**
* Sanitizes an array of query strings by URL encoding their components.
*
* This method takes an array of query strings, where each string is expected to be in the format
* 'key=value'. It applies the sanitizeQueryPart method to each query string to ensure the keys
* and values are URL encoded, making them safe for use in URLs.
*
* @param string[] $query An array of query strings to be sanitized.
* @return string[] The sanitized array with URL encoded query strings.
*/
public function sanitizeQuery(array $query): array
{
// Use array_map to apply the sanitizeQueryPart method to each element of the $query array
return array_map([$this, 'sanitizeQueryPart'], $query);
}

/**
* Sanitizes a single query string part by URL encoding its key and value.
*
* This method takes a query string part, expected to be in the format 'key=value', splits it into
* its key and value components, URL encodes each component, and then recombines them into a single
* query string part.
*
* @param string $queryPart A single query string part to be sanitized.
* @return string The sanitized query string part with URL encoded components.
*/
public function sanitizeQueryPart(string $queryPart): string
{
if (strpos($queryPart, '=') === false) {
return $queryPart;
}

// Split the query part into key and value based on the '=' delimiter
[$key, $value] = explode('=', $queryPart);

$key = rawurlencode($key);
$value = rawurlencode($value);

return sprintf('%s=%s', $key, $value);
}
}
2 changes: 1 addition & 1 deletion src/Subscriber/OrderDeliverySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function onOrderDeliveryChanged(StateMachineStateChangeEvent $event): voi

$this->mollieShipment->shipOrderRest($order, null, $event->getContext());
} catch (\Throwable $ex) {
$this->logger->error('Failed to transfer delivery state to mollie: '.$ex->getMessage());
$this->logger->error('Failed to transfer delivery state to mollie: '.$ex->getMessage(), ['exception' => $ex]);
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Kiener\MolliePayments\Service\OrderService;
use Kiener\MolliePayments\Service\TrackingInfoStructFactory;
use Kiener\MolliePayments\Service\Transition\DeliveryTransitionService;
use Kiener\MolliePayments\Service\UrlParsingService;
use MolliePayments\Tests\Fakes\FakeShipment;
use MolliePayments\Tests\Traits\OrderTrait;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -65,7 +66,7 @@ public function setUp(): void
$orderService,
$deliveryExtractor,
new OrderItemsExtractor(),
new TrackingInfoStructFactory()
new TrackingInfoStructFactory(new UrlParsingService())
);

$this->context = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Kiener\MolliePayments\Service\Router\RoutingDetector;
use Kiener\MolliePayments\Service\SettingsService;
use Kiener\MolliePayments\Service\Transition\TransactionTransitionServiceInterface;
use Kiener\MolliePayments\Service\UrlParsingService;
use Kiener\MolliePayments\Setting\MollieSettingStruct;
use Kiener\MolliePayments\Validator\IsOrderLineItemValid;
use MolliePayments\Tests\Fakes\FakeCompatibilityGateway;
Expand Down Expand Up @@ -180,7 +181,7 @@ public function setUp(): void
new MollieLineItemBuilder(
new IsOrderLineItemValid(),
new PriceCalculator(),
new LineItemDataExtractor(),
new LineItemDataExtractor(new UrlParsingService()),
new FakeCompatibilityGateway(),
new RoundingDifferenceFixer(),
new MollieLineItemHydrator(new MollieOrderPriceBuilder()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Kiener\MolliePayments\Service\MollieApi\Fixer\RoundingDifferenceFixer;
use Kiener\MolliePayments\Service\MollieApi\LineItemDataExtractor;
use Kiener\MolliePayments\Service\MollieApi\PriceCalculator;
use Kiener\MolliePayments\Service\UrlParsingService;
use Kiener\MolliePayments\Setting\MollieSettingStruct;
use Kiener\MolliePayments\Validator\IsOrderLineItemValid;
use Mollie\Api\Types\OrderLineType;
Expand Down Expand Up @@ -38,7 +39,7 @@ public function setUp(): void
$this->builder = new MollieLineItemBuilder(
(new IsOrderLineItemValid()),
(new PriceCalculator()),
(new LineItemDataExtractor()),
(new LineItemDataExtractor(new UrlParsingService())),
new FakeCompatibilityGateway(),
new RoundingDifferenceFixer(),
new MollieLineItemHydrator(new MollieOrderPriceBuilder()),
Expand Down
11 changes: 6 additions & 5 deletions tests/PHPUnit/Service/MollieApi/LineItemDataExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace MolliePayments\Tests\Service\MollieApi;

use Kiener\MolliePayments\Service\MollieApi\LineItemDataExtractor;
use Kiener\MolliePayments\Service\UrlParsingService;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Content\Media\MediaEntity;
Expand All @@ -17,7 +18,7 @@ class LineItemDataExtractorTest extends TestCase
{
public function testWithMissingProduct(): void
{
$extractor = new LineItemDataExtractor();
$extractor = new LineItemDataExtractor(new UrlParsingService());
$lineItemId = Uuid::randomHex();
$lineItem = new OrderLineItemEntity();
$lineItem->setId($lineItemId);
Expand All @@ -31,7 +32,7 @@ public function testWithMissingProduct(): void
public function testNoMediaNoSeo(): void
{
$expected = 'foo';
$extractor = new LineItemDataExtractor();
$extractor = new LineItemDataExtractor(new UrlParsingService());
$lineItem = new OrderLineItemEntity();
$product = new ProductEntity();
$product->setProductNumber($expected);
Expand All @@ -47,7 +48,7 @@ public function testMediaExtraction(): void
{
$expectedImageUrl = 'https://bar.baz';
$expectedProductNumber = 'foo';
$extractor = new LineItemDataExtractor();
$extractor = new LineItemDataExtractor(new UrlParsingService());
$lineItem = new OrderLineItemEntity();
$product = new ProductEntity();
$product->setProductNumber($expectedProductNumber);
Expand All @@ -71,7 +72,7 @@ public function testSeoUrlExtraction(): void
{
$expectedSeoUrl = 'https://bar.foo';
$expectedProductNumber = 'foo';
$extractor = new LineItemDataExtractor();
$extractor = new LineItemDataExtractor(new UrlParsingService());
$lineItem = new OrderLineItemEntity();
$product = new ProductEntity();
$product->setProductNumber($expectedProductNumber);
Expand All @@ -93,7 +94,7 @@ public function testCompleteExtraction(): void
$expectedImageUrl = 'https://bar.baz';
$expectedSeoUrl = 'https://bar.foo';
$expectedProductNumber = 'foo';
$extractor = new LineItemDataExtractor();
$extractor = new LineItemDataExtractor(new UrlParsingService());
$lineItem = new OrderLineItemEntity();
$product = new ProductEntity();
$product->setProductNumber($expectedProductNumber);
Expand Down
Loading