diff --git a/application/dashlet.class.inc.php b/application/dashlet.class.inc.php index f43f232bb..42eeca069 100644 --- a/application/dashlet.class.inc.php +++ b/application/dashlet.class.inc.php @@ -459,17 +459,21 @@ EOF $sAttType = $aTargetAttCodes[$sTargetAttCode]; $sExtFieldAttCode = $sTargetAttCode; } - if (is_a($sAttType, 'AttributeLinkedSet', true)) - { - continue; - } - if (is_a($sAttType, 'AttributeFriendlyName', true)) - { - continue; - } - if (is_a($sAttType, 'AttributeOneWayPassword', true)) - { - continue; + + $aForbidenAttType = [ + 'AttributeLinkedSet', + 'AttributeFriendlyName', + + 'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether + 'AttributeOneWayPassword', + 'AttributeEncryptedString', + 'AttributePassword', + ]; + foreach ($aForbidenAttType as $sForbidenAttType) { + if (is_a($sAttType, $sForbidenAttType, true)) + { + continue 2; + } } $sLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode); diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 2114a8dfb..1fa904ff3 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -446,8 +446,21 @@ class DisplayBlock $this->m_oSet = new CMDBObjectSet($this->m_oFilter, $aOrderBy, $aQueryParams); } $this->m_oSet->SetShowObsoleteData($this->m_bShowObsoleteData); - switch($this->m_sStyle) - { + + switch($this->m_sStyle) { + case 'list_search': + case 'list': + break; + default: + // N°3473: except for 'list_search' and 'list' (which have more granularity, see the other switch below), + // refuse to render if the user is not allowed to see the class. + if (! UserRights::IsActionAllowed($this->m_oSet->GetClass(), UR_ACTION_READ, $this->m_oSet) == UR_ALLOWED_YES) { + $sHtml .= $oPage->GetP(Dict::Format('UI:Error:ReadNotAllowedOn_Class', $this->m_oSet->GetClass())); + return $sHtml; + } + } + + switch ($this->m_sStyle) { case 'count': if (isset($aExtraParams['group_by'])) { diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 879fc4ce0..44b165b63 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -103,6 +103,10 @@ 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 +interface iAttributeNoGroupBy +{ + //no method, just a contract on implement +} /** * Attribute definition API, implemented in and many flavours (Int, String, Enum, etc.) @@ -3761,7 +3765,7 @@ class AttributeFinalClass extends AttributeString * * @package iTopORM */ -class AttributePassword extends AttributeString +class AttributePassword extends AttributeString implements iAttributeNoGroupBy { const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; @@ -3837,7 +3841,7 @@ class AttributePassword extends AttributeString * * @package iTopORM */ -class AttributeEncryptedString extends AttributeString +class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy { const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; @@ -9217,7 +9221,7 @@ class AttributeSubItem extends AttributeDefinition /** * One way encrypted (hashed) password */ -class AttributeOneWayPassword extends AttributeDefinition +class AttributeOneWayPassword extends AttributeDefinition implements iAttributeNoGroupBy { const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW; diff --git a/core/dbsearch.class.php b/core/dbsearch.class.php index e59493d80..f9de30d76 100644 --- a/core/dbsearch.class.php +++ b/core/dbsearch.class.php @@ -1186,6 +1186,30 @@ abstract class DBSearch } } } + + if (is_array($aGroupByExpr)) + { + foreach($aGroupByExpr as $sAlias => $oGroupByExp) + { + /** @var \Expression $oGroupByExp */ + + $aFields = $oGroupByExp->ListRequiredFields(); + foreach($aFields as $sFieldAlias) + { + $aMatches = array(); + if (preg_match('/^([^.]+)\\.([^.]+)$/', $sFieldAlias, $aMatches)) + { + $sFieldClass = $this->GetClassName($aMatches[1]); + $oAttDef = MetaModel::GetAttributeDef($sFieldClass, $aMatches[2]); + if ( $oAttDef instanceof iAttributeNoGroupBy) + { + throw new Exception("Grouping on '$sFieldClass' fields is not supported."); + } + } + } + } + } + $oSQLQuery = $oSearch->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, null, $aSelectExpr); $oSQLQuery->SetSourceOQL($oSearch->ToOQL()); diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 5d4a4215f..6206470ee 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -457,6 +457,7 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Error:ObjectsAlreadyDeleted' => 'Error: objects have already been deleted!', 'UI:Error:BulkDeleteNotAllowedOn_Class' => 'You are not allowed to perform a bulk delete of objects of class %1$s', 'UI:Error:DeleteNotAllowedOn_Class' => 'You are not allowed to delete objects of class %1$s', + 'UI:Error:ReadNotAllowedOn_Class' => 'You are not allowed to view objects of class %1$s', 'UI:Error:BulkModifyNotAllowedOn_Class' => 'You are not allowed to perform a bulk update of objects of class %1$s', 'UI:Error:ObjectAlreadyCloned' => 'Error: the object has already been cloned!', 'UI:Error:ObjectAlreadyCreated' => 'Error: the object has already been created!', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index db43f78bf..0bdfdb57b 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -439,7 +439,8 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI:Error:ObjectCannotBeUpdated' => 'Erreur: l\'objet ne peut pas être mis à jour.', 'UI:Error:ObjectsAlreadyDeleted' => 'Erreur: les objets ont déjà été supprimés !', 'UI:Error:BulkDeleteNotAllowedOn_Class' => 'Vous n\'êtes pas autorisé à faire une suppression massive sur les objets de type %1$s', - 'UI:Error:DeleteNotAllowedOn_Class' => 'Vous n\'êtes pas autorisé supprimer des objets de type %1$s', + 'UI:Error:DeleteNotAllowedOn_Class' => 'Vous n\'êtes pas autorisé à supprimer des objets de type %1$s', + 'UI:Error:ReadNotAllowedOn_Class' => 'Vous n\'êtes pas autorisé à voir des objets de type %1$s', 'UI:Error:BulkModifyNotAllowedOn_Class' => 'Vous n\'êtes pas autorisé à faire une modification massive sur les objets de type %1$s', 'UI:Error:ObjectAlreadyCloned' => 'Erreur: l\'objet a déjà été dupliqué !', 'UI:Error:ObjectAlreadyCreated' => 'Erreur: l\'objet a déjà été créé !',