add exceptions, fix auth, implement response, sanitize recipient and sender

This commit is contained in:
Daniel Seifert 2022-06-24 14:35:17 +02:00
parent 0cdfd0185b
commit 50cf733101
Signed by: DanielS
GPG Key ID: 8A7C4C6ED1915C6F
17 changed files with 286 additions and 76 deletions

48
.github/workflows/code-checks.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: CodeChecks
on:
push:
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php:
- '7.3'
- '7.4'
- '8.0'
name: PHP ${{ matrix.php }} tests
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-php${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php${{ matrix.php }}-
- name: Composer
run: composer install --no-progress
- name: PHPUnit
run: vendor/bin/phpunit --coverage-clover=coverage.xml
- name: "Upload coverage to Codecov"
uses: "codecov/codecov-action@v2"
with:
fail_ci_if_error: true

View File

@ -19,7 +19,7 @@
], ],
"require": { "require": {
"php": "^7.0", "php": "^7.0",
"beberlei/assert": "^2.7", "beberlei/assert": "^2.9",
"guzzlehttp/guzzle": "~6.2", "guzzlehttp/guzzle": "~6.2",
"psr/http-message": "~1.0", "psr/http-message": "~1.0",
"symfony/options-resolver": "^3.0|^4.0", "symfony/options-resolver": "^3.0|^4.0",

View File

@ -15,7 +15,10 @@
namespace D3\LinkmobilityClient; namespace D3\LinkmobilityClient;
use D3\LinkmobilityClient\Exceptions\ApiException;
use D3\LinkmobilityClient\Request\RequestInterface; use D3\LinkmobilityClient\Request\RequestInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
class Client class Client
{ {
@ -34,10 +37,11 @@ class Client
$this->requestClient = $client ?: new \GuzzleHttp\Client( [ 'base_uri' => $this->apiUrl->getBaseUri() ] ); $this->requestClient = $client ?: new \GuzzleHttp\Client( [ 'base_uri' => $this->apiUrl->getBaseUri() ] );
} }
public function request(RequestInterface $request) : ResponseInterface public function request(RequestInterface $request) : \D3\LinkmobilityClient\Response\ResponseInterface
{ {
$request->validate(); $request->validate();
$responseClass = $request->getResponseClass(); $responseClass = $request->getResponseClass();
return $request->getResponseInstance( return $request->getResponseInstance(
$this->rawRequest($request->getUri(), $request->getMethod(), $request->getOptions()) $this->rawRequest($request->getUri(), $request->getMethod(), $request->getOptions())
); );
@ -48,22 +52,14 @@ class Client
* @param string $method * @param string $method
* @param array $postArgs * @param array $postArgs
* *
* @return string * @return ResponseInterface
* @throws ApiException * @throws ApiException
* @throws GuzzleException * @throws GuzzleException
*/ */
protected function rawRequest( $url, string $method = RequestInterface::METHOD_GET, array $options = []): string protected function rawRequest( $url, string $method = RequestInterface::METHOD_GET, array $options = []): ResponseInterface
{ {
$options['headers']['Authorization'] = 'access_token '.$this->accessToken; $options['headers']['Authorization'] = 'Bearer '.$this->accessToken;
if (!empty($body)) {
$options['json'] = $body;
}
dumpvar(__METHOD__.__LINE__.PHP_EOL);
dumpvar($options);
dumpVar($method);
dumpvar($url);
die();
$response = $this->requestClient->request( $response = $this->requestClient->request(
$method, $method,
$url, $url,

View File

@ -0,0 +1,21 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @link http://www.oxidmodule.com
*/
namespace D3\LinkmobilityClient\Exceptions;
class ApiException extends LinkmobilityException
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @link http://www.oxidmodule.com
*/
declare(strict_types=1);
namespace D3\LinkmobilityClient\Exceptions;
class LinkmobilityException extends \Exception
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @link http://www.oxidmodule.com
*/
declare(strict_types=1);
namespace D3\LinkmobilityClient\Exceptions;
class RecipientException extends LinkmobilityException
{
}

View File

@ -18,6 +18,7 @@ declare(strict_types=1);
namespace D3\LinkmobilityClient\RecipientsList; namespace D3\LinkmobilityClient\RecipientsList;
use D3\LinkmobilityClient\ValueObject\Recipient; use D3\LinkmobilityClient\ValueObject\Recipient;
use libphonenumber\PhoneNumberType;
class RecipientsList implements RecipientsListInterface, \Iterator class RecipientsList implements RecipientsListInterface, \Iterator
{ {
@ -26,14 +27,33 @@ class RecipientsList implements RecipientsListInterface, \Iterator
*/ */
private $recipients = []; private $recipients = [];
public function add(Recipient $recipient) public function add(Recipient $recipient) : RecipientsListInterface
{ {
$this->recipients[md5(serialize($recipient))] = $recipient; $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
try {
$phoneNumber = $phoneUtil->parse($recipient->get(), $recipient->getCountryCode());
if (false === $phoneUtil->isValidNumber($phoneNumber)) {
throw new \D3\LinkmobilityClient\Exceptions\RecipientException('invalid recipient phone number');
} elseif (false === in_array($phoneUtil->getNumberType($phoneNumber), [PhoneNumberType::MOBILE, PhoneNumberType::FIXED_LINE_OR_MOBILE])) {
throw new \D3\LinkmobilityClient\Exceptions\RecipientException('not a mobile number');
} }
public function clearRecipents() $this->recipients[md5(serialize($recipient))] = $recipient;
} catch (\libphonenumber\NumberParseException $e) {
// var_dump($e);
} catch (\D3\LinkmobilityClient\Exceptions\RecipientException $e) {
// var_dump($e);
}
return $this;
}
public function clearRecipents() : RecipientsListInterface
{ {
$this->recipients = []; $this->recipients = [];
return $this;
} }
public function getRecipients() : array public function getRecipients() : array

View File

@ -21,9 +21,9 @@ use D3\LinkmobilityClient\ValueObject\Recipient;
interface RecipientsListInterface interface RecipientsListInterface
{ {
public function add(Recipient $recipient); public function add(Recipient $recipient) : RecipientsListInterface;
public function clearRecipents(); public function clearRecipents() : RecipientsListInterface;
public function getRecipients() : array; public function getRecipients() : array;
} }

View File

@ -25,6 +25,7 @@ use D3\LinkmobilityClient\ValueObject\Recipient;
use D3\LinkmobilityClient\ValueObject\Sender; use D3\LinkmobilityClient\ValueObject\Sender;
use D3\LinkmobilityClient\ValueObject\SmsMessage; use D3\LinkmobilityClient\ValueObject\SmsMessage;
use D3\LinkmobilityClient\ValueObject\StringValueObject; use D3\LinkmobilityClient\ValueObject\StringValueObject;
use GuzzleHttp\RequestOptions;
use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Registry;
abstract class Request implements RequestInterface abstract class Request implements RequestInterface
@ -125,7 +126,8 @@ abstract class Request implements RequestInterface
Assert::that($this->getOptions())->isArray(); Assert::that($this->getOptions())->isArray();
Assert::that( $this->getRecipientsList() )->isInstanceOf(RecipientsList::class)->notEmpty(); Assert::that( $this->getRecipientsList() )->isInstanceOf(RecipientsList::class)->notEmpty();
Assert::thatAll( $this->getRecipientsList() )->isInstanceOf( Recipient::class ); Assert::that( $this->getRecipientsList()->getRecipients())->notEmpty();
Assert::thatAll( $this->getRecipientsList() )->isInstanceOf( Recipient::class )->notEmpty();
// optional properties // optional properties
Assert::thatNullOr( $this->getClientMessageId() )->string(); Assert::thatNullOr( $this->getClientMessageId() )->string();
@ -151,7 +153,7 @@ abstract class Request implements RequestInterface
'priority' => $this->getPriority(), 'priority' => $this->getPriority(),
'recipientAddressList' => $this->getRecipientsList()->getRecipients(), 'recipientAddressList' => $this->getRecipientsList()->getRecipients(),
'sendAsFlashSms' => $this->getSendAsFlashSms(), 'sendAsFlashSms' => $this->getSendAsFlashSms(),
'senderAddress' => $this->getSenderAddress()->get(), 'senderAddress' => $this->getSenderAddress() ? $this->getSenderAddress()->get() : null,
'senderAddressType' => $this->getSenderAddressType(), 'senderAddressType' => $this->getSenderAddressType(),
'test' => $this->isTest(), 'test' => $this->isTest(),
'validityPeriode' => $this->getValidityPeriode() 'validityPeriode' => $this->getValidityPeriode()
@ -166,7 +168,7 @@ abstract class Request implements RequestInterface
switch ($this->getContentType()) { switch ($this->getContentType()) {
case RequestInterface::CONTENTTYPE_JSON: case RequestInterface::CONTENTTYPE_JSON:
return ['json' => json_encode($body)]; return [RequestOptions::JSON => $body];
default: default:
return $body; return $body;
} }
@ -180,7 +182,6 @@ abstract class Request implements RequestInterface
'Accept' => $this->contentType, 'Accept' => $this->contentType,
'Content-Type' => $this->contentType, 'Content-Type' => $this->contentType,
] ]
], ],
$this->getBody() $this->getBody()
); );
@ -382,9 +383,9 @@ abstract class Request implements RequestInterface
} }
/** /**
* @return string|null * @return Sender|null
*/ */
public function getSenderAddress() : Sender public function getSenderAddress()
{ {
return $this->senderAddress; return $this->senderAddress;
} }
@ -444,6 +445,7 @@ abstract class Request implements RequestInterface
*/ */
public function getResponseInstance(\Psr\Http\Message\ResponseInterface $rawResponse): ResponseInterface public function getResponseInstance(\Psr\Http\Message\ResponseInterface $rawResponse): ResponseInterface
{ {
return new $this->getResponseClass($rawResponse); $FQClassName = $this->getResponseClass();
return new $FQClassName($rawResponse);
} }
} }

