Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
/* Social Media */
.symbol-hashtag:before {
content: '#';
}
.shuffle-item.integration {
width: 100px;
}
.integration-disabled img {
filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale"); /* Firefox 10+ */
filter: gray; /* IE6-9 */
-webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */
-webkit-transition: all .6s ease; /* Fade to color for Chrome and Safari */
-webkit-backface-visibility: hidden; /* Fix for transition flickering */
}
.integration-disabled img:hover {
filter: url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 1 0\'/></filter></svg>#grayscale");
-webkit-filter: grayscale(0%);
}
.field-selector {
width: 500px;
}
.col-centered{
margin: 0 auto;
float: none;
}
.placeholder
{
position: relative;
}
.placeholder::after
{
position: absolute;
right: 3px;
top: 19px;
content: attr(data-placeholder);
pointer-events: none;
opacity: 0.3;
font-size: 9px
}
.integration-fields{
font-size: 12px;
padding-left: 7px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,322 @@
/* PluginBundle */
Mautic.matchedFields = function (index, object, integration) {
var compoundMauticFields = ['mauticContactId','mauticContactTimelineLink'];
if (mQuery('#integration_details_featureSettings_updateDncByDate_0').is(':checked')) {
compoundMauticFields.push('mauticContactIsContactableByEmail');
}
var integrationField = mQuery('#integration_details_featureSettings_'+object+'Fields_i_' + index).attr('data-value');
var mauticField = mQuery('#integration_details_featureSettings_'+object+'Fields_m_' + index + ' option:selected').val();
if(mQuery('.btn-arrow' + index).parent().attr('data-force-direction') != 1) {
if (mQuery.inArray(mauticField, compoundMauticFields) >= 0) {
mQuery('.btn-arrow' + index).removeClass('active');
mQuery('#integration_details_featureSettings_' + object + 'Fields_update_mautic' + index + '_0').attr('checked', 'checked');
mQuery('input[name="integration_details[featureSettings][' + object + 'Fields][update_mautic' + index + ']"]').prop('disabled', true).trigger("chosen:updated");
mQuery('.btn-arrow' + index).addClass('disabled');
}
else {
mQuery('input[name="integration_details[featureSettings][' + object + 'Fields][update_mautic' + index + ']"]').prop('disabled', false).trigger("chosen:updated");
mQuery('.btn-arrow' + index).removeClass('disabled');
}
}
if (object == 'lead') {
var updateMauticField = mQuery('input[name="integration_details[featureSettings]['+object+'Fields][update_mautic' + index + ']"]:checked').val();
} else {
var updateMauticField = mQuery('input[name="integration_details[featureSettings]['+object+'Fields][update_mautic_company' + index + ']"]:checked').val();
}
Mautic.ajaxActionRequest('plugin:matchFields', {object: object, integration: integration, integrationField : integrationField, mauticField: mauticField, updateMautic : updateMauticField}, function(response) {
var theMessage = (response.success) ? '<i class="ri-check-line-circle text-success"></i>' : '';
mQuery('#matched-' + index + "-" + object).html(theMessage);
});
};
Mautic.initiateIntegrationAuthorization = function() {
mQuery('#integration_details_in_auth').val(1);
Mautic.postForm(mQuery('form[name="integration_details"]'), 'loadIntegrationAuthWindow');
};
Mautic.loadIntegrationAuthWindow = function(response) {
if (response.newContent) {
Mautic.processModalContent(response, '#IntegrationEditModal');
} else {
Mautic.stopPageLoadingBar();
Mautic.stopIconSpinPostEvent();
mQuery('#integration_details_in_auth').val(0);
if (response.authUrl) {
var generator = window.open(response.authUrl, 'integrationauth', 'height=500,width=500');
if (!generator || generator.closed || typeof generator.closed == 'undefined') {
alert(mauticLang.popupBlockerMessage);
}
}
}
};
Mautic.refreshIntegrationForm = function() {
var opener = window.opener;
if(opener) {
var form = opener.mQuery('form[name="integration_details"]');
if (form.length) {
var action = form.attr('action');
if (action) {
opener.Mautic.startModalLoadingBar('#IntegrationEditModal');
opener.Mautic.loadAjaxModal('#IntegrationEditModal', action);
}
}
}
window.close()
};
Mautic.integrationOnLoad = function(container, response) {
if (response && response.name) {
var integration = '.integration-' + response.name;
if (response.enabled) {
mQuery(integration).removeClass('integration-disabled');
} else {
mQuery(integration).addClass('integration-disabled');
}
} else {
Mautic.filterIntegrations();
}
mQuery('[data-toggle="tooltip"]').tooltip();
};
Mautic.integrationConfigOnLoad = function(container) {
if (mQuery('.fields-container select.integration-field').length) {
var selects = mQuery('.fields-container select.integration-field');
selects.on('change', function() {
var select = mQuery(this),
newValue = select.val(),
previousValue = select.attr('data-value');
select.attr('data-value', newValue);
var groupSelects = mQuery(this).closest('.fields-container').find('select.integration-field').not(select);
// Enable old value
if (previousValue) {
mQuery('option[value="' + previousValue + '"]', groupSelects).each(function() {
if (!mQuery(this).closest('select').prop('disabled')) {
mQuery(this).prop('disabled', false);
mQuery(this).removeAttr('disabled');
}
});
}
if (newValue) {
mQuery('option[value="' + newValue + '"]', groupSelects).each(function() {
if (!mQuery(this).closest('select').prop('disabled')) {
mQuery(this).prop('disabled', true);
mQuery(this).attr('disabled', 'disabled');
}
});
}
groupSelects.each(function() {
mQuery(this).trigger('chosen:updated');
});
});
selects.each(function() {
if (!mQuery(this).closest('.field-container').hasClass('hide')) {
mQuery(this).trigger('change');
}
});
}
};
Mautic.filterIntegrations = function(update) {
var filter = mQuery('#integrationFilter').val();
if (update) {
mQuery.ajax({
url: mauticAjaxUrl,
type: "POST",
data: "action=plugin:setIntegrationFilter&plugin=" + filter
});
}
//activate shuffles
if (mQuery('.native-integrations').length) {
//give a slight delay in order for images to load so that shuffle starts out with correct dimensions
setTimeout(function () {
var Shuffle = window.Shuffle,
element = document.querySelector('.native-integrations'),
shuffleOptions = {
itemSelector: '.shuffle-item'
};
// Using global variable to make it available outside of the scope of this function
window.nativeIntegrationsShuffleInstance = new Shuffle(element, shuffleOptions);
window.nativeIntegrationsShuffleInstance.filter(function($el) {
if (filter) {
return mQuery($el).hasClass('plugin' + filter);
} else {
// Shuffle.js has a bug. It hides the first item when we reset the filter.
// This fixes it.
mQuery(shuffleOptions.itemSelector).first().css('transform', '');
return true;
}
});
// Update shuffle on sidebar minimize/maximize
mQuery("html")
.on("fa.sidebar.minimize", function() {
setTimeout(function() {
window.nativeIntegrationsShuffleInstance.update();
}, 1000);
})
.on("fa.sidebar.maximize", function() {
setTimeout(function() {
window.nativeIntegrationsShuffleInstance.update();
}, 1000);
});
// This delay is needed so that the tab has time to render and the sizes are correctly calculated
mQuery('#plugin-nav-tabs a').click(function () {
setTimeout(function() {
window.nativeIntegrationsShuffleInstance.update();
}, 500);
});
}, 500);
}
};
Mautic.getIntegrationLeadFields = function (integration, el, settings) {
if (typeof settings == 'undefined') {
settings = {};
}
settings.integration = integration;
settings.object = 'lead';
Mautic.getIntegrationFields(settings, 1, el);
};
Mautic.getIntegrationCompanyFields = function (integration, el, settings) {
if (typeof settings == 'undefined') {
settings = {};
}
settings.integration = integration;
settings.object = 'company';
Mautic.getIntegrationFields(settings, 1, el);
};
Mautic.getIntegrationFields = function(settings, page, el) {
var object = settings.object ? settings.object : 'lead';
var fieldsTab = ('lead' === object) ? '#fields-tab' : '#'+object+'-fields-container';
if (el && mQuery(el).is('input')) {
Mautic.activateLabelLoadingIndicator(mQuery(el).attr('id'));
var namePrefix = mQuery(el).attr('name').split('[')[0];
if ('integration_details' !== namePrefix) {
var nameParts = mQuery(el).attr('name').match(/\[.*?\]+/g);
nameParts = nameParts.slice(0, -1);
settings.prefix = namePrefix + nameParts.join('') + "[" + object + "Fields]";
}
}
var fieldsContainer = '#'+object+'FieldsContainer';
var inModal = mQuery(fieldsContainer).closest('.modal');
if (inModal) {
var modalId = '#'+mQuery(fieldsContainer).closest('.modal').attr('id');
Mautic.startModalLoadingBar(modalId);
}
Mautic.ajaxActionRequest('plugin:getIntegrationFields',
{
page: page,
integration: (settings.integration) ? settings.integration : null,
settings: settings
},
function(response) {
if (response.success) {
mQuery(fieldsContainer).replaceWith(response.html);
Mautic.onPageLoad(fieldsContainer);
Mautic.integrationConfigOnLoad(fieldsContainer);
if (mQuery(fieldsTab).length) {
mQuery(fieldsTab).removeClass('hide');
}
} else {
if (mQuery(fieldsTab).length) {
mQuery(fieldsTab).addClass('hide');
}
}
if (el) {
Mautic.removeLabelLoadingIndicator();
}
if (inModal) {
Mautic.stopModalLoadingBar(modalId);
}
}
);
};
Mautic.getIntegrationConfig = function (el, settings) {
Mautic.activateLabelLoadingIndicator(mQuery(el).attr('id'));
if (typeof settings == 'undefined') {
settings = {};
}
settings.name = mQuery(el).attr('name');
var data = {integration: mQuery(el).val(), settings: settings};
mQuery('.integration-campaigns-status').html('');
mQuery('.integration-config-container').html('');
Mautic.ajaxActionRequest('plugin:getIntegrationConfig', data,
function (response) {
if (response.success) {
mQuery('.integration-config-container').html(response.html);
Mautic.onPageLoad('.integration-config-container', response);
}
Mautic.integrationConfigOnLoad('.integration-config-container');
Mautic.removeLabelLoadingIndicator();
},
false,
false,
"GET"
);
};
Mautic.getIntegrationCampaignStatus = function (el, settings) {
Mautic.activateLabelLoadingIndicator(mQuery(el).attr('id'));
if (typeof settings == 'undefined') {
settings = {};
}
// Extract the name and ID prefixes
var prefix = mQuery(el).attr('name').split("[")[0];
settings.name = mQuery('#'+prefix+'_properties_integration').attr('name');
var data = {integration:mQuery('#'+prefix+'_properties_integration').val(),campaign: mQuery(el).val(), settings: settings};
mQuery('.integration-campaigns-status').html('');
mQuery('.integration-campaigns-status').removeClass('hide');
Mautic.ajaxActionRequest('plugin:getIntegrationCampaignStatus', data,
function (response) {
if (response.success) {
mQuery('.integration-campaigns-status').append(response.html);
Mautic.onPageLoad('.integration-campaigns-status', response);
}
Mautic.integrationConfigOnLoad('.integration-campaigns-status');
Mautic.removeLabelLoadingIndicator();
},
false,
false,
"GET"
);
};

View File

@@ -0,0 +1,12 @@
<?php
namespace Mautic\PluginBundle\Bundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Base Bundle class which should be extended by addon bundles.
*/
abstract class PluginBundleBase extends Bundle
{
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Bundle;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Tools\SchemaTool;
use Mautic\IntegrationsBundle\Migration\Engine;
use Mautic\PluginBundle\Entity\Plugin;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class PluginDatabase
{
private readonly string $mauticDbPrefix;
public function __construct(
private readonly EntityManagerInterface $em,
private readonly Connection $connection,
#[Autowire(env: 'MAUTIC_TABLE_PREFIX')]
?string $mauticDbPrefix,
) {
$this->mauticDbPrefix = $mauticDbPrefix ?? '';
}
/**
* Install plugin schema based on Doctrine metadata.
*
* @param array<class-string, ClassMetadata> $metadata
*
* @throws \Exception
*/
public function installPluginSchema(array $metadata, ?bool $installedSchema = null): void
{
if (null !== $installedSchema) {
// Schema already exists, so no need to proceed
return;
}
$schemaTool = new SchemaTool($this->em);
$installQueries = $schemaTool->getCreateSchemaSql(array_values($metadata));
$connection = $this->connection;
foreach ($installQueries as $q) {
// Check if the query is a DDL statement
if (self::isDDLStatement($q)) {
// Execute DDL statements outside of a transaction
$connection->executeStatement($q);
} else {
// For non-DDL statements, use transactions
try {
$connection->beginTransaction();
$connection->executeStatement($q);
$connection->commit();
} catch (\Exception $e) {
// Rollback only for non-DDL statements
if ($connection->isTransactionActive()) {
$connection->rollBack();
}
throw $e;
}
}
}
}
/**
* @throws \Exception
*/
public function onPluginUpdate(Plugin $plugin): void
{
$migrationEngine = new Engine(
$this->em,
$this->mauticDbPrefix,
__DIR__.'/../../../../plugins/'.$plugin->getBundle(),
$plugin->getBundle()
);
$migrationEngine->up();
}
/**
* @param array<int, ClassMetadata> $metadata
*/
public function dropPluginSchema(array $metadata): void
{
$db = $this->em->getConnection();
$schemaTool = new SchemaTool($this->em);
$dropQueries = $schemaTool->getDropSchemaSQL($metadata);
$db->beginTransaction();
try {
foreach ($dropQueries as $q) {
$db->executeStatement($q);
}
$db->commit();
} catch (\Exception $e) {
$db->rollback();
throw $e;
}
}
private static function isDDLStatement(string $query): bool|int
{
return preg_match('/^(CREATE|ALTER|DROP|RENAME|TRUNCATE|COMMENT)\s/i', $query);
}
}

View File

@@ -0,0 +1,287 @@
<?php
namespace Mautic\PluginBundle\Command;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:integration:fetchleads',
description: 'Fetch leads from integration.',
aliases: [
'mautic:integration:synccontacts',
]
)]
class FetchLeadsCommand extends Command
{
public function __construct(
private TranslatorInterface $translator,
private IntegrationHelper $integrationHelper,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption(
'--integration',
'-i',
InputOption::VALUE_REQUIRED,
'Fetch leads from integration. Integration must be enabled and authorised.',
null
)
->addOption('--start-date', '-d', InputOption::VALUE_REQUIRED, 'Set start date for updated values.')
->addOption(
'--end-date',
'-t',
InputOption::VALUE_REQUIRED,
'Set end date for updated values.'
)
->addOption(
'--fetch-all',
null,
InputOption::VALUE_NONE,
'Get all CRM contacts whatever the date is. Should be used at instance initialization only'
)
->addOption(
'--time-interval',
'-a',
InputOption::VALUE_OPTIONAL,
'Send time interval to check updates on Salesforce, it should be a correct php formatted time interval in the past eg:(10 minutes)'
)
->addOption(
'--limit',
'-l',
InputOption::VALUE_OPTIONAL,
'Number of records to process when syncing objects',
100
)
->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$integration = $input->getOption('integration');
$startDate = $input->getOption('start-date');
$endDate = $input->getOption('end-date');
$interval = $input->getOption('time-interval');
$limit = $input->getOption('limit');
$fetchAll = $input->getOption('fetch-all');
$leadsExecuted = $contactsExecuted = null;
// @TODO Since integration is mandatory it should really be turned into an agument, but that would not be B.C.
if (!$integration) {
throw new \RuntimeException('An integration must be specified');
}
$integrationObject = $this->integrationHelper->getIntegrationObject($integration);
if (!$integrationObject instanceof UnifiedIntegrationInterface) {
$availableIntegrations = array_filter($this->integrationHelper->getIntegrationObjects(),
fn (UnifiedIntegrationInterface $availableIntegration) => $availableIntegration->isConfigured());
throw new \RuntimeException(sprintf('The Integration "%s" is not one of the available integrations (%s)', $integration, implode(', ', array_keys($availableIntegrations))));
}
if (!$interval) {
$interval = '15 minutes';
}
$startDate = !$startDate ? date('c', strtotime('-'.$interval)) : date('c', strtotime($startDate));
$endDate = !$endDate ? date('c') : date('c', strtotime($endDate));
if (!$endDate) {
$output->writeln(sprintf('<info>Invalid date rage given %s -> %s</info>', $startDate, $endDate));
return 255;
}
$integrationObject = $this->integrationHelper->getIntegrationObject($integration);
if (!$integrationObject->isAuthorized()) {
$output->writeln(sprintf('<error>ERROR:</error> <info>'.$this->translator->trans('mautic.plugin.command.notauthorized').'</info>', $integration));
return 255;
}
// Tell audit log to use integration name
define('MAUTIC_AUDITLOG_USER', $integration);
$config = $integrationObject->mergeConfigToFeatureSettings();
$supportedFeatures = $integrationObject->getIntegrationSettings()->getSupportedFeatures();
defined('MAUTIC_CONSOLE_VERBOSITY') or define('MAUTIC_CONSOLE_VERBOSITY', $output->getVerbosity());
if (!isset($config['objects'])) {
$config['objects'] = [];
}
$params['start'] = $startDate;
$params['end'] = $endDate;
$params['limit'] = $limit;
$params['fetchAll'] = $fetchAll;
$params['output'] = $output;
$integrationObject->setCommandParameters($params);
// set this constant to ensure that all contacts have the same date modified time and date synced time to prevent a pull/push loop
define('MAUTIC_DATE_MODIFIED_OVERRIDE', time());
if (isset($supportedFeatures) && in_array('get_leads', $supportedFeatures)) {
if (null !== $integrationObject && method_exists($integrationObject, 'getLeads') && isset($config['objects'])) {
$output->writeln('<info>'.$this->translator->trans('mautic.plugin.command.fetch.leads', ['%integration%' => $integration]).'</info>');
$output->writeln('<comment>'.$this->translator->trans('mautic.plugin.command.fetch.leads.starting').'</comment>');
// Handle case when integration object are named "Contacts" and "Leads"
$leadObjectName = 'Lead';
if (in_array('Leads', $config['objects'])) {
$leadObjectName = 'Leads';
}
$contactObjectName = 'Contact';
if (in_array(strtolower('Contacts'), array_map(fn ($i): string => strtolower($i), $config['objects']), true)) {
$contactObjectName = 'Contacts';
}
$updated = $created = $processed = 0;
if (in_array($leadObjectName, $config['objects'])) {
$leadList = [];
$results = $integrationObject->getLeads($params, null, $leadsExecuted, $leadList, $leadObjectName);
if (is_array($results)) {
[$justUpdated, $justCreated] = $results;
$updated += (int) $justUpdated;
$created += (int) $justCreated;
} else {
$processed += (int) $results;
}
}
if (in_array(strtolower($contactObjectName), array_map(fn ($i): string => strtolower($i), $config['objects']), true)) {
$output->writeln('');
$output->writeln('<comment>'.$this->translator->trans('mautic.plugin.command.fetch.contacts.starting').'</comment>');
$contactList = [];
$results = $integrationObject->getLeads($params, null, $contactsExecuted, $contactList, $contactObjectName);
if (is_array($results)) {
[$justUpdated, $justCreated] = $results;
$updated += (int) $justUpdated;
$created += (int) $justCreated;
} else {
$processed += (int) $results;
}
}
$output->writeln('');
if ($processed) {
$output->writeln(
'<comment>'.$this->translator->trans('mautic.plugin.command.fetch.leads.events_executed', ['%events%' => $processed])
.'</comment>'."\n"
);
} else {
$output->writeln(
'<comment>'.$this->translator->trans(
'mautic.plugin.command.fetch.leads.events_executed_breakout',
['%updated%' => $updated, '%created%' => $created]
)
.'</comment>'."\n"
);
}
}
if (null !== $integrationObject && method_exists($integrationObject, 'getCompanies') && isset($config['objects'])
&& in_array(
'company',
$config['objects']
)
) {
$updated = $created = $processed = 0;
$output->writeln('<info>'.$this->translator->trans('mautic.plugin.command.fetch.companies', ['%integration%' => $integration]).'</info>');
$output->writeln('<comment>'.$this->translator->trans('mautic.plugin.command.fetch.companies.starting').'</comment>');
$results = $integrationObject->getCompanies($params);
if (is_array($results)) {
[$justUpdated, $justCreated] = $results;
$updated += (int) $justUpdated;
$created += (int) $justCreated;
} else {
$processed += (int) $results;
}
$output->writeln('');
if ($processed) {
$output->writeln(
'<comment>'.$this->translator->trans('mautic.plugin.command.fetch.companies.events_executed', ['%events%' => $processed])
.'</comment>'."\n"
);
} else {
$output->writeln(
'<comment>'.$this->translator->trans(
'mautic.plugin.command.fetch.companies.events_executed_breakout',
['%updated%' => $updated, '%created%' => $created]
)
.'</comment>'."\n"
);
}
}
}
if (isset($supportedFeatures) && in_array('push_leads', $supportedFeatures) && method_exists($integrationObject, 'pushLeads')) {
$output->writeln('<info>'.$this->translator->trans('mautic.plugin.command.pushing.leads', ['%integration%' => $integration]).'</info>');
$result = $integrationObject->pushLeads($params);
$ignored = 0;
if (4 === count($result)) {
[$updated, $created, $errored, $ignored] = $result;
} elseif (3 === count($result)) {
[$updated, $created, $errored] = $result;
} else {
$errored = '?';
[$updated, $created] = $result;
}
$output->writeln(
'<comment>'.$this->translator->trans(
'mautic.plugin.command.fetch.pushing.leads.events_executed',
[
'%updated%' => $updated,
'%created%' => $created,
'%errored%' => $errored,
'%ignored%' => $ignored,
]
)
.'</comment>'."\n"
);
if (in_array('push_companies', $supportedFeatures) && method_exists($integrationObject, 'pushCompanies')) {
$output->writeln('<info>'.$this->translator->trans('mautic.plugin.command.pushing.companies', ['%integration%' => $integration]).'</info>');
$result = $integrationObject->pushCompanies($params);
$ignored = 0;
if (4 === count($result)) {
[$updated, $created, $errored, $ignored] = $result;
} elseif (3 === count($result)) {
[$updated, $created, $errored] = $result;
} else {
$errored = '?';
[$updated, $created] = $result;
}
$output->writeln(
'<comment>'.$this->translator->trans(
'mautic.plugin.command.fetch.pushing.companies.events_executed',
[
'%updated%' => $updated,
'%created%' => $created,
'%errored%' => $errored,
'%ignored%' => $ignored,
]
)
.'</comment>'."\n"
);
}
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Mautic\PluginBundle\Command;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:integration:pushleadactivity',
description: 'Push lead activity to integration.',
aliases: [
'mautic:integration:pushactivity',
]
)]
class PushLeadActivityCommand extends Command
{
public function __construct(
private TranslatorInterface $translator,
private IntegrationHelper $integrationHelper,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption(
'--integration',
'-i',
InputOption::VALUE_REQUIRED,
'Integration name. Integration must be enabled and authorised.',
null
)
->addOption('--start-date', '-d', InputOption::VALUE_REQUIRED, 'Set start date for updated values.')
->addOption(
'--end-date',
'-t',
InputOption::VALUE_REQUIRED,
'Set end date for updated values.'
)
->addOption(
'--time-interval',
'-a',
InputOption::VALUE_OPTIONAL,
'Send time interval to check updates on Salesforce, it should be a correct php formatted time interval in the past eg:(-10 minutes)'
)
->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$integration = $input->getOption('integration');
$startDate = $input->getOption('start-date');
$endDate = $input->getOption('end-date');
$interval = $input->getOption('time-interval');
if (!$interval) {
$interval = '15 minutes';
}
if (!$startDate) {
$startDate = date('c', strtotime('-'.$interval));
}
if (!$endDate) {
$endDate = date('c');
}
if ($integration) {
$integrationObject = $this->integrationHelper->getIntegrationObject($integration);
if (null !== $integrationObject && method_exists($integrationObject, 'pushLeadActivity')) {
$output->writeln('<info>'.$this->translator->trans('mautic.plugin.command.push.leads.activity', ['%integration%' => $integration]).'</info>');
$params['start'] = $startDate;
$params['end'] = $endDate;
$processed = intval($integrationObject->pushLeadActivity($params));
$output->writeln('<comment>'.$this->translator->trans('mautic.plugin.command.push.leads.events_executed', ['%events%' => $processed]).'</comment>'."\n");
}
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\PluginBundle\Command;
use Mautic\PluginBundle\Facade\ReloadFacade;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'mautic:plugins:reload',
description: 'Installs, updates, enable and/or disable plugins.',
aliases: [
'mautic:plugins:install',
'mautic:plugins:update',
]
)]
class ReloadCommand extends Command
{
public function __construct(
private ReloadFacade $reloadFacade,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeLn($this->reloadFacade->reloadPlugins());
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,93 @@
<?php
return [
'routes' => [
'main' => [
'mautic_integration_auth_callback_secure' => [
'path' => '/plugins/integrations/authcallback/{integration}',
'controller' => 'Mautic\PluginBundle\Controller\AuthController::authCallbackAction',
],
'mautic_integration_auth_postauth_secure' => [
'path' => '/plugins/integrations/authstatus/{integration}',
'controller' => 'Mautic\PluginBundle\Controller\AuthController::authStatusAction',
],
'mautic_plugin_index' => [
'path' => '/plugins',
'controller' => 'Mautic\PluginBundle\Controller\PluginController::indexAction',
],
'mautic_plugin_config' => [
'path' => '/plugins/config/{name}/{page}',
'controller' => 'Mautic\PluginBundle\Controller\PluginController::configAction',
],
'mautic_plugin_info' => [
'path' => '/plugins/info/{name}',
'controller' => 'Mautic\PluginBundle\Controller\PluginController::infoAction',
],
'mautic_plugin_reload' => [
'path' => '/plugins/reload',
'controller' => 'Mautic\PluginBundle\Controller\PluginController::reloadAction',
],
],
'public' => [
'mautic_integration_auth_user' => [
'path' => '/plugins/integrations/authuser/{integration}',
'controller' => 'Mautic\PluginBundle\Controller\AuthController::authUserAction',
],
'mautic_integration_auth_callback' => [
'path' => '/plugins/integrations/authcallback/{integration}',
'controller' => 'Mautic\PluginBundle\Controller\AuthController::authCallbackAction',
],
'mautic_integration_auth_postauth' => [
'path' => '/plugins/integrations/authstatus/{integration}',
'controller' => 'Mautic\PluginBundle\Controller\AuthController::authStatusAction',
],
],
],
'menu' => [
'admin' => [
'priority' => 50,
'items' => [
'mautic.plugin.plugins' => [
'id' => 'mautic_plugin_root',
'access' => 'plugin:plugins:manage',
'route' => 'mautic_plugin_index',
'parent' => 'mautic.core.integrations',
'iconClass' => 'ri-plug-line',
],
],
],
],
'services' => [
'other' => [
'mautic.helper.integration' => [
'class' => Mautic\PluginBundle\Helper\IntegrationHelper::class,
'arguments' => [
'service_container',
'doctrine.orm.entity_manager',
'mautic.helper.paths',
'mautic.helper.bundle',
'mautic.helper.core_parameters',
'twig',
'mautic.plugin.model.plugin',
],
],
'mautic.plugin.helper.reload' => [
'class' => Mautic\PluginBundle\Helper\ReloadHelper::class,
'arguments' => [
'event_dispatcher',
],
],
],
'facades' => [
'mautic.plugin.facade.reload' => [
'class' => Mautic\PluginBundle\Facade\ReloadFacade::class,
'arguments' => [
'mautic.plugin.model.plugin',
'mautic.plugin.helper.reload',
'translator',
],
],
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
use Mautic\PluginBundle\EventListener\CampaignSubscriber;
use Mautic\PluginBundle\EventListener\FormSubscriber;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
return function (ContainerConfigurator $configurator): void {
$services = $configurator->services()
->defaults()
->autowire()
->autoconfigure()
->public();
$excludes = [
'Helper/oAuthHelper.php',
'Integration/IntegrationObject.php',
];
$services->load('Mautic\\PluginBundle\\', '../')
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
$services->load('Mautic\\PluginBundle\\Entity\\', '../Entity/*Repository.php');
$services->alias('mautic.plugin.model.plugin', Mautic\PluginBundle\Model\PluginModel::class);
$services->alias('mautic.plugin.model.integration_entity', Mautic\PluginBundle\Model\IntegrationEntityModel::class);
$services->set(FormSubscriber::class)
->call('setIntegrationHelper', [service('mautic.helper.integration')]);
$services->set(CampaignSubscriber::class)
->call('setIntegrationHelper', [service('mautic.helper.integration')]);
};

View File

@@ -0,0 +1,271 @@
<?php
namespace Mautic\PluginBundle\Controller;
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
use Mautic\PluginBundle\Form\Type\CompanyFieldsType;
use Mautic\PluginBundle\Form\Type\FieldsType;
use Mautic\PluginBundle\Form\Type\IntegrationCampaignsType;
use Mautic\PluginBundle\Form\Type\IntegrationConfigType;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Model\PluginModel;
use Symfony\Component\HttpFoundation\Request;
class AjaxController extends CommonAjaxController
{
public function setIntegrationFilterAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
{
$session = $request->getSession();
$pluginFilter = (int) $request->get('plugin');
$session->set('mautic.integrations.filter', $pluginFilter);
return $this->sendJsonResponse(['success' => 1]);
}
/**
* Get the HTML for list of fields.
*/
public function getIntegrationFieldsAction(Request $request, IntegrationHelper $helper): \Symfony\Component\HttpFoundation\JsonResponse
{
$integration = $request->query->get('integration');
$settings = $request->query->all()['settings'] ?? [];
$page = $request->query->get('page');
$dataArray = ['success' => 0];
if (!empty($integration) && !empty($settings)) {
/** @var \Mautic\PluginBundle\Integration\AbstractIntegration $integrationObject */
$integrationObject = $helper->getIntegrationObject($integration);
if ($integrationObject) {
if (!$object = $request->attributes->get('object')) {
$object = $settings['object'] ?? 'lead';
}
$isLead = ('lead' === $object);
$integrationFields = ($isLead)
? $integrationObject->getFormLeadFields($settings)
: $integrationObject->getFormCompanyFields(
$settings
);
if (!empty($integrationFields)) {
$session = $request->getSession();
$session->set('mautic.plugin.'.$integration.'.'.$object.'.page', $page);
/** @var PluginModel $pluginModel */
$pluginModel = $this->getModel('plugin');
// Get a list of custom form fields
$mauticFields = ($isLead) ? $pluginModel->getLeadFields() : $pluginModel->getCompanyFields();
$featureSettings = $integrationObject->getIntegrationSettings()->getFeatureSettings();
$enableDataPriority = $integrationObject->getDataPriority();
$formType = $isLead ? 'integration_fields' : 'integration_company_fields';
$form = $this->createForm(
$isLead ? FieldsType::class : CompanyFieldsType::class,
$featureSettings[$object.'Fields'] ?? [],
[
'mautic_fields' => $mauticFields,
'data' => $featureSettings,
'integration_fields' => $integrationFields,
'csrf_protection' => false,
'integration_object' => $integrationObject,
'enable_data_priority' => $enableDataPriority,
'integration' => $integration,
'page' => $page,
'limit' => $this->coreParametersHelper->get('default_pagelimit'),
]
);
$html = $this->render('@MauticCore/Helper/blank_form.html.twig', [
'form' => $form->createView(),
'formTheme' => '@MauticPlugin/FormTheme/Integration/layout.html.twig',
'function' => 'row',
]
)->getContent();
if (!isset($settings['prefix'])) {
$prefix = 'integration_details[featureSettings]['.$object.'Fields]';
} else {
$prefix = $settings['prefix'];
}
$idPrefix = str_replace(['][', '[', ']'], '_', $prefix);
if (str_ends_with($idPrefix, '_')) {
$idPrefix = substr($idPrefix, 0, -1);
}
$html = preg_replace('/'.$form->getName().'\[(.*?)\]/', $prefix.'[$1]', $html);
$html = str_replace($form->getName(), $idPrefix, $html);
$dataArray['success'] = 1;
$dataArray['html'] = $html;
}
}
}
return $this->sendJsonResponse($dataArray);
}
/**
* Get the HTML for integration properties.
*/
public function getIntegrationConfigAction(Request $request, IntegrationHelper $integrationHelper): \Symfony\Component\HttpFoundation\JsonResponse
{
$integration = $request->query->get('integration');
$settings = $request->query->all()['settings'] ?? [];
$dataArray = ['success' => 0];
if (!empty($integration) && !empty($settings)) {
/** @var \Mautic\PluginBundle\Integration\AbstractIntegration $object */
$object = $integrationHelper->getIntegrationObject($integration);
if ($object) {
$data = $statusData = [];
$objectSettings = $object->getIntegrationSettings();
$defaults = $objectSettings->getFeatureSettings();
if (method_exists($object, 'getCampaigns')) {
$campaigns = $object->getCampaigns();
if (isset($campaigns['records']) && !empty($campaigns['records'])) {
foreach ($campaigns['records'] as $campaign) {
$data[$campaign['Id']] = $campaign['Name'];
}
}
}
$form = $this->createForm(IntegrationConfigType::class, $defaults, [
'integration' => $object,
'csrf_protection' => false,
'campaigns' => $data,
]);
$html = $this->render('@MauticCore/Helper/blank_form.html.twig', [
'form' => $form->createView(),
'function' => 'widget',
'formTheme' => '@MauticPlugin/FormTheme/Integration/layout.html.twig',
'variables' => [
'integration' => $object,
],
])->getContent();
$prefix = str_replace('[integration]', '[config]', $settings['name']);
$idPrefix = str_replace(['][', '[', ']'], '_', $prefix);
if (str_ends_with($idPrefix, '_')) {
$idPrefix = substr($idPrefix, 0, -1);
}
$html = preg_replace('/integration_config\[(.*?)\]/', $prefix.'[$1]', $html);
$html = str_replace('integration_config', $idPrefix, $html);
$dataArray['success'] = 1;
$dataArray['html'] = $html;
}
}
return $this->sendJsonResponse($dataArray);
}
public function getIntegrationCampaignStatusAction(Request $request, IntegrationHelper $integrationHelper): \Symfony\Component\HttpFoundation\JsonResponse
{
$integration = $request->query->get('integration');
$campaign = $request->query->get('campaign');
$settings = $request->query->all()['settings'] ?? [];
$dataArray = ['success' => 0];
$statusData = [];
if (!empty($integration) && !empty($campaign)) {
/** @var \Mautic\PluginBundle\Integration\AbstractIntegration $object */
$object = $integrationHelper->getIntegrationObject($integration);
if ($object) {
if (method_exists($object, 'getCampaignMemberStatus')) {
$campaignMemberStatus = $object->getCampaignMemberStatus($campaign);
if (isset($campaignMemberStatus['records']) && !empty($campaignMemberStatus['records'])) {
foreach ($campaignMemberStatus['records'] as $status) {
$statusData[$status['Label']] = $status['Label'];
}
}
}
$form = $this->createForm(IntegrationCampaignsType::class, $statusData, [
'csrf_protection' => false,
'campaignContactStatus' => $statusData,
]);
$html = $this->render('@MauticCore/Helper/blank_form.html.twig', [
'form' => $form->createView(),
'formTheme' => '@MauticPlugin/FormTheme/Integration/layout.html.twig',
'function' => 'widget',
'variables' => [
'integration' => $object,
],
])->getContent();
$prefix = str_replace('[integration]', '[campaign_member_status][campaign_member_status]', $settings['name']);
$idPrefix = str_replace(['][', '[', ']'], '_', $prefix);
if (str_ends_with($idPrefix, '_')) {
$idPrefix = substr($idPrefix, 0, -1);
}
$html = preg_replace('/integration_campaign_status_campaign_member_status\[(.*?)\]/', $prefix.'[$1]', $html);
$html = str_replace('integration_campaign_status_campaign_member_status', $idPrefix, $html);
$html = str_replace('integration_campaign_status[campaign_member_status]', $prefix, $html);
$dataArray['success'] = 1;
$dataArray['html'] = $html;
}
}
return $this->sendJsonResponse($dataArray);
}
public function matchFieldsAction(Request $request, IntegrationHelper $integrationHelper): \Symfony\Component\HttpFoundation\JsonResponse
{
$integration = $request->request->get('integration');
$integration_field = $request->request->get('integrationField');
$mautic_field = $request->request->get('mauticField');
$update_mautic = $request->request->get('updateMautic');
$object = $request->request->get('object');
$integration_object = $integrationHelper->getIntegrationObject($integration);
$entity = $integration_object->getIntegrationSettings();
$featureSettings = $entity->getFeatureSettings();
$doNotMatchField = ('-1' === $mautic_field || '' === $mautic_field);
if ('lead' == $object) {
$fields = 'leadFields';
$updateFields = 'update_mautic';
} else {
$fields = 'companyFields';
$updateFields = 'update_mautic_company';
}
$newFeatureSettings = [];
if ($doNotMatchField) {
if (isset($featureSettings[$updateFields]) && array_key_exists($integration_field, $featureSettings[$updateFields])) {
unset($featureSettings[$updateFields][$integration_field]);
}
if (isset($featureSettings[$fields]) && array_key_exists($integration_field, $featureSettings[$fields])) {
unset($featureSettings[$fields][$integration_field]);
}
$dataArray = ['success' => 0];
} else {
$newFeatureSettings[$integration_field] = $update_mautic;
if (isset($featureSettings[$updateFields])) {
$featureSettings[$updateFields] = array_merge($featureSettings[$updateFields], $newFeatureSettings);
} else {
$featureSettings[$updateFields] = $newFeatureSettings;
}
$newFeatureSettings[$integration_field] = $mautic_field;
if (isset($featureSettings[$fields])) {
$featureSettings[$fields] = array_merge($featureSettings[$fields], $newFeatureSettings);
} else {
$featureSettings[$fields] = $newFeatureSettings;
}
$dataArray = ['success' => 1];
}
$entity->setFeatureSettings($featureSettings);
$pluginModel = $this->getModel('plugin');
\assert($pluginModel instanceof PluginModel);
$pluginModel->saveFeatureSettings($entity);
return $this->sendJsonResponse($dataArray);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Mautic\PluginBundle\Controller;
use Mautic\CoreBundle\Controller\FormController;
use Mautic\PluginBundle\Event\PluginIntegrationAuthRedirectEvent;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
class AuthController extends FormController
{
/**
* @param string $integration
*
* @return JsonResponse
*/
public function authCallbackAction(Request $request, IntegrationHelper $integrationHelper, $integration): JsonResponse|RedirectResponse
{
$isAjax = $request->isXmlHttpRequest();
$session = $request->getSession();
$integrationObject = $integrationHelper->getIntegrationObject($integration);
// check to see if the service exists
if (!$integrationObject) {
$session->set('mautic.integration.postauth.message', ['mautic.integration.notfound', ['%name%' => $integration], 'error']);
if ($isAjax) {
return new JsonResponse(['url' => $this->generateUrl('mautic_integration_auth_postauth', ['integration' => $integration])]);
} else {
return new RedirectResponse($this->generateUrl('mautic_integration_auth_postauth', ['integration' => $integration]));
}
}
try {
$error = $integrationObject->authCallback();
} catch (\InvalidArgumentException $e) {
$session->set('mautic.integration.postauth.message', [$e->getMessage(), [], 'error']);
$redirectUrl = $this->generateUrl('mautic_integration_auth_postauth', ['integration' => $integration]);
if ($isAjax) {
return new JsonResponse(['url' => $redirectUrl]);
} else {
return new RedirectResponse($redirectUrl);
}
}
// check for error
if ($error) {
$type = 'error';
$message = 'mautic.integration.error.oauthfail';
$params = ['%error%' => $error];
} else {
$type = 'notice';
$message = 'mautic.integration.notice.oauthsuccess';
$params = [];
}
$session->set('mautic.integration.postauth.message', [$message, $params, $type]);
$identifier[$integration] = null;
$socialCache = [];
$userData = $integrationObject->getUserData($identifier, $socialCache);
$session->set('mautic.integration.'.$integration.'.userdata', $userData);
return new RedirectResponse($this->generateUrl('mautic_integration_auth_postauth', ['integration' => $integration]));
}
public function authStatusAction(Request $request, $integration): \Symfony\Component\HttpFoundation\Response
{
$postAuthTemplate = '@MauticPlugin/Auth/postauth.html.twig';
$session = $request->getSession();
$postMessage = $session->get('mautic.integration.postauth.message');
$userData = [];
if (isset($integration)) {
$userData = $session->get('mautic.integration.'.$integration.'.userdata');
}
$message = $type = '';
$alert = 'success';
if (!empty($postMessage)) {
$message = $this->translator->trans($postMessage[0], $postMessage[1], 'flashes');
$session->remove('mautic.integration.postauth.message');
$type = $postMessage[2];
if ('error' == $type) {
$alert = 'danger';
}
}
return $this->render($postAuthTemplate, ['message' => $message, 'alert' => $alert, 'data' => $userData]);
}
public function authUserAction(IntegrationHelper $integrationHelper, $integration): RedirectResponse
{
$integrationObject = $integrationHelper->getIntegrationObject($integration);
$settings['method'] = 'GET';
$settings['integration'] = $integrationObject->getName();
/** @var \Mautic\PluginBundle\Integration\AbstractIntegration $integrationObject */
$event = $this->dispatcher->dispatch(
new PluginIntegrationAuthRedirectEvent(
$integrationObject,
$integrationObject->getAuthLoginUrl()
),
PluginEvents::PLUGIN_ON_INTEGRATION_AUTH_REDIRECT
);
$oauthUrl = $event->getAuthUrl();
return new RedirectResponse($oauthUrl);
}
}

View File

@@ -0,0 +1,464 @@
<?php
namespace Mautic\PluginBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Controller\FormController;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\PluginBundle\Event\PluginIntegrationAuthRedirectEvent;
use Mautic\PluginBundle\Event\PluginIntegrationEvent;
use Mautic\PluginBundle\Facade\ReloadFacade;
use Mautic\PluginBundle\Form\Type\DetailsType;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Mautic\PluginBundle\Model\PluginModel;
use Mautic\PluginBundle\PluginEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PluginController extends FormController
{
/**
* @return JsonResponse|Response
*/
public function indexAction(Request $request, IntegrationHelper $integrationHelper)
{
if (!$this->security->isGranted('plugin:plugins:manage')) {
return $this->accessDenied();
}
/** @var PluginModel $pluginModel */
$pluginModel = $this->getModel('plugin');
// List of plugins for filter and to show as a single integration
$plugins = $pluginModel->getEntities(
[
'filter' => [
'force' => [
[
'column' => 'p.isMissing',
'expr' => 'eq',
'value' => 0,
],
],
],
'hydration_mode' => 'hydrate_array',
]
);
$session = $request->getSession();
$pluginFilter = $request->get('plugin', $session->get('mautic.integrations.filter', ''));
$session->set('mautic.integrations.filter', $pluginFilter);
$integrationObjects = $integrationHelper->getIntegrationObjects(null, null, true);
$integrations = $foundPlugins = [];
foreach ($integrationObjects as $name => $object) {
$settings = $object->getIntegrationSettings();
$plugin = $settings->getPlugin();
$pluginId = $plugin ? $plugin->getId() : $name;
if (isset($plugins[$pluginId]) || $pluginId === $name) {
$integrations[$name] = [
'name' => $object->getName(),
'display' => $object->getDisplayName(),
'icon' => $integrationHelper->getIconPath($object),
'enabled' => $settings->isPublished(),
'plugin' => $pluginId,
'isBundle' => false,
];
}
$foundPlugins[$pluginId] = true;
}
$nonIntegrationPlugins = array_diff_key($plugins, $foundPlugins);
foreach ($nonIntegrationPlugins as $plugin) {
$integrations[$plugin['name']] = [
'name' => $plugin['bundle'],
'display' => $plugin['name'],
'icon' => $integrationHelper->getIconPath($plugin),
'enabled' => true,
'plugin' => $plugin['id'],
'description' => $plugin['description'],
'isBundle' => true,
];
}
// sort by name
uksort(
$integrations,
fn ($a, $b): int => strnatcasecmp($a, $b)
);
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
if (!empty($pluginFilter)) {
foreach ($plugins as $plugin) {
if ($plugin['id'] == $pluginFilter) {
$pluginName = $plugin['name'];
$pluginId = $plugin['id'];
break;
}
}
}
return $this->delegateView(
[
'viewParameters' => [
'items' => $integrations,
'tmpl' => $tmpl,
'pluginFilter' => ($pluginFilter) ? ['id' => $pluginId, 'name' => $pluginName] : false,
'plugins' => $plugins,
],
'contentTemplate' => '@MauticPlugin/Integration/grid.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_plugin_index',
'mauticContent' => 'integration',
'route' => $this->generateUrl('mautic_plugin_index'),
],
]
);
}
/**
* @param string $name
*
* @return JsonResponse|Response
*/
public function configAction(Request $request, EntityManagerInterface $em, IntegrationHelper $integrationHelper, LoggerInterface $mauticLogger, $name, $activeTab = 'details-container', $page = 1)
{
if (!$this->security->isGranted('plugin:plugins:manage')) {
return $this->accessDenied();
}
if (!empty($request->get('activeTab'))) {
$activeTab = $request->get('activeTab');
}
$session = $request->getSession();
$integrationDetailsPost = $request->request->all()['integration_details'] ?? [];
$authorize = empty($integrationDetailsPost['in_auth']) ? false : true;
/** @var AbstractIntegration $integrationObject */
$integrationObject = $integrationHelper->getIntegrationObject($name);
// Verify that the requested integration exists
if (empty($integrationObject)) {
throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404'));
}
$object = ('leadFieldsContainer' === $activeTab) ? 'lead' : 'company';
$limit = $this->coreParametersHelper->get('default_pagelimit');
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
if ($start < 0) {
$start = 0;
}
$session->set('mautic.plugin.'.$name.'.'.$object.'.start', $start);
$session->set('mautic.plugin.'.$name.'.'.$object.'.page', $page);
/** @var PluginModel $pluginModel */
$pluginModel = $this->getModel('plugin');
$leadFields = $pluginModel->getLeadFields();
$companyFields = $pluginModel->getCompanyFields();
/** @var AbstractIntegration $integrationObject */
$entity = $integrationObject->getIntegrationSettings();
$existingPublishedState = $entity->getIsPublished();
$form = $this->createForm(
DetailsType::class,
$entity,
[
'integration' => $entity->getName(),
'lead_fields' => $leadFields,
'company_fields' => $companyFields,
'integration_object' => $integrationObject,
'action' => $this->generateUrl('mautic_plugin_config', ['name' => $name]),
]
);
if ('POST' == $request->getMethod()) {
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
$currentKeys = $integrationObject->getDecryptedApiKeys($entity);
$currentFeatureSettings = $entity->getFeatureSettings();
$valid = $this->isFormValid($form);
if ($authorize || $valid) {
$integration = $entity->getName();
if (isset($form['apiKeys'])) {
$keys = $form['apiKeys']->getData();
// Prevent merged keys
$secretKeys = $integrationObject->getSecretKeys();
foreach ($secretKeys as $secretKey) {
if (empty($keys[$secretKey]) && !empty($currentKeys[$secretKey])) {
$keys[$secretKey] = $currentKeys[$secretKey];
}
}
$keys = $this->removeAuthData($keys, $currentKeys, $integrationObject);
$integrationObject->encryptAndSetApiKeys($keys, $entity);
$integrationObject->encryptAndSetApiKeys($keys, $entity);
}
if (!$authorize) {
$features = $entity->getSupportedFeatures();
if (in_array('public_profile', $features) || in_array('push_lead', $features)) {
// Ungroup the fields
$mauticLeadFields = [];
foreach ($leadFields as $groupFields) {
$mauticLeadFields = array_merge($mauticLeadFields, $groupFields);
}
$mauticCompanyFields = [];
foreach ($companyFields as $groupFields) {
$mauticCompanyFields = array_merge($mauticCompanyFields, $groupFields);
}
if ($missing = $integrationObject->cleanUpFields($entity, $mauticLeadFields, $mauticCompanyFields)) {
if ($entity->getIsPublished()) {
// Only fail validation if the integration is enabled
if (!empty($missing['leadFields'])) {
$valid = false;
$form->get('featureSettings')->get('leadFields')->addError(
new FormError(
$this->translator->trans('mautic.plugin.field.required_mapping_missing', [], 'validators')
)
);
}
if (!empty($missing['companyFields'])) {
$valid = false;
$form->get('featureSettings')->get('companyFields')->addError(
new FormError(
$this->translator->trans('mautic.plugin.field.required_mapping_missing', [], 'validators')
)
);
}
}
}
}
} else {
// make sure they aren't overwritten because of API connection issues
$entity->setFeatureSettings($currentFeatureSettings);
}
if ($valid || $authorize) {
$dispatcher = $this->dispatcher;
$mauticLogger->info('Dispatching integration config save event.');
if ($dispatcher->hasListeners(PluginEvents::PLUGIN_ON_INTEGRATION_CONFIG_SAVE)) {
$mauticLogger->info('Event dispatcher has integration config save listeners.');
if (!$valid && !$existingPublishedState) {
$integrationObject->getIntegrationSettings()->setIsPublished(false);
}
$event = new PluginIntegrationEvent($integrationObject);
$dispatcher->dispatch($event, PluginEvents::PLUGIN_ON_INTEGRATION_CONFIG_SAVE);
$entity = $event->getEntity();
}
$em->persist($entity);
$em->flush();
}
if ($authorize) {
// redirect to the oauth URL
/** @var AbstractIntegration $integrationObject */
$event = $this->dispatcher->dispatch(
new PluginIntegrationAuthRedirectEvent(
$integrationObject,
$integrationObject->getAuthLoginUrl()
),
PluginEvents::PLUGIN_ON_INTEGRATION_AUTH_REDIRECT
);
$oauthUrl = $event->getAuthUrl();
return new JsonResponse(
[
'integration' => $integration,
'authUrl' => $oauthUrl,
'authorize' => 1,
'popupBlockerMessage' => $this->translator->trans('mautic.core.popupblocked'),
]
);
}
}
}
if (($cancelled || ($valid && !$this->isFormApplied($form))) && !$authorize) {
// Close the modal and return back to the list view
return new JsonResponse(
[
'closeModal' => 1,
'enabled' => $entity->getIsPublished(),
'name' => $integrationObject->getName(),
'mauticContent' => 'integrationConfig',
'sidebar' => $this->renderView('@MauticCore/LeftPanel/index.html.twig'),
]
);
}
}
$template = $integrationObject->getFormTemplate();
$objectTheme = $integrationObject->getFormTheme();
$themes = [
'@MauticPlugin/FormTheme/Integration/layout.html.twig',
];
if (is_array($objectTheme)) {
$themes = array_merge($themes, $objectTheme);
} elseif (is_string($objectTheme)) {
$themes[] = $objectTheme;
}
$themes = array_unique($themes);
$formSettings = $integrationObject->getFormSettings();
$callbackUrl = !empty($formSettings['requires_callback']) ? $integrationObject->getAuthCallbackUrl() : '';
$formNotes = [];
$noteSections = ['authorization', 'features', 'feature_settings', 'custom'];
foreach ($noteSections as $section) {
if ('custom' === $section) {
$formNotes[$section] = $integrationObject->getFormNotes($section);
} else {
[$specialInstructions, $alertType] = $integrationObject->getFormNotes($section);
if (!empty($specialInstructions)) {
$formNotes[$section] = [
'note' => $specialInstructions,
'type' => $alertType,
];
}
}
}
return $this->delegateView(
[
'viewParameters' => [
'form' => $form->createView(),
'description' => $integrationObject->getDescription(),
'formSettings' => $formSettings,
'formNotes' => $formNotes,
'callbackUrl' => $callbackUrl,
'activeTab' => $activeTab,
'formThemes' => $themes,
],
'contentTemplate' => $template,
'passthroughVars' => [
'activeLink' => '#mautic_plugin_index',
'mauticContent' => 'integrationConfig',
'route' => false,
'sidebar' => $this->renderView('@MauticCore/LeftPanel/index.html.twig'),
],
]
);
}
/**
* @return array|JsonResponse|RedirectResponse|Response
*/
public function infoAction(IntegrationHelper $integrationHelper, $name)
{
if (!$this->security->isGranted('plugin:plugins:manage')) {
return $this->accessDenied();
}
/** @var PluginModel $pluginModel */
$pluginModel = $this->getModel('plugin');
$bundle = $pluginModel->getRepository()->findOneBy(
[
'bundle' => InputHelper::clean($name),
]
);
if (!$bundle) {
return $this->accessDenied();
}
$bundle->splitDescriptions();
return $this->delegateView(
[
'viewParameters' => [
'bundle' => $bundle,
'icon' => $integrationHelper->getIconPath($bundle),
],
'contentTemplate' => '@MauticPlugin/Integration/info.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_plugin_index',
'mauticContent' => 'integration',
'route' => false,
],
]
);
}
/**
* Scans the addon bundles directly and loads bundles which are not registered to the database.
*
* @return Response
*/
public function reloadAction(Request $request, ReloadFacade $reloadFacade)
{
if (!$this->security->isGranted('plugin:plugins:manage')) {
return $this->accessDenied();
}
$this->addFlashMessage(
$reloadFacade->reloadPlugins()
);
$viewParameters = [
'page' => $request->getSession()->get('mautic.plugin.page'),
];
// Refresh the index contents
return $this->postActionRedirect(
[
'returnUrl' => $this->generateUrl('mautic_plugin_index', $viewParameters),
'viewParameters' => $viewParameters,
'contentTemplate' => 'Mautic\PluginBundle\Controller\PluginController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_plugin_index',
'mauticContent' => 'plugin',
],
]
);
}
/**
* @param array <string,mixed> $keys
* @param array <string,mixed> $currentKeys
*
* @return array <string,mixed>
*
* @phpstan-ignore-next-line Ignore as AbstractIntegration is deprecated
*/
private function removeAuthData(array $keys, array $currentKeys, AbstractIntegration $integrationObject): array
{
$resetTokens = false;
$secretKeys = array_unique(array_merge($integrationObject->getSecretKeys(), [$integrationObject->getClientIdKey()]));
foreach ($secretKeys as $secretKey) {
if (($keys[$secretKey] ?? null) !== ($currentKeys[$secretKey] ?? null)) {
$resetTokens = true;
break;
}
}
if (!$resetTokens) {
return $keys;
}
$keysToRemove = array_unique(array_merge($integrationObject->getRefreshTokenKeys(), [$integrationObject->getAuthTokenKey()]));
return array_diff_key($keys, array_flip($keysToRemove));
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class MauticPluginExtension extends Extension
{
/**
* @param mixed[] $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
$loader->load('services.php');
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
use Mautic\CoreBundle\Entity\CommonEntity;
class Integration extends CommonEntity implements CacheInvalidateInterface
{
public const CACHE_NAMESPACE = 'IntegrationSettings';
/**
* @var int
*/
private $id;
/**
* @var Plugin|null
*/
private $plugin;
/**
* @var string
*/
private $name;
/**
* @var bool
*/
private $isPublished = false;
/**
* @var array
*/
private $supportedFeatures = [];
/**
* @var array
*/
private $apiKeys = [];
/**
* @var array
*/
private $featureSettings = [];
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('plugin_integration_settings')
->setCustomRepositoryClass(IntegrationRepository::class);
$builder->createField('id', 'integer')
->makePrimaryKey()
->generatedValue()
->build();
$builder->createManyToOne('plugin', 'Plugin')
->inversedBy('integrations')
->addJoinColumn('plugin_id', 'id', true, false, 'CASCADE')
->build();
$builder->addField('name', 'string');
$builder->createField('isPublished', 'boolean')
->columnName('is_published')
->build();
$builder->createField('supportedFeatures', 'array')
->columnName('supported_features')
->nullable()
->build();
$builder->createField('apiKeys', 'array')
->columnName('api_keys')
->build();
$builder->createField('featureSettings', 'array')
->columnName('feature_settings')
->nullable()
->build();
}
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return Plugin|null
*/
public function getPlugin()
{
return $this->plugin;
}
/**
* @param mixed $plugin
*
* @return Integration
*/
public function setPlugin($plugin)
{
$this->plugin = $plugin;
return $this;
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param mixed $name
*
* @return Integration
*/
public function setName($name)
{
$this->isChanged('name', $name);
$this->name = $name;
return $this;
}
/**
* @return mixed
*/
public function getIsPublished()
{
return $this->isPublished;
}
/**
* @param mixed $isPublished
*
* @return Integration
*/
public function setIsPublished($isPublished)
{
$this->isChanged('isPublished', $isPublished);
$this->isPublished = $isPublished;
return $this;
}
public function isPublished(): bool
{
return $this->isPublished;
}
/**
* @return mixed
*/
public function getSupportedFeatures()
{
return $this->supportedFeatures;
}
/**
* @param mixed $supportedFeatures
*
* @return Integration
*/
public function setSupportedFeatures($supportedFeatures)
{
$this->isChanged('supportedFeatures', $supportedFeatures);
$this->supportedFeatures = $supportedFeatures;
return $this;
}
/**
* @return mixed
*/
public function getApiKeys()
{
return $this->apiKeys;
}
/**
* @param mixed $apiKeys
*
* @return Integration
*/
public function setApiKeys($apiKeys)
{
$this->apiKeys = $apiKeys;
return $this;
}
/**
* @return mixed
*/
public function getFeatureSettings()
{
return $this->featureSettings;
}
/**
* @param mixed $featureSettings
*
* @return Integration
*/
public function setFeatureSettings($featureSettings)
{
$this->isChanged('featureSettings', $featureSettings);
$this->featureSettings = $featureSettings;
return $this;
}
public function getCacheNamespacesToDelete(): array
{
return [self::CACHE_NAMESPACE];
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CommonEntity;
class IntegrationEntity extends CommonEntity
{
/**
* @var int
*/
private $id;
/**
* @var string|null
*/
private $integration;
/**
* @var string|null
*/
private $integrationEntity;
/**
* @var string|null
*/
private $integrationEntityId;
/**
* @var \DateTimeInterface
*/
private $dateAdded;
/**
* @var \DateTimeInterface
*/
private $lastSyncDate;
/**
* @var string|null
*/
private $internalEntity;
/**
* @var int|null
*/
private $internalEntityId;
/**
* @var array
*/
private $internal;
public function __construct()
{
$this->internal = new ArrayCollection();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('integration_entity')
->setCustomRepositoryClass(IntegrationEntityRepository::class)
->addIndex(['integration', 'integration_entity', 'integration_entity_id'], 'integration_external_entity')
->addIndex(['integration', 'internal_entity', 'internal_entity_id'], 'integration_internal_entity')
->addIndex(['integration', 'internal_entity', 'integration_entity'], 'integration_entity_match')
->addIndex(['integration', 'last_sync_date'], 'integration_last_sync_date')
->addIndex(['internal_entity_id', 'integration_entity_id', 'internal_entity', 'integration_entity'], 'internal_integration_entity');
$builder->addId();
$builder->addDateAdded();
$builder->addNullableField('integration', 'string');
$builder->createField('integrationEntity', 'string')
->columnName('integration_entity')
->nullable()
->build();
$builder->createField('integrationEntityId', 'string')
->columnName('integration_entity_id')
->nullable()
->build();
$builder->createField('internalEntity', 'string')
->columnName('internal_entity')
->nullable()
->build();
$builder->createField('internalEntityId', 'integer')
->columnName('internal_entity_id')
->nullable()
->build();
$builder->createField('lastSyncDate', 'datetime')
->columnName('last_sync_date')
->nullable()
->build();
$builder->addNullableField('internal', 'array');
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getIntegration()
{
return $this->integration;
}
/**
* @param string $integration
*
* @return IntegrationEntity
*/
public function setIntegration($integration)
{
$this->integration = $integration;
return $this;
}
/**
* @return string
*/
public function getIntegrationEntity()
{
return $this->integrationEntity;
}
/**
* @param string $integrationEntity
*
* @return IntegrationEntity
*/
public function setIntegrationEntity($integrationEntity)
{
$this->integrationEntity = $integrationEntity;
return $this;
}
/**
* @return string
*/
public function getIntegrationEntityId()
{
return $this->integrationEntityId;
}
/**
* @param string $integrationEntityId
*
* @return IntegrationEntity
*/
public function setIntegrationEntityId($integrationEntityId)
{
$this->integrationEntityId = $integrationEntityId;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getDateAdded()
{
return $this->dateAdded;
}
/**
* @param \DateTime $dateAdded
*
* @return IntegrationEntity
*/
public function setDateAdded($dateAdded)
{
$this->dateAdded = $dateAdded;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getLastSyncDate()
{
return $this->lastSyncDate;
}
/**
* @param \DateTime $lastSyncDate
*
* @return IntegrationEntity
*/
public function setLastSyncDate($lastSyncDate)
{
$this->lastSyncDate = $lastSyncDate;
return $this;
}
/**
* @return string
*/
public function getInternalEntity()
{
return $this->internalEntity;
}
/**
* @param string $internalEntity
*
* @return IntegrationEntity
*/
public function setInternalEntity($internalEntity)
{
$this->internalEntity = $internalEntity;
return $this;
}
/**
* @return int
*/
public function getInternalEntityId()
{
return $this->internalEntityId;
}
/**
* @param int $internalEntityId
*
* @return IntegrationEntity
*/
public function setInternalEntityId($internalEntityId)
{
$this->internalEntityId = $internalEntityId;
return $this;
}
/**
* @return array
*/
public function getInternal()
{
return $this->internal;
}
/**
* @param array $internal
*
* @return IntegrationEntity
*/
public function setInternal($internal)
{
$this->internal = $internal;
return $this;
}
}

View File

@@ -0,0 +1,539 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<IntegrationEntity>
*/
class IntegrationEntityRepository extends CommonRepository
{
/**
* @param array<int>|int|null $internalEntityIds
* @param mixed $startDate
* @param mixed $endDate
* @param bool $push
* @param int $start
* @param int $limit
* @param int|string|array<int|string>|null $integrationEntityIds
*/
public function getIntegrationsEntityId(
$integration,
$integrationEntity,
$internalEntity,
$internalEntityIds = null,
$startDate = null,
$endDate = null,
$push = false,
$start = 0,
$limit = 0,
$integrationEntityIds = null,
): array {
$q = $this->_em->getConnection()->createQueryBuilder()
->select('DISTINCT(i.integration_entity_id), i.id, i.internal_entity_id, i.integration_entity, i.internal_entity')
->from(MAUTIC_TABLE_PREFIX.'integration_entity', 'i');
$q->where('i.integration = :integration')
->andWhere('i.internal_entity = :internalEntity')
->setParameter('integration', $integration)
->setParameter('internalEntity', $internalEntity);
if ($integrationEntity) {
$q->andWhere('i.integration_entity = :integrationEntity')
->setParameter('integrationEntity', $integrationEntity);
}
if ('lead' === $internalEntity) {
$joinCondition = $q->expr()->and(
$q->expr()->eq('l.id', 'i.internal_entity_id')
);
if ($push) {
$joinCondition->with(
$q->expr()->gte('l.last_active', ':startDate')
);
$q->setParameter('startDate', $startDate);
}
$q->join('i', MAUTIC_TABLE_PREFIX.'leads', 'l', $joinCondition);
}
if ($internalEntityIds) {
if (is_array($internalEntityIds)) {
$q->andWhere('i.internal_entity_id in (:internalEntityIds)')
->setParameter('internalEntityIds', $internalEntityIds, ArrayParameterType::STRING);
} else {
$q->andWhere('i.internal_entity_id = :internalEntityId')
->setParameter('internalEntityId', $internalEntityIds);
}
}
if ($startDate and !$push) {
$q->andWhere('i.last_sync_date >= :startDate')
->setParameter('startDate', $startDate);
}
if ($endDate and !$push) {
$q->andWhere('i.last_sync_date <= :endDate')
->setParameter('endDate', $endDate);
}
if ($integrationEntityIds) {
if (is_array($integrationEntityIds)) {
$q->andWhere('i.integration_entity_id in (:integrationEntityIds)')
->setParameter('integrationEntityIds', $integrationEntityIds, ArrayParameterType::STRING);
} else {
$q->andWhere('i.integration_entity_id = :integrationEntityId')
->setParameter('integrationEntityId', $integrationEntityIds);
}
}
if ($start) {
$q->setFirstResult((int) $start);
}
if ($limit) {
$q->setMaxResults((int) $limit);
}
return $q->executeQuery()->fetchAllAssociative();
}
/**
* @return array
*/
public function getIntegrationEntity($integration, $integrationEntity, $internalEntity, $internalEntityId, $leadFields = null)
{
$q = $this->_em->getConnection()->createQueryBuilder()
->from(MAUTIC_TABLE_PREFIX.'integration_entity', 'i')
->join('i', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = i.internal_entity_id');
$q->select('i.integration_entity_id, i.integration_entity, i.id, i.internal_entity_id');
if ($leadFields) {
$q->addSelect($leadFields);
}
$q->where(
$q->expr()->and(
$q->expr()->eq('i.integration', ':integration'),
$q->expr()->eq('i.internal_entity', ':internalEntity'),
$q->expr()->eq('i.integration_entity', ':integrationEntity'),
$q->expr()->eq('i.internal_entity_id', (int) $internalEntityId)
)
)
->setParameter('integration', $integration)
->setParameter('internalEntity', $internalEntity)
->setParameter('integrationEntity', $integrationEntity)
->setMaxResults(1);
$results = $q->executeQuery()->fetchAllAssociative();
return ($results) ? $results[0] : null;
}
/**
* @return IntegrationEntity[]
*/
public function getIntegrationEntities($integration, $integrationEntity, $internalEntity, $internalEntityIds)
{
$q = $this->createQueryBuilder('i', 'i.internalEntityId');
$q->where(
$q->expr()->andX(
$q->expr()->eq('i.integration', ':integration'),
$q->expr()->eq('i.internalEntity', ':internalEntity'),
$q->expr()->eq('i.integrationEntity', ':integrationEntity'),
$q->expr()->in('i.internalEntityId', ':internalEntityIds')
)
)
->setParameter('integration', $integration)
->setParameter('internalEntity', $internalEntity)
->setParameter('integrationEntity', $integrationEntity)
->setParameter('internalEntityIds', $internalEntityIds);
return $q->getQuery()->getResult();
}
/**
* @param int $limit
* @param array|string $integrationEntity
* @param array $excludeIntegrationIds
*
* @return mixed[]
*/
public function findLeadsToUpdate(
$integration,
$internalEntity,
$leadFields,
$limit = 25,
$fromDate = null,
$toDate = null,
$integrationEntity = ['Contact', 'Lead'],
$excludeIntegrationIds = [],
): array {
if ('company' == $internalEntity) {
$joinTable = 'companies';
} else {
$joinTable = 'leads';
}
$q = $this->_em->getConnection()->createQueryBuilder()
->from(MAUTIC_TABLE_PREFIX.'integration_entity', 'i')
->join('i', MAUTIC_TABLE_PREFIX.$joinTable, 'l', 'l.id = i.internal_entity_id');
if (false === $limit) {
$q->select('count(i.integration_entity_id) as total');
if ($integrationEntity) {
$q->addSelect('i.integration_entity');
}
} else {
$q->select('i.integration_entity_id, i.integration_entity, i.id, i.internal_entity_id,'.$leadFields);
}
$q->where('i.integration = :integration');
if ($integrationEntity) {
if (!is_array($integrationEntity)) {
$integrationEntity = [$integrationEntity];
}
$sub = null;
foreach ($integrationEntity as $key => $entity) {
if (null === $sub) {
$sub = CompositeExpression::or($q->expr()->eq('i.integration_entity', ':entity'.$key));
$q->setParameter('entity'.$key, $entity);
continue;
}
$sub->with($q->expr()->eq('i.integration_entity', ':entity'.$key));
$q->setParameter('entity'.$key, $entity);
}
$q->andWhere($sub);
}
$q->andWhere('i.internal_entity = :internalEntity')
->setParameter('integration', $integration)
->setParameter('internalEntity', $internalEntity);
if (!empty($excludeIntegrationIds)) {
$q->andWhere(
$q->expr()->notIn(
'i.integration_entity_id',
array_map(
fn ($x): string => "'".$x."'",
$excludeIntegrationIds
)
)
);
}
$q->andWhere(
$q->expr()->and(
$q->expr()->isNotNull('i.integration_entity_id'),
$q->expr()->or(
$q->expr()->and(
$q->expr()->isNotNull('i.last_sync_date'),
$q->expr()->gt('l.date_modified', 'i.last_sync_date')
),
$q->expr()->and(
$q->expr()->isNull('i.last_sync_date'),
$q->expr()->isNotNull('l.date_modified'),
$q->expr()->gt('l.date_modified', 'l.date_added')
)
)
)
);
if ('lead' == $internalEntity) {
$q->andWhere(
$q->expr()->and($q->expr()->isNotNull('l.email')));
} else {
$q->andWhere(
$q->expr()->and($q->expr()->isNotNull('l.companyname')));
}
if ($fromDate) {
if ($toDate) {
$q->andWhere(
$q->expr()->comparison('l.date_modified', 'BETWEEN', ':dateFrom and :dateTo')
)
->setParameter('dateFrom', $fromDate)
->setParameter('dateTo', $toDate);
} else {
$q->andWhere(
$q->expr()->gte('l.date_modified', ':dateFrom')
)
->setParameter('dateFrom', $fromDate);
}
} elseif ($toDate) {
$q->andWhere(
$q->expr()->lte('l.date_modified', ':dateTo')
)
->setParameter('dateTo', $toDate);
}
// Group by email to prevent duplicates from affecting this
if (false === $limit and $integrationEntity) {
$q->groupBy('i.integration_entity')->having('total');
}
if ($limit) {
$q->setMaxResults($limit);
}
$results = $q->executeQuery()->fetchAllAssociative();
$leads = [];
if ($integrationEntity) {
foreach ($integrationEntity as $entity) {
$leads[$entity] = (false === $limit) ? 0 : [];
}
}
foreach ($results as $result) {
if ($integrationEntity) {
if (false === $limit) {
$leads[$result['integration_entity']] = (int) $result['total'];
} else {
$leads[$result['integration_entity']][$result['internal_entity_id']] = $result;
}
} else {
$leads[$result['internal_entity_id']] = $result['internal_entity_id'];
}
}
return $leads;
}
/**
* @param int $limit
*
* @return array|int
*/
public function findLeadsToCreate($integration, $leadFields, $limit = 25, $fromDate = null, $toDate = null, $internalEntity = 'lead')
{
if ('company' == $internalEntity) {
$joinTable = 'companies';
} else {
$joinTable = 'leads';
}
$q = $this->_em->getConnection()->createQueryBuilder()
->from(MAUTIC_TABLE_PREFIX.$joinTable, 'l');
if (false === $limit) {
$q->select('count(*) as total');
} else {
$q->select('l.id as internal_entity_id,'.$leadFields);
}
if ('company' == $internalEntity) {
$q->where('not exists (select null from '.MAUTIC_TABLE_PREFIX
.'integration_entity i where i.integration = :integration and i.internal_entity LIKE "'.$internalEntity.'%" and i.internal_entity_id = l.id)')
->setParameter('integration', $integration);
} else {
$q->where('l.date_identified is not null')
->andWhere(
'not exists (select null from '.MAUTIC_TABLE_PREFIX
.'integration_entity i where i.integration = :integration and i.internal_entity LIKE "'.$internalEntity.'%" and i.internal_entity_id = l.id)'
)
->setParameter('integration', $integration);
}
if ('company' == $internalEntity) {
$q->andWhere('l.companyname is not null');
} else {
$q->andWhere('l.email is not null');
}
if ($limit) {
$q->setMaxResults($limit);
}
if ($fromDate) {
if ($toDate) {
$q->andWhere(
$q->expr()->or(
$q->expr()->and(
$q->expr()->isNotNull('l.date_modified'),
$q->expr()->comparison('l.date_modified', 'BETWEEN', ':dateFrom and :dateTo')
),
$q->expr()->and(
$q->expr()->isNull('l.date_modified'),
$q->expr()->comparison('l.date_added', 'BETWEEN', ':dateFrom and :dateTo')
)
)
)
->setParameter('dateFrom', $fromDate)
->setParameter('dateTo', $toDate);
} else {
$q->andWhere(
$q->expr()->or(
$q->expr()->and(
$q->expr()->isNotNull('l.date_modified'),
$q->expr()->gte('l.date_modified', ':dateFrom')
),
$q->expr()->and(
$q->expr()->isNull('l.date_modified'),
$q->expr()->gte('l.date_added', ':dateFrom')
)
)
)
->setParameter('dateFrom', $fromDate);
}
} elseif ($toDate) {
$q->andWhere(
$q->expr()->or(
$q->expr()->and(
$q->expr()->isNotNull('l.date_modified'),
$q->expr()->lte('l.date_modified', ':dateTo')
),
$q->expr()->and(
$q->expr()->isNull('l.date_modified'),
$q->expr()->lte('l.date_added', ':dateTo')
)
)
)
->setParameter('dateTo', $toDate);
}
$results = $q->executeQuery()->fetchAllAssociative();
if (false === $limit) {
return (int) $results[0]['total'];
}
$leads = [];
foreach ($results as $result) {
$leads[$result['internal_entity_id']] = $result;
}
return $leads;
}
/**
* @return int
*/
public function getIntegrationEntityCount($leadId, $integration = null, $integrationEntity = null, $internalEntity = null)
{
return $this->getIntegrationEntityByLead($leadId, $integration, $integrationEntity, $internalEntity, false);
}
/**
* @param int|bool $limit
*
* @return array|int
*/
public function getIntegrationEntityByLead($leadId, $integration = null, $integrationEntity = null, $internalEntity = null, $limit = 100)
{
$q = $this->_em->getConnection()->createQueryBuilder()
->from(MAUTIC_TABLE_PREFIX.'integration_entity', 'i');
if (false === $limit) {
$q->select('count(*) as total');
} else {
$q->select('i.integration, i.integration_entity, i.integration_entity_id, i.date_added, i.last_sync_date, i.internal');
}
$q->where('i.internal not like \'%error%\' and i.integration_entity_id is not null');
$q->orderBy('i.last_sync_date', 'DESC');
if (empty($integration)) {
// get list of published integrations
$pq = $this->_em->getConnection()->createQueryBuilder()
->select('p.name')
->from(MAUTIC_TABLE_PREFIX.'plugin_integration_settings', 'p')
->where('p.is_published = 1');
$rows = $pq->executeQuery()->fetchAllAssociative();
$plugins = array_map(static fn ($i): string => "'{$i['name']}'", $rows);
if (count($plugins) > 0) {
$q->andWhere($q->expr()->in('i.integration', $plugins));
} else {
return [];
}
} else {
$q->andWhere($q->expr()->eq('i.integration', ':integration'));
$q->setParameter('integration', $integration);
}
$q->andWhere(
$q->expr()->and(
"i.internal_entity='lead'",
$q->expr()->eq('i.internal_entity_id', ':internalEntityId')
)
);
$q->setParameter('internalEntityId', $leadId);
if (!empty($internalEntity)) {
$q->andWhere($q->expr()->eq('i.internalEntity', ':internalEntity'));
$q->setParameter('internalEntity', $internalEntity);
}
if (!empty($integrationEntity)) {
$q->andWhere($q->expr()->eq('i.integrationEntity', ':integrationEntity'));
$q->setParameter('integrationEntity', $integrationEntity);
}
$results = $q->executeQuery()->fetchAllAssociative();
if (false === $limit && count($results) > 0) {
return (int) $results[0]['total'];
}
return $results;
}
public function markAsDeleted(array $integrationIds, $integration, $internalEntityType): void
{
$q = $this->_em->getConnection()->createQueryBuilder();
$q->update(MAUTIC_TABLE_PREFIX.'integration_entity')
->set('internal_entity', ':entity')
->where(
$q->expr()->and(
$q->expr()->eq('integration', ':integration'),
$q->expr()->in('integration_entity_id', array_map([$q->expr(), 'literal'], $integrationIds))
)
)
->setParameter('integration', $integration)
->setParameter('entity', $internalEntityType.'-deleted')
->executeStatement();
}
public function findLeadsToDelete($internalEntity, $leadId): void
{
$q = $this->_em->getConnection()->createQueryBuilder()
->delete(MAUTIC_TABLE_PREFIX.'integration_entity')
->from(MAUTIC_TABLE_PREFIX.'integration_entity');
$q->where('internal_entity_id = :leadId')
->andWhere($q->expr()->like('internal_entity', ':internalEntity'))
->setParameter('leadId', $leadId)
->setParameter('internalEntity', $internalEntity)
->executeStatement();
}
public function updateErrorLeads($internalEntity, $leadId): void
{
$q = $this->_em->getConnection()->createQueryBuilder()
->update(MAUTIC_TABLE_PREFIX.'integration_entity')
->set('internal_entity', ':lead')->setParameter('lead', 'lead');
$q->where('internal_entity_id = :leadId')
->andWhere($q->expr()->isNotNull('integration_entity_id'))
->andWhere($q->expr()->eq('internal_entity', ':internalEntity'))
->setParameter('leadId', $leadId)
->setParameter('internalEntity', $internalEntity)
->executeStatement();
$z = $this->_em->getConnection()->createQueryBuilder()
->delete(MAUTIC_TABLE_PREFIX.'integration_entity')
->from(MAUTIC_TABLE_PREFIX.'integration_entity');
$z->where('internal_entity_id = :leadId')
->andWhere($q->expr()->isNull('integration_entity_id'))
->andWhere($q->expr()->like('internal_entity', ':internalEntity'))
->setParameter('leadId', $leadId)
->setParameter('internalEntity', $internalEntity)
->executeStatement();
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Doctrine\ORM\Query;
use Mautic\CoreBundle\Cache\ResultCacheHelper;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Integration>
*/
class IntegrationRepository extends CommonRepository
{
/**
* @return mixed[]
*/
public function getIntegrations(): array
{
$query = $this->createQueryBuilder('i')
->join('i.plugin', 'p')
->getQuery();
$this->enableCache($query);
$services = $query->getResult();
$results = [];
foreach ($services as $s) {
$results[$s->getName()] = $s;
}
return $results;
}
/**
* Get core (no plugin) integrations.
*
* @return mixed[]
*/
public function getCoreIntegrations(): array
{
$query = $this->createQueryBuilder('i')
->getQuery();
$this->enableCache($query);
$services = $query->getResult();
$results = [];
foreach ($services as $s) {
$results[$s->getName()] = $s;
}
return $results;
}
public function findOneByName(string $name): ?Integration
{
$query = $this->createQueryBuilder('i')
->where('i.name = :name')
->setParameter('name', $name)
->setMaxResults(1)
->getQuery();
$this->enableCache($query);
return $query->getOneOrNullResult();
}
private function enableCache(Query $query): void
{
ResultCacheHelper::enableOrmQueryCache($query, new ResultCacheOptions(Integration::CACHE_NAMESPACE));
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
use Mautic\CoreBundle\Entity\CommonEntity;
class Plugin extends CommonEntity implements CacheInvalidateInterface
{
public const DESCRIPTION_DELIMITER_REGEX = "/\R---\R/";
public const CACHE_NAMESPACE = 'Plugin';
/**
* @var int
*/
private $id;
/**
* @var string
*/
private $name;
/**
* @var string|null
*/
private $description;
/**
* @var string
*/
private $primaryDescription;
/**
* @var string
*/
private $secondaryDescription;
/**
* @var bool
*/
private $isMissing = false;
/**
* @var string
*/
private $bundle;
/**
* @var string|null
*/
private $version;
/**
* @var string|null
*/
private $author;
/**
* @var ArrayCollection<int, Integration>
*/
private $integrations;
public function __construct()
{
$this->integrations = new ArrayCollection();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('plugins')
->setCustomRepositoryClass(PluginRepository::class)
->addUniqueConstraint(['bundle'], 'unique_bundle');
$builder->addIdColumns();
$builder->createField('isMissing', 'boolean')
->columnName('is_missing')
->build();
$builder->createField('bundle', 'string')
->length(50)
->build();
$builder->createField('version', 'string')
->nullable()
->build();
$builder->createField('author', 'string')
->nullable()
->build();
$builder->createOneToMany('integrations', 'Integration')
->setIndexBy('id')
->mappedBy('plugin')
->fetchExtraLazy()
->build();
}
public function __clone()
{
$this->id = null;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Plugin
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $bundle
*/
public function setBundle($bundle): void
{
$this->bundle = $bundle;
}
/**
* @return string
*/
public function getBundle()
{
return $this->bundle;
}
/**
* @return mixed
*/
public function getIntegrations()
{
return $this->integrations;
}
/**
* @return mixed
*/
public function getDescription()
{
return $this->description;
}
/**
* @param mixed $description
*/
public function setDescription($description): void
{
$this->description = $description;
$this->splitDescriptions();
}
/**
* @return string|null
*/
public function getPrimaryDescription()
{
return $this->primaryDescription ?: $this->description;
}
public function hasSecondaryDescription(): bool
{
return $this->description && preg_match(self::DESCRIPTION_DELIMITER_REGEX, $this->description) >= 1;
}
/**
* @return string|null
*/
public function getSecondaryDescription()
{
return $this->secondaryDescription;
}
/**
* @return mixed
*/
public function getVersion()
{
return $this->version;
}
/**
* @param mixed $version
*/
public function setVersion($version): void
{
$this->version = $version;
}
/**
* @return mixed
*/
public function getIsMissing()
{
return $this->isMissing;
}
/**
* @param mixed $isMissing
*/
public function setIsMissing($isMissing): void
{
$this->isMissing = $isMissing;
}
/**
* @return mixed
*/
public function getAuthor()
{
return $this->author;
}
/**
* @param mixed $author
*/
public function setAuthor($author): void
{
$this->author = $author;
}
/**
* Splits description into primary and secondary.
*/
public function splitDescriptions(): void
{
if ($this->hasSecondaryDescription()) {
$parts = preg_split(self::DESCRIPTION_DELIMITER_REGEX, $this->description);
$this->primaryDescription = trim($parts[0]);
$this->secondaryDescription = trim($parts[1]);
}
}
public function getCacheNamespacesToDelete(): array
{
return [
self::CACHE_NAMESPACE,
Integration::CACHE_NAMESPACE,
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Mautic\PluginBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Plugin>
*/
class PluginRepository extends CommonRepository
{
/**
* Find an addon record by bundle name.
*
* @param string $bundle
*
* @return mixed
*
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function findByBundle($bundle)
{
$q = $this->createQueryBuilder($this->getTableAlias());
$q->where($q->expr()->eq('p.bundle', ':bundle'))
->setParameter('bundle', $bundle);
return $q->getQuery()->getOneOrNullResult();
}
public function getEntities(array $args = [])
{
$q = $this->_em->createQueryBuilder();
$q->select($this->getTableAlias())
->from(Plugin::class, $this->getTableAlias(), (!empty($args['index'])) ? $this->getTableAlias().'.'.$args['index'] : $this->getTableAlias().'.id');
$args['qb'] = $q;
$args['ignore_paginator'] = true;
return parent::getEntities($args);
}
protected function getDefaultOrder(): array
{
return [
['p.name', 'ASC'],
];
}
public function getTableAlias(): string
{
return 'p';
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Contracts\EventDispatcher\Event;
class AbstractPluginIntegrationEvent extends Event
{
/**
* @var AbstractIntegration
*/
protected $integration;
/**
* Get the integration's name.
*
* @return mixed
*/
public function getIntegrationName()
{
return $this->integration->getName();
}
/**
* Get the integration object.
*
* @return AbstractIntegration
*/
public function getIntegration()
{
return $this->integration;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Event;
use Doctrine\ORM\Mapping\ClassMetadata;
use Mautic\PluginBundle\Entity\Plugin;
use Symfony\Contracts\EventDispatcher\Event;
class PluginInstallEvent extends Event
{
/**
* @param array<class-string, ClassMetadata>|null $metadata
*/
public function __construct(
private Plugin $plugin,
private ?array $metadata,
private ?bool $installedSchema,
) {
}
public function getPlugin(): Plugin
{
return $this->plugin;
}
/**
* @return array<class-string, ClassMetadata>|null
*/
public function getMetadata(): ?array
{
return $this->metadata;
}
public function getInstalledSchema(): ?bool
{
return $this->installedSchema;
}
public function checkContext(string $pluginName): bool
{
return $pluginName === $this->plugin->getName();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
class PluginIntegrationAuthCallbackUrlEvent extends AbstractPluginIntegrationEvent
{
/**
* @param string $callbackUrl
*/
public function __construct(
UnifiedIntegrationInterface $integration,
private $callbackUrl,
) {
$this->integration = $integration;
}
/**
* @return string
*/
public function getCallbackUrl()
{
return $this->callbackUrl;
}
/**
* @param string $callbackUrl
*/
public function setCallbackUrl($callbackUrl): void
{
$this->callbackUrl = $callbackUrl;
$this->stopPropagation();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
class PluginIntegrationAuthRedirectEvent extends AbstractPluginIntegrationEvent
{
/**
* @param string $authUrl
*/
public function __construct(
UnifiedIntegrationInterface $integration,
private $authUrl,
) {
$this->integration = $integration;
}
/**
* @return string
*/
public function getAuthUrl()
{
return $this->authUrl;
}
/**
* @param string $authUrl
*/
public function setAuthUrl($authUrl): void
{
$this->authUrl = $authUrl;
$this->stopPropagation();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
class PluginIntegrationEvent extends AbstractPluginIntegrationEvent
{
public function __construct(UnifiedIntegrationInterface $integration)
{
$this->integration = $integration;
}
/**
* @return Integration
*/
public function getEntity()
{
return $this->integration->getIntegrationSettings();
}
public function setEntity(Integration $integration): void
{
$this->integration->setIntegrationSettings($integration);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Symfony\Component\Form\FormBuilderInterface;
class PluginIntegrationFormBuildEvent extends AbstractPluginIntegrationEvent
{
public function __construct(
UnifiedIntegrationInterface $integration,
private FormBuilderInterface $builder,
private array $options,
) {
$this->integration = $integration;
}
/**
* @return FormBuilderInterface
*/
public function getFormBuilder()
{
return $this->builder;
}
/**
* @return array
*/
public function getOptions()
{
return $this->options;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
class PluginIntegrationFormDisplayEvent extends AbstractPluginIntegrationEvent
{
/**
* @param array<string, mixed> $settings
*/
public function __construct(
UnifiedIntegrationInterface $integration,
private array $settings,
) {
$this->integration = $integration;
}
/**
* @return array
*/
public function getSettings()
{
return $this->settings;
}
public function setSettings(array $settings): void
{
$this->settings = $settings;
$this->stopPropagation();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
class PluginIntegrationKeyEvent extends AbstractPluginIntegrationEvent
{
public function __construct(
UnifiedIntegrationInterface $integration,
private ?array $keys = null,
) {
$this->integration = $integration;
}
/**
* Get the keys array.
*/
public function getKeys()
{
return $this->keys;
}
/**
* Set new keys array.
*/
public function setKeys(array $keys): void
{
$this->keys = $keys;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Mautic\PluginBundle\Event;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Psr\Http\Message\ResponseInterface;
class PluginIntegrationRequestEvent extends AbstractPluginIntegrationEvent
{
private ?ResponseInterface $response = null;
/**
* @param mixed[] $parameters
* @param string $method
* @param mixed[] $settings
* @param string $authType
*/
public function __construct(
UnifiedIntegrationInterface $integration,
private $url,
private $parameters,
private $headers,
private $method,
private $settings,
private $authType,
) {
$this->integration = $integration;
}
/**
* @return mixed
*/
public function getUrl()
{
return $this->url;
}
/**
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* @return array
*/
public function getSettings()
{
return $this->settings;
}
/**
* @return string
*/
public function getAuthType()
{
return $this->authType;
}
public function setResponse(ResponseInterface $response): void
{
$this->response = $response;
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* @return mixed
*/
public function getHeaders()
{
return $this->headers;
}
public function setHeaders(array $headers): void
{
$this->headers = $headers;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Event;
class PluginIsPublishedEvent extends \Symfony\Contracts\EventDispatcher\Event
{
private string $message = '';
private bool $canPublish = true;
public function __construct(private int $value, private string $integrationName)
{
}
public function getValue(): int
{
return $this->value;
}
public function getMessage(): string
{
return $this->message;
}
public function setMessage(string $message): void
{
$this->message = $message;
}
public function isCanPublish(): bool
{
return $this->canPublish;
}
public function setCanPublish(bool $canPublish): void
{
$this->canPublish = $canPublish;
}
public function getIntegrationName(): string
{
return $this->integrationName;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Event;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\ORM\Mapping\ClassMetadata;
use Mautic\PluginBundle\Entity\Plugin;
use Symfony\Contracts\EventDispatcher\Event;
class PluginUpdateEvent extends Event
{
/**
* @param array<class-string, ClassMetadata> $metadata
*/
public function __construct(
private Plugin $plugin,
private string $oldVersion,
private array $metadata,
private ?Schema $installedSchema,
) {
}
public function getPlugin(): Plugin
{
return $this->plugin;
}
public function getOldVersion(): string
{
return $this->oldVersion;
}
/**
* @return array<class-string, ClassMetadata>
*/
public function getMetadata(): array
{
return $this->metadata;
}
public function getInstalledSchema(): ?Schema
{
return $this->installedSchema;
}
public function checkContext(string $pluginName): bool
{
return $pluginName === $this->plugin->getName();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
use Mautic\PluginBundle\Form\Type\IntegrationsListType;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CampaignSubscriber implements EventSubscriberInterface
{
use PushToIntegrationTrait;
public static function getSubscribedEvents(): array
{
return [
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
PluginEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignTriggerAction', 0],
];
}
public function onCampaignBuild(CampaignBuilderEvent $event): void
{
$action = [
'label' => 'mautic.plugin.actions.push_lead',
'description' => 'mautic.plugin.actions.tooltip',
'formType' => IntegrationsListType::class,
'formTheme' => '@MauticPlugin/FormTheme/Integration/layout.html.twig',
'eventName' => PluginEvents::ON_CAMPAIGN_TRIGGER_ACTION,
];
$event->addAction('plugin.leadpush', $action);
}
public function onCampaignTriggerAction(CampaignExecutionEvent $event): void
{
$config = $event->getConfig();
$config['campaignEvent'] = $event->getEvent();
$config['leadEventLog'] = $event->getLogEntry();
$lead = $event->getLead();
$errors = [];
$success = $this->pushToIntegration($config, $lead, $errors);
if (count($errors)) {
$log = $event->getLogEntry();
$log->appendToMetadata(
[
'failed' => 1,
'reason' => implode('<br />', $errors),
]
);
}
$event->setResult($success);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\FormBundle\Event\FormBuilderEvent;
use Mautic\FormBundle\Event\SubmissionEvent;
use Mautic\FormBundle\FormEvents;
use Mautic\PluginBundle\Form\Type\IntegrationsListType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FormSubscriber implements EventSubscriberInterface
{
use PushToIntegrationTrait;
public static function getSubscribedEvents(): array
{
return [
FormEvents::FORM_ON_BUILD => ['onFormBuild', 0],
FormEvents::ON_EXECUTE_SUBMIT_ACTION => ['onFormSubmitActionTriggered', 0],
];
}
public function onFormBuild(FormBuilderEvent $event): void
{
$event->addSubmitAction('plugin.leadpush', [
'group' => 'mautic.plugin.actions',
'description' => 'mautic.plugin.actions.tooltip',
'label' => 'mautic.plugin.actions.push_lead',
'formType' => IntegrationsListType::class,
'formTheme' => '@MauticPlugin/FormTheme/Integration/layout.html.twig',
'eventName' => FormEvents::ON_EXECUTE_SUBMIT_ACTION,
'template' => '@MauticPlugin/Action/integration.html.twig',
]);
}
public function onFormSubmitActionTriggered(SubmissionEvent $event): void
{
if (false === $event->checkContext('plugin.leadpush')) {
return;
}
$this->pushToIntegration($event->getActionConfig(), $event->getSubmission()->getLead());
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\PluginBundle\Event\PluginIntegrationRequestEvent;
use Mautic\PluginBundle\Helper\oAuthHelper;
use Mautic\PluginBundle\PluginEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This class can provide useful debugging information for API requests and responses.
* The information is displayed when a command is executed from the console and the -vv flag is passed to it.
*/
class IntegrationSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
PluginEvents::PLUGIN_ON_INTEGRATION_RESPONSE => ['onResponse', 0],
PluginEvents::PLUGIN_ON_INTEGRATION_REQUEST => ['onRequest', 0],
];
}
/*
* Request event
*/
public function onRequest(PluginIntegrationRequestEvent $event): void
{
$name = strtoupper($event->getIntegrationName());
$headers = var_export($event->getHeaders(), true);
$params = var_export($event->getParameters(), true);
$settings = var_export($event->getSettings(), true);
if (defined('IN_MAUTIC_CONSOLE') && defined('MAUTIC_CONSOLE_VERBOSITY')
&& MAUTIC_CONSOLE_VERBOSITY >= ConsoleOutput::VERBOSITY_VERY_VERBOSE) {
$output = new ConsoleOutput();
$output->writeln('<fg=magenta>REQUEST:</>');
$output->writeln('<fg=white>'.$event->getMethod().' '.$event->getUrl().'</>');
$output->writeln('<fg=cyan>'.$headers.'</>');
$output->writeln('');
$output->writeln('<fg=cyan>'.$params.'</>');
$output->writeln('');
$output->writeln('<fg=cyan>'.$settings.'</>');
} else {
$this->logger->debug("$name REQUEST URL: ".$event->getMethod().' '.$event->getUrl());
if ('' !== $headers) {
$hashedHeaders = oAuthHelper::sanitizeHeaderData($event->getHeaders());
$headers = var_export($hashedHeaders, true);
$this->logger->debug("$name REQUEST HEADERS: \n".$headers.PHP_EOL);
}
if ('' !== $params) {
$this->logger->debug("$name REQUEST PARAMS: \n".$params.PHP_EOL);
}
if ('' !== $settings) {
$this->logger->debug("$name REQUEST SETTINGS: \n".$settings.PHP_EOL);
}
}
}
/*
* Response event
*/
public function onResponse(PluginIntegrationRequestEvent $event): void
{
$response = $event->getResponse();
$headers = var_export($response->getHeaders(), true);
$name = strtoupper($event->getIntegrationName());
$isJson = isset($response->getHeaders()['Content-Type']) && preg_grep('/application\/json/', $response->getHeaders()['Content-Type']);
$json = $isJson ? str_replace(' ', ' ', json_encode(json_decode($response->getBody()), JSON_PRETTY_PRINT)) : '';
$xml = '';
$isXml = isset($response->getHeaders()['Content-Type']) && preg_grep('/text\/xml/', $response->getHeaders()['Content-Type']);
if ($isXml) {
$doc = new \DOMDocument('1.0');
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
$doc->loadXML($response->getBody());
$xml = $doc->saveXML();
}
if (defined('IN_MAUTIC_CONSOLE') && defined('MAUTIC_CONSOLE_VERBOSITY')
&& MAUTIC_CONSOLE_VERBOSITY >= ConsoleOutput::VERBOSITY_VERY_VERBOSE) {
$output = new ConsoleOutput();
$output->writeln(sprintf('<fg=magenta>RESPONSE: %d</>', $response->getStatusCode()));
$output->writeln('<fg=cyan>'.$headers.'</>');
$output->writeln('');
if ($isJson) {
$output->writeln('<fg=cyan>'.$json.'</>');
} elseif ($isXml) {
$output->writeln('<fg=cyan>'.$xml.'</>');
} else {
$output->writeln('<fg=cyan>'.$response->getBody().'</>');
}
} else {
$this->logger->debug("$name RESPONSE CODE: {$response->getStatusCode()}");
if ('' !== $headers) {
$this->logger->debug("$name RESPONSE HEADERS: \n".$headers.PHP_EOL);
}
if ('' !== $json || '' !== $xml || '' !== $response->getBody()) {
$body = "$name RESPONSE BODY: ";
if ($isJson) {
$body .= $json;
} elseif ($isXml) {
$body .= $xml;
} else {
$body = $response->getBody();
}
$this->logger->debug($body);
}
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event\CompanyEvent;
use Mautic\LeadBundle\Event\LeadEvent;
use Mautic\LeadBundle\LeadEvents;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\IntegrationRepository;
use Mautic\PluginBundle\Model\PluginModel;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LeadSubscriber implements EventSubscriberInterface
{
private const FEATURE_PUSH_LEAD = 'push_lead';
public function __construct(
private PluginModel $pluginModel,
private IntegrationRepository $integrationRepository,
) {
}
public static function getSubscribedEvents(): array
{
return [
LeadEvents::LEAD_PRE_DELETE => ['onLeadDelete', 0],
LeadEvents::LEAD_POST_SAVE => ['onLeadSave', 0],
LeadEvents::COMPANY_PRE_DELETE => ['onCompanyDelete', 0],
];
}
/*
* Delete lead event
*/
public function onLeadDelete(LeadEvent $event): bool
{
/** @var Lead $lead */
$lead = $event->getLead();
$integrationEntityRepo = $this->pluginModel->getIntegrationEntityRepository();
$integrationEntityRepo->findLeadsToDelete('lead%', $lead->getId());
return false;
}
/*
* Delete company event
*/
public function onCompanyDelete(CompanyEvent $event): bool
{
/** @var \Mautic\LeadBundle\Entity\Company $company */
$company = $event->getCompany();
$integrationEntityRepo = $this->pluginModel->getIntegrationEntityRepository();
$integrationEntityRepo->findLeadsToDelete('company%', $company->getId());
return false;
}
/*
* Change lead event
*/
public function onLeadSave(LeadEvent $event): void
{
/** @var Lead $lead */
$lead = $event->getLead();
$integrationEntityRepo = $this->pluginModel->getIntegrationEntityRepository();
if ($this->isAnyIntegrationEnabled()) {
$integrationEntityRepo->updateErrorLeads('lead-error', $lead->getId());
}
}
private function isAnyIntegrationEnabled(): bool
{
$integrations = $this->integrationRepository->getIntegrations();
foreach ($integrations as $integration) {
/** @var Integration $integration */
$supportedFeatures = $integration->getSupportedFeatures();
if ($integration->getIsPublished() && !empty($integration->getApiKeys()) && in_array(self::FEATURE_PUSH_LEAD, $supportedFeatures)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\EventListener;
use Mautic\PluginBundle\Bundle\PluginDatabase;
use Mautic\PluginBundle\Event\PluginInstallEvent;
use Mautic\PluginBundle\Event\PluginUpdateEvent;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PluginSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly PluginDatabase $pluginDatabase)
{
}
public function onInstall(PluginInstallEvent $event): void
{
$metadata = $event->getMetadata();
if (null === $metadata) {
return;
}
$this->pluginDatabase->installPluginSchema(
$metadata,
$event->getInstalledSchema()
);
}
public function onUpdate(PluginUpdateEvent $event): void
{
$this->pluginDatabase->onPluginUpdate($event->getPlugin());
}
/**
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
*/
public static function getSubscribedEvents(): array
{
return [
PluginEvents::ON_PLUGIN_INSTALL => ['onInstall', 0],
PluginEvents::ON_PLUGIN_UPDATE => ['onUpdate', 0],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\PluginBundle\Form\Type\IntegrationsListType;
use Mautic\PluginBundle\Helper\EventHelper;
use Mautic\PointBundle\Event\TriggerBuilderEvent;
use Mautic\PointBundle\PointEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PointSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
PointEvents::TRIGGER_ON_BUILD => ['onTriggerBuild', 0],
];
}
public function onTriggerBuild(TriggerBuilderEvent $event): void
{
$action = [
'group' => 'mautic.plugin.point.action',
'label' => 'mautic.plugin.actions.push_lead',
'formType' => IntegrationsListType::class,
// 'formTheme' => 'MauticPluginBundle:FormTheme:Integration',
'callback' => [EventHelper::class, 'pushLead'],
];
$event->addEvent('plugin.leadpush', $action);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Mautic\PluginBundle\EventListener;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
/**
* Static methods must be used due to the Point triggers not being converted to Events yet
* Once that happens, this can be converted to a standard method classes.
*
* Trait PushToIntegrationTrait
*/
trait PushToIntegrationTrait
{
/**
* @var IntegrationHelper
*/
protected static $integrationHelper;
/**
* Used by methodCalls to event subscribers.
*/
public function setIntegrationHelper(IntegrationHelper $integrationHelper): void
{
static::setStaticIntegrationHelper($integrationHelper);
}
/**
* Used by callback methods such as point triggers.
*/
public static function setStaticIntegrationHelper(IntegrationHelper $integrationHelper): void
{
static::$integrationHelper = $integrationHelper;
}
protected function pushToIntegration(array $config, Lead $lead, array &$errors = []): bool
{
return static::pushIt($config, $lead, $errors);
}
/**
* Used because the the Point trigger actions have not be converted to Events yet and thus must leverage a callback.
*/
protected static function pushIt($config, $lead, &$errors): bool
{
$integration = (!empty($config['integration'])) ? $config['integration'] : null;
$integrationCampaign = (!empty($config['config']['campaigns'])) ? $config['config']['campaigns'] : null;
$integrationMemberStatus = (!empty($config['campaign_member_status']['campaign_member_status']))
? $config['campaign_member_status']['campaign_member_status'] : null;
$services = static::$integrationHelper->getIntegrationObjects($integration);
$success = true;
foreach ($services as $s) {
/** @var AbstractIntegration $s */
$settings = $s->getIntegrationSettings();
if (!$settings->isPublished()) {
continue;
}
$personIds = null;
if (method_exists($s, 'pushLead')) {
if (!$personIds = $s->resetLastIntegrationError()->pushLead($lead, $config)) {
$success = false;
if ($error = $s->getLastIntegrationError()) {
$errors[] = $error;
}
}
}
if ($success && $integrationCampaign && method_exists($s, 'pushLeadToCampaign')) {
if (!$s->resetLastIntegrationError()->pushLeadToCampaign($lead, $integrationCampaign, $integrationMemberStatus)) {
$success = false;
if ($error = $s->getLastIntegrationError()) {
$errors[] = $error;
}
}
}
}
return $success;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Mautic\PluginBundle\Exception;
use Mautic\LeadBundle\Entity\Lead;
class ApiErrorException extends \Exception
{
private $contactId;
private ?Lead $contact = null;
private string $shortMessage = '';
/**
* @param string $message
* @param int $code
*/
public function __construct($message = 'API error', $code = 0, ?\Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* @return mixed
*/
public function getContactId()
{
return $this->contactId;
}
/**
* @param mixed $contactId
*
* @return ApiErrorException
*/
public function setContactId($contactId)
{
$this->contactId = $contactId;
return $this;
}
/**
* @return Lead
*/
public function getContact()
{
return $this->contact;
}
/**
* @return ApiErrorException
*/
public function setContact(Lead $contact)
{
$this->contact = $contact;
return $this;
}
public function getShortMessage(): string
{
return $this->shortMessage;
}
public function setShortMessage(string $shortMessage): ApiErrorException
{
$this->shortMessage = $shortMessage;
return $this;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Mautic\PluginBundle\Facade;
use Mautic\PluginBundle\Helper\ReloadHelper;
use Mautic\PluginBundle\Model\PluginModel;
use Symfony\Contracts\Translation\TranslatorInterface;
class ReloadFacade
{
public function __construct(
private PluginModel $pluginModel,
private ReloadHelper $reloadHelper,
private TranslatorInterface $translator,
) {
}
/**
* This method finds all plugins that needs to be enabled, disabled, installed and updated
* and do all those actions.
*
* Returns humanly understandable message about its doings.
*/
public function reloadPlugins(): string
{
$plugins = $this->pluginModel->getAllPluginsConfig();
$pluginMetadata = $this->pluginModel->getPluginsMetadata();
$installedPlugins = $this->pluginModel->getInstalledPlugins();
$installedPluginTables = $this->pluginModel->getInstalledPluginTables($pluginMetadata);
$installedPluginsSchemas = $this->pluginModel->createPluginSchemas($installedPluginTables);
$disabledPlugins = $this->reloadHelper->disableMissingPlugins($plugins, $installedPlugins);
$enabledPlugins = $this->reloadHelper->enableFoundPlugins($plugins, $installedPlugins);
$updatedPlugins = $this->reloadHelper->updatePlugins($plugins, $installedPlugins, $pluginMetadata, $installedPluginsSchemas);
$installedPlugins = $this->reloadHelper->installPlugins($plugins, $installedPlugins, $pluginMetadata, $installedPluginsSchemas);
$persist = array_values((array) ($disabledPlugins + $enabledPlugins + $updatedPlugins + $installedPlugins));
$this->pluginModel->saveEntities($persist);
// Alert the user to the number of additions
return $this->translator->trans(
'mautic.plugin.notice.reloaded',
[
'%added%' => count($installedPlugins),
'%disabled%' => count($disabledPlugins),
'%updated%' => count($updatedPlugins),
],
'flashes'
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Form\Constraint;
use Symfony\Component\Validator\Constraint;
class CanPublish extends Constraint
{
public string $message = 'mautic.lead_list.not_allowed_plugin_publish';
public string $integrationName;
public function getDefaultOption(): string
{
return 'integrationName';
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Form\Constraint;
use Mautic\PluginBundle\Event\PluginIsPublishedEvent;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CanPublishValidator extends ConstraintValidator
{
public function __construct(private EventDispatcherInterface $eventDispatcher)
{
}
public function validate(mixed $value, Constraint $constraint): void
{
if (1 !== $value) {
return;
}
if (!$constraint instanceof CanPublish) {
throw new \Symfony\Component\Validator\Exception\UnexpectedTypeException($constraint, CanPublish::class);
}
$event = new PluginIsPublishedEvent($value, $constraint->integrationName);
$event = $this->eventDispatcher->dispatch($event, PluginEvents::PLUGIN_IS_PUBLISHED_STATE_CHANGING);
if (!$event->isCanPublish()) {
$this->context->buildViolation($event->getMessage())
->addViolation();
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class CompanyFieldsType extends AbstractType
{
use FieldsTypeTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->buildFormFields($builder, $options, $options['integration_fields'], $options['mautic_fields'], 'company', $options['limit'], $options['start']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$this->configureFieldOptions($resolver, 'company');
}
public function getBlockPrefix(): string
{
return 'integration_company_fields';
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$this->buildFieldView($view, $options);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\StandAloneButtonType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Form\Constraint\CanPublish;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Integration>
*/
class DetailsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('isPublished', YesNoButtonGroupType::class, [
'constraints' => [
new CanPublish($options['integration'] ?? ''),
],
]);
/** @var AbstractIntegration $integrationObject */
$integrationObject = $options['integration_object'];
/** @var Integration $integration */
$integration = $options['data'];
$formSettings = $integrationObject->getFormDisplaySettings();
$decryptedKeys = $integrationObject->decryptApiKeys($integration->getApiKeys());
$keys = $integrationObject->getRequiredKeyFields();
if (!empty($formSettings['hide_keys'])) {
foreach ($formSettings['hide_keys'] as $key) {
unset($keys[$key]);
}
}
$builder->add(
'apiKeys',
KeysType::class,
[
'label' => false,
'integration_keys' => $keys,
'data' => $decryptedKeys,
'integration_object' => $integrationObject,
]
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($keys, $decryptedKeys, $options): void {
$data = $event->getData();
$form = $event->getForm();
$form->add(
'apiKeys',
KeysType::class,
[
'label' => false,
'integration_keys' => $keys,
'data' => $decryptedKeys,
'integration_object' => $options['integration_object'],
'is_published' => (int) $data['isPublished'],
]
);
}
);
if (!empty($formSettings['requires_authorization'])) {
$label = ($integrationObject->isAuthorized()) ? 'reauthorize' : 'authorize';
$builder->add(
'authButton',
StandAloneButtonType::class,
[
'attr' => [
'class' => 'btn btn-success btn-lg',
'onclick' => 'Mautic.initiateIntegrationAuthorization()',
'icon' => 'ri-key-2-line',
],
'label' => 'mautic.integration.form.'.$label,
'disabled' => false,
]
);
}
$features = $integrationObject->getSupportedFeatures();
$tooltips = $integrationObject->getSupportedFeatureTooltips();
if (!empty($features)) {
// Check to see if the integration is a new entry and thus not configured
$configured = null !== $integration->getId();
$enabledFeatures = $integration->getSupportedFeatures();
$data = ($configured) ? $enabledFeatures : $features;
$choices = [];
foreach ($features as $f) {
$choices['mautic.integration.form.feature.'.$f] = $f;
}
$builder->add(
'supportedFeatures',
ChoiceType::class,
[
'choices' => $choices,
'expanded' => true,
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'label' => 'mautic.integration.form.features',
'required' => false,
'data' => $data,
'choice_attr' => function ($val) use ($tooltips): array {
if (array_key_exists($val, $tooltips)) {
return [
'data-toggle' => 'tooltip',
'title' => $tooltips[$val],
];
}
return [];
},
]
);
}
$builder->add(
'featureSettings',
FeatureSettingsType::class,
[
'label' => 'mautic.integration.form.feature.settings',
'required' => true,
'data' => $integration->getFeatureSettings(),
'label_attr' => ['class' => 'control-label'],
'integration' => $options['integration'],
'integration_object' => $integrationObject,
'lead_fields' => $options['lead_fields'],
'company_fields' => $options['company_fields'],
]
);
$builder->add('name', HiddenType::class, ['data' => $options['integration']]);
$builder->add('in_auth', HiddenType::class, ['mapped' => false]);
$builder->add('buttons', FormButtonsType::class);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
$integrationObject->modifyForm($builder, $options);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Integration::class,
]
);
$resolver->setRequired(['integration', 'integration_object', 'lead_fields', 'company_fields']);
$resolver->setAllowedTypes('integration_object', [AbstractIntegration::class]);
}
public function getBlockPrefix(): string
{
return 'integration_details';
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class FeatureSettingsType extends AbstractType
{
public function __construct(
protected RequestStack $requestStack,
protected CoreParametersHelper $coreParametersHelper,
protected LoggerInterface $logger,
) {
}
/**
* @param FormBuilderInterface<array<mixed>|null> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$integrationObject = $options['integration_object'];
// add custom feature settings
$integrationObject->appendToForm($builder, $options['data'], 'features');
$leadFields = $options['lead_fields'];
$companyFields = $options['company_fields'];
$formModifier = function (FormInterface $form, $data, $method = 'get') use ($integrationObject, $leadFields, $companyFields): void {
$integrationName = $integrationObject->getName();
$session = $this->requestStack->getSession();
$limit = $session->get(
'mautic.plugin.'.$integrationName.'.lead.limit',
$this->coreParametersHelper->get('default_pagelimit')
);
$page = $session->get('mautic.plugin.'.$integrationName.'.lead.page', 1);
$companyPage = $session->get('mautic.plugin.'.$integrationName.'.company.page', 1);
$settings = [
'silence_exceptions' => false,
'feature_settings' => $data,
'ignore_field_cache' => (1 == $page && 'POST' !== strtoupper($method)) ? true : false,
];
try {
if (empty($fields)) {
$fields = $integrationObject->getFormLeadFields($settings);
$fields = $fields[0] ?? $fields;
}
if (isset($settings['feature_settings']['objects']) and in_array('company', $settings['feature_settings']['objects'])) {
if (empty($integrationCompanyFields)) {
$integrationCompanyFields = $integrationObject->getFormCompanyFields($settings);
}
if (isset($integrationCompanyFields['company'])) {
$integrationCompanyFields = $integrationCompanyFields['company'];
}
}
if (!is_array($fields)) {
$fields = [];
}
$error = '';
} catch (\Exception $e) {
$error = $e->getMessage();
$this->logger->error($e);
// Prevent pagination from confusing things by using the cache
$page = 1;
$fields = $integrationCompanyFields = [];
}
$enableDataPriority = $integrationObject->getDataPriority();
$form->add(
'leadFields',
FieldsType::class,
[
'label' => 'mautic.integration.leadfield_matches',
'required' => true,
'mautic_fields' => $leadFields,
'data' => $data,
'integration_fields' => $fields,
'enable_data_priority' => $enableDataPriority,
'integration' => $integrationObject->getName(),
'integration_object' => $integrationObject,
'limit' => $limit,
'page' => $page,
'mapped' => false,
'error_bubbling' => false,
]
);
if (!empty($integrationCompanyFields)) {
$form->add(
'companyFields',
CompanyFieldsType::class,
[
'label' => 'mautic.integration.companyfield_matches',
'required' => true,
'mautic_fields' => $companyFields,
'data' => $data,
'integration_fields' => $integrationCompanyFields,
'enable_data_priority' => $enableDataPriority,
'integration' => $integrationObject->getName(),
'integration_object' => $integrationObject,
'limit' => $limit,
'page' => $companyPage,
'mapped' => false,
'error_bubbling' => false,
]
);
}
if ('get' == $method && $error) {
$form->addError(new FormError($error));
}
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$formModifier($event->getForm(), $data);
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$formModifier($event->getForm(), $data, 'post');
}
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['integration', 'integration_object', 'lead_fields', 'company_fields']);
}
public function getBlockPrefix(): string
{
return 'integration_featuresettings';
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class FieldsType extends AbstractType
{
use FieldsTypeTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->buildFormFields($builder, $options, $options['integration_fields'], $options['mautic_fields'], '', $options['limit'], $options['start']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$this->configureFieldOptions($resolver, 'lead');
}
public function getBlockPrefix(): string
{
return 'integration_fields';
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$this->buildFieldView($view, $options);
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\ButtonGroupType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
trait FieldsTypeTrait
{
/**
* @param string $fieldObject
*/
protected function buildFormFields(
FormBuilderInterface $builder,
array $options,
array $integrationFields,
array $mauticFields,
$fieldObject,
$limit,
$start,
) {
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options, $integrationFields, $mauticFields, $fieldObject, $limit, $start): void {
$form = $event->getForm();
$index = 0;
$choices = [];
$requiredFields = [];
$optionalFields = [];
$group = [];
$fieldData = $event->getData();
foreach ($mauticFields as $key => $value) {
if (is_array($value)) {
$mauticFields[$key] = array_flip($value);
}
}
// First loop to build options
foreach ($integrationFields as $field => $details) {
$groupName = '0default';
if (is_array($details)) {
if (isset($details['group'])) {
if (!isset($choices[$details['group']])) {
$choices[$details['group']] = [];
}
$label = $details['optionLabel'] ?? $details['label'];
$group[$field] = $groupName = $details['group'];
$choices[$field] = $label;
} else {
$choices[$field] = $details['label'];
}
} else {
$choices[$field] = $details;
}
if (!isset($requiredFields[$groupName])) {
$requiredFields[$groupName] = [];
$optionalFields[$groupName] = [];
}
if (is_array($details) && (!empty($details['required']) || 'Email' == $choices[$field])) {
$requiredFields[$groupName][$field] = $details;
} else {
$optionalFields[$groupName][$field] = $details;
}
}
// Order the fields by label
ksort($requiredFields, SORT_NATURAL);
ksort($optionalFields, SORT_NATURAL);
$sortFieldsFunction = function ($a, $b): int {
if (is_array($a)) {
$aLabel = $a['optionLabel'] ?? $a['label'];
} else {
$aLabel = $a;
}
if (is_array($b)) {
$bLabel = $b['optionLabel'] ?? $b['label'];
} else {
$bLabel = $b;
}
return strnatcasecmp($aLabel, $bLabel);
};
$fields = [];
foreach ($requiredFields as $groupedFields) {
uasort($groupedFields, $sortFieldsFunction);
$fields = array_merge($fields, $groupedFields);
}
foreach ($optionalFields as $groupedFields) {
uasort($groupedFields, $sortFieldsFunction);
$fields = array_merge($fields, $groupedFields);
}
// Ensure that fields aren't hidden
if ($start > count($fields) || 0 == $options['page']) {
$start = 0;
}
$paginatedFields = array_slice($fields, $start, $limit);
$fieldsName = 'leadFields';
if ($fieldObject) {
$fieldsName = $fieldObject.'Fields';
}
if (isset($fieldData[$fieldsName])) {
$fieldData[$fieldsName] = $options['integration_object']->formatMatchedFields($fieldData[$fieldsName]);
}
foreach ($paginatedFields as $field => $details) {
$matched = isset($fieldData[$fieldsName][$field]);
$required = (int) (!empty($integrationFields[$field]['required']) || 'Email' == $choices[$field]);
++$index;
$form->add(
'label_'.$index,
TextType::class,
[
'label' => false,
'data' => $choices[$field],
'attr' => [
'class' => 'form-control integration-fields',
'data-required' => $required,
'data-label' => $choices[$field],
'placeholder' => $group[$field] ?? '',
'readonly' => true,
],
'by_reference' => true,
'mapped' => false,
]
);
if (isset($options['enable_data_priority']) and $options['enable_data_priority']) {
$updateName = 'update_mautic';
if ($fieldObject) {
$updateName .= '_'.$fieldObject;
}
$forceDirection = false;
$disabled = (isset($fieldData[$fieldsName][$field])) ? $options['integration_object']->isCompoundMauticField($fieldData[$fieldsName][$field]) : false;
$data = isset($fieldData[$updateName][$field]) ? (int) $fieldData[$updateName][$field] : 1;
// Force to use just one way for certainly fields
if (isset($fields[$field]['update_mautic'])) {
$data = (bool) $fields[$field]['update_mautic'];
$disabled = true;
$forceDirection = true;
}
$form->add(
$updateName.$index,
ButtonGroupType::class,
[
'choices' => [
'<btn class="btn-nospin ri-arrow-left-circle-line"></btn>' => 0,
'<btn class="btn-nospin ri-arrow-right-circle-line"></btn>' => 1,
],
'label' => false,
'data' => $data,
'placeholder' => false,
'attr' => [
'data-toggle' => 'tooltip',
'title' => 'mautic.plugin.direction.data.update',
'disabled' => $disabled,
'forceDirection'=> $forceDirection,
],
]
);
}
if (!$fieldObject) {
$mauticFields['mautic.lead.report.contact_id'] = 'mauticContactId';
$mauticFields['mautic.plugin.integration.contact.timeline.link'] = 'mauticContactTimelineLink';
$mauticFields['mautic.plugin.integration.contact.donotcontact.email'] = 'mauticContactIsContactableByEmail';
}
$form->add(
'm_'.$index,
ChoiceType::class,
[
'choices' => $mauticFields,
'label' => false,
'data' => $matched && isset($fieldData[$fieldsName][$field]) ? $fieldData[$fieldsName][$field] : '',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'field-selector',
'data-placeholder' => ' ',
'data-required' => $required,
'data-value' => $matched && isset($fieldData[$fieldsName][$field]) ? $fieldData[$fieldsName][$field] : '',
'data-choices' => $mauticFields,
],
]
);
$form->add(
'i_'.$index,
HiddenType::class,
[
'data' => $field,
'attr' => [
'data-required' => $required,
'data-value' => $field,
],
]
);
$form->add(
$field,
HiddenType::class,
[
'data' => $index,
'attr' => [
'data-required' => $required,
'data-value' => $index,
],
]
);
}
}
);
}
protected function configureFieldOptions(OptionsResolver $resolver, $object)
{
$resolver->setRequired(['integration_fields', 'mautic_fields', 'integration', 'integration_object', 'page']);
$resolver->setDefined([('lead' === $object) ? 'update_mautic' : 'update_mautic_company']);
$resolver->setDefaults(
[
'special_instructions' => function (Options $options) {
[$specialInstructions, $alertType] = $options['integration_object']->getFormNotes('leadfield_match');
return $specialInstructions;
},
'alert_type' => function (Options $options) {
[$specialInstructions, $alertType] = $options['integration_object']->getFormNotes('leadfield_match');
return $alertType;
},
'allow_extra_fields' => true,
'enable_data_priority' => false,
'totalFields' => fn (Options $options): int => count($options['integration_fields']),
'fixedPageNum' => fn (Options $options): float => ceil($options['totalFields'] / $options['limit']),
'limit' => 10,
'start' => fn (Options $options): int => (1 === (int) $options['page']) ? 0 : ((int) $options['page'] - 1) * (int) $options['limit'],
]
);
}
protected function buildFieldView(FormView $view, array $options)
{
$view->vars['specialInstructions'] = $options['special_instructions'];
$view->vars['alertType'] = $options['alert_type'];
$view->vars['integration'] = $options['integration'];
$view->vars['totalFields'] = $options['totalFields'];
$view->vars['page'] = $options['page'];
$view->vars['fixedPageNum'] = $options['fixedPageNum'];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class IntegrationCampaignsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'campaign_member_status',
ChoiceType::class,
[
'choices' => array_flip($options['campaignContactStatus']),
'attr' => [
'class' => 'form-control', ],
'label' => 'mautic.plugin.integration.campaigns.member.status',
'required' => false,
]
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
['campaignContactStatus' => []]);
}
public function getBlockPrefix(): string
{
return 'integration_campaign_status';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>|mixed>
*/
class IntegrationConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (null != $options['integration']) {
$options['integration']->appendToForm($builder, $options['data'], 'integration');
}
if (!empty($options['campaigns'])) {
$builder->add(
'campaigns',
ChoiceType::class,
[
'choices' => array_flip($options['campaigns']),
'attr' => [
'class' => 'form-control', 'onchange' => 'Mautic.getIntegrationCampaignStatus(this);', ],
'label' => 'mautic.plugin.integration.campaigns',
'placeholder' => 'mautic.plugin.config.campaign.member.chooseone',
'required' => false,
]
);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['integration']);
$resolver->setDefaults([
'label' => false,
'campaigns' => [],
]);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class IntegrationsListType extends AbstractType
{
public function __construct(
private IntegrationHelper $integrationHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$integrationObjects = $this->integrationHelper->getIntegrationObjects(null, $options['supported_features'], true);
$integrations = ['' => ''];
foreach ($integrationObjects as $object) {
$settings = $object->getIntegrationSettings();
if ($settings->isPublished()) {
$pluginName = $settings->getPlugin()->getName();
if (!isset($integrations[$pluginName])) {
$integrations[$pluginName] = [];
}
$integrations[$pluginName][$object->getDisplayName()] = $object->getName();
}
}
$builder->add(
'integration',
ChoiceType::class,
[
'choices' => $integrations,
'expanded' => false,
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'label' => 'mautic.integration.integration',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.integration.integration.tooltip',
'onchange' => 'Mautic.getIntegrationConfig(this);',
],
'required' => true,
'constraints' => [
new NotBlank(
['message' => 'mautic.core.value.required']
),
],
]
);
$formModifier = function (FormEvent $event) use ($integrationObjects): void {
$data = $event->getData();
$form = $event->getForm();
$statusChoices = [];
$campaignChoices = [];
if (!empty($data['integration'])) {
$integrationObject = $this->integrationHelper->getIntegrationObject($data['integration']);
if (method_exists($integrationObject, 'getCampaigns')) {
$campaigns = $integrationObject->getCampaigns();
if (isset($campaigns['records']) && !empty($campaigns['records'])) {
foreach ($campaigns['records'] as $campaign) {
$campaignChoices[$campaign['Id']] = $campaign['Name'];
}
}
}
if (method_exists($integrationObject, 'getCampaignMemberStatus') && isset($data['config']['campaigns'])) {
$campaignStatus = $integrationObject->getCampaignMemberStatus($data['config']['campaigns']);
if (isset($campaignStatus['records']) && !empty($campaignStatus['records'])) {
foreach ($campaignStatus['records'] as $campaignS) {
$statusChoices[$campaignS['Label']] = $campaignS['Label'];
}
}
}
}
$form->add(
'config',
IntegrationConfigType::class,
[
'label' => false,
'attr' => [
'class' => 'integration-config-container',
],
'integration' => isset($data['integration'], $integrationObjects[$data['integration']]) ? $integrationObjects[$data['integration']] : null,
'campaigns' => $campaignChoices,
'data' => $data['config'] ?? [],
]
);
$hideClass = (isset($data['campaign_member_status']) && !empty($data['campaign_member_status']['campaign_member_status'])) ? '' : ' hide';
$form->add(
'campaign_member_status',
IntegrationCampaignsType::class,
[
'label' => false,
'attr' => [
'class' => 'integration-campaigns-status'.$hideClass,
],
'campaignContactStatus' => $statusChoices,
'data' => $data['campaign_member_status'] ?? [],
]
);
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, $formModifier);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $formModifier);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['supported_features']);
$resolver->setDefaults(
[
'supported_features' => 'push_lead',
]
);
}
public function getBlockPrefix(): string
{
return 'integration_list';
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Mautic\PluginBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class KeysType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$object = $options['integration_object'];
$secretKeys = $object->getSecretKeys();
$requiredKeys = $object->getRequiredKeyFields();
foreach ($options['integration_keys'] as $key => $label) {
$isSecret = in_array($key, $secretKeys);
$required = (isset($requiredKeys[$key]));
// Password fields are going to be blank even if a value exists so only require if a password is not already saved
if ($isSecret && !empty($options['data'][$key])) {
$required = false;
}
$constraints = ($required)
? [
new Callback(
function ($validateMe, ExecutionContextInterface $context) use ($options): void {
if (empty($validateMe) && !empty($options['is_published'])) {
$context->buildViolation('mautic.core.value.required')->addViolation();
}
}
),
] : [];
$type = ($isSecret) ? PasswordType::class : TextType::class;
$builder->add(
$key,
$type,
[
'label' => $label,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'placeholder' => (PasswordType::class === $type) ? '**************' : '',
'autocomplete' => 'off',
],
'required' => $required,
'constraints' => $constraints,
'error_bubbling' => false,
]
);
}
$object->appendToForm($builder, $options['data'], 'keys');
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['integration_object', 'integration_keys']);
$resolver->setDefined(['secret_keys']);
$resolver->setDefaults(['secret_keys' => [], 'is_published' => true]);
}
public function getBlockPrefix(): string
{
return 'integration_keys';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Mautic\PluginBundle\Helper;
class Cleaner
{
public const FIELD_TYPE_STRING = 'string';
public const FIELD_TYPE_BOOL = 'boolean';
public const FIELD_TYPE_NUMBER = 'number';
public const FIELD_TYPE_DATETIME = 'datetime';
public const FIELD_TYPE_DATE = 'date';
/**
* @return bool|float|string
*/
public static function clean($value, $fieldType = self::FIELD_TYPE_STRING)
{
$clean = strip_tags(html_entity_decode($value, ENT_QUOTES));
switch ($fieldType) {
case self::FIELD_TYPE_BOOL:
return (bool) $clean;
case self::FIELD_TYPE_NUMBER:
return (float) $clean;
case self::FIELD_TYPE_DATETIME:
$dateTimeValue = new \DateTime($value);
return (!empty($clean)) ? $dateTimeValue->format('c') : '';
case self::FIELD_TYPE_DATE:
$dateTimeValue = new \DateTime($value);
return (!empty($clean)) ? $dateTimeValue->format('Y-m-d') : '';
default:
return $clean;
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Mautic\PluginBundle\Helper;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\PluginBundle\EventListener\PushToIntegrationTrait;
class EventHelper
{
use PushToIntegrationTrait;
public static function pushLead($config, $lead, EntityManagerInterface $em, IntegrationHelper $integrationHelper): bool
{
$contact = $em->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->getEntityWithPrimaryCompany($lead);
static::setStaticIntegrationHelper($integrationHelper);
$errors = [];
return static::pushIt($config, $contact, $errors);
}
}

View File

@@ -0,0 +1,618 @@
<?php
namespace Mautic\PluginBundle\Helper;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\CoreBundle\Helper\BundleHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Mautic\PluginBundle\Model\PluginModel;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Finder\Finder;
use Twig\Environment;
class IntegrationHelper
{
/**
* @var array<string, mixed>
*/
private array $integrations = [];
/**
* @var mixed[]
*/
private array $available = [];
/**
* @var array<string, mixed>
*/
private array $byFeatureList = [];
/**
* @var array<int, mixed>
*/
private array $byPlugin = [];
public function __construct(
private ContainerInterface $container,
protected EntityManager $em,
protected PathsHelper $pathsHelper,
protected BundleHelper $bundleHelper,
protected CoreParametersHelper $coreParametersHelper,
protected Environment $twig,
protected PluginModel $pluginModel,
) {
}
/**
* Get a list of integration helper classes.
*
* @param array|string $specificIntegrations
* @param array $withFeatures
* @param bool $alphabetical
* @param int|null $pluginFilter
* @param bool|false $publishedOnly
*
* @return array<AbstractIntegration>
*
* @throws \Doctrine\ORM\ORMException
*/
public function getIntegrationObjects($specificIntegrations = null, $withFeatures = null, $alphabetical = false, $pluginFilter = null, $publishedOnly = false): array
{
// Build the service classes
if ([] === $this->available) {
// Get currently installed integrations
$integrationSettings = $this->getIntegrationSettings();
// And we'll be scanning the addon bundles for additional classes, so have that data on standby
$plugins = $this->bundleHelper->getPluginBundles();
// Get a list of already installed integrations
$integrationRepo = $this->em->getRepository(Integration::class);
// get a list of plugins for filter
$installedPlugins = $this->pluginModel->getEntities(
[
'hydration_mode' => 'hydrate_array',
'index' => 'bundle',
'result_cache' => new ResultCacheOptions(Plugin::CACHE_NAMESPACE),
]
);
$newIntegrations = [];
// Scan the plugins for integration classes
foreach ($plugins as $plugin) {
// Do not list the integration if the bundle has not been "installed"
if (!isset($plugin['bundle']) || !isset($installedPlugins[$plugin['bundle']])) {
continue;
}
if (is_dir($plugin['directory'].'/Integration')) {
$finder = new Finder();
$finder->files()->name('*Integration.php')->in($plugin['directory'].'/Integration')->ignoreDotFiles(true);
$id = $installedPlugins[$plugin['bundle']]['id'];
$this->byPlugin[$id] = [];
$pluginReference = $this->em->getReference(Plugin::class, $id);
$pluginNamespace = str_replace('MauticPlugin', '', $plugin['bundle']);
foreach ($finder as $file) {
$integrationName = substr($file->getBaseName(), 0, -15);
if (!isset($integrationSettings[$integrationName])) {
$newIntegration = new Integration();
$newIntegration->setName($integrationName)
->setPlugin($pluginReference);
$integrationSettings[$integrationName] = $newIntegration;
$integrationContainerKey = strtolower("mautic.integration.{$integrationName}");
// Initiate the class in order to get the features supported
if ($this->container->has($integrationContainerKey)) {
$this->integrations[$integrationName] = $this->container->get($integrationContainerKey);
$features = $this->integrations[$integrationName]->getSupportedFeatures();
$newIntegration->setSupportedFeatures($features);
// Go ahead and stash it since it's built already
$this->integrations[$integrationName]->setIntegrationSettings($newIntegration);
$newIntegrations[] = $newIntegration;
unset($newIntegration);
}
}
/** @var Integration $settings */
$settings = $integrationSettings[$integrationName];
$this->available[$integrationName] = [
'isPlugin' => true,
'integration' => $integrationName,
'settings' => $settings,
'namespace' => $pluginNamespace,
];
// Sort by feature and plugin for later
$features = $settings->getSupportedFeatures();
foreach ($features as $feature) {
if (!isset($this->byFeatureList[$feature])) {
$this->byFeatureList[$feature] = [];
}
$this->byFeatureList[$feature][] = $integrationName;
}
$this->byPlugin[$id][] = $integrationName;
}
}
}
$coreIntegrationSettings = $this->getCoreIntegrationSettings();
// Scan core bundles for integration classes
foreach ($this->bundleHelper->getMauticBundles() as $coreBundle) {
if (
// Skip plugin bundles
str_contains($coreBundle['relative'], 'app/bundles')
// Skip core bundles without an Integration directory
&& is_dir($coreBundle['directory'].'/Integration')
) {
$finder = new Finder();
$finder->files()->name('*Integration.php')->in($coreBundle['directory'].'/Integration')->ignoreDotFiles(true);
$coreBundleNamespace = str_replace('Mautic', '', $coreBundle['bundle']);
foreach ($finder as $file) {
$integrationName = substr($file->getBaseName(), 0, -15);
if (!isset($coreIntegrationSettings[$integrationName])) {
$newIntegration = new Integration();
$newIntegration->setName($integrationName);
$integrationSettings[$integrationName] = $newIntegration;
$integrationContainerKey = strtolower("mautic.integration.{$integrationName}");
// Initiate the class in order to get the features supported
if ($this->container->has($integrationContainerKey)) {
$this->integrations[$integrationName] = $this->container->get($integrationContainerKey);
$features = $this->integrations[$integrationName]->getSupportedFeatures();
$newIntegration->setSupportedFeatures($features);
// Go ahead and stash it since it's built already
$this->integrations[$integrationName]->setIntegrationSettings($newIntegration);
$newIntegrations[] = $newIntegration;
} else {
continue;
}
}
/** @var Integration $settings */
$settings = $coreIntegrationSettings[$integrationName] ?? $newIntegration;
$this->available[$integrationName] = [
'isPlugin' => false,
'integration' => $integrationName,
'settings' => $settings,
'namespace' => $coreBundleNamespace,
];
}
}
}
// Save newly found integrations
if (!empty($newIntegrations)) {
$integrationRepo->saveEntities($newIntegrations);
unset($newIntegrations);
}
}
// Ensure appropriate formats
if (null !== $specificIntegrations && !is_array($specificIntegrations)) {
$specificIntegrations = [$specificIntegrations];
}
if (null !== $withFeatures && !is_array($withFeatures)) {
$withFeatures = [$withFeatures];
}
// Build the integrations wanted
if (!empty($pluginFilter)) {
// Filter by plugin
$filteredIntegrations = $this->byPlugin[$pluginFilter];
} elseif (!empty($specificIntegrations)) {
// Filter by specific integrations
$filteredIntegrations = $specificIntegrations;
} else {
// All services by default
$filteredIntegrations = array_keys($this->available);
}
// Filter by features
if (!empty($withFeatures)) {
$integrationsWithFeatures = [];
foreach ($withFeatures as $feature) {
if (isset($this->byFeatureList[$feature])) {
$integrationsWithFeatures = $integrationsWithFeatures + $this->byFeatureList[$feature];
}
}
$filteredIntegrations = array_intersect($filteredIntegrations, $integrationsWithFeatures);
}
$returnServices = [];
// Build the classes if not already
foreach ($filteredIntegrations as $integrationName) {
if (!isset($this->available[$integrationName]) || ($publishedOnly && !$this->available[$integrationName]['settings']->isPublished())) {
continue;
}
if (!isset($this->integrations[$integrationName])) {
$integration = $this->available[$integrationName];
$integrationContainerKey = strtolower("mautic.integration.{$integrationName}");
if ($this->container->has($integrationContainerKey)) {
$this->integrations[$integrationName] = $this->container->get($integrationContainerKey);
$this->integrations[$integrationName]->setIntegrationSettings($integration['settings']);
}
}
if (isset($this->integrations[$integrationName])) {
$returnServices[$integrationName] = $this->integrations[$integrationName];
}
}
foreach ($returnServices as $key => $value) {
if (!$value) {
unset($returnServices[$key]);
}
}
if (empty($alphabetical)) {
// Sort by priority
uasort($returnServices, function ($a, $b): int {
$aP = (int) $a->getPriority();
$bP = (int) $b->getPriority();
return $aP <=> $bP;
});
} else {
// Sort by display name
uasort($returnServices, function ($a, $b): int {
$aName = $a->getDisplayName();
$bName = $b->getDisplayName();
return strcasecmp($aName, $bName);
});
}
return $returnServices;
}
/**
* Get a single integration object.
*
* @return AbstractIntegration|false
*/
public function getIntegrationObject($name)
{
$integrationObjects = $this->getIntegrationObjects($name);
return $integrationObjects[$name] ?? false;
}
/**
* Gets a count of integrations.
*/
public function getIntegrationCount($plugin): int
{
if (!is_array($plugin)) {
$plugins = $this->coreParametersHelper->get('plugin.bundles');
if (array_key_exists($plugin, $plugins)) {
$plugin = $plugins[$plugin];
} else {
// It doesn't exist so return 0
return 0;
}
}
if (is_dir($plugin['directory'].'/Integration')) {
$finder = new Finder();
$finder->files()->name('*Integration.php')->in($plugin['directory'].'/Integration')->ignoreDotFiles(true);
return iterator_count($finder);
}
return 0;
}
/**
* Returns popular social media services and regex URLs for parsing purposes.
*
* @param bool $find If true, array of regexes to find a handle will be returned;
* If false, array of URLs with a placeholder of %handle% will be returned
*
* @return array
*
* @todo Extend this method to allow plugins to add URLs to these arrays
*/
public function getSocialProfileUrlRegex($find = true)
{
if ($find) {
// regex to find a match
return [
'twitter' => "/twitter.com\/(.*?)($|\/)/",
'facebook' => [
"/facebook.com\/(.*?)($|\/)/",
"/fb.me\/(.*?)($|\/)/",
],
'linkedin' => "/linkedin.com\/in\/(.*?)($|\/)/",
'instagram' => "/instagram.com\/(.*?)($|\/)/",
'pinterest' => "/pinterest.com\/(.*?)($|\/)/",
'klout' => "/klout.com\/(.*?)($|\/)/",
'youtube' => [
"/youtube.com\/user\/(.*?)($|\/)/",
"/youtu.be\/user\/(.*?)($|\/)/",
],
'flickr' => "/flickr.com\/photos\/(.*?)($|\/)/",
'skype' => "/skype:(.*?)($|\?)/",
];
} else {
// populate placeholder
return [
'twitter' => 'https://twitter.com/%handle%',
'facebook' => 'https://facebook.com/%handle%',
'linkedin' => 'https://linkedin.com/in/%handle%',
'instagram' => 'https://instagram.com/%handle%',
'pinterest' => 'https://pinterest.com/%handle%',
'klout' => 'https://klout.com/%handle%',
'youtube' => 'https://youtube.com/user/%handle%',
'flickr' => 'https://flickr.com/photos/%handle%',
'skype' => 'skype:%handle%?call',
];
}
}
/**
* Get array of integration entities.
*
* @return mixed
*/
public function getIntegrationSettings()
{
return $this->em->getRepository(Integration::class)->getIntegrations();
}
public function getCoreIntegrationSettings()
{
return $this->em->getRepository(Integration::class)->getCoreIntegrations();
}
/**
* Get the user's social profile data from cache or integrations if indicated.
*
* @param \Mautic\LeadBundle\Entity\Lead $lead
* @param array $fields
* @param bool $refresh
* @param string $specificIntegration
* @param bool $persistLead
* @param bool $returnSettings
*
* @return array
*/
public function getUserProfiles($lead, $fields = [], $refresh = false, $specificIntegration = null, $persistLead = true, $returnSettings = false)
{
$socialCache = $lead->getSocialCache();
$featureSettings = [];
if ($refresh) {
// regenerate from integrations
$now = new DateTimeHelper();
// check to see if there are social profiles activated
$socialIntegrations = $this->getIntegrationObjects($specificIntegration, ['public_profile', 'public_activity']);
/* @var \MauticPlugin\MauticSocialBundle\Integration\SocialIntegration $sn */
foreach ($socialIntegrations as $integration => $sn) {
$settings = $sn->getIntegrationSettings();
$features = $settings->getSupportedFeatures();
$identifierField = $this->getUserIdentifierField($sn, $fields);
if ($returnSettings) {
$featureSettings[$integration] = $settings->getFeatureSettings();
}
if ($identifierField && $settings->isPublished()) {
$profile = (!isset($socialCache[$integration])) ? [] : $socialCache[$integration];
// clear the cache
unset($profile['profile'], $profile['activity']);
if (in_array('public_profile', $features) && $sn->isAuthorized()) {
$sn->getUserData($identifierField, $profile);
}
if (in_array('public_activity', $features) && $sn->isAuthorized()) {
$sn->getPublicActivity($identifierField, $profile);
}
if (!empty($profile['profile']) || !empty($profile['activity'])) {
if (!isset($socialCache[$integration])) {
$socialCache[$integration] = [];
}
$socialCache[$integration]['profile'] = (!empty($profile['profile'])) ? $profile['profile'] : [];
$socialCache[$integration]['activity'] = (!empty($profile['activity'])) ? $profile['activity'] : [];
$socialCache[$integration]['lastRefresh'] = $now->toUtcString();
}
} elseif (isset($socialCache[$integration])) {
// integration is now not applicable
unset($socialCache[$integration]);
}
}
if ($persistLead && !empty($socialCache)) {
$lead->setSocialCache($socialCache);
$this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->saveEntity($lead);
}
} elseif ($returnSettings) {
$socialIntegrations = $this->getIntegrationObjects($specificIntegration, ['public_profile', 'public_activity']);
foreach ($socialIntegrations as $integration => $sn) {
$settings = $sn->getIntegrationSettings();
$featureSettings[$integration] = $settings->getFeatureSettings();
}
}
if ($specificIntegration) {
return ($returnSettings) ? [[$specificIntegration => $socialCache[$specificIntegration]], $featureSettings]
: [$specificIntegration => $socialCache[$specificIntegration]];
}
return ($returnSettings) ? [$socialCache, $featureSettings] : $socialCache;
}
/**
* @param bool $integration
*
* @return array
*/
public function clearIntegrationCache($lead, $integration = false)
{
$socialCache = $lead->getSocialCache();
if (!empty($integration)) {
unset($socialCache[$integration]);
} else {
$socialCache = [];
}
$lead->setSocialCache($socialCache);
$this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->saveEntity($lead);
return $socialCache;
}
/**
* Gets an array of the HTML for share buttons.
*/
public function getShareButtons()
{
static $shareBtns = [];
if (empty($shareBtns)) {
$socialIntegrations = $this->getIntegrationObjects(null, ['share_button'], true);
/**
* @var string $integration
* @var AbstractIntegration $details
*/
foreach ($socialIntegrations as $integration => $details) {
/** @var Integration $settings */
$settings = $details->getIntegrationSettings();
$featureSettings = $settings->getFeatureSettings();
$apiKeys = $details->decryptApiKeys($settings->getApiKeys());
$plugin = $settings->getPlugin();
$shareSettings = $featureSettings['shareButton'] ?? [];
// add the api keys for use within the share buttons
$shareSettings['keys'] = $apiKeys;
$shareBtns[$integration] = $this->twig->render($plugin->getBundle()."/Integration/$integration:share.html.twig", [
'settings' => $shareSettings,
]);
}
}
return $shareBtns;
}
/**
* Loops through field values available and finds the field the integration needs to obtain the user.
*
* @return bool
*/
public function getUserIdentifierField($integrationObject, $fields)
{
$identifierField = $integrationObject->getIdentifierFields();
$identifier = (is_array($identifierField)) ? [] : false;
$matchFound = false;
$findMatch = function ($f, $fields) use (&$identifierField, &$identifier, &$matchFound): void {
if (is_array($identifier)) {
// there are multiple fields the integration can identify by
foreach ($identifierField as $idf) {
$value = (is_array($fields[$f]) && isset($fields[$f]['value'])) ? $fields[$f]['value'] : $fields[$f];
if (!in_array($value, $identifier) && str_contains($f, $idf)) {
$identifier[$f] = $value;
if (count($identifier) === count($identifierField)) {
// found enough matches so break
$matchFound = true;
break;
}
}
}
} elseif ($identifierField === $f || str_contains($f, $identifierField)) {
$matchFound = true;
$identifier = (is_array($fields[$f])) ? $fields[$f]['value'] : $fields[$f];
}
};
$groups = ['core', 'social', 'professional', 'personal'];
$keys = array_keys($fields);
if (0 !== count(array_intersect($groups, $keys)) && count($keys) <= 4) {
// fields are group
foreach ($fields as $groupFields) {
$availableFields = array_keys($groupFields);
foreach ($availableFields as $f) {
$findMatch($f, $groupFields);
if ($matchFound) {
break;
}
}
}
} else {
$availableFields = array_keys($fields);
foreach ($availableFields as $f) {
$findMatch($f, $fields);
if ($matchFound) {
break;
}
}
}
return $identifier;
}
/**
* Get the path to the integration's icon relative to the site root.
*
* @return string
*/
public function getIconPath($integration)
{
$systemPath = $this->pathsHelper->getSystemPath('root');
$bundlePath = $this->pathsHelper->getSystemPath('bundles');
$pluginPath = $this->pathsHelper->getSystemPath('plugins');
$genericIcon = $bundlePath.'/PluginBundle/Assets/img/generic.png';
if (is_array($integration)) {
// A bundle so check for an icon
$icon = $pluginPath.'/'.$integration['bundle'].'/Assets/img/icon.png';
} elseif ($integration instanceof Plugin) {
// A bundle so check for an icon
$icon = $pluginPath.'/'.$integration->getBundle().'/Assets/img/icon.png';
} elseif ($integration instanceof UnifiedIntegrationInterface) {
return $integration->getIcon();
}
if (file_exists($systemPath.'/'.$icon)) {
return $icon;
}
return $genericIcon;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Mautic\PluginBundle\Helper;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\ORM\Mapping\ClassMetadata;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Event\PluginInstallEvent;
use Mautic\PluginBundle\Event\PluginUpdateEvent;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Caution: none of the methods persist data.
*/
class ReloadHelper
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
) {
}
/**
* Disables plugins that are in the database but are missing in the filesystem.
*/
public function disableMissingPlugins(array $allPlugins, array $installedPlugins): array
{
$disabledPlugins = [];
foreach ($installedPlugins as $plugin) {
if (!isset($allPlugins[$plugin->getBundle()]) && !$plugin->getIsMissing()) {
// files are no longer found
$plugin->setIsMissing(true);
$disabledPlugins[$plugin->getBundle()] = $plugin;
}
}
return $disabledPlugins;
}
/**
* Re-enables plugins that were disabled because they were missing in the filesystem
* but appeared in it again.
*/
public function enableFoundPlugins(array $allPlugins, array $installedPlugins): array
{
$enabledPlugins = [];
foreach ($installedPlugins as $plugin) {
if (isset($allPlugins[$plugin->getBundle()]) && $plugin->getIsMissing()) {
// files are no longer found
$plugin->setIsMissing(false);
$enabledPlugins[$plugin->getBundle()] = $plugin;
}
}
return $enabledPlugins;
}
/**
* Updates plugins that exist in the filesystem and in the database and their version changed.
*
* @param array<string, array<class-string, ClassMetadata>> $pluginMetadata
* @param array<string, Plugin> $installedPlugins
* @param array<string, Schema> $installedPluginsSchemas
*/
public function updatePlugins(array $allPlugins, array $installedPlugins, array $pluginMetadata, array $installedPluginsSchemas): array
{
$updatedPlugins = [];
foreach ($installedPlugins as $bundle => $plugin) {
if (isset($allPlugins[$bundle])) {
$pluginConfig = $allPlugins[$bundle];
$oldVersion = $plugin->getVersion();
$plugin = $this->mapConfigToPluginEntity($plugin, $pluginConfig);
// compare versions to see if an update is necessary
if ((empty($oldVersion) && !empty($plugin->getVersion())) || (!empty($oldVersion) && -1 === version_compare($oldVersion, $plugin->getVersion()))) {
$metadata = $pluginMetadata[$pluginConfig['namespace']] ?? null;
$installedSchema = isset($installedPluginsSchemas[$pluginConfig['namespace']])
? $installedPluginsSchemas[$allPlugins[$bundle]['namespace']] : null;
$event = new PluginUpdateEvent($plugin, $oldVersion, $metadata, $installedSchema);
$this->eventDispatcher->dispatch($event, PluginEvents::ON_PLUGIN_UPDATE);
unset($metadata, $installedSchema);
$updatedPlugins[$plugin->getBundle()] = $plugin;
}
}
}
return $updatedPlugins;
}
/**
* Installs plugins that does not exist in the database yet.
*
* @param array<string, array<class-string, ClassMetadata>> $pluginMetadata
*/
public function installPlugins(array $allPlugins, array $existingPlugins, array $pluginMetadata, array $installedPluginsSchemas): array
{
$installedPlugins = [];
foreach ($allPlugins as $bundle => $pluginConfig) {
if (!isset($existingPlugins[$bundle])) {
$entity = $this->mapConfigToPluginEntity(new Plugin(), $pluginConfig);
$metadata = $pluginMetadata[$pluginConfig['namespace']] ?? null;
$installedSchema = null;
if (isset($installedPluginsSchemas[$pluginConfig['namespace']]) && 0 !== count($installedPluginsSchemas[$pluginConfig['namespace']]->getTables())) {
$installedSchema = true;
}
$event = new PluginInstallEvent($entity, $metadata, $installedSchema);
$this->eventDispatcher->dispatch($event, PluginEvents::ON_PLUGIN_INSTALL);
$installedPlugins[$entity->getBundle()] = $entity;
}
}
return $installedPlugins;
}
private function mapConfigToPluginEntity(Plugin $plugin, array $config): Plugin
{
$plugin->setBundle($config['bundle']);
if (isset($config['config'])) {
$details = $config['config'];
if (isset($details['version'])) {
$plugin->setVersion($details['version']);
}
$plugin->setName(
$details['name'] ?? $config['base']
);
if (isset($details['description'])) {
$plugin->setDescription($details['description']);
}
if (isset($details['author'])) {
$plugin->setAuthor($details['author']);
}
}
return $plugin;
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace Mautic\PluginBundle\Helper;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Portions modified from https://code.google.com/p/simple-php-oauth/.
*/
class oAuthHelper
{
private $clientId;
private $clientSecret;
private $accessToken;
private $accessTokenSecret;
private $callback;
private $settings;
public function __construct(
UnifiedIntegrationInterface $integration,
private ?Request $request = null,
$settings = [],
) {
$clientId = $integration->getClientIdKey();
$clientSecret = $integration->getClientSecretKey();
$keys = $integration->getDecryptedApiKeys();
$this->clientId = $keys[$clientId] ?? null;
$this->clientSecret = $keys[$clientSecret] ?? null;
$authToken = $integration->getAuthTokenKey();
$this->accessToken = $keys[$authToken] ?? '';
$this->accessTokenSecret = $settings['token_secret'] ?? '';
$this->callback = $integration->getAuthCallbackUrl();
$this->settings = $settings;
}
public function getAuthorizationHeader($url, $parameters, $method): array
{
// Get standard OAuth headers
$headers = $this->getOauthHeaders();
if (!empty($this->settings['include_verifier']) && $this->request && $this->request->query->has('oauth_verifier')) {
$headers['oauth_verifier'] = $this->request->query->get('oauth_verifier');
}
if (!empty($this->settings['query'])) {
// Include query in the base string if appended
$parameters = array_merge($parameters, $this->settings['query']);
}
if (!empty($this->settings['double_encode_basestring_parameters'])) {
// Parameters must be encoded before going through buildBaseString
array_walk($parameters, function (&$val, $key, $oauth): void {
$val = $oauth->encode($val);
}, $this);
}
$signature = array_merge($headers, $parameters);
$base_info = $this->buildBaseString($url, $method, $signature);
$composite_key = $this->getCompositeKey();
$headers['oauth_signature'] = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true));
return [$this->buildAuthorizationHeader($headers), 'Expect:'];
}
/**
* Get composite key for OAuth 1 signature signing.
*/
private function getCompositeKey(): string
{
if (strlen($this->accessTokenSecret) > 0) {
$composite_key = $this->encode($this->clientSecret).'&'.$this->encode($this->accessTokenSecret);
} else {
$composite_key = $this->encode($this->clientSecret).'&';
}
return $composite_key;
}
/**
* Get OAuth 1.0 Headers.
*/
private function getOauthHeaders(): array
{
$oauth = [
'oauth_consumer_key' => $this->clientId,
'oauth_nonce' => $this->generateNonce(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
];
if (empty($this->settings['authorize_session']) && !empty($this->accessToken)) {
$oauth['oauth_token'] = $this->accessToken;
} elseif (!empty($this->settings['request_token'])) {
// OAuth1.a access_token request that requires the retrieved request_token to be appended
$oauth['oauth_token'] = $this->settings['request_token'];
}
if (!empty($this->settings['append_callback']) && !empty($this->callback)) {
$oauth['oauth_callback'] = urlencode($this->callback);
}
return $oauth;
}
/**
* Build base string for OAuth 1 signature signing.
*/
private function buildBaseString($baseURI, $method, $params): string
{
$r = $this->normalizeParameters($params);
return $method.'&'.$this->encode($baseURI).'&'.$this->encode($r);
}
/**
* Build header for OAuth 1 authorization.
*/
private function buildAuthorizationHeader($oauth): string
{
$r = 'Authorization: OAuth ';
$values = $this->normalizeParameters($oauth, true, true);
return $r.implode(', ', $values);
}
/**
* Normalize parameters.
*
* @param bool $encode
* @param bool $returnarray
*
* @return string|array<string,string>
*/
private function normalizeParameters($parameters, $encode = false, $returnarray = false, $normalized = [], $key = '')
{
// Sort by key
ksort($parameters);
foreach ($parameters as $k => $v) {
if (is_array($v)) {
$normalized = $this->normalizeParameters($v, $encode, true, $normalized, $k);
} else {
if ($key) {
// Multidimensional array; using foo=baz&foo=bar rather than foo[bar]=baz&foo[baz]=bar as this is
// what the server expects when creating the signature
$k = $key;
}
if ($encode) {
$normalized[] = $this->encode($k).'="'.$this->encode($v).'"';
} else {
$normalized[] = $k.'='.$v;
}
}
}
return $returnarray ? $normalized : implode('&', $normalized);
}
/**
* Returns an encoded string according to the RFC3986.
*/
public function encode($string): string
{
return str_replace('%7E', '~', rawurlencode($string));
}
/**
* OAuth1.0 nonce generator.
*
* @param int $bits
*/
private function generateNonce($bits = 64): string
{
$result = '';
$accumulatedBits = 0;
$random = mt_getrandmax();
for ($totalBits = 0; 0 != $random; $random >>= 1) {
++$totalBits;
}
$usableBits = intval($totalBits / 8) * 8;
while ($accumulatedBits < $bits) {
$bitsToAdd = min($totalBits - $usableBits, $bits - $accumulatedBits);
if (0 != $bitsToAdd % 4) {
// add bits in whole increments of 4
$bitsToAdd += 4 - $bitsToAdd % 4;
}
// isolate leftmost $bits_to_add from mt_rand() result
$moreBits = mt_rand() & ((1 << $bitsToAdd) - 1);
// format as hex (this will be safe)
$format_string = '%0'.($bitsToAdd / 4).'x';
$result .= sprintf($format_string, $moreBits);
$accumulatedBits += $bitsToAdd;
}
return $result;
}
/**
* @param string[] $data
*
* @return string[]
*/
public static function sanitizeHeaderData(array $data): array
{
foreach ($data as &$value) {
if (preg_match('/Authorization:\s+(Bearer|Basic)\s+(\S+)/', $value, $match)) {
$value = sprintf('Authorization: %s %s', $match[1], '[REDACTED]');
}
}
return $data;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Mautic\PluginBundle\Integration;
/**
* Used by SSO auth plugins that use credentials from the login form to authenticate.
*/
abstract class AbstractSsoFormIntegration extends AbstractSsoServiceIntegration
{
/**
* @return array
*/
public function getSupportedFeatures()
{
return [
'sso_form',
];
}
/**
* Get form settings; authorization is not needed since it is done when a user logs in.
*
* @return array<string, mixed>
*/
public function getFormSettings(): array
{
return [
'requires_callback' => false,
'requires_authorization' => false,
];
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Mautic\PluginBundle\Integration;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Form\Type\RoleListType;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* Used by SSO auth plugins that use OAuth2, etc means of logins.
*/
abstract class AbstractSsoServiceIntegration extends AbstractIntegration
{
/**
* Called after the user is authenticated with the 3rd party service to obtain the users
* details.
*
* @param $response mixed Typically the response from request to authenticating service
*
* @return mixed
*/
abstract public function getUser($response);
/**
* Get the user role for new users.
*
* @return bool|\Doctrine\Common\Proxy\Proxy|object|null
*
* @throws \Doctrine\ORM\ORMException
*/
public function getUserRole()
{
$featureSettings = $this->settings->getFeatureSettings();
$role = $featureSettings['new_user_role'] ?? false;
if ($role) {
return $this->em->getReference(Role::class, $role);
}
throw new AuthenticationException('mautic.integration.sso.error.no_role');
}
/**
* Returns if a new user should be created if authenticated and not found locally.
*/
public function shouldAutoCreateNewUser(): bool
{
$featureSettings = $this->settings->getFeatureSettings();
return isset($featureSettings['auto_create_user']) && (bool) $featureSettings['auto_create_user'];
}
/**
* Set the callback URL to sso_login.
*/
public function getAuthCallbackUrl()
{
return $this->router->generate('mautic_sso_login_check',
['integration' => $this->getName()],
\Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL // absolute
);
}
/**
* @param array $settings
* @param array $parameters
*
* @return bool|string
*/
public function ssoAuthCallback($settings = [], $parameters = [])
{
$response = $this->authCallback($settings, $parameters);
// Get user data
return $this->getUser($response);
}
/**
* Don't save the keys as they are only used to validate user login.
*
* @return array
*/
public function extractAuthKeys($data, $tokenOverride = null)
{
// Prepare the keys for extraction such as renaming, setting expiry, etc
$data = $this->prepareResponseForExtraction($data);
// parse the response
$authTokenKey = $tokenOverride ?: $this->getAuthTokenKey();
if (is_array($data) && isset($data[$authTokenKey])) {
return $data;
}
$error = $this->getErrorsFromResponse($data);
if (empty($error)) {
$error = $this->translator->trans('mautic.integration.error.genericerror', [], 'flashes');
}
throw new AuthenticationException($error);
}
/**
* @return array
*/
public function getSupportedFeatures()
{
return [
'sso_service',
];
}
/**
* Get form settings; authorization is not needed since it is done when a user logs in.
*
* @return array<string, mixed>
*/
public function getFormSettings(): array
{
return [
'requires_callback' => true,
'requires_authorization' => false,
];
}
/**
* @param Form|\Symfony\Component\Form\FormBuilder $builder
* @param array $data
* @param string $formArea
*/
public function appendToForm(&$builder, $data, $formArea): void
{
if ('features' == $formArea) {
$builder->add('auto_create_user',
YesNoButtonGroupType::class,
[
'label' => 'mautic.integration.sso.auto_create_user',
'data' => isset($data['auto_create_user']) && (bool) $data['auto_create_user'],
'attr' => [
'tooltip' => 'mautic.integration.sso.auto_create_user.tooltip',
],
]
);
$builder->add(
'new_user_role',
RoleListType::class,
[
'label' => 'mautic.integration.sso.new_user_role',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.integration.sso.new_user_role.tooltip',
],
]
);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Mautic\PluginBundle\Integration;
class IntegrationObject
{
/**
* @param string $type
* @param string $internalType
*/
public function __construct(
private $type,
private $internalType,
) {
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return string
*/
public function getInternalType()
{
return $this->internalType;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Mautic\PluginBundle\Integration;
/**
* Interface UnifiedIntegrationInterface is used for type hinting.
*/
interface UnifiedIntegrationInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle;
use Mautic\PluginBundle\Tests\DependencyInjection\Compiler\TestPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MauticPluginBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
if ('test' === $container->getParameter('kernel.environment')) {
$container->addCompilerPass(new TestPass());
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Mautic\PluginBundle\Model;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\PluginBundle\Entity\IntegrationEntity;
use Mautic\PluginBundle\Integration\IntegrationObject;
/**
* @extends FormModel<IntegrationEntity>
*/
class IntegrationEntityModel extends FormModel
{
public function getIntegrationEntityRepository()
{
return $this->em->getRepository(IntegrationEntity::class);
}
public function logDataSync(IntegrationObject $integrationObject): void
{
}
public function getSyncedRecords(IntegrationObject $integrationObject, $integrationName, $recordList, $internalEntityId = null)
{
if (!$formattedRecords = $this->formatListOfContacts($recordList)) {
return [];
}
$integrationEntityRepo = $this->getIntegrationEntityRepository();
return $integrationEntityRepo->getIntegrationsEntityId(
$integrationName,
$integrationObject->getType(),
$integrationObject->getInternalType(),
$internalEntityId,
null,
null,
false,
0,
0,
$formattedRecords
);
}
/**
* @return array<mixed, array<'id', mixed>>
*/
public function getRecordList($integrationObject): array
{
$recordList = [];
foreach ($integrationObject->getRecords() as $record) {
$recordList[$record['Id']] = [
'id' => $record['Id'],
];
}
return $recordList;
}
public function formatListOfContacts($recordList): ?string
{
if (empty($recordList)) {
return null;
}
$csList = is_array($recordList) ? implode('", "', array_keys($recordList)) : $recordList;
return '"'.$csList.'"';
}
public function getMauticContactsById($mauticContactIds, $integrationName, $internalObject)
{
if (!$formattedRecords = $this->formatListOfContacts($mauticContactIds)) {
return [];
}
$integrationEntityRepo = $this->getIntegrationEntityRepository();
return $integrationEntityRepo->getIntegrationsEntityId(
$integrationName,
null,
$internalObject,
null,
null,
null,
false,
0,
0,
$formattedRecords
);
}
/**
* @param int $id
*
* @return IntegrationEntity|null
*/
public function getEntityByIdAndSetSyncDate($id, \DateTime $dateTime)
{
$entity = $this->getIntegrationEntityRepository()->find($id);
if ($entity) {
$entity->setLastSyncDate($dateTime);
}
return $entity;
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Mautic\PluginBundle\Model;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Mautic\CoreBundle\Helper\BundleHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Field\FieldList;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\PluginBundle\Entity\Plugin;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* @extends FormModel<Plugin>
*/
class PluginModel extends FormModel
{
public function __construct(
protected FieldModel $leadFieldModel,
private FieldList $fieldList,
CoreParametersHelper $coreParametersHelper,
private BundleHelper $bundleHelper,
EntityManager $em,
CorePermissions $security,
EventDispatcherInterface $dispatcher,
UrlGeneratorInterface $router,
Translator $translator,
UserHelper $userHelper,
LoggerInterface $mauticLogger,
) {
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
}
/**
* @return \Mautic\PluginBundle\Entity\PluginRepository
*/
public function getRepository()
{
return $this->em->getRepository(Plugin::class);
}
public function getIntegrationEntityRepository()
{
return $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
}
public function getPermissionBase(): string
{
return 'plugin:plugins';
}
/**
* Get lead fields used in selects/matching.
*
* @return mixed[]
*/
public function getLeadFields(): array
{
return $this->fieldList->getFieldList();
}
/**
* Get Company fields.
*
* @return mixed[]
*/
public function getCompanyFields(): array
{
return $this->fieldList->getFieldList(true, true, ['isPublished' => true, 'object' => 'company']);
}
public function saveFeatureSettings($entity): void
{
$this->em->persist($entity);
$this->em->flush();
}
/**
* Loads config.php arrays for all plugins.
*
* @return array
*/
public function getAllPluginsConfig()
{
return $this->bundleHelper->getPluginBundles();
}
/**
* Loads all installed Plugin entities from database.
*
* @return Plugin[]
*/
public function getInstalledPlugins()
{
return $this->getEntities(
[
'index' => 'bundle',
]
);
}
/**
* Returns metadata for all plugins.
*
* @return array<string, array<class-string, ClassMetadata>>
*/
public function getPluginsMetadata(): array
{
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
$pluginsMetadata = [];
foreach ($allMetadata as $meta) {
$namespace = $meta->namespace;
if (str_contains($namespace, 'MauticPlugin')) {
$bundleName = preg_replace('/\\\Entity$/', '', $namespace);
if (!isset($pluginsMetadata[$bundleName])) {
$pluginsMetadata[$bundleName] = [];
}
$pluginsMetadata[$bundleName][$meta->getName()] = $meta;
}
}
return $pluginsMetadata;
}
/**
* Returns all tables of installed plugins.
*
* @param array<string, array<class-string, ClassMetadata>> $pluginsMetadata
*
* @return array<string, array<int, Table>>
*/
public function getInstalledPluginTables(array $pluginsMetadata): array
{
$currentSchema = $this->em->getConnection()->createSchemaManager()->introspectSchema();
$installedPluginsTables = [];
foreach ($pluginsMetadata as $bundleName => $pluginMetadata) {
foreach ($pluginMetadata as $meta) {
$table = $meta->getTableName();
if (!isset($installedPluginsTables[$bundleName])) {
$installedPluginsTables[$bundleName] = [];
}
if ($currentSchema->hasTable($table)) {
$installedPluginsTables[$bundleName][] = $currentSchema->getTable($table);
}
}
}
return $installedPluginsTables;
}
/**
* Generates new Schema objects for all installed plugins.
*/
public function createPluginSchemas(array $installedPluginsTables): array
{
$installedPluginsSchemas = [];
foreach ($installedPluginsTables as $bundleName => $tables) {
$installedPluginsSchemas[$bundleName] = new Schema($tables);
}
return $installedPluginsSchemas;
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Mautic\PluginBundle;
/**
* Events available for PluginEvents.
*/
final class PluginEvents
{
/**
* The mautic.plugin_on_integration_config_save event is dispatched when an integration's configuration is saved.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_CONFIG_SAVE = 'mautic.plugin_on_integration_config_save';
/**
* The mautic.plugin_on_integration_keys_encrypt event is dispatched prior to encrypting keys to be stored into the database.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationKeyEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_KEYS_ENCRYPT = 'mautic.plugin_on_integration_keys_encrypt';
/**
* The mautic.plugin_on_integration_keys_decrypt event is dispatched after fetching and decrypting keys from the database.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationKeyEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_KEYS_DECRYPT = 'mautic.plugin_on_integration_keys_decrypt';
/**
* The mautic.plugin_on_integration_keys_merge event is dispatched after new keys are merged into existing ones.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationKeyEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_KEYS_MERGE = 'mautic.plugin_on_integration_keys_merge';
/**
* The mautic.plugin_on_integration_request event is dispatched before a request is made.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationRequestEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_REQUEST = 'mautic.plugin_on_integration_request';
/**
* The mautic.plugin_on_integration_response event is dispatched after a request is made.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationRequestEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_RESPONSE = 'mautic.plugin_on_integration_response';
/**
* The mautic.plugin_on_integration_auth_redirect event is dispatched when an authorization URL is generated and before the user is redirected to it.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationAuthRedirectEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_AUTH_REDIRECT = 'mautic.plugin_on_integration_auth_redirect';
/**
* The mautic.plugin.on_campaign_trigger_action event is fired when the campaign action triggers.
*
* The event listener receives a
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
*
* @var string
*/
public const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.plugin.on_campaign_trigger_action';
/**
* The mautic.plugin_on_integration_get_auth_callback_url event is dispatched when generating the redirect/callback URL.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationAuthCallbackUrlEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_GET_AUTH_CALLBACK_URL = 'mautic.plugin_on_integration_get_auth_callback_url';
/**
* The mautic.plugin_on_integration_form_display event is dispatched when fetching display settings for the integration's config form.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationFormDisplayEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_FORM_DISPLAY = 'mautic.plugin_on_integration_form_display';
/**
* The mautic.plugin_on_integration_form_build event is dispatched when building an integration's config form.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationFormBuildEvent instance.
*
* @var string
*/
public const PLUGIN_ON_INTEGRATION_FORM_BUILD = 'mautic.plugin_on_integration_form_build';
/**
* The mautic.plugin.on_form_submit_action_triggered event is dispatched when a plugin related submit action is executed.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginIntegrationFormBuildEvent instance.
*
* @var string
*/
public const ON_FORM_SUBMIT_ACTION_TRIGGERED = 'mautic.plugin.on_form_submit_action_triggered';
/**
* The mautic.plugin.on_plugin_update event is dispatched when a plugin is updated.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginUpdateEvent instance.
*
* @var string
*/
public const ON_PLUGIN_UPDATE = 'mautic.plugin.on_plugin_update';
/**
* The mautic.plugin.on_plugin_install event is dispatched when a plugin is installed.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginInstallEvent instance.
*
* @var string
*/
public const ON_PLUGIN_INSTALL = 'mautic.plugin.on_plugin_install';
/**
* The mautic.plugin.is_published_state_changing event is dispatched when a user tries to change the published state of a plugin.
*
* The event listener receives a Mautic\PluginBundle\Event\PluginPublishedEvent instance.
*
* @var string
*/
public const PLUGIN_IS_PUBLISHED_STATE_CHANGING= 'mautic.plugin.is_published_state_changing';
}

View File

@@ -0,0 +1,17 @@
{% extends '@MauticForm/Action/base_form_action.html.twig' %}
{% block action_label %}
{% include '@MauticCore/Helper/_tag.html.twig' with {
tags: [
{
label: action.properties.integration,
icon: 'ri-plug-line',
color: 'warm-gray',
attributes: securityIsGranted('plugin:plugins:manage') ? {
href: path('mautic_plugin_index'),
'target': '_blank'
} : {}
}
]
} %}
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% set contentOnly = true %}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block headerTitle '' %}
{% block content %}
{{- assetAddScriptDeclaration('Mautic.handleIntegrationCallback("'~integration~'", "'~csrfToken~'", "'~code~'", "'~callbackUrl~'", "'~clientIdKey~'", "'~clientSecretKey~'");', 'bodyClose') -}}
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% set contentOnly = true %}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent 'social' %}
{% block headerTitle '' %}
{% block content %}
{% set data = data|json_encode %}
<script>
function postFormHandler() {
var opener = window.opener;
if (opener && typeof opener.postAuthCallback == 'function') {
opener.postAuthCallback({$data});
} else {
Mautic.refreshIntegrationForm();
}
window.close()
}
{% if message is not empty and 'success' is same as alert %}
(function() { postFormHandler(); })();
{% endif %}
</script>
{% if message is not empty %}
<div class="alert alert-{{ alert }}">
{{ message|purify }}
</div>
{% endif %}
<div class="row">
<div class="col-sm-12 text-center">
<a class="btn btn-lg btn-primary" href="javascript:void(0);" onclick="postFormHandler();">
{{ 'mautic.integration.closewindow'|trans }}
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,226 @@
{% block integration_company_fields_row %}
{%- set containerId = 'companyFieldsContainer' -%}
{%- set numberOfFields = (form.offsetExists('update_mautic_company1')) ? 5 : 4 -%}
{%- set object = 'company' -%}
{{- block('fields_row') -}}
{% endblock %}
{% block integration_fields_row %}
{%- set containerId = 'leadFieldsContainer' -%}
{%- set numberOfFields = (form.offsetExists('update_mautic1')) ? 5 : 4 -%}
{%- set object = 'lead' -%}
{{- block('fields_row') -}}
{% endblock %}
{% block _integration_details_supportedFeatures_row %}
{% set attr = form.vars.attr %}
{% set builtin = formSettings.builtin_features|default([]) %}
{% set showLabel = builtin|length != form.children|length %}
<div class="row">
<div class="col-sm-12">
{% if showLabel %}
<h4 class="mb-sm">{{ form.vars['label']|trans }}</h4>
{% endif %}
{% if formNotes.supported_features is defined and formNotes.supported_features is not empty %}
<div class="alert alert-{{ formNotes['supported_features']['type'] }}">
{{ formNotes['supported_features']['note']|trans }}
</div>
{% endif %}
{% for child in form.children %}
{% if child.vars.value not in builtin %}
<div class="checkbox" >
<label>
{{ form_widget(child, {'attr': attr}) }}
{{ child.vars.label|trans }}
</label>
</div>
{% else %}
{{ child.isRendered() }}
<input type="hidden" id="{{ child.vars['id'] }}" name="{{ child.vars['full_name'] }}" value="{{ child.vars['value']|e }}" />
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
{% block _integration_details_featureSettings_row %}
<div class="row">
<div class="col-sm-12">
<h4 class="mb-sm mt-lg">
{{ form.vars['label']|trans }}
</h4>
{% if formNotes.features is defined and formNotes.features is not empty %}
<div class="alert alert-{{ formNotes['features']['type'] }}">
{{ formNotes['features']['note']|trans }}
</div>
{% endif %}
{{ form_widget(form) }}
</div>
</div>
{% endblock %}
{% block fields_row %}
{#
Variables
- containerId (required, string)
- numberOfFields (required, int)
- object (required, string)
- form
- specialInstructions (optional)
If set, `alertType` is required
- alertType (conditional)
#}
{# @var int $numberOfFields #}
{%- set rowCount = 0 -%}
{%- set indexCount = 1 -%}
<div class="row fields-container" id="{{ containerId }}">
{% if specialInstructions is defined and specialInstructions is not empty %}
<div class="alert alert-{{ alertType }}">
{{- specialInstructions|trans -}}
</div>
{% endif %}
{% if form.vars.errors|length > 0 %}
<div class="alert alert-danger">
{% for error in form.vars.errors %}
<p>{{ error.message }}</p>
{% endfor %}
</div>
{% endif %}
<div class="{{ object }}-field form-group col-xs-12">
<div class="row">
<div class="mb-xs col-sm-{{ (5 == numberOfFields) ? 5 : 6 }} text-center"><h4>{{ 'mautic.plugins.integration.fields'|trans }}</h4></div>
{% if 5 == numberOfFields -%}
<div class="col-sm-2"></div>
{%- endif %}
<div class="mb-xs col-sm-{{ (5 == numberOfFields) ? 5 : 6 }} text-center"><h4>{{ 'mautic.plugins.mautic.fields'|trans }}</h4></div>
</div>
{% for child in form.children %}
{% set selected = false %}
{% set isRequired = child.vars.attr['data-required'] is defined and child.vars.attr['data-required'] is not same as 0 %}
{% if rowCount is divisible by(numberOfFields) %}
<div id="{{ object }}-{{ rowCount }}" class="field-container row {% if 5 != numberOfFields %}pb-md{% endif %}">
{% endif %}
{% set rowCount = rowCount + 1 %}
{% if 'hidden' == child.vars.block_prefixes[1] %}
{{ form_row(child) }}
{% else %}
{% set class = '' %}
{% set remainder = rowCount % numberOfFields %}
{% if 1 == remainder or 3 == remainder %}
{% set class = (5 == numberOfFields) ? 'col-sm-5' : 'col-sm-6' %}
{% elseif 2 == remainder %}
{% set class = (5 == numberOfFields) ? 'col-sm-2' : 'col-sm-6' %}
{% endif %}
{% endif %}
{% if ('label_' ~ indexCount) == child.vars.name %}
{% if isRequired %}
{% set name = child.vars.full_name %}
<input type="hidden" value="{{ child.vars['attr']['data-label'] }}" name="{{ name }}" />
{% endif %}
<div class="pl-xs pr-xs {{ class }} {% if isRequired %}has-error{% endif %}">
<div class="placeholder" data-placeholder="{{ child.vars.attr.placeholder }}">
<input type="text"
id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
class="{{ child.vars.attr.class }}"
value="{{ child.vars.attr['data-label']|default('')|e }}" readonly />
</div>
</div>
{% endif %}
{%- if 'update_mautic' in child.vars.name -%}
<div class="pr-xs {{ class }}" style="padding-left: 8px;" data-toggle="tooltip" title="{{ 'mautic.plugin.direction.data.update'|trans }}">
<div class="row">
<div class="form-group col-xs-12 ">
<div class="choice-wrapper">
<div class="btn-group btn-block" data-toggle="buttons" {% if child.vars['attr']['forceDirection'] %}data-force-direction="1"{% endif %}>
{% set checked = '0' == child.vars.value %}
<label class="btn-arrow{{ indexCount }} btn btn-ghost {% if checked %}active{% endif %} {% if child.vars['attr']['disabled'] %}disabled{% endif %}">
<input type="radio"
id="{{ child.vars['id'] }}_0"
name="{{ child.vars['full_name'] }}"
title=""
autocomplete="false"
value="0"
onchange="Mautic.matchedFields({{ indexCount }}, '{{ object }}', '{{ integration }}')"
{% if checked %}checked="checked"{% endif %}
{% if child.vars['attr']['disabled'] %}disabled{% endif %}>
<btn class="btn-nospin ri-arrow-left-circle-line"></btn>
</label>
{% set checked = '1' == child.vars.value %}
<label class="btn-arrow{{ indexCount }} btn btn-ghost {% if checked %}active{% endif %} {% if child.vars['attr']['disabled'] %}disabled{% endif %}">
<input type="radio" id="{{ child.vars['id'] }}_1"
name="{{ child.vars['full_name'] }}"
title=""
autocomplete="false"
value="1"
onchange="Mautic.matchedFields({{ indexCount }}, '{{ object }}', '{{ integration }}')"
{% if '1' == child.vars['value'] %}checked="checked"{% endif %}
{% if child.vars['attr']['disabled'] %}disabled{% endif %}>
<btn class="btn-nospin ri-arrow-right-circle-line"></btn>
</label>
</div>
</div>
</div>
</div>
</div>
{%- endif -%}
{% if ('m_' ~ indexCount) == child.vars.name %}
<div class="pl-xs pr-xs {{ class }}">
{% if isRequired %}<div class="has-errors">{% endif %}
<select id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
class="{{ child.vars.attr.class }}"
data-placeholder=""
autocomplete="false" onchange="Mautic.matchedFields({{ indexCount }}, '{{ object }}', '{{ integration }}')">
<option value=""></option>
{%- set mauticChoices = child.vars.attr['data-choices'] -%}
{% for keyLabel, options in mauticChoices %}
{% if options is iterable %}
<optgroup label="{{ keyLabel }}">
{% for optionLabel, keyValue in options %}
<option value="{{ keyValue|e }}" {% if keyValue == child.vars.data %}selected{% set selected = true %}{% elseif selected is defined and selected is empty and '-1' == keyValue %}selected{% endif %}>
{{- optionLabel|trans -}}
</option>
{% endfor %}
</optgroup>
{% else %}
<option value="{{ options|e }}"{% if options == child.vars.data %}selected{% set selected = true %}{% elseif selected is defined and selected is empty and '-1' == options %}selected{% endif %}>
{{- keyLabel|trans -}}
</option>
{% endif %}
{% endfor %}
</select>
{% if isRequired %}</div>{% endif %}
</div>
{% endif %}
{% if rowCount is divisible by(numberOfFields) %}
</div>
{% set indexCount = indexCount + 1 %}
{% endif %}
{% endfor %}
</div>
{% if (indexCount - 1) < totalFields %}
<div class="panel-footer">
{{ include('@MauticCore/Helper/pagination.html.twig', {
'page': page,
'fixedPages': fixedPageNum,
'fixedLimit': true,
'target': '#IntegrationEditModal',
'totalItems': totalFields,
'jsCallback': 'Mautic.getIntegrationFields',
'jsArguments': [
{
'object': object,
'integration': integration,
},
],
}) }}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,166 @@
{#
Variables
- form
- description
- formSettings
- formNotes
- callbackUrl
- activeTab
- formThemes (array)
May include one or more form themes that need to be applied
#}
{%- form_theme form with formThemes -%}
<!-- form themes: {{ formThemes|join(', ') }} -->
{%- set nSupportedFeatures = form.supportedFeatures is defined ? form.supportedFeatures|length : 0 -%}
{%- set hasSupportedFeatures = nSupportedFeatures > 0 -%}
{%- set nFeatureSettings = form.featureSettings is defined ? form.featureSettings|length : 0 -%}
{%- set hasFields = ((formSettings.dynamic_contact_fields is defined and formSettings.dynamic_contact_fields is not empty) or form.featureSettings is defined) and form.featureSettings.leadFields|length > 0
-%}
{# Unset if set to prevent features tab from showing when there's no feature to show #}
{%- if not hasFields %}{% do form.featureSettings.leadFields.setRendered() %}{% set nFeatureSettings = nFeatureSettings - 1 %}{% endif -%}
{%- set hideContactFieldTab = (hasFields and formSettings.dynamic_contact_fields is defined and formSettings.dynamic_contact_fields is not empty and form.featureSettings.leadFields|length == 0) -%}
{%- set hasFeatureSettings = (
form.featureSettings is defined
and (
(hasFields and nFeatureSettings > 1)
or
(not hasFields and nFeatureSettings > 0)
)
) -%}
{%- if not hasFeatureSettings and form.featureSettings is defined %}{% do form.featureSettings.setRendered() %}{% endif -%}
{%- set hasCompanyFields = form.featureSettings.companyFields is defined and form.featureSettings.companyFields|length > 0 -%}
{%- set companyFieldHtml = hasCompanyFields ? form_row(form.featureSettings.companyFields) : '' -%}
{%- set fieldHtml = hasFields ? form_row(form.featureSettings.leadFields) -%}
{%- set fieldLabel = hasFields ? form.featureSettings.leadFields.vars.label -%}
{%- set fieldTabClass = (hasFields and hideContactFieldTab == false) ?: 'hide' -%}
{%- set hasLeadFieldErrors = hasFields and formContainsErrors(form.featureSettings.leadFields) -%}
{%- set hasCompanyFieldErrors = hasCompanyFields and formContainsErrors(form.featureSettings.companyFields) -%}
{%- if form.featureSettings.leadFields is defined %}{% do form.featureSettings.leadFields.setRendered() %}{% endif -%}
{%- if form.featureSettings.companyFields is defined %}{% do form.featureSettings.companyFields.setRendered() %}{% endif -%}
{%- if description is not empty -%}
<div class="alert alert-info">
{{- description|purify -}}
</div>
{%- endif -%}
<!-- tabs controls -->
<ul class="nav nav-tabs nav-tabs-line">
<li class="{% if 'details-container' == activeTab %}active{% endif %}" id="details-tab">
<a href="#details-container" role="tab" data-toggle="tab">{{ 'mautic.plugin.integration.tab.details'|trans }}</a>
</li>
{%- if hasSupportedFeatures or hasFeatureSettings -%}
<li class="" id="features-tab">
<a href="#features-container" role="tab" data-toggle="tab">
{{- 'mautic.plugin.integration.tab.features'|trans -}}
{%- if (hasSupportedFeatures and formContainsErrors(form.supportedFeatures)) or (hasFeatureSettings and formContainsErrors(form.featureSettings, ['leadFields'])) -%}
<i class="ri-fw ri-alert-fill text-danger"></i>
{%- endif -%}
</a>
</li>
{%- endif -%}
{%- if hasFields -%}
<li class="{{ fieldTabClass }} {% if 'leadFieldsContainer' == activeTab %}active{% endif %}" id="fields-tab">
<a href="#fields-container" role="tab" data-toggle="tab">
{{- 'mautic.plugin.integration.tab.fieldmapping'|trans -}}
{%- if hasLeadFieldErrors -%}
<i class="ri-fw ri-alert-fill text-danger"></i>
{%- endif -%}
</a>
</li>
{%- endif -%}
{%- if companyFieldHtml is not empty -%}
<li class="{{ fieldTabClass }} {% if 'companyFieldsContainer' == activeTab %}active{% endif %}" id="company-fields-tab">
<a href="#company-fields-container" role="tab" data-toggle="tab">
{{- 'mautic.plugin.integration.tab.companyfieldmapping'|trans -}}
{%- if hasCompanyFieldErrors -%}
<i class="ri-fw ri-alert-fill text-danger"></i>
{% endif %}
</a>
</li>
{%- endif -%}
</ul>
<!--/ tabs controls -->
{{- form_start(form) -}}
<div class="tab-content pa-md">
<div class="tab-pane fade {% if 'details-container' == activeTab %}in active{% endif %} bdr-w-0" id="details-container">
{{- form_row(form.isPublished) -}}
{%- if form.virtual is defined %}{{ form_row(form.virtual) }}{% endif %}
{%- if form.apiKeys is defined -%}
{{- form_row(form.apiKeys) -}}
{%- if formNotes.authorization is defined -%}
<div class="alert alert-{{ formNotes.authorization.type }}">
{{- formNotes.authorization.note|purify -}}
</div>
{%- endif -%}
<div class="row">
{%- if form.apiKeys|length > 0 and callbackUrl is not empty -%}
<div class="well well-sm">
{{- 'mautic.integration.callbackuri'|trans }}<br/>
{{ include('@MauticCore/Components/code-snippet.html.twig', {
variant: 'single',
innerText: callbackUrl,
className: 'layer-two mt-sm'
}) }}
</div>
{%- endif -%}
</div>
{%- if form.authButton is defined -%}
<div class="row">
<div class="col-xs-12 text-center">
{{- form_widget(form.authButton, {'attr': {'class': 'btn btn-success btn-lg'}}) -}}
</div>
</div>
{%- endif -%}
{%- endif -%}
{%- if formNotes.custom is defined -%}
{%- if formNotes.custom is string -%}
{{ formNotes.custom|purify }}
{%- elseif formNotes.custom.custom is defined and formNotes.custom.template is string -%}
<!-- start: "{{ formNotes.custom.template }}" -->
{{ include(formNotes.custom.template, formNotes.custom.parameters|default([]), ignore_missing=true) }}
<!-- end: "{{ formNotes.custom.template }}" -->
{%- endif -%}
{%- endif -%}
</div>
{%- if hasSupportedFeatures or hasFeatureSettings -%}
<div class="tab-pane fade bdr-w-0" id="features-container">
{%- if hasSupportedFeatures -%}
{{- form_row(form.supportedFeatures, {
'formSettings': formSettings,
'formNotes': formNotes,
}) -}}
{%- endif -%}
{%- if hasFeatureSettings -%}
{{ form_row(form.featureSettings, {
'formSettings': formSettings,
'formNotes': formNotes,
}) -}}
{%- endif -%}
</div>
{%- endif -%}
{%- if hasFields -%}
<div class="tab-pane fade {% if 'leadFieldsContainer' == activeTab %}in active{% endif %} bdr-w-0" id="fields-container">
<h4 class="mb-sm">{{ fieldLabel|trans }}</h4>
{{- fieldHtml|raw -}}
</div>
{%- endif -%}
{%- if hasCompanyFields -%}
<div class="tab-pane fade {% if 'companyFieldsContainer' == activeTab %}in active{% endif %} bdr-w-0" id="company-fields-container">
<h4 class="mb-sm">{{ 'mautic.integration.companyfield_matches'|trans }}</h4>
{{- companyFieldHtml|raw -}}
</div>
{%- endif -%}
</div>
{{ form_end(form) }}

View File

@@ -0,0 +1,102 @@
{#
Variables
- items
- tmpl
- pluginFilter
- plugins
#}
{% set isIndex = ('index' == tmpl) %}
{% set tmpl = 'list' %}
{% set filterValue = pluginFilter.id|default(null) %}
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
{% block mauticContent 'integration' %}
{% block headerTitle %}
{{ 'mautic.plugin.manage.plugins'|trans }} {% if pluginFilter %}- {{ pluginFilter.name }}{% endif %}
{% endblock %}
{% block actions %}
{{ include('@MauticCore/Helper/page_actions.html.twig', {
'customButtons': [
{
'attr': {
'data-toggle': 'ajax',
'href': path('mautic_plugin_reload'),
'class': 'btn btn-primary'
},
'btnText': 'mautic.plugin.reload.plugins'|trans,
'iconClass': 'ri-instance-fill',
'tooltip': 'mautic.plugin.reload.plugins.tooltip',
},
],
}) }}
{% endblock %}
{% block content %}
{% if isIndex %}
<div id="page-list-wrapper" class="panel panel-default">
<div class="panel-body">
<div class="box-layout">
<div class="row">
<div class="col-xs-3 va-m">
<select id="integrationFilter" onchange="Mautic.filterIntegrations(true);"
class="form-control"
data-placeholder="{{ 'mautic.integration.filter.all'|trans }}">
<option value=""></option>
{% for plugin in plugins %}
<option {% if filterValue is same as(plugin.id) %}selected{% endif %} value="{{ plugin.id|e }}">
{{- plugin.name -}}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<div class="page-list">
{% endif %}
{% if items|length > 0 %}
<div class="pa-md">
<div class="row shuffle-integrations native-integrations">
{% for item in items %}
{% if item.plugin in plugins|keys %}
{% set pluginTitle = plugins[item.plugin].name ~ ' - ' ~ item.display %}
{% else %}
{% set pluginTitle = item.name ~ ' - ' ~ item.display %}
{% endif %}
<div class="shuffle shuffle-item grid ma-10 pull-left text-center integration plugin{{ item.plugin }} integration-{{ item.name }} {% if not item.enabled %}integration-disabled{% endif %}">
<div class="panel ovf-h pa-10">
<a href="{{ path((item.isBundle ? 'mautic_plugin_info' : 'mautic_plugin_config'), {'name': item.name}) }}"
{% if item.isBundle %}data-footer="false"{% endif %}
data-prevent-dismiss="true"
data-toggle="ajaxmodal"
data-target="#IntegrationEditModal"
data-header="{{ item.display }}">
<p><img style="height: 78px;" class="img img-responsive" src="{{ getAssetUrl(item.icon) }}" /></p>
<h5 class="mt-20">
<span class="ellipsis" data-toggle="tooltip" title="{{ pluginTitle }}">{{ item.display }}</span>
</h5>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{{ include('@MauticCore/Helper/modal.html.twig', {
'id': 'IntegrationEditModal',
'footerButtons': true,
}) }}
{% else %}
{{ include('@MauticCore/Helper/noresults.html.twig', {
'message': 'mautic.integrations.noresults',
'tip': 'mautic.integration.noresults.tip',
}) }}
{% endif %}
{% if isIndex %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,22 @@
{#
Variables
- bundle
- icon
#}
<div class="row">
<div class="col-xs-4">
<img class="img img-responsive" style="margin: auto;" src="{{ getAssetUrl(icon) }}" />
</div>
<div class="col-xs-8">
<h3>{{ bundle.primaryDescription|purify }}</h3>
</div>
</div>
{% if bundle.hasSecondaryDescription %}
<div class="row mt-lg">
<div class="col-xs-12">
{{ bundle.secondaryDescription|purify }}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,25 @@
<?php
namespace Mautic\PluginBundle\Security\Permissions;
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
use Symfony\Component\Form\FormBuilderInterface;
class PluginPermissions extends AbstractPermissions
{
public function __construct($params)
{
parent::__construct($params);
$this->addManagePermission('plugins');
}
public function getName(): string
{
return 'plugin';
}
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
{
$this->addManageFormFields('plugin', 'plugins', $builder, $data);
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Mautic\PluginBundle\Tests;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\CoreBundle\Helper\BundleHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
use Mautic\PluginBundle\Entity\IntegrationRepository;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Entity\PluginRepository;
use Mautic\PluginBundle\Event\PluginIntegrationKeyEvent;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Model\PluginModel;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Twig\Environment;
class ConfigFormTest extends KernelTestCase
{
protected function setUp(): void
{
self::bootKernel();
}
public function testConfigForm(): void
{
$plugins = $this->getIntegrationObject()->getIntegrationObjects();
foreach ($plugins as $name => $s) {
$featureSettings = $s->getFormSettings();
$this->assertArrayHasKey('requires_callback', $featureSettings);
$this->assertArrayHasKey('requires_authorization', $featureSettings);
if ($featureSettings['requires_callback']) {
$this->assertNotEmpty($s->getAuthCallbackUrl());
}
}
}
public function testOauth(): void
{
$connectWiseHeader = ['appcookie' => 'rookie'];
self::getContainer()->get('event_dispatcher')->addListener(
PluginEvents::PLUGIN_ON_INTEGRATION_KEYS_DECRYPT,
function (PluginIntegrationKeyEvent $event) use ($connectWiseHeader): PluginIntegrationKeyEvent {
$event->setKeys($connectWiseHeader);
return $event;
}
);
$plugins = $this->getIntegrationObject()->getIntegrationObjects();
$url = 'https://test.com';
$parameters = ['a' => 'testa', 'b' => 'testb'];
$method = 'GET';
$authType = 'oauth2';
$expected = [];
$expected['Connectwise'] = $this->getOauthData('', ['clientId' => $connectWiseHeader['appcookie']]);
$expected['OneSignal'] = $this->getOauthData('');
$expected['Twilio'] = $this->getOauthData('');
$expected['Vtiger'] = $this->getOauthData('sessionName');
$expected['Dynamics'] = $this->getOauthData('access_token');
$expected['Salesforce'] = $this->getOauthData('access_token');
$expected['Sugarcrm'] = $this->getOauthData('access_token');
$expected['Zoho'] = $this->getOauthData('access_token');
$expected['Hubspot'] = $this->getOauthData('hapikey');
foreach ($plugins as $index => $integration) {
$this->assertSame($expected[$index], $integration->prepareRequest($url, $parameters, $method, ['appcookie' => 'ololo'], $authType));
}
}
/**
* @param array<string> $headers
*
* @return array<mixed>
*/
private function getOauthData(string $key, array $headers = []): array
{
$result = [
[
'a' => 'testa',
'b' => 'testb',
$key => '',
], [
'oauth-token: '.$key,
'Authorization: OAuth ',
],
];
if ([] !== $headers) {
$result[1] = array_merge($result[1], $headers);
}
return $result;
}
public function testAmendLeadDataBeforeMauticPopulate(): void
{
$plugins = $this->getIntegrationObject()->getIntegrationObjects();
$object = 'company';
$data = ['company_name' => 'company_name', 'email' => 'company_email'];
foreach ($plugins as $integration) {
$methodExists = method_exists($integration, 'amendLeadDataBeforeMauticPopulate');
if ($methodExists) {
$count = $integration->amendLeadDataBeforeMauticPopulate($data, $object);
$this->assertGreaterThanOrEqual(0, $count);
}
}
}
public function getIntegrationObject()
{
// create an integration object
$pathsHelper = $this->createMock(PathsHelper::class);
$bundleHelper = $this->createMock(BundleHelper::class);
$pluginModel = $this->createMock(PluginModel::class);
$coreParametersHelper = new CoreParametersHelper(self::$kernel->getContainer());
$twig = $this->createMock(Environment::class);
$entityManager = $this->createMock(EntityManager::class);
$pluginRepository = $this->createMock(PluginRepository::class);
$registeredPluginBundles = static::getContainer()->getParameter('mautic.plugin.bundles');
$mauticPlugins = static::getContainer()->getParameter('mautic.bundles');
$bundleHelper->method('getPluginBundles')->willReturn($registeredPluginBundles);
$bundleHelper->method('getMauticBundles')->willReturn(array_merge($mauticPlugins, $registeredPluginBundles));
$integrationEntityRepository = $this->createMock(IntegrationEntityRepository::class);
$integrationRepository = $this->createMock(IntegrationRepository::class);
$entityManager
->method('getRepository')
->willReturnMap(
[
[Plugin::class, $pluginRepository],
[\Mautic\PluginBundle\Entity\Integration::class, $integrationRepository],
[\Mautic\PluginBundle\Entity\IntegrationEntity::class, $integrationEntityRepository],
]
);
$pluginModel->method('getEntities')
->with(
[
'hydration_mode' => 'hydrate_array',
'index' => 'bundle',
'result_cache' => new ResultCacheOptions(Plugin::CACHE_NAMESPACE),
]
)->willReturn([
'MauticCrmBundle' => ['id' => 1],
]);
$integrationHelper = new IntegrationHelper(
self::getContainer(),
$entityManager,
$pathsHelper,
$bundleHelper,
$coreParametersHelper,
$twig,
$pluginModel
);
return $integrationHelper;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
class PluginControllerTest extends MauticMysqlTestCase
{
public function testConfigurePluginSuccessValidation(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/plugins/config/Twilio');
$form = $crawler->filter('form')->form();
$form->setValues([
'integration_details' => [
'isPublished' => 0,
'apiKeys' => [
'username' => 'valid_username',
'password' => 'valid_password',
],
],
]);
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk());
}
public function testConfigurePluginValidationError(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/plugins/config/Twilio');
$form = $crawler->filter('form')->form();
$form->setValues([
'integration_details' => [
'isPublished' => 1,
'apiKeys' => [
'username' => '',
'password' => 'bbb',
],
],
]);
$crawler = $this->client->submit($form);
Assert::assertStringContainsString('A value is required.', $crawler->filter('#integration_details_apiKeys div')->html());
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\DependencyInjection\Compiler;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Mautic\PluginBundle\Tests\Integration\ClientFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TestPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$container->register(ClientFactory::class, ClientFactory::class)
->setArguments([new Reference('mautic.http.client')]);
foreach ($container->getDefinitions() as $definition) {
$class = (string) $definition->getClass();
/** @phpstan-ignore-next-line Ignore as AbstractIntegration is deprecated */
if (str_starts_with($class, 'Mautic') && is_subclass_of($class, AbstractIntegration::class)) {
$definition->addMethodCall('setClientFactory', [new Reference(ClientFactory::class)]);
}
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Entity;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
use PHPUnit\Framework\Assert;
/**
* IntegrationRepository.
*/
class IntegrationEntityRepositoryTest extends MauticMysqlTestCase
{
/**
* @var string
*/
private $prefix;
/**
* @var IntegrationEntityRepository
*/
private $integrationEntityRepository;
protected function setUp(): void
{
parent::setUp();
$this->prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
$this->integrationEntityRepository = $this->em->getRepository(\Mautic\PluginBundle\Entity\IntegrationEntity::class);
}
public function testThatGetIntegrationsEntityIdReturnsCorrectValues(): void
{
$now = new \DateTimeImmutable();
$integrationEntityId = random_int(1, 1000);
$internalEntityId = random_int(1, 1000);
$this->connection->insert($this->prefix.'integration_entity', [
'date_added' => $now->format('Y-m-d H:i:s'),
'integration' => 'someIntegration',
'integration_entity' => 'someIntegrationEntity',
'integration_entity_id' => $integrationEntityId,
'internal_entity' => 'someInternalEntity',
'internal_entity_id' => $internalEntityId,
'last_sync_date' => null,
'internal' => 'someInternalValue',
]);
$results = $this->integrationEntityRepository->getIntegrationsEntityId(
'someIntegration',
'someIntegrationEntity',
'someInternalEntity',
[$internalEntityId],
null,
null,
false,
0,
0,
null
);
Assert::assertCount(1, $results);
Assert::assertSame($integrationEntityId, (int) $results[0]['integration_entity_id']);
Assert::assertSame($internalEntityId, (int) $results[0]['internal_entity_id']);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Mautic\PluginBundle\Tests\Entity;
use Mautic\PluginBundle\Entity\Plugin;
class PluginTest extends \PHPUnit\Framework\TestCase
{
public function testEmptyDescription(): void
{
$plugin = new Plugin();
$this->assertNull($plugin->getDescription());
$this->assertNull($plugin->getPrimaryDescription());
$this->assertNull($plugin->getSecondaryDescription());
$this->assertFalse($plugin->hasSecondaryDescription());
}
public function testSimpleDescription(): void
{
$description = 'This is the best plugin in the whole galaxy';
$plugin = new Plugin();
$plugin->setDescription($description);
$this->assertEquals($description, $plugin->getDescription());
$this->assertEquals($description, $plugin->getPrimaryDescription());
$this->assertNull($plugin->getSecondaryDescription());
$this->assertFalse($plugin->hasSecondaryDescription());
}
public function testSecondaryDescriptionWithUnixLineEnding(): void
{
$description = "This is the best plugin in the whole galaxy\n---\nLearn more about it <a href=\"#\">here</a>";
$plugin = new Plugin();
$plugin->setDescription($description);
$this->assertEquals($description, $plugin->getDescription());
$this->assertEquals('This is the best plugin in the whole galaxy', $plugin->getPrimaryDescription());
$this->assertEquals('Learn more about it <a href="#">here</a>', $plugin->getSecondaryDescription());
$this->assertTrue($plugin->hasSecondaryDescription());
}
public function testSecondaryDescriptionWithWinLineEnding(): void
{
$description = "This is the best plugin in the whole galaxy\n\r---\n\rLearn more about it <a href=\"#\">here</a>";
$plugin = new Plugin();
$plugin->setDescription($description);
$this->assertEquals($description, $plugin->getDescription());
$this->assertEquals('This is the best plugin in the whole galaxy', $plugin->getPrimaryDescription());
$this->assertEquals('Learn more about it <a href="#">here</a>', $plugin->getSecondaryDescription());
$this->assertTrue($plugin->hasSecondaryDescription());
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Event;
use Mautic\PluginBundle\Event\PluginIsPublishedEvent;
class PluginIsPublishedEventTest extends \PHPUnit\Framework\TestCase
{
public function testSettersGetters(): void
{
$pluginIsPublishedEvent = new PluginIsPublishedEvent(1, 'testIntegration');
$this->assertSame('testIntegration', $pluginIsPublishedEvent->getIntegrationName());
$this->assertSame(1, $pluginIsPublishedEvent->getValue());
$this->assertSame('', $pluginIsPublishedEvent->getMessage());
$this->assertTrue($pluginIsPublishedEvent->isCanPublish());
$pluginIsPublishedEvent->setMessage('This is test message.');
$this->assertSame('This is test message.', $pluginIsPublishedEvent->getMessage());
$pluginIsPublishedEvent->setCanPublish(false);
$this->assertFalse($pluginIsPublishedEvent->isCanPublish());
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\EventListener;
use Mautic\PluginBundle\Event\PluginIntegrationRequestEvent;
use Mautic\PluginBundle\EventListener\IntegrationSubscriber;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
final class IntegrationSubscriberTest extends TestCase
{
public function testOnRequestLogging(): void
{
$event = $this->createMock(PluginIntegrationRequestEvent::class);
$event->method('getIntegrationName')->willReturn('Integration');
$event->method('getHeaders')->willReturn(['Authorization: Bearer some_token']);
$event->method('getMethod')->willReturn('POST');
$event->method('getUrl')->willReturn('https://mautic.org');
$event->method('getParameters')->willReturn(['key' => 'value']);
$event->method('getSettings')->willReturn(['setting' => 'value']);
$authorization = ['Authorization: Bearer [REDACTED]'];
$authorization = var_export($authorization, true);
$logger = $this->createMock(LoggerInterface::class);
$matcher = $this->exactly(4);
$logger->expects($matcher)
->method('debug')->willReturnCallback(function (...$parameters) use ($matcher, $authorization) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('INTEGRATION REQUEST URL: POST https://mautic.org', $parameters[0]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame("INTEGRATION REQUEST HEADERS: \n".$authorization.PHP_EOL, $parameters[0]);
}
});
$subscriber = new IntegrationSubscriber($logger);
$subscriber->onRequest($event);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\EventListener;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event\LeadEvent;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\IntegrationEntityRepository;
use Mautic\PluginBundle\Entity\IntegrationRepository;
use Mautic\PluginBundle\EventListener\LeadSubscriber;
use Mautic\PluginBundle\Model\PluginModel;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class LeadSubscriberTest extends TestCase
{
private LeadSubscriber $subscriber;
/**
* @var IntegrationEntityRepository|MockObject
*/
private $integrationEntityRepository;
/**
* @var IntegrationRepository|MockObject
*/
private $integrationRepository;
protected function setUp(): void
{
$pluginModel = $this->createMock(PluginModel::class);
$this->integrationRepository = $this->createMock(IntegrationRepository::class);
$this->integrationEntityRepository = $this->createMock(IntegrationEntityRepository::class);
$this->subscriber = new LeadSubscriber(
$pluginModel,
$this->integrationRepository
);
$pluginModel->expects($this->once())
->method('getIntegrationEntityRepository')
->willReturn($this->integrationEntityRepository);
}
public function testOnLeadSaveWithoutActiveIntegration(): void
{
$this->integrationRepository->expects($this->once())
->method('getIntegrations')
->willReturn([]);
$this->integrationEntityRepository->expects($this->never())
->method('updateErrorLeads');
$this->subscriber->onLeadSave(new LeadEvent(new Lead()));
}
public function testOnLeadSaveWithActiveIntegration(): void
{
$integration = new Integration();
$integration->setIsPublished(true);
$integration->setApiKeys(['key' => 'some']);
$integration->setSupportedFeatures(['push_lead']);
$this->integrationRepository->expects($this->once())
->method('getIntegrations')
->willReturn([$integration]);
$this->integrationEntityRepository->expects($this->once())
->method('updateErrorLeads');
$this->subscriber->onLeadSave(new LeadEvent(new Lead()));
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Exception;
use Mautic\PluginBundle\Exception\ApiErrorException;
class ApiErrorExceptionTest extends \PHPUnit\Framework\TestCase
{
public function testShortMessage(): void
{
$apiErrorException = new ApiErrorException('Main Error message.');
$this->assertEmpty($apiErrorException->getShortMessage());
$shortMessage = 'This is short message';
$apiErrorException->setShortMessage($shortMessage);
$this->assertSame($shortMessage, $apiErrorException->getShortMessage());
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Form\Constraint;
use Mautic\PluginBundle\Event\PluginIsPublishedEvent;
use Mautic\PluginBundle\Form\Constraint\CanPublish;
use Mautic\PluginBundle\Form\Constraint\CanPublishValidator;
use Mautic\PluginBundle\PluginEvents;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class CanPublishValidatorTest extends TestCase
{
/**
* @var MockObject|EventDispatcherInterface
*/
private $dispatcher;
/**
* @var MockObject|PluginIsPublishedEvent
*/
private $event;
private CanPublishValidator $canPublishValidator;
protected function setUp(): void
{
parent::setUp();
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->event = $this->createMock(PluginIsPublishedEvent::class);
$this->canPublishValidator = new CanPublishValidator($this->dispatcher);
}
public function testValidate(): void
{
$this->event->expects($this->once())
->method('isCanPublish')
->willReturn(false);
$this->event->expects($this->once())
->method('getMessage')
->willReturn('Error in validation');
$this->dispatcher->expects($this->once())
->method('dispatch')
->willReturn($this->event);
$this->canPublishValidator->initialize($this->createMock(ExecutionContext::class));
$this->canPublishValidator->validate(1, new CanPublish('testIntegration'));
}
public function testEventNotDispatchedIfUnpublished(): void
{
$this->event->expects($this->never())
->method('isCanPublish')
->willReturn(false);
$this->event->expects($this->never())
->method('getMessage')
->willReturn('Error in validation');
$this->dispatcher->expects($this->never())
->method('dispatch')
->with(PluginEvents::PLUGIN_IS_PUBLISHED_STATE_CHANGING)
->willReturn($this->event);
$this->canPublishValidator->initialize($this->createMock(ExecutionContext::class));
$this->canPublishValidator->validate(0, new CanPublish('testIntegration'));
}
public function testExceptionIsThrown(): void
{
$this->event->expects($this->never())
->method('isCanPublish')
->willReturn(false);
$this->event->expects($this->never())
->method('getMessage')
->willReturn('Error in validation');
$this->dispatcher->expects($this->never())
->method('dispatch')
->with(PluginEvents::PLUGIN_IS_PUBLISHED_STATE_CHANGING)
->willReturn($this->event);
$this->canPublishValidator->initialize($this->createMock(ExecutionContext::class));
$this->expectException(UnexpectedTypeException::class);
$this->canPublishValidator->validate(1, new class extends Constraint {});
}
}

View File

@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Form\Type;
use Mautic\CoreBundle\Form\Type\StandAloneButtonType;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Form\Type\DetailsType;
use Mautic\PluginBundle\Form\Type\KeysType;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class DetailsTypeTest extends TestCase
{
public function testBuildFormRemovesHiddenKeys(): void
{
/** @var MockObject&FormBuilderInterface $builder */
$builder = $this->createMock(FormBuilderInterface::class);
$options = ['integration' => 'integration', 'lead_fields' => 'lead_fields', 'company_fields' => 'company_fields'];
$integrationObject = $this->createMock(AbstractIntegration::class);
$integrationObject->expects(self::once())
->method('getFormDisplaySettings')
->willReturn(['hide_keys' => ['key1', 'key3']]);
$integrationObject->expects(self::once())
->method('getRequiredKeyFields')
->willReturn(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4']);
$integrationObject->expects(self::once())
->method('decryptApiKeys')
->willReturn([]);
$integrationObject->expects(self::never())
->method('isAuthorized');
$integrationObject->expects(self::once())
->method('getSupportedFeatures')
->willReturn([]);
$integration = $this->createMock(Integration::class);
$integration->method('getApiKeys')
->willReturn([]);
$integration->expects(self::never())
->method('getId');
$integration->expects(self::never())
->method('getSupportedFeatures');
$options['integration_object'] = $integrationObject;
$options['data'] = $integration;
$calls = 0;
$builder->expects(self::never())
->method('setAction');
$builder->expects(self::atLeastOnce())
->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use (&$calls, $builder): FormBuilderInterface {
if ('apiKeys' === $key) {
++$calls;
self::assertSame(KeysType::class, $fieldFQCN);
self::assertArrayHasKey('integration_keys', $options);
self::assertSame(['key2' => 'value2', 'key4' => 'value4'], $options['integration_keys']);
}
if ('authButton' === $key) {
++$calls;
}
if ('supportedFeatures' === $key) {
++$calls;
}
return $builder;
});
$integrationObject->expects(self::once())
->method('modifyForm')
->with($builder, $options);
$form = new DetailsType();
$form->buildForm($builder, $options);
self::assertSame(1, $calls);
}
#[\PHPUnit\Framework\Attributes\DataProvider('authorizedDataProvider')]
public function testBuildFormRequiresAuthorization(bool $isAuthorized, string $label): void
{
/** @var MockObject&FormBuilderInterface $builder */
$builder = $this->createMock(FormBuilderInterface::class);
$options = ['integration' => 'integration', 'lead_fields' => 'lead_fields', 'company_fields' => 'company_fields'];
$integrationObject = $this->createMock(AbstractIntegration::class);
$integrationObject->expects(self::once())
->method('getFormDisplaySettings')
->willReturn(['hide_keys' => ['key3'], 'requires_authorization' => true]);
$integrationObject->expects(self::once())
->method('getRequiredKeyFields')
->willReturn(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4']);
$integrationObject->expects(self::once())
->method('decryptApiKeys')
->willReturn(['decrypted']);
$integrationObject->expects(self::once())
->method('isAuthorized')
->willReturn($isAuthorized);
$integrationObject->expects(self::once())
->method('getSupportedFeatures')
->willReturn([]);
$integration = $this->createMock(Integration::class);
$integration->method('getApiKeys')
->willReturn([]);
$integration->expects(self::never())
->method('getId');
$integration->expects(self::never())
->method('getSupportedFeatures');
$options['integration_object'] = $integrationObject;
$options['data'] = $integration;
$calls = 0;
$builder->expects(self::never())
->method('setAction');
$builder->expects(self::atLeastOnce())
->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use ($label, &$calls, $builder): FormBuilderInterface {
if ('apiKeys' === $key) {
++$calls;
self::assertSame(KeysType::class, $fieldFQCN);
self::assertArrayHasKey('integration_keys', $options);
self::assertSame(['key1' => 'value1', 'key2' => 'value2', 'key4' => 'value4'], $options['integration_keys']);
}
if ('authButton' === $key) {
++$calls;
self::assertSame(StandAloneButtonType::class, $fieldFQCN);
self::assertArrayHasKey('label', $options);
self::assertSame('mautic.integration.form.'.$label, $options['label']);
}
if ('supportedFeatures' === $key) {
++$calls;
}
return $builder;
});
$integrationObject->expects(self::once())
->method('modifyForm')
->with($builder, $options);
$form = new DetailsType();
$form->buildForm($builder, $options);
self::assertSame(2, $calls);
}
public static function authorizedDataProvider(): \Generator
{
yield 'authorized' => [true, 'reauthorize'];
yield 'not authorized' => [false, 'authorize'];
}
/**
* @param array<string> $expectedFeatures
*/
#[\PHPUnit\Framework\Attributes\DataProvider('withFeaturesProvider')]
public function testBuildFormWithFeatures(?int $integrationId, array $expectedFeatures): void
{
/** @var MockObject&FormBuilderInterface $builder */
$builder = $this->createMock(FormBuilderInterface::class);
$options = ['integration' => 'integration', 'lead_fields' => 'lead_fields', 'company_fields' => 'company_fields'];
$integrationObject = $this->createMock(AbstractIntegration::class);
$integrationObject->expects(self::once())
->method('getFormDisplaySettings')
->willReturn(['hide_keys' => ['key1']]);
$integrationObject->expects(self::once())
->method('getRequiredKeyFields')
->willReturn(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4']);
$integrationObject->expects(self::once())
->method('decryptApiKeys')
->willReturn(['decrypted']);
$integrationObject->expects(self::never())
->method('isAuthorized');
$integrationObject->expects(self::once())
->method('getSupportedFeatures')
->willReturn(['non-configured']);
$integration = $this->createMock(Integration::class);
$integration->method('getApiKeys')
->willReturn([]);
$integration->expects(self::once())
->method('getId')
->willReturn($integrationId);
$integration->expects(self::once())
->method('getSupportedFeatures')
->willReturn(['configured']);
$options['integration_object'] = $integrationObject;
$options['data'] = $integration;
$calls = 0;
$builder->expects(self::never())
->method('setAction');
$builder->expects(self::atLeastOnce())
->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use ($expectedFeatures, &$calls, $builder): FormBuilderInterface {
if ('apiKeys' === $key) {
++$calls;
self::assertSame(KeysType::class, $fieldFQCN);
self::assertArrayHasKey('integration_keys', $options);
self::assertSame(['key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4'], $options['integration_keys']);
}
if ('supportedFeatures' === $key) {
++$calls;
self::assertSame(ChoiceType::class, $fieldFQCN);
self::assertArrayHasKey('choices', $options);
self::assertSame(['mautic.integration.form.feature.non-configured' => 'non-configured'], $options['choices']);
self::assertArrayHasKey('data', $options);
self::assertSame($expectedFeatures, $options['data']);
}
if ('authButton' === $key) {
++$calls;
}
return $builder;
});
$integrationObject->expects(self::once())
->method('modifyForm')
->with($builder, $options);
$form = new DetailsType();
$form->buildForm($builder, $options);
self::assertSame(2, $calls);
}
public static function withFeaturesProvider(): \Generator
{
yield 'create integration' => [null, ['non-configured']];
yield 'edit integration' => [1, ['configured']];
}
public function testBuildFormWithAction(): void
{
$action = 'the_action';
$options = ['action' => $action, 'integration' => 'integration', 'lead_fields' => 'lead_fields', 'company_fields' => 'company_fields'];
/** @var MockObject&FormBuilderInterface $builder */
$builder = $this->createMock(FormBuilderInterface::class);
$integrationObject = $this->createMock(AbstractIntegration::class);
$integrationObject->expects(self::once())
->method('getFormDisplaySettings')
->willReturn([]);
$integrationObject->expects(self::once())
->method('getRequiredKeyFields')
->willReturn(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4']);
$integrationObject->expects(self::once())
->method('decryptApiKeys')
->willReturn(['decrypted']);
$integrationObject->expects(self::never())
->method('isAuthorized');
$integrationObject->expects(self::once())
->method('getSupportedFeatures');
$integration = $this->createMock(Integration::class);
$integration->method('getApiKeys')
->willReturn([]);
$integration->expects(self::never())
->method('getId');
$integration->expects(self::never())
->method('getSupportedFeatures');
$options['integration_object'] = $integrationObject;
$options['data'] = $integration;
$calls = 0;
$builder->expects(self::once())
->method('setAction')
->with($action);
$builder->expects(self::atLeastOnce())
->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use (&$calls, $builder): FormBuilderInterface {
if ('apiKeys' === $key) {
++$calls;
self::assertSame(KeysType::class, $fieldFQCN);
self::assertArrayHasKey('integration_keys', $options);
self::assertSame(['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3', 'key4' => 'value4'], $options['integration_keys']);
}
if ('supportedFeatures' === $key) {
++$calls;
}
if ('authButton' === $key) {
++$calls;
}
return $builder;
});
$integrationObject->expects(self::once())
->method('modifyForm')
->with($builder, $options);
$form = new DetailsType();
$form->buildForm($builder, $options);
self::assertSame(1, $calls);
}
}

View File

@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Form\Type;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Form\Type\IntegrationCampaignsType;
use Mautic\PluginBundle\Form\Type\IntegrationConfigType;
use Mautic\PluginBundle\Form\Type\IntegrationsListType;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
class IntegrationsListTypeTest extends TestCase
{
public function testDataDoesNotHaveIntegration(): void
{
$pluginName = 'plugin name';
$integration1 = $this->createMock(Integration::class);
$integration1->expects(self::once())
->method('isPublished')
->willReturn(false);
$integration1->expects(self::never())
->method('getPlugin');
$plugin = $this->createMock(Plugin::class);
$plugin->expects(self::once())
->method('getName')
->willReturn($pluginName);
$integration2 = $this->createMock(Integration::class);
$integration2->expects(self::once())
->method('isPublished')
->willReturn(true);
$integration2->expects(self::once())
->method('getPlugin')
->willReturn($plugin);
$integrationInstance1 = $this->createMock(AbstractIntegration::class);
$integrationInstance1->expects(self::once())
->method('getIntegrationSettings')
->willReturn($integration1);
$integrationInstance2 = $this->createMock(AbstractIntegration::class);
$integrationInstance2->expects(self::once())
->method('getIntegrationSettings')
->willReturn($integration2);
$integrationInstance2->expects(self::once())
->method('getDisplayName')
->willReturn('Integration 2');
$integrationInstance2->expects(self::once())
->method('getName')
->willReturn('integration-2');
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with(null, 'features', true)
->willReturn(['integration1' => $integrationInstance1, 'integration2' => $integrationInstance2]);
$integrationHelper->method('getIntegrationObject')
->willReturn($this->createMock(AbstractIntegration::class));
$callsForm = 0;
/** @var MockObject&FormInterface $form */
$form = $this->createMock(FormInterface::class);
$form->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use (&$callsForm, $form): FormInterface {
if ('config' === $key) {
++$callsForm;
self::assertSame(IntegrationConfigType::class, $fieldFQCN);
self::assertArrayHasKey('integration', $options);
self::assertNull($options['integration']);
self::assertArrayHasKey('data', $options);
self::assertSame([], $options['data']);
}
if ('campaign_member_status' === $key) {
++$callsForm;
self::assertSame(IntegrationCampaignsType::class, $fieldFQCN);
self::assertArrayHasKey('attr', $options);
self::assertSame('integration-campaigns-status hide', $options['attr']['class']);
self::assertArrayHasKey('data', $options);
self::assertSame([], $options['data']);
}
return $form;
});
$data = [];
$formEvent = $this->createMock(FormEvent::class);
$formEvent->expects(self::once())
->method('getForm')
->willReturn($form);
$formEvent->expects(self::once())
->method('getData')
->willReturn($data);
/** @var MockObject&FormBuilderInterface $builder */
$builder = $this->createMock(FormBuilderInterface::class);
$callsBuilder = 0;
$builder->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use ($pluginName, &$callsBuilder, $builder): FormBuilderInterface {
if ('integration' === $key) {
++$callsBuilder;
self::assertSame(ChoiceType::class, $fieldFQCN);
self::assertArrayHasKey('choices', $options);
self::assertSame([
'' => '',
$pluginName => [
'Integration 2' => 'integration-2',
],
], $options['choices']);
}
return $builder;
});
$calledCallback = false;
$builder->expects(self::exactly(2))
->method('addEventListener')
->willReturnCallback(static function (string $eventName, callable $callback) use ($formEvent, &$calledCallback, $builder): FormBuilderInterface {
self::assertContains($eventName, [FormEvents::PRE_SET_DATA, FormEvents::PRE_SUBMIT]);
if (!$calledCallback) {
$calledCallback = true;
$callback($formEvent);
}
return $builder;
});
$integrationsListType = new IntegrationsListType($integrationHelper);
$integrationsListType->buildForm($builder, ['supported_features' => 'features']);
self::assertSame(1, $callsBuilder);
self::assertSame(2, $callsForm);
}
public function testDataHaveIntegration(): void
{
$pluginName = 'plugin name';
$integration1 = $this->createMock(Integration::class);
$integration1->expects(self::once())
->method('isPublished')
->willReturn(false);
$integration1->expects(self::never())
->method('getPlugin');
$plugin = $this->createMock(Plugin::class);
$plugin->expects(self::once())
->method('getName')
->willReturn($pluginName);
$integration2 = $this->createMock(Integration::class);
$integration2->expects(self::once())
->method('isPublished')
->willReturn(true);
$integration2->expects(self::once())
->method('getPlugin')
->willReturn($plugin);
$integrationInstance1 = $this->createMock(AbstractIntegration::class);
$integrationInstance1->expects(self::once())
->method('getIntegrationSettings')
->willReturn($integration1);
$integrationInstance2 = $this->createMock(AbstractIntegration::class);
$integrationInstance2->expects(self::once())
->method('getIntegrationSettings')
->willReturn($integration2);
$integrationInstance2->expects(self::once())
->method('getDisplayName')
->willReturn('Integration 2');
$integrationInstance2->expects(self::once())
->method('getName')
->willReturn('integration-2');
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with(null, 'features', true)
->willReturn(['integration1' => $integrationInstance1, 'integration2' => $integrationInstance2]);
$integrationHelper->method('getIntegrationObject')
->willReturn($this->createMock(AbstractIntegration::class));
$callsForm = 0;
$form = $this->createMock(FormInterface::class);
$form->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use ($integrationInstance1, &$callsForm, $form): FormInterface {
if ('config' === $key) {
++$callsForm;
self::assertSame(IntegrationConfigType::class, $fieldFQCN);
self::assertArrayHasKey('integration', $options);
self::assertSame($integrationInstance1, $options['integration']);
self::assertArrayHasKey('data', $options);
self::assertSame(['config' => 'test'], $options['data']);
}
if ('campaign_member_status' === $key) {
++$callsForm;
self::assertSame(IntegrationCampaignsType::class, $fieldFQCN);
self::assertArrayHasKey('attr', $options);
self::assertSame('integration-campaigns-status', $options['attr']['class']);
self::assertArrayHasKey('data', $options);
self::assertSame([
'campaign_member_status' => true,
'some' => 'other',
], $options['data']);
}
return $form;
});
$data = [
'integration' => 'integration1',
'config' => [
'config' => 'test',
],
'campaign_member_status' => [
'campaign_member_status' => true,
'some' => 'other',
],
];
$formEvent = $this->createMock(FormEvent::class);
$formEvent->expects(self::exactly(2))
->method('getForm')
->willReturn($form);
$formEvent->expects(self::exactly(2))
->method('getData')
->willReturn($data);
$callsBuilder = 0;
$builder = $this->createMock(FormBuilderInterface::class);
\assert($builder instanceof FormBuilderInterface);
$builder->method('add')
->willReturnCallback(static function (string $key, string $fieldFQCN, array $options) use ($pluginName, &$callsBuilder, $builder): FormBuilderInterface {
if ('integration' === $key) {
++$callsBuilder;
self::assertSame(ChoiceType::class, $fieldFQCN);
self::assertArrayHasKey('choices', $options);
self::assertSame([
'' => '',
$pluginName => [
'Integration 2' => 'integration-2',
],
], $options['choices']);
}
return $builder;
});
$calledCallback = 0;
$builder->expects(self::exactly(2))
->method('addEventListener')
->willReturnCallback(static function (string $eventName, callable $callback) use ($formEvent, &$calledCallback, $builder): FormBuilderInterface {
self::assertContains($eventName, [FormEvents::PRE_SET_DATA, FormEvents::PRE_SUBMIT]);
++$calledCallback;
$callback($formEvent);
return $builder;
});
$integrationsListType = new IntegrationsListType($integrationHelper);
$integrationsListType->buildForm($builder, ['supported_features' => 'features']);
self::assertSame(1, $callsBuilder);
self::assertSame(4, $callsForm, 'Because callback is called twice due to coverage.');
self::assertSame(2, $calledCallback);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Mautic\PluginBundle\Tests\Helper;
use Doctrine\DBAL\Schema\Schema;
use Mautic\PluginBundle\Entity\Plugin;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* A stub Base Bundle class which implements stub methods for testing purposes.
*/
abstract class PluginBundleBaseStub extends Bundle
{
public static function onPluginInstall(Plugin $plugin, $metadata = null, $installedSchema = null): void
{
}
/**
* Called by PluginController::reloadAction when the addon version does not match what's installed.
*/
public static function onPluginUpdate(Plugin $plugin, $metadata = null, ?Schema $installedSchema = null)
{
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Mautic\PluginBundle\Tests\Helper;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\ORM\Mapping\ClassMetadata;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Event\PluginInstallEvent;
use Mautic\PluginBundle\Event\PluginUpdateEvent;
use Mautic\PluginBundle\Helper\ReloadHelper;
use Mautic\PluginBundle\PluginEvents;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ReloadHelperTest extends \PHPUnit\Framework\TestCase
{
private ReloadHelper $helper;
private array $sampleAllPlugins = [];
private array $sampleMetaData = [];
private array $sampleSchemas = [];
/**
* @var MockObject&EventDispatcherInterface
*/
private MockObject $eventDispatcher;
protected function setUp(): void
{
parent::setUp();
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$this->helper = new ReloadHelper($this->eventDispatcher);
$this->sampleMetaData = [
'MauticPlugin\MauticZapierBundle' => [
'MauticPlugin\MauticZapierBundle\Entity\SomeTest' => $this->createMock(ClassMetadata::class),
],
];
$sampleSchema = $this->createMock(Schema::class);
$sampleSchema->method('getTables')
->willReturn([]);
$this->sampleSchemas = [
'MauticPlugin\MauticZapierBundle' => $sampleSchema,
];
$this->sampleAllPlugins = [
'MauticZapierBundle' => [
'isPlugin' => true,
'base' => 'MauticZapier',
'bundle' => 'MauticZapierBundle',
'namespace' => 'MauticPlugin\MauticZapierBundle',
'symfonyBundleName' => 'MauticZapierBundle',
'bundleClass' => PluginBundleBaseStub::class,
'permissionClasses' => [],
'relative' => 'plugins/MauticZapierBundle',
'directory' => '/Users/jan/dev/mautic/plugins/MauticZapierBundle',
'config' => [
'name' => 'Zapier Integration',
'description' => 'Zapier lets you connect Mautic with 1100+ other apps',
'version' => '1.0',
'author' => 'Mautic',
],
],
];
}
public function testDisableMissingPlugins(): void
{
$sampleInstalledPlugins = [
'MauticZapierBundle' => $this->createSampleZapierPlugin(),
'MauticHappierBundle' => $this->createSampleHappierPlugin(),
];
$disabledPlugins = $this->helper->disableMissingPlugins($this->sampleAllPlugins, $sampleInstalledPlugins);
$this->assertEquals(1, count($disabledPlugins));
$this->assertEquals('Happier Integration', $disabledPlugins['MauticHappierBundle']->getName());
$this->assertTrue($disabledPlugins['MauticHappierBundle']->isMissing());
}
public function testEnableFoundPlugins(): void
{
$zapierPlugin = $this->createSampleZapierPlugin();
$zapierPlugin->setIsMissing(true);
$sampleInstalledPlugins = [
'MauticZapierBundle' => $zapierPlugin,
];
$enabledPlugins = $this->helper->enableFoundPlugins($this->sampleAllPlugins, $sampleInstalledPlugins);
$this->assertEquals(1, count($enabledPlugins));
$this->assertEquals('Zapier Integration', $enabledPlugins['MauticZapierBundle']->getName());
$this->assertFalse($enabledPlugins['MauticZapierBundle']->isMissing());
}
public function testUpdatePlugins(): void
{
$this->sampleAllPlugins['MauticZapierBundle']['config']['version'] = '1.0.1';
$this->sampleAllPlugins['MauticZapierBundle']['config']['description'] = 'Updated description';
$sampleInstalledPlugins = [
'MauticZapierBundle' => $this->createSampleZapierPlugin(),
'MauticHappierBundle' => $this->createSampleHappierPlugin(),
];
$plugin = $this->createSampleZapierPlugin();
$plugin->setVersion('1.0.1');
$plugin->setDescription('Updated description');
$event = new PluginUpdateEvent(
$plugin,
'1.0',
$this->sampleMetaData['MauticPlugin\MauticZapierBundle'],
$this->sampleSchemas['MauticPlugin\MauticZapierBundle']
);
$this->eventDispatcher->expects($this->once())->method('dispatch')->with($event, PluginEvents::ON_PLUGIN_UPDATE);
$updatedPlugins = $this->helper->updatePlugins($this->sampleAllPlugins, $sampleInstalledPlugins, $this->sampleMetaData, $this->sampleSchemas);
$this->assertEquals(1, count($updatedPlugins));
$this->assertEquals('Zapier Integration', $updatedPlugins['MauticZapierBundle']->getName());
$this->assertEquals('1.0.1', $updatedPlugins['MauticZapierBundle']->getVersion());
$this->assertEquals('Updated description', $updatedPlugins['MauticZapierBundle']->getDescription());
}
public function testInstallPlugins(): void
{
$sampleInstalledPlugins = [
'MauticHappierBundle' => $this->createSampleHappierPlugin(),
];
$event = new PluginInstallEvent(
$this->createSampleZapierPlugin(),
$this->sampleMetaData['MauticPlugin\MauticZapierBundle'],
null
);
$this->eventDispatcher->expects($this->once())->method('dispatch')->with($event, PluginEvents::ON_PLUGIN_INSTALL);
$installedPlugins = $this->helper->installPlugins($this->sampleAllPlugins, $sampleInstalledPlugins, $this->sampleMetaData, $this->sampleSchemas);
$this->assertEquals(1, count($installedPlugins));
$this->assertEquals('Zapier Integration', $installedPlugins['MauticZapierBundle']->getName());
$this->assertEquals('1.0', $installedPlugins['MauticZapierBundle']->getVersion());
$this->assertEquals('MauticZapierBundle', $installedPlugins['MauticZapierBundle']->getBundle());
$this->assertEquals('Mautic', $installedPlugins['MauticZapierBundle']->getAuthor());
$this->assertEquals('Zapier lets you connect Mautic with 1100+ other apps', $installedPlugins['MauticZapierBundle']->getDescription());
$this->assertFalse($installedPlugins['MauticZapierBundle']->isMissing());
}
private function createSampleZapierPlugin()
{
$plugin = new Plugin();
$plugin->setName('Zapier Integration');
$plugin->setDescription('Zapier lets you connect Mautic with 1100+ other apps');
$plugin->isMissing(false);
$plugin->setBundle('MauticZapierBundle');
$plugin->setVersion('1.0');
$plugin->setAuthor('Mautic');
return $plugin;
}
private function createSampleHappierPlugin()
{
$plugin = new Plugin();
$plugin->setName('Happier Integration');
$plugin->setDescription('Happier lets you connect Mautic with 1100+ other apps');
$plugin->isMissing(false);
$plugin->setBundle('MauticHappierBundle');
$plugin->setVersion('1.0');
$plugin->setAuthor('Mautic');
return $plugin;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Helper;
use Mautic\PluginBundle\Helper\oAuthHelper;
use PHPUnit\Framework\TestCase;
final class oAuthHelperTest extends TestCase
{
/**
* @param array<int, string> $headers
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataForHashSensitiveHeaderData')]
public function testHashSensitiveHeaderData(string $authorization, array $headers): void
{
$hashedHeaders = oAuthHelper::sanitizeHeaderData($headers);
$this->assertStringContainsString(sprintf('Authorization: %s [REDACTED]', $authorization), $hashedHeaders[0]);
}
/**
* @return \Generator<string, array<int, string|array<int, string>>>
*/
public static function dataForHashSensitiveHeaderData(): \Generator
{
yield 'For Bearer' => [
'Bearer',
[
'Authorization: Bearer SME-ASA',
],
];
yield 'For Basic' => [
'Basic',
[
'Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l',
],
];
}
}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Integration;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\RequestOptions;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\ResponseInterface;
class AbstractIntegrationTest extends AbstractIntegrationTestCase
{
public function testPopulatedLeadDataReturnsIntAndNotDncEntityForMauticContactIsContactableByEmail(): void
{
/**
* @var MockObject&AbstractIntegration
*/
$integration = $this->getMockBuilder(AbstractIntegration::class)
->setConstructorArgs([
$this->dispatcher,
$this->cache,
$this->em,
$this->request,
$this->router,
$this->translator,
$this->logger,
$this->encryptionHelper,
$this->leadModel,
$this->companyModel,
$this->pathsHelper,
$this->notificationModel,
$this->fieldModel,
$this->integrationEntityModel,
$this->doNotContact,
$this->fieldsWithUniqueIdentifier,
])
->onlyMethods(['getName', 'getAuthenticationType', 'getAvailableLeadFields'])
->getMock();
$integration->method('getAvailableLeadFields')
->willReturn(
[
'dnc' => [
'type' => 'bool',
'required' => false,
'label' => 'DNC',
],
]
);
$this->assertEquals(
['dnc' => 0],
$integration->populateLeadData(
['id' => 1],
[
'leadFields' => [
'dnc' => 'mauticContactIsContactableByEmail',
],
]
)
);
}
/**
* @param mixed[] $parameters
* @param mixed[] $settings
*/
#[\PHPUnit\Framework\Attributes\DataProvider('requestProvider')]
public function testMakeRequest(string $uri, array $parameters, string $method, array $settings, object $assertRequest): void
{
/**
* @var MockObject&AbstractIntegration
*/
$integration = $this->getMockBuilder(AbstractIntegration::class)
->setConstructorArgs([
$this->dispatcher,
$this->cache,
$this->em,
$this->request,
$this->router,
$this->translator,
$this->logger,
$this->encryptionHelper,
$this->leadModel,
$this->companyModel,
$this->pathsHelper,
$this->notificationModel,
$this->fieldModel,
$this->integrationEntityModel,
$this->doNotContact,
$this->fieldsWithUniqueIdentifier,
])
->onlyMethods(['getName', 'getAuthenticationType', 'makeHttpClient'])
->getMock();
$integration->method('makeHttpClient')
->willReturn(
new class($assertRequest) extends Client {
public function __construct(
private object $assertRequest,
) {
}
/**
* @param mixed[] $options
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
$this->assertRequest->assert($method, $uri, $options);
return new Response();
}
}
);
$this->assertEquals([], $integration->makeRequest($uri, $parameters, $method, $settings));
}
/**
* @return iterable<mixed[]>
*/
public static function requestProvider(): iterable
{
// Test with JSON.
yield [
'https://some.uri',
['this will be' => 'encoded to json string'],
'POST',
[
'ignore_event_dispatch' => true,
'encode_parameters' => 'json',
],
new class {
/**
* @param mixed[] $options
*/
public function assert(string $method, string $uri = '', array $options = []): void
{
Assert::assertSame('POST', $method);
Assert::assertSame('https://some.uri', $uri);
Assert::assertSame(
[
RequestOptions::BODY => '{"this will be":"encoded to json string"}',
'headers' => ['Content-Type' => 'application/json'],
'timeout' => 10,
],
$options
);
}
},
];
// Test with form params.
yield [
'https://some.uri',
['this will be' => 'encoded to form array'],
'POST',
['ignore_event_dispatch' => true],
new class {
/**
* @param mixed[] $options
*/
public function assert(string $method, string $uri = '', array $options = []): void
{
Assert::assertSame('POST', $method);
Assert::assertSame('https://some.uri', $uri);
Assert::assertSame(
[
RequestOptions::FORM_PARAMS => ['this will be' => 'encoded to form array'],
'headers' => [],
'timeout' => 10,
],
$options
);
}
},
];
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Mautic\PluginBundle\Tests\Integration;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CacheStorageHelper;
use Mautic\CoreBundle\Helper\EncryptionHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Model\NotificationModel;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\DoNotContact;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PluginBundle\Model\IntegrationEntityModel;
use Monolog\Logger;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Router;
use Symfony\Contracts\Translation\TranslatorInterface;
class AbstractIntegrationTestCase extends TestCase
{
/**
* @var EventDispatcherInterface&MockObject
*/
protected $dispatcher;
/**
* @var CacheStorageHelper&MockObject
*/
protected $cache;
/**
* @var EntityManager&MockObject
*/
protected $em;
/**
* @var Session&MockObject
*/
protected $session;
/**
* @var RequestStack&MockObject
*/
protected $request;
/**
* @var Router&MockObject
*/
protected $router;
/**
* @var TranslatorInterface&MockObject
*/
protected $translator;
/**
* @var Logger&MockObject
*/
protected $logger;
/**
* @var EncryptionHelper&MockObject
*/
protected $encryptionHelper;
/**
* @var LeadModel&MockObject
*/
protected $leadModel;
/**
* @var CompanyModel&MockObject
*/
protected $companyModel;
/**
* @var PathsHelper&MockObject
*/
protected $pathsHelper;
/**
* @var NotificationModel&MockObject
*/
protected $notificationModel;
/**
* @var FieldModel&MockObject
*/
protected $fieldModel;
/**
* @var IntegrationEntityModel&MockObject
*/
protected $integrationEntityModel;
/**
* @var DoNotContact&MockObject
*/
protected $doNotContact;
/**
* @var MockObject&FieldsWithUniqueIdentifier
*/
protected MockObject $fieldsWithUniqueIdentifier;
protected function setUp(): void
{
parent::setUp();
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->cache = $this->createMock(CacheStorageHelper::class);
$this->em = $this->createMock(EntityManager::class);
$this->session = $this->createMock(Session::class);
$this->request = $this->createMock(RequestStack::class);
$this->router = $this->createMock(Router::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->logger = $this->createMock(Logger::class);
$this->encryptionHelper = $this->createMock(EncryptionHelper::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->companyModel = $this->createMock(CompanyModel::class);
$this->pathsHelper = $this->createMock(PathsHelper::class);
$this->notificationModel = $this->createMock(NotificationModel::class);
$this->fieldModel = $this->createMock(FieldModel::class);
$this->integrationEntityModel = $this->createMock(IntegrationEntityModel::class);
$this->doNotContact = $this->createMock(DoNotContact::class);
$this->fieldsWithUniqueIdentifier = $this->createMock(FieldsWithUniqueIdentifier::class);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mautic\PluginBundle\Tests\Integration;
use GuzzleHttp\Client;
class ClientFactory
{
private Client $httpClient;
public function __construct(Client $httpClient)
{
$this->httpClient = $httpClient;
}
public function __invoke(): Client
{
return $this->httpClient;
}
}

View File

@@ -0,0 +1,12 @@
mautic.plugin.notice.reloaded="%added% new plugins were installed and %updated% updated."
mautic.plugin.notice.saved="Settings for the %name% integration have been saved"
mautic.integration.auth.invalid.state="Invalid session. Please try again."
mautic.integration.error.genericerror="There was an unknown error encountered when trying to obtain the access token."
mautic.integration.error.oauthfail="Authorization failed with the error message, '%error%'"
mautic.integration.notfound="%name% was not found!"
mautic.integration.notice.oauthsuccess="Authorization was successful."
mautic.integration.notice.saved="Settings saved"
mautic.integration.sso.error.no_email="Authenticated user does not have an email."
mautic.integration.sso.error.no_name="Authenticated user does not have a first and last name."
mautic.integration.sso.error.no_role="Authenticated user does not have a role."
mautic.integration.sso.error.no_username="Authenticated user does not have a username."

View File

@@ -0,0 +1,96 @@
mautic.campaign.plugin.leadpush="Push contact"
mautic.integration.callbackuri="If applicable, use the following as the callback URL (may also be called the return URI) when configuring your application:"
mautic.integration.closewindow="Close Window"
mautic.integration.error="%name% Error"
mautic.integration.error.generic_contact_name="Contact ID# %id%"
mautic.integration.error.refreshtoken_expired="The refresh token has expired. Re-authorization is required."
mautic.integration.filter.all="Show all plugins"
mautic.integration.form.authorize="Authorize App"
mautic.integration.form.enabled="Is enabled?"
mautic.integration.form.feature.login_button="Login Button"
mautic.integration.form.feature.public_activity="Display public activity"
mautic.integration.form.feature.public_profile="Display public profile and enable profile to contact field matching"
mautic.integration.form.feature.push_lead="Triggered action push contacts to integration"
mautic.integration.form.feature.settings="Feature Specific Settings"
mautic.integration.form.feature.share_button="Display share button on landing page social widget"
mautic.integration.form.feature.sso_service="Single Sign On - Service Authentication"
mautic.integration.form.feature.sso_form="Single Sign On - Form Authentication"
mautic.integration.form.features="Enabled features"
mautic.integration.form.field_match_notes="If the values are empty for the Mautic object, a value of 'Unknown' will be sent. If the integration field is a pick list, be sure the list values of Mautic's field match those of the integration."
mautic.integration.form.lead.unknown="Unknown"
mautic.integration.form.profile="Public Profile"
mautic.integration.form.reauthorize="Reauthorize App"
mautic.integration.form.savefirst="Required keys are missing in order to authenticate. Please enter the keys then save."
mautic.integration.form.sharebutton="Share Buttons"
mautic.integration.integrations="Integrations"
mautic.integration.integration="Integration"
mautic.integration.integration.tooltip="Select the integration to be used."
mautic.integration.keyfield.api="API Key"
mautic.integration.keyfield.appid="App ID"
mautic.integration.keyfield.appsecret="App Secret"
mautic.integration.keyfield.clientid="Client ID"
mautic.integration.keyfield.clientsecret="Client Secret"
mautic.integration.keyfield.consumerid="Consumer ID"
mautic.integration.keyfield.consumersecret="Consumer Secret"
mautic.integration.keyfield.username="Username"
mautic.integration.keyfield.password="Password"
mautic.integration.leadfield_matches="Assign available integration fields to local contact fields."
mautic.integration.companyfield_matches="Assign available integration fields to local company fields."
mautic.integration.missingkeys="Keys are not available for this transaction to take place. Please verify your settings then try again."
mautic.integration.noresults.tip="Expecting integrations but see none? Enable the associated addon via the Addon Manager! For example, the Social Media addon must be enabled in order for Facebook to be listed."
mautic.integration.sso.auto_create_user="Automatically create local user?"
mautic.integration.sso.auto_create_user.tooltip="If the user is authenticated and does not exist locally, a new local user will be created."
mautic.integration.sso.new_user_role="Role for created user"
mautic.integration.sso.new_user_role.tooltip="If new user creation is enabled, select the role the new user should be assigned."
mautic.integrations.noresults=""
mautic.plugin.actions="Addon actions"
mautic.plugin.actions.facebookLogin="Facebook Login"
mautic.plugin.actions.push_lead="Push contact to integration"
mautic.plugin.actions.tooltip="Push a contact to the selected integration."
mautic.plugin.actions.social_share="Social Networks Share Icons"
mautic.plugin.actions.social_share_tooltip="Adds social network icons to share form"
mautic.plugin.actions.twitterLogin="Twitter Login"
mautic.plugin.command.fetch.leads="Command to fetch contacts from integration"
mautic.plugin.command.fetch.leads.starting="Fetch contacts command is starting"
mautic.plugin.command.fetch.contacts.starting="Fetching contacts..."
mautic.plugin.command.fetch.leads.events_executed="Number of leads/contacts fetched: %events%"
mautic.plugin.command.fetch.leads.events_executed_breakout="%updated% contacts were updated and %created% contacts were created"
mautic.plugin.command.fetch.companies.events_executed="Number of companies fetched: %events%"
mautic.plugin.command.fetch.companies.events_executed_breakout="%updated% companies were updated and %created% companies were created"
mautic.plugin.command.push.leads.events_executed="Number of contacts processed: %events%"
mautic.plugin.form.add.fields="Add Field"
mautic.plugin.plugins="Plugins"
mautic.plugin.integration.tab.details="Enabled/Auth"
mautic.plugin.integration.tab.features="Features"
mautic.plugin.integration.tab.fieldmapping="Contact Mapping"
mautic.plugin.integration.tab.companyfieldmapping="Company Mapping"
mautic.plugin.manage.plugins="Manage Plugins"
mautic.plugin.permissions.plugins="Plugins - User has access to"
mautic.plugin.permissions.header="Plugin Permissions"
mautic.plugin.point.action="Addon triggers"
mautic.plugin.reload.plugins="Install/Upgrade Plugins"
mautic.plugin.reload.plugins.tooltip="Upload the plugin via FTP or some other protocol to the plugins directory then click this button to install/upgrade."
mautic.integration.form.feature.get_leads="Pull contacts and/or companies from integration"
mautic.plugin.command.push.leads.activity="Push activity timeline to %integration% mautic object"
mautic.plugin.command.fetch.companies="Fetching companies"
mautic.plugin.command.fetch.companies.starting="Fetch companies command is starting"
mautic.plugin.command.pushing.leads="Updating/creating leads from Mautic to %integration%"
mautic.plugin.command.fetch.pushing.leads.events_executed="Number of contacts pushed: %updated% updated, %created% created, %errored% had errors and %ignored% were ignored (likely duplicates or didn't match field criteria)"
mautic.plugins.integration.fields="Integration fields"
mautic.plugins.mautic.direction="Direction"
mautic.plugins.mautic.fields="Mautic fields"
mautic.plugin.direction.data.update="Pick direction of data update"
mautic.integration.form.feature.push_leads="Push contacts and/or companies to this integration"
mautic.plugin.integration.campaign_members="Integration Campaign Members"
mautic.plugin.integration.contact.timeline.link="Contact's timeline link"
mautic.plugin.integration.campaigns="Push contacts to this integration campaign"
mautic.plugin.config.campaign.member.chooseone="Choose a campaign to insert contacts into"
mautic.plugin.integration.campaigns.member.status="Campaign member status"
mautic.integrations.blanks="Update blank values"
mautic.integrations.form.blanks="This will update blank values regardless of data priority, on both Integration and Mautic."
mautic.plugin.command.notauthorized="%s is not authorized"
mautic.plugin.integration.contact.donotcontact.email="Do not contact by email"
mautic.plugin.command.pushing.companies="Updating/creating companies from Mautic to %integration%"
mautic.plugin.command.fetch.pushing.companies.events_executed="Number of companies pushed: %updated% updated, %created% created, %errored% had errors and %ignored% were ignored (likely duplicates or didn't match field criteria)"
mautic.integrations.update.dnc.by.date="Use latest updated Do Not Contact record"
mautic.integrations.form.update.dnc.by.date.label="Select this option if you wish to update the Do not contact field by the latest updated on either systems"

View File

@@ -0,0 +1,2 @@
mautic.plugin.field.required_mapping_missing="At least one required field is not mapped."
mautic.lead_list.not_allowed_plugin_publish="You are not allowed to publish this plugin due to insufficient configurations."