Spaces:
No application file
No application file
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\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\HttpFoundation\Session\SessionInterface; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
use Symfony\Component\Routing\RouterInterface; | |
use Symfony\Contracts\Translation\TranslatorInterface; | |
/** | |
* @method pushLead(Lead $lead, array $config = []) | |
* @method pushLeadToCampaign(Lead $lead, mixed $integrationCampaign, mixed $integrationMemberStatus) | |
* @method getLeads(array $params, string $query, &$executed, array $result = [], $object = 'Lead') | |
* @method getCompanies(array $params) | |
* | |
* @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 ?SessionInterface $session; | |
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 = []; | |
public function __construct( | |
protected EventDispatcherInterface $dispatcher, | |
CacheStorageHelper $cacheStorageHelper, | |
protected EntityManager $em, | |
SessionInterface $session, | |
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 | |
) { | |
$this->cache = $cacheStorageHelper->getCache($this->getName()); | |
$this->session = (!defined('IN_MAUTIC_CONSOLE')) ? $session : null; | |
$this->request = (!defined('IN_MAUTIC_CONSOLE')) ? $requestStack->getCurrentRequest() : null; | |
} | |
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 | |
*/ | |
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) { | |
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->session) { | |
$this->session->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 array $settings | |
* @param array $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->session) { | |
$state = $this->session->get($this->getName().'_csrf_token', false); | |
$givenState = ($this->request->isXmlHttpRequest()) ? $this->request->request->get('state') : $this->request->get('state'); | |
if ($state && $state !== $givenState) { | |
$this->session->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->session) { | |
$this->session->set($this->getName().'_tokenResponse', $data); | |
} | |
$error = false; | |
} elseif (is_array($data) && isset($data['access_token'])) { | |
if ($this->session) { | |
$this->session->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. | |
* | |
* @return array | |
*/ | |
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] && null !== $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->fieldModel->getUniqueIdentifierFields(); | |
$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]) and !is_object($data)) { | |
$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-circle', | |
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 | |
* | |
* @throws ApiErrorException | |
*/ | |
public function getLeadDoNotContactByDate($channel, $records, $object, $lead, $integrationData, $params = []) | |
{ | |
return $records; | |
} | |
/** | |
* 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 new Client(['handler' => HandlerStack::create(new CurlHandler([ | |
'options' => $options, | |
]))]); | |
} | |
} | |