mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-24 11:08:45 +02:00
494 lines
18 KiB
PHP
494 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Copyright (C) 2013-2020 Combodo SARL
|
|
*
|
|
* This file is part of iTop.
|
|
*
|
|
* iTop is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* iTop is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
*/
|
|
|
|
namespace Combodo\iTop\Portal\Controller;
|
|
|
|
use AttributeExternalKey;
|
|
use AttributeLinkedSetIndirect;
|
|
use BinaryExpression;
|
|
use Combodo\iTop\Portal\Brick\AbstractBrick;
|
|
use Combodo\iTop\Portal\Brick\BrowseBrick;
|
|
use Combodo\iTop\Portal\Helper\BrowseBrickHelper;
|
|
use DBObjectSearch;
|
|
use DBObjectSet;
|
|
use DBSearch;
|
|
use FieldExpression;
|
|
use IssueLog;
|
|
use LogChannels;
|
|
use MetaModel;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use VariableExpression;
|
|
|
|
/**
|
|
* Class BrowseBrickController
|
|
*
|
|
* @package Combodo\iTop\Portal\Controller
|
|
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
|
|
* @since 2.3.0
|
|
*/
|
|
class BrowseBrickController extends BrickController
|
|
{
|
|
/**
|
|
* @param \Symfony\Component\HttpFoundation\Request $oRequest
|
|
* @param string $sBrickId
|
|
* @param string $sBrowseMode
|
|
* @param string $sDataLoading
|
|
*
|
|
* @return \Symfony\Component\HttpFoundation\Response
|
|
*
|
|
* @throws \CoreException
|
|
* @throws \CoreUnexpectedValue
|
|
* @throws \DictExceptionMissingString
|
|
* @throws \MissingQueryArgument
|
|
* @throws \MySQLException
|
|
* @throws \MySQLHasGoneAwayException
|
|
* @throws \OQLException
|
|
* @throws \Exception
|
|
*/
|
|
public function DisplayAction(Request $oRequest, $sBrickId, $sBrowseMode = null, $sDataLoading = null)
|
|
{
|
|
$sPortalId = $this->getParameter('combodo.portal.instance.id');
|
|
|
|
/** @var \Combodo\iTop\Portal\Helper\BrowseBrickHelper $oBrowseBrickHelper */
|
|
$oBrowseBrickHelper = $this->get('browse_brick');
|
|
/** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */
|
|
$oRequestManipulator = $this->get('request_manipulator');
|
|
/** @var \Combodo\iTop\Portal\Helper\BrickControllerHelper */
|
|
$oBrickControllerHelper = $this->get('brick_controller_helper');
|
|
|
|
/** @var \Combodo\iTop\Portal\Brick\BrowseBrick $oBrick */
|
|
$oBrick = $this->get('brick_collection')->getBrickById($sBrickId);
|
|
|
|
// Getting available browse modes
|
|
$aBrowseModes = $oBrick->GetAvailablesBrowseModes();
|
|
$aBrowseButtons = array_keys($aBrowseModes);
|
|
// Getting current browse mode (First from router parameter, then default brick value)
|
|
$sBrowseMode = (!empty($sBrowseMode)) ? $sBrowseMode : $oBrick->GetDefaultBrowseMode();
|
|
// Getting current dataloading mode (First from router parameter, then query parameter, then default brick value)
|
|
$sDataLoading = ($sDataLoading !== null) ? $sDataLoading : $oRequestManipulator->ReadParam('sDataLoading',
|
|
$oBrick->GetDataLoading());
|
|
// Getting search value
|
|
$sRawSearchValue = $oRequestManipulator->ReadParam('sSearchValue', '');
|
|
$sSearchValue = html_entity_decode($sRawSearchValue);
|
|
if (strlen($sSearchValue) > 0)
|
|
{
|
|
$sDataLoading = AbstractBrick::ENUM_DATA_LOADING_LAZY;
|
|
}
|
|
|
|
$aData = array();
|
|
$aLevelsProperties = array();
|
|
$aLevelsClasses = array();
|
|
$oBrowseBrickHelper->TreeToFlatLevelsProperties($oBrick->GetLevels(), $aLevelsProperties);
|
|
|
|
// Consistency checks
|
|
if (!in_array($sBrowseMode, array_keys($aBrowseModes)))
|
|
{
|
|
throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR,
|
|
'Browse brick "'.$sBrickId.'" : Unknown browse mode "'.$sBrowseMode.'", availables are '.implode(' / ',
|
|
array_keys($aBrowseModes)));
|
|
}
|
|
if (empty($aLevelsProperties))
|
|
{
|
|
throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Browse brick "'.$sBrickId.'" : No levels to display.');
|
|
}
|
|
|
|
// Building DBObjectSearch
|
|
$oQuery = null;
|
|
// ... In this case only we have to build a specific query for the current level only
|
|
if (in_array($sBrowseMode, array(
|
|
BrowseBrick::ENUM_BROWSE_MODE_TREE,
|
|
BrowseBrick::ENUM_BROWSE_MODE_MOSAIC,
|
|
)) && ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY))
|
|
{
|
|
// Will be handled later in the pagination part
|
|
}
|
|
// .. Otherwise
|
|
else
|
|
{
|
|
// We iterate (in reverse mode /!\) over the levels to build the whole query, starting from the bottom
|
|
$aLevelsPropertiesKeys = array_keys($aLevelsProperties);
|
|
$iLoopMax = count($aLevelsPropertiesKeys) - 1;
|
|
$oFullBinExpr = null;
|
|
for ($i = $iLoopMax; $i >= 0; $i--)
|
|
{
|
|
// Retrieving class alias for all depth
|
|
array_unshift($aLevelsClasses, $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->GetClassAlias());
|
|
|
|
// Joining queries from bottom-up
|
|
if ($i < $iLoopMax)
|
|
{
|
|
$aRealiasingMap = array();
|
|
$oParentAtt = MetaModel::GetAttributeDef($aLevelsProperties[$aLevelsPropertiesKeys[$i + 1]]['search']->GetClass(), $aLevelsProperties[$aLevelsPropertiesKeys[$i + 1]]['parent_att']);
|
|
// If we work on a n:n link
|
|
if($oParentAtt instanceof AttributeLinkedSetIndirect)
|
|
{
|
|
// Create a DBSearch from Link class
|
|
$oSubSearch = new DBObjectSearch($oParentAtt->GetLinkedClass());
|
|
// Join it to the bottom query
|
|
$oSubSearch = $oSubSearch->Join($aLevelsProperties[$aLevelsPropertiesKeys[$i + 1]]['search'],
|
|
DBSearch::JOIN_POINTING_TO, $oParentAtt->GetExtKeyToMe(), TREE_OPERATOR_EQUALS, $aRealiasingMap);
|
|
// Join our Link class + bottom query to the up query
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search'] = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search'] ->Join($oSubSearch, DBSearch::JOIN_REFERENCED_BY,
|
|
$oParentAtt->GetExtKeyToRemote(), TREE_OPERATOR_EQUALS, $aRealiasingMap);
|
|
}
|
|
else
|
|
{
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search'] = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->Join($aLevelsProperties[$aLevelsPropertiesKeys[$i + 1]]['search'],
|
|
DBSearch::JOIN_REFERENCED_BY, $aLevelsProperties[$aLevelsPropertiesKeys[$i + 1]]['parent_att'],
|
|
TREE_OPERATOR_EQUALS, $aRealiasingMap);
|
|
}
|
|
foreach ($aLevelsPropertiesKeys as $sLevelAlias)
|
|
{
|
|
if (array_key_exists($sLevelAlias, $aRealiasingMap))
|
|
{
|
|
/** @since 2.7.2 */
|
|
foreach ($aRealiasingMap[$sLevelAlias] as $sAliasToChange)
|
|
{
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->RenameAlias($sAliasToChange, $sLevelAlias);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adding search clause
|
|
// Note : For know the search is naive and looks only for the exact match. It doesn't search for words separately
|
|
if (strlen($sSearchValue) > 0)
|
|
{
|
|
// - Cleaning the search value by exploding and trimming spaces
|
|
$aExplodedSearchValues = explode(' ', $sSearchValue);
|
|
$aSearchValues = [];
|
|
foreach ($aExplodedSearchValues as $sValue) {
|
|
if (strlen($sValue) > 0) {
|
|
$aSearchValues[] = $sValue;
|
|
}
|
|
}
|
|
|
|
// - Retrieving fields to search
|
|
$aSearchFields = array($aLevelsProperties[$aLevelsPropertiesKeys[$i]]['name_att']);
|
|
if (!empty($aLevelsProperties[$aLevelsPropertiesKeys[$i]]['fields']))
|
|
{
|
|
$sTmpFieldClass = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->GetClass();
|
|
foreach ($aLevelsProperties[$aLevelsPropertiesKeys[$i]]['fields'] as $aTmpField)
|
|
{
|
|
$sTmpFieldAttCode = $aTmpField['code'];
|
|
|
|
// Skip invalid attcodes
|
|
if(!MetaModel::IsValidAttCode($sTmpFieldClass, $sTmpFieldAttCode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// For external key, force search on the friendlyname instead of the ID.
|
|
// This should be addressed more globally with the bigger issue, see N°1970
|
|
$oTmpFieldAttDef = MetaModel::GetAttributeDef($sTmpFieldClass, $sTmpFieldAttCode);
|
|
if($oTmpFieldAttDef instanceof AttributeExternalKey)
|
|
{
|
|
$sTmpFieldAttCode .= '_friendlyname';
|
|
}
|
|
|
|
$aSearchFields[] = $sTmpFieldAttCode;
|
|
}
|
|
}
|
|
// - Building query for the search values parts
|
|
$oLevelBinExpr = null;
|
|
$iFieldLoopMax = count($aSearchFields) - 1;
|
|
$iSearchLoopMax = count($aSearchValues) - 1;
|
|
for ($j = 0; $j <= $iFieldLoopMax; $j++)
|
|
{
|
|
$sTmpFieldAttCode = $aSearchFields[$j];
|
|
$oFieldBinExpr = null;
|
|
//$oFieldBinExpr = new BinaryExpression(new FieldExpression($aSearchFields[$j], $aLevelsPropertiesKeys[$i]), )
|
|
|
|
for ($k = 0; $k <= $iSearchLoopMax; $k++)
|
|
{
|
|
$oSearchBinExpr = new BinaryExpression(new FieldExpression($sTmpFieldAttCode, $aLevelsPropertiesKeys[$i]),
|
|
'LIKE', new VariableExpression('search_value_'.$k));
|
|
if ($k === 0)
|
|
{
|
|
$oFieldBinExpr = $oSearchBinExpr;
|
|
}
|
|
else
|
|
{
|
|
$oFieldBinExpr = new BinaryExpression($oFieldBinExpr, 'AND', $oSearchBinExpr);
|
|
}
|
|
}
|
|
|
|
if ($j === 0)
|
|
{
|
|
$oLevelBinExpr = $oFieldBinExpr;
|
|
}
|
|
else
|
|
{
|
|
$oLevelBinExpr = new BinaryExpression($oLevelBinExpr, 'OR', $oFieldBinExpr);
|
|
}
|
|
}
|
|
|
|
// - Building query for the level
|
|
if ($i === $iLoopMax)
|
|
{
|
|
$oFullBinExpr = $oLevelBinExpr;
|
|
}
|
|
else
|
|
{
|
|
$oFullBinExpr = new BinaryExpression($oFullBinExpr, 'OR', $oLevelBinExpr);
|
|
}
|
|
|
|
// - Adding it to the query when complete
|
|
if ($i === 0)
|
|
{
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->AddConditionExpression($oFullBinExpr);
|
|
}
|
|
}
|
|
|
|
// Setting selected classes and binding parameters
|
|
if ($i === 0)
|
|
{
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->SetSelectedClasses($aLevelsClasses);
|
|
|
|
if (strlen($sSearchValue) > 0)
|
|
{
|
|
// Note : This could be way more simpler if we had a SetInternalParam($sParam, $value) verb
|
|
$aQueryParams = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->GetInternalParams();
|
|
// Note : $iSearchloopMax was initialized on the previous loop
|
|
for ($j = 0; $j <= $iSearchLoopMax; $j++) {
|
|
$aQueryParams['search_value_'.$j] = '%'.$aSearchValues[$j].'%';
|
|
}
|
|
$aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->SetInternalParams($aQueryParams);
|
|
}
|
|
}
|
|
}
|
|
$oQuery = $aLevelsProperties[$aLevelsPropertiesKeys[0]]['search'];
|
|
|
|
// Testing appropriate data loading mode if we are in auto
|
|
if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_AUTO) {
|
|
// - Check how many records there is.
|
|
// - Update $sDataLoading with its new value regarding the number of record and the threshold
|
|
$oCountSet = new DBObjectSet($oQuery);
|
|
$fThreshold = (float)MetaModel::GetModuleSetting($sPortalId,
|
|
'lazy_loading_threshold');
|
|
$sDataLoading = ($oCountSet->Count() > $fThreshold) ? AbstractBrick::ENUM_DATA_LOADING_LAZY : AbstractBrick::ENUM_DATA_LOADING_FULL;
|
|
unset($oCountSet);
|
|
}
|
|
}
|
|
|
|
// Setting query pagination if needed
|
|
if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY)
|
|
{
|
|
switch ($sBrowseMode)
|
|
{
|
|
case BrowseBrick::ENUM_BROWSE_MODE_LIST:
|
|
// Retrieving parameters
|
|
$iPageNumber = (int)$oRequestManipulator->ReadParam('iPageNumber', 1, FILTER_SANITIZE_NUMBER_INT);
|
|
$iListLength = (int)$oRequestManipulator->ReadParam('iListLength', BrowseBrick::DEFAULT_LIST_LENGTH,
|
|
FILTER_SANITIZE_NUMBER_INT);
|
|
|
|
// Getting total records number
|
|
$oCountSet = new DBObjectSet($oQuery);
|
|
$aData['recordsTotal'] = $oCountSet->Count();
|
|
$aData['recordsFiltered'] = $oCountSet->Count();
|
|
unset($oCountSet);
|
|
|
|
$oSet = new DBObjectSet($oQuery);
|
|
$oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
|
|
|
|
break;
|
|
case BrowseBrick::ENUM_BROWSE_MODE_TREE:
|
|
case BrowseBrick::ENUM_BROWSE_MODE_MOSAIC:
|
|
// Retrieving parameters
|
|
$sLevelAlias = $oRequestManipulator->ReadParam('sLevelAlias', '');
|
|
$sNodeId = $oRequestManipulator->ReadParam('sNodeId', '');
|
|
|
|
// If no values for those parameters, we might be loading page in lazy mode for the first time, therefore the URL doesn't have those information.
|
|
if (empty($sLevelAlias))
|
|
{
|
|
reset($aLevelsProperties);
|
|
$oQuery = $aLevelsProperties[key($aLevelsProperties)]['search'];
|
|
if (!empty($sNodeId))
|
|
{
|
|
$oQuery->AddCondition('id', $sNodeId);
|
|
}
|
|
}
|
|
// Else we need to find the OQL for that particular level
|
|
else
|
|
{
|
|
$bFoundLevel = false;
|
|
foreach ($aLevelsProperties as $aLevelProperties)
|
|
{
|
|
if ($aLevelProperties['alias'] === $sLevelAlias)
|
|
{
|
|
if (isset($aLevelProperties['levels']) && !empty($aLevelProperties['levels']) && isset($aLevelsProperties[$aLevelProperties['levels'][0]]))
|
|
{
|
|
$oQuery = $aLevelsProperties[$aLevelProperties['levels'][0]]['search'];
|
|
if (!empty($sNodeId))
|
|
{
|
|
$sParentAttCode = $aLevelsProperties[$aLevelProperties['levels'][0]]['parent_att'];
|
|
$oParentAtt = MetaModel::GetAttributeDef($oQuery->GetClass(), $sParentAttCode);
|
|
if($oParentAtt instanceof AttributeLinkedSetIndirect)
|
|
{
|
|
$oQuery->AddConditionAdvanced($sParentAttCode.'->'.$oParentAtt->GetExtKeyToRemote(), $sNodeId);
|
|
}
|
|
else
|
|
{
|
|
$oQuery->AddCondition($sParentAttCode, $sNodeId);
|
|
}
|
|
}
|
|
$bFoundLevel = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$bFoundLevel)
|
|
{
|
|
throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR,
|
|
'Browse brick "'.$sBrickId.'" : Level alias "'.$sLevelAlias.'" is not defined for that brick.');
|
|
}
|
|
}
|
|
|
|
$oSet = new DBObjectSet($oQuery);
|
|
break;
|
|
|
|
default:
|
|
// We should never be there. If there is an other browse mode for that brick :
|
|
// - If it's from a custom brick extension, it should be handle by the extension router/controller
|
|
// - If it's from a base brick, it should be handle in a case above this one
|
|
// - If none of the previous statements was done, this fail safe will load all data as it's not able to know how to handle the pagination
|
|
$oSet = new DBObjectSet($oQuery);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$oSet = new DBObjectSet($oQuery);
|
|
}
|
|
|
|
// Optimizing the ObjectSet to retrieve only necessary columns
|
|
$aColumnAttrs = array();
|
|
foreach ($oSet->GetFilter()->GetSelectedClasses() as $sTmpClassAlias => $sTmpClassName)
|
|
{
|
|
if (isset($aLevelsProperties[$sTmpClassAlias]))
|
|
{
|
|
$aTmpLevelProperties = $aLevelsProperties[$sTmpClassAlias];
|
|
// Mandatory main attribute
|
|
$aTmpColumnAttrs = array($aTmpLevelProperties['name_att']);
|
|
// Optional attributes, only if in list mode
|
|
if ($sBrowseMode === BrowseBrick::ENUM_BROWSE_MODE_LIST)
|
|
{
|
|
foreach ($aTmpLevelProperties['fields'] as $aTmpField)
|
|
{
|
|
$aTmpColumnAttrs[] = $aTmpField['code'];
|
|
}
|
|
}
|
|
// Optional attributes
|
|
foreach (BrowseBrickHelper::OPTIONAL_ATTRIBUTES as $sOptionalAttribute)
|
|
{
|
|
if ($aTmpLevelProperties[$sOptionalAttribute] !== null)
|
|
{
|
|
$aTmpColumnAttrs[] = $aTmpLevelProperties[$sOptionalAttribute];
|
|
}
|
|
}
|
|
|
|
$aColumnAttrs[$sTmpClassAlias] = $aTmpColumnAttrs;
|
|
}
|
|
}
|
|
// Note: $aColumnAttrs already contains array of aliases => attcodes
|
|
$oSet->OptimizeColumnLoad($aColumnAttrs);
|
|
|
|
// Setting specified column sort, setting default datamodel one otherwise
|
|
$aSortedParams = $oBrickControllerHelper->ExtractSortParams();
|
|
if (!empty($aSortedParams))
|
|
{
|
|
$oSet->SetOrderBy($aSortedParams);
|
|
}
|
|
else
|
|
{
|
|
$oSet->SetOrderByClasses();
|
|
}
|
|
// Retrieving results and organizing them for templating
|
|
$aItems = array();
|
|
while ($aCurrentRow = $oSet->FetchAssoc())
|
|
{
|
|
switch ($sBrowseMode)
|
|
{
|
|
case BrowseBrick::ENUM_BROWSE_MODE_TREE:
|
|
case BrowseBrick::ENUM_BROWSE_MODE_MOSAIC:
|
|
$oBrowseBrickHelper->AddToTreeItems($aItems, $aCurrentRow, $aLevelsProperties, null);
|
|
break;
|
|
|
|
case BrowseBrick::ENUM_BROWSE_MODE_LIST:
|
|
default:
|
|
$aItems[] = $oBrowseBrickHelper->AddToFlatItems($aCurrentRow, $aLevelsProperties);
|
|
break;
|
|
}
|
|
}
|
|
|
|
IssueLog::Debug('Portal BrowseBrick query', LogChannels::PORTAL, array(
|
|
'sPortalId' => $sPortalId,
|
|
'sBrickId' => $sBrickId,
|
|
'oql' => $oSet->GetFilter()->ToOQL(),
|
|
));
|
|
|
|
|
|
// Preparing response
|
|
if ($oRequest->isXmlHttpRequest()) {
|
|
$aData = $aData + array(
|
|
'data' => $aItems,
|
|
'levelsProperties' => $aLevelsProperties,
|
|
);
|
|
$oResponse = new JsonResponse($aData);
|
|
} else {
|
|
$aData = $aData + array(
|
|
'oBrick' => $oBrick,
|
|
'sBrickId' => $sBrickId,
|
|
'sBrowseMode' => $sBrowseMode,
|
|
'aBrowseButtons' => $aBrowseButtons,
|
|
'sSearchValue' => $sRawSearchValue,
|
|
'sDataLoading' => $sDataLoading,
|
|
'aItems' => json_encode($aItems),
|
|
'iItemsCount' => count($aItems),
|
|
'aLevelsProperties' => json_encode($aLevelsProperties),
|
|
'iDefaultLengthList' => $oBrick->GetDefaultListLength(),
|
|
);
|
|
|
|
// Note : To extend this brick's template, depending on what you want to do :
|
|
// a) Modify the whole template :
|
|
// - Create a template and specify it in the brick configuration
|
|
// b) Add a new browse mode :
|
|
// - Create a template for that browse mode,
|
|
// - Add the mode to those available in the brick configuration,
|
|
// - Create a router and add a route for the new browse mode
|
|
if ($oBrick->GetPageTemplatePath() !== null)
|
|
{
|
|
$sTemplatePath = $oBrick->GetPageTemplatePath();
|
|
}
|
|
else
|
|
{
|
|
$sTemplatePath = $aBrowseModes[$sBrowseMode]['template'];
|
|
}
|
|
$oResponse = $this->render($sTemplatePath, $aData);
|
|
}
|
|
|
|
return $oResponse;
|
|
}
|
|
}
|