diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index cd507e9ea..23a867347 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -1581,38 +1581,14 @@ EOD } } - /** - * @param string $sTitle - * @param string[] $aIssues - * - * @see AddHeaderMessage - */ - public function AddHeaderMessageForErrors($sTitle, $aIssues) - { - $sContent = "$sTitle"; - - if (count($aIssues) == 1) - { - $sContent .= " {$aIssues[0]}"; - } - else - { - $sContent .= '\n'; - } - - $this->AddHeaderMessage($sContent, 'message_error'); - } /** * Adds in the page a container with the header_message CSS class * * @param string $sContent * @param string $sCssClasses CSS classes to add to the container + * + * @since 2.6 */ public function AddHeaderMessage($sContent, $sCssClasses = 'message_info') { diff --git a/core/coreexception.class.inc.php b/core/coreexception.class.inc.php index f0ffbd229..b5840b5c9 100644 --- a/core/coreexception.class.inc.php +++ b/core/coreexception.class.inc.php @@ -107,6 +107,81 @@ class CoreException extends Exception } } +/** + * Class CoreCannotSaveObjectException + * + * Specialized exception to raise if {@link DBObject::CheckToWrite()} fails, which allow easy data retrieval + * + * @see \DBObject::DBInsertNoReload() + * @see \DBObject::DBUpdate() + * + * @since 2.6 N°659 uniqueness constraint + */ +class CoreCannotSaveObjectException extends CoreException +{ + /** @var string[] */ + private $aIssues; + /** @var int */ + private $iObjectId; + /** @var string */ + private $sObjectClass; + + /** + * CoreCannotSaveObjectException constructor. + * + * @param array $aContextData containing at least those keys : issues, id, class + */ + public function __construct($aContextData) + { + $this->aIssues = $aContextData['issues']; + $this->iObjectId = $aContextData['id']; + $this->sObjectClass = $aContextData['class']; + + $sIssues = implode(', ', $this->aIssues); + parent::__construct($sIssues, $aContextData); + } + + /** + * @return string + */ + public function getHtmlMessage() + { + $sTitle = Dict::S('UI:Error:SaveFailed'); + $sContent = "{$sTitle}"; + + if (count($this->aIssues) == 1) + { + $sContent .= " {$this->aIssues[0]}"; + } + else + { + $sContent .= '\n'; + } + + return $sContent; + } + + public function getIssues() + { + return $this->aIssues; + } + + public function getObjectId() + { + return $this->iObjectId; + } + + public function getObjectClass() + { + return $this->sObjectClass; + } +} + class CoreWarning extends CoreException { } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 95a526646..777926f95 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -1905,8 +1905,18 @@ abstract class DBObject implements iDisplay return $this->m_iKey; } - // Insert of record for the new object into the database - // Returns the key of the newly created object + /** + * Insert of record for the new object into the database + * + * @return int key of the newly created object + * @throws \ArchivedObjectException + * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \CoreWarning + * @throws \MySQLException + * @throws \OQLException + */ public function DBInsertNoReload() { if ($this->m_bIsInDB) @@ -1940,8 +1950,7 @@ abstract class DBObject implements iDisplay list($bRes, $aIssues) = $this->CheckToWrite(); if (!$bRes) { - $sIssues = implode(', ', $aIssues); - throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); + throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey())); } // Stop watches @@ -2152,7 +2161,13 @@ abstract class DBObject implements iDisplay $this->m_iKey = self::GetNextTempId(get_class($this)); } - // Update a record + /** + * Update an object in DB + * + * @return int object key + * @throws \CoreException + * @throws \CoreCannotSaveObjectException if {@link CheckToWrite()} returns issues + */ public function DBUpdate() { if (!$this->m_bIsInDB) @@ -2205,8 +2220,7 @@ abstract class DBObject implements iDisplay list($bRes, $aIssues) = $this->CheckToWrite(); if (!$bRes) { - $sIssues = implode(', ', $aIssues); - throw new CoreException("Object not following integrity rules", array('issues' => $sIssues, 'class' => get_class($this), 'id' => $this->GetKey())); + throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey())); } // Save the original values (will be reset to the new values when the object get written to the DB) @@ -2326,13 +2340,19 @@ abstract class DBObject implements iDisplay $this->RecordAttChanges($aChanges, $aOriginalValues); } } - catch (Exception $e) + catch (CoreCannotSaveObjectException $e) { - unset($aUpdateReentrance[$sKey]); throw $e; } + catch (Exception $e) + { + throw $e; + } + finally + { + unset($aUpdateReentrance[$sKey]); + } - unset($aUpdateReentrance[$sKey]); return $this->m_iKey; } @@ -2342,7 +2362,13 @@ abstract class DBObject implements iDisplay return $this->DBUpdate(); } - // Make the current changes persistent - clever wrapper for Insert or Update + /** + * Make the current changes persistent - clever wrapper for Insert or Update + * + * @return int + * @throws \CoreCannotSaveObjectException + * @throws \CoreException + */ public function DBWrite() { if ($this->m_bIsInDB) diff --git a/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php index 38b25d480..88a7be0f8 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php @@ -19,29 +19,30 @@ namespace Combodo\iTop\Portal\Form; -use Exception; -use Silex\Application; -use utils; -use Dict; -use IssueLog; -use UserRights; -use MetaModel; -use CMDBSource; -use DBObject; -use DBObjectSet; -use DBSearch; -use DBObjectSearch; -use InlineImage; -use ormTagSet; +use AttachmentPlugIn; use AttributeDateTime; use AttributeTagSet; -use AttachmentPlugIn; -use Combodo\iTop\Form\FormManager; -use Combodo\iTop\Form\Form; +use CMDBSource; use Combodo\iTop\Form\Field\Field; use Combodo\iTop\Form\Field\FileUploadField; use Combodo\iTop\Form\Field\LabelField; +use Combodo\iTop\Form\Form; +use Combodo\iTop\Form\FormManager; use Combodo\iTop\Portal\Helper\ApplicationHelper; +use CoreCannotSaveObjectException; +use DBObject; +use DBObjectSearch; +use DBObjectSet; +use DBSearch; +use Dict; +use Exception; +use InlineImage; +use IssueLog; +use MetaModel; +use ormTagSet; +use Silex\Application; +use UserRights; +use utils; /** * Description of objectformmanager @@ -976,7 +977,14 @@ class ObjectFormManager extends FormManager // Writing object to DB $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified()); $bWasModified = $this->oObject->IsModified(); - $this->oObject->DBWrite(); + try + { + $this->oObject->DBWrite(); + } + catch (CoreCannotSaveObjectException $e) + { + throw new Exception($e->getHtmlMessage()); + } // Finalizing images link to object, otherwise it will be cleaned by the GC InlineImage::FinalizeInlineImages($this->oObject); // Finalizing attachments link to object diff --git a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php index bf5bf7cbc..8c92b7c5e 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php @@ -20,6 +20,7 @@ namespace Combodo\iTop\Portal\Helper; use ApplicationContext; +use cmdbAbstractObject; use Combodo\iTop\Portal\Brick\AbstractBrick; use Combodo\iTop\Portal\Brick\PortalBrick; use DBObjectSearch; @@ -30,12 +31,10 @@ use Exception; use iPortalUIExtension; use IssueLog; use MetaModel; -use cmdbAbstractObject; use ModuleDesign; use Silex\Application; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\Debug\ExceptionHandler; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\HttpException; use Twig_Environment; use Twig_SimpleFilter; @@ -1343,6 +1342,45 @@ class ApplicationHelper return $aUIExtensions; } + public static function LoadSessionMessages(Application $oApp) + { + $aAllMessages = array(); + if ((array_key_exists('obj_messages', $_SESSION)) && (!empty($_SESSION['obj_messages']))) + { + foreach ($_SESSION['obj_messages'] as $sMessageKey => $aMessageObjectData) + { + $aObjectMessages = array(); + $aRanks = array(); + foreach ($aMessageObjectData as $sMessageId => $aMessageData) + { + $sMsgClass = 'alert alert-'; + switch ($aMessageData['severity']) + { + case 'info': + $sMsgClass .= 'info'; + break; + case 'error': + $sMsgClass .= 'danger'; + break; + case 'ok': + default: + $sMsgClass .= 'success'; + break; + } + $aObjectMessages[] = array('cssClass' => $sMsgClass, 'message' => $aMessageData['message']); + $aRanks[] = $aMessageData['rank']; + } + unset($_SESSION['obj_messages'][$sMessageKey]); + array_multisort($aRanks, $aObjectMessages); + foreach ($aObjectMessages as $aObjectMessage) + { + $aAllMessages[] = $aObjectMessage; + } + } + } + $oApp['combodo.current_user.session_messages'] = $aAllMessages; + } + /** * Generate the form data for the $sClass. * Form will look like the "Properties" tab of a $sClass object in the console. diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig index 29b9fb232..2dd7ef64c 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/src/views/layout.html.twig @@ -313,7 +313,17 @@
- + + {% if app['combodo.current_user.session_messages']|length > 0 %} +
+
+ {% for sessionMessage in app['combodo.current_user.session_messages'] %} +
{{ sessionMessage['message'] }}
+ {% endfor %} +
+
+ {% endif %} +
{% block pMainHeader %} {% endblock %} diff --git a/datamodels/2.x/itop-portal-base/portal/web/index.php b/datamodels/2.x/itop-portal-base/portal/web/index.php index c635eadf5..06302e340 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/index.php +++ b/datamodels/2.x/itop-portal-base/portal/web/index.php @@ -45,8 +45,8 @@ require_once __DIR__ . '/../src/helpers/lifecyclevalidatorhelper.class.inc.php'; require_once __DIR__ . '/../src/helpers/securityhelper.class.inc.php'; require_once __DIR__ . '/../src/helpers/applicationhelper.class.inc.php'; -use Silex\Application; use Combodo\iTop\Portal\Helper\ApplicationHelper; +use Silex\Application; // Stacking context tag so it knows we are in the portal $oContex = new ContextTag('GUI:Portal'); @@ -141,6 +141,7 @@ $oApp->before(function(Symfony\Component\HttpFoundation\Request $oRequest, Silex // Loading portal configuration from the module design $oKPI = new ExecutionKPI(); ApplicationHelper::LoadPortalConfiguration($oApp); + ApplicationHelper::LoadSessionMessages($oApp); $oKPI->ComputeAndReport('Parsing portal configuration'); // Loading current user ApplicationHelper::LoadCurrentUser($oApp); diff --git a/pages/UI.php b/pages/UI.php index 98f9499d5..e31982250 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -920,40 +920,40 @@ EOF } else { - list($bRes, $aIssues) = $oObj->CheckToWrite(); // called also in DBUpdate() - if ($bRes) + try { - try - { - CMDBSource::Query('START TRANSACTION'); - $oObj->DBUpdate(); - CMDBSource::Query('COMMIT'); - $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'ok'; - } - catch(DeleteException $e) - { - CMDBSource::Query('ROLLBACK'); - // Say two things: 1) Don't be afraid nothing was modified - $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'info'; - cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, $sSeverity, 0, true /* must not exist */); - // 2) Ok, there was some trouble indeed - $sMessage = $e->getMessage(); - $sSeverity = 'error'; - $bDisplayDetails = true; - } - utils::RemoveTransaction($sTransactionId); - + CMDBSource::Query('START TRANSACTION'); + $oObj->DBUpdate(); + CMDBSource::Query('COMMIT'); + $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'ok'; } - else + catch (CoreCannotSaveObjectException $e) { - $bDisplayDetails = false; // Found issues, explain and give the user a second chance // - $oP->AddHeaderMessageForErrors(Dict::S('UI:Error:SaveFailed'), $aIssues); - $oObj->DisplayModifyForm($oP, array('wizard_container' => true)); // wizard_container: display the wizard border and the title + CMDBSource::Query('ROLLBACK'); + $bDisplayDetails = false; + $aIssues = $e->getIssues(); + $oP->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); + $oObj->DisplayModifyForm($oP, + array('wizard_container' => true)); // wizard_container: display the wizard border and the title } + catch (DeleteException $e) + { + CMDBSource::Query('ROLLBACK'); + // Say two things: + // - 1) Don't be afraid nothing was modified + $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'info'; + cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, + $sSeverity, 0, true /* must not exist */); + // - 2) Ok, there was some trouble indeed + $sMessage = $e->getMessage(); + $sSeverity = 'error'; + $bDisplayDetails = true; + } + utils::RemoveTransaction($sTransactionId); } } if ($bDisplayDetails) @@ -1101,18 +1101,18 @@ EOF $sClass = get_class($oObj); $sClassLabel = MetaModel::GetName($sClass); - list($bRes, $aIssues) = $oObj->CheckToWrite(); // called also in DBInsertNoReload() - if ($bRes) + try { - $oObj->DBInsertNoReload(); // No need to reload + $oObj->DBInsertNoReload();// No need to reload + utils::RemoveTransaction($sTransactionId); $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); - + // Compute the name, by reloading the object, even if it disappeared from the silo $oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/); - $sName = $oObj->GetName(); + $sName = $oObj->GetName(); $sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel); - + $sNextAction = utils::ReadPostedParam('next_action', ''); if (!empty($sNextAction)) { @@ -1125,14 +1125,16 @@ EOF ReloadAndDisplay($oP, $oObj, 'create', $sMessage, 'ok'); } } - else + catch (CoreCannotSaveObjectException $e) { // Found issues, explain and give the user a second chance // + $aIssues = $e->getIssues(); + $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); $oP->add("

".MetaModel::GetClassIcon($sClass)." ".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."

\n"); $oP->add("
\n"); - $oP->AddHeaderMessageForErrors(Dict::S('UI:Error:SaveFailed'), $aIssues); + $oP->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); cmdbAbstractObject::DisplayCreationForm($oP, $sClass, $oObj); $oP->add("
\n"); }