Chapter 4. Configuring for Chrome and a Server
In this chapter, we will move our application from its home in a test directory to a setting that is more consistent with commercial implementations.
We will create two key pieces for implementing our application:
A local XUL application communicating with a Personal Hypertext Processor (PHP) server
A remote XUL application being served by PHP scripts
Both implementations will use an SQL database to hold user and password information to conduct the authentication process. Figure 4-1 shows a block diagram illustrating the relationship of the main elements of our design.
Chrome Overview
This book uses the term XUL application to describe our NewsSearch project. This differs from conventional web applications that are designed to be served web pages that are rendered by web browsers. A XUL application, however, can be implemented as a page served to a XUL-capable browser, or it can be configured to run as an application local to the user’s machine. In the case of the latter, the files that comprise the application (JavaScript source, stylesheets, datafiles) are installed in a chrome directory and run as a chrome URL.
To date, the most popular XUL applications, such as the Firefox browser, Thunderbird mailer, and Sunbird calendar, are implemented as bundled applications running from the user’s chrome directory.
Web developers refer to traditional web applications as being subject to the rules of a security sandbox. Originating from the Java language’s security policy, the sandbox philosophy limits the reach of executable code to a certain area. For browsers, this means that unprivileged JavaScript can access data from a served document and from documents sharing its URL, but the browser infrastructure allows no access to the local filesystem or to potentially destructive operating system and network services.
There are cases when the local browser needs to store information on a user’s computer. This information may include bookmarks, runtime preferences, or other saved settings that would impair the user experience if not kept locally accessible; the sandbox is therefore expanded to include a controlled portion of the local filesystem. Applications that are registered within Firefox’s chrome are granted an area on the local disk to which full read/write access is granted.
In addition to meeting the security requirements imposed by the Firefox framework, a chrome application has a different look to it.
Before launching our code as a chrome application, we should change the testStyles.css stylesheet to provide a default background color for our window:
window {
background-color:white;
}
(The default window color is needed when launching a chrome application from the command line.)
To see what our test application would look like as a chrome application, open a command window and launch Firefox from the command line, specifying the -chrome
option along with the pathname to the source file. The -chrome
option directs the framework to display the source as a chrome window, not as a browser window.
On an OS X machine, the code would look like this:
theUser% pwd theUser% /Applications/Firefox.app/Contents/MacOS theUser% ./firefox -chrome "file:Macintosh HD:tests:theWindowWithLogin.xul"
Our NewsSearch application now appears as a chrome window with none of the controls we associate with a browser (see Figure 4-2).
Simply launching an application with the -chrome
option and a file URL will give us a chrome appearance, but to run it as a client/server chrome application, we will need to install the application as a chrome package, or serve the interface as a XUL page.
Running as a Local Installation
We will first look at what we need to install the application on the local machine and to communicate with a server through a form of HTTP request. For both versions, we must have an Apache web server running, PHP installed, and an SQL database up and running.
Chrome Directory Structure
An application registered as a chrome package is not required to be located in a specific directory; convention, however, has placed most chrome applications within Firefox’s chrome directory. The chrome directory is located in the same directory as the binary executable for Firefox. Most of the files in the chrome directory are .jar files, or Java archive files.
Tip
Although XUL applications use the Java archive as a preferred distribution medium, the source code files are not written in Java. XUL applications are developed as JavaScript and XUL source files.
To see the content of any of the archives, use the jar -tf
command. For example, to view the contents of the inspector.jar file, change to the chrome directory and type the following:
jar -tf inspector.jar
If we were to do the same for all the archive files, we would see that they share a common directory structure:
For our application, we will work with only the content
root.
Before we continue installing our application in the Firefox chrome directory, we need to understand that such an application is referred to through a special chrome URL. The form of this URL is:
chrome://<package
>/<part
>/<fileName>
The package
and part
names are consistent with conventions used for .jar files. This URL instructs the Firefox framework to scan its installed packages to access a filename located as part of a specific package. Once our application is registered, we would use a command line with the -chrome
option (to open the file in a chrome window) or specify a chrome URL (to launch the application with chrome privileges):
firefox -chrome chrome://<package
>/<part
>/<filename>
Figure 4-3 illustrates an example directory structure.
Package Registration
Every time the Firefox browser launches, its framework looks for any file with an extension of .manifest to inform it of the chrome content packages, skins, or locales that need to be loaded. To inform Firefox of our NewsSearch application, we will use a text editor to create a line for a file named localApps.manifest (any unique filename with a .manifest extension will do) and save it in Firefox’s chrome directory:
content newssearch NewsSearch/content/newssearch.xul
The format of this line consists of space-delimited entries as follows:
The first field,
content
, declares the installation to be a content package. Other options includeskin
andlocale
.The
newssearch
entry names the package.The last entry is the source file location relative to the chrome directory.
We can now launch Firefox and type the chrome URL directly in the locator window to launch the same application, but within a browser window (see Figure 4-4).
Once we make this entry in the URL bar, Firefox will autocomplete the line to add the filename that matches the package name—in this case, our newssearch.xul file.
Alternatively, we could launch our application directly from the command line, using the -chrome
option and a chrome URL that specifies our package name and package type:
firefox -chrome chrome://newssearch/content/
Figure 4-5 illustrates the results of launching our chrome application from the command line and entering correct login information, for both a Windows and an OS X implementation of the same code.
XUL-to-Server Communications
A user can communicate to a server using several models, including a XUL application that runs locally while obtaining needed server information through asynchronous HTTP requests. Using this approach, the local application is in complete control of the user interface, with the server providing only textual information in response to requests. Here are the steps for implementing this model:
Configuring the Server
We first must install a web server on our local machine; for this application we install Apache. Once the web server is installed and running, we should see the startup page shown in Figure 4-6 when we enter http://localhost/
in a browser’s URL field.
Apache is managed through the text file httpd.conf. On Unix systems, this is located in the /etc/http/ directory, whereas Windows systems will put this file in Apache’s conf directory. You can edit the file using a text editor.
The DocumentRoot directory is used to select the home directory for serving web pages. That entry, in httpd.conf, is operating-system-specific, and depends on the options we have specified during the installation:
DocumentRoot "<YourApacheDocumentRoot
>"
If the installation was performed correctly, an entry should provide Apache with information about the directories served:
<Directory "YourApacheDocumentRoot
">
Although we don’t need to make any changes to the httpd.conf file, we must note what these entries are in order to place our Common Gateway Interface (CGI) scripts in the correct directory. We will use the PHP scripting engine to provide the logic for our CGI.
We can install the PHP binaries into any directory. In a Windows environment, an example would be C:\php\, whereas a Unix install may be in /usr/local/php/. Regardless, we must make changes to Apache’s httpd.conf to properly serve PHP-generated output.
Depending on the version of PHP installed, entries in Apache’s configuration file will take one of two forms. In the first:
LoadModule phpn
_module " <YourPHPInstallDirectory
> "
n
may be the version of PHP being installed.
The second form would instruct Apache to use an external configuration file to load the PHP libraries:
Include <YourPHPInstallDirectory>/httpd.conf.php
Regardless of the particular PHP version and operating system, we can check the installation by using a text editor to write a simple PHP script to echo version information:
<?php phpinfo( ); ?>
Save the preceding text as test.php in your Apache document root directory:
<YourApacheDocumentRoot>
/test.php
Now type the URL into a browser to yield a page that looks like Figure 4-7.
With a PHP engine running on our server, we can now conduct some simple connection tests between our application and the server.
The Client/Server Protocol
This approach will use the XMLHttpRequest
object to send inquiries to our PHP server, and PHP scripts to return textual responses to the client.
Conventional POST
messages from a browser to a server result in the server generating an entire page to be displayed by the browser. This process is what causes many web applications to “freeze up” after the user presses a form submission button and waits for a response.
We will first use a simpler protocol to issue a request for data resembling “classic” client/server operations. The server provides only data; the client determines what to do with the data and how to render the interface.
We can write a JavaScript function to receive the server data and format HTML table data with responses, populate text areas, or otherwise modify the GUI without forcing a complete redraw of the page. For many applications that require only a partial update of the screen, this asynchronous approach reduces the waste incurred by conventional page-based updates.
The client-side request
Firefox supports the XMLHttpRequest
, which is generally modeled after Microsoft’s IXMLRequest
design. We can use the request to retrieve either simple text strings, or XML fragments that may be parsed and passed to the interface document’s Document Object Model (DOM) structure.
To use the request, JavaScript scripts create an instance of an XMLHttpRequest
object to be sent to the server, passing the object a reference to a callback function to be invoked once the request is completed. The callback function then takes whatever action is determined by the response.
Table 4-1 summarizes the functions and properties of the XMLHttpRequest
object most relevant to us for the upcoming exercise.
Description | |
|
|
|
Function called when the request’s “ready” state changes. |
|
Sends the request to the server. |
|
Monitored within the callback; |
|
Monitored within the callback; |
|
Accesses the text returned by the server to be parsed and processed in an application-specific format. |
|
Accesses the text returned by the server when the text is presumed to consist of an XML document. |
Before we start to code, we need to decide on a send/receive protocol between our client application and the server. We will use a very simple URL string to send commands to our server:
command=someCommand¶m1=val1¶m2=val2...
From our server, we will expect a comma-delimited string for a response:
RetCode=ReturnCode,retKey1=retVal1...
We will expect the first value token to be a case-insensitive true|false
value to indicate the success of the command.
We will now change our newssearch.js file to implement a few modifications for our client/server model:
We will build a generic
doServerRequest
function to build anXMLHttpRequest
object and send it to the server. To support that function, we will also create acommandArg
object to hold name-value pairs of arguments that can be used to populate an array of arguments. The function will set theasync
flag in theopen
function toTRUE
, meaning that control will immediately return to the application once the request is sent.We will change our login-button command handler to call the newly created
doServerRequest
function.We will change the logic to move successful and failed paths for login into separate functions.
We will write some JavaScript to add a callback function to interpret the response from the server and call the successful or failed login functions:
function genericBtnHandler(event) { try { // try block var infoString = "Type = " + event.type + ","; infoString += "Target = " + event.target + ","; infoString += "Target.tagName = " + event.target.tagName + ","; infoString += "Target.id = " + event.target.id + "."; dump(infoString + "\n"); } // try block catch (e) { alert("genericBtnHandler exception: " + e); } } function doLogin(event) { try { // try var theArgs = new Array; theArgs[0] = new commandArg("un",document.getElementById("userName").value); theArgs[1] = new commandArg("pd",document.getElementById("password").value); doServerRequest("login",theArgs); } // try catch (e) { // alert("doLogin exception: " + e); }// } // // Dynamically assign our event handler properties // function initialize( ) { try { document.getElementById("B1").addEventListener("command",genericBtnHandler,true); document.getElementById("B2").addEventListener("command",genericBtnHandler,true); document.getElementById("B3").addEventListener("command",genericBtnHandler,true); // // Add a login script document.getElementById("loginButton").addEventListener("command",doLogin,true); } catch (e) { alert ("Exception: " + e); } }
function loginOK( )
{ var theParent = document.getElementById("contentArea"); while(theParent.childNodes.length != 0) theParent.removeChild(theParent.childNodes[0]); // Now re-create a welcome area theParent.style.backgroundColor = "LightSeaGreen"; theParent.style.borderColor = "gray"; theParent.style.borderStyle = "ridge"; var leftSpacer = document.createElement("spacer"); leftSpacer.setAttribute("flex","1"); theParent.appendChild(leftSpacer); var newDescription = document.createElement("description"); var newDescriptionText = document.createTextNode("Welcome!"); newDescription.appendChild(newDescriptionText); theParent.appendChild(newDescription); var rightSpacer = document.createElement("spacer"); rightSpacer.setAttribute("flex","1"); theParent.appendChild(rightSpacer); } function commandArg(argKey,argValue) { this.key = argKey; this.value = argValue; }function loginFail( )
{ document.getElementById("msgDescription").style.backgroundColor="red"; document.getElementById("msgDescription").style.color="white"; document.getElementById("msgDescription").childNodes[0].nodeValue = "User not authenticated."; document.getElementById("userName").value = ""; document.getElementById("password").value = ""; } // // CreateServerRequest // var theServerRequest; // // commandArgs is an array of arguments, each element // is converted into a PHP GET URL field function doServerRequest(commandString,commandArgs) { theServerRequest = new XMLHttpRequest( ); var theString ="http://localhost/doCommand.php?"+"&command="+commandString+"&"; for (var i = 0; i < commandArgs.length; i++) { // build remaining parameters theString += commandArgs[i].key + "=" + commandArgs[i].value ; if (i != (commandArgs.length-1)) theString += "&"; } // build remaining parameters theServerRequest.onreadystatechange = retrieveServerResponse; theServerRequest.open("GET",theString,true); theServerRequest.send(null); } function retrieveServerResponse( ) { if (theServerRequest.readyState == 4) { // all done // Check return code if (theServerRequest.status == 200) { // request terminated OK alert("Response is " + theServerRequest.responseText); if (theServerRequest.responseText == "retcode=true") { // all OK loginOK( ); } // all OK else loginFail( ); } // request terminated OK else { // something is wrong alert("Response failed."); } // something is wrong } // all done }
We wrote the doServerRequest
function to take an array of input arguments. The code then steps through the array to build a URL containing our complete GET
request URL.
The request object calls the retrieveServerResponse
function each time the state changes, and once it reaches "4"
(a code that means the server has completed the request), it checks for a status of 200
(meaning the server successfully completed the request) and finally parses the text returned by the server to see whether the user is authenticated.
The server-side response
PHP scripts result in text being returned to a browser in one of two ways:
The parameters sent to a script from a browser’s GET
request arrive as values in an associative array named $_GET
. To assign a PHP variable (a token that begins with $
) a parameter from a GET
request, we would write:
$myValue = $_GET['keyString']
For now, our command processor script will presume only valid commands, and compare the command against our temporarily hardcoded strings to return a true
or false
response to the client. We can use any text editor to create the doCommand.php file:
<?php $cmd = trim($_GET['command']); $uName = trim($_GET['un']); $uPass = trim($_GET['pd']); echo check_user($uName,$uPass); function check_user($name,$pass) { if (($name == 'XULuser') && ($pass == 'XULpass')) return 'retcode=true'; else return 'retcode=false'; } ?>
Save this file in the Apache document root directory.
The code illustrates assignment of the GET
parameters to global variables. The trim
function takes care of any leading or trailing spaces that may have been entered. The script then passes the username and password to a comparison function that returns true
or false
, which is echoed back to the client.
Repeating the same launch of our newssearch
chrome package will yield the same results with our client-only code, but we are now operating in client/server mode.
When Things Go Wrong
If things don’t work out, we can take a few steps to quickly isolate the cause of the problem. We can use either JavaScript alert
or dump
commands to see what we are sending and what we are retrieving. We can also use simplified PHP scripts to return a known value to make certain the script is doing what we think.
Assuming we want to dump
information to the screen, we could make the following changes to the doServerRequest
function in our newssearch.js code:
theServerRequest.onreadystatechange = retrieveServerResponse;
theServerRequest.open("GET",theString,true);
dump("About to send " + theString + "\n");
theServerRequest.send(null);
And we could report the entire string returned in our retrieveServerResponse
function:
if (theServerRequest.status == 200) { // request terminated OK
dump("Received from server: " + theServerRequest.responseText + "\n");
if (theServerRequest.responseText == "true") { // all OK
We can also stub out our login check in doCommand.php just to return the parameters we received from the client:
/* echo check_user($uName,$uPass); */
echo 'Script has received: '."$cmd".','."$uName".','."$uPass";
We have removed the call to check_user
in favor of a change to echo a string that concatenates (using the .
operator in PHP) a simple message with the values for command, username, and password.
When we launch Firefox from the command line with the -console
option, we will see our sent and retrieved strings displayed on the console:
About to send http://localhost/doCommand.php?&command= login&un=SomeUser&pd=SomePass Received from server: Script has received: login,SomeUser,SomePass
For developers just beginning to work with PHP, including echo
statements to use the browser to display variable values is often the best way to identify many of the most common PHP coding errors. Displaying variable values is one sure way to quickly identify problems related to misuse of the single quote (used to form strings with special characters), double quote (used to assemble strings that include the evaluation of variables), and terminating semicolons. Echoing variables is also a good tool to find misspelled variable names and the occasionally misplaced $
variable prefix.
We change our command parser to remove the debug echo
statement and uncomment the call to check_user
that will now be used with a database of valid user identifiers.
Adding a Database
Using hardcoded information in a script file may be appropriate for some limited applications, but the designer can improve the reliability of server code by allowing access to a dynamic information store without having to change any script files. Such a store can be maintained and updated to reflect the changing nature of business operations without risking any side effects caused by changes in server or client code.
Tip
Using a relational database is not a required element for this book’s sample application. If database use is inappropriate for some reason, we could write PHP “stub code” to mimic our database. The use of MySQL is included here as an example of an implementation that models true commercial applications.
The use of a relational database is the most commonly accepted technique to provide a secure and maintainable information store that server scripts can access in response to a client request. We will use the open source MySQL database engine to provide the store for our username and password information. The steps for this task are as follows:
Create the NewsSearch database.
Create a database user account to act as administrator.
Create the tables for our account name and password.
Create a database user account to act as a “guest” with read-only access to database information.
Configure PHP to use libraries to access the database.
Write the PHP scripts to communicate with the database in response to a client request.
Creating the database
Once we have installed MySQL, we create the database for our project.
Upon initial installation, MySQL has a root
account that we can use to create a new table (refer to the glossary to set up a root password if you have not already done so). We log into the database with the following command:
mysql -u root -p
You will now be prompted for the root password. When you’re successfully logged in, you will see a welcome message that looks like this:
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 to server version: Version specific information mysql>
We create a database with this command:
mysql> create database newssearch; Query OK, 1 row affected (0.59 sec)
Creating account tables
Rather than having the PHP script compare usernames and passwords that are kept in the script, we can write code to access the database and compare its entries with the fields entered in the XUL interface.
Databases are organized as tables of information rows. Users can access row information through SQL SELECT
statements that include qualifiers to help identify the information being sought. We can embed the SELECT
statement in our PHP scripts that support the SQL access libraries.
The database administrator creates the database table, specifies its structure, and assigns privileges to restrict a user account’s ability to access and modify table data.
For our simple application, we will create a table named accounts
to hold the username and password information. We will also add information to indicate a user’s status (e.g., active or inactive), and the time and date of the last login. The initial assumptions of the format for the row data are:
- Username
Up to 40 characters (arbitrary)
- Password
Up to 40 characters
- Status
A string of up to 16 characters to support entries such as active, suspended, provisional (for temporary accounts), and terminated
- Date
A string that describes the date and time of the last successful login session
The SQL statement to create such a table consists of a CREATE
statement:
mysql> create table account (
-> username char(40) not null primary key,
-> password char(40) not null,
-> last_session datetime,
-> status char(32) not null);
Query OK, 0 rows affected (0.47 sec)
This statement creates our tables with rows named username
, password
, last_session
, and status
. The data types include character strings and a timestamp (an SQL-formatted statement for time of day). Additional qualifiers allow us to specify that the username will be unique (as a primary key), and which fields must have values specified when a table row is created.
We can see the results of our work through the describe
command:
mysql> describe account;
+--------------+---+-----+-------------------+----------+
| Field | Type | Null | Key | Default | Extra
+--------------+---------+-------------------+----------+
| username | char(40) | | PRI | |
| password | char(40) | | | |
|last_session| datetime | YES | |CURRENT_TIMESTAMP|
| status | char(32) | | | |
+--------------+-----------+------+-----+-----------------
4 rows in set (0.00 sec)
mysql>
We can now create the accounts for the application users—individuals who will be using our NewsSearch service.
Rather than storing the passwords for our users in plain text, we will add some security by using MySQL’s one-way encryption.
A one-way encryption scheme applies a function to input text to render an encrypted form of it in the database. The programmer or application scripts do not need to know what the encrypted output is, only that the encryption function is used consistently for comparison purposes. Should the database be compromised, there is no way for the intruder to reverse the encrypted data into the user-provided passwords.
A number of encryption functions are available for both PHP and MySQL. For this application, we will use MySQL’s function for the Secure Hash Algorithm (SHA): sha1('
someEntry
')
. We can now create a couple of application users who will be in our authentication database.
Although we could use the MySQL command-line interpreter to do this, it is often easier to create a text file with the commands we need to manipulate the database. A createUser.sql file to create two users would look like this:
use newssearch; insert into account values ('bugsbunny',sha1('wabbit'),'','active'); insert into account values ('elmerfudd',sha1('scwewy'),'','active');
We could read in this script file directly from the operating system command prompt:
%mysql -u root -p < createUsers.sql; >password: 'sqlRootPassword' %
We could also use the source command (\.
) to enter the script name while within the MySQL interpreter. The following command shows how to load the script from the current working directory:
mysql> \. createUsers.sql
We can view the results of our script files by issuing a SELECT
command:
mysql> select * from account;
The results will show the account information created by the script file, along with the encrypted passwords. The last_session
entries will be initialized to 0, as no entry was entered in the INSERT
statement.
Creating database user accounts
When the server software requests information from a database, the software will have to make its request through a MySQL account. We should set this account so that it has only the minimum privileges necessary for the task at hand. That means we limit a user’s read and write authorization to specific tables in the database.
The commands to manage account privileges involve specifying the account or user name, setting a boundary to a limited set of objects that are affected, and setting the privilege itself that describes what degree of access and modification is granted. The minimal, general form of a command to assign a privilege is as follows:
GRANT priv_type [(column_list)] [, priv_type [(column_list)]] ... ON [object_type] {tbl_name | * | *.* | db_name.*} TO user [IDENTIFIED BY [PASSWORD] 'password'] [, user [IDENTIFIED BY [PASSWORD] 'password']] ... [REQUIRE NONE | [{SSL| X509}] [CIPHER 'cipher' [AND]] [ISSUER 'issuer' [AND]] [SUBJECT 'subject']] [WITH with_option [with_option] ...] object_type = TABLE | FUNCTION | PROCEDURE
The priv_type
parameter is one of a fairly substantial number of tokens that define the privilege being granted. The most familiar of these types includes SELECT
(to select information from tables), INSERT
(to insert new rows into tables), UPDATE
(to modify table entries), and DELETE
(to remove rows from a table).
The object_type
parameter sets a boundary on the privilege being granted. The object type can range from *
(all tables on all databases), to an entry of the form databaseName.tableName
to further qualify the objects to which the GRANT
statement applies.
To create a newssearch_guest
account that can read any data from the table, you could specify the following script file to create the account:
use newssearch; grant select on newssearch.account to newssearch_guest identified by 'nsgst'; grant update (last_session) on account to newssearch_guest;
Reading this script file (or typing it into the MySQL interpreter) will create a database user account, newssearch_guest
, that can SELECT
data only from the accounts
table. The second statement adds UPDATE
privileges to the database account to allow scripts to update the session information in the database. We now have enough information to turn our attention to the PHP script that accesses the database.
Connecting PHP to MySQL
To configure PHP to use the MySQL programming interface, we must modify PHP’s configuration file to load the MySQL libraries. The configuration file, php.ini, is located in the user’s PHP installation directory.
The specific switches to set depend on the versions of MySQL and PHP in use (check the most recent content of the MySQL and PHP web sites for specific entries). We will be using the MySQL Improved extension from our PHP scripts. The settings to enable the MySQL Improved extension are:
mysqli.max_links "-1" mysqli.default_port "3306" mysqli.default_socket NULL mysqli.default_host NULL mysqli.default_user NULL mysqli.default_pw NULL
Once we have the correct settings, we can verify that Apache, PHP, and MySQL are up and running by using Firefox to open http://localhost/test.php. Scrolling down the window, we see the entries confirming a successful configuration (see Figure 4-8).
Calling the MySQLi API
We will use functions for the MySQLi library to compare the user input login and password against the entries in the accounts
table.
Using the object-oriented approach to the MySQLi library, we create a database connection object and use that object reference to execute an SQL SELECT
statement against the database.
The results of this statement are contained in a variable pointing to a result object. The result object will contain a collection of all the database rows that were selected. Table 4-2 shows a summary of the mysqli
PHP calls that we will be using.
Use | |
|
Creates a database object reference by connecting to the database with the username and password specified. The database is identified by |
|
If the database object creation fails, these functions are used to audit an error code, and to extract the |
|
Executes an SQL query, returning the result in a |
|
Returns the number of rows selected from the database as a result of the query. |
|
Fetches the next row from the result object, returning the results as an associative array in which the keys to the array match the names of the row’s columns (e.g., to return the contents of a row’s “status” column, the PHP script would read |
|
Search results must be closed before attempting any additional queries. |
|
Scripts must close the database prior to exiting. |
We will rewrite the PHP scripts to use the database to select the rows for the username and password supplied by the interface. If there is a match (if one row is returned), we will return the proper flag, along with the last time the user logged in.
On the interface, if the user is authenticated, we will display the last login time in the application’s status bar.
The PHP doCommand.php file now has a checkUser
function that looks like this:
function check_user($name,$pass) { $database = new mysqli('localhost','newssearch_guest','nsgst','newssearch'); if (mysqli_connect_errno( )) { // failing case $retString = 'retcode=false,message='.mysqli_connect_error( ); return $retString; } // failing case $encryptPass = sha1($pass); $query = "select status,last_session from account where username = '$name' and password = '$encryptPass'"; if ($theResult = $database->query("$query")) { // we have some kind of result if ($theResult->num_rows == 1) { // we have our user $theRow = $theResult->fetch_assoc( ); // get the only row that exists $lastLogin = $theRow["last_session"]; if ($theRow['status'] == 'active') { // all OK $retString='retcode=true,last_login='.$theRow['last_session']; // update the session info $theResult->close( ); $curTime = date('c'); $update = "update account set last_session = '$curTime' where username = '$name'"; $theResult = $database->query("$update"); } // account is active else { // account not active $theResult->close( ); $retString = 'retcode=false,message=user account not active'; } // account not active } // we have our user else { // user not found $theResult->close( ); $retString = 'retcode=false,message=user not found'; } // user not cound } // we have some kind of result else { // no result returned $retString = 'retcode=false,message=invalid query'; } // no results returned $database->close( ); return $retString; }
Before building the command string, we see the call to encrypt the password prior to its comparison with the database. Our query statement returns columns for last_session
and status
. If one row is returned, there are statements to verify that the user has an “active” account before building a successful return code. We also call an UPDATE
command on the database to set the last_session
entry to the current date and time.
We can change the XUL interface slightly to add a horizontal box with a status area to show the time of the user’s last login. The file newssearch.xul now looks like this:
<?xml version="1.0"?> <?xml-stylesheet href="testStyles.css" type="text/css"?> <window id="theMainWindow" onload="initialize( );" title="Test Window" orient="horizontal" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><script src="newssearch.js"/> <!-- main top level container --> <vbox flex="1">
<hbox flex="1" > <!-- a container for some kind of list --> <vbox flex="1" > </vbox> <!-- container for messages and tool areas --> <vbox flex="2" > <!-- used to display message --> <hbox id="contentArea" flex="3" > <spacer flex="1"/> <vbox > <!-- stack message and login controls vertically --> <spacer flex="1"/> <description id="msgDescription"> Waiting for login. </description> <label value="User Name:" control="userName"/> <textbox id="userName"/> <label value="Password:" control="userName"/> <textbox id="password" type="password" maxlength="8"/> <button id="loginButton" label="Logon"/> <spacer flex="1"/> </vbox> <spacer flex="1"/> </hbox> <!-- used to display message --> <!-- used to display typing area --> <hbox flex="3" > </hbox> <!-- used to display tool area--> <hbox flex="1" > <spacer flex="1"/> <vbox> <spacer flex="1"/> <hbox> <button id="B1" label="B1" /> <button id="B2" label="B2"/> <button id="B3" label="B3"/> <button label="B4"/> </hbox> <spacer flex="1"/> </vbox> <spacer flex="1"/> </hbox> </vbox> <!-- container for messages and tool areas --> </hbox> <!-- horizontal container for all content (except status info) --><hbox > <!-- stack info and resizer horizontally --> <!-- right align our status bar --> <statusbar id="status-bar" class="chromeclass-status"> <statusbarpanel id="status-text" /> </statusbar> <spacer flex="1"/> </hbox>
<!-- main container --> </vbox> </window>
Within the JavaScript code, first we need to make certain we are responding to the correct command, so we add a global variable at the top of the source file to hold the last command issued:
var K_XUL_NAMESPACE = "http://www.mozilla.org/keymaster/
gatekeeper/there.is.only.xul";
var USER_LOGGED_IN = 0;
var lastCommand = "";
and modify the doLogin
function to save the last command sent:
function doLogin(event) {
try { // try
var theArgs = new Array;
theArgs[0] = new commandArg("un",document.getElementById("userName").value);
theArgs[1] = new commandArg("pd",document.getElementById("password").value);
lastCommand = "login";
doServerRequest("login",theArgs);
} // try
catch (e) { //
alert("doLogin exception: " + e);
}//
}
When processing the server response, we use the JavaScript split( )
function to break our comma-delimited responses into an array of name-value pairs. We then compare the value of the first entry to "true"
before extracting the second returned value and setting the status text:
function retrieveServerResponse( ) { if (theServerRequest.readyState == 4) { // all done // Check return code if (theServerRequest.status == 200) { // request terminated OK dump("Received from server: " + theServerRequest.responseText + "\n"); // var theResults = theServerRequest.responseText.split(","); // var rCode = (theResults[0].substring((theResults[0].indexOf("=")+1), theResults[0].length)).toLowerCase( ); if (lastCommand == "login") { // process login command if (rCode == "true") { // everything OK, we know next parameter is // session info var lastSession = "Last login was "; lastSession += (theResults[1].substring((theResults[1].indexOf("=")+1), theResults[1].length)).toLowerCase( ); loginOK( ); setStatusText(lastSession); } // everthing OK else { // user NG loginFail( ); setStatusText("No user logged in"); } // user NG } // process login command } // request terminated OK else { // something is wrong alert("Response failed."); } // something is wrong } // all done }
This version of the program will now log into the database, and if the username and password match (and the account is active), the welcome screen will include a status line along the bottom of the display reporting the last date and time of the user’s login:
Last login was 0000-00-00 00:00:00
All zeros appear because our initial script to create the database tables did not set an initial date in the database. Once we log in under a valid account name, the next login will yield a more welcoming message.
When Things Go Wrong
There are many “moving parts” to this integration involving XUL source, PHP statements, and SQL statements. Most of the problems at this stage will involve either syntax or logic errors with PHP, or problems with the structured SQL statements. These errors can be difficult to track down, but these suggestions may help:
Errors in PHP scripts often result in nothing being displayed on the browser screen. Try using the JavaScript
dump
function to unconditionally display the results of theXMLHttpRequest
. Then change the PHP script to set a variable such as$returnString
to some suspect variable andecho
the variable. If nothing is returned to the JavaScript function, the syntax error occurs prior to theecho
statement. Otherwise, we can use the variable to help identify any other syntax errors.Errors with the SQL statements often involve syntax or an error in setting user privileges. When suspecting such problems, try using the MySQL command-line interface to type in a statement identical to the script-generated code to verify the expected results.
When all else fails, continue to simplify the code (e.g., use
SELECT * from tableName
) or use PHP stubs to build a string ofdebug trace
statements that end up returned to the JavaScript function.
Serving XUL Files
Sometimes we may want to use a web server to deliver the XUL files. To implement our login user interface by placing the XUL file on our web server, do the following:
Write a source XUL file that will be delivered upon a successful login and place that file along with stylesheets and JavaScript source files on the server.
Configure the server to properly serve a XUL MIME type.
Change the XUL file to a PHP script file.
Add an HTML screen to read the user login and password.
Modify our PHP
doCommand
function to call the PHP script that delivers the XUL source.
Creating a XUL File to Be Served
The XUL source to be served will actually be sent by a PHP script--but a good first step is to develop a XUL file that we will convert into PHP.
We copy the newssearch.js and NewsSearchStyle.css files into the Apache root directory.
We will copy the newssearch.xul file but rename it to startupScreen.xul. We also change the source file to remove the login areas, replacing them with the graphics that render a successful login screen:
<hbox id="contentArea"
style="borderColor:gray;
border-style:ridge;background-color:LightSeaGreen;
border-color:gray;" flex="3" >
<spacer flex="1"/>
<vbox > <!-- stack message and login controls vertically -->
<spacer flex="1"/>
<description id="msgDescription">
Welcome.
</description>
<spacer flex="1"/>
</vbox>
<spacer flex="1"/>
</hbox>
Configuring the Server
Without “understanding” what to do with a XUL file, the Apache web server would deliver a XUL file to a browser as an XML text file. A browser receiving such a file most often just presents the source to the user of the browser.
We must add an entry to either the mime.types or the httpd.conf file for Apache:
application/vnd.mozilla.xul+xml xul
After making this change, restarting the web server will allow us to enter the XUL file reference URL:
http://localhost/startupScreen.xul
The browser will now render the XUL interface shown earlier in our client/server implementation.
PHP Serving XUL
We will modify our code to read username and password information from the user, and if we have a valid user, we will render our XUL success screen. The first step to that process is to build an HTML interface to read our entries, and modify the PHP login script to return the XUL source rather than the simple text response.
The PHP interpreter processes any text between the PHP tags (<?php... ?>
). All other text outside of the PHP tags is returned directly to the browser. PHP scripts build an HTML interface when a designer builds an HTML page and places PHP scripts where conditional processing will change the text returned to the browser.
Our first step to cut over to a served XUL file is to modify our original PHP script to output a standard FORM
to read in the username and password. The POST
action will send the data to the same script file (we will add logic to check for input values that will help flag different entry points to the script).
Once the script obtains the username and password, we will use the same logic to determine success or failure. Although our finished version will then serve a XUL interface, for now we will simply send our previous return code string to the browser.
The changes from the previous doCommand.php file to a doCommandXUL.php file are summarized as follows:
The variables that were obtained through
$_GET
variables are now obtained through$_POST
variables (we will be using an HTML form that uses thePOST
method for input).A check will be added to read the username and password variables. If they are blank, we will issue a login screen; if they are not blank, we will check the input information to see whether the user is authorized.
The login screen is designed to be a simple HTML table with input fields.
If the user is authorized, the script echoes the return code to the browser for display.
The PHP script file doCommandXUL.php now looks like this:
<?php $uName = trim($_POST['un']); $uPass = trim($_POST['pd']); if (empty($uName) || empty($uPass)) { // build our HTML login stuff ?> <h1>REGISTERED NEWSSEARCH USERS ONLY!</h1> <form method="post" action="doCommandXUL.php"> <table> <tr> <td>User name:</td> <td> <input type="text" name="un"/></td> </tr> <tr> <td>Password:</td> <td> <input type="password" name="pd"/></td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" value="LOG IN"/></td> </tr> </table> </form> <?php } else { echo check_user($uName,$uPass); } ?> <?php // Check user will make certain the user exists, and return // true with the last login date in the command string // // Error conditions return false with a 'message' parameter // set to the string returned by mysql // function check_user($name,$pass) { $database = new mysqli('localhost','newssearch_guest','nsgst','newssearch'); if (mysqli_connect_errno( )) { // failing case $retString = 'retcode=false,message='.mysqli_connect_error( ); return $retString; } // failing case $encryptPass = sha1($pass); $query = "select status,last_session from account where username = '$name' and password = '$encryptPass'"; if ($theResult = $database->query("$query")) { // we have some kind of result if ($theResult->num_rows == 1) { // we have our user $theRow = $theResult->fetch_assoc( ); // get the only row that exists $lastLogin = $theRow["last_session"]; if ($theRow['status'] == 'active') { // all OK $retString='retcode=true,last_login='.$theRow['last_session']; // update the session info $theResult->close( ); $curTime = date('c'); $update = "update account set last_session = '$curTime' where username = '$name'"; $theResult = $database->query("$update"); } // account is active else { // account not active $theResult->close( ); $retString = 'retcode=false,message=user account not active'; } // account not active } // we have our user else { // user not found $theResult->close( ); $retString = 'retcode=false,message=user not found'; } // user not found } // we have some kind of result else { // no result returned $retString = 'retcode=false,message=invalid query'; } // no results returned $database->close( ); return $retString; } ?>
When we enter the URL for this file into a Firefox browser, and enter a valid username and password into the fields, we get a browser’s rendering of the return code generated by the check_user
function, as shown in Figure 4-9.
The remaining step to this transition is to serve the XUL source to the user. By renaming our startupScreen.xul file to startupScreen.php, we can merge PHP statements into the XUL source to accomplish the required tasks to report the last login time for a registered user.
Using PHP require( )
This test application will use a PHP require
function to insert the XUL source file into the output stream being returned to the user. The code will execute the same user test, and if the user is registered, a PHP variable will be set to the login time, and a require statement will be set to merge the XUL source file into the output stream. The XUL source file will be changed to pass the login time to the status label. Figure 4-10 illustrates the logic.
PHP serving XUL files
If we were to use only a require script to serve a XUL file, we would not be able to merge PHP statements to set the status field of our interface. But by renaming only the startupScreen.xul file to startupScreen.php, the PHP interpreter would send the XUL source file to the web server without sufficient information to instruct the browser of the required media type.
To allow PHP to properly serve the XUL file, we must make a change to our php.ini file:
short_open_tag = Off
This tells the PHP interpreter to ignore parsing the tags <?
and ?>
, except for those that include the PHP token. Otherwise, PHP would attempt to interpret the XML-structured XUL content.
Our renamed startupScreen.php file must also include a PHP directive to set the content type of the text being sent to the browser. The first line in our startupScreen.php file must be changed to:
<?php header("Content-type: application/vnd.mozilla.xul+xml"); ?>
<?xml version="1.0"?>
<?xml-stylesheet href="NewsSearchStyles.css" type="text/css"?>
Logic changes
Next, we change our doCommandXUL.php frontend script to execute the logic that decides what interface to serve. The only change from the last iteration of the code is to alter the path that actually checks the result if the user entered data into the name and password fields.
The code takes advantage of the PHP explode
function to break up the returned string into an array of name-value pairs.
We use the PHP subst(inString,startingChar,lastChar)
functions to break the result array’s strings into values, given the fact that we know the length of the names being used as tags. If the first returned value is true
, we save a string that holds session information into a variable, and include the PHP file that holds the XUL interface content. If the first returned value is false
, we build a message that flags an unregistered user:
else {
$retString = check_user($uName,$uPass);
$resArray = explode(',',$retString);
if ($resArray[0] == 'retcode=true') {
$lastLoginTime = 'Last login was '.substr($resArray[1],
11,strlen($resArray[1])); // extract last session
require('startupScreen.php');
}
else { // invalid user, send rejection page
echo '<h1>Sorry!</h1>';
echo '<h2>You are not registered to use this service.</h2>';
} // invalid user
}
?>
We need to insert another subtle change into the newssearch.js file. Because the login button is no longer on our startup screen (the doCommandXUL.php file is generating it), we can no longer try to attach a login script upon initialization. We could edit the code to remove all initialization references, or (if we wanted to use the same file for both local and served versions) we could add a test to make certain the button exists before attaching an event listener:
function initialize( ) {
try {
document.getElementById("B1").
addEventListener("command",genericBtnHandler,true);
document.getElementById("B2").
addEventListener("command",genericBtnHandler,true);
document.getElementById("B3").
addEventListener("command",genericBtnHandler,true);
//
// Add a login script
if (document.getElementById("loginButton")) {
document.getElementById("loginButton").
addEventListener("command",doLogin,true);
}
}
catch (e) {
alert ("Exception: " + e);
}
}
Finally, we change the segment of the XUL source in our startupScreen.php file that manages the status label to include the PHP directives to substitute the $lastLoginTime
variable into the label
attribute of the status display:
<hbox >
<!-- stack info and resize horizontally -->
<!-- right align our status bar -->
<statusbar id="status-bar" class="chromeclass-status">
<statusbarpanel id="status-text"
<?php
echo('label=\''."$lastLoginTime".'\'/>');
?>
</statusbar>
<spacer flex="1"/>
</hbox>
Now referencing the doCommandXUL.php file from the Firefox URL will present the same interaction with the user, except with the interface delivered from the web server. Figure 4-11 shows our XUL-served page for a successful login and a failed login.
Summary
We have now completed a number of exercises that demonstrate two of the most common implementations of a XUL application. These examples included:
Moving the application into a chrome package
Connecting the application to a server running PHP scripts
Using a relational database engine to store user account information
Implementing the same interface using XUL source served by PHP scripts
Although we can use any of these forms of implementation for our application, we will continue to focus on the client/server form. The use of the server to provide only textual information lends itself well to our design, in which the Firefox framework accepts the lion’s share of interface rendering, and leaves the server to focus on protocol, security, and content.
Get Programming Firefox now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.