<?php

/**
 * Copyright (C) 2013-2024 Combodo SAS
 *
 * 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 AttachmentPlugIn;
use AttributeEnum;
use AttributeFinalClass;
use AttributeFriendlyName;
use AttributeImage;
use BinaryExpression;
use Combodo\iTop\Form\Field\DateTimeField;
use Combodo\iTop\Portal\Brick\BrickCollection;
use Combodo\iTop\Portal\Brick\CreateBrick;
use Combodo\iTop\Portal\Helper\ApplicationHelper;
use Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
use Combodo\iTop\Portal\Helper\NavigationRuleHelper;
use Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper;
use Combodo\iTop\Portal\Helper\RequestManipulatorHelper;
use Combodo\iTop\Portal\Helper\ScopeValidatorHelper;
use Combodo\iTop\Portal\Helper\SecurityHelper;
use Combodo\iTop\Portal\Routing\UrlGenerator;
use Combodo\iTop\Portal\Service\TemplatesProvider\TemplateDefinitionDto;
use Combodo\iTop\Portal\Service\TemplatesProvider\TemplatesRegister;
use Combodo\iTop\Renderer\Bootstrap\FieldRenderer\BsLinkedSetFieldRenderer;
use DBObject;
use DBObjectSearch;
use DBObjectSet;
use DBSearch;
use Dict;
use Exception;
use FalseExpression;
use FieldExpression;
use FileUploadException;
use IssueLog;
use JSButtonItem;
use ListExpression;
use MetaModel;
use ScalarExpression;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use UserRights;
use utils;
use VariableExpression;

/**
 * Class ObjectController
 *
 * Controller to handle basic view / edit / create of cmdbAbstractObjectClass ManageBrickController
 *
 * @package Combodo\iTop\Portal\Controller
 * @author  Guillaume Lajarige <guillaume.lajarige@combodo.com>
 * @since   2.3.0
 */
class ObjectController extends BrickController
{
	public const DEFAULT_PAGE_NUMBER = 1;
	public const DEFAULT_LIST_LENGTH = 10;

	/** @inheritdoc  */
	public static function RegisterTemplates(TemplatesRegister $oTemplatesRegister): void
	{
		parent::RegisterTemplates($oTemplatesRegister);
		$oTemplatesRegister->RegisterTemplates(
			self::class,
			TemplateDefinitionDto::Create('page', static::TEMPLATES_BASE_PATH.'bricks/object/layout.html.twig'),
			TemplateDefinitionDto::Create('modal', static::TEMPLATES_BASE_PATH.'bricks/object/modal.html.twig'),
			TemplateDefinitionDto::Create('mode_create', static::TEMPLATES_BASE_PATH.'bricks/object/mode_create.html.twig', true, 'create'),
			TemplateDefinitionDto::Create('mode_edit', static::TEMPLATES_BASE_PATH.'bricks/object/mode_edit.html.twig', true, 'edit'),
			TemplateDefinitionDto::Create('mode_search_hierarchy', static::TEMPLATES_BASE_PATH.'bricks/object/mode_search_hierarchy.html.twig', true, 'search_hierarchy'),
			TemplateDefinitionDto::Create('mode_search_regular', static::TEMPLATES_BASE_PATH.'bricks/object/mode_search_regular.html.twig', true, 'search_regular'),
			TemplateDefinitionDto::Create('mode_view', static::TEMPLATES_BASE_PATH.'bricks/object/mode_view.html.twig', true, 'view'),
			TemplateDefinitionDto::Create('mode_apply_stimulus', static::TEMPLATES_BASE_PATH.'bricks/object/mode_apply_stimulus.html.twig', true, 'apply_stimulus'),
			TemplateDefinitionDto::Create('mode_loader', static::TEMPLATES_BASE_PATH.'modal/mode_loader.html.twig'),
			TemplateDefinitionDto::Create('plugins_buttons', static::TEMPLATES_BASE_PATH.'bricks/object/plugins_buttons.html.twig'),
		);
	}

	/**
	 * @param \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper
	 * @param \Combodo\iTop\Portal\Helper\ScopeValidatorHelper $oScopeValidatorHelper
	 * @param \Combodo\iTop\Portal\Helper\RequestManipulatorHelper $oRequestManipulatorHelper
	 * @param \Combodo\iTop\Portal\Routing\UrlGenerator $oUrlGenerator
	 * @param \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection
	 * @param \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $oObjectFormHandlerHelper
	 * @param \Combodo\iTop\Portal\Helper\NavigationRuleHelper $oNavigationRuleHelper
	 * @param \Combodo\iTop\Portal\Helper\ContextManipulatorHelper $oContextManipulatorHelper
	 * @param array $aCombodoPortalInstanceConf
	 *
	 * @since 3.2.0 N°6933
	 * @since 3.2.1 Added $aCombodoPortalInstanceConf parameter
	 */
	public function __construct(
		protected SecurityHelper $oSecurityHelper,
		protected ScopeValidatorHelper $oScopeValidatorHelper,
		protected RequestManipulatorHelper $oRequestManipulatorHelper,
		protected UrlGenerator $oUrlGenerator,
		protected BrickCollection $oBrickCollection,
		protected ObjectFormHandlerHelper $oObjectFormHandlerHelper,
		protected NavigationRuleHelper $oNavigationRuleHelper,
		protected ContextManipulatorHelper $oContextManipulatorHelper,
		protected array $aCombodoPortalInstanceConf = []
	) {

	}

