Skip to content

Commit

Permalink
PISHPS-329: added UrlParsingService to separate tracking code from tr…
Browse files Browse the repository at this point in the history
…acking url when not entered correctly (#805)
  • Loading branch information
m-muxfeld-diw authored Aug 8, 2024
1 parent 1549c12 commit d61758c
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 128 deletions.
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
91 changes: 11 additions & 80 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,83 +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 = '';
if (isset($urlParts['query'])) {
$urlParts['query'] = $this->sanitizeQuery(explode('&', $urlParts['query']));
$query = '?' . implode('&', $urlParts['query']);
}


$fragment = isset($urlParts['fragment']) ? '#' . $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.
*/
private 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.
*/
private 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);
}
}
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
{
// 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
Loading

0 comments on commit d61758c

Please sign in to comment.