Files
iTop/datamodels/2.x/itop-portal-base/portal/src/Controller/ManageBrickController.php
2021-06-25 17:13:35 +02:00

1083 lines
35 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 AttributeDate;
use AttributeDateTime;
use AttributeDefinition;
use AttributeExternalKey;
use AttributeImage;
use AttributeSet;
use AttributeTagSet;
use BinaryExpression;
use BulkExport;
use CMDBSource;
use Combodo\iTop\Portal\Brick\AbstractBrick;
use Combodo\iTop\Portal\Brick\ManageBrick;
use Combodo\iTop\Portal\Helper\ApplicationHelper;
use DBObject;
use DBObjectSet;
use DBSearch;
use DBUnionSearch;
use Dict;
use Exception;
use FieldExpression;
use iPopupMenuExtension;
use IssueLog;
use JSButtonItem;
use LogChannels;
use MetaModel;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use UnaryExpression;
use URLButtonItem;
/**
* Class ManageBrickController
*
* @package Combodo\iTop\Portal\Controller
* @author Bruno Da Silva <bruno.dasilva@combodo.com>
* @author Eric Espie <eric.espie@combodo.com>
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @author Pierre Goiffon <pierre.goiffon@combodo.com>
* @since 2.3.0
*/
class ManageBrickController extends BrickController
{
/** @var string EXCEL_EXPORT_TEMPLATE_PATH */
const EXCEL_EXPORT_TEMPLATE_PATH = 'itop-portal-base/portal/templates/bricks/manage/popup-export-excel.html.twig';
/**
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sBrickId
* @param string $sGroupingTab
* @param string $sDisplayMode
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \DictExceptionMissingString
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function DisplayAction(Request $oRequest, $sBrickId, $sGroupingTab, $sDisplayMode = null)
{
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
/** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
if (is_null($sDisplayMode)) {
$sDisplayMode = $oBrick->GetDefaultDisplayMode();
}
$aData = $this->GetData($oRequest, $sBrickId, $sGroupingTab, $oBrick::AreDetailsNeededForDisplayMode($sDisplayMode));
$aExportFields = $oBrick->GetExportFields();
$aData = $aData + array(
'sDisplayMode' => $sDisplayMode,
'bCanExport' => !empty($aExportFields),
'iDefaultListLength' => $oBrick->GetDefaultListLength(),
);
// Preparing response
if ($oRequest->isXmlHttpRequest()) {
$oResponse = new JsonResponse($aData);
}
else
{
$sLayoutTemplate = $oBrick::GetPageTemplateFromDisplayMode($sDisplayMode);
$oResponse = $this->render($sLayoutTemplate, $aData);
}
return $oResponse;
}
/**
* Method for the brick's tile on home page
*
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sBrickId
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
*/
public function TileAction(Request $oRequest, $sBrickId)
{
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
/** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
try
{
$aData = $this->GetData($oRequest, $sBrickId, null);
}
catch (Exception $e)
{
// TODO Default values
$aData = array();
}
return $this->render($oBrick->GetTileTemplatePath(), $aData);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sBrickId
* @param string $sGroupingTab
* @param string $sGroupingArea
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \DictExceptionMissingString
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
* @throws \Exception
*/
public function ExcelExportStartAction(Request $oRequest, $sBrickId, $sGroupingTab, $sGroupingArea)
{
/** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */
$oScopeValidator = $this->get('scope_validator');
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
/** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
$oQuery = DBSearch::FromOQL($oBrick->GetOql());
$sClass = $oQuery->GetClass();
$aData = $this->GetData($oRequest, $sBrickId, $sGroupingTab, true);
if (isset($aData['aQueries']) && count($aData['aQueries']) === 1)
{
$aQueries = $aData['aQueries'];
reset($aQueries);
$sKey = key($aQueries);
$oSearch = $aData['aQueries'][$sKey];
}
else
{
$oScopeValidator->AddScopeToQuery($oQuery, $sClass);
$aData = array();
$this->ManageSearchValue($aData, $oQuery, $sClass);
// Grouping tab
if ($oBrick->HasGroupingTabs())
{
$aGroupingTabs = $oBrick->GetGroupingTabs();
// If tabs are made of the distinct values of an attribute, we have a find them via a query
if ($oBrick->IsGroupingTabsByDistinctValues())
{
$sGroupingTabAttCode = $aGroupingTabs['attribute'];
$aGroupingTabsValues = $this->GroupByAttribute($oQuery, $sGroupingTabAttCode, $oBrick);
$oQuery = $oQuery->Intersect($aGroupingTabsValues[$sGroupingTab]['condition']);
}
else
{
foreach ($aGroupingTabs['groups'] as $aGroup)
{
if ($aGroup['id'] === $sGroupingTab)
{
$oConditionQuery = $oQuery->Intersect(DBSearch::FromOQL($aGroup['condition']));
$oQuery = $oQuery->Intersect($oConditionQuery);
break;
}
}
}
}
// Finalclass
$oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GARE');
$oExpression = new BinaryExpression(new FieldExpression('finalclass', 'GARE'), '=',
new UnaryExpression($sGroupingArea));
$oConditionQuery->AddConditionExpression($oExpression);
/** @var DBSearch $oSearch */
$oSearch = $oQuery->Intersect($oConditionQuery);
}
$aColumnsAttrs = $oBrick->GetExportFields();
$aFields = array();
$sTitleAttrCode = 'friendlyname';
if (!in_array($sTitleAttrCode, $aColumnsAttrs))
{
$aFields[] = $sTitleAttrCode;
}
foreach ($aColumnsAttrs as $sAttCode)
{
$oAttributeDef = MetaModel::GetAttributeDef($sGroupingArea, $sAttCode);
if ($oAttributeDef->IsExternalKey(EXTKEY_ABSOLUTE))
{
$aFields[] = $sAttCode.'_friendlyname';
}
else
{
$aFields[] = $sAttCode;
}
}
$sFields = implode(',', $aFields);
$sFormat = 'xlsx';
$oSearch->UpdateContextFromUser();
$oExporter = BulkExport::FindExporter($sFormat, $oSearch);
$oExporter->SetObjectList($oSearch);
$oExporter->SetFormat($sFormat);
$oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE);
$oExporter->SetLocalizeOutput(true);
$oExporter->SetFields($sFields);
$aData = array(
'oBrick' => $oBrick,
'sBrickId' => $sBrickId,
'sToken' => $oExporter->SaveState(),
);
return $this->render(static::EXCEL_EXPORT_TEMPLATE_PATH, $aData);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sBrickId
* @param string $sGroupingTab
* @param bool $bNeedDetails
*
* @return array
*
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \DictExceptionMissingString
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
* @throws \Exception
*/
public function GetData(Request $oRequest, $sBrickId, $sGroupingTab, $bNeedDetails = false)
{
/** @var string $sPortalId */
$sPortalId = $this->getParameter('combodo.portal.instance.id');
/** @var \Combodo\iTop\Portal\Routing\UrlGenerator $oUrlGenerator */
$oUrlGenerator = $this->get('url_generator');
/** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */
$oRequestManipulator = $this->get('request_manipulator');
/** @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\Helper\BrickControllerHelper */
$oBrickControllerHelper = $this->get('brick_controller_helper');
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
/** @var \Combodo\iTop\Portal\Brick\ManageBrick $oBrick */
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
$aData = array();
$aGroupingTabsValues = array();
$aGroupingAreasValues = array();
$aQueries = array();
$bHasScope = true;
// Getting current data loading mode (First from router parameter, then query parameter, then default brick value)
$sDataLoading = $oRequestManipulator->ReadParam('sDataLoading', $oBrick->GetDataLoading());
// - Retrieving the grouping areas to display
$sGroupingArea = $oRequestManipulator->ReadParam('sGroupingArea', '');
if (!empty($sGroupingArea))
{
$bNeedDetails = true;
}
// Getting area columns properties
$aColumnsAttrs = $oBrick->GetFields();
// Adding friendlyname attribute to the list if not already in it
$sTitleAttrCode = 'friendlyname';
if (($sTitleAttrCode !== null) && !in_array($sTitleAttrCode, $aColumnsAttrs))
{
$aColumnsAttrs = array_merge(array($sTitleAttrCode), $aColumnsAttrs);
}
// Starting to build query
$oQuery = DBSearch::FromOQL($oBrick->GetOql());
$sClass = $oQuery->GetClass();
$sIconURL = MetaModel::GetClassIcon($sClass, false);
// Preparing tabs
// - We need to retrieve distinct values for the grouping attribute
$iCount = 0;
if ($oBrick->HasGroupingTabs())
{
$aGroupingTabs = $oBrick->GetGroupingTabs();
// If tabs are made of the distinct values of an attribute, we have a find them via a query
if ($oBrick->IsGroupingTabsByDistinctValues())
{
$sGroupingTabAttCode = $aGroupingTabs['attribute'];
$aGroupingTabsValues = $this->GroupByAttribute($oQuery, $sGroupingTabAttCode, $oBrick);
foreach ($aGroupingTabsValues as $aResult)
{
$iCount += $aResult['count'];
}
}
// Otherwise we create the tabs from the SQL expressions
else
{
$aConditionQueryGrouping = array();
foreach ($aGroupingTabs['groups'] as $aGroup)
{
$oDBSearch = DBSearch::FromOQL($aGroup['condition']);
$oConditionQuery = $oQuery->Intersect($oDBSearch);
// - Restricting query to scope
array_push($aConditionQueryGrouping,$oDBSearch);
$bHasScope = $oScopeValidator->AddScopeToQuery($oConditionQuery, $oConditionQuery->GetClass());
if ($bHasScope)
{
// - Building ObjectSet
$oConditionSet = new DBObjectSet($oConditionQuery);
$iGroupCount = $oConditionSet->Count();
}
else
{
$oConditionSet = null;
$iGroupCount = 0;
}
$aGroupingTabsValues[$aGroup['id']] = array(
'value' => $aGroup['id'],
'label' => Dict::S($aGroup['title']),
'label_html' => Dict::S($aGroup['title']),
'description' => array_key_exists('description',$aGroup) ? Dict::S($aGroup['description']) : null,
'condition' => $oConditionQuery,
'count' => $iGroupCount,
);
}
try
{
$oConditionQuery = $oQuery->Intersect(new DBUnionSearch($aConditionQueryGrouping));
$bHasScope = $oScopeValidator->AddScopeToQuery($oConditionQuery, $oConditionQuery->GetClass());
if ($bHasScope)
{
// - Building ObjectSet
$oConditionSet = new DBObjectSet($oConditionQuery);
$iCount = $oConditionSet->Count();
}
else
{
$oConditionSet = null;
$iCount = 0;
}
}
catch (Exception $e){
$oConditionSet = null;
$iCount = -1;
}
}
}
else
{
$oConditionQuery = $this->GetScopedQuery($oBrick, $sClass);
if (!is_null($oConditionQuery))
{
$oSet = new DBObjectSet($oConditionQuery);
$iCount = $oSet->Count();
}
}
// - Retrieving the current grouping tab to display if necessary and altering the query to do so
if (empty($sGroupingTab))
{
if ($oBrick->HasGroupingTabs())
{
reset($aGroupingTabsValues);
$sGroupingTab = key($aGroupingTabsValues);
if ($aGroupingTabsValues[$sGroupingTab]['condition'] !== null)
{
$oQuery = $aGroupingTabsValues[$sGroupingTab]['condition']->DeepClone();
}
}
}
else
{
if ($aGroupingTabsValues[$sGroupingTab]['condition'] !== null)
{
$oQuery = $aGroupingTabsValues[$sGroupingTab]['condition']->DeepClone();
}
}
// Retrieve the current tab description to set the page subtitle (if it exists)
$aData['sBrickSubtitle'] = !empty($aGroupingTabsValues[$sGroupingTab]['description']) ? $aGroupingTabsValues[$sGroupingTab]['description'] : null;
// - Transforming search sort params to OQL format
$aSortedParams = $oBrickControllerHelper->ExtractSortParams();
// Preparing areas
// - We need to retrieve distinct values for the grouping attribute
// Note : Will have to be changed when we consider grouping on something else than the finalclass
$sParentAlias = $oQuery->GetClassAlias();
if ($bNeedDetails)
{
$sGroupingAreaAttCode = 'finalclass';
// For root classes
if (MetaModel::IsValidAttCode($sClass, $sGroupingAreaAttCode))
{
$oDistinctQuery = $this->GetScopedQuery($oBrick, $sClass);
// Adding grouping conditions
$oFieldExp = new FieldExpression($sGroupingAreaAttCode, $oDistinctQuery->GetClassAlias());
$sDistinctSql = $oDistinctQuery->MakeGroupByQuery(array(), array('grouped_by_1' => $oFieldExp), true);
$aDistinctResults = CMDBSource::QueryToArray($sDistinctSql);
foreach ($aDistinctResults as $aDistinctResult)
{
$oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GARE');
$oExpression = new BinaryExpression(new FieldExpression($sGroupingAreaAttCode, 'GARE'), '=',
new UnaryExpression($aDistinctResult['grouped_by_1']));
$oConditionQuery->AddConditionExpression($oExpression);
$aGroupingAreasValues[$aDistinctResult['grouped_by_1']] = array(
'value' => $aDistinctResult['grouped_by_1'],
'label' => MetaModel::GetName($aDistinctResult['grouped_by_1']),
// Caution : This works only because we froze the grouping areas on the finalclass attribute.
'condition' => $oConditionQuery,
'count' => $aDistinctResult['_itop_count_'],
);
unset($oConditionQuery);
}
unset($aDistinctResults);
}
// For leaf classes
else
{
$aGroupingAreasValues[$sClass] = array(
'value' => $sClass,
'label' => MetaModel::GetName($sClass),
// Caution : This works only because we froze the grouping areas on the finalclass attribute.
'condition' => null,
'count' => 0,
);
}
// - If specified or lazy loading, we truncate the $aGroupingAreasValues to keep only this one
if (!empty($sGroupingArea))
{
$aGroupingAreasValues = array($sGroupingArea => $aGroupingAreasValues[$sGroupingArea]);
}
// - Preparing the queries
foreach ($aGroupingAreasValues as $sKey => $aGroupingAreasValue)
{
$oAreaQuery = DBSearch::CloneWithAlias($oQuery, $sParentAlias);
if ($aGroupingAreasValue['condition'] !== null)
{
$oAreaQuery = $aGroupingAreasValue['condition']->DeepClone();
}
// Restricting query to allowed scope on each classes
// Note: Will need to moved the scope restriction on queries elsewhere when we consider grouping on something else than finalclass
// Note: We now get view scope instead of edit scope as we allowed users to view/edit objects in the brick regarding their rights
$bHasScope = $oScopeValidator->AddScopeToQuery($oAreaQuery, $aGroupingAreasValue['value']);
if (!$bHasScope)
{
// if no scope apply does not allow any data
$oAreaQuery = null;
}
$aQueries[$sKey] = $oAreaQuery;
}
$aData['aQueries'] = $aQueries;
// Testing appropriate data loading mode if we are in auto
// - For all (html) tables, this doesn't care for the grouping ares (finalclass)
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);
$oCountSet->OptimizeColumnLoad(array($oQuery->GetClassAlias() => array()));
$fThreshold = (float)MetaModel::GetModuleSetting($sPortalId,
'lazy_loading_threshold');
$sDataLoading = ($oCountSet->Count() > $fThreshold) ? AbstractBrick::ENUM_DATA_LOADING_LAZY : AbstractBrick::ENUM_DATA_LOADING_FULL;
unset($oCountSet);
}
// Preparing data sets
$aSets = array();
/** @var DBSearch $oQuery */
foreach ($aQueries as $sKey => $oQuery)
{
// Checking if we have a valid query
if ($oQuery !== null)
{
// - Adding search clause if necessary
$this->ManageSearchValue($aData, $oQuery, $sKey, $aColumnsAttrs);
// Setting query pagination if needed
if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY)
{
// Retrieving parameters
$iPageNumber = (int)$oRequestManipulator->ReadParam('iPageNumber', 1, FILTER_SANITIZE_NUMBER_INT);
$iListLength = (int)$oRequestManipulator->ReadParam('iListLength', ManageBrick::DEFAULT_LIST_LENGTH,
FILTER_SANITIZE_NUMBER_INT);
// Getting total records number
$oCountSet = new DBObjectSet($oQuery);
$oCountSet->OptimizeColumnLoad(array($oQuery->GetClassAlias() => $aColumnsAttrs));
$aData['recordsTotal'] = $oCountSet->Count();
$aData['recordsFiltered'] = $oCountSet->Count();
unset($oCountSet);
$oSet = new DBObjectSet($oQuery);
$oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
}
else
{
$oSet = new DBObjectSet($oQuery);
}
// Setting specified column sort, setting default datamodel one otherwise
if (!empty($aSortedParams))
{
$oSet->SetOrderBy($aSortedParams);
}
else
{
$oSet->SetOrderByClasses();
}
// Adding always_in_tables attributes
$aColumnsToLoad = array($oQuery->GetClassAlias() => $aColumnsAttrs);
foreach ($oQuery->GetSelectedClasses() as $sAlias => $sClassSelected)
{
/** @var AttributeDefinition $oAttDef */
foreach (MetaModel::ListAttributeDefs($sClassSelected) as $sAttCode => $oAttDef)
{
if ($oAttDef->AlwaysLoadInTables())
{
$aColumnsToLoad[$sAlias][] = $sAttCode;
}
}
}
// Note: $aColumnToLoad already contains array of aliases => attcodes
$oSet->OptimizeColumnLoad($aColumnsToLoad);
$oSecurityHelper->PreloadForCache($oSet->GetFilter(),
$aColumnsToLoad[$oQuery->GetClassAlias()] /* preloading only extkeys from the main class */);
$aSets[$sKey] = $oSet;
}
}
// Retrieving and preparing data for rendering
$aGroupingAreasData = array();
$bHasObjectListItemExtension = false;
foreach ($aSets as $sKey => $oSet)
{
// Set properties
$sCurrentClass = $sKey;
// Defining which attribute will open the edition form)
$sMainActionAttrCode = $aColumnsAttrs[0];
// Loading columns definition
$aColumnsDefinition = array();
foreach ($aColumnsAttrs as $sColumnAttr)
{
$oAttDef = MetaModel::GetAttributeDef($sKey, $sColumnAttr);
$aColumnsDefinition[$sColumnAttr] = array(
'title' => $oAttDef->GetLabel(),
'type' => ($oAttDef instanceof AttributeDateTime) ? 'moment-'.$oAttDef->GetFormat()->ToMomentJS() : 'html',
// Special sorting for Date & Time
);
}
// Getting items
$aItems = array();
// ... For each item
/** @var DBObject $oCurrentRow */
while ($oCurrentRow = $oSet->Fetch())
{
$sCurrentObjectClass = get_class($oCurrentRow);
$sCurrentObjectId = $oCurrentRow->GetKey();
// ... Retrieving item's attributes values
$aItemAttrs = array();
foreach ($aColumnsAttrs as $sItemAttr)
{
$aActions = array();
// Set the edit action to the main (first) attribute only
//if ($sItemAttr === $sTitleAttrCode)
if ($sItemAttr === $sMainActionAttrCode)
{
// Checking if we can edit the object
if (($oBrick->GetOpeningMode() === ManageBrick::ENUM_ACTION_EDIT) && $oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY,
$sCurrentClass, $oCurrentRow->GetKey()))
{
$sActionType = ManageBrick::ENUM_ACTION_EDIT;
}
// - Otherwise, check if view is allowed
elseif ($oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sCurrentClass,
$oCurrentRow->GetKey()))
{
$sActionType = ManageBrick::ENUM_ACTION_VIEW;
}
else
{
$sActionType = null;
}
// - Then set allowed action
if ($sActionType !== null)
{
$aActions[] = array(
'type' => $sActionType,
'class' => $sCurrentClass,
'id' => $oCurrentRow->GetKey(),
'opening_target' => $oBrick->GetOpeningTarget(),
);
}
}
/** @var \AttributeDefinition $oAttDef */
$oAttDef = MetaModel::GetAttributeDef($sCurrentClass, $sItemAttr);
$sAttDefClass = get_class($oAttDef);
if ($oAttDef->IsExternalKey())
{
/** @var \AttributeExternalKey $oAttDef */
$sValue = $oCurrentRow->GetAsHTML($sItemAttr.'_friendlyname');
$sSortValue = $oCurrentRow->Get($sItemAttr.'_friendlyname');
// Adding a view action on the external keys
if ($oCurrentRow->Get($sItemAttr) !== $oAttDef->GetNullValue())
{
// Checking if we can view the object
if (($oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $oAttDef->GetTargetClass(),
$oCurrentRow->Get($sItemAttr))))
{
$aActions[] = array(
'type' => ManageBrick::ENUM_ACTION_VIEW,
'class' => $oAttDef->GetTargetClass(),
'id' => $oCurrentRow->Get($sItemAttr),
'opening_target' => $oBrick->GetOpeningTarget(),
);
}
}
}
elseif ($oAttDef instanceof AttributeImage)
{
$oOrmDoc = $oCurrentRow->Get($sItemAttr);
if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty())
{
$sUrl = $oUrlGenerator->generate('p_object_document_display', array(
'sObjectClass' => get_class($oCurrentRow),
'sObjectId' => $oCurrentRow->GetKey(),
'sObjectField' => $sItemAttr,
'cache' => 86400,
));
}
else
{
$sUrl = $oAttDef->Get('default_image');
}
$sValue = '<img src="'.$sUrl.'" />';
$sSortValue = null;
}
elseif ($oAttDef instanceof AttributeTagSet)
{
/** @var \ormTagSet $oSetValues */
$oSetValues = $oCurrentRow->Get($sItemAttr);
$aCodes = $oSetValues->GetTags();
/** @var \AttributeTagSet $oAttDef */
$sValue = $oAttDef->GenerateViewHtmlForValues($aCodes, '', false);
$sSortValue = implode(' ', $aCodes);
}
elseif ($oAttDef instanceof AttributeSet)
{
$oAttDef->SetDisplayLink(false);
$sValue = $oAttDef->GetAsHTML($oCurrentRow->Get($sItemAttr));
$sSortValue = "".$oCurrentRow->Get($sItemAttr);
}
else
{
$sValue = $oAttDef->GetAsHTML($oCurrentRow->Get($sItemAttr));
$sSortValue = $oCurrentRow->Get($sItemAttr);
}
unset($oAttDef);
// For simple fields, we get the raw (stored) value as well
$bExcludeRawValue = false;
foreach (ApplicationHelper::GetAttDefClassesToExcludeFromMarkupMetadataRawValue() as $sAttDefClassToExclude)
{
if (is_a($sAttDefClass, $sAttDefClassToExclude, true))
{
$bExcludeRawValue = true;
break;
}
}
$attValueRaw = ($bExcludeRawValue === false) ? $oCurrentRow->Get($sItemAttr) : null;
$aItemAttrs[$sItemAttr] = array(
'object_class' => $sCurrentObjectClass,
'object_id' => $sCurrentObjectId,
'attribute_code' => $sItemAttr,
'attribute_type' => $sAttDefClass,
'value_raw' => $attValueRaw,
'value_html' => $sValue,
'sort_value' => $sSortValue,
'actions' => $aActions,
);
}
// ... Checking menu extensions
$aItemButtons = array();
/** @var iPopupMenuExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
{
foreach ($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJLISTITEM_ACTIONS, array(
'portal_id' => $sPortalId,
'object' => $oCurrentRow,
)) as $oMenuItem)
{
if (is_object($oMenuItem))
{
if ($oMenuItem instanceof JSButtonItem)
{
$aItemButtons[] = $oMenuItem->GetMenuItem() + array(
'js_files' => $oMenuItem->GetLinkedScripts(),
'type' => 'button',
);
}
elseif ($oMenuItem instanceof URLButtonItem)
{
$aItemButtons[] = $oMenuItem->GetMenuItem() + array('type' => 'link');
}
}
}
}
// ... And item's properties
$aItems[] = array(
'id' => $oCurrentRow->GetKey(),
'class' => $sCurrentClass,
'attributes' => $aItemAttrs,
'highlight_class' => $oCurrentRow->GetHilightClass(),
'actions' => $aItemButtons,
);
if (!empty($aItemButtons))
{
$bHasObjectListItemExtension = true;
}
}
// Adding an extra column for object list item extensions
if ($bHasObjectListItemExtension === true)
{
$aColumnsDefinition['_ui_extensions'] = array(
'title' => Dict::S('Brick:Portal:Manage:Table:ItemActions'),
'type' => 'html',
);
}
$aGroupingAreasData[$sKey] = array(
'sId' => $sKey,
'sTitle' => $aGroupingAreasValues[$sKey]['label'],
'aItems' => $aItems,
'iItemsCount' => $oSet->Count(),
'aColumnsDefinition' => $aColumnsDefinition,
);
}
IssueLog::Debug('Portal ManageBrick query', LogChannels::PORTAL, array(
'sPortalId' => $sPortalId,
'sBrickId' => $sBrickId,
'sGroupingTab' => $sGroupingTab,
'oql' => $oSet->GetFilter()->ToOQL(),
'aGroupingTabs' => $aGroupingTabs,
));
} else {
$aGroupingAreasData = array();
$sGroupingArea = null;
}
// Preparing response
if ($oRequest->isXmlHttpRequest()) {
$aData = $aData + array(
'data' => $aGroupingAreasData[$sGroupingArea]['aItems'],
);
} else {
$aDisplayValues = array();
$aUrls = array();
$aColumns = array();
$aNames = array();
if ($bHasScope) {
foreach ($aGroupingTabsValues as $aValues) {
$aDisplayValues[] = array(
'value' => $aValues['count'],
'label' => $aValues['label'],
'label_html' => $aValues['label_html'],
);
$aUrls[] = $oUrlGenerator->generate('p_manage_brick_display_as', array(
'sBrickId' => $sBrickId,
'sDisplayMode' => 'list',
'sGroupingTab' => $aValues['value'],
));
}
foreach ($aDisplayValues as $idx => $aValue)
{
$aColumns[] = array('series_'.$idx, (int)$aValue['value']);
$aNames['series_'.$idx] = $aValue['label'];
}
}
// Preparing data to pass to the templating service
$aData = $aData + array(
'sFct' => 'count',
'sIconURL' => $sIconURL,
'aColumns' => $aColumns,
'aNames' => $aNames,
'aDisplayValues' => $aDisplayValues,
'aUrls' => $aUrls,
'oBrick' => $oBrick,
'sBrickId' => $sBrickId,
'sGroupingTab' => $sGroupingTab,
'aGroupingTabsValues' => $aGroupingTabsValues,
'sDataLoading' => $sDataLoading,
'aGroupingAreasData' => $aGroupingAreasData,
'sDateFormat' => AttributeDate::GetFormat()->ToMomentJS(),
'sDateTimeFormat' => AttributeDateTime::GetFormat()->ToMomentJS(),
'iCount' => $iCount,
);
}
return $aData;
}
/**
* @param array $aData
* @param \DBSearch $oQuery
* @param string $sClass
* @param array $aColumnsAttrs
*
* @throws \CoreException
* @throws \Exception
*/
protected function ManageSearchValue(&$aData, DBSearch &$oQuery, $sClass, $aColumnsAttrs = array())
{
/** @var \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulator */
$oRequestManipulator = $this->get('request_manipulator');
// Getting search value
$sRawSearchValue = trim($oRequestManipulator->ReadParam('sSearchValue', ''));
$sSearchValue = html_entity_decode($sRawSearchValue);
// - Adding search clause if necessary
// Note : This is a very naive search at the moment
if (strlen($sSearchValue) > 0) {
// Putting only valid attributes as one can define attributes of leaf classes in the brick definition (<fields>), but at this stage we are working on the abstract class.
// Note: This won't fix everything as the search will not be looking in all fields.
$aSearchListItems = [];
foreach ($aColumnsAttrs as $sColumnAttr) {
// Skip invalid attCodes
if (!MetaModel::IsValidAttCode($sClass, $sColumnAttr)) {
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
$oAttDef = MetaModel::GetAttributeDef($sClass, $sColumnAttr);
if ($oAttDef instanceof AttributeExternalKey) {
$sColumnAttr .= '_friendlyname';
}
$aSearchListItems[] = $sColumnAttr;
}
if (preg_match('/^"(.*)"$/', $sSearchValue, $aMatches)) {
// The text is surrounded by double-quotes, remove the quotes and treat it as one single expression
$aSearchNeedles = [$aMatches[1]];
} else {
// Split the text on the blanks and treat this as a search for <word1> AND <word2> AND <word3>
$aExplodedSearchNeedles = explode(' ', $sSearchValue);
$aSearchNeedles = [];
foreach ($aExplodedSearchNeedles as $sValue) {
if (strlen($sValue) > 0) {
$aSearchNeedles[] = $sValue;
}
}
}
foreach ($aSearchNeedles as $sSearchWord) {
$oQuery->AddCondition_FullTextOnAttributes($aSearchListItems, $sSearchWord);
}
}
$aData['sSearchValue'] = $sRawSearchValue;
}
/**
* Get the groups using a given attribute code.
* If a limit is given, the remaining groups are aggregated (group by result and search request).
*
* @param \DBSearch $oQuery Initial query
* @param string $sGroupingTabAttCode Attribute code to group by
* @param \Combodo\iTop\Portal\Brick\ManageBrick $oBrick
*
* @return array of results from the group by request and the corresponding search.
*
* @throws \CoreException
* @throws \MySQLException
* @throws \OQLException
* @throws \Exception
*/
protected function GroupByAttribute(DBSearch $oQuery, $sGroupingTabAttCode, ManageBrick $oBrick) {
/** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */
$oScopeValidator = $this->get('scope_validator');
$aGroupingTabsValues = array();
$aDistinctResults = array();
$oDistinctQuery = DBSearch::FromOQL($oBrick->GetOql());
$bHasScope = $oScopeValidator->AddScopeToQuery($oDistinctQuery, $oDistinctQuery->GetClass());
if ($bHasScope)
{
// - Adding field condition
$oFieldExp = new FieldExpression($sGroupingTabAttCode, $oDistinctQuery->GetClassAlias());
$sDistinctSql = $oDistinctQuery->MakeGroupByQuery(array(), array('grouped_by_1' => $oFieldExp), true);
$aDistinctResults = CMDBSource::QueryToArray($sDistinctSql);
if (!empty($aDistinctResults))
{
$iLimit = $oBrick->GetGroupLimit();
$aOthers = array();
if ($iLimit > 0)
{
uasort($aDistinctResults, function ($a, $b) {
$v1 = $a['_itop_count_'];
$v2 = $b['_itop_count_'];
return ($v1 == $v2) ? 0 : (($v1 > $v2) ? -1 : 1);
});
if (count($aDistinctResults) > $iLimit)
{
if ($oBrick->ShowGroupOthers())
{
$aOthers = array_slice($aDistinctResults, $iLimit);
}
$aDistinctResults = array_slice($aDistinctResults, 0, $iLimit);
}
}
foreach ($aDistinctResults as $aDistinctResult)
{
$oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GTAB');
$oExpression = new BinaryExpression(new FieldExpression($sGroupingTabAttCode,
$oConditionQuery->GetClassAlias()), '=', new UnaryExpression($aDistinctResult['grouped_by_1']));
$oConditionQuery->AddConditionExpression($oExpression);
$sHtmlLabel = $oFieldExp->MakeValueLabel($oDistinctQuery, $aDistinctResult['grouped_by_1'], '');
$aGroupingTabsValues[$aDistinctResult['grouped_by_1']] = array(
'value' => $aDistinctResult['grouped_by_1'],
'label_html' => $sHtmlLabel,
'label' => strip_tags(html_entity_decode($sHtmlLabel, ENT_QUOTES, 'UTF-8')),
'condition' => $oConditionQuery,
'count' => $aDistinctResult['_itop_count_'],
);
unset($oConditionQuery);
}
if (!empty($aOthers))
{
// Aggregate others
$oConditionQuery = DBSearch::CloneWithAlias($oQuery, 'GTAB');
$oExpression = null;
$iOtherCount = 0;
foreach ($aOthers as $aResult)
{
$iOtherCount += $aResult['_itop_count_'];
$oExpr = new BinaryExpression(new FieldExpression($sGroupingTabAttCode,
$oConditionQuery->GetClassAlias()), '=', new UnaryExpression($aResult['grouped_by_1']));
if (is_null($oExpression))
{
$oExpression = $oExpr;
}
else
{
$oExpression = new BinaryExpression($oExpression, 'OR', $oExpr);
}
}
$oConditionQuery->AddConditionExpression($oExpression);
$sLabel = Dict::S('Brick:Portal:Manage:Others');
$aGroupingTabsValues['Others'] = array(
'value' => 'Others',
'label_html' => $sLabel,
'label' => $sLabel,
'condition' => $oConditionQuery,
'count' => $iOtherCount,
);
unset($oConditionQuery);
}
}
}
if (empty($aDistinctResults))
{
$sLabel = Dict::S('Brick:Portal:Manage:All');
$aGroupingTabsValues['undefined'] = array(
'value' => 'All',
'label_html' => $sLabel,
'label' => $sLabel,
'condition' => null,
'count' => 0,
);
}
return $aGroupingTabsValues;
}
/**
* @param \Combodo\iTop\Portal\Brick\ManageBrick $oBrick
* @param string $sClass
*
* @return \DBSearch
*
* @throws \CoreException
* @throws \OQLException
*/
protected function GetScopedQuery(ManageBrick $oBrick, $sClass)
{
/** @var \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidator */
$oScopeValidator = $this->get('scope_validator');
$oQuery = DBSearch::FromOQL($oBrick->GetOql());
$oScopeValidator->AddScopeToQuery($oQuery, $sClass);
return $oQuery;
}
}