dispatcher = $eventDispatcher; $this->cache = $cacheStorageHelper->getCache($this->getName()); $this->em = $entityManager; $this->session = (!defined('IN_MAUTIC_CONSOLE')) ? $session : null; $this->request = (!defined('IN_MAUTIC_CONSOLE')) ? $requestStack->getCurrentRequest() : null; $this->router = $router; $this->translator = $translator; $this->logger = $logger; $this->encryptionHelper = $encryptionHelper; $this->leadModel = $leadModel; $this->companyModel = $companyModel; $this->pathsHelper = $pathsHelper; $this->notificationModel = $notificationModel; $this->fieldModel = $fieldModel; $this->integrationEntityModel = $integrationEntityModel; $this->doNotContact = $doNotContact; } public function setCommandParameters(array $params) { $this->commandParameters = $params; } /** * @return CacheStorageHelper */ public function getCache() { return $this->cache; } /** * @return \Mautic\CoreBundle\Translation\Translator */ 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. * * @return int */ public function updateDncByDate() { 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. * * @return string */ public function getDataPriority() { 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 */ 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 'MauticPluginBundle:Integration:form.html.php'; } /** * Allows integration to set a custom theme folder. * * @return string */ public function getFormTheme() { return 'MauticPluginBundle:FormTheme\Integration'; } /** * Set the social integration entity. */ public function setIntegrationSettings(Integration $settings) { $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() { $this->em->persist($this->settings); $this->em->flush(); } /** * Merge api keys. * * @param $mergeKeys * @param $withKeys * @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) { /** @var PluginIntegrationKeyEvent $event */ $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) { return []; } $decrypted[$name] = $key; } return $decrypted; } /** * Get the array key for clientId. * * @return string */ public function getClientIdKey() { switch ($this->getAuthenticationType()) { case 'oauth1a': return 'consumer_id'; case 'oauth2': return 'client_id'; case 'key': return 'key'; default: return ''; } } /** * Get the array key for client secret. * * @return string */ public function getClientSecretKey() { switch ($this->getAuthenticationType()) { case 'oauth1a': return 'consumer_secret'; case 'oauth2': return 'client_secret'; case 'basic': return 'password'; default: return ''; } } /** * 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() { switch ($this->getAuthenticationType()) { case 'oauth2': return 'access_token'; case 'oauth1a': return 'oauth_token'; default: return ''; } } /** * 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() { switch ($this->getAuthenticationType()) { case 'oauth1a': return [ 'consumer_id' => 'mautic.integration.keyfield.consumerid', 'consumer_secret' => 'mautic.integration.keyfield.consumersecret', ]; case 'oauth2': return [ 'client_id' => 'mautic.integration.keyfield.clientid', 'client_secret' => 'mautic.integration.keyfield.clientsecret', ]; case 'key': return [ 'key' => 'mautic.integration.keyfield.api', ]; case 'basic': return [ 'username' => 'mautic.integration.keyfield.username', 'password' => 'mautic.integration.keyfield.password', ]; default: return []; } } /** * Extract the tokens returned by the oauth callback. * * @param string $data * @param bool $postAuthorization * * @return mixed */ public function parseCallbackResponse($data, $postAuthorization = false) { if (!$parsed = json_decode($data, true)) { parse_str($data, $parsed); } return $parsed; } /** * Generic error parser. * * @param $response * * @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( PluginEvents::PLUGIN_ON_INTEGRATION_REQUEST, new PluginIntegrationRequestEvent($this, $url, $parameters, $headers, $method, $settings, $authType) ); $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 .= (false === strpos($url, '?')) ? '?'.$query : '&'.$query; } elseif (!empty($settings['query'])) { $query = http_build_query($settings['query']); $url .= (false === strpos($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) && false !== strpos($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 (\Exception $exception) { return ['error' => ['message' => $exception->getMessage(), 'code' => $exception->getCode()]]; } if (empty($settings['ignore_event_dispatch'])) { $event->setResponse($result); $this->dispatcher->dispatch( PluginEvents::PLUGIN_ON_INTEGRATION_RESPONSE, $event ); } if (!empty($settings['return_raw'])) { return $result; } else { return $this->parseCallbackResponse($result->getBody(), !empty($settings['authorize_session'])); } } /** * @param $integrationEntity * @param $integrationEntityId * @param $internalEntity * @param $internalEntityId * @param bool $persist */ public function createIntegrationEntity( $integrationEntity, $integrationEntityId, $internalEntity, $internalEntityId, array $internal = null, $persist = true ) { $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('MauticPluginBundle:IntegrationEntity')->saveEntity($entity); } return $entity; } /** * @return IntegrationEntityRepository */ public function getIntegrationEntityRepository() { return $this->em->getRepository('MauticPluginBundle:IntegrationEntity'); } /** * Method to prepare the request parameters. Builds array of headers and parameters. * * @param $url * @param $parameters * @param $method * @param $settings * @param $authType * * @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 => isset($this->keys[$clientSecretKey]) ? $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( PluginEvents::PLUGIN_ON_INTEGRATION_GET_AUTH_CALLBACK_URL, new PluginIntegrationAuthCallbackUrlEvent($this, $defaultUrl) ); 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. * * @param $data * @param $tokenOverride * * @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) ? $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. * * @param $data * * @return mixed */ public function prepareResponseForExtraction($data) { return $data; } /** * Checks to see if the integration is configured by checking that required keys are populated. * * @return bool */ public function isConfigured() { $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. * * @param $inAuthorization * * @return string */ public function getBearerToken($inAuthorization = false) { return ''; } /** * Get an array of public activity. * * @param $identifier * @param $socialCache * * @return array */ public function getPublicActivity($identifier, &$socialCache) { return []; } /** * Get an array of public data. * * @param $identifier * @param $socialCache * * @return array */ public function getUserData($identifier, &$socialCache) { return []; } /** * Generates current URL to set as referer for curl calls. * * @return string */ protected function getRefererUrl() { 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 array $settings * * @return array */ public function getAvailableLeadFields($settings = []) { if (empty($settings['ignore_field_cache'])) { $cacheSuffix = (isset($settings['cache_suffix'])) ? $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 = (isset($featureSettings['leadFields'])) ? $featureSettings['leadFields'] : []; $submittedCompanyFields = (isset($featureSettings['companyFields'])) ? $featureSettings['companyFields'] : []; $submittedObjects = (isset($featureSettings['objects'])) ? $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) { $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 * @param $config * * @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 = isset($config['object']) ? $config['object'] : null; $leadFields = $config['leadFields']; $availableFields = $this->getAvailableLeadFields($config); if ($object) { $availableFields = $availableFields[$config['object']]; } else { $availableFields = (isset($availableFields[0])) ? $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 (is_array($integrationKey)) { [$integrationKey, $matchIntegrationKey] = $integrationKey; } elseif (!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], (isset($field['type'])) ? $field['type'] : 'string' ); } } if (!empty($field['required']) && empty($matched[$matchIntegrationKey])) { $matched[$matchIntegrationKey] = $unknown; } } return $matched; } /** * Match Company data with integration fields. * * @param $entity * @param $config * * @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], (isset($field['type'])) ? $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 $data * @param array $config * @param 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 \Mautic\LeadBundle\Model\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('MauticLeadBundle:Lead') ->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->addWarning($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. * * @param $key * @param $field * * @return mixed */ public function convertLeadFieldKey($key, $field) { return $key; } /** * Sets whether fields should be sorted alphabetically or by the order the integration feeds. */ public function sortFieldsAlphabetically() { 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 * * @return bool */ public function checkImageExists($url) { $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 \Mautic\CoreBundle\Model\NotificationModel */ public function getNotificationModel() { return $this->notificationModel; } public function logIntegrationError(\Exception $e, Lead $contact = null) { $logger = $this->logger; if ($e instanceof ApiErrorException) { if (null === $this->adminUsers) { $this->adminUsers = $this->em->getRepository('MauticUserBundle:User')->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 .= ' '.$contactName.''; } // 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 fa-exclamation-circle', null, $user ); } $this->notifications[$messageHash] = true; } } $logger->addError('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). * * @param $section * * @return string */ 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) { } /** * @param FormBuilder $builder * @param array $options */ public function modifyForm($builder, $options) { $this->dispatcher->dispatch( PluginEvents::PLUGIN_ON_INTEGRATION_FORM_BUILD, new PluginIntegrationFormBuildEvent($this, $builder, $options) ); } /** * Returns settings for the integration form. * * @return array */ public function getFormSettings() { $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( PluginEvents::PLUGIN_ON_INTEGRATION_FORM_DISPLAY, new PluginIntegrationFormDisplayEvent($this, $this->getFormSettings()) ); return $event->getSettings(); } /** * Get available fields for choices in the config UI. * * @param array $settings * * @return array */ public function getFormLeadFields($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; } /** * @param $contactId * * @return string */ public function getContactTimelineLink($contactId) { return $this->router->generate( 'mautic_plugin_timeline_view', ['integration' => $this->getName(), 'leadId' => $contactId], UrlGeneratorInterface::ABSOLUTE_URL ); } /** * @param $eventName * @param array $keys * * @return array */ protected function dispatchIntegrationKeyEvent($eventName, $keys = []) { /** @var PluginIntegrationKeyEvent $event */ $event = $this->dispatcher->dispatch( $eventName, new PluginIntegrationKeyEvent($this, $keys) ); 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 $value * @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); $this->em->clear(Lead::class); $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 = []; } $this->em->clear(IntegrationEntity::class); 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 $integrationEntity * @param $internalEntity * @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); $this->em->clear(IntegrationEntity::class); } /** * @param null $entity * @param array $params * @param bool $ignoreEntityChanges * * @return bool|\DateTime|null */ protected function getLastSyncDate($entity = null, $params = [], $ignoreEntityChanges = true) { $isNew = method_exists($entity, 'isNew') && $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(); } /** * @param $fields * @param $keys * @param null $object * * @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). * * @param $fields * * @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 $leadId * @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. * * @param $lead * * @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; } /** * @param $fieldName * * @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, ]))]); } }