unlock-8.x-1.0/src/Client.php
src/Client.php
<?php
namespace Drupal\unlock;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Password\PasswordGeneratorInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\unlock\Form\SettingsForm;
use Drupal\user\UserDataInterface;
use GuzzleHttp\ClientInterface;
/**
* Client service for interact with Unlock Protocol.
*/
class Client {
use StringTranslationTrait;
/**
* Login base url.
*
* @var string
*/
public const BASE_URL = 'https://app.unlock-protocol.com/checkout';
/**
* Validation url.
*
* @var string
*/
public const VALIDATE_URL = 'https://locksmith.unlock-protocol.com/api/oauth';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The user data service.
*
* @var \Drupal\user\UserDataInterface
*/
protected $userData;
/**
* The Guzzle client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $client;
/**
* The password generator service.
*
* @var \Drupal\Core\Password\PasswordGeneratorInterface
*/
protected $passwordGenerator;
/**
* Configuration for unlock.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* Constructs a Client object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* @param \Drupal\user\UserDataInterface $user_data
* @param \GuzzleHttp\ClientInterface $client
* @param \Drupal\Core\Password\PasswordGeneratorInterface $password_generator
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, UserDataInterface $user_data, ClientInterface $client, PasswordGeneratorInterface $password_generator, ConfigFactoryInterface $config_factory) {
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
$this->userData = $user_data;
$this->client = $client;
$this->passwordGenerator = $password_generator;
$this->config = $config_factory->get(SettingsForm::SETTINGS);
}
/**
* Method description.
*/
public function getLoginUrl(): string {
if (empty($_SESSION['unlock_token'])) {
$state = $this->passwordGenerator->generate(8);
$_SESSION['unlock_token'] = $state;
}
$args = [
'client_id' => self::getClientId(),
'redirect_uri' => self::getRedirectUrl(),
'state' => $_SESSION['unlock_token'],
];
return self::BASE_URL . '?' . http_build_query($args);
}
/**
* @throws \JsonException
*/
public function getCheckoutUrl($lock_address, $lock_network): string {
$paywall_locks[$lock_address] = [
'network' => (int) $lock_network,
];
$paywall_config = [
'metadataInputs' => [[
'name' => 'Email',
'type' => 'email',
'required' => TRUE,
],
],
'locks' => $paywall_locks,
'pessimistic' => TRUE,
];
$args = [
'redirectUri' => self::getRedirectUrl(),
'paywallConfig' => json_encode($paywall_config, JSON_THROW_ON_ERROR),
];
return self::BASE_URL . '?' . urldecode(http_build_query($args));
}
/**
*
*/
public static function getRedirectUrl(): string {
$host = \Drupal::request()->getSchemeAndHttpHost();
$path = \Drupal::request()->getPathInfo();
return $host . $path;
}
/**
*
*/
public static function getClientId(): string {
return \Drupal::request()->getHost();
}
/**
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \JsonException
*/
public function authenticate(): void {
if (!$this->needAuthenticate()) {
return;
}
$code = \Drupal::request()->query->get('code');
$unlock_token = \Drupal::request()->query->get('state');
if (!$code || empty($_SESSION['unlock_token']) || ($_SESSION['unlock_token'] !== $unlock_token)) {
return;
}
$ethereum_address = $this->validateAuthCode($code);
$ethereum_address = Html::escape($ethereum_address);
if ($this->currentUser->isAnonymous()) {
$users = $this->userData->get('unlock', NULL, 'ethereum_address');
if ($users) {
$uid = array_key_first($users);
/** @var \Drupal\user\UserInterface $user */
$user = $this->entityTypeManager
->getStorage('user')
->load($uid);
if ($user) {
user_login_finalize($user);
return;
}
}
}
if ($this->currentUser->isAuthenticated()) {
$this->userData->set('unlock', $this->currentUser->id(), 'ethereum_address', $ethereum_address);
return;
}
$email = self::getEmail($ethereum_address);
$users = $this->entityTypeManager->getStorage('user')->loadByProperties(['mail' => $email]);
if ($users) {
/** @var \Drupal\user\UserInterface $user */
$user = reset($users);
$this->userData->set('unlock', $user->id(), 'ethereum_address', $ethereum_address);
user_login_finalize($user);
return;
}
// @todo Check if global settings allow automatic registration
/** @var \Drupal\user\UserInterface $user */
$user = $this->entityTypeManager
->getStorage('user')
->create();
// Mandatory settings.
$user->setPassword($this->passwordGenerator->generate());
$user->enforceIsNew();
$user->setEmail($email);
$user->setUsername($ethereum_address);
$user->activate();
$user->save();
$this->userData->set('unlock', $user->id(), 'ethereum_address', $ethereum_address);
user_login_finalize($user);
}
/**
* Check that user logged in and have etherium address associated.
*
* @return bool
*/
public function needAuthenticate(): bool {
$result = TRUE;
if ($this->currentUser->isAuthenticated()) {
$ethereum_address = $this->userData->get('unlock', $this->currentUser->id(), 'ethereum_address');
if ($ethereum_address) {
$result = FALSE;
}
}
return $result;
}
/**
* @param $code
*
* @return mixed
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \JsonException
*/
public function validateAuthCode($code) {
$response = $this->client->request('POST', self::VALIDATE_URL, [
'json' => [
'grant_type' => 'authorization_code',
'client_id' => self::getClientId(),
'redirect_uri' => self::getRedirectUrl(),
'code' => $code,
],
]);
$json = (string) $response->getBody();
$json = json_decode($json, TRUE, 512, JSON_THROW_ON_ERROR);
if (empty($json) || !array_key_exists('me', $json)) {
throw new \Exception('Invalid account');
}
return $json['me'];
}
/**
*
*/
public static function getEmail($ethereum_address): string {
return sprintf('%1$s@%2$s', Html::escape($ethereum_address), Html::escape(self::getClientId()));
}
/**
* @param $rpc_endpoint
* @param $lock_address
* @param null $user_ethereum_address
*
* @return bool
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \JsonException
*/
public function validate($rpc_endpoint, $lock_address, $user_ethereum_address = NULL): bool {
if (!$user_ethereum_address) {
$user_ethereum_address = $this->userData->get('unlock', $this->currentUser->id(), 'ethereum_address');
}
$user_ethereum_address = substr($user_ethereum_address, 2);
$response = $this->client->request('POST', $rpc_endpoint, [
'json' => [
'method' => 'eth_call',
'params' => [
[
'to' => $lock_address,
'data' => sprintf('0x6d8ea5b4000000000000000000000000%s', $user_ethereum_address),
],
'latest',
],
'id' => 31337,
'jsonrpc' => '2.0',
],
]);
$json = (string) $response->getBody();
$json = json_decode($json, TRUE, 512, JSON_THROW_ON_ERROR);
return !empty($json['result']) && (hexdec($json['result']) == 1);
}
/**
* @param $entity
*
* @return array|false
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \JsonException
*/
public function getUnlockButton($entity) {
if (!$entity) {
return FALSE;
}
if (!($entity instanceof ThirdPartySettingsInterface)) {
return FALSE;
}
if (!$entity->getThirdPartySetting('unlock', 'status')) {
return FALSE;
}
$result = FALSE;
$visible_until_paid = $entity->getThirdPartySetting('unlock', 'type');
$type_word = $visible_until_paid ? 'hide' : 'show';
$this->authenticate();
$need_login = $this->needAuthenticate();
if ($need_login) {
$button_text = $entity->getThirdPartySetting('unlock', 'login_text') ?: SettingsForm::LOGIN_TEXT;
$matches = NULL;
if (preg_match('~<link>(.*?)</link>~iu', $button_text, $matches)) {
$button_text = str_replace([$matches[0], '@type'], ['{{ link }}', $type_word], $button_text);
$result = [
'#type' => 'inline_template',
'#template' => $this->t($button_text)->__toString(),
'#context' => [
'link' => [
'#type' => 'link',
'#title' => $this->t($matches[1], ['@type' => $type_word]),
'#url' => Url::fromUri($this->getLoginUrl()),
'#cache' => [
'max-age' => 0,
],
],
],
];
}
else {
$result = [
'#type' => 'link',
'#title' => $this->t($button_text, ['@type' => $type_word]),
'#url' => Url::fromUri($this->getLoginUrl()),
'#cache' => [
'max-age' => 0,
],
];
}
}
else {
$lock_address = $entity->getThirdPartySetting('unlock', 'lock_address');
$lock_network = $entity->getThirdPartySetting('unlock', 'network');
if (!$lock_network) {
$lock_network = $this->config->get('network') ?: 1;
}
$url = SettingsForm::RPC_ENDPOINTS[$lock_network];
$has_unlocked = $this->validate($url, $lock_address);
if (!$has_unlocked) {
$button_text = $entity->getThirdPartySetting('unlock', 'checkout_text') ?: SettingsForm::CHECKOUT_TEXT;
$matches = NULL;
if (preg_match('~<link>(.*?)</link>~iu', $button_text, $matches)) {
$button_text = str_replace([$matches[0], '@type'], ['{{ link }}', $type_word], $button_text);
$result = [
'#type' => 'inline_template',
'#template' => $this->t($button_text)->__toString(),
'#context' => [
'link' => [
'#type' => 'link',
'#title' => $this->t($matches[1], ['@type' => $type_word]),
'#url' => Url::fromUri($this->getCheckoutUrl($lock_address, $lock_network)),
'#cache' => [
'max-age' => 0,
],
],
],
];
}
else {
$result = [
'#type' => 'link',
'#title' => $this->t($button_text, ['@type' => $type_word]),
'#url' => Url::fromUri($this->getCheckoutUrl($lock_address, $lock_network)),
'#cache' => [
'max-age' => 0,
],
];
}
}
}
if ($result) {
$result = [
'#theme' => 'unlock_button',
'#label' => $result,
'#type' => $type_word,
'#level' => $need_login ? 'login' : 'checkout',
];
}
return $result;
}
}
