diff --git a/core/action.class.inc.php b/core/action.class.inc.php deleted file mode 100644 index 62f2e8ad57..0000000000 --- a/core/action.class.inc.php +++ /dev/null @@ -1,938 +0,0 @@ - - -use Combodo\iTop\Application\TwigBase\Twig\TwigHelper; -use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; -use Combodo\iTop\Application\WebPage\WebPage; -use Combodo\iTop\Service\Notification\NotificationsRepository; -use Combodo\iTop\Service\Notification\NotificationsService; -use Combodo\iTop\Service\Router\Router; - -/** - * Persistent classes (internal): user defined actions - * - * @copyright Copyright (C) 2010-2024 Combodo SAS - * @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'); - } -} diff --git a/core/asynctask.class.inc.php b/core/asynctask.class.inc.php deleted file mode 100644 index c8a3a417d1..0000000000 --- a/core/asynctask.class.inc.php +++ /dev/null @@ -1,544 +0,0 @@ - -use Combodo\iTop\Service\Notification\Event\EventNotificationNewsroomService; - - -/** - * Persistent classes (internal): user defined actions - * - * @copyright Copyright (C) 2010-2024 Combodo SAS - * @license http://opensource.org/licenses/AGPL-3.0 - */ - - -class ExecAsyncTask implements iBackgroundProcess -{ - public function GetPeriodicity() - { - return 2; // seconds - } - - public function Process($iTimeLimit) - { - $sNow = date(AttributeDateTime::GetSQLFormat()); - // Criteria: planned, and expected to occur... ASAP or in the past - $sOQL = "SELECT AsyncTask WHERE (status = 'planned') AND (ISNULL(planned) OR (planned < '$sNow'))"; - $iProcessed = 0; - while (time() < $iTimeLimit) - { - // Next one ? - $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */); - $oTask = $oSet->Fetch(); - if (is_null($oTask)) - { - // Nothing to be done - break; - } - $iProcessed++; - if ($oTask->Process()) - { - $oTask->DBDelete(); - } - } - return "processed $iProcessed tasks"; - } -} - -/** - * A - * - * @package iTopORM - */ -abstract class AsyncTask extends DBObject -{ - /** - * @throws \CoreException - * @throws \Exception - */ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => array('created'), - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_async_task", - "db_key_field" => "id", - "db_finalclass_field" => "realclass", - ); - MetaModel::Init_Params($aParams); - - // Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition - // The value is set from null to planned in the setup program - MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('planned,running,idle,error'), "sql"=>"status", "default_value"=>"planned", "is_null_allowed"=>true, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"NOW()", "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values"=>null, "sql"=>"started", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values"=>null, "sql"=>"planned", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass"=>"Event", "jointype"=> "", "allowed_values"=>null, "sql"=>"event_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); - - MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values"=>null, "sql"=>"remaining_retries", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("last_error_code", array("allowed_values"=>null, "sql"=>"last_error_code", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("last_attempt", array("allowed_values"=>null, "sql"=>"last_attempt", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); - } - - /** - * Every is fine - */ - const OK = 0; - /** - * The task no longer exists - */ - const DELETED = 1; - /** - * The task is already being executed - */ - const ALREADY_RUNNING = 2; - - /** - * The current process requests the ownership on the task. - * In case the task can be accessed concurrently, this function can be overloaded to add a critical section. - * The function must not block the caller if another process is already owning the task - * - * @return integer A code among OK/DELETED/ALREADY_RUNNING. - */ - public function MarkAsRunning() - { - try - { - if ($this->Get('status') == 'running') - { - return self::ALREADY_RUNNING; - } - else - { - $this->Set('status', 'running'); - $this->Set('started', time()); - $this->DBUpdate(); - return self::OK; - } - } - catch(Exception $e) - { - // Corrupted task !! (for example: "Failed to reload object") - IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - fatal error, deleting the task.'); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', 'Failed, corrupted data: '.$e->getMessage()); - $oEventLog->DBUpdate(); - } - $this->DBDelete(); - return self::DELETED; - } - } - - public function GetRetryDelay($iErrorCode = null) - { - $iRetryDelay = 600; - $aRetries = MetaModel::GetConfig()->Get('async_task_retries'); - if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) - { - $aConfig = $aRetries[get_class($this)]; - $iRetryDelay = $aConfig['retry_delay'] ?? $iRetryDelay; - } - return $iRetryDelay; - } - - public function GetMaxRetries($iErrorCode = null) - { - $iMaxRetries = 0; - $aRetries = MetaModel::GetConfig()->Get('async_task_retries'); - if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) - { - $aConfig = $aRetries[get_class($this)]; - $iMaxRetries = $aConfig['max_retries'] ?? $iMaxRetries; - } - return $iMaxRetries; - } - - public function IsRetryDelayExponential() - { - $bExponential = false; - $aRetries = MetaModel::GetConfig()->Get('async_task_retries'); - if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries)) - { - $aConfig = $aRetries[get_class($this)]; - $bExponential = (bool) ($aConfig['exponential_delay'] ?? $bExponential); - } - return $bExponential; - } - - public static function CheckRetryConfig(Config $oConfig, $sAsyncTaskClass) - { - $aMessages = []; - $aRetries = $oConfig->Get('async_task_retries'); - if (is_array($aRetries) && array_key_exists($sAsyncTaskClass, $aRetries)) - { - $aValidKeys = array("retry_delay", "max_retries", "exponential_delay"); - $aConfig = $aRetries[$sAsyncTaskClass]; - if (!is_array($aConfig)) - { - $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_Keys', $sAsyncTaskClass, implode(', ', $aValidKeys)); - } - else - { - foreach($aConfig as $key => $value) - { - if (!in_array($key, $aValidKeys)) - { - $aMessages[] = Dict::Format('Class:AsyncTask:InvalidConfig_Class_InvalidKey_Keys', $sAsyncTaskClass, $key, implode(', ', $aValidKeys)); - } - } - } - } - return $aMessages; - } - - /** - * Compute the delay to wait for the "next retry", based on the given parameters - * @param bool $bIsExponential - * @param int $iRetryDelay - * @param int $iMaxRetries - * @param int $iRemainingRetries - * @return int - */ - public static function GetNextRetryDelay($bIsExponential, $iRetryDelay, $iMaxRetries, $iRemainingRetries) - { - if ($bIsExponential) - { - $iExponent = $iMaxRetries - $iRemainingRetries; - if ($iExponent < 0) $iExponent = 0; // Safety net in case on configuration change in the middle of retries - return $iRetryDelay * (2 ** $iExponent); - } - else - { - return $iRetryDelay; - } - } - - /** - * Override to notify people that a task cannot be performed - */ - protected function OnDefinitiveFailure() - { - } - - protected function OnInsert() - { - $this->Set('created', time()); - } - - /** - * @return boolean True if the task record can be deleted - */ - public function Process() - { - // By default: consider that the task is not completed - $bRet = false; - - // Attempt to take the ownership - $iStatus = $this->MarkAsRunning(); - if ($iStatus == self::OK) - { - try - { - $sStatus = $this->DoProcess(); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', $sStatus); - $oEventLog->DBUpdate(); - } - $bRet = true; - } catch (Exception $e) - { - $this->HandleError($e->getMessage(), $e->getCode()); - } - } - else - { - // Already done or being handled by another process... skip... - $bRet = false; - } - - return $bRet; - } - - /** - * Overridable to extend the behavior in case of error (logging) - */ - protected function HandleError($sErrorMessage, $iErrorCode) - { - if ($this->Get('last_attempt') == '') - { - // First attempt - $this->Set('remaining_retries', $this->GetMaxRetries($iErrorCode)); - } - - $this->SetTrim('last_error', $sErrorMessage); - $this->Set('last_error_code', $iErrorCode); // Note: can be ZERO !!! - $this->Set('last_attempt', time()); - - $iRemaining = $this->Get('remaining_retries'); - if ($iRemaining > 0) - { - $iRetryDelay = $this->GetRetryDelay($iErrorCode); - $iNextRetryDelay = static::GetNextRetryDelay($this->IsRetryDelayExponential(), $iRetryDelay, $this->GetMaxRetries($iErrorCode), $iRemaining); - IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iNextRetryDelay.'s'); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s"); - try - { - $oEventLog->DBUpdate(); - } - catch (Exception $e) - { - $oEventLog->Set('message', "Failed to process async task. Remaining retries: $iRemaining. Next retry in {$iNextRetryDelay}s, more details in the log"); - $oEventLog->DBUpdate(); - } - } - $this->Set('remaining_retries', $iRemaining - 1); - $this->Set('status', 'planned'); - $this->Set('started', null); - $this->Set('planned', time() + $iNextRetryDelay); - } - else - { - IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage); - if ($this->Get('event_id') != 0) - { - $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id')); - $oEventLog->Set('message', "$sErrorMessage\nFailed to process async task."); - try - { - $oEventLog->DBUpdate(); - } - catch (Exception $e) - { - $oEventLog->Set('message', 'Failed to process async task, more details in the log'); - $oEventLog->DBUpdate(); - } - } - $this->Set('status', 'error'); - $this->Set('started', null); - $this->Set('planned', null); - $this->OnDefinitiveFailure(); - } - $this->DBUpdate(); - } - - /** - * Throws an exception (message and code) - * - * @return string - */ - abstract public function DoProcess(); - - /** - * Describes the error codes that DoProcess can return by the mean of exceptions - */ - static public function EnumErrorCodes() - { - return array(); - } -} - -/** - * An email notification - * - * @package iTopORM - */ -class AsyncSendEmail extends AsyncTask -{ - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "created", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_async_send_email", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>Email::ORIGINAL_FORMAT, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - - // Display lists -// MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details -// MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list - // Search criteria -// MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form -// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form - } - - static public function AddToQueue(EMail $oEMail, $oLog) - { - $oNew = MetaModel::NewObject(__class__); - if ($oLog) - { - $oNew->Set('event_id', $oLog->GetKey()); - } - $oNew->Set('to', $oEMail->GetRecipientTO(true /* string */)); - $oNew->Set('subject', $oEMail->GetSubject()); - - $oNew->Set('version', 2); - $sMessage = $oEMail->SerializeV2(); - $oNew->Set('message', $sMessage); - $oNew->DBInsert(); - } - - /** - * @inheritDoc - * @throws \ArchivedObjectException - * @throws \CoreException - */ - public function DoProcess() - { - $sMessage = $this->Get('message'); - $iVersion = (int) $this->Get('version'); - switch($iVersion) - { - case Email::FORMAT_V2: - $oEMail = Email::UnSerializeV2($sMessage); - break; - - case Email::ORIGINAL_FORMAT: - $oEMail = unserialize($sMessage); - break; - - default: - return 'Unknown version of the serialization format: '.$iVersion; - } - $iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */); - switch ($iRes) - { - case EMAIL_SEND_OK: - return "Sent"; - - case EMAIL_SEND_PENDING: - return "Bug - the email should be sent in synchronous mode"; - - case EMAIL_SEND_ERROR: - if (is_array($aIssues)) { - $sMessage = "Sending eMail failed: ".implode(', ', $aIssues); - } else { - $sMessage = "Sending eMail failed."; - } - throw new Exception($sMessage); - } - return ''; - } -} - -/** - * An async notification to be sent to iTop users through the newsroom - * @since 3.2.0 - */ -class AsyncSendNewsroom extends AsyncTask { - - public static function Init() - { - $aParams = array - ( - "category" => "core/cmdb", - "key_type" => "autoincrement", - "name_attcode" => "created", - "state_attcode" => "", - "reconc_keys" => array(), - "db_table" => "priv_async_send_newsroom", - "db_key_field" => "id", - "db_finalclass_field" => "", - ); - MetaModel::Init_Params($aParams); - MetaModel::Init_InheritAttributes(); - - MetaModel::Init_AddAttribute(new AttributeText("recipients", array("allowed_values"=>null, "sql"=>"recipients", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("action_id", array("targetclass"=>"Action", "allowed_values"=>null, "sql"=>"action_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeExternalKey("trigger_id", array("targetclass"=>"Trigger", "allowed_values"=>null, "sql"=>"trigger_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("title", array("allowed_values"=>null, "sql"=>"title", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeInteger("object_id", array("allowed_values"=>null, "sql"=>"object_id", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeString("object_class", array("allowed_values"=>null, "sql"=>"object_class", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeText("url", array("allowed_values"=>null, "sql"=>"url", "default_value"=>null, "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeDateTime("date", array("allowed_values"=>null, "sql"=>"date", "default_value"=>'NOW()', "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array()))); - - } - - /** - * @throws \ArchivedObjectException - * @throws \CoreCannotSaveObjectException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \CoreWarning - * @throws \MySQLException - * @throws \OQLException - */ - public static function AddToQueue(int $iActionId, int $iTriggerId, array $aRecipients, string $sMessage, string $sTitle, string $sUrl, int $iObjectId, ?string $sObjectClass): void - { - $oNew = new static(); - $oNew->Set('action_id', $iActionId); - $oNew->Set('trigger_id', $iTriggerId); - $oNew->Set('recipients', json_encode($aRecipients)); - $oNew->Set('message', $sMessage); - $oNew->Set('title', $sTitle); - $oNew->Set('url', $sUrl); - $oNew->Set('object_id', $iObjectId); - $oNew->Set('object_class', $sObjectClass); - $oNew->SetCurrentDate('date'); - - $oNew->DBInsert(); - } - - /** - * @inheritDoc - */ - public function DoProcess() - { - $oAction = MetaModel::GetObject('Action', $this->Get('action_id')); - $iTriggerId = $this->Get('trigger_id'); - $aRecipients = json_decode($this->Get('recipients')); - $sMessage = $this->Get('message'); - $sTitle = $this->Get('title'); - $sUrl = $this->Get('url'); - $iObjectId = $this->Get('object_id'); - $sObjectClass = $this->Get('object_class'); - $sDate = $this->Get('date'); - - foreach ($aRecipients as $iRecipientId) - { - $oEvent = EventNotificationNewsroomService::MakeEventFromAction($oAction, $iRecipientId, $iTriggerId, $sMessage, $sTitle, $sUrl, $iObjectId, $sObjectClass, $sDate); - $oEvent->DBInsertNoReload(); - } - - return "Sent"; - } -} \ No newline at end of file diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 796f3fe16f..3c5182502a 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -16,13820 +16,84 @@ use Combodo\iTop\Service\Links\LinkSetModel; require_once('MyHelpers.class.inc.php'); -require_once('ormdocument.class.inc.php'); -require_once('ormstopwatch.class.inc.php'); -require_once('ormpassword.class.inc.php'); -require_once('ormcaselog.class.inc.php'); -require_once('ormlinkset.class.inc.php'); -require_once('ormset.class.inc.php'); -require_once('ormtagset.class.inc.php'); require_once('htmlsanitizer.class.inc.php'); require_once('customfieldshandler.class.inc.php'); -require_once('ormcustomfieldsvalue.class.inc.php'); require_once('datetimeformat.class.inc.php'); -/** - * MissingColumnException - sent if an attribute is being created but the column is missing in the row - * - * @package iTopORM - */ -class MissingColumnException extends Exception -{ -} - -/** - * add some description here... - * - * @package iTopORM - */ -define('EXTKEY_RELATIVE', 1); - -/** - * add some description here... - * - * @package iTopORM - */ -define('EXTKEY_ABSOLUTE', 2); - -/** - * Propagation of the deletion through an external key - ask the user to delete the referencing object - * - * @package iTopORM - */ -define('DEL_MANUAL', 1); - -/** - * Propagation of the deletion through an external key - remove linked objects if ext key has is_null_allowed=false - * - * @package iTopORM - */ -define('DEL_AUTO', 2); -/** - * Fully silent delete... not yet implemented - */ -define('DEL_SILENT', 2); -/** - * For HierarchicalKeys only: move all the children up one level automatically - */ -define('DEL_MOVEUP', 3); - -/** - * Do nothing at least automatically - */ -define('DEL_NONE', 4); - - -/** - * For Link sets: tracking_level - * - * @package iTopORM - */ -define('ATTRIBUTE_TRACKING_NONE', 0); // Do not track changes of the attribute -define('ATTRIBUTE_TRACKING_ALL', 3); // Do track all changes of the attribute -define('LINKSET_TRACKING_NONE', 0); // Do not track changes in the link set -define('LINKSET_TRACKING_LIST', 1); // Do track added/removed items -define('LINKSET_TRACKING_DETAILS', 2); // Do track modified items -define('LINKSET_TRACKING_ALL', 3); // Do track added/removed/modified items - -define('LINKSET_EDITMODE_NONE', 0); // The linkset cannot be edited at all from inside this object -define('LINKSET_EDITMODE_ADDONLY', 1); // The only possible action is to open a new window to create a new object -define('LINKSET_EDITMODE_ACTIONS', 2); // Show the usual 'Actions' popup menu -define('LINKSET_EDITMODE_INPLACE', 3); // The "linked" objects can be created/modified/deleted in place -define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/removed in place - -define('LINKSET_EDITWHEN_NEVER', 0); // The linkset cannot be edited at all from inside this object -define('LINKSET_EDITWHEN_ON_HOST_EDITION', 1); // The only possible action is to open a new window to create a new object -define('LINKSET_EDITWHEN_ON_HOST_DISPLAY', 2); // Show the usual 'Actions' popup menu -define('LINKSET_EDITWHEN_ALWAYS', 3); // Show the usual 'Actions' popup menu - - -define('LINKSET_DISPLAY_STYLE_PROPERTY', 'property'); -define('LINKSET_DISPLAY_STYLE_TAB', 'tab'); - -/** - * Attributes implementing this interface won't be accepted as `group by` field - * - * @since 2.7.4 N°3473 - */ -interface iAttributeNoGroupBy -{ - //no method, just a contract on implement -} - -/** - * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) - * - * @package iTopORM - */ -abstract class AttributeDefinition -{ - const SEARCH_WIDGET_TYPE_RAW = 'raw'; - const SEARCH_WIDGET_TYPE_STRING = 'string'; - const SEARCH_WIDGET_TYPE_NUMERIC = 'numeric'; - const SEARCH_WIDGET_TYPE_ENUM = 'enum'; - const SEARCH_WIDGET_TYPE_EXTERNAL_KEY = 'external_key'; - const SEARCH_WIDGET_TYPE_HIERARCHICAL_KEY = 'hierarchical_key'; - const SEARCH_WIDGET_TYPE_EXTERNAL_FIELD = 'external_field'; - const SEARCH_WIDGET_TYPE_DATE_TIME = 'date_time'; - const SEARCH_WIDGET_TYPE_DATE = 'date'; - const SEARCH_WIDGET_TYPE_SET = 'set'; - const SEARCH_WIDGET_TYPE_TAG_SET = 'tag_set'; - - - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - const INDEX_LENGTH = 95; - - protected $aCSSClasses; - - public function GetType() - { - return Dict::S('Core:'.get_class($this)); - } - - public function GetTypeDesc() - { - return Dict::S('Core:'.get_class($this).'+'); - } - - abstract public function GetEditClass(); - - /** - * @return array Css classes - * @since 3.1.0 N°3190 - */ - public function GetCssClasses(): array - { - return $this->aCSSClasses; - } - - /** - * Return the search widget type corresponding to this attribute - * - * @return string - */ - public function GetSearchType() - { - return static::SEARCH_WIDGET_TYPE; - } - - /** - * @return bool - */ - public function IsSearchable() - { - return $this->GetSearchType() != static::SEARCH_WIDGET_TYPE_RAW; - } - - /** @var string */ - protected $m_sCode; - /** @var array */ - protected $m_aParams; - /** @var string */ - protected $m_sHostClass = '!undefined!'; - - public function Get($sParamName) - { - return $this->m_aParams[$sParamName]; - } - - public function GetIndexLength() - { - $iMaxLength = $this->GetMaxSize(); - if (is_null($iMaxLength)) - { - return null; - } - if ($iMaxLength > static::INDEX_LENGTH) - { - return static::INDEX_LENGTH; - } - - return $iMaxLength; - } - - public function IsParam($sParamName) - { - return (array_key_exists($sParamName, $this->m_aParams)); - } - - protected function GetOptional($sParamName, $default) - { - if (array_key_exists($sParamName, $this->m_aParams)) - { - return $this->m_aParams[$sParamName]; - } - else - { - return $default; - } - } - - /** - * AttributeDefinition constructor. - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - */ - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $this->m_aParams = $aParams; - $this->ConsistencyCheck(); - $this->aCSSClasses = array('attribute'); - } - - public function GetParams() - { - return $this->m_aParams; - } - - public function HasParam($sParam) - { - return array_key_exists($sParam, $this->m_aParams); - } - - public function SetHostClass($sHostClass) - { - $this->m_sHostClass = $sHostClass; - } - - public function GetHostClass() - { - return $this->m_sHostClass; - } - - /** - * @return array - * - * @throws \CoreException - */ - public function ListSubItems() - { - $aSubItems = array(); - foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef) - { - if ($oAttDef instanceof AttributeSubItem) - { - if ($oAttDef->Get('target_attcode') == $this->m_sCode) - { - $aSubItems[$sAttCode] = $oAttDef; - } - } - } - - return $aSubItems; - } - - // Note: I could factorize this code with the parameter management made for the AttributeDef class - // to be overloaded - public static function ListExpectedParams() - { - return array(); - } - - /** - * @throws \Exception - */ - protected function ConsistencyCheck() - { - // Check that any mandatory param has been specified - // - $aExpectedParams = static::ListExpectedParams(); - foreach($aExpectedParams as $sParamName) - { - if (!array_key_exists($sParamName, $this->m_aParams)) - { - $aBacktrace = debug_backtrace(); - $sTargetClass = $aBacktrace[2]["class"]; - $sCodeInfo = $aBacktrace[1]["file"]." - ".$aBacktrace[1]["line"]; - throw new Exception("ERROR missing parameter '$sParamName' in ".get_class($this)." declaration for class $sTargetClass ($sCodeInfo)"); - } - } - } - - /** - * Check the validity of the given value - * - * @param \DBObject $oHostObject - * @param $value Object error if any, null otherwise - * - * @return bool|string true for no errors, false or error message otherwise - */ - public function CheckValue(DBObject $oHostObject, $value) - { - // later: factorize here the cases implemented into DBObject - return true; - } - - // table, key field, name field - - /** - * @return string - * @deprecated never used - */ - public function ListDBJoins() - { - DeprecatedCallsLog::NotifyDeprecatedPhpMethod(); - - return ""; - // e.g: return array("Site", "infrid", "name"); - } - - public function GetFinalAttDef() - { - return $this; - } - - /** - * Deprecated - use IsBasedOnDBColumns instead - * - * @return bool - */ - public function IsDirectField() - { - return static::IsBasedOnDBColumns(); - } - - /** - * Returns true if the attribute value is built after DB columns - * - * @return bool - */ - public static function IsBasedOnDBColumns() - { - return false; - } - - /** - * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via - * GetOQLExpression) - * - * @return bool - */ - public static function IsBasedOnOQLExpression() - { - return false; - } - - /** - * Returns true if the attribute value can be shown as a string - * - * @return bool - */ - public static function IsScalar() - { - return false; - } - - /** - * Returns true if the attribute can be used in bulk modify. - * - * @return bool - * @since 3.1.0 N°3190 - * - */ - public static function IsBulkModifyCompatible(): bool - { - return static::IsScalar(); - } - - /** - * Returns true if the attribute value is a set of related objects (1-N or N-N) - * - * @return bool - */ - public static function IsLinkSet() - { - return false; - } - - /** - * @param int $iType - * - * @return bool true if the attribute is an external key, either directly (RELATIVE to the host class), or - * indirectly (ABSOLUTELY) - */ - public function IsExternalKey($iType = EXTKEY_RELATIVE) - { - return false; - } - - /** - * @return bool true if the attribute value is an external key, pointing to the host class - */ - public static function IsHierarchicalKey() - { - return false; - } - - /** - * @return bool true if the attribute value is stored on an object pointed to be an external key - */ - public static function IsExternalField() - { - return false; - } - - /** - * @return bool true if the attribute can be written (by essence : metamodel field option) - * @see \DBObject::IsAttributeReadOnlyForCurrentState() for a specific object instance (depending on its workflow) - */ - public function IsWritable() - { - return false; - } - - /** - * @return bool true if the attribute has been added automatically by the framework - */ - public function IsMagic() - { - return $this->GetOptional('magic', false); - } - - /** - * @return bool true if the attribute value is kept in the loaded object (in memory) - */ - public static function LoadInObject() - { - return true; - } - - /** - * @return bool true if the attribute value comes from the database in one way or another - */ - public static function LoadFromClassTables() - { - return true; - } - - /** - * Write attribute values outside the current class tables - * - * @param \DBObject $oHostObject - * - * @return void - * @since 3.1.0 Method creation, to offer a generic method for all attributes - before we were calling directly \AttributeCustomFields::WriteValue - * - * @used-by \DBObject::WriteExternalAttributes() - */ - public function WriteExternalValues(DBObject $oHostObject): void - { - } - - /** - * Read the data from where it has been stored (outside the current class tables). - * This verb must be implemented as soon as LoadFromClassTables returns false and LoadInObject returns true - * - * @param DBObject $oHostObject - * - * @return mixed|null - * @since 3.1.0 - */ - public function ReadExternalValues(DBObject $oHostObject) - { - return null; - } - - /** - * Cleanup data upon object deletion (outside the current class tables) - * object id still available here - * - * @param \DBObject $oHostObject - * - * @since 3.1.0 - */ - public function DeleteExternalValues(DBObject $oHostObject): void - { - } - - /** - * @return bool true if the attribute should be loaded anytime (in addition to the column selected by the user) - */ - public function AlwaysLoadInTables() - { - return $this->GetOptional('always_load_in_tables', false); - } - - /** - * @param \DBObject $oHostObject - * - * @return mixed Must return the value if LoadInObject returns false - */ - public function GetValue($oHostObject) - { - return null; - } - - /** - * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull()) - * - * @return bool - */ - public function IsNullAllowed() - { - return true; - } - - /** - * Returns the attribute code (identifies the attribute in the host class) - * - * @return string - */ - public function GetCode() - { - return $this->m_sCode; - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * - * @return null | AttributeDefinition - */ - public function GetMirrorLinkAttribute() - { - return null; - } - - /** - * Helper to browse the hierarchy of classes, searching for a label - * - * @param string $sDictEntrySuffix - * @param string $sDefault - * @param bool $bUserLanguageOnly - * - * @return string - * @throws \Exception - */ - protected function SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly) - { - $sLabel = Dict::S('Class:'.$this->m_sHostClass.$sDictEntrySuffix, '', $bUserLanguageOnly); - if (strlen($sLabel) == 0) - { - // Nothing found: go higher in the hierarchy (if possible) - // - $sLabel = $sDefault; - $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); - if ($sParentClass) - { - if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) - { - $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); - $sLabel = $oAttDef->SearchLabel($sDictEntrySuffix, $sDefault, $bUserLanguageOnly); - } - } - } - - return $sLabel; - } - - /** - * @param string|null $sDefault if null, will return the attribute code replacing "_" by " " - * - * @return string - * - * @throws \Exception - */ - public function GetLabel($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = str_replace('_', ' ', $this->m_sCode); - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode, $sDefault, false); - } - - return $sLabel; - } - - /** - * To be overloaded for localized enums - * - * @param string $sValue - * - * @return string label corresponding to the given value (in plain text) - */ - public function GetValueLabel($sValue) - { - return $sValue; - } - - /** - * Get the value from a given string (plain text, CSV import) - * - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return mixed null if no match could be found - */ - public function MakeValueFromString( - $sProposedValue, - $bLocalizedValue = false, - $sSepItem = null, - $sSepAttribute = null, - $sSepValue = null, - $sAttributeQualifier = null - ) { - return $this->MakeRealValue($sProposedValue, null); - } - - /** - * Parses a search string coming from user input - * - * @param string $sSearchString - * - * @return string - */ - public function ParseSearchString($sSearchString) - { - return $sSearchString; - } - - /** - * @return string - * - * @throws \Exception - */ - public function GetLabel_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('label', $this->m_aParams)) - { - return $this->m_aParams['label']; - } - else - { - return $this->GetLabel(); - } - } - - /** - * @param string|null $sDefault - * - * @return string - * - * @throws \Exception - */ - public function GetDescription($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = ''; - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'+', $sDefault, false); - } - - return $sLabel; - } - - /** - * @return bool True if the attribute has a description {@see \AttributeDefinition::GetDescription()} - * @throws \Exception - * @since 3.1.0 - */ - public function HasDescription(): bool - { - return utils::IsNotNullOrEmptyString($this->GetDescription()); - } - - /** - * @param string|null $sDefault - * - * @return string - * - * @throws \Exception - */ - public function GetHelpOnEdition($sDefault = null) - { - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', null, true /*user lang*/); - if (is_null($sLabel)) - { - // If no default value is specified, let's define the most relevant one for developping purposes - if (is_null($sDefault)) - { - $sDefault = ''; - } - // Browse the hierarchy again, accepting default (english) translations - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'?', $sDefault, false); - } - - return $sLabel; - } - - public function GetHelpOnSmartSearch() - { - $aParents = array_merge(array(get_class($this) => get_class($this)), class_parents($this)); - foreach($aParents as $sClass) - { - $sHelp = Dict::S("Core:$sClass?SmartSearch", '-missing-'); - if ($sHelp != '-missing-') - { - return $sHelp; - } - } - - return ''; - } - - /** - * @return string - * - * @throws \Exception - */ - public function GetDescription_Obsolete() - { - // Written for compatibility with a data model written prior to version 0.9.1 - if (array_key_exists('description', $this->m_aParams)) - { - return $this->m_aParams['description']; - } - else - { - return $this->GetDescription(); - } - } - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', ATTRIBUTE_TRACKING_ALL); - } - - /** - * @return \ValueSetObjects - */ - public function GetValuesDef() - { - return null; - } - - public function GetPrerequisiteAttributes($sClass = null) - { - return array(); - } - - public function GetNullValue() - { - return null; - } - - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - /** - * @param mixed $proposedValue - * - * @return bool True if $proposedValue is an actual value set in the attribute, false is the attribute remains "empty" - * @since 3.0.3, 3.1.0 N°5784 - */ - public function HasAValue($proposedValue): bool - { - // Default implementation, we don't really know what type $proposedValue will be - return !(is_null($proposedValue)); - } - - /** - * force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing! - * - * @param mixed $proposedValue - * @param \DBObject $oHostObj - * - * @return mixed - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - return $proposedValue; - } - - public function Equals($val1, $val2) - { - return ($val1 == $val2); - } - - /** - * @param string $sPrefix - * - * @return array suffix/expression pairs (1 in most of the cases), for READING (Select) - */ - public function GetSQLExpressions($sPrefix = '') - { - return array(); - } - - /** - * @param array $aCols - * @param string $sPrefix - * - * @return mixed a value out of suffix/value pairs, for SELECT result interpretation - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - return null; - } - - /** - * @param bool $bFullSpec - * - * @return array column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation) - * @see \CMDBSource::GetFieldSpec() - */ - public function GetSQLColumns($bFullSpec = false) - { - return array(); - } - - /** - * @param $value - * - * @return array column/value pairs (1 in most of the cases), for WRITING (Insert, Update) - */ - public function GetSQLValues($value) - { - return array(); - } - - public function RequiresIndex() - { - return false; - } - - public function RequiresFullTextIndex() - { - return false; - } - - public function CopyOnAllTables() - { - return false; - } - - public function GetOrderBySQLExpressions($sClassAlias) - { - // Note: This is the responsibility of this function to place backticks around column aliases - return array('`'.$sClassAlias.$this->GetCode().'`'); - } - - public function GetOrderByHint() - { - return ''; - } - - // Import - differs slightly from SQL input, but identical in most cases - // - public function GetImportColumns() - { - return $this->GetSQLColumns(); - } - - public function FromImportToValue($aCols, $sPrefix = '') - { - $aValues = array(); - foreach($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr) - { - // This is working, based on the assumption that importable fields - // are not computed fields => the expression is the name of a column - $aValues[$sPrefix.$sAlias] = $aCols[$sExpr]; - } - - return $this->FromSQLToValue($aValues, $sPrefix); - } - - public function GetValidationPattern() - { - return ''; - } - - public function CheckFormat($value) - { - return true; - } - - public function GetMaxSize() - { - return null; - } - - /** - * @return mixed|null - * @deprecated never used - */ - public function MakeValue() - { - DeprecatedCallsLog::NotifyDeprecatedPhpMethod(); - $sComputeFunc = $this->Get("compute_func"); - if (empty($sComputeFunc)) { - return null; - } - - return call_user_func($sComputeFunc); - } - - abstract public function GetDefaultValue(DBObject $oHostObject = null); - - // - // To be overloaded in subclasses - // - - abstract public function GetBasicFilterOperators(); // returns an array of "opCode"=>"description" - - abstract public function GetBasicFilterLooseOperator(); // returns an "opCode" - - //abstract protected GetBasicFilterHTMLInput(); - abstract public function GetBasicFilterSQLExpr($sOpCode, $value); - - public function GetMagicFields() - { - return []; - } - - public function GetEditValue($sValue, $oHostObj = null) - { - return (string)$sValue; - } - - /** - * For fields containing a potential markup, return the value without this markup - * - * @param string $sValue - * @param \DBObject $oHostObj - * - * @return string - */ - public function GetAsPlainText($sValue, $oHostObj = null) - { - return (string)$this->GetEditValue($sValue, $oHostObj); - } - - /** - * Helper to get a value that will be JSON encoded - * - * @see FromJSONToValue for the reverse operation - * - * @param mixed $value field value - * - * @return string|array PHP struct that can be properly encoded - * - */ - public function GetForJSON($value) - { - // In most of the cases, that will be the expected behavior... - return $this->GetEditValue($value); - } - - /** - * Helper to form a value, given JSON decoded data. This way the attribute itself handles the transformation from the JSON structure to the expected data (the one that - * needs to be used in the {@see \DBObject::Set()} method). - * - * Note that for CSV and XML this isn't done yet (no delegation to the attribute but switch/case inside controllers) :/ - * - * @see GetForJSON for the reverse operation - * - * @param string $json JSON encoded value - * - * @return mixed JSON decoded data, depending on the attribute type - * - */ - public function FromJSONToValue($json) - { - // Pass-through in most of the cases - return $json; - } - - /** - * Override to display the value in the GUI - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - return Str::pure2html((string)$sValue); - } - - /** - * Override to export the value in XML - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return mixed - */ - public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) - { - return Str::pure2xml((string)$sValue); - } - - /** - * Override to escape the value when read by DBObject::GetAsCSV() - * - * @param string $sValue - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - */ - public function GetAsCSV( - $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, - $bConvertToPlainText = false - ) { - return (string)$sValue; - } - - /** - * Override to differentiate a value displayed in the UI or in the history - * - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - */ - public function GetAsHTMLForHistory($sValue, $oHostObject = null, $bLocalize = true) - { - return $this->GetAsHTML($sValue, $oHostObject, $bLocalize); - } - - public static function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\StringField'; - } - - /** - * Override to specify Field class - * - * When called first, $oFormField is null and will be created (eg. Make). Then when the ::parent is called and the - * $oFormField is passed, MakeFormField behave more like a Prepare. - * - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\Field|null $oFormField - * - * @return \Combodo\iTop\Form\Field\Field - * @throws \CoreException - * @throws \Exception - * - * @noinspection PhpMissingReturnTypeInspection - * @noinspection PhpMissingParamTypeInspection - * @noinspection ReturnTypeCanBeDeclaredInspection - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - // This is a fallback in case the AttributeDefinition subclass has no overloading of this function. - if ($oFormField === null) { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - //$oFormField->SetReadOnly(true); - } - - $oFormField->SetLabel($this->GetLabel()); - - // Attributes flags - // - Retrieving flags for the current object - if ($oObject->IsNew()) { - $iFlags = $oObject->GetInitialStateAttributeFlags($this->GetCode()); - } else { - $iFlags = $oObject->GetAttributeFlags($this->GetCode()); - } - - // - Comparing flags - if ($this->IsWritable() && (!$this->IsNullAllowed() || (($iFlags & OPT_ATT_MANDATORY) === OPT_ATT_MANDATORY))) { - $oFormField->SetMandatory(true); - } - if ((!$oObject->IsNew() || !$oFormField->GetMandatory()) && (($iFlags & OPT_ATT_READONLY) === OPT_ATT_READONLY)) { - $oFormField->SetReadOnly(true); - } - - // CurrentValue - $oFormField->SetCurrentValue($oObject->Get($this->GetCode())); - - // Validation pattern - if ($this->GetValidationPattern() !== '') { - $oFormField->AddValidator(new CustomRegexpValidator($this->GetValidationPattern())); - } - - // Description - $sAttDescription = $this->GetDescription(); - if (!empty($sAttDescription)) { - $oFormField->SetDescription($this->GetDescription()); - } - - // Metadata - $oFormField->AddMetadata('attribute-code', $this->GetCode()); - $oFormField->AddMetadata('attribute-type', get_class($this)); - $oFormField->AddMetadata('attribute-label', $this->GetLabel()); - // - Attribute flags - $aPossibleAttFlags = MetaModel::EnumPossibleAttributeFlags(); - foreach ($aPossibleAttFlags as $sFlagCode => $iFlagValue) { - // Note: Skip normal flag as we don't need it. - if ($sFlagCode === 'normal') { - continue; - } - $sFormattedFlagCode = str_ireplace('_', '-', $sFlagCode); - $sFormattedFlagValue = (($iFlags & $iFlagValue) === $iFlagValue) ? 'true' : 'false'; - $oFormField->AddMetadata('attribute-flag-'.$sFormattedFlagCode, $sFormattedFlagValue); - } - // - Value raw - if ($this::IsScalar()) { - $oFormField->AddMetadata('value-raw', (string)$oObject->Get($this->GetCode())); - } - - // We don't want to invalidate field because of old untouched values that are no longer valid - $aModifiedAttCodes = $oObject->ListChanges(); - $bAttributeHasBeenModified = array_key_exists($this->GetCode(), $aModifiedAttCodes); - if (false === $bAttributeHasBeenModified) { - $oFormField->SetValidationDisabled(true); - } - - return $oFormField; - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Plain text (unlocalized) representation', - 'html' => 'HTML representation', - 'label' => 'Localized representation', - 'text' => 'Plain text representation (without any markup)', - ); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param mixed $value The current value of the field - * @param string $sVerb The verb specifying the representation of the value - * @param \DBObject $oHostObject - * @param bool $bLocalize Whether or not to localize the value - * - * @return mixed|null|string - * - * @throws \Exception - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - if ($this->IsScalar()) - { - switch ($sVerb) - { - case '': - return $value; - - case 'html': - return $this->GetAsHtml($value, $oHostObject, $bLocalize); - - case 'label': - return $this->GetEditValue($value); - - case 'text': - return $this->GetAsPlainText($value); - break; - - default: - throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); - } - } - - return null; - } - - /** - * @param array $aArgs - * @param string $sContains - * - * @return array|null - * @throws \CoreException - * @throws \OQLException - */ - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oValSetDef = $this->GetValuesDef(); - if (!$oValSetDef) - { - return null; - } - - return $oValSetDef->GetValues($aArgs, $sContains); - } - - /** - * GetAllowedValuesForSelect is the same as GetAllowedValues except for field with obsolescence flag - * @param array $aArgs - * @param string $sContains - * - * @return array|null - * @throws \CoreException - * @throws \OQLException - */ - public function GetAllowedValuesForSelect($aArgs = array(), $sContains = '') - { - return $this->GetAllowedValues($aArgs, $sContains); - } - - /** - * Explain the change of the attribute (history) - * - * @param string $sOldValue - * @param string $sNewValue - * @param string $sLabel - * - * @return string - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \OQLException - * @throws \Exception - */ - public function DescribeChangeAsHTML($sOldValue, $sNewValue, $sLabel = null) - { - if (is_null($sLabel)) - { - $sLabel = $this->GetLabel(); - } - - $sNewValueHtml = $this->GetAsHTMLForHistory($sNewValue); - $sOldValueHtml = $this->GetAsHTMLForHistory($sOldValue); - - if ($this->IsExternalKey()) - { - /** @var \AttributeExternalKey $this */ - $sTargetClass = $this->GetTargetClass(); - $sOldValueHtml = (int)$sOldValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sOldValue) : null; - $sNewValueHtml = (int)$sNewValue ? MetaModel::GetHyperLink($sTargetClass, (int)$sNewValue) : null; - } - if ((($this->GetType() == 'String') || ($this->GetType() == 'Text')) && - (strlen($sNewValue) > strlen($sOldValue))) - { - // Check if some text was not appended to the field - if (substr($sNewValue, 0, strlen($sOldValue)) == $sOldValue) // Text added at the end - { - $sDelta = $this->GetAsHTML(substr($sNewValue, strlen($sOldValue))); - $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); - } - else - { - if (substr($sNewValue, -strlen($sOldValue)) == $sOldValue) // Text added at the beginning - { - $sDelta = $this->GetAsHTML(substr($sNewValue, 0, strlen($sNewValue) - strlen($sOldValue))); - $sResult = Dict::Format('Change:Text_AppendedTo_AttName', $sDelta, $sLabel); - } - else - { - if (strlen($sOldValue) == 0) - { - $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); - } - else - { - if (is_null($sNewValue)) - { - $sNewValueHtml = Dict::S('UI:UndefinedObject'); - } - $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, - $sNewValueHtml, $sOldValueHtml); - } - } - } - } - else - { - if (strlen($sOldValue) == 0) - { - $sResult = Dict::Format('Change:AttName_SetTo', $sLabel, $sNewValueHtml); - } - else - { - if (is_null($sNewValue)) - { - $sNewValueHtml = Dict::S('UI:UndefinedObject'); - } - $sResult = Dict::Format('Change:AttName_SetTo_NewValue_PreviousValue_OldValue', $sLabel, $sNewValueHtml, - $sOldValueHtml); - } - } - - return $sResult; - } - - /** - * @param \DBObject $oObject - * @param mixed $original - * @param mixed $value - * - * @throws \ArchivedObjectException - * @throws \CoreCannotSaveObjectException - * @throws \CoreException if cannot create object - * @throws \CoreUnexpectedValue - * @throws \CoreWarning - * @throws \MySQLException - * @throws \OQLException - * - * @uses GetChangeRecordAdditionalData - * @uses GetChangeRecordClassName - * - * @since 3.1.0 N°6042 - */ - public function RecordAttChange(DBObject $oObject, $original, $value): void - { - /** @var CMDBChangeOp $oMyChangeOp */ - $oMyChangeOp = MetaModel::NewObject($this->GetChangeRecordClassName()); - $oMyChangeOp->Set("objclass", get_class($oObject)); - $oMyChangeOp->Set("objkey", $oObject->GetKey()); - $oMyChangeOp->Set("attcode", $this->GetCode()); - - $this->GetChangeRecordAdditionalData($oMyChangeOp, $oObject, $original, $value); - - $oMyChangeOp->DBInsertNoReload(); - } - - /** - * Add attribute specific information in the {@link \CMDBChangeOp} instance - * - * @param \CMDBChangeOp $oMyChangeOp - * @param \DBObject $oObject - * @param $original - * @param $value - * - * @return void - * @used-by RecordAttChange - */ - protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void - { - $oMyChangeOp->Set("oldvalue", $original); - $oMyChangeOp->Set("newvalue", $value); - } - - /** - * @return string name of the children of {@link \CMDBChangeOp} class to use for the history record - * @used-by RecordAttChange - */ - protected function GetChangeRecordClassName(): string - { - return CMDBChangeOpSetAttributeScalar::class; - } - - /** - * Parses a string to find some smart search patterns and build the corresponding search/OQL condition - * Each derived class is reponsible for defining and processing their own smart patterns, the base class - * does nothing special, and just calls the default (loose) operator - * - * @param string $sSearchText The search string to analyze for smart patterns - * @param \FieldExpression $oField - * @param array $aParams Values of the query parameters - * - * @return \Expression The search condition to be added (AND) to the current search - * - * @throws \CoreException - */ - public function GetSmartConditionExpression($sSearchText, FieldExpression $oField, &$aParams) - { - $sParamName = $oField->GetParent().'_'.$oField->GetName(); - $oRightExpr = new VariableExpression($sParamName); - $sOperator = $this->GetBasicFilterLooseOperator(); - switch ($sOperator) - { - case 'Contains': - $aParams[$sParamName] = "%$sSearchText%"; - $sSQLOperator = 'LIKE'; - break; - - default: - $sSQLOperator = $sOperator; - $aParams[$sParamName] = $sSearchText; - } - $oNewCondition = new BinaryExpression($oField, $sSQLOperator, $oRightExpr); - - return $oNewCondition; - } - - /** - * Tells if an attribute is part of the unique fingerprint of the object (used for comparing two objects) - * All attributes which value is not based on a value from the object itself (like ExternalFields or LinkedSet) - * must be excluded from the object's signature - * - * @return boolean - */ - public function IsPartOfFingerprint() - { - return true; - } - - /** - * The part of the current attribute in the object's signature, for the supplied value - * - * @param mixed $value The value of this attribute for the object - * - * @return string The "signature" for this field/attribute - */ - public function Fingerprint($value) - { - return (string)$value; - } - - /* - * return string - */ - public function GetRenderForDataTable(string $sClassAlias) :string - { - $sRenderFunction = "return data;"; - return $sRenderFunction; - } -} - -class AttributeDashboard extends AttributeDefinition -{ - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), - array("definition_file", "is_user_editable")); - } - - public function GetDashboard() - { - $sAttCode = $this->GetCode(); - $sClass = MetaModel::GetAttributeOrigin($this->GetHostClass(), $sAttCode); - $sFilePath = APPROOT.'env-'.utils::GetCurrentEnvironment().'/'.$this->Get('definition_file'); - return RuntimeDashboard::GetDashboard($sFilePath, $sClass.'__'.$sAttCode); - } - - public function IsUserEditable() - { - return $this->Get('is_user_editable'); - } - - public function IsWritable() - { - return false; - } - - public function GetEditClass() - { - return ""; - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return null; - } - - public function GetBasicFilterOperators() - { - return array(); - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return ''; - } - - /** - * @inheritdoc - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - return null; - } - - // if this verb returns false, then GetValue must be implemented - public static function LoadInObject() - { - return false; - } - - public function GetValue($oHostObject) - { - return ''; - } - - /** - * @inheritDoc - */ - public function HasAValue($proposedValue): bool - { - // Always return false for now, we don't consider a custom version of a dashboard - return false; - } -} - -/** - * Set of objects directly linked to an object, and being part of its definition - * - * @package iTopORM - */ -class AttributeLinkedSet extends AttributeDefinition -{ - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - $this->aCSSClasses[] = 'attribute-set'; - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), - array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max")); - } - - public function GetEditClass() - { - return "LinkedSet"; - } - - /** @inheritDoc */ - public static function IsBulkModifyCompatible(): bool - { - return false; - } - - public function IsWritable() - { - return true; - } - - public static function IsLinkSet() - { - return true; - } - - public function IsIndirect() - { - return false; - } - - public function GetValuesDef() - { - $oValSetDef = $this->Get("allowed_values"); - if (!$oValSetDef) { - // Let's propose every existing value - $oValSetDef = new ValueSetObjects('SELECT '.LinkSetModel::GetTargetClass($this)); - } - - return $oValSetDef; - } - - public function GetEditValue($value, $oHostObj = null) - { - /** @var ormLinkSet $value * */ - if ($value->Count() === 0) { - return ''; - } - - /** Return linked objects key as string **/ - $aValues = $value->GetValues(); - - return implode(' ', $aValues); - } - - public function GetPrerequisiteAttributes($sClass = null) - { - return $this->Get("depends_on"); - } - - /** - * @param \DBObject|null $oHostObject - * - * @return \ormLinkSet - * - * @throws \Exception - * @throws \CoreException - * @throws \CoreWarning - */ - public function GetDefaultValue(DBObject $oHostObject = null) - { - if ($oHostObject === null) - { - return null; - } - - $sLinkClass = $this->GetLinkedClass(); - $sExtKeyToMe = $this->GetExtKeyToMe(); - - // The class to target is not the current class, because if this is a derived class, - // it may differ from the target class, then things start to become confusing - /** @var \AttributeExternalKey $oRemoteExtKeyAtt */ - $oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe); - $sMyClass = $oRemoteExtKeyAtt->GetTargetClass(); - - $oMyselfSearch = new DBObjectSearch($sMyClass); - if ($oHostObject !== null) - { - $oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '='); - } - - $oLinkSearch = new DBObjectSearch($sLinkClass); - $oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe); - if ($this->IsIndirect()) - { - // Join the remote class so that the archive flag will be taken into account - /** @var \AttributeLinkedSetIndirect $this */ - $sExtKeyToRemote = $this->GetExtKeyToRemote(); - /** @var \AttributeExternalKey $oExtKeyToRemote */ - $oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote); - $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); - if (MetaModel::IsArchivable($sRemoteClass)) - { - $oRemoteSearch = new DBObjectSearch($sRemoteClass); - /** @var \AttributeLinkedSetIndirect $this */ - $oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote()); - } - } - $oLinks = new DBObjectSet($oLinkSearch); - $oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks); - - return $oLinkSet; - } - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default')); - } - - /** - * @return string see LINKSET_EDITMODE_* constants - */ - public function GetEditMode() - { - return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS); - } - - /** - * @return int see LINKSET_EDITWHEN_* constants - * @since 3.1.1 3.2.0 N°6385 - */ - public function GetEditWhen(): int - { - return $this->GetOptional('edit_when', LINKSET_EDITWHEN_ALWAYS); - } - - /** - * @return string see LINKSET_DISPLAY_STYLE_* constants - * @since 3.1.0 N°3190 - */ - public function GetDisplayStyle() - { - $sDisplayStyle = $this->GetOptional('display_style', LINKSET_DISPLAY_STYLE_TAB); - if ($sDisplayStyle === '') { - $sDisplayStyle = LINKSET_DISPLAY_STYLE_TAB; - } - - return $sDisplayStyle; - } - - /** - * Indicates if the current Attribute has constraints (php constraints or datamodel constraints) - * @return bool true if Attribute has constraints - * @since 3.1.0 N°6228 - */ - public function HasPHPConstraint(): bool - { - return $this->GetOptional('with_php_constraint', false); - } - - /** - * @return bool true if Attribute has computation (DB_LINKS_CHANGED event propagation, `with_php_computation` attribute xml property), false otherwise - * @since 3.1.1 3.2.0 N°6228 - */ - public function HasPHPComputation(): bool - { - return $this->GetOptional('with_php_computation', false); - } - - public function GetLinkedClass() - { - return $this->Get('linked_class'); - } - - public function GetExtKeyToMe() - { - return $this->Get('ext_key_to_me'); - } - - public function GetBasicFilterOperators() - { - return array(); - } - - public function GetBasicFilterLooseOperator() - { - return ''; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return ''; - } - - /** @inheritDoc * */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if($this->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB){ - return $this->GetAsHTMLForTab($sValue, $oHostObject, $bLocalize); - } - else{ - return $this->GetAsHTMLForProperty($sValue, $oHostObject, $bLocalize); - } - } - - public function GetAsHTMLForTab($sValue, $oHostObject = null, $bLocalize = true) - { - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $aItems = array(); - while ($oObj = $sValue->Fetch()) - { - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) - { - if ($sAttCode == $this->GetExtKeyToMe()) - { - continue; - } - if ($oAttDef->IsExternalField()) - { - continue; - } - $sAttValue = $oObj->GetAsHTML($sAttCode); - if (strlen($sAttValue) > 0) - { - $aAttributes[] = $sAttValue; - } - } - $sAttributes = implode(', ', $aAttributes); - $aItems[] = $sAttributes; - } - - return implode('
', $aItems); - } - - return null; - } - - public function GetAsHTMLForProperty($sValue, $oHostObject = null, $bLocalize = true): string - { - try { - - /** @var ormLinkSet $sValue */ - if (is_null($sValue) || $sValue->Count() === 0) { - return ''; - } - - $oLinkSetBlock = new BlockLinkSetDisplayAsProperty($this->GetCode(), $this, $sValue); - - return ConsoleBlockRenderer::RenderBlockTemplates($oLinkSetBlock); - } - catch (Exception $e) { - $sMessage = "Error while displaying attribute {$this->GetCode()}"; - IssueLog::Error($sMessage, IssueLog::CHANNEL_DEFAULT, [ - 'host_object_class' => $this->GetHostClass(), - 'host_object_key' => $oHostObject->GetKey(), - 'attribute' => $this->GetCode(), - ]); - - return $sMessage; - } - } - - /** - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string - * - * @throws \CoreException - */ - public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true) - { - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $sRes = "\n"; - while ($oObj = $sValue->Fetch()) - { - $sObjClass = get_class($oObj); - $sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n"; - // Show only relevant information (hide the external key to the current object) - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) - { - continue; - } - if ($oAttDef->IsExternalField()) - { - /** @var \AttributeExternalField $oAttDef */ - if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) - { - continue; - } - /** @var AttributeExternalField $oAttDef */ - if ($oAttDef->IsFriendlyName()) - { - continue; - } - } - if ($oAttDef instanceof AttributeFriendlyName) - { - continue; - } - if (!$oAttDef->IsScalar()) - { - continue; - } - $sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize); - $sRes .= "<$sAttCode>$sAttValue\n"; - } - $sRes .= "\n"; - } - $sRes .= "\n"; - } - else - { - $sRes = ''; - } - - return $sRes; - } - - /** - * @param $sValue - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return mixed|string - * @throws \CoreException - */ - public function GetAsCSV( - $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, - $bConvertToPlainText = false - ) { - $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); - $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); - $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); - $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); - - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $aItems = array(); - while ($oObj = $sValue->Fetch()) - { - $sObjClass = get_class($oObj); - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) - { - continue; - } - if ($oAttDef->IsExternalField()) - { - continue; - } - if (!$oAttDef->IsBasedOnDBColumns()) - { - continue; - } - if (!$oAttDef->IsScalar()) - { - continue; - } - $sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize); - if (strlen($sAttValue) > 0) - { - $sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier, - $sAttCode.$sSepValue.$sAttValue); - $aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier; - } - } - $sAttributes = implode($sSepAttribute, $aAttributes); - $aItems[] = $sAttributes; - } - $sRes = implode($sSepItem, $aItems); - } - else - { - $sRes = ''; - } - $sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes); - $sRes = $sTextQualifier.$sRes.$sTextQualifier; - - return $sRes; - } - - /** - * List the available verbs for 'GetForTemplate' - */ - public function EnumTemplateVerbs() - { - return array( - '' => 'Plain text (unlocalized) representation', - 'html' => 'HTML representation (unordered list)', - ); - } - - /** - * Get various representations of the value, for insertion into a template (e.g. in Notifications) - * - * @param mixed $value The current value of the field - * @param string $sVerb The verb specifying the representation of the value - * @param DBObject $oHostObject The object - * @param bool $bLocalize Whether or not to localize the value - * - * @return string - * @throws \Exception - */ - public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) - { - $sRemoteName = $this->IsIndirect() ? - /** @var \AttributeLinkedSetIndirect $this */ - $this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; - - $oLinkSet = clone $value; // Workaround/Safety net for Trac #887 - $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); - $iCount = 0; - $aNames = array(); - foreach($oLinkSet as $oItem) - { - if (($iLimit > 0) && ($iCount == $iLimit)) - { - $iTotal = $oLinkSet->Count(); - $aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal); - break; - } - $aNames[] = $oItem->Get($sRemoteName); - $iCount++; - } - - switch ($sVerb) - { - case '': - return implode("\n", $aNames); - - case 'html': - return ''; - - default: - throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject)); - } - } - - public function DuplicatesAllowed() - { - return false; - } // No duplicates for 1:n links, never - - public function GetImportColumns() - { - $aColumns = array(); - $aColumns[$this->GetCode()] = 'MEDIUMTEXT'.CMDBSource::GetSqlStringColumnDefinition(); - - return $aColumns; - } - - /** - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return \DBObjectSet|mixed - * @throws \CSVParserException - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \MySQLHasGoneAwayException - * @throws \Exception - */ - public function MakeValueFromString( - $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, - $sAttributeQualifier = null - ) { - if (is_null($sSepItem) || empty($sSepItem)) - { - $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); - } - if (is_null($sSepAttribute) || empty($sSepAttribute)) - { - $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); - } - if (is_null($sSepValue) || empty($sSepValue)) - { - $sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator'); - } - if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) - { - $sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier'); - } - - $sTargetClass = $this->Get('linked_class'); - - $sInput = str_replace($sSepItem, "\n", $sProposedValue); - $oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier); - - $aInput = $oCSVParser->ToArray(0 /* do not skip lines */); - - $aLinks = array(); - foreach($aInput as $aRow) - { - // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value - $aExtKeys = array(); - $aValues = array(); - foreach($aRow as $sCell) - { - $iSepPos = strpos($sCell, $sSepValue); - if ($iSepPos === false) - { - // Houston... - throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell)); - } - - $sAttCode = trim(substr($sCell, 0, $iSepPos)); - $sValue = substr($sCell, $iSepPos + strlen($sSepValue)); - - if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) - { - $sKeyAttCode = $aMatches[1]; - $sRemoteAttCode = $aMatches[2]; - $aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue; - if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', - array('class' => $sTargetClass, 'attcode' => $sKeyAttCode)); - } - /** @var \AttributeExternalKey $oKeyAttDef */ - $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); - $sRemoteClass = $oKeyAttDef->GetTargetClass(); - if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', - array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode)); - } - } - else - { - if (!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) - { - throw new CoreException('Wrong attribute code for link attribute specification', - array('class' => $sTargetClass, 'attcode' => $sAttCode)); - } - $oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode); - $aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem, - $sSepAttribute, $sSepValue, $sAttributeQualifier); - } - } - - // 2nd - Instanciate the object and set the value - if (isset($aValues['finalclass'])) - { - $sLinkClass = $aValues['finalclass']; - if (!is_subclass_of($sLinkClass, $sTargetClass)) - { - throw new CoreException('Wrong class for link attribute specification', - array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); - } - } - elseif (MetaModel::IsAbstract($sTargetClass)) - { - throw new CoreException('Missing finalclass for link attribute specification'); - } - else - { - $sLinkClass = $sTargetClass; - } - - $oLink = MetaModel::NewObject($sLinkClass); - foreach($aValues as $sAttCode => $sValue) - { - $oLink->Set($sAttCode, $sValue); - } - - // 3rd - Set external keys from search conditions - foreach($aExtKeys as $sKeyAttCode => $aReconciliation) - { - $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); - $sKeyClass = $oKeyAttDef->GetTargetClass(); - $oExtKeyFilter = new DBObjectSearch($sKeyClass); - $aReconciliationDesc = array(); - foreach($aReconciliation as $sRemoteAttCode => $sValue) - { - $oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '='); - $aReconciliationDesc[] = "$sRemoteAttCode=$sValue"; - } - $oExtKeySet = new DBObjectSet($oExtKeyFilter); - switch ($oExtKeySet->Count()) - { - case 0: - $sReconciliationDesc = implode(', ', $aReconciliationDesc); - throw new CoreException("Found no match", - array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); - break; - case 1: - $oRemoteObj = $oExtKeySet->Fetch(); - $oLink->Set($sKeyAttCode, $oRemoteObj->GetKey()); - break; - default: - $sReconciliationDesc = implode(', ', $aReconciliationDesc); - throw new CoreException("Found several matches", - array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc)); - // Found several matches, ambiguous - } - } - - // Check (roughly) if such a link is valid - $aErrors = array(); - foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsExternalKey()) - { - /** @var \AttributeExternalKey $oAttDef */ - if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), - $oAttDef->GetTargetClass()))) - { - continue; // Don't check the key to self - } - } - - if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) - { - $aErrors[] = $sAttCode; - } - } - if (count($aErrors) > 0) - { - throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); - } - - $aLinks[] = $oLink; - } - $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); - - return $oSet; - } - - /** - * @inheritDoc - * - * @param \ormLinkSet $value - */ - public function GetForJSON($value) - { - $aRet = array(); - if (is_object($value) && ($value instanceof ormLinkSet)) - { - $value->Rewind(); - while ($oObj = $value->Fetch()) - { - $sObjClass = get_class($oObj); - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) - { - if ($sAttCode == 'finalclass') - { - if ($sObjClass == $this->GetLinkedClass()) - { - // Simplify the output if the exact class could be determined implicitely - continue; - } - } - if ($sAttCode == $this->GetExtKeyToMe()) - { - continue; - } - if ($oAttDef->IsExternalField()) - { - continue; - } - if (!$oAttDef->IsBasedOnDBColumns()) - { - continue; - } - if (!$oAttDef->IsScalar()) - { - continue; - } - $attValue = $oObj->Get($sAttCode); - $aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue); - } - $aRet[] = $aAttributes; - } - } - - return $aRet; - } - - /** - * @inheritDoc - * - * @return \DBObjectSet - * @throws \CoreException - * @throws \CoreUnexpectedValue - * @throws \Exception - */ - public function FromJSONToValue($json) - { - $sTargetClass = $this->Get('linked_class'); - - $aLinks = array(); - foreach($json as $aValues) - { - if (isset($aValues['finalclass'])) - { - $sLinkClass = $aValues['finalclass']; - if (!is_subclass_of($sLinkClass, $sTargetClass)) - { - throw new CoreException('Wrong class for link attribute specification', - array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); - } - } - elseif (MetaModel::IsAbstract($sTargetClass)) - { - throw new CoreException('Missing finalclass for link attribute specification'); - } - else - { - $sLinkClass = $sTargetClass; - } - - $oLink = MetaModel::NewObject($sLinkClass); - foreach($aValues as $sAttCode => $sValue) - { - $oLink->Set($sAttCode, $sValue); - } - - // Check (roughly) if such a link is valid - $aErrors = array(); - foreach(MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) - { - if ($oAttDef->IsExternalKey()) - { - /** @var AttributeExternalKey $oAttDef */ - if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(), - $oAttDef->GetTargetClass()))) - { - continue; // Don't check the key to self - } - } - - if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) - { - $aErrors[] = $sAttCode; - } - } - if (count($aErrors) > 0) - { - throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors)); - } - - $aLinks[] = $oLink; - } - $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); - - return $oSet; - } - - /** - * @param $proposedValue - * @param $oHostObj - * - * @return mixed - * @throws \Exception - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - if ($proposedValue === null) - { - $sLinkedClass = $this->GetLinkedClass(); - $aLinkedObjectsArray = array(); - $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray); - - return new ormLinkSet( - get_class($oHostObj), - $this->GetCode(), - $oSet - ); - } - - return $proposedValue; - } - - /** - * @param ormLinkSet $val1 - * @param ormLinkSet $val2 - * - * @return bool - */ - public function Equals($val1, $val2) - { - if ($val1 === $val2) - { - $bAreEquivalent = true; - } - else - { - $bAreEquivalent = ($val2->HasDelta() === false); - } - - return $bAreEquivalent; - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * - * @return null | AttributeDefinition - * @throws \Exception - */ - public function GetMirrorLinkAttribute() - { - $oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe()); - - return $oRemoteAtt; - } - - public static function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\LinkedSetField $oFormField - * - * @return \Combodo\iTop\Form\Field\LinkedSetField - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \Exception - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - // Setting target class - if (!$this->IsIndirect()) { - $sTargetClass = $this->GetLinkedClass(); - } else { - /** @var \AttributeExternalKey $oRemoteAttDef */ - /** @var \AttributeLinkedSetIndirect $this */ - $oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); - $sTargetClass = $oRemoteAttDef->GetTargetClass(); - - /** @var \AttributeLinkedSetIndirect $this */ - $oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote()); - } - $oFormField->SetTargetClass($sTargetClass); - $oFormField->SetLinkedClass($this->GetLinkedClass()); - $oFormField->SetIndirect($this->IsIndirect()); - // Setting attcodes to display - $aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list')); - // - Adding friendlyname attribute to the list is not already in it - $sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass); - if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) { - $aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay); - } - // - Adding attribute properties - $aAttributesToDisplay = array(); - foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) { - $oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay); - $aAttributesToDisplay[$sAttCodeToDisplay] = [ - 'att_code' => $sAttCodeToDisplay, - 'label' => $oAttDefToDisplay->GetLabel(), - ]; - } - $oFormField->SetAttributesToDisplay($aAttributesToDisplay); - - // Append lnk attributes (filtered from zlist) - if ($this->IsIndirect()) { - $aLnkAttDefToDisplay = MetaModel::GetZListAttDefsFilteredForIndirectLinkClass($this->m_sHostClass, $this->m_sCode); - $aLnkAttributesToDisplay = array(); - foreach ($aLnkAttDefToDisplay as $oLnkAttDefToDisplay) { - $aLnkAttributesToDisplay[$oLnkAttDefToDisplay->GetCode()] = [ - 'att_code' => $oLnkAttDefToDisplay->GetCode(), - 'label' => $oLnkAttDefToDisplay->GetLabel(), - 'mandatory' => !$oLnkAttDefToDisplay->IsNullAllowed(), - ]; - } - $oFormField->SetLnkAttributesToDisplay($aLnkAttributesToDisplay); - } - - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - public function IsPartOfFingerprint() - { - return false; - } - - /** - * @inheritDoc - * @param \ormLinkSet $proposedValue - */ - public function HasAValue($proposedValue): bool - { - // Protection against wrong value type - if (false === ($proposedValue instanceof ormLinkSet)) { - return parent::HasAValue($proposedValue); - } - - // We test if there is at least 1 item in the linkset (new or existing), not if an item is being added to it. - return $proposedValue->Count() > 0; - } - - /** - * SearchSpecificLabel. - * - * @param string $sDictEntrySuffix - * @param string $sDefault - * @param bool $bUserLanguageOnly - * @param ...$aArgs - * @return string - * @since 3.1.0 - */ - public function SearchSpecificLabel(string $sDictEntrySuffix, string $sDefault, bool $bUserLanguageOnly, ...$aArgs): string - { - try { - $sNextClass = $this->m_sHostClass; - - do { - $sKey = "Class:{$sNextClass}/Attribute:{$this->m_sCode}/{$sDictEntrySuffix}"; - if (Dict::S($sKey, null, $bUserLanguageOnly) !== $sKey) { - return Dict::Format($sKey, ...$aArgs); - } - $sNextClass = MetaModel::GetParentClass($sNextClass); - } while ($sNextClass !== null); - - if (Dict::S($sDictEntrySuffix, null, $bUserLanguageOnly) !== $sKey) { - return Dict::Format($sDictEntrySuffix, ...$aArgs); - } else { - return $sDefault; - } - } catch (Exception $e) { - ExceptionLog::LogException($e); - return $sDefault; - } - } -} - -/** - * Set of objects linked to an object (n-n), and being part of its definition - * - * @package iTopORM - */ -class AttributeLinkedSetIndirect extends AttributeLinkedSet -{ - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("ext_key_to_remote")); - } - - public function IsIndirect() - { - return true; - } - - public function GetExtKeyToRemote() - { - return $this->Get('ext_key_to_remote'); - } - - public function GetEditClass() - { - return "LinkedSet"; - } - - public function DuplicatesAllowed() - { - return $this->GetOptional("duplicates", false); - } // The same object may be linked several times... or not... - - public function GetTrackingLevel() - { - return $this->GetOptional('tracking_level', - MetaModel::GetConfig()->Get('tracking_level_linked_set_indirect_default')); - } - - /** - * Find the corresponding "link" attribute on the target class, if any - * - * @return null | AttributeDefinition - * @throws \CoreException - */ - public function GetMirrorLinkAttribute() - { - $oRet = null; - /** @var \AttributeExternalKey $oExtKeyToRemote */ - $oExtKeyToRemote = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote()); - $sRemoteClass = $oExtKeyToRemote->GetTargetClass(); - foreach(MetaModel::ListAttributeDefs($sRemoteClass) as $sRemoteAttCode => $oRemoteAttDef) { - if (!$oRemoteAttDef instanceof AttributeLinkedSetIndirect) { - continue; - } - if ($oRemoteAttDef->GetLinkedClass() != $this->GetLinkedClass()) { - continue; - } - if ($oRemoteAttDef->GetExtKeyToMe() != $this->GetExtKeyToRemote()) { - continue; - } - if ($oRemoteAttDef->GetExtKeyToRemote() != $this->GetExtKeyToMe()) { - continue; - } - $oRet = $oRemoteAttDef; - break; - } - - return $oRet; - } - - /** @inheritDoc */ - public static function IsBulkModifyCompatible(): bool - { - return true; - } - -} - -/** - * Abstract class implementing default filters for a DB column - * - * @package iTopORM - */ -class AttributeDBFieldVoid extends AttributeDefinition -{ - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("allowed_values", "depends_on", "sql")); - } - - // To be overriden, used in GetSQLColumns - protected function GetSQLCol($bFullSpec = false) - { - return 'VARCHAR(255)' - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - protected function GetSQLColSpec() - { - $default = $this->ScalarToSQL($this->GetDefaultValue()); - if (is_null($default)) - { - $sRet = ''; - } - else - { - if (is_numeric($default)) - { - // Though it is a string in PHP, it will be considered as a numeric value in MySQL - // Then it must not be quoted here, to preserve the compatibility with the value returned by CMDBSource::GetFieldSpec - $sRet = " DEFAULT $default"; - } - else - { - $sRet = " DEFAULT ".CMDBSource::Quote($default); - } - } - - return $sRet; - } - - public function GetEditClass() - { - return "String"; - } - - public function GetValuesDef() - { - return $this->Get("allowed_values"); - } - - public function GetPrerequisiteAttributes($sClass = null) - { - return $this->Get("depends_on"); - } - - public static function IsBasedOnDBColumns() - { - return true; - } - - public static function IsScalar() - { - return true; - } - - public function IsWritable() - { - return !$this->IsMagic(); - } - - public function GetSQLExpr() - { - return $this->Get("sql"); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return $this->MakeRealValue("", $oHostObject); - } - - public function IsNullAllowed() - { - return false; - } - - // - protected function ScalarToSQL($value) - { - return $value; - } // format value as a valuable SQL literal (quoted outside) - - public function GetSQLExpressions($sPrefix = '') - { - $aColumns = array(); - // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix - $aColumns[''] = $this->Get("sql"); - - return $aColumns; - } - - public function FromSQLToValue($aCols, $sPrefix = '') - { - $value = $this->MakeRealValue($aCols[$sPrefix.''], null); - - return $value; - } - - public function GetSQLValues($value) - { - $aValues = array(); - $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); - - return $aValues; - } - - public function GetSQLColumns($bFullSpec = false) - { - $aColumns = array(); - $aColumns[$this->Get("sql")] = $this->GetSQLCol($bFullSpec); - - return $aColumns; - } - - public function GetBasicFilterOperators() - { - return array("=" => "equals", "!=" => "differs from"); - } - - public function GetBasicFilterLooseOperator() - { - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '=': - default: - return $this->GetSQLExpr()." = $sQValue"; - } - } -} - -/** - * Base class for all kind of DB attributes, with the exception of external keys - * - * @package iTopORM - */ -class AttributeDBField extends AttributeDBFieldVoid -{ - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array("default_value", "is_null_allowed")); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return $this->MakeRealValue($this->Get("default_value"), $oHostObject); - } - - public function IsNullAllowed() - { - return $this->Get("is_null_allowed"); - } -} - -/** - * Map an integer column to an attribute - * - * @package iTopORM - */ -class AttributeInteger extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() - { - return "String"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return "INT(11)".($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetValidationPattern() - { - return "^[0-9]+$"; - } - - public function GetBasicFilterOperators() - { - return array( - "!=" => "differs from", - "=" => "equals", - ">" => "greater (strict) than", - ">=" => "greater than", - "<" => "less (strict) than", - "<=" => "less than", - "in" => "in" - ); - } - - public function GetBasicFilterLooseOperator() - { - // Unless we implement an "equals approximately..." or "same order of magnitude" - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '>': - return $this->GetSQLExpr()." > $sQValue"; - break; - case '>=': - return $this->GetSQLExpr()." >= $sQValue"; - break; - case '<': - return $this->GetSQLExpr()." < $sQValue"; - break; - case '<=': - return $this->GetSQLExpr()." <= $sQValue"; - break; - case 'in': - if (!is_array($value)) - { - throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); - } - - return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; - break; - - case '=': - default: - return $this->GetSQLExpr()." = \"$value\""; - } - } - - public function GetNullValue() - { - return null; - } - - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - /** - * @inheritDoc - */ - public function HasAValue($proposedValue): bool - { - return utils::IsNotNullOrEmptyString($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return null; - } - if ($proposedValue === '') - { - return null; - } // 0 is transformed into '' ! - - return (int)$proposedValue; - } - - public function ScalarToSQL($value) - { - assert(is_numeric($value) || is_null($value)); - - return $value; // supposed to be an int - } -} - -/** - * An external key for which the class is defined as the value of another attribute - * - * @package iTopORM - */ -class AttributeObjectKey extends AttributeDBFieldVoid -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_EXTERNAL_KEY; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('class_attcode', 'is_null_allowed')); - } - - public function GetEditClass() - { - return "String"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return "INT(11)".($bFullSpec ? " DEFAULT 0" : ""); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return 0; - } - - public function IsNullAllowed() - { - return $this->Get("is_null_allowed"); - } - - - public function GetBasicFilterOperators() - { - return parent::GetBasicFilterOperators(); - } - - public function GetBasicFilterLooseOperator() - { - return parent::GetBasicFilterLooseOperator(); - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - return parent::GetBasicFilterSQLExpr($sOpCode, $value); - } - - public function GetNullValue() - { - return 0; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == 0); - } - - /** - * @inheritDoc - */ - public function HasAValue($proposedValue): bool - { - return ((int) $proposedValue) !== 0; - } - - /** - * @inheritDoc - * - * @param int|DBObject $proposedValue Object key or valid ({@see MetaModel::IsValidObject()}) datamodel object - */ - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return 0; - } - if ($proposedValue === '') - { - return 0; - } - if (MetaModel::IsValidObject($proposedValue)) - { - return $proposedValue->GetKey(); - } - - return (int)$proposedValue; - } -} - -/** - * Display an integer between 0 and 100 as a percentage / horizontal bar graph - * - * @package iTopORM - */ -class AttributePercentage extends AttributeInteger -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - $iWidth = 5; // Total width of the percentage bar graph, in em... - $iValue = (int)$sValue; - if ($iValue > 100) - { - $iValue = 100; - } - else - { - if ($iValue < 0) - { - $iValue = 0; - } - } - if ($iValue > 90) - { - $sColor = "#cc3300"; - } - else - { - if ($iValue > 50) - { - $sColor = "#cccc00"; - } - else - { - $sColor = "#33cc00"; - } - } - $iPercentWidth = ($iWidth * $iValue) / 100; - - return "
 
 $sValue %"; - } -} - -/** - * Map a decimal value column (suitable for financial computations) to an attribute - * internally in PHP such numbers are represented as string. Should you want to perform - * a calculation on them, it is recommended to use the BC Math functions in order to - * retain the precision - * - * @package iTopORM - */ -class AttributeDecimal extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_NUMERIC; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('digits', 'decimals' /* including precision */)); - } - - public function GetEditClass() - { - return "String"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return "DECIMAL(".$this->Get('digits').",".$this->Get('decimals').")".($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetValidationPattern() - { - $iNbDigits = $this->Get('digits'); - $iPrecision = $this->Get('decimals'); - $iNbIntegerDigits = $iNbDigits - $iPrecision; - - return "^[\-\+]?\d{1,$iNbIntegerDigits}(\.\d{0,$iPrecision})?$"; - } - - /** - * @inheritDoc - * @since 3.2.0 - */ - public function CheckFormat($value) - { - $sRegExp = $this->GetValidationPattern(); - return preg_match("/$sRegExp/", $value); - } - - public function GetBasicFilterOperators() - { - return array( - "!=" => "differs from", - "=" => "equals", - ">" => "greater (strict) than", - ">=" => "greater than", - "<" => "less (strict) than", - "<=" => "less than", - "in" => "in" - ); - } - - public function GetBasicFilterLooseOperator() - { - // Unless we implement an "equals approximately..." or "same order of magnitude" - return "="; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '!=': - return $this->GetSQLExpr()." != $sQValue"; - break; - case '>': - return $this->GetSQLExpr()." > $sQValue"; - break; - case '>=': - return $this->GetSQLExpr()." >= $sQValue"; - break; - case '<': - return $this->GetSQLExpr()." < $sQValue"; - break; - case '<=': - return $this->GetSQLExpr()." <= $sQValue"; - break; - case 'in': - if (!is_array($value)) - { - throw new CoreException("Expected an array for argument value (sOpCode='$sOpCode')"); - } - - return $this->GetSQLExpr()." IN ('".implode("', '", $value)."')"; - break; - - case '=': - default: - return $this->GetSQLExpr()." = \"$value\""; - } - } - - public function GetNullValue() - { - return null; - } - - public function IsNull($proposedValue) - { - return is_null($proposedValue); - } - - /** - * @inheritDoc - */ - public function HasAValue($proposedValue): bool - { - return utils::IsNotNullOrEmptyString($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return null; - } - if ($proposedValue === '') - { - return null; - } - - return $this->ScalarToSQL($proposedValue); - } - - public function ScalarToSQL($value) - { - assert(is_null($value) || preg_match('/'.$this->GetValidationPattern().'/', $value)); - - if (!is_null($value) && ($value !== '')) - { - $value = sprintf("%1.".$this->Get('decimals')."F", $value); - } - return $value; // null or string - } -} - -/** - * Map a boolean column to an attribute - * - * @package iTopORM - */ -class AttributeBoolean extends AttributeInteger -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() - { - return "Integer"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return "TINYINT(1)".($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return null; - } - if ($proposedValue === '') - { - return null; - } - if ((int)$proposedValue) - { - return true; - } - - return false; - } - - public function ScalarToSQL($value) - { - if ($value) - { - return 1; - } - - return 0; - } - - public function GetValueLabel($bValue) - { - if (is_null($bValue)) - { - $sLabel = Dict::S('Core:'.get_class($this).'/Value:null'); - } - else - { - $sValue = $bValue ? 'yes' : 'no'; - $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue); - $sLabel = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue, $sDefault, true /*user lang*/); - } - - return $sLabel; - } - - public function GetValueDescription($bValue) - { - if (is_null($bValue)) - { - $sDescription = Dict::S('Core:'.get_class($this).'/Value:null+'); - } - else - { - $sValue = $bValue ? 'yes' : 'no'; - $sDefault = Dict::S('Core:'.get_class($this).'/Value:'.$sValue.'+'); - $sDescription = $this->SearchLabel('/Attribute:'.$this->m_sCode.'/Value:'.$sValue.'+', $sDefault, - true /*user lang*/); - } - - return $sDescription; - } - - public function GetAsHTML($bValue, $oHostObject = null, $bLocalize = true) - { - if (is_null($bValue)) - { - $sRes = ''; - } - elseif ($bLocalize) - { - $sLabel = $this->GetValueLabel($bValue); - $sDescription = $this->GetValueDescription($bValue); - // later, we could imagine a detailed description in the title - $sRes = "".parent::GetAsHtml($sLabel).""; - } - else - { - $sRes = $bValue ? 'yes' : 'no'; - } - - return $sRes; - } - - public function GetAsXML($bValue, $oHostObject = null, $bLocalize = true) - { - if (is_null($bValue)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($bValue); - } - else - { - $sFinalValue = $bValue ? 'yes' : 'no'; - } - $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); - - return $sRes; - } - - public function GetAsCSV( - $bValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, - $bConvertToPlainText = false - ) { - if (is_null($bValue)) - { - $sFinalValue = ''; - } - elseif ($bLocalize) - { - $sFinalValue = $this->GetValueLabel($bValue); - } - else - { - $sFinalValue = $bValue ? 'yes' : 'no'; - } - $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); - - return $sRes; - } - - public static function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\SelectField'; - } - - /** - * @param \DBObject $oObject - * @param \Combodo\iTop\Form\Field\SelectField $oFormField - * - * @return \Combodo\iTop\Form\Field\SelectField - * @throws \CoreException - */ - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - - $oFormField->SetChoices(array('yes' => $this->GetValueLabel(true), 'no' => $this->GetValueLabel(false))); - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - - public function GetEditValue($value, $oHostObj = null) - { - if (is_null($value)) - { - return ''; - } - else - { - return $this->GetValueLabel($value); - } - } - - public function GetForJSON($value) - { - return (bool)$value; - } - - public function MakeValueFromString( - $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, - $sAttributeQualifier = null - ) { - $sInput = mb_strtolower(trim($sProposedValue)); - if ($bLocalizedValue) - { - switch ($sInput) - { - case '1': // backward compatibility - case $this->GetValueLabel(true): - $value = true; - break; - case '0': // backward compatibility - case 'no': - case $this->GetValueLabel(false): - $value = false; - break; - default: - $value = null; - } - } - else - { - switch ($sInput) - { - case '1': // backward compatibility - case 'yes': - $value = true; - break; - case '0': // backward compatibility - case 'no': - $value = false; - break; - default: - $value = null; - } - } - - return $value; - } - - public function RecordAttChange(DBObject $oObject, $original, $value): void - { - parent::RecordAttChange($oObject, $original ? 1 : 0, $value ? 1 : 0); - } - - protected function GetChangeRecordClassName(): string - { - return CMDBChangeOpSetAttributeScalar::class; - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') : array - { - return [ - 0 => $this->GetValueLabel(false), - 1 => $this->GetValueLabel(true) - ]; - } - - public function GetDisplayStyle() - { - return $this->GetOptional('display_style', 'select'); - } -} - -/** - * Map a varchar column (size < ?) to an attribute - * - * @package iTopORM - */ -class AttributeString extends AttributeDBField -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() - { - return "String"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return 'VARCHAR(255)' - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetValidationPattern() - { - $sPattern = $this->GetOptional('validation_pattern', ''); - if (empty($sPattern)) - { - return parent::GetValidationPattern(); - } - else - { - return $sPattern; - } - } - - public function CheckFormat($value) - { - $sRegExp = $this->GetValidationPattern(); - if (empty($sRegExp)) - { - return true; - } - else - { - $sRegExp = str_replace('/', '\\/', $sRegExp); - - return preg_match("/$sRegExp/", $value); - } - } - - public function GetMaxSize() - { - return 255; - } - - public function GetBasicFilterOperators() - { - return array( - "=" => "equals", - "!=" => "differs from", - "Like" => "equals (no case)", - "NotLike" => "differs from (no case)", - "Contains" => "contains", - "Begins with" => "begins with", - "Finishes with" => "finishes with" - ); - } - - public function GetBasicFilterLooseOperator() - { - return "Contains"; - } - - public function GetBasicFilterSQLExpr($sOpCode, $value) - { - $sQValue = CMDBSource::Quote($value); - switch ($sOpCode) - { - case '=': - case '!=': - return $this->GetSQLExpr()." $sOpCode $sQValue"; - case 'Begins with': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("$value%"); - case 'Finishes with': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value"); - case 'Contains': - return $this->GetSQLExpr()." LIKE ".CMDBSource::Quote("%$value%"); - case 'NotLike': - return $this->GetSQLExpr()." NOT LIKE $sQValue"; - case 'Like': - default: - return $this->GetSQLExpr()." LIKE $sQValue"; - } - } - - public function GetNullValue() - { - return ''; - } - - public function IsNull($proposedValue) - { - return ($proposedValue == ''); - } - - /** - * @inheritDoc - */ - public function HasAValue($proposedValue): bool - { - return utils::IsNotNullOrEmptyString($proposedValue); - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return ''; - } - - return (string)$proposedValue; - } - - public function ScalarToSQL($value) - { - if (!is_string($value) && !is_null($value)) - { - throw new CoreWarning('Expected the attribute value to be a string', array( - 'found_type' => gettype($value), - 'value' => $value, - 'class' => $this->GetHostClass(), - 'attribute' => $this->GetCode() - )); - } - - return $value; - } - - public function GetAsCSV( - $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, - $bConvertToPlainText = false - ) { - $sFrom = array("\r\n", $sTextQualifier); - $sTo = array("\n", $sTextQualifier.$sTextQualifier); - $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); - - return $sTextQualifier.$sEscaped.$sTextQualifier; - } - - public function GetDisplayStyle() - { - return $this->GetOptional('display_style', 'select'); - } - - public static function GetFormFieldClass() - { - return '\\Combodo\\iTop\\Form\\Field\\StringField'; - } - - public function MakeFormField(DBObject $oObject, $oFormField = null) - { - if ($oFormField === null) - { - $sFormFieldClass = static::GetFormFieldClass(); - $oFormField = new $sFormFieldClass($this->GetCode()); - } - parent::MakeFormField($oObject, $oFormField); - - return $oFormField; - } - -} - -/** - * An attribute that matches an object class - * - * @package iTopORM - */ -class AttributeClass extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_ENUM; - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('class_category', 'more_values')); - } - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aParams["allowed_values"] = new ValueSetEnumClasses($aParams['class_category'], $aParams['more_values']); - parent::__construct($sCode, $aParams); - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - $sDefault = parent::GetDefaultValue($oHostObject); - if (!$this->IsNullAllowed() && $this->IsNull($sDefault)) - { - // For this kind of attribute specifying null as default value - // is authorized even if null is not allowed - - // Pick the first one... - $aClasses = $this->GetAllowedValues(); - $sDefault = key($aClasses); - } - - return $sDefault; - } - - /** - * @param array $aArgs - * @param string $sContains - * - * @return array|null - * @throws \CoreException - */ - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $oValSetDef = $this->GetValuesDef(); - if (!$oValSetDef) { - return null; - } - - $aListClass = $oValSetDef->GetValues($aArgs, $sContains); - /* @since 3.3.0 remove elements in class_exclusion_list*/ - $sClassExclusionList = $this->GetOptional('class_exclusion_list',null); - if (!empty($sClassExclusionList)) { - foreach (explode(',', $sClassExclusionList) as $sClassName) { - unset($aListClass[trim($sClassName)]); - } - } - - return $aListClass; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) { - return ''; - } - - return MetaModel::GetName($sValue); - } - - public function RequiresIndex() - { - return true; - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } - -} - - -/** - * An attribute that matches a class state - * - * @package iTopORM - */ -class AttributeClassState extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return array_merge(parent::ListExpectedParams(), array('class_field')); - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - if (isset($aArgs['this'])) - { - $oHostObj = $aArgs['this']; - $sTargetClass = $this->Get('class_field'); - $sClass = $oHostObj->Get($sTargetClass); - - $aAllowedStates = array(); - foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) - { - $aValues = MetaModel::EnumStates($sChildClass); - foreach (array_keys($aValues) as $sState) - { - $aAllowedStates[$sState] = $sState.' ('.MetaModel::GetStateLabel($sChildClass, $sState).')'; - } - } - return $aAllowedStates; - } - - return null; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) - { - return ''; - } - - if (!empty($oHostObject)) - { - $sTargetClass = $this->Get('class_field'); - $sClass = $oHostObject->Get($sTargetClass); - foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) - { - $aValues = MetaModel::EnumStates($sChildClass); - if (in_array($sValue, $aValues)) - { - $sLabelForHtmlAttribute = utils::EscapeHtml($sValue.' ('.MetaModel::GetStateLabel($sChildClass, $sValue).')'); - $sHTML = ''.$sValue.''; - - return $sHTML; - } - } - } - - return $sValue; - } - -} - -/** - * An attibute that matches one of the language codes availables in the dictionnary - * - * @package iTopORM - */ -class AttributeApplicationLanguage extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - - public static function ListExpectedParams() - { - return parent::ListExpectedParams(); - } - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aAvailableLanguages = Dict::GetLanguages(); - $aLanguageCodes = array(); - foreach($aAvailableLanguages as $sLangCode => $aInfo) - { - $aLanguageCodes[$sLangCode] = $aInfo['description'].' ('.$aInfo['localized_description'].')'; - } - - // N°6462 This should be sorted directly in \Dict during the compilation but we can't for 2 reasons: - // - Additional languages can be added on the fly even though it is not recommended - // - Formatting is done at run time (just above) - natcasesort($aLanguageCodes); - - $aParams["allowed_values"] = new ValueSetEnum($aLanguageCodes); - parent::__construct($sCode, $aParams); - } - - public function RequiresIndex() - { - return true; - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } -} - -/** - * The attribute dedicated to the finalclass automatic attribute - * - * @package iTopORM - */ -class AttributeFinalClass extends AttributeString -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_STRING; - public $m_sValue; - - public function __construct($sCode, $aParams) - { - $this->m_sCode = $sCode; - $aParams["allowed_values"] = null; - parent::__construct($sCode, $aParams); - - $this->m_sValue = $this->Get("default_value"); - } - - public function IsWritable() - { - return false; - } - - public function IsMagic() - { - return true; - } - - public function RequiresIndex() - { - return true; - } - - public function SetFixedValue($sValue) - { - $this->m_sValue = $sValue; - } - - public function GetDefaultValue(DBObject $oHostObject = null) - { - return $this->m_sValue; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (empty($sValue)) - { - return ''; - } - if ($bLocalize) - { - return MetaModel::GetName($sValue); - } - else - { - return $sValue; - } - } - - /** - * An enum can be localized - * - * @param string $sProposedValue - * @param bool $bLocalizedValue - * @param string $sSepItem - * @param string $sSepAttribute - * @param string $sSepValue - * @param string $sAttributeQualifier - * - * @return mixed|null|string - * @throws \CoreException - * @throws \OQLException - */ - public function MakeValueFromString( - $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, - $sAttributeQualifier = null - ) { - if ($bLocalizedValue) - { - // Lookup for the value matching the input - // - $sFoundValue = null; - $aRawValues = self::GetAllowedValues(); - if (!is_null($aRawValues)) - { - foreach($aRawValues as $sKey => $sValue) - { - if ($sProposedValue == $sValue) - { - $sFoundValue = $sKey; - break; - } - } - } - if (is_null($sFoundValue)) - { - return null; - } - - return $this->MakeRealValue($sFoundValue, null); - } - else - { - return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, - $sAttributeQualifier); - } - } - - - // Because this is sometimes used to get a localized/string version of an attribute... - public function GetEditValue($sValue, $oHostObj = null) - { - if (empty($sValue)) - { - return ''; - } - - return MetaModel::GetName($sValue); - } - - public function GetForJSON($value) - { - // JSON values are NOT localized - return $value; - } - - /** - * @param $value - * @param string $sSeparator - * @param string $sTextQualifier - * @param \DBObject $oHostObject - * @param bool $bLocalize - * @param bool $bConvertToPlainText - * - * @return string - * @throws \CoreException - * @throws \DictExceptionMissingString - */ - public function GetAsCSV( - $value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, - $bConvertToPlainText = false - ) { - if ($bLocalize && $value != '') - { - $sRawValue = MetaModel::GetName($value); - } - else - { - $sRawValue = $value; - } - - return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText); - } - - public function GetAsXML($value, $oHostObject = null, $bLocalize = true) - { - if (empty($value)) - { - return ''; - } - if ($bLocalize) - { - $sRawValue = MetaModel::GetName($value); - } - else - { - $sRawValue = $value; - } - - return Str::pure2xml($sRawValue); - } - - public function GetBasicFilterLooseOperator() - { - return '='; - } - - public function GetValueLabel($sValue) - { - if (empty($sValue)) - { - return ''; - } - - return MetaModel::GetName($sValue); - } - - public function GetAllowedValues($aArgs = array(), $sContains = '') - { - $aRawValues = MetaModel::EnumChildClasses($this->GetHostClass(), ENUM_CHILD_CLASSES_ALL); - $aLocalizedValues = array(); - foreach($aRawValues as $sClass) - { - $aLocalizedValues[$sClass] = MetaModel::GetName($sClass); - } - - return $aLocalizedValues; - } - - /** - * @return bool - * @since 2.7.0 N°2272 OQL perf finalclass in all intermediary tables - */ - public function CopyOnAllTables() - { - $sClass = self::GetHostClass(); - if (MetaModel::IsLeafClass($sClass)) - { - // Leaf class, no finalclass - return false; - } - return true; - } -} - - -/** - * Map a varchar column (size < ?) to an attribute that must never be shown to the user - * - * @package iTopORM - */ -class AttributePassword extends AttributeString implements iAttributeNoGroupBy -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - /** - * Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329) - * - * @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited - * @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9 - * - * @param string $sCode - * @param array $aParams - * - * @throws \Exception - * @noinspection SenselessProxyMethodInspection - */ - public function __construct($sCode, $aParams) - { - parent::__construct($sCode, $aParams); - } - - public static function ListExpectedParams() - { - return parent::ListExpectedParams(); - //return array_merge(parent::ListExpectedParams(), array()); - } - - public function GetEditClass() - { - return "Password"; - } - - protected function GetSQLCol($bFullSpec = false) - { - return "VARCHAR(64)" - .CMDBSource::GetSqlStringColumnDefinition() - .($bFullSpec ? $this->GetSQLColSpec() : ''); - } - - public function GetMaxSize() - { - return 64; - } - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) - { - if (utils::IsNullOrEmptyString($sValue)) - { - return ''; - } - else - { - return '******'; - } - } - - public function IsPartOfFingerprint() - { - return false; - } // Cannot reliably compare two encrypted passwords since the same password will be encrypted in diffferent manners depending on the random 'salt' -} - -/** - * Map a text column (size < 255) to an attribute that is encrypted in the database - * The encryption is based on a key set per iTop instance. Thus if you export your - * database (in SQL) to someone else without providing the key at the same time - * the encrypted fields will remain encrypted - * - * @package iTopORM - */ -class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy -{ - const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; - - protected function GetSQLCol($bFullSpec = false) - { - return "TINYBLOB"; - } - - public function GetMaxSize() - { - return 255; - } - - public function MakeRealValue($proposedValue, $oHostObj) - { - if (is_null($proposedValue)) - { - return null; - } - - return (string)$proposedValue; - } - - /** - * Decrypt the value when reading from the database - * - * @param array $aCols - * @param string $sPrefix - * - * @return string - * @throws \Exception - */ - public function FromSQLToValue($aCols, $sPrefix = '') - { - $oSimpleCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary()); - $sValue = $oSimpleCrypt->Decrypt(MetaModel::GetConfig()->GetEncryptionKey(), $aCols[$sPrefix]); - - return $sValue; - } - - /** - * Encrypt the value before storing it in the database - * - * @param $value - * - * @return array - * @throws \Exception - */ - public function GetSQLValues($value) - { - $oSimpleCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary()); - $encryptedValue = $oSimpleCrypt->Encrypt(MetaModel::GetConfig()->GetEncryptionKey(), $value); - - $aValues = array(); - $aValues[$this->Get("sql")] = $encryptedValue; - - return $aValues; - } - - protected function GetChangeRecordAdditionalData(CMDBChangeOp $oMyChangeOp, DBObject $oObject, $original, $value): void - { - if (is_null($original)) { - $original = ''; - } - $oMyChangeOp->Set("prevstring", $original); - } - - protected function GetChangeRecordClassName(): string - { - return CMDBChangeOpSetAttributeEncrypted::class; - } - - -} - - -/** - * Wiki formatting - experimental - * - * [[:|