Issues (3627)

PluginBundle/Integration/AbstractIntegration.php (2 issues)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\PluginBundle\Integration;
13
14
use Doctrine\ORM\EntityManager;
15
use Joomla\Http\HttpFactory;
16
use Mautic\CoreBundle\Entity\FormEntity;
17
use Mautic\CoreBundle\Helper\CacheStorageHelper;
18
use Mautic\CoreBundle\Helper\EncryptionHelper;
19
use Mautic\CoreBundle\Helper\PathsHelper;
20
use Mautic\CoreBundle\Model\NotificationModel;
21
use Mautic\LeadBundle\DataObject\LeadManipulator;
22
use Mautic\LeadBundle\Entity\DoNotContact;
23
use Mautic\LeadBundle\Entity\Lead;
24
use Mautic\LeadBundle\Model\CompanyModel;
25
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
26
use Mautic\LeadBundle\Model\FieldModel;
27
use Mautic\LeadBundle\Model\LeadModel;
28
use Mautic\PluginBundle\Entity\Integration;
29
use Mautic\PluginBundle\Entity\IntegrationEntity;
30
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
31
use Mautic\PluginBundle\Event\PluginIntegrationAuthCallbackUrlEvent;
32
use Mautic\PluginBundle\Event\PluginIntegrationFormBuildEvent;
33
use Mautic\PluginBundle\Event\PluginIntegrationFormDisplayEvent;
34
use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent;
35
use Mautic\PluginBundle\Event\PluginIntegrationRequestEvent;
36
use Mautic\PluginBundle\Exception\ApiErrorException;
37
use Mautic\PluginBundle\Helper\Cleaner;
38
use Mautic\PluginBundle\Helper\oAuthHelper;
39
use Mautic\PluginBundle\Model\IntegrationEntityModel;
40
use Mautic\PluginBundle\PluginEvents;
41
use Monolog\Logger;
42
use Psr\Log\LoggerInterface;
43
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
44
use Symfony\Component\Form\FormBuilder;
45
use Symfony\Component\HttpFoundation\Request;
46
use Symfony\Component\HttpFoundation\RequestStack;
47
use Symfony\Component\HttpFoundation\Session\Session;
48
use Symfony\Component\HttpFoundation\Session\SessionInterface;
49
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
50
use Symfony\Component\Routing\Router;
51
use Symfony\Component\Translation\TranslatorInterface;
52
53
/**
54
 * @method pushLead(Lead $lead, array $config = [])
55
 * @method pushLeadToCampaign(Lead $lead, mixed $integrationCampaign, mixed $integrationMemberStatus)
56
 * @method getLeads(array $params, string $query, &$executed, array $result = [], $object = 'Lead')
57
 * @method getCompanies(array $params)
58
 */
