Per WP security best practices, sanitization should be handled as early as possible. So this commit move updates the calls to sanitize the segment name and description to the part of the code where the user input is first processed, instead of when the data is saved to the database. [MAILPOET-5232]
304 lines
10 KiB
PHP
304 lines
10 KiB
PHP
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
|
|
|
namespace MailPoet\Segments;
|
|
|
|
use DateTime;
|
|
use MailPoet\ConflictException;
|
|
use MailPoet\Doctrine\Repository;
|
|
use MailPoet\Entities\DynamicSegmentFilterData;
|
|
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
|
use MailPoet\Entities\NewsletterSegmentEntity;
|
|
use MailPoet\Entities\SegmentEntity;
|
|
use MailPoet\Entities\SubscriberSegmentEntity;
|
|
use MailPoet\Form\FormsRepository;
|
|
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
|
|
use MailPoet\NotFoundException;
|
|
use MailPoet\WP\Functions as WPFunctions;
|
|
use MailPoetVendor\Carbon\Carbon;
|
|
use MailPoetVendor\Doctrine\DBAL\Connection;
|
|
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
|
use MailPoetVendor\Doctrine\ORM\ORMException;
|
|
|
|
/**
|
|
* @extends Repository<SegmentEntity>
|
|
*/
|
|
class SegmentsRepository extends Repository {
|
|
|
|
/** @var NewsletterSegmentRepository */
|
|
private $newsletterSegmentRepository;
|
|
|
|
/** @var FormsRepository */
|
|
private $formsRepository;
|
|
|
|
/** @var WPFunctions */
|
|
private $wp;
|
|
|
|
public function __construct(
|
|
EntityManager $entityManager,
|
|
NewsletterSegmentRepository $newsletterSegmentRepository,
|
|
FormsRepository $formsRepository,
|
|
WPFunctions $wp
|
|
) {
|
|
parent::__construct($entityManager);
|
|
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
|
|
$this->formsRepository = $formsRepository;
|
|
$this->wp = $wp;
|
|
}
|
|
|
|
protected function getEntityClassName() {
|
|
return SegmentEntity::class;
|
|
}
|
|
|
|
public function getWPUsersSegment(): ?SegmentEntity {
|
|
$segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WP_USERS]);
|
|
|
|
if (!$segment) {
|
|
// create the wp users segment
|
|
$segment = new SegmentEntity(
|
|
__('WordPress Users', 'mailpoet'),
|
|
SegmentEntity::TYPE_WP_USERS,
|
|
__('This list contains all of your WordPress users.', 'mailpoet')
|
|
);
|
|
|
|
$this->entityManager->persist($segment);
|
|
$this->entityManager->flush();
|
|
}
|
|
|
|
return $segment;
|
|
}
|
|
|
|
public function getWooCommerceSegment(): SegmentEntity {
|
|
$segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WC_USERS]);
|
|
if (!$segment) {
|
|
// create the WooCommerce customers segment
|
|
$segment = new SegmentEntity(
|
|
__('WooCommerce Customers', 'mailpoet'),
|
|
SegmentEntity::TYPE_WC_USERS,
|
|
__('This list contains all of your WooCommerce customers.', 'mailpoet')
|
|
);
|
|
$this->entityManager->persist($segment);
|
|
$this->entityManager->flush();
|
|
}
|
|
return $segment;
|
|
}
|
|
|
|
public function getCountsPerType(): array {
|
|
$results = $this->doctrineRepository->createQueryBuilder('s')
|
|
->select('s.type, COUNT(s) as cnt')
|
|
->where('s.deletedAt IS NULL')
|
|
->groupBy('s.type')
|
|
->getQuery()
|
|
->getResult();
|
|
|
|
$countMap = [];
|
|
foreach ($results as $result) {
|
|
$countMap[$result['type']] = (int)$result['cnt'];
|
|
}
|
|
return $countMap;
|
|
}
|
|
|
|
public function isNameUnique(string $name, ?int $id): bool {
|
|
$qb = $this->doctrineRepository->createQueryBuilder('s')
|
|
->select('s')
|
|
->where('s.name = :name')
|
|
->setParameter('name', $name);
|
|
|
|
if ($id !== null) {
|
|
$qb->andWhere('s.id != :id')
|
|
->setParameter('id', $id);
|
|
}
|
|
|
|
$results = $qb->getQuery()
|
|
->getResult();
|
|
|
|
return count($results) === 0;
|
|
}
|
|
|
|
/**
|
|
* @throws ConflictException
|
|
*/
|
|
public function verifyNameIsUnique(string $name, ?int $id): void {
|
|
if (!$this->isNameUnique($name, $id)) {
|
|
throw new ConflictException("Could not create new segment with name [{$name}] because a segment with that name already exists.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param DynamicSegmentFilterData[] $filtersData
|
|
* @throws ConflictException
|
|
* @throws NotFoundException
|
|
* @throws ORMException
|
|
*/
|
|
public function createOrUpdate(
|
|
string $name,
|
|
string $description = '',
|
|
string $type = SegmentEntity::TYPE_DEFAULT,
|
|
array $filtersData = [],
|
|
?int $id = null,
|
|
bool $displayInManageSubscriptionPage = true
|
|
): SegmentEntity {
|
|
$displayInManageSubPage = $type === SegmentEntity::TYPE_DEFAULT ? $displayInManageSubscriptionPage : false;
|
|
|
|
if ($id) {
|
|
$segment = $this->findOneById($id);
|
|
if (!$segment instanceof SegmentEntity) {
|
|
throw new NotFoundException("Segment with ID [{$id}] was not found.");
|
|
}
|
|
if ($name !== $segment->getName()) {
|
|
$this->verifyNameIsUnique($name, $id);
|
|
$segment->setName($name);
|
|
}
|
|
$segment->setDescription($description);
|
|
$segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
|
|
} else {
|
|
$this->verifyNameIsUnique($name, $id);
|
|
$segment = new SegmentEntity($name, $type, $description);
|
|
$segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
|
|
$this->persist($segment);
|
|
}
|
|
|
|
// We want to remove redundant filters before update
|
|
while ($segment->getDynamicFilters()->count() > count($filtersData)) {
|
|
$filterEntity = $segment->getDynamicFilters()->last();
|
|
if ($filterEntity) {
|
|
$segment->getDynamicFilters()->removeElement($filterEntity);
|
|
$this->entityManager->remove($filterEntity);
|
|
}
|
|
}
|
|
|
|
$createOrUpdateFilter = function ($filterData, $key) use ($segment) {
|
|
if ($filterData instanceof DynamicSegmentFilterData) {
|
|
$filterEntity = $segment->getDynamicFilters()->get($key);
|
|
if (!$filterEntity instanceof DynamicSegmentFilterEntity) {
|
|
$filterEntity = new DynamicSegmentFilterEntity($segment, $filterData);
|
|
$segment->getDynamicFilters()->add($filterEntity);
|
|
$this->entityManager->persist($filterEntity);
|
|
} else {
|
|
$filterEntity->setFilterData($filterData);
|
|
}
|
|
}
|
|
};
|
|
|
|
$wpActionName = 'mailpoet_dynamic_segments_filters_save';
|
|
if ($this->wp->hasAction($wpActionName)) {
|
|
$this->wp->doAction($wpActionName, $createOrUpdateFilter, $filtersData);
|
|
} else {
|
|
$filterData = reset($filtersData);
|
|
$key = key($filtersData);
|
|
$createOrUpdateFilter($filterData, $key);
|
|
}
|
|
|
|
$this->flush();
|
|
return $segment;
|
|
}
|
|
|
|
public function bulkDelete(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$count = 0;
|
|
$this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, $type, &$count) {
|
|
$subscriberSegmentTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
|
$segmentTable = $entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
|
$segmentFiltersTable = $entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
|
|
|
|
$entityManager->getConnection()->executeStatement("
|
|
DELETE ss FROM $subscriberSegmentTable ss
|
|
JOIN $segmentTable s ON ss.`segment_id` = s.`id`
|
|
WHERE ss.`segment_id` IN (:ids)
|
|
AND s.`type` = :type
|
|
", [
|
|
'ids' => $ids,
|
|
'type' => $type,
|
|
], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
$entityManager->getConnection()->executeStatement("
|
|
DELETE df FROM $segmentFiltersTable df
|
|
WHERE df.`segment_id` IN (:ids)
|
|
", [
|
|
'ids' => $ids,
|
|
], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
$queryBuilder = $entityManager->createQueryBuilder();
|
|
$count = $queryBuilder->delete(SegmentEntity::class, 's')
|
|
->where('s.id IN (:ids)')
|
|
->andWhere('s.type = :type')
|
|
->setParameter('ids', $ids, Connection::PARAM_INT_ARRAY)
|
|
->setParameter('type', $type, \PDO::PARAM_STR)
|
|
->getQuery()->execute();
|
|
|
|
$queryBuilder = $entityManager->createQueryBuilder();
|
|
$queryBuilder->delete(NewsletterSegmentEntity::class, 'ns')
|
|
->where('ns.segment IN (:ids)')
|
|
->setParameter('ids', $ids, Connection::PARAM_INT_ARRAY)
|
|
->getQuery()->execute();
|
|
});
|
|
return $count;
|
|
}
|
|
|
|
public function bulkTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
|
$activelyUsedInNewsletters = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
|
|
$activelyUsedInForms = $this->formsRepository->getNamesOfFormsForSegments();
|
|
$activelyUsed = array_unique(array_merge(array_keys($activelyUsedInNewsletters), array_keys($activelyUsedInForms)));
|
|
$ids = array_diff($ids, $activelyUsed);
|
|
return $this->updateDeletedAt($ids, new Carbon(), $type);
|
|
}
|
|
|
|
public function doTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
|
return $this->updateDeletedAt($ids, new Carbon(), $type);
|
|
}
|
|
|
|
public function bulkRestore(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
|
return $this->updateDeletedAt($ids, null, $type);
|
|
}
|
|
|
|
private function updateDeletedAt(array $ids, ?DateTime $deletedAt, string $type): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$rows = $this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
|
|
->set('s.deletedAt', ':deletedAt')
|
|
->where('s.id IN (:ids)')
|
|
->andWhere('s.type IN (:type)')
|
|
->setParameter('deletedAt', $deletedAt)
|
|
->setParameter('ids', $ids)
|
|
->setParameter('type', $type)
|
|
->getQuery()->execute();
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public function findByUpdatedScoreNotInLastDay(int $limit): array {
|
|
$dateTime = (new Carbon())->subDay();
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s')
|
|
->from(SegmentEntity::class, 's')
|
|
->where('s.averageEngagementScoreUpdatedAt IS NULL')
|
|
->orWhere('s.averageEngagementScoreUpdatedAt < :dateTime')
|
|
->setParameter('dateTime', $dateTime)
|
|
->getQuery()
|
|
->setMaxResults($limit)
|
|
->getResult();
|
|
}
|
|
|
|
/**
|
|
* Returns count of segments that have more than one dynamic filter
|
|
*/
|
|
public function getSegmentCountWithMultipleFilters(): int {
|
|
$segmentFiltersTable = $this->entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
|
|
$qbInner = $this->entityManager->getConnection()->createQueryBuilder()
|
|
->select('COUNT(DISTINCT sf.id) AS segmentCount')
|
|
->from($segmentFiltersTable, 'sf')
|
|
->groupBy('sf.segment_id')
|
|
->having('COUNT(sf.id) > 1');
|
|
$result = $this->entityManager->getConnection()->createQueryBuilder()
|
|
->select('count(*)')
|
|
->from(sprintf('(%s) as subCounts', $qbInner->getSQL()))
|
|
->execute()
|
|
->fetchOne();
|
|
return (int)$result;
|
|
}
|
|
}
|