Commit 77a8948f authored by Eleo Basili's avatar Eleo Basili
Browse files

Issue #3276126 by eleonel: Add support of mailchimp

parent e7a953e7
Loading
Loading
Loading
Loading
+69 −0
Original line number Diff line number Diff line
CONTENTS OF THIS FILE
---------------------

 * Introduction
 * Requirements
 * Installation
 * Configuration
 * Troubleshooting
 * Maintainers


INTRODUCTION
------------

The Enricher Mailchimp is a submodule of Enricher. Enricher Mailchimp
provides an implementation of an EnricherDatasource plugin to allow Enricher
to interface with Mailchimp's API.

REQUIREMENTS
------------

This module requires the following modules:

 * [Mailchimp](https://www.drupal.org/project/mailchimp):


INSTALLATION
------------

 * Install as you would normally install a contributed Drupal module. Visit
   https://www.drupal.org/node/1897420 for further information.


CONFIGURATION
-------------

 * Configure the user permissions in Administration » People » Permissions:

   - Administer enrichers

     Users with this permission will see the webservices > enrichers
     configuration list page. From here they can add, configure, delete, enable
     and disabled enrichers.

     Warning: Give to trusted roles only; this permission has security
     implications. Allows full administration access to create and edit
     enrichers.

TROUBLESHOOTING
---------------

 * If you are not receiving data back for your user.

   - Check the recent log messages report for exception messages.


MAINTAINERS
-----------

Current maintainers:
 * Eleo Basili (eleonel) - https://www.drupal.org/u/eleonel
 * Naveen Valecha (naveenvalecha) - https://www.drupal.org/u/naveenvalecha

This project has been sponsored by:
 * Morpht Pty Ltd
   We are a team of dedicated and enthusiastic designers, programmers and site
   builders who know how to get the most from Drupal.
   We work for a variety of clients in government, education, media and
   pharmaceutical sectors.
+13 −0
Original line number Diff line number Diff line
convivial_enricher.datasource.mailchimp:
  type: mapping
  label: 'Mailchimp'
  mapping:
    mailchimp_list_id:
      label: 'Mailchimp list ID'
      type: string
    allowed_contact_properties:
      label: 'Contact Properties'
      type: string
    allowed_contact_tags:
      label: 'Contact tags'
      type: string
+8 −0
Original line number Diff line number Diff line
name: Convivial Enricher Mailchimp
type: module
description: Provides integration of Mailchimp API with Enricher.
package: Convivial
core_version_requirement: ^9
dependencies:
  - drupal:convivial_enricher
  - drupal:mailchimp
+21 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Implements the Convivial Enricher Mailchimp module.
 */

use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Implements hook_help().
 */
function convivial_enricher_mailchimp_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.convivial_enricher_mailchimp':
      $output = '';
      $output .= '<h3>' . t('Convivial Enricher Mailchimp') . '</h3>';
      $output .= '<p>' . t('Provides integration of Mailchimp API with Enricher') . '</p>';
      return $output;
  }
}
+385 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\convivial_enricher_mailchimp\Plugin\EnricherDatasource;

use Drupal\convivial_enricher\EnricherDatasourceBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\mailchimp\ClientFactory;
use Drupal\Component\Serialization\Json;

/**
 * Plugin implementation of the datasource.
 *
 * @EnricherDatasource(
 *   id = "mailchimp",
 *   label = @Translation("Mailchimp"),
 *   description = @Translation("Mailchimp datasource for enricher."),
 * )
 */
class MailchimpEnricherDatasource extends EnricherDatasourceBase implements ContainerFactoryPluginInterface {

  /**
   * Mailchimp API client.
   *
   * @var null|\Mailchimp\Mailchimp
   */
  private $mailchimpApiClient;

