diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index f5eef0f39..d68e1e434 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,5 +1,5 @@
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index de8d7f4ee..c2bae49d7 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 0683dad00..f7a9fe164 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -1851,7 +1851,28 @@ EOF
$sConfigJS = json_encode($aConfig);
$oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, $sConfigJS);"); // Transform $iId into a CKEdit
- break;
+
+ $oPage->add_ready_script(
+<<GetEditValue($value);
@@ -2636,7 +2657,7 @@ EOF
else
{
$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
- if (count($aAllowedValues) == 1)
+ if (is_array($aAllowedValues) && count($aAllowedValues) == 1)
{
$aValues = array_keys($aAllowedValues);
$this->Set($sAttCode, $aValues[0]);
@@ -3954,7 +3975,7 @@ EOF
$currValue = $oObj->Get($sAttCode);
if ($oAttDef instanceof AttributeCaseLog)
{
- $currValue = ' '; // Don't put an empty string, in case the field would be considered as mandatory...
+ $currValue = ''; // Put a single scalar value to force caselog to mock a new entry. For more info see N°1059.
}
elseif ($currValue instanceof ormSet)
{
diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php
index 4da079129..3f7863c29 100644
--- a/core/attributedef.class.inc.php
+++ b/core/attributedef.class.inc.php
@@ -7241,10 +7241,8 @@ class AttributeImage extends AttributeBlob
$value = $oObject->Get($this->GetCode());
if (is_object($value) && !$value->IsEmpty())
{
- $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(),
- $this->GetCode()));
- $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(),
- $this->GetCode()));
+ $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
+ $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode()));
}
else
{
diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php
index 797dda9e9..37bcc1382 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php
@@ -1,1531 +1,1531 @@
-
-
-namespace Combodo\iTop\Portal\Controller;
-
-use Silex\Application;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Exception;
-use FileUploadException;
-use utils;
-use Dict;
-use IssueLog;
-use MetaModel;
-use DBObject;
-use DBSearch;
-use DBObjectSearch;
-use FalseExpression;
-use BinaryExpression;
-use FieldExpression;
-use VariableExpression;
-use ListExpression;
-use ScalarExpression;
-use DBObjectSet;
-use AttributeEnum;
-use AttributeImage;
-use AttributeFinalClass;
-use AttributeFriendlyName;
-use UserRights;
-use iPopupMenuExtension;
-use URLButtonItem;
-use JSButtonItem;
-use Combodo\iTop\Portal\Helper\ApplicationHelper;
-use Combodo\iTop\Portal\Helper\SecurityHelper;
-use Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
-use Combodo\iTop\Portal\Form\ObjectFormManager;
-use Combodo\iTop\Renderer\Bootstrap\BsFormRenderer;
-
-/**
- * Class ObjectController
- *
- * Controller to handle basic view / edit / create of cmdbAbstractObjectClass ManageBrickController
- *
- * @package Combodo\iTop\Portal\Controller
- * @author Guillaume Lajarige
- * @since 2.3.0
- */
-class ObjectController extends AbstractController
-{
-
- const ENUM_MODE_VIEW = 'view';
- const ENUM_MODE_EDIT = 'edit';
- const ENUM_MODE_CREATE = 'create';
-
- const DEFAULT_PAGE_NUMBER = 1;
- const DEFAULT_LIST_LENGTH = 10;
-
- /**
- * Displays an cmdbAbstractObject if the connected user is allowed to.
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @param string $sObjectClass (Class must be instance of cmdbAbstractObject)
- * @param string $sObjectId
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \DictExceptionMissingString
- */
- public function ViewAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId)
- {
- // Checking parameters
- if ($sObjectClass === '' || $sObjectId === '')
- {
- IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.');
- $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id'));
- }
-
- // Checking security layers
- if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass, $sObjectId))
- {
- IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . '::' . $sObjectId . ' object.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Retrieving object
- $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
-
- $aData = array('sMode' => 'view');
- $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId);
- $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), $oObject->GetName());
-
- // Add an edit button if user is allowed
- if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId))
- {
- $oModifyButton = new URLButtonItem(
- 'modify_object',
- Dict::S('UI:Menu:Modify'),
- $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
- );
- // Putting this one first
- $aData['form']['buttons']['links'][] = $oModifyButton->GetMenuItem();
- }
-
- // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
- }
- else
- {
- $oResponse = $oApp->json($aData);
- }
- }
- else
- {
- // Adding brick if it was passed
- $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
- if (!empty($sBrickId))
- {
- $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
- if ($oBrick !== null)
- {
- $aData['oBrick'] = $oBrick;
- }
- }
- $aData['sPageTitle'] = $aData['form']['title'];
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
- }
-
- return $oResponse;
- }
-
- /**
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @param $sObjectClass
- * @param $sObjectId
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \DictExceptionMissingString
- */
- public function EditAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId)
- {
- // Checking parameters
- if ($sObjectClass === '' || $sObjectId === '')
- {
- IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.');
- $oApp->abort(500, 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 (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite)
- {
- IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Retrieving object
- $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
-
- $aData = array('sMode' => 'edit');
- $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId);
- $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', MetaModel::GetName($sObjectClass), $aData['form']['object_name']);
-
- // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
- }
- else
- {
- $oResponse = $oApp->json($aData);
- }
- }
- else
- {
- // Adding brick if it was passed
- $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
- if (!empty($sBrickId))
- {
- $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
- if ($oBrick !== null)
- {
- $aData['oBrick'] = $oBrick;
- }
- }
- $aData['sPageTitle'] = $aData['form']['title'];
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
- }
-
- return $oResponse;
- }
-
- /**
- * Creates an cmdbAbstractObject of the $sObjectClass
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @param string $sObjectClass
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Exception
- * @throws \CoreException
- * @throws \DictExceptionMissingString
- */
- public function CreateAction(Request $oRequest, Application $oApp, $sObjectClass)
- {
- // Checking security layers
- if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_CREATE, $sObjectClass))
- {
- IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to create ' . $sObjectClass . ' object.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
-
- $aData = array('sMode' => 'create');
- $aData['form'] = $this->HandleForm($oRequest, $oApp, $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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
- }
- else
- {
- $oResponse = $oApp->json($aData);
- }
- }
- else
- {
- // Adding brick if it was passed
- $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
- if (!empty($sBrickId))
- {
- $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
- if ($oBrick !== null)
- {
- $aData['oBrick'] = $oBrick;
- }
- }
- $aData['sPageTitle'] = $aData['form']['title'];
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
- }
-
- 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 \Silex\Application $oApp
- * @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 \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- */
- public function CreateFromFactoryAction(Request $oRequest, Application $oApp, $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.');
- $oApp->abort(500, '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 = array(
- 'sObjectClass' => get_class($oTargetObject)
- );
- $sRedirectRoute = $oApp['url_generator']->generate('p_object_create', $aRouteParams);
- // - Request
- $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all());
-
- return $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true);
- }
-
- /**
- * Applies a stimulus $sStimulus on an cmdbAbstractObject
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @param string $sObjectClass
- * @param string $sObjectId
- * @param string $sStimulusCode
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- */
- public function ApplyStimulusAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sStimulusCode)
- {
- // Checking parameters
- if ($sObjectClass === '' || $sObjectId === '' || $sStimulusCode === '')
- {
- IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, sObjectId and $sStimulusCode expected, "' . $sObjectClass . '", "' . $sObjectId . '" and "' . $sStimulusCode . '" given.');
- $oApp->abort(500, Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus'));
- }
-
- // Checking security layers
- if(!SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass))
- {
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Retrieving object
- $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Retrieving request parameters
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
-
- // Retrieving form properties
- $aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($oApp, $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 = array(
- 'id' => 'apply-stimulus',
- 'type' => 'custom_list',
- 'fields' => array(),
- 'layout' => null
- );
- }
-
- // Adding stimulus code to form
- $aFormProperties['stimulus_code'] = $sStimulusCode;
-
- // Adding target_state to current_values
- $oRequest->request->set('apply_stimulus', array('code' => $sStimulusCode));
-
- $aData = array('sMode' => 'apply_stimulus');
- $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties);
- $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Stimulus:Title');
- $aData['form']['validation']['redirection'] = array(
- 'url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
- );
-
- // 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', '');
-
- $aData = array('sMode' => 'apply_stimulus');
- $aData['form'] = $this->HandleForm($oSubRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties);
- // Redefining the array to be as simple as possible :
- $aData = array('redirection' =>
- array('url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)))
- );
- }
- }
-
- // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
- }
- elseif ($sOperation === 'redirect')
- {
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/modal/mode_loader.html.twig', $aData);
- }
- else
- {
- $oResponse = $oApp->json($aData);
- }
- }
- else
- {
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
- }
-
- return $oResponse;
- }
-
- /**
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @param string $sMode
- * @param string $sObjectClass
- * @param string $sObjectId
- * @param string $aFormProperties
- *
- * @return array
- *
- * @throws \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \OQLException
- * @throws \Twig_Error_Loader
- * @throws \Twig_Error_Runtime
- * @throws \Twig_Error_Syntax
- */
- public static function HandleForm(Request $oRequest, Application $oApp, $sMode, $sObjectClass, $sObjectId = null, $aFormProperties = null)
- {
- $aFormData = array();
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
- $bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation));
-
- // - Retrieve form properties
- if ($aFormProperties === null)
- {
- $aFormProperties = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, $sMode);
- }
-
- // - Create and
- if (empty($sOperation))
- {
- // 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 = $oApp['request_manipulator']->ReadParam('ar_token', '');
- $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array();
-
- // Preparing object
- if ($sObjectId === null)
- {
- // Create new UserRequest
- $oObject = MetaModel::NewObject($sObjectClass);
-
- // Retrieve action rules information to auto-fill the form if available
- // Preparing object
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oObject);
- $aPrefillFormParam = array( 'user' => $_SESSION["auth_user"],
- 'origin' => 'portal');
- $oObject->PrefillForm('creation_from_0', $aPrefillFormParam);
- }
- else
- {
- $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass));
- }
-
- // Preparing buttons
- $aFormData['buttons'] = array(
- 'transitions' => array(),
- 'actions' => array(),
- 'links' => array(),
- 'submit' => array(
- 'label' => Dict::S('Portal:Button:Submit'),
- ),
- );
- if ($sMode !== 'apply_stimulus')
- {
- // Add transition buttons
- $oSetToCheckRights = DBObjectSet::FromObject($oObject);
- $aStimuli = Metamodel::EnumStimuli($sObjectClass);
- foreach ($oObject->EnumTransitions() as $sStimulusCode => $aTransitionDef)
- {
- if(SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass, $oSetToCheckRights))
- {
- $aFormData['buttons']['transitions'][$sStimulusCode] = $aStimuli[$sStimulusCode]->GetLabel();
- }
- }
-
- // Add plugin buttons
- foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
- {
- foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJDETAILS_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oObject)) as $oMenuItem)
- {
- if (is_object($oMenuItem))
- {
- if($oMenuItem instanceof JSButtonItem)
- {
- $aFormData['buttons']['actions'][] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts());
- }
- elseif($oMenuItem instanceof URLButtonItem)
- {
- $aFormData['buttons']['links'][] = $oMenuItem->GetMenuItem();
- }
- }
- }
- }
-
- // Hiding submit button or changing its label if necessary
- if(!empty($aFormData['buttons']['transitions']) && isset($aFormProperties['properties']) &&$aFormProperties['properties']['always_show_submit'] === false)
- {
- unset($aFormData['buttons']['submit']);
- }
- elseif($sMode === static::ENUM_MODE_EDIT)
- {
- $aFormData['buttons']['submit']['label'] = Dict::S('Portal:Button:Apply');
- }
- }
- else
- {
- $aPrefillFormParam = array(
- 'user' => $_SESSION["auth_user"],
- 'origin' => 'portal',
- 'stimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)['code'],
- );
- $oObject->PrefillForm('state_change', $aPrefillFormParam);
- }
-
- // Preparing callback urls
- $aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal);
- $aFormData['submit_callback'] = $aCallbackUrls['submit'];
- $aFormData['cancel_callback'] = $aCallbackUrls['cancel'];
-
- // Preparing renderer
- // Note : We might need to distinguish form & renderer endpoints
- if (in_array($sMode, array('create', 'edit', 'view')))
- {
- $sFormEndpoint = $oApp['url_generator']->generate('p_object_' . $sMode, array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId));
- }
- else
- {
- $sFormEndpoint = $_SERVER['REQUEST_URI'];
- }
- $oFormRenderer = new BsFormRenderer();
- $oFormRenderer->SetEndpoint($sFormEndpoint);
-
- $oFormManager = new ObjectFormManager();
- $oFormManager->SetApplication($oApp)
- ->SetObject($oObject)
- ->SetMode($sMode)
- ->SetActionRulesToken($sActionRulesToken)
- ->SetRenderer($oFormRenderer)
- ->SetFormProperties($aFormProperties);
-
- $oFormManager->Build();
-
- // Check the number of editable fields
- $aFormData['editable_fields_count'] = $oFormManager->GetForm()->GetEditableFieldCount();
- }
- else
- {
- // Update / Submit / Cancel
- $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
- $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
- if ( empty($sFormManagerClass) || empty($sFormManagerData) )
- {
- IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.');
- $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.');
- }
-
- $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
- $oFormManager->SetApplication($oApp);
-
- // Applying action rules if present
- if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
- {
- $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
- $oObj = $oFormManager->GetObject();
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
- $oFormManager->SetObject($oObj);
- }
-
- switch ($sOperation)
- {
- case 'submit':
- // Applying modification to object
- $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'attachmentIds' => $oApp['request_manipulator']->ReadParam('attachment_ids', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties, 'applyStimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)));
- if ($aFormData['validation']['valid'] === true)
- {
- // Note : We don't use $sObjectId there as it can be null if we are creating a new one. Instead we use the id from the created object once it has been seralized
- // Check if stimulus has to be applied
- $sStimulusCode = $oApp['request_manipulator']->ReadParam('stimulus_code', '');
- if (!empty($sStimulusCode))
- {
- $aFormData['validation']['redirection'] = array(
- 'url' => $oApp['url_generator']->generate('p_object_apply_stimulus', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey(), 'sStimulusCode' => $sStimulusCode)),
- 'ajax' => true
- );
- }
- // Otherwise, we show the object if there is no default
-// else
-// {
-// $aFormData['validation']['redirection'] = array(
-// 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey()))
-// );
-// }
- }
- break;
-
- case 'update':
- $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties));
- break;
-
- case 'cancel':
- $oFormManager->OnCancel();
- break;
- }
- }
-
- // Preparing field_set data
- $aFieldSetData = array(
- //'fields_list' => $oFormManager->GetRenderer()->Render(), // GLA : This should be done just after in the if statement.
- 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(),
- 'form_path' => $oFormManager->GetForm()->GetId()
- );
-
- // Preparing fields list regarding the operation
- if ($sOperation === 'update')
- {
- $aRequestedFields = $oApp['request_manipulator']->ReadParam('requested_fields', array(), FILTER_UNSAFE_RAW);
- $sFormPath = $oApp['request_manipulator']->ReadParam('form_path', '');
-
- // Checking if the update was on a subform, if so we need to make the rendering for that part only
- if ( !empty($sFormPath) && $sFormPath !== $oFormManager->GetForm()->GetId() )
- {
- $oSubForm = $oFormManager->GetForm()->FindSubForm($sFormPath);
- $oSubFormRenderer = new BsFormRenderer($oSubForm);
- $oSubFormRenderer->SetEndpoint($oFormManager->GetRenderer()->GetEndpoint());
- $aFormData['updated_fields'] = $oSubFormRenderer->Render($aRequestedFields);
- }
- else
- {
- $aFormData['updated_fields'] = $oFormManager->GetRenderer()->Render($aRequestedFields);
- }
- }
- else
- {
- $aFieldSetData['fields_list'] = $oFormManager->GetRenderer()->Render();
- }
-
- // Preparing form data
- $aFormData['id'] = $oFormManager->GetForm()->GetId();
- $aFormData['transaction_id'] = $oFormManager->GetForm()->GetTransactionId();
- $aFormData['formmanager_class'] = $oFormManager->GetClass();
- $aFormData['formmanager_data'] = $oFormManager->ToJSON();
- $aFormData['renderer'] = $oFormManager->GetRenderer();
- $aFormData['object_name'] = $oFormManager->GetObject()->GetName();
- $aFormData['object_state'] = $oFormManager->GetObject()->GetState();
- $aFormData['fieldset'] = $aFieldSetData;
- $aFormData['display_mode'] = (isset($aFormProperties['properties'])) ? $aFormProperties['properties']['display_mode'] : ApplicationHelper::FORM_DEFAULT_DISPLAY_MODE;
-
- return $aFormData;
- }
-
- /**
- * Handles the autocomplete search
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @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 \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \OQLException
- */
- public function SearchAutocompleteAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
- {
- $aData = array(
- 'results' => array(
- 'count' => 0,
- 'items' => array()
- )
- );
-
- // 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.');
- $oApp->abort(500, Dict::Format('UI:Error:ParameterMissing', 'sQuery'));
- }
-
- // Retrieving parameters
- $sQuery = $aRequestContent['sQuery'];
- $sFieldId = $aRequestContent['sFieldId'];
-
- // Checking security layers
- if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId))
- {
- IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sHostObjectClass . '::' . $sHostObjectId . '.');
- $oApp->abort(404, 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 = $oApp['request_manipulator']->ReadParam('ar_token', '');
- $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array();
- // Preparing object
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject);
- }
-
- // Updating host object with form data / values
- $sFormManagerClass = $aRequestContent['formmanager_class'];
- $sFormManagerData = $aRequestContent['formmanager_data'];
- if (!empty($sFormManagerClass) && !empty($sFormManagerData))
- {
- $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
- $oFormManager->SetApplication($oApp);
- $oFormManager->SetObject($oHostObject);
-
- // Applying action rules if present
- if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
- {
- $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
- $oObj = $oFormManager->GetObject();
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
- $oFormManager->SetObject($oObj);
- }
-
- // Updating host object
- $oFormManager->OnUpdate(array('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);
- $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 responsability of the template designer to write the right query so the user see only what he should.
- if ($oTargetAttDef->GetEditClass() !== 'CustomFields')
- {
- $oScopeSearch = $oApp['scope_validator']->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, array(), array('this' => $oHostObject, 'ac_query' => '%' . $sQuery . '%'));
- $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => array('friendlyname')));
- // Note : This limit is also used in the field renderer by typeahead to determine how many suggestions to display
- if ($oTargetAttDef->GetEditClass() === 'CustomFields')
- {
- $oSet->SetLimit(static::DEFAULT_LIST_LENGTH);
- }
- else
- {
- $oSet->SetLimit($oTargetAttDef->GetMaximumComboLength()); // TODO : Is this the right limit value ? We might want to use another parameter
- }
- // - Retrieving objects
- while ($oItem = $oSet->Fetch())
- {
- $aData['results']['items'][] = array('id' => $oItem->GetKey(), 'name' => html_entity_decode($oItem->GetName(), ENT_QUOTES, 'UTF-8'));
- $aData['results']['count'] ++;
- }
-
- // Preparing response
- if ($oRequest->isXmlHttpRequest())
- {
- $oResponse = $oApp->json($aData);
- }
- else
- {
- $oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- return $oResponse;
- }
-
- /**
- * Handles the regular (table) search from an attribute
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @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 \Exception
- * @throws \ArchivedObjectException
- * @throws \CoreException
- * @throws \DictExceptionMissingString
- * @throws \OQLException
- */
- public function SearchFromAttributeAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
- {
- $aData = array(
- 'sMode' => 'search_regular',
- 'sTargetAttCode' => $sTargetAttCode,
- 'sHostObjectClass' => $sHostObjectClass,
- 'sHostObjectId' => $sHostObjectId,
- 'sActionRulesToken' => $oApp['request_manipulator']->ReadParam('ar_token', ''),
- );
-
- // Checking security layers
- if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId))
- {
- IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sHostObjectClass . '::' . $sHostObjectId . ' object.');
- $oApp->abort(404, 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']) : array();
- // Preparing object
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject);
- }
-
- // Updating host object with form data / values
- $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
- $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
- if ( !empty($sFormManagerClass) && !empty($sFormManagerData) )
- {
- $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
- $oFormManager->SetApplication($oApp);
- $oFormManager->SetObject($oHostObject);
-
- // Applying action rules if present
- if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
- {
- $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
- $oObj = $oFormManager->GetObject();
- $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
- $oFormManager->SetObject($oObj);
- }
-
- // Updating host object
- $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW)));
- $oHostObject = $oFormManager->GetObject();
- }
-
- // Retrieving request parameters
- $iPageNumber = $oApp['request_manipulator']->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT);
- $iListLength = $oApp['request_manipulator']->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT);
- $bInitalPass = $oApp['request_manipulator']->HasParam('draw') ? false : true;
- $sQuery = $oApp['request_manipulator']->ReadParam('sSearchValue', '');
- $sFormPath = $oApp['request_manipulator']->ReadParam('sFormPath', '');
- $sFieldId = $oApp['request_manipulator']->ReadParam('sFieldId', '');
- $aObjectIdsToIgnore = $oApp['request_manipulator']->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW);
-
- // Building search query
- // - Retrieving target object class from attcode
- $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode);
- if ($oTargetAttDef->IsExternalKey())
- {
- $sTargetObjectClass = $oTargetAttDef->GetTargetClass();
- }
- elseif ($oTargetAttDef->IsLinkSet())
- {
- if (!$oTargetAttDef->IsIndirect())
- {
- $sTargetObjectClass = $oTargetAttDef->GetLinkedClass();
- }
- else
- {
- $oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote());
- $sTargetObjectClass = $oRemoteAttDef->GetTargetClass();
- }
- }
- elseif ($oTargetAttDef->GetEditClass() === 'CustomFields')
- {
- $oRequestTemplate = $oHostObject->Get($sTargetAttCode);
- $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($oApp, $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(array($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 responsability of the template designer to write the right query so the user see only what he should.
- $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ);
- $aInternalParams = array();
- if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields'))
- {
- IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' has no scope query for ' . $sTargetObjectClass . ' class.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // - Base query from meta model
- 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 = array();
- 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;
- 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 = array();
- 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 attributs
- 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 responsability 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, array(), $aInternalParams);
- $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => $aAttCodes));
- $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
- // - Retrieving columns properties
- $aColumnProperties = array();
- foreach ($aAttCodes as $sAttCode)
- {
- $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode);
- $aColumnProperties[$sAttCode] = array(
- 'title' => $oAttDef->GetLabel()
- );
- }
- // - Retrieving objects
- $aItems = array();
- while ($oItem = $oSet->Fetch())
- {
- $aItems[] = $this->PrepareObjectInformations($oApp, $oItem, $aAttCodes);
- }
-
- // Preparing response
- if ($bInitalPass)
- {
- $aData = $aData + array(
- 'form' => array(
- 'id' => 'object_search_form_' . time(),
- 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel(), MetaModel::GetName($sTargetObjectClass))
- ),
- 'aColumnProperties' => json_encode($aColumnProperties),
- 'aResults' => array(
- 'aItems' => json_encode($aItems),
- 'iCount' => count($aItems)
- ),
- 'bMultipleSelect' => $oTargetAttDef->IsLinkSet(),
- 'aSource' => array(
- 'sFormPath' => $sFormPath,
- 'sFieldId' => $sFieldId,
- 'aObjectIdsToIgnore' => $aObjectIdsToIgnore,
- 'sFormManagerClass' => $sFormManagerClass,
- 'sFormManagerData' => $sFormManagerData
- )
- );
-
- if ($oRequest->isXmlHttpRequest())
- {
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
- }
- else
- {
- //$oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
- }
- }
- else
- {
- $aData = $aData + array(
- 'levelsProperties' => $aColumnProperties,
- 'data' => $aItems,
- 'recordsTotal' => $oSet->Count(),
- 'recordsFiltered' => $oSet->Count()
- );
-
- $oResponse = $oApp->json($aData);
- }
-
- return $oResponse;
- }
-
- /**
- * Handles the hierarchical search from an attribute
- *
- * @param \Symfony\Component\HttpFoundation\Request $oRequest
- * @param \Silex\Application $oApp
- * @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 void
- *
- */
- public function SearchHierarchyAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
- {
- // TODO
- }
-
- /**
- * 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 \Silex\Application $oApp
- * @param string $sOperation
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \ArchivedObjectException
- * @throws \CoreException
- */
- public function DocumentAction(Request $oRequest, Application $oApp, $sOperation = null)
- {
- // Setting default operation
- if($sOperation === null)
- {
- $sOperation = 'display';
- }
-
- // Retrieving ormDocument's host object
- $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', '');
- $sObjectId = $oApp['request_manipulator']->ReadParam('sObjectId', '');
- $sObjectField = $oApp['request_manipulator']->ReadParam('sObjectField', '');
-
- // 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, true, true);
- $sHostClass = $oAttachment->Get('item_class');
- $sHostId = $oAttachment->Get('item_id');
- }
- else
- {
- $sHostClass = $sObjectClass;
- $sHostId = $sObjectId;
- }
-
- // Checking security layers
- if (!SecurityHelper::IsActionAllowed($oApp, 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.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Retrieving object
- $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* Must not be found */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sHostClass));
- 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 . '.');
- $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
- }
-
- // Setting cache timeout
- // Note: Attachment download should be handle through AttachmentAction()
- if($sObjectClass === 'Attachment')
- {
- // One year ahead: an attachement cannot change
- $iCacheSec = 31556926;
- }
- else
- {
- $iCacheSec = $oApp['request_manipulator']->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT);
- }
-
- $aHeaders = array();
- 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';
- // 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().'"';
-
- 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 \Silex\Application $oApp
- * @param string $sOperation
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \Exception
- * @throws \CoreException
- * @throws \CoreUnexpectedValue
- */
- public function AttachmentAction(Request $oRequest, Application $oApp, $sOperation = null)
- {
- $aData = array(
- 'att_id' => 0,
- 'preview' => false,
- 'msg' => ''
- );
-
- // Retrieving sOperation from request only if it wasn't forced (determined by the route)
- if ($sOperation === null)
- {
- $sOperation = $oApp['request_manipulator']->ReadParam('operation', null);
- }
- switch ($sOperation)
- {
- case 'add':
- $sFieldName = $oApp['request_manipulator']->ReadParam('field_name', '');
- $sObjectClass = $oApp['request_manipulator']->ReadParam('object_class', '');
- $sTempId = $oApp['request_manipulator']->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);
- $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'] = htmlentities($oDocument->GetFileName(), ENT_QUOTES, 'UTF-8');
- // TODO : Change icon location when itop-attachment is refactored
- //$aData['icon'] = utils::GetAbsoluteUrlAppRoot() . AttachmentPlugIn::GetFileIcon($oDoc->GetFileName());
- $aData['icon'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-attachments/icons/image.png';
- $aData['att_id'] = $iAttId;
- $aData['preview'] = $oDocument->IsPreviewAvailable() ? 'true' : 'false';
- }
- 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 = $oApp->json($aData, 200, array('Content-Type' => 'text/plain'));
- break;
-
- case 'download':
- // Preparing redirection
- // - Route
- $aRouteParams = array(
- 'sObjectClass' => 'Attachment',
- 'sObjectId' => $oApp['request_manipulator']->ReadParam('sAttachmentId', null),
- 'sObjectField' => 'contents',
- );
- $sRedirectRoute = $oApp['url_generator']->generate('p_object_document_download', $aRouteParams);
- // - Request
- $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all());
-
- $oResponse = $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true);
- break;
-
- default:
- $oApp->abort(403);
- 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
- * @param \Silex\Application $oApp
- *
- * @return \Symfony\Component\HttpFoundation\Response
- *
- * @throws \OQLException
- * @throws \CoreException
- */
- public function GetInformationsAsJsonAction(Request $oRequest, Application $oApp)
- {
- $aData = array();
-
- // Retrieving parameters
- $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', '');
- $aObjectIds = $oApp['request_manipulator']->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW);
- $aObjectAttCodes = $oApp['request_manipulator']->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW);
- if ( empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes) )
- {
- IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, aObjectIds and aObjectAttCodes expected, "' . $sObjectClass . '", "' . implode('/', $aObjectIds) . '" given.');
- $oApp->abort(500, 'Invalid request data, some informations are missing');
- }
-
- // Checking that id is in the AttCodes
- if (!in_array('id', $aObjectAttCodes))
- {
- $aObjectAttCodes = array_merge(array('id'), $aObjectAttCodes);
- }
-
- // Building the search
- $bIgnoreSilos = $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
- $oSearch = DBObjectSearch::FromOQL("SELECT " . $sObjectClass . " WHERE id IN ('" . implode("','", $aObjectIds) . "')");
- if ($bIgnoreSilos === true)
- {
- $oSearch->AllowAllData();
- }
- $oSet = new DBObjectSet($oSearch);
- $oSet->OptimizeColumnLoad($aObjectAttCodes);
-
- // Retrieving objects
- while ($oObject = $oSet->Fetch())
- {
- $aData['items'][] = $this->PrepareObjectInformations($oApp, $oObject, $aObjectAttCodes);
- }
-
- return $oApp->json($aData);
- }
-
- /**
- * Prepare a DBObject informations as an array for a client side usage (typically, add a row in a table)
- *
- * @param \Silex\Application $oApp
- * @param \DBObject $oObject
- * @param array $aAttCodes
- *
- * @return array
- *
- * @throws \Exception
- * @throws \CoreException
- */
- protected function PrepareObjectInformations(Application $oApp, DBObject $oObject, $aAttCodes = array())
- {
- $sObjectClass = get_class($oObject);
- $aObjectData = array(
- 'id' => $oObject->GetKey(),
- 'name' => $oObject->GetName(),
- 'attributes' => array(),
- );
-
- // Retrieving attributes definitions
- $aAttDefs = array();
- foreach ($aAttCodes as $sAttCode)
- {
- if ($sAttCode === 'id')
- continue;
-
- $aAttDefs[$sAttCode] = MetaModel::GetAttributeDef($sObjectClass, $sAttCode);
- }
-
- // Preparing attribute data
- foreach ($aAttDefs as $oAttDef)
- {
- $aAttData = array(
- 'att_code' => $oAttDef->GetCode()
- );
-
- if ($oAttDef->IsExternalKey())
- {
- $aAttData['value'] = $oObject->GetAsHTML($oAttDef->GetCode() . '_friendlyname');
-
- // Checking if user can access object's external key
- if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass()))
- {
- $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $oAttDef->GetTargetClass(), 'sObjectId' => $oObject->Get($oAttDef->GetCode())));
- }
- }
- elseif ($oAttDef->IsLinkSet())
- {
- // We skip it
- continue;
- }
- elseif ($oAttDef instanceof AttributeImage)
- {
- $oOrmDoc = $oObject->Get($oAttDef->GetCode());
- if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty())
- {
- $sUrl = $oApp['url_generator']->generate('p_object_document_display', array('sObjectClass' => get_class($oObject), 'sObjectId' => $oObject->GetKey(), 'sObjectField' => $oAttDef->GetCode(), 'cache' => 86400));
- }
- else
- {
- $sUrl = $oAttDef->Get('default_image');
- }
- $aAttData['value'] = ' ';
- }
- else
- {
- $aAttData['value'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode()));
-
- if ($oAttDef instanceof AttributeFriendlyName)
- {
- // Checking if user can access object
- if(SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass))
- {
- $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oObject->GetKey()));
- }
- }
- }
-
- $aObjectData['attributes'][$oAttDef->GetCode()] = $aAttData;
- }
-
- return $aObjectData;
- }
-
-}
+
+
+namespace Combodo\iTop\Portal\Controller;
+
+use Silex\Application;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Exception;
+use FileUploadException;
+use utils;
+use Dict;
+use IssueLog;
+use MetaModel;
+use DBObject;
+use DBSearch;
+use DBObjectSearch;
+use FalseExpression;
+use BinaryExpression;
+use FieldExpression;
+use VariableExpression;
+use ListExpression;
+use ScalarExpression;
+use DBObjectSet;
+use AttributeEnum;
+use AttributeImage;
+use AttributeFinalClass;
+use AttributeFriendlyName;
+use UserRights;
+use iPopupMenuExtension;
+use URLButtonItem;
+use JSButtonItem;
+use Combodo\iTop\Portal\Helper\ApplicationHelper;
+use Combodo\iTop\Portal\Helper\SecurityHelper;
+use Combodo\iTop\Portal\Helper\ContextManipulatorHelper;
+use Combodo\iTop\Portal\Form\ObjectFormManager;
+use Combodo\iTop\Renderer\Bootstrap\BsFormRenderer;
+
+/**
+ * Class ObjectController
+ *
+ * Controller to handle basic view / edit / create of cmdbAbstractObjectClass ManageBrickController
+ *
+ * @package Combodo\iTop\Portal\Controller
+ * @author Guillaume Lajarige
+ * @since 2.3.0
+ */
+class ObjectController extends AbstractController
+{
+
+ const ENUM_MODE_VIEW = 'view';
+ const ENUM_MODE_EDIT = 'edit';
+ const ENUM_MODE_CREATE = 'create';
+
+ const DEFAULT_PAGE_NUMBER = 1;
+ const DEFAULT_LIST_LENGTH = 10;
+
+ /**
+ * Displays an cmdbAbstractObject if the connected user is allowed to.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @param string $sObjectClass (Class must be instance of cmdbAbstractObject)
+ * @param string $sObjectId
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ */
+ public function ViewAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId)
+ {
+ // Checking parameters
+ if ($sObjectClass === '' || $sObjectId === '')
+ {
+ IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.');
+ $oApp->abort(500, Dict::Format('UI:Error:2ParametersMissing', 'class', 'id'));
+ }
+
+ // Checking security layers
+ if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass, $sObjectId))
+ {
+ IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . '::' . $sObjectId . ' object.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Retrieving object
+ $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
+
+ $aData = array('sMode' => 'view');
+ $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId);
+ $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), $oObject->GetName());
+
+ // Add an edit button if user is allowed
+ if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId))
+ {
+ $oModifyButton = new URLButtonItem(
+ 'modify_object',
+ Dict::S('UI:Menu:Modify'),
+ $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
+ );
+ // Putting this one first
+ $aData['form']['buttons']['links'][] = $oModifyButton->GetMenuItem();
+ }
+
+ // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
+ }
+ else
+ {
+ $oResponse = $oApp->json($aData);
+ }
+ }
+ else
+ {
+ // Adding brick if it was passed
+ $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
+ if (!empty($sBrickId))
+ {
+ $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
+ if ($oBrick !== null)
+ {
+ $aData['oBrick'] = $oBrick;
+ }
+ }
+ $aData['sPageTitle'] = $aData['form']['title'];
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
+ }
+
+ return $oResponse;
+ }
+
+ /**
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @param $sObjectClass
+ * @param $sObjectId
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ */
+ public function EditAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId)
+ {
+ // Checking parameters
+ if ($sObjectClass === '' || $sObjectId === '')
+ {
+ IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass and sObjectId expected, "' . $sObjectClass . '" and "' . $sObjectId . '" given.');
+ $oApp->abort(500, 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 (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite)
+ {
+ IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Retrieving object
+ $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
+
+ $aData = array('sMode' => 'edit');
+ $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId);
+ $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', MetaModel::GetName($sObjectClass), $aData['form']['object_name']);
+
+ // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
+ }
+ else
+ {
+ $oResponse = $oApp->json($aData);
+ }
+ }
+ else
+ {
+ // Adding brick if it was passed
+ $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
+ if (!empty($sBrickId))
+ {
+ $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
+ if ($oBrick !== null)
+ {
+ $aData['oBrick'] = $oBrick;
+ }
+ }
+ $aData['sPageTitle'] = $aData['form']['title'];
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
+ }
+
+ return $oResponse;
+ }
+
+ /**
+ * Creates an cmdbAbstractObject of the $sObjectClass
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @param string $sObjectClass
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Exception
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ */
+ public function CreateAction(Request $oRequest, Application $oApp, $sObjectClass)
+ {
+ // Checking security layers
+ if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_CREATE, $sObjectClass))
+ {
+ IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to create ' . $sObjectClass . ' object.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
+
+ $aData = array('sMode' => 'create');
+ $aData['form'] = $this->HandleForm($oRequest, $oApp, $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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
+ }
+ else
+ {
+ $oResponse = $oApp->json($aData);
+ }
+ }
+ else
+ {
+ // Adding brick if it was passed
+ $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', '');
+ if (!empty($sBrickId))
+ {
+ $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId);
+ if ($oBrick !== null)
+ {
+ $aData['oBrick'] = $oBrick;
+ }
+ }
+ $aData['sPageTitle'] = $aData['form']['title'];
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
+ }
+
+ 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 \Silex\Application $oApp
+ * @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 \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ */
+ public function CreateFromFactoryAction(Request $oRequest, Application $oApp, $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.');
+ $oApp->abort(500, '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 = array(
+ 'sObjectClass' => get_class($oTargetObject)
+ );
+ $sRedirectRoute = $oApp['url_generator']->generate('p_object_create', $aRouteParams);
+ // - Request
+ $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all());
+
+ return $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true);
+ }
+
+ /**
+ * Applies a stimulus $sStimulus on an cmdbAbstractObject
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @param string $sObjectClass
+ * @param string $sObjectId
+ * @param string $sStimulusCode
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ */
+ public function ApplyStimulusAction(Request $oRequest, Application $oApp, $sObjectClass, $sObjectId, $sStimulusCode)
+ {
+ // Checking parameters
+ if ($sObjectClass === '' || $sObjectId === '' || $sStimulusCode === '')
+ {
+ IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, sObjectId and $sStimulusCode expected, "' . $sObjectClass . '", "' . $sObjectId . '" and "' . $sStimulusCode . '" given.');
+ $oApp->abort(500, Dict::Format('UI:Error:3ParametersMissing', 'class', 'id', 'stimulus'));
+ }
+
+ // Checking security layers
+ if(!SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass))
+ {
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Retrieving object
+ $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* MustBeFound */, $oApp['scope_validator']->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 . '.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Retrieving request parameters
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
+
+ // Retrieving form properties
+ $aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($oApp, $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 = array(
+ 'id' => 'apply-stimulus',
+ 'type' => 'custom_list',
+ 'fields' => array(),
+ 'layout' => null
+ );
+ }
+
+ // Adding stimulus code to form
+ $aFormProperties['stimulus_code'] = $sStimulusCode;
+
+ // Adding target_state to current_values
+ $oRequest->request->set('apply_stimulus', array('code' => $sStimulusCode));
+
+ $aData = array('sMode' => 'apply_stimulus');
+ $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties);
+ $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Stimulus:Title');
+ $aData['form']['validation']['redirection'] = array(
+ 'url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId))
+ );
+
+ // 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', '');
+
+ $aData = array('sMode' => 'apply_stimulus');
+ $aData['form'] = $this->HandleForm($oSubRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties);
+ // Redefining the array to be as simple as possible :
+ $aData = array('redirection' =>
+ array('url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId)))
+ );
+ }
+ }
+
+ // 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 = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
+ }
+ elseif ($sOperation === 'redirect')
+ {
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/modal/mode_loader.html.twig', $aData);
+ }
+ else
+ {
+ $oResponse = $oApp->json($aData);
+ }
+ }
+ else
+ {
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
+ }
+
+ return $oResponse;
+ }
+
+ /**
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @param string $sMode
+ * @param string $sObjectClass
+ * @param string $sObjectId
+ * @param string $aFormProperties
+ *
+ * @return array
+ *
+ * @throws \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \OQLException
+ * @throws \Twig_Error_Loader
+ * @throws \Twig_Error_Runtime
+ * @throws \Twig_Error_Syntax
+ */
+ public static function HandleForm(Request $oRequest, Application $oApp, $sMode, $sObjectClass, $sObjectId = null, $aFormProperties = null)
+ {
+ $aFormData = array();
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', '');
+ $bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation));
+
+ // - Retrieve form properties
+ if ($aFormProperties === null)
+ {
+ $aFormProperties = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, $sMode);
+ }
+
+ // - Create and
+ if (empty($sOperation))
+ {
+ // 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 = $oApp['request_manipulator']->ReadParam('ar_token', '');
+ $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array();
+
+ // Preparing object
+ if ($sObjectId === null)
+ {
+ // Create new UserRequest
+ $oObject = MetaModel::NewObject($sObjectClass);
+
+ // Retrieve action rules information to auto-fill the form if available
+ // Preparing object
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oObject);
+ $aPrefillFormParam = array( 'user' => $_SESSION["auth_user"],
+ 'origin' => 'portal');
+ $oObject->PrefillForm('creation_from_0', $aPrefillFormParam);
+ }
+ else
+ {
+ $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, true, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass));
+ }
+
+ // Preparing buttons
+ $aFormData['buttons'] = array(
+ 'transitions' => array(),
+ 'actions' => array(),
+ 'links' => array(),
+ 'submit' => array(
+ 'label' => Dict::S('Portal:Button:Submit'),
+ ),
+ );
+ if ($sMode !== 'apply_stimulus')
+ {
+ // Add transition buttons
+ $oSetToCheckRights = DBObjectSet::FromObject($oObject);
+ $aStimuli = Metamodel::EnumStimuli($sObjectClass);
+ foreach ($oObject->EnumTransitions() as $sStimulusCode => $aTransitionDef)
+ {
+ if(SecurityHelper::IsStimulusAllowed($oApp, $sStimulusCode, $sObjectClass, $oSetToCheckRights))
+ {
+ $aFormData['buttons']['transitions'][$sStimulusCode] = $aStimuli[$sStimulusCode]->GetLabel();
+ }
+ }
+
+ // Add plugin buttons
+ foreach (MetaModel::EnumPlugins('iPopupMenuExtension') as $oExtensionInstance)
+ {
+ foreach($oExtensionInstance->EnumItems(iPopupMenuExtension::PORTAL_OBJDETAILS_ACTIONS, array('portal_id' => $oApp['combodo.portal.instance.id'], 'object' => $oObject)) as $oMenuItem)
+ {
+ if (is_object($oMenuItem))
+ {
+ if($oMenuItem instanceof JSButtonItem)
+ {
+ $aFormData['buttons']['actions'][] = $oMenuItem->GetMenuItem() + array('js_files' => $oMenuItem->GetLinkedScripts());
+ }
+ elseif($oMenuItem instanceof URLButtonItem)
+ {
+ $aFormData['buttons']['links'][] = $oMenuItem->GetMenuItem();
+ }
+ }
+ }
+ }
+
+ // Hiding submit button or changing its label if necessary
+ if(!empty($aFormData['buttons']['transitions']) && isset($aFormProperties['properties']) &&$aFormProperties['properties']['always_show_submit'] === false)
+ {
+ unset($aFormData['buttons']['submit']);
+ }
+ elseif($sMode === static::ENUM_MODE_EDIT)
+ {
+ $aFormData['buttons']['submit']['label'] = Dict::S('Portal:Button:Apply');
+ }
+ }
+ else
+ {
+ $aPrefillFormParam = array(
+ 'user' => $_SESSION["auth_user"],
+ 'origin' => 'portal',
+ 'stimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)['code'],
+ );
+ $oObject->PrefillForm('state_change', $aPrefillFormParam);
+ }
+
+ // Preparing callback urls
+ $aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal);
+ $aFormData['submit_callback'] = $aCallbackUrls['submit'];
+ $aFormData['cancel_callback'] = $aCallbackUrls['cancel'];
+
+ // Preparing renderer
+ // Note : We might need to distinguish form & renderer endpoints
+ if (in_array($sMode, array('create', 'edit', 'view')))
+ {
+ $sFormEndpoint = $oApp['url_generator']->generate('p_object_' . $sMode, array('sObjectClass' => $sObjectClass, 'sObjectId' => $sObjectId));
+ }
+ else
+ {
+ $sFormEndpoint = $_SERVER['REQUEST_URI'];
+ }
+ $oFormRenderer = new BsFormRenderer();
+ $oFormRenderer->SetEndpoint($sFormEndpoint);
+
+ $oFormManager = new ObjectFormManager();
+ $oFormManager->SetApplication($oApp)
+ ->SetObject($oObject)
+ ->SetMode($sMode)
+ ->SetActionRulesToken($sActionRulesToken)
+ ->SetRenderer($oFormRenderer)
+ ->SetFormProperties($aFormProperties);
+
+ $oFormManager->Build();
+
+ // Check the number of editable fields
+ $aFormData['editable_fields_count'] = $oFormManager->GetForm()->GetEditableFieldCount();
+ }
+ else
+ {
+ // Update / Submit / Cancel
+ $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
+ $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
+ if ( empty($sFormManagerClass) || empty($sFormManagerData) )
+ {
+ IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.');
+ $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.');
+ }
+
+ $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+ $oFormManager->SetApplication($oApp);
+
+ // Applying action rules if present
+ if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
+ {
+ $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
+ $oObj = $oFormManager->GetObject();
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
+ $oFormManager->SetObject($oObj);
+ }
+
+ switch ($sOperation)
+ {
+ case 'submit':
+ // Applying modification to object
+ $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'attachmentIds' => $oApp['request_manipulator']->ReadParam('attachment_ids', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties, 'applyStimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)));
+ if ($aFormData['validation']['valid'] === true)
+ {
+ // Note : We don't use $sObjectId there as it can be null if we are creating a new one. Instead we use the id from the created object once it has been seralized
+ // Check if stimulus has to be applied
+ $sStimulusCode = $oApp['request_manipulator']->ReadParam('stimulus_code', '');
+ if (!empty($sStimulusCode))
+ {
+ $aFormData['validation']['redirection'] = array(
+ 'url' => $oApp['url_generator']->generate('p_object_apply_stimulus', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey(), 'sStimulusCode' => $sStimulusCode)),
+ 'ajax' => true
+ );
+ }
+ // Otherwise, we show the object if there is no default
+// else
+// {
+// $aFormData['validation']['redirection'] = array(
+// 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey()))
+// );
+// }
+ }
+ break;
+
+ case 'update':
+ $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties));
+ break;
+
+ case 'cancel':
+ $oFormManager->OnCancel();
+ break;
+ }
+ }
+
+ // Preparing field_set data
+ $aFieldSetData = array(
+ //'fields_list' => $oFormManager->GetRenderer()->Render(), // GLA : This should be done just after in the if statement.
+ 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(),
+ 'form_path' => $oFormManager->GetForm()->GetId()
+ );
+
+ // Preparing fields list regarding the operation
+ if ($sOperation === 'update')
+ {
+ $aRequestedFields = $oApp['request_manipulator']->ReadParam('requested_fields', array(), FILTER_UNSAFE_RAW);
+ $sFormPath = $oApp['request_manipulator']->ReadParam('form_path', '');
+
+ // Checking if the update was on a subform, if so we need to make the rendering for that part only
+ if ( !empty($sFormPath) && $sFormPath !== $oFormManager->GetForm()->GetId() )
+ {
+ $oSubForm = $oFormManager->GetForm()->FindSubForm($sFormPath);
+ $oSubFormRenderer = new BsFormRenderer($oSubForm);
+ $oSubFormRenderer->SetEndpoint($oFormManager->GetRenderer()->GetEndpoint());
+ $aFormData['updated_fields'] = $oSubFormRenderer->Render($aRequestedFields);
+ }
+ else
+ {
+ $aFormData['updated_fields'] = $oFormManager->GetRenderer()->Render($aRequestedFields);
+ }
+ }
+ else
+ {
+ $aFieldSetData['fields_list'] = $oFormManager->GetRenderer()->Render();
+ }
+
+ // Preparing form data
+ $aFormData['id'] = $oFormManager->GetForm()->GetId();
+ $aFormData['transaction_id'] = $oFormManager->GetForm()->GetTransactionId();
+ $aFormData['formmanager_class'] = $oFormManager->GetClass();
+ $aFormData['formmanager_data'] = $oFormManager->ToJSON();
+ $aFormData['renderer'] = $oFormManager->GetRenderer();
+ $aFormData['object_name'] = $oFormManager->GetObject()->GetName();
+ $aFormData['object_state'] = $oFormManager->GetObject()->GetState();
+ $aFormData['fieldset'] = $aFieldSetData;
+ $aFormData['display_mode'] = (isset($aFormProperties['properties'])) ? $aFormProperties['properties']['display_mode'] : ApplicationHelper::FORM_DEFAULT_DISPLAY_MODE;
+
+ return $aFormData;
+ }
+
+ /**
+ * Handles the autocomplete search
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @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 \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \OQLException
+ */
+ public function SearchAutocompleteAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
+ {
+ $aData = array(
+ 'results' => array(
+ 'count' => 0,
+ 'items' => array()
+ )
+ );
+
+ // 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.');
+ $oApp->abort(500, Dict::Format('UI:Error:ParameterMissing', 'sQuery'));
+ }
+
+ // Retrieving parameters
+ $sQuery = $aRequestContent['sQuery'];
+ $sFieldId = $aRequestContent['sFieldId'];
+
+ // Checking security layers
+ if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId))
+ {
+ IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : Could not load object ' . $sHostObjectClass . '::' . $sHostObjectId . '.');
+ $oApp->abort(404, 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 = $oApp['request_manipulator']->ReadParam('ar_token', '');
+ $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array();
+ // Preparing object
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject);
+ }
+
+ // Updating host object with form data / values
+ $sFormManagerClass = $aRequestContent['formmanager_class'];
+ $sFormManagerData = $aRequestContent['formmanager_data'];
+ if (!empty($sFormManagerClass) && !empty($sFormManagerData))
+ {
+ $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+ $oFormManager->SetApplication($oApp);
+ $oFormManager->SetObject($oHostObject);
+
+ // Applying action rules if present
+ if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
+ {
+ $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
+ $oObj = $oFormManager->GetObject();
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
+ $oFormManager->SetObject($oObj);
+ }
+
+ // Updating host object
+ $oFormManager->OnUpdate(array('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);
+ $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 responsability of the template designer to write the right query so the user see only what he should.
+ if ($oTargetAttDef->GetEditClass() !== 'CustomFields')
+ {
+ $oScopeSearch = $oApp['scope_validator']->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, array(), array('this' => $oHostObject, 'ac_query' => '%' . $sQuery . '%'));
+ $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => array('friendlyname')));
+ // Note : This limit is also used in the field renderer by typeahead to determine how many suggestions to display
+ if ($oTargetAttDef->GetEditClass() === 'CustomFields')
+ {
+ $oSet->SetLimit(static::DEFAULT_LIST_LENGTH);
+ }
+ else
+ {
+ $oSet->SetLimit($oTargetAttDef->GetMaximumComboLength()); // TODO : Is this the right limit value ? We might want to use another parameter
+ }
+ // - Retrieving objects
+ while ($oItem = $oSet->Fetch())
+ {
+ $aData['results']['items'][] = array('id' => $oItem->GetKey(), 'name' => html_entity_decode($oItem->GetName(), ENT_QUOTES, 'UTF-8'));
+ $aData['results']['count'] ++;
+ }
+
+ // Preparing response
+ if ($oRequest->isXmlHttpRequest())
+ {
+ $oResponse = $oApp->json($aData);
+ }
+ else
+ {
+ $oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ return $oResponse;
+ }
+
+ /**
+ * Handles the regular (table) search from an attribute
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @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 \Exception
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \DictExceptionMissingString
+ * @throws \OQLException
+ */
+ public function SearchFromAttributeAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
+ {
+ $aData = array(
+ 'sMode' => 'search_regular',
+ 'sTargetAttCode' => $sTargetAttCode,
+ 'sHostObjectClass' => $sHostObjectClass,
+ 'sHostObjectId' => $sHostObjectId,
+ 'sActionRulesToken' => $oApp['request_manipulator']->ReadParam('ar_token', ''),
+ );
+
+ // Checking security layers
+ if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sHostObjectClass, $sHostObjectId))
+ {
+ IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sHostObjectClass . '::' . $sHostObjectId . ' object.');
+ $oApp->abort(404, 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']) : array();
+ // Preparing object
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject);
+ }
+
+ // Updating host object with form data / values
+ $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
+ $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
+ if ( !empty($sFormManagerClass) && !empty($sFormManagerData) )
+ {
+ $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+ $oFormManager->SetApplication($oApp);
+ $oFormManager->SetObject($oHostObject);
+
+ // Applying action rules if present
+ if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
+ {
+ $aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
+ $oObj = $oFormManager->GetObject();
+ $oApp['context_manipulator']->PrepareObject($aActionRules, $oObj);
+ $oFormManager->SetObject($oObj);
+ }
+
+ // Updating host object
+ $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW)));
+ $oHostObject = $oFormManager->GetObject();
+ }
+
+ // Retrieving request parameters
+ $iPageNumber = $oApp['request_manipulator']->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT);
+ $iListLength = $oApp['request_manipulator']->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT);
+ $bInitalPass = $oApp['request_manipulator']->HasParam('draw') ? false : true;
+ $sQuery = $oApp['request_manipulator']->ReadParam('sSearchValue', '');
+ $sFormPath = $oApp['request_manipulator']->ReadParam('sFormPath', '');
+ $sFieldId = $oApp['request_manipulator']->ReadParam('sFieldId', '');
+ $aObjectIdsToIgnore = $oApp['request_manipulator']->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW);
+
+ // Building search query
+ // - Retrieving target object class from attcode
+ $oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode);
+ if ($oTargetAttDef->IsExternalKey())
+ {
+ $sTargetObjectClass = $oTargetAttDef->GetTargetClass();
+ }
+ elseif ($oTargetAttDef->IsLinkSet())
+ {
+ if (!$oTargetAttDef->IsIndirect())
+ {
+ $sTargetObjectClass = $oTargetAttDef->GetLinkedClass();
+ }
+ else
+ {
+ $oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote());
+ $sTargetObjectClass = $oRemoteAttDef->GetTargetClass();
+ }
+ }
+ elseif ($oTargetAttDef->GetEditClass() === 'CustomFields')
+ {
+ $oRequestTemplate = $oHostObject->Get($sTargetAttCode);
+ $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($oApp, $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(array($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 responsability of the template designer to write the right query so the user see only what he should.
+ $oScopeSearch = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ);
+ $aInternalParams = array();
+ if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields'))
+ {
+ IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' has no scope query for ' . $sTargetObjectClass . ' class.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // - Base query from meta model
+ 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 = array();
+ 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;
+ 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 = array();
+ 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 attributs
+ 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 responsability 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, array(), $aInternalParams);
+ $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => $aAttCodes));
+ $oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
+ // - Retrieving columns properties
+ $aColumnProperties = array();
+ foreach ($aAttCodes as $sAttCode)
+ {
+ $oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode);
+ $aColumnProperties[$sAttCode] = array(
+ 'title' => $oAttDef->GetLabel()
+ );
+ }
+ // - Retrieving objects
+ $aItems = array();
+ while ($oItem = $oSet->Fetch())
+ {
+ $aItems[] = $this->PrepareObjectInformations($oApp, $oItem, $aAttCodes);
+ }
+
+ // Preparing response
+ if ($bInitalPass)
+ {
+ $aData = $aData + array(
+ 'form' => array(
+ 'id' => 'object_search_form_' . time(),
+ 'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel(), MetaModel::GetName($sTargetObjectClass))
+ ),
+ 'aColumnProperties' => json_encode($aColumnProperties),
+ 'aResults' => array(
+ 'aItems' => json_encode($aItems),
+ 'iCount' => count($aItems)
+ ),
+ 'bMultipleSelect' => $oTargetAttDef->IsLinkSet(),
+ 'aSource' => array(
+ 'sFormPath' => $sFormPath,
+ 'sFieldId' => $sFieldId,
+ 'aObjectIdsToIgnore' => $aObjectIdsToIgnore,
+ 'sFormManagerClass' => $sFormManagerClass,
+ 'sFormManagerData' => $sFormManagerData
+ )
+ );
+
+ if ($oRequest->isXmlHttpRequest())
+ {
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData);
+ }
+ else
+ {
+ //$oResponse = $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/layout.html.twig', $aData);
+ }
+ }
+ else
+ {
+ $aData = $aData + array(
+ 'levelsProperties' => $aColumnProperties,
+ 'data' => $aItems,
+ 'recordsTotal' => $oSet->Count(),
+ 'recordsFiltered' => $oSet->Count()
+ );
+
+ $oResponse = $oApp->json($aData);
+ }
+
+ return $oResponse;
+ }
+
+ /**
+ * Handles the hierarchical search from an attribute
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $oRequest
+ * @param \Silex\Application $oApp
+ * @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 void
+ *
+ */
+ public function SearchHierarchyAction(Request $oRequest, Application $oApp, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
+ {
+ // TODO
+ }
+
+ /**
+ * 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 \Silex\Application $oApp
+ * @param string $sOperation
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ */
+ public function DocumentAction(Request $oRequest, Application $oApp, $sOperation = null)
+ {
+ // Setting default operation
+ if($sOperation === null)
+ {
+ $sOperation = 'display';
+ }
+
+ // Retrieving ormDocument's host object
+ $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', '');
+ $sObjectId = $oApp['request_manipulator']->ReadParam('sObjectId', '');
+ $sObjectField = $oApp['request_manipulator']->ReadParam('sObjectField', '');
+
+ // 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, true, true);
+ $sHostClass = $oAttachment->Get('item_class');
+ $sHostId = $oAttachment->Get('item_id');
+ }
+ else
+ {
+ $sHostClass = $sObjectClass;
+ $sHostId = $sObjectId;
+ }
+
+ // Checking security layers
+ if (!SecurityHelper::IsActionAllowed($oApp, 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.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Retrieving object
+ $oObject = MetaModel::GetObject($sObjectClass, $sObjectId, false /* Must not be found */, $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sHostClass));
+ 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 . '.');
+ $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist'));
+ }
+
+ // Setting cache timeout
+ // Note: Attachment download should be handle through AttachmentAction()
+ if($sObjectClass === 'Attachment')
+ {
+ // One year ahead: an attachement cannot change
+ $iCacheSec = 31556926;
+ }
+ else
+ {
+ $iCacheSec = $oApp['request_manipulator']->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT);
+ }
+
+ $aHeaders = array();
+ 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';
+ // 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().'"';
+
+ 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 \Silex\Application $oApp
+ * @param string $sOperation
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Exception
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ */
+ public function AttachmentAction(Request $oRequest, Application $oApp, $sOperation = null)
+ {
+ $aData = array(
+ 'att_id' => 0,
+ 'preview' => false,
+ 'msg' => ''
+ );
+
+ // Retrieving sOperation from request only if it wasn't forced (determined by the route)
+ if ($sOperation === null)
+ {
+ $sOperation = $oApp['request_manipulator']->ReadParam('operation', null);
+ }
+ switch ($sOperation)
+ {
+ case 'add':
+ $sFieldName = $oApp['request_manipulator']->ReadParam('field_name', '');
+ $sObjectClass = $oApp['request_manipulator']->ReadParam('object_class', '');
+ $sTempId = $oApp['request_manipulator']->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);
+ $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'] = htmlentities($oDocument->GetFileName(), ENT_QUOTES, 'UTF-8');
+ // TODO : Change icon location when itop-attachment is refactored
+ //$aData['icon'] = utils::GetAbsoluteUrlAppRoot() . AttachmentPlugIn::GetFileIcon($oDoc->GetFileName());
+ $aData['icon'] = utils::GetAbsoluteUrlAppRoot() . 'env-' . utils::GetCurrentEnvironment() . '/itop-attachments/icons/image.png';
+ $aData['att_id'] = $iAttId;
+ $aData['preview'] = $oDocument->IsPreviewAvailable() ? 'true' : 'false';
+ }
+ 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 = $oApp->json($aData, 200, array('Content-Type' => 'text/plain'));
+ break;
+
+ case 'download':
+ // Preparing redirection
+ // - Route
+ $aRouteParams = array(
+ 'sObjectClass' => 'Attachment',
+ 'sObjectId' => $oApp['request_manipulator']->ReadParam('sAttachmentId', null),
+ 'sObjectField' => 'contents',
+ );
+ $sRedirectRoute = $oApp['url_generator']->generate('p_object_document_download', $aRouteParams);
+ // - Request
+ $oSubRequest = Request::create($sRedirectRoute, 'GET', $oRequest->query->all(), $oRequest->cookies->all(), array(), $oRequest->server->all());
+
+ $oResponse = $oApp->handle($oSubRequest, HttpKernelInterface::SUB_REQUEST, true);
+ break;
+
+ default:
+ $oApp->abort(403);
+ 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
+ * @param \Silex\Application $oApp
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \OQLException
+ * @throws \CoreException
+ */
+ public function GetInformationsAsJsonAction(Request $oRequest, Application $oApp)
+ {
+ $aData = array();
+
+ // Retrieving parameters
+ $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', '');
+ $aObjectIds = $oApp['request_manipulator']->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW);
+ $aObjectAttCodes = $oApp['request_manipulator']->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW);
+ if ( empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes) )
+ {
+ IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, aObjectIds and aObjectAttCodes expected, "' . $sObjectClass . '", "' . implode('/', $aObjectIds) . '" given.');
+ $oApp->abort(500, 'Invalid request data, some informations are missing');
+ }
+
+ // Checking that id is in the AttCodes
+ if (!in_array('id', $aObjectAttCodes))
+ {
+ $aObjectAttCodes = array_merge(array('id'), $aObjectAttCodes);
+ }
+
+ // Building the search
+ $bIgnoreSilos = $oApp['scope_validator']->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
+ $oSearch = DBObjectSearch::FromOQL("SELECT " . $sObjectClass . " WHERE id IN ('" . implode("','", $aObjectIds) . "')");
+ if ($bIgnoreSilos === true)
+ {
+ $oSearch->AllowAllData();
+ }
+ $oSet = new DBObjectSet($oSearch);
+ $oSet->OptimizeColumnLoad($aObjectAttCodes);
+
+ // Retrieving objects
+ while ($oObject = $oSet->Fetch())
+ {
+ $aData['items'][] = $this->PrepareObjectInformations($oApp, $oObject, $aObjectAttCodes);
+ }
+
+ return $oApp->json($aData);
+ }
+
+ /**
+ * Prepare a DBObject informations as an array for a client side usage (typically, add a row in a table)
+ *
+ * @param \Silex\Application $oApp
+ * @param \DBObject $oObject
+ * @param array $aAttCodes
+ *
+ * @return array
+ *
+ * @throws \Exception
+ * @throws \CoreException
+ */
+ protected function PrepareObjectInformations(Application $oApp, DBObject $oObject, $aAttCodes = array())
+ {
+ $sObjectClass = get_class($oObject);
+ $aObjectData = array(
+ 'id' => $oObject->GetKey(),
+ 'name' => $oObject->GetName(),
+ 'attributes' => array(),
+ );
+
+ // Retrieving attributes definitions
+ $aAttDefs = array();
+ foreach ($aAttCodes as $sAttCode)
+ {
+ if ($sAttCode === 'id')
+ continue;
+
+ $aAttDefs[$sAttCode] = MetaModel::GetAttributeDef($sObjectClass, $sAttCode);
+ }
+
+ // Preparing attribute data
+ foreach ($aAttDefs as $oAttDef)
+ {
+ $aAttData = array(
+ 'att_code' => $oAttDef->GetCode()
+ );
+
+ if ($oAttDef->IsExternalKey())
+ {
+ $aAttData['value'] = $oObject->GetAsHTML($oAttDef->GetCode() . '_friendlyname');
+
+ // Checking if user can access object's external key
+ if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass()))
+ {
+ $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $oAttDef->GetTargetClass(), 'sObjectId' => $oObject->Get($oAttDef->GetCode())));
+ }
+ }
+ elseif ($oAttDef->IsLinkSet())
+ {
+ // We skip it
+ continue;
+ }
+ elseif ($oAttDef instanceof AttributeImage)
+ {
+ $oOrmDoc = $oObject->Get($oAttDef->GetCode());
+ if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty())
+ {
+ $sUrl = $oApp['url_generator']->generate('p_object_document_display', array('sObjectClass' => get_class($oObject), 'sObjectId' => $oObject->GetKey(), 'sObjectField' => $oAttDef->GetCode(), 'cache' => 86400));
+ }
+ else
+ {
+ $sUrl = $oAttDef->Get('default_image');
+ }
+ $aAttData['value'] = ' ';
+ }
+ else
+ {
+ $aAttData['value'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode()));
+
+ if ($oAttDef instanceof AttributeFriendlyName)
+ {
+ // Checking if user can access object
+ if(SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $sObjectClass))
+ {
+ $aAttData['url'] = $oApp['url_generator']->generate('p_object_view', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oObject->GetKey()));
+ }
+ }
+ }
+
+ $aObjectData['attributes'][$oAttDef->GetCode()] = $aAttData;
+ }
+
+ return $aObjectData;
+ }
+
+}
diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php
index 57a0ba9d8..1d5189195 100644
--- a/dictionaries/da.dictionary.itop.ui.php
+++ b/dictionaries/da.dictionary.itop.ui.php
@@ -1010,7 +1010,7 @@ Ved tilknytningen til en trigger, bliver hver handling tildelt et "rækkefølge"
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php
index 6ce532ff0..21f13aef2 100644
--- a/dictionaries/de.dictionary.itop.ui.php
+++ b/dictionaries/de.dictionary.itop.ui.php
@@ -1177,6 +1177,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm
// Search form
'UI:Search:Toggle' => 'Ein-/Ausklappen',
'UI:Search:AutoSubmit:DisabledHint' => 'Automatische Eingabe für diese Klasse deaktiviert',
+ 'UI:Search:NoAutoSubmit:ExplainText' => 'Add some criterion on the search box or click the search button to view the objects.~~',
'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Kriterium hinzufügen',
// - Add new criteria button
'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Kürzlich verwendet',
diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php
index b5130f606..8e13d427c 100644
--- a/dictionaries/en.dictionary.itop.ui.php
+++ b/dictionaries/en.dictionary.itop.ui.php
@@ -1414,6 +1414,7 @@ When associated with a trigger, each action is given an "order" number, specifyi
// Search form
'UI:Search:Toggle' => 'Minimize / Expand',
'UI:Search:AutoSubmit:DisabledHint' => 'Auto submit has been disabled for this class',
+ 'UI:Search:NoAutoSubmit:ExplainText' => 'Add some criterion on the search box or click the search button to view the objects.',
'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Add new criteria',
// - Add new criteria button
'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Recently used',
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index d9cc4d077..8b1a9e062 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -1242,6 +1242,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé
// Search form
'UI:Search:Toggle' => 'Réduire / Ouvrir',
'UI:Search:AutoSubmit:DisabledHint' => 'La soumission automatique a été desactivée pour cette classe',
+ 'UI:Search:NoAutoSubmit:ExplainText' => 'Ajoutez des critères dans le formulaire de recherche ou cliquez sur le bouton rechercher pour voir les objets.',
'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Ajouter un critère',
// - Add new criteria button
'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Récents',
diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php
index a0fc99d6d..d799f8170 100755
--- a/dictionaries/hu.dictionary.itop.ui.php
+++ b/dictionaries/hu.dictionary.itop.ui.php
@@ -822,7 +822,7 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php
index 4c1654fe2..ddbd0749e 100644
--- a/dictionaries/it.dictionary.itop.ui.php
+++ b/dictionaries/it.dictionary.itop.ui.php
@@ -946,7 +946,7 @@ Quando è associata a un trigger, ad ogni azione è assegnato un numero "ordine"
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php
index 68cc9586d..8a1deb723 100644
--- a/dictionaries/ja.dictionary.itop.ui.php
+++ b/dictionaries/ja.dictionary.itop.ui.php
@@ -1007,7 +1007,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php
index a5f8b0de9..c389baf87 100644
--- a/dictionaries/tr.dictionary.itop.ui.php
+++ b/dictionaries/tr.dictionary.itop.ui.php
@@ -930,7 +930,7 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/dictionaries/zh.dictionary.itop.ui.php b/dictionaries/zh.dictionary.itop.ui.php
index 4c83e8f8c..b9aa25263 100644
--- a/dictionaries/zh.dictionary.itop.ui.php
+++ b/dictionaries/zh.dictionary.itop.ui.php
@@ -928,7 +928,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~',
'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~',
'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~',
- 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
+ 'UI:ResetPwd-EmailBody' => 'You have requested to reset your iTop password.
Please follow this link (single usage) to enter a new password
.~~',
'UI:ResetPwd-Title' => 'Reset password~~',
'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~',
'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~',
diff --git a/js/search/search_form_handler.js b/js/search/search_form_handler.js
index f297c352a..e64f5056f 100644
--- a/js/search/search_form_handler.js
+++ b/js/search/search_form_handler.js
@@ -745,9 +745,7 @@ $(function()
// Make placeholder if nothing yet
if(oResultAreaElem.html() === '')
{
- // TODO: Make a good UI for this POC.
- // TODO: Translate sentence.
- oResultAreaElem.html('Add some criterion on the search box or click the search button to view the objects.
Search
');
+ oResultAreaElem.html('' + Dict.S('UI:Search:NoAutoSubmit:ExplainText') + '
' + Dict.S('UI:Button:Search') + '
');
oResultAreaElem.find('button').on('click', function(){
// TODO: Bug: Open "Search for CI", change child classe in the dropdown, click the search button. It submit the search for the original child classe, not the current one; whereas a click on the upper right pictogram does. This might be due to the form reloading.
me._onSubmitClick();
diff --git a/js/utils.js b/js/utils.js
index 44e2a959c..346ebf71a 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -1,714 +1,718 @@
-// Some general purpose JS functions for the iTop application
-
-//IE 8 compatibility, copied from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf
-if (!Array.prototype.indexOf) {
-
- if (false) // deactivated since it causes troubles: for(k in aData) => returns the indexOf function as first element on empty arrays !
- {
- Array.prototype.indexOf = function (searchElement /*, fromIndex */) {
- "use strict";
- if (this == null) {
- throw new TypeError();
- }
- var t = Object(this);
- var len = t.length >>> 0;
- if (len === 0) {
- return -1;
- }
- var n = 0;
- if (arguments.length > 1) {
- n = Number(arguments[1]);
- if (n != n) { // shortcut for verifying if it's NaN
- n = 0;
- } else if (n != 0 && n != Infinity && n != -Infinity) {
- n = (n > 0 || -1) * Math.floor(Math.abs(n));
- }
- }
- if (n >= len) {
- return -1;
- }
- var k = n >= 0 ? n : Math.max(len-Math.abs(n), 0);
- for (; k < len; k++) {
- if (k in t && t[k] === searchElement) {
- return k;
- }
- }
- return -1;
- }
- }
-}
-// Polyfill for Array.from for IE
-// Copied from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
-if (!Array.from) {
- Array.from = (function () {
- var toStr = Object.prototype.toString;
- var isCallable = function (fn) {
- return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
- };
- var toInteger = function (value) {
- var number = Number(value);
- if (isNaN(number)) { return 0; }
- if (number === 0 || !isFinite(number)) { return number; }
- return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
- };
- var maxSafeInteger = Math.pow(2, 53) - 1;
- var toLength = function (value) {
- var len = toInteger(value);
- return Math.min(Math.max(len, 0), maxSafeInteger);
- };
-
- // The length property of the from method is 1.
- return function from(arrayLike/*, mapFn, thisArg */) {
- // 1. Let C be the this value.
- var C = this;
-
- // 2. Let items be ToObject(arrayLike).
- var items = Object(arrayLike);
-
- // 3. ReturnIfAbrupt(items).
- if (arrayLike == null) {
- throw new TypeError('Array.from requires an array-like object - not null or undefined');
- }
-
- // 4. If mapfn is undefined, then let mapping be false.
- var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
- var T;
- if (typeof mapFn !== 'undefined') {
- // 5. else
- // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
- if (!isCallable(mapFn)) {
- throw new TypeError('Array.from: when provided, the second argument must be a function');
- }
-
- // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
- if (arguments.length > 2) {
- T = arguments[2];
- }
- }
-
- // 10. Let lenValue be Get(items, "length").
- // 11. Let len be ToLength(lenValue).
- var len = toLength(items.length);
-
- // 13. If IsConstructor(C) is true, then
- // 13. a. Let A be the result of calling the [[Construct]] internal method
- // of C with an argument list containing the single item len.
- // 14. a. Else, Let A be ArrayCreate(len).
- var A = isCallable(C) ? Object(new C(len)) : new Array(len);
-
- // 16. Let k be 0.
- var k = 0;
- // 17. Repeat, while k < len… (also steps a - h)
- var kValue;
- while (k < len) {
- kValue = items[k];
- if (mapFn) {
- A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
- } else {
- A[k] = kValue;
- }
- k += 1;
- }
- // 18. Let putStatus be Put(A, "length", len, true).
- A.length = len;
- // 20. Return A.
- return A;
- };
- }());
-}
-
-/**
- * Reload a truncated list
- */
-aTruncatedLists = {}; // To keep track of the list being loaded, each member is an ajaxRequest object
-
-function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams) {
- $('#'+divId).block();
- //$('#'+divId).blockUI();
- if (aTruncatedLists[divId] != undefined) {
- try {
- aAjaxRequest = aTruncatedLists[divId];
- aAjaxRequest.abort();
- }
- catch (e) {
- // Do nothing special, just continue
- console.log('Uh,uh, exception !');
- }
- }
- aTruncatedLists[divId] = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style=list',
- {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams},
- function (data) {
- aTruncatedLists[divId] = undefined;
- if (data.length > 0) {
- $('#'+divId).html(data);
- $('#'+divId+' .listResults').tableHover(); // hover tables
- $('#'+divId+' .listResults').each(function () {
- var table = $(this);
- var id = $(this).parent();
- aTruncatedLists[divId] = undefined;
- var checkbox = (table.find('th:first :checkbox').length > 0);
- if (checkbox) {
- // There is a checkbox in the first column, don't make it sortable
- table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager")}); // sortable and zebra tables
- }
- else {
- // There is NO checkbox in the first column, all columns are considered sortable
- table.tablesorter({widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager"), totalRows: 97, filter: sSerializedFilter, extra_params: sExtraParams}); // sortable and zebra tables
- }
- });
- $('#'+divId).unblock();
- }
- }
- );
-}
-
-/**
- * Truncate a previously expanded list !
- */
-function TruncateList(divId, iLimit, sNewLabel, sLinkLabel) {
- $('#'+divId).block();
- var iCount = 0;
- $('#'+divId+' table.listResults tr:gt('+iLimit+')').each(function () {
- $(this).remove();
- });
- $('#lbl_'+divId).html(sNewLabel);
- $('#'+divId+' table.listResults tr:last td').addClass('truncated');
- $('#'+divId+' table.listResults').addClass('truncated');
- $('#trc_'+divId).html(sLinkLabel);
- $('#'+divId+' .listResults').trigger("update"); // Reset the cache
- $('#'+divId).unblock();
-}
-
-/**
- * Reload any block -- used for periodic auto-reload
- */
-function ReloadBlock(divId, sStyle, sSerializedFilter, sExtraParams) {
- // Check if the user is not editing the list properties right now
- var bDialogOpen = false;
- var oDataTable = $('#'+divId+' :itop-datatable');
- var bIsDataTable = false;
- if (oDataTable.length > 0) {
- bDialogOpen = oDataTable.datatable('IsDialogOpen');
- bIsDataTable = true;
- }
- if (!bDialogOpen) {
- if (bIsDataTable) {
- oDataTable.datatable('DoRefresh');
- }
- else {
- $('#'+divId).block();
-
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style='+sStyle,
- {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams},
- function (data) {
- $('#'+divId).empty();
- $('#'+divId).append(data);
- $('#'+divId).removeClass('loading');
- }
- );
- }
- }
-}
-
-function SaveGroupBySortOrder(sTableId, aValues) {
- var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id');
- var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId;
- if (aValues.length != 0) {
- $sValue = JSON.stringify(aValues);
- if (GetUserPreference(sPrefKey, null) != $sValue) {
- SetUserPreference(sPrefKey, $sValue, true);
- }
- }
-}
-
-function LoadGroupBySortOrder(sTableId) {
- var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id');
- var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId;
- var sValues = GetUserPreference(sPrefKey, null);
- if (sValues != null) {
- aValues = JSON.parse(sValues);
- window.setTimeout(function () {
- $('#'+sTableId+' table.listResults').trigger('sorton', [aValues]);
- }, 50);
- }
-
-}
-
-/**
- * Update the display and value of a file input widget when the user picks a new file
- */
-function UpdateFileName(id, sNewFileName) {
- var aPath = sNewFileName.split('\\');
- var sNewFileName = aPath[aPath.length-1];
-
- $('#'+id).val(sNewFileName);
- $('#'+id).trigger('validate');
- $('#name_'+id).text(sNewFileName);
- return true;
-}
-
-/**
- * Reload a search form for the specified class
- */
-function ReloadSearchForm(divId, sClassName, sBaseClass, sContext, sTableId, sExtraParams) {
- var oDiv = $('#ds_'+divId);
- oDiv.block();
- // deprecated in jQuery 1.8
- //var oFormEvents = $('#ds_'+divId+' form').data('events');
- var oForm = $('#ds_'+divId+' form');
- var oFormEvents = $._data(oForm[0], "events");
-
- // Save the submit handlers
- aSubmit = new Array();
- if ((oFormEvents != null) && (oFormEvents.submit != undefined)) {
- for (var index = 0; index < oFormEvents.submit.length; index++) {
- aSubmit [index] = {data: oFormEvents.submit[index].data, namespace: oFormEvents.submit[index].namespace, handler: oFormEvents.submit[index].handler};
- }
- }
- sAction = $('#ds_'+divId+' form').attr('action');
-
- // Save the current values in the form
- var oMap = {};
- $('#ds_'+divId+" form :input[name!='']").each(function () {
- oMap[this.name] = this.value;
- });
- oMap.operation = 'search_form';
- oMap.className = sClassName;
- oMap.baseClass = sBaseClass;
- oMap.currentId = divId;
- oMap._table_id_ = sTableId;
- oMap.action = sAction;
- if(sExtraParams['selection_mode'])
- {
- oMap.selection_mode = sExtraParams['selection_mode'];
- }
- if(sExtraParams['result_list_outer_selector'])
- {
- oMap.result_list_outer_selector = sExtraParams['result_list_outer_selector'];
- }
- if(sExtraParams['cssCount'])
- {
- oMap.css_count = sExtraParams['cssCount'];
- $(sExtraParams['cssCount']).val(0).trigger('change');
- }
- if(sExtraParams['table_inner_id'])
- {
- oMap.table_inner_id = sExtraParams['table_inner_id'];
- }
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, oMap,
- function (data) {
- oDiv.empty();
- oDiv.append(data);
- oDiv.unblock();
- oDiv.parent().resize(); // Inform the parent that the form has just been (potentially) resized
- oDiv.find('form').triggerHandler('itop.search.form.reloaded');
- }
- );
-}
-
-/**
- * Stores - in a persistent way - user specific preferences
- * depends on a global variable oUserPreferences created/filled by the iTopWebPage
- * that acts as a local -write through- cache
- */
-function SetUserPreference(sPreferenceCode, sPrefValue, bPersistent) {
- sPreviousValue = undefined;
- try {
- sPreviousValue = oUserPreferences[sPreferenceCode];
- }
- catch (err) {
- sPreviousValue = undefined;
- }
- oUserPreferences[sPreferenceCode] = sPrefValue;
- if (bPersistent && (sPrefValue != sPreviousValue)) {
- ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
- {operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}); // Make it persistent
- }
-}
-
-/**
- * Get user specific preferences
- * depends on a global variable oUserPreferences created/filled by the iTopWebPage
- * that acts as a local -write through- cache
- */
-function GetUserPreference(sPreferenceCode, sDefaultValue) {
- var value = sDefaultValue;
- if (oUserPreferences[sPreferenceCode] != undefined) {
- value = oUserPreferences[sPreferenceCode];
- }
- return value;
-}
-
-/**
- * Check/uncheck a whole list of checkboxes
- */
-function CheckAll(sSelector, bValue) {
- var value = bValue;
- $(sSelector).each(function () {
- if (this.checked != value) {
- this.checked = value;
- $(this).trigger('change');
- }
- });
-}
-
-
-/**
- * Toggle (enabled/disabled) the specified field of a form
- */
-function ToggleField(value, field_id) {
- if (value) {
- $('#'+field_id).prop('disabled', false);
- // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
- $('#'+field_id+' :input').prop('disabled', false);
- }
- else {
- $('#'+field_id).prop('disabled', true);
- // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
- $('#'+field_id+' :input').prop('disabled', true);
- }
- $('#'+field_id).trigger('update');
- $('#'+field_id).trigger('validate');
-}
-
-/**
- * For the fields that cannot be visually disabled, they can be blocked
- * @return
- */
-function BlockField(field_id, bBlocked) {
- if (bBlocked) {
- $('#'+field_id).block({message: ' ** disabled ** '});
- }
- else {
- $('#'+field_id).unblock();
- }
-}
-
-/**
- * Updates (enables/disables) a "duration" field
- */
-function ToggleDurationField(field_id) {
- // Toggle all the subfields that compose the "duration" input
- aSubFields = new Array('d', 'h', 'm', 's');
-
- if ($('#'+field_id).prop('disabled')) {
- for (var i = 0; i < aSubFields.length; i++) {
- $('#'+field_id+'_'+aSubFields[i]).prop('disabled', true);
- }
- }
- else {
- for (var i = 0; i < aSubFields.length; i++) {
- $('#'+field_id+'_'+aSubFields[i]).prop('disabled', false);
- }
- }
-}
-
-/**
- * PropagateCheckBox
- */
-function PropagateCheckBox(bCurrValue, aFieldsList, bCheck) {
- if (bCurrValue == bCheck) {
- for (var i = 0; i < aFieldsList.length; i++) {
- $('#enable_'+aFieldsList[i]).prop('checked', bCheck);
- ToggleField(bCheck, aFieldsList[i]);
- }
- }
-}
-
-function FixTableSorter(table) {
- if (table[0].config == undefined) {
- // Table is not sort-able, let's fix it
- var checkbox = (table.find('th:first :checkbox').length > 0);
- if (checkbox) {
- // There is a checkbox in the first column, don't make it sort-able
- table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables
- }
- else {
- // There is NO checkbox in the first column, all columns are considered sort-able
- table.tablesorter({widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables
- }
- }
-}
-
-function DashletCreationDlg(sOQL, sContext) {
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'dashlet_creation_dlg', oql: sOQL}, function (data) {
- $('body').append(data);
- });
- return false;
-}
-
-function ShortcutListDlg(sOQL, sDataTableId, sContext) {
- var sDataTableName = 'datatable_'+sDataTableId;
- var oTableSettings = {
- oColumns: $('#'+sDataTableName).datatable('option', 'oColumns'),
- iPageSize: $('#'+sDataTableName).datatable('option', 'iPageSize')
- };
- var sTableSettings = JSON.stringify(oTableSettings);
-
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'shortcut_list_dlg', oql: sOQL, table_settings: sTableSettings}, function (data) {
- $('body').append(data);
- });
- return false;
-}
-
-function ExportListDlg(sOQL, sDataTableId, sFormat, sDlgTitle) {
- var aFields = [];
- if (sDataTableId != '') {
- var sDataTableName = 'datatable_'+sDataTableId;
- var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns');
- for (var j in oColumns) {
- for (var k in oColumns[j]) {
- if (oColumns[j][k].checked) {
- var sCode = oColumns[j][k].code;
- if (sCode == '_key_') {
- sCode = 'id';
- }
- aFields.push(j+'.'+sCode);
- }
- }
- }
- }
-
- var oParams = {
- interactive: 1,
- mode: 'dialog',
- expression: sOQL,
- suggested_fields: aFields.join(','),
- dialog_title: sDlgTitle
- };
-
- if (sFormat !== null) {
- oParams.format = sFormat;
- }
-
- $.post(GetAbsoluteUrlAppRoot()+'webservices/export-v2.php', oParams, function (data) {
- $('body').append(data);
- });
- return false;
-}
-
-function ExportToggleFormat(sFormat) {
- $('.form_part').hide();
- for (k in window.aFormParts[sFormat]) {
- $('#form_part_'+window.aFormParts[sFormat][k]).show().trigger('form-part-activate');
- }
-}
-
-function ExportStartExport() {
- var oParams = {};
- $('.form_part:visible :input').each(function () {
- if (this.name != '') {
- if ((this.type == 'radio') || (this.type == 'checkbox')) {
- if (this.checked) {
- oParams[this.name] = $(this).val();
- }
- }
- else {
- oParams[this.name] = $(this).val();
- }
- }
- });
- $(':itop-tabularfieldsselector:visible').tabularfieldsselector('close_all_tooltips');
- $('#export-form').hide();
- $('#export-feedback').show();
- oParams.operation = 'export_build';
- oParams.format = $('#export-form :input[name=format]').val();
- var sQueryMode = $(':input[name=query_mode]:checked').val();
- if ($(':input[name=query_mode]:checked').length > 0) {
- if (sQueryMode == 'oql') {
- oParams.expression = $('#export-form :input[name=expression]').val();
- }
- else {
- oParams.query = $('#export-form :input[name=query]').val();
- }
- }
- else {
- oParams.expression = $('#export-form :input[name=expression]').val();
- oParams.query = $('#export-form :input[name=query]').val();
- }
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
- if (data == null) {
- ExportError('Export failed (no data provided), please contact your administrator');
- }
- else {
- ExportRun(data);
- }
- }, 'json')
- .fail(function () {
- ExportError('Export failed, please contact your administrator');
- });
-}
-
-function ExportError(sMessage) {
- $('.export-message').html(sMessage);
- $('.export-progress-bar').hide();
- $('#export-btn').hide();
-}
-
-function ExportRun(data) {
- switch (data.code) {
- case 'run':
- // Continue
- $('.export-progress-bar').progressbar({value: data.percentage});
- $('.export-message').html(data.message);
- oParams = {};
- oParams.token = data.token;
- var sDataState = $('#export-form').attr('data-state');
- if (sDataState == 'cancelled') {
- oParams.operation = 'export_cancel';
- }
- else {
- oParams.operation = 'export_build';
- }
-
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
- ExportRun(data);
- },
- 'json');
- break;
-
- case 'done':
- $('#export-btn').hide();
- sMessage = ''+data.message+' ';
- $('.export-message').html(sMessage);
- $('.export-progress-bar').hide();
- $('#export-btn').hide();
- $('#export-form').attr('data-state', 'done');
- if (data.text_result != undefined) {
- if (data.mime_type == 'text/html') {
- $('#export_content').parent().html(data.text_result);
- $('#export_text_result').show();
- $('#export_text_result .listResults').tableHover();
- $('#export_text_result .listResults').tablesorter({widgets: ['myZebra']});
- }
- else {
- if ($('#export_text_result').closest('ui-dialog').length == 0) {
- // not inside a dialog box, adjust the height... approximately
- var jPane = $('#export_text_result').closest('.ui-layout-content');
- var iTotalHeight = jPane.height();
- jPane.children(':visible').each(function () {
- if ($(this).attr('id') != '') {
- iTotalHeight -= $(this).height();
- }
- });
- $('#export_content').height(iTotalHeight-80);
- }
- $('#export_content').val(data.text_result);
- $('#export_text_result').show();
- }
- }
- $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
- break;
-
- case 'error':
- $('#export-form').attr('data-state', 'error');
- $('.export-progress-bar').progressbar({value: data.percentage});
- $('.export-message').html(data.message);
- $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
- $('#export-btn').hide();
- default:
- }
-}
-
-function ExportInitButton(sSelector) {
- $(sSelector).on('click', function () {
- var sDataState = $('#export-form').attr('data-state');
- switch (sDataState) {
- case 'not-yet-started':
- $('.form_part:visible').each(function () {
- $('#export-form').data('validation_messages', []);
- var ret = $(this).trigger('validate');
- });
- var aMessages = $('#export-form').data('validation_messages');
-
- if (aMessages.length > 0) {
- alert(aMessages.join(''));
- return;
- }
- if ($(this).hasClass('ui-button')) {
- $(this).button('option', 'label', Dict.S('UI:Button:Cancel'));
- }
- else {
- $(this).html(Dict.S('UI:Button:Cancel'));
- }
- $('#export-form').attr('data-state', 'running');
- ExportStartExport();
- break;
-
- case 'running':
- if ($(this).hasClass('ui-button')) {
- $(this).button('disable');
- }
- else {
- $(this).prop('disabled', true);
- }
- $('#export-form').attr('data-state', 'cancelled');
- break;
-
- case 'done':
- case 'error':
- $('#interactive_export_dlg').dialog('close');
- break;
-
- default:
- // Do nothing
- }
- });
-}
-
-function DisplayHistory(sSelector, sFilter, iCount, iStart) {
- $(sSelector).block();
- var oParams = {operation: 'history_from_filter', filter: sFilter, start: iStart, count: iCount};
- $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
- $(sSelector).html(data).unblock();
- }
- );
-}
-
-// Very simple equivalent to format: placeholders are %1$s %2$d ...
-function Format() {
- var args = [];
- var str = '';
- if (arguments[0] instanceof Array) {
- str = arguments[0][0].toString();
- args = arguments[0];
- }
- else {
- str = arguments[0].toString();
- if (arguments.length > 1) {
- var t = typeof arguments[1];
- args = ("string" === t || "number" === t) ? Array.prototype.slice.call(arguments) : arguments[1];
- }
- }
- var key;
- for (key in args) {
- str = str.replace(new RegExp("\\%"+key+"\\$.", "gi"), args[key]);
- }
-
- return str;
-}
-
-/**
- * Enable to access translation keys client side.
- * The called keys needs to be exported using \WebPage::add_dict_entry
- */
-var Dict = {};
-if (typeof aDictEntries == 'undefined') {
- Dict._entries = {}; // Entries have not been loaded (we are in the setup ?)
-}
-else {
- Dict._entries = aDictEntries; // Entries were loaded asynchronously via their own js files
-}
-Dict.S = function (sEntry) {
- if (sEntry in Dict._entries) {
- return Dict._entries[sEntry];
- }
- else {
- return sEntry;
- }
-};
-Dict.Format = function () {
- var args = Array.from(arguments);
- args[0] = Dict.S(arguments[0]);
- return Format(args);
+// Some general purpose JS functions for the iTop application
+
+//IE 8 compatibility, copied from: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf
+if (!Array.prototype.indexOf) {
+
+ if (false) // deactivated since it causes troubles: for(k in aData) => returns the indexOf function as first element on empty arrays !
+ {
+ Array.prototype.indexOf = function (searchElement /*, fromIndex */) {
+ "use strict";
+ if (this == null) {
+ throw new TypeError();
+ }
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (len === 0) {
+ return -1;
+ }
+ var n = 0;
+ if (arguments.length > 1) {
+ n = Number(arguments[1]);
+ if (n != n) { // shortcut for verifying if it's NaN
+ n = 0;
+ } else if (n != 0 && n != Infinity && n != -Infinity) {
+ n = (n > 0 || -1) * Math.floor(Math.abs(n));
+ }
+ }
+ if (n >= len) {
+ return -1;
+ }
+ var k = n >= 0 ? n : Math.max(len-Math.abs(n), 0);
+ for (; k < len; k++) {
+ if (k in t && t[k] === searchElement) {
+ return k;
+ }
+ }
+ return -1;
+ }
+ }
+}
+// Polyfill for Array.from for IE
+// Copied from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
+if (!Array.from) {
+ Array.from = (function () {
+ var toStr = Object.prototype.toString;
+ var isCallable = function (fn) {
+ return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
+ };
+ var toInteger = function (value) {
+ var number = Number(value);
+ if (isNaN(number)) { return 0; }
+ if (number === 0 || !isFinite(number)) { return number; }
+ return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
+ };
+ var maxSafeInteger = Math.pow(2, 53) - 1;
+ var toLength = function (value) {
+ var len = toInteger(value);
+ return Math.min(Math.max(len, 0), maxSafeInteger);
+ };
+
+ // The length property of the from method is 1.
+ return function from(arrayLike/*, mapFn, thisArg */) {
+ // 1. Let C be the this value.
+ var C = this;
+
+ // 2. Let items be ToObject(arrayLike).
+ var items = Object(arrayLike);
+
+ // 3. ReturnIfAbrupt(items).
+ if (arrayLike == null) {
+ throw new TypeError('Array.from requires an array-like object - not null or undefined');
+ }
+
+ // 4. If mapfn is undefined, then let mapping be false.
+ var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
+ var T;
+ if (typeof mapFn !== 'undefined') {
+ // 5. else
+ // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
+ if (!isCallable(mapFn)) {
+ throw new TypeError('Array.from: when provided, the second argument must be a function');
+ }
+
+ // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (arguments.length > 2) {
+ T = arguments[2];
+ }
+ }
+
+ // 10. Let lenValue be Get(items, "length").
+ // 11. Let len be ToLength(lenValue).
+ var len = toLength(items.length);
+
+ // 13. If IsConstructor(C) is true, then
+ // 13. a. Let A be the result of calling the [[Construct]] internal method
+ // of C with an argument list containing the single item len.
+ // 14. a. Else, Let A be ArrayCreate(len).
+ var A = isCallable(C) ? Object(new C(len)) : new Array(len);
+
+ // 16. Let k be 0.
+ var k = 0;
+ // 17. Repeat, while k < len… (also steps a - h)
+ var kValue;
+ while (k < len) {
+ kValue = items[k];
+ if (mapFn) {
+ A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
+ } else {
+ A[k] = kValue;
+ }
+ k += 1;
+ }
+ // 18. Let putStatus be Put(A, "length", len, true).
+ A.length = len;
+ // 20. Return A.
+ return A;
+ };
+ }());
+}
+
+/**
+ * Reload a truncated list
+ */
+aTruncatedLists = {}; // To keep track of the list being loaded, each member is an ajaxRequest object
+
+function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams) {
+ $('#'+divId).block();
+ //$('#'+divId).blockUI();
+ if (aTruncatedLists[divId] != undefined) {
+ try {
+ aAjaxRequest = aTruncatedLists[divId];
+ aAjaxRequest.abort();
+ }
+ catch (e) {
+ // Do nothing special, just continue
+ console.log('Uh,uh, exception !');
+ }
+ }
+ aTruncatedLists[divId] = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style=list',
+ {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams},
+ function (data) {
+ aTruncatedLists[divId] = undefined;
+ if (data.length > 0) {
+ $('#'+divId).html(data);
+ $('#'+divId+' .listResults').tableHover(); // hover tables
+ $('#'+divId+' .listResults').each(function () {
+ var table = $(this);
+ var id = $(this).parent();
+ aTruncatedLists[divId] = undefined;
+ var checkbox = (table.find('th:first :checkbox').length > 0);
+ if (checkbox) {
+ // There is a checkbox in the first column, don't make it sortable
+ table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager")}); // sortable and zebra tables
+ }
+ else {
+ // There is NO checkbox in the first column, all columns are considered sortable
+ table.tablesorter({widgets: ['myZebra', 'truncatedList']}).tablesorterPager({container: $("#pager"), totalRows: 97, filter: sSerializedFilter, extra_params: sExtraParams}); // sortable and zebra tables
+ }
+ });
+ $('#'+divId).unblock();
+ }
+ }
+ );
+}
+
+/**
+ * Truncate a previously expanded list !
+ */
+function TruncateList(divId, iLimit, sNewLabel, sLinkLabel) {
+ $('#'+divId).block();
+ var iCount = 0;
+ $('#'+divId+' table.listResults tr:gt('+iLimit+')').each(function () {
+ $(this).remove();
+ });
+ $('#lbl_'+divId).html(sNewLabel);
+ $('#'+divId+' table.listResults tr:last td').addClass('truncated');
+ $('#'+divId+' table.listResults').addClass('truncated');
+ $('#trc_'+divId).html(sLinkLabel);
+ $('#'+divId+' .listResults').trigger("update"); // Reset the cache
+ $('#'+divId).unblock();
+}
+
+/**
+ * Reload any block -- used for periodic auto-reload
+ */
+function ReloadBlock(divId, sStyle, sSerializedFilter, sExtraParams) {
+ // Check if the user is not editing the list properties right now
+ var bDialogOpen = false;
+ var oDataTable = $('#'+divId+' :itop-datatable');
+ var bIsDataTable = false;
+ if (oDataTable.length > 0) {
+ bDialogOpen = oDataTable.datatable('IsDialogOpen');
+ bIsDataTable = true;
+ }
+ if (!bDialogOpen) {
+ if (bIsDataTable) {
+ oDataTable.datatable('DoRefresh');
+ }
+ else {
+ $('#'+divId).block();
+
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?style='+sStyle,
+ {operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams},
+ function (data) {
+ $('#'+divId).empty();
+ $('#'+divId).append(data);
+ $('#'+divId).removeClass('loading');
+ }
+ );
+ }
+ }
+}
+
+function SaveGroupBySortOrder(sTableId, aValues) {
+ var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id');
+ var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId;
+ if (aValues.length != 0) {
+ $sValue = JSON.stringify(aValues);
+ if (GetUserPreference(sPrefKey, null) != $sValue) {
+ SetUserPreference(sPrefKey, $sValue, true);
+ }
+ }
+}
+
+function LoadGroupBySortOrder(sTableId) {
+ var sDashboardId = $('#'+sTableId).closest('.dashboard_contents').attr('id');
+ var sPrefKey = 'GroupBy_'+sDashboardId+'_'+sTableId;
+ var sValues = GetUserPreference(sPrefKey, null);
+ if (sValues != null) {
+ aValues = JSON.parse(sValues);
+ window.setTimeout(function () {
+ $('#'+sTableId+' table.listResults').trigger('sorton', [aValues]);
+ }, 50);
+ }
+
+}
+
+/**
+ * Update the display and value of a file input widget when the user picks a new file
+ */
+function UpdateFileName(id, sNewFileName) {
+ var aPath = sNewFileName.split('\\');
+ var sNewFileName = aPath[aPath.length-1];
+
+ $('#'+id).val(sNewFileName);
+ $('#'+id).trigger('validate');
+ $('#name_'+id).text(sNewFileName);
+ return true;
+}
+
+/**
+ * Reload a search form for the specified class
+ */
+function ReloadSearchForm(divId, sClassName, sBaseClass, sContext, sTableId, sExtraParams) {
+ var oDiv = $('#ds_'+divId);
+ oDiv.block();
+ // deprecated in jQuery 1.8
+ //var oFormEvents = $('#ds_'+divId+' form').data('events');
+ var oForm = $('#ds_'+divId+' form');
+ var oFormEvents = $._data(oForm[0], "events");
+
+ // Save the submit handlers
+ aSubmit = new Array();
+ if ((oFormEvents != null) && (oFormEvents.submit != undefined)) {
+ for (var index = 0; index < oFormEvents.submit.length; index++) {
+ aSubmit [index] = {data: oFormEvents.submit[index].data, namespace: oFormEvents.submit[index].namespace, handler: oFormEvents.submit[index].handler};
+ }
+ }
+ sAction = $('#ds_'+divId+' form').attr('action');
+
+ // Save the current values in the form
+ var oMap = {};
+ $('#ds_'+divId+" form :input[name!='']").each(function () {
+ oMap[this.name] = this.value;
+ });
+ oMap.operation = 'search_form';
+ oMap.className = sClassName;
+ oMap.baseClass = sBaseClass;
+ oMap.currentId = divId;
+ oMap._table_id_ = sTableId;
+ oMap.action = sAction;
+ if(sExtraParams['selection_mode'])
+ {
+ oMap.selection_mode = sExtraParams['selection_mode'];
+ }
+ if(sExtraParams['result_list_outer_selector'])
+ {
+ oMap.result_list_outer_selector = sExtraParams['result_list_outer_selector'];
+ }
+ if(sExtraParams['cssCount'])
+ {
+ oMap.css_count = sExtraParams['cssCount'];
+ $(sExtraParams['cssCount']).val(0).trigger('change');
+ }
+ if(sExtraParams['table_inner_id'])
+ {
+ oMap.table_inner_id = sExtraParams['table_inner_id'];
+ }
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, oMap,
+ function (data) {
+ oDiv.empty();
+ oDiv.append(data);
+ oDiv.unblock();
+ oDiv.parent().resize(); // Inform the parent that the form has just been (potentially) resized
+ oDiv.find('form').triggerHandler('itop.search.form.reloaded');
+ }
+ );
+}
+
+/**
+ * Stores - in a persistent way - user specific preferences
+ * depends on a global variable oUserPreferences created/filled by the iTopWebPage
+ * that acts as a local -write through- cache
+ */
+function SetUserPreference(sPreferenceCode, sPrefValue, bPersistent) {
+ sPreviousValue = undefined;
+ try {
+ sPreviousValue = oUserPreferences[sPreferenceCode];
+ }
+ catch (err) {
+ sPreviousValue = undefined;
+ }
+ oUserPreferences[sPreferenceCode] = sPrefValue;
+ if (bPersistent && (sPrefValue != sPreviousValue)) {
+ ajax_request = $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php',
+ {operation: 'set_pref', code: sPreferenceCode, value: sPrefValue}); // Make it persistent
+ }
+}
+
+/**
+ * Get user specific preferences
+ * depends on a global variable oUserPreferences created/filled by the iTopWebPage
+ * that acts as a local -write through- cache
+ */
+function GetUserPreference(sPreferenceCode, sDefaultValue) {
+ var value = sDefaultValue;
+ if (oUserPreferences[sPreferenceCode] != undefined) {
+ value = oUserPreferences[sPreferenceCode];
+ }
+ return value;
+}
+
+/**
+ * Check/uncheck a whole list of checkboxes
+ */
+function CheckAll(sSelector, bValue) {
+ var value = bValue;
+ $(sSelector).each(function () {
+ if (this.checked != value) {
+ this.checked = value;
+ $(this).trigger('change');
+ }
+ });
+}
+
+
+/**
+ * Toggle (enabled/disabled) the specified field of a form
+ */
+function ToggleField(value, field_id) {
+ if (value) {
+ $('#'+field_id).prop('disabled', false);
+ // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
+ $('#'+field_id+' :input').prop('disabled', false);
+ }
+ else {
+ $('#'+field_id).prop('disabled', true);
+ // In case the field is rendered as a div containing several inputs (e.g. RedundancySettings)
+ $('#'+field_id+' :input').prop('disabled', true);
+ }
+ $('#'+field_id).trigger('update');
+ $('#'+field_id).trigger('validate');
+}
+
+/**
+ * For the fields that cannot be visually disabled, they can be blocked
+ * @return
+ */
+function BlockField(field_id, bBlocked) {
+ if (bBlocked) {
+ $('#'+field_id).block({message: ' ** disabled ** '});
+ }
+ else {
+ $('#'+field_id).unblock();
+ }
+}
+
+/**
+ * Updates (enables/disables) a "duration" field
+ */
+function ToggleDurationField(field_id) {
+ // Toggle all the subfields that compose the "duration" input
+ aSubFields = new Array('d', 'h', 'm', 's');
+
+ if ($('#'+field_id).prop('disabled')) {
+ for (var i = 0; i < aSubFields.length; i++) {
+ $('#'+field_id+'_'+aSubFields[i]).prop('disabled', true);
+ }
+ }
+ else {
+ for (var i = 0; i < aSubFields.length; i++) {
+ $('#'+field_id+'_'+aSubFields[i]).prop('disabled', false);
+ }
+ }
+}
+
+/**
+ * PropagateCheckBox
+ */
+function PropagateCheckBox(bCurrValue, aFieldsList, bCheck) {
+ if (bCurrValue == bCheck) {
+ for (var i = 0; i < aFieldsList.length; i++) {
+ var sFieldId = aFieldsList[i];
+ $('#enable_'+sFieldId).prop('checked', bCheck);
+ ToggleField(bCheck, sFieldId);
+
+ // Cascade propagation
+ $('#enable_'+sFieldId).trigger('change');
+ }
+ }
+}
+
+function FixTableSorter(table) {
+ if (table[0].config == undefined) {
+ // Table is not sort-able, let's fix it
+ var checkbox = (table.find('th:first :checkbox').length > 0);
+ if (checkbox) {
+ // There is a checkbox in the first column, don't make it sort-able
+ table.tablesorter({headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables
+ }
+ else {
+ // There is NO checkbox in the first column, all columns are considered sort-able
+ table.tablesorter({widgets: ['myZebra', 'truncatedList']}); // sort-able and zebra tables
+ }
+ }
+}
+
+function DashletCreationDlg(sOQL, sContext) {
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'dashlet_creation_dlg', oql: sOQL}, function (data) {
+ $('body').append(data);
+ });
+ return false;
+}
+
+function ShortcutListDlg(sOQL, sDataTableId, sContext) {
+ var sDataTableName = 'datatable_'+sDataTableId;
+ var oTableSettings = {
+ oColumns: $('#'+sDataTableName).datatable('option', 'oColumns'),
+ iPageSize: $('#'+sDataTableName).datatable('option', 'iPageSize')
+ };
+ var sTableSettings = JSON.stringify(oTableSettings);
+
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?'+sContext, {operation: 'shortcut_list_dlg', oql: sOQL, table_settings: sTableSettings}, function (data) {
+ $('body').append(data);
+ });
+ return false;
+}
+
+function ExportListDlg(sOQL, sDataTableId, sFormat, sDlgTitle) {
+ var aFields = [];
+ if (sDataTableId != '') {
+ var sDataTableName = 'datatable_'+sDataTableId;
+ var oColumns = $('#'+sDataTableName).datatable('option', 'oColumns');
+ for (var j in oColumns) {
+ for (var k in oColumns[j]) {
+ if (oColumns[j][k].checked) {
+ var sCode = oColumns[j][k].code;
+ if (sCode == '_key_') {
+ sCode = 'id';
+ }
+ aFields.push(j+'.'+sCode);
+ }
+ }
+ }
+ }
+
+ var oParams = {
+ interactive: 1,
+ mode: 'dialog',
+ expression: sOQL,
+ suggested_fields: aFields.join(','),
+ dialog_title: sDlgTitle
+ };
+
+ if (sFormat !== null) {
+ oParams.format = sFormat;
+ }
+
+ $.post(GetAbsoluteUrlAppRoot()+'webservices/export-v2.php', oParams, function (data) {
+ $('body').append(data);
+ });
+ return false;
+}
+
+function ExportToggleFormat(sFormat) {
+ $('.form_part').hide();
+ for (k in window.aFormParts[sFormat]) {
+ $('#form_part_'+window.aFormParts[sFormat][k]).show().trigger('form-part-activate');
+ }
+}
+
+function ExportStartExport() {
+ var oParams = {};
+ $('.form_part:visible :input').each(function () {
+ if (this.name != '') {
+ if ((this.type == 'radio') || (this.type == 'checkbox')) {
+ if (this.checked) {
+ oParams[this.name] = $(this).val();
+ }
+ }
+ else {
+ oParams[this.name] = $(this).val();
+ }
+ }
+ });
+ $(':itop-tabularfieldsselector:visible').tabularfieldsselector('close_all_tooltips');
+ $('#export-form').hide();
+ $('#export-feedback').show();
+ oParams.operation = 'export_build';
+ oParams.format = $('#export-form :input[name=format]').val();
+ var sQueryMode = $(':input[name=query_mode]:checked').val();
+ if ($(':input[name=query_mode]:checked').length > 0) {
+ if (sQueryMode == 'oql') {
+ oParams.expression = $('#export-form :input[name=expression]').val();
+ }
+ else {
+ oParams.query = $('#export-form :input[name=query]').val();
+ }
+ }
+ else {
+ oParams.expression = $('#export-form :input[name=expression]').val();
+ oParams.query = $('#export-form :input[name=query]').val();
+ }
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
+ if (data == null) {
+ ExportError('Export failed (no data provided), please contact your administrator');
+ }
+ else {
+ ExportRun(data);
+ }
+ }, 'json')
+ .fail(function () {
+ ExportError('Export failed, please contact your administrator');
+ });
+}
+
+function ExportError(sMessage) {
+ $('.export-message').html(sMessage);
+ $('.export-progress-bar').hide();
+ $('#export-btn').hide();
+}
+
+function ExportRun(data) {
+ switch (data.code) {
+ case 'run':
+ // Continue
+ $('.export-progress-bar').progressbar({value: data.percentage});
+ $('.export-message').html(data.message);
+ oParams = {};
+ oParams.token = data.token;
+ var sDataState = $('#export-form').attr('data-state');
+ if (sDataState == 'cancelled') {
+ oParams.operation = 'export_cancel';
+ }
+ else {
+ oParams.operation = 'export_build';
+ }
+
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
+ ExportRun(data);
+ },
+ 'json');
+ break;
+
+ case 'done':
+ $('#export-btn').hide();
+ sMessage = ''+data.message+' ';
+ $('.export-message').html(sMessage);
+ $('.export-progress-bar').hide();
+ $('#export-btn').hide();
+ $('#export-form').attr('data-state', 'done');
+ if (data.text_result != undefined) {
+ if (data.mime_type == 'text/html') {
+ $('#export_content').parent().html(data.text_result);
+ $('#export_text_result').show();
+ $('#export_text_result .listResults').tableHover();
+ $('#export_text_result .listResults').tablesorter({widgets: ['myZebra']});
+ }
+ else {
+ if ($('#export_text_result').closest('ui-dialog').length == 0) {
+ // not inside a dialog box, adjust the height... approximately
+ var jPane = $('#export_text_result').closest('.ui-layout-content');
+ var iTotalHeight = jPane.height();
+ jPane.children(':visible').each(function () {
+ if ($(this).attr('id') != '') {
+ iTotalHeight -= $(this).height();
+ }
+ });
+ $('#export_content').height(iTotalHeight-80);
+ }
+ $('#export_content').val(data.text_result);
+ $('#export_text_result').show();
+ }
+ }
+ $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
+ break;
+
+ case 'error':
+ $('#export-form').attr('data-state', 'error');
+ $('.export-progress-bar').progressbar({value: data.percentage});
+ $('.export-message').html(data.message);
+ $('#export-dlg-submit').button('option', 'label', Dict.S('UI:Button:Done')).button('enable');
+ $('#export-btn').hide();
+ default:
+ }
+}
+
+function ExportInitButton(sSelector) {
+ $(sSelector).on('click', function () {
+ var sDataState = $('#export-form').attr('data-state');
+ switch (sDataState) {
+ case 'not-yet-started':
+ $('.form_part:visible').each(function () {
+ $('#export-form').data('validation_messages', []);
+ var ret = $(this).trigger('validate');
+ });
+ var aMessages = $('#export-form').data('validation_messages');
+
+ if (aMessages.length > 0) {
+ alert(aMessages.join(''));
+ return;
+ }
+ if ($(this).hasClass('ui-button')) {
+ $(this).button('option', 'label', Dict.S('UI:Button:Cancel'));
+ }
+ else {
+ $(this).html(Dict.S('UI:Button:Cancel'));
+ }
+ $('#export-form').attr('data-state', 'running');
+ ExportStartExport();
+ break;
+
+ case 'running':
+ if ($(this).hasClass('ui-button')) {
+ $(this).button('disable');
+ }
+ else {
+ $(this).prop('disabled', true);
+ }
+ $('#export-form').attr('data-state', 'cancelled');
+ break;
+
+ case 'done':
+ case 'error':
+ $('#interactive_export_dlg').dialog('close');
+ break;
+
+ default:
+ // Do nothing
+ }
+ });
+}
+
+function DisplayHistory(sSelector, sFilter, iCount, iStart) {
+ $(sSelector).block();
+ var oParams = {operation: 'history_from_filter', filter: sFilter, start: iStart, count: iCount};
+ $.post(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', oParams, function (data) {
+ $(sSelector).html(data).unblock();
+ }
+ );
+}
+
+// Very simple equivalent to format: placeholders are %1$s %2$d ...
+function Format() {
+ var args = [];
+ var str = '';
+ if (arguments[0] instanceof Array) {
+ str = arguments[0][0].toString();
+ args = arguments[0];
+ }
+ else {
+ str = arguments[0].toString();
+ if (arguments.length > 1) {
+ var t = typeof arguments[1];
+ args = ("string" === t || "number" === t) ? Array.prototype.slice.call(arguments) : arguments[1];
+ }
+ }
+ var key;
+ for (key in args) {
+ str = str.replace(new RegExp("\\%"+key+"\\$.", "gi"), args[key]);
+ }
+
+ return str;
+}
+
+/**
+ * Enable to access translation keys client side.
+ * The called keys needs to be exported using \WebPage::add_dict_entry
+ */
+var Dict = {};
+if (typeof aDictEntries == 'undefined') {
+ Dict._entries = {}; // Entries have not been loaded (we are in the setup ?)
+}
+else {
+ Dict._entries = aDictEntries; // Entries were loaded asynchronously via their own js files
+}
+Dict.S = function (sEntry) {
+ if (sEntry in Dict._entries) {
+ return Dict._entries[sEntry];
+ }
+ else {
+ return sEntry;
+ }
+};
+Dict.Format = function () {
+ var args = Array.from(arguments);
+ args[0] = Dict.S(arguments[0]);
+ return Format(args);
}
\ No newline at end of file
diff --git a/pages/UI.php b/pages/UI.php
index cbc02ec19..d534e6e36 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -1205,6 +1205,8 @@ EOF
$aExpectedAttributes = MetaModel::GetTransitionAttributes($sClass, $sStimulus, $sState);
$aDetails = array();
+ $sFormId = 'apply_stimulus';
+ $sFormPrefix = $sFormId.'_';
$iFieldIndex = 0;
$aFieldsMap = array();
$aValues = array();
@@ -1220,6 +1222,7 @@ EOF
$sReadyScript = '';
foreach($aExpectedAttributes as $sAttCode => $iExpectCode)
{
+ $sFieldInputId = $sFormPrefix.$sAttCode;
// Prompt for an attribute if
// - the attribute must be changed or must be displayed to the user for confirmation
// - or the field is mandatory and currently empty
@@ -1232,19 +1235,19 @@ EOF
if (count($aPrerequisites) > 0)
{
// When 'enabling' a field, all its prerequisites must be enabled too
- $sFieldList = "['".implode("','", $aPrerequisites)."']";
- $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n");
+ $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aPrerequisites)."']";
+ $oP->add_ready_script("$('#enable_{$sFieldInputId}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n");
}
$aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one
if (count($aDependents) > 0)
{
// When 'disabling' a field, all its dependent fields must be disabled too
- $sFieldList = "['".implode("','", $aDependents)."']";
- $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n");
+ $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aDependents)."']";
+ $oP->add_ready_script("$('#enable_{$sFieldInputId}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n");
}
$aArgs = array('this' => $oObj);
- $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sAttCode, '', $iExpectCode, $aArgs);
- $sComments = ' ';
+ $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sFieldInputId, '', $iExpectCode, $aArgs);
+ $sComments = ' ';
if (!isset($aValues[$sAttCode]))
{
$aValues[$sAttCode] = array();
@@ -1272,11 +1275,11 @@ EOF
}
$sTip .= "
";
$sTip = addslashes($sTip);
- $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n";
- $sComments .= ''.count($aValues[$sAttCode]).'
';
+ $sReadyScript .= "$('#multi_values_$sFieldInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n";
+ $sComments .= ''.count($aValues[$sAttCode]).'
';
}
- $aDetails[] = array('label' => ''.$oAttDef->GetLabel().' ', 'value' => "$sHTMLValue ", 'comments' => $sComments);
- $aFieldsMap[$sAttCode] = $sAttCode;
+ $aDetails[] = array('label' => ''.$oAttDef->GetLabel().' ', 'value' => "$sHTMLValue ", 'comments' => $sComments);
+ $aFieldsMap[$sAttCode] = $sFieldInputId;
$iFieldIndex++;
}
}
@@ -1289,7 +1292,7 @@ EOF
$oP->add('');
}
$oP->add("