diff --git a/core/action.class.inc.php b/core/action.class.inc.php index 62f2e8ad5..e8a445d30 100644 --- a/core/action.class.inc.php +++ b/core/action.class.inc.php @@ -30,909 +30,9 @@ use Combodo\iTop\Service\Router\Router; * @license http://opensource.org/licenses/AGPL-3.0 */ - require_once(APPROOT.'/core/asynctask.class.inc.php'); require_once(APPROOT.'/core/email.class.inc.php'); - -/** - * A user defined action, to customize the application - * - * @package iTopORM - */ -abstract class Action extends cmdbAbstractObject -{ - /** - * @throws \CoreException - * @throws \Exception - */ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "name", - "complementary_name_attcode" => ['finalclass', 'description'], - "state_attcode" => "status", - "reconc_keys" => ['name'], - "db_table" => "priv_action", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - "style" => new ormStyle("ibo-dm-class--Action", "ibo-dm-class-alt--Action", "var(--ibo-dm-class--Action--main-color)", "var(--ibo-dm-class--Action--complementary-color)", null, '../images/icons/icons8-in-transit.svg'), - ); - MetaModel::Init_Params($aParams); - //MetaModel::Init_InheritAttributes(); - MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values" => null, "sql" => "name", "default_value" => null, "is_null_allowed" => false, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values" => null, "sql" => "description", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - - MetaModel::Init_AddAttribute(new AttributeEnum("status", array( - "allowed_values" => new ValueSetEnum(array('test' => 'Being tested', 'enabled' => 'In production', 'disabled' => 'Inactive')), - "styled_values" => [ - 'test' => new ormStyle('ibo-dm-enum--Action-status-test', 'ibo-dm-enum-alt--Action-status-test', 'var(--ibo-dm-enum--Action-status-test--main-color)', 'var(--ibo-dm-enum--Action-status-test--complementary-color)', null, null), - 'enabled' => new ormStyle('ibo-dm-enum--Action-status-enabled', 'ibo-dm-enum-alt--Action-status-enabled', 'var(--ibo-dm-enum--Action-status-enabled--main-color)', 'var(--ibo-dm-enum--Action-status-enabled--complementary-color)', 'fas fa-check', null), - 'disabled' => new ormStyle('ibo-dm-enum--Action-status-disabled', 'ibo-dm-enum-alt--Action-status-disabled', 'var(--ibo-dm-enum--Action-status-disabled--main-color)', 'var(--ibo-dm-enum--Action-status-disabled--complementary-color)', null, null), - ], - "display_style" => 'list', - "sql" => "status", - "default_value" => "test", - "is_null_allowed" => false, - "depends_on" => array(), - ))); - - MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("trigger_list", - array("linked_class" => "lnkTriggerAction", "ext_key_to_me" => "action_id", "ext_key_to_remote" => "trigger_id", "allowed_values" => null, "count_min" => 0, "count_max" => 0, "depends_on" => array(), "display_style" => 'property'))); - MetaModel::Init_AddAttribute(new AttributeEnum("asynchronous", array("allowed_values" => new ValueSetEnum(['use_global_setting' => 'Use global settings','yes' => 'Yes' ,'no' => 'No']), "sql" => "asynchronous", "default_value" => 'use_global_setting', "is_null_allowed" => false, "depends_on" => array()))); - - // Display lists - // - Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); - // - Attributes to be displayed for a list - MetaModel::Init_SetZListItems('list', array('finalclass', 'name', 'description', 'status')); - // Search criteria - // - Default criteria of the search form - MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status')); - - } - - /** - * Encapsulate the execution of the action and handle failure & logging - * - * @param \Trigger $oTrigger - * @param array $aContextArgs - * - * @return mixed - */ - abstract public function DoExecute($oTrigger, $aContextArgs); - - /** - * @return bool - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function IsActive() - { - switch($this->Get('status')) - { - case 'enabled': - case 'test': - return true; - - default: - return false; - } - } - - /** - * Return true if the current action status is set on "test" - * - * @return bool - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function IsBeingTested() - { - switch($this->Get('status')) - { - case 'test': - return true; - - default: - return false; - } - } - - /** - * @inheritDoc - * @since 3.0.0 - */ - public function AfterInsert() - { - parent::AfterInsert(); - $this->DoCheckIfHasTrigger(); - } - - /** - * @inheritDoc - * @since 3.0.0 - */ - public function AfterUpdate() - { - parent::AfterUpdate(); - $this->DoCheckIfHasTrigger(); - } - - /** - * Check if the Action has at least 1 trigger linked. Otherwise, it adds a warning. - * @return void - * @since 3.0.0 - */ - protected function DoCheckIfHasTrigger() - { - $oTriggersSet = $this->Get('trigger_list'); - if ($oTriggersSet->Count() === 0) { - $this->m_aCheckWarnings[] = Dict::S('Action:WarningNoTriggerLinked'); - } - } - - /** - * @since 3.2.0 N°5472 method creation - */ - public function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, false); - - if ($oPage instanceof iTopWebPage && !$this->IsNew()) { - $this->GenerateLastExecutionsTab($oPage, $bEditMode); - } - } - - /** - * @since 3.2.0 N°5472 method creation - */ - protected function GenerateLastExecutionsTab(iTopWebPage $oPage, $bEditMode) - { - $oRouter = Router::GetInstance(); - $sActionLastExecutionsPageUrl = $oRouter->GenerateUrl('notifications.action.last_executions_tab', ['action_id' => $this->GetKey()]); - $oPage->AddAjaxTab('action_errors', $sActionLastExecutionsPageUrl, false, Dict::S('Action:last_executions_tab')); - } - - /** - * @param \Combodo\iTop\Application\WebPage\WebPage $oPage - * - * @throws \ApplicationException - * @throws \ArchivedObjectException - * @throws \ConfigException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \DictExceptionMissingString - * @throws \InvalidConfigParamException - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \OQLException - * @throws \ReflectionException - * @since 3.2.0 N°5472 method creation - */ - public function GetLastExecutionsTabContent(WebPage $oPage): void - { - $oConfig = utils::GetConfig(); - $sLastExecutionDaysConfigParamName = 'notifications.last_executions_days'; - $iLastExecutionDays = $oConfig->Get($sLastExecutionDaysConfigParamName); - - if ($iLastExecutionDays < 0) { - throw new InvalidConfigParamException("Invalid value for {$sLastExecutionDaysConfigParamName} config parameter. Param desc: " . $oConfig->GetDescription($sLastExecutionDaysConfigParamName)); - } - - $sActionQueryOql = 'SELECT EventNotification WHERE action_id = :action_id'; - $aActionQueryParams = ['action_id' => $this->GetKey()]; - if ($iLastExecutionDays > 0) { - $sActionQueryOql .= ' AND date > DATE_SUB(NOW(), INTERVAL :days DAY)'; - $aActionQueryParams['days'] = $iLastExecutionDays; - $sActionQueryLimit = Dict::Format('Action:last_executions_tab_limit_days', $iLastExecutionDays); - } else { - $sActionQueryLimit = Dict::S('Action:last_executions_tab_limit_none'); - } - - $oActionFilter = DBObjectSearch::FromOQL($sActionQueryOql, $aActionQueryParams); - $oSet = new DBObjectSet($oActionFilter, ['date' => false]); - - $sPanelTitle = Dict::Format('Action:last_executions_tab_panel_title', $sActionQueryLimit); - $oExecutionsListBlock = DataTableUIBlockFactory::MakeForResult($oPage, 'action_executions_list', $oSet, ['panel_title' => $sPanelTitle]); - - $oPage->AddUiBlock($oExecutionsListBlock); - } - - /** - * Will be overloaded by the children classes to return the value of their global asynchronous setting (eg. `email_asynchronous` for `\ActionEmail`, `prefer_asynchronous` for `\ActionWebhook`, ...) - * - * @return bool true if the global setting for this kind of action if to be executed asynchronously, false otherwise. - * @since 3.2.0 - */ - public static function GetAsynchronousGlobalSetting(): bool - { - return false; - } - - /** - * @return bool true if that action instance should be executed asynchronously, otherwise false - * @throws \ArchivedObjectException - * @throws \CoreException - * @since 3.2.0 - */ - public function IsAsynchronous(): bool - { - $sAsynchronous = $this->Get('asynchronous'); - if ($sAsynchronous === 'use_global_setting') { - return static::GetAsynchronousGlobalSetting(); - } - return $sAsynchronous === 'yes'; - } -} - -/** - * A notification - * - * @package iTopORM - */ -abstract class ActionNotification extends Action -{ - /** - * @inheritDoc - * @throws \CoreException - */ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "name", - "complementary_name_attcode" => ['finalclass', 'description'], - "state_attcode" => "", - "reconc_keys" => ['name'], - "db_table" => "priv_action_notification", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - // Display lists - // - Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'trigger_list')); - // - Attributes to be displayed for a list - MetaModel::Init_SetZListItems('list', array('finalclass', 'description', 'status')); - MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - - // Search criteria - // - Criteria of the std search form -// MetaModel::Init_SetZListItems('standard_search', array('name')); - // - Default criteria of the search form -// MetaModel::Init_SetZListItems('default_search', array('name')); - } - - /** - * @param $sLanguage - * @param $sLanguageCode - * - * @return array [$sPreviousLanguage, $aPreviousPluginProperties] - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionUnknownLanguage - * @since 3.2.0 - */ - public function SetNotificationLanguage($sLanguage = null, $sLanguageCode = null){ - $sPreviousLanguage = Dict::GetUserLanguage(); - $aPreviousPluginProperties = ApplicationContext::GetPluginProperties('QueryLocalizerPlugin'); - $sLanguage = $sLanguage ?? $this->Get('language'); - $sLanguageCode = $sLanguageCode ?? $sLanguage; - if (!utils::IsNullOrEmptyString($sLanguage)) { - // If a language is specified for this action, force this language - // when rendering all placeholders inside this message - Dict::SetUserLanguage($sLanguage); - AttributeDateTime::LoadFormatFromConfig(); - ApplicationContext::SetPluginProperty('QueryLocalizerPlugin', 'language_code', $sLanguageCode); - } - return [$sPreviousLanguage, $aPreviousPluginProperties]; - } -} - -/** - * An email notification - * - * @package iTopORM - */ -class ActionEmail extends ActionNotification -{ - /** - * @var string - * @since 3.0.1 - */ - const ENUM_HEADER_NAME_MESSAGE_ID = 'Message-ID'; - /** - * @var string - * @since 3.0.1 - */ - const ENUM_HEADER_NAME_REFERENCES = 'References'; - /** - * @var string - * @since 3.1.0 - */ - const TEMPLATE_BODY_CONTENT = '$content$'; - /** - * Wraps the 'body' of the message for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied - * @var string - * @since 3.1.0 - */ - const CONTENT_HIGHLIGHT = '
$content$
%s
'; - /** - * Wraps a placeholder of the email's body for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied - * @var string - */ - const FIELD_HIGHLIGHT = '\\$$1\\$'; - /** - * @inheritDoc - */ - public static function Init() - { - $aParams = array - ( - "category" => "grant_by_profile,core/cmdb,application", - "key_type" => "autoincrement", - "name_attcode" => "name", - "state_attcode" => "", - "reconc_keys" => array('name'), - "db_table" => "priv_action_email", - "db_key_field" => "id", - "db_finalclass_field" => "", - 'style' => new ormStyle(null, null, null, null, null, '../images/icons/icons8-mailing.svg'), - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeEmailAddress("test_recipient", array("allowed_values" => null, "sql" => "test_recipient", "default_value" => "", "is_null_allowed" => true, "depends_on" => array()))); - - MetaModel::Init_AddAttribute(new AttributeString("from", array("allowed_values" => null, "sql" => "from", "default_value" => null, "is_null_allowed" => false, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeString("from_label", array("allowed_values" => null, "sql" => "from_label", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeString("reply_to", array("allowed_values" => null, "sql" => "reply_to", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeString("reply_to_label", array("allowed_values" => null, "sql" => "reply_to_label", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("to", array("allowed_values" => null, "sql" => "to", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("cc", array("allowed_values" => null, "sql" => "cc", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeOQL("bcc", array("allowed_values" => null, "sql" => "bcc", "default_value" => null, "is_null_allowed" => true, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values" => null, "sql" => "subject", "default_value" => null, "is_null_allowed" => false, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values" => null, "sql" => "body", "default_value" => null, "is_null_allowed" => false, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values" => new ValueSetEnum('low,normal,high'), "sql" => "importance", "default_value" => 'normal', "is_null_allowed" => false, "depends_on" => array()))); - MetaModel::Init_AddAttribute(new AttributeBlob("html_template", array("is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false))); - MetaModel::Init_AddAttribute(new AttributeEnum("ignore_notify", array("allowed_values" => new ValueSetEnum('yes,no'), "sql" => "ignore_notify", "default_value" => 'yes', "is_null_allowed" => false, "depends_on" => array()))); - - - // Display lists - // - Attributes to be displayed for the complete details - MetaModel::Init_SetZListItems('details', array( - 'col:col1' => array( - 'fieldset:ActionEmail:main' => array( - 0 => 'name', - 1 => 'description', - 2 => 'status', - 3 => 'language', - 4 => 'html_template', - 5 => 'subject', - 6 => 'body', - // 5 => 'importance', not handled when sending the mail, better hide it then - ), - 'fieldset:ActionEmail:trigger' => array( - 0 => 'trigger_list', - 1 => 'asynchronous' - ), - ), - 'col:col2' => array( - 'fieldset:ActionEmail:recipients' => array( - 0 => 'from', - 1 => 'from_label', - 2 => 'reply_to', - 3 => 'reply_to_label', - 4 => 'test_recipient', - 5 => 'ignore_notify', - 6 => 'to', - 7 => 'cc', - 8 => 'bcc', - ), - ), - )); - - // - Attributes to be displayed for a list - MetaModel::Init_SetZListItems('list', array('status', 'to', 'subject', 'language')); - // Search criteria - // - Standard criteria of the search - MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'status', 'subject', 'language')); - // - Default criteria for the search - MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status', 'subject', 'language')); - } - - // count the recipients found - protected $m_iRecipients; - - // Errors management : not that simple because we need that function to be - // executed in the background, while making sure that any issue would be reported clearly - protected $m_aMailErrors; //array of strings explaining the issue - - /** - * Return the list of emails as a string, or a detailed error description - * - * @param string $sRecipAttCode - * @param array $aArgs - * - * @return string - * @throws \ArchivedObjectException - * @throws \CoreCannotSaveObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \CoreWarning - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \OQLException - */ - protected function FindRecipients($sRecipAttCode, $aArgs) - { - $oTrigger = $aArgs['trigger->object()'] ?? null; - $sOQL = $this->Get($sRecipAttCode); - if (utils::IsNullOrEmptyString($sOQL)) return ''; - - try - { - $oSearch = DBObjectSearch::FromOQL($sOQL); - if ($this->Get('ignore_notify') === 'no') { - // In theory, it is possible to notify *any* kind of object, - // as long as there is an email attribute in the class - // So let's not assume that the selected class is a Person - $sFirstSelectedClass = $oSearch->GetClass(); - if (MetaModel::IsValidAttCode($sFirstSelectedClass, 'notify')) { - $oSearch->AddCondition('notify', 'yes'); - } - } - $oSearch->AllowAllData(); - } - catch (OQLException $e) - { - $this->m_aMailErrors[] = "query syntax error for recipient '$sRecipAttCode'"; - return $e->getMessage(); - } - - $sClass = $oSearch->GetClass(); - // Determine the email attribute (the first one will be our choice) - foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeEmailAddress) - { - $sEmailAttCode = $sAttCode; - // we've got one, exit the loop - break; - } - } - if (!isset($sEmailAttCode)) - { - $this->m_aMailErrors[] = "wrong target for recipient '$sRecipAttCode'"; - return "The objects of the class '$sClass' do not have any email attribute"; - } - - if($oTrigger !== null && in_array('Contact', MetaModel::EnumParentClasses($sClass, ENUM_CHILD_CLASSES_ALL), true)) { - $aArgs['trigger_id'] = $oTrigger->GetKey(); - $aArgs['action_id'] = $this->GetKey(); - - $sSubscribedContactsOQL = NotificationsRepository::GetInstance()->GetSearchOQLContactUnsubscribedByTriggerAndAction(); - $sSubscribedContactsOQL->ApplyParameters($aArgs); - $sAlias = $oSearch->GetClassAlias(); - $oSearch->AddConditionExpression(Expression::FromOQL("`$sAlias`.id NOT IN ($sSubscribedContactsOQL)")); - } - - $oSet = new DBObjectSet($oSearch, array() /* order */, $aArgs); - $aRecipients = array(); - while ($oObj = $oSet->Fetch()) - { - $sAddress = trim($oObj->Get($sEmailAttCode)); - if (utils::IsNotNullOrEmptyString($sAddress)) - { - $aRecipients[] = $sAddress; - $this->m_iRecipients++; - } - if ($oTrigger !== null && in_array('Contact', MetaModel::EnumParentClasses($sClass, ENUM_CHILD_CLASSES_ALL), true)) { - NotificationsService::GetInstance()->RegisterSubscription($oTrigger, $this, $oObj); - } - } - return implode(', ', $aRecipients); - } - - /** - * @inheritDoc - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \CoreWarning - */ - public function DoExecute($oTrigger, $aContextArgs) - { - if (MetaModel::IsLogEnabledNotification()) - { - $oLog = new EventNotificationEmail(); - if ($this->IsBeingTested()) - { - $oLog->Set('message', 'TEST - Notification sent ('.$this->Get('test_recipient').')'); - } - else - { - $oLog->Set('message', 'Notification pending'); - } - $oLog->Set('userinfo', UserRights::GetUser()); - $oLog->Set('trigger_id', $oTrigger->GetKey()); - $oLog->Set('action_id', $this->GetKey()); - $oLog->Set('object_id', $aContextArgs['this->object()']->GetKey()); - $oLog->Set('object_class', get_class($aContextArgs['this->object()'])); - // Must be inserted now so that it gets a valid id that will make the link - // between an eventual asynchronous task (queued) and the log - $oLog->DBInsertNoReload(); - } - else - { - $oLog = null; - } - - try - { - $sRes = $this->_DoExecute($oTrigger, $aContextArgs, $oLog); - - if ($this->IsBeingTested()) - { - $sPrefix = 'TEST ('.$this->Get('test_recipient').') - '; - } - else - { - $sPrefix = ''; - } - - if ($oLog) - { - $oLog->Set('message', $sPrefix . $sRes); - $oLog->DBUpdate(); - } - - } - catch (Exception $e) - { - if ($oLog) - { - $oLog->Set('message', 'Error: '.$e->getMessage()); - - try - { - $oLog->DBUpdate(); - } - catch (Exception $eSecondTryUpdate) - { - IssueLog::Error("Failed to process email ".$oLog->GetKey()." - reason: ".$e->getMessage()."\nTrace:\n".$e->getTraceAsString()); - - $oLog->Set('message', 'Error: more details in the log for email "'.$oLog->GetKey().'"'); - $oLog->DBUpdate(); - } - } - } - - } - - /** - * @param \Trigger $oTrigger - * @param array $aContextArgs - * @param \EventNotification $oLog - * - * @return string - * @throws \CoreException - * @throws \Exception - */ - protected function _DoExecute($oTrigger, $aContextArgs, &$oLog) - { - $sStyles = file_get_contents(APPROOT . utils::GetCSSFromSASS("css/email.scss")); - $sStyles .= MetaModel::GetConfig()->Get('email_css'); - - $oEmail = new EMail(); - - $aEmailContent = $this->PrepareMessageContent($aContextArgs, $oLog); - $oEmail->SetSubject($aEmailContent['subject']); - $oEmail->SetBody($aEmailContent['body'], 'text/html', $sStyles); - $oEmail->SetRecipientTO($aEmailContent['to']); - $oEmail->SetRecipientCC($aEmailContent['cc']); - $oEmail->SetRecipientBCC($aEmailContent['bcc']); - $oEmail->SetRecipientFrom($aEmailContent['from'], $aEmailContent['from_label']); - $oEmail->SetRecipientReplyTo($aEmailContent['reply_to'], $aEmailContent['reply_to_label']); - $oEmail->SetReferences($aEmailContent['references']); - $oEmail->SetMessageId($aEmailContent['message_id']); - $oEmail->SetInReplyTo($aEmailContent['in_reply_to']); - - foreach($aEmailContent['attachments'] as $aAttachment) { - $oEmail->AddAttachment($aAttachment['data'], $aAttachment['filename'], $aAttachment['mime_type']); - } - - if (empty($this->m_aMailErrors)) - { - if ($this->m_iRecipients == 0) - { - return 'No recipient'; - } - else - { - $aErrors = []; - $iRes = $oEmail->Send($aErrors, $this->IsAsynchronous() ? Email::ENUM_SEND_FORCE_ASYNCHRONOUS : Email::ENUM_SEND_FORCE_SYNCHRONOUS, $oLog); - switch ($iRes) - { - case EMAIL_SEND_OK: - return "Sent"; - - case EMAIL_SEND_PENDING: - return "Pending"; - - case EMAIL_SEND_ERROR: - return "Errors: ".implode(', ', $aErrors); - } - } - } else { - if (is_array($this->m_aMailErrors) && count($this->m_aMailErrors) > 0) { - $sError = implode(', ', $this->m_aMailErrors); - } else { - $sError = 'Unknown reason'; - } - - return 'Notification was not sent: '.$sError; - } - } - - /** - * @param array $aContextArgs - * @param \EventNotification $oLog - * - * @return array - * @throws \ArchivedObjectException - * @throws \CoreCannotSaveObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \CoreWarning - * @throws \DictExceptionMissingString - * @throws \DictExceptionUnknownLanguage - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \OQLException - * @since 3.1.0 N°918 - */ - protected function PrepareMessageContent($aContextArgs, &$oLog): array - { - $aMessageContent = [ - 'to' => '', - 'cc' => '', - 'bcc' => '', - 'from' => '', - 'from_label' => '', - 'reply_to' => '', - 'reply_to_label' => '', - 'subject' => '', - 'body' => '', - 'references' => '', - 'message_id' => '', - 'in_reply_to' => '', - 'attachments' => [], - ]; - $sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass(); - [$sPreviousLanguage, $aPreviousPluginProperties] = $this->SetNotificationLanguage(); - - try - { - $this->m_iRecipients = 0; - $this->m_aMailErrors = array(); - - // Determine recipients - // - $aMessageContent['to'] = $this->FindRecipients('to', $aContextArgs); - $aMessageContent['cc'] = $this->FindRecipients('cc', $aContextArgs); - $aMessageContent['bcc'] = $this->FindRecipients('bcc', $aContextArgs); - - $aMessageContent['from'] = MetaModel::ApplyParams($this->Get('from'), $aContextArgs); - $aMessageContent['from_label'] = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs); - $aMessageContent['reply_to'] = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs); - $aMessageContent['reply_to_label'] = MetaModel::ApplyParams($this->Get('reply_to_label'), $aContextArgs); - - $aMessageContent['subject'] = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs); - $sBody = $this->BuildMessageBody(false); - $aMessageContent['body'] = MetaModel::ApplyParams($sBody, $aContextArgs); - - $oObj = $aContextArgs['this->object()']; - $aMessageContent['message_id'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_MESSAGE_ID); - $aMessageContent['references'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_REFERENCES); - } - catch (Exception $e) { - /** @noinspection PhpUnhandledExceptionInspection */ - throw $e; - } - finally { - ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker); - $this->SetNotificationLanguage($sPreviousLanguage, $aPreviousPluginProperties['language_code'] ?? null); - } - - if (!is_null($oLog)) { - // Note: we have to secure this because those values are calculated - // inside the try statement, and we would like to keep track of as - // many data as we could while some variables may still be undefined - if (isset($aMessageContent['to'])) { - $oLog->Set('to', $aMessageContent['to']); - } - if (isset($aMessageContent['cc'])) { - $oLog->Set('cc', $aMessageContent['cc']); - } - if (isset($aMessageContent['bcc'])) { - $oLog->Set('bcc', $aMessageContent['bcc']); - } - if (isset($aMessageContent['from'])) { - $oLog->Set('from', $aMessageContent['from']); - } - if (isset($aMessageContent['subject'])) { - $oLog->Set('subject', $aMessageContent['subject']); - } - if (isset($aMessageContent['body'])) { - $oLog->Set('body', HTMLSanitizer::Sanitize($aMessageContent['body'])); - } - } - - if ($this->IsBeingTested()) { - $sTestBody = $aMessageContent['body']; - $sTestBody .= "
\n"; - $sTestBody .= "

Testing email notification ".$this->GetHyperlink()."

\n"; - $sTestBody .= "

The email should be sent with the following properties\n"; - $sTestBody .= "

\n"; - $sTestBody .= "

\n"; - $sTestBody .= "
\n"; - $aMessageContent['subject'] = 'TEST['.$aMessageContent['subject'].']'; - $aMessageContent['body'] = $sTestBody; - $aMessageContent['to'] = $this->Get('test_recipient'); - // N°6677 Ensure emails in test are never sent to cc'd and bcc'd addresses - $aMessageContent['cc'] = ''; - $aMessageContent['bcc'] = ''; - } - // Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification - $aMessageContent['in_reply_to'] = $aMessageContent['references']; - - if (isset($aContextArgs['attachments'])) - { - $aAttachmentReport = array(); - /** @var \ormDocument $oDocument */ - foreach($aContextArgs['attachments'] as $oDocument) - { - $aMessageContent['attachments'][] = ['data' => $oDocument->GetData(), 'filename' => $oDocument->GetFileName(), 'mime_type' => $oDocument->GetMimeType()]; - $aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData() ?? '')); - } - $oLog->Set('attachments', $aAttachmentReport); - } - - return $aMessageContent; - } - - /** - * @param \DBObject $oObject - * @param string $sHeaderName {@see \ActionEmail::ENUM_HEADER_NAME_REFERENCES}, {@see \ActionEmail::ENUM_HEADER_NAME_MESSAGE_ID} - * - * @return string The formatted identifier for $sHeaderName based on $oObject - * @throws \Exception - * @since 3.1.0 N°4849 - */ - protected function GenerateIdentifierForHeaders(DBObject $oObject, string $sHeaderName): string - { - $sObjClass = get_class($oObject); - $sObjId = $oObject->GetKey(); - $sAppName = utils::Sanitize(ITOP_APPLICATION_SHORT, '', utils::ENUM_SANITIZATION_FILTER_VARIABLE_NAME); - - switch ($sHeaderName) { - case static::ENUM_HEADER_NAME_MESSAGE_ID: - case static::ENUM_HEADER_NAME_REFERENCES: - // Prefix - $sPrefix = sprintf('%s_%s_%d', $sAppName, $sObjClass, $sObjId); - if ($sHeaderName === static::ENUM_HEADER_NAME_MESSAGE_ID) { - $sPrefix .= sprintf('_%F', microtime(true /* get as float*/)); - } - // Suffix - $sSuffix = sprintf('@%s.openitop.org', MetaModel::GetEnvironmentId()); - // Identifier - $sIdentifier = $sPrefix.$sSuffix; - if ($sHeaderName === static::ENUM_HEADER_NAME_REFERENCES) { - $sIdentifier = "<$sIdentifier>"; - } - - return $sIdentifier; - } - - // Requested header name invalid - $sErrorMessage = sprintf('%s: Could not generate identifier for header "%s", only %s are supported', static::class, $sHeaderName, implode(' / ', [static::ENUM_HEADER_NAME_MESSAGE_ID, static::ENUM_HEADER_NAME_REFERENCES])); - IssueLog::Error($sErrorMessage, LogChannels::NOTIFICATIONS, [ - 'Object' => $sObjClass.'::'.$sObjId.' ('.$oObject->GetRawName().')', - 'Action' => get_class($this).'::'.$this->GetKey().' ('.$this->GetRawName().')', - ]); - throw new Exception($sErrorMessage); - } - - /** - * Compose the body of the message from the 'body' attribute and the HTML template (if any) - * @since 3.1.0 N°4849 - * @param bool $bHighlightPlaceholders If true add some extra HTML around placeholders to highlight them - * @return string - */ - protected function BuildMessageBody(bool $bHighlightPlaceholders = false): string - { - // Wrap content with a specific class in order to apply styles of HTML fields through the emogrifier (see `css/email.scss`) - $sBody = << - {$this->Get('body')} - -HTML; - - /** @var ormDocument $oHtmlTemplate */ - $oHtmlTemplate = $this->Get('html_template'); - if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) { - $sHtmlTemplate = $oHtmlTemplate->GetData(); - if (false !== mb_strpos($sHtmlTemplate, static::TEMPLATE_BODY_CONTENT)) { - if ($bHighlightPlaceholders) { - // Highlight the $content$ block - $sBody = sprintf(static::CONTENT_HIGHLIGHT, $sBody); - } - $sBody = str_replace(static::TEMPLATE_BODY_CONTENT, $sBody, $oHtmlTemplate->GetData()); // str_replace is ok as long as both strings are well-formed UTF-8 - } else { - $sBody = $oHtmlTemplate->GetData(); - } - } - if($bHighlightPlaceholders) { - // Highlight all placeholders - $sBody = preg_replace('/\\$([^$]+)\\$/', static::FIELD_HIGHLIGHT, $sBody); - } - return $sBody; - } - - /** - * @since 3.1.0 N°4849 - * @inheritDoc - * @see cmdbAbstractObject::DisplayBareRelations() - */ - public function DisplayBareRelations(WebPage $oPage, $bEditMode = false) - { - parent::DisplayBareRelations($oPage, false); - if (!$bEditMode) { - $oPage->SetCurrentTab('action_email__preview', Dict::S('ActionEmail:preview_tab'), Dict::S('ActionEmail:preview_tab+')); - $sBody = $this->BuildMessageBody(true); - TwigHelper::RenderIntoPage($oPage, APPROOT.'/', 'templates/datamodel/ActionEmail/email-notification-preview', ['iframe_content' => $sBody]); - } - } - - /** - * @since 3.1.0 - * @inheritDoc - * @see cmdbAbstractObject::DoCheckToWrite() - */ - public function DoCheckToWrite() - { - parent::DoCheckToWrite(); - $oHtmlTemplate = $this->Get('html_template'); - if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) { - if (false === mb_strpos($oHtmlTemplate->GetData(), static::TEMPLATE_BODY_CONTENT)) { - $this->m_aCheckWarnings[] = Dict::Format('ActionEmail:content_placeholder_missing', static::TEMPLATE_BODY_CONTENT, Dict::S('Class:ActionEmail/Attribute:body')); - } - } - } - - /** - * @inheritDoc - * @since 3.2.0 - */ - public static function GetAsynchronousGlobalSetting(): bool - { - return utils::GetConfig()->Get('email_asynchronous'); - } -} +//Actions +require_once(APPROOT.'/core/action/Action.php'); +require_once(APPROOT.'/core/action/ActionNotification.php'); +require_once(APPROOT.'/core/action/ActionEmail.php');