* @package Combodo\iTop\Controller\Notifications * @since 3.2.0 */ class NotificationsCenterController extends Controller { public const ROUTE_NAMESPACE = 'notificationscenter'; public function CheckPostedCSRF(){ $sToken = utils::ReadParam('token', '', true, 'raw_data'); return utils::IsTransactionValid($sToken, false); } /** * Displays a table containing all ActionNotifications that current user is likely to receive and allows to unsubscribe from them. * * @return iTopWebPage * @throws \ArchivedObjectException * @throws \ConfigException * @throws \CoreException * @throws \CoreUnexpectedValue * @throws \DictExceptionMissingString * @throws \MySQLException * @throws \ReflectionException * @throws \Twig\Error\LoaderError * @throws \Twig\Error\RuntimeError * @throws \Twig\Error\SyntaxError */ public function OperationDisplayPage() { $oPage = new iTopWebPage(Dict::S('UI:NotificationsCenter:Page:Title')); // Create a panel that will contain the table $oNotificationsPanel = new Panel(Dict::S('UI:NotificationsCenter:Panel:Title'), array(), 'grey', 'ibo-notifications-center'); $oNotificationsPanel->AddCSSClass('ibo-datatable-panel'); $oSubtitleBlock = new UIContentBlock(null, ['ibo-notifications-center--sub-title']); $sDisplayAdvancedPageUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.display_advanced_page', [], true); $oSubtitleBlock->AddSubBlock(new Html(Dict::Format('UI:NotificationsCenter:Panel:SubTitle', $sDisplayAdvancedPageUrl))); $oNotificationsPanel->SetSubTitleBlock($oSubtitleBlock); $oNotificationsCenterTableColumns = [ 'trigger' => array('label' => MetaModel::GetName('Trigger')), 'channels' => array('label' => Dict::S('UI:NotificationsCenter:Panel:Table:Channels')), ]; // Get all subscribed/unsubscribed actions notifications for the current user $oLnkNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByContact(\UserRights::GetContactId()); $oActionsNotificationsByTrigger = []; $aSubscribedActionsNotificationsByTrigger = []; while ($oLnkActionsNotifications = $oLnkNotificationsSet->Fetch()) { $oSubscribedActionNotification = MetaModel::GetObject(ActionNotification::class, $oLnkActionsNotifications->Get('action_id')); $oTrigger = MetaModel::GetObject('Trigger', $oLnkActionsNotifications->Get('trigger_id')); $iTriggerId = $oTrigger->GetKey(); // Create an new array for the trigger if it doesn't exist if (!isset($oActionsNotificationsByTrigger[$iTriggerId])) { $oActionsNotificationsByTrigger[$iTriggerId] = []; $aSubscribedActionsNotificationsByTrigger[$iTriggerId] = []; } // Add the action notification to the list of actions notifications for the trigger $oActionsNotificationsByTrigger[$iTriggerId][] = $oSubscribedActionNotification; // Add the subscribed status to the list of subscribed actions notifications for the trigger $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oSubscribedActionNotification->GetKey()] = $oLnkActionsNotifications->Get('subscribed') || $oTrigger->Get('subscription_policy') === SubscriptionPolicy::ForceAllChannels; } // Build table rows $sSubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.subscribe_to_channel', [], true); $sUnsubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.unsubscribe_from_channel', [], true); $aInputSetOptions = []; $aTableRows = []; foreach ($oActionsNotificationsByTrigger as $iTriggerId => $aActionsNotifications) { $oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false); if ($oTrigger === null) { continue; } $sTriggerSubscriptionPolicy = $oTrigger->Get('subscription_policy'); $aChannels = []; $aSetValues = []; // Create a channel for each action notification class and add it to the channels array foreach ($aActionsNotifications as $oActionNotification) { $sNotificationClass = get_class($oActionNotification); // Create a new channel if it doesn't exist for the current action notification class if (!array_key_exists($sNotificationClass, $aChannels)) { $aChannels[$sNotificationClass] = [ 'friendlyname' => MetaModel::GetName($sNotificationClass), 'has_additional_field' => true, 'additional_field' => '', 'value' => $oTrigger->GetKey().'|'.$sNotificationClass, 'has_image' => true, 'picture_url' => 'url("'.MetaModel::GetClassIcon($sNotificationClass, false).'")', 'subscribed' => $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true, 'status' => [$oActionNotification->Get('status') => 1], 'class' => 'ibo-is-not-medallion', 'total' => 1, 'total_subscribed' => $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true ? 1 : 0, 'mixed' => false, ]; } else { // Check if all actions from the same type are subscribed or not if (($aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true) !== $aChannels[$sNotificationClass]['subscribed']) { $aChannels[$sNotificationClass]['subscribed'] = 'mixed'; } $aChannels[$sNotificationClass]['total']++; $aChannels[$sNotificationClass]['total_subscribed'] += $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true ? 1 : 0; // Count the number of actions with the same status if (isset($aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')])) { $aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')]++; } else { $aChannels[$sNotificationClass]['status'][$oActionNotification->Get('status')] = 1; } } } foreach ($aChannels as $sNotificationClass => $aChannel) { // Define if all actions from the same type are subscribed or not if ($aChannel['subscribed'] === 'mixed') { $aSetValues[] = $aChannel['value']; $aChannels[$sNotificationClass]['mixed'] = true; $aChannels[$sNotificationClass]['mixed_value'] = Dict::Format('UI:NotificationsCenter:Channel:OutOf:Text', $aChannel['total_subscribed'], $aChannel['total']); } else if ($aChannel['subscribed'] === true) { $aSetValues[] = $aChannel['value']; } // Explode status array into a readable string $aChannels[$sNotificationClass]['additional_field'] = implode(', ', array_map(function($iCount, $sStatus) use($sNotificationClass) { return $iCount.' '. MetaModel::GetStateLabel($sNotificationClass, $sStatus); }, $aChannel['status'], array_keys($aChannel['status']))); } // Create a input set for the channels $oChannelSet = SetUIBlockFactory::MakeForSimple('ibochannel'.$oTrigger->GetKey(), array_values($aChannels), 'friendlyname', 'value', ['friendlyname', 'additional_field'], null, 'additional_field'); $oChannelSet->SetName('channel-'.$oTrigger->GetKey()); $oChannelSet->SetInitialValue(json_encode($aSetValues)); $oChannelSet->SetValue(json_encode($aSetValues)); $oChannelSet->SetOptionsTemplate('application/object/set/option_renderer.html.twig'); $oChannelSet->SetItemsTemplate('application/preferences/notification-center/item_renderer.html.twig'); // Disable the input set if the subscription policy is 'force_all_channels' if ($sTriggerSubscriptionPolicy === SubscriptionPolicy::ForceAllChannels) { $oChannelSet->SetIsDisabled(true); } // Add a CSRF Token $sCSRFToken = utils::GetNewTransactionId(); $oChannelSet->SetOnItemAddJs( <<SetMinItems(1); } $oChannelSet->SetOnItemRemoveJs( << $oTrigger->Get('description'), 'channels' => $oBlockRenderer->RenderHtml(), 'js' => $oBlockRenderer->RenderJsInline($oChannelSet::ENUM_JS_TYPE_ON_READY), ]; } $oNotificationsCenterTable = DataTableUIBlockFactory::MakeForStaticData('', $oNotificationsCenterTableColumns, $aTableRows, 'ibo-notifications-center--datatable', ['surround_with_panel' => true]); $oNotificationsPanel->AddSubBlock($oNotificationsCenterTable); // Add input js on each page draw so when it's refreshed we keep js interactivity foreach ($aTableRows as $aAtt) { $sJS = $aAtt['js']; $oPage->add_ready_script( <<LinkScriptFromAppRoot($sJsFile); } $oPage->AddSubBlock($oNotificationsPanel); return $oPage; } public function OperationDisplayAdvancedPage(){ $oPage = new iTopWebPage(Dict::S('UI:NotificationsCenter:Page:Title')); // Create a panel that will contain the table $oNotificationsPanel = new Panel(Dict::S('UI:NotificationsCenter:Panel:Title'), array(), 'grey', 'ibo-notifications-center'); $oSubtitleBlock = new UIContentBlock(null, ['ibo-notifications-center--sub-title']); $sDisplayAdvancedPageUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.display_page', [], true); $oSubtitleBlock->AddSubBlock(new Html(Dict::Format('UI:NotificationsCenter:Panel:Advanced:SubTitle', $sDisplayAdvancedPageUrl))); $oNotificationsPanel->SetSubTitleBlock($oSubtitleBlock); $oPage->AddUiBlock($oNotificationsPanel); // Get all subscribed/unsubscribed actions notifications for the current user $oLnkNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByContact(\UserRights::GetContactId()); $oActionsNotificationsByTrigger = []; $aSubscribedActionsNotificationsByTrigger = []; while ($oLnkActionsNotifications = $oLnkNotificationsSet->Fetch()) { $oSubscribedActionNotification = MetaModel::GetObject(ActionNotification::class, $oLnkActionsNotifications->Get('action_id')); $oTrigger = MetaModel::GetObject('Trigger', $oLnkActionsNotifications->Get('trigger_id')); $iTriggerId = $oTrigger->GetKey(); // Create a new array for the trigger if it doesn't exist if (!isset($oActionsNotificationsByTrigger[$iTriggerId])) { $oActionsNotificationsByTrigger[$iTriggerId] = []; $aSubscribedActionsNotificationsByTrigger[$iTriggerId] = []; } // Add the action notification to the list of actions notifications for the trigger $oActionsNotificationsByTrigger[$iTriggerId][] = $oSubscribedActionNotification; // Add the subscribed status to the list of subscribed actions notifications for the trigger $aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oSubscribedActionNotification->GetKey()] = $oLnkActionsNotifications->Get('subscribed') || $oTrigger->Get('subscription_policy') === SubscriptionPolicy::ForceAllChannels; } $oPage->AddTabContainer('NotificationsCenter', '', $oNotificationsPanel); $oPage->SetCurrentTabContainer('NotificationsCenter'); // Create a new tab for each trigger foreach ($oActionsNotificationsByTrigger as $iTriggerId => $aActionsNotifications) { $oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false); if ($oTrigger === null) { continue; } foreach ($aActionsNotifications as $oActionNotification) { $oPage->SetCurrentTab(MetaModel::GetName(get_class($oActionNotification))); $oCheckBox = InputUIBlockFactory::MakeForInputWithLabel( Dict::Format('UI:NotificationsCenter:Advanced:Input:Label', $oTrigger->Get('description'), $oActionNotification->Get('name')), $oTrigger->GetKey().'|'.$oActionNotification->GetKey(), "", $oTrigger->GetKey().'|'.$oActionNotification->GetKey(), "checkbox" ); $oCheckBox->GetInput()->SetIsChecked($aSubscribedActionsNotificationsByTrigger[$iTriggerId][$oActionNotification->GetKey()] === true); $oCheckBox->SetBeforeInput(false); $oCheckBox->GetInput()->AddCSSClass('ibo-input--label-right'); $oCheckBox->GetInput()->AddCSSClass('ibo-input-checkbox'); $oContainer = new UIContentBlock(null, ['ibo-notifications-center-advanced--input--container']); $oContainer->AddSubBlock($oCheckBox); $oPage->AddUiBlock($oContainer); } } $sSubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.subscribe', [], true); $sUnsubscribeUrl = Router::GetInstance()->GenerateUrl(self::ROUTE_NAMESPACE.'.unsubscribe', [], true); $sCSRFToken = utils::GetNewTransactionId(); $oPage->add_ready_script( <<CheckPostedCSRF()) { throw new \Exception('Invalid token'); } $sChannel = utils::ReadParam('channel', '', true, 'raw_data'); $aChannel = explode('|', $sChannel); $oPage = new \JsonPage(); $aReturnData = []; try { if (count($aChannel) !== 2) { throw new \Exception('Invalid channel'); } $iTriggerKey = $aChannel[0]; $iActionNotificationKey = $aChannel[1]; $oTrigger = MetaModel::GetObject('Trigger', $iTriggerKey, false); if ($oTrigger === null) { throw new \Exception('Invalid trigger'); } $oActionNotification = MetaModel::GetObject('ActionNotification', $iActionNotificationKey, false); if ($oActionNotification === null) { throw new \Exception('Invalid action notification'); } $oSubscribedActionsNotificationsSearch = \DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.action_id = :actionnotification_id AND lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = '1'"); $oSubscribedActionsNotificationsSet = new \DBObjectSet($oSubscribedActionsNotificationsSearch, array(), array('actionnotification_id' => $iActionNotificationKey, 'contact_id' => \UserRights::GetContactId(), 'trigger_id' => $iTriggerKey)); if ($oSubscribedActionsNotificationsSet->Count() === 0) { throw new \Exception('You are not subscribed to this channel'); } while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) { $oSubscribedActionsNotifications->Set('subscribed', false); $oSubscribedActionsNotifications->DBUpdate(); } $aReturnData = [ 'status' => 'success', 'message' => Dict::S('UI:NotificationsCenter:Unsubscribe:Success'), ]; } catch (Exception $e) { $aReturnData = [ 'status' => 'error', 'message' => $e->getMessage(), ]; } $oPage->SetData($aReturnData); $oPage->SetOutputDataOnly(true); return $oPage; } function OperationSubscribe() { // Get the CSRF token from the request and check if it's valid if (!$this->CheckPostedCSRF()) { throw new \Exception('Invalid token'); } $sChannel = utils::ReadParam('channel', '', true, 'raw_data'); $aChannel = explode('|', $sChannel); $oPage = new \JsonPage(); $aReturnData = []; try { if (count($aChannel) !== 2) { throw new \Exception('Invalid channel'); } $iTriggerKey = $aChannel[0]; $iActionNotificationKey = $aChannel[1]; $oTrigger = MetaModel::GetObject('Trigger', $iTriggerKey, false); if ($oTrigger === null) { throw new \Exception('Invalid trigger'); } $oActionNotification = MetaModel::GetObject('ActionNotification', $iActionNotificationKey, false); if ($oActionNotification === null) { throw new \Exception('Invalid action notification'); } $oSubscribedActionsNotificationsSearch = \DBObjectSearch::FromOQL("SELECT lnkActionNotificationToContact AS lnk WHERE lnk.action_id = :actionnotification_id AND lnk.contact_id = :contact_id AND lnk.trigger_id = :trigger_id AND lnk.subscribed = '0'"); $oSubscribedActionsNotificationsSet = new \DBObjectSet($oSubscribedActionsNotificationsSearch, array(), array('actionnotification_id' => $iActionNotificationKey, 'contact_id' => \UserRights::GetContactId(), 'trigger_id' => $iTriggerKey)); if ($oSubscribedActionsNotificationsSet->Count() === 0) { throw new \Exception('You are not allow to subscribe to this channel'); } while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) { $oSubscribedActionsNotifications->Set('subscribed', true); $oSubscribedActionsNotifications->DBUpdate(); } $aReturnData = [ 'status' => 'success', 'message' => Dict::S('UI:NotificationsCenter:Subscribe:Success'), ]; } catch (Exception $e) { $aReturnData = [ 'status' => 'error', 'message' => $e->getMessage(), ]; } $oPage->SetData($aReturnData); $oPage->SetOutputDataOnly(true); return $oPage; } /** * @return \JsonPage * @throws \Exception */ function OperationUnsubscribeFromChannel(): \JsonPage { // Get the CSRF token from the request and check if it's valid if (!$this->CheckPostedCSRF()) { throw new \Exception('Invalid token'); } // Get the channel from the request $sChannel = utils::ReadParam('channel', '', true, 'raw_data'); $aChannel = explode('|', $sChannel); $oPage = new \JsonPage(); $aReturnData = []; try { if (count($aChannel) !== 2) { throw new \Exception('Invalid channel'); } [$iTriggerId, $sFinalclass] = $aChannel; $oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false); if ($oTrigger === null) { throw new \Exception('Invalid trigger'); } // Check the trigger subscription policy if ($oTrigger->Get('subscription_policy') === SubscriptionPolicy::ForceAllChannels) { throw new \Exception('You are not allowed to unsubscribe from this channel'); } // Check if we are subscribed to at least 1 channel $oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactSubscriptionAndFinalclass($iTriggerId, \UserRights::GetContactId(), '1', $sFinalclass); if ($oSubscribedActionsNotificationsSet->Count() === 0) { throw new \Exception('You are not subscribed to any channel'); } // Check the trigger subscription policy and if we are subscribed to at least 1 channel if necessary if($oTrigger->Get('subscription_policy') === SubscriptionPolicy::ForceAtLeastOneChannel) { $oTotalSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactAndSubscription($iTriggerId, \UserRights::GetContactId(), '1'); if (($oTotalSubscribedActionsNotificationsSet->Count() - $oSubscribedActionsNotificationsSet->Count()) === 0) { throw new \Exception('You can\'t unsubscribe from this channel, you must be subscribed to at least one channel'); } } // Unsubscribe from all channels while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) { $oSubscribedActionsNotifications->Set('subscribed', false); $oSubscribedActionsNotifications->DBUpdate(); } $aReturnData = [ 'status' => 'success', 'message' => Dict::S('UI:NotificationsCenter:Unsubscribe:Success'), ]; } catch (Exception $e) { // Return an error message if an exception is thrown $aReturnData = [ 'status' => 'error', 'message' => $e->getMessage(), ]; } $oPage->SetData($aReturnData); $oPage->SetOutputDataOnly(true); return $oPage; } /** * @return \JsonPage * @throws \Exception */ function OperationSubscribeToChannel(): \JsonPage { // Get the CSRF token from the request and check if it's valid if (!$this->CheckPostedCSRF()) { throw new \Exception('Invalid token'); } // Get the channel from the request $sChannel = utils::ReadParam('channel', '', true, 'raw_data'); $aChannel = explode('|', $sChannel); $oPage = new \JsonPage(); $aReturnData = []; try { if (count($aChannel) !== 2) { throw new \Exception('Invalid channel'); } [$iTriggerId, $sFinalclass] = $aChannel; $oTrigger = MetaModel::GetObject('Trigger', $iTriggerId, false); if ($oTrigger === null) { throw new \Exception('Invalid trigger'); } $oSubscribedActionsNotificationsSet = NotificationsRepository::GetInstance()->SearchSubscriptionByTriggerContactSubscriptionAndFinalclass($iTriggerId, \UserRights::GetContactId(), '0', $sFinalclass); if ($oSubscribedActionsNotificationsSet->Count() === 0) { throw new \Exception('You are not subscribed to any channel'); } // Subscribe to all channels while ($oSubscribedActionsNotifications = $oSubscribedActionsNotificationsSet->Fetch()) { $oSubscribedActionsNotifications->Set('subscribed', true); $oSubscribedActionsNotifications->DBUpdate(); } $aReturnData = [ 'status' => 'success', 'message' => Dict::S('UI:NotificationsCenter:Subscribe:Success'), ]; } catch (Exception $e) { // Return an error message if an exception is thrown $aReturnData = [ 'status' => 'error', 'message' => $e->getMessage(), ]; } $oPage->SetData($aReturnData); $oPage->SetOutputDataOnly(true); return $oPage; } }