Although many Gallery users are oblivious to their presence, HTML forms are a very important part of navigating around a Gallery installation, so it's unsurprising that the Gallery framework provides tools and special functions to make creating and using forms as simple as possible. On the other hand some of these tools are buried deep in the Gallery structure making the parts that stick out above ground seem complicated, difficult or just plain incomprehensible. In this page we'll take a look at the life-cycle of the form on a Gallery page - from the time when the user fills in fields and causes the form to submit, through the mechanism that Gallery provides to pull the information from the form and present it to the code that needs it, then finishing up with how the html of the form is created and presented to the browser in the first place.
As a simple example we will use Gallery's login page, which is part of the Core distribution. For a standard Gallery installation you can reach this form via the following URL:
http://localhost/main.php?g2_view=core.UserAdmin&g2_subView=core.UserLoginObviously substituting your hostname and path for localhost.
Code examples and line numberings will be drawn from the 2.3RC1 release so you're looking the code up as you read you may need to scan a little to find the relevant portions for other Gallery2 releases.
To follow this page you should have a basic appreciation of Gallery Controllers and Views and how different Controllers and Views can be selected by setting appropriate request variables in the URL. Some experience of using Smarty (Gallery's templating engine) such as basic knowledge of how Gallery uses .tpl template files will be helpful, and we'll also assume a basic familiarity with HTML form operations - including how simple GET and POST requests work.
So let's dive in at the deep end and examine the HTML for this controller. I've simplified by removing all the HTML that isn't related to the form, including the header, and all the formatting. What's left is an example of a pretty standard form:
<form action="main.php" method="post" id="userAdminForm" enctype="application/x-www-form-urlencoded"> <input type="hidden" name="g2_return" value="/main.php?g2_itemId=181&"/> <input type="hidden" name="g2_formUrl" value="/main.php?g2_view=core.UserAdmin&g2_subView=core.UserLogin"/> <input type="hidden" name="g2_authToken" value="7afa82c7999d"/> <input type="hidden" name="g2_controller" value="core.UserLogin"/> <input type="hidden" name="g2_form[formName]" value="UserLogin"/> <input type="text" id="giFormUsername" size="16" name="g2_form[username]" value=""/> <input type="password" id="giFormPassword" size="16" name="g2_form[password]"/> <input type="submit" class="inputTypeSubmit" name="g2_form[action][login]" value="Login"/> <input type="submit" class="inputTypeSubmit" name="g2_form[action][cancel]" value="Cancel"/> </form>
I've restored some broken lines, but apart from that nothing is changed. Let's break it down line-by-line.
The form declaration includes the action and method fields. The action field specifies the URL the browser connects to to submit the form; in this case it's a relative URL referring to main.php. Remember that all gallery page loads execute main.php first, with different destinations within Gallery selected by an appropriate set of request variables. The method for this form is given as "post" - which means that the browser will return the form contents as part of the body of the request, instead of in the header as part of the URL. After POSTing a form to Gallery the browser's address bar will show only up to the "main.php" part - the form inputs will be invisible to the user. If you want to see them you can use something like the "Live HTTP Headers" extension to the Firefox browser. The id and the enctype attributes of the form declaration are significant only to the browser displaying the form and not to Gallery so we will not concern ourselves with them.
The next three lines of the form are three hidden fields. Although they don't show up on the webpage they contain information that is returned to Gallery and is used by Gallery as follows:
<input type="hidden" name="g2_return" value="/main.php?g2_itemId=181&"/>
Leaving aside the naming convention for the present, the input g2_return holds the URL of the page from which the browser was diverted to view this form. We'll examine the route this piece of information took to reach this point later; but when the form is returned to gallery it can be used to send the user's browser back to the previous page - so after the login sequence is complete the user can carry on without interruption.
<input type="hidden" name="g2_formUrl" value="/main.php?g2_view=core.UserAdmin&g2_subView=core.UserLogin"/>
The input g2_formUrl holds the value of the url that was used to create the form in the first place.
<input type="hidden" name="g2_authToken" value="7afa82c7999d"/>
The g2_authToken is a security feature: Before any Controller is executed, Gallery checks that a request variable called g2_authToken is set and contains a particular string of random characters. The string is stored in the Gallery Session store, so it remains associated with a particular user for the duration of their Gallery session. Because it's not stored in the user's cookie it can't be read by a certain class of hack so it helps make the session more secure. However every form or link created by Gallery that returns to run a Controller needs to have the authToken included with it so it can be sent back by the user's browser, and that's why we see it here.
Moving on, the next form input is this:
<input type="hidden" name="g2_controller" value="core.UserLogin"/>
The g2_controller input contains the name of the Controller that will be executed by Gallery. In this case it's the core.UserLogin controller. You could also see an input with the name g2_view, to return control to a View, although it's less likely; recalling the Model-View-Controller convention that only Controllers change data while Views display it - a form is most likely to be used to input data from the user in order to change something, hence a Controller should be used to process the form.
We'll take the next three lines together:
<input type="hidden" name="g2_form[formName]" value="UserLogin"/> <input type="text" size="16" name="g2_form[username]" value=""/> <input type="password" size="16" name="g2_form[password]"/>
I've removed some attributes for clarity, leaving the names types and values. One hidden field with its value set (the 'g2_form[formName]' set to 'UserLogin'), one text input for the user's name, and a one password-type field for the password. Notice the names for these fields include some array notation, using square brackets.
And finishing off the form are the submission buttons:
<input type="submit" name="g2_form[action][login]" value="Login"/> <input type="submit" name="g2_form[action][cancel]" value="Cancel"/>
Again I've removed some formatting attributes leaving the types, names and values.
So what happens to the contents of the form when either of the submission buttons is pushed? The browser forms the POST details with the names and values of all the form fields and sends them to in a request to main.php. The webserver is presented with a list of name/value pairs which might look something like this:
g2_return = /main.php?g2_itemId=181& g2_formUrl = /main.php?g2_view=core.UserAdmin&g2_subView=core.UserLogin g2_authToken = 7afa82c7999d g2_controller = core.UserLogin g2_form[formName] = UserLogin g2_form[username] = Alice g2_form[password] = AlicePassword g2_form[action][login] = Login g2_form[action][cancel]
Gallery uses a standard g2_ prefix for request variables so when, in main.php at line 169 we run this:
list ($controllerName, $viewName) = GalleryUtilities::getRequestVariables('controller', 'view');
Our incoming form produces a value for $controllerName of 'core.UserLogin'. Let's move on to line 237:
/* Get our form and return variables */ $form = GalleryUtilities::getFormVariables('form');
getFormVariables(...) returns the array made of all request variables that begin with a specified key, in this case those beginning with 'form'. It's a php feature (http://uk.php.net/manual/en/faq.html.php#faq.html.arrays) to turn request variables which use array syntax in their name attributes into an actual array; the Gallery function getFormVariables goes one step further and returns a single merged array from $_POST global (variables returned in a POST request), the $_GET global (variables returned as part of the url) and $_FILES (filenames of uploaded files). So $form becomes an array of all variables whose names started with g2_form no matter how they were returned to Gallery by the browser. In our example the equivalent array would be created in PHP as:
$form = array ( 'formName' => 'UserLogin', 'username' => 'Alice', 'password' => 'AlicePassword', 'action' => array ('login' => 'Login', 'cancel' => '' ) );
Moving on a little, we check the authToken value (line 240):
/* Verify the genuineness of the request */ if (!$controller->omitAuthTokenCheck()) { $ret = GalleryController::assertIsGenuineRequest(); if ($ret) { return array($ret, null); } }
Then send execution to the Controller whose name we discovered from the form contents (line 248):
/* Let the controller handle the input */ list ($ret, $results) = $controller->handleRequest($form);
You'll see that the $form array was explicitly passed to the controller. This means that every input that was in the form with the correct naming convention - beginning 'g2_form...' and following up with some array indices - will be presented to the Controller code.
We'll look briefly at the login controller at this time and see what it does with the $form array containing the data from the form. Our interest is not so much in how the controller works per-se, but rather to track the form data. You can look at the code in file modules/core/LoginController.inc, and the bulk of the work is done in function handleRequest from line 59.
Most forms have more than one submit button - even if nothing more exotic than 'save' and 'cancel' - so need to check which button was pressed, and this Controller is no exception. If it was the 'Login' button that was hit then we'll check the username and password from the form. Note the elements of the $form array match the array notation in the relevant form input field names.
if (isset($form['action']['login'])) { if (empty($form['username'])) { $error[] = 'form[error][username][missing]'; . . } } else if (isset($form['action']['cancel'])) { . . }
Look carefully in the code section above and see what happens if the username was left blank. The controller appends an entry to the $error array - where the value of the entry is a string again in array syntax beginning with 'form'. This pattern is repeated throughout the controller for different error conditions, for instance, if the password is found to be invalid:
. $error[] = 'form[error][invalidPassword]'; .
At the end this particular Controller either delegates to the core.userAdmin view and the core.UserLogin subview if the login wasn't successful and if the $error array is non-empty, or else if the login was successful (or if the user hit the cancel button) the controller redirects by returning $results['return'] = 1. This special redirect causes main.php to use the contents of the request variable g2_return as a destination to which to redirect the browser. But that variable was set in the form submission to be the URL of the page before login was selected, which has the required effect of sending the user back to the page from which they decided to login.
In the unsuccessful case the execution passes via a delegation; remember that whatever a Controller sets in the $return['delegate'] array is also added by main.php as new request variables; so by exiting with
$results['delegate']['view'] = 'core.UserAdmin'; $results['delegate']['subView'] = 'core.UserLogin';
then 'view' and 'subView' are set as new request variables.
Also set by the controller is the $results['error'] element, with whatever errors were serially collected by being appended to $error:
$results['error'] = $error;
Just before the controller exits.
When execution returns to main.php if a delegation is indicated then the $results['error'] return value is scanned (main.php line 298):
/* Load any errors into the request */ if (!empty($results['error'])) { foreach ($results['error'] as $error) { GalleryUtilities::putRequestVariable($error, 1); } }
and its contents are transferred to the set of request variables by adding them to PHP's global $_GET array. That means the next time we call GalleryUtilities::getFormVariables('form') (i.e. the next time in this request that we collect $form) the array it returns will include the error flags, because our controller set strings into $error that took the format of elements of the array $form. In the code we're examining, if the username had been missing from the submission the controller would set the string "form[error][username][missing]" into one of the elements of $return['error'] which would after the next call to GalleryUtilities::getFormVariables('form') $form['error']['username']['missing'] to be set to 1.
By appropriate return values in the $results['error'] array a form can therefore set flags in subsequent $form arrays, which will become useful later. One thing to bear in mind is that this technique will only work for delegations; in the case of a redirect the new page load means that anything we had added to $_GET would get washed away when PHP execution finishes as the browser is redirected; so the Gallery framework doesn't bother to examine $results['error'] on a redirect.
It is possible, however, for a controller to add to the $form array after a redirect. How? Recall that the GalleryUtilities::getFormVariables('form') call in main.php gathers all request variables whose name begins g2_form into one array, combining entries from the $_POST and the $_GET global variables. The $_GET variable of the next request is made up of the header variables appearing in that next URL, and that URL is formed by the gallery main.php location followed by key/value pairs (in HTTP GET format) specified in the $results['redirect'] array. So it's legitimate for a Controller to return
$results['redirect']['form[LeagueChampions]'] = 'Manchester'; $results['redirect']['form[FavouriteFruit]'] = 'Satsuma';
alongside the required Controller or View details. This would generate a URL including
... &g2_form[LeagueChampions]=Manchester&g2_form[FavouriteFruit]=Satsuma&...
Those header variables would be included in the $form array on the next page load as though this had executed:
$form['LeagueChampions'] = 'Manchester'; $form['FavouriteFruit'] = 'Satsuma';
Let's take up the story from where control had returned to main.php after an unsuccessful login attempt by the user. The Controller set $results to cause a delegate, as well as a new entry into the set of request variables - for instance 'form[error][username][missing]' set to 1 which would have flagged an empty username field.
Thereafter main.php loads the View at line 326:
list ($ret, $view) = GalleryView::loadView($viewName);
Then at line 465 we create a Template object and tell the View to doLoadTemplate:
$template = new GalleryTemplate(dirname(__FILE__)); list ($ret, $results, $theme) = $view->doLoadTemplate($template);
Gallery uses the Smarty templating system, and $template object is Gallery's container for the Smarty object.
doLoadTemplate (part of GalleryView.class) at line 224 performs a call to
/* Get our form variables */ $form = GalleryUtilities::getFormVariables('form'); if (!isset($form['formName'])) { $form['formName'] = ''; } $template->setVariableByReference('form', $form);
which makes a fresh copy of the form contents in $form, this time including whatever new request variables were added by the $results['error'] mechanism. The function setVariableByReference is a stub that simply calls the Smarty function assign_by_ref($key, $value) on the smarty object in $template, assuring that at template rendering the template has access to the $form array in the guise of the Smarty {$form} variable. Because it was assigned by reference any future changes made to that same $form array in the PHP code will also be visible to Smarty.
doLoadTemplate in line 291 does
/* Let the view and theme load up template data */ list ($ret, $results) = $this->loadTemplate($template, $form);
At which point execution transfers to code specific to this particular View, to the UserAdminView class in file modules/core/UserAdmin.inc at function loadTemplate(...). The UserAdminView is used to collate Views for a collection of different modules; The user gets the choice of a range of user administration Views in the left side-bar, and in the main panel on the right a specific subView selected from that choice is displayed. Any module registered with Gallery can register a callback for 'getUserAdminViews' and whenever the UserAdminView is loaded the registering module's view of choice is included in that left-sidebar list.
The particular choice of subView to put in the main panel is determined from the g2_subView request variable. In our example the subView was specified by the Controller as 'core.UserLogin' so the UserAdminView loads the UserLoginView class from file modules/core/UserLogin.inc and puts in a call to the loadTemplate(...) function there. Class UserLoginView is actually located alongside class UserLoginController beginning just a few lines after the code of UserLoginController finished; in a roundabout manner we've jumped the gap between them.
Although the UserAdminView doesn't explicitly touch the $form variable, its subView UserLoginView does, at line 219, where we find:
if ($form['formName'] != 'UserLogin') { $form['formName'] = 'UserLogin'; $form['username'] = ''; }
This pattern of testing then setting the formName is common to Gallery Views. Although it looks a little curious at first sight - we've already got a $form variable, and it already contains a formName field set to 'UserLogin' - that is only because we've already displayed this form once - and now we are displaying it a second time courtesy of a delegation request from the Controller. The first time the form is displayed then the $form['formName'] is not set: the $form array is empty as there were no request variables with the 'form' prefix. This conditional test in the View allows us to distinguish between the first and subsequent delegated visits to the same form. If this is the first visit we make sure that the $form['username'] field exists as an empty string; if this is our second visit (in this example case because the username/password combination was set wrong when the form was just submitted) then $form['formName'] is already set so we don't clear $form['username'] instead it remains set at whatever value it held at submission. That's a neat trick to avoid forcing the user to fill in every field again - but it's only going to work if we can use the contents of $form in creating the next rendering of the form. And that is exactly what we do.
Before we move on, see that UserLoginView also sets one other relevant variable:
$template->setVariable('controller', 'core.UserLogin');
We'll see it again later.
Execution returns back to UserAdminView, then to doLoadTemplate(...) in GalleryView.class, and from there back to main.php. The $results[] array from the View is examined in main.php and if it contains the name and path of a template file in $results['body'] we instruct the Template object to load that file:
$templatePath = 'gallery:' . $results['body']; . . . list ($ret, $html) = $template->fetch($templatePath);
That is when the template file is compiled to html. So next we'll turn to the contents of the template (.tpl) file and see how they are turned into HTML.
The template file specified in the UserAdminView was 'modules/core/templates/UserAdmin.tpl' - so let's have a look at the form-specific portions of that file. Here they are:
<form action="{g->url}" method="post" id="userAdminForm" enctype="{$UserAdmin.enctype|default:"application/x-www-form-urlencoded"}"> {g->hiddenFormVars} <input type="hidden" name="{g->formVar var="controller"}" value="{$controller}"/> <input type="hidden" name="{g->formVar var="form[formName]"}" value="{$form.formName}"/> . . . . </form>
This should be starting to look familiar from the HTML in the form we started with. The Smarty {g->hiddenFormVars} is one of a number of Gallery-specific tags added to Smarty. They're coded in core/classes/GalleryTemplateAdapter.class - and this one adds automatically the return URL, the URL of the current form, and the correct authToken, just as they appeared in the form HTML. So it's no difficulty to add those fields to any form, just include the one Smarty tag.
The next two parts of the form template are two hidden fields, and they also match up to the HTML. The name attributes are rendered with the help of another Gallery-specific Smarty tag: {g->formVar...}. Once again this is coded in GalleryTemplateAdapter.class, and what it does is return the argument it's called with, prefixed with 'g2_'. So {g->formVar var="controller"} returns the string 'g2_controller', and {g->formVar var="form[formName]"} returns 'g2_form[formName]' - precisely the way they appeared in the HTML. Why bother with a programmatic way of prepending three characters? Well, the 'g2_' prefix is configurable in Gallery's config.php. We need to add it to variables in forms and URLs, particularly to avoid clashes when Gallery is embedded in another website, and then to be able to strip it out when looking at request variables. Since the prefix isn't fixed it make sense to add it in only one place.
Let's turn to the value attributes of those two inputs: UserLoginView added a template variable called $controller and assigned it the value 'core.UserLogin' - so naturally this is what we see in the rendered output as the value of the 'controller' field. And the 'form[formName]' input's value is included from the value of $form['formName'] which was also set in UserLoginView. Recall that the purpose of the formName field is as a flag so that next time the View is loaded it is possible to tell that because this same form has already been displayed the form field values don't need to be intialised. In this example if a wrong username/password had been entered we show the page with the same username - carried through all the way from the original form submission as a request variable then via the PHP $form array and Smarty $form variable - the user doesn't need to retype it.
However we don't yet see the username field itself, nor the password field and the submit buttons. They are to be found in modules\core\templates\UserLogin.tpl, which is rendered right in the middle of UserAdmin.tpl. UserAdmin.tpl provides only the top and tail of the form - the parts that every UserAdmin page requires such as the form declaration itself, the controller, the formName, the authToken return and formUrl inputs. The bits that are specific to the UserLogin subView are provided by a template file specific to that subView: UserLogin.tpl. That's the next place we look.
Stripping out the formatting and other irrelevant content from UserLogin.tpl this is what's left:
<input type="text" name="{g->formVar var="form[username]"}" value="{$form.username}"/> {if isset($form.error.username.missing)} {g->text text="You must enter a username"} {/if} {if isset($form.error.username.disabled)} {g->text text=" ..... " } {/if} <input type="password" name="{g->formVar var="form[password]"}"/> {if isset($form.error.password.missing)} {g->text text="You must enter a password"} {/if} {if isset($form.error.invalidPassword)} {g->text text="Your login information is incorrect. Please try again."} {/if} <input type="submit" class="inputTypeSubmit" name="{g->formVar var="form[action][login]"}" value="{g->text text="Login"}"/> <input type="submit" class="inputTypeSubmit" name="{g->formVar var="form[action][cancel]"}" value="{g->text text="Cancel"}"/>
You'll recognise the inputs as the missing bits from the form, with the names replaced by the same Smarty syntax used for the controller and form-name inputs, and the values set from the values of the $form Smarty variable.
And along side them there are some Smarty conditional statements testing the value of {$form.error.password.missing}, {$form.error.username.missing} etc - the error flags that the Controller inserted into the $results['error'] variable which were then reinserted as header variables via the $_GET global then harvested the second time the $form array was gathered and passed to the Smarty object. They are used to display relevant messages adjacent to the inputs to which they apply. If this was the first viewing of the form or a redirect had occured then PHP's $form['error'] and Smarty's {$form.error} would be unset so no error messages would be shown.
If you think about it, you'll see how the UserAdmin and UserLogin Views conspired to include all the needed elements for the form - the form declaration itself, the hidden inputs, the visible inputs and the submission buttons. This division of labour is common in Gallery. In many cases the main View is provided as part of the core distribution and a module author writing a subView only provides a portion of the form contents. That would be the case not just for UserAdmin Views but for SiteAdmin Views, ItemEdit Plugins, and others. In writing such a subView you should make sure that your template doesn't double up what's provided by the main View; on the other hand if you're creating a page that displays solo then your template needs to include all of these elements itself.
Finally when the HTML has been rendered and returned to main.php, we're able to output the html to the browser (line 564):
print $session->replaceTempSessionIdIfNecessary($html);
Which in the case of the user login page brings us right back to the beginning again!