From 6297809716baab5b2ec219eef5da4ae558773122 Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Thu, 12 May 2016 10:22:23 +0000 Subject: [PATCH] Customer portal : User Profile brick that allows basic Contact informations edition, password / preferences change from the portal SVN:trunk[4068] --- .../en.dict.itop-portal-base.php | 2 + .../fr.dict.itop-portal-base.php | 2 + .../objectcontroller.class.inc.php | 14 +- .../userprofilebrickcontroller.class.inc.php | 171 +++++++++++++++-- .../entities/userprofilebrick.class.inc.php | 110 +++++++++++ .../src/forms/objectformmanager.class.inc.php | 48 +++-- .../forms/passwordformmanager.class.inc.php | 179 ++++++++++++++++++ .../preferencesformmanager.class.inc.php | 169 +++++++++++++++++ .../helpers/applicationhelper.class.inc.php | 19 +- .../bricks/user-profile/layout.html.twig | 148 +++++++++++---- .../2.x/itop-portal-base/portal/web/index.php | 9 +- .../portal/web/js/portal_form_handler.js | 6 +- js/field_set.js | 16 +- js/form_field.js | 2 +- 14 files changed, 812 insertions(+), 83 deletions(-) create mode 100644 datamodels/2.x/itop-portal-base/portal/src/forms/passwordformmanager.class.inc.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/forms/preferencesformmanager.class.inc.php diff --git a/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php b/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php index d7cc82ea2..89ef41ba4 100644 --- a/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/en.dict.itop-portal-base.php @@ -67,6 +67,8 @@ Dict::Add('EN US', 'English', 'English', array( 'Brick:Portal:UserProfile:Password:Title' => 'Password', 'Brick:Portal:UserProfile:Password:ChoosePassword' => 'Choose password', 'Brick:Portal:UserProfile:Password:ConfirmPassword' => 'Confirm password', + 'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator' => 'To change your password, please contact your iTop administrator', + 'Brick:Portal:UserProfile:Password:CantChangeForUnknownReason' => 'Can\'t change password, please contact your iTop administrator', 'Brick:Portal:UserProfile:PersonalInformations:Title' => 'Personal informations', 'Brick:Portal:UserProfile:Photo:Title' => 'Photo', )); diff --git a/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php b/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php index 982282fd4..5d6969039 100644 --- a/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php +++ b/datamodels/2.x/itop-portal-base/fr.dict.itop-portal-base.php @@ -67,6 +67,8 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Brick:Portal:UserProfile:Password:Title' => 'Mot de passe', 'Brick:Portal:UserProfile:Password:ChoosePassword' => 'Choisissez un mot de passe', 'Brick:Portal:UserProfile:Password:ConfirmPassword' => 'Confirmer le mot de passe', + 'Brick:Portal:UserProfile:Password:CantChangeContactAdministrator' => 'Veuillez vous adresser à votre administrateur iTop pour changer votre mot de passe', + 'Brick:Portal:UserProfile:Password:CantChangeForUnknownReason' => 'Impossible de modifier votre mot de passe, veuillez contacter votre administrateur iTop', 'Brick:Portal:UserProfile:PersonalInformations:Title' => 'Informations personnelles', 'Brick:Portal:UserProfile:Photo:Title' => 'Photo', )); 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 5ced83695..ffff48b8f 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 @@ -25,7 +25,6 @@ use \Symfony\Component\HttpFoundation\Response; use \Symfony\Component\HttpFoundation\RedirectResponse; use \Symfony\Component\HttpKernel\HttpKernelInterface; use \Exception; -use \SecurityException; use \FileUploadException; use \utils; use \Dict; @@ -36,10 +35,8 @@ use \BinaryExpression; use \FieldExpression; use \VariableExpression; use \DBObjectSet; -use \CMDBObject; use \cmdbAbstractObject; use \UserRights; -use \Combodo\iTop\Portal\Brick\BrowseBrick; use \Combodo\iTop\Portal\Helper\ApplicationHelper; use \Combodo\iTop\Portal\Helper\SecurityHelper; use \Combodo\iTop\Portal\Helper\ContextManipulatorHelper; @@ -133,7 +130,9 @@ class ObjectController extends AbstractController } // Checking security layers - if (!SecurityHelper::IsActionAllowed($oApp, UR_ACTION_MODIFY, $sObjectClass, $sObjectId)) + // 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) { $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); } @@ -432,7 +431,6 @@ class ObjectController extends AbstractController $aCallbackUrls = $oApp['context_manipulator']->GetCallbackUrls($oApp, $aActionRules, $oObject, $bModal); $aFormData['submit_callback'] = $aCallbackUrls['submit']; $aFormData['cancel_callback'] = $aCallbackUrls['cancel']; - //var_dump($aFormData); // Preparing renderer // Note : We might need to distinguish form & renderer endpoints @@ -501,9 +499,9 @@ class ObjectController extends AbstractController // 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())) - ); +// $aFormData['validation']['redirection'] = array( +// 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey())) +// ); } } break; diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php index 83cdbe816..3cd3f2932 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php @@ -19,12 +19,16 @@ namespace Combodo\iTop\Portal\Controller; +use \Exception; use \UserRights; use \Silex\Application; use \Symfony\Component\HttpFoundation\Request; use \Combodo\iTop\Portal\Helper\ApplicationHelper; use \Combodo\iTop\Portal\Brick\UserProfileBrick; use \Combodo\iTop\Portal\Controller\ObjectController; +use \Combodo\iTop\Portal\Form\PreferencesFormManager; +use \Combodo\iTop\Portal\Form\PasswordFormManager; +use \Combodo\iTop\Renderer\Bootstrap\BsFormRenderer; class UserProfileBrickController extends BrickController { @@ -55,22 +59,165 @@ class UserProfileBrickController extends BrickController } $aData = array(); - - // Retrieving current contact - $oCurContact = UserRights::GetContactObject(); - $sCurContactClass = get_class($oCurContact); - $sCurContactId = $oCurContact->GetKey(); - // Preparing contact form - $aData['forms']['contact'] = ObjectController::HandleForm($oRequest, $oApp, ObjectController::ENUM_MODE_EDIT, $sCurContactClass, $sCurContactId); -// var_dump($aData['forms']['contact']); -// die(); + // If this is ajax call, we are just submiting preferences or password forms + if ($oRequest->isXmlHttpRequest()) + { + $aCurrentValues = $oRequest->request->get('current_values'); + $sFormType = $aCurrentValues['form_type']; + if ($sFormType === PreferencesFormManager::FORM_TYPE) + { + $aData['form'] = $this->HandlePreferencesForm($oRequest, $oApp); + } + elseif ($sFormType === PasswordFormManager::FORM_TYPE) + { + $aData['form'] = $this->HandlePasswordForm($oRequest, $oApp); + } + else + { + throw new Exception('Unknown form type.'); + } + $oResponse = $oApp->json($aData); + } + // Else, we are displaying page for first time + else + { + // Retrieving current contact + $oCurContact = UserRights::GetContactObject(); + $sCurContactClass = get_class($oCurContact); + $sCurContactId = $oCurContact->GetKey(); - $aData = $aData + array( - 'oBrick' => $oBrick + // Preparing forms + $aData['forms']['contact'] = ObjectController::HandleForm($oRequest, $oApp, ObjectController::ENUM_MODE_EDIT, $sCurContactClass, $sCurContactId, $oBrick->GetForm()); + $aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $oApp); + // - If user can change password, we display the form + $aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $oApp) : null; + + $aData = $aData + array( + 'oBrick' => $oBrick + ); + + $oResponse = $oApp['twig']->render($oBrick->GetPageTemplatePath(), $aData); + } + + return $oResponse; + } + + public function HandlePreferencesForm(Request $oRequest, Application $oApp) + { + $aFormData = array(); + $oRequestParams = $oRequest->request; + + // Handling form + $sOperation = $oRequestParams->get('operation'); + // - Create + if ($sOperation === null) + { + // - Creating renderer + $oFormRenderer = new BsFormRenderer(); + $oFormRenderer->SetEndpoint($_SERVER['REQUEST_URI']); + // - Creating manager + $oFormManager = new PreferencesFormManager(); + $oFormManager->SetRenderer($oFormRenderer) + ->Build(); + } + // - Submit + else if ($sOperation === 'submit') + { + $sFormManagerClass = $oRequestParams->get('formmanager_class'); + $sFormManagerData = $oRequestParams->get('formmanager_data'); + if ($sFormManagerClass === null || $sFormManagerData === null) + { + $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.'); + } + + // Rebuilding manager from json + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + // Applying modification to object + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values'))); + // Reloading page only if preferences were changed + if (($aFormData['validation']['valid'] === true) && !empty($aFormData['validation']['messages']['success'])) + { + $aFormData['validation']['redirection'] = array( + 'url' => $oApp['url_generator']->generate('p_user_profile_brick'), + ); + } + } + else + { + // Else, submit from another form + } + + // Preparing field_set data + $aFieldSetData = array( + 'fields_list' => $oFormManager->GetRenderer()->Render(), + 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(), + 'form_path' => $oFormManager->GetForm()->GetId() ); - return $oApp['twig']->render($oBrick->GetPageTemplatePath(), $aData); + // Preparing form data + $aFormData['id'] = $oFormManager->GetForm()->GetId(); + $aFormData['formmanager_class'] = $oFormManager->GetClass(); + $aFormData['formmanager_data'] = $oFormManager->ToJSON(); + $aFormData['renderer'] = $oFormManager->GetRenderer(); + $aFormData['fieldset'] = $aFieldSetData; + + return $aFormData; + } + + public function HandlePasswordForm(Request $oRequest, Application $oApp) + { + $aFormData = array(); + $oRequestParams = $oRequest->request; + + // Handling form + $sOperation = $oRequestParams->get('operation'); + // - Create + if ($sOperation === null) + { + // - Creating renderer + $oFormRenderer = new BsFormRenderer(); + $oFormRenderer->SetEndpoint($_SERVER['REQUEST_URI']); + // - Creating manager + $oFormManager = new PasswordFormManager(); + $oFormManager->SetRenderer($oFormRenderer) + ->Build(); + } + // - Submit + else if ($sOperation === 'submit') + { + $sFormManagerClass = $oRequestParams->get('formmanager_class'); + $sFormManagerData = $oRequestParams->get('formmanager_data'); + if ($sFormManagerClass === null || $sFormManagerData === null) + { + $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.'); + } + + // Rebuilding manager from json + $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); + // Applying modification to object + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values'))); + } + else + { + // Else, submit from another form + } + + // Preparing field_set data + $aFieldSetData = array( + 'fields_list' => $oFormManager->GetRenderer()->Render(), + 'fields_impacts' => $oFormManager->GetForm()->GetFieldsImpacts(), + 'form_path' => $oFormManager->GetForm()->GetId() + ); + + // Preparing form data + $aFormData['id'] = $oFormManager->GetForm()->GetId(); + $aFormData['formmanager_class'] = $oFormManager->GetClass(); + $aFormData['formmanager_data'] = $oFormManager->ToJSON(); + $aFormData['renderer'] = $oFormManager->GetRenderer(); + $aFormData['fieldset'] = $aFieldSetData; + + return $aFormData; } } diff --git a/datamodels/2.x/itop-portal-base/portal/src/entities/userprofilebrick.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/entities/userprofilebrick.class.inc.php index 726bca926..8af0388a1 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/entities/userprofilebrick.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/entities/userprofilebrick.class.inc.php @@ -18,6 +18,8 @@ namespace Combodo\iTop\Portal\Brick; +use \DOMFormatException; +use \Combodo\iTop\DesignElement; use \Combodo\iTop\Portal\Brick\PortalBrick; /** @@ -34,6 +36,114 @@ class UserProfileBrick extends PortalBrick const DEFAUT_TITLE = 'Brick:Portal:UserProfile:Title'; static $sRouteName = 'p_user_profile_brick'; + protected $aForm; + + public function __construct() + { + parent::__construct(); + + $this->aForm = array( + 'id' => 'default-user-profile', + 'type' => 'zlist', + 'fields' => 'details', + 'layout' => null + ); + } + + /** + * + * @return array + */ + public function GetForm() + { + return $this->aForm; + } + + /** + * + * @param array $aForm + * @return \Combodo\iTop\Portal\Brick\UserProfileBrick + */ + public function SetForm($aForm) + { + $this->aForm = $aForm; + return $this; + } + + /** + * Load the brick's data from the xml passed as a ModuleDesignElement. + * This is used to set all the brick attributes at once. + * + * @param \Combodo\iTop\DesignElement $oMDElement + * @return UserProfileBrick + * @throws DOMFormatException + */ + public function LoadFromXml(DesignElement $oMDElement) + { + parent::LoadFromXml($oMDElement); + + // Checking specific elements + foreach ($oMDElement->GetNodes('./*') as $oBrickSubNode) + { + switch ($oBrickSubNode->nodeName) + { + case 'form': + // Note : This is inspired by Combodo\iTop\Portal\Helper\ApplicationHelper::LoadFormsConfiguration() + // Enumerating fields + if ($oBrickSubNode->GetOptionalElement('fields') !== null) + { + $this->aForm['type'] = 'custom_list'; + $this->aForm['fields'] = array(); + + foreach ($oBrickSubNode->GetOptionalElement('fields')->GetNodes('field') as $oFieldNode) + { + $sFieldId = $oFieldNode->getAttribute('id'); + if ($sFieldId !== '') + { + $aField = array(); + // Parsing field options like read_only, hidden and mandatory + if ($oFieldNode->GetOptionalElement('read_only')) + { + $aField['readonly'] = ($oFieldNode->GetOptionalElement('read_only')->GetText('true') === 'true') ? true : false; + } + if ($oFieldNode->GetOptionalElement('mandatory')) + { + $aField['mandatory'] = ($oFieldNode->GetOptionalElement('mandatory')->GetText('true') === 'true') ? true : false; + } + if ($oFieldNode->GetOptionalElement('hidden')) + { + $aField['hidden'] = ($oFieldNode->GetOptionalElement('hidden')->GetText('true') === 'true') ? true : false; + } + + $this->aForm['fields'][$sFieldId] = $aField; + } + else + { + throw new DOMFormatException('Field tag must have an id attribute', null, null, $oFormNode); + } + } + } + // Parsing presentation + if ($oBrickSubNode->GetOptionalElement('twig') !== null) + { + // Extracting the twig template and removing the first and last lines (twig tags) + $sXml = $oBrickSubNode->GetOptionalElement('twig')->Dump(true); + //$sXml = $oMDElement->saveXML($oBrickSubNode->GetOptionalElement('twig')); + $sXml = preg_replace('/^.+\n/', '', $sXml); + $sXml = preg_replace('/\n.+$/', '', $sXml); + + $this->aForm['layout'] = array( + 'type' => (preg_match('/\{\{|\{\#|\{\%/', $sXml) === 1) ? 'twig' : 'xhtml', + 'content' => $sXml + ); + } + break; + } + } + + return $this; + } + } ?> \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php index be88126ce..078b94be9 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/forms/objectformmanager.class.inc.php @@ -20,10 +20,10 @@ namespace Combodo\iTop\Portal\Form; use \Exception; -use \DOMFormatException; use \Silex\Application; use \utils; use \Dict; +use \UserRights; use \MetaModel; use \CMDBSource; use \DBObject; @@ -31,21 +31,11 @@ use \DBObjectSet; use \DBObjectSearch; use \DBObjectSetComparator; use \InlineImage; -use \UserRights; use \AttributeDateTime; use \Combodo\iTop\Form\FormManager; use \Combodo\iTop\Form\Form; use \Combodo\iTop\Form\Field\FileUploadField; -use \Combodo\iTop\Form\Field\HiddenField; use \Combodo\iTop\Form\Field\LabelField; -use \Combodo\iTop\Form\Field\StringField; -use \Combodo\iTop\Form\Field\TextAreaField; -use \Combodo\iTop\Form\Field\SelectField; -use \Combodo\iTop\Form\Field\RadioField; -use \Combodo\iTop\Form\Field\CheckboxField; -use \Combodo\iTop\Form\Validator\IntegerValidator; -use \Combodo\iTop\Form\Validator\NotEmptyValidator; -use \Combodo\iTop\Renderer\Bootstrap\BsFormRenderer; /** * Description of objectformmanager @@ -110,6 +100,12 @@ class ObjectFormManager extends FormManager $oFormManager->SetActionRulesToken($aJson['formactionrulestoken']); } + // Retrieving form properties + if (isset($aJson['formproperties'])) + { + $oFormManager->SetFormProperties($aJson['formproperties']); + } + // Retrieving callback urls if (!isset($aJson['formcallbacks'])) { @@ -215,6 +211,10 @@ class ObjectFormManager extends FormManager */ public function SetFormProperties($aFormProperties) { +// echo '
';
+//		print_r($aFormProperties);
+//		echo '
'; +// die(); $this->aFormProperties = $aFormProperties; return $this; } @@ -256,6 +256,7 @@ class ObjectFormManager extends FormManager $aJson['formobject_id'] = $this->oObject->GetKey(); $aJson['formmode'] = $this->sMode; $aJson['formactionrulestoken'] = $this->sActionRulesToken; + $aJson['formproperties'] = $this->aFormProperties; return $aJson; } @@ -433,7 +434,7 @@ class ObjectFormManager extends FormManager $oAttDef = MetaModel::GetAttributeDef(get_class($this->oObject), $sAttCode); // TODO : Make AttributeDefinition::MakeFormField() for all kind of fields - if (in_array(get_class($oAttDef), array('AttributeString', 'AttributeText', 'AttributeLongText', 'AttributeCaseLog', 'AttributeHTML', 'AttributeFriendlyName', 'AttributeEnum', 'AttributeExternalKey', 'AttributeCustomFields', 'AttributeLinkedSet', 'AttributeLinkedSetIndirect', 'AttributeDate', 'AttributeDateTime'))) + if (in_array(get_class($oAttDef), array('AttributeString', 'AttributeEmailAddress', 'AttributeText', 'AttributeLongText', 'AttributeCaseLog', 'AttributeHTML', 'AttributeFriendlyName', 'AttributeEnum', 'AttributeExternalKey', 'AttributeCustomFields', 'AttributeLinkedSet', 'AttributeLinkedSetIndirect', 'AttributeDate', 'AttributeDateTime'))) { $oField = $oAttDef->MakeFormField($this->oObject); @@ -631,10 +632,20 @@ class ObjectFormManager extends FormManager // The try catch is essentially to start a MySQL transaction in order to ensure that all or none objects are persisted when creating an object with links try { + $sObjectClass = get_class($this->oObject); + // Starting transaction CMDBSource::Query('START TRANSACTION'); + // Forcing allowed writing on the object if necessary. This is used in some particular cases. + $bAllowWrite = ($sObjectClass === 'Person' && $this->oObject->GetKey() == UserRights::GetContactId()); + if ($bAllowWrite) + { + $this->oObject->AllowWrite(true); + } + // Writing object to DB $bActivateTriggers = (!$this->oObject->IsNew() && $this->oObject->IsModified()); + $bWasModified = $this->oObject->IsModified(); $this->oObject->DBWrite(); // Finalizing images link to object, otherwise it will be cleaned by the GC InlineImage::FinalizeInlineImages($this->oObject); @@ -655,7 +666,7 @@ class ObjectFormManager extends FormManager $sTriggersQuery = $this->oApp['combodo.portal.instance.conf']['properties']['triggers_query']; if ($sTriggersQuery !== null) { - $aParentClasses = MetaModel::EnumParentClasses(get_class($this->oObject), ENUM_PARENT_CLASSES_ALL); + $aParentClasses = MetaModel::EnumParentClasses($sObjectClass, ENUM_PARENT_CLASSES_ALL); $oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL($sTriggersQuery), array(), array('parent_classes' => $aParentClasses)); while ($oTrigger = $oTriggerSet->Fetch()) { @@ -668,7 +679,10 @@ class ObjectFormManager extends FormManager // Ending transaction with a commit as everything was fine CMDBSource::Query('COMMIT'); - $aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved'))); + if ($bWasModified) + { + $aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved'))); + } } catch (Exception $e) { @@ -810,7 +824,11 @@ class ObjectFormManager extends FormManager } } // Then we build and update form - $this->SetFormProperties($aFormProperties); + // - We update form properties only we don't have any yet. This is a fallback for cases when form properties where not among the JSON data + if ($this->GetFormProperties() === null) + { + $this->SetFormProperties($aFormProperties); + } $this->Build(); } diff --git a/datamodels/2.x/itop-portal-base/portal/src/forms/passwordformmanager.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/forms/passwordformmanager.class.inc.php new file mode 100644 index 000000000..44abbe84c --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/forms/passwordformmanager.class.inc.php @@ -0,0 +1,179 @@ + + +namespace Combodo\iTop\Portal\Form; + +use \Exception; +use \CMDBSource; +use \Dict; +use \UserRights; +use \Combodo\iTop\Form\FormManager; +use \Combodo\iTop\Form\Form; +use \Combodo\iTop\Form\Field\HiddenField; +use \Combodo\iTop\Form\Field\PasswordField; + +/** + * Description of passwordformmanager + * + * @author Guillaume Lajarige + */ +class PasswordFormManager extends FormManager +{ + const FORM_TYPE = 'change_password'; + + public function Build() + { + // Building the form + $oForm = new Form('change_password'); + + // Adding hidden field with form type + $oField = new HiddenField('form_type'); + $oField->SetCurrentValue('change_password'); + $oForm->AddField($oField); + + // Adding old password field + $oField = new PasswordField('old_password'); + $oField->SetMandatory(true) + ->SetLabel(Dict::S('UI:Login:OldPasswordPrompt')); + $oForm->AddField($oField); + // Adding new password field + $oField = new PasswordField('new_password'); + $oField->SetMandatory(true) + ->SetLabel(Dict::S('Brick:Portal:UserProfile:Password:ChoosePassword')); + $oForm->AddField($oField); + // Adding confirm password field + $oField = new PasswordField('confirm_password'); + $oField->SetMandatory(true) + ->SetLabel(Dict::S('Brick:Portal:UserProfile:Password:ConfirmPassword')); + $oForm->AddField($oField); + + $oForm->Finalize(); + $this->oForm = $oForm; + $this->oRenderer->SetForm($this->oForm); + } + + /** + * Validates the form and returns an array with the validation status and the messages. + * If the form is valid, creates/updates the object. + * + * eg : + * array( + * 'status' => true|false + * 'messages' => array( + * 'errors' => array() + * ) + * + * @param array $aArgs + * @return array + */ + public function OnSubmit($aArgs = null) + { + $aData = array( + 'valid' => true, + 'messages' => array( + 'success' => array(), + 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this. + 'error' => array() + ) + ); + + // Update object and form + $this->OnUpdate($aArgs); + + // Check if form valid + if ($this->oForm->Validate()) + { + // The try catch is essentially to start a MySQL transaction + try + { + // Updating password + $sAuthUser = $_SESSION['auth_user']; + $sOldPassword = $this->oForm->GetField('old_password')->GetCurrentValue(); + $sNewPassword = $this->oForm->GetField('new_password')->GetCurrentValue(); + $sConfirmPassword = $this->oForm->GetField('confirm_password')->GetCurrentValue(); + + if ($sOldPassword !== '' && $sNewPassword !== '' && $sConfirmPassword !== '') + { + if (!UserRights::CanChangePassword()) + { + $aData['valid'] = false; + $aData['messages']['error'] += array('_main' => array(Dict::S('Brick:Portal:UserProfile:Password:CantChangeContactAdministrator'))); + } + else if (!UserRights::CheckCredentials($sAuthUser, $sOldPassword)) + { + $aData['valid'] = false; + $aData['messages']['error'] += array('old_password' => array(Dict::S('UI:Login:IncorrectOldPassword'))); + } + else if ($sNewPassword !== $sConfirmPassword) + { + $aData['valid'] = false; + $aData['messages']['error'] += array('confirm_password' => array(Dict::S('UI:Login:RetypePwdDoesNotMatch'))); + } + else if (!UserRights::ChangePassword($sOldPassword, $sNewPassword)) + { + $aData['valid'] = false; + $aData['messages']['error'] += array('confirm_password' => array(Dict::S('Brick:Portal:UserProfile:Password:CantChangeForUnknownReason'))); + } + else + { + $aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved'))); + } + } + } + catch (Exception $e) + { + $aData['valid'] = false; + $aData['messages']['error'] += array('_main' => array($e->getMessage())); + } + } + else + { + // Handle errors + $aData['valid'] = false; + $aData['messages']['error'] += $this->oForm->GetErrorMessages(); + } + + return $aData; + } + + public function OnUpdate($aArgs = null) + { + + // We build the form + $this->Build(); + + // Then we update it with new values + if (is_array($aArgs)) + { + if (isset($aArgs['currentValues'])) + { + foreach ($aArgs['currentValues'] as $sPreferenceName => $value) + { + $this->oForm->GetField($sPreferenceName)->SetCurrentValue($value); + } + } + } + } + + public function OnCancel($aArgs = null) + { + + } + +} diff --git a/datamodels/2.x/itop-portal-base/portal/src/forms/preferencesformmanager.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/forms/preferencesformmanager.class.inc.php new file mode 100644 index 000000000..460881921 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/forms/preferencesformmanager.class.inc.php @@ -0,0 +1,169 @@ + + +namespace Combodo\iTop\Portal\Form; + +use \Exception; +use \CMDBSource; +use \Dict; +use \UserRights; +use \Combodo\iTop\Form\FormManager; +use \Combodo\iTop\Form\Form; +use \Combodo\iTop\Form\Field\HiddenField; +use \Combodo\iTop\Form\Field\SelectField; + +/** + * Description of preferencesformmanager + * + * @author Guillaume Lajarige + */ +class PreferencesFormManager extends FormManager +{ + const FORM_TYPE = 'preferences'; + + public function Build() + { + // Building the form + $oForm = new Form('preferences'); + + // Adding hidden field with form type + $oField = new HiddenField('form_type'); + $oField->SetCurrentValue('preferences'); + $oForm->AddField($oField); + + // Adding language field + $oField = new SelectField('language'); + $oField->SetMandatory(true) + ->SetLabel(Dict::S('UI:Favorites:SelectYourLanguage')) + ->SetCurrentValue(Dict::GetUserLanguage()) + ->SetStartsWithNullChoice(false); + // - Preparing choices + foreach (Dict::GetLanguages() as $sCode => $aLanguage) + { + $oField->AddChoice($sCode, $aLanguage['description'] . ' (' . $aLanguage['localized_description'] . ')'); + } + // - Adding to form + $oForm->AddField($oField); + + $oForm->Finalize(); + $this->oForm = $oForm; + $this->oRenderer->SetForm($this->oForm); + } + + /** + * Validates the form and returns an array with the validation status and the messages. + * If the form is valid, creates/updates the object. + * + * eg : + * array( + * 'status' => true|false + * 'messages' => array( + * 'errors' => array() + * ) + * + * @param array $aArgs + * @return array + */ + public function OnSubmit($aArgs = null) + { + $aData = array( + 'valid' => true, + 'messages' => array( + 'success' => array(), + 'warnings' => array(), // Not used as of today, just to show that the structure is ready for change like this. + 'error' => array() + ) + ); + + // Update object and form + $this->OnUpdate($aArgs); + + // Check if form valid + if ($this->oForm->Validate()) + { + // The try catch is essentially to start a MySQL transaction + try + { + // Starting transaction + CMDBSource::Query('START TRANSACTION'); + $iFieldChanged = 0; + + // Updating user + $oCurUser = UserRights::GetUserObject(); + // - Language + $sLanguage = $this->oForm->GetField('language')->GetCurrentValue(); + if (($sLanguage !== null) && ($oCurUser->Get('language') !== $sLanguage)) + { + $oCurUser->Set('language', $sLanguage); + $iFieldChanged++; + } + + // Updating only if preferences changed + if ($iFieldChanged > 0) + { + $oCurUser->DBUpdate(); + $aData['messages']['success'] += array('_main' => array(Dict::S('Brick:Portal:Object:Form:Message:Saved'))); + } + + // Ending transaction with a commit as everything was fine + CMDBSource::Query('COMMIT'); + } + catch (Exception $e) + { + // End transaction with a rollback as something failed + CMDBSource::Query('ROLLBACK'); + $aData['valid'] = false; + $aData['messages']['error'] += array('_main' => array($e->getMessage())); + } + } + else + { + // Handle errors + $aData['valid'] = false; + $aData['messages']['error'] += $this->oForm->GetErrorMessages(); + } + + return $aData; + } + + public function OnUpdate($aArgs = null) + { + + // We build the form + $this->Build(); + + // Then we update it with new values + if (is_array($aArgs)) + { + if (isset($aArgs['currentValues'])) + { + foreach ($aArgs['currentValues'] as $sPreferenceName => $value) + { + $this->oForm->GetField($sPreferenceName)->SetCurrentValue($value); + } + } + } + } + + public function OnCancel($aArgs = null) + { + + } + +} diff --git a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php index 8c2fecedb..ce1d87c44 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/helpers/applicationhelper.class.inc.php @@ -122,6 +122,23 @@ class ApplicationHelper self::LoadClasses($sScannedDir, 'brick.class.inc.php', 'brick'); } + /** + * Loads form managers from the base portal + * + * @param string $sScannedDir Directory to load the managers from + * @throws \Exception + */ + static function LoadFormManagers($sScannedDir = null) + { + if ($sScannedDir === null) + { + $sScannedDir = __DIR__ . '/../forms'; + } + + // Loading form managers from base portal (those from modules have already been loaded by module.xxx.php files) + self::LoadClasses($sScannedDir, 'formmanager.class.inc.php', 'brick'); + } + /** * Registers routes in the Silex Application from all declared Router classes * @@ -581,7 +598,7 @@ class ApplicationHelper // $oApp['combodo.portal.instance.routes'] = $aRoutes; // } // Checking brick security - if ($oBrick->IsGrantedForProfiles(UserRights::ListProfiles())) + if ($oBrick->GetActive() && $oBrick->IsGrantedForProfiles(UserRights::ListProfiles())) { $aPortalConf['bricks'][] = $oBrick; $aPortalConf['bricks_total_width'] += $oBrick->GetWidth(); diff --git a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/user-profile/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/user-profile/layout.html.twig index 13053ac9c..13ed82045 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/views/bricks/user-profile/layout.html.twig +++ b/datamodels/2.x/itop-portal-base/portal/src/views/bricks/user-profile/layout.html.twig @@ -2,13 +2,16 @@ {# Browse brick base layout #} {% extends 'itop-portal-base/portal/src/views/bricks/layout.html.twig' %} +{% set oContactForm = forms.contact %} +{% set oPreferencesForm = forms.preferences %} +{% set oPasswordForm = forms.password %} {% block pMainHeaderTitle %} {{ oBrick.GetTitle()|dict_s }} {% endblock %} {% block pMainContentHolder%} -
+
@@ -16,30 +19,17 @@

{{ 'Brick:Portal:UserProfile:PersonalInformations:Title'|dict_s }}

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + +
+ + + +
+
+ {{ oContactForm.renderer.GetBaseLayout()|raw }} +
+
@@ -60,9 +50,16 @@

{{ 'Class:appUserPreferences/Attribute:preferences'|dict_s }}

-
-
-
+
+
+ + + +
+
+ {{ oPreferencesForm.renderer.GetBaseLayout()|raw }} +
+
@@ -70,15 +67,96 @@

{{ 'Brick:Portal:UserProfile:Password:Title'|dict_s }}

-
-
-
-
-
-
+ {% if oPasswordForm is not null %} +
+
+ + + +
+
+ {{ oPasswordForm.renderer.GetBaseLayout()|raw }} +
+
+ {% else %} + pas le droit + {% endif %}
+ +
+
+ +
- + +{% endblock %} + +{% block pPageReadyScripts %} + {{ parent() }} + + // Personal informations form + var oContactFormFieldSet = $('#{{ oContactForm.id }} > .form_fields').field_set({{ oContactForm.fieldset|json_encode()|raw }}); + $('#{{ oContactForm.id }}').portal_form_handler({ + formmanager_class: "{{ oContactForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oContactForm.formmanager_data|json_encode()|raw }}, + field_set: oContactFormFieldSet, + endpoint: "{{ oContactForm.renderer.GetEndpoint()|raw }}" + }); + + // Preferences form + var oPreferencesFormFieldSet = $('#{{ oPreferencesForm.id }} > .form_fields').field_set({{ oPreferencesForm.fieldset|json_encode()|raw }}); + $('#{{ oPreferencesForm.id }}').portal_form_handler({ + formmanager_class: "{{ oPreferencesForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oPreferencesForm.formmanager_data|json_encode()|raw }}, + field_set: oPreferencesFormFieldSet, + endpoint: "{{ oPreferencesForm.renderer.GetEndpoint()|raw }}" + }); + + {% if oPasswordForm is not null %} + // Password form + var oPasswordFormFieldSet = $('#{{ oPasswordForm.id }} > .form_fields').field_set({{ oPasswordForm.fieldset|json_encode()|raw }}); + $('#{{ oPasswordForm.id }}').portal_form_handler({ + formmanager_class: "{{ oPasswordForm.formmanager_class|escape('js') }}", + formmanager_data: {{ oPasswordForm.formmanager_data|json_encode()|raw }}, + field_set: oPasswordFormFieldSet, + endpoint: "{{ oPasswordForm.renderer.GetEndpoint()|raw }}" + }); + {% endif %} + + // Submit button + $('#user-profile-wrapper .form_buttons .form_btn_submit').off('click').on('click', function(oEvent){ + oEvent.preventDefault(); + + // Resetting feedback + $('#user-profile-wrapper .form_alerts .alert').hide(); + $('#user-profile-wrapper .form_alerts .alert > p').remove(); + $('#user-profile-wrapper .form_field').removeClass('has-error'); + $('#user-profile-wrapper .form_field .help-block > p').remove(); + + // Submiting contact form through AJAX + //if($('#{{ oContactForm.id }} .field_set').field_set('hasTouchedFields')) + //{ + $('#{{ oContactForm.id }}').portal_form_handler('submit', oEvent); + //} + + // Submiting preferences form through AJAX + //if($('#{{ oPreferencesForm.id }} .field_set').field_set('hasTouchedFields')) + //{ + $('#{{ oPreferencesForm.id }}').portal_form_handler('submit', oEvent); + //} + + {% if oPasswordForm is not null %} + // Submiting password form through AJAX + // Only if fields are filled + $('#{{ oPasswordForm.id }} :password').each(function(iIndex, oElem){ + if($(oElem).val() !== '') + { + $('#{{ oPasswordForm.id }}').portal_form_handler('submit', oEvent); + return false; + } + }); + {% endif %} + }); {% endblock %} \ No newline at end of file diff --git a/datamodels/2.x/itop-portal-base/portal/web/index.php b/datamodels/2.x/itop-portal-base/portal/web/index.php index e21b538ca..78eaecad9 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/index.php +++ b/datamodels/2.x/itop-portal-base/portal/web/index.php @@ -39,15 +39,15 @@ require_once __DIR__ . '/../src/providers/scopevalidatorserviceprovider.class.in require_once __DIR__ . '/../src/helpers/scopevalidatorhelper.class.inc.php'; require_once __DIR__ . '/../src/helpers/securityhelper.class.inc.php'; require_once __DIR__ . '/../src/helpers/applicationhelper.class.inc.php'; -// Forms -require_once __DIR__ . '/../src/forms/objectformmanager.class.inc.php'; -use \Exception; -use \Symfony\Component\HttpFoundation\Response; use \Combodo\iTop\Portal\Helper\ApplicationHelper; // Checking user rights and prompt if needed LoginWebPage::DoLoginEx(PORTAL_ID); +if (UserRights::GetContactId() == 0) +{ + die(Dict::S('Portal:ErrorNoContactForThisUser')); +} // Initializing Silex framework $oApp = new Silex\Application(); @@ -81,6 +81,7 @@ ApplicationHelper::LoadControllers(); ApplicationHelper::LoadRouters(); ApplicationHelper::RegisterRoutes($oApp); ApplicationHelper::LoadBricks(); +ApplicationHelper::LoadFormManagers(); ApplicationHelper::RegisterTwigExtensions($oApp); // Loading portal configuration from the module design diff --git a/datamodels/2.x/itop-portal-base/portal/web/js/portal_form_handler.js b/datamodels/2.x/itop-portal-base/portal/web/js/portal_form_handler.js index 56a6ba323..fc91c61f7 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/js/portal_form_handler.js +++ b/datamodels/2.x/itop-portal-base/portal/web/js/portal_form_handler.js @@ -81,7 +81,7 @@ $(function() var oValidation = oData.form.validation; // First we build the form - me.options.field_set.field_set('option', 'fields_list', oData.form.fields_list); + me.options.field_set.field_set('option', 'fields_list', oData.form.fieldset.fields_list); me.options.field_set.field_set('option', 'is_valid', oValidation.valid); me.options.field_set.field_set('buildForm'); @@ -299,6 +299,10 @@ $(function() { $('#page_overlay').fadeOut(200); }, + submit: function(oEvent) + { + this._onSubmitClick(oEvent); + }, getAttachmentIds: function() { var me = this; diff --git a/js/field_set.js b/js/field_set.js index ee0553aa8..eaeaff286 100644 --- a/js/field_set.js +++ b/js/field_set.js @@ -231,11 +231,6 @@ $(function() } return this.options.is_valid; }, - // Debug helper - showOptions: function() - { - return this.options; - }, _loadCssFile: function(url) { if (!$('link[href="' + url + '"]').length) @@ -322,6 +317,15 @@ $(function() this.options.style_element.text(this.buildData.style_code); eval(this.options.script_element.text()); - } + }, + hasTouchedFields: function() + { + return (this.options.touched_fields.length > 0); + }, + // Debug helper + showOptions: function() + { + return this.options; + }, }); }); diff --git a/js/form_field.js b/js/form_field.js index 97572b612..6f33ead5a 100644 --- a/js/form_field.js +++ b/js/form_field.js @@ -78,7 +78,7 @@ $(function() var value = null; this.element.find(':input').each(function(iIndex, oElem){ - if($(oElem).is(':hidden') || $(oElem).is(':text') || $(oElem).is('textarea')) + if($(oElem).is(':hidden') || $(oElem).is(':text') || $(oElem).is(':password') || $(oElem).is('textarea')) { value = $(oElem).val(); }