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": {
"php": "^7.0",
"beberlei/assert": "^2.7",
"beberlei/assert": "^2.9",
"guzzlehttp/guzzle": "~6.2",
"psr/http-message": "~1.0",
"symfony/options-resolver": "^3.0|^4.0",

View File

@ -15,7 +15,10 @@
namespace D3\LinkmobilityClient;
use D3\LinkmobilityClient\Exceptions\ApiException;
use D3\LinkmobilityClient\Request\RequestInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
class Client
{
@ -34,10 +37,11 @@ class Client
$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();
$responseClass = $request->getResponseClass();
return $request->getResponseInstance(
$this->rawRequest($request->getUri(), $request->getMethod(), $request->getOptions())
);
@ -48,22 +52,14 @@ class Client
* @param string $method
* @param array $postArgs
*
* @return string
* @return ResponseInterface
* @throws ApiException
* @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;
if (!empty($body)) {
$options['json'] = $body;
}
dumpvar(__METHOD__.__LINE__.PHP_EOL);
dumpvar($options);
dumpVar($method);
dumpvar($url);
die();
$options['headers']['Authorization'] = 'Bearer '.$this->accessToken;
$response = $this->requestClient->request(
$method,
$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;
use D3\LinkmobilityClient\ValueObject\Recipient;
use libphonenumber\PhoneNumberType;
class RecipientsList implements RecipientsListInterface, \Iterator
{
@ -26,14 +27,33 @@ class RecipientsList implements RecipientsListInterface, \Iterator
*/
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');
}
$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()
public function clearRecipents() : RecipientsListInterface
{
$this->recipients = [];
return $this;
}
public function getRecipients() : array

View File

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

View File

@ -25,6 +25,7 @@ use D3\LinkmobilityClient\ValueObject\Recipient;
use D3\LinkmobilityClient\ValueObject\Sender;
use D3\LinkmobilityClient\ValueObject\SmsMessage;
use D3\LinkmobilityClient\ValueObject\StringValueObject;
use GuzzleHttp\RequestOptions;
use OxidEsales\Eshop\Core\Registry;
abstract class Request implements RequestInterface
@ -125,7 +126,8 @@ abstract class Request implements RequestInterface
Assert::that($this->getOptions())->isArray();
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
Assert::thatNullOr( $this->getClientMessageId() )->string();
@ -151,10 +153,10 @@ abstract class Request implements RequestInterface
'priority' => $this->getPriority(),
'recipientAddressList' => $this->getRecipientsList()->getRecipients(),
'sendAsFlashSms' => $this->getSendAsFlashSms(),
'senderAddress' => $this->getSenderAddress()->get(),
'senderAddress' => $this->getSenderAddress() ? $this->getSenderAddress()->get() : null,
'senderAddressType' => $this->getSenderAddressType(),
'test' => $this->isTest(),
'validityPeriode' => $this->getValidityPeriode()
'validityPeriode' => $this->getValidityPeriode()
];
}
@ -166,7 +168,7 @@ abstract class Request implements RequestInterface
switch ($this->getContentType()) {
case RequestInterface::CONTENTTYPE_JSON:
return ['json' => json_encode($body)];
return [RequestOptions::JSON => $body];
default:
return $body;
}
@ -180,7 +182,6 @@ abstract class Request implements RequestInterface
'Accept' => $this->contentType,
'Content-Type' => $this->contentType,
]
],
$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;
}
@ -444,6 +445,7 @@ abstract class Request implements RequestInterface
*/
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
{
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
*/
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);
}
public function getRawResponse() : \Psr\Http\Message\ResponseInterface
{
return $this->rawResponse;
}
$this->data = $data;
$this->status = (int)$this->data['status'];
public function getContent()
{
return $this->content;
}
if ($this->isSuccessful()) {
$this->init();
}
/**
* @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
{
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
{
/**
* Should instantiate the object from the data given
*/
public function init() : void;
public function __construct(\Psr\Http\Message\ResponseInterface $rawResponse);
public function __construct(array $data);
public function getRawResponse() : \Psr\Http\Message\ResponseInterface;
/**
* Must return true if the request was successful
*
* @return bool
*/
public function isSuccessful() : bool;
public function getInternalStatus() : int;
/**
* This must return the error, if any occurred, else it must return ''
*
* @return string
*/
public function getError() : string;
public function getStatusMessage() : string;
public function getClientMessageId();
public function getTransferId();
public function getSmsCount() : int;
public function isSuccessful(): bool;
public function getErrorMessage(): string;
}

View File

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

View File

@ -5,21 +5,36 @@ declare(strict_types=1);
namespace D3\LinkmobilityClient\ValueObject;
use Assert\Assert;
use libphonenumber\PhoneNumberType;
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
// eine führende 0 scheint lokale Version
// zwei führende Nullen einfach weggeschnitten
Assert::that($iso2CountryCode)->string()->length(2);
//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.');
parent::__construct($value);
/**
* @return string
*/
public function getCountryCode() :string
{
return $this->countryCode;
}
}

View File

@ -8,13 +8,22 @@ use Assert\Assert;
class Sender extends StringValueObject
{
public function __construct(string $value)
public function __construct(string $number, string $iso2CountryCode)
{
// if the sender is alphanumeric, test the length
if (!preg_match('/^\+[0-9]+$/i', $value)) {
Assert::that($value)->maxLength(11);
Assert::that($iso2CountryCode)->string()->length(2);
$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_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->validate();

View File

@ -8,11 +8,11 @@ use Assert\Assert;
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()

2
todo.txt Normal file
View File

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