	/**
	 * 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 an instance of cmdbAbstractObject)
	 * @param string $sObjectId
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function ViewAction(Request $oRequest, $sObjectClass, $sObjectId)
	{
		// Checking parameters
		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 (!$this->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'));
		}

		// Retrieving object
		$oObject = MetaModel::GetObject(
			$sObjectClass,
			$sObjectId,
			false /* MustBeFound */,
			$this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)
		);
		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)
	{
		// 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,
			$this->oScopeValidatorHelper->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 (!$this->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)
	{

		$sOperation = $this->oRequestManipulatorHelper->ReadParam('operation', '');
		$sObjectClass = get_class($oObject);
		$sObjectId = $oObject->GetKey();

		$oObject->FireEvent(EVENT_DISPLAY_OBJECT_DETAILS);

		$aData = ['sMode' => 'view'];
		$aData['form'] = $this->oObjectFormHandlerHelper->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId);
		$aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', $oObject->GetName());
		$aData['form']['title_complement'] = MetaModel::GetName($sObjectClass);

		// Add an edit button if user is allowed
		if ($this->oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) {
			$sModifyUrl = $this->oUrlGenerator->generate('p_object_edit', ['sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId]);
			$oModifyButton = new JSButtonItem(
				'modify_object',
				Dict::S('UI:Menu:Modify'),
				'CombodoModal.OpenUrlInModal("'.$sModifyUrl.'", true);'
			);
			// Putting this one first
			$aData['form']['buttons']['actions'][] = $oModifyButton->GetMenuItem() + ['js_files' => $oModifyButton->GetLinkedScripts()];
		}

		// Preparing response
		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)) {
				$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
			} else {
				$oResponse = new JsonResponse($aData);
			}
		} else {
			// Adding brick if it was passed
			$sBrickId = $this->oRequestManipulatorHelper->ReadParam('sBrickId', '');
			if (!empty($sBrickId)) {
				$oBrick = $this->oBrickCollection->GetBrickById($sBrickId);
				if ($oBrick !== null) {
					$aData['oBrick'] = $oBrick;
				}
			}
			$aData['sPageTitle'] = $aData['form']['title'];
			$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
		}

		return $oResponse;
	}

	/**
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param                                           $sObjectClass
	 * @param                                           $sObjectId
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function EditAction(Request $oRequest, $sObjectClass, $sObjectId)
	{
		// Checking parameters
		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
		// Warning : This is a dirty quick fix to allow editing its own contact information
		$bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId());
		if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to modify '.$sObjectClass.'::'.$sObjectId.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Retrieving object
		$oObject = MetaModel::GetObject(
			$sObjectClass,
			$sObjectId,
			false /* MustBeFound */,
			$this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass)
		);
		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'));
		}

		$sOperation = $this->oRequestManipulatorHelper->ReadParam('operation', '');

		$aData = ['sMode' => 'edit'];
		$aData['form'] = $this->oObjectFormHandlerHelper->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId);
		$aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', $aData['form']['object_name']);
		$aData['form']['title_complement'] = MetaModel::GetName($sObjectClass);

		// Preparing response
		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)) {
				$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
			} else {
				$oResponse = new JsonResponse($aData);
			}
		} else {
			// Adding brick if it was passed
			$sBrickId = $this->oRequestManipulatorHelper->ReadParam('sBrickId', '');
			if (!empty($sBrickId)) {
				$oBrick = $this->oBrickCollection->GetBrickById($sBrickId);
				if ($oBrick !== null) {
					$aData['oBrick'] = $oBrick;
				}
			}
			$aData['sPageTitle'] = $aData['form']['title'];
			$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
		}

		return $oResponse;
	}

	/**
	 * Creates an cmdbAbstractObject of the $sObjectClass
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sObjectClass
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \OQLException
	 */
	public function CreateAction(Request $oRequest, $sObjectClass)
	{
		$oResponse = null;
		// Checking if the target object class is abstract or not
		// - If is not abstract, we redirect to object creation form
		if (!MetaModel::IsAbstract($sObjectClass)) {
			$oResponse = $this->DisplayCreationForm($oRequest, $sObjectClass);
		}
		// - Else, we list the leaf classes as an intermediate step
		else {
			$oResponse = $this->DisplayLeafClassesForm($sObjectClass);
		}

		return $oResponse;
	}

	/**
	 * Creates an cmdbAbstractObject of a class determined by the method encoded in $sEncodedMethodName.
	 * This method use an origin DBObject in order to determine the created cmdbAbstractObject.
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sObjectClass       Class of the origin object
	 * @param string                                    $sObjectId          ID of the origin object
	 * @param string                                    $sEncodedMethodName Base64 encoded factory method name
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 */
	public function CreateFromFactoryAction(Request $oRequest, $sObjectClass, $sObjectId, $sEncodedMethodName)
	{
		$sMethodName = base64_decode($sEncodedMethodName);

		// Checking that the factory method is valid
		if (!is_callable($sMethodName)) {
			IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Invalid factory method "'.$sMethodName.'" used when creating an object.');
			throw new HttpException(
				Response::HTTP_INTERNAL_SERVER_ERROR,
				'Invalid factory method "'.$sMethodName.'" used when creating an object'
			);
		}

		// Retrieving origin object
		// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
		$oOriginObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, true);

		// Retrieving target object (We check if the method is a simple function or if it's part of a class in which case only static function are supported)
		if (!strpos($sMethodName, '::')) {
			$oTargetObject = $sMethodName($oOriginObject);
		} else {
			$aMethodNameParts = explode('::', $sMethodName);
			$sMethodClass = $aMethodNameParts[0];
			$sMethodName = $aMethodNameParts[1];
			$oTargetObject = $sMethodClass::$sMethodName($oOriginObject);
		}

		// Preparing redirection
		// - Route
		$aRouteParams = [
			'sObjectClass' => get_class($oTargetObject),
		];

		return $this->ForwardToRoute('p_object_create', $aRouteParams, $oRequest->query->all());
	}

	/**
	 * Applies a stimulus $sStimulus on an cmdbAbstractObject
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sObjectClass
	 * @param string                                    $sObjectId
	 * @param string                                    $sStimulusCode
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function ApplyStimulusAction(Request $oRequest, $sObjectClass, $sObjectId, $sStimulusCode)
	{
		/** @var array $aCombodoPortalInstanceConf */
		$aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf');

		// Checking parameters
		if ($sObjectClass === '' || $sObjectId === '' || $sStimulusCode === '') {
			IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, sObjectId and $sStimulusCode expected, "'.$sObjectClass.'", "'.$sObjectId.'" and "'.$sStimulusCode.'" given.');
			throw new HttpException(
				Response::HTTP_INTERNAL_SERVER_ERROR,
				Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus')
			);
		}

		// Checking security layers
		if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to modify '.$sObjectClass.'::'.$sObjectId.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}
		if (!$this->oSecurityHelper->IsStimulusAllowed($sStimulusCode, $sObjectClass)) {
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Retrieving object
		$oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass));
		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'));
		}

		// Retrieving request parameters
		$sOperation = $this->oRequestManipulatorHelper->ReadParam('operation', '');

		// Retrieving form properties
		$aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($aCombodoPortalInstanceConf['forms'], $sObjectClass, 'apply_stimulus');
		if (array_key_exists($sStimulusCode, $aStimuliForms)) {
			$aFormProperties = $aStimuliForms[$sStimulusCode];
		}
		// Or preparing a default form for the stimulus application
		else {
			// Preparing default form
			$aFormProperties = [
				'id' => 'apply-stimulus',
				'type' => 'custom_list',
				'fields' => [],
				'layout' => null,
			];
		}

		// Adding stimulus code to form
		$aFormProperties['stimulus_code'] = $sStimulusCode;

		// Adding target_state to current_values
		$oRequest->request->set('apply_stimulus', ['code' => $sStimulusCode]);

		$aData = ['sMode' => 'apply_stimulus'];
		$aData['form'] = $this->oObjectFormHandlerHelper->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties);
		$aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Stimulus:Title');

		// TODO : This is a ugly patch to avoid showing a modal with a readonly form to the user as it would prevent user from finishing the transition.
		// Instead, we apply the stimulus directly here and then go to the edited object.
		if (empty($sOperation)) {
			if (isset($aData['form']['editable_fields_count']) && $aData['form']['editable_fields_count'] === 0) {
				$sOperation = 'redirect';

				$oSubRequest = $oRequest;
				$oSubRequest->request->set('operation', 'submit');
				$oSubRequest->request->set('stimulus_code', '');
				$oSubRequest->request->set('formmanager_class', $aData['form']['formmanager_class']);
				$oSubRequest->request->set('formmanager_data', json_encode($aData['form']['formmanager_data']));

				$aData = ['sMode' => 'apply_stimulus'];
				$aData['form'] = $this->oObjectFormHandlerHelper->HandleForm(
					$oSubRequest,
					$aData['sMode'],
					$sObjectClass,
					$sObjectId,
					$aFormProperties
				);

				// Reload the object to make sure we have it in a clean state
				$oObject->Reload(true);
				$aNavigationRules = $this->oNavigationRuleHelper->PrepareRulesForForm($aFormProperties, $oObject, true);

				// Redefining the array to be as simple as possible :
				$aData = [
					'redirection' =>
						[
							'url' => $aNavigationRules['submit']['url'],
						],
				];
			}
		}

		// Preparing response
		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)) {
				$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
			} elseif ($sOperation === 'redirect') {
				$oResponse = $this->render($this->GetTemplatePath('mode_loader'), $aData);
			} else {
				$oResponse = new JsonResponse($aData);
			}
		} else {
			$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
		}

		return $oResponse;
	}

	/**
	 * Handles the autocomplete search
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sTargetAttCode   Attribute code of the host object pointing to the Object class to
	 *                                                                    search
	 * @param string                                    $sHostObjectClass Class name of the host object
	 * @param string                                    $sHostObjectId    Id of the host object
	 *
	 * @return \Symfony\Component\HttpFoundation\JsonResponse
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \MySQLException
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function SearchAutocompleteAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
	{

		$aData = [
			'results' => [
				'count' => 0,
				'items' => [],
			],
		];

		// Parsing parameters from request payload
		parse_str($oRequest->getContent(), $aRequestContent);

		// Checking parameters
		if (!isset($aRequestContent['sQuery'])) {
			IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Parameter sQuery missing.');
			throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, Dict::Format('UI:Error:ParameterMissing', 'sQuery'));
		}

		// Retrieving parameters
		$sQuery = $aRequestContent['sQuery'];
		$sFieldId = $aRequestContent['sFieldId'];

		// Checking security layers
		if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : Could not load object '.$sHostObjectClass.'::'.$sHostObjectId.'.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Retrieving host object for future DBSearch parameters
		if ($sHostObjectId !== null) {
			// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
			$oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true);
		} else {
			$oHostObject = MetaModel::NewObject($sHostObjectClass);
			// Retrieving action rules
			//
			// Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values.
			// But it would not be a security issue as it only presets values in the form.
			$sActionRulesToken = $this->oRequestManipulatorHelper->ReadParam('ar_token', '');
			$aActionRules = (!empty($sActionRulesToken)) ? $this->oContextManipulatorHelper->DecodeRulesToken($sActionRulesToken) : [];
			// Preparing object
			$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oHostObject);
		}

		// Updating host object with form data / values
		$sFormManagerClass = $aRequestContent['formmanager_class'];
		$sFormManagerData = $aRequestContent['formmanager_data'];
		if (!empty($sFormManagerClass) && !empty($sFormManagerData)) {
			/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */
			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
			$oFormManager->SetObjectFormHandlerHelper($this->oObjectFormHandlerHelper);
			$oFormManager->SetObject($oHostObject);

			// Applying action rules if present
			if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) {
				$aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
				$oObj = $oFormManager->GetObject();
				$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oObj);
				$oFormManager->SetObject($oObj);
			}

			// Updating host object
			$oFormManager->OnUpdate(['currentValues' => $aRequestContent['current_values']]);
			$oHostObject = $oFormManager->GetObject();
		}

		// Building search query
		// - Retrieving target object class from attcode
		$oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode);
		if ($oTargetAttDef->GetEditClass() === 'CustomFields') {
			$oRequestTemplate = $oHostObject->Get($sTargetAttCode);
			/** @var \DBSearch $oTemplateFieldSearch */
			$oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch();
			$sTargetObjectClass = $oTemplateFieldSearch->GetClass();
		} elseif ($oTargetAttDef->IsLinkSet()) {
			throw new Exception('Search autocomplete cannot apply on AttributeLinkedSet objects, '.get_class($oTargetAttDef).' ('.$sHostObjectClass.'->'.$sTargetAttCode.') given.');
		} else {
			$sTargetObjectClass = $oTargetAttDef->GetTargetClass();
		}
		// - Base query from meta model
		if ($oTargetAttDef->GetEditClass() === 'CustomFields') {
			$oSearch = $oTemplateFieldSearch;
		} else {
			$oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression());
		}
		// - Adding query condition
		$oSearch->AddConditionExpression(new BinaryExpression(
			new FieldExpression('friendlyname', $oSearch->GetClassAlias()),
			'LIKE',
			new VariableExpression('ac_query')
		));
		// - Intersecting with scope constraints
		// Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates.
		// It is the responsibility of the template designer to write the right query so the user see only what he should.
		if ($oTargetAttDef->GetEditClass() !== 'CustomFields') {
			$oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ);
			$oSearch = $oSearch->Intersect($oScopeSearch);
			// - Allowing all data if necessary
			if ($oScopeSearch->IsAllDataAllowed()) {
				$oSearch->AllowAllData();
			}
		}

		// Retrieving results
		// - Preparing object set
		$oSet = new DBObjectSet($oSearch, [], ['this' => $oHostObject, 'ac_query' => '%'.$sQuery.'%']);
		$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => ['friendlyname']]);
		// Note : This limit is also used in the field renderer by typeahead to determine how many suggestions to display
		$oSet->SetLimit(MetaModel::GetConfig()->Get('max_autocomplete_results'));

		// - Retrieving objects
		while ($oItem = $oSet->Fetch()) {
			$aData['results']['items'][] = [
				'id' => $oItem->GetKey(),
				'name' => html_entity_decode($oItem->GetName(), ENT_QUOTES, 'UTF-8'),
			];
			$aData['results']['count']++;
		}

		// Preparing response
		if ($oRequest->isXmlHttpRequest()) {
			$oResponse = new JsonResponse($aData);
		} else {
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		return $oResponse;
	}

	/**
	 * Handles the regular (table) search from an attribute
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sTargetAttCode   Attribute code of the host object pointing to the Object class to
	 *                                                                    search
	 * @param string                                    $sHostObjectClass Class name of the host object
	 * @param string                                    $sHostObjectId    Id of the host object
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \DictExceptionMissingString
	 * @throws \MissingQueryArgument
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
	{
		/** @var array $aCombodoPortalInstanceConf */
		$aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf');

		$aData = [
			'sMode'             => 'search_regular',
			'sTargetAttCode'    => $sTargetAttCode,
			'sHostObjectClass'  => $sHostObjectClass,
			'sHostObjectId'     => $sHostObjectId,
			'sActionRulesToken' => $this->oRequestManipulatorHelper->ReadParam('ar_token', ''),
		];

		// Checking security layers
		if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sHostObjectClass.'::'.$sHostObjectId.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Retrieving host object for future DBSearch parameters
		if ($sHostObjectId !== null) {
			// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
			$oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true);
		} else {
			$oHostObject = MetaModel::NewObject($sHostObjectClass);
			// Retrieving action rules
			//
			// Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values.
			// But it would not be a security issue as it only presets values in the form.
			$aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : [];
			// Preparing object
			$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oHostObject);
		}

		// Updating host object with form data / values
		$sFormManagerClass = $this->oRequestManipulatorHelper->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
		$sFormManagerData = $this->oRequestManipulatorHelper->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
		if (!empty($sFormManagerClass) && !empty($sFormManagerData)) {
			/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */
			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
			$oFormManager->SetObjectFormHandlerHelper($this->oObjectFormHandlerHelper);
			$oFormManager->SetObject($oHostObject);

			// Applying action rules if present
			if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) {
				$aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
				$oObj = $oFormManager->GetObject();
				$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oObj);
				$oFormManager->SetObject($oObj);
			}

			// Updating host object
			$oFormManager->OnUpdate([
				'currentValues' => $this->oRequestManipulatorHelper->ReadParam('current_values', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY),
			]);
			$oHostObject = $oFormManager->GetObject();
		}

		// Retrieving request parameters
		$iPageNumber = $this->oRequestManipulatorHelper->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT);
		$iListLength = $this->oRequestManipulatorHelper->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT);
		$bInitialPass = $this->oRequestManipulatorHelper->HasParam('draw') ? false : true;
		$sQuery = $this->oRequestManipulatorHelper->ReadParam('sSearchValue', '');
		$sFormPath = $this->oRequestManipulatorHelper->ReadParam('sFormPath', '');
		$sFieldId = $this->oRequestManipulatorHelper->ReadParam('sFieldId', '');
		$aObjectIdsToIgnore = $this->oRequestManipulatorHelper->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);

		// Building search query
		// - Retrieving target object class from attcode
		$oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode);
		if ($oTargetAttDef->IsExternalKey()) {
			/** @var \AttributeExternalKey $oTargetAttDef */
			$sTargetObjectClass = $oTargetAttDef->GetTargetClass();
		} elseif ($oTargetAttDef->IsLinkSet()) {
			/** @var \AttributeLinkedSet $oTargetAttDef */
			if (!$oTargetAttDef->IsIndirect()) {
				$sTargetObjectClass = $oTargetAttDef->GetLinkedClass();
			} else {
				/** @var \AttributeLinkedSetIndirect $oTargetAttDef */
				/** @var \AttributeExternalKey $oRemoteAttDef */
				$oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote());
				$sTargetObjectClass = $oRemoteAttDef->GetTargetClass();
			}
		} elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') {
			$oRequestTemplate = $oHostObject->Get($sTargetAttCode);
			/** @var \DBSearch $oTemplateFieldSearch */
			$oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch();
			$sTargetObjectClass = $oTemplateFieldSearch->GetClass();
		} else {
			throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, '.get_class($oTargetAttDef).' given.');
		}

		// - Retrieving class attribute list
		$aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sTargetObjectClass, 'list');
		// - Adding friendlyname attribute to the list is not already in it
		$sTitleAttCode = 'friendlyname';
		if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) {
			$aAttCodes = array_merge([$sTitleAttCode], $aAttCodes);
		}

		// - Retrieving scope search
		// Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates.
		// It is the responsibility of the template designer to write the right query so the user see only what he should.
		$oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ);
		$aInternalParams = [];
		if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) {
			IssueLog::Info(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' has no scope query for '.$sTargetObjectClass.' class.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// - Base query from meta model
		/** @var \DBSearch $oSearch */
		if ($oTargetAttDef->IsExternalKey()) {
			$oSearch = DBSearch::FromOQL($oTargetAttDef->GetValuesDef()->GetFilterExpression());
		} elseif ($oTargetAttDef->IsLinkSet()) {
			$oSearch = $oScopeSearch;
		} elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') {
			// Note : $oTemplateFieldSearch has been defined in the "Retrieving target object class from attcode" part, it is not available otherwise
			$oSearch = $oTemplateFieldSearch;
		}

		// - Filtering objects to ignore
		if (($aObjectIdsToIgnore !== null) && (is_array($aObjectIdsToIgnore))) {
			//$oSearch->AddConditionExpression('id', $aObjectIdsToIgnore, 'NOT IN');
			$aExpressions = [];
			foreach ($aObjectIdsToIgnore as $sObjectIdToIgnore) {
				$aExpressions[] = new ScalarExpression($sObjectIdToIgnore);
			}
			$oSearch->AddConditionExpression(new BinaryExpression(
				new FieldExpression('id', $oSearch->GetClassAlias()),
				'NOT IN',
				new ListExpression($aExpressions)
			));
		}

		// - Adding query condition
		$aInternalParams['this'] = $oHostObject;
		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
				$oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $aAttCodes[$i]);
				$sAttCode = (!$oAttDef->IsExternalKey()) ? $aAttCodes[$i] : $aAttCodes[$i].'_friendlyname';
				// Building expression for the current attcode
				// - For attributes that need conversion from their display value to storage value
				//   Note : This is dirty hack that will need to be refactored in the OQL core in order to be nicer and to be extended to other types such as dates etc...
				if (($oAttDef instanceof AttributeEnum) || ($oAttDef instanceof AttributeFinalClass)) {
					// Looking up storage value
					$aMatchedCodes = [];
					foreach ($oAttDef->GetAllowedValues() as $sValueCode => $sValueLabel) {
						if (stripos($sValueLabel, $sQuery) !== false) {
							$aMatchedCodes[] = $sValueCode;
						}
					}
					// Building expression
					if (!empty($aMatchedCodes)) {
						$oEnumeratedListExpr = ListExpression::FromScalars($aMatchedCodes);
						$oBinExpr = new BinaryExpression(
							new FieldExpression($sAttCode, $oSearch->GetClassAlias()),
							'IN',
							$oEnumeratedListExpr
						);
					} else {
						$oBinExpr = new FalseExpression();
					}
				}
				// - For regular attributes
				else {
					$oBinExpr = new BinaryExpression(
						new FieldExpression($sAttCode, $oSearch->GetClassAlias()),
						'LIKE',
						new VariableExpression('re_query')
					);
				}
				// Adding expression to the full expression (all attcodes)
				if ($i === 0) {
					$oFullExpr = $oBinExpr;
				} else {
					$oFullExpr = new BinaryExpression($oFullExpr, 'OR', $oBinExpr);
				}
			}
			// Adding full expression to the search object
			$oSearch->AddConditionExpression($oFullExpr);
			$aInternalParams['re_query'] = '%'.$sQuery.'%';
		}

		// - Intersecting with scope constraints
		// Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates.
		// It is the responsibility of the template designer to write the right query so the user see only what he should.
		if (($oScopeSearch !== null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) {
			$oSearch = $oSearch->Intersect($oScopeSearch);
			// - Allowing all data if necessary
			if ($oScopeSearch->IsAllDataAllowed()) {
				$oSearch->AllowAllData();
			}
		}

		// Retrieving results
		// - Preparing object set
		$oSet = new DBObjectSet($oSearch, [], $aInternalParams);
		$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aAttCodes]);
		$oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
		// - Retrieving columns properties
		$aColumnProperties = [];
		foreach ($aAttCodes as $sAttCode) {
			$oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode);
			$aColumnProperties[$sAttCode] = [
				'title' => $oAttDef->GetLabel(),
			];
		}
		// - Retrieving objects
		$aItems = [];
		while ($oItem = $oSet->Fetch()) {
			$aItems[] = $this->PrepareObjectInformation($oItem, $aAttCodes);
		}

		// Preparing response
		if ($bInitialPass) {
			$aData = $aData + [
					'form' => [
						'id' => 'object_search_form_'.time(),
						'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel()),
						'title_complement' => MetaModel::GetName($sTargetObjectClass),
					],
					'aColumnProperties' => json_encode($aColumnProperties),
					'aResults' => [
						'aItems' => json_encode($aItems),
						'iCount' => count($aItems),
					],
					'bMultipleSelect' => $oTargetAttDef->IsLinkSet(),
					'aSource' => [
						'sFormPath' => $sFormPath,
						'sFieldId' => $sFieldId,
						'aObjectIdsToIgnore' => $aObjectIdsToIgnore,
						'sFormManagerClass' => $sFormManagerClass,
						'sFormManagerData' => $sFormManagerData,
					],
				];

			if ($oRequest->isXmlHttpRequest()) {
				$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
			} else {
				//throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
				$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
			}
		} else {
			$aData = $aData + [
					'levelsProperties' => $aColumnProperties,
					'data' => $aItems,
					'recordsTotal' => $oSet->Count(),
					'recordsFiltered' => $oSet->Count(),
				];

			$oResponse = new JsonResponse($aData);
		}

		return $oResponse;
	}

	/**
	 * Handles ormDocument display / download from an object
	 *
	 * Note: This is inspired from pages/ajax.document.php, but duplicated as there is no secret mecanism for ormDocument yet.
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sOperation
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreException
	 * @throws \Exception
	 */
	public function DocumentAction(Request $oRequest, $sOperation = null)
	{

		// Setting default operation
		if ($sOperation === null) {
			$sOperation = 'display';
		}

		// Retrieving ormDocument's host object
		$sObjectClass = $this->oRequestManipulatorHelper->ReadParam('sObjectClass', '');
		$sObjectId = $this->oRequestManipulatorHelper->ReadParam('sObjectId', '');
		$sObjectField = $this->oRequestManipulatorHelper->ReadParam('sObjectField', '');
		$bCheckSecurity = true;

		// When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself
		if ($sObjectClass === 'Attachment') {

			$oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, false, true);
			if ($oAttachment === null) {
				throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
			}
			$sHostClass = $oAttachment->Get('item_class');
			$sHostId = $oAttachment->Get('item_id');

			// Attachments could be linked to host objects without an org_id. Retrieving the attachment would fail if enforced silos are based on org_id
			if ($oAttachment->Get('item_org_id') === 0 && ($sHostId > 0) && $this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostClass, $sHostId)) {
				$bCheckSecurity = false;
			}

		} else {
			$sHostClass = $sObjectClass;
			$sHostId = $sObjectId;

			// Security bypass for the image attribute of a class
			if (MetaModel::GetImageAttributeCode($sObjectClass) === $sObjectField) {
				$bCheckSecurity = false;
			}
		}

		// Checking security layers
		// Note: Checking if host object already exists as we can try to download document from an object that is being created
		if (($bCheckSecurity === true) && ($sHostId > 0) && !$this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostClass, $sHostId)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to retrieve document from attribute '.$sObjectField.' as it not allowed to read '.$sHostClass.'::'.$sHostId.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Retrieving object
		$bAllowAllDataFlag = ($bCheckSecurity === false) ? true : $this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sHostClass);
		$oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* Must not be found */, $bAllowAllDataFlag);
		if ($oObject === null) {
			IssueLog::Info(__METHOD__.' at line '.__LINE__.': Could not load object '.$sObjectClass.'::'.$sObjectId.'.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		// Setting cache timeout
		// Note: Attachment download should be handle through AttachmentAction()
		if ($sObjectClass === 'Attachment') {
			// One year ahead: an attachment cannot change
			$iCacheSec = 31556926;
		} else {
			$iCacheSec = $this->oRequestManipulatorHelper->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT);
		}

		$aHeaders = [];
		if ($iCacheSec > 0) {
			$aHeaders['Expires'] = '';
			$aHeaders['Cache-Control'] = 'no-transform, public,max-age='.$iCacheSec.',s-maxage='.$iCacheSec;
			// Reset the value set previously
			$aHeaders['Pragma'] = 'cache';

			// N°3423 Fix bug in Symphony 3.x in Response::sendHeaders(): Headers need to send directly as SF doesn't replace header of page except for Content-Type
			header('Cache-Control: no-transform, public,max-age='.$iCacheSec.',s-maxage='.$iCacheSec);
			header('Pragma: cache');
			header('Expires: ');

			// An arbitrary date in the past is ok
			$aHeaders['Last-Modified'] = 'Wed, 15 Jun 2015 13:21:15 GMT';
		}

		/** @var \ormDocument $oDocument */
		$oDocument = $oObject->Get($sObjectField);
		$aHeaders['Content-Type'] = $oDocument->GetMimeType();
		$aHeaders['Content-Disposition'] = (($sOperation === 'display') ? 'inline' : 'attachment').';filename="'.$oDocument->GetFileName().'"';

		if (MetaModel::GetImageAttributeCode($sObjectClass) === $sObjectField) {
			$sRequestedHash = $oRequest->get('s');
			$sComputedHash = md5($oDocument->GetData());
			if ($sRequestedHash !== $sComputedHash) {
				throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
			}
		}

		// N°4129 - Prevent XSS attacks & other script executions
		if (utils::GetConfig()->Get('security.disable_inline_documents_sandbox') === false) {
			$aHeaders['Content-Security-Policy'] = 'sandbox';
		}

		return new Response($oDocument->GetData(), Response::HTTP_OK, $aHeaders);
	}

	/**
	 * Handles attachment add/remove on an object
	 *
	 * Note: This is inspired from itop-attachment/ajax.attachment.php
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string $sOperation
	 *
	 * @return \Symfony\Component\HttpFoundation\JsonResponse
	 *
	 * @throws \CoreException
	 * @throws \Exception
	 */
	public function AttachmentAction(Request $oRequest, $sOperation = null)
	{
		$aData = [
			'att_id' => 0,
			'preview' => false,
			'msg' => '',
		];

		// Retrieving sOperation from request only if it wasn't forced (determined by the route)
		if ($sOperation === null) {
			$sOperation = $this->oRequestManipulatorHelper->ReadParam('operation', null);
		}
		switch ($sOperation) {
			case 'add':
				$sFieldName = $this->oRequestManipulatorHelper->ReadParam('field_name', '');
				$sObjectClass = $this->oRequestManipulatorHelper->ReadParam('object_class', '');
				$sTempId = $this->oRequestManipulatorHelper->ReadParam('temp_id', '');

				if (empty($sObjectClass) || empty($sTempId)) {
					$aData['error'] = Dict::Format('UI:Error:2ParametersMissing', 'object_class', 'temp_id');
				} else {
					try {
						$oDocument = utils::ReadPostedDocument($sFieldName);
						/** @noinspection PhpUndefinedClassInspection */
						/** @var \Attachment $oAttachment */
						$oAttachment = MetaModel::NewObject('Attachment');
						$oAttachment->Set('expire', time() + MetaModel::GetConfig()->Get('draft_attachments_lifetime')); // one hour...
						$oAttachment->Set('temp_id', $sTempId);
						$oAttachment->Set('item_class', $sObjectClass);
						$oAttachment->SetDefaultOrgId();
						$oAttachment->Set('contents', $oDocument);
						$iAttId = $oAttachment->DBInsert();

						$aData['msg'] = utils::EscapeHtml($oDocument->GetFileName());
						$aData['icon'] = utils::GetAbsoluteUrlAppRoot().'env-'.utils::GetCurrentEnvironment().'/itop-attachments/icons/icons8-image-file.svg';

						// Checking if the instance has attachments
						if (class_exists('AttachmentPlugIn')) {
							$aData['icon'] = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($oDocument->GetFileName());
						}

						$aData['att_id'] = $iAttId;
						$aData['preview'] = $oDocument->IsPreviewAvailable();
						$aData['file_size'] = $oDocument->GetFormattedSize();
						$aData['downloads_count'] = $oDocument->GetDownloadsCount();
						$aData['creation_date'] = $oAttachment->Get('creation_date');
						$aData['user_id_friendlyname'] = $oAttachment->Get('user_id_friendlyname');
						$aData['file_type'] = $oDocument->GetMimeType();
					} catch (FileUploadException $e) {
						$aData['error'] = $e->GetMessage();
					}
				}

				// Note : The Content-Type header is set to 'text/plain' in order to be IE9 compatible. Otherwise ('application/json') IE9 will download the response as a JSON file to the user computer...
				$oResponse = new JsonResponse($aData, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
				break;

			case 'download':
				// Preparing redirection
				// - Route
				$aRouteParams = [
					'sObjectClass' => 'Attachment',
					'sObjectId' => $this->oRequestManipulatorHelper->ReadParam('sAttachmentId', null),
					'sObjectField' => 'contents',
				];

				$oResponse = $this->ForwardToRoute('p_object_document_download', $aRouteParams, $oRequest->query->all());

				break;

			case 'display':
				// Preparing redirection
				// - Route
				$aRouteParams = [
					'sObjectClass' => 'Attachment',
					'sObjectId' => $this->oRequestManipulatorHelper->ReadParam('sAttachmentId', null),
					'sObjectField' => 'contents',
				];

				$oResponse = $this->ForwardToRoute('p_object_document_display', $aRouteParams, $oRequest->query->all());

				break;

			default:
				throw new HttpException(Response::HTTP_FORBIDDEN, Dict::S('Error:HTTP:400'));
				break;
		}

		return $oResponse;
	}

	/**
	 * Returns a json response containing an array of objects informations.
	 *
	 * The service must be given 3 parameters :
	 * - sObjectClass : The class of objects to retrieve information from
	 * - aObjectIds : An array of object ids
	 * - aObjectAttCodes : An array of attribute codes to retrieve
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 *
	 * @return \Symfony\Component\HttpFoundation\JsonResponse
	 *
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \MySQLException
	 * @throws \OQLException
	 * @throws \Exception
	 */
	public function GetInformationAsJsonAction(Request $oRequest)
	{

		$aData = [];

		// Retrieving parameters
		$sObjectClass = $this->oRequestManipulatorHelper->ReadParam('sObjectClass', '');
		$aObjectIds = $this->oRequestManipulatorHelper->ReadParam('aObjectIds', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		$aObjectAttCodes = $this->oRequestManipulatorHelper->ReadParam('aObjectAttCodes', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes)) {
			IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, aObjectIds and aObjectAttCodes expected, "'.$sObjectClass.'", "'.implode(
				'/',
				$aObjectIds
			).'" given.');
			throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Invalid request data, some information are missing');
		}

		// Building the search
		$bIgnoreSilos = $this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
		$aParams = ['objects_id' => $aObjectIds];
		$oSearch = DBObjectSearch::FromOQL("SELECT $sObjectClass WHERE id IN (:objects_id)");
		if (!$this->oScopeValidatorHelper->AddScopeToQuery($oSearch, $sObjectClass)
		) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sObjectClass.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}
		if ($bIgnoreSilos === true) {
			$oSearch->AllowAllData();
		}
		$oSet = new DBObjectSet($oSearch, [], $aParams);
		$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aObjectAttCodes]);

		// Checking that id is in the AttCodes
		// Note: We do that AFTER the array is used in OptimizeColumnLoad() because the function doesn't support this anymore.
		if (!in_array('id', $aObjectAttCodes)) {
			$aObjectAttCodes = array_merge(['id'], $aObjectAttCodes);
		}

		// Retrieving objects
		while ($oObject = $oSet->Fetch()) {
			$aData['items'][] = $this->PrepareObjectInformation($oObject, $aObjectAttCodes);
		}

		return new JsonResponse($aData);
	}

	/**
	 * GetInformationAsJsonAction for linked set usages.
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 *
	 * @return \Symfony\Component\HttpFoundation\JsonResponse
	 *
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \MySQLException
	 * @throws \OQLException
	 * @throws \Exception
	 * @since 3.1
	 *
	 */
	public function GetInformationForLinkedSetAsJsonAction(Request $oRequest)
	{
		// Data array
		$aData = [
			'js_inline'  => '',
			'css_inline' => '',
		];

		// Retrieving parameters
		$sObjectClass = $this->oRequestManipulatorHelper->ReadParam('sObjectClass', '');
		$sLinkClass = $this->oRequestManipulatorHelper->ReadParam('sLinkClass', '');
		$aObjectIds = $this->oRequestManipulatorHelper->ReadParam('aObjectIds', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		$aObjectAttCodes = $this->oRequestManipulatorHelper->ReadParam('aObjectAttCodes', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		$aLinkAttCodes = $this->oRequestManipulatorHelper->ReadParam('aLinkAttCodes', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		$sDateTimePickerWidgetParent = $this->oRequestManipulatorHelper->ReadParam('sDateTimePickerWidgetParent', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
		if (!MetaModel::IsLinkClass($sLinkClass)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' asked for wrong lnk class '.$sLinkClass);
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}
		if (empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes)) {
			IssueLog::Info(__METHOD__.' at line '.__LINE__.' : sObjectClass, aObjectIds and aObjectAttCodes expected, "'.$sObjectClass.'", "'.implode(
				'/',
				$aObjectIds
			).'" given.');
			throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Invalid request data, some information are missing');
		}

		// Building the search
		$bIgnoreSilos = $this->oScopeValidatorHelper->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
		$aParams = ['objects_id' => $aObjectIds];
		$oSearch = DBObjectSearch::FromOQL("SELECT $sObjectClass WHERE id IN (:objects_id)");
		if (!$this->oScopeValidatorHelper->AddScopeToQuery($oSearch, $sObjectClass)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sObjectClass.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}
		if ($bIgnoreSilos === true) {
			$oSearch->AllowAllData();
		}
		$oSet = new DBObjectSet($oSearch, [], $aParams);
		$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aObjectAttCodes]);

		// Checking that id is in the AttCodes
		// Note: We do that AFTER the array is used in OptimizeColumnLoad() because the function doesn't support this anymore.
		if (!in_array('id', $aObjectAttCodes)) {
			$aObjectAttCodes = array_merge(['id'], $aObjectAttCodes);
		}

		// Retrieving objects
		while ($oObject = $oSet->Fetch()) {
			// Prepare link data
			$aObjectData = $this->PrepareObjectInformation($oObject, $aObjectAttCodes);
			// New link object (needed for renderers)
			$aAttCodes = MetaModel::GetAttributesList($sLinkClass, ['AttributeExternalKey']);
			$sAttCodeToObject = '';
			foreach ($aAttCodes as $sAttCode) {
				$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
				/** @var \AttributeExternalKey $oAttDef */
				if ($oAttDef->GetTargetClass() === $sObjectClass) {
					$sAttCodeToObject = $sAttCode;
				}
			}
			if ($sAttCodeToObject === '') {
				IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' asked for incoherent lnk class '.$sLinkClass.' with object class '.$sObjectClass);
				throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
			}
			$oNewLink = MetaModel::NewObject($sLinkClass, [
				$sAttCodeToObject => $oObject->GetKey(), // so later placeholders in filters will be applied on external keys on the same link
			]);
			foreach ($aLinkAttCodes as $sAttCode) {
				$oAttDef = MetaModel::GetAttributeDef($sLinkClass, $sAttCode);
				/** @var \Combodo\iTop\Form\Field\SelectObjectField $oField */
				$oField = $oAttDef->MakeFormField($oNewLink);
				if ($oAttDef::GetFormFieldClass() === '\\Combodo\\iTop\\Form\\Field\\SelectObjectField') {
					$oFieldSearch = $oField->GetSearch();
					$sFieldClass = $oFieldSearch->GetClass();
					if ($this->oScopeValidatorHelper->AddScopeToQuery($oFieldSearch, $sFieldClass)) {
						$oField->SetSearch($oFieldSearch);
					} else {
						$oField->SetSearch(DBObjectSearch::FromOQL("SELECT $sFieldClass WHERE 1=0"));
					}
				}
				// Prevent datetimepicker popup to be truncated
				if ($oField instanceof DateTimeField) {
					$oField->SetDateTimePickerWidgetParent($sDateTimePickerWidgetParent);
				}

				// View data
				$sValue = $oAttDef->GetAsHTML($oNewLink->Get($sAttCode));
				$aObjectData['attributes']['lnk__'.$sAttCode] = [
					'object_class'   => $sLinkClass,
					'object_id'      => $oNewLink->GetKey(),
					'prefix'         => 'lnk__',
					'attribute_code' => $sAttCode,
					'attribute_type' => $oAttDef->GetTypeShortClassName(),
					'value_html'     => $sValue,
				];

				// If the field has a renderer we adjust view data
				$sFieldRendererClass = BsLinkedSetFieldRenderer::GetFieldRendererClass($oField);
				if ($sFieldRendererClass !== null) {
					$oFieldRenderer = new $sFieldRendererClass($oField);
					$oFieldOutput = $oFieldRenderer->Render();
					$aObjectData['attributes']['lnk__'.$sAttCode]['value_html'] = $oFieldOutput->GetHtml();
					$aObjectData['attributes']['lnk__'.$sAttCode]['css_inline'] = $oFieldOutput->GetCss();
					$aObjectData['attributes']['lnk__'.$sAttCode]['js_inline'] = $oFieldOutput->GetJs();
				}
			}

			$aData['items'][] = $aObjectData;
		}

		return new JsonResponse($aData);
	}

	/**
	 * Prepare a DBObject information as an array for a client side usage (typically, add a row in a table)
	 *
	 * @param \DBObject $oObject
	 * @param array $aAttCodes
	 *
	 * @return array
	 *
	 * @throws \CoreException
	 * @throws \Exception
	 */
	protected function PrepareObjectInformation(DBObject $oObject, $aAttCodes = [])
	{
		$sObjectClass = get_class($oObject);
		$aObjectData = [
			'id'         => $oObject->GetKey(),
			'name'       => $oObject->GetName(),
			'attributes' => [],
		];

		// Retrieving attributes definitions
		$aAttDefs = [];
		foreach ($aAttCodes as $sAttCode) {
			if ($sAttCode === 'id') {
				continue;
			}

			$aAttDefs[$sAttCode] = MetaModel::GetAttributeDef($sObjectClass, $sAttCode);
		}

		// Preparing attribute data
		foreach ($aAttDefs as $oAttDef) {
			$aAttData = [
				'object_class'  => $sObjectClass,
				'object_id'  => $oObject->GetKey(),
				'attribute_code' => $oAttDef->GetCode(),
				'attribute_type' => $oAttDef->GetTypeShortClassName(),
			];

			// - Value raw
			// For simple fields, we get the raw (stored) value as well
			$bExcludeRawValue = false;
			foreach (ApplicationHelper::GetAttDefClassesToExcludeFromMarkupMetadataRawValue() as $sAttDefClassToExclude) {
				if (is_a($oAttDef, $sAttDefClassToExclude, true)) {
					$bExcludeRawValue = true;
					break;
				}
			}
			$aAttData['value_raw'] = ($bExcludeRawValue === false) ? $oObject->Get($oAttDef->GetCode()) : null;

			if ($oAttDef->IsExternalKey()) {
				$aAttData['value_html'] = $oObject->GetAsHTML($oAttDef->GetCode().'_friendlyname');

				// Checking if user can access object's external key
				if ($this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $oAttDef->GetTargetClass())) {
					$aAttData['url'] = $this->oUrlGenerator->generate(
						'p_object_view',
						['sObjectClass' => $oAttDef->GetTargetClass(), 'sObjectId' => $oObject->Get($oAttDef->GetCode())]
					);
				}
			} elseif ($oAttDef->IsLinkSet()) {
				// We skip it
				continue;
			} elseif ($oAttDef instanceof AttributeImage) {
				/** @var \ormDocument $oOrmDoc */
				$oOrmDoc = $oObject->Get($oAttDef->GetCode());
				if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty()) {
					$sUrl = $this->oUrlGenerator->generate('p_object_document_display', [
						'sObjectClass' => get_class($oObject),
						'sObjectId' => $oObject->GetKey(),
						'sObjectField' => $oAttDef->GetCode(),
						'cache' => 86400,
						's' => $oOrmDoc->GetSignature(),
					]);
				} else {
					$sUrl = $oAttDef->Get('default_image');
				}
				$aAttData['value_html'] = '<img src="'.$sUrl.'" />';
			} elseif ($oAttDef instanceof AttributeEnum) {
				$aAttData['value_html'] = $oAttDef->GetAsPlainText($oObject->Get($oAttDef->GetCode()));
			} else {
				$aAttData['value_html'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode()));

				if ($oAttDef instanceof AttributeFriendlyName) {
					// Checking if user can access object
					if ($this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sObjectClass)) {
						$aAttData['url'] = $this->oUrlGenerator->generate(
							'p_object_view',
							['sObjectClass' => $sObjectClass, 'sObjectId' => $oObject->GetKey()]
						);
					}
				}
			}

			$aObjectData['attributes'][$oAttDef->GetCode()] = $aAttData;
		}

		return $aObjectData;
	}

	/**
	 * Displays the creation form of an instantiable class
	 *
	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
	 * @param string                                    $sObjectClass
	 *
	 * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
	 * @throws \ArchivedObjectException
	 * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \MissingQueryArgument
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 */
	protected function DisplayCreationForm(Request $oRequest, $sObjectClass)
	{
		// Checking security layers
		if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_CREATE, $sObjectClass)) {
			IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to create '.$sObjectClass.' object.');
			throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
		}

		$sOperation = $this->oRequestManipulatorHelper->ReadParam('operation', '');

		$aData = ['sMode' => 'create'];
		$aData['form'] = $this->oObjectFormHandlerHelper->HandleForm($oRequest, $aData['sMode'], $sObjectClass);
		$aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Create:Title', MetaModel::GetName($sObjectClass));

		// Preparing response
		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)) {
				$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
			} else {
				$oResponse = new JsonResponse($aData);
			}
		} else {
			// Adding brick if it was passed
			$sBrickId = $this->oRequestManipulatorHelper->ReadParam('sBrickId', '');
			if (!empty($sBrickId)) {
				$oBrick = $this->oBrickCollection->GetBrickById($sBrickId);
				if ($oBrick !== null) {
					$aData['oBrick'] = $oBrick;
				}
			}
			$aData['sPageTitle'] = $aData['form']['title'];
			$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
		}

		return $oResponse;
	}

	/**
	 * Displays a list of leaf classes from the abstract $sObjectClass which will lead to the actual creation form.
	 *
	 * @param string $sObjectClass
	 *
	 * @return \Symfony\Component\HttpFoundation\Response
	 * @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
	 * @throws \CoreException
	 * @throws \DictExceptionMissingString
	 * @throws \MissingQueryArgument
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 */
	protected function DisplayLeafClassesForm($sObjectClass)
	{
		$aData = [
			'aLeafClasses' => [],
			'sPageTitle' => Dict::Format('Brick:Portal:Object:Form:Create:Title', MetaModel::GetName($sObjectClass)),
			'sLeafClassesListId' => 'leaf_classes_list_'.uniqid(),
			'ar_token' => $this->oRequestManipulatorHelper->ReadParam('ar_token', ''),
		];
		$sTemplatePath = CreateBrick::DEFAULT_PAGE_TEMPLATE_PATH;

		$sBrickId = $this->oRequestManipulatorHelper->ReadParam('sBrickId', '');
		if (!empty($sBrickId)) {
			$oBrick = $this->oBrickCollection->GetBrickById($sBrickId);
			$sTemplatePath = $oBrick->GetTemplatePath('page');

			$aData['sBrickId'] = $sBrickId;
			$aData['oBrick'] = $oBrick;
			$aData['sPageTitle'] = $oBrick->GetTitle();
		}

		$aLeafClasses = [];
		$aChildClasses = MetaModel::EnumChildClasses($sObjectClass);
		foreach ($aChildClasses as $sChildClass) {
			if (!MetaModel::IsAbstract($sChildClass) && $this->oSecurityHelper->IsActionAllowed(UR_ACTION_CREATE, $sChildClass)) {
				$aLeafClasses[] = [
					'id' => $sChildClass,
					'name' => MetaModel::GetName($sChildClass),
				];
			}
		}
		$aData['aLeafClasses'] = $aLeafClasses;

		$oResponse = $this->render($sTemplatePath, $aData);

		return $oResponse;
	}
}
