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.

'); + oResultAreaElem.html('

' + Dict.S('UI:Search:NoAutoSubmit:ExplainText') + '

'); 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("
\n"); - $oP->add("
\n"); + $oP->add("\n"); $oP->add("
\n"); $oP->details($aDetails); $oP->add("
\n"); @@ -1328,7 +1331,7 @@ EOF $oP->add_ready_script( <<GetSelectedClasses(); foreach($aSelectedClasses as $sAlias => $sClassName) { - $aAllFields['zlist'] = array_merge($aAllFields['zlist'], $aAllFields[$sAlias.'_zlist']); - unset($aAllFields[$sAlias.'_zlist']); - $aAllFields['others'] = array_merge($aAllFields['others'], $aAllFields[$sAlias.'_others']); - unset($aAllFields[$sAlias.'_others']); - + if(array_key_exists($sAlias.'_zlist', $aAllFields)) + { + $aAllFields['zlist'] = array_merge($aAllFields['zlist'], $aAllFields[$sAlias.'_zlist']); + unset($aAllFields[$sAlias.'_zlist']); + } + if(array_key_exists($sAlias.'_others', $aAllFields)) + { + $aAllFields['others'] = array_merge($aAllFields['others'], $aAllFields[$sAlias.'_others']); + unset($aAllFields[$sAlias.'_others']); + } } return $aAllFields; diff --git a/webservices/export-v2.php b/webservices/export-v2.php index 26a30a50c..ef7a70fa6 100644 --- a/webservices/export-v2.php +++ b/webservices/export-v2.php @@ -731,9 +731,17 @@ try $sMimeType = $oExporter->GetMimeType(); if ($sMimeType == 'text/html') { - $oP = new NiceWebPage('iTop export'); + // Note: Using NiceWebPage only for HTML export as it includes JS scripts & files, which makes no sense in other export formats. More over, it breaks Excel spreadsheet import. + if($oExporter instanceof HTMLBulkExport) + { + $oP = new NiceWebPage('iTop export'); + $oP->add_ready_script("$('table.listResults').tablesorter({widgets: ['MyZebra']});"); + } + else + { + $oP = new WebPage('iTop export'); + } $oP->add_style("body { overflow: auto; }"); - $oP->add_ready_script("$('table.listResults').tablesorter({widgets: ['MyZebra']});"); } else {