2463 lines
78 KiB
PHP
Executable File
2463 lines
78 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\PluginBundle\Integration;
|
|
|
|
use Doctrine\ORM\EntityManager;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Handler\CurlHandler;
|
|
use GuzzleHttp\HandlerStack;
|
|
use GuzzleHttp\RequestOptions;
|
|
use Mautic\CoreBundle\Entity\CommonEntity;
|
|
use Mautic\CoreBundle\Entity\FormEntity;
|
|
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
|
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
|
use Mautic\CoreBundle\Helper\PathsHelper;
|
|
use Mautic\CoreBundle\Model\NotificationModel;
|
|
use Mautic\LeadBundle\DataObject\LeadManipulator;
|
|
use Mautic\LeadBundle\Entity\DoNotContact;
|
|
use Mautic\LeadBundle\Entity\Lead;
|
|
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
|
|
use Mautic\LeadBundle\Model\CompanyModel;
|
|
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
|
use Mautic\LeadBundle\Model\FieldModel;
|
|
use Mautic\LeadBundle\Model\LeadModel;
|
|
use Mautic\PluginBundle\Entity\Integration;
|
|
use Mautic\PluginBundle\Entity\IntegrationEntity;
|
|
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
|
|
use Mautic\PluginBundle\Event\PluginIntegrationAuthCallbackUrlEvent;
|
|
use Mautic\PluginBundle\Event\PluginIntegrationFormBuildEvent;
|
|
use Mautic\PluginBundle\Event\PluginIntegrationFormDisplayEvent;
|
|
use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent;
|
|
use Mautic\PluginBundle\Event\PluginIntegrationRequestEvent;
|
|
use Mautic\PluginBundle\Exception\ApiErrorException;
|
|
use Mautic\PluginBundle\Helper\Cleaner;
|
|
use Mautic\PluginBundle\Helper\oAuthHelper;
|
|
use Mautic\PluginBundle\Model\IntegrationEntityModel;
|
|
use Mautic\PluginBundle\PluginEvents;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\Form\FormBuilder;
|
|
use Symfony\Component\Form\FormBuilderInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
use Symfony\Component\Routing\RouterInterface;
|
|
use Symfony\Contracts\Service\Attribute\Required;
|
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
|
|
/**
|
|
* @deprecated To be removed in Mautic 6.0. Please use the IntegrationsBundle instead, which is meant to be a drop-in replacement for AbstractIntegration.
|
|
*/
|
|
abstract class AbstractIntegration implements UnifiedIntegrationInterface
|
|
{
|
|
public const FIELD_TYPE_STRING = 'string';
|
|
|
|
public const FIELD_TYPE_BOOL = 'boolean';
|
|
|
|
public const FIELD_TYPE_NUMBER = 'number';
|
|
|
|
public const FIELD_TYPE_DATETIME = 'datetime';
|
|
|
|
public const FIELD_TYPE_DATE = 'date';
|
|
|
|
protected bool $coreIntegration = false;
|
|
|
|
protected Integration $settings;
|
|
|
|
protected array $keys = [];
|
|
|
|
protected ?CacheStorageHelper $cache;
|
|
|
|
protected ?Request $request;
|
|
|
|
/**
|
|
* Used for notifications.
|
|
*
|
|
* @var \Doctrine\ORM\Tools\Pagination\Paginator<\Mautic\UserBundle\Entity\User>
|
|
*/
|
|
protected ?\Doctrine\ORM\Tools\Pagination\Paginator $adminUsers = null;
|
|
|
|
protected array $notifications = [];
|
|
|
|
protected ?string $lastIntegrationError = null;
|
|
|
|
protected array $mauticDuplicates = [];
|
|
|
|
protected array $salesforceIdMapping = [];
|
|
|
|
protected array $deleteIntegrationEntities = [];
|
|
|
|
protected array $persistIntegrationEntities = [];
|
|
|
|
protected array $commandParameters = [];
|
|
private \Closure $clientFactory;
|
|
|
|
public function __construct(
|
|
protected EventDispatcherInterface $dispatcher,
|
|
CacheStorageHelper $cacheStorageHelper,
|
|
protected EntityManager $em,
|
|
protected RequestStack $requestStack,
|
|
protected RouterInterface $router,
|
|
protected TranslatorInterface $translator,
|
|
protected LoggerInterface $logger,
|
|
protected EncryptionHelper $encryptionHelper,
|
|
protected LeadModel $leadModel,
|
|
protected CompanyModel $companyModel,
|
|
protected PathsHelper $pathsHelper,
|
|
protected NotificationModel $notificationModel,
|
|
protected FieldModel $fieldModel,
|
|
protected IntegrationEntityModel $integrationEntityModel,
|
|
protected DoNotContactModel $doNotContact,
|
|
protected FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
|
) {
|
|
$this->cache = $cacheStorageHelper->getCache($this->getName());
|
|
$this->request = (!defined('IN_MAUTIC_CONSOLE')) ? $requestStack->getCurrentRequest() : null;
|
|
|
|
$this->setClientFactory(fn (array $options): Client => new Client([
|
|
'handler' => HandlerStack::create(new CurlHandler([
|
|
'options' => $options,
|
|
])),
|
|
]));
|
|
}
|
|
|
|
public function setCommandParameters(array $params): void
|
|
{
|
|
$this->commandParameters = $params;
|
|
}
|
|
|
|
/**
|
|
* @return CacheStorageHelper
|
|
*/
|
|
public function getCache()
|
|
{
|
|
return $this->cache;
|
|
}
|
|
|
|
/**
|
|
* @return TranslatorInterface
|
|
*/
|
|
public function getTranslator()
|
|
{
|
|
return $this->translator;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isCoreIntegration()
|
|
{
|
|
return $this->coreIntegration;
|
|
}
|
|
|
|
/**
|
|
* Determines what priority the integration should have against the other integrations.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getPriority()
|
|
{
|
|
return 9999;
|
|
}
|
|
|
|
/**
|
|
* Determines if DNC records should be updated by date or by priority.
|
|
*/
|
|
public function updateDncByDate(): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the social integration that must match the name of the file
|
|
* For example, IcontactIntegration would need Icontact here.
|
|
*
|
|
* @return string
|
|
*/
|
|
abstract public function getName();
|
|
|
|
/**
|
|
* Name to display for the integration. e.g. iContact Uses value of getName() by default.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
return $this->getName();
|
|
}
|
|
|
|
/**
|
|
* Returns a description shown in the config form.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDescription()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get icon for Integration.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getIcon()
|
|
{
|
|
$systemPath = $this->pathsHelper->getSystemPath('root');
|
|
$bundlePath = $this->pathsHelper->getSystemPath('bundles');
|
|
$pluginPath = $this->pathsHelper->getSystemPath('plugins');
|
|
$genericIcon = $bundlePath.'/PluginBundle/Assets/img/generic.png';
|
|
|
|
$name = $this->getName();
|
|
$bundle = $this->settings->getPlugin()->getBundle();
|
|
$icon = $pluginPath.'/'.$bundle.'/Assets/img/'.strtolower($name).'.png';
|
|
|
|
if (file_exists($systemPath.'/'.$icon)) {
|
|
return $icon;
|
|
}
|
|
|
|
return $genericIcon;
|
|
}
|
|
|
|
/**
|
|
* Get the type of authentication required for this API. Values can be none, key, oauth2 or callback
|
|
* (will call $this->authenticationTypeCallback).
|
|
*
|
|
* @return string
|
|
*/
|
|
abstract public function getAuthenticationType();
|
|
|
|
/**
|
|
* Get if data priority is enabled in the integration or not default is false.
|
|
*/
|
|
public function getDataPriority(): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get a list of supported features for this integration.
|
|
*
|
|
* Options are:
|
|
* cloud_storage - Asset remote storage
|
|
* public_profile - Lead social profile
|
|
* public_activity - Lead social activity
|
|
* share_button - Landing page share button
|
|
* sso_service - SSO using 3rd party service via sso_login and sso_login_check routes
|
|
* sso_form - SSO using submitted credentials through the login form
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSupportedFeatures()
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get a list of tooltips for the specified supported features.
|
|
* This allows you to add detail / informational tooltips to your
|
|
* supported feature checkbox group.
|
|
*
|
|
* Example:
|
|
* 'cloud_storage' => 'mautic.integration.form.features.cloud_storage.tooltip'
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
public function getSupportedFeatureTooltips()
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns the field the integration needs in order to find the user.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getIdentifierFields()
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Allows integration to set a custom form template.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFormTemplate()
|
|
{
|
|
return '@MauticPlugin/Integration/form.html.twig';
|
|
}
|
|
|
|
/**
|
|
* Allows integration to set a custom theme folder.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFormTheme()
|
|
{
|
|
return '@MauticPlugin/FormTheme/Integration/layout.html.twig';
|
|
}
|
|
|
|
/**
|
|
* Set the social integration entity.
|
|
*/
|
|
public function setIntegrationSettings(Integration $settings): void
|
|
{
|
|
$this->settings = $settings;
|
|
|
|
$this->keys = $this->getDecryptedApiKeys();
|
|
}
|
|
|
|
/**
|
|
* Get the social integration entity.
|
|
*
|
|
* @return Integration
|
|
*/
|
|
public function getIntegrationSettings()
|
|
{
|
|
return $this->settings;
|
|
}
|
|
|
|
/**
|
|
* Persist settings to the database.
|
|
*/
|
|
public function persistIntegrationSettings(): void
|
|
{
|
|
$this->em->persist($this->settings);
|
|
$this->em->flush();
|
|
}
|
|
|
|
/**
|
|
* Merge api keys.
|
|
*
|
|
* @param bool|false $return Returns the key array rather than setting them
|
|
*
|
|
* @return void|array
|
|
*/
|
|
public function mergeApiKeys($mergeKeys, $withKeys = [], $return = false)
|
|
{
|
|
$settings = $this->settings;
|
|
if (empty($withKeys)) {
|
|
$withKeys = $this->keys;
|
|
}
|
|
|
|
foreach ($withKeys as $k => $v) {
|
|
if (!empty($mergeKeys[$k])) {
|
|
$withKeys[$k] = $mergeKeys[$k];
|
|
}
|
|
unset($mergeKeys[$k]);
|
|
}
|
|
|
|
// merge remaining new keys
|
|
$withKeys = array_merge($withKeys, $mergeKeys);
|
|
|
|
if ($return) {
|
|
$this->keys = $this->dispatchIntegrationKeyEvent(
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_MERGE,
|
|
$withKeys
|
|
);
|
|
|
|
return $this->keys;
|
|
} else {
|
|
$this->encryptAndSetApiKeys($withKeys, $settings);
|
|
|
|
// reset for events that depend on rebuilding auth objects
|
|
$this->setIntegrationSettings($settings);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encrypts and saves keys to the entity.
|
|
*/
|
|
public function encryptAndSetApiKeys(array $keys, Integration $entity): void
|
|
{
|
|
$keys = $this->dispatchIntegrationKeyEvent(
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT,
|
|
$keys
|
|
);
|
|
|
|
// Update keys
|
|
$this->keys = array_merge($this->keys, $keys);
|
|
|
|
$encrypted = $this->encryptApiKeys($keys);
|
|
$entity->setApiKeys($encrypted);
|
|
}
|
|
|
|
/**
|
|
* Returns already decrypted keys.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getKeys()
|
|
{
|
|
return $this->keys;
|
|
}
|
|
|
|
/**
|
|
* Returns decrypted API keys.
|
|
*
|
|
* @param bool $entity
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getDecryptedApiKeys($entity = false)
|
|
{
|
|
static $decryptedKeys = [];
|
|
|
|
if (!$entity) {
|
|
$entity = $this->settings;
|
|
}
|
|
|
|
$keys = $entity->getApiKeys();
|
|
|
|
$serialized = serialize($keys);
|
|
if (empty($decryptedKeys[$serialized])) {
|
|
$decrypted = $this->decryptApiKeys($keys, true);
|
|
if (0 !== count($keys) && 0 === count($decrypted)) {
|
|
$decrypted = $this->decryptApiKeys($keys);
|
|
$this->encryptAndSetApiKeys($decrypted, $entity);
|
|
$this->em->flush($entity);
|
|
}
|
|
$decryptedKeys[$serialized] = $this->dispatchIntegrationKeyEvent(
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_DECRYPT,
|
|
$decrypted
|
|
);
|
|
}
|
|
|
|
return $decryptedKeys[$serialized];
|
|
}
|
|
|
|
/**
|
|
* Encrypts API keys.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function encryptApiKeys(array $keys)
|
|
{
|
|
$encrypted = [];
|
|
|
|
foreach ($keys as $name => $key) {
|
|
$key = $this->encryptionHelper->encrypt($key);
|
|
$encrypted[$name] = $key;
|
|
}
|
|
|
|
return $encrypted;
|
|
}
|
|
|
|
/**
|
|
* Decrypts API keys.
|
|
*
|
|
* @param bool $mainDecryptOnly
|
|
*
|
|
* @return array
|
|
*/
|
|
public function decryptApiKeys(array $keys, $mainDecryptOnly = false)
|
|
{
|
|
$decrypted = [];
|
|
|
|
foreach ($keys as $name => $key) {
|
|
$key = $this->encryptionHelper->decrypt($key, $mainDecryptOnly);
|
|
if (false === $key) {
|
|
continue;
|
|
}
|
|
$decrypted[$name] = $key;
|
|
}
|
|
|
|
return $decrypted;
|
|
}
|
|
|
|
/**
|
|
* Get the array key for clientId.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getClientIdKey()
|
|
{
|
|
return match ($this->getAuthenticationType()) {
|
|
'oauth1a' => 'consumer_id',
|
|
'oauth2' => 'client_id',
|
|
'key' => 'key',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the array key for client secret.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getClientSecretKey()
|
|
{
|
|
return match ($this->getAuthenticationType()) {
|
|
'oauth1a' => 'consumer_secret',
|
|
'oauth2' => 'client_secret',
|
|
'basic' => 'password',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Array of keys to mask in the config form.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSecretKeys()
|
|
{
|
|
return [$this->getClientSecretKey()];
|
|
}
|
|
|
|
/**
|
|
* Get the array key for the auth token.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthTokenKey()
|
|
{
|
|
return match ($this->getAuthenticationType()) {
|
|
'oauth2' => 'access_token',
|
|
'oauth1a' => 'oauth_token',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the keys for the refresh token and expiry.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getRefreshTokenKeys()
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get a list of keys required to make an API call. Examples are key, clientId, clientSecret.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getRequiredKeyFields()
|
|
{
|
|
return match ($this->getAuthenticationType()) {
|
|
'oauth1a' => [
|
|
'consumer_id' => 'mautic.integration.keyfield.consumerid',
|
|
'consumer_secret' => 'mautic.integration.keyfield.consumersecret',
|
|
],
|
|
'oauth2' => [
|
|
'client_id' => 'mautic.integration.keyfield.clientid',
|
|
'client_secret' => 'mautic.integration.keyfield.clientsecret',
|
|
],
|
|
'key' => [
|
|
'key' => 'mautic.integration.keyfield.api',
|
|
],
|
|
'basic' => [
|
|
'username' => 'mautic.integration.keyfield.username',
|
|
'password' => 'mautic.integration.keyfield.password',
|
|
],
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract the tokens returned by the oauth callback.
|
|
*
|
|
* @param string $data
|
|
* @param bool $postAuthorization
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function parseCallbackResponse($data, $postAuthorization = false)
|
|
{
|
|
// remove control characters that will break json_decode from parsing
|
|
$data = preg_replace('/[[:cntrl:]]/', '', $data);
|
|
if (!$parsed = json_decode($data, true)) {
|
|
parse_str($data, $parsed);
|
|
}
|
|
|
|
return $parsed;
|
|
}
|
|
|
|
/**
|
|
* Generic error parser.
|
|
*
|
|
* @return string|mixed[]
|
|
*/
|
|
public function getErrorsFromResponse($response)
|
|
{
|
|
if (is_object($response)) {
|
|
if (!empty($response->errors)) {
|
|
$errors = [];
|
|
foreach ($response->errors as $e) {
|
|
$errors[] = $e->message;
|
|
}
|
|
|
|
return implode('; ', $errors);
|
|
} elseif (!empty($response->error->message)) {
|
|
return $response->error->message;
|
|
} else {
|
|
return (string) $response;
|
|
}
|
|
} elseif (is_array($response)) {
|
|
if (isset($response['error_description'])) {
|
|
return $response['error_description'];
|
|
} elseif (isset($response['error'])) {
|
|
if (is_array($response['error'])) {
|
|
if (isset($response['error']['message'])) {
|
|
return $response['error']['message'];
|
|
} else {
|
|
return implode(', ', $response['error']);
|
|
}
|
|
} else {
|
|
return $response['error'];
|
|
}
|
|
} elseif (isset($response['errors'])) {
|
|
$errors = [];
|
|
foreach ($response['errors'] as $err) {
|
|
if (is_array($err)) {
|
|
if (isset($err['message'])) {
|
|
$errors[] = $err['message'];
|
|
} else {
|
|
$errors[] = implode(', ', $err);
|
|
}
|
|
} else {
|
|
$errors[] = $err;
|
|
}
|
|
}
|
|
|
|
return implode('; ', $errors);
|
|
}
|
|
|
|
return $response;
|
|
} else {
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make a basic call using cURL to get the data.
|
|
*
|
|
* @param string $url
|
|
* @param array $parameters
|
|
* @param string $method
|
|
* @param array $settings Set $settings['return_raw'] to receive a ResponseInterface
|
|
*
|
|
* @return mixed|string|ResponseInterface
|
|
*/
|
|
public function makeRequest($url, $parameters = [], $method = 'GET', $settings = [])
|
|
{
|
|
// If not authorizing the session itself, check isAuthorized which will refresh tokens if applicable
|
|
if (empty($settings['authorize_session'])) {
|
|
$this->isAuthorized();
|
|
}
|
|
|
|
$method = strtoupper($method);
|
|
$authType = (empty($settings['auth_type'])) ? $this->getAuthenticationType() : $settings['auth_type'];
|
|
|
|
[$parameters, $headers] = $this->prepareRequest($url, $parameters, $method, $settings, $authType);
|
|
|
|
if (empty($settings['ignore_event_dispatch'])) {
|
|
$event = $this->dispatcher->dispatch(
|
|
new PluginIntegrationRequestEvent($this, $url, $parameters, $headers, $method, $settings, $authType),
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_REQUEST
|
|
);
|
|
|
|
$headers = $event->getHeaders();
|
|
$parameters = $event->getParameters();
|
|
}
|
|
|
|
if (!isset($settings['query'])) {
|
|
$settings['query'] = [];
|
|
}
|
|
|
|
if (isset($parameters['append_to_query'])) {
|
|
$settings['query'] = array_merge(
|
|
$settings['query'],
|
|
$parameters['append_to_query']
|
|
);
|
|
|
|
unset($parameters['append_to_query']);
|
|
}
|
|
|
|
if (isset($parameters['post_append_to_query'])) {
|
|
$postAppend = $parameters['post_append_to_query'];
|
|
unset($parameters['post_append_to_query']);
|
|
}
|
|
|
|
if (!$this->isConfigured()) {
|
|
return [
|
|
'error' => [
|
|
'message' => $this->translator->trans(
|
|
'mautic.integration.missingkeys'
|
|
),
|
|
],
|
|
];
|
|
}
|
|
|
|
if ('GET' == $method && !empty($parameters)) {
|
|
$parameters = array_merge($settings['query'], $parameters);
|
|
$query = http_build_query($parameters);
|
|
$url .= (!str_contains($url, '?')) ? '?'.$query : '&'.$query;
|
|
} elseif (!empty($settings['query'])) {
|
|
$query = http_build_query($settings['query']);
|
|
$url .= (!str_contains($url, '?')) ? '?'.$query : '&'.$query;
|
|
}
|
|
|
|
if (isset($postAppend)) {
|
|
$url .= $postAppend;
|
|
}
|
|
|
|
// Check for custom content-type header
|
|
if (!empty($settings['content_type'])) {
|
|
$settings['encoding_headers_set'] = true;
|
|
$headers[] = "Content-Type: {$settings['content_type']}";
|
|
}
|
|
|
|
if ('GET' !== $method) {
|
|
if (!empty($parameters)) {
|
|
if ('oauth1a' == $authType) {
|
|
$parameters = http_build_query($parameters);
|
|
}
|
|
if (!empty($settings['encode_parameters'])) {
|
|
if ('json' == $settings['encode_parameters']) {
|
|
// encode the arguments as JSON
|
|
$parameters = json_encode($parameters);
|
|
if (empty($settings['encoding_headers_set'])) {
|
|
$headers[] = 'Content-Type: application/json';
|
|
}
|
|
}
|
|
}
|
|
} elseif (isset($settings['post_data'])) {
|
|
$parameters = $settings['post_data'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set some cURL settings for backward compatibility
|
|
* https://docs.guzzlephp.org/en/latest/faq.html?highlight=curl#how-can-i-add-custom-curl-options.
|
|
*/
|
|
$options = [
|
|
CURLOPT_HEADER => 1,
|
|
CURLOPT_RETURNTRANSFER => 1,
|
|
CURLOPT_FOLLOWLOCATION => 0,
|
|
CURLOPT_REFERER => $this->getRefererUrl(),
|
|
CURLOPT_USERAGENT => $this->getUserAgent(),
|
|
];
|
|
|
|
if (isset($settings['curl_options']) && is_array($settings['curl_options'])) {
|
|
$options = $settings['curl_options'] + $options;
|
|
}
|
|
|
|
if (isset($settings['ssl_verifypeer'])) {
|
|
$options[CURLOPT_SSL_VERIFYPEER] = $settings['ssl_verifypeer'];
|
|
}
|
|
|
|
$client = $this->makeHttpClient($options);
|
|
|
|
$parseHeaders = (isset($settings['headers'])) ? array_merge($headers, $settings['headers']) : $headers;
|
|
// HTTP library requires that headers are in key => value pairs
|
|
$headers = [];
|
|
if (is_array($parseHeaders)) {
|
|
foreach ($parseHeaders as $key => $value) {
|
|
// Ignore string keys which assume it is already parsed and avoids splitting up a value that includes colons (such as a date/time)
|
|
if (!is_string($key) && str_contains($value, ':')) {
|
|
[$key, $value] = explode(':', $value);
|
|
$key = trim($key);
|
|
$value = trim($value);
|
|
}
|
|
|
|
$headers[$key] = $value;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$timeout = (isset($settings['request_timeout'])) ? (int) $settings['request_timeout'] : 10;
|
|
switch ($method) {
|
|
case 'GET':
|
|
$result = $client->get($url, [
|
|
RequestOptions::HEADERS => $headers,
|
|
RequestOptions::TIMEOUT => $timeout,
|
|
]);
|
|
break;
|
|
case 'POST':
|
|
case 'PUT':
|
|
case 'PATCH':
|
|
$payloadKey = is_string($parameters) ? RequestOptions::BODY : RequestOptions::FORM_PARAMS;
|
|
$result = $client->request($method, $url, [
|
|
$payloadKey => $parameters,
|
|
RequestOptions::HEADERS => $headers,
|
|
RequestOptions::TIMEOUT => $timeout,
|
|
]);
|
|
break;
|
|
case 'DELETE':
|
|
$result = $client->delete($url, [
|
|
RequestOptions::HEADERS => $headers,
|
|
RequestOptions::TIMEOUT => $timeout,
|
|
]);
|
|
break;
|
|
}
|
|
} catch (\GuzzleHttp\Exception\RequestException $exception) {
|
|
if (!empty($settings['return_raw'])) {
|
|
return $exception->getResponse();
|
|
} else {
|
|
return [
|
|
'error' => [
|
|
'message' => $exception->getResponse()->getBody()->getContents(),
|
|
'code' => $exception->getCode(),
|
|
],
|
|
];
|
|
}
|
|
}
|
|
if (empty($settings['ignore_event_dispatch'])) {
|
|
$event->setResponse($result);
|
|
$this->dispatcher->dispatch(
|
|
$event,
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_RESPONSE
|
|
);
|
|
}
|
|
if (!empty($settings['return_raw'])) {
|
|
return $result;
|
|
} else {
|
|
return $this->parseCallbackResponse($result->getBody(), !empty($settings['authorize_session']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param bool $persist
|
|
*/
|
|
public function createIntegrationEntity(
|
|
$integrationEntity,
|
|
$integrationEntityId,
|
|
$internalEntity,
|
|
$internalEntityId,
|
|
?array $internal = null,
|
|
$persist = true,
|
|
): ?IntegrationEntity {
|
|
$date = (defined('MAUTIC_DATE_MODIFIED_OVERRIDE')) ? \DateTime::createFromFormat('U', MAUTIC_DATE_MODIFIED_OVERRIDE)
|
|
: new \DateTime();
|
|
$entity = new IntegrationEntity();
|
|
$entity->setDateAdded($date)
|
|
->setLastSyncDate($date)
|
|
->setIntegration($this->getName())
|
|
->setIntegrationEntity($integrationEntity)
|
|
->setIntegrationEntityId($integrationEntityId)
|
|
->setInternalEntity($internalEntity)
|
|
->setInternal($internal)
|
|
->setInternalEntityId($internalEntityId);
|
|
|
|
if ($persist) {
|
|
$this->em->getRepository(IntegrationEntity::class)->saveEntity($entity);
|
|
}
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* @return IntegrationEntityRepository
|
|
*/
|
|
public function getIntegrationEntityRepository()
|
|
{
|
|
return $this->em->getRepository(IntegrationEntity::class);
|
|
}
|
|
|
|
/**
|
|
* Method to prepare the request parameters. Builds array of headers and parameters.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function prepareRequest($url, $parameters, $method, $settings, $authType)
|
|
{
|
|
$clientIdKey = $this->getClientIdKey();
|
|
$clientSecretKey = $this->getClientSecretKey();
|
|
$authTokenKey = $this->getAuthTokenKey();
|
|
$authToken = '';
|
|
if (isset($settings['override_auth_token'])) {
|
|
$authToken = $settings['override_auth_token'];
|
|
} elseif (isset($this->keys[$authTokenKey])) {
|
|
$authToken = $this->keys[$authTokenKey];
|
|
}
|
|
|
|
// Override token parameter key if neede
|
|
if (!empty($settings[$authTokenKey])) {
|
|
$authTokenKey = $settings[$authTokenKey];
|
|
}
|
|
|
|
$headers = [];
|
|
|
|
if (!empty($settings['authorize_session'])) {
|
|
switch ($authType) {
|
|
case 'oauth1a':
|
|
$requestTokenUrl = $this->getRequestTokenUrl();
|
|
if (!array_key_exists('append_callback', $settings) && !empty($requestTokenUrl)) {
|
|
$settings['append_callback'] = false;
|
|
}
|
|
$oauthHelper = new oAuthHelper($this, $this->request, $settings);
|
|
$headers = $oauthHelper->getAuthorizationHeader($url, $parameters, $method);
|
|
break;
|
|
case 'oauth2':
|
|
if ($bearerToken = $this->getBearerToken(true)) {
|
|
$headers = [
|
|
"Authorization: Basic {$bearerToken}",
|
|
'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
|
|
];
|
|
$parameters['grant_type'] = 'client_credentials';
|
|
} else {
|
|
$defaultGrantType = (!empty($settings['refresh_token'])) ? 'refresh_token'
|
|
: 'authorization_code';
|
|
$grantType = (!isset($settings['grant_type'])) ? $defaultGrantType
|
|
: $settings['grant_type'];
|
|
|
|
$useClientIdKey = (empty($settings[$clientIdKey])) ? $clientIdKey : $settings[$clientIdKey];
|
|
$useClientSecretKey = (empty($settings[$clientSecretKey])) ? $clientSecretKey
|
|
: $settings[$clientSecretKey];
|
|
$parameters = array_merge(
|
|
$parameters,
|
|
[
|
|
$useClientIdKey => $this->keys[$clientIdKey],
|
|
$useClientSecretKey => $this->keys[$clientSecretKey] ?? '',
|
|
'grant_type' => $grantType,
|
|
]
|
|
);
|
|
|
|
if (!empty($settings['refresh_token']) && !empty($this->keys[$settings['refresh_token']])) {
|
|
$parameters[$settings['refresh_token']] = $this->keys[$settings['refresh_token']];
|
|
}
|
|
|
|
if ('authorization_code' === $grantType) {
|
|
$parameters['code'] = $this->request->get('code');
|
|
}
|
|
if (empty($settings['ignore_redirecturi'])) {
|
|
$callback = $this->getAuthCallbackUrl();
|
|
$parameters['redirect_uri'] = $callback;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
switch ($authType) {
|
|
case 'basic':
|
|
$headers = [
|
|
'Authorization' => 'Basic '.base64_encode($this->keys['username'].':'.$this->keys['password']),
|
|
];
|
|
break;
|
|
case 'oauth1a':
|
|
$oauthHelper = new oAuthHelper($this, $this->request, $settings);
|
|
$headers = $oauthHelper->getAuthorizationHeader($url, $parameters, $method);
|
|
break;
|
|
case 'oauth2':
|
|
if ($bearerToken = $this->getBearerToken()) {
|
|
$headers = [
|
|
"Authorization: Bearer {$bearerToken}",
|
|
// "Content-Type: application/x-www-form-urlencoded;charset=UTF-8"
|
|
];
|
|
} else {
|
|
if (!empty($settings['append_auth_token'])) {
|
|
// Workaround because $settings cannot be manipulated here
|
|
$parameters['append_to_query'] = [
|
|
$authTokenKey => $authToken,
|
|
];
|
|
} else {
|
|
$parameters[$authTokenKey] = $authToken;
|
|
}
|
|
|
|
$headers = [
|
|
"oauth-token: $authTokenKey",
|
|
"Authorization: OAuth {$authToken}",
|
|
];
|
|
}
|
|
break;
|
|
case 'key':
|
|
$parameters[$authTokenKey] = $authToken;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [$parameters, $headers];
|
|
}
|
|
|
|
/**
|
|
* Generate the auth login URL. Note that if oauth2, response_type=code is assumed. If this is not the case,
|
|
* override this function.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthLoginUrl()
|
|
{
|
|
$authType = $this->getAuthenticationType();
|
|
|
|
if ('oauth2' == $authType) {
|
|
$callback = $this->getAuthCallbackUrl();
|
|
$clientIdKey = $this->getClientIdKey();
|
|
$state = $this->getAuthLoginState();
|
|
$url = $this->getAuthenticationUrl()
|
|
.'?client_id='.$this->keys[$clientIdKey]
|
|
.'&response_type=code'
|
|
.'&redirect_uri='.urlencode($callback)
|
|
.'&state='.$state;
|
|
|
|
if ($scope = $this->getAuthScope()) {
|
|
$url .= '&scope='.urlencode($scope);
|
|
}
|
|
|
|
if ($this->requestStack->getCurrentRequest()?->hasSession()) {
|
|
$this->requestStack->getSession()->set($this->getName().'_csrf_token', $state);
|
|
}
|
|
|
|
return $url;
|
|
} else {
|
|
return $this->router->generate(
|
|
'mautic_integration_auth_callback',
|
|
['integration' => $this->getName()]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* State variable to append to login url (usually used in oAuth flows).
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthLoginState()
|
|
{
|
|
return hash('sha1', uniqid(mt_rand()));
|
|
}
|
|
|
|
/**
|
|
* Get the scope for auth flows.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthScope()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Gets the URL for the built in oauth callback.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthCallbackUrl()
|
|
{
|
|
$defaultUrl = $this->router->generate(
|
|
'mautic_integration_auth_callback',
|
|
['integration' => $this->getName()],
|
|
UrlGeneratorInterface::ABSOLUTE_URL // absolute
|
|
);
|
|
|
|
/** @var PluginIntegrationAuthCallbackUrlEvent $event */
|
|
$event = $this->dispatcher->dispatch(
|
|
new PluginIntegrationAuthCallbackUrlEvent($this, $defaultUrl),
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_GET_AUTH_CALLBACK_URL
|
|
);
|
|
|
|
return $event->getCallbackUrl();
|
|
}
|
|
|
|
/**
|
|
* Retrieves and stores tokens returned from oAuthLogin.
|
|
*
|
|
* @param mixed[] $settings
|
|
* @param mixed[] $parameters
|
|
*
|
|
* @return bool|string false if no error; otherwise the error string
|
|
*
|
|
* @throws ApiErrorException if OAuth2 state does not match
|
|
*/
|
|
public function authCallback($settings = [], $parameters = [])
|
|
{
|
|
$authType = $this->getAuthenticationType();
|
|
|
|
switch ($authType) {
|
|
case 'oauth2':
|
|
if ($this->requestStack->getCurrentRequest()?->hasSession()) {
|
|
$state = $this->requestStack->getSession()->get($this->getName().'_csrf_token', false);
|
|
$givenState = ($this->request->isXmlHttpRequest()) ? $this->request->request->get('state') : $this->request->get('state');
|
|
|
|
if ($state && $state !== $givenState) {
|
|
$this->requestStack->getSession()->remove($this->getName().'_csrf_token');
|
|
throw new ApiErrorException($this->translator->trans('mautic.integration.auth.invalid.state'));
|
|
}
|
|
}
|
|
|
|
if (!empty($settings['use_refresh_token'])) {
|
|
// Try refresh token
|
|
$refreshTokenKeys = $this->getRefreshTokenKeys();
|
|
|
|
if (!empty($refreshTokenKeys)) {
|
|
[$refreshTokenKey, $expiryKey] = $refreshTokenKeys;
|
|
|
|
$settings['refresh_token'] = $refreshTokenKey;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'oauth1a':
|
|
// After getting request_token and authorizing, post back to access_token
|
|
$settings['append_callback'] = true;
|
|
$settings['include_verifier'] = true;
|
|
|
|
// Get request token returned from Twitter and submit it to get access_token
|
|
$settings['request_token'] = ($this->request) ? $this->request->get('oauth_token') : '';
|
|
|
|
break;
|
|
}
|
|
|
|
$settings['authorize_session'] = true;
|
|
|
|
$method = (!isset($settings['method'])) ? 'POST' : $settings['method'];
|
|
$data = $this->makeRequest($this->getAccessTokenUrl(), $parameters, $method, $settings);
|
|
|
|
return $this->extractAuthKeys($data);
|
|
}
|
|
|
|
/**
|
|
* Extacts the auth keys from response and saves entity.
|
|
*
|
|
* @return bool|string false if no error; otherwise the error string
|
|
*/
|
|
public function extractAuthKeys($data, $tokenOverride = null)
|
|
{
|
|
// check to see if an entity exists
|
|
$entity = $this->getIntegrationSettings();
|
|
if (null == $entity) {
|
|
$entity = new Integration();
|
|
$entity->setName($this->getName());
|
|
}
|
|
// Prepare the keys for extraction such as renaming, setting expiry, etc
|
|
$data = $this->prepareResponseForExtraction($data);
|
|
|
|
// parse the response
|
|
$authTokenKey = $tokenOverride ?: $this->getAuthTokenKey();
|
|
if (is_array($data) && isset($data[$authTokenKey])) {
|
|
$keys = $this->mergeApiKeys($data, null, true);
|
|
$encrypted = $this->encryptApiKeys($keys);
|
|
$entity->setApiKeys($encrypted);
|
|
|
|
if ($this->requestStack->getCurrentRequest()?->hasSession()) {
|
|
$this->requestStack->getSession()->set($this->getName().'_tokenResponse', $data);
|
|
}
|
|
|
|
$error = false;
|
|
} elseif (is_array($data) && isset($data['access_token'])) {
|
|
if ($this->requestStack->getCurrentRequest()?->hasSession()) {
|
|
$this->requestStack->getSession()->set($this->getName().'_tokenResponse', $data);
|
|
}
|
|
$error = false;
|
|
} else {
|
|
$error = $this->getErrorsFromResponse($data);
|
|
if (empty($error)) {
|
|
$error = $this->translator->trans(
|
|
'mautic.integration.error.genericerror',
|
|
[],
|
|
'flashes'
|
|
);
|
|
}
|
|
}
|
|
|
|
// save the data
|
|
$this->em->persist($entity);
|
|
$this->em->flush();
|
|
|
|
$this->setIntegrationSettings($entity);
|
|
|
|
return $error;
|
|
}
|
|
|
|
/**
|
|
* Called in extractAuthKeys before key comparison begins to give opportunity to set expiry, rename keys, etc.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function prepareResponseForExtraction($data)
|
|
{
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the integration is configured by checking that required keys are populated.
|
|
*/
|
|
public function isConfigured(): bool
|
|
{
|
|
$requiredTokens = $this->getRequiredKeyFields();
|
|
foreach ($requiredTokens as $token => $label) {
|
|
if (empty($this->keys[$token])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if an integration is authorized and/or authorizes the request.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isAuthorized()
|
|
{
|
|
if (!$this->isConfigured()) {
|
|
return false;
|
|
}
|
|
|
|
$type = $this->getAuthenticationType();
|
|
$authTokenKey = $this->getAuthTokenKey();
|
|
|
|
switch ($type) {
|
|
case 'oauth1a':
|
|
case 'oauth2':
|
|
$refreshTokenKeys = $this->getRefreshTokenKeys();
|
|
if (!isset($this->keys[$authTokenKey])) {
|
|
$valid = false;
|
|
} elseif (!empty($refreshTokenKeys)) {
|
|
[$refreshTokenKey, $expiryKey] = $refreshTokenKeys;
|
|
if (!empty($this->keys[$refreshTokenKey]) && !empty($expiryKey) && isset($this->keys[$expiryKey])
|
|
&& time() > $this->keys[$expiryKey]
|
|
) {
|
|
// token has expired so try to refresh it
|
|
$error = $this->authCallback(['refresh_token' => $refreshTokenKey]);
|
|
$valid = (empty($error));
|
|
} else {
|
|
// The refresh token doesn't have an expiry so the integration will have to check for expired sessions and request new token
|
|
$valid = true;
|
|
}
|
|
} else {
|
|
$valid = true;
|
|
}
|
|
break;
|
|
case 'key':
|
|
$valid = isset($this->keys['api_key']);
|
|
break;
|
|
case 'rest':
|
|
$valid = isset($this->keys[$authTokenKey]);
|
|
break;
|
|
case 'basic':
|
|
$valid = (!empty($this->keys['username']) && !empty($this->keys['password']));
|
|
break;
|
|
default:
|
|
$valid = true;
|
|
break;
|
|
}
|
|
|
|
return $valid;
|
|
}
|
|
|
|
/**
|
|
* Get the URL required to obtain an oauth2 access token.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAccessTokenUrl()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get the authentication/login URL for oauth2 access.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAuthenticationUrl()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get request token for oauth1a authorization request.
|
|
*
|
|
* @param array $settings
|
|
*
|
|
* @return mixed|string
|
|
*/
|
|
public function getRequestToken($settings = [])
|
|
{
|
|
// Child classes can easily pass in custom settings this way
|
|
$settings = array_merge(
|
|
['authorize_session' => true, 'append_callback' => false, 'ssl_verifypeer' => true],
|
|
$settings
|
|
);
|
|
|
|
// init result to empty string
|
|
$result = '';
|
|
|
|
$url = $this->getRequestTokenUrl();
|
|
if (!empty($url)) {
|
|
$result = $this->makeRequest(
|
|
$url,
|
|
[],
|
|
'POST',
|
|
$settings
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Url to post in order to get the request token if required; leave empty if not required.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRequestTokenUrl()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Generate a bearer token.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBearerToken($inAuthorization = false)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get an array of public activity.
|
|
*
|
|
* @return array|void
|
|
*/
|
|
public function getPublicActivity($identifier, &$socialCache)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Get an array of public data.
|
|
*
|
|
* @return mixed[]|void
|
|
*/
|
|
public function getUserData($identifier, &$socialCache)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Generates current URL to set as referer for curl calls.
|
|
*/
|
|
protected function getRefererUrl(): ?string
|
|
{
|
|
return ($this->request) ? $this->request->getRequestUri() : null;
|
|
}
|
|
|
|
/**
|
|
* Generate a user agent string.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getUserAgent()
|
|
{
|
|
return ($this->request) ? $this->request->server->get('HTTP_USER_AGENT') : null;
|
|
}
|
|
|
|
/**
|
|
* Get a list of available fields from the connecting API.
|
|
*
|
|
* @param mixed[] $settings
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function getAvailableLeadFields(array $settings = []): array
|
|
{
|
|
if (empty($settings['ignore_field_cache'])) {
|
|
$cacheSuffix = $settings['cache_suffix'] ?? '';
|
|
if ($fields = $this->cache->get('leadFields'.$cacheSuffix)) {
|
|
return $fields;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function cleanUpFields(Integration $entity, array $mauticLeadFields, array $mauticCompanyFields)
|
|
{
|
|
$featureSettings = $entity->getFeatureSettings();
|
|
$submittedFields = $featureSettings['leadFields'] ?? [];
|
|
$submittedCompanyFields = $featureSettings['companyFields'] ?? [];
|
|
$submittedObjects = $featureSettings['objects'] ?? [];
|
|
$missingRequiredFields = [];
|
|
|
|
// add special case in order to prevent it from being removed
|
|
$mauticLeadFields['mauticContactId'] = '';
|
|
$mauticLeadFields['mauticContactTimelineLink'] = '';
|
|
$mauticLeadFields['mauticContactIsContactableByEmail'] = '';
|
|
|
|
// make sure now non-existent aren't saved
|
|
$settings = [
|
|
'ignore_field_cache' => false,
|
|
];
|
|
$settings['feature_settings']['objects'] = $submittedObjects;
|
|
$availableIntegrationFields = $this->getAvailableLeadFields($settings);
|
|
$leadFields = [];
|
|
|
|
/**
|
|
* @param $mappedFields
|
|
* @param $integrationFields
|
|
* @param $mauticFields
|
|
* @param $fieldType
|
|
*/
|
|
$cleanup = function (&$mappedFields, $integrationFields, $mauticFields, $fieldType) use (&$missingRequiredFields, &$featureSettings): void {
|
|
$updateKey = ('companyFields' === $fieldType) ? 'update_mautic_company' : 'update_mautic';
|
|
$removeFields = array_keys(array_diff_key($mappedFields, $integrationFields));
|
|
|
|
// Find all the mapped fields that no longer exist in Mautic
|
|
if ($nonExistentFields = array_diff($mappedFields, array_keys($mauticFields))) {
|
|
// Remove those fields
|
|
$removeFields = array_merge($removeFields, array_keys($nonExistentFields));
|
|
}
|
|
|
|
foreach ($removeFields as $field) {
|
|
unset($mappedFields[$field]);
|
|
|
|
if (isset($featureSettings[$updateKey])) {
|
|
unset($featureSettings[$updateKey][$field]);
|
|
}
|
|
}
|
|
|
|
// Check that the remaining fields have an updateKey set
|
|
foreach ($mappedFields as $field => $mauticField) {
|
|
if (!isset($featureSettings[$updateKey][$field])) {
|
|
// Assume it's mapped to Mautic
|
|
$featureSettings[$updateKey][$field] = 1;
|
|
}
|
|
}
|
|
|
|
// Check if required fields are missing
|
|
$required = $this->getRequiredFields($integrationFields, $fieldType);
|
|
if (array_diff_key($required, $mappedFields)) {
|
|
$missingRequiredFields[$fieldType] = true;
|
|
}
|
|
};
|
|
|
|
if ($submittedObjects) {
|
|
if (in_array('company', $submittedObjects)) {
|
|
// special handling for company fields
|
|
if (isset($availableIntegrationFields['company'])) {
|
|
$cleanup($submittedCompanyFields, $availableIntegrationFields['company'], $mauticCompanyFields, 'companyFields');
|
|
$featureSettings['companyFields'] = $submittedCompanyFields;
|
|
unset($availableIntegrationFields['company']);
|
|
}
|
|
}
|
|
|
|
// Rest of the objects are merged and assumed to be leadFields
|
|
// BC compatibility If extends fields to objects - 0 === contacts
|
|
if (isset($availableIntegrationFields[0])) {
|
|
$leadFields = array_merge($leadFields, $availableIntegrationFields[0]);
|
|
}
|
|
|
|
foreach ($submittedObjects as $object) {
|
|
if (isset($availableIntegrationFields[$object])) {
|
|
$leadFields = array_merge($leadFields, $availableIntegrationFields[$object]);
|
|
}
|
|
}
|
|
} else {
|
|
// Cleanup assuming there are no objects as keys
|
|
$leadFields = $availableIntegrationFields;
|
|
}
|
|
|
|
if (!empty($leadFields)) {
|
|
$cleanup($submittedFields, $leadFields, $mauticLeadFields, 'leadFields');
|
|
$featureSettings['leadFields'] = $submittedFields;
|
|
}
|
|
|
|
$entity->setFeatureSettings($featureSettings);
|
|
|
|
return $missingRequiredFields;
|
|
}
|
|
|
|
/**
|
|
* @param string $fieldType
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getRequiredFields(array $fields, $fieldType = '')
|
|
{
|
|
// use $fieldType to determine if email should be required. we use email as unique identifier for contacts only,
|
|
// if any other fieldType use integrations own field types
|
|
$requiredFields = [];
|
|
foreach ($fields as $field => $details) {
|
|
if ('leadFields' === $fieldType) {
|
|
if ((is_array($details) && !empty($details['required'])) || 'email' === $field
|
|
|| (isset($details['optionLabel'])
|
|
&& 'email' == strtolower(
|
|
$details['optionLabel']
|
|
))
|
|
) {
|
|
$requiredFields[$field] = $field;
|
|
}
|
|
} else {
|
|
if (is_array($details) && !empty($details['required'])
|
|
) {
|
|
$requiredFields[$field] = $field;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $requiredFields;
|
|
}
|
|
|
|
/**
|
|
* Match lead data with integration fields.
|
|
*
|
|
* @param Lead|mixed[] $lead
|
|
* @param mixed[] $config
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function populateLeadData($lead, $config = [])
|
|
{
|
|
if (!isset($config['leadFields'])) {
|
|
$config = $this->mergeConfigToFeatureSettings($config);
|
|
|
|
if (empty($config['leadFields'])) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($lead instanceof Lead) {
|
|
$fields = $lead->getProfileFields();
|
|
$leadId = $lead->getId();
|
|
} else {
|
|
$fields = $lead;
|
|
$leadId = $lead['id'];
|
|
}
|
|
|
|
$object = $config['object'] ?? null;
|
|
$leadFields = $config['leadFields'];
|
|
$availableFields = $this->getAvailableLeadFields($config);
|
|
|
|
if ($object) {
|
|
$availableFields = $availableFields[$config['object']];
|
|
} else {
|
|
$availableFields = $availableFields[0] ?? $availableFields;
|
|
}
|
|
|
|
$unknown = $this->translator->trans('mautic.integration.form.lead.unknown');
|
|
$matched = [];
|
|
|
|
foreach ($availableFields as $key => $field) {
|
|
$integrationKey = $matchIntegrationKey = $this->convertLeadFieldKey($key, $field);
|
|
if (!isset($config['leadFields'][$integrationKey])) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($leadFields[$integrationKey])) {
|
|
if ('mauticContactTimelineLink' === $leadFields[$integrationKey]) {
|
|
$matched[$integrationKey] = $this->getContactTimelineLink($leadId);
|
|
|
|
continue;
|
|
}
|
|
if ('mauticContactIsContactableByEmail' === $leadFields[$integrationKey]) {
|
|
$matched[$integrationKey] = $this->getLeadDoNotContact($leadId);
|
|
|
|
continue;
|
|
}
|
|
if ('mauticContactId' === $leadFields[$integrationKey]) {
|
|
$matched[$integrationKey] = $lead->getId();
|
|
continue;
|
|
}
|
|
$mauticKey = $leadFields[$integrationKey];
|
|
if (isset($fields[$mauticKey]) && '' !== $fields[$mauticKey]) {
|
|
$matched[$matchIntegrationKey] = $this->cleanPushData(
|
|
$fields[$mauticKey],
|
|
$field['type'] ?? 'string'
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!empty($field['required']) && empty($matched[$matchIntegrationKey])) {
|
|
$matched[$matchIntegrationKey] = $unknown;
|
|
}
|
|
}
|
|
|
|
return $matched;
|
|
}
|
|
|
|
/**
|
|
* Match Company data with integration fields.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function populateCompanyData($entity, $config = [])
|
|
{
|
|
if (!isset($config['companyFields'])) {
|
|
$config = $this->mergeConfigToFeatureSettings($config);
|
|
|
|
if (empty($config['companyFields'])) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($entity instanceof Lead) {
|
|
$fields = $entity->getPrimaryCompany();
|
|
} else {
|
|
$fields = $entity['primaryCompany'];
|
|
}
|
|
|
|
$companyFields = $config['companyFields'];
|
|
$availableFields = $this->getAvailableLeadFields($config)['company'];
|
|
$unknown = $this->translator->trans('mautic.integration.form.lead.unknown');
|
|
$matched = [];
|
|
|
|
foreach ($availableFields as $key => $field) {
|
|
$integrationKey = $this->convertLeadFieldKey($key, $field);
|
|
|
|
if (isset($companyFields[$key])) {
|
|
$mauticKey = $companyFields[$key];
|
|
if (isset($fields[$mauticKey]) && !empty($fields[$mauticKey])) {
|
|
$matched[$integrationKey] = $this->cleanPushData($fields[$mauticKey], $field['type'] ?? 'string');
|
|
}
|
|
}
|
|
|
|
if (!empty($field['required']) && empty($matched[$integrationKey])) {
|
|
$matched[$integrationKey] = $unknown;
|
|
}
|
|
}
|
|
|
|
return $matched;
|
|
}
|
|
|
|
/**
|
|
* Takes profile data from an integration and maps it to Mautic's lead fields.
|
|
*
|
|
* @param array $config
|
|
* @param string|null $object
|
|
*
|
|
* @return array
|
|
*/
|
|
public function populateMauticLeadData($data, $config = [], $object = null)
|
|
{
|
|
// Glean supported fields from what was returned by the integration
|
|
$gleanedData = $data;
|
|
|
|
if (null == $object) {
|
|
$object = 'lead';
|
|
}
|
|
if ('company' == $object) {
|
|
if (!isset($config['companyFields'])) {
|
|
$config = $this->mergeConfigToFeatureSettings($config);
|
|
|
|
if (empty($config['companyFields'])) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
$fields = $config['companyFields'];
|
|
}
|
|
if ('lead' == $object) {
|
|
if (!isset($config['leadFields'])) {
|
|
$config = $this->mergeConfigToFeatureSettings($config);
|
|
|
|
if (empty($config['leadFields'])) {
|
|
return [];
|
|
}
|
|
}
|
|
$fields = $config['leadFields'];
|
|
}
|
|
|
|
$matched = [];
|
|
foreach ($gleanedData as $key => $field) {
|
|
if (isset($fields[$key]) && isset($gleanedData[$key])
|
|
&& $this->translator->trans('mautic.integration.form.lead.unknown') !== $gleanedData[$key]
|
|
) {
|
|
$matched[$fields[$key]] = $gleanedData[$key];
|
|
}
|
|
}
|
|
|
|
return $matched;
|
|
}
|
|
|
|
/**
|
|
* Create or update existing Mautic lead from the integration's profile data.
|
|
*
|
|
* @param mixed $data Profile data from integration
|
|
* @param bool|true $persist Set to false to not persist lead to the database in this method
|
|
* @param array|null $socialCache
|
|
* @param mixed|null $identifiers
|
|
*
|
|
* @return Lead
|
|
*/
|
|
public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null)
|
|
{
|
|
if (is_object($data)) {
|
|
// Convert to array in all levels
|
|
$data = json_encode(json_decode($data, true));
|
|
} elseif (is_string($data)) {
|
|
// Assume JSON
|
|
$data = json_decode($data, true);
|
|
}
|
|
|
|
// Match that data with mapped lead fields
|
|
$matchedFields = $this->populateMauticLeadData($data);
|
|
|
|
if (empty($matchedFields)) {
|
|
return;
|
|
}
|
|
|
|
// Find unique identifier fields used by the integration
|
|
/** @var LeadModel $leadModel */
|
|
$leadModel = $this->leadModel;
|
|
$uniqueLeadFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier();
|
|
$uniqueLeadFieldData = [];
|
|
|
|
foreach ($matchedFields as $leadField => $value) {
|
|
if (array_key_exists($leadField, $uniqueLeadFields) && !empty($value)) {
|
|
$uniqueLeadFieldData[$leadField] = $value;
|
|
}
|
|
}
|
|
|
|
// Default to new lead
|
|
$lead = new Lead();
|
|
$lead->setNewlyCreated(true);
|
|
|
|
if (count($uniqueLeadFieldData)) {
|
|
$existingLeads = $this->em->getRepository(Lead::class)
|
|
->getLeadsByUniqueFields($uniqueLeadFieldData);
|
|
|
|
if (!empty($existingLeads)) {
|
|
$lead = array_shift($existingLeads);
|
|
}
|
|
}
|
|
|
|
$leadModel->setFieldValues($lead, $matchedFields, false, false);
|
|
|
|
// Update the social cache
|
|
$leadSocialCache = $lead->getSocialCache();
|
|
if (!isset($leadSocialCache[$this->getName()])) {
|
|
$leadSocialCache[$this->getName()] = [];
|
|
}
|
|
|
|
if (null !== $socialCache) {
|
|
$leadSocialCache[$this->getName()] = array_merge($leadSocialCache[$this->getName()], $socialCache);
|
|
}
|
|
|
|
// Check for activity while here
|
|
if (null !== $identifiers && in_array('public_activity', $this->getSupportedFeatures())) {
|
|
$this->getPublicActivity($identifiers, $leadSocialCache[$this->getName()]);
|
|
}
|
|
|
|
$lead->setSocialCache($leadSocialCache);
|
|
|
|
// Update the internal info integration object that has updated the record
|
|
if (isset($data['internal'])) {
|
|
$internalInfo = $lead->getInternal();
|
|
$internalInfo[$this->getName()] = $data['internal'];
|
|
$lead->setInternal($internalInfo);
|
|
}
|
|
|
|
if ($persist && !empty($lead->getChanges(true))) {
|
|
// Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
|
|
try {
|
|
$lead->setManipulator(new LeadManipulator(
|
|
'plugin',
|
|
$this->getName(),
|
|
null,
|
|
$this->getDisplayName()
|
|
));
|
|
$leadModel->saveEntity($lead, false);
|
|
} catch (\Exception $exception) {
|
|
$this->logger->warning($exception->getMessage());
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
return $lead;
|
|
}
|
|
|
|
/**
|
|
* Merges a config from integration_list with feature settings.
|
|
*
|
|
* @param array $config
|
|
*
|
|
* @return array|mixed
|
|
*/
|
|
public function mergeConfigToFeatureSettings($config = [])
|
|
{
|
|
$featureSettings = $this->settings->getFeatureSettings();
|
|
|
|
if (isset($config['config'])
|
|
&& (empty($config['integration'])
|
|
|| (!empty($config['integration'])
|
|
&& $config['integration'] == $this->getName()))
|
|
) {
|
|
$featureSettings = array_merge($featureSettings, $config['config']);
|
|
}
|
|
|
|
return $featureSettings;
|
|
}
|
|
|
|
/**
|
|
* Return key recognized by integration.
|
|
*/
|
|
public function convertLeadFieldKey(string $key, $field): string
|
|
{
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Sets whether fields should be sorted alphabetically or by the order the integration feeds.
|
|
*/
|
|
public function sortFieldsAlphabetically(): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Used to match local field name with remote field name.
|
|
*
|
|
* @param string $field
|
|
* @param string $subfield
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function matchFieldName($field, $subfield = '')
|
|
{
|
|
if (!empty($field) && !empty($subfield)) {
|
|
return $subfield.ucfirst($field);
|
|
}
|
|
|
|
return $field;
|
|
}
|
|
|
|
/**
|
|
* Convert and assign the data to assignable fields.
|
|
*
|
|
* @param mixed $data
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function matchUpData($data)
|
|
{
|
|
$info = [];
|
|
$available = $this->getAvailableLeadFields();
|
|
|
|
foreach ($available as $field => $fieldDetails) {
|
|
if (is_array($data)) {
|
|
if (!isset($data[$field])) {
|
|
$info[$field] = '';
|
|
continue;
|
|
} else {
|
|
$values = $data[$field];
|
|
}
|
|
} else {
|
|
if (!isset($data->$field)) {
|
|
$info[$field] = '';
|
|
continue;
|
|
} else {
|
|
$values = $data->$field;
|
|
}
|
|
}
|
|
|
|
switch ($fieldDetails['type']) {
|
|
case 'string':
|
|
case 'boolean':
|
|
$info[$field] = $values;
|
|
break;
|
|
case 'object':
|
|
foreach ($fieldDetails['fields'] as $f) {
|
|
if (isset($values->$f)) {
|
|
$fn = $this->matchFieldName($field, $f);
|
|
|
|
$info[$fn] = $values->$f;
|
|
}
|
|
}
|
|
break;
|
|
case 'array_object':
|
|
$objects = [];
|
|
if (!empty($values)) {
|
|
foreach ($values as $v) {
|
|
if (isset($v->value)) {
|
|
$objects[] = $v->value;
|
|
}
|
|
}
|
|
}
|
|
$fn = (isset($fieldDetails['fields'][0])) ? $this->matchFieldName(
|
|
$field,
|
|
$fieldDetails['fields'][0]
|
|
) : $field;
|
|
$info[$fn] = implode('; ', $objects);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Get the path to the profile templates for this integration.
|
|
*/
|
|
public function getSocialProfileTemplate()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks to ensure an image still exists before caching.
|
|
*
|
|
* @param string $url
|
|
*/
|
|
public function checkImageExists($url): bool
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
curl_setopt(
|
|
$ch,
|
|
CURLOPT_USERAGENT,
|
|
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13'
|
|
);
|
|
curl_exec($ch);
|
|
$retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
return 200 == $retcode;
|
|
}
|
|
|
|
/**
|
|
* @return NotificationModel
|
|
*/
|
|
public function getNotificationModel()
|
|
{
|
|
return $this->notificationModel;
|
|
}
|
|
|
|
public function logIntegrationError(\Exception $e, ?Lead $contact = null): void
|
|
{
|
|
$logger = $this->logger;
|
|
|
|
if ($e instanceof ApiErrorException) {
|
|
if (null === $this->adminUsers) {
|
|
$this->adminUsers = $this->em->getRepository(\Mautic\UserBundle\Entity\User::class)->getEntities(
|
|
[
|
|
'filter' => [
|
|
'force' => [
|
|
[
|
|
'column' => 'r.isAdmin',
|
|
'expr' => 'eq',
|
|
'value' => true,
|
|
],
|
|
],
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
$errorMessage = $e->getMessage();
|
|
$errorHeader = $this->getTranslator()->trans(
|
|
'mautic.integration.error',
|
|
[
|
|
'%name%' => $this->getName(),
|
|
]
|
|
);
|
|
|
|
if ($contact || $contact = $e->getContact()) {
|
|
// Append a link to the contact
|
|
$contactId = $contact->getId();
|
|
$contactName = $contact->getPrimaryIdentifier();
|
|
} elseif ($contactId = $e->getContactId()) {
|
|
$contactName = $this->getTranslator()->trans('mautic.integration.error.generic_contact_name', ['%id%' => $contactId]);
|
|
}
|
|
|
|
$this->lastIntegrationError = $errorHeader.': '.$errorMessage;
|
|
|
|
if ($contactId) {
|
|
$contactLink = $this->router->generate(
|
|
'mautic_contact_action',
|
|
[
|
|
'objectAction' => 'view',
|
|
'objectId' => $contactId,
|
|
],
|
|
UrlGeneratorInterface::ABSOLUTE_URL
|
|
);
|
|
$errorMessage .= ' <a href="'.$contactLink.'">'.$contactName.'</a>';
|
|
}
|
|
|
|
// Prevent a flood of the same messages
|
|
$messageHash = md5($errorMessage);
|
|
if (!array_key_exists($messageHash, $this->notifications)) {
|
|
foreach ($this->adminUsers as $user) {
|
|
$this->getNotificationModel()->addNotification(
|
|
$errorMessage,
|
|
$this->getName(),
|
|
false,
|
|
$errorHeader,
|
|
'text-danger ri-error-warning-line',
|
|
null,
|
|
$user
|
|
);
|
|
}
|
|
|
|
$this->notifications[$messageHash] = true;
|
|
}
|
|
}
|
|
|
|
$logger->error('INTEGRATION ERROR: '.$this->getName().' - '.(('dev' == MAUTIC_ENV) ? (string) $e : $e->getMessage()));
|
|
}
|
|
|
|
/**
|
|
* @return string|null
|
|
*/
|
|
public function getLastIntegrationError()
|
|
{
|
|
return $this->lastIntegrationError;
|
|
}
|
|
|
|
/**
|
|
* @return $this
|
|
*/
|
|
public function resetLastIntegrationError()
|
|
{
|
|
$this->lastIntegrationError = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns notes specific to sections of the integration form (if applicable).
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getFormNotes($section)
|
|
{
|
|
if ('leadfield_match' == $section) {
|
|
return ['mautic.integration.form.field_match_notes', 'info'];
|
|
} else {
|
|
return ['', 'info'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allows appending extra data to the config.
|
|
*
|
|
* @param FormBuilder|Form $builder
|
|
* @param array $data
|
|
* @param string $formArea Section of form being built keys|features|integration
|
|
* keys can be used to store login/request related settings; keys are encrypted
|
|
* features can be used for configuring share buttons, etc
|
|
* integration is called when adding an integration to events like point triggers,
|
|
* campaigns actions, forms actions, etc
|
|
*/
|
|
public function appendToForm(&$builder, $data, $formArea): void
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @param FormBuilderInterface $builder
|
|
* @param array<mixed> $options
|
|
*/
|
|
public function modifyForm($builder, $options): void
|
|
{
|
|
$this->dispatcher->dispatch(
|
|
new PluginIntegrationFormBuildEvent($this, $builder, $options),
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_FORM_BUILD
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns settings for the integration form.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getFormSettings(): array
|
|
{
|
|
$type = $this->getAuthenticationType();
|
|
$enableDataPriority = $this->getDataPriority();
|
|
switch ($type) {
|
|
case 'oauth1a':
|
|
case 'oauth2':
|
|
$callback = true;
|
|
$requiresAuthorization = true;
|
|
break;
|
|
default:
|
|
$callback = false;
|
|
$requiresAuthorization = false;
|
|
break;
|
|
}
|
|
|
|
return [
|
|
'requires_callback' => $callback,
|
|
'requires_authorization' => $requiresAuthorization,
|
|
'default_features' => [],
|
|
'enable_data_priority' => $enableDataPriority,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getFormDisplaySettings()
|
|
{
|
|
/** @var PluginIntegrationFormDisplayEvent $event */
|
|
$event = $this->dispatcher->dispatch(
|
|
new PluginIntegrationFormDisplayEvent($this, $this->getFormSettings()),
|
|
PluginEvents::PLUGIN_ON_INTEGRATION_FORM_DISPLAY
|
|
);
|
|
|
|
return $event->getSettings();
|
|
}
|
|
|
|
/**
|
|
* Get available fields for choices in the config UI.
|
|
*
|
|
* @param mixed[] $settings
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function getFormLeadFields(array $settings = [])
|
|
{
|
|
if (isset($settings['feature_settings']['objects']['company'])) {
|
|
unset($settings['feature_settings']['objects']['company']);
|
|
}
|
|
|
|
return ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
|
|
}
|
|
|
|
/**
|
|
* Get available company fields for choices in the config UI.
|
|
*
|
|
* @param array $settings
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFormCompanyFields($settings = [])
|
|
{
|
|
$settings['feature_settings']['objects']['company'] = 'company';
|
|
|
|
return ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
|
|
}
|
|
|
|
/**
|
|
* returns template to render on popup window after trying to run OAuth.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getPostAuthTemplate()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getContactTimelineLink($contactId)
|
|
{
|
|
return $this->router->generate(
|
|
'mautic_plugin_timeline_view',
|
|
['integration' => $this->getName(), 'leadId' => $contactId],
|
|
UrlGeneratorInterface::ABSOLUTE_URL
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array $keys
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function dispatchIntegrationKeyEvent($eventName, $keys = [])
|
|
{
|
|
/** @var PluginIntegrationKeyEvent $event */
|
|
$event = $this->dispatcher->dispatch(
|
|
new PluginIntegrationKeyEvent($this, $keys),
|
|
$eventName
|
|
);
|
|
|
|
return $event->getKeys();
|
|
}
|
|
|
|
/**
|
|
* Cleans the identifier for api calls.
|
|
*
|
|
* @param mixed $identifier
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function cleanIdentifier($identifier)
|
|
{
|
|
if (is_array($identifier)) {
|
|
foreach ($identifier as &$i) {
|
|
$i = urlencode($i);
|
|
}
|
|
} else {
|
|
$identifier = urlencode($identifier);
|
|
}
|
|
|
|
return $identifier;
|
|
}
|
|
|
|
/**
|
|
* @param string $fieldType
|
|
*
|
|
* @return bool|float|string
|
|
*/
|
|
public function cleanPushData($value, $fieldType = self::FIELD_TYPE_STRING)
|
|
{
|
|
return Cleaner::clean($value, $fieldType);
|
|
}
|
|
|
|
/**
|
|
* @return \Monolog\Logger|LoggerInterface
|
|
*/
|
|
public function getLogger()
|
|
{
|
|
return $this->logger;
|
|
}
|
|
|
|
/**
|
|
* @param bool|\Exception $error
|
|
*
|
|
* @return int Number ignored due to being duplicates
|
|
*
|
|
* @throws ApiErrorException
|
|
* @throws \Exception
|
|
*/
|
|
protected function cleanupFromSync(&$leadsToSync = [], $error = false)
|
|
{
|
|
$duplicates = 0;
|
|
if ($this->mauticDuplicates) {
|
|
// Create integration entities for these to be ignored until they are updated
|
|
foreach ($this->mauticDuplicates as $id => $dup) {
|
|
$this->persistIntegrationEntities[] = $this->createIntegrationEntity('Lead', null, $dup, $id, [], false);
|
|
++$duplicates;
|
|
}
|
|
|
|
$this->mauticDuplicates = [];
|
|
}
|
|
|
|
$integrationEntityRepo = $this->getIntegrationEntityRepository();
|
|
if (!empty($leadsToSync)) {
|
|
// Let's only sync thos that have actual changes to prevent a loop
|
|
$integrationEntityRepo->saveEntities($leadsToSync);
|
|
$integrationEntityRepo->deleteEntity($leadsToSync);
|
|
$leadsToSync = [];
|
|
}
|
|
|
|
// Persist updated entities if applicable
|
|
if ($this->persistIntegrationEntities) {
|
|
$integrationEntityRepo->saveEntities($this->persistIntegrationEntities);
|
|
$this->persistIntegrationEntities = [];
|
|
}
|
|
|
|
// If there are any deleted, mark it as so to prevent them from being queried over and over or recreated
|
|
if ($this->deleteIntegrationEntities) {
|
|
$integrationEntityRepo->deleteEntities($this->deleteIntegrationEntities);
|
|
$this->deleteIntegrationEntities = [];
|
|
}
|
|
$integrationEntityRepo->deleteEntities($this->deleteIntegrationEntities);
|
|
|
|
if ($error) {
|
|
if ($error instanceof \Exception) {
|
|
throw $error;
|
|
}
|
|
|
|
throw new ApiErrorException($error);
|
|
}
|
|
|
|
return $duplicates;
|
|
}
|
|
|
|
/**
|
|
* @param array $mapping array of [$mauticId => ['entity' => FormEntity, 'integration_entity_id' => $integrationId]]
|
|
* @param array $params
|
|
*/
|
|
protected function buildIntegrationEntities(array $mapping, $integrationEntity, $internalEntity, $params = [])
|
|
{
|
|
$integrationEntityRepo = $this->getIntegrationEntityRepository();
|
|
$integrationEntities = $integrationEntityRepo->getIntegrationEntities(
|
|
$this->getName(),
|
|
$integrationEntity,
|
|
$internalEntity,
|
|
array_keys($mapping)
|
|
);
|
|
|
|
// Find those that don't exist and create them
|
|
$createThese = array_diff_key($mapping, $integrationEntities);
|
|
|
|
foreach ($mapping as $internalEntityId => $entity) {
|
|
if (is_array($entity)) {
|
|
$integrationEntityId = $entity['integration_entity_id'];
|
|
$internalEntityObject = $entity['entity'];
|
|
} else {
|
|
$integrationEntityId = $entity;
|
|
$internalEntityObject = null;
|
|
}
|
|
|
|
if (isset($createThese[$internalEntityId])) {
|
|
$entity = $this->createIntegrationEntity(
|
|
$integrationEntity,
|
|
$integrationEntityId,
|
|
$internalEntity,
|
|
$internalEntityId,
|
|
[],
|
|
false
|
|
);
|
|
$entity->setLastSyncDate($this->getLastSyncDate($internalEntityObject, $params, false));
|
|
$integrationEntities[$internalEntityId] = $entity;
|
|
} else {
|
|
$integrationEntities[$internalEntityId]->setLastSyncDate($this->getLastSyncDate($internalEntityObject, $params, false));
|
|
}
|
|
}
|
|
|
|
$integrationEntityRepo->saveEntities($integrationEntities);
|
|
$integrationEntityRepo->detachEntities($integrationEntities);
|
|
}
|
|
|
|
/**
|
|
* @param CommonEntity|null $entity
|
|
* @param array $params
|
|
* @param bool $ignoreEntityChanges
|
|
*
|
|
* @return bool|\DateTime|null
|
|
*/
|
|
protected function getLastSyncDate($entity = null, $params = [], $ignoreEntityChanges = true)
|
|
{
|
|
$isNew = ($entity instanceof FormEntity) && $entity->isNew();
|
|
if (!$isNew && !$ignoreEntityChanges && isset($params['start']) && $entity && method_exists($entity, 'getChanges')) {
|
|
// Check to see if this contact was modified prior to the fetch so that the push catches it
|
|
/** @var FormEntity $entity */
|
|
$changes = $entity->getChanges(true);
|
|
if (empty($changes) || isset($changes['dateModified'])) {
|
|
$startSyncDate = \DateTime::createFromFormat(\DateTime::ISO8601, $params['start']);
|
|
$entityDateModified = $entity->getDateModified();
|
|
|
|
if (isset($changes['dateModified'])) {
|
|
$originalDateModified = \DateTime::createFromFormat(\DateTime::ISO8601, $changes['dateModified'][0]);
|
|
} elseif ($entityDateModified) {
|
|
$originalDateModified = $entityDateModified;
|
|
} else {
|
|
$originalDateModified = $entity->getDateAdded();
|
|
}
|
|
|
|
if ($originalDateModified >= $startSyncDate) {
|
|
// Return null so that the push sync catches
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (defined('MAUTIC_DATE_MODIFIED_OVERRIDE')) ? \DateTime::createFromFormat('U', MAUTIC_DATE_MODIFIED_OVERRIDE)
|
|
: new \DateTime();
|
|
}
|
|
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
public function prepareFieldsForSync($fields, $keys, $object = null)
|
|
{
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* Function used to format unformated fields coming from FieldsTypeTrait
|
|
* (usually used in campaign actions).
|
|
*
|
|
* @return array
|
|
*/
|
|
public function formatMatchedFields($fields)
|
|
{
|
|
$formattedFields = [];
|
|
|
|
if (isset($fields['m_1'])) {
|
|
$xfields = count($fields) / 3;
|
|
for ($i = 1; $i < $xfields; ++$i) {
|
|
if (isset($fields['i_'.$i]) && isset($fields['m_'.$i])) {
|
|
$formattedFields[$fields['i_'.$i]] = $fields['m_'.$i];
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($formattedFields)) {
|
|
$fields = $formattedFields;
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* @param string $channel
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getLeadDoNotContact($leadId, $channel = 'email')
|
|
{
|
|
$isDoNotContact = 0;
|
|
if ($lead = $this->leadModel->getEntity($leadId)) {
|
|
$isContactableReason = $this->doNotContact->isContactable($lead, $channel);
|
|
if (DoNotContact::IS_CONTACTABLE !== $isContactableReason) {
|
|
$isDoNotContact = 1;
|
|
}
|
|
}
|
|
|
|
return $isDoNotContact;
|
|
}
|
|
|
|
/**
|
|
* Get pseudo fields from mautic, these are lead properties we want to map to integration fields.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getCompoundMauticFields($lead)
|
|
{
|
|
if ($lead['internal_entity_id']) {
|
|
$lead['mauticContactId'] = $lead['internal_entity_id'];
|
|
$lead['mauticContactTimelineLink'] = $this->getContactTimelineLink($lead['internal_entity_id']);
|
|
$lead['mauticContactIsContactableByEmail'] = $this->getLeadDoNotContact($lead['internal_entity_id']);
|
|
}
|
|
|
|
return $lead;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isCompoundMauticField($fieldName)
|
|
{
|
|
$compoundFields = [
|
|
'mauticContactTimelineLink' => 'mauticContactTimelineLink',
|
|
'mauticContactId' => 'mauticContactId',
|
|
];
|
|
|
|
if (true === $this->updateDncByDate()) {
|
|
$compoundFields['mauticContactIsContactableByEmail'] = 'mauticContactIsContactableByEmail';
|
|
}
|
|
|
|
return isset($compoundFields[$fieldName]);
|
|
}
|
|
|
|
/**
|
|
* Update the record in each system taking the last modified record.
|
|
*
|
|
* @param string $channel
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getLeadDoNotContactByDate($channel, $records, $object, $lead, $integrationData, $params = [])
|
|
{
|
|
return $records;
|
|
}
|
|
|
|
/**
|
|
* @param callable(mixed[]): Client $clientFactory
|
|
*/
|
|
public function setClientFactory(callable $clientFactory): void
|
|
{
|
|
$this->clientFactory = \Closure::fromCallable($clientFactory);
|
|
}
|
|
|
|
/**
|
|
* Because so many integrations extend this class and mautic.http.client is not in the
|
|
* constructor at the time of writing, let's just create a new client here. In addition,
|
|
* we add some custom cURL options.
|
|
*
|
|
* @param mixed[] $options
|
|
*/
|
|
protected function makeHttpClient(array $options): Client
|
|
{
|
|
return ($this->clientFactory)($options);
|
|
}
|
|
}
|