Merge pull request #17588 from artfulrobot/artfulrobot-property-bag-support-empty
[civicrm-core.git] / CRM / Mailing / Selector / Browse.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * This class is used to browse past mailings.
20 */
21 class CRM_Mailing_Selector_Browse extends CRM_Core_Selector_Base implements CRM_Core_Selector_API {
22
23 /**
24 * Array of supported links, currently null
25 *
26 * @var array
27 */
28 public static $_links = NULL;
29
30 /**
31 * We use desc to remind us what that column is, name is used in the tpl
32 *
33 * @var array
34 */
35 public static $_columnHeaders;
36
37 protected $_parent;
38
39 /**
40 * Class constructor.
41 *
42 *
43 * @return \CRM_Mailing_Selector_Browse
44 */
45 public function __construct() {
46 }
47
48 /**
49 * This method returns the links that are given for each search row.
50 *
51 * @return array
52 */
53 public static function &links() {
54 return self::$_links;
55 }
56
57 /**
58 * Getter for array of the parameters required for creating pager.
59 *
60 * @param $action
61 * @param array $params
62 */
63 public function getPagerParams($action, &$params) {
64 $params['csvString'] = NULL;
65 $params['rowCount'] = CRM_Utils_Pager::ROWCOUNT;
66 $params['status'] = ts('Mailings %%StatusMessage%%');
67 $params['buttonTop'] = 'PagerTopButton';
68 $params['buttonBottom'] = 'PagerBottomButton';
69 }
70
71 /**
72 * Returns the column headers as an array of tuples:
73 * (name, sortName (key to the sort array))
74 *
75 * @param string $action
76 * The action being performed.
77 * @param string $output
78 * What should the result set include (web/email/csv).
79 *
80 * @return array
81 * the column headers that need to be displayed
82 */
83 public function &getColumnHeaders($action = NULL, $output = NULL) {
84 $mailing = CRM_Mailing_BAO_Mailing::getTableName();
85 $job = CRM_Mailing_BAO_MailingJob::getTableName();
86 if (!isset(self::$_columnHeaders)) {
87 $completedOrder = NULL;
88
89 // Set different default sort depending on type of mailings (CRM-7652)
90 $unscheduledOrder = $scheduledOrder = $archivedOrder = CRM_Utils_Sort::DONTCARE;
91 if ($this->_parent->get('unscheduled')) {
92 $unscheduledOrder = CRM_Utils_Sort::DESCENDING;
93 }
94 elseif ($this->_parent->get('scheduled')) {
95 $scheduledOrder = CRM_Utils_Sort::DESCENDING;
96 }
97 else {
98 // sort by completed date for archived and undefined get
99 $completedOrder = CRM_Utils_Sort::DESCENDING;
100 }
101 $nameHeaderLabel = ($this->_parent->get('sms')) ? ts('SMS Name') : ts('Mailing Name');
102
103 self::$_columnHeaders = [
104 [
105 'name' => $nameHeaderLabel,
106 'sort' => 'name',
107 'direction' => CRM_Utils_Sort::DONTCARE,
108 ],
109 ];
110
111 if (CRM_Core_I18n::isMultilingual()) {
112 self::$_columnHeaders = array_merge(
113 self::$_columnHeaders,
114 [
115 [
116 'name' => ts('Language'),
117 'sort' => 'language',
118 'direction' => CRM_Utils_Sort::DONTCARE,
119 ],
120 ]
121 );
122 }
123
124 self::$_columnHeaders = array_merge(
125 self::$_columnHeaders,
126 [
127 [
128 'name' => ts('Status'),
129 'sort' => 'status',
130 'direction' => CRM_Utils_Sort::DONTCARE,
131 ],
132 [
133 'name' => ts('Created By'),
134 'sort' => 'created_by',
135 'direction' => CRM_Utils_Sort::DONTCARE,
136 ],
137 [
138 'name' => ts('Created Date'),
139 'sort' => 'created_date',
140 'direction' => $unscheduledOrder,
141 ],
142 [
143 'name' => ts('Sent By'),
144 'sort' => 'scheduled_by',
145 'direction' => CRM_Utils_Sort::DONTCARE,
146 ],
147 [
148 'name' => ts('Scheduled'),
149 'sort' => 'scheduled_date',
150 'direction' => $scheduledOrder,
151 ],
152 [
153 'name' => ts('Started'),
154 'sort' => 'start_date',
155 'direction' => CRM_Utils_Sort::DONTCARE,
156 ],
157 [
158 'name' => ts('Completed'),
159 'sort' => 'end_date',
160 'direction' => $completedOrder,
161 ],
162 ]
163 );
164
165 if (CRM_Campaign_BAO_Campaign::isCampaignEnable()) {
166 self::$_columnHeaders[] = [
167 'name' => ts('Campaign'),
168 'sort' => 'campaign_id',
169 'direction' => CRM_Utils_Sort::DONTCARE,
170 ];
171 }
172
173 if ($output != CRM_Core_Selector_Controller::EXPORT) {
174 self::$_columnHeaders[] = ['name' => ts('Action')];
175 }
176 }
177
178 CRM_Core_Smarty::singleton()->assign('multilingual', CRM_Core_I18n::isMultilingual());
179 return self::$_columnHeaders;
180 }
181
182 /**
183 * Returns total number of rows for the query.
184 *
185 * @param string $action
186 *
187 * @return int
188 * Total number of rows
189 */
190 public function getTotalCount($action) {
191 $job = CRM_Mailing_BAO_MailingJob::getTableName();
192 $mailing = CRM_Mailing_BAO_Mailing::getTableName();
193 $mailingACL = CRM_Mailing_BAO_Mailing::mailingACL();
194
195 // get the where clause.
196 $params = [];
197 $whereClause = "$mailingACL AND " . $this->whereClause($params);
198
199 // CRM-11919 added addition ON clauses to mailing_job to match getRows
200 $query = "
201 SELECT COUNT( DISTINCT $mailing.id ) as count
202 FROM $mailing
203 LEFT JOIN $job ON ( $mailing.id = $job.mailing_id AND civicrm_mailing_job.is_test = 0 AND civicrm_mailing_job.parent_id IS NULL )
204 LEFT JOIN civicrm_contact createdContact ON ( $mailing.created_id = createdContact.id )
205 LEFT JOIN civicrm_contact scheduledContact ON ( $mailing.scheduled_id = scheduledContact.id )
206 WHERE $whereClause";
207
208 return CRM_Core_DAO::singleValueQuery($query, $params);
209 }
210
211 /**
212 * Returns all the rows in the given offset and rowCount.
213 *
214 * @param string $action
215 * The action being performed.
216 * @param int $offset
217 * The row number to start from.
218 * @param int $rowCount
219 * The number of rows to return.
220 * @param string $sort
221 * The sql string that describes the sort order.
222 * @param string $output
223 * What should the result set include (web/email/csv).
224 *
225 * @return int
226 * the total number of rows for this action
227 */
228 public function &getRows($action, $offset, $rowCount, $sort, $output = NULL) {
229 static $actionLinks = NULL;
230 if (empty($actionLinks)) {
231 $cancelExtra = ts('Are you sure you want to cancel this mailing?');
232 $deleteExtra = ts('Are you sure you want to delete this mailing?');
233 $archiveExtra = ts('Are you sure you want to archive this mailing?');
234
235 $actionLinks = [
236 CRM_Core_Action::ENABLE => [
237 'name' => ts('Approve/Reject'),
238 'url' => 'civicrm/mailing/approve',
239 'qs' => 'mid=%%mid%%&reset=1',
240 'title' => ts('Approve/Reject Mailing'),
241 ],
242 CRM_Core_Action::VIEW => [
243 'name' => ts('Report'),
244 'url' => 'civicrm/mailing/report',
245 'qs' => 'mid=%%mid%%&reset=1',
246 'title' => ts('View Mailing Report'),
247 ],
248 CRM_Core_Action::UPDATE => [
249 'name' => ts('Re-Use'),
250 'url' => 'civicrm/mailing/send',
251 'qs' => 'mid=%%mid%%&reset=1',
252 'title' => ts('Re-Send Mailing'),
253 ],
254 CRM_Core_Action::DISABLE => [
255 'name' => ts('Cancel'),
256 'url' => 'civicrm/mailing/browse',
257 'qs' => 'action=disable&mid=%%mid%%&reset=1',
258 'extra' => 'onclick="if (confirm(\'' . $cancelExtra . '\')) this.href+=\'&amp;confirmed=1\'; else return false;"',
259 'title' => ts('Cancel Mailing'),
260 ],
261 CRM_Core_Action::PREVIEW => [
262 'name' => ts('Continue'),
263 'url' => 'civicrm/mailing/send',
264 'qs' => 'mid=%%mid%%&continue=true&reset=1',
265 'title' => ts('Continue Mailing'),
266 ],
267 CRM_Core_Action::DELETE => [
268 'name' => ts('Delete'),
269 'url' => 'civicrm/mailing/browse',
270 'qs' => 'action=delete&mid=%%mid%%&reset=1',
271 'extra' => 'onclick="if (confirm(\'' . $deleteExtra . '\')) this.href+=\'&amp;confirmed=1\'; else return false;"',
272 'title' => ts('Delete Mailing'),
273 ],
274 CRM_Core_Action::RENEW => [
275 'name' => ts('Archive'),
276 'url' => 'civicrm/mailing/browse/archived',
277 'qs' => 'action=renew&mid=%%mid%%&reset=1',
278 'extra' => 'onclick="if (confirm(\'' . $archiveExtra . '\')) this.href+=\'&amp;confirmed=1\'; else return false;"',
279 'title' => ts('Archive Mailing'),
280 ],
281 CRM_Core_Action::REOPEN => [
282 'name' => ts('Resume'),
283 'url' => 'civicrm/mailing/browse',
284 'qs' => 'action=reopen&mid=%%mid%%&reset=1',
285 'title' => ts('Resume mailing'),
286 ],
287 CRM_Core_Action::CLOSE => [
288 'name' => ts('Pause'),
289 'url' => 'civicrm/mailing/browse',
290 'qs' => 'action=close&mid=%%mid%%&reset=1',
291 'title' => ts('Pause mailing'),
292 ],
293 ];
294 }
295
296 $allAccess = TRUE;
297 $workFlow = $showApprovalLinks = $showScheduleLinks = $showCreateLinks = FALSE;
298 if (CRM_Mailing_Info::workflowEnabled()) {
299 $allAccess = FALSE;
300 $workFlow = TRUE;
301 // supercedes all permission
302 if (CRM_Core_Permission::check('access CiviMail')) {
303 $allAccess = TRUE;
304 }
305
306 if (CRM_Core_Permission::check('approve mailings')) {
307 $showApprovalLinks = TRUE;
308 }
309
310 if (CRM_Core_Permission::check('create mailings')) {
311 $showCreateLinks = TRUE;
312 }
313
314 if (CRM_Core_Permission::check('schedule mailings')) {
315 $showScheduleLinks = TRUE;
316 }
317 }
318 $mailing = new CRM_Mailing_BAO_Mailing();
319
320 $params = [];
321
322 $whereClause = ' AND ' . $this->whereClause($params);
323
324 if (empty($params)) {
325 $this->_parent->assign('isSearch', 0);
326 }
327 else {
328 $this->_parent->assign('isSearch', 1);
329 }
330 $rows = &$mailing->getRows($offset, $rowCount, $sort, $whereClause, $params);
331
332 // get the search base mailing Ids, CRM-3711.
333 $searchMailings = $mailing->searchMailingIDs();
334
335 // check for delete CRM-4418
336 $allowToDelete = CRM_Core_Permission::check('delete in CiviMail');
337
338 if ($output != CRM_Core_Selector_Controller::EXPORT) {
339
340 // create the appropriate $op to use for hook_civicrm_links
341 $pageTypes = ['view', 'mailing', 'browse'];
342 if ($this->_parent->_unscheduled) {
343 $pageTypes[] = 'unscheduled';
344 }
345 if ($this->_parent->_scheduled) {
346 $pageTypes[] = 'scheduled';
347 }
348 if ($this->_parent->_archived) {
349 $pageTypes[] = 'archived';
350 }
351 $opString = implode('.', $pageTypes);
352
353 // get languages for later conversion
354 $languages = CRM_Core_I18n::languages();
355
356 foreach ($rows as $key => $row) {
357 $actionMask = NULL;
358 if ($row['sms_provider_id']) {
359 $actionLinks[CRM_Core_Action::PREVIEW]['url'] = 'civicrm/sms/send';
360 }
361
362 if (!($row['status'] == 'Not scheduled') && !$row['sms_provider_id']) {
363 if ($allAccess || $showCreateLinks) {
364 $actionMask = CRM_Core_Action::VIEW;
365 }
366
367 if (!in_array($row['id'], $searchMailings)) {
368 if ($allAccess || $showCreateLinks) {
369 $actionMask |= CRM_Core_Action::UPDATE;
370 }
371 }
372 }
373 else {
374 if ($allAccess || ($showCreateLinks || $showScheduleLinks)) {
375 $actionMask = CRM_Core_Action::PREVIEW;
376 }
377 }
378 if (in_array($row['status'], [
379 'Scheduled',
380 'Running',
381 'Paused',
382 ])) {
383 if ($allAccess ||
384 ($showApprovalLinks && $showCreateLinks && $showScheduleLinks)
385 ) {
386
387 $actionMask |= CRM_Core_Action::DISABLE;
388 if ($row['status'] == "Paused") {
389 $actionMask |= CRM_Core_Action::REOPEN;
390 }
391 else {
392 $actionMask |= CRM_Core_Action::CLOSE;
393 }
394 }
395 if ($row['status'] == 'Scheduled' &&
396 empty($row['approval_status_id'])
397 ) {
398 if ($workFlow && ($allAccess || $showApprovalLinks)) {
399 $actionMask |= CRM_Core_Action::ENABLE;
400 }
401 }
402 }
403
404 if (in_array($row['status'], ['Complete', 'Canceled']) &&
405 !$row['archived']
406 ) {
407 if ($allAccess || $showCreateLinks) {
408 $actionMask |= CRM_Core_Action::RENEW;
409 }
410 }
411
412 // check for delete permission.
413 if ($allowToDelete) {
414 $actionMask |= CRM_Core_Action::DELETE;
415 }
416
417 if ($actionMask == NULL) {
418 $actionMask = CRM_Core_Action::ADD;
419 }
420 // get status strings as per locale settings CRM-4411.
421 $rows[$key]['status'] = CRM_Mailing_BAO_MailingJob::status($row['status']);
422
423 // get language string
424 $rows[$key]['language'] = (isset($row['language']) ? $languages[$row['language']] : NULL);
425
426 $validLinks = $actionLinks;
427 if (($mailingUrl = CRM_Mailing_BAO_Mailing::getPublicViewUrl($row['id'])) != FALSE) {
428 $validLinks[CRM_Core_Action::BROWSE] = [
429 'name' => ts('Public View'),
430 'url' => 'civicrm/mailing/view',
431 'qs' => 'id=%%hashOrMid%%&reset=1',
432 'title' => ts('Public View'),
433 'fe' => TRUE,
434 ];
435 $actionMask |= CRM_Core_Action::BROWSE;
436 }
437
438 $hash = CRM_Mailing_BAO_Mailing::getMailingHash($row['id']);
439 $rows[$key]['action'] = CRM_Core_Action::formLink(
440 $validLinks,
441 $actionMask,
442 [
443 'mid' => $row['id'],
444 'hashOrMid' => $hash ? $hash : $row['id'],
445 ],
446 "more",
447 FALSE,
448 $opString,
449 "Mailing",
450 $row['id']
451 );
452
453 // unset($rows[$key]['id']);
454 // if the scheduled date is 0, replace it with an empty string
455 if ($rows[$key]['scheduled_iso'] == '0000-00-00 00:00:00') {
456 $rows[$key]['scheduled'] = '';
457 }
458 unset($rows[$key]['scheduled_iso']);
459 }
460 }
461
462 // also initialize the AtoZ pager
463 $this->pagerAtoZ();
464 return $rows;
465 }
466
467 /**
468 * Name of export file.
469 *
470 * @param string $output
471 * Type of output.
472 *
473 * @return string
474 * name of the file
475 */
476 public function getExportFileName($output = 'csv') {
477 return ts('CiviMail Mailings');
478 }
479
480 /**
481 * @param $parent
482 */
483 public function setParent($parent) {
484 $this->_parent = $parent;
485 }
486
487 /**
488 * @param array $params
489 * @param bool $sortBy
490 *
491 * @return int|string
492 */
493 public function whereClause(&$params, $sortBy = TRUE) {
494 $values = $clauses = [];
495 $isFormSubmitted = $this->_parent->get('hidden_find_mailings');
496
497 $title = $this->_parent->get('mailing_name');
498 if ($title) {
499 $clauses[] = 'name LIKE %1';
500 if (strpos($title, '%') !== FALSE) {
501 $params[1] = [$title, 'String', FALSE];
502 }
503 else {
504 $params[1] = [$title, 'String', TRUE];
505 }
506 }
507
508 $dateClause1 = $dateClause2 = [];
509 $from = $this->_parent->get('mailing_low');
510 if (!CRM_Utils_System::isNull($from)) {
511 if ($this->_parent->get('unscheduled')) {
512 $dateClause1[] = 'civicrm_mailing.created_date >= %2';
513 }
514 else {
515 $dateClause1[] = 'civicrm_mailing_job.start_date >= %2';
516 $dateClause2[] = 'civicrm_mailing_job.scheduled_date >= %2';
517 }
518 $params[2] = [$from, 'String'];
519 }
520
521 $to = $this->_parent->get('mailing_high');
522 if (!CRM_Utils_System::isNull($to)) {
523 if ($this->_parent->get('unscheduled')) {
524 $dateClause1[] = ' civicrm_mailing.created_date <= %3 ';
525 }
526 else {
527 $dateClause1[] = 'civicrm_mailing_job.start_date <= %3';
528 $dateClause2[] = 'civicrm_mailing_job.scheduled_date <= %3';
529 }
530 $params[3] = [$to, 'String'];
531 }
532
533 $dateClauses = [];
534 if (!empty($dateClause1)) {
535 $dateClauses[] = implode(' AND ', $dateClause1);
536 }
537 if (!empty($dateClause2)) {
538 $dateClauses[] = implode(' AND ', $dateClause2);
539 }
540 $dateClauses = implode(' OR ', $dateClauses);
541 if (!empty($dateClauses)) {
542 $clauses[] = "({$dateClauses})";
543 }
544
545 if ($this->_parent->get('sms')) {
546 $clauses[] = "civicrm_mailing.sms_provider_id IS NOT NULL";
547 }
548 else {
549 $clauses[] = "civicrm_mailing.sms_provider_id IS NULL";
550 }
551
552 // get values submitted by form
553 $isDraft = $this->_parent->get('status_unscheduled');
554 $isArchived = $this->_parent->get('is_archived');
555 $mailingStatus = $this->_parent->get('mailing_status');
556
557 if (!$isFormSubmitted && $this->_parent->get('scheduled')) {
558 // mimic default behavior for scheduled screen
559 $isArchived = 0;
560 $mailingStatus = ['Scheduled' => 1, 'Complete' => 1, 'Running' => 1, 'Paused' => 1, 'Canceled' => 1];
561 }
562 if (!$isFormSubmitted && $this->_parent->get('archived')) {
563 // mimic default behavior for archived screen
564 $isArchived = 1;
565 }
566 if (!$isFormSubmitted && $this->_parent->get('unscheduled')) {
567 // mimic default behavior for draft screen
568 $isDraft = 1;
569 }
570
571 $statusClauses = [];
572 if ($isDraft) {
573 $statusClauses[] = "civicrm_mailing.scheduled_id IS NULL";
574 }
575 if (!empty($mailingStatus)) {
576 $statusClauses[] = "civicrm_mailing_job.status IN ('" . implode("', '", array_keys($mailingStatus)) . "')";
577 }
578 if (!empty($statusClauses)) {
579 $clauses[] = "(" . implode(' OR ', $statusClauses) . ")";
580 }
581
582 if (isset($isArchived)) {
583 if ($isArchived) {
584 $clauses[] = "civicrm_mailing.is_archived = 1";
585 }
586 else {
587 $clauses[] = "(civicrm_mailing.is_archived IS NULL OR civicrm_mailing.is_archived = 0)";
588 }
589 }
590
591 if ($sortBy &&
592 $this->_parent->_sortByCharacter !== NULL
593 ) {
594 $clauses[] = "name LIKE '" . strtolower(CRM_Core_DAO::escapeWildCardString($this->_parent->_sortByCharacter)) . "%'";
595 }
596
597 // dont do a the below assignement when doing a
598 // AtoZ pager clause
599 if ($sortBy) {
600 if (count($clauses) > 1) {
601 $this->_parent->assign('isSearch', 1);
602 }
603 else {
604 $this->_parent->assign('isSearch', 0);
605 }
606 }
607
608 $createOrSentBy = $this->_parent->get('sort_name');
609 if (!CRM_Utils_System::isNull($createOrSentBy)) {
610 $clauses[] = '(createdContact.sort_name LIKE %4 OR scheduledContact.sort_name LIKE %4)';
611 $params[4] = ['%' . $createOrSentBy . '%', 'String'];
612 }
613
614 $createdId = $this->_parent->get('createdId');
615 if ($createdId) {
616 $clauses[] = "(created_id = {$createdId})";
617 $params[5] = [$createdId, 'Integer'];
618 }
619
620 $campainIds = $this->_parent->get('campaign_id');
621 if (!CRM_Utils_System::isNull($campainIds)) {
622 if (!is_array($campainIds)) {
623 $campaignIds = [$campaignIds];
624 }
625 $clauses[] = '( campaign_id IN ( ' . implode(' , ', array_values($campainIds)) . ' ) )';
626 }
627
628 if ($language = $this->_parent->get('language')) {
629 $clauses[] = "civicrm_mailing.language = %6";
630 $params[6] = [$language, 'String'];
631 }
632
633 if (empty($clauses)) {
634 return 1;
635 }
636
637 return implode(' AND ', $clauses);
638 }
639
640 public function pagerAtoZ() {
641
642 $params = [];
643 $whereClause = $this->whereClause($params, FALSE);
644
645 $query = "
646 SELECT DISTINCT UPPER(LEFT(name, 1)) as sort_name
647 FROM civicrm_mailing
648 LEFT JOIN civicrm_mailing_job ON (civicrm_mailing_job.mailing_id = civicrm_mailing.id)
649 LEFT JOIN civicrm_contact createdContact ON ( civicrm_mailing.created_id = createdContact.id )
650 LEFT JOIN civicrm_contact scheduledContact ON ( civicrm_mailing.scheduled_id = scheduledContact.id )
651 WHERE $whereClause
652 ORDER BY UPPER(LEFT(name, 1))
653 ";
654
655 $dao = CRM_Core_DAO::executeQuery($query, $params);
656
657 $aToZBar = CRM_Utils_PagerAToZ::getAToZBar($dao, $this->_parent->_sortByCharacter, TRUE);
658 $this->_parent->assign('aToZ', $aToZBar);
659 }
660
661 }