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,41 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event\ListChangeEvent;
class CampaignEventHelper
{
public static function validatePointChange($event, Lead $lead): bool
{
$properties = $event['properties'];
$checkPoints = $properties['points'];
if (!empty($checkPoints)) {
$points = $lead->getPoints();
if ($points < $checkPoints) {
return false;
}
}
return true;
}
public static function validateListChange(ListChangeEvent $eventDetails, $event): bool
{
$limitAddTo = $event['properties']['addedTo'];
$limitRemoveFrom = $event['properties']['removedFrom'];
$list = $eventDetails->getList();
if ($eventDetails->wasAdded() && !empty($limitAddTo) && !in_array($list->getId(), $limitAddTo)) {
return false;
}
if ($eventDetails->wasRemoved() && !empty($limitRemoveFrom) && !in_array($list->getId(), $limitRemoveFrom)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\ClickthroughHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\EmailBundle\Helper\BotRatioHelper;
use Mautic\LeadBundle\DataObject\LeadManipulator;
use Mautic\LeadBundle\Deduplicate\ContactMerger;
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event\ContactIdentificationEvent;
use Mautic\LeadBundle\Exception\ContactNotFoundException;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class ContactRequestHelper
{
/**
* @var Lead|null
*/
private $trackedContact;
private array $queryFields = [];
private array $publiclyUpdatableFieldValues = [];
public function __construct(
private LeadModel $leadModel,
private ContactTracker $contactTracker,
private IpLookupHelper $ipLookupHelper,
private RequestStack $requestStack,
private LoggerInterface $logger,
private EventDispatcherInterface $eventDispatcher,
private ContactMerger $contactMerger,
private StatRepository $statRepository,
private BotRatioHelper $botRatioHelper,
) {
}
public function getContactFromQuery(array $queryFields = []): ?Lead
{
$request = $this->getCurrentRequest();
if ($request && $request->cookies->get('Blocked-Tracking')) {
return null;
}
$ipAddress = $this->ipLookupHelper->getIpAddress();
if (!$ipAddress->isTrackable()) {
return null;
}
$dateTime = new \DateTime();
$userAgent = $request ? $request->server->get('HTTP_USER_AGENT') : '';
if (!empty($queryFields['ct'])) {
$queryFields['ct'] = (is_array($queryFields['ct'])) ? $queryFields['ct'] : ClickthroughHelper::decodeArrayFromUrl($queryFields['ct']);
}
if (isset($queryFields['ct']['stat'])) {
/** @var Stat $stat */
$stat = $this->statRepository->findOneBy(['trackingHash' => $queryFields['ct']['stat']]);
if (null !== $stat && $this->botRatioHelper->isHitByBot($stat, $dateTime, $ipAddress, (string) $userAgent)) {
return null;
}
}
unset($queryFields['page_url']); // This is set now automatically by PageModel
$this->queryFields = $queryFields;
try {
$foundContact = $this->getContactFromUrl();
$this->trackedContact = $foundContact;
$this->contactTracker->setTrackedContact($this->trackedContact);
} catch (ContactNotFoundException) {
}
if (!$this->trackedContact) {
$this->trackedContact = $this->contactTracker->getContact();
}
if (!$this->trackedContact) {
return null;
}
$this->prepareContactFromRequest();
return $this->trackedContact;
}
/**
* @throws ContactNotFoundException
*/
private function getContactFromUrl(): Lead
{
$request = $this->getCurrentRequest();
if ($request && $request->cookies->get('Blocked-Tracking')) {
throw new ContactNotFoundException();
}
// Check for a lead requested through clickthrough query parameter
if (isset($this->queryFields['ct'])) {
$clickthrough = (is_array($this->queryFields['ct'])) ? $this->queryFields['ct'] : ClickthroughHelper::decodeArrayFromUrl($this->queryFields['ct']);
} elseif ($request && $clickthrough = $request->get('ct', [])) {
$clickthrough = ClickthroughHelper::decodeArrayFromUrl($clickthrough);
}
if (!is_array($clickthrough)) {
throw new ContactNotFoundException();
}
try {
return $this->getContactFromClickthrough($clickthrough);
} catch (ContactNotFoundException) {
}
/* @var Lead $foundContact */
if (!empty($this->queryFields)) {
[$foundContact, $this->publiclyUpdatableFieldValues] = $this->leadModel->checkForDuplicateContact(
$this->queryFields,
true,
true
);
if ($this->trackedContact && $this->trackedContact->getId() && $foundContact->getId()) {
try {
$foundContact = $this->contactMerger->merge($this->trackedContact, $foundContact);
} catch (SameContactException) {
}
}
if (is_null($this->trackedContact) or $foundContact->getId() !== $this->trackedContact->getId()) {
// A contact was found by a publicly updatable field
if (!$foundContact->isNew()) {
return $foundContact;
}
}
}
throw new ContactNotFoundException();
}
/**
* Identify a contact through a clickthrough URL.
*
* @return Lead
*
* @throws ContactNotFoundException
*/
private function getContactFromClickthrough(array $clickthrough)
{
$event = new ContactIdentificationEvent($clickthrough);
$this->eventDispatcher->dispatch($event, LeadEvents::ON_CLICKTHROUGH_IDENTIFICATION);
if ($contact = $event->getIdentifiedContact()) {
$this->logger->debug("LEAD: Contact ID# {$contact->getId()} tracked through clickthrough query by the ".$event->getIdentifier().' channel');
// Merge tracked visitor into the clickthrough contact
return $this->mergeWithTrackedContact($contact);
}
throw new ContactNotFoundException();
}
private function prepareContactFromRequest(): void
{
$ipAddress = $this->ipLookupHelper->getIpAddress();
$contactIpAddresses = $this->trackedContact->getIpAddresses();
if (!$contactIpAddresses->contains($ipAddress)) {
$this->trackedContact->addIpAddress($ipAddress);
}
if (!empty($this->publiclyUpdatableFieldValues)) {
$this->leadModel->setFieldValues(
$this->trackedContact,
$this->publiclyUpdatableFieldValues,
false,
true,
true
);
}
// Assume a web request as this is likely a tracking request from DWC or tracking code
$this->trackedContact->setManipulator(
new LeadManipulator(
'page',
'hit',
null,
$this->queryFields['page_url'] ?? ''
)
);
if (isset($this->queryFields['tags'])) {
$this->leadModel->modifyTags($this->trackedContact, $this->queryFields['tags']);
}
}
/**
* @return Lead
*/
private function mergeWithTrackedContact(Lead $foundContact)
{
if ($this->trackedContact && $this->trackedContact->getId() && $this->trackedContact->isAnonymous()) {
try {
return $this->contactMerger->merge($this->trackedContact, $foundContact);
} catch (SameContactException) {
}
}
return $foundContact;
}
private function getCurrentRequest(): ?Request
{
return $this->requestStack->getCurrentRequest();
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
/**
* Helper class custom field operations.
*/
class CustomFieldHelper
{
public const TYPE_BOOLEAN = 'boolean';
public const TYPE_NUMBER = 'number';
public const TYPE_SELECT = 'select';
/**
* Fixes value type for specific field types.
*
* @param string $type
* @param mixed $value
*
* @return mixed
*/
public static function fixValueType($type, $value)
{
if (null === $value) {
// do not transform null values
return null;
}
return match ($type) {
self::TYPE_NUMBER => is_numeric($value) || '' === $value ? (float) $value : $value,
self::TYPE_BOOLEAN => (bool) $value,
self::TYPE_SELECT => is_scalar($value) ? (string) $value : $value,
default => $value,
};
}
/**
* @param mixed $value This value can be at least array, string, null and maybe others
*
* @return mixed|string|null
*/
public static function fieldValueTransfomer(array $field, $value, ?DateTimeHelper $dateTimeHelper = null)
{
if (null === $value) {
// do not transform null values
return null;
}
$type = $field['type'];
switch ($type) {
case 'datetime':
case 'date':
case 'time':
// Not sure if this happens anywhere but just in case do not transform empty strings
if ('' === $value) {
return null;
}
if (!($value instanceof \DateTimeInterface) && !is_string($value)) {
throw new \InvalidArgumentException('Wrong type given. String or DateTimeInterface expected.');
}
$dtHelper = $dateTimeHelper ?: new DateTimeHelper($value, null, 'local');
$dtHelper->setDateTime($value);
switch ($type) {
case 'datetime':
$value = $dtHelper->toLocalString('Y-m-d H:i:s');
break;
case 'date':
$value = $dtHelper->toLocalString('Y-m-d');
break;
case 'time':
$value = $dtHelper->toLocalString('H:i:s');
break;
}
break;
}
return $value;
}
/**
* Transform all fields values.
*
* @param mixed[] $fields
* @param mixed[] $values
*
* @return mixed[]
*/
public static function fieldsValuesTransformer(array $fields, array $values, ?DateTimeHelper $dateTimeHelper = null): array
{
foreach ($values as $alias => &$value) {
if (!empty($fields[$alias]) && is_array($fields[$alias])) {
$value = self::fieldValueTransfomer($fields[$alias], $value, $dateTimeHelper);
}
}
return $values;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\Serializer;
/**
* Helper class custom field operations.
*/
class CustomFieldValueHelper
{
public const TYPE_BOOLEAN = 'boolean';
public const TYPE_SELECT = 'select';
public const TYPE_MULTISELECT = 'multiselect';
public static function normalizeValues(array $customFields): array
{
if (isset($customFields['core'])) {
foreach ($customFields as $group => $fields) {
foreach ($fields as $alias => $field) {
if (is_array($field)) {
$customFields[$group][$alias]['normalizedValue'] = self::normalizeValue($field);
}
}
}
} else {
foreach ($customFields as $alias => &$field) {
if (is_array($field)) {
$customFields[$alias]['normalizedValue'] = self::normalizeValue($field);
}
}
}
return $customFields;
}
/**
* @return mixed
*/
public static function normalizeValue(array $field)
{
$value = $field['value'] ?? '';
$type = $field['type'] ?? null;
$properties = $field['properties'] ?? null;
return self::normalize($value, $type, $properties);
}
/**
* @param string $value
*
* @return string
*/
public static function setValueFromPropertiesList(array $properties, $value)
{
if (isset($properties['list']) && is_array($properties['list'])) {
$list = $properties['list'];
if (!is_array($list)) {
return $value;
}
foreach ($list as $property) {
if (isset($property[$value])) {
return $property[$value];
} elseif (isset($property['value']) && $property['value'] == $value) {
return $property['label'];
}
}
}
return $value;
}
/**
* @param mixed $value
* @param string|null $type
* @param string|array<int, string>|null $properties
*
* @return mixed|string
*/
public static function normalize($value, $type, $properties)
{
if ('' !== $value && $type && $properties) {
if (!is_array($properties)) {
$properties = Serializer::decode($properties);
}
switch ($type) {
case self::TYPE_BOOLEAN:
foreach ($properties as $key => $property) {
if ('yes' === $key && !isset($properties[1])) {
$properties[1] = $property;
} elseif ('no' === $key && !isset($properties[0])) {
$properties[0] = $property;
}
}
if (isset($properties[$value])) {
$value = $properties[$value];
}
break;
case self::TYPE_SELECT:
$value = self::setValueFromPropertiesList($properties, $value);
break;
case self::TYPE_MULTISELECT:
$values = explode('|', $value);
foreach ($values as &$val) {
$val = self::setValueFromPropertiesList($properties, $val);
}
$value = implode('|', $values);
break;
}
}
return $value;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Helper;
use Mautic\ChannelBundle\Helper\ChannelListHelper;
use Mautic\LeadBundle\Entity\DoNotContact;
use Symfony\Contracts\Translation\TranslatorInterface;
final class DncFormatterHelper
{
/**
* @var array<int, string>|null
*/
private ?array $dncReasons = null;
public function __construct(
private TranslatorInterface $translator,
private ChannelListHelper $channelListHelper,
) {
}
/**
* Returns all available DNC reasons.
*
* @return array<int, string>
*/
public function getDncReasons(): array
{
if (null === $this->dncReasons) {
$this->dncReasons = [
DoNotContact::IS_CONTACTABLE => $this->translator->trans('mautic.lead.report.dnc_contactable'),
DoNotContact::UNSUBSCRIBED => $this->translator->trans('mautic.lead.report.dnc_unsubscribed'),
DoNotContact::BOUNCED => $this->translator->trans('mautic.lead.report.dnc_bounced'),
DoNotContact::MANUAL => $this->translator->trans('mautic.lead.report.dnc_manual'),
];
}
return $this->dncReasons;
}
/**
* Gets the label for a specific DNC reason.
*
* @throws \InvalidArgumentException if the reason ID is invalid
*/
public function getDncReasonLabel(int $reasonId): string
{
$reasons = $this->getDncReasons();
if (!isset($reasons[$reasonId])) {
throw new \InvalidArgumentException(sprintf('Invalid DNC reason ID: %d', $reasonId));
}
return $reasons[$reasonId];
}
public function printReasonWithChannel(int $reason, string $channel): string
{
return $this->getDncReasonLabel($reason).': '.$this->channelListHelper->getChannelLabel($channel);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Field\FieldList;
class FakeContactHelper
{
public function __construct(
private readonly FieldList $fieldList,
) {
}
/**
* @return array<int|string, int|string|array<int|string, mixed>|null>
*/
public function prepareFakeContactWithPrimaryCompany(): array
{
$contact = $this->prepareFakeEntity('lead');
$company = $this->prepareFakeEntity('company');
$company['is_primary'] = 1;
$contact['companies'][] = $company;
return $contact;
}
/**
* @return array<int|string, int|string|array<int|string, mixed>|null>
*/
private function prepareFakeEntity(string $object): array
{
$fields = $this->fieldList->getFieldList(false, false, [
'isPublished' => true,
'object' => $object,
]);
array_walk($fields, function (&$field): void {
$field = "[$field]";
});
$fields['id'] = 0;
return $fields;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Model\FieldModel;
class FieldAliasHelper
{
public function __construct(
private FieldModel $fieldModel,
) {
}
/**
* Cleans the alias and if it's not unique it will make it unique.
*/
public function makeAliasUnique(LeadField $field): LeadField
{
// alias cannot be changed for existing fields
if ($field->getId()) {
return $field;
}
// set alias as name if alias is empty
$alias = ($field->getAlias() ?: $field->getName()) ?: '';
// clean the alias
$alias = $this->fieldModel->cleanAlias($alias, 'f_', 25);
// make sure alias is not already taken
$repo = $this->fieldModel->getRepository();
$testAlias = $alias;
$aliases = $repo->getAliases($field->getId(), false, true, null);
$count = (int) in_array($testAlias, $aliases);
$aliasTag = $count;
while ($count) {
$testAlias = $alias.$aliasTag;
$count = (int) in_array($testAlias, $aliases);
++$aliasTag;
}
if ($testAlias !== $alias) {
$alias = $testAlias;
}
$field->setAlias($alias);
return $field;
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\AbstractFormFieldHelper;
use Symfony\Component\Intl\Locales;
class FormFieldHelper extends AbstractFormFieldHelper
{
private static array $types = [
'text' => [
'properties' => [],
],
'textarea' => [
'properties' => [],
],
'multiselect' => [
'properties' => [
'list' => [
'required' => true,
'error_msg' => 'mautic.lead.field.select.listmissing',
],
],
],
'select' => [
'properties' => [
'list' => [
'required' => true,
'error_msg' => 'mautic.lead.field.select.listmissing',
],
],
],
'boolean' => [
'properties' => [
'yes' => [
'required' => true,
'error_msg' => 'mautic.lead.field.boolean.yesmissing',
],
'no' => [
'required' => true,
'error_msg' => 'mautic.lead.field.boolean.nomissing',
],
],
],
'lookup' => [
'properties' => [
'list' => [],
],
],
'date' => [
'properties' => [
'format' => [],
],
],
'datetime' => [
'properties' => [
'format' => [],
],
],
'time' => [
'properties' => [],
],
'timezone' => [
'properties' => [],
],
'email' => [
'properties' => [],
],
'html' => [
'properties' => [],
],
'number' => [
'properties' => [
'roundmode' => [],
'scale' => [],
],
],
'tel' => [
'properties' => [],
],
'url' => [
'properties' => [],
],
'country' => [
'properties' => [],
],
'region' => [
'properties' => [],
],
'locale' => [
'properties' => [],
],
];
/**
* Set the translation key prefix.
*/
public function setTranslationKeyPrefix(): void
{
$this->translationKeyPrefix = 'mautic.lead.field.type.';
}
/**
* @return array
*/
public function getTypes()
{
return self::$types;
}
public static function getListTypes(): array
{
return ['select', 'multiselect', 'boolean', 'lookup', 'country', 'region', 'timezone', 'locale'];
}
/**
* @return array{0: bool, 1:string}
*/
public static function validateProperties($type, &$properties): array
{
if (!array_key_exists($type, self::$types)) {
// ensure the field type is supported
return [false, 'mautic.lead.field.typenotrecognized'];
}
$fieldType = self::$types[$type]['properties'];
foreach ($fieldType as $key => $property) {
$value = array_key_exists($key, $properties) ? $properties[$key] : null;
if (!empty($property['required']) && empty($value)) {
return [false, $property['error_msg']];
}
}
return [true, ''];
}
/**
* @return array<string, string>
*/
public static function getCountryChoices(): array
{
$customFile = $_ENV['MAUTIC_UPLOAD_DIR'].'/countries.json';
$listFile = file_exists($customFile) ? $customFile : __DIR__.'/../../CoreBundle/Assets/json/countries.json';
$json = file_get_contents($listFile);
$countries = json_decode($json);
return array_combine($countries, $countries);
}
/**
* @return array<string, array<string, string>>
*/
public static function getRegionChoices(): array
{
$customFile = $_ENV['MAUTIC_UPLOAD_DIR'].'/regions.json';
$listFile = file_exists($customFile) ? $customFile : __DIR__.'/../../CoreBundle/Assets/json/regions.json';
$json = file_get_contents($listFile);
$regions = json_decode($json);
$choices = [];
foreach ($regions as $country => $regionGroup) {
$choices[$country] = array_combine($regionGroup, $regionGroup);
}
return $choices;
}
/**
* Symfony deprecated and changed Symfony\Component\Form\Extension\Core\Type\TimezoneType::getTimezones to private
* in 3.0 - so duplicated code here.
*
* @return array<string, mixed>
*/
public static function getTimezonesChoices()
{
static $timezones;
if (null === $timezones) {
$timezones = [];
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);
if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}
$timezones[$region][str_replace('_', ' ', $name)] = $timezone;
}
}
return $timezones;
}
/**
* Get locale choices.
*
* @return array<string, string>
*/
public static function getLocaleChoices(): array
{
return array_flip(Locales::getNames());
}
/**
* Get date field choices.
*/
public function getDateChoices(): array
{
return [
'anniversary' => $this->translator->trans('mautic.campaign.event.timed.choice.anniversary'),
'+P0D' => $this->translator->trans('mautic.campaign.event.timed.choice.today'),
'-P1D' => $this->translator->trans('mautic.campaign.event.timed.choice.yesterday'),
'+P1D' => $this->translator->trans('mautic.campaign.event.timed.choice.tomorrow'),
];
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Exception\UniqueFieldNotFoundException;
use Mautic\LeadBundle\Model\CompanyModel;
class IdentifyCompanyHelper
{
/**
* @param array $data
* @param mixed $lead
*/
public static function identifyLeadsCompany($data, $lead, CompanyModel $companyModel): array
{
$addContactToCompany = true;
$parameters = self::normalizeParameters($data);
if (!self::hasCompanyParameters($parameters, $companyModel)) {
return [null, false, null];
}
try {
$companies = $companyModel->checkForDuplicateCompanies($parameters);
} catch (UniqueFieldNotFoundException) {
return [null, false, null];
}
if (!empty($companies)) {
$companyEntity = end($companies);
$companyData = $companyEntity->getProfileFields();
if ($lead) {
$companyLeadRepo = $companyModel->getCompanyLeadRepository();
$companyLead = $companyLeadRepo->getCompaniesByLeadId($lead->getId(), $companyEntity->getId());
if (!empty($companyLead)) {
$addContactToCompany = false;
}
}
} else {
$companyData = $parameters;
// create new company
$companyEntity = new Company();
$companyModel->setFieldValues($companyEntity, $companyData, true);
$companyModel->saveEntity($companyEntity);
$companyData['id'] = $companyEntity->getId();
}
return [$companyData, $addContactToCompany, $companyEntity];
}
public static function findCompany(array $data, CompanyModel $companyModel): array
{
$parameters = self::normalizeParameters($data);
if (!self::hasCompanyParameters($parameters, $companyModel)) {
return [[], []];
}
try {
$companyEntities = $companyModel->checkForDuplicateCompanies($parameters);
} catch (UniqueFieldNotFoundException) {
return [[], []];
}
$companyData = $parameters;
if (!empty($companyEntities)) {
$key = array_key_last($companyEntities);
$companyData['id'] = $companyEntities[$key]->getId();
}
return [$companyData, $companyEntities];
}
private static function hasCompanyParameters(array $parameters, CompanyModel $companyModel): bool
{
$companyFields = $companyModel->fetchCompanyFields();
foreach ($parameters as $alias => $value) {
foreach ($companyFields as $companyField) {
if ($companyField['alias'] === $alias) {
return true;
}
}
}
return false;
}
/**
* @param mixed[] $parameters
*
* @return mixed[]
*/
private static function normalizeParameters(array $parameters): array
{
if (isset($parameters['company'])) {
$parameters['companyname'] = filter_var($parameters['company']);
unset($parameters['company']);
}
$fields= ['country', 'city', 'state'];
foreach ($fields as $field) {
if (isset($parameters[$field]) && !isset($parameters['company'.$field])) {
$parameters['company'.$field] = $parameters[$field];
unset($parameters[$field]);
}
}
return $parameters;
}
/**
* Checks if email address' domain has a DNS MX record. Returns the domain if found.
*
* @param string $email
*
* @return string|false
*/
protected static function domainExists($email)
{
if (!strstr($email, '@')) { // not a valid email adress
return false;
}
[$user, $domain] = explode('@', $email);
$arr = dns_get_record($domain, DNS_MX);
if (empty($arr)) {
return false;
}
if ($arr[0]['host'] === $domain) {
return $domain;
}
return false;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event as Events;
use Mautic\LeadBundle\LeadEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class LeadChangeEventDispatcher
{
private ?Lead $lead = null;
private ?array $changes = null;
public function __construct(
private EventDispatcherInterface $dispatcher,
) {
}
public function dispatchEvents(Events\LeadEvent $event, array $changes): void
{
$this->lead = $event->getLead();
$this->changes = $changes;
$this->dispatchDateIdentifiedEvent($event);
$this->dispatchPointChangeEvent($event);
$this->dispatchUtmTagsChangeEvent();
$this->dispatchDncChangeEvent();
}
private function dispatchDateIdentifiedEvent(Events\LeadEvent $event): void
{
if (!isset($this->changes['dateIdentified'])) {
return;
}
$this->dispatcher->dispatch($event, LeadEvents::LEAD_IDENTIFIED);
}
private function dispatchPointChangeEvent(Events\LeadEvent $event): void
{
if (!isset($this->changes['points'])) {
return;
}
if ($this->lead->imported) {
return;
}
if ((int) $this->changes['points'][0] <= 0 && (int) $this->changes['points'][1] <= 0) {
return;
}
if ($event->isNew()) {
return;
}
$pointsEvent = new Events\PointsChangeEvent($this->lead, $this->changes['points'][0], $this->changes['points'][1]);
$this->dispatcher->dispatch($pointsEvent, LeadEvents::LEAD_POINTS_CHANGE);
}
private function dispatchUtmTagsChangeEvent(): void
{
if (!isset($this->changes['utmtags'])) {
return;
}
$utmTagsEvent = new Events\LeadUtmTagsEvent($this->lead, $this->changes['utmtags']);
$this->dispatcher->dispatch($utmTagsEvent, LeadEvents::LEAD_UTMTAGS_ADD);
}
private function dispatchDncChangeEvent(): void
{
if (!isset($this->changes['dnc_channel_status'])) {
return;
}
foreach ($this->changes['dnc_channel_status'] as $channel => $status) {
$oldStatus = $status['old_reason'] ?? DoNotContact::IS_CONTACTABLE;
$newStatus = $status['reason'];
$event = new Events\ChannelSubscriptionChange($this->lead, $channel, $oldStatus, $newStatus);
$this->dispatcher->dispatch($event, LeadEvents::CHANNEL_SUBSCRIPTION_CHANGED);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
use Mautic\LeadBundle\Entity\Lead;
class PrimaryCompanyHelper
{
public function __construct(
private CompanyLeadRepository $companyLeadRepository,
) {
}
/**
* @return array|null
*/
public function getProfileFieldsWithPrimaryCompany(Lead $lead)
{
return $this->mergeInPrimaryCompany(
$this->companyLeadRepository->getCompaniesByLeadId($lead->getId()),
$lead->getProfileFields()
);
}
/**
* @return array
*/
public function mergePrimaryCompanyWithProfileFields($contactId, array $profileFields)
{
return $this->mergeInPrimaryCompany(
$this->companyLeadRepository->getCompaniesByLeadId($contactId),
$profileFields
);
}
/**
* @return array
*/
private function mergeInPrimaryCompany(array $companies, array $profileFields)
{
foreach ($companies as $company) {
if (empty($company['is_primary'])) {
continue;
}
unset($company['id'], $company['score'], $company['date_added'], $company['date_associated'], $company['is_primary']);
return array_merge($profileFields, $company);
}
return $profileFields;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\ProgressBarHelper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\OutputInterface;
class Progress
{
/**
* Total number of items representing 100%.
*
* @var int
*/
protected $total = 0;
/**
* Currently proccessed items.
*
* @var int
*/
protected $done = 0;
/**
* @var ProgressBar|null
*/
protected $bar;
public function __construct(
protected ?OutputInterface $output = null,
) {
}
/**
* Returns count of all items.
*
* @return int
*/
public function getTotal()
{
return $this->total;
}
/**
* Set total value.
*
* @param int $total
*
* @return Progress
*/
public function setTotal($total)
{
$this->total = (int) $total;
if ($this->output) {
$this->bar = ProgressBarHelper::init($this->output, $this->total);
$this->bar->start();
}
return $this;
}
/**
* Returns count of processed items.
*
* @return int
*/
public function getDone()
{
return $this->done;
}
/**
* Set total value.
*
* @return Progress
*/
public function setDone($done)
{
$this->done = (int) $done;
if ($this->bar) {
$this->bar->setProgress($this->done);
if ($this->isFinished()) {
$this->bar->finish();
$this->output->writeln('');
}
}
return $this;
}
/**
* Increase done count by 1.
*
* @return Progress
*/
public function increase()
{
$this->setDone($this->done + 1);
return $this;
}
/**
* Checked if the progress is 100 or more %.
*/
public function isFinished(): bool
{
return $this->done >= $this->total;
}
/**
* Bind Progress from simple array.
*
* @return Progress
*/
public function bindArray(array $progress)
{
if (isset($progress[0])) {
$this->setDone($progress[0]);
}
if (isset($progress[1])) {
$this->setTotal($progress[1]);
}
return $this;
}
/**
* Convert this object to a simple array.
*/
public function toArray(): array
{
return [
$this->done,
$this->total,
];
}
/**
* Counts percentage of the progress.
*
* @return int
*/
public function toPercent()
{
return ($this->total) ? ceil(($this->done / $this->total) * 100) : 100;
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Helper;
use Mautic\CacheBundle\Cache\CacheProviderInterface;
use Psr\Cache\InvalidArgumentException;
class SegmentCountCacheHelper
{
public function __construct(
private CacheProviderInterface $cacheProvider,
) {
}
/**
* @throws InvalidArgumentException
*/
public function getSegmentContactCount(int $segmentId): int
{
return (int) $this->cacheProvider->getItem($this->generateCacheKey($segmentId))->get();
}
/**
* @throws InvalidArgumentException
*/
public function setSegmentContactCount(int $segmentId, int $count): void
{
$item = $this->cacheProvider->getItem($this->generateCacheKey($segmentId));
$item->set($count);
$this->cacheProvider->save($item);
if ($this->hasSegmentIdForReCount($segmentId)) {
$this->cacheProvider->deleteItem($this->generateCacheKeyForRecount($segmentId));
}
}
/**
* @throws InvalidArgumentException
*/
public function hasSegmentContactCount(int $segmentId): bool
{
return $this->cacheProvider->hasItem($this->generateCacheKey($segmentId));
}
/**
* @throws InvalidArgumentException
*/
public function hasSegmentIdForReCount(int $segmentId): bool
{
return $this->cacheProvider->hasItem($this->generateCacheKeyForRecount($segmentId));
}
/**
* @throws InvalidArgumentException
*/
public function invalidateSegmentContactCount(int $segmentId): void
{
$item = $this->cacheProvider->getItem($this->generateCacheKeyForRecount($segmentId));
$item->set(true);
$this->cacheProvider->save($item);
}
/**
* @throws InvalidArgumentException
*/
public function incrementSegmentContactCount(int $segmentId): void
{
$count = $this->hasSegmentContactCount($segmentId) ? $this->getSegmentContactCount($segmentId) : 0;
$this->setSegmentContactCount($segmentId, ++$count);
}
/**
* @throws InvalidArgumentException
*/
public function deleteSegmentContactCount(int $segmentId): void
{
if ($this->hasSegmentContactCount($segmentId)) {
$this->cacheProvider->deleteItem($this->generateCacheKey($segmentId));
}
}
/**
* @throws InvalidArgumentException
*/
public function decrementSegmentContactCount(int $segmentId): void
{
if ($this->hasSegmentContactCount($segmentId)) {
$count = $this->getSegmentContactCount($segmentId);
if ($count <= 0) {
$count = 1;
}
$this->setSegmentContactCount($segmentId, --$count);
}
}
private function generateCacheKey(int $segmentId): string
{
return sprintf('%s.%s.%s', 'segment', $segmentId, 'lead');
}
private function generateCacheKeyForRecount(int $segmentId): string
{
return sprintf('%s.%s', $this->generateCacheKey($segmentId), 'recount');
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Mautic\LeadBundle\Helper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\ParamsLoaderHelper;
use Mautic\LeadBundle\Entity\LeadRepository;
class TokenHelper
{
/**
* @const REGEX
*/
public const REGEX = '/({|%7B)contactfield=(.*?)(}|%7D)/';
/**
* @var array
*/
private static $parameters;
/**
* @param string $content
* @param array $lead
* @param bool $replace If true, search/replace will be executed on $content and the modified $content returned
* rather than an array of found matches
*
* @return array|string
*/
public static function findLeadTokens($content, $lead, $replace = false)
{
if (!$lead) {
return $replace ? $content : [];
}
// Search for bracket or bracket encoded
$tokenList = [];
$foundMatches = preg_match_all(self::REGEX, $content, $matches);
$foundDateMatches = preg_match_all('/({|%7B)datetime=(.*?)(}|%7D)/', $content, $dateMatches);
if ($foundMatches || $foundDateMatches) {
foreach ($matches[2] as $key => $match) {
$token = $matches[0][$key];
if (isset($tokenList[$token])) {
continue;
}
$alias = self::getFieldAlias($match);
$defaultValue = self::getTokenDefaultValue($match);
$tokenList[$token] = self::getTokenValue($lead, $alias, $defaultValue);
}
foreach ($dateMatches[2] as $key => $match) {
$token = $dateMatches[0][$key];
if (isset($tokenList[$token])) {
continue;
}
$dt = new DateTimeHelper($match);
$tokenList[$token] = $dt->toLocalString(DateTimeHelper::FORMAT_DB);
}
if ($replace) {
$content = str_replace(array_keys($tokenList), $tokenList, $content);
}
}
return $replace ? $content : $tokenList;
}
/**
* Returns correct token value from provided list of tokens and the concrete token.
*
* @param array $tokens like ['{contactfield=website}' => 'https://mautic.org']
* @param string $token like '{contactfield=website|https://default.url}'
*
* @return string empty string if no match
*/
public static function getValueFromTokens(array $tokens, $token)
{
$token = str_replace(['{', '}'], '', $token);
$alias = self::getFieldAlias($token);
$default = self::getTokenDefaultValue($token);
return empty($tokens["{{$alias}}"]) ? $default : $tokens["{{$alias}}"];
}
/**
* @return mixed
*/
private static function getTokenValue(array $lead, $alias, $defaultValue)
{
$value = '';
if (isset($lead[$alias])) {
$value = $lead[$alias];
} elseif (!empty($lead['companies'])) {
foreach ($lead['companies'] as $company) {
if (isset($company['is_primary'], $company[$alias]) && 1 === (int) $company['is_primary']) {
$value = $company[$alias];
break;
}
}
}
if ('' !== $value) {
switch ($defaultValue) {
case 'label':
$value = self::getNormalizeValue($alias, $value);
break;
case 'true':
$value = urlencode($value);
break;
case 'datetime':
case 'date':
case 'time':
$dt = new DateTimeHelper($value);
$date = $dt->getDateTime()->format(
self::getParameter('date_format_dateonly')
);
$time = $dt->getDateTime()->format(
self::getParameter('date_format_timeonly')
);
switch ($defaultValue) {
case 'datetime':
$value = $date.' '.$time;
break;
case 'date':
$value = $date;
break;
case 'time':
$value = $time;
break;
}
break;
}
}
if (in_array($defaultValue, ['true', 'date', 'time', 'datetime', 'label'])) {
return $value;
} else {
return '' !== $value ? $value : $defaultValue;
}
}
private static function getTokenDefaultValue($match): string
{
$fallbackCheck = explode('|', $match);
if (!isset($fallbackCheck[1])) {
return '';
}
return $fallbackCheck[1];
}
private static function getFieldAlias($match): string
{
$fallbackCheck = explode('|', $match);
return $fallbackCheck[0];
}
/**
* @param string $parameter
*
* @return mixed
*/
private static function getParameter($parameter)
{
if (null === self::$parameters) {
self::$parameters = (new ParamsLoaderHelper())->getParameters();
}
return self::$parameters[$parameter];
}
/**
* @param mixed $value
*
* @return mixed|string
*/
private static function getNormalizeValue(string $alias, $value)
{
$field = array_merge(LeadRepository::getLeadFieldRepository()->getFields()[$alias], ['value' => $value]);
return CustomFieldValueHelper::normalizeValue($field);
}
}