PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.21.2
MailPoet – Newsletters, Email Marketing, and Automation v5.21.2
5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / Listing / NewsletterListingRepository.php
mailpoet / lib / Newsletter / Listing Last commit date
NewsletterListingRepository.php 7 months ago index.php 3 years ago
NewsletterListingRepository.php
359 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Newsletter\Listing;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\NewsletterEntity;
9 use MailPoet\Listing\ListingDefinition;
10 use MailPoet\Listing\ListingRepository;
11 use MailPoet\Util\Helpers;
12 use MailPoetVendor\Doctrine\ORM\QueryBuilder;
13
14 class NewsletterListingRepository extends ListingRepository {
15 private static $supportedStatuses = [
16 NewsletterEntity::STATUS_DRAFT,
17 NewsletterEntity::STATUS_SCHEDULED,
18 NewsletterEntity::STATUS_SENDING,
19 NewsletterEntity::STATUS_SENT,
20 NewsletterEntity::STATUS_ACTIVE,
21 ];
22
23 private static $supportedTypes = [
24 NewsletterEntity::TYPE_STANDARD,
25 NewsletterEntity::TYPE_RE_ENGAGEMENT,
26 NewsletterEntity::TYPE_WELCOME,
27 NewsletterEntity::TYPE_AUTOMATIC,
28 NewsletterEntity::TYPE_NOTIFICATION,
29 NewsletterEntity::TYPE_NOTIFICATION_HISTORY,
30 ];
31
32 public function getFilters(ListingDefinition $definition): array {
33 $group = $definition->getGroup();
34 $typeParam = $definition->getParameters()['type'] ?? null;
35 $groupParam = $definition->getParameters()['group'] ?? null;
36
37 // newsletter types without filters
38 if (in_array($typeParam, [NewsletterEntity::TYPE_NOTIFICATION_HISTORY])) {
39 return [];
40 }
41
42 $queryBuilder = clone $this->queryBuilder;
43 $this->applyFromClause($queryBuilder);
44
45 if ($group) {
46 $this->applyGroup($queryBuilder, $group);
47 }
48
49 if ($typeParam) {
50 $this->applyType($queryBuilder, $typeParam, $groupParam);
51 }
52
53 $queryBuilder
54 ->select('s.id, s.name, COUNT(n) AS newsletterCount')
55 ->join('n.newsletterSegments', 'ns')
56 ->join('ns.segment', 's')
57 ->groupBy('s.id')
58 ->addGroupBy('s.name')
59 ->orderBy('s.name')
60 ->having('COUNT(n) > 0');
61
62 // format segment list
63 $segmentList = [
64 [
65 'label' => __('All Lists', 'mailpoet'),
66 'value' => '',
67 ],
68 ];
69
70 foreach ($queryBuilder->getQuery()->getResult() as $item) {
71 $segmentList[] = [
72 'label' => sprintf('%s (%d)', $item['name'], $item['newsletterCount']),
73 'value' => $item['id'],
74 ];
75 }
76 return ['segment' => $segmentList];
77 }
78
79 public function getGroups(ListingDefinition $definition): array {
80 $queryBuilder = clone $this->queryBuilder;
81 $this->applyFromClause($queryBuilder);
82 $this->applyParameters($queryBuilder, $definition->getParameters());
83
84 // total count
85 $countQueryBuilder = clone $queryBuilder;
86 $countQueryBuilder->select('COUNT(n) AS newsletterCount');
87 $countQueryBuilder->andWhere('n.deletedAt IS NULL');
88 $totalCount = (int)$countQueryBuilder->getQuery()->getSingleScalarResult();
89
90 // trashed count
91 $trashedCountQueryBuilder = clone $queryBuilder;
92 $trashedCountQueryBuilder->select('COUNT(n) AS newsletterCount');
93 $trashedCountQueryBuilder->andWhere('n.deletedAt IS NOT NULL');
94 $trashedCount = (int)$trashedCountQueryBuilder->getQuery()->getSingleScalarResult();
95
96 // count-by-status query
97 $queryBuilder->select('n.status, COUNT(n) AS newsletterCount');
98 $queryBuilder->andWhere('n.deletedAt IS NULL');
99 $queryBuilder->groupBy('n.status');
100
101 $map = [];
102 foreach ($queryBuilder->getQuery()->getResult() as $item) {
103 $map[$item['status']] = (int)$item['newsletterCount'];
104 }
105
106 $groups = [
107 [
108 'name' => 'all',
109 'label' => __('All', 'mailpoet'),
110 'count' => $totalCount,
111 ],
112 ];
113
114 $type = $definition->getParameters()['type'] ?? null;
115 switch ($type) {
116 case NewsletterEntity::TYPE_STANDARD:
117 $groups = array_merge($groups, [
118 [
119 'name' => NewsletterEntity::STATUS_DRAFT,
120 'label' => __('Draft', 'mailpoet'),
121 'count' => $map[NewsletterEntity::STATUS_DRAFT] ?? 0,
122 ],
123 [
124 'name' => NewsletterEntity::STATUS_SCHEDULED,
125 'label' => __('Scheduled', 'mailpoet'),
126 'count' => $map[NewsletterEntity::STATUS_SCHEDULED] ?? 0,
127 ],
128 [
129 'name' => NewsletterEntity::STATUS_SENDING,
130 'label' => __('Sending', 'mailpoet'),
131 'count' => $map[NewsletterEntity::STATUS_SENDING] ?? 0,
132 ],
133 [
134 'name' => NewsletterEntity::STATUS_SENT,
135 'label' => __('Sent', 'mailpoet'),
136 'count' => $map[NewsletterEntity::STATUS_SENT] ?? 0,
137 ],
138 ]);
139 break;
140
141 case NewsletterEntity::TYPE_NOTIFICATION_HISTORY:
142 $groups = array_merge($groups, [
143 [
144 'name' => NewsletterEntity::STATUS_SENDING,
145 'label' => __('Sending', 'mailpoet'),
146 'count' => $map[NewsletterEntity::STATUS_SENDING] ?? 0,
147 ],
148 [
149 'name' => NewsletterEntity::STATUS_SENT,
150 'label' => __('Sent', 'mailpoet'),
151 'count' => $map[NewsletterEntity::STATUS_SENT] ?? 0,
152 ],
153 ]);
154 break;
155
156 case NewsletterEntity::TYPE_WELCOME:
157 case NewsletterEntity::TYPE_RE_ENGAGEMENT:
158 case NewsletterEntity::TYPE_NOTIFICATION:
159 case NewsletterEntity::TYPE_AUTOMATIC:
160 $groups = array_merge($groups, [
161 [
162 'name' => NewsletterEntity::STATUS_ACTIVE,
163 'label' => __('Active', 'mailpoet'),
164 'count' => $map[NewsletterEntity::STATUS_ACTIVE] ?? 0,
165 ],
166 [
167 'name' => NewsletterEntity::STATUS_DRAFT,
168 'label' => __('Not active', 'mailpoet'),
169 'count' => $map[NewsletterEntity::STATUS_DRAFT] ?? 0,
170 ],
171 ]);
172 break;
173 }
174
175 $groups[] = [
176 'name' => 'trash',
177 'label' => __('Trash', 'mailpoet'),
178 'count' => $trashedCount,
179 ];
180
181 return $groups;
182 }
183
184 protected function applySelectClause(QueryBuilder $queryBuilder) {
185 $queryBuilder->select("PARTIAL n.{id,subject,hash,type,status,sentAt,updatedAt,deletedAt}, PARTIAL wpPost.{id,postTitle}");
186 }
187
188 protected function applyFromClause(QueryBuilder $queryBuilder) {
189 $queryBuilder->from(NewsletterEntity::class, 'n')
190 ->leftJoin('n.wpPost', 'wpPost');
191 }
192
193 protected function applyGroup(QueryBuilder $queryBuilder, string $group) {
194 // include/exclude deleted
195 if ($group === 'trash') {
196 $queryBuilder->andWhere('n.deletedAt IS NOT NULL');
197 } else {
198 $queryBuilder->andWhere('n.deletedAt IS NULL');
199 }
200
201 if (!in_array($group, self::$supportedStatuses)) {
202 return;
203 }
204
205 $queryBuilder
206 ->andWhere('n.status = :status')
207 ->setParameter('status', $group);
208 }
209
210 protected function applySearch(QueryBuilder $queryBuilder, string $search, array $parameters = []) {
211 $search = Helpers::escapeSearch($search);
212
213 $type = $parameters['type'] ?? null;
214
215 if ($type && $type === NewsletterEntity::TYPE_NOTIFICATION_HISTORY) {
216 $queryBuilder
217 ->leftJoin('n.queues', 'sq')
218 ->andWhere('sq.newsletterRenderedSubject LIKE :search or n.subject LIKE :search')
219 ->setParameter('search', "%$search%");
220 } else {
221 $queryBuilder
222 ->andWhere('n.subject LIKE :search')
223 ->setParameter('search', "%$search%");
224 }
225 }
226
227 protected function applyFilters(QueryBuilder $queryBuilder, array $filters) {
228 $segmentId = $filters['segment'] ?? null;
229 if ($segmentId && is_numeric($segmentId)) {
230 $queryBuilder
231 ->join('n.newsletterSegments', 'ns')
232 ->andWhere('ns.segment = :segmentId')
233 ->setParameter('segmentId', (int)$segmentId);
234 }
235
236 // Filter by sent_at/scheduled_at date
237 $sentAtFrom = $filters['sent_at_from'] ?? null;
238 if ($sentAtFrom && is_string($sentAtFrom) && $this->isValidDateTime($sentAtFrom)) {
239 $subQueryFrom = $queryBuilder->getEntityManager()->createQueryBuilder()
240 ->select('1')
241 ->from('MailPoet\Entities\SendingQueueEntity', 'queueFrom')
242 ->join('queueFrom.task', 'taskFrom')
243 ->where('queueFrom.newsletter = n.id')
244 ->andWhere('taskFrom.scheduledAt >= :sentAtFrom')
245 ->getDQL();
246
247 $queryBuilder
248 ->andWhere('(n.sentAt >= :sentAtFrom OR EXISTS (' . $subQueryFrom . '))')
249 ->setParameter('sentAtFrom', $sentAtFrom);
250 }
251
252 $sentAtTo = $filters['sent_at_to'] ?? null;
253 if ($sentAtTo && is_string($sentAtTo) && $this->isValidDateTime($sentAtTo)) {
254 $subQueryTo = $queryBuilder->getEntityManager()->createQueryBuilder()
255 ->select('1')
256 ->from('MailPoet\Entities\SendingQueueEntity', 'queueTo')
257 ->join('queueTo.task', 'taskTo')
258 ->where('queueTo.newsletter = n.id')
259 ->andWhere('taskTo.scheduledAt <= :sentAtTo')
260 ->getDQL();
261
262 $queryBuilder
263 ->andWhere('(n.sentAt <= :sentAtTo OR EXISTS (' . $subQueryTo . '))')
264 ->setParameter('sentAtTo', $sentAtTo);
265 }
266
267 // Filter by segment IDs with advanced operators
268 $segmentIds = $filters['segment_ids'] ?? null;
269 if (!$segmentIds || !is_array($segmentIds)) {
270 return;
271 }
272 $segmentIds = array_filter($segmentIds, 'is_numeric');
273 $segmentIds = array_map('intval', $segmentIds);
274 if (empty($segmentIds)) {
275 return;
276 }
277
278 $segmentOperator = $filters['segment_operator'] ?? null;
279 if (!in_array($segmentOperator, ['isAny', 'isNone'], true)) {
280 return;
281 }
282
283 if ($segmentOperator === 'isAny') {
284 $queryBuilder
285 ->join('n.newsletterSegments', 'ns2')
286 ->andWhere('ns2.segment IN (:segmentIds)')
287 ->setParameter('segmentIds', $segmentIds);
288 } elseif ($segmentOperator === 'isNone') {
289 $subQuery = $queryBuilder->getEntityManager()->createQueryBuilder()
290 ->select('1')
291 ->from(NewsletterEntity::class, 'nNone')
292 ->join('nNone.newsletterSegments', 'nsNone')
293 ->where('nNone.id = n.id')
294 ->andWhere('nsNone.segment IN (:segmentIdsNone)')
295 ->getDQL();
296 $queryBuilder
297 ->andWhere('NOT EXISTS (' . $subQuery . ')')
298 ->setParameter('segmentIdsNone', $segmentIds);
299 }
300 }
301
302 private function isValidDateTime(string $dateTime): bool {
303 try {
304 new \DateTime($dateTime);
305 return true;
306 } catch (\Exception $e) {
307 return false;
308 }
309 }
310
311 protected function applyParameters(QueryBuilder $queryBuilder, array $parameters) {
312 $type = $parameters['type'] ?? null;
313 $group = $parameters['group'] ?? null;
314 $parentId = $parameters['parentId'] ?? null;
315
316 if ($type) {
317 $this->applyType($queryBuilder, $type, $group);
318 }
319
320 if ($parentId) {
321 $queryBuilder
322 ->andWhere('n.parent = :parentId')
323 ->setParameter('parentId', $parentId);
324 }
325 }
326
327 protected function applySorting(QueryBuilder $queryBuilder, string $sortBy, string $sortOrder) {
328 if ($sortBy === 'name') {
329 $queryBuilder->addSelect('CONCAT(COALESCE(wpPost.postTitle, \'\'), n.subject) AS HIDDEN sortingName');
330 $queryBuilder->addOrderBy("sortingName", $sortOrder);
331 return;
332 }
333 if ($sortBy === 'sentAt') {
334 $queryBuilder->addSelect('CASE WHEN n.sentAt IS NULL THEN 1 ELSE 0 END AS HIDDEN sentAtIsNull');
335 $queryBuilder->addOrderBy('sentAtIsNull', 'DESC');
336 }
337 $queryBuilder->addOrderBy("n.$sortBy", $sortOrder);
338 }
339
340 private function applyType(QueryBuilder $queryBuilder, string $type, ?string $group = null) {
341 if (!in_array($type, self::$supportedTypes)) {
342 return;
343 }
344
345 if ($type === NewsletterEntity::TYPE_AUTOMATIC && $group) {
346 $queryBuilder
347 ->join('n.options', 'o')
348 ->join('o.optionField', 'opf')
349 ->andWhere('o.value = :group')
350 ->setParameter('group', $group)
351 ->andWhere('opf.newsletterType = n.type');
352 } else {
353 $queryBuilder
354 ->andWhere('n.type = :type')
355 ->setParameter('type', $type);
356 }
357 }
358 }
359