59
abstract class AbstractIntegration implements UnifiedIntegrationInterface
60
{
61
    const FIELD_TYPE_STRING   = 'string';
62
    const FIELD_TYPE_BOOL     = 'boolean';
63
    const FIELD_TYPE_NUMBER   = 'number';
64
    const FIELD_TYPE_DATETIME = 'datetime';
65
    const FIELD_TYPE_DATE     = 'date';
66
67
    /**
68
     * @var bool
69
     */
70
    protected $coreIntegration = false;
71
72
    /**
73
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
74
     */
75
    protected $dispatcher;
76
77
    /**
78
     * @var Integration
79
     */
80
    protected $settings;
81
82
    /**
83
     * @var array Decrypted keys
84
     */
85
    protected $keys = [];
86
87
    /**
88
     * @var CacheStorageHelper
89
     */
90
    protected $cache;
91
92
    /**
93
     * @var \Doctrine\ORM\EntityManager
94
     */
95
    protected $em;
96
97
    /**
98
     * @var SessionInterface|null
99
     */
100
    protected $session;
101
102
    /**
103
     * @var Request|null
104
     */
105
    protected $request;
106
107
    /**
108
     * @var Router
109
     */
110
    protected $router;
111
112
    /**
113
     * @var LoggerInterface
114
     */
115
    protected $logger;
116
117
    /**
118
     * @var TranslatorInterface
119
     */
120
    protected $translator;
121
122
    /**
123
     * @var EncryptionHelper
124
     */
125
    protected $encryptionHelper;
126
127
    /**
128
     * @var LeadModel
129
     */
130
    protected $leadModel;
131
132
    /**
133
     * @var CompanyModel
134
     */
135
    protected $companyModel;
136
137
    /**
138
     * @var PathsHelper
139
     */
140
    protected $pathsHelper;
141
142
    /**
143
     * @var NotificationModel
144
     */
145
    protected $notificationModel;
146
147
    /**
148
     * @var FieldModel
149
     */
150
    protected $fieldModel;
151
152
    /**
153
     * Used for notifications.
154
     *
155
     * @var array|null
156
     */
157
    protected $adminUsers;
158
159
    /**
160
     * @var array
161
     */
162
    protected $notifications = [];
163
164
    /**
165
     * @var string|null
166
     */
167
    protected $lastIntegrationError;
168
169
    /**
170
     * @var array
171
     */
172
    protected $mauticDuplicates = [];
173
174
    /**
175
     * @var array
176
     */
177
    protected $salesforceIdMapping = [];
178
179
    /**
180
     * @var array
181
     */
182
    protected $deleteIntegrationEntities = [];
183
184
    /**
185
     * @var array
186
     */
187
    protected $persistIntegrationEntities = [];
188
189
    /**
190
     * @var IntegrationEntityModel
191
     */
192
    protected $integrationEntityModel;
193
194
    /**
195
     * @var DoNotContactModel
196
     */
197
    protected $doNotContact;
198
199
    /**
200
     * @var array
201
     */
202
    protected $commandParameters = [];
203
204
    public function __construct(
205
        EventDispatcherInterface $eventDispatcher,
206
        CacheStorageHelper $cacheStorageHelper,
207
        EntityManager $entityManager,
208
        Session $session,
209
        RequestStack $requestStack,
210
        Router $router,
211
        TranslatorInterface $translator,
212
        Logger $logger,
213
        EncryptionHelper $encryptionHelper,
214
        LeadModel $leadModel,
215
        CompanyModel $companyModel,
216
        PathsHelper $pathsHelper,
217
        NotificationModel $notificationModel,
218
        FieldModel $fieldModel,
219
        IntegrationEntityModel $integrationEntityModel,
220
        DoNotContactModel $doNotContact
221
    ) {
222
        $this->dispatcher             = $eventDispatcher;
223
        $this->cache                  = $cacheStorageHelper->getCache($this->getName());
224
        $this->em                     = $entityManager;
225
        $this->session                = (!defined('IN_MAUTIC_CONSOLE')) ? $session : null;
226
        $this->request                = (!defined('IN_MAUTIC_CONSOLE')) ? $requestStack->getCurrentRequest() : null;
227
        $this->router                 = $router;
228
        $this->translator             = $translator;
229
        $this->logger                 = $logger;
230
        $this->encryptionHelper       = $encryptionHelper;
231
        $this->leadModel              = $leadModel;
232
        $this->companyModel           = $companyModel;
233
        $this->pathsHelper            = $pathsHelper;
234
        $this->notificationModel      = $notificationModel;
235
        $this->fieldModel             = $fieldModel;
236
        $this->integrationEntityModel = $integrationEntityModel;
237
        $this->doNotContact           = $doNotContact;
238
    }
239
240
    public function setCommandParameters(array $params)
241
    {
242
        $this->commandParameters = $params;
243
    }
244
245
    /**
246
     * @return CacheStorageHelper
247
     */
248
    public function getCache()
249
    {
250
        return $this->cache;
251
    }
252
253
    /**
254
     * @return \Mautic\CoreBundle\Translation\Translator
255
     */
256
    public function getTranslator()
257
    {
258
        return $this->translator;
259
    }
260
261
    /**
262
     * @return bool
263
     */
264
    public function isCoreIntegration()
265
    {
266
        return $this->coreIntegration;
267
    }
268
269
    /**
270
     * Determines what priority the integration should have against the other integrations.
271
     *
272
     * @return int
273
     */
274
    public function getPriority()
275
    {
276
        return 9999;
277
    }
278
279
    /**
280
     * Determines if DNC records should be updated by date or by priority.
281
     *
282
     * @return int
283
     */
284
    public function updateDncByDate()
285
    {
286
        return false;
287
    }
288
289
    /**
290
     * Returns the name of the social integration that must match the name of the file
291
     * For example, IcontactIntegration would need Icontact here.
292
     *
293
     * @return string
294
     */
295
    abstract public function getName();
296
297
    /**
298
     * Name to display for the integration. e.g. iContact  Uses value of getName() by default.
299
     *
300
     * @return string
301
     */
302
    public function getDisplayName()
303
    {
304
        return $this->getName();
305
    }
306
307
    /**
308
     * Returns a description shown in the config form.
309
     *
310
     * @return string
311
     */
312
    public function getDescription()
313
    {
314
        return '';
315
    }
316
317
    /**
318
     * Get icon for Integration.
319
     *
320
     * @return string
321
     */
322
    public function getIcon()
323
    {
324
        $systemPath  = $this->pathsHelper->getSystemPath('root');
325
        $bundlePath  = $this->pathsHelper->getSystemPath('bundles');
326
        $pluginPath  = $this->pathsHelper->getSystemPath('plugins');
327
        $genericIcon = $bundlePath.'/PluginBundle/Assets/img/generic.png';
328
329
        $name   = $this->getName();
330
        $bundle = $this->settings->getPlugin()->getBundle();
331
        $icon   = $pluginPath.'/'.$bundle.'/Assets/img/'.strtolower($name).'.png';
332
333
        if (file_exists($systemPath.'/'.$icon)) {
334
            return $icon;
335
        }
336
337
        return $genericIcon;
338
    }
339
340
    /**
341
     * Get the type of authentication required for this API.  Values can be none, key, oauth2 or callback
342
     * (will call $this->authenticationTypeCallback).
343
     *
344
     * @return string
345
     */
346
    abstract public function getAuthenticationType();
347
348
    /**
349
     * Get if data priority is enabled in the integration or not default is false.
350
     *
351
     * @return string
352
     */
353
    public function getDataPriority()
354
    {
355
        return false;
356
    }
357
358
    /**
359
     * Get a list of supported features for this integration.
360
     *
361
     * Options are:
362
     *  cloud_storage - Asset remote storage
363
     *  public_profile - Lead social profile
364
     *  public_activity - Lead social activity
365
     *  share_button - Landing page share button
366
     *  sso_service - SSO using 3rd party service via sso_login and sso_login_check routes
367
     *  sso_form - SSO using submitted credentials through the login form
368
     *
369
     * @return array
370
     */
371
    public function getSupportedFeatures()
372
    {
373
        return [];
374
    }
375
376
    /**
377
     * Get a list of tooltips for the specified supported features.
378
     * This allows you to add detail / informational tooltips to your
379
     * supported feature checkbox group.
380
     *
381
     * Example:
382
     *  'cloud_storage' => 'mautic.integration.form.features.cloud_storage.tooltip'
383
     *
384
     * @return array
385
     */
386
    public function getSupportedFeatureTooltips()
387
    {
388
        return [];
389
    }
390
391
    /**
392
     * Returns the field the integration needs in order to find the user.
393
     *
394
     * @return mixed
395
     */
396
    public function getIdentifierFields()
397
    {
398
        return [];
399
    }
400
401
    /**
402
     * Allows integration to set a custom form template.
403
     *
404
     * @return string
405
     */
406
    public function getFormTemplate()
407
    {
408
        return 'MauticPluginBundle:Integration:form.html.php';
409
    }
410
411
    /**
412
     * Allows integration to set a custom theme folder.
413
     *
414
     * @return string
415
     */
416
    public function getFormTheme()
417
    {
418
        return 'MauticPluginBundle:FormTheme\Integration';
419
    }
420
421
    /**
422
     * Set the social integration entity.
423
     */
424
    public function setIntegrationSettings(Integration $settings)
425
    {
426
        $this->settings = $settings;
427
428
        $this->keys = $this->getDecryptedApiKeys();
429
    }
430
431
    /**
432
     * Get the social integration entity.
433
     *
434
     * @return Integration
435
     */
436
    public function getIntegrationSettings()
437
    {
438
        return $this->settings;
439
    }
440
441
    /**
442
     * Persist settings to the database.
443
     */
444
    public function persistIntegrationSettings()
445
    {
446
        $this->em->persist($this->settings);
447
        $this->em->flush();
448
    }
449
450
    /**
451
     * Merge api keys.
452
     *
453
     * @param            $mergeKeys
454
     * @param            $withKeys
455
     * @param bool|false $return    Returns the key array rather than setting them
456
     *
457
     * @return void|array
458
     */
459
    public function mergeApiKeys($mergeKeys, $withKeys = [], $return = false)
460
    {
461
        $settings = $this->settings;
462
        if (empty($withKeys)) {
463
            $withKeys = $this->keys;
464
        }
465
466
        foreach ($withKeys as $k => $v) {
467
            if (!empty($mergeKeys[$k])) {
468
                $withKeys[$k] = $mergeKeys[$k];
469
            }
470
            unset($mergeKeys[$k]);
471
        }
472
473
        //merge remaining new keys
474
        $withKeys = array_merge($withKeys, $mergeKeys);
475
476
        if ($return) {
477
            $this->keys = $this->dispatchIntegrationKeyEvent(
478
                PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_MERGE,
479
                $withKeys
480
            );
481
482
            return $this->keys;
483
        } else {
484
            $this->encryptAndSetApiKeys($withKeys, $settings);
485
486
            //reset for events that depend on rebuilding auth objects
487
            $this->setIntegrationSettings($settings);
488
        }
489
    }
490
491
    /**
492
     * Encrypts and saves keys to the entity.
493
     */
494
    public function encryptAndSetApiKeys(array $keys, Integration $entity)
495
    {
496
        /** @var PluginIntegrationKeyEvent $event */
497
        $keys = $this->dispatchIntegrationKeyEvent(
498
            PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT,
499
            $keys
500
        );
501
502
        // Update keys
503
        $this->keys = array_merge($this->keys, $keys);
504
505
        $encrypted = $this->encryptApiKeys($keys);
506
        $entity->setApiKeys($encrypted);
507
    }
508
509
    /**
510
     * Returns already decrypted keys.
511
     *
512
     * @return mixed
513
     */
514
    public function getKeys()
515
    {
516
        return $this->keys;
517
    }
518
519
    /**
520
     * Returns decrypted API keys.
521
     *
522
     * @param bool $entity
523
     *
524
     * @return array
525
     */
526
    public function getDecryptedApiKeys($entity = false)
527
    {
528
        static $decryptedKeys = [];
529
530
        if (!$entity) {
531
            $entity = $this->settings;
532
        }
533
534
        $keys = $entity->getApiKeys();
535
536
        $serialized = serialize($keys);
537
        if (empty($decryptedKeys[$serialized])) {
538
            $decrypted = $this->decryptApiKeys($keys, true);
539
            if (0 !== count($keys) && 0 === count($decrypted)) {
540
                $decrypted = $this->decryptApiKeys($keys);
541
                $this->encryptAndSetApiKeys($decrypted, $entity);
0 ignored issues
show
It seems like $entity can also be of type true; however, parameter $entity of Mautic\PluginBundle\Inte...:encryptAndSetApiKeys() does only seem to accept Mautic\PluginBundle\Entity\Integration, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

541
                $this->encryptAndSetApiKeys($decrypted, /** @scrutinizer ignore-type */ $entity);
Loading history...
542
                $this->em->flush($entity);
0 ignored issues
show
It seems like $entity can also be of type true; however, parameter $entity of Doctrine\ORM\EntityManager::flush() does only seem to accept array|null|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

542
                $this->em->flush(/** @scrutinizer ignore-type */ $entity);
Loading history...
543
            }
544
            $decryptedKeys[$serialized] = $this->dispatchIntegrationKeyEvent(
545
                PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_DECRYPT,
546
                $decrypted
547
            );
548
        }
549
550
        return $decryptedKeys[$serialized];
551
    }
552
553
    /**
554
     * Encrypts API keys.
555
     *
556
     * @return array
557
     */
558
    public function encryptApiKeys(array $keys)
559
    {
560
        $encrypted = [];
561
562
        foreach ($keys as $name => $key) {
563
            $key              = $this->encryptionHelper->encrypt($key);
564
            $encrypted[$name] = $key;
565
        }
566
567
        return $encrypted;
568
    }
569
570
    /**
571
     * Decrypts API keys.
572
     *
573
     * @param bool $mainDecryptOnly
574
     *
575
     * @return array
576
     */
577
    public function decryptApiKeys(array $keys, $mainDecryptOnly = false)
578
    {
579
        $decrypted = [];
580
581
        foreach ($keys as $name => $key) {
582
            $key = $this->encryptionHelper->decrypt($key, $mainDecryptOnly);
583
            if (false === $key) {
584
                return [];
585
            }
586
            $decrypted[$name] = $key;
587
        }
588
589
        return $decrypted;
590
    }
591
592
    /**
593
     * Get the array key for clientId.
594
     *
595
     * @return string
596
     */
597
    public function getClientIdKey()
598
    {
599
        switch ($this->getAuthenticationType()) {
600
            case 'oauth1a':
601
                return 'consumer_id';
602
            case 'oauth2':
603
                return 'client_id';
604
            case 'key':
605
                return 'key';
606
            default:
607
                return '';
608
        }
609
    }
610
611
    /**
612
     * Get the array key for client secret.
613
     *
614
     * @return string
615
     */
616
    public function getClientSecretKey()
617
    {
618
        switch ($this->getAuthenticationType()) {
619
            case 'oauth1a':
620
                return 'consumer_secret';
621
            case 'oauth2':
622
                return 'client_secret';
623
            case 'basic':
624
                return 'password';
625
            default:
626
                return '';
627
        }
628
    }
629
630
    /**
631
     * Array of keys to mask in the config form.
632
     *
633
     * @return array
634
     */
635
    public function getSecretKeys()
636
    {
637
        return [$this->getClientSecretKey()];
638
    }
639
640
    /**
641
     * Get the array key for the auth token.
642
     *
643
     * @return string
644
     */
645
    public function getAuthTokenKey()
646
    {
647
        switch ($this->getAuthenticationType()) {
648
            case 'oauth2':
649
                return 'access_token';
650
            case 'oauth1a':
651
                return 'oauth_token';
652
            default:
653
                return '';
654
        }
655
    }
656
657
    /**
658
     * Get the keys for the refresh token and expiry.
659
     *
660
     * @return array
661
     */
662
    public function getRefreshTokenKeys()
663
    {
664
        return [];
665
    }
666
667
    /**
668
     * Get a list of keys required to make an API call.  Examples are key, clientId, clientSecret.
669
     *
670
     * @return array
671
     */
672
    public function getRequiredKeyFields()
673
    {
674
        switch ($this->getAuthenticationType()) {
675
            case 'oauth1a':
676
                return [
677
                    'consumer_id'     => 'mautic.integration.keyfield.consumerid',
678
                    'consumer_secret' => 'mautic.integration.keyfield.consumersecret',
679
                ];
680
            case 'oauth2':
681
                return [
682
                    'client_id'     => 'mautic.integration.keyfield.clientid',
683
                    'client_secret' => 'mautic.integration.keyfield.clientsecret',
684
                ];
685
            case 'key':
686
                return [
687
                    'key' => 'mautic.integration.keyfield.api',
688
                ];
689
            case 'basic':
690
                return [
691
                    'username' => 'mautic.integration.keyfield.username',
692
                    'password' => 'mautic.integration.keyfield.password',
693
                ];
694
            default:
695
                return [];
696
        }
697
    }
698
699
    /**
700
     * Extract the tokens returned by the oauth callback.
701
     *
702
     * @param string $data
703
     * @param bool   $postAuthorization
704
     *
705
     * @return mixed
706
     */
707
    public function parseCallbackResponse($data, $postAuthorization = false)
708
    {
709
        if (!$parsed = json_decode($data, true)) {
710
            parse_str($data, $parsed);
711
        }
712
713
        return $parsed;
714
    }
715
716
    /**
717
     * Generic error parser.
718
     *
719
     * @param $response
720
     *
721
     * @return string
722
     */
723
    public function getErrorsFromResponse($response)
724
    {
725
        if (is_object($response)) {
726
            if (!empty($response->errors)) {
727
                $errors = [];
728
                foreach ($response->errors as $e) {
729
                    $errors[] = $e->message;
730
                }
731
732
                return implode('; ', $errors);
733
            } elseif (!empty($response->error->message)) {
734
                return $response->error->message;
735
            } else {
736
                return (string) $response;
737
            }
738
        } elseif (is_array($response)) {
739
            if (isset($response['error_description'])) {
740
                return $response['error_description'];
741
            } elseif (isset($response['error'])) {
742
                if (is_array($response['error'])) {
743
                    if (isset($response['error']['message'])) {
744
                        return $response['error']['message'];
745
                    } else {
746
                        return implode(', ', $response['error']);
747
                    }
748
                } else {
749
                    return $response['error'];
750
                }
751
            } elseif (isset($response['errors'])) {
752
                $errors = [];
753
                foreach ($response['errors'] as $err) {
754
                    if (is_array($err)) {
755
                        if (isset($err['message'])) {
756
                            $errors[] = $err['message'];
757
                        } else {
758
                            $errors[] = implode(', ', $err);
759
                        }
760
                    } else {
761
                        $errors[] = $err;
762
                    }
763
                }
764
765
                return implode('; ', $errors);
766
            }
767
768
            return $response;
769
        } else {
770
            return $response;
771
        }
772
    }
773
774
    /**
775
     * Make a basic call using cURL to get the data.
776
     *
777
     * @param        $url
778
     * @param array  $parameters
779
     * @param string $method
780
     * @param array  $settings
781
     *
782
     * @return mixed|string
783
     */
784
    public function makeRequest($url, $parameters = [], $method = 'GET', $settings = [])
785
    {
786
        // If not authorizing the session itself, check isAuthorized which will refresh tokens if applicable
787
        if (empty($settings['authorize_session'])) {
788
            $this->isAuthorized();
789
        }
790
791
        $method   = strtoupper($method);
792
        $authType = (empty($settings['auth_type'])) ? $this->getAuthenticationType() : $settings['auth_type'];
793
794
        [$parameters, $headers] = $this->prepareRequest($url, $parameters, $method, $settings, $authType);
795
796
        if (empty($settings['ignore_event_dispatch'])) {
797
            $event = $this->dispatcher->dispatch(
798
                PluginEvents::PLUGIN_ON_INTEGRATION_REQUEST,
799
                new PluginIntegrationRequestEvent($this, $url, $parameters, $headers, $method, $settings, $authType)
800
            );
801
802
            $headers    = $event->getHeaders();
803
            $parameters = $event->getParameters();
804
        }
805
806
        if (!isset($settings['query'])) {
807
            $settings['query'] = [];
808
        }
809
810
        if (isset($parameters['append_to_query'])) {
811
            $settings['query'] = array_merge(
812
                $settings['query'],
813
                $parameters['append_to_query']
814
            );
815
816
            unset($parameters['append_to_query']);
817
        }
818
819
        if (isset($parameters['post_append_to_query'])) {
820
            $postAppend = $parameters['post_append_to_query'];
821
            unset($parameters['post_append_to_query']);
822
        }
823
824
        if (!$this->isConfigured()) {
825
            return [
826
                'error' => [
827
                    'message' => $this->translator->trans(
828
                        'mautic.integration.missingkeys'
829
                    ),
830
                ],
831
            ];
832
        }
833
834
        if ('GET' == $method && !empty($parameters)) {
835
            $parameters = array_merge($settings['query'], $parameters);
836
            $query      = http_build_query($parameters);
837
            $url .= (false === strpos($url, '?')) ? '?'.$query : '&'.$query;
838
        } elseif (!empty($settings['query'])) {
839
            $query = http_build_query($settings['query']);
840
            $url .= (false === strpos($url, '?')) ? '?'.$query : '&'.$query;
841
        }
842
843
        if (isset($postAppend)) {
844
            $url .= $postAppend;
845
        }
846
847
        // Check for custom content-type header
848
        if (!empty($settings['content_type'])) {
849
            $settings['encoding_headers_set'] = true;
850
            $headers[]                        = "Content-Type: {$settings['content_type']}";
851
        }
852
853
        if ('GET' !== $method) {
854
            if (!empty($parameters)) {
855
                if ('oauth1a' == $authType) {
856
                    $parameters = http_build_query($parameters);
857
                }
858
                if (!empty($settings['encode_parameters'])) {
859
                    if ('json' == $settings['encode_parameters']) {
860
                        //encode the arguments as JSON
861
                        $parameters = json_encode($parameters);
862
                        if (empty($settings['encoding_headers_set'])) {
863
                            $headers[] = 'Content-Type: application/json';
864
                        }
865
                    }
866
                }
867
            } elseif (isset($settings['post_data'])) {
868
                $parameters = $settings['post_data'];
869
            }
870
        }
871
872
        $options = [
873
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
874
            CURLOPT_HEADER         => 1,
875
            CURLOPT_RETURNTRANSFER => 1,
876
            CURLOPT_FOLLOWLOCATION => 0,
877
            CURLOPT_REFERER        => $this->getRefererUrl(),
878
            CURLOPT_USERAGENT      => $this->getUserAgent(),
879
        ];
880
881
        if (isset($settings['curl_options']) && is_array($settings['curl_options'])) {
882
            $options = $settings['curl_options'] + $options;
883
        }
884
885
        if (isset($settings['ssl_verifypeer'])) {
886
            $options[CURLOPT_SSL_VERIFYPEER] = $settings['ssl_verifypeer'];
887
        }
888
889
        $connector = HttpFactory::getHttp(
890
            [
891
                'transport.curl' => $options,
892
            ]
893
        );
894
895
        $parseHeaders = (isset($settings['headers'])) ? array_merge($headers, $settings['headers']) : $headers;
896
        // HTTP library requires that headers are in key => value pairs
897
        $headers = [];
898
        if (is_array($parseHeaders)) {
899
            foreach ($parseHeaders as $key => $value) {
900
                // Ignore string keys which assume it is already parsed and avoids splitting up a value that includes colons (such as a date/time)
901
                if (!is_string($key) && false !== strpos($value, ':')) {
902
                    [$key, $value]     = explode(':', $value);
903
                    $key               = trim($key);
904
                    $value             = trim($value);
905
                }
906
907
                $headers[$key] = $value;
908
            }
909
        }
910
911
        try {
912
            $timeout = (isset($settings['request_timeout'])) ? (int) $settings['request_timeout'] : 10;
913
            switch ($method) {
914
                case 'GET':
915
                    $result = $connector->get($url, $headers, $timeout);
916
                    break;
917
                case 'POST':
918
                case 'PUT':
919
                case 'PATCH':
920
                    $connectorMethod = strtolower($method);
921
                    $result          = $connector->$connectorMethod($url, $parameters, $headers, $timeout);
922
                    break;
923
                case 'DELETE':
924
                    $result = $connector->delete($url, $headers, $timeout);
925
                    break;
926
            }
927
        } catch (\Exception $exception) {
928
            return ['error' => ['message' => $exception->getMessage(), 'code' => $exception->getCode()]];
929
        }
930
        if (empty($settings['ignore_event_dispatch'])) {
931
            $event->setResponse($result);
932
            $this->dispatcher->dispatch(
933
                PluginEvents::PLUGIN_ON_INTEGRATION_RESPONSE,
934
                $event
935
            );
936
        }
937
        if (!empty($settings['return_raw'])) {
938
            return $result;
939
        } else {
940
            return $this->parseCallbackResponse($result->body, !empty($settings['authorize_session']));
941
        }
942
    }
943
944
    /**
945
     * @param      $integrationEntity
946
     * @param      $integrationEntityId
947
     * @param      $internalEntity
948
     * @param      $internalEntityId
949
     * @param bool $persist
950
     */
951
    public function createIntegrationEntity(
952
        $integrationEntity,
953
        $integrationEntityId,
954
        $internalEntity,
955
        $internalEntityId,
956
        array $internal = null,
957
        $persist = true
958
    ) {
959
        $date = (defined('MAUTIC_DATE_MODIFIED_OVERRIDE')) ? \DateTime::createFromFormat('U', MAUTIC_DATE_MODIFIED_OVERRIDE)
960
            : new \DateTime();
961
        $entity = new IntegrationEntity();
962
        $entity->setDateAdded($date)
963
            ->setLastSyncDate($date)
964
            ->setIntegration($this->getName())
965
            ->setIntegrationEntity($integrationEntity)
966
            ->setIntegrationEntityId($integrationEntityId)
967
            ->setInternalEntity($internalEntity)
968
            ->setInternal($internal)
969
            ->setInternalEntityId($internalEntityId);
970
971
        if ($persist) {
972
            $this->em->getRepository('MauticPluginBundle:IntegrationEntity')->saveEntity($entity);
973
        }
974
975
        return $entity;
976
    }
977
978
    /**
979
     * @return IntegrationEntityRepository
980
     */
981
    public function getIntegrationEntityRepository()
982
    {
983
        return $this->em->getRepository('MauticPluginBundle:IntegrationEntity');
984
    }
985
986
    /**
987
     * Method to prepare the request parameters. Builds array of headers and parameters.
988
     *
989
     * @param $url
990
     * @param $parameters
991
     * @param $method
992
     * @param $settings
993
     * @param $authType
994
     *
995
     * @return array
996
     */
997
    public function prepareRequest($url, $parameters, $method, $settings, $authType)
998
    {
999
        $clientIdKey     = $this->getClientIdKey();
1000
        $clientSecretKey = $this->getClientSecretKey();
1001
        $authTokenKey    = $this->getAuthTokenKey();
1002
        $authToken       = '';
1003
        if (isset($settings['override_auth_token'])) {
1004
            $authToken = $settings['override_auth_token'];
1005
        } elseif (isset($this->keys[$authTokenKey])) {
1006
            $authToken = $this->keys[$authTokenKey];
1007
        }
1008
1009
        // Override token parameter key if neede
1010
        if (!empty($settings[$authTokenKey])) {
1011
            $authTokenKey = $settings[$authTokenKey];
1012
        }
1013
1014
        $headers = [];
1015
1016
        if (!empty($settings['authorize_session'])) {
1017
            switch ($authType) {
1018
                case 'oauth1a':
1019
                    $requestTokenUrl = $this->getRequestTokenUrl();
1020
                    if (!array_key_exists('append_callback', $settings) && !empty($requestTokenUrl)) {
1021
                        $settings['append_callback'] = false;
1022
                    }
1023
                    $oauthHelper = new oAuthHelper($this, $this->request, $settings);
1024
                    $headers     = $oauthHelper->getAuthorizationHeader($url, $parameters, $method);
1025
                    break;
1026
                case 'oauth2':
1027
                    if ($bearerToken = $this->getBearerToken(true)) {
1028
                        $headers = [
1029
                            "Authorization: Basic {$bearerToken}",
1030
                            'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
1031
                        ];
1032
                        $parameters['grant_type'] = 'client_credentials';
1033
                    } else {
1034
                        $defaultGrantType = (!empty($settings['refresh_token'])) ? 'refresh_token'
1035
                            : 'authorization_code';
1036
                        $grantType = (!isset($settings['grant_type'])) ? $defaultGrantType
1037
                            : $settings['grant_type'];
1038
1039
                        $useClientIdKey     = (empty($settings[$clientIdKey])) ? $clientIdKey : $settings[$clientIdKey];
1040
                        $useClientSecretKey = (empty($settings[$clientSecretKey])) ? $clientSecretKey
1041
                            : $settings[$clientSecretKey];
1042
                        $parameters = array_merge(
1043
                            $parameters,
1044
                            [
1045
                                $useClientIdKey     => $this->keys[$clientIdKey],
1046
                                $useClientSecretKey => isset($this->keys[$clientSecretKey]) ? $this->keys[$clientSecretKey] : '',
1047
                                'grant_type'        => $grantType,
1048
                            ]
1049
                        );
1050
1051
                        if (!empty($settings['refresh_token']) && !empty($this->keys[$settings['refresh_token']])) {
1052
                            $parameters[$settings['refresh_token']] = $this->keys[$settings['refresh_token']];
1053
                        }
1054
1055
                        if ('authorization_code' == $grantType) {
1056
                            $parameters['code'] = $this->request->get('code');
1057
                        }
1058
                        if (empty($settings['ignore_redirecturi'])) {
1059
                            $callback                   = $this->getAuthCallbackUrl();
1060
                            $parameters['redirect_uri'] = $callback;
1061
                        }
1062
                    }
1063
                    break;
1064
            }
1065
        } else {
1066
            switch ($authType) {
1067
                case 'basic':
1068
                    $headers = [
1069
                        'Authorization' => 'Basic '.base64_encode($this->keys['username'].':'.$this->keys['password']),
1070
                    ];
1071
                    break;
1072
                case 'oauth1a':
1073
                    $oauthHelper = new oAuthHelper($this, $this->request, $settings);
1074
                    $headers     = $oauthHelper->getAuthorizationHeader($url, $parameters, $method);
1075
                    break;
1076
                case 'oauth2':
1077
                    if ($bearerToken = $this->getBearerToken()) {
1078
                        $headers = [
1079
                            "Authorization: Bearer {$bearerToken}",
1080
                            //"Content-Type: application/x-www-form-urlencoded;charset=UTF-8"
1081
                        ];
1082
                    } else {
1083
                        if (!empty($settings['append_auth_token'])) {
1084
                            // Workaround because $settings cannot be manipulated here
1085
                            $parameters['append_to_query'] = [
1086
                                $authTokenKey => $authToken,
1087
                            ];
1088
                        } else {
1089
                            $parameters[$authTokenKey] = $authToken;
1090
                        }
1091
1092
                        $headers = [
1093
                            "oauth-token: $authTokenKey",
1094
                            "Authorization: OAuth {$authToken}",
1095
                        ];
1096
                    }
1097
                    break;
1098
                case 'key':
1099
                    $parameters[$authTokenKey] = $authToken;
1100
                    break;
1101
            }
1102
        }
1103
1104
        return [$parameters, $headers];
1105
    }
1106
1107
    /**
1108
     * Generate the auth login URL.  Note that if oauth2, response_type=code is assumed.  If this is not the case,
1109
     * override this function.
1110
     *
1111
     * @return string
1112
     */
1113
    public function getAuthLoginUrl()
1114
    {
1115
        $authType = $this->getAuthenticationType();
1116
1117
        if ('oauth2' == $authType) {
1118
            $callback    = $this->getAuthCallbackUrl();
1119
            $clientIdKey = $this->getClientIdKey();
1120
            $state       = $this->getAuthLoginState();
1121
            $url         = $this->getAuthenticationUrl()
1122
                .'?client_id='.$this->keys[$clientIdKey]
1123
                .'&response_type=code'
1124
                .'&redirect_uri='.urlencode($callback)
1125
                .'&state='.$state;
1126
1127
            if ($scope = $this->getAuthScope()) {
1128
                $url .= '&scope='.urlencode($scope);
1129
            }
1130
1131
            if ($this->session) {
1132
                $this->session->set($this->getName().'_csrf_token', $state);
1133
            }
1134
1135
            return $url;
1136
        } else {
1137
            return $this->router->generate(
1138
                'mautic_integration_auth_callback',
1139
                ['integration' => $this->getName()]
1140
            );
1141
        }
1142
    }
1143
1144
    /**
1145
     * State variable to append to login url (usually used in oAuth flows).
1146
     *
1147
     * @return string
1148
     */
1149
    public function getAuthLoginState()
1150
    {
1151
        return hash('sha1', uniqid(mt_rand()));
1152
    }
1153
1154
    /**
1155
     * Get the scope for auth flows.
1156
     *
1157
     * @return string
1158
     */
1159
    public function getAuthScope()
1160
    {
1161
        return '';
1162
    }
1163
1164
    /**
1165
     * Gets the URL for the built in oauth callback.
1166
     *
1167
     * @return string
1168
     */
1169
    public function getAuthCallbackUrl()
1170
    {
1171
        $defaultUrl = $this->router->generate(
1172
            'mautic_integration_auth_callback',
1173
            ['integration' => $this->getName()],
1174
            UrlGeneratorInterface::ABSOLUTE_URL //absolute
1175
        );
1176
1177
        /** @var PluginIntegrationAuthCallbackUrlEvent $event */
1178
        $event = $this->dispatcher->dispatch(
1179
            PluginEvents::PLUGIN_ON_INTEGRATION_GET_AUTH_CALLBACK_URL,
1180
            new PluginIntegrationAuthCallbackUrlEvent($this, $defaultUrl)
1181
        );
1182
1183
        return $event->getCallbackUrl();
1184
    }
1185
1186
    /**
1187
     * Retrieves and stores tokens returned from oAuthLogin.
1188
     *
1189
     * @param array $settings
1190
     * @param array $parameters
1191
     *
1192
     * @return bool|string false if no error; otherwise the error string
1193
     *
1194
     * @throws ApiErrorException if OAuth2 state does not match
1195
     */
1196
    public function authCallback($settings = [], $parameters = [])
1197
    {
1198
        $authType = $this->getAuthenticationType();
1199
1200
        switch ($authType) {
1201
            case 'oauth2':
1202
                if ($this->session) {
1203
                    $state      = $this->session->get($this->getName().'_csrf_token', false);
1204
                    $givenState = ($this->request->isXmlHttpRequest()) ? $this->request->request->get('state') : $this->request->get('state');
1205
1206
                    if ($state && $state !== $givenState) {
1207
                        $this->session->remove($this->getName().'_csrf_token');
1208
                        throw new ApiErrorException($this->translator->trans('mautic.integration.auth.invalid.state'));
1209
                    }
1210
                }
1211
1212
                if (!empty($settings['use_refresh_token'])) {
1213
                    // Try refresh token
1214
                    $refreshTokenKeys = $this->getRefreshTokenKeys();
1215
1216
                    if (!empty($refreshTokenKeys)) {
1217
                        [$refreshTokenKey, $expiryKey] = $refreshTokenKeys;
1218
1219
                        $settings['refresh_token'] = $refreshTokenKey;
1220
                    }
1221
                }
1222
                break;
1223
1224
            case 'oauth1a':
1225
                // After getting request_token and authorizing, post back to access_token
1226
                $settings['append_callback']  = true;
1227
                $settings['include_verifier'] = true;
1228
1229
                // Get request token returned from Twitter and submit it to get access_token
1230
                $settings['request_token'] = ($this->request) ? $this->request->get('oauth_token') : '';
1231
1232
                break;
1233
        }
1234
1235
        $settings['authorize_session'] = true;
1236
1237
        $method = (!isset($settings['method'])) ? 'POST' : $settings['method'];
1238
        $data   = $this->makeRequest($this->getAccessTokenUrl(), $parameters, $method, $settings);
1239
1240
        return $this->extractAuthKeys($data);
1241
    }
1242
1243
    /**
1244
     * Extacts the auth keys from response and saves entity.
1245
     *
1246
     * @param $data
1247
     * @param $tokenOverride
1248
     *
1249
     * @return bool|string false if no error; otherwise the error string
1250
     */
1251
    public function extractAuthKeys($data, $tokenOverride = null)
1252
    {
1253
        //check to see if an entity exists
1254
        $entity = $this->getIntegrationSettings();
1255
        if (null == $entity) {
1256
            $entity = new Integration();
1257
            $entity->setName($this->getName());
1258
        }
1259
        // Prepare the keys for extraction such as renaming, setting expiry, etc
1260
        $data = $this->prepareResponseForExtraction($data);
1261
1262
        //parse the response
1263
        $authTokenKey = ($tokenOverride) ? $tokenOverride : $this->getAuthTokenKey();
1264
        if (is_array($data) && isset($data[$authTokenKey])) {
1265
            $keys      = $this->mergeApiKeys($data, null, true);
1266
            $encrypted = $this->encryptApiKeys($keys);
1267
            $entity->setApiKeys($encrypted);
1268
1269
            if ($this->session) {
1270
                $this->session->set($this->getName().'_tokenResponse', $data);
1271
            }
1272
1273
            $error = false;
1274
        } elseif (is_array($data) && isset($data['access_token'])) {
1275
            if ($this->session) {
1276
                $this->session->set($this->getName().'_tokenResponse', $data);
1277
            }
1278
            $error = false;
1279
        } else {
1280
            $error = $this->getErrorsFromResponse($data);
1281
            if (empty($error)) {
1282
                $error = $this->translator->trans(
1283
                    'mautic.integration.error.genericerror',
1284
                    [],
1285
                    'flashes'
1286
                );
1287
            }
1288
        }
1289
1290
        //save the data
1291
        $this->em->persist($entity);
1292
        $this->em->flush();
1293
1294
        $this->setIntegrationSettings($entity);
1295
1296
        return $error;
1297
    }
1298
1299
    /**
1300
     * Called in extractAuthKeys before key comparison begins to give opportunity to set expiry, rename keys, etc.
1301
     *
1302
     * @param $data
1303
     *
1304
     * @return mixed
1305
     */
1306
    public function prepareResponseForExtraction($data)
1307
    {
1308
        return $data;
1309
    }
1310
1311
    /**
1312
     * Checks to see if the integration is configured by checking that required keys are populated.
1313
     *
1314
     * @return bool
1315
     */
1316
    public function isConfigured()
1317
    {
1318
        $requiredTokens = $this->getRequiredKeyFields();
1319
        foreach ($requiredTokens as $token => $label) {
1320
            if (empty($this->keys[$token])) {
1321
                return false;
1322
            }
1323
        }
1324
1325
        return true;
1326
    }
1327
1328
    /**
1329
     * Checks if an integration is authorized and/or authorizes the request.
1330
     *
1331
     * @return bool
1332
     */
1333
    public function isAuthorized()
1334
    {
1335
        if (!$this->isConfigured()) {
1336
            return false;
1337
        }
1338
1339
        $type         = $this->getAuthenticationType();
1340
        $authTokenKey = $this->getAuthTokenKey();
1341
1342
        switch ($type) {
1343
            case 'oauth1a':
1344
            case 'oauth2':
1345
                $refreshTokenKeys = $this->getRefreshTokenKeys();
1346
                if (!isset($this->keys[$authTokenKey])) {
1347
                    $valid = false;
1348
                } elseif (!empty($refreshTokenKeys)) {
1349
                    [$refreshTokenKey, $expiryKey] = $refreshTokenKeys;
1350
                    if (!empty($this->keys[$refreshTokenKey]) && !empty($expiryKey) && isset($this->keys[$expiryKey])
1351
                        && time() > $this->keys[$expiryKey]
1352
                    ) {
1353
                        //token has expired so try to refresh it
1354
                        $error = $this->authCallback(['refresh_token' => $refreshTokenKey]);
1355
                        $valid = (empty($error));
1356
                    } else {
1357
                        // The refresh token doesn't have an expiry so the integration will have to check for expired sessions and request new token
1358
                        $valid = true;
1359
                    }
1360
                } else {
1361
                    $valid = true;
1362
                }
1363
                break;
1364
            case 'key':
1365
                $valid = isset($this->keys['api_key']);
1366
                break;
1367
            case 'rest':
1368
                $valid = isset($this->keys[$authTokenKey]);
1369
                break;
1370
            case 'basic':
1371
                $valid = (!empty($this->keys['username']) && !empty($this->keys['password']));
1372
                break;
1373
            default:
1374
                $valid = true;
1375
                break;
1376
        }
1377
1378
        return $valid;
1379
    }
1380
1381
    /**
1382
     * Get the URL required to obtain an oauth2 access token.
1383
     *
1384
     * @return string
1385
     */
1386
    public function getAccessTokenUrl()
1387
    {
1388
        return '';
1389
    }
1390
1391
    /**
1392
     * Get the authentication/login URL for oauth2 access.
1393
     *
1394
     * @return string
1395
     */
1396
    public function getAuthenticationUrl()
1397
    {
1398
        return '';
1399
    }
1400
1401
    /**
1402
     * Get request token for oauth1a authorization request.
1403
     *
1404
     * @param array $settings
1405
     *
1406
     * @return mixed|string
1407
     */
1408
    public function getRequestToken($settings = [])
1409
    {
1410
        // Child classes can easily pass in custom settings this way
1411
        $settings = array_merge(
1412
            ['authorize_session' => true, 'append_callback' => false, 'ssl_verifypeer' => true],
1413
            $settings
1414
        );
1415
1416
        // init result to empty string
1417
        $result = '';
1418
1419
        $url = $this->getRequestTokenUrl();
1420
        if (!empty($url)) {
1421
            $result = $this->makeRequest(
1422
                $url,
1423
                [],
1424
                'POST',
1425
                $settings
1426
            );
1427
        }
1428
1429
        return $result;
1430
    }
1431
1432
    /**
1433
     * Url to post in order to get the request token if required; leave empty if not required.
1434
     *
1435
     * @return string
1436
     */
1437
    public function getRequestTokenUrl()
1438
    {
1439
        return '';
1440
    }
1441
1442
    /**
1443
     * Generate a bearer token.
1444
     *
1445
     * @param $inAuthorization
1446
     *
1447
     * @return string
1448
     */
1449
    public function getBearerToken($inAuthorization = false)
1450
    {
1451
        return '';
1452
    }
1453
1454
    /**
1455
     * Get an array of public activity.
1456
     *
1457
     * @param $identifier
1458
     * @param $socialCache
1459
     *
1460
     * @return array
1461
     */
1462
    public function getPublicActivity($identifier, &$socialCache)
1463
    {
1464
        return [];
1465
    }
1466
1467
    /**
1468
     * Get an array of public data.
1469
     *
1470
     * @param $identifier
1471
     * @param $socialCache
1472
     *
1473
     * @return array
1474
     */
1475
    public function getUserData($identifier, &$socialCache)
1476
    {
1477
        return [];
1478
    }
1479
1480
    /**
1481
     * Generates current URL to set as referer for curl calls.
1482
     *
1483
     * @return string
1484
     */
1485
    protected function getRefererUrl()
1486
    {
1487
        return ($this->request) ? $this->request->getRequestUri() : null;
1488
    }
1489
1490
    /**
1491
     * Generate a user agent string.
1492
     *
1493
     * @return string
1494
     */
1495
    protected function getUserAgent()
1496
    {
1497
        return ($this->request) ? $this->request->server->get('HTTP_USER_AGENT') : null;
1498
    }
1499
1500
    /**
1501
     * Get a list of available fields from the connecting API.
1502
     *
1503
     * @param array $settings
1504
     *
1505
     * @return array
1506
     */
1507
    public function getAvailableLeadFields($settings = [])
1508
    {
1509
        if (empty($settings['ignore_field_cache'])) {
1510
            $cacheSuffix = (isset($settings['cache_suffix'])) ? $settings['cache_suffix'] : '';
1511
            if ($fields = $this->cache->get('leadFields'.$cacheSuffix)) {
1512
                return $fields;
1513
            }
1514
        }
1515
1516
        return [];
1517
    }
1518
1519
    /**
1520
     * @return array
1521
     */
1522
    public function cleanUpFields(Integration $entity, array $mauticLeadFields, array $mauticCompanyFields)
1523
    {
1524
        $featureSettings        = $entity->getFeatureSettings();
1525
        $submittedFields        = (isset($featureSettings['leadFields'])) ? $featureSettings['leadFields'] : [];
1526
        $submittedCompanyFields = (isset($featureSettings['companyFields'])) ? $featureSettings['companyFields'] : [];
1527
        $submittedObjects       = (isset($featureSettings['objects'])) ? $featureSettings['objects'] : [];
1528
        $missingRequiredFields  = [];
1529
1530
        // add special case in order to prevent it from being removed
1531
        $mauticLeadFields['mauticContactId']                   = '';
1532
        $mauticLeadFields['mauticContactTimelineLink']         = '';
1533
        $mauticLeadFields['mauticContactIsContactableByEmail'] = '';
1534
1535
        //make sure now non-existent aren't saved
1536
        $settings = [
1537
            'ignore_field_cache' => false,
1538
        ];
1539
        $settings['feature_settings']['objects'] = $submittedObjects;
1540
        $availableIntegrationFields              = $this->getAvailableLeadFields($settings);
1541
        $leadFields                              = [];
1542
1543
        /**
1544
         * @param $mappedFields
1545
         * @param $integrationFields
1546
         * @param $mauticFields
1547
         * @param $fieldType
1548
         */
1549
        $cleanup = function (&$mappedFields, $integrationFields, $mauticFields, $fieldType) use (&$missingRequiredFields, &$featureSettings) {
1550
            $updateKey    = ('companyFields' === $fieldType) ? 'update_mautic_company' : 'update_mautic';
1551
            $removeFields = array_keys(array_diff_key($mappedFields, $integrationFields));
1552
1553
            // Find all the mapped fields that no longer exist in Mautic
1554
            if ($nonExistentFields = array_diff($mappedFields, array_keys($mauticFields))) {
1555
                // Remove those fields
1556
                $removeFields = array_merge($removeFields, array_keys($nonExistentFields));
1557
            }
1558
1559
            foreach ($removeFields as $field) {
1560
                unset($mappedFields[$field]);
1561
1562
                if (isset($featureSettings[$updateKey])) {
1563
                    unset($featureSettings[$updateKey][$field]);
1564
                }
1565
            }
1566
1567
            // Check that the remaining fields have an updateKey set
1568
            foreach ($mappedFields as $field => $mauticField) {
1569
                if (!isset($featureSettings[$updateKey][$field])) {
1570
                    // Assume it's mapped to Mautic
1571
                    $featureSettings[$updateKey][$field] = 1;
1572
                }
1573
            }
1574
1575
            // Check if required fields are missing
1576
            $required = $this->getRequiredFields($integrationFields, $fieldType);
1577
            if (array_diff_key($required, $mappedFields)) {
1578
                $missingRequiredFields[$fieldType] = true;
1579
            }
1580
        };
1581
1582
        if ($submittedObjects) {
1583
            if (in_array('company', $submittedObjects)) {
1584
                // special handling for company fields
1585
                if (isset($availableIntegrationFields['company'])) {
1586
                    $cleanup($submittedCompanyFields, $availableIntegrationFields['company'], $mauticCompanyFields, 'companyFields');
1587
                    $featureSettings['companyFields'] = $submittedCompanyFields;
1588
                    unset($availableIntegrationFields['company']);
1589
                }
1590
            }
1591
1592
            // Rest of the objects are merged and assumed to be leadFields
1593
            // BC compatibility If extends fields to objects - 0 === contacts
1594
            if (isset($availableIntegrationFields[0])) {
1595
                $leadFields = array_merge($leadFields, $availableIntegrationFields[0]);
1596
            }
1597
1598
            foreach ($submittedObjects as $object) {
1599
                if (isset($availableIntegrationFields[$object])) {
1600
                    $leadFields = array_merge($leadFields, $availableIntegrationFields[$object]);
1601
                }
1602
            }
1603
        } else {
1604
            // Cleanup assuming there are no objects as keys
1605
            $leadFields = $availableIntegrationFields;
1606
        }
1607
1608
        if (!empty($leadFields)) {
1609
            $cleanup($submittedFields, $leadFields, $mauticLeadFields, 'leadFields');
1610
            $featureSettings['leadFields'] = $submittedFields;
1611
        }
1612
1613
        $entity->setFeatureSettings($featureSettings);
1614
1615
        return $missingRequiredFields;
1616
    }
1617
1618
    /**
1619
     * @param string $fieldType
1620
     *
1621
     * @return array
1622
     */
1623
    public function getRequiredFields(array $fields, $fieldType = '')
1624
    {
1625
        //use $fieldType to determine if email should be required. we use email as unique identifier for contacts only,
1626
        // if any other fieldType use integrations own field types
1627
        $requiredFields = [];
1628
        foreach ($fields as $field => $details) {
1629
            if ('leadFields' === $fieldType) {
1630
                if ((is_array($details) && !empty($details['required'])) || 'email' === $field
1631
                    || (isset($details['optionLabel'])
1632
                        && 'email' == strtolower(
1633
                            $details['optionLabel']
1634
                        ))
1635
                ) {
1636
                    $requiredFields[$field] = $field;
1637
                }
1638
            } else {
1639
                if ((is_array($details) && !empty($details['required']))
1640
                ) {
1641
                    $requiredFields[$field] = $field;
1642
                }
1643
            }
1644
        }
1645
1646
        return $requiredFields;
1647
    }
1648
1649
    /**
1650
     * Match lead data with integration fields.
1651
     *
1652
     * @param $lead
1653
     * @param $config
1654
     *
1655
     * @return array
1656
     */
1657
    public function populateLeadData($lead, $config = [])
1658
    {
1659
        if (!isset($config['leadFields'])) {
1660
            $config = $this->mergeConfigToFeatureSettings($config);
1661
1662
            if (empty($config['leadFields'])) {
1663
                return [];
1664
            }
1665
        }
1666
1667
        if ($lead instanceof Lead) {
1668
            $fields = $lead->getProfileFields();
1669
            $leadId = $lead->getId();
1670
        } else {
1671
            $fields = $lead;
1672
            $leadId = $lead['id'];
1673
        }
1674
1675
        $object          = isset($config['object']) ? $config['object'] : null;
1676
        $leadFields      = $config['leadFields'];
1677
        $availableFields = $this->getAvailableLeadFields($config);
1678
1679
        if ($object) {
1680
            $availableFields = $availableFields[$config['object']];
1681
        } else {
1682
            $availableFields = (isset($availableFields[0])) ? $availableFields[0] : $availableFields;
1683
        }
1684
1685
        $unknown = $this->translator->trans('mautic.integration.form.lead.unknown');
1686
        $matched = [];
1687
1688
        foreach ($availableFields as $key => $field) {
1689
            $integrationKey = $matchIntegrationKey = $this->convertLeadFieldKey($key, $field);
1690
            if (is_array($integrationKey)) {
1691
                [$integrationKey, $matchIntegrationKey] = $integrationKey;
1692
            } elseif (!isset($config['leadFields'][$integrationKey])) {
1693
                continue;
1694
            }
1695
1696
            if (isset($leadFields[$integrationKey])) {
1697
                if ('mauticContactTimelineLink' === $leadFields[$integrationKey]) {
1698
                    $matched[$integrationKey] = $this->getContactTimelineLink($leadId);
1699
1700
                    continue;
1701
                }
1702
                if ('mauticContactIsContactableByEmail' === $leadFields[$integrationKey]) {
1703
                    $matched[$integrationKey] = $this->getLeadDoNotContact($leadId);
1704
1705
                    continue;
1706
                }
1707
                if ('mauticContactId' === $leadFields[$integrationKey]) {
1708
                    $matched[$integrationKey] = $lead->getId();
1709
                    continue;
1710
                }
1711
                $mauticKey = $leadFields[$integrationKey];
1712
                if (isset($fields[$mauticKey]) && '' !== $fields[$mauticKey] && null !== $fields[$mauticKey]) {
1713
                    $matched[$matchIntegrationKey] = $this->cleanPushData(
1714
                        $fields[$mauticKey],
1715
                        (isset($field['type'])) ? $field['type'] : 'string'
1716
                    );
1717
                }
1718
            }
1719
1720
            if (!empty($field['required']) && empty($matched[$matchIntegrationKey])) {
1721
                $matched[$matchIntegrationKey] = $unknown;
1722
            }
1723
        }
1724
1725
        return $matched;
1726
    }
1727
1728
    /**
1729
     * Match Company data with integration fields.
1730
     *
1731
     * @param $entity
1732
     * @param $config
1733
     *
1734
     * @return array
1735
     */
1736
    public function populateCompanyData($entity, $config = [])
1737
    {
1738
        if (!isset($config['companyFields'])) {
1739
            $config = $this->mergeConfigToFeatureSettings($config);
1740
1741
            if (empty($config['companyFields'])) {
1742
                return [];
1743
            }
1744
        }
1745
1746
        if ($entity instanceof Lead) {
1747
            $fields = $entity->getPrimaryCompany();
1748
        } else {
1749
            $fields = $entity['primaryCompany'];
1750
        }
1751
1752
        $companyFields   = $config['companyFields'];
1753
        $availableFields = $this->getAvailableLeadFields($config)['company'];
1754
        $unknown         = $this->translator->trans('mautic.integration.form.lead.unknown');
1755
        $matched         = [];
1756
1757
        foreach ($availableFields as $key => $field) {
1758
            $integrationKey = $this->convertLeadFieldKey($key, $field);
1759
1760
            if (isset($companyFields[$key])) {
1761
                $mauticKey = $companyFields[$key];
1762
                if (isset($fields[$mauticKey]) && !empty($fields[$mauticKey])) {
1763
                    $matched[$integrationKey] = $this->cleanPushData($fields[$mauticKey], (isset($field['type'])) ? $field['type'] : 'string');
1764
                }
1765
            }
1766
1767
            if (!empty($field['required']) && empty($matched[$integrationKey])) {
1768
                $matched[$integrationKey] = $unknown;
1769
            }
1770
        }
1771
1772
        return $matched;
1773
    }
1774
1775
    /**
1776
     * Takes profile data from an integration and maps it to Mautic's lead fields.
1777
     *
1778
     * @param       $data
1779
     * @param array $config
1780
     * @param null  $object
1781
     *
1782
     * @return array
1783
     */
1784
    public function populateMauticLeadData($data, $config = [], $object = null)
1785
    {
1786
        // Glean supported fields from what was returned by the integration
1787
        $gleanedData = $data;
1788
1789
        if (null == $object) {
1790
            $object = 'lead';
1791
        }
1792
        if ('company' == $object) {
1793
            if (!isset($config['companyFields'])) {
1794
                $config = $this->mergeConfigToFeatureSettings($config);
1795
1796
                if (empty($config['companyFields'])) {
1797
                    return [];
1798
                }
1799
            }
1800
1801
            $fields = $config['companyFields'];
1802
        }
1803
        if ('lead' == $object) {
1804
            if (!isset($config['leadFields'])) {
1805
                $config = $this->mergeConfigToFeatureSettings($config);
1806
1807
                if (empty($config['leadFields'])) {
1808
                    return [];
1809
                }
1810
            }
1811
            $fields = $config['leadFields'];
1812
        }
1813
1814
        $matched = [];
1815
        foreach ($gleanedData as $key => $field) {
1816
            if (isset($fields[$key]) && isset($gleanedData[$key])
1817
                && $this->translator->trans('mautic.integration.form.lead.unknown') !== $gleanedData[$key]
1818
            ) {
1819
                $matched[$fields[$key]] = $gleanedData[$key];
1820
            }
1821
        }
1822
1823
        return $matched;
1824
    }
1825
1826
    /**
1827
     * Create or update existing Mautic lead from the integration's profile data.
1828
     *
1829
     * @param mixed       $data        Profile data from integration
1830
     * @param bool|true   $persist     Set to false to not persist lead to the database in this method
1831
     * @param array|null  $socialCache
1832
     * @param mixed||null $identifiers
1833
     *
1834
     * @return Lead
1835
     */
1836
    public function getMauticLead($data, $persist = true, $socialCache = null, $identifiers = null)
1837
    {
1838
        if (is_object($data)) {
1839
            // Convert to array in all levels
1840
            $data = json_encode(json_decode($data), true);
1841
        } elseif (is_string($data)) {
1842
            // Assume JSON
1843
            $data = json_decode($data, true);
1844
        }
1845
1846
        // Match that data with mapped lead fields
1847
        $matchedFields = $this->populateMauticLeadData($data);
1848
1849
        if (empty($matchedFields)) {
1850
            return;
1851
        }
1852
1853
        // Find unique identifier fields used by the integration
1854
        /** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */
1855
        $leadModel           = $this->leadModel;
1856
        $uniqueLeadFields    = $this->fieldModel->getUniqueIdentifierFields();
1857
        $uniqueLeadFieldData = [];
1858
1859
        foreach ($matchedFields as $leadField => $value) {
1860
            if (array_key_exists($leadField, $uniqueLeadFields) && !empty($value)) {
1861
                $uniqueLeadFieldData[$leadField] = $value;
1862
            }
1863
        }
1864
1865
        // Default to new lead
1866
        $lead = new Lead();
1867
        $lead->setNewlyCreated(true);
1868
1869
        if (count($uniqueLeadFieldData)) {
1870
            $existingLeads = $this->em->getRepository('MauticLeadBundle:Lead')
1871
                ->getLeadsByUniqueFields($uniqueLeadFieldData);
1872
1873
            if (!empty($existingLeads)) {
1874
                $lead = array_shift($existingLeads);
1875
            }
1876
        }
1877
1878
        $leadModel->setFieldValues($lead, $matchedFields, false, false);
1879
1880
        // Update the social cache
1881
        $leadSocialCache = $lead->getSocialCache();
1882
        if (!isset($leadSocialCache[$this->getName()])) {
1883
            $leadSocialCache[$this->getName()] = [];
1884
        }
1885
1886
        if (null !== $socialCache) {
1887
            $leadSocialCache[$this->getName()] = array_merge($leadSocialCache[$this->getName()], $socialCache);
1888
        }
1889
1890
        // Check for activity while here
1891
        if (null !== $identifiers && in_array('public_activity', $this->getSupportedFeatures())) {
1892
            $this->getPublicActivity($identifiers, $leadSocialCache[$this->getName()]);
1893
        }
1894
1895
        $lead->setSocialCache($leadSocialCache);
1896
1897
        // Update the internal info integration object that has updated the record
1898
        if (isset($data['internal'])) {
1899
            $internalInfo                   = $lead->getInternal();
1900
            $internalInfo[$this->getName()] = $data['internal'];
1901
            $lead->setInternal($internalInfo);
1902
        }
1903
1904
        if ($persist && !empty($lead->getChanges(true))) {
1905
            // Only persist if instructed to do so as it could be that calling code needs to manipulate the lead prior to executing event listeners
1906
            try {
1907
                $lead->setManipulator(new LeadManipulator(
1908
                    'plugin',
1909
                    $this->getName(),
1910
                    null,
1911
                    $this->getDisplayName()
1912
                ));
1913
                $leadModel->saveEntity($lead, false);
1914
            } catch (\Exception $exception) {
1915
                $this->logger->addWarning($exception->getMessage());
1916
1917
                return;
1918
            }
1919
        }
1920
1921
        return $lead;
1922
    }
1923
1924
    /**
1925
     * Merges a config from integration_list with feature settings.
1926
     *
1927
     * @param array $config
1928
     *
1929
     * @return array|mixed
1930
     */
1931
    public function mergeConfigToFeatureSettings($config = [])
1932
    {
1933
        $featureSettings = $this->settings->getFeatureSettings();
1934
1935
        if (isset($config['config'])
1936
            && (empty($config['integration'])
1937
                || (!empty($config['integration'])
1938
                    && $config['integration'] == $this->getName()))
1939
        ) {
1940
            $featureSettings = array_merge($featureSettings, $config['config']);
1941
        }
1942
1943
        return $featureSettings;
1944
    }
1945
1946
    /**
1947
     * Return key recognized by integration.
1948
     *
1949
     * @param $key
1950
     * @param $field
1951
     *
1952
     * @return mixed
1953
     */
1954
    public function convertLeadFieldKey($key, $field)
1955
    {
1956
        return $key;
1957
    }
1958
1959
    /**
1960
     * Sets whether fields should be sorted alphabetically or by the order the integration feeds.
1961
     */
1962
    public function sortFieldsAlphabetically()
1963
    {
1964
        return true;
1965
    }
1966
1967
    /**
1968
     * Used to match local field name with remote field name.
1969
     *
1970
     * @param string $field
1971
     * @param string $subfield
1972
     *
1973
     * @return mixed
1974
     */
1975
    public function matchFieldName($field, $subfield = '')
1976
    {
1977
        if (!empty($field) && !empty($subfield)) {
1978
            return $subfield.ucfirst($field);
1979
        }
1980
1981
        return $field;
1982
    }
1983
1984
    /**
1985
     * Convert and assign the data to assignable fields.
1986
     *
1987
     * @param mixed $data
1988
     *
1989
     * @return array
1990
     */
1991
    protected function matchUpData($data)
1992
    {
1993
        $info      = [];
1994
        $available = $this->getAvailableLeadFields();
1995
1996
        foreach ($available as $field => $fieldDetails) {
1997
            if (is_array($data)) {
1998
                if (!isset($data[$field]) and !is_object($data)) {
1999
                    $info[$field] = '';
2000
                    continue;
2001
                } else {
2002
                    $values = $data[$field];
2003
                }
2004
            } else {
2005
                if (!isset($data->$field)) {
2006
                    $info[$field] = '';
2007
                    continue;
2008
                } else {
2009
                    $values = $data->$field;
2010
                }
2011
            }
2012
2013
            switch ($fieldDetails['type']) {
2014
                case 'string':
2015
                case 'boolean':
2016
                    $info[$field] = $values;
2017
                    break;
2018
                case 'object':
2019
                    foreach ($fieldDetails['fields'] as $f) {
2020
                        if (isset($values->$f)) {
2021
                            $fn = $this->matchFieldName($field, $f);
2022
2023
                            $info[$fn] = $values->$f;
2024
                        }
2025
                    }
2026
                    break;
2027
                case 'array_object':
2028
                    $objects = [];
2029
                    if (!empty($values)) {
2030
                        foreach ($values as $v) {
2031
                            if (isset($v->value)) {
2032
                                $objects[] = $v->value;
2033
                            }
2034
                        }
2035
                    }
2036
                    $fn = (isset($fieldDetails['fields'][0])) ? $this->matchFieldName(
2037
                        $field,
2038
                        $fieldDetails['fields'][0]
2039
                    ) : $field;
2040
                    $info[$fn] = implode('; ', $objects);
2041
2042
                    break;
2043
            }
2044
        }
2045
2046
        return $info;
2047
    }
2048
2049
    /**
2050
     * Get the path to the profile templates for this integration.
2051
     */
2052
    public function getSocialProfileTemplate()
2053
    {
2054
        return null;
2055
    }
2056
2057
    /**
2058
     * Checks to ensure an image still exists before caching.
2059
     *
2060
     * @param string $url
2061
     *
2062
     * @return bool
2063
     */
2064
    public function checkImageExists($url)
2065
    {
2066
        $ch = curl_init($url);
2067
        curl_setopt($ch, CURLOPT_NOBODY, true);
2068
        curl_setopt(
2069
            $ch,
2070
            CURLOPT_USERAGENT,
2071
            'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13'
2072
        );
2073
        curl_exec($ch);
2074
        $retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
2075
        curl_close($ch);
2076
2077
        return 200 == $retcode;
2078
    }
2079
2080
    /**
2081
     * @return \Mautic\CoreBundle\Model\NotificationModel
2082
     */
2083
    public function getNotificationModel()
2084
    {
2085
        return $this->notificationModel;
2086
    }
2087
2088
    public function logIntegrationError(\Exception $e, Lead $contact = null)
2089
    {
2090
        $logger = $this->logger;
2091
2092
        if ($e instanceof ApiErrorException) {
2093
            if (null === $this->adminUsers) {
2094
                $this->adminUsers = $this->em->getRepository('MauticUserBundle:User')->getEntities(
2095
                    [
2096
                        'filter' => [
2097
                            'force' => [
2098
                                [
2099
                                    'column' => 'r.isAdmin',
2100
                                    'expr'   => 'eq',
2101
                                    'value'  => true,
2102
                                ],
2103
                            ],
2104
                        ],
2105
                    ]
2106
                );
2107
            }
2108
2109
            $errorMessage = $e->getMessage();
2110
            $errorHeader  = $this->getTranslator()->trans(
2111
                'mautic.integration.error',
2112
                [
2113
                    '%name%' => $this->getName(),
2114
                ]
2115
            );
2116
2117
            if ($contact || $contact = $e->getContact()) {
2118
                // Append a link to the contact
2119
                $contactId   = $contact->getId();
2120
                $contactName = $contact->getPrimaryIdentifier();
2121
            } elseif ($contactId = $e->getContactId()) {
2122
                $contactName = $this->getTranslator()->trans('mautic.integration.error.generic_contact_name', ['%id%' => $contactId]);
2123
            }
2124
2125
            $this->lastIntegrationError = $errorHeader.': '.$errorMessage;
2126
2127
            if ($contactId) {
2128
                $contactLink = $this->router->generate(
2129
                    'mautic_contact_action',
2130
                    [
2131
                        'objectAction' => 'view',
2132
                        'objectId'     => $contactId,
2133
                    ],
2134
                    UrlGeneratorInterface::ABSOLUTE_URL
2135
                );
2136
                $errorMessage .= ' <a href="'.$contactLink.'">'.$contactName.'</a>';
2137
            }
2138
2139
            // Prevent a flood of the same messages
2140
            $messageHash = md5($errorMessage);
2141
            if (!array_key_exists($messageHash, $this->notifications)) {
2142
                foreach ($this->adminUsers as $user) {
2143
                    $this->getNotificationModel()->addNotification(
2144
                        $errorMessage,
2145
                        $this->getName(),
2146
                        false,
2147
                        $errorHeader,
2148
                        'text-danger fa-exclamation-circle',
2149
                        null,
2150
                        $user
2151
                    );
2152
                }
2153
2154
                $this->notifications[$messageHash] = true;
2155
            }
2156
        }
2157
2158
        $logger->addError('INTEGRATION ERROR: '.$this->getName().' - '.(('dev' == MAUTIC_ENV) ? (string) $e : $e->getMessage()));
2159
    }
2160
2161
    /**
2162
     * @return string|null
2163
     */
2164
    public function getLastIntegrationError()
2165
    {
2166
        return $this->lastIntegrationError;
2167
    }
2168
2169
    /**
2170
     * @return $this
2171
     */
2172
    public function resetLastIntegrationError()
2173
    {
2174
        $this->lastIntegrationError = null;
2175
2176
        return $this;
2177
    }
2178
2179
    /**
2180
     * Returns notes specific to sections of the integration form (if applicable).
2181
     *
2182
     * @param $section
2183
     *
2184
     * @return string
2185
     */
2186
    public function getFormNotes($section)
2187
    {
2188
        if ('leadfield_match' == $section) {
2189
            return ['mautic.integration.form.field_match_notes', 'info'];
2190
        } else {
2191
            return ['', 'info'];
2192
        }
2193
    }
2194
2195
    /**
2196
     * Allows appending extra data to the config.
2197
     *
2198
     * @param FormBuilder|Form $builder
2199
     * @param array            $data
2200
     * @param string           $formArea Section of form being built keys|features|integration
2201
     *                                   keys can be used to store login/request related settings; keys are encrypted
2202
     *                                   features can be used for configuring share buttons, etc
2203
     *                                   integration is called when adding an integration to events like point triggers,
2204
     *                                   campaigns actions, forms actions, etc
2205
     */
2206
    public function appendToForm(&$builder, $data, $formArea)
2207
    {
2208
    }
2209
2210
    /**
2211
     * @param FormBuilder $builder
2212
     * @param array       $options
2213
     */
2214
    public function modifyForm($builder, $options)
2215
    {
2216
        $this->dispatcher->dispatch(
2217
            PluginEvents::PLUGIN_ON_INTEGRATION_FORM_BUILD,
2218
            new PluginIntegrationFormBuildEvent($this, $builder, $options)
2219
        );
2220
    }
2221
2222
    /**
2223
     * Returns settings for the integration form.
2224
     *
2225
     * @return array
2226
     */
2227
    public function getFormSettings()
2228
    {
2229
        $type               = $this->getAuthenticationType();
2230
        $enableDataPriority = $this->getDataPriority();
2231
        switch ($type) {
2232
            case 'oauth1a':
2233
            case 'oauth2':
2234
                $callback              = true;
2235
                $requiresAuthorization = true;
2236
                break;
2237
            default:
2238
                $callback              = false;
2239
                $requiresAuthorization = false;
2240
                break;
2241
        }
2242
2243
        return [
2244
            'requires_callback'      => $callback,
2245
            'requires_authorization' => $requiresAuthorization,
2246
            'default_features'       => [],
2247
            'enable_data_priority'   => $enableDataPriority,
2248
        ];
2249
    }
2250
2251
    /**
2252
     * @return array
2253
     */
2254
    public function getFormDisplaySettings()
2255
    {
2256
        /** @var PluginIntegrationFormDisplayEvent $event */
2257
        $event = $this->dispatcher->dispatch(
2258
            PluginEvents::PLUGIN_ON_INTEGRATION_FORM_DISPLAY,
2259
            new PluginIntegrationFormDisplayEvent($this, $this->getFormSettings())
2260
        );
2261
2262
        return $event->getSettings();
2263
    }
2264
2265
    /**
2266
     * Get available fields for choices in the config UI.
2267
     *
2268
     * @param array $settings
2269
     *
2270
     * @return array
2271
     */
2272
    public function getFormLeadFields($settings = [])
2273
    {
2274
        if (isset($settings['feature_settings']['objects']['company'])) {
2275
            unset($settings['feature_settings']['objects']['company']);
2276
        }
2277
2278
        return ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
2279
    }
2280
2281
    /**
2282
     * Get available company fields for choices in the config UI.
2283
     *
2284
     * @param array $settings
2285
     *
2286
     * @return array
2287
     */
2288
    public function getFormCompanyFields($settings = [])
2289
    {
2290
        $settings['feature_settings']['objects']['company'] = 'company';
2291
2292
        return ($this->isAuthorized()) ? $this->getAvailableLeadFields($settings) : [];
2293
    }
2294
2295
    /**
2296
     * returns template to render on popup window after trying to run OAuth.
2297
     *
2298
     * @return string|null
2299
     */
2300
    public function getPostAuthTemplate()
2301
    {
2302
        return null;
2303
    }
2304
2305
    /**
2306
     * @param $contactId
2307
     *
2308
     * @return string
2309
     */
2310
    public function getContactTimelineLink($contactId)
2311
    {
2312
        return $this->router->generate(
2313
            'mautic_plugin_timeline_view',
2314
            ['integration' => $this->getName(), 'leadId' => $contactId],
2315
            UrlGeneratorInterface::ABSOLUTE_URL
2316
        );
2317
    }
2318
2319
    /**
2320
     * @param       $eventName
2321
     * @param array $keys
2322
     *
2323
     * @return array
2324
     */
2325
    protected function dispatchIntegrationKeyEvent($eventName, $keys = [])
2326
    {
2327
        /** @var PluginIntegrationKeyEvent $event */
2328
        $event = $this->dispatcher->dispatch(
2329
            $eventName,
2330
            new PluginIntegrationKeyEvent($this, $keys)
2331
        );
2332
2333
        return $event->getKeys();
2334
    }
2335
2336
    /**
2337
     * Cleans the identifier for api calls.
2338
     *
2339
     * @param mixed $identifier
2340
     *
2341
     * @return string
2342
     */
2343
    protected function cleanIdentifier($identifier)
2344
    {
2345
        if (is_array($identifier)) {
2346
            foreach ($identifier as &$i) {
2347
                $i = urlencode($i);
2348
            }
2349
        } else {
2350
            $identifier = urlencode($identifier);
2351
        }
2352
2353
        return $identifier;
2354
    }
2355
2356
    /**
2357
     * @param        $value
2358
     * @param string $fieldType
2359
     *
2360
     * @return bool|float|string
2361
     */
2362
    public function cleanPushData($value, $fieldType = self::FIELD_TYPE_STRING)
2363
    {
2364
        return Cleaner::clean($value, $fieldType);
2365
    }
2366
2367
    /**
2368
     * @return \Monolog\Logger|LoggerInterface
2369
     */
2370
    public function getLogger()
2371
    {
2372
        return $this->logger;
2373
    }
2374
2375
    /**
2376
     * @param                 $leadsToSync
2377
     * @param bool|\Exception $error
2378
     *
2379
     * @return int Number ignored due to being duplicates
2380
     *
2381
     * @throws ApiErrorException
2382
     * @throws \Exception
2383
     */
2384
    protected function cleanupFromSync(&$leadsToSync = [], $error = false)
2385
    {
2386
        $duplicates = 0;
2387
        if ($this->mauticDuplicates) {
2388
            // Create integration entities for these to be ignored until they are updated
2389
            foreach ($this->mauticDuplicates as $id => $dup) {
2390
                $this->persistIntegrationEntities[] = $this->createIntegrationEntity('Lead', null, $dup, $id, [], false);
2391
                ++$duplicates;
2392
            }
2393
2394
            $this->mauticDuplicates = [];
2395
        }
2396
2397
        $integrationEntityRepo = $this->getIntegrationEntityRepository();
2398
        if (!empty($leadsToSync)) {
2399
            // Let's only sync thos that have actual changes to prevent a loop
2400
            $integrationEntityRepo->saveEntities($leadsToSync);
2401
            $this->em->clear(Lead::class);
2402
            $leadsToSync = [];
2403
        }
2404
2405
        // Persist updated entities if applicable
2406
        if ($this->persistIntegrationEntities) {
2407
            $integrationEntityRepo->saveEntities($this->persistIntegrationEntities);
2408
            $this->persistIntegrationEntities = [];
2409
        }
2410
2411
        // If there are any deleted, mark it as so to prevent them from being queried over and over or recreated
2412
        if ($this->deleteIntegrationEntities) {
2413
            $integrationEntityRepo->deleteEntities($this->deleteIntegrationEntities);
2414
            $this->deleteIntegrationEntities = [];
2415
        }
2416
2417
        $this->em->clear(IntegrationEntity::class);
2418
2419
        if ($error) {
2420
            if ($error instanceof \Exception) {
2421
                throw $error;
2422
            }
2423
2424
            throw new ApiErrorException($error);
2425
        }
2426
2427
        return $duplicates;
2428
    }
2429
2430
    /**
2431
     * @param array $mapping           array of [$mauticId => ['entity' => FormEntity, 'integration_entity_id' => $integrationId]]
2432
     * @param       $integrationEntity
2433
     * @param       $internalEntity
2434
     * @param array $params
2435
     */
2436
    protected function buildIntegrationEntities(array $mapping, $integrationEntity, $internalEntity, $params = [])
2437
    {
2438
        $integrationEntityRepo = $this->getIntegrationEntityRepository();
2439
        $integrationEntities   = $integrationEntityRepo->getIntegrationEntities(
2440
            $this->getName(),
2441
            $integrationEntity,
2442
            $internalEntity,
2443
            array_keys($mapping)
2444
        );
2445
2446
        // Find those that don't exist and create them
2447
        $createThese = array_diff_key($mapping, $integrationEntities);
2448
2449
        foreach ($mapping as $internalEntityId => $entity) {
2450
            if (is_array($entity)) {
2451
                $integrationEntityId  = $entity['integration_entity_id'];
2452
                $internalEntityObject = $entity['entity'];
2453
            } else {
2454
                $integrationEntityId  = $entity;
2455
                $internalEntityObject = null;
2456
            }
2457
2458
            if (isset($createThese[$internalEntityId])) {
2459
                $entity = $this->createIntegrationEntity(
2460
                    $integrationEntity,
2461
                    $integrationEntityId,
2462
                    $internalEntity,
2463
                    $internalEntityId,
2464
                    [],
2465
                    false
2466
                );
2467
                $entity->setLastSyncDate($this->getLastSyncDate($internalEntityObject, $params, false));
2468
                $integrationEntities[$internalEntityId] = $entity;
2469
            } else {
2470
                $integrationEntities[$internalEntityId]->setLastSyncDate($this->getLastSyncDate($internalEntityObject, $params, false));
2471
            }
2472
        }
2473
2474
        $integrationEntityRepo->saveEntities($integrationEntities);
2475
        $this->em->clear(IntegrationEntity::class);
2476
    }
2477
2478
    /**
2479
     * @param null  $entity
2480
     * @param array $params
2481
     * @param bool  $ignoreEntityChanges
2482
     *
2483
     * @return bool|\DateTime|null
2484
     */
2485
    protected function getLastSyncDate($entity = null, $params = [], $ignoreEntityChanges = true)
2486
    {
2487
        $isNew = method_exists($entity, 'isNew') && $entity->isNew();
2488
        if (!$isNew && !$ignoreEntityChanges && isset($params['start']) && $entity && method_exists($entity, 'getChanges')) {
2489
            // Check to see if this contact was modified prior to the fetch so that the push catches it
2490
            /** @var FormEntity $entity */
2491
            $changes = $entity->getChanges(true);
2492
            if (empty($changes) || isset($changes['dateModified'])) {
2493
                $startSyncDate      = \DateTime::createFromFormat(\DateTime::ISO8601, $params['start']);
2494
                $entityDateModified = $entity->getDateModified();
2495
2496
                if (isset($changes['dateModified'])) {
2497
                    $originalDateModified = \DateTime::createFromFormat(\DateTime::ISO8601, $changes['dateModified'][0]);
2498
                } elseif ($entityDateModified) {
2499
                    $originalDateModified = $entityDateModified;
2500
                } else {
2501
                    $originalDateModified = $entity->getDateAdded();
2502
                }
2503
2504
                if ($originalDateModified >= $startSyncDate) {
2505
                    // Return null so that the push sync catches
2506
                    return null;
2507
                }
2508
            }
2509
        }
2510
2511
        return (defined('MAUTIC_DATE_MODIFIED_OVERRIDE')) ? \DateTime::createFromFormat('U', MAUTIC_DATE_MODIFIED_OVERRIDE)
2512
            : new \DateTime();
2513
    }
2514
2515
    /**
2516
     * @param      $fields
2517
     * @param      $keys
2518
     * @param null $object
2519
     *
2520
     * @return mixed
2521
     */
2522
    public function prepareFieldsForSync($fields, $keys, $object = null)
2523
    {
2524
        return $fields;
2525
    }
2526
2527
    /**
2528
     * Function used to format unformated fields coming from FieldsTypeTrait
2529
     * (usually used in campaign actions).
2530
     *
2531
     * @param $fields
2532
     *
2533
     * @return array
2534
     */
2535
    public function formatMatchedFields($fields)
2536
    {
2537
        $formattedFields = [];
2538
2539
        if (isset($fields['m_1'])) {
2540
            $xfields = count($fields) / 3;
2541
            for ($i = 1; $i < $xfields; ++$i) {
2542
                if (isset($fields['i_'.$i]) && isset($fields['m_'.$i])) {
2543
                    $formattedFields[$fields['i_'.$i]] = $fields['m_'.$i];
2544
                } else {
2545
                    continue;
2546
                }
2547
            }
2548
        }
2549
2550
        if (!empty($formattedFields)) {
2551
            $fields = $formattedFields;
2552
        }
2553
2554
        return $fields;
2555
    }
2556
2557
    /**
2558
     * @param        $leadId
2559
     * @param string $channel
2560
     *
2561
     * @return int
2562
     */
2563
    public function getLeadDoNotContact($leadId, $channel = 'email')
2564
    {
2565
        $isDoNotContact = 0;
2566
        if ($lead = $this->leadModel->getEntity($leadId)) {
2567
            $isContactableReason = $this->doNotContact->isContactable($lead, $channel);
2568
            if (DoNotContact::IS_CONTACTABLE !== $isContactableReason) {
2569
                $isDoNotContact = 1;
2570
            }
2571
        }
2572
2573
        return $isDoNotContact;
2574
    }
2575
2576
    /**
2577
     * Get pseudo fields from mautic, these are lead properties we want to map to integration fields.
2578
     *
2579
     * @param $lead
2580
     *
2581
     * @return mixed
2582
     */
2583
    public function getCompoundMauticFields($lead)
2584
    {
2585
        if ($lead['internal_entity_id']) {
2586
            $lead['mauticContactId']                   = $lead['internal_entity_id'];
2587
            $lead['mauticContactTimelineLink']         = $this->getContactTimelineLink($lead['internal_entity_id']);
2588
            $lead['mauticContactIsContactableByEmail'] = $this->getLeadDoNotContact($lead['internal_entity_id']);
2589
        }
2590
2591
        return $lead;
2592
    }
2593
2594
    /**
2595
     * @param $fieldName
2596
     *
2597
     * @return bool
2598
     */
2599
    public function isCompoundMauticField($fieldName)
2600
    {
2601
        $compoundFields = [
2602
            'mauticContactTimelineLink' => 'mauticContactTimelineLink',
2603
            'mauticContactId'           => 'mauticContactId',
2604
        ];
2605
2606
        if (true === $this->updateDncByDate()) {
2607
            $compoundFields['mauticContactIsContactableByEmail'] = 'mauticContactIsContactableByEmail';
2608
        }
2609
2610
        return isset($compoundFields[$fieldName]);
2611
    }
2612
2613
    /**
2614
     * Update the record in each system taking the last modified record.
2615
     *
2616
     * @param string $channel
2617
     *
2618
     * @return int
2619
     *
2620
     * @throws ApiErrorException
2621
     */
2622
    public function getLeadDoNotContactByDate($channel, $records, $object, $lead, $integrationData, $params = [])
2623
    {
2624
        return $records;
2625
    }
2626
}
2627