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,26 @@
# Workflow name:
name: Close Pull Requests
# Workflow triggers:
on:
pull_request_target:
types: [opened]
# Workflow jobs:
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thank you for submitting a pull request. :raised_hands:
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/MauticClearbitBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
Thank you again, and we look forward to receiving your contribution! :smiley:
Best,
The Mautic team

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,61 @@
<?php
return [
'name' => 'Clearbit',
'description' => 'Enables integration with Clearbit for contact and company lookup',
'version' => '1.0',
'author' => 'Werner Garcia',
'routes' => [
'public' => [
'mautic_plugin_clearbit_index' => [
'path' => '/clearbit/callback',
'controller' => 'MauticPlugin\MauticClearbitBundle\Controller\PublicController::callbackAction',
],
],
'main' => [
'mautic_plugin_clearbit_action' => [
'path' => '/clearbit/{objectAction}/{objectId}',
'controller' => 'MauticPlugin\MauticClearbitBundle\Controller\ClearbitController::executeAction',
],
],
],
'services' => [
'others' => [
'mautic.plugin.clearbit.lookup_helper' => [
'class' => MauticPlugin\MauticClearbitBundle\Helper\LookupHelper::class,
'arguments' => [
'mautic.helper.integration',
'mautic.helper.user',
'monolog.logger.mautic',
'mautic.lead.model.lead',
'mautic.lead.model.company',
],
],
],
'integrations' => [
'mautic.integration.clearbit' => [
'class' => MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration::class,
'arguments' => [
'event_dispatcher',
'mautic.helper.cache_storage',
'doctrine.orm.entity_manager',
'request_stack',
'router',
'translator',
'monolog.logger.mautic',
'mautic.helper.encryption',
'mautic.lead.model.lead',
'mautic.lead.model.company',
'mautic.helper.paths',
'mautic.core.model.notification',
'mautic.lead.model.field',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.lead.field.fields_with_unique_identifier',
],
],
],
],
];

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $configurator): void {
$services = $configurator->services()
->defaults()
->autowire()
->autoconfigure()
->public();
$excludes = [
'Services',
];
$services->load('MauticPlugin\\MauticClearbitBundle\\', '../')
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
};

View File

