diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..32a1e27
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f1ca35
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/coverage/
+/vendor/
+/composer.lock
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a5b9754
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: php
+
+php:
+ - '5.6'
+ - '7.0'
+
+before_script:
+ - composer self-update
+ - composer install --no-interaction --prefer-source --dev
+
+script: ./vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-text --coverage-clover=coverage.clover
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..178685a
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,3 @@
+CC BY-NC-SA
+
+See https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode for more informations.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d60b8e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+# `La Presse Libre` Client Library
+
+Unofficial PHP SDK for the project [La Presse Libre](https://github.com/NextINpact/LaPresseLibreSDK). The difference with the official package offered by NextINpact is compatibility with PSR4, PSR7 and php7 environment.
+
+## Usage
+
+```php
+$account_always_exists = function ($data, $is_testing) use ($public_key) {
+ $now = new DateTime('next year');
+
+ return [
+ 'Mail' => $data['Mail'],
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'TypeAbonnement' => SubscriptionType::MONTHLY,
+ 'DateExpiration' => $now->format("Y-m-d\TH:i:sO"),
+ 'DateSouscription' => $now->format("Y-m-d\TH:i:sO"),
+ 'AccountExist' => true,
+ 'PartenaireID' => $public_key,
+ ];
+};
+$verification = Endpoint::answer(Verification::class, $account_always_exists);
+```
+
+Detailed examples for each endpoints are available :
+
+- [exemples/verification.php](exemples/verification.php)
+- [exemples/account-creation.php](exemples/account-creation.php)
+- [exemples/account-update.php](exemples/account-update.php)
+- [exemples/register.php](exemples/register.php)
+
+## Installation
+
+Simply install this package with [Composer](http://getcomposer.org/).
+
+```bash
+composer require mediapart/lapresselibre
+```
+
+## Read More
+
+- Official `La Presse Libre` [documentation](https://github.com/NextINpact/LaPresseLibreSDK/wiki/) (fr).
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..2f75d6e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "mediapart/lapresselibre",
+ "type": "library",
+ "description": "Unofficial Client API of La Presse Libre project",
+ "license": "CC BY-NC-SA",
+ "authors": [
+ {
+ "name": "Thomas GASC",
+ "email": "thomas.gasc@mediapart.fr"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Mediapart\\LaPresseLibre\\": "src/",
+ "Mediapart\\LaPresseLibre\\Tests\\": "tests/"
+ }
+ },
+ "require": {
+ "php": "^5.6|^7.0",
+ "ext-json": "*",
+ "ext-openssl": "*",
+ "ext-hash": "*",
+ "psr/http-message": "^1.0",
+ "psr/log": "^1.0",
+ "symfony/options-resolver": "^2.8|^3.3"
+ },
+ "require-dev": {
+ "zendframework/zend-diactoros": "^1.4",
+ "phpunit/phpunit": "^5.7|^6.3"
+ },
+ "suggest": {
+ "monolog/monolog": "Allows more advanced logging of the application flow"
+ }
+}
diff --git a/exemples/account-creation.php b/exemples/account-creation.php
new file mode 100644
index 0000000..d3a7fcf
--- /dev/null
+++ b/exemples/account-creation.php
@@ -0,0 +1,60 @@
+ true,
+ 'PartenaireID' => $public_key,
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'CodeEtat' => AccountCreation::SUCCESS,
+ ];
+ };
+ $account_creation = Endpoint::answer(AccountCreation::class, $account_always_created);
+ $result = $transaction->process($account_creation);
+ $status = 200;
+} catch (\InvalidArgumentException $e) {
+ $result = $e->getMessage();
+ $status = 400;
+} catch (\UnexpectedValueException $e) {
+ $result = $e->getMessage();
+ $status = 401;
+} catch (\Exception $e) {
+ $result = 'Internal Error';
+ $status = 500;
+} finally {
+ $response = new Response(
+ 200 != $status ? json_encode(['error' => $result]) : $result,
+ $status,
+ [
+ 'X-PART' => (string) $public_key,
+ 'X-LPL' => $identity->sign($public_key),
+ 'X-TS' => (string) $identity->getDatetime()->getTimestamp(),
+ ]
+ );
+}
+
+$emitter = new SapiEmitter();
+$emitter->emit($response);
diff --git a/exemples/account-update.php b/exemples/account-update.php
new file mode 100644
index 0000000..96f5f8e
--- /dev/null
+++ b/exemples/account-update.php
@@ -0,0 +1,55 @@
+process($account_update);
+ $status = 200;
+} catch (\InvalidArgumentException $e) {
+ $result = $e->getMessage();
+ $status = 400;
+} catch (\UnexpectedValueException $e) {
+ $result = $e->getMessage();
+ $status = 401;
+} catch (\Exception $e) {
+ $result = 'Internal Error';
+ $status = 500;
+} finally {
+ $response = new Response(
+ 200 != $status ? json_encode(['error' => $result]) : $result,
+ $status,
+ [
+ 'X-PART' => (string) $public_key,
+ 'X-LPL' => $identity->sign($public_key),
+ 'X-TS' => (string) $identity->getDatetime()->getTimestamp(),
+ ]
+ );
+}
+
+$emitter = new SapiEmitter();
+$emitter->emit($response);
diff --git a/exemples/register.php b/exemples/register.php
new file mode 100644
index 0000000..10c1192
--- /dev/null
+++ b/exemples/register.php
@@ -0,0 +1,14 @@
+generateLink('username@domain.tld', 'username');
+
+echo 'register into La Presse Libre';
diff --git a/exemples/verification.php b/exemples/verification.php
new file mode 100644
index 0000000..3dc5057
--- /dev/null
+++ b/exemples/verification.php
@@ -0,0 +1,66 @@
+ $data['Mail'],
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'TypeAbonnement' => SubscriptionType::MONTHLY,
+ 'DateExpiration' => $now->format("Y-m-d\TH:i:sO"),
+ 'DateSouscription' => $now->format("Y-m-d\TH:i:sO"),
+ 'AccountExist' => true,
+ 'PartenaireID' => $public_key,
+ ];
+ };
+ $verification = Endpoint::answer(Verification::class, $account_always_exists);
+ $result = $transaction->process($verification);
+ $status = 200;
+} catch (\InvalidArgumentException $e) {
+ $result = $e->getMessage();
+ $status = 400;
+} catch (\UnexpectedValueException $e) {
+ $result = $e->getMessage();
+ $status = 401;
+} catch (\Exception $e) {
+ $result = 'Internal Error';
+ $status = 500;
+} finally {
+ $response = new Response(
+ 200 != $status ? json_encode(['error' => $result]) : $result,
+ $status,
+ [
+ 'X-PART' => (string) $public_key,
+ 'X-LPL' => $identity->sign($public_key),
+ 'X-TS' => (string) $identity->getDatetime()->getTimestamp(),
+ ]
+ );
+}
+
+$emitter = new SapiEmitter();
+$emitter->emit($response);
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..bbb9543
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,22 @@
+
+
+
+
+ ./tests
+
+
+
+
+
+ ./src
+
+ ./vendor
+ ./tests
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Endpoint.php b/src/Endpoint.php
new file mode 100644
index 0000000..0dd45eb
--- /dev/null
+++ b/src/Endpoint.php
@@ -0,0 +1,104 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\NullLogger;
+
+abstract class Endpoint implements LoggerAwareInterface
+{
+ use \Psr\Log\LoggerAwareTrait;
+
+ /**
+ * @var callable
+ */
+ protected $callback;
+
+ /**
+ * Construct a new Endpoint to answer one of the API expected operation
+ *
+ * @param string $operation Class name of the Endpoint
+ * @param callable $callback
+ *
+ * @return Endpoint
+ * @throws \LogicException if $operation is not a child of Endpoint
+ */
+ public static function answer($operation, $callback)
+ {
+ if (!in_array(self::class, class_parents($operation))) {
+ throw new \LogicException(sprintf(
+ '%s is not a child of %s',
+ $operation,
+ self::class
+ ));
+ }
+
+ return new $operation($callback);
+ }
+
+ /**
+ * @param callable $callback
+ *
+ * @throws \InvalidArgumentException if the callback is not callable.
+ */
+ private function __construct($callback)
+ {
+ $this->callback = $callback;
+ $this->logger = new NullLogger();
+
+ if (!is_callable($callback)) {
+ throw new \InvalidArgumentException('Create endpoint with invalid callback');
+ }
+ }
+
+ /**
+ * Execute the endpoint.
+ *
+ * @param array $data
+ * @param bool $isTesting
+ *
+ * @return mixed
+ */
+ public function execute($data, $isTesting = false)
+ {
+ $arguments = $this->resolveInput($data);
+ $response = call_user_func($this->callback, $arguments, $isTesting);
+ $result = $this->resolveOutput($response);
+
+ $this->logger->debug(
+ sprintf('Executed endpoint %s', get_class($this)),
+ [$arguments, $result, $isTesting]
+ );
+
+ return $result;
+ }
+
+ /**
+ * Verify and complete the expected input values
+ * when calling the web service.
+ *
+ * @param array $arguments
+ *
+ * @return array Arguments.
+ */
+ abstract protected function resolveInput(array $arguments);
+
+ /**
+ * Verify and complete the expected output values
+ * when calling the web service.
+ *
+ * @param array $arguments
+ *
+ * @return array Data
+ */
+ abstract protected function resolveOutput(array $data);
+}
diff --git a/src/Operation/AccountChange.php b/src/Operation/AccountChange.php
new file mode 100644
index 0000000..5e9bcaf
--- /dev/null
+++ b/src/Operation/AccountChange.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Operation;
+
+use Mediapart\LaPresseLibre\Endpoint;
+
+abstract class AccountChange extends Endpoint
+{
+ /**
+ * `CodeEtat` output argument could have the following values :
+ *
+ * @var int
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#param%C3%A8tres-de-sortie-1
+ */
+ const SUCCESS = 1;
+ const FAILED_EXISTING_EMAIL = 2;
+ const FAILED_EXISTING_USERNAME = 3;
+ const FAILED = 4;
+
+ public static function allStates()
+ {
+ return [
+ self::SUCCESS,
+ self::FAILED_EXISTING_EMAIL,
+ self::FAILED_EXISTING_USERNAME,
+ self::FAILED,
+ ];
+ }
+}
diff --git a/src/Operation/AccountCreation.php b/src/Operation/AccountCreation.php
new file mode 100644
index 0000000..62105a9
--- /dev/null
+++ b/src/Operation/AccountCreation.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Operation;
+
+use Mediapart\LaPresseLibre\Subscription\Status as SubscriptionStatus;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ *
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#web-service-de-cr%C3%A9ation-de-comptes
+ */
+class AccountCreation extends AccountChange
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveInput(array $arguments)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setRequired([
+ 'Pseudo',
+ 'Mail',
+ 'Password',
+ 'TypeAbonnement',
+ 'DateSouscription',
+ 'DateExpiration',
+ 'Tarif',
+ 'CodeUtilisateur',
+ 'Statut',
+ ]);
+
+ $resolver->setAllowedTypes('Tarif', 'float');
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+ $resolver->setAllowedTypes('Statut', 'int');
+ $resolver->setAllowedValues('Statut', SubscriptionStatus::all());
+
+ return $resolver->resolve($arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveOutput(array $data)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setRequired([
+ 'IsValid',
+ 'PartenaireID',
+ 'CodeUtilisateur',
+ 'CodeEtat',
+ ]);
+
+ $resolver->setAllowedTypes('IsValid', 'bool');
+ $resolver->setAllowedTypes('PartenaireID', 'int');
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+ $resolver->setAllowedTypes('CodeEtat', 'int');
+ $resolver->setAllowedValues('CodeEtat', self::allStates());
+
+ return $resolver->resolve($data);
+ }
+}
diff --git a/src/Operation/AccountUpdate.php b/src/Operation/AccountUpdate.php
new file mode 100644
index 0000000..e9d0d42
--- /dev/null
+++ b/src/Operation/AccountUpdate.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Operation;
+
+use Mediapart\LaPresseLibre\Subscription\Type as SubscriptionType;
+use Mediapart\LaPresseLibre\Subscription\Status as SubscriptionStatus;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#web-service-de-mise-%C3%A0-jour-de-comptes
+ */
+class AccountUpdate extends AccountChange
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveInput(array $arguments)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setRequired([
+ 'CodeUtilisateur',
+ 'TypeAbonnement',
+ 'DateSouscription',
+ 'DateExpiration',
+ 'Tarif',
+ 'Statut',
+ ]);
+
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+ $resolver->setAllowedTypes('TypeAbonnement', 'string');
+ $resolver->setAllowedTypes('Tarif', 'float');
+ $resolver->setAllowedTypes('Statut', 'int');
+ $resolver->setAllowedValues('TypeAbonnement', SubscriptionType::all());
+ $resolver->setAllowedValues('Statut', SubscriptionStatus::all());
+
+ return $resolver->resolve($arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveOutput(array $data)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setRequired([
+ 'IsValid',
+ 'PartenaireID',
+ 'CodeUtilisateur',
+ 'CodeEtat',
+ ]);
+
+ $resolver->setAllowedTypes('IsValid', 'bool');
+ $resolver->setAllowedTypes('PartenaireID', 'int');
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+ $resolver->setAllowedTypes('CodeEtat', 'int');
+ $resolver->setAllowedValues('CodeEtat', self::allStates());
+
+ return $resolver->resolve($data);
+ }
+}
diff --git a/src/Operation/Verification.php b/src/Operation/Verification.php
new file mode 100644
index 0000000..76d6f81
--- /dev/null
+++ b/src/Operation/Verification.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Operation;
+
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Subscription\Type as SubscriptionType;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#web-service-de-v%C3%A9rification-de-comptes-existants
+ */
+class Verification extends Endpoint
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveInput(array $arguments)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setDefaults([
+ 'Mail' => null,
+ 'Password' => null,
+ ]);
+
+ $resolver->setRequired('CodeUtilisateur');
+ $resolver->setAllowedTypes('Mail', ['string', 'null']);
+ $resolver->setAllowedTypes('Password', ['string', 'null']);
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+
+ return $resolver->resolve($arguments);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function resolveOutput(array $data)
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setDefaults([
+ 'TypeAbonnement' => null,
+ 'DateExpiration' => null,
+ 'DateSouscription' => null,
+ ]);
+
+ $resolver->setRequired(['Mail', 'CodeUtilisateur', 'AccountExist', 'PartenaireID']);
+ $resolver->setAllowedTypes('Mail', 'string');
+ $resolver->setAllowedTypes('CodeUtilisateur', 'string');
+ $resolver->setAllowedValues('TypeAbonnement', array_merge(SubscriptionType::all(), [null]));
+ $resolver->setAllowedTypes('DateExpiration', ['string', 'null']);
+ $resolver->setAllowedTypes('DateSouscription', ['string', 'null']);
+ $resolver->setAllowedTypes('AccountExist', ['boolean']);
+ $resolver->setAllowedTypes('PartenaireID', 'int');
+
+ return $resolver->resolve($data);
+ }
+}
diff --git a/src/Registration.php b/src/Registration.php
new file mode 100644
index 0000000..b40aad6
--- /dev/null
+++ b/src/Registration.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre;
+
+use Mediapart\LaPresseLibre\Security\Encryption;
+
+class Registration
+{
+ /**
+ * @var string Route where your user will register into La Presse Libre platform.
+ */
+ const LINK_ROUTE = 'https://www.lapresselibre.fr/inscription-partenaire';
+
+ /**
+ * @var int
+ */
+ private $public_key;
+
+ /**
+ * @var Encryption
+ */
+ private $encryption;
+
+ /**
+ * @param int $public_key
+ * @param Encryption $encryption
+ */
+ public function __construct($public_key, Encryption $encryption)
+ {
+ $this->public_key = $public_key;
+ $this->encryption = $encryption;
+ }
+
+ /**
+ * Generate the link that allows you former users to
+ * register themselves into La Presse Libre platform.
+ *
+ * @param string $email
+ * @param string $userName
+ * @param string $guid
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Int%C3%A9gration-et-configuration-du-SDK#inscription-%C3%A0-la-presse-libre-depuis-une-plateforme-partenaire
+ *
+ * @return string
+ */
+ public function generateLink($email, $userName, $guid = null)
+ {
+ $user = $this->encryption->encrypt(
+ [
+ 'Email' => $email,
+ 'Pseudo' => $userName,
+ 'Guid' => $guid,
+ ],
+ OPENSSL_RAW_DATA & OPENSSL_NO_PADDING
+ );
+
+ return sprintf(
+ '%s?user=%s&partId=%s',
+ self::LINK_ROUTE,
+ rawurlencode($user),
+ $this->public_key
+ );
+ }
+}
diff --git a/src/Security/Encryption.php b/src/Security/Encryption.php
new file mode 100644
index 0000000..4d8c44b
--- /dev/null
+++ b/src/Security/Encryption.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Security;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * Used to encrypt/decrypt messages has described in the API specifications.
+ *
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#g%C3%A9n%C3%A9ralit%C3%A9s
+ */
+class Encryption implements LoggerAwareInterface
+{
+ use \Psr\Log\LoggerAwareTrait;
+
+ /**
+ * @var string
+ */
+ private $password;
+
+ /**
+ * @var int
+ */
+ private $iv;
+
+ /**
+ * @var int
+ */
+ private $options = 0;
+
+ /**
+ * @var string
+ */
+ private $method = 'AES-256-CBC';
+
+ /**
+ * @param mixed $password
+ * @param mixed $iv
+ * @param int $options
+ */
+ public function __construct($password, $iv = null, $options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING)
+ {
+ if (null == $iv) {
+ $iv_len = openssl_cipher_iv_length($this->method);
+ $iv = openssl_random_pseudo_bytes($iv_len);
+ }
+
+ $this->password = $password;
+ $this->iv = $iv;
+ $this->options = $options;
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * @param string $message
+ *
+ * @return string Crypted message
+ */
+ public function encrypt($message, $options = null)
+ {
+ $options = !is_null($options) ? $options : $this->options;
+ $result = json_encode($message);
+ $result = openssl_encrypt(
+ $result,
+ $this->method,
+ $this->password,
+ $options,
+ $this->iv
+ );
+ $result = base64_encode($result);
+
+ $this->logger->debug('Encrypting message', [$message, $result]);
+
+ return $result;
+ }
+
+ /**
+ * @param string $message Crypted message
+ *
+ * @return string Uncrypted message
+ */
+ public function decrypt($message, $options = null)
+ {
+ $options = !is_null($options) ? $options : $this->options;
+ $result = base64_decode($message);
+ $result = openssl_decrypt(
+ $result,
+ $this->method,
+ $this->password,
+ $options,
+ $this->iv
+ );
+ $result = json_decode(rtrim($result, "\0"), true);
+
+ $this->logger->debug('Uncrypting message', [$message, $result]);
+
+ return $result;
+ }
+}
diff --git a/src/Security/Identity.php b/src/Security/Identity.php
new file mode 100644
index 0000000..8380faf
--- /dev/null
+++ b/src/Security/Identity.php
@@ -0,0 +1,85 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Security;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * Used to sign documents has described in the API specifications.
+ *
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#g%C3%A9n%C3%A9ralit%C3%A9s
+ */
+class Identity implements LoggerAwareInterface
+{
+ use \Psr\Log\LoggerAwareTrait;
+
+ /**
+ * @var string Name of the hashing algorithm
+ */
+ const HASH_ALGORITHM = 'SHA1';
+
+ /**
+ * @var string Pattern used to concatenate public/secret key and timestamp
+ */
+ const SIGNATURE_PATTERN = '%s+%s+%s';
+
+ /**
+ * @var string
+ */
+ private $secret_key;
+
+ /**
+ * @var \Datetime
+ */
+ protected $datetime;
+
+ /**
+ * @param string $secret_key
+ */
+ public function __construct($secret_key)
+ {
+ $this->secret_key = $secret_key;
+ $this->datetime = new \DateTime();
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * Generate a signature based on your private key.
+ *
+ * @param string $public_key
+ * @param int $timestamp
+ *
+ * @return string
+ */
+ public function sign($public_key, $timestamp = null)
+ {
+ $timestamp = $timestamp ? $timestamp : $this->datetime->getTimestamp();
+ $signchain = sprintf(self::SIGNATURE_PATTERN, $public_key, $timestamp, $this->secret_key);
+ $signature = hash(self::HASH_ALGORITHM, $signchain);
+
+ $this->logger->debug(
+ 'Generated signature',
+ [$public_key, $timestamp, $signature]
+ );
+
+ return $signature;
+ }
+
+ /**
+ * @return \Datetime
+ */
+ public function getDatetime()
+ {
+ return $this->datetime;
+ }
+}
diff --git a/src/Subscription/Status.php b/src/Subscription/Status.php
new file mode 100644
index 0000000..7df8b20
--- /dev/null
+++ b/src/Subscription/Status.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Subscription;
+
+/**
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#statut-de-labonnement
+ */
+final class Status
+{
+ const ACTIVE = 1;
+ const WAITING_FOR_VALIDATION = 2;
+ const SUSPENDED = 3;
+ const TERMINATED = 4;
+ const CANCELED = 5;
+ const IN_PAYMENT = 6;
+ const REFUNDED = 7;
+
+ /**
+ * @return Array All subscription statuses.
+ */
+ public static function all()
+ {
+ return [
+ self::ACTIVE,
+ self::WAITING_FOR_VALIDATION,
+ self::SUSPENDED,
+ self::TERMINATED,
+ self::CANCELED,
+ self::IN_PAYMENT,
+ self::REFUNDED,
+ ];
+ }
+}
diff --git a/src/Subscription/Type.php b/src/Subscription/Type.php
new file mode 100644
index 0000000..1ed93e3
--- /dev/null
+++ b/src/Subscription/Type.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Subscription;
+
+final class Type
+{
+ const MONTHLY = 'mensuel';
+ const ANNUAL = 'annuel';
+ const OTHER = 'autre';
+
+ /**
+ * @return Array All subscription type.
+ */
+ public static function all()
+ {
+ return [
+ self::MONTHLY,
+ self::ANNUAL,
+ self::OTHER,
+ ];
+ }
+}
diff --git a/src/Transaction.php b/src/Transaction.php
new file mode 100644
index 0000000..ab0094b
--- /dev/null
+++ b/src/Transaction.php
@@ -0,0 +1,131 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\NullLogger;
+use Psr\Http\Message\RequestInterface as Request;
+use Mediapart\LaPresseLibre\Security\Identity;
+use Mediapart\LaPresseLibre\Security\Encryption;
+
+/**
+ * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Fonctionnement-des-web-services#g%C3%A9n%C3%A9ralit%C3%A9s
+ */
+class Transaction implements LoggerAwareInterface
+{
+ use \Psr\Log\LoggerAwareTrait;
+
+ /**
+ * @var Request
+ */
+ private $request;
+
+ /**
+ * @var Encryption
+ */
+ private $encryption;
+
+ /**
+ * Initiate a new transaction.
+ *
+ * @param int $secret
+ * @param Encryption $encryption
+ * @param Request $request
+ *
+ * To initiate a transaction, Request should have the following headers :
+ *
+ * - X-PART (int32): partner identifier code
+ * - X-TS (timestamp): sending transaction datetime
+ * - X-LPL (string): hashed signature
+ *
+ * @throws \InvalidArgumentException if one of these headers are missing
+ * @throws \UnexpectedValueException if one of these headers has invalid value
+ */
+ public function __construct(Identity $identity, Encryption $encryption, Request $request)
+ {
+ $this->request = $request;
+ $this->encryption = $encryption;
+ $this->logger = new NullLogger();
+
+ $signature = $identity->sign(
+ $this->requireHeader('X-PART'),
+ $this->requireHeader('X-TS')
+ );
+
+ if ($signature != $this->requireHeader('X-LPL')) {
+ throw new \UnexpectedValueException(sprintf(
+ 'Request signed by %s but expected %s',
+ $request->getHeaderLine('X-LPL'),
+ $signature
+ ));
+ }
+ }
+
+ /**
+ * @param string $name
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return mixed
+ */
+ private function requireHeader($name)
+ {
+ if (!$this->request->hasHeader($name)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Missing header %s',
+ $name
+ ));
+ }
+
+ return $this->request->getHeaderLine($name);
+ }
+
+ /**
+ * Returns if the transaction is in testing mode.
+ *
+ * If true, the endpoint should returns valid response but without applying
+ * its effetcs on database.
+ * Based on the X-CTX HTTP request header.
+ *
+ * @return bool returns true if testing mode is active, false otherwise
+ */
+ public function isTesting()
+ {
+ return (bool) $this->request->hasHeader('X-CTX');
+ }
+
+ /**
+ * Execute an endpoint and returns his result encrypted.
+ *
+ * @param Endpoint $endpoint
+ *
+ * @return string
+ */
+ public function process(Endpoint $endpoint)
+ {
+ if (in_array($this->request->getMethod(), ['PUT', 'POST'])) {
+ $input = $this->request->getBody();
+ } else {
+ parse_str($this->request->getUri()->getQuery(), $query);
+ $input = $query['crd'];
+ }
+
+ $data = $this->encryption->decrypt($input);
+ $this->logger->debug('receive data', [$input, $data]);
+ $result = $endpoint->execute($data, $this->isTesting());
+
+ $noPaddingOption = OPENSSL_RAW_DATA & OPENSSL_NO_PADDING;
+ $encryptedResult = $this->encryption->encrypt($result, $noPaddingOption);
+
+ return $encryptedResult;
+ }
+}
diff --git a/tests/EndpointTest.php b/tests/EndpointTest.php
new file mode 100644
index 0000000..018e970
--- /dev/null
+++ b/tests/EndpointTest.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Operation\Verification;
+
+class EndpointTest extends TestCase
+{
+ public function testAnswerWithInvalidOperation()
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage(sprintf(
+ '%s is not a child of %s',
+ \stdClass::class,
+ Endpoint::class
+ ));
+
+ $endpoint = Endpoint::answer(\stdClass::class, function () {
+ });
+ }
+
+ public function testAnswerWithInvalidCallback()
+ {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('Create endpoint with invalid callback');
+
+ $endpoint = Endpoint::answer(Verification::class, 42);
+ }
+}
diff --git a/tests/Operation/AccountCreationTest.php b/tests/Operation/AccountCreationTest.php
new file mode 100644
index 0000000..fcf2e96
--- /dev/null
+++ b/tests/Operation/AccountCreationTest.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Subscription\Status;
+use Mediapart\LaPresseLibre\Operation\AccountCreation;
+
+class AccountCreationTest extends TestCase
+{
+ public function testExecute()
+ {
+ $public_key = 2;
+ $callback = function ($data, $isTesting) use ($public_key) {
+ return [
+ 'IsValid' => true,
+ 'PartenaireID' => $public_key,
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'CodeEtat' => AccountCreation::SUCCESS,
+ ];
+ };
+
+ $endpoint = Endpoint::answer(AccountCreation::class, $callback);
+ $result = $endpoint->execute([
+ 'Pseudo' => 'pseudo',
+ 'Mail' => 'pseudo@domain.tld',
+ 'Password' => 'pass',
+ 'TypeAbonnement' => 'mensuel',
+ 'DateSouscription' => '',
+ 'DateExpiration' => '',
+ 'Tarif' => 9.90,
+ 'CodeUtilisateur' => 'aaaa-bbbb-2222',
+ 'Statut' => Status::ACTIVE,
+ ]);
+
+ $this->assertEquals([
+ 'IsValid' => true,
+ 'PartenaireID' => $public_key,
+ 'CodeUtilisateur' => 'aaaa-bbbb-2222',
+ 'CodeEtat' => AccountCreation::SUCCESS,
+ ], $result);
+ }
+}
diff --git a/tests/Operation/AccountUpdateTest.php b/tests/Operation/AccountUpdateTest.php
new file mode 100644
index 0000000..72c2e7c
--- /dev/null
+++ b/tests/Operation/AccountUpdateTest.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Operation\AccountUpdate;
+use Mediapart\LaPresseLibre\Subscription\Type as SubscriptionType;
+use Mediapart\LaPresseLibre\Subscription\Status as SubscriptionStatus;
+
+class AccountUpdateTest extends TestCase
+{
+ public function testExecute()
+ {
+ $public_key = 2;
+ $callback = function ($data, $isTesting) use ($public_key) {
+ return [
+ 'IsValid' => true,
+ 'PartenaireID' => $public_key,
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'CodeEtat' => AccountUpdate::SUCCESS,
+ ];
+ };
+
+ $endpoint = Endpoint::answer(AccountUpdate::class, $callback);
+ $result = $endpoint->execute([
+ 'CodeUtilisateur' => 'aaaa-bbbb-2222',
+ 'TypeAbonnement' => SubscriptionType::MONTHLY,
+ 'DateSouscription' => '',
+ 'DateExpiration' => '',
+ 'Tarif' => 9.90,
+ 'Statut' => SubscriptionStatus::ACTIVE,
+ ]);
+
+ $this->assertEquals([
+ 'IsValid' => true,
+ 'PartenaireID' => $public_key,
+ 'CodeUtilisateur' => 'aaaa-bbbb-2222',
+ 'CodeEtat' => AccountUpdate::SUCCESS,
+ ], $result);
+ }
+}
diff --git a/tests/Operation/VerificationTest.php b/tests/Operation/VerificationTest.php
new file mode 100644
index 0000000..71d47d9
--- /dev/null
+++ b/tests/Operation/VerificationTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Operation\Verification;
+
+class VerificationTest extends TestCase
+{
+ public function testExecute()
+ {
+ $userMail = 'user@domain.tld';
+ $userCode = '42';
+
+ $callback = function ($data, $isTesting) {
+ return [
+ 'Mail' => $data['Mail'],
+ 'CodeUtilisateur' => $data['CodeUtilisateur'],
+ 'AccountExist' => false,
+ 'PartenaireID' => 1,
+ ];
+ };
+
+ $endpoint = Endpoint::answer(Verification::class, $callback);
+ $result = $endpoint->execute(['Mail' => $userMail, 'CodeUtilisateur' => $userCode]);
+
+ $this->assertEquals([
+ 'TypeAbonnement' => null,
+ 'DateExpiration' => null,
+ 'DateSouscription' => null,
+ 'Mail' => $userMail,
+ 'CodeUtilisateur' => $userCode,
+ 'AccountExist' => false,
+ 'PartenaireID' => 1,
+ ], $result);
+ }
+}
diff --git a/tests/RegistrationTest.php b/tests/RegistrationTest.php
new file mode 100644
index 0000000..b4c6580
--- /dev/null
+++ b/tests/RegistrationTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Registration;
+use Mediapart\LaPresseLibre\Security\Encryption;
+
+class RegistrationTest extends TestCase
+{
+ public function testGenerateLink()
+ {
+ $encryption = $this->createMock(Encryption::class);
+
+ $encryption
+ ->method('encrypt')
+ ->with([
+ 'Email' => 'user@domain.tld',
+ 'Pseudo' => 'username',
+ 'Guid' => null,
+ ])
+ ->willReturn('foobar')
+ ;
+
+ $registration = new Registration(1, $encryption);
+ $link = $registration->generateLink('user@domain.tld', 'username');
+
+ $this->assertEquals(Registration::LINK_ROUTE.'?user=foobar&partId=1', $link);
+ }
+}
diff --git a/tests/Security/EncryptionTest.php b/tests/Security/EncryptionTest.php
new file mode 100644
index 0000000..7df7692
--- /dev/null
+++ b/tests/Security/EncryptionTest.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Security\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Security\Encryption;
+
+class EncryptionTest extends TestCase
+{
+ public function testEncrypt()
+ {
+ $string = 'lorem ipsum dolor';
+ $encryption = new Encryption('passphrase');
+
+ $encrypted = $encryption->encrypt($string);
+ $decrypted = $encryption->decrypt($string);
+
+ $this->assertEquals($encrypted, $decrypted);
+ }
+}
diff --git a/tests/Security/IdentityTest.php b/tests/Security/IdentityTest.php
new file mode 100644
index 0000000..1bdee44
--- /dev/null
+++ b/tests/Security/IdentityTest.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Security\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Mediapart\LaPresseLibre\Security\Identity;
+
+class IdentityTest extends TestCase
+{
+ public function testSign()
+ {
+ $identity = new Identity('secretkey');
+ $signature = $identity->sign('publickey', 1500000000);
+
+ $this->assertEquals('b031e33b78f12e5246b64bd6a17935372ded5bf3', $signature);
+ $this->assertInstanceOf(\Datetime::class, $identity->getDatetime());
+ }
+}
diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php
new file mode 100644
index 0000000..d051ad2
--- /dev/null
+++ b/tests/TransactionTest.php
@@ -0,0 +1,225 @@
+
+ *
+ * For the full license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Mediapart\LaPresseLibre\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\UriInterface as Uri;
+use Psr\Http\Message\RequestInterface as Request;
+use Mediapart\LaPresseLibre\Transaction;
+use Mediapart\LaPresseLibre\Endpoint;
+use Mediapart\LaPresseLibre\Security\Identity;
+use Mediapart\LaPresseLibre\Security\Encryption;
+
+class TransactionTest extends TestCase
+{
+ public function testWithInvalidSignature()
+ {
+ $identity = $this->createMock(Identity::class);
+ $encryption = $this->createMock(Encryption::class);
+ $request = $this->createMock(Request::class);
+
+ $request
+ ->method('hasHeader')
+ ->will($this->returnValueMap([
+ ['X-PART', true],
+ ['X-TS', true],
+ ['X-LPL', true],
+ ['X-CTX', false],
+ ]))
+ ;
+ $request
+ ->method('getHeaderLine')
+ ->will($this->returnValueMap([
+ ['X-PART', 'partnerkey'],
+ ['X-TS', time()],
+ ['X-LPL', 'signatureA'],
+ ]))
+ ;
+
+ $identity
+ ->method('sign')
+ ->willReturn('signatureB')
+ ;
+
+ $this->expectException(\UnexpectedValueException::class);
+ $this->expectExceptionMessage('Request signed by signatureA but expected signatureB');
+
+ $transaction = new Transaction($identity, $encryption, $request);
+ }
+
+ public function testMissingRequiredHeader()
+ {
+ $identity = $this->createMock(Identity::class);
+ $encryption = $this->createMock(Encryption::class);
+ $request = $this->createMock(Request::class);
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Missing header X-PART');
+
+ $transaction = new Transaction($identity, $encryption, $request);
+ }
+
+ public function testIsTesting()
+ {
+ $identity = $this->createMock(Identity::class);
+ $encryption = $this->createMock(Encryption::class);
+ $request = $this->createMock(Request::class);
+
+ $request
+ ->method('hasHeader')
+ ->will($this->returnValueMap([
+ ['X-PART', true],
+ ['X-TS', true],
+ ['X-LPL', true],
+ ['X-CTX', false],
+ ]))
+ ;
+ $request
+ ->method('getHeaderLine')
+ ->will($this->returnValueMap([
+ ['X-PART', 'partnerkey'],
+ ['X-TS', time()],
+ ['X-LPL', 'signatureA'],
+ ]))
+ ;
+
+ $identity
+ ->method('sign')
+ ->willReturn('signatureA')
+ ;
+
+ $transaction = new Transaction($identity, $encryption, $request);
+
+ $this->assertFalse($transaction->isTesting());
+ }
+
+ public function testProcess()
+ {
+ $identity = $this->createMock(Identity::class);
+ $encryption = $this->createMock(Encryption::class);
+ $request = $this->createMock(Request::class);
+ $endpoint = $this->createMock(Endpoint::class);
+ $uri = $this->createMock(Uri::class);
+
+ $request
+ ->method('hasHeader')
+ ->will($this->returnValueMap([
+ ['X-PART', true],
+ ['X-TS', true],
+ ['X-LPL', true],
+ ]))
+ ;
+ $request
+ ->method('getHeaderLine')
+ ->will($this->returnValueMap([
+ ['X-PART', 'partnerkey'],
+ ['X-TS', time()],
+ ['X-LPL', 'signatureA'],
+ ]))
+ ;
+ $identity
+ ->method('sign')
+ ->willReturn('signatureA')
+ ;
+
+ $request
+ ->method('getUri')
+ ->willReturn($uri)
+ ;
+ $uri
+ ->method('getQuery')
+ ->willReturn('crd=input')
+ ;
+ $encryption
+ ->expects($this->once())
+ ->method('decrypt')
+ ->with('input')
+ ->willReturn('data')
+ ;
+ $endpoint
+ ->method('execute')
+ ->with('data')
+ ->willReturn('result')
+ ;
+ $encryption
+ ->expects($this->once())
+ ->method('encrypt')
+ ->with('result')
+ ->willReturn('blabla')
+ ;
+
+ $transaction = new Transaction($identity, $encryption, $request);
+ $result = $transaction->process($endpoint);
+
+ $this->assertEquals('blabla', $result);
+ }
+
+ public function testProcessFromPost()
+ {
+ $identity = $this->createMock(Identity::class);
+ $encryption = $this->createMock(Encryption::class);
+ $request = $this->createMock(Request::class);
+ $endpoint = $this->createMock(Endpoint::class);
+
+ $request
+ ->method('hasHeader')
+ ->will($this->returnValueMap([
+ ['X-PART', true],
+ ['X-TS', true],
+ ['X-LPL', true],
+ ]))
+ ;
+ $request
+ ->method('getHeaderLine')
+ ->will($this->returnValueMap([
+ ['X-PART', 'lorem'],
+ ['X-TS', time()],
+ ['X-LPL', 'toto'],
+ ]))
+ ;
+ $identity
+ ->method('sign')
+ ->willReturn('toto')
+ ;
+
+ $request
+ ->method('getMethod')
+ ->willReturn('POST')
+ ;
+ $request
+ ->method('getBody')
+ ->willReturn('input')
+ ;
+ $encryption
+ ->expects($this->once())
+ ->method('decrypt')
+ ->with('input')
+ ->willReturn('data')
+ ;
+ $endpoint
+ ->method('execute')
+ ->with('data')
+ ->willReturn('result')
+ ;
+ $encryption
+ ->expects($this->once())
+ ->method('encrypt')
+ ->with('result')
+ ->willReturn('blabla')
+ ;
+
+ $transaction = new Transaction($identity, $encryption, $request);
+ $result = $transaction->process($endpoint);
+
+ $this->assertEquals('blabla', $result);
+ }
+}