View File

@ -8,26 +8,78 @@ use Assert\Assert;
abstract class Response implements ResponseInterface abstract class Response implements ResponseInterface
{ {
const STATUSCODE = 'statusCode';
const STATUSMESSAGE = 'statusMessage';
const CLIENTMESSAGEID = 'clientMessageId';
const TRANSFERID = 'transferId';
const SMSCOUNT = 'smsCount';
/** /**
* @var array * @var \Psr\Http\Message\ResponseInterface
*/ */
protected $data; protected $rawResponse;
protected $content;
/** /**
* @var int * @var int
*/ */
protected $status; protected $status;
public function __construct(array $data) public function __construct(\Psr\Http\Message\ResponseInterface $rawResponse)
{ {
Assert::that($data)->keyExists('status'); $this->rawResponse = $rawResponse;
$this->content = json_decode($this->rawResponse->getBody()->getContents(), true);
$this->data = $data;
$this->status = (int)$this->data['status'];
if ($this->isSuccessful()) {
$this->init();
} }
public function getRawResponse() : \Psr\Http\Message\ResponseInterface
{
return $this->rawResponse;
}
public function getContent()
{
return $this->content;
}
/**
* @return int
*/
public function getInternalStatus() : int
{
return $this->getContent()[self::STATUSCODE];
}
/**
* @return string
*/
public function getStatusMessage() : string
{
return $this->getContent()[self::STATUSMESSAGE];
}
/**
* @return string|null
*/
public function getClientMessageId()
{
return $this->getContent()[self::CLIENTMESSAGEID];
}
/**
* @return string|null
*/
public function getTransferId()
{
return $this->getContent()[self::TRANSFERID];
}
/**
* @return string|null
*/
public function getSmsCount() : int
{
return $this->getContent()[self::SMSCOUNT];
} }
/** /**
@ -35,11 +87,13 @@ abstract class Response implements ResponseInterface
*/ */
public function isSuccessful(): bool public function isSuccessful(): bool
{ {
return $this->status >= 200 && $this->status < 300; $status = $this->getInternalStatus();
return $status >= 2000 && $status <= 2999;
} }
public function getError(): string public function getErrorMessage(): string
{ {
return $this->isSuccessful() ? '' : $this->data['message']; return $this->isSuccessful() ? '' : $this->getStatusMessage();
} }
} }

