3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2018
37 * This class contains functions for managing Scheduled Reminders
39 class CRM_Core_BAO_ActionSchedule
extends CRM_Core_DAO_ActionSchedule
{
42 * @param array $filters
43 * Filter by property (e.g. 'id').
45 * Array(scalar $id => Mapping $mapping).
47 public static function getMappings($filters = NULL) {
48 static $_action_mapping;
50 if ($_action_mapping === NULL) {
51 $event = \Civi\Core\Container
::singleton()->get('dispatcher')
52 ->dispatch(\Civi\ActionSchedule\Events
::MAPPINGS
,
53 new \Civi\ActionSchedule\Event\
MappingRegisterEvent());
54 $_action_mapping = $event->getMappings();
57 if (empty($filters)) {
58 return $_action_mapping;
60 elseif (isset($filters['id'])) {
62 $filters['id'] => $_action_mapping[$filters['id']],
66 throw new CRM_Core_Exception("getMappings() called with unsupported filter: " . implode(', ', array_keys($filters)));
71 * @param string|int $id
72 * @return \Civi\ActionSchedule\Mapping|NULL
74 public static function getMapping($id) {
75 $mappings = self
::getMappings();
76 return isset($mappings[$id]) ?
$mappings[$id] : NULL;
80 * For each entity, get a list of entity-value labels.
83 * Ex: $entityValueLabels[$mappingId][$valueId] = $valueLabel.
84 * @throws CRM_Core_Exception
86 public static function getAllEntityValueLabels() {
87 $entityValueLabels = array();
88 foreach (CRM_Core_BAO_ActionSchedule
::getMappings() as $mapping) {
89 /** @var \Civi\ActionSchedule\Mapping $mapping */
90 $entityValueLabels[$mapping->getId()] = $mapping->getValueLabels();
91 $valueLabel = array('- ' . strtolower($mapping->getValueHeader()) . ' -');
92 $entityValueLabels[$mapping->getId()] = $valueLabel +
$entityValueLabels[$mapping->getId()];
94 return $entityValueLabels;
98 * For each entity, get a list of entity-status labels.
101 * Ex: $entityValueLabels[$mappingId][$valueId][$statusId] = $statusLabel.
103 public static function getAllEntityStatusLabels() {
104 $entityValueLabels = self
::getAllEntityValueLabels();
105 $entityStatusLabels = array();
106 foreach (CRM_Core_BAO_ActionSchedule
::getMappings() as $mapping) {
107 /** @var \Civi\ActionSchedule\Mapping $mapping */
108 $statusLabel = array('- ' . strtolower($mapping->getStatusHeader()) . ' -');
109 $entityStatusLabels[$mapping->getId()] = $entityValueLabels[$mapping->getId()];
110 foreach ($entityStatusLabels[$mapping->getId()] as $kkey => & $vval) {
111 $vval = $statusLabel +
$mapping->getStatusLabels($kkey);
114 return $entityStatusLabels;
118 * Retrieve list of Scheduled Reminders.
120 * @param bool $namesOnly
121 * Return simple list of names.
123 * @param \Civi\ActionSchedule\Mapping|NULL $filterMapping
124 * Filter by the schedule's mapping type.
125 * @param int $filterValue
126 * Filter by the schedule's entity_value.
129 * (reference) reminder list
131 public static function &getList($namesOnly = FALSE, $filterMapping = NULL, $filterValue = NULL) {
137 cas.entity_value as entityValueIds,
138 cas.entity_status as entityStatusIds,
139 cas.start_action_date as entityDate,
140 cas.start_action_offset,
141 cas.start_action_unit,
142 cas.start_action_condition,
147 FROM civicrm_action_schedule cas
149 $queryParams = array();
150 $where = " WHERE 1 ";
151 if ($filterMapping and $filterValue) {
152 $where .= " AND cas.entity_value = %1 AND cas.mapping_id = %2";
153 $queryParams[1] = array($filterValue, 'Integer');
154 $queryParams[2] = array($filterMapping->getId(), 'String');
156 $where .= " AND cas.used_for IS NULL";
158 $dao = CRM_Core_DAO
::executeQuery($query, $queryParams);
159 while ($dao->fetch()) {
160 /** @var Civi\ActionSchedule\Mapping $filterMapping */
161 $filterMapping = CRM_Utils_Array
::first(self
::getMappings(array(
162 'id' => $dao->mapping_id
,
164 $list[$dao->id
]['id'] = $dao->id
;
165 $list[$dao->id
]['title'] = $dao->title
;
166 $list[$dao->id
]['start_action_offset'] = $dao->start_action_offset
;
167 $list[$dao->id
]['start_action_unit'] = $dao->start_action_unit
;
168 $list[$dao->id
]['start_action_condition'] = $dao->start_action_condition
;
169 $list[$dao->id
]['entityDate'] = ucwords(str_replace('_', ' ', $dao->entityDate
));
170 $list[$dao->id
]['absolute_date'] = $dao->absolute_date
;
171 $list[$dao->id
]['entity'] = $filterMapping->getLabel();
172 $list[$dao->id
]['value'] = implode(', ', CRM_Utils_Array
::subset(
173 $filterMapping->getValueLabels(),
174 explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $dao->entityValueIds
)
176 $list[$dao->id
]['status'] = implode(', ', CRM_Utils_Array
::subset(
177 $filterMapping->getStatusLabels($dao->entityValueIds
),
178 explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $dao->entityStatusIds
)
180 $list[$dao->id
]['is_repeat'] = $dao->is_repeat
;
181 $list[$dao->id
]['is_active'] = $dao->is_active
;
188 * Add the schedules reminders in the db.
190 * @param array $params
191 * (reference ) an assoc array of name/value pairs.
195 * @return CRM_Core_DAO_ActionSchedule
197 public static function add(&$params, $ids = array()) {
198 $actionSchedule = new CRM_Core_DAO_ActionSchedule();
199 $actionSchedule->copyValues($params);
201 return $actionSchedule->save();
205 * Retrieve DB object based on input parameters.
207 * It also stores all the retrieved values in the default array.
209 * @param array $params
210 * (reference ) an assoc array of name/value pairs.
211 * @param array $values
212 * (reference ) an assoc array to hold the flattened values.
214 * @return CRM_Core_DAO_ActionSchedule|null
215 * object on success, null otherwise
217 public static function retrieve(&$params, &$values) {
218 if (empty($params)) {
221 $actionSchedule = new CRM_Core_DAO_ActionSchedule();
223 $actionSchedule->copyValues($params);
225 if ($actionSchedule->find(TRUE)) {
226 $ids['actionSchedule'] = $actionSchedule->id
;
228 CRM_Core_DAO
::storeValues($actionSchedule, $values);
230 return $actionSchedule;
239 * ID of the Reminder to be deleted.
242 public static function del($id) {
244 $dao = new CRM_Core_DAO_ActionSchedule();
246 if ($dao->find(TRUE)) {
251 CRM_Core_Error
::fatal(ts('Invalid value passed to delete function.'));
255 * Update the is_active flag in the db.
258 * Id of the database record.
259 * @param bool $is_active
260 * Value we want to set the is_active field.
263 * DAO object on success, null otherwise
265 public static function setIsActive($id, $is_active) {
266 return CRM_Core_DAO
::setFieldValue('CRM_Core_DAO_ActionSchedule', $id, 'is_active', $is_active);
270 * @param int $mappingID
273 * @throws CRM_Core_Exception
275 public static function sendMailings($mappingID, $now) {
276 $mapping = CRM_Utils_Array
::first(self
::getMappings(array(
280 $actionSchedule = new CRM_Core_DAO_ActionSchedule();
281 $actionSchedule->mapping_id
= $mappingID;
282 $actionSchedule->is_active
= 1;
283 $actionSchedule->find(FALSE);
285 while ($actionSchedule->fetch()) {
286 $query = CRM_Core_BAO_ActionSchedule
::prepareMailingQuery($mapping, $actionSchedule);
287 $dao = CRM_Core_DAO
::executeQuery($query,
288 array(1 => array($actionSchedule->id
, 'Integer'))
291 $multilingual = CRM_Core_I18n
::isMultilingual();
292 while ($dao->fetch()) {
293 // switch language if necessary
295 $preferred_language = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $dao->contactID
, 'preferred_language');
296 CRM_Core_BAO_ActionSchedule
::setCommunicationLanguage($actionSchedule->communication_language
, $preferred_language);
301 $tokenProcessor = self
::createTokenProcessor($actionSchedule, $mapping);
302 $tokenProcessor->addRow()
303 ->context('contactId', $dao->contactID
)
304 ->context('actionSearchResult', (object) $dao->toArray());
305 foreach ($tokenProcessor->evaluate()->getRows() as $tokenRow) {
306 if ($actionSchedule->mode
== 'SMS' or $actionSchedule->mode
== 'User_Preference') {
307 CRM_Utils_Array
::extend($errors, self
::sendReminderSms($tokenRow, $actionSchedule, $dao->contactID
));
310 if ($actionSchedule->mode
== 'Email' or $actionSchedule->mode
== 'User_Preference') {
311 CRM_Utils_Array
::extend($errors, self
::sendReminderEmail($tokenRow, $actionSchedule, $dao->contactID
));
313 // insert activity log record if needed
314 if ($actionSchedule->record_activity
&& empty($errors)) {
315 $caseID = empty($dao->case_id
) ?
NULL : $dao->case_id
;
316 CRM_Core_BAO_ActionSchedule
::createMailingActivity($tokenRow, $mapping, $dao->contactID
, $dao->entityID
, $caseID);
320 catch (\Civi\Token\TokenException
$e) {
321 $errors['token_exception'] = $e->getMessage();
324 // update action log record
326 'id' => $dao->reminderID
,
327 'is_error' => !empty($errors),
328 'message' => empty($errors) ?
"null" : implode(' ', $errors),
329 'action_date_time' => $now,
331 CRM_Core_BAO_ActionLog
::create($logParams);
339 * @param int $mappingID
341 * @param array $params
343 * @throws API_Exception
345 public static function buildRecipientContacts($mappingID, $now, $params = array()) {
346 $actionSchedule = new CRM_Core_DAO_ActionSchedule();
347 $actionSchedule->mapping_id
= $mappingID;
348 $actionSchedule->is_active
= 1;
349 if (!empty($params)) {
350 _civicrm_api3_dao_set_filter($actionSchedule, $params, FALSE);
352 $actionSchedule->find();
354 while ($actionSchedule->fetch()) {
355 /** @var \Civi\ActionSchedule\Mapping $mapping */
356 $mapping = CRM_Utils_Array
::first(self
::getMappings(array(
359 $builder = new \Civi\ActionSchedule\
RecipientBuilder($now, $actionSchedule, $mapping);
366 * @param array $params
370 public static function processQueue($now = NULL, $params = array()) {
371 $now = $now ? CRM_Utils_Time
::setTime($now) : CRM_Utils_Time
::getTime();
373 $mappings = CRM_Core_BAO_ActionSchedule
::getMappings();
374 foreach ($mappings as $mappingID => $mapping) {
375 CRM_Core_BAO_ActionSchedule
::buildRecipientContacts($mappingID, $now, $params);
376 CRM_Core_BAO_ActionSchedule
::sendMailings($mappingID, $now);
381 'messages' => ts('Sent all scheduled reminders successfully'),
388 * @param int $mappingID
390 * @return null|string
392 public static function isConfigured($id, $mappingID) {
393 $queryString = "SELECT count(id) FROM civicrm_action_schedule
394 WHERE mapping_id = %1 AND
398 1 => array($mappingID, 'String'),
399 2 => array($id, 'Integer'),
401 return CRM_Core_DAO
::singleValueQuery($queryString, $params);
405 * @param int $mappingID
406 * @param $recipientType
410 public static function getRecipientListing($mappingID, $recipientType) {
415 /** @var \Civi\ActionSchedule\Mapping $mapping */
416 $mapping = CRM_Utils_Array
::first(CRM_Core_BAO_ActionSchedule
::getMappings(array(
419 return $mapping->getRecipientListing($recipientType);
423 * @param $communication_language
424 * @param $preferred_language
426 public static function setCommunicationLanguage($communication_language, $preferred_language) {
427 $currentLocale = CRM_Core_I18n
::getLocale();
428 $language = $currentLocale;
430 // prepare the language for the email
431 if ($communication_language == CRM_Core_I18n
::AUTO
) {
432 if (!empty($preferred_language)) {
433 $language = $preferred_language;
437 $language = $communication_language;
440 // language not in the existing language, use default
441 $languages = CRM_Core_I18n
::languages(TRUE);
442 if (!array_key_exists($language, $languages)) {
443 $language = $currentLocale;
446 // change the language
447 $i18n = CRM_Core_I18n
::singleton();
448 $i18n->setLocale($language);
452 * Save a record about the delivery of a reminder email.
454 * WISHLIST: Instead of saving $actionSchedule->body_html, call this immediately after
455 * sending the message and pass in the fully rendered text of the message.
457 * @param object $tokenRow
458 * @param Civi\ActionSchedule\Mapping $mapping
459 * @param int $contactID
460 * @param int $entityID
461 * @param int|NULL $caseID
462 * @throws CRM_Core_Exception
464 protected static function createMailingActivity($tokenRow, $mapping, $contactID, $entityID, $caseID) {
465 $session = CRM_Core_Session
::singleton();
467 if ($mapping->getEntity() == 'civicrm_membership') {
468 // @todo - not required with api
470 = CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Membership Renewal Reminder');
473 // @todo - not required with api
475 = CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Reminder Sent');
478 $activityParams = array(
479 'subject' => $tokenRow->render('subject'),
480 'details' => $tokenRow->render('body_html'),
481 'source_contact_id' => $session->get('userID') ?
$session->get('userID') : $contactID,
482 'target_contact_id' => $contactID,
483 // @todo - not required with api
484 'activity_date_time' => CRM_Utils_Time
::getTime('YmdHis'),
485 // @todo - not required with api
486 'status_id' => CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'status_id', 'Completed'),
487 'activity_type_id' => $activityTypeID,
488 'source_record_id' => $entityID,
490 // @todo use api, remove all the above wrangling
491 $activity = CRM_Activity_BAO_Activity
::create($activityParams);
493 //file reminder on case if source activity is a case activity
494 if (!empty($caseID)) {
495 $caseActivityParams = array();
496 $caseActivityParams['case_id'] = $caseID;
497 $caseActivityParams['activity_id'] = $activity->id
;
498 CRM_Case_BAO_Case
::processCaseActivity($caseActivityParams);
503 * @param \Civi\ActionSchedule\MappingInterface $mapping
504 * @param \CRM_Core_DAO_ActionSchedule $actionSchedule
507 protected static function prepareMailingQuery($mapping, $actionSchedule) {
508 $select = CRM_Utils_SQL_Select
::from('civicrm_action_log reminder')
509 ->select("reminder.id as reminderID, reminder.contact_id as contactID, reminder.entity_table as entityTable, reminder.*, e.id AS entityID")
510 ->join('e', "!casMailingJoinType !casMappingEntity e ON !casEntityJoinExpr")
511 ->select("e.id as entityID, e.*")
512 ->where("reminder.action_schedule_id = #casActionScheduleId")
513 ->where("reminder.action_date_time IS NULL")
515 'casActionScheduleId' => $actionSchedule->id
,
516 'casMailingJoinType' => ($actionSchedule->limit_to
== 0) ?
'LEFT JOIN' : 'INNER JOIN',
517 'casMappingId' => $mapping->getId(),
518 'casMappingEntity' => $mapping->getEntity(),
519 'casEntityJoinExpr' => 'e.id = reminder.entity_id',
522 if ($actionSchedule->limit_to
== 0) {
523 $select->where("e.id = reminder.entity_id OR reminder.entity_table = 'civicrm_contact'");
526 \Civi\Core\Container
::singleton()->get('dispatcher')
528 \Civi\ActionSchedule\Events
::MAILING_QUERY
,
529 new \Civi\ActionSchedule\Event\
MailingQueryEvent($actionSchedule, $mapping, $select)
532 return $select->toSQL();
536 * @param \Civi\Token\TokenRow $tokenRow
537 * @param CRM_Core_DAO_ActionSchedule $schedule
538 * @param int $toContactID
539 * @throws CRM_Core_Exception
541 * List of error messages.
543 protected static function sendReminderSms($tokenRow, $schedule, $toContactID) {
544 $toPhoneNumber = self
::pickSmsPhoneNumber($toContactID);
545 if (!$toPhoneNumber) {
546 return array("sms_phone_missing" => "Couldn't find recipient's phone number.");
549 $messageSubject = $tokenRow->render('subject');
550 $sms_body_text = $tokenRow->render('sms_body_text');
552 $session = CRM_Core_Session
::singleton();
553 $userID = $session->get('userID') ?
$session->get('userID') : $tokenRow->context
['contactId'];
555 'To' => $toPhoneNumber,
556 'provider_id' => $schedule->sms_provider_id
,
557 'activity_subject' => $messageSubject,
559 $activityTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'SMS');
560 $activityParams = array(
561 'source_contact_id' => $userID,
562 'activity_type_id' => $activityTypeID,
563 'activity_date_time' => date('YmdHis'),
564 'subject' => $messageSubject,
565 'details' => $sms_body_text,
566 'status_id' => CRM_Core_OptionGroup
::getValue('activity_status', 'Completed', 'name'),
569 $activity = CRM_Activity_BAO_Activity
::create($activityParams);
571 CRM_Activity_BAO_Activity
::sendSMSMessage($tokenRow->context
['contactId'],
582 * @param CRM_Core_DAO_ActionSchedule $actionSchedule
584 * Ex: "Alice <alice@example.org>".
586 protected static function pickFromEmail($actionSchedule) {
587 $domainValues = CRM_Core_BAO_Domain
::getNameAndEmail();
588 $fromEmailAddress = "$domainValues[0] <$domainValues[1]>";
589 if ($actionSchedule->from_email
) {
590 $fromEmailAddress = "$actionSchedule->from_name <$actionSchedule->from_email>";
591 return $fromEmailAddress;
593 return $fromEmailAddress;
597 * @param \Civi\Token\TokenRow $tokenRow
598 * @param CRM_Core_DAO_ActionSchedule $schedule
599 * @param int $toContactID
601 * List of error messages.
603 protected static function sendReminderEmail($tokenRow, $schedule, $toContactID) {
604 $toEmail = CRM_Contact_BAO_Contact
::getPrimaryEmail($toContactID);
606 return array("email_missing" => "Couldn't find recipient's email address.");
609 $body_text = $tokenRow->render('body_text');
610 $body_html = $tokenRow->render('body_html');
611 if (!$schedule->body_text
) {
612 $body_text = CRM_Utils_String
::htmlToText($body_html);
615 // set up the parameters for CRM_Utils_Mail::send
617 'groupName' => 'Scheduled Reminder Sender',
618 'from' => self
::pickFromEmail($schedule),
619 'toName' => $tokenRow->context
['contact']['display_name'],
620 'toEmail' => $toEmail,
621 'subject' => $tokenRow->render('subject'),
622 'entity' => 'action_schedule',
623 'entity_id' => $schedule->id
,
626 if (!$body_html ||
$tokenRow->context
['contact']['preferred_mail_format'] == 'Text' ||
627 $tokenRow->context
['contact']['preferred_mail_format'] == 'Both'
629 // render the & entities in text mode, so that the links work
630 $mailParams['text'] = str_replace('&', '&', $body_text);
632 if ($body_html && ($tokenRow->context
['contact']['preferred_mail_format'] == 'HTML' ||
633 $tokenRow->context
['contact']['preferred_mail_format'] == 'Both'
636 $mailParams['html'] = $body_html;
638 $result = CRM_Utils_Mail
::send($mailParams);
639 if (!$result ||
is_a($result, 'PEAR_Error')) {
640 return array('email_fail' => 'Failed to send message');
647 * @param CRM_Core_DAO_ActionSchedule $schedule
648 * @param \Civi\ActionSchedule\Mapping $mapping
649 * @return \Civi\Token\TokenProcessor
651 protected static function createTokenProcessor($schedule, $mapping) {
652 $tp = new \Civi\Token\
TokenProcessor(\Civi\Core\Container
::singleton()->get('dispatcher'), array(
653 'controller' => __CLASS__
,
654 'actionSchedule' => $schedule,
655 'actionMapping' => $mapping,
658 $tp->addMessage('body_text', $schedule->body_text
, 'text/plain');
659 $tp->addMessage('body_html', $schedule->body_html
, 'text/html');
660 $tp->addMessage('sms_body_text', $schedule->sms_body_text
, 'text/plain');
661 $tp->addMessage('subject', $schedule->subject
, 'text/plain');
666 * Pick SMS phone number.
668 * @param int $smsToContactId
670 * @return NULL|string
672 protected static function pickSmsPhoneNumber($smsToContactId) {
673 $toPhoneNumbers = CRM_Core_BAO_Phone
::allPhones($smsToContactId, FALSE, 'Mobile', array(
678 //to get primary mobile ph,if not get a first mobile phONE
679 if (!empty($toPhoneNumbers)) {
680 $toPhoneNumberDetails = reset($toPhoneNumbers);
681 $toPhoneNumber = CRM_Utils_Array
::value('phone', $toPhoneNumberDetails);
682 return $toPhoneNumber;
688 * Get the list of generic recipient types supported by all entities/mappings.
691 * array(mixed $value => string $label).
693 public static function getAdditionalRecipients() {
695 'manual' => ts('Choose Recipient(s)'),
696 'group' => ts('Select Group'),