LOGIC Library

This site is the Logic involvement in sharing expertise and skills acquired in daily work. The goal is to create a solid knowledge base and share best practices in software development and systems management.

More info about us can be found on logicsistemi.it.

Joomla! 2.5 user plugin: preserving values during validation

In a previous post I've explained how to add fields to the user registration by creating a user plugin. We come back to it to solve a problem recently appeared on Stackoverflow.

The problem exposed there was caused by a wrong template that, creating an error in the Javascript code, blocked the client side validation, causing the form to be submitted with wrong fields.

Server side validation worked great and the user was not created and the wrong fields listed, but the values inserted in additional fields were lost.

We try to modify our plugin to preserve these values. This could be some sort of pedantry, but we use it to explore some other aspect of the Joomla! engine.

 

Reproduce the problem

To reproduce the problem we can simply disabling Javascript on our browser an try the user registration with the personalized plugin.

Compile the fields, leaving blank at least one of the required ones and writing something in the “Organization” field.

The Javascript validation is disabled, so your form is submitted, but after the validation some error is reported at the beginning of the page.

If you go down the form you will find all the inserted values, but not what you've written in the Organization field.

The cause of the problem

Users' registration is managed by a component that is part of the Joomla core and is realized using the MVC pattern. This component is com_users and you will find the frontend part of it in /components/com_users.

This is not the time to explain how the MVC Joomla! framework works, but if you followed this tutorial, you know that the action performed is choosed by the task URL parameters that invoke the corrisponding function in the controller.

In our case the task value is “registration.register” as you can find in the action of the form tag for the user registration page.

<form id="member-registration" action="/index.php/evidences-inventory/login?task=registration.register" method="post" class="form-validate">

On the submit is called the function register of the registration controller. Take a look at it and you will find that, if the form validation fail, all request data are stored in the user state. Here the piece of code that perform this action:

...
$data	= $model->validate($form, $requestData);
// Check for validation errors.
if ($data === false) {
	// Get the validation messages.
	$errors	= $model->getErrors();
	// Push up to three validation messages out to the user.
	for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) {
		if ($errors[$i] instanceof Exception) {
			$app->enqueueMessage($errors[$i]->getMessage(), 'warning');
		} else {
			$app->enqueueMessage($errors[$i], 'warning');
		}
	}
	// Save the data in the session.
	$app->setUserState('com_users.registration.data', $requestData);
	// Redirect back to the registration screen.
	$this->setRedirect(JRoute::_('index.php?option=com_users&view=registration', false));
	return false;
}
...

This action store all request information, also additional fields, so why the informations added by our plugin are lost? This can be explained by taking a look to the getData function of the registration model (/components/com_users/models/registration.php).

public function getData()
{
	if ($this->data === null) {
		$this->data	= new stdClass();
		$app	= JFactory::getApplication();
		$params	= JComponentHelper::getParams('com_users');
		// Override the base user data with any data in the session.
		$temp = (array)$app->getUserState('com_users.registration.data', array());
		foreach ($temp as $k => $v) {
			$this->data->$k = $v;
		}
		// Get the groups the user should be added to after registration.
		$this->data->groups = array();
		// Get the default new user group, Registered if not specified.
		$system	= $params->get('new_usertype', 2);
		$this->data->groups[] = $system;
		// Unset the passwords.
		unset($this->data->password1);
		unset($this->data->password2);
		// Get the dispatcher and load the users plugins.
		$dispatcher	= JDispatcher::getInstance();
		JPluginHelper::importPlugin('user');
		// Trigger the data preparation event.
		$results = $dispatcher->trigger('onContentPrepareData', array('com_users.registration', $this->data));
		// Check for errors encountered while preparing the data.
		if (count($results) && in_array(false, $results, true)) {
			$this->setError($dispatcher->getError());
			$this->data = false;
		}
	}
	return $this->data;
}

This function is called to populate the form with data and, for a user registration, we only need to populate it when the validation fail, to show again the informations inserted.

As you can see in the bold section, data are recovered from the user state, but are stored as attributes of the data properties. If you look at our plugin you will find that it stores informations in the array data->testprofile.

The solution

The solution is to add some code to the method onContentPrepareData of our plugin. This method, is invoked by the getData function (in bold in the previous code), so can load our informations from the user state and put them in the right place.

Here you are the new method:

function onContentPrepareData($context, $data)
{
	// Check we are manipulating a valid form.
	if (!in_array($context, array('com_users.profile','com_users.registration','com_users.user','com_admin.profile'))){
		return true;
	}
 
	$userId = isset($data->id) ? $data->id : 0;
 
	if (isset($data->id)) {
		$userId = $data->id;
		
		// Load the profile data from the database.
		$db = &JFactory::getDbo();
		$db->setQuery(
			'SELECT profile_key, profile_value FROM #__user_profiles' .
			' WHERE user_id = '.(int) $userId .
			' AND profile_key LIKE \'testprofile.%\'' .
			' ORDER BY ordering'
		);
		$results = $db->loadRowList();
	 
		// Check for a database error.
		if ($db->getErrorNum()) {
			$this->_subject->setError($db->getErrorMsg());
			return false;
		}
	 
		// Merge the profile data.
		$data->testprofile = array();
		foreach ($results as $v) {
			$k = str_replace('testprofile.', '', $v[0]);
			$data->testprofile[$k] = $v[1];
		}
	} else {		
		$app	= JFactory::getApplication();
		$temp = (array)$app->getUserState('com_users.registration.data', array());
		foreach ($temp as $k => $v) {
			$this->data->testprofile[$k] = $v;
		}
	}
 
	return true;
}

Conclusions

This solution, such as the plugin, is not perfect, because during data loading informations are duplicated as attributes of data and as elements of the testprofile array. This means that you must be carefull to not duplicate fields names in different sections to avoid problems.

You can find here the complete package to try.