View File

@ -6,24 +6,21 @@ namespace D3\LinkmobilityClient\Response;
interface ResponseInterface interface ResponseInterface
{ {
/** public function __construct(\Psr\Http\Message\ResponseInterface $rawResponse);
* Should instantiate the object from the data given
*/
public function init() : void;
public function __construct(array $data); public function getRawResponse() : \Psr\Http\Message\ResponseInterface;
public function getInternalStatus() : int;
public function getStatusMessage() : string;
public function getClientMessageId();
public function getTransferId();
public function getSmsCount() : int;
/**
* Must return true if the request was successful
*
* @return bool
*/
public function isSuccessful(): bool; public function isSuccessful(): bool;
/** public function getErrorMessage(): string;
* This must return the error, if any occurred, else it must return ''
*
* @return string
*/
public function getError() : string;
} }

View File

@ -133,7 +133,7 @@ class TextRequest extends \D3\LinkmobilityClient\Request\Request
public function getUri(): string public function getUri(): string
{ {
return '/smsmessaging/text/'; return '/rest/smsmessaging/text';
} }
public function validate(): void public function validate(): void

View File

@ -5,21 +5,36 @@ declare(strict_types=1);
namespace D3\LinkmobilityClient\ValueObject; namespace D3\LinkmobilityClient\ValueObject;
use Assert\Assert; use Assert\Assert;
use libphonenumber\PhoneNumberType;
class Recipient extends StringValueObject class Recipient extends StringValueObject
{ {
public function __construct(string $value) /**
* @var string
*/
private $countryCode;
public function __construct(string $number, string $iso2CountryCode)
{ {
// ohne +, dafür mit Ländervorwahl Assert::that($iso2CountryCode)->string()->length(2);
// eine führende 0 scheint lokale Version
// zwei führende Nullen einfach weggeschnitten
//https://github.com/matmar10/msisdn-format-bundle/blob/master/Matmar10/Bundle/MsisdnFormatBundle/Resources/config/msisdn-country-formats.xml $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
try {
$phoneNumber = $phoneUtil->parse($number, strtoupper($iso2CountryCode));
$number = ltrim($phoneUtil->format($phoneNumber, \libphonenumber\PhoneNumberFormat::E164), '+');
} catch (\libphonenumber\NumberParseException $e) {
var_dump($e);
}
parent::__construct($number);
$this->countryCode = $iso2CountryCode;
}
// valid formats can be found here: https://linkmobility.atlassian.net/wiki/spaces/COOL/pages/26017807/08.+Messages#id-08.Messages-recipients /**
Assert::that($value)->regex('/^(\+|c)?[0-9]+$/i', 'Recipient does not match valid phone number.'); * @return string
*/
parent::__construct($value); public function getCountryCode() :string
{
return $this->countryCode;
} }
} }