  /**
   * An object representing a Mailchimp list member.
   *
   * @var object
   */
  private $listMember;

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, ClientFactory $mailchimp_client_factory) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $logger);
    $this->mailchimpApiClient = $mailchimp_client_factory->getByClassNameOrNull('MailchimpLists');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('logger.factory')->get('convivial_enricher'),
      $container->get('mailchimp.client_factory'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getSummary() {
    $summary = parent::getSummary();
    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return parent::defaultConfiguration() + [
      'mailchimp_list_id' => NULL,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form['mailchimp_list_id'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Mailchimp list ID'),
      '#description' => $this->t('The Mailchimp list ID.'),
      '#default_value' => $this->configuration['mailchimp_list_id'],
      '#required' => TRUE,
    ];

    $help = nl2br($this->t("Enter a list of allowed parameters to be retrieved, one per line. All other parameters are ignored.
The '*' character is a wildcard. All entries are treated as a shell wildcard pattern.
Examples:
  123* matches every tag that starts with 123.
  *123 matches every tag that ends 123.
  *123* matches every tag that contains 123.

Other shell wildcards such as ., ?, !, [] are honored for advanced users."));

    $form['allowed_contact_properties'] = [
      '#title' => $this->t('Contact Properties'),
      '#type' => 'textarea',
      '#default_value' => $this->configuration['allowed_contact_properties'] ?? '',
      '#required' => TRUE,
      '#description' => $help,
    ];

    $help = nl2br($this->t("Enter a list of allowed tags to be retrieved, one per line. All other tags are ignored.
The '*' character is a wildcard. All entries are treated as a shell wildcard pattern.
Examples:
  123* matches every tag that starts with 123.
  *123 matches every tag that ends 123.
  *123* matches every tag that contains 123.

Other shell wildcards such as ., ?, !, [] are honored for advanced users."));

    $form['allowed_contact_tags'] = [
      '#title' => $this->t('Contact Tags'),
      '#type' => 'textarea',
      '#default_value' => $this->configuration['allowed_contact_tags'] ?? '',
      '#description' => $help,
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);
    $this->configuration['mailchimp_list_id'] = $form_state->getValue('mailchimp_list_id');
    $this->configuration['allowed_contact_properties'] = $form_state->getValue('allowed_contact_properties');
    $this->configuration['allowed_contact_tags'] = $form_state->getValue('allowed_contact_tags');
  }

  /**
   * {@inheritdoc}
   *
   * Take the incoming path and process it if necessary.
   *
   * Sample use case: converting /mypath/<sometoken>/return/to/path, as
   * drupal does not allow / is parameters, we would need to process this url
   * to encode it differently and then drupal can digest the new path.
   *
   * This is designed to be called from our
   * \Drupal\convivial_enricher\PathProcessor\InboundPathProcessor
   */
  public function processIncomingPath(&$path, $endpoint_path) {
    $this->endpoint_path = $endpoint_path;
    return $this->convertPathToDataEncodedString($path);
  }

  /**
   * Modify the incoming path/convert incoming path to something usable.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return bool
   *   TRUE if path was converted by this method, false if not.
   */
  private function convertPathToDataEncodedString(string &$path): string {
    if ($this->pathRequiresConversion($path)) {
      $token     = $this->getTokenFromPath($path);
      $return_to = $this->getReturnToPortionOfPath($path);

      $encoded_data_string = base64_encode("return_to={$return_to}&token={$token}");

      $path = '/' . $this->endpoint_path . "/data:$encoded_data_string";
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Get the token from the provided path string.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return string
   *   The token value calculated from the supplied path string.
   */
  private function getTokenFromPath(string $path) {
    $token = preg_replace('|^\/' . $this->endpoint_path . '\/(.*?)\/.*$|', '$1', $path);
    return $token;
  }

  /**
   * Get the return_to parameter encoded in the path.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return string
   *   The return_to value calculated from the supplied path string.
   */
  private function getReturnToPortionOfPath(string $path): string {
    $return_to = preg_replace('|^\/' . $this->endpoint_path . '\/.*?(\/.*)$|', '$1', $path);
    return $return_to;
  }

  /**
   * Determine if the format of this incoming path requires conversion.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return bool
   *   TRUE if path requires conversion to a drupal usable format.
   */
  private function pathRequiresConversion(string $path): bool {
    return $this->pathIsAnEndpointPath($path, $this->endpoint_path) && $this->pathDoesNotHaveLoadedData($path, $this->endpoint_path);
  }

  /**
   * Analyse this path to determine if it contains encoded data we can read.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return bool
   *   TRUE if path contains encoded data, FALSE if not.
   */
  private function pathDoesNotHaveLoadedData(string $path): bool {
    return strpos($path, "/$this->endpoint_path/data:") !== 0;
  }

  /**
   * Determine if the given path is our endpoint.
   *
   * Compare the path to the endpoint_path to determine if this is one of
   * our defined endpoints.
   *
   * @param string $path
   *   The incoming path string. eg: The /my/path portion of the incoming URL.
   *
   * @return bool
   *   TRUE if path is a processable endpoint.
   */
  private function pathIsAnEndpointPath(string $path): bool {
    return strpos($path, "/$this->endpoint_path/") === 0;
  }

  /**
   * {@inheritdoc}
   */
  public function fetchAndProcessData($key) {
    $output = [];
    $return_cookies = [];
    $allowed_properties = $this->getAllowedContactProperties();
    $allowed_tags = $this->getAllowedContactTags();

    try {
      $list_id = $this->configuration['mailchimp_list_id'];
      $this->listMember = $this->getListMemberInfoById($list_id, $key);
    }
    catch (\Exception $exception) {
      $variables = Error::decodeException($exception);
      $this->logger->warning('%type: @message in %function (line %line of %file).', $variables);
    }

    // Create the cookies of all the output whitelisted fields.
    foreach ($this->listMember->members as $member) {
      $fields = (array) $member;
      $fields = $this->filterKeyValueSetOnAcceptList($fields, $allowed_properties);
      foreach ($fields as $field_name => $field_value) {

        if ($field_name == 'tags') {
          $tags = $this->getContactTags($field_value);
          $tags = $this->filterKeyValueSetOnAcceptList($tags, $allowed_tags);

          foreach ($tags as $name => $value) {
            $return_cookies[] = $this->createCookie($name, $value);
          }
          continue;
        }
        elseif (is_array($field_value) || is_object($field_value)) {
          $field_value = Json::encode($field_value);
        }

        $return_cookies[] = $this->createCookie($field_name, $field_value);
      }
    }
    return $return_cookies;
  }

  /**
   * Gets information about a member of a Mailchimp list.
   *
   * @param string $list_id
   *   The ID of the list.
   * @param string $uniqid
   *   The member's unique ID (UNIQID).
   *
   * @return null|object
   *   An object representing a Mailchimp list member or NULL if not found.
   */
  protected function getListMemberInfoById($list_id, $uniqid) {
    if (isset($this->mailchimpApiClient)) {
      return $this->mailchimpApiClient->getMemberInfoById($list_id, $uniqid);
    }
  }

  /**
   * Return the $key_values filtered on key by the patterns in the allow_list.
   *
   * @param array $key_values
   *   An array of keys and values to filter.
   * @param array $allow_list
   *   A list of patterns suitable for fnmatch() used to filter by key.
   *
   * @return array
   *   A filtered array of keys and values.
   *
   *   With keys matching patterns in allow_list removed.
   */
  private function filterKeyValueSetOnAcceptList(array $key_values, array $allow_list): array {

    foreach ($key_values as $key => $value) {

      $tag_allowed = FALSE;
      foreach ($allow_list as $pattern) {
        // Ignore PHPCS Warning for "The use of function fnmatch() is
        // discouraged"
        // According to PHP's documentation
        // "For now, this function is not available on non-POSIX compliant
        // systems except Windows."
        // - https://www.php.net/manual/en/function.fnmatch.php
        // Running this on a non-posix compliant system and not windows
        // is very unlikely.
        // @codingStandardsIgnoreStart
        if (is_string($key) && fnmatch(trim($pattern), $key)) {
          // @codingStandardsIgnoreEnd
          $tag_allowed = TRUE;
        }
      }

      if (!$tag_allowed) {
        unset($key_values[$key]);
      }

    }

    return $key_values ?? [];
  }

  /**
   * Gets a list of whitelisted contact properties.
   *
   * @return array
   *   An array with all the allowed contact properties.
   */
  protected function getAllowedContactProperties() {
    return explode(PHP_EOL, $this->configuration['allowed_contact_properties']);
  }

  /**
   * Gets a list of whitelisted contact tags.
   *
   * @return array
   *   An array with all the allowed contact tags.
   */
  protected function getAllowedContactTags() {
    return explode(PHP_EOL, $this->configuration['allowed_contact_tags']);
  }

  /**
   * Gets a list of contact tags.
   *
   * @return array
   *   An array with all the contact tags.
   */
  protected function getContactTags($raw_tags) {
    $tags = [];

    foreach ($raw_tags as $raw_tag) {
      if (str_contains($raw_tag->name, '/')) {
        $tag = explode('/', $raw_tag->name);
        $tags[$tag[0]] = $tag[1];
      }
    }
    return $tags;
  }

}