@@ -0,0 +1,520 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Controller;
use Mautic\FormBundle\Controller\FormController;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use MauticPlugin\MauticClearbitBundle\Form\Type\BatchLookupType;
use MauticPlugin\MauticClearbitBundle\Form\Type\LookupType;
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ClearbitController extends FormController
{
/**
* @param string $objectId
*
* @return JsonResponse
*
* @throws \InvalidArgumentException
*/
public function lookupPersonAction(Request $request, LookupHelper $lookupHelper, $objectId = '')
{
if ('POST' === $request->getMethod()) {
$data = $request->request->all()['clearbit_lookup'] ?? [];
$objectId = $data['objectId'];
}
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
$model = $this->getModel('lead');
$lead = $model->getEntity($objectId);
if (!$this->security->hasEntityAccess(
'lead:leads:editown',
'lead:leads:editother',
$lead->getPermissionUser()
)
) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.forbidden'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
if ('GET' === $request->getMethod()) {
$route = $this->generateUrl(
'mautic_plugin_clearbit_action',
[
'objectAction' => 'lookupPerson',
]
);
return $this->delegateView(
[
'viewParameters' => [
'form' => $this->createForm(
LookupType::class,
[
'objectId' => $objectId,
],
[
'action' => $route,
]
)->createView(),
'lookupItem' => $lead->getEmail(),
],
'contentTemplate' => '@MauticClearbit/Clearbit/lookup.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_contact_index',
'mauticContent' => 'lead',
'route' => $route,
],
]
);
} else {
if ('POST' === $request->getMethod()) {
try {
$lookupHelper->lookupContact($lead, array_key_exists('notify', $data));
$this->addFlashMessage(
'mautic.lead.batch_leads_affected',
[
'%count%' => 1,
]
);
} catch (\Exception $ex) {
$this->addFlashMessage(
$ex->getMessage(),
[],
'error'
);
}
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
return new Response('Bad Request', 400);
}
/**
* @return JsonResponse
*
* @throws \InvalidArgumentException
*/
public function batchLookupPersonAction(Request $request, LookupHelper $lookupHelper)
{
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
$model = $this->getModel('lead');
if ('GET' === $request->getMethod()) {
$data = $request->query->all()['clearbit_batch_lookup'] ?? [];
} else {
$data = $request->request->all()['clearbit_batch_lookup'] ?? [];
}
$entities = [];
if (array_key_exists('ids', $data)) {
$ids = $data['ids'];
if (!is_array($ids)) {
$ids = json_decode($ids, true);
}
if (is_array($ids) && count($ids)) {
$entities = $model->getEntities(
[
'filter' => [
'force' => [
[
'column' => 'l.id',
'expr' => 'in',
'value' => $ids,
],
],
],
'ignore_paginator' => true,
]
);
}
}
$lookupEmails = [];
if ($count = count($entities)) {
/** @var Lead $lead */
foreach ($entities as $lead) {
if ($this->security->hasEntityAccess(
'lead:leads:editown',
'lead:leads:editother',
$lead->getPermissionUser()
)
&& $lead->getEmail()
) {
$lookupEmails[$lead->getId()] = $lead->getEmail();
}
}
$count = count($lookupEmails);
}
if (0 === $count) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.empty'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
} else {
if ($count > 20) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.toomany'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
if ('GET' === $request->getMethod()) {
$route = $this->generateUrl(
'mautic_plugin_clearbit_action',
[
'objectAction' => 'batchLookupPerson',
]
);
return $this->delegateView(
[
'viewParameters' => [
'form' => $this->createForm(
BatchLookupType::class,
[],
[
'action' => $route,
]
)->createView(),
'lookupItems' => array_values($lookupEmails),
],
'contentTemplate' => '@MauticClearbit/Clearbit/batchLookup.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_contact_index',
'mauticContent' => 'leadBatch',
'route' => $route,
],
]
);
} else {
if ('POST' === $request->getMethod()) {
$notify = array_key_exists('notify', $data);
foreach ($lookupEmails as $id => $lookupEmail) {
if ($lead = $model->getEntity($id)) {
try {
$lookupHelper->lookupContact($lead, $notify);
} catch (\Exception $ex) {
$this->addFlashMessage(
$ex->getMessage(),
[],
'error'
);
--$count;
}
}
}
if ($count) {
$this->addFlashMessage(
'mautic.lead.batch_leads_affected',
[
'%count%' => $count,
]
);
}
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
return new Response('Bad Request', 400);
}
/***************** COMPANY ***********************/
/**
* @param string $objectId
*
* @return JsonResponse
*
* @throws \InvalidArgumentException
*/
public function lookupCompanyAction(Request $request, LookupHelper $lookupHelper, $objectId = '')
{
if ('POST' === $request->getMethod()) {
$data = $request->request->all()['clearbit_lookup'] ?? [];
$objectId = $data['objectId'];
}
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
$model = $this->getModel('lead.company');
/** @var Company $company */
$company = $model->getEntity($objectId);
if ('GET' === $request->getMethod()) {
$route = $this->generateUrl(
'mautic_plugin_clearbit_action',
[
'objectAction' => 'lookupCompany',
]
);
$website = $company->getFieldValue('companywebsite');
if (!$website) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.compempty'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
$parse = parse_url($website);
return $this->delegateView(
[
'viewParameters' => [
'form' => $this->createForm(
LookupType::class,
[
'objectId' => $objectId,
],
[
'action' => $route,
]
)->createView(),
'lookupItem' => $parse['host'],
],
'contentTemplate' => '@MauticClearbit/Clearbit/lookup.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_company_index',
'mauticContent' => 'company',
'route' => $route,
],
]
);
} else {
if ('POST' === $request->getMethod()) {
try {
$lookupHelper->lookupCompany($company, array_key_exists('notify', $data));
$this->addFlashMessage(
'mautic.company.batch_companies_affected',
[
'%count%' => 1,
]
);
} catch (\Exception $ex) {
$this->addFlashMessage(
$ex->getMessage(),
[],
'error'
);
}
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
return new Response('Bad Request', 400);
}
/**
* @return JsonResponse
*
* @throws \InvalidArgumentException
*/
public function batchLookupCompanyAction(Request $request, LookupHelper $lookupHelper)
{
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
$model = $this->getModel('lead.company');
if ('GET' === $request->getMethod()) {
$data = $request->query->all()['clearbit_batch_lookup'] ?? [];
} else {
$data = $request->request->all()['clearbit_batch_lookup'] ?? [];
}
$entities = [];
if (array_key_exists('ids', $data)) {
$ids = $data['ids'];
if (!is_array($ids)) {
$ids = json_decode($ids, true);
}
if (is_array($ids) && count($ids)) {
$entities = $model->getEntities(
[
'filter' => [
'force' => [
[
'column' => 'comp.id',
'expr' => 'in',
'value' => $ids,
],
],
],
'ignore_paginator' => true,
]
);
}
}
$lookupWebsites = [];
if ($count = count($entities)) {
/** @var Company $company */
foreach ($entities as $company) {
if ($company->getFieldValue('companywebsite')) {
$website = $company->getFieldValue('companywebsite');
$parse = parse_url($website);
if (!isset($parse['host'])) {
continue;
}
$lookupWebsites[$company->getId()] = $parse['host'];
}
}
$count = count($lookupWebsites);
}
if (0 === $count) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.compempty'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
} else {
if ($count > 20) {
$this->addFlashMessage(
$this->translator->trans('mautic.plugin.clearbit.comptoomany'),
[],
'error'
);
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
if ('GET' === $request->getMethod()) {
$route = $this->generateUrl(
'mautic_plugin_clearbit_action',
[
'objectAction' => 'batchLookupCompany',
]
);
return $this->delegateView(
[
'viewParameters' => [
'form' => $this->createForm(
BatchLookupType::class,
[],
[
'action' => $route,
]
)->createView(),
'lookupItems' => array_values($lookupWebsites),
],
'contentTemplate' => '@MauticClearbit/Clearbit/batchLookup.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_company_index',
'mauticContent' => 'companyBatch',
'route' => $route,
],
]
);
} else {
if ('POST' === $request->getMethod()) {
$notify = array_key_exists('notify', $data);
foreach ($lookupWebsites as $id => $lookupWebsite) {
if ($company = $model->getEntity($id)) {
try {
$lookupHelper->lookupCompany($company, $notify);
} catch (\Exception $ex) {
$this->addFlashMessage(
$ex->getMessage(),
[],
'error'
);
--$count;
}
}
}
if ($count) {
$this->addFlashMessage(
'mautic.company.batch_companies_affected',
[
'%count%' => $count,
]
);
}
return new JsonResponse(
[
'closeModal' => true,
'flashes' => $this->getFlashContent(),
]
);
}
}
return new Response('Bad Request', 400);
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Controller;
use Mautic\FormBundle\Controller\FormController;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Model\UserModel;
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class PublicController extends FormController
{
/**
* Write a notification.
*
* @param string $message Message of the notification
* @param string $header Header for message
* @param string $iconClass CSS class for the icon (e.g. ri-eye-line)
* @param User|null $user User object; defaults to current user
*/
public function addNewNotification($message, $header, $iconClass, User $user): void
{
/** @var \Mautic\CoreBundle\Model\NotificationModel $notificationModel */
$notificationModel = $this->getModel('core.notification');
$notificationModel->addNotification($message, 'FullContact', false, $header, $iconClass, null, $user);
}
/**
* @throws \InvalidArgumentException
*/
public function callbackAction(Request $request, LoggerInterface $mauticLogger, LookupHelper $lookupHelper): Response
{
if (!$request->request->has('body') || !$request->request->has('id')
|| !$request->request->has('type')
|| !$request->request->has('status')
|| 200 !== $request->request->get('status')
) {
$mauticLogger->log('error', 'ERROR on Clearbit callback: Malformed request variables: '.json_encode($request->request->all(), JSON_PRETTY_PRINT));
return new Response('ERROR');
}
/** @var array $result */
$result = $request->request->all()['body'] ?? [];
$oid = $request->request->get('id');
$validatedRequest = $lookupHelper->validateRequest($oid, $request->request->get('type'));
if (!$validatedRequest || !is_array($result)) {
$mauticLogger->log('error', 'ERROR on Clearbit callback: Wrong body or id in request: id='.$oid.' body='.json_encode($result, JSON_PRETTY_PRINT));
return new Response('ERROR');
}
$notify = $validatedRequest['notify'];
try {
if ('person' === $request->request->get('type')) {
/** @var \Mautic\LeadBundle\Model\LeadModel $model */
$model = $this->getModel('lead');
/** @var Lead $lead */
$lead = $validatedRequest['entity'];
$currFields = $lead->getFields(true);
$mauticLogger->log('debug', 'CURRFIELDS: '.var_export($currFields, true));
$loc = [];
if (array_key_exists('geo', $result)) {
$loc = $result['geo'];
}
$data = [];
foreach ([
'facebook' => 'http://www.facebook.com/',
'linkedin' => 'http://www.linkedin.com/',
'twitter' => 'http://www.twitter.com/',
] as $p => $u) {
foreach ($result as $type => $socialProfile) {
if ($type === $p && empty($currFields[$p]['value'])) {
$data[$p] = (array_key_exists('handle', $socialProfile) && $socialProfile['handle']) ? $u.$socialProfile['handle'] : '';
break;
}
}
}
if (array_key_exists('name', $result)
&& array_key_exists(
'familyName',
$result['name']
)
&& empty($currFields['lastname']['value'])
) {
$data['lastname'] = $result['name']['familyName'];
}
if (array_key_exists('name', $result)
&& array_key_exists(
'givenName',
$result['name']
)
&& empty($currFields['firstname']['value'])
) {
$data['firstname'] = $result['name']['givenName'];
}
if (array_key_exists('site', $result) && empty($currFields['website']['value'])) {
$data['website'] = $result['site'];
}
if (array_key_exists('employment', $result)
&& array_key_exists(
'name',
$result['employment']
)
&& empty($currFields['company']['value'])
) {
$data['company'] = $result['employment']['name'];
}
if (array_key_exists('employment', $result)
&& array_key_exists(
'title',
$result['employment']
)
&& empty($currFields['position']['value'])
) {
$data['position'] = $result['employment']['title'];
}
if (array_key_exists('city', $loc) && empty($currFields['city']['value'])) {
$data['city'] = $loc['city'];
}
if (array_key_exists('state', $loc) && empty($currFields['state']['value'])) {
$data['state'] = $loc['state'];
}
if (array_key_exists('country', $loc) && empty($currFields['country']['value'])) {
$data['country'] = $loc['country'];
}
$mauticLogger->log('debug', 'SETTING FIELDS: '.print_r($data, true));
// Unset the nonce so that it's not used again
$socialCache = $lead->getSocialCache();
unset($socialCache['clearbit']['nonce']);
$lead->setSocialCache($socialCache);
$model->setFieldValues($lead, $data);
$model->saveEntity($lead);
if ($notify && (!isset($lead->imported) || !$lead->imported)) {
/** @var UserModel $userModel */
$userModel = $this->getModel('user');
if ($user = $userModel->getEntity($notify)) {
$this->addNewNotification(
sprintf($this->translator->trans('mautic.plugin.clearbit.contact_retrieved'), $lead->getEmail()),
'Clearbit Plugin',
'ri-search-line',
$user
);
}
}
} else {
/****************** COMPANY STUFF *********************/
if ('company' === $request->request->get('type')) {
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
$model = $this->getModel('lead.company');
/** @var Company $company */
$company = $validatedRequest['entity'];
$currFields = $company->getFields(true);
$loc = [];
if (array_key_exists('geo', $result)) {
$loc = $result['geo'];
}
$data = [];
if (array_key_exists('streetNumber', $loc)
&& array_key_exists(
'streetName',
$loc
)
&& empty($currFields['companyaddress1']['value'])
) {
$data['companyaddress1'] = $loc['streetNumber'].' '.$loc['streetName'];
}
if (array_key_exists('city', $loc) && empty($currFields['companycity']['value'])) {
$data['companycity'] = $loc['city'];
}
if (array_key_exists('metrics', $result)
&& array_key_exists(
'employees',
$result['metrics']
)
&& empty($currFields['companynumber_of_employees']['value'])
) {
$data['companynumber_of_employees'] = $result['metrics']['employees'];
}
if (array_key_exists('description', $result) && empty($currFields['companydescription']['value'])) {
$data['companydescription'] = $result['description'];
}
if (array_key_exists('phone', $result) && empty($currFields['companyphone']['value'])) {
$data['companyphone'] = $result['phone'];
}
if (array_key_exists('site', $result)
&& array_key_exists(
'emailAddresses',
$result['site']
)
&& count($result['site']['emailAddresses'])
&& empty($currFields['companyemail']['value'])
) {
$data['companyemail'] = $result['site']['emailAddresses'][0];
}
if (array_key_exists('country', $loc) && empty($currFields['companycountry']['value'])) {
$data['companycountry'] = $loc['country'];
}
if (array_key_exists('state', $loc) && empty($currFields['companystate']['value'])) {
$data['companystate'] = $loc['state'];
}
$mauticLogger->log('debug', 'SETTING FIELDS: '.print_r($data, true));
// Unset the nonce so that it's not used again
$socialCache = $company->getSocialCache();
unset($socialCache['clearbit']['nonce']);
$company->setSocialCache($socialCache);
$model->setFieldValues($company, $data);
$model->saveEntity($company);
if ($notify) {
/** @var UserModel $userModel */
$userModel = $this->getModel('user');
if ($user = $userModel->getEntity($notify)) {
$this->addNewNotification(
sprintf($this->translator->trans('mautic.plugin.clearbit.company_retrieved'), $company->getName()),
'Clearbit Plugin',
'ri-search-line',
$user
);
}
}
}
}
} catch (\Exception $ex) {
$mauticLogger->log('error', 'ERROR on Clearbit callback: '.$ex->getMessage());
try {
if ($notify) {
/** @var UserModel $userModel */
$userModel = $this->getModel('user');
if ($user = $userModel->getEntity($notify)) {
$this->addNewNotification(
sprintf(
$this->translator->trans('mautic.plugin.clearbit.unable'),
$ex->getMessage()
),
'Clearbit Plugin',
'ri-error-warning-line',
$user
);
}
}
} catch (\Exception $ex2) {
$mauticLogger->log('error', 'Clearbit: '.$ex2->getMessage());
}
}
return new Response('OK');
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\MauticClearbitBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class MauticClearbitExtension 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,141 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\Event\CustomButtonEvent;
use Mautic\CoreBundle\Twig\Helper\ButtonHelper;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ButtonSubscriber implements EventSubscriberInterface
{
public function __construct(
private IntegrationHelper $helper,
private TranslatorInterface $translator,
private RouterInterface $router,
) {
}
public static function getSubscribedEvents(): array
{
return [
CoreEvents::VIEW_INJECT_CUSTOM_BUTTONS => ['injectViewButtons', 0],
];
}
public function injectViewButtons(CustomButtonEvent $event): void
{
/** @var ClearbitIntegration $myIntegration */
$myIntegration = $this->helper->getIntegrationObject('Clearbit');
if (false === $myIntegration || !$myIntegration->getIntegrationSettings()->getIsPublished()) {
return;
}
if (str_starts_with($event->getRoute(), 'mautic_contact_')) {
$event->addButton(
[
'attr' => [
'class' => 'btn btn-ghost btn-sm btn-nospin',
'data-toggle' => 'ajaxmodal',
'data-target' => '#MauticSharedModal',
'onclick' => 'this.href=\''.
$this->router->generate(
'mautic_plugin_clearbit_action',
['objectAction' => 'batchLookupPerson']
).
'?\' + mQuery.param({\'clearbit_batch_lookup\':{\'ids\':JSON.parse(Mautic.getCheckedListIds(false, true))}});return true;',
'data-header' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
],
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
'iconClass' => 'ri-search-line',
],
ButtonHelper::LOCATION_BULK_ACTIONS
);
if ($event->getItem()) {
$lookupContactButton = [
'attr' => [
'data-toggle' => 'ajaxmodal',
'data-target' => '#MauticSharedModal',
'data-header' => $this->translator->trans(
'mautic.plugin.clearbit.lookup.header',
['%item%' => $event->getItem()->getEmail()]
),
'href' => $this->router->generate(
'mautic_plugin_clearbit_action',
['objectId' => $event->getItem()->getId(), 'objectAction' => 'lookupPerson']
),
],
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
'iconClass' => 'ri-search-line',
];
$event->addButton(
$lookupContactButton,
ButtonHelper::LOCATION_PAGE_ACTIONS,
['mautic_contact_action', ['objectAction' => 'view']]
);
$event->addButton(
$lookupContactButton,
ButtonHelper::LOCATION_LIST_ACTIONS,
'mautic_contact_index'
);
}
} else {
if (str_starts_with($event->getRoute(), 'mautic_company_')) {
$event->addButton(
[
'attr' => [
'class' => 'btn btn-ghost btn-sm btn-nospin',
'data-toggle' => 'ajaxmodal',
'data-target' => '#MauticSharedModal',
'onclick' => 'this.href=\''.
$this->router->generate(
'mautic_plugin_clearbit_action',
['objectAction' => 'batchLookupCompany']
).
'?\' + mQuery.param({\'clearbit_batch_lookup\':{\'ids\':JSON.parse(Mautic.getCheckedListIds(false, true))}});return true;',
'data-header' => $this->translator->trans(
'mautic.plugin.clearbit.button.caption'
),
],
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
'iconClass' => 'ri-search-line',
],
ButtonHelper::LOCATION_BULK_ACTIONS
);
if ($event->getItem()) {
$lookupCompanyButton = [
'attr' => [
'data-toggle' => 'ajaxmodal',
'data-target' => '#MauticSharedModal',
'data-header' => $this->translator->trans(
'mautic.plugin.clearbit.lookup.header',
['%item%' => $event->getItem()->getName()]
),
'href' => $this->router->generate(
'mautic_plugin_clearbit_action',
['objectId' => $event->getItem()->getId(), 'objectAction' => 'lookupCompany']
),
],
'btnText' => $this->translator->trans('mautic.plugin.clearbit.button.caption'),
'iconClass' => 'ri-search-line',
];
$event->addButton(
$lookupCompanyButton,
ButtonHelper::LOCATION_LIST_ACTIONS,
'mautic_company_index'
);
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\EventListener;
use Mautic\LeadBundle\Event\CompanyEvent;
use Mautic\LeadBundle\Event\LeadEvent;
use Mautic\LeadBundle\LeadEvents;
use MauticPlugin\MauticClearbitBundle\Helper\LookupHelper;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LeadSubscriber implements EventSubscriberInterface
{
public function __construct(
private LookupHelper $lookupHelper,
) {
}
public static function getSubscribedEvents(): array
{
return [
LeadEvents::LEAD_POST_SAVE => ['leadPostSave', 0],
LeadEvents::COMPANY_POST_SAVE => ['companyPostSave', 0],
];
}
public function leadPostSave(LeadEvent $event): void
{
$this->lookupHelper->lookupContact($event->getLead(), true, true);
}
public function companyPostSave(CompanyEvent $event): void
{
$this->lookupHelper->lookupCompany($event->getCompany(), true, true);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class BatchLookupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('ids', HiddenType::class);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.submit',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
$builder->add(
'notify',
YesNoButtonGroupType::class,
[
'label' => 'mautic.plugin.clearbit.notify',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'data' => true,
'required' => false,
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function getBlockPrefix(): string
{
return 'clearbit_batch_lookup';
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class LookupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'objectId',
HiddenType::class,
[
'attr' => [
'value' => $options['data']['objectId'],
],
]
);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.submit',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
$builder->add(
'notify',
YesNoButtonGroupType::class,
[
'label' => 'mautic.plugin.clearbit.notify',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'data' => true,
'required' => false,
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function getBlockPrefix(): string
{
return 'clearbit_lookup';
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Helper;
use Mautic\CoreBundle\Helper\EncryptionHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticClearbitBundle\Integration\ClearbitIntegration;
use MauticPlugin\MauticClearbitBundle\Services\Clearbit_Company;
use MauticPlugin\MauticClearbitBundle\Services\Clearbit_Person;
use Monolog\Logger;
class LookupHelper
{
/**
* @var bool|ClearbitIntegration
*/
protected $integration;
public function __construct(
IntegrationHelper $integrationHelper,
protected UserHelper $userHelper,
protected Logger $logger,
protected LeadModel $leadModel,
protected CompanyModel $companyModel,
) {
$this->integration = $integrationHelper->getIntegrationObject('Clearbit');
}
/**
* @param bool $notify
* @param bool $checkAuto
*/
public function lookupContact(Lead $lead, $notify = false, $checkAuto = false): void
{
if (!$lead->getEmail()) {
return;
}
/* @var Clearbit_Person $clearbit */
if ($clearbit = $this->getClearbit()) {
if (!$checkAuto || ($checkAuto && $this->integration->shouldAutoUpdate())) {
try {
[$cacheId, $webhookId, $cache] = $this->getCache($lead, $notify);
if (!array_key_exists($cacheId, $cache['clearbit'])) {
$clearbit->setWebhookId($webhookId);
$res = $clearbit->lookupByEmail($lead->getEmail());
// Prevent from filling up the cache
$cache['clearbit'] = [
$cacheId => serialize($res),
'nonce' => $cache['clearbit']['nonce'],
];
$lead->setSocialCache($cache);
if ($checkAuto) {
$this->leadModel->getRepository()->saveEntity($lead);
} else {
$this->leadModel->saveEntity($lead);
}
}
} catch (\Exception $ex) {
$this->logger->log('error', 'Error while using Clearbit to lookup '.$lead->getEmail().': '.$ex->getMessage());
}
}
}
}
/**
* @param bool $notify
* @param bool $checkAuto
*/
public function lookupCompany(Company $company, $notify = false, $checkAuto = false): void
{
if (!$website = $company->getFieldValue('companywebsite')) {
return;
}
/* @var Clearbit_Company $clearbit */
if ($clearbit = $this->getClearbit(false)) {
if (!$checkAuto || ($checkAuto && $this->integration->shouldAutoUpdate())) {
try {
$parse = parse_url($company->getFieldValue('companywebsite'));
[$cacheId, $webhookId, $cache] = $this->getCache($company, $notify);
if (isset($parse['host']) && !array_key_exists($cacheId, $cache['clearbit'])) {
/* @var Router $router */
$clearbit->setWebhookId($webhookId);
$res = $clearbit->lookupByDomain($parse['host']);
// Prevent from filling up the cache
$cache['clearbit'] = [
$cacheId => serialize($res),
'nonce' => $cache['clearbit']['nonce'],
];
$company->setSocialCache($cache);
if ($checkAuto) {
$this->companyModel->getRepository()->saveEntity($company);
} else {
$this->companyModel->saveEntity($company);
}
}
} catch (\Exception $ex) {
$this->logger->log('error', 'Error while using Clearbit to lookup '.$parse['host'].': '.$ex->getMessage());
}
}
}
}
public function validateRequest($oid, $type)
{
// prefix#entityId#hour#userId#nonce
[$w, $id, $hour, $uid, $nonce] = explode('#', $oid, 5);
$notify = (str_contains($w, '_notify') && $uid) ? $uid : false;
switch ($type) {
case 'person':
$entity = $this->leadModel->getEntity($id);
break;
case 'company':
$entity = $this->companyModel->getEntity($id);
break;
}
if ($entity) {
$socialCache = $entity->getSocialCache();
$cacheId = $w.'#'.$id.'#'.$hour;
if (isset($socialCache['clearbit'][$cacheId]) && !empty($socialCache['clearbit']['nonce']) && !empty($nonce)
&& $socialCache['clearbit']['nonce'] === $nonce
) {
return [
'notify' => $notify,
'entity' => $entity,
];
}
}
return false;
}
/**
* @param bool $person
*
* @return bool|Clearbit_Company|Clearbit_Person
*/
protected function getClearbit($person = true)
{
if (!$this->integration || !$this->integration->getIntegrationSettings()->getIsPublished()) {
return false;
}
// get api_key from plugin settings
$keys = $this->integration->getDecryptedApiKeys();
return ($person) ? new Clearbit_Person($keys['apikey']) : new Clearbit_Company($keys['apikey']);
}
protected function getCache($entity, $notify): array
{
/** @var User $user */
$user = $this->userHelper->getUser();
$nonce = substr(EncryptionHelper::generateKey(), 0, 16);
$cacheId = sprintf('clearbit%s#', $notify ? '_notify' : '').$entity->getId().'#'.gmdate('YmdH');
$webhookId = $cacheId.'#'.$user->getId().'#'.$nonce;
$cache = $entity->getSocialCache();
if (!isset($cache['clearbit'])) {
$cache['clearbit'] = [];
}
$cache['clearbit']['nonce'] = $nonce;
return [$cacheId, $webhookId, $cache];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Integration;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class ClearbitIntegration extends AbstractIntegration
{
public function getName(): string
{
return 'Clearbit';
}
/**
* Return's authentication method such as oauth2, oauth1a, key, etc.
*/
public function getAuthenticationType(): string
{
return 'none';
}
/**
* Return array of key => label elements that will be converted to inputs to
* obtain from the user.
*
* @return array<string, string>
*/
public function getRequiredKeyFields(): array
{
// Do not rename field. clearbit.js depends on it
return [
'apikey' => 'mautic.integration.clearbit.apikey',
];
}
/**
* @param FormBuilder|Form $builder
* @param array $data
* @param string $formArea
*/
public function appendToForm(&$builder, $data, $formArea): void
{
if ('keys' === $formArea) {
$builder->add(
'auto_update',
YesNoButtonGroupType::class,
[
'label' => 'mautic.plugin.clearbit.auto_update',
'data' => isset($data['auto_update']) && (bool) $data['auto_update'],
'attr' => [
'tooltip' => 'mautic.plugin.clearbit.auto_update.tooltip',
],
]
);
}
}
public function shouldAutoUpdate(): bool
{
$featureSettings = $this->getKeys();
return isset($featureSettings['auto_update']) && (bool) $featureSettings['auto_update'];
}
/**
* @return string|array
*/
public function getFormNotes($section)
{
if ('custom' === $section) {
return [
'template' => '@MauticClearbit/Integration/form.html.twig',
'parameters' => [
'mauticUrl' => $this->router->generate(
'mautic_plugin_clearbit_index', [], UrlGeneratorInterface::ABSOLUTE_URL
),
],
];
}
return parent::getFormNotes($section);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace MauticPlugin\MauticClearbitBundle;
use Mautic\PluginBundle\Bundle\PluginBundleBase;
class MauticClearbitBundle extends PluginBundleBase
{
}

View File

@@ -0,0 +1,5 @@
# Mautic bundle for Clearbit plugin
## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/MauticClearbitBundle and this is a read-only mirror repository.
**📣 Please make PRs and issues against Mautic Core, not here!**

View File

@@ -0,0 +1,18 @@
<div class="alert alert-info">{{ 'mautic.plugin.clearbit.submit_items'|trans }}</div>
<div style="margin-top: 10px">
<ul class="list-group" style="max-height: 400px;overflow-y: auto">
{% for item in lookupItems %}
<li class="list-group-item">{{ item }}</li>
{% endfor %}
</ul>
</div>
<script>
(function () {
var ids = Mautic.getCheckedListIds(false, true);
if (mQuery('#clearbit_batch_lookup_ids').length) {
mQuery('#clearbit_batch_lookup_ids').val(ids);
}
})();
</script>
{{ form(form) }}

View File

@@ -0,0 +1,7 @@
<div class="alert alert-info">{{ 'mautic.plugin.clearbit.submit'|trans }}</div>
<div style="margin-top: 10px">
<ul class="list-group" style="max-height: 400px;overflow-y: auto">
<li class="list-group-item">{{ lookupItem }}</li>
</ul>
</div>
{{ form(form) }}

View File

@@ -0,0 +1,9 @@
<div class="well well-sm" style="margin-bottom:0 !important;">
<p>
{{ 'mautic.plugin.clearbit.webhook_info'|trans|purify }}
</p>
<div class="alert alert-warning">
{{ 'mautic.plugin.clearbit.public_info'|trans|purify }}
</div>
<input type="text" readonly="" onclick="this.setSelectionRange(0, this.value.length);" value="{{ mauticUrl }}" class="form-control">
</div>

View File

@@ -0,0 +1,132 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Services;
/**
* This class handles the actually HTTP request to the Clearbit endpoint.
*/
class Clearbit_Base
{
public const REQUEST_LATENCY = 0.2;
public const USER_AGENT = 'mautic/clearbit-php-0.1.0';
private \DateTime $_next_req_time;
protected $_baseUri = '';
protected $_resourceUri = '';
protected $_version = 'v2';
protected $_webhookId;
public $response_obj;
public $response_code;
public $response_json;
/**
* Slow down calls to the Clearbit API if needed.
*/
private function _wait_for_rate_limit(): void
{
$now = new \DateTime();
if ($this->_next_req_time->getTimestamp() > $now->getTimestamp()) {
$t = $this->_next_req_time->getTimestamp() - $now->getTimestamp();
sleep($t);
}
}
/**
* @param mixed[] $hdr
*/
private function _update_rate_limit($hdr): void
{
$remaining = (float) $hdr['X-RateLimit-Remaining'];
$reset = (float) $hdr['X-RateLimit-Reset'];
$spacing = $reset / (1.0 + $remaining);
$delay = $spacing - self::REQUEST_LATENCY;
$this->_next_req_time = new \DateTime('now + '.$delay.' seconds');
}
/**
* The base constructor Sets the API key available from here:
* https://dashboard.clearbit.com/keys.
*
* @param string $api_key
*/
public function __construct(
protected $api_key,
) {
$this->_next_req_time = new \DateTime('@0');
}
/**
* @param string $id
*
* @return object
*/
public function setWebhookId($id = null)
{
$this->_webhookId = $id;
return $this;
}
/**
* @param array $params
*
* @return object
*/
protected function _execute($params = [])
{
$this->_wait_for_rate_limit();
if ($this->_webhookId) {
$params['webhook_id'] = $this->_webhookId;
}
$fullUrl = $this->_baseUri.$this->_version.$this->_resourceUri.
'?'.http_build_query($params);
// open connection
$connection = curl_init($fullUrl);
curl_setopt($connection, CURLOPT_RETURNTRANSFER, true);
curl_setopt($connection, CURLOPT_USERAGENT, self::USER_AGENT);
curl_setopt($connection, CURLOPT_HEADER, true); // return HTTP headers with response
curl_setopt($connection, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.$this->api_key]);
// execute request
$resp = curl_exec($connection);
[$response_headers, $this->response_json] = explode("\r\n\r\n", $resp, 2);
// $response_headers now has a string of the HTTP headers
// $response_json is the body of the HTTP response
$headers = [];
foreach (explode("\r\n", $response_headers) as $i => $line) {
if (0 === $i) {
$headers['http_code'] = $line;
} else {
[$key, $value] = explode(': ', $line);
$headers[$key] = $value;
}
}
$this->response_code = curl_getinfo($connection, CURLINFO_HTTP_CODE);
$this->response_obj = json_decode($this->response_json);
if (!in_array($this->response_code, [200, 201, 202], true)) {
throw new \Exception($this->response_obj->error->message);
} else {
if ('200' === $this->response_code) {
$this->_update_rate_limit($headers);
}
}
return $this->response_obj;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Services;
/**
* This class handles everything related to the Company lookup API.
*/
class Clearbit_Company extends Clearbit_Base
{
public function __construct($api_key)
{
parent::__construct($api_key);
$this->_baseUri = 'https://company.clearbit.com/';
$this->_resourceUri = '/companies/find';
}
public function lookupByDomain($search)
{
$this->_execute(['domain' => $search]);
return $this->response_obj;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace MauticPlugin\MauticClearbitBundle\Services;
/**
* This class handles everything related to the Person lookup API.
*/
class Clearbit_Person extends Clearbit_Base
{
protected $_resourceUri = '/people/find';
protected $_baseUri = 'https://person.clearbit.com/';
public function lookupByEmail($search)
{
$this->_execute(['email' => $search]);
return $this->response_obj;
}
}

View File

@@ -0,0 +1,21 @@
mautic.integration.clearbit.apikey="Clearbit API Key"
mautic.plugin.clearbit.button.caption="Lookup using Clearbit"
mautic.plugin.clearbit.lookup.header="Clearbit - Lookup information for %item%"
mautic.plugin.clearbit.test_api="Test API and get Stats"
mautic.plugin.clearbit.stats="Test Results"
mautic.plugin.clearbit.toomany="You can only lookup 20 contacts at once!"
mautic.plugin.clearbit.comptoomany="You can only lookup 20 companies at once!"
mautic.plugin.clearbit.empty="There are no contacts to lookup!"
mautic.plugin.clearbit.compempty="There are no company domains to lookup!<br/>(Company website is empty?)"
mautic.plugin.clearbit.forbidden="You don't have permissions to update this contact"
mautic.plugin.clearbit.compforbidden="You don't have permissions to update this company"
mautic.plugin.clearbit.auto_update="Automatically update on save?"
mautic.plugin.clearbit.auto_update.tooltip="WARNING: This could easily exhaust your quota of API calls per month."
mautic.plugin.clearbit.notify="Show a notification when the information has been received."
mautic.plugin.clearbit.contact_retrieved="The contact information for %s has been retrieved"
mautic.plugin.clearbit.company_retrieved="The company information for %s has been retrieved"
mautic.plugin.clearbit.unable="Unable to save the information: %s"
mautic.plugin.clearbit.webhook_info="For the plugin to work, you must use the following as the Webhook URL in your account settings on the <a href=\"https://dashboard.clearbit.com/account\" target=\"_blank\">Clearbit Dashboard</a>:"
mautic.plugin.clearbit.public_info="<strong>Warning!</strong> This must be a public accessible URL for the Webhook to work."
mautic.plugin.clearbit.submit="Click submit to lookup the information for:"
mautic.plugin.clearbit.submit_items="Click submit to lookup the information for the selected item(s)."

View File

@@ -0,0 +1,17 @@
{
"name": "mautic/plugin-clearbit",
"description": "Clearbit Plugin",
"type": "mautic-plugin",
"keywords": [
"mautic",
"plugin",
"integration"
],
"extra": {
"install-directory-name": "MauticClearbitBundle"
},
"minimum-stability": "dev",
"require": {
"mautic/core-lib": "^7.0"
}
}