View File

@ -8,13 +8,22 @@ use Assert\Assert;
class Sender extends StringValueObject class Sender extends StringValueObject
{ {
public function __construct(string $value) public function __construct(string $number, string $iso2CountryCode)
{ {
// if the sender is alphanumeric, test the length Assert::that($iso2CountryCode)->string()->length(2);
if (!preg_match('/^\+[0-9]+$/i', $value)) {
Assert::that($value)->maxLength(11); $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
try {
$phoneNumber = $phoneUtil->parse( $number, strtoupper($iso2CountryCode) );
$number = $phoneUtil->format( $phoneNumber, \libphonenumber\PhoneNumberFormat::E164 );
if (false === $phoneUtil->isValidNumber($phoneNumber)) {
throw new \D3\LinkmobilityClient\Exceptions\RecipientException( 'invalid sender phone number' );
}
} catch (\libphonenumber\NumberParseException $e) {
var_dump($e);
} }
parent::__construct($value); parent::__construct( $number);
} }
} }

View File

@ -12,9 +12,9 @@ class SmsMessage extends StringValueObject
const GSM_7BIT = '7-bit'; const GSM_7BIT = '7-bit';
const GSM_UCS2 = 'ucs-2'; const GSM_UCS2 = 'ucs-2';
public function __construct(string $value) public function __construct(string $number)
{ {
parent::__construct($value); parent::__construct( $number);
$smsLength = new SmsLength($this->value); $smsLength = new SmsLength($this->value);
$smsLength->validate(); $smsLength->validate();

View File

@ -8,11 +8,11 @@ use Assert\Assert;
abstract class StringValueObject extends ValueObject abstract class StringValueObject extends ValueObject
{ {
public function __construct(string $value) public function __construct(string $number)
{ {
Assert::that($value)->notEmpty(); Assert::that( $number)->notEmpty();
$this->value = $value; $this->value = $number;
} }
public function __toString() public function __toString()

2
todo.txt Normal file
View File

@ -0,0 +1,2 @@
RecipientsList als eigene Klasse
Debug mit none, error, all log