Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class BotRatioHelper
|
||||
{
|
||||
/**
|
||||
* @param string[] $blockedUserAgents
|
||||
* @param string[] $blockedIPAddresses
|
||||
*/
|
||||
public function __construct(
|
||||
#[Autowire(env: 'float:MAUTIC_BOT_HELPER_BOT_RATIO_THRESHOLD')]
|
||||
private float $botRatioThreshold = 0.6,
|
||||
|
||||
#[Autowire(env: 'int:MAUTIC_BOT_HELPER_TIME_EMAIL_THRESHOLD')]
|
||||
private int $timeFromEmailThreshold = 2,
|
||||
|
||||
#[Autowire(env: 'json:MAUTIC_BOT_HELPER_BLOCKED_USER_AGENTS')]
|
||||
private array $blockedUserAgents = [],
|
||||
|
||||
#[Autowire(env: 'json:MAUTIC_BOT_HELPER_BLOCKED_IP_ADDRESSES')]
|
||||
private array $blockedIPAddresses = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function isHitByBot(Stat $emailStat, \DateTimeInterface $emailHitDateTime, IpAddress $ipAddress, string $userAgent): bool
|
||||
{
|
||||
$totalPoints = (int) $this->isUnderTimeThreshold($emailStat, $emailHitDateTime) +
|
||||
(int) $this->isIpInIgnoreList($ipAddress) +
|
||||
(int) $this->isUserAgentInIgnoreList($userAgent);
|
||||
|
||||
return $totalPoints / 3 >= $this->botRatioThreshold;
|
||||
}
|
||||
|
||||
private function isUnderTimeThreshold(Stat $emailStat, \DateTimeInterface $emailHitDateTime): bool
|
||||
{
|
||||
$timeFromSend = $emailHitDateTime->getTimestamp() - $emailStat->getDateSent()->getTimestamp();
|
||||
|
||||
return $timeFromSend < $this->timeFromEmailThreshold;
|
||||
}
|
||||
|
||||
private function isIpInIgnoreList(IpAddress $ipAddress): bool
|
||||
{
|
||||
// Create a clone so that setting up do not track IP list here will not update original blocked Ip List
|
||||
$ipAddressLocal = clone $ipAddress;
|
||||
$ipAddressLocal->setDoNotTrackList($this->blockedIPAddresses);
|
||||
|
||||
return !$ipAddressLocal->isTrackable();
|
||||
}
|
||||
|
||||
private function isUserAgentInIgnoreList(string $userAgent): bool
|
||||
{
|
||||
foreach ($this->blockedUserAgents as $blockedUserAgent) {
|
||||
if (str_contains($userAgent, $blockedUserAgent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper\DTO;
|
||||
|
||||
use Mautic\EmailBundle\Helper\Exception\TokenNotFoundOrEmptyException;
|
||||
use Symfony\Component\Mime\Address;
|
||||
|
||||
final class AddressDTO
|
||||
{
|
||||
private ?string $name = null;
|
||||
|
||||
public function __construct(private string $email, ?string $name = null)
|
||||
{
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,?string> $address
|
||||
*/
|
||||
public static function fromAddressArray(array $address): self
|
||||
{
|
||||
$email = key($address);
|
||||
|
||||
if (!$email) {
|
||||
throw new \InvalidArgumentException('Address array must have an email as key');
|
||||
}
|
||||
|
||||
return new self($email, $address[$email] ?? null);
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $contact
|
||||
*
|
||||
* @throws TokenNotFoundOrEmptyException
|
||||
*/
|
||||
public function getEmailTokenValue(array $contact): string
|
||||
{
|
||||
if (!preg_match('/{contactfield=(.*?)}/', $this->email, $matches)) {
|
||||
throw new TokenNotFoundOrEmptyException();
|
||||
}
|
||||
|
||||
$emailToken = $matches[1];
|
||||
|
||||
if (empty($contact[$emailToken])) {
|
||||
throw new TokenNotFoundOrEmptyException("$emailToken was not found or empty in the contact array");
|
||||
}
|
||||
|
||||
return $contact[$emailToken];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $contact
|
||||
*
|
||||
* @throws TokenNotFoundOrEmptyException
|
||||
*/
|
||||
public function getNameTokenValue(array $contact): string
|
||||
{
|
||||
if (!preg_match('/{contactfield=(.*?)}/', $this->name, $matches)) {
|
||||
throw new TokenNotFoundOrEmptyException();
|
||||
}
|
||||
|
||||
$nameToken = $matches[1];
|
||||
|
||||
if (empty($contact[$nameToken])) {
|
||||
throw new TokenNotFoundOrEmptyException("$nameToken was not found or empty in the contact array");
|
||||
}
|
||||
|
||||
return $contact[$nameToken];
|
||||
}
|
||||
|
||||
public function isEmailTokenized(): bool
|
||||
{
|
||||
return (bool) preg_match('/{contactfield=(.*?)}/', $this->email);
|
||||
}
|
||||
|
||||
public function isNameTokenized(): bool
|
||||
{
|
||||
return (bool) ($this->name ? preg_match('/{contactfield=(.*?)}/', $this->name) : false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,?string>
|
||||
*/
|
||||
public function getAddressArray(): array
|
||||
{
|
||||
return [$this->email => $this->name];
|
||||
}
|
||||
|
||||
public function toMailerAddress(): Address
|
||||
{
|
||||
return new Address($this->email, $this->name ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode apostrophes and other special characters.
|
||||
*/
|
||||
public function setName(?string $name): void
|
||||
{
|
||||
if (!$name) {
|
||||
$this->name = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->name = trim(html_entity_decode($name, ENT_QUOTES));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
final class EmailConfig implements EmailConfigInterface
|
||||
{
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public function isDraftEnabled(): bool
|
||||
{
|
||||
return (bool) $this->coreParametersHelper->get('email_draft_enabled', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
interface EmailConfigInterface
|
||||
{
|
||||
public function isDraftEnabled(): bool;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Event\EmailValidationEvent;
|
||||
use Mautic\EmailBundle\Exception\InvalidEmailException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class EmailValidator
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an email is the correct format, doesn't have invalid characters, a MX record is associated with the domain, and
|
||||
* leverage integrations to validate.
|
||||
*
|
||||
* @param bool $doDnsCheck
|
||||
*
|
||||
* @throws UnexpectedValueException
|
||||
* @throws InvalidEmailException
|
||||
*/
|
||||
public function validate($address, $doDnsCheck = false): void
|
||||
{
|
||||
if (!is_string($address)) {
|
||||
throw new UnexpectedValueException($address, 'string');
|
||||
}
|
||||
|
||||
if (!$this->isValidFormat($address)) {
|
||||
throw new InvalidEmailException($address, $this->translator->trans('mautic.email.address.invalid_format', ['%email%' => $address ?: '?']));
|
||||
}
|
||||
|
||||
if ($this->hasValidCharacters($address)) {
|
||||
throw new InvalidEmailException($address, $this->translator->trans('mautic.email.address.invalid_characters', ['%email%' => $address]));
|
||||
}
|
||||
|
||||
if ($doDnsCheck && !$this->hasValidDomain($address)) {
|
||||
throw new InvalidEmailException($address, $this->translator->trans('mautic.email.address.invalid_domain', ['%email%' => $address]));
|
||||
}
|
||||
|
||||
$this->doPluginValidation($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that email is in an acceptable format.
|
||||
*
|
||||
* @returns bool
|
||||
*/
|
||||
public function isValidFormat($address): bool
|
||||
{
|
||||
return !empty($address) && filter_var($address, FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that email does not have invalid characters.
|
||||
*
|
||||
* @returns bool
|
||||
*/
|
||||
public function hasValidCharacters($address)
|
||||
{
|
||||
$invalidChar = strpbrk($address, '^&*%');
|
||||
|
||||
return $invalidChar ? substr($invalidChar, 0, 1) : $invalidChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the domain of an email.
|
||||
*
|
||||
* @returns bool
|
||||
*/
|
||||
public function hasValidDomain($address): bool
|
||||
{
|
||||
[$user, $domain] = explode('@', $address);
|
||||
|
||||
return checkdnsrr($domain, 'MX');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate using 3rd party integrations.
|
||||
*
|
||||
* @throws InvalidEmailException
|
||||
*/
|
||||
public function doPluginValidation($address): void
|
||||
{
|
||||
$event = $this->dispatcher->dispatch(
|
||||
new EmailValidationEvent($address),
|
||||
EmailEvents::ON_EMAIL_VALIDATION
|
||||
);
|
||||
|
||||
if (!$event->isValid()) {
|
||||
throw new InvalidEmailException($address, $event->getInvalidReason());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper\Exception;
|
||||
|
||||
class OwnerNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper\Exception;
|
||||
|
||||
class TokenNotFoundOrEmptyException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Helper\DTO\AddressDTO;
|
||||
use Mautic\EmailBundle\Helper\Exception\OwnerNotFoundException;
|
||||
use Mautic\EmailBundle\Helper\Exception\TokenNotFoundOrEmptyException;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
|
||||
class FromEmailHelper
|
||||
{
|
||||
/**
|
||||
* @var array<int,mixed[]>
|
||||
*/
|
||||
private array $owners = [];
|
||||
|
||||
private ?AddressDTO $defaultFrom = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
private ?array $lastOwner = null;
|
||||
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper, private LeadRepository $leadRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function setDefaultFrom(AddressDTO $from): void
|
||||
{
|
||||
$this->defaultFrom = $from;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $contact
|
||||
*/
|
||||
public function getFromAddressConsideringOwner(AddressDTO $address, ?array $contact = null, ?Email $email = null): AddressDTO
|
||||
{
|
||||
// Reset last owner
|
||||
$this->lastOwner = null;
|
||||
|
||||
// Check for token
|
||||
if ($address->isEmailTokenized() || $address->isNameTokenized()) {
|
||||
return $this->getEmailFromToken($address, $contact, true, $email);
|
||||
}
|
||||
|
||||
if (!$contact) {
|
||||
return $address;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->getFromEmailAsOwner($contact, $email);
|
||||
} catch (OwnerNotFoundException) {
|
||||
return $this->getFrom($email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $contact
|
||||
*/
|
||||
public function getFromAddressDto(AddressDTO $address, ?array $contact = null, ?Email $email = null): AddressDTO
|
||||
{
|
||||
// Reset last owner
|
||||
$this->lastOwner = null;
|
||||
|
||||
// Check for token
|
||||
if ($address->isEmailTokenized() || $address->isNameTokenized()) {
|
||||
return $this->getEmailFromToken($address, $contact, false, $email);
|
||||
}
|
||||
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws OwnerNotFoundException
|
||||
*/
|
||||
public function getContactOwner(int $userId, ?Email $email = null): array
|
||||
{
|
||||
// Reset last owner
|
||||
$this->lastOwner = null;
|
||||
|
||||
if ($email) {
|
||||
if (!$email->getUseOwnerAsMailer()) {
|
||||
throw new OwnerNotFoundException("mailer_is_owner is not enabled for this email ({$email->getId()})");
|
||||
}
|
||||
} elseif (!$this->coreParametersHelper->get('mailer_is_owner')) {
|
||||
throw new OwnerNotFoundException('mailer_is_owner is not enabled in global configuration');
|
||||
}
|
||||
|
||||
if (isset($this->owners[$userId])) {
|
||||
return $this->lastOwner = $this->owners[$userId];
|
||||
}
|
||||
|
||||
if ($owner = $this->leadRepository->getLeadOwner($userId)) {
|
||||
$this->owners[$userId] = $this->lastOwner = $owner;
|
||||
|
||||
return $owner;
|
||||
}
|
||||
|
||||
throw new OwnerNotFoundException();
|
||||
}
|
||||
|
||||
public function getSignature(): string
|
||||
{
|
||||
if (!$this->lastOwner) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->replaceSignatureTokens($this->lastOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $owner
|
||||
*/
|
||||
private function replaceSignatureTokens(array $owner): string
|
||||
{
|
||||
$signature = nl2br($owner['signature'] ?? '');
|
||||
$signature = str_replace('|FROM_NAME|', $owner['first_name'].' '.$owner['last_name'], $signature);
|
||||
|
||||
foreach ($owner as $key => $value) {
|
||||
$token = sprintf('|USER_%s|', strtoupper($key));
|
||||
$signature = str_replace($token, (string) $value, (string) $signature);
|
||||
}
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
public function getFrom(?Email $email): AddressDTO
|
||||
{
|
||||
if ($email && $email->getFromAddress()) {
|
||||
return new AddressDTO($email->getFromAddress(), $email->getFromName());
|
||||
}
|
||||
|
||||
return $this->getDefaultFrom();
|
||||
}
|
||||
|
||||
private function getDefaultFrom(): AddressDTO
|
||||
{
|
||||
if ($this->defaultFrom) {
|
||||
return $this->defaultFrom;
|
||||
}
|
||||
|
||||
return $this->getSystemDefaultFrom();
|
||||
}
|
||||
|
||||
private function getSystemDefaultFrom(): AddressDTO
|
||||
{
|
||||
$email = $this->coreParametersHelper->get('mailer_from_email');
|
||||
$name = $this->coreParametersHelper->get('mailer_from_name') ?: null;
|
||||
|
||||
return new AddressDTO($email, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $contact
|
||||
*/
|
||||
private function getEmailFromToken(AddressDTO $address, ?array $contact = null, bool $asOwner = true, ?Email $email = null): AddressDTO
|
||||
{
|
||||
try {
|
||||
if (!$contact) {
|
||||
throw new TokenNotFoundOrEmptyException();
|
||||
}
|
||||
|
||||
$name = $address->isNameTokenized() ? $address->getNameTokenValue($contact) : $address->getName();
|
||||
} catch (TokenNotFoundOrEmptyException) {
|
||||
$name = $this->defaultFrom ? $this->defaultFrom->getName() : $this->getSystemDefaultFrom()->getName();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$contact) {
|
||||
throw new TokenNotFoundOrEmptyException();
|
||||
}
|
||||
|
||||
$emailAddress = $address->isEmailTokenized() ? $address->getEmailTokenValue($contact) : $address->getEmail();
|
||||
|
||||
return new AddressDTO($emailAddress, $name);
|
||||
} catch (TokenNotFoundOrEmptyException) {
|
||||
if ($contact && $asOwner) {
|
||||
try {
|
||||
return $this->getFromEmailAsOwner($contact, $email);
|
||||
} catch (OwnerNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getDefaultFrom();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $contact
|
||||
*
|
||||
* @throws OwnerNotFoundException
|
||||
*/
|
||||
private function getFromEmailAsOwner(array $contact, ?Email $email = null): AddressDTO
|
||||
{
|
||||
if (empty($contact['owner_id'])) {
|
||||
throw new OwnerNotFoundException();
|
||||
}
|
||||
|
||||
$owner = $this->getContactOwner((int) $contact['owner_id'], $email);
|
||||
$ownerEmail = $owner['email'];
|
||||
$ownerName = sprintf('%s %s', $owner['first_name'], $owner['last_name']);
|
||||
|
||||
// Decode apostrophes and other special characters
|
||||
$ownerName = trim(html_entity_decode($ownerName, ENT_QUOTES));
|
||||
|
||||
return new AddressDTO($ownerEmail, $ownerName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
final class MailHashHelper
|
||||
{
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public function getEmailHash(string $email): string
|
||||
{
|
||||
$secret = $this->coreParametersHelper->get('secret_key');
|
||||
|
||||
return self::getEmailHashForSecret($email, $secret);
|
||||
}
|
||||
|
||||
public static function getEmailHashForSecret(string $email, string $secret): string
|
||||
{
|
||||
return hash_hmac('sha256', $email, $secret);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
class PlainTextHelper
|
||||
{
|
||||
public const ENCODING = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Contains the HTML content to convert.
|
||||
*/
|
||||
protected string $html = '';
|
||||
|
||||
/**
|
||||
* Contains the converted, formatted text.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $text;
|
||||
|
||||
/**
|
||||
* Maximum width of the formatted text, in columns.
|
||||
*
|
||||
* Set this value to 0 (or less) to ignore word wrapping
|
||||
* and not constrain text to a fixed-width column.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $width = 70;
|
||||
|
||||
/**
|
||||
* List of preg* regular expression patterns to search for,
|
||||
* used in conjunction with $replace.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $replace
|
||||
*/
|
||||
protected $search = [
|
||||
"/\r/", // Non-legal carriage return
|
||||
"/[\n\t]+/", // Newlines and tabs
|
||||
'/<head[^>]*>.*?<\/head>/i', // <head>
|
||||
'/<script[^>]*>.*?<\/script>/i', // <script>s -- which strip_tags supposedly has problems with
|
||||
'/<style[^>]*>.*?<\/style>/i', // <style>s -- which strip_tags supposedly has problems with
|
||||
'/<p[^>]*>/i', // <P>
|
||||
'/<br[^>]*>/i', // <br>
|
||||
'/<i[^>]*>(.*?)<\/i>/i', // <i>
|
||||
'/<em[^>]*>(.*?)<\/em>/i', // <em>
|
||||
'/(<ul[^>]*>|<\/ul>)/i', // <ul> and </ul>
|
||||
'/(<ol[^>]*>|<\/ol>)/i', // <ol> and </ol>
|
||||
'/(<dl[^>]*>|<\/dl>)/i', // <dl> and </dl>
|
||||
'/<li[^>]*>(.*?)<\/li>/i', // <li> and </li>
|
||||
'/<dd[^>]*>(.*?)<\/dd>/i', // <dd> and </dd>
|
||||
'/<dt[^>]*>(.*?)<\/dt>/i', // <dt> and </dt>
|
||||
'/<li[^>]*>/i', // <li>
|
||||
'/<hr[^>]*>/i', // <hr>
|
||||
'/<div[^>]*>/i', // <div>
|
||||
'/(<table[^>]*>|<\/table>)/i', // <table> and </table>
|
||||
'/(<tr[^>]*>|<\/tr>)/i', // <tr> and </tr>
|
||||
'/<td[^>]*>(.*?)<\/td>/i', // <td> and </td>
|
||||
'/<span class="_html2text_ignore">.+?<\/span>/i', // <span class="_html2text_ignore">...</span>
|
||||
];
|
||||
|
||||
/**
|
||||
* List of pattern replacements corresponding to patterns searched.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $search
|
||||
*/
|
||||
protected $replace = [
|
||||
'', // Non-legal carriage return
|
||||
' ', // Newlines and tabs
|
||||
'', // <head>
|
||||
'', // <script>s -- which strip_tags supposedly has problems with
|
||||
'', // <style>s -- which strip_tags supposedly has problems with
|
||||
"\n\n", // <P>
|
||||
"\n", // <br>
|
||||
'_\\1_', // <i>
|
||||
'_\\1_', // <em>
|
||||
"\n\n", // <ul> and </ul>
|
||||
"\n\n", // <ol> and </ol>
|
||||
"\n\n", // <dl> and </dl>
|
||||
"\t* \\1\n", // <li> and </li>
|
||||
" \\1\n", // <dd> and </dd>
|
||||
"\t* \\1", // <dt> and </dt>
|
||||
"\n\t* ", // <li>
|
||||
"\n-------------------------\n", // <hr>
|
||||
"<div>\n", // <div>
|
||||
"\n\n", // <table> and </table>
|
||||
"\n", // <tr> and </tr>
|
||||
"\t\t\\1\n", // <td> and </td>
|
||||
'', // <span class="_html2text_ignore">...</span>
|
||||
];
|
||||
|
||||
/**
|
||||
* List of preg* regular expression patterns to search for,
|
||||
* used in conjunction with $entReplace.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $entReplace
|
||||
*/
|
||||
protected $entSearch = [
|
||||
'/™/i', // TM symbol in win-1252
|
||||
'/—/i', // m-dash in win-1252
|
||||
'/&(amp|#38);/i', // Ampersand: see converter()
|
||||
'/[ ]{2,}/', // Runs of spaces, post-handling
|
||||
];
|
||||
|
||||
/**
|
||||
* List of pattern replacements corresponding to patterns searched.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $entSearch
|
||||
*/
|
||||
protected $entReplace = [
|
||||
'™', // TM symbol
|
||||
'—', // m-dash
|
||||
'|+|amp|+|', // Ampersand: see converter()
|
||||
' ', // Runs of spaces, post-handling
|
||||
];
|
||||
|
||||
/**
|
||||
* List of preg* regular expression patterns to search for
|
||||
* and replace using callback function.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $callbackSearch = [
|
||||
'/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6
|
||||
'/<(b)( [^>]*)?>(.*?)<\/b>/i', // <b>
|
||||
'/<(strong)( [^>]*)?>(.*?)<\/strong>/i', // <strong>
|
||||
'/<(th)( [^>]*)?>(.*?)<\/th>/i', // <th> and </th>
|
||||
'/<(a) [^>]*href=("|\')([^"\']+)\2([^>]*)>(.*?)<\/a>/is', // <a href="">
|
||||
];
|
||||
|
||||
/**
|
||||
* List of preg* regular expression patterns to search for in PRE body,
|
||||
* used in conjunction with $preReplace.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $preReplace
|
||||
*/
|
||||
protected $preSearch = [
|
||||
"/\n/",
|
||||
"/\t/",
|
||||
'/ /',
|
||||
'/<pre[^>]*>/',
|
||||
'/<\/pre>/',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of pattern replacements corresponding to patterns searched for PRE body.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see $preSearch
|
||||
*/
|
||||
protected $preReplace = [
|
||||
'<br>',
|
||||
' ',
|
||||
' ',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
|
||||
/**
|
||||
* Temporary workspace used during PRE processing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $preContent = '';
|
||||
|
||||
/**
|
||||
* Indicates whether content in the $html variable has been converted yet.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @see $html, $text
|
||||
*/
|
||||
protected $converted = false;
|
||||
|
||||
/**
|
||||
* Contains URL addresses from links to be rendered in plain text.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see buildlinkList()
|
||||
*/
|
||||
protected $linkList = [];
|
||||
|
||||
/**
|
||||
* Various configuration options (able to be set in the constructor).
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $options = [
|
||||
'do_links' => 'inline', // 'none'
|
||||
// 'inline' (show links inline)
|
||||
// 'nextline' (show links on the next line)
|
||||
// 'table' (if a table of link URLs should be listed after the text.
|
||||
|
||||
'width' => 70, // Maximum width of the formatted text, in columns.
|
||||
// Set this value to 0 (or less) to ignore word wrapping
|
||||
// and not constrain text to a fixed-width column.
|
||||
|
||||
'base_url' => '',
|
||||
|
||||
'preview_length' => 119, // Maximum length of the preview text
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options Set configuration options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source HTML.
|
||||
*
|
||||
* @param string|null $html HTML source content
|
||||
*
|
||||
* @return PlainTextHelper
|
||||
*/
|
||||
public function setHtml($html)
|
||||
{
|
||||
$this->html = $html ?? '';
|
||||
$this->converted = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text, converted from HTML.
|
||||
*/
|
||||
public function getText(): string
|
||||
{
|
||||
if (!$this->converted) {
|
||||
$this->convert();
|
||||
}
|
||||
|
||||
return trim($this->text);
|
||||
}
|
||||
|
||||
public function getPreview(): string
|
||||
{
|
||||
$textContent = $this->getText();
|
||||
$preview = trim(substr($textContent, 0, $this->options['preview_length']));
|
||||
|
||||
// If the text is longer than the preview length, append an ellipsis
|
||||
if (strlen($textContent) > $this->options['preview_length']) {
|
||||
$preview .= '...';
|
||||
}
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
protected function convert()
|
||||
{
|
||||
$this->linkList = [];
|
||||
|
||||
$text = trim(stripslashes($this->html));
|
||||
|
||||
$this->converter($text);
|
||||
|
||||
if ($this->linkList) {
|
||||
$text .= "\n\nLinks:\n------\n";
|
||||
foreach ($this->linkList as $i => $url) {
|
||||
$text .= '['.($i + 1).'] '.$url."\n";
|
||||
}
|
||||
}
|
||||
|
||||
$this->text = $text;
|
||||
|
||||
$this->converted = true;
|
||||
}
|
||||
|
||||
protected function converter(&$text)
|
||||
{
|
||||
$this->convertBlockquotes($text);
|
||||
$this->convertPre($text);
|
||||
$text = preg_replace($this->search, $this->replace, $text);
|
||||
$text = preg_replace_callback($this->callbackSearch, [$this, 'pregCallback'], $text);
|
||||
$text = strip_tags($text);
|
||||
$text = preg_replace($this->entSearch, $this->entReplace, $text);
|
||||
$text = html_entity_decode($text, ENT_QUOTES, self::ENCODING);
|
||||
|
||||
// Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
|
||||
$text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
|
||||
|
||||
// Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
|
||||
// This properly handles situation of "&quot;" in input string
|
||||
$text = str_replace('|+|amp|+|', '&', $text);
|
||||
|
||||
// Normalise empty lines
|
||||
$text = preg_replace("/\n\s+\n/", "\n\n", $text);
|
||||
$text = preg_replace("/[\n]{3,}/", "\n\n", $text);
|
||||
|
||||
// remove leading empty lines (can be produced by eg. P tag on the beginning)
|
||||
$text = ltrim($text, "\n");
|
||||
|
||||
if ($this->options['width'] > 0) {
|
||||
$text = $this->linewrap($text, $this->options['width']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function called by preg_replace() on link replacement.
|
||||
*
|
||||
* Maintains an internal list of links to be displayed at the end of the
|
||||
* text, with numeric indices to the original point in the text they
|
||||
* appeared. Also makes an effort at identifying and handling absolute
|
||||
* and relative links.
|
||||
*
|
||||
* @param string $link URL of the link
|
||||
* @param string $display Part of the text to associate number with
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function buildlinkList($link, $display, ?string $linkOverride = null)
|
||||
{
|
||||
$linkMethod = $linkOverride ?: $this->options['do_links'];
|
||||
if ('none' == $linkMethod) {
|
||||
return $display;
|
||||
}
|
||||
|
||||
// Ignored link types
|
||||
if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
|
||||
return $display;
|
||||
}
|
||||
|
||||
if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link) || preg_match('!({|%7B)(.*?)(}|%7D)!', $link)) {
|
||||
$url = $link;
|
||||
} else {
|
||||
$url = $this->options['base_url'];
|
||||
if (!str_starts_with($link, '/')) {
|
||||
$url .= '/';
|
||||
}
|
||||
$url .= $link;
|
||||
}
|
||||
|
||||
if ('table' == $linkMethod) {
|
||||
if (false === ($index = array_search($url, $this->linkList))) {
|
||||
$index = count($this->linkList);
|
||||
$this->linkList[] = $url;
|
||||
}
|
||||
|
||||
return $display.' ['.($index + 1).']';
|
||||
} elseif ('nextline' == $linkMethod) {
|
||||
return $display."\n[".$url.']';
|
||||
} else { // link_method defaults to inline
|
||||
return $display.' ['.$url.']';
|
||||
}
|
||||
}
|
||||
|
||||
protected function convertPre(&$text)
|
||||
{
|
||||
// get the content of PRE element
|
||||
while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
|
||||
$this->preContent = $matches[1];
|
||||
|
||||
// Run our defined tags search-and-replace with callback
|
||||
$this->preContent = preg_replace_callback(
|
||||
$this->callbackSearch,
|
||||
[$this, 'pregCallback'],
|
||||
$this->preContent
|
||||
);
|
||||
|
||||
// convert the content
|
||||
$this->preContent = sprintf(
|
||||
'<div><br>%s<br></div>',
|
||||
preg_replace($this->preSearch, $this->preReplace, $this->preContent)
|
||||
);
|
||||
|
||||
// replace the content (use callback because content can contain $0 variable)
|
||||
$text = preg_replace_callback(
|
||||
'/<pre[^>]*>.*<\/pre>/ismU',
|
||||
[$this, 'pregPreCallback'],
|
||||
$text,
|
||||
1
|
||||
);
|
||||
|
||||
// free memory
|
||||
$this->preContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for BLOCKQUOTE body conversion.
|
||||
*
|
||||
* @param string $text HTML content
|
||||
*/
|
||||
protected function convertBlockquotes(&$text)
|
||||
{
|
||||
if (preg_match_all('/<\/*blockquote[^>]*>/i', $text, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$start = 0;
|
||||
$taglen = 0;
|
||||
$level = 0;
|
||||
$diff = 0;
|
||||
foreach ($matches[0] as $m) {
|
||||
if ('<' == $m[0][0] && '/' == $m[0][1]) {
|
||||
--$level;
|
||||
if ($level < 0) {
|
||||
$level = 0; // malformed HTML: go to next blockquote
|
||||
} elseif ($level > 0) {
|
||||
// skip inner blockquote
|
||||
} else {
|
||||
$end = $m[1];
|
||||
$len = $end - $taglen - $start;
|
||||
// Get blockquote content
|
||||
$body = substr($text, $start + $taglen - $diff, $len);
|
||||
|
||||
// Set text width
|
||||
$pWidth = $this->options['width'];
|
||||
if ($this->options['width'] > 0) {
|
||||
$this->options['width'] -= 2;
|
||||
}
|
||||
// Convert blockquote content
|
||||
$body = trim($body);
|
||||
$this->converter($body);
|
||||
// Add citation markers and create PRE block
|
||||
$body = preg_replace('/((^|\n)>*)/', '\\1> ', trim($body));
|
||||
$body = '<pre>'.htmlspecialchars($body).'</pre>';
|
||||
// Re-set text width
|
||||
$this->options['width'] = $pWidth;
|
||||
// Replace content
|
||||
$text = substr($text, 0, $start - $diff)
|
||||
.$body.substr($text, $end + strlen($m[0]) - $diff);
|
||||
|
||||
$diff = $len + $taglen + strlen($m[0]) - strlen($body);
|
||||
unset($body);
|
||||
}
|
||||
} else {
|
||||
if (0 == $level) {
|
||||
$start = $m[1];
|
||||
$taglen = strlen($m[0]);
|
||||
}
|
||||
++$level;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for preg_replace_callback use.
|
||||
*
|
||||
* @param array $matches PREG matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function pregCallback($matches)
|
||||
{
|
||||
switch (strtolower($matches[1])) {
|
||||
case 'b':
|
||||
case 'strong':
|
||||
return $matches[3];
|
||||
case 'th':
|
||||
return $this->toupper("\t\t".$matches[3]."\n");
|
||||
case 'h':
|
||||
return $this->toupper("\n\n".$matches[3]."\n\n");
|
||||
case 'a':
|
||||
// override the link method
|
||||
$linkOverride = null;
|
||||
if (preg_match('/_html2text_link_(\w+)/', $matches[4], $linkOverrideMatch)) {
|
||||
$linkOverride = $linkOverrideMatch[1];
|
||||
}
|
||||
// Remove spaces in URL (#1487805)
|
||||
$url = str_replace(' ', '', $matches[3]);
|
||||
|
||||
return $this->buildlinkList($url, $matches[5], $linkOverride);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for preg_replace_callback use in PRE content handler.
|
||||
*
|
||||
* @param array $matches PREG matches
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function pregPreCallback(/* @noinspection PhpUnusedParameterInspection */ $matches)
|
||||
{
|
||||
return $this->preContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strtoupper function with HTML tags and entities handling.
|
||||
*
|
||||
* @param string $str Text to convert
|
||||
*
|
||||
* @return string Converted text
|
||||
*/
|
||||
private function toupper($str): string
|
||||
{
|
||||
// string can contain HTML tags
|
||||
$chunks = preg_split('/(<[^>]*>)/', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
// convert toupper only the text between HTML tags
|
||||
foreach ($chunks as $i => $chunk) {
|
||||
if ('<' != $chunk[0]) {
|
||||
$chunks[$i] = $this->strtoupper($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return implode('', $chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strtoupper multibyte wrapper function with HTML entities handling.
|
||||
*
|
||||
* @param string $str Text to convert
|
||||
*
|
||||
* @return string Converted text
|
||||
*/
|
||||
private function strtoupper($str): string
|
||||
{
|
||||
$str = html_entity_decode($str, ENT_COMPAT, self::ENCODING);
|
||||
|
||||
if (function_exists('mb_strtoupper')) {
|
||||
$str = mb_strtoupper($str, self::ENCODING);
|
||||
} else {
|
||||
$str = strtoupper($str);
|
||||
}
|
||||
|
||||
return htmlspecialchars($str, ENT_COMPAT, self::ENCODING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $breakline
|
||||
* @param bool|false $cut
|
||||
*/
|
||||
private function linewrap($text, $width, $breakline = "\n", $cut = false): string
|
||||
{
|
||||
$lines = explode("\n", $text);
|
||||
$text = '';
|
||||
foreach ($lines as $line) {
|
||||
$text .= trim(wordwrap(trim($line), $width, $breakline, $cut));
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class PointEventHelper
|
||||
{
|
||||
public function __construct(private EmailModel $emailModel)
|
||||
{
|
||||
}
|
||||
|
||||
public static function validateEmail($eventDetails, $action): bool
|
||||
{
|
||||
if (null === $eventDetails) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$emailId = $eventDetails->getId();
|
||||
|
||||
if (isset($action['properties']['emails'])) {
|
||||
$limitToEmails = $action['properties']['emails'];
|
||||
}
|
||||
|
||||
if (!empty($limitToEmails) && !in_array($emailId, $limitToEmails)) {
|
||||
// no points change
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendEmail($event, Lead $lead): bool
|
||||
{
|
||||
$properties = $event['properties'];
|
||||
$emailId = (int) $properties['email'];
|
||||
|
||||
$email = $this->emailModel->getEntity($emailId);
|
||||
|
||||
// make sure the email still exists and is published
|
||||
if (null != $email && $email->isPublished()) {
|
||||
$leadFields = $lead->getFields();
|
||||
if (isset($leadFields['core']['email']['value']) && $leadFields['core']['email']['value']) {
|
||||
$leadCredentials = $lead->getProfileFields();
|
||||
$leadCredentials['id'] = $lead->getId();
|
||||
|
||||
$options = ['source' => ['trigger', $event['id']]];
|
||||
$emailSent = $this->emailModel->sendEmail($email, $leadCredentials, $options);
|
||||
|
||||
return is_array($emailSent) ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
use Mautic\EmailBundle\Stats\FetchOptions\EmailStatOptions;
|
||||
use Mautic\EmailBundle\Stats\Helper\BouncedHelper;
|
||||
use Mautic\EmailBundle\Stats\Helper\ClickedHelper;
|
||||
use Mautic\EmailBundle\Stats\Helper\FailedHelper;
|
||||
use Mautic\EmailBundle\Stats\Helper\OpenedHelper;
|
||||
use Mautic\EmailBundle\Stats\Helper\SentHelper;
|
||||
use Mautic\EmailBundle\Stats\Helper\UnsubscribedHelper;
|
||||
use Mautic\EmailBundle\Stats\StatHelperContainer;
|
||||
use Mautic\StatsBundle\Aggregate\Collection\StatCollection;
|
||||
|
||||
class StatsCollectionHelper
|
||||
{
|
||||
public const GENERAL_STAT_PREFIX = 'email';
|
||||
|
||||
public function __construct(
|
||||
private StatHelperContainer $helperContainer,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchSentStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(SentHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchOpenedStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(OpenedHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchFailedStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(FailedHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchClickedStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(ClickedHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchBouncedStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(BouncedHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stats from listeners.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function fetchUnsubscribedStats(\DateTime $fromDateTime, \DateTime $toDateTime, EmailStatOptions $options)
|
||||
{
|
||||
return $this->helperContainer->getHelper(UnsubscribedHelper::NAME)->fetchStats($fromDateTime, $toDateTime, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate stats from Mautic's raw data.
|
||||
*
|
||||
* @throws \Mautic\EmailBundle\Stats\Exception\InvalidStatHelperException
|
||||
*/
|
||||
public function generateStats(
|
||||
$statName,
|
||||
\DateTime $fromDateTime,
|
||||
\DateTime $toDateTime,
|
||||
EmailStatOptions $options,
|
||||
StatCollection $statCollection,
|
||||
): void {
|
||||
$this->helperContainer->getHelper($statName)->generateStats($fromDateTime, $toDateTime, $options, $statCollection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Helper;
|
||||
|
||||
class UrlMatcher
|
||||
{
|
||||
public static function hasMatch(array $urlsToCheckAgainst, $urlToFind): bool
|
||||
{
|
||||
$urlToFind = self::sanitizeUrl($urlToFind);
|
||||
|
||||
foreach ($urlsToCheckAgainst as $url) {
|
||||
$url = self::sanitizeUrl($url);
|
||||
|
||||
if (preg_match('/'.preg_quote($url, '/').'/i', $urlToFind)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|string
|
||||
*/
|
||||
private static function sanitizeUrl($url)
|
||||
{
|
||||
// Handle escaped forward slashes as BC
|
||||
$url = str_replace('\\/', '/', $url);
|
||||
|
||||
// Ignore ending slash
|
||||
$url = rtrim($url, '/');
|
||||
|
||||
// Ignore http/https
|
||||
$url = str_replace(['http://', 'https://'], '', $url);
|
||||
|
||||
// Remove preceding //
|
||||
if (str_starts_with($url, '//')) {
|
||||
$url = str_replace('//', '', $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user