diff --git a/core/metamodel.class.php b/core/metamodel.class.php index e975ae448..a2db13804 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -7145,20 +7145,28 @@ abstract class MetaModel /** * @param string $sClass * @param string $sAttCode - * @param $value + * @param mixed $value * @param bool $bMustBeFoundUnique + * @param bool $bAllowAllData + * + * @return \DBObject if $bMustBeFoundUnique=true and no object or multiple objects found will throw a CoreException + * else will return null * - * @return \DBObject * @throws \CoreException - * @throws \Exception + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * + * @since 2.7.7 Add new $bAllowAllData parameter */ - public static function GetObjectByColumn($sClass, $sAttCode, $value, $bMustBeFoundUnique = true) + public static function GetObjectByColumn($sClass, $sAttCode, $value, $bMustBeFoundUnique = true, $bAllowAllData = false) { - if (!isset(self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value])) - { + if (!isset(self::$m_aCacheObjectByColumn[$sClass][$sAttCode][$value])) { self::_check_subclass($sClass); $oObjSearch = new DBObjectSearch($sClass); + $oObjSearch->AllowAllData($bAllowAllData); $oObjSearch->AddCondition($sAttCode, $value, '='); $oSet = new DBObjectSet($oObjSearch); if ($oSet->Count() == 1) diff --git a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml index a2648e462..4419c7eee 100644 --- a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml +++ b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml @@ -35,6 +35,11 @@ p_object_view: defaults: _controller: 'Combodo\iTop\Portal\Controller\ObjectController::ViewAction' +p_object_view_from_attribute: + path: '/object/view/{sObjectClass}/{sObjectAttCode}/{sObjectAttValue}' + defaults: + _controller: 'Combodo\iTop\Portal\Controller\ObjectController::ViewFromAttributeAction' + p_object_apply_stimulus: path: '/object/apply-stimulus/{sStimulusCode}/{sObjectClass}/{sObjectId}' defaults: diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index 8e8f8d3d9..89864664b 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -66,11 +66,11 @@ class ObjectController extends BrickController const DEFAULT_LIST_LENGTH = 10; /** - * Displays an cmdbAbstractObject if the connected user is allowed to. + * Displays an cmdbAbstractObject (from its ID) if the connected user is allowed to. * * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param string $sObjectClass (Class must be instance of cmdbAbstractObject) - * @param string $sObjectId + * @param string $sObjectClass (Class must be an instance of cmdbAbstractObject) + * @param string $sObjectId * * @return \Symfony\Component\HttpFoundation\Response * @@ -83,29 +83,19 @@ class ObjectController extends BrickController */ public function ViewAction(Request $oRequest, $sObjectClass, $sObjectId) { - /** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */ - $oRequestManipulator = $this->get('request_manipulator'); - /** @var \Combodo\iTop\Portal\Routing\UrlGenerator $oUrlGenerator */ - $oUrlGenerator = $this->get('url_generator'); - /** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $oObjectFormHandler */ - $oObjectFormHandler = $this->get('object_form_handler'); /** @var \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper */ $oSecurityHelper = $this->get('security_helper'); /** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */ $oScopeValidator = $this->get('scope_validator'); - /** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */ - $oBrickCollection = $this->get('brick_collection'); // Checking parameters - if ($sObjectClass === '' || $sObjectId === '') - { + if ($sObjectClass === '' || $sObjectId === '') { IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass and sObjectId expected, "'.$sObjectClass.'" and "'.$sObjectId.'" given.'); throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); } // Checking security layers - if (!$oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sObjectClass, $sObjectId)) - { + if (!$oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sObjectClass, $sObjectId)) { IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sObjectClass.'::'.$sObjectId.' object.'); throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); } @@ -113,14 +103,97 @@ class ObjectController extends BrickController // Retrieving object $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oScopeValidator->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); - if ($oObject === null) - { + if ($oObject === null) { // We should never be there as the secuirty helper makes sure that the object exists, but just in case. IssueLog::Info(__METHOD__.' at line '.__LINE__.' : Could not load object '.$sObjectClass.'::'.$sObjectId.'.'); throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); } + return $this->PrepareViewObjectResponse($oRequest, $oObject); + } + + /** + * Displays an cmdbAbstractObject (if the connected user is allowed to) from a specific attribute. If several or none objects are found with the attribute value, an exception is thrown. + * + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param string $sObjectClass (Class must be an instance of cmdbAbstractObject) + * @param string $sObjectAttCode + * @param string $sObjectAttValue + * + * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response|null + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \OQLException + * + * @since 2.7.7 method creation + */ + public function ViewFromAttributeAction(Request $oRequest, $sObjectClass, $sObjectAttCode, $sObjectAttValue) + { + /** @var \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper */ + $oSecurityHelper = $this->get('security_helper'); + /** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */ + $oScopeValidator = $this->get('scope_validator'); + + // Checking parameters + if ($sObjectClass === '' || $sObjectAttCode === '' || $sObjectAttValue === '') { + IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass and sObjectAttCode/sObjectAttValue expected, "' + .$sObjectClass.'" and "'.$sObjectAttCode.' / '.$sObjectAttValue.'" given.'); + throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, Dict::Format('UI:Error:3ParametersMissing', 'class', 'attcode', 'attvalue')); + } + + $oObject = MetaModel::GetObjectByColumn($sObjectClass, $sObjectAttCode, $sObjectAttValue, false, + $oScopeValidator->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)); + if ($oObject === null) { + // null if object not found or multiple matches + IssueLog::Info(__METHOD__.' at line '.__LINE__.' : Could not load object '.$sObjectClass.'" and "'.$sObjectAttCode.' / '.$sObjectAttValue.'.'); + throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); + } + + // Checking security layers + $sObjectId = $oObject->GetKey(); + if (!$oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sObjectClass, $sObjectId)) { + IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sObjectClass.'::'.$sObjectId.' object.'); + throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist')); + } + + return $this->PrepareViewObjectResponse($oRequest, $oObject); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $oRequest + * @param \DBObject $oObject + * + * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response|null + * @throws \ArchivedObjectException + * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \OQLException + * + * @since 2.7.7 method creation (refactor for new `p_object_view_from_attribute` route) + */ + protected function PrepareViewObjectResponse(Request $oRequest, DBObject $oObject) + { + /** @var \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper */ + $oSecurityHelper = $this->get('security_helper'); + /** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */ + $oRequestManipulator = $this->get('request_manipulator'); + /** @var \Combodo\iTop\Portal\Routing\UrlGenerator $oUrlGenerator */ + $oUrlGenerator = $this->get('url_generator'); + /** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $oObjectFormHandler */ + $oObjectFormHandler = $this->get('object_form_handler'); + /** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */ + $oBrickCollection = $this->get('brick_collection'); + $sOperation = $oRequestManipulator->ReadParam('operation', ''); + $sObjectClass = get_class($oObject); + $sObjectId = $oObject->GetKey(); $aData = array('sMode' => 'view'); $aData['form'] = $oObjectFormHandler->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId); @@ -128,8 +201,7 @@ class ObjectController extends BrickController $oObject->GetName()); // Add an edit button if user is allowed - if ($oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) - { + if ($oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) { $sModifyUrl = $oUrlGenerator->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)); $oModifyButton = new JSButtonItem( 'modify_object', @@ -141,27 +213,19 @@ class ObjectController extends BrickController } // Preparing response - if ($oRequest->isXmlHttpRequest()) - { + if ($oRequest->isXmlHttpRequest()) { // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if (empty($sOperation)) - { + if (empty($sOperation)) { $oResponse = $this->render('itop-portal-base/portal/templates/bricks/object/modal.html.twig', $aData); - } - else - { + } else { $oResponse = new JsonResponse($aData); } - } - else - { + } else { // Adding brick if it was passed $sBrickId = $oRequestManipulator->ReadParam('sBrickId', ''); - if (!empty($sBrickId)) - { + if (!empty($sBrickId)) { $oBrick = $oBrickCollection->GetBrickById($sBrickId); - if ($oBrick !== null) - { + if ($oBrick !== null) { $aData['oBrick'] = $oBrick; } } @@ -862,6 +926,7 @@ class ObjectController extends BrickController if (!empty($sQuery)) { $oFullExpr = null; + /** @noinspection SlowArrayOperationsInLoopInspection */ for ($i = 0; $i < count($aAttCodes); $i++) { // Checking if the current attcode is an external key in order to search on the friendlyname diff --git a/pages/UI.php b/pages/UI.php index 77847b8a7..2b81ea92b 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -346,22 +346,30 @@ try case 'details': // Details of an object $sClass = utils::ReadParam('class', '', false, 'class'); - $id = utils::ReadParam('id', ''); - if ( empty($sClass) || empty($id)) - { - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + + if (empty($sClass)) { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); } - if (is_numeric($id)) - { - $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); + $id = utils::ReadParam('id', null); + if (false === is_null($id)) { + if (is_numeric($id)) { + $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); + } else { + $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); + } + } else { + $sAttCode = utils::ReadParam('attcode', ''); + $sAttValue = utils::ReadParam('attvalue', ''); + + if ((strlen($sAttCode) === 0) || (strlen($sAttValue) === 0)) { + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'id')); + } + + $oObj = MetaModel::GetObjectByColumn($sClass, $sAttCode, $sAttValue, true); } - else - { - $oObj = MetaModel::GetObjectByName($sClass, $id, false /* MustBeFound */); - } - if (is_null($oObj)) - { + + if (is_null($oObj)) { // Check anyhow if there is a message for this object (like you've just created it) $sMessageKey = $sClass.'::'.$id; DisplayMessages($sMessageKey, $oP); @@ -369,8 +377,7 @@ try // Attempt to load the object in archive mode utils::PushArchiveMode(true); - if (is_numeric($id)) - { + if (is_numeric($id)) { $oObj = MetaModel::GetObject($sClass, $id, false /* MustBeFound */); } else