 | Level: Intermediate Alister Lewis-Bowen (alister.lewisbowen@gmail.com), Senior Software Engineer, IBM Stephen Evanchik (evanchsa@gmail.com), Software Engineer, IBM Louis Weitzman (louis.weitzman@gmail.com ), Senior Software Engineer, IBM
21 Nov 2006 Follow along in this series of articles as the IBM® Internet Technology Group designs, develops, and deploys an extranet Web site for a fictitious company, International Business Council (IBC), using a suite of freely available software. In this installment, learn how to define an extranet to meet client requirements and explore implementation techniques to create an extranet Web site.
Introduction
In this series of articles, the team created a customized Web site for a fictitious company, International Business Council (IBC). The site requires document storage, discussion groups, specialized workgroups, conference scheduling, and schedule session descriptions.
In Part 2 of this series, we explored the analysis phase, which helped clarify the business and user goals of IBC. From the analysis, we captured a set of requirements implementing the usage policy of the IBC Web site that defined an environment where a community of people (IBC members) can view and collaborate on confidential material. The requirements are:
- Each member must authenticate before viewing or interacting with any information.
- If a member is inactive, (that is, he or she has no interaction with the Web site for more than a defined period), the member's authentication is removed.
- When a member is not authenticated, no information will be shown other than that relating to reauthenticating.
- Every member needs to acknowledge compliance to the terms and conditions of usage on the first successful authentication.
- Acknowledgment of compliance to the terms and conditions of usage needs to be reviewed by every member regularly.
 | |
Extranet. A private network that uses Internet protocols, network connectivity, and possibly the public telecommunication system to securely share part of an organization's information or operations with suppliers, vendors, partners, customers or other businesses. |
|
These requirements fit into the definition of an extranet. In this article you learn how to create a login page, session expiration, and the presentation of compliance information that a user needs to acknowledge before continuing to view the remainder of the Web site. You will use the theme templates created in Part 7, adapt the Automated Logout module to create a timed-client side logout with an alert, and develop a new module to implement a compliance acknowledgment system.
While we hope the extranet functions we built for our use might be of interest, we hope that the explanation of how we modified an existing module and created a new module will be useful too. The information provided here should not be interpreted as a rigid set of development guidelines, but rather as a place to start when building your own solutions.
So let's start with the login page.
Logging into the extranet
Since we're creating an "Internet within an Internet," you need to make sure that any information within this private network is not seen by anyone other than the users of the network. When we view the IBC Web site, the only thing we should ever see is a user login page -- assuming they're not automatically authenticated by a cookie or the usual authentication storage tools that often come with modern browsers. (Cookies and password storage utilities are outside the scope of this article.) Figure 1 shows the IBC login page.
Figure 1. Login page for IBC Web site
In Part 7 you learned about a technique to choose an alternative template file based on whether a Drupal user is authenticated. This involved the use of some simple PHP code at the very top of the page.tpl.php file. For convenience, this code showing the login page when the user is not authenticated is shown in Listing 1.
Listing 1. Code fragment from page.tpl.php
<?php
global $user;
if (!$user->uid) {
include('login.tpl.php');
return;
}
?>
|
The uid property only appears within the $user object when the user is authenticated, so this provides a reliable method of telling the Drupal theme system to use another template file. In this case, we've named the login page template login.tpl.php.
As seen in Figure 1, the look of the login page is a lot different from the layout of any IBC Web pages shown in previous articles. Apart from the IBC branding, only information pertaining to authentication is displayed. The login.tpl.php template file is almost exclusively XHTML. We could have crafted our own login form, making sure the input form elements used the correct values to trigger the Drupal authentication system. But we chose to play it safe and have Drupal create the form for us, and embed this into our XHTML structure.
The code in Listing 2 shows us using the
module_invoke
function to safely call the
user_login
function in the user.module, and printing the resulting themed login form we need. We now have a Drupal generated login form within a separate page controlled by our theme, which is only shown when the user is not authenticated.
Listing 2. Embedding a Drupal generated login form
<div id="login_form">
<h2>Please log in</h2>
<?php
print module_invoke('user','login');
if ($messages != "") { print $messages; }
?>
</div>
|
Timed inactivity and logout
With extranet Web sites, it is standard that some or all of the information is sensitive, and should only be seen by authenticated users. To minimize the possibility of a user leaving an authenticated browser unattended, it is required that a mechanism be used to automatically log off the user after a defined period of time.
Initially, we implemented a solution where we added code to the session.inc file, part of the Drupal core, so we could hook into the HTTP request cycle early enough to affect the user session after the predefined amount of time of inactivity. Our solution worked well, but is not a recommended solution. Changing the Drupal core outside of the normal development cycle incurs substantial pain down the road. This is especially true when you upgrade to the next Drupal version.
We've since learned that a patch was submitted to the Drupal project that allows the loading of a different session.inc file during the bootstrap phase of the HTTPD request cycle.
As with anything, there are many ways of implementing automatic logout. Using the PHP session timeout setting didn't give us enough control, so we had two major ways to approach this: server side and client side.
- Server side
- Placing all the logic that determines how long a user has been inactive and then logging them off, entirely on the server. The benefits are primarily increased security. By keeping the code server side, you reduce the risk of any malicious activity through cross-site scripting, SQL injection, and so on. The disadvantages are that you can only check the timing on a request-by-request basis. We also considered employing a minute-by-minute trigger using the cron hook to check the timing.
- Client side
- Using a client-side solution means all the logic is kept on the client using something like JavaScript™. Of course, this could open your Web site up to cross-site scripting attacks if developed poorly. The benefit is that the logic can check the length of inactivity in real time. It is also easier to create an alert of an impending auto logout.
A combined approach could use an Ajax solution that can pick up invisible messages on the client side polled by the server-side logic to create an alert.
Modifying the Automated Logout module
This module provided a great starting point on which to develop our solution; we began by installing it in our Drupal environment. Given all the previous considerations, we wanted to give our client (IBC) the flexibility of using either a server-side or client-side solution. Our attention was drawn to the Automated Logout module. This module provides the logic to time the inactivity of a user on a request-by-request basis and logs the user off (that is, it removes the session information and redirects them to the front page) if this inactivity has passed a predefined time. We started by installing this module in our Drupal environment.
We added several features to the Automated Logout module, including:
- Set up any database fields and variables needed to enable the client-side script.
- Configuring a client side script to perform the autologout logic.
- Passing the server-side variables to the client side script.
- Insert JavaScript that times user inactivity, alerts the user a certain amount of time before the auto logout, and tells the server to log the user out of their session.
- If a user is automatically logged out, redirect that user to their previous location in the Web site when they log back in.
Now, let's create the supporting database and variables.
Implementing database modifications and new variables
Given the requirements, we need a number of additional variables to implement this new functionality with the autologout.module. These variables include:
- Flag whether or not to use a client-side script
- Time at which to show an alert
- Text to use for an alert
- Current URL to return the user to (before they were automatically logged off)
You might be aware of the Drupal destination variable. It can be defined in a hidden input element in a form, and is picked up by the
drupal_goto
function to provide a redirect after a form is submitted. We thought of using this for getting the user back to where they were, but ended up simply storing the URL in the autologout table so we could retrieve it at any point in the module request cycle or future requests.
Part 9 explains the function of a module install file and how updates are applied. Listing 3 shows how to update the existing autologout table with an update function to store a URL.
Listing 3. Updating autologout table schema in autologout.install file
function autologout_update_1() {
global $db_type;
switch ($db_type) {
case 'mysql':
case 'mysqli':
db_query('ALTER TABLE {autologout} ADD COLUMN url VARCHAR(255) NOT NULL ');
break;
case 'pgsql':
break;
}
}
|
If this module is already installed, the Web site administrator can run the update.php script to update the autologout table. If this module is newly installed, Drupal will run the update function to make sure the schema for the autologout table is up to date.
You can create new variables in the variables table using the
variable_set
function in the install file. However, to be consistent with the autologout.module, we added to the autologout_default_settings class object defined at the top of the file. The additions are shown in Listing 4. Notice that the $alert_text variable contains text with % preceding them. These are used later on to substitute values into the alert text.
Listing 4. Additional variables and defaults in the autologout.module file
class autologout_default_settings {
...
var $clientsidetrigger = FALSE; // Initially disabled
var $alert_time = 160; // default 2 minutes
var $alert_text = 'ALERT! %user_name, you have been inactive for some time. '.
'You will be automatically logged out in %time_remaining seconds '.
'unless you interact with our web site.';
...
}
|
Adding new module settings
Now that we have the variables we think we need for the client-side logic, we need to make these appear in the module settings page (admin/settings/autologout). Using the code shown in Listing 5, you can add a check box to enable the client-side logic and input elements to allow the modification of the alert time and alert text values.
Listing 5. Additional form elements to modify new variables in autologout settings page
function autologout_settings() {
...
$form['autologout']['clientsidetrigger'] = array(
'#type' => 'checkbox',
'#title' => t('Enable client side timeout'),
'#default_value' => _autologout_local_settings('clientsidetrigger'),
'#description' => t('Check this to allow javascript to trigger the '.
'auto logout instead of relying on the HTTP '.
'request mechanism. Enabling this disables any '.
'use of the browser refresh delta')
);
$form['autologout']['alert_time'] = array(
'#type' => 'textfield',
'#title' => t('Alert time value in seconds'),
'#default_value' => _autologout_local_settings('alert_time'),
'#size' => 10,
'#maxlength' => 12,
'#description' => t('The length of time, in seconds, before auto logout '.
'when an alert is shown warning the user. This time '.
'needs to be smaller than the timeout setting if an '.
'alert is displayed. This settings works only when '.
'the client side trigger is enabled.')
);
$form['autologout']['alert_text'] = array(
'#type' => 'textarea',
'#title' => t('Alert text'),
'#default_value' => _autologout_local_settings('alert_text'),
'#cols' => 60,
'#rows' => 3,
'#description' => t('The text to be used in the text alert dialog that '.
'warns the user of a pending auto logout. You can '.
'use the variables %timeout, %alert_time and %user_name.')
);
...
}
|
These three new form elements are built using the Drupal form API and use the existing _autologout_local_settings function to fetch the values used to populate the form elements. When Drupal processes a settings form, if there is a common parent array name this will be used as the name field for the record cut to the variables table; the child name and values will be serialized into the value field.
For example, the $form['autologout']['alert_text'] item contains the parent name autologout. Drupal will serialize the key alert_text and the value associated with that form item after submission and cut a record in the variable table using autologout for the name field and the serialized key/value pairs for the value field. The _autologout_local_settings function effectively deserializes the key/value pairs from the variable table to provide the value for the setting key passed as the argument.
In the Compliance module described in a moment, you use a different method to organize how the settings are stored in the variable table.
Passing the server-side variables to the client-side script
Now you need a way to pass these server-side variables to the client-side JavaScript. One method you can use is to create the JavaScript as a variable in the module, substitute the variables into it, and present this modified code to Drupal for inclusion into the Web page.
Another, perhaps more typical, method is to create new named containers, like DIV elements with id attribute values, within the Web page so that the JavaScript can pick them up at run time. Semantic markup purists might dislike this idea, and certainly an unstyled page with this extra markup will show unwanted content. Here, we'll show how to create hidden form elements in the closure variable that contains the variables needed by the JavaScript. While this still adds nonsemantic markup, at least the content will still be hidden from view when styling is turned off.
The existing autologout.module uses the
footer
hook to contain the logic that checks if a user has been idle for the requisite time before closing the session and redirecting them to the front page. Listing 6 shows the modifications added to this existing function.
Listing 6. Modification in the footer hook of autologout.module file
function autologout_footer() {
...
$footer = '';
...
if (_autologout_local_settings('enabled')) {
if (_autologout_local_settings('clientsidetrigger')) {
$alert_text = t(_autologout_local_settings('alert_text'), array(
'%timeout' => (int)_autologout_local_settings('timeout'),
'%time_remaining' => (int)_autologout_local_settings('alert_time'),
'%user_name' => check_plain($user->name)
));
$form = array();
$form['clientsidetrigger']['timeout'] = array(
'#type' => 'hidden',
'#value' => (int)_autologout_local_settings('timeout');
);
$form['clientsidetrigger']['alert_time'] = array(
'#type' => 'hidden',
'#value' => (int)_autologout_local_settings('alert_time')
);
$form['clientsidetrigger']['alert_text'] = array(
'#type' => 'hidden',
'#value' => check_plain($alert_text)
);
$footer = drupal_get_form('autologout_clientside_trigger_js', $form);
drupal_add_js(drupal_get_path('module', 'autologout') . '/clientsidetrigger.js');
}
else {
...
// existing logic for server side timing and logout
...
}
}
return $footer;
}
|
To preserve the existing functions but allow for the choice of a client-side approach, we will wrap this logic in a conditional statement that uses the $clientsidetrigger variable to determine which logic to run.
Let's look at the logic added to enable the client-side functions. First the $alert_text is constructed using the t function. This allows the substitution of the %string content in the alert text for the values they represent. Next the hidden form elements used to contain the arguments we want the client side logic to pick up are built using the Drupal Forms API. The drupal_get_form function generates the XHTML for the form, which will be presented in the closure region variable by the phptemplate engine. To make this appear in the Web page, use the code in Listing 7 before the end of the XHTML structure defined in the page.tpl.php template.
Listing 7. Including closure region XHTML in Web page defined in page.tpl.php file
The JavaScript that provides the client-side logic is presented to Drupal for inclusion in the Web page using the
drupal_add_js
function. The
drupal_get_path
function helps identify the path of the autologout.module under which the new JavaScript file has been placed.
The client-side script
The autologout.module comes with a simple countdown timer implemented in JavaScript. This can be displayed in a block to give the user an idea of when the session will timeout. While we could have added the alert and logout logic to this, we decided to keep the modification separate to simplify implementation.
The function of the client-side JavaScript is to create a countdown from the defined timeout value to zero, upon which the user is redirected to a URL that logs them off. At a defined time, an alert is displayed to warn the user that they will be logged off if they don't interact with the Web site.
The client-side JavaScript for this modification is contained in the file clientsidetrigger.js, which is in the same directory as the autologout.module. As seen in the previous section, this is presented to Drupal using the drupal_add_js function. The first thing that is done in this script is to tell Drupal to run it when the Web page is loaded, as shown in Listing 8.
Listing 8. Making sure this JavaScript is run when the Web page is loaded
if (isJsEnabled()) {
addLoadEvent(autologoutClientsideTriggerStart);
}
|
Drupal 4.7 provides some useful JavaScript utilities. The isJsEnabled function tests for the availability of necessary JavaScript methods, such as getElementsByTagName and getElementById. Without these, other JavaScript functions provided by the Drupal environment may not work. The addLoadEvent function adds a JavaScript function to the window.onload event, making sure it is run when the Web page is loaded.
In Listing 8 you can see that the autologoutClientsideTriggerStart function is added to the window.onload event.
The autologoutClientsideTriggerStart function initializes the countdown logic, as shown in Listing 9.
Listing 9. Initializing the client side countdown timer
var AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 0;
var AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID;
function autologoutClientsideTriggerStart() {
var countdown = parseInt(document.getElementById('edit-timeout').value);
var alertTime = parseInt(document.getElementById('edit-alert_time').value);
AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 1;
autologoutClientsideTriggerCountdown(countdown,alertTime);
}
|
Here two global variables are defined to handle the loop created by the setTimeout function used later on. You can see how the values in the form elements are pulled into the JavaScript as associated with the countdown and alertTime variables. We set our loop global variable, and then start the countdown by calling the autologoutClientsideTriggerCountdown function.
The countdown timer is just a simple
setTimout
structure used in JavaScript applications for timed operations. Listing 10 shows the code used to create the timer logic.
Listing 10. The client-side countdown timer
function autologoutClientsideTriggerCountdown(countdown,alertTime) {
if (countdown == 0) {
clearTimeout(AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID);
AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED = 0;
window.location.href = window.location.protocol + '//' +
window.location.hostname + ':' +
window.location.port +
'/autologout/logout';
}
if (countdown == alertTime) {
autologoutClientsideTriggerAlert();
}
if (AUTOLOGOUT_CLIENTSIDE_TRIGGER_ENABLED) {
countdown --;
AUTOLOGOUT_CLIENTSIDE_TRIGGER_TIMEOUT_ID =
setTimeout('autologoutClientsideTriggerCountdown(' + countdown + ',
' + alertTime + ')',
1000);
}
}
|
The autologoutClientsideTriggerCountdown function checks the $countdown variable. If it is the same as $alertTime then the autologoutClientsideTriggerAlert function is used to display a warning to the user. If the $countdown variable is zero, then the user is redirected to the URL, /autologout/logout.
The logic to create the alert is placed in a separate function called autologoutClientsideTriggerAlert(). This means we can load the alert text once from the input field element when the alert is built. Listing 11 shows this function.
Listing 11. Making sure this JavaScript is run when the Web page is loaded
function autologoutClientsideTriggerAlert() {
alert(document.getElementById('edit-alert_text').value);
}
|
This is a very simple alert, but you could create a more sophisticated alert mechanism by overriding this JavaScript function with one of your own in the page.tpl.php file or in a new JavaScript file presented to Drupal using the drupal_add_js function.
Controlling the logout on the server side
Because the client-side logic calls the URL /autologout/logout, we need to create a new menu item. The existing autologout.module needs a new menu hook to do this, as shown in Listing 12.
Listing 12. Registering the logout URL in autologout.module
function autologout_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array('path' => 'autologout/logout',
'title' => t('Autologut'),
'access' => TRUE,
'type' => MENU_CALLBACK,
'callback' => '_autologout_logout');
}
return $items;
}
|
This URL will now call the _autologout_logout function. The existing autologout.module performs the logout logic within the footer hook as described above. We reuse this code by placing it into a separate function and call it from this original location in the footer hook. The function is called _autologout_logout; a partial listing is shown in Listing 13.
Returning the user to the correct place
To improve usability when reauthenticating, a user should be placed back at the Web page when they were automatically logged out. This is where the URL column in the database is used.
You can make several modifications to the autologout.module to create this feature:
- Make sure any
INSERT queries to the autologout table include the new URL column.
- Store the URL in the
autologout table before logging off the user in the _autologout_logout function.
- Redirect the user to the stored URL found in the
autologout table after they log in again.
- Clear any stored URL in the
autologout table if the user logs out manually.
The only instance of an existing INSERT query in the autologout.module is in the logic to act on an update operation in the user hook. This operation was modified so that the URL field was set to an empty string.
To store the URL when a user is automatically logged out, we added the code in Listing 13 to the _autologout_logout function before the call to drupal_goto redirecting the user to the front page.
Listing 13. Additions to _autologout_logout() function to store the URL
function _autologout_logout() {
...
global $user;
$r = db_query("SELECT * FROM {autologout} WHERE uid = %d",
$user->uid);
if (db_num_rows($r) > 0) {
$r = db_query("UPDATE {autologout} SET url = '%s' WHERE uid = %d",
check_url($_SERVER["HTTP_REFERER"]), $user->uid);
}
else {
$r = db_query("INSERT INTO {autologout} SET uid = %d, url = '%s'",
$user->uid, check_url($_SERVER["HTTP_REFERER"]));
}
if (!$r) {
watchdog('user', 'Unable to insert current url before auto logout',
WATCHDOG_ERROR);
}
...
}
|
This is pretty standard code to make sure the URL is stored in the autologout table using the users ID as the key. A watchdog message is issued if there is a problem for audit purposes.
To ensure that the user is redirected to the URL after logging in again, you can use the log-in operation in the existing user hook. The autologout.module uses a switch statement in the existing user hook to recognize the operation that triggers the function call. Listing 14 shows the case statement used to retrieve the URL and redirect the user back to it.
Listing 14. Redirecting user after login in autologin.module
function autologout_user($op, &$edit, &$account, $category = NULL) {
...
case 'login':
$q = db_query("SELECT url FROM {autologout} WHERE uid = %d",
$account->uid);
if ($r = db_fetch_object($q)) {
if (isset($r->url) and $r->url != '') {
drupal_goto($r->url);
}
}
break;
...
|
If you successfully retrieve a URL from the autologout table for the user's ID, the drupal_goto function redirects the user to the URL.
Of course, if the user logs off the Web site out of choice, then any URL stored for that user needs to be reset to an empty string. To do this you can use the logout operation in the existing user hook. Listing 15 continues with the user hook showing how this is done.
Listing 15. Clearing the stored URL
...
case 'logout':
$r = db_query("SELECT * FROM {autologout} WHERE uid = %d",
$account->uid);
if (db_num_rows($r) > 0) {
$r = db_query("UPDATE {autologout} SET url = '' WHERE uid = %d",
$account->uid);
if (!$r) {
watchdog('user', 'Unable to reset URL before logout',
WATCHDOG_ERROR);
}
}
break;
...
|
These modifications to the autologout.module enable a client-side timeout with alert and the additional feature of storing the user's last URL before being automatically logged off.
In the next section you'll learn how to create a module to present compliance information to the user for them to acknowledge and create a review cycle of these terms and conditions.
A compliance mechanism
IBC maintains a policy of usage for the Web site, consisting of a set of terms and conditions that IBC members need to acknowledge and agree to before interacting with the content of the Web site. It is required that there be a regular review of these terms to maintain user compliance.
Before creating any new module, be sure to check if one already exists that could suffice, be enhanced, or used as a starting point for a new module. We were made aware of the Legal module, which overlaps functions with our new module. The Legal module provides a Terms and Conditions page for the reader to sign during the account registration process. However, our client's process was more strict. After the IBC administration team had created the member account, the new member was required to see and agree to the compliance page during their first login, before seeing any other content. Nonetheless, the Legal module would have been a great starting point.
The Compliance module presents a page of compliance information, like the one shown in Figure 2, just after the user logs in.
Figure 2. Compliance page presented after login, requesting user's acknowledgment
This page requests that the user acknowledge the information and restricts their interaction with the Web site until they agree. It keeps a record of which users have complied and creates a view on the compliance information, shown in Figure 3, for audit purposes. This module also provides a review cycle to make sure the compliance information is acknowledged by the Web site users at regular intervals. The construction of a basic module is explained in Part 6, so we won't talk about the overall structure of the module but instead concentrate on the details of the specific implementation of this new compliance module.
Figure 3. Compliance audit page for administrators
The install file
The schema for the table containing data for the compliance module contains:
uid
| User's ID as the key |
accept_date
| The date they last accepted the compliance information |
expiration_date
| Date the user needs to re-acknowledge the compliance information (if a review cycle is enabled) |
url
| A temporary store of a URL to redirect the user to where they were going before being asked to acknowledge the compliance information |
The use of the expiration_date column is redundant, since this can be determined by adding the review time offset to the accept_date time. However, it is used for convenience.
Listing 16 shows the code that creates the table to store the compliance information in a mysql/mysqli database.
Listing 16. Creating users_compliance table in compliance.install
function compliance_install() {
global $db_type;
$created = FALSE;
switch ($db_type) {
case 'mysql':
case 'mysqli':
$q = db_query("
CREATE TABLE IF NOT EXISTS users_compliance (
uid integer unsigned NOT NULL default '0',
accept_date integer NOT NULL default '0',
expiration_date integer NOT NULL default '0',
url varchar(255) NOT NULL default '',
PRIMARY KEY (uid)
);
");
if($q) {
$created = TRUE;
}
break;
case 'pgsql':
break;
}
_compliance_install_init();
if ($created) {
drupal_set_message(t('Compliance module installation was successful.'));
}
else {
drupal_set_message(t('Installation for the Compliance module was unsuccessful.'));
}
}
|
After the table is created, the _compliance_install_init() function is called. This initializes the variables used by the compliance module with the default values, as shown in Listing 17.
Listing 17. Setting compliance module defaults in compliance.install file
function _compliance_install_init() {
variable_set('compliance_page_node', 0);
variable_set('compliance_page_tile', t('Terms and conditions'));
variable_set('compliance_review_enabled', 0);
variable_set('compliance_review_cycle_time', 365);
variable_set('compliance_message',t('Please read the following and answer the '.
'question at the bottom of the page. Thank you.'));
variable_set('compliance_question',t('Do you agree to these term and conditions?'));
variable_set('compliance_positive_answer',t('Agree'));
variable_set('compliance_negative_answer',t('Disagree'));
variable_set('compliance_disagree_url', '/logout');
variable_set('compliance_reset', 0);
}
|
The variable_set function stores these variables and their values in the variables table. Now when this module is installed by enabling it from the list of modules presented at admin/modules, the database table will be created and the default values for the module installed into the Drupal system. These variables are editable using the compliance settings (admin/settings/compliance), assuming the appropriate access permissions.
The variables are:
compliance_page_node
| Node ID of the page used to describe the compliance information. |
compliance_page_title
| Title of the compliance information when being displayed as a form for user acknowledgment. |
compliance_review_enabled
| A flag used to enable or disable the review cycle mechanism. |
compliance_review_cycle_time
| The time, in days, before asking the user to review their acknowledgment of the compliance information. |
compliance_message
| An optional message placed at the top of the compliance information when being displayed as a form for user acknowledgment. |
compliance_question
| The question asking the user to acknowledge the compliance information in the form. |
compliance_positive_answer
| The text used in the button that the user presses as a positive response to the compliance question. |
compliance_negative_answer
| The text used in the button that the user presses as a negative response to the compliance question. |
compliance_disagree_url
| The URL the user is redirected to if their response is negative. A positive response will redirect to the Web page they were originally intending to go to. |
Access permissions
Be aware that the
variable_set function
can also serialize an array of values passed to it. This way you can use one record for the compliance settings instead of cutting a record for each variable. The
drupal_unpack function
can be used to deserialize these values on retrieval.
We assume that viewing the compliance information, with a form asking the user to acknowledge this information, is something any authenticated user can see. The only access restrictions needed are those to allow an administrator to configure the compliance module and view the compliance audit page. The audit page shows which users have acknowledged the compliance information. Listing 18 shows the perm and access hooks that implement these access permissions.
Listing 18. Configuring access permissions in compliance.module
/***
* Implementation of hook_perm
*/
function compliance_perm() {
return array('administer compliance','audit compliance');
}
/**
* Implementation of hook_access().
*/
function compliance_access($op, $node) {
if ($op == 'view') {
return user_access('access content');
}
else if ($op == 'create' || $op == 'update' || $op == 'delete') {
if(user_access('administer compliance')) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
|
URL definitions
The page containing the compliance information is created as a basic page node (/node/add/page). For the IBC Web site, we used the Path module that comes with Drupal 4.7 to alias the page node of the compliance information to the URL /tncs, a contraction of "terms and conditions." This is used in the links at the bottom of the page so users can view the Web site compliance information at any time.
The compliance page, /compliance, is the tncs page node wrapped with some extra content to let users register their acknowledgment. We define this URL in the menu hook shown in Listing 19.
One other URL, /compliance/audit, is registered in this menu hook. This provides the view on the database that describes which users have acknowledged the compliance information, as shown in Figure 3.
Notice that the default Cascading StyleSheet (CSS) file that we use to style the highlighting in the compliance audit page is presented to the Drupal system using the theme_add_style function.
Listing 19. Registering URLs in menu hook of compliance.module
function compliance_menu($may_cache) {
$items = array();
if ($may_cache) {
$items[] = array('path' => 'compliance',
'title' => variable_get('compliance_page_title',
t('Terms and conditions')),
'access' => user_access('access content'),
'type' => MENU_CALLBACK,
'callback' => 'compliance_display');
$items[] = array('path' => 'compliance/audit',
'title' => t("Compliance audit"),
'access' => user_access('audit compliance'),
'type' => MENU_CALLBACK,
'callback' => 'compliance_audit');
theme_add_style(drupal_get_path('module', 'compliance') .
'/compliance.css');
}
return $items;
}
|
Settings
As described in Part 6, the settings hook provides a form to interactively manage the stored variables for your module. This hook uses the Drupal Forms API to build the form layout. The interesting parts of the settings hook in the compliance module are shown in Listing 20.
One of the form widgets creates a SELECT form element that lists the page nodes as OPTION elements. One of these can then be selected as the compliance information page. If no page nodes are found, the user is prompted to add one.
Notice that the $form array name for each element matches the variable name in the variable_get function. When Drupal processes a submitted form built with the settings hook, it uses this array name as the variable when storing the associated form element value. So, the submitted value of the form element defined in $form['autologout']['alert_time'] will be stored in the variable table as alert_time. This is a slightly different approach to the way the variables are stored in the autologout_settings function.
Listing 20. Building page node options for select widget in settings hook in compliance.module
function compliance_settings() {
...
$q = db_query('SELECT n.nid, r.title FROM node n INNER JOIN
node_revisions r USING (vid) WHERE n.type="page"');
while ($r = db_fetch_object($q)) {
$options[$r->nid] = t($r->title);
$selected = $r->nid;
}
if (count($options) == 0) {
drupal_set_message(t('Please %link that will display the compliance terms '.
'and conditions and then come back to this page to complete '.
'the compliance settings.',
array('%link' => l(t('add a page'), 'node/add/page/'))));
return;
}
...
$form['compliance_page']['compliance_page_node'] = array(
'#type' => 'select',
'#title' => t('Page'),
'#default_value' => variable_get('compliance_page_node', $selected),
'#options' => $options,
'#description' => t('This is the page displayed to the user describing '.
'the compliance terms and conditions. You can create '.
'a %link if no existing pages are appropriate to use.',
array('%link' => l(t('new page'), 'node/add/page/')))
);
$form['compliance_page']['compliance_page_title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => variable_get('compliance_page_title',
t('Terms and conditions')),
'#description' => t('This is the text of the title that will '.
'override the one that comes with the node chosen above.')
);
$form['compliance_ack'] = array(
'#type' => 'fieldset',
'#title' => t('Acknowledgment'),
'#weight' => -14,
'#description' => t('So that we can confirm that the user has read '.
'and/or agrees to this compliance content, a question '.
'is presented to ask the user to acknowledge this '.
'compliance page before moving on. If a positive answer '.
'is given, the user is redirected to the original page '.
'they would have gone to. If a negative answer is given, '.
'then the user is redirected to the URL defined below.')
);
$form['compliance_ack']['compliance_message'] = array(
'#type' => 'textfield',
'#title' => t('Message'),
'#default_value' => variable_get('compliance_message',
t('Please read the following and answer '.
'the question at the bottom of the page. '
'Thank you.')),
'#description' => t('This is the text of a message that will appear at '.
'the top of the compliance content intended to '.
'instruct the user to read the page and answer '.
'the question defined below.')
);
...
}
|
The compliance page
As shown in Listing 19, the URL /compliance is registered with the Drupal menu system to trigger the compliance_display function. Listing 21 shows this function. We want to use the page node created to display the compliance information, but control it from the compliance module, so we can use functions like the view hook to add content such as the acknowledgment form.
Listing 21. Controlling display of compliance page in compliance.module
function compliance_display() {
$nid = variable_get('compliance_page_node', 0);
if ($nid == 0 || !is_numeric($nid)) {
drupal_not_found();
}
$node = node_load($nid);
if ($node->nid) {
$title = check_plain(variable_get('compliance_page_title', t('Terms and conditions')));
drupal_set_title($title);
$node->title = $title;
$node->type = 'compliance';
return node_show($node, 'view');
}
}
|
The first half of this function uses the node ID to fetch the node object of the page node using the node_load function. The second half overrides the page and node title using the stored title text, changes the node type from page to compliance, then asks Drupal to show the node using the node_show function.
Switching the node type value gives the compliance module control of what was a page node. Listing 22 shows how you can use the view hook to add more content.
Listing 22. Adding content to the existing compliance page in compliance.module
function compliance_view(&$node) {
$form = array();
$form['compliance_ack_question'] = array(
'#type' => 'markup',
'#value' => '<p>'.variable_get('compliance_question',
t('Do you agree to these term and conditions?')).'</p>'
);
$form['compliance_ack_disagree'] = array(
'#type' => 'submit',
'#value' => variable_get('compliance_negative_answer', t('Disagree'))
);
$form['compliance_ack_agree'] = array(
'#type' => 'submit',
'#value' => variable_get('compliance_positive_answer', t('Agree'))
);
$node->body .= drupal_get_form('compliance_ack', $form);
drupal_set_message(variable_get('compliance_message',
t('Please read the following and answer the '.
'question at the bottom of the page. '.
'Thank you.')));
return $node;
}
|
The form to prompt the user to acknowledge the compliance information is built using the Form API. This is themed into XHTML using the drupal_get_form function. This function uses the form ID, compliance_ack, to set up possible callbacks for processing the form validation and submission (described in the next section). This XHTML form is then appended to the body of the existing page, now compliance, node content. Then the stored message is added to the Drupal message queue using the drupal_set_message function.
Processing the acknowledgment form
The use of the drupal_get_form function with the form ID, compliance_ack, tells Drupal to look for the submit and validate hook functions connected to this form ID. Given the form ID, the hook functions are called, compliance_ack_submit and compliance_ack_validate respectively. The compliance module only uses the submit hook shown in Listing 23.
Listing 23. Processing acknowledgment form in compliance.module
function compliance_ack_submit($form_id, $form_values) {
switch($_POST["op"]) {
case variable_get('compliance_positive_answer', t('Agree')):
$url = _compliance_get_destination();
_compliance_set_accept();
drupal_goto($url);
break;
case variable_get('compliance_negative_answer', t('Disagree')):
drupal_goto(variable_get('compliance_disagree_url', '/logout'));
break;
}
}
|
A switch statement is used to test the $op variable passed back in the $_POST array after the form has been submitted. If the positive response is given by the user, the URL of the original request, explained in the next section, is retrieved by using the _compliance_get_destination function shown in Listing 24. The record for the user in the users_compliance table is updated with the accept and expiration dates using the _compliance_set_accept function shown in Listing 25. Users are redirected to the location of the original request. If a negative response is given, the user is redirected to the stored URL used to place users at a specific location after a negative response.
The _compliance_get_destination function, shown in Listing 24, simply retrieves the URL information previously stored for the user at login. If no URL is found, then an empty string is returned. Passing an empty string to the drupal_goto function redirects the user to the front page.
Listing 24. Fetching stored URL in compliance.module
function _compliance_get_destination($uid = NULL) {
global $user;
if ($uid == NULL) {
$uid = $user->uid;
}
$url = '';
$q = db_query('SELECT url FROM {users_compliance} WHERE uid = %d', $uid);
if (db_num_rows($q) > 0) {
$r = db_fetch_object($q);
$url = $r->url;
}
return $url;
}
|
The _compliance_set_accept function updates or creates a record in the user_compliance table with the times of acceptance and expiration. Expiration time is only used when the compliance review cycle is enabled.
Listing 25. Storing compliance acceptance in compliance.module
function _compliance_set_accept($uid = NULL) {
global $user;
if ($uid == NULL) {
$uid = $user->uid;
}
$now = time();
$later = $now + (variable_get('compliance_review_cycle_time',365) * 60 * 60 * 24 );
db_query("DELETE FROM {users_compliance} WHERE uid = %d", $user->uid);
db_query("INSERT INTO {users_compliance} SET uid = %d, accept_date = %d, ".
"expiration_date = %d, url=''", $user->uid, $now, $later);
watchdog('user', 'Compliance has been acknowledged', WATCHDOG_NOTICE);
}
|
Triggering the display of the compliance page
The compliance page should be displayed just as the user authenticates or logs into the Web site. The compliance module asks the user to acknowledge the compliance information before doing anything else. Using the login operation in the user hook, shown in Listing 26, enables us to ensure this happens.
Listing 26. Triggering display of compliance page after login in compliance.module
function compliance_user($op, &$edit, &$account, $category = NULL) {
if ($account->uid < 2) {
return; // UID 0 or UID 1 not applicable
}
switch ($op) {
case 'login':
if (_compliance_expired()) {
_compliance_set_destination();
drupal_goto('compliance');
}
break;
}
}
|
Since the first user of a Drupal system is deemed special, as the root user, we make sure not to apply the compliance process to it. Using a switch statement to test for the login value in the operation variable $op, we check if the user has given any previous positive response to the compliance information, or if they need to view and acknowledge the compliance information again using the _compliance_expired function shown in Listing 27.
If the user is required to view the compliance page, their current URL is stored, using the _compliance_set_destination function, so that they can be redirected to this location after a positive acknowledgement of the compliance information. Finally, the user is redirected to the compliance page.
The _compliance_expired function is shown in Listing 27. The expiration date field is pulled from the user's record in the users_compliance table. If there is no record found or the expiration date field is zero, the user needs to view the compliance page. If the compliance review cycle is enabled and the expiration date value is in the past, then the user needs to view the compliance page.
Listing 27. Checking if compliance page should be displayed in compliance.module
function _compliance_expired($uid = NULL) {
global $user;
if ($uid == NULL) {
$uid = $user->uid;
}
$q = db_query('SELECT expiration_date FROM {users_compliance} '.
'WHERE uid = %d', $user->uid);
if (db_num_rows($q) > 0) {
$r = db_fetch_object($q);
if (((time() > (int)$r->expiration_date)) &&
(variable_get('compliance_review_enabled', 0) == 1)) {
// expired – need to ack again
return true;
}
elseif ((int)$r-> expiration_date == 0) {
// record exists but no previous ack
return true;
}
}
else {
// no previous ack
return true;
}
return false;
}
|
The _compliance_set_destination function, shown in Listing 28, creates or updates the user's record in the users_compliance table, making sure that the URL field is set to the current URL.
Listing 28. Storing the user destination URL in the user_compliance table in compliance.module
function _compliance_set_destination($uid = NULL) {
global $user;
if ($uid == NULL) {
$uid = $user->uid;
}
$q = db_query('SELECT * FROM {users_compliance} WHERE uid = %d', $uid);
$url = check_url(url($_GET["q"]));
if (db_num_rows($q) > 0) {
$q = db_query('UPDATE {users_compliance} SET url = "%s" WHERE '.
'uid = %d', $url ,$uid);
}
else {
$q = db_query('INSERT INTO {users_compliance} '.
'(uid, accept_date, expiration_date, url) '.
'VALUES (%d, %d, %d, "%s")', $uid, 0, 0, $url);
}
return $q;
}
|
The URL is retrieved using the url function on the $_GET["q"] value, and passed to the check_url function. The check_url call is essential to prevent any malicious use caused by cross-site scripting techniques.
The compliance audit page
The audit page, shown in Figure 3, is displayed using the URL /compliance/audit. This URL triggers a call to the compliance_audit function shown in Listing 29. This page basically provides a themed view of the user_compliance table. By default, the themed output highlights users who have not positively acknowledged the compliance information or need to review the compliance information again.
Listing 29. Creating audit view in compliance.module
function compliance_audit() {
$q = db_query('SELECT u.uid,u.name,uc.accept_date,uc.expiration_date '.
'FROM users_compliance uc RIGHT OUTER JOIN users u USING '.
'(uid) ORDER BY u.name ASC');
$acknowledments = array();
while ($r = db_fetch_object($q)) {
$acknowledments[] = $r;
}
return theme('compliance_audit', $acknowledments);
}
|
The users_compliance table data is extracted row by row into an array, and then passed to the theme function to create the XHTML content used in the body of the compliance audit page. The theme function hook, compliance_audit, is also passed to this function. Using the theme function selection process described in Part 7, Drupal looks for an appropriate theme function to create the XHTML.
A default theme function for the audit page, called theme_compliance_audit, is provided in the compliance.module file, and is shown in Listing 30.
Listing 30. Default theme for audit page in compliance.module
function theme_compliance_audit($ack) {
$header = array(t('User name'), t('Accept date'), t('Expiration date'),t('Status'));
$rows = array();
foreach($ack as $row) {
if ($row->uid < 2) {
continue;
}
$class = 'compliance_okay';
$status = 'Okay';
$accept = ' - ';
$expiration = ' - ';
if ((time() > $row->expiration_date) &&
variable_get('compliance_review_enabled',0) == 1) {
$class = 'compliance_expired';
$status = 'Expired';
}
if (!isset($row->expiration_date)) {
$class = 'compliance_unaccounted';
$status = 'Unaccounted';
}
$name = l($row->name,'user/'.$row->uid);
if (isset($row->accept_date)) {
$accept = format_date($row->accept_date,'custom','l, F j, Y');
}
if (isset($row->expiration_date)) {
$expiration = format_date($row->expiration_date,'custom','l, F j, Y');
}
$rows[] = array('data'=>
array($name,$accept,$expiration,$status),'class'=>$class);
}
$output = '<div id="compliance-audit">';
$output .= '<h2>Compliance audit</h2>';
if (variable_get('compliance_review_enabled',0) == 1) {
$output .= '<p>A review cycle of ' .
format_plural(variable_get('compliance_review_cycle_time',365),
'a day','%count days') . ' has been enabled.</p>';
}
$output .= theme('table', $header, $rows);
$output .= '</div>';
return $output;
}
|
Here, each row of data from the database is added to a $rows array, which is then themed as a table. Any links are created using the l function. You will notice that classes are added to the rows array to help identify the different states of user compliance. These styles are defined in the compliance.css file added in the menu hook described earlier.
Of course, this is just the default theme function and can be overridden with another theme function to create more refined audit visualizations.
Summary
In this article you learned about the requirements for the IBC Web site and how those requirements led to an extranet site. To implement these requirements, you learned how to create a login page that keeps unauthenticated users from accessing content within the Web site, the adaptation of the Automated Logout module to enable a client-side logout timer and alert, and functions of a new module to provide a compliance mechanism as required by the Web site usage policy.
Stay tuned for the next article in this series, where you'll get a look at Drupal's taxonomy system and learn how to use it for the organization and navigation of content.
Acknowledgements
The authors wish to thank Moshe Weitzman, Károly Négyesi (a.k.a. "chx"), Boris Mann, and Jeff Robbins for their insight and suggestions.
Resources Learn
Get products and technologies
- Build your
next development project with IBM trial software, available for download directly from
developerWorks.
Discuss
About the authors  | 
|  | Alister Lewis-Bowen is a senior software engineer in IBM's Internet Technology Group. He has worked on Internet and
Web technologies as an IBM UK employee since 1993. Alister was brought to the U.S. to work on the Web sites for the
IBM-sponsored sports events, then as senior Webmaster for ibm.com. He is currently helping create semantic Web prototypes. Contact Alister at alister@us.ibm.com. |
 | 
|  | Stephen Evanchik is a software engineer in IBM's Internet Technology Group.
He has been a contributor to many open source software projects, the most notable being his IBM TrackPoint driver in the
Linux kernel. Stephen is currently working with emerging semantic Web technologies. Contact Stephen at evanchik@us.ibm.com. |
 | 
|  | Louis Weitzman is a senior software engineer in IBM's Internet Technology Group. For 30 years
he has worked at the intersection of design and computation. He helped develop an
XML, fragment-based content management system in use by ibm.com, and currently is involved with
bringing the design process to emerging projects. Contact Louis at louisw@us.ibm.com. |
Rate this page
|  |