patreon-8.x-2.x-dev/modules/patreon_user/src/PatreonUserService.php

modules/patreon_user/src/PatreonUserService.php
<?php

namespace Drupal\patreon_user;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Password\DefaultPasswordGenerator;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Drupal\patreon\PatreonMissingTokenException;
use Drupal\patreon\PatreonService;
use Drupal\user\UserInterface;
use Drupal\Core\Url;
use Drupal\patreon\PatreonGeneralException;
use Drupal\Component\Utility\Xss;
use HansPeterOrding\OAuth2\Client\Provider\Patreon;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Token\AccessTokenInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Class extending the PatreonAPI service with user specific functions.
 *
 * @package Drupal\patreon_user
 */
class PatreonUserService extends PatreonService {

  const int PATREON_USER_NO_LOGIN = 0;
  const int PATREON_USER_COPY_ACCOUNT = 1;
  const int PATREON_USER_SINGLE_SIGN_ON = 2;
  const int PATREON_USER_ONLY_PATRONS = 1;
  const int PATREON_USER_ALL_USERS = 2;

  /**
   * A token for use with the API.
   *
   * @var null|AccessTokenInterface
   *   An API token,
   */
  protected null|AccessTokenInterface $token = NULL;

  /**
   * Constructs a ParagraphsTypeIconUuidLookup instance.
   *
   * @param \Drupal\Core\Path\CurrentPathStack $path
   *   The Drupal Path service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   A Drupal Config Factory.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
   *   A logger channel.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
   *   An Entity Type Manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $stack
   *   The request stack service.
   * @param \Drupal\Core\State\StateInterface $stateApi
   *   A state service.
   * @param \Drupal\Core\Session\AccountInterface $currentUser
   *   The current user account.
   * @param \Drupal\Core\Extension\ModuleHandler $handler
   *   A Module Handler.
   * @param \Drupal\Core\Password\DefaultPasswordGenerator $passwordGenerator
   *   Password Generator service.
   */
  public function __construct(
    CurrentPathStack $path,
    ConfigFactoryInterface $configFactory,
    LoggerChannelFactoryInterface $logger,
    MessengerInterface $messenger,
    EntityTypeManager $entityTypeManager,
    RequestStack $stack,
    StateInterface $stateApi,
    protected AccountInterface $currentUser,
    protected readonly ModuleHandler $handler,
    protected readonly DefaultPasswordGenerator $passwordGenerator,
  ) {
    parent::__construct($path, $configFactory, $logger, $messenger, $entityTypeManager, $stack, $stateApi);
    $this->config = $this->configFactory->getEditable('patreon_user.settings');
  }

  /**
   * {@inheritdoc}
   */
  public function getOauth(): ?Patreon {
    $return = NULL;

    try {
      $key = $this->configFactory->get('patreon.settings')->get('patreon_client_id');
      $secret = $this->configFactory->get('patreon.settings')->get('patreon_client_secret');

      if (!$key || !$secret) {
        throw new PatreonMissingTokenException('No client details set.');
      }

      $return = new Patreon([
        'clientId' => $key,
        'clientSecret' => $secret,
        'redirectUri' => $this->getCallback()->toString(),
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error($this->t('Error obtaining new Oauth - :error', [
        ':error' => $e->getMessage(),
      ]));
    }

    return $return;
  }

  /**
   * A helper to store a token against the service.
   *
   * @param \League\OAuth2\Client\Token\AccessTokenInterface $token
   *   A valid API token.
   */
  public function setToken(AccessTokenInterface $token): void {
    $this->token = $token;
  }

  /**
   * Returns the redirect path from settings.
   *
   * @return array|mixed|null
   *   The return of the config get call.
   */
  public function getReturnPath(): mixed {
    return $this->config->get('patreon_user_redirect_path');
  }

  /**
   * {@inheritdoc}
   */
  public function getCallback(): Url {
    return Url::fromRoute('patreon_user.patreon_user_controller_oauth', [], ['absolute' => TRUE]);
  }

  /**
   * {@inheritdoc}
   */
  public function storeTokens(AccessTokenInterface $tokens, ?UserInterface $account = NULL): void {
    if (!$account) {
      $current = $this->currentUser;
      $account = $this->entityTypeManager->getStorage('user')->load($current->id());
    }

    if ($account->id() > 0) {
      try {
        $account->set('user_patreon_token', Json::encode($tokens));
        $account->save();
      }
      catch (\Exception $e) {
        $this->logger->error($this->t('Error storing user :uid tokens - :error', [
          ':user' => $account->id(),
          ':error' => $e->getMessage(),
        ]));
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getStoredTokens(?UserInterface $account = NULL): ?AccessTokenInterface {
    $return = NULL;

    if ($account && $account->id() > 0) {
      if ($data = $account->get('user_patreon_token')->getString()) {
        if ($values = Json::decode($data)) {
          try {
            $return = new AccessToken($values);
          }
          catch (\Exception $e) {
            $this->logger->error($this->t('Error loading stored tokens - :error', [
              ':error' => $e->getMessage(),
            ]));

            return NULL;
          }
        }
      }
    }
    else {

      // In all other cases, use the token set against this service. If it is
      // empty, the custom code will need to handle that as it would any other
      // missing token issue.
      $return = $this->token;
    }

    return $return;
  }

  /**
   * Helper to check if a Patreon user is a patron of the client.
   *
   * @param array $patreon_return
   *   Results array from the user endpoint.
   *
   * @return bool
   *   TRUE is user's pledges match creator id. Defaults to FALSE.
   */
  public function isPatron(array $patreon_return): bool {
    $return = FALSE;

    if ($creator_campaigns = $this->stateApi->get('patreon.campaigns')) {
      if ($memberships = $this->getValueByKey($patreon_return, [
        'data',
        'relationships',
        'memberships',
        'data',
      ])) {
        $key = 0;

        while ($return == FALSE && array_key_exists($key, $memberships)) {
          if ($member_data = $this->fetchMemberDetails($memberships[$key]['id'])) {
            if ($campaigns = $this->getValueByKey($member_data, [
              'data',
              'relationships',
              'campaign',
              'data',
            ])) {
              if (in_array($campaigns['id'], $creator_campaigns)) {
                $return = TRUE;
              }
            }
          }

          $key++;
        }
      }
    }

    return $return;
  }

  /**
   * Helper to check whether current Patreon user is allowed to log in.
   *
   * @param array $user_return
   *   Results array from the user endpoint.
   *
   * @return bool
   *   TRUE if user meets current Patreon settings restrictions on log in.
   */
  public function canLogin(array $user_return): bool {
    $return = FALSE;

    if ($settings = $this->config->get('patreon_user_registration')) {
      if ($settings != $this::PATREON_USER_NO_LOGIN) {
        if ($settings == $this::PATREON_USER_ONLY_PATRONS) {
          if ($this->isPatron($user_return)) {
            $return = TRUE;
          }
        }
        else {
          $return = TRUE;
        }
      }
    }

    return $return;
  }

  /**
   * Helper to fetch an existing user or create a new one from Patreon account.
   *
   * @param array $patreon_user
   *   Results array from the user endpoint.
   *
   * @return bool|\Drupal\user\UserInterface
   *   A Drupal user object, or FALSE on error.
   *
   * @throws \Drupal\patreon_user\PatreonUserUserException
   */
  public function getUser(array $patreon_user): bool|UserInterface {
    $return = FALSE;

    if ($patreon_id = $this->getValueByKey($patreon_user, ['data', 'id'])) {
      try {
        if ($account = $this->getUserFromId($patreon_id)) {
          $return = $account;
        }
        else {
          $return = $this->createUserFromReturn($patreon_user);
        }
      }
      catch (\Exception $e) {

        // Pass the Exception up to the next level.
        throw new PatreonUserUserException($e->getMessage());
      }
    }

    return $return;
  }

  /**
   * Returns a Drupal user account linked to a Patreon account id.
   *
   * @param int $patreon_id
   *   A valid patreon account id.
   *
   * @return \Drupal\user\UserInterface|bool
   *   A loaded user or FALSE on error.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\patreon\PatreonGeneralException
   */
  public function getUserFromId(int $patreon_id): bool|UserInterface {
    $return = FALSE;
    $result = $this->entityTypeManager->getStorage('user')->getQuery()
      ->condition('user_patreon_id', $patreon_id)
      ->accessCheck(FALSE)
      ->execute();

    if (!empty($result)) {
      if (count($result) > 1) {
        throw new PatreonGeneralException('Multiple users linked to the Patreon account :id', [':id' => $patreon_id]);
      }
      elseif ($account = $this->entityTypeManager->getStorage('user')->load(key($result))) {
        if ($account->id() == 1) {
          $this->logger
            ->notice($this->t('Patreon user :id linked to User 1. This could cause security issues.', [':id' => $patreon_id]));
        }
        /** @var \Drupal\user\UserInterface $return */
        $return = $account;
      }
    }

    return $return;
  }

  /**
   * Creates a Drupal user account from Patreon API data.
   *
   * @param array $data
   *   Results array from the user endpoint.
   *
   * @return \Drupal\user\UserInterface
   *   A Drupal user object.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\patreon_user\PatreonUserUserException
   */
  public function createUserFromReturn(array $data): UserInterface {
    if ($patreon_id = $this->getValueByKey($data, ['data', 'id'])) {

      // We need an email address, or we can't continue.
      if ($mail = $this->getValueByKey($data, ['data', 'attributes', 'email'])) {

        // If the user mail exists, it must be the same user.
        if ($existing_mail = $this->entityTypeManager->getStorage('user')->loadByProperties([
          'mail' => $mail,
        ])) {

          /** @var \Drupal\User\UserInterface $return */
          $return = reset($existing_mail);
          unset($existing_mail);
        }
        else {

          /** @var \Drupal\User\UserInterface $return */
          $return = $this->entityTypeManager->getStorage('user')->create([
            'mail' => $mail,
          ]);
          $return->setPassword($this->passwordGenerator->generate(20));
          $return->enforceIsNew();
          $return->activate();
        }

        // But if the name exists, it could be someone else.
        if ($name = Xss::filter($this->getValueByKey($data, [
          'data',
          'attributes',
          'full_name',
        ]))) {
          $name = $this->getUniqueUserName($name, $patreon_id);
          $return->setUsername($name);
          $alter = ['#user' => &$return, '#patreon_data' => $data];

          // Allow other modules to add field data.
          $this->handler->alter('patreon_user_create_user', $alter);

          // Add the Patreon ID.
          $return->set('user_patreon_id', Xss::filter($patreon_id));
          $this->assignRoles($return, $data);
        }
      }
      else {
        throw new PatreonUserUserException('No Patreon Email address in provided data array.');
      }
    }
    else {
      throw new PatreonUserUserException('No Patreon ID in provided data array. Please check your scopes.');
    }

    if ($return) {
      try {
        $return->save();
      }
      catch (\Exception $e) {
        throw new PatreonUserUserException($e->getMessage());
      }
    }
    else {
      throw new PatreonUserUserException('Error creating user.');
    }

    return $return;
  }

  /**
   * Assign the patreon user or deleted patreon user roles based on status.
   *
   * @param \Drupal\User\UserInterface $account
   *   A Drupal user account to update.
   * @param array $patreon_user
   *   Results array from the user endpoint.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function assignRoles(UserInterface $account, array $patreon_user): void {
    $deleted = $this->isDeletedUser($patreon_user, $account->getAccountName());
    $patreon_user_roles = $this->stateApi->get('patreon.user_roles');

    if (!$patreon_user_roles) {
      $this->createRoles();
      $patreon_user_roles = $this->stateApi->get('patreon.user_roles');
    }

    if ($deleted) {
      $account->addRole('deleted_patreon_user');
    }
    else {
      $account->addRole('patreon_user');
    }

    foreach ($this->getPatronPledges($patreon_user) as $id => $pledge) {
      if ($pledge['patron_status'] == 'active_patron') {
        if ($membership = $this->fetchMemberDetails($id)) {
          if ($tier = $this->getTierData($membership)) {
            $key = key($tier);

            if (array_key_exists($key, $patreon_user_roles)) {
              $account->addRole($patreon_user_roles[$key]);
            }
          }
        }
      }
    }
  }

  /**
   * Helper to get a user's pledges.
   *
   * @param array $patreon_return
   *   Results array from the user endpoint.
   *
   * @return array
   *   An array of all pledges.
   */
  public function getPatronPledges(array $patreon_return): array {
    $return = [];

    if ($pledges = $this->getValueByKey($patreon_return, [
      'included',
    ])) {
      foreach ($pledges as $pledge) {
        if (isset($pledge['type']) && $pledge['type'] == 'member') {
          $return[$pledge['id']] = $pledge['attributes'];
        }
      }
    }

    return $return;
  }

  /**
   * Helper to find if a Patreon User has been deleted or blocked.
   *
   * @param array $patreon_account
   *   Results array from the user endpoint.
   * @param string $drupal_account_name
   *   A valid Drupal user account name.
   *
   * @return bool
   *   Returns TRUE if any value
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function isDeletedUser(array $patreon_account, string $drupal_account_name): bool {
    return $this->getValueByKey($patreon_account, [
      'data',
      'attributes',
      'is_deleted',
    ]) == TRUE ||
    $this->getValueByKey($patreon_account, [
      'data',
      'attributes',
      'is_nuked',
    ]) == TRUE ||
    $this->getValueByKey($patreon_account, [
      'data',
      'attributes',
      'is_suspended',
    ]) == TRUE ||
      (bool) $this->entityTypeManager->getStorage('user')->getQuery()
        ->accessCheck(FALSE)
        ->condition('name', $drupal_account_name)
        ->condition('status', 0)
        ->execute() == TRUE;
  }

  /**
   * Helper to make a username unique if it exists.
   *
   * @param string $name
   *   A Patreon user's full name.
   * @param string $patreon_id
   *   A Patreon user's patreon id.
   *
   * @return string
   *   A de-duped username if required. Defaults to provided.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function getUniqueUserName(string $name, string $patreon_id): string {
    if ($this->entityTypeManager->getStorage('user')->loadByProperties([
      'name' => $name,
    ])) {
      $name .= '_' . $patreon_id;
    }

    // By rights, if the combination of Patreon Fullname and Patreon ID already
    // exists as a username, it must have been because of this function. But to
    // reach this point, we have already failed to find the user by their email
    // address so we will deduplicate the user name to be sure we do not expose
    // private data to the wrong people.
    if ($this->entityTypeManager->getStorage('user')
      ->loadByProperties([
        'name' => $name,
      ])) {
      $key = 0;

      while ($this->entityTypeManager->getStorage('user')
        ->loadByProperties([
          'name' => $name . '_' . $key,
        ])) {
        $key++;
      }

      $name = $name . '_' . $key;

    }

    return $name;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc