Media Temple’s Usability Faux Pas

Clearly, I’m a fan of Media Temple, but today I was shocked by a piece of bad usability on their site when I was looking into signing up a new client for some DV hosting.

The client for this new job is actually the client of a client and I was charged with setting up the new hosting. My client wanted to know all the information that the MT sign-up form would ask for during registration so that we would only have to ask the end client for their details once. I went to the MT sign-up form to write down a list of all the fields that were asked for.

As can be seen in the image their ‘activate’ process is as a three step affair.

The first step wants to know a domain name to associate with the new account. Since I wasn’t actually registering I put in ‘test.com’ so that I could ‘continue order’.

The second step of the registration form is where the bulk of the fields are. It asks for first and last name, company, email, contact and billing address as well as the credit card details. I made a note of all the required fields and then I wondered if step 3 had any additional ones.

In order to get to step 3 I needed to fill in some more dummy information. I populated all of the different fields with variations of the word ‘test’ and then I filled in the universal Visa testing number 4111 1111 1111 1111. I then clicked the ‘continue order’ button at the bottom of the page.

I was astounded to see Media Temple process and finalize my order and I soon learned that the ‘Confirmation’ of step 3 is actually the ‘Thank you for your order’ page.

Needless to say, this is not what I expected. Why? Because the button at the bottom of page 2 doesn’t say ‘Place Order Now’ or ‘Confirm Purchase’ but instead says ‘Continue Order’ just like the button at the end of step 1 said; the button that didn’t place my order when I pressed it the last time; the button that instead advanced me to the next step just like I thought this ‘continue order’ button would. I also didn’t expect the order to processed because step 3 is the ‘Confirmation’ stage, which is pretty universally recognized as the ‘please review your order details to make sure there haven’t been any mistakes made and then we’ll finalize this thing’ stage.

This is a usability fail. Every other website that sells something — be it Amazon, Ebay, Etsy, PayPal, whoever — has the final step in the order process be an order summary page that offers the opportunity to edit the order details and then finalize the order with a big, fat button that clearly states that pressing it will place the order. I am so conditioned by this common practice that it didn’t even occur to me that pressing the ‘continue order’ button at the end of step 2 would place my order.

Media Temple, you have an awesome service but this little fly-in-the-ointment is embarrassing, especially because it occurs when people are signing up for your service (first impressions and all that). Thankfully, this is an easy fix in the short term: get in there and change that button to something more appropriate. Over the long-term, when the form is being redesigned, create an order confirmation page that let’s users review their details before finalizing. Everyone else is doing it and you should adopt the practice.

With much love.

Posted in Usability | Tagged , | 3 Comments

An Upload Progress Bar with Media Temple DV and APC

I had a client that wanted to allow users to upload files up to 100 Megs in size but didn’t want to have any Flash on the page. A 100 Meg upload can take 10 minutes for some users and 10 minutes with no visual indication of progress is a recipe for disaster. I decided I would find a solution that didn’t use Flash and my research led me to using APC, the alternative php cache, to monitor the upload and report back. The following is how I set up the form on Media Temple DV 4 server.

To begin you need to install APC. APC works great as a caching engine for PHP but to take advantage of the upload monitoring feature PHP 5.2 or higher needs to be installed. Thankfully, Media Temple’s DV4 comes installed with 5.3.5 (as of Feb 2011). What doesn’t come installed is APC and to install it requires command line access so hopefully you know how to do that.

The following directions were taken almost verbatim from A Media Creative’s blog post. For a more detailed description visit their page as I will only go over the steps briefly.

SSH into the server and issue these commands one at a time. Note that 3.1.6 was the most recent stable version of APC available at the time I installed it. There may be a newer version so check the PECL APC page to see.

cd /usr/local/src
wget http://pecl.php.net/get/APC-3.1.6.tgz
gunzip -c APC-3.1.6.tgz
tar tf APC-3.1.6.tar
cd APC-3.1.6
/usr/src/phpize
/configure -enable-apc -enable-apc-mmap -with-apxs2=/usr/sbin/apxs -with-php-config=/usr/bin/php-config
make
make install

APC should now be installed. However, the apc.ini file needs to be edited. Once again, A Media Creative has the answer:

cd /etc/php.d/
vi apc.ini

Then, add this line to the file.

extension=apc.so

And reboot the server.

/etc/init.d/httpd restart

Is APC installed? Go over to your server and load a page with php_info() in it. If you see a big APC section, it worked. Such a php_info() page would consist of the following:

<?php
php_info();
?>

Be sure to delete it afterwards, it’s bad form to leave it kicking around.

APC is now installed but a few things need to be done before we are ready to go. The most important is that we need to instruct APC to watch for file uploads by setting the apc.rfc1867 flag in the php.ini file. To do this, open php.ini using vi.

vi /etc/php.ini

Scroll down to the part of the file that talks about upload_max_filesize. In vi, the easiest way to do this is to type /upload_max and then hit return. Vi’s searching routines are activated by a forward slash (when not in insert i mode) and then you just type the search string you are looking for, hit enter, and vi jumps you to that spot.

Since I needed to upload a 100 Meg file and a 2 Meg image I set upload_max_filesize to 103 M. I have found that adding an extra meg helps account for the rest of the (usually text) data in the form.

upload_max_filesize = 103M

I also set the following parameters;

post_max_size=103M
apc.max_file_size=103M

Finally, add this line to tell APC to track uploads.

apc.rfc1867 = on

Exit vi and save. Now reboot apache.

/etc/init.d/httpd restart

The next step is to set up the form that will be doing the uploading. Create a new PHP file named upload.php on the server with this source:

<?php
$apc_id=uniqid();
?>
<html>
<head>
</head>
<body>
<form enctype="multipart/form-data"  action="upload.php" method="post" id="upload">
<input type="hidden" name="APC_UPLOAD_PROGRESS" value="<?php print $apc_id;?>">
<input type="file" name="file">
<input type="submit">
<div id="container" style="width:400px;">
	<div id="bar" style="background:#ccc;">
  		 <div id="status"></div>
	</div>
</div>
</form>
</body>
</html>

Now, there’s more to it but this is a good place to start. First, notice that a variable named apc_id is set with a random value when the page loads and then that variable is used as the value for a hidden input field named APC_UPLOAD_PROGRESS. This is the key to whole solution because it turns out that when APC is turned on and that variable is present in a form APC will automatically begin tracking the file uploads for the form and report back, when queried with the proper identifier, on the progress of the upload.

There are a couple of things to keep in mind. First, the APC_UPLOAD_PROGRESS variable needs to appear before the type=”file” field in order for this to work so don’t put it at the bottom of the form. Second, if you have more than one file field you only need the one APC_UPLOAD_PROGRESS field. This is actually an interesting aspect of this whole process. The progress bar represents how the entire upload is going, not the individual files like you see with Flash uploaders. So if you have two files you know that when the progress bar reaches 100% that both files have been uploaded.

The final bit of HTML is a series of divs that will be the progress bar. The example has a very simple bar, a div 400 pixels wide that fills with a light grey background as the file uploads. Since the bar can be manipulated with css the sky is the limit in terms of appearance. In fact, you don’t even need to use divs at all, it’s just convenient for the example. You could, if you wanted, use an img tag and grow it horizontally (or even vertically) as the data comes in. It’s very flexible.

But to make it work, we need some jquery so modify the PHP page you created by adding the following between the head tags.

<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.5.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
  $("#upload").submit(function() {
  	setInterval(function() {
  	$.get("progress.php?apc_id=<?php print $apc_id; ?>&r="+ Math.random(), {},
  		function(data) {
  			$("#bar").width(data +"%");
  			$("#status").html(parseInt(data) +"%");
  		}
  		)}
  		,500);
  });
});
</script>

First thing we do is call in jquery 1.5 from the MS cdn. Next I create an on submit event for the form that says that when the form is submitted an interval should be set that calls a function every half second (500 milliseconds). The function performs a get request to a file named progress.php and passes the apc_id variable as well as a random value to prevent caching. The get request will receive some data back and that data is used to set the width of the div with id=bar and to set the html of id=status. The HTML in question is just a numeric percentage but it can be as fancy or as plain as you like.

Finally we need to create the progress.php file. Create the page with the following source.

<?php
if (isset($_GET["apc_id"])) {
  $status = apc_fetch("upload_" . $_GET["apc_id"]);
  print $status["current"]/$status["total"]*100;
  die();
}
?>

This page is very simple as it checks to make sure apc_id is set and then uses it to fetch the apc status for the file upload. The status is then used to calculate a percentage that is then printed to the screen and the script ends. The output percentage is the data that the jquery get request in progress.php is receiving.

Try uploading a file now to see the progress bar in action. Pick a larger file, say 500 kb so that the upload takes a few seconds and you can see the bar.

You should note that there is no code here to save the uploaded file, that’s another blog post, but the web is full of tutorials on how to save PHP uploads.

I feel that the best part of this approach is that unlike the Flash uploads, which upload files separately from the rest of the form and require special logic to keep the uploaded files associated with the rest of the form data, this approach is a traditional form upload where everything goes together and can be processed together. Another benefit is that it degrades nicely (well, it still works at any rate just no progress bar) in the absence in javascript.

One final note, if, for some reason, you don’t like the variable name APC_UPLOAD_PROGRESS you can change it in the php.ini file. Just open it back up in vi and set the following flag.

apc.rfc1867_name = some_other_name

After you restart the server the new name will be used by APC to watch for uploads.

Happy uploading.

Posted in Web Development | Tagged , , | 1 Comment

Exploring Data Encryption with MySQL

This post is about securely storing data in a MySQL table. And by secure I mean storing encrypted data. To keep it simple I’m going to use symmetric encryption (i.e. secret key encryption) rather than asymmetric encryption (i.e. public key encryption). The post is a bit of a red herring because it starts off examining MySQL’s AES_ENCRYPT/AES_DECRYPT functionality but then reveals why you might not want to use it. I then offer a PHP alternative.

To fuel this experiment I am going to demonstrate how to store credit card numbers in an encrypted manner. Normally, you don’t want to store credit card details because who needs the hassle? But sometimes business rules dictate that you need to keep card details around for a while (for example, maybe you don’t bill the card until the purchased item is shipped). At any rate, there are legitimate reasons for storing credit card numbers in a database and the PCI-DSS, the data security standard for looking after credit card details, has a lot to say about what you may and may not do. I’m not saying this solution is compliant with PCI-DSS, since compliance is more of a policy matter than a technical one, but you sure as heck don’t want to be storing credit card numbers in plain text in your database, that’s suicide.

The first thing to do is create a table in your database to hold the encrypted data. I’ll keep it super simple. Run this code through your MySQL server or through phpMyAdmin to create the table. Note the use of the varbinary data type. I’ve found that 32 is a reasonable size for storing credit card info. If you need to store something else, maybe you’ll need to make it bigger.

CREATE TABLE `creditcards` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `number` varbinary(32) NOT NULL,
  `name` varbinary(32) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Now that we have a table, the next thing we’re going to need is a secret key. I typically use the GRC Ultra High Security Password Generator to create a 63 character string of gibberish. Visit the page and copy the ’63 random printable ASCII characters’ string into a text file. I name this file .secret and I store it in a folder above the webroot. If you haven’t done that before than read this post about storing files above the webroot on media temple. The important thing is to put the file somewhere where it can’t be addressed by a URL on the server. You see, it’s a secret.

Now, somewhere that is web addressable, create a new PHP file named, I dunno, ‘secure.php’ and add the following code at the top.

if (!($fd = fopen({/full/path/to/.secret}, "r"))) {
  die("Can't fine file");
}
fscanf($fd, "%s", $secret);
fclose($fd);

You’ll need to fill in the full path to the .secret file for the open statement. What this snippet does is read the file from its location above the webroot and place the contents of the file into the variable $secret. This is how our secret key gets loaded into PHP.

Lots of people ask around the Internet “how do I keep my secret key safe” and I think this approach, at least on a single server in a hosting environment that is not physically under your control, is pretty good. For a Media Temple server, you’ve got the physical security down pat, if you’ve got solid ssh and ftp passwords you’ve got your network security locked down, and as long as you secure your PHP code so that people can’t upload scripts to be executed on the server, you’re doing alright. My impression is that a lot of people want to know how to protect the integrity of an encrypted database when someone has complete access to the server. This is like asking how to stop the coup after the king is dead and the usurper is on the throne; it’s far too late at that point. These tactics work when someone does not have complete access to your server, if they do, then it’s game over man, game over.

So, first attempt to store encrypted data in MySQL will use the built in AES_ENCRYPT function. Code the following INSERT statement.

$insert_sql = "insert into creditcards (";
$insert_sql.= "number,";
$insert_sql.= "name";
$insert_sql.= ") values (";
$insert_sql.= "AES_ENCRYPT('4111111111111111', ?),"; // the ? are bound parameters in PDO
$insert_sql.= "AES_ENCRYPT('Prion Interactive', ?)";
$insert_sql.= ");";
$parameters = array($secret, $secret); // I know, there is a more elegant way to do it

I typically use PDO for all my database needs so the following code will open a PDO connection for MySQL. If you’re using mysql then replace the bound parameters ? with the $secret variable escaped with mysql_real_escape_string and surrounded in single quotes.

First, set up the server.

define("HOST", "{host}");
define("DB_NAME", "{database_name}");
define("DB_USER", "{db_username}");
define("DB_PASS", "{db_password}");
$db = new PDO("mysql:host=" . HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS, array(PDO::ATTR_PERSISTENT, true));
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Naturally, you need to fill in your own database info for the define statements.

After initializing PDO user the following command to run the $insert_sql query.

$st = $db->prepare($insert_sql); // has bound variables
$st->execute($parameters); // parameters contains the values to bind

Save all the above to the PHP file and run it. Then go have a gander in phpMyAdmin and browse the creditcards table, you’ll see the encrypted info in there.

If you want to decrypt the info you go the opposite way with a SELECT statement.

$select_sql = "select AES_DECRYPT(number, ?) as number from creditcards order by id desc limit 1;";
$parameters = array($secret);
$st = $db->prepare($select_sql); // has bound variables
$st->execute($parameters); // parameters contains the values to bind
$result = $st->fetchAll(PDO::FETCH_ASSOC);
print $result[0]["number"]; // should print 4111111111111111

So, wonderful, it worked right? Encrypted data in the database, secret key stored securely below the web root, perfect.

Except for one thing. Do you replicate your MySQL database? For back-up or master-slave or anything like that? Because if you do then you know that that functionality is achieved using the bin logs. That’s a problem.

Media Temple does not turn bin logging on by default. This is another situation where you need to edit the /etc/my.cnf file to enable some server functionality. SSH on to the server and open the file in vi.

vi /etc/my.cnf

Move down to some empty lines and add the following three lines.

log_bin=mysql-bin
expire_logs_days = 5
max_binlog_size = 100M

Quickly, this tells MySQL to name the bin log file mysql-bin, that these files (of which there can be many) should only stick around for 5 days and that the max size should never exceed 100 megs.

So what is bin logging? Every command that is issued to the server that modifies data, that is, every insert, update, delete, what have you, everything but select, is logged to the bin log. The back up servers, or the MySQL slaves, when configured properly, look for those bin logs and run their contents to make a clone of the master database. It’s a pretty good way to do backups. Except in this case.

Restart the MySQL server.

/etc/init.d/mysqld restart

Now go and reload the PHP page so that it inserts the encrypted data again.

Back in SSH, cd to /var/lib/mysql and look for a file named mysql-bin.000001. The first of many bin log files. Issue the following command to see its contents.

mysqlbinlog mysql-bin.000001

Do you see what’s in there? Yeah, the insert statement WITH the plain text values to be encrypted and WITH the secret key for the whole world to see. Bin log files are, as you might expect, bin files, but mysqlbinlog prints them out as plain text. Nevertheless, all the work we went to encrypt that data is undone by the presence of this bin file. Not only that, but bin files get spread around to different servers, so they need to travel. It’s a big problem. But it also makes sense since of course the back up server or the slave servers need the full insert statement to make an accurate duplicate, how else could they function? What it means for us is that the AES_ENCRYPT/AES_DECRYPT method of MySQL, while cool, is not suitable if bin logging is needed for any reason.

Be sure to go back into my.cnf and remove those lines about bin logging if you don’t want to keep them. MySQL bin logs can eat your hard drive if you’re not careful. Don’t forget to reboot the server if you change my.cnf.

So, what to do? What must be done is that the data needs to be encrypted by PHP and inserted into the database in encrypted form. This will preserve the security of the data in the tables and allow the use of bin logging since the statements being logged will only contain the encrypted data, not the plain text and not the secret key.

How to do this? We’re going to use PHP’s mcrypt library with the RIJNDAEL 256 cipher algorithm. There is a variety of ciphers to choose from but this one will serve for the demonstration.

Modify your PHP page to include the following code (keep the code that loads the .secret key from above the web root and that initializes the PDO database connection).

$secret = mhash(MHASH_MD5, $secret);
$cipher_alg = MCRYPT_RIJNDAEL_256;
$iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher_alg, MCRYPT_MODE_ECB), MCRYPT_RAND);

First, I mhash the $secret variable so that it’s an acceptable size for the MCRYPT_RIJNDAEL_256 algorithm. Next I define the cipher and then I create the ‘initialization vector’ $iv.

The following code uses the cipher to encrypt the credit card number and name.

$number = mcrypt_encrypt($cipher_alg, $secret, "4111111111111111", MCRYPT_MODE_CBC, $iv);
$name = mcrypt_encrypt($cipher_alg, $secret, "Prion Interactive", MCRYPT_MODE_CBC, $iv);

And the following code is the modified $insert_sql statement.

$insert_sql = "insert into creditcards (";
$insert_sql.= "number,";
$insert_sql.= "name";
$insert_sql.= ") values (";
$insert_sql.= "?,?";
$insert_sql.= ");";
$parameters = array($number,$name);
$st = $db->prepare($insert_sql); // has bound variables
$st->execute($parameters); // parameters contains the values to bind

Where once again I am using PDO to handle the insert.

Have a look inside the creditcards table using phpMyAdmin and you’ll see the encrypted text.

To decrypt the contents, use the following code.

$select_sql = "select number from creditcards order by id desc limit 1;";
$result = Database::getDatabase()->query($select_sql);
$number_encrypted = $result[0]["number"];
$number_decrypted = mcrypt_decrypt($cipher_alg, $secret, $number_encrypted, MCRYPT_MODE_CBC, $iv);
$number_decrypted = rtrim($decrypted_str, "\0\4");
print $number_decrypted;

If you want to satisfy yourself that bin logging is not disclosing your secrets you can turn it back on, reboot the server, rerun the PHP file and examine the logs. Or you can trust me when I tell you that only the encrypted values are in the bin logs now.

Ta-da.

Posted in Web Development | Tagged , , | 3 Comments

MySQL Security on Media Temple DV, Part 2

This is a serious ‘Playing With Fire’ blog post. Don’t do this on a server you can’t afford to restore to factory defaults.

I was doing some reading today about securing MySQL and I learned about about a MySQL function named old_passwords(). As it turns out, old_passwords() is used to generate the hash for a MySQL user password and, according to O’Reilly’s High Performance MySQL, is “easy to crack” (p 529). This is a concern because even though I pride myself on creating long passwords the server could still be subverted by a poor hashing algorithm. I decided to investigate my MySQL server to see if this was an issue for me.

A little history. After MySQL 4.1 the weak password() function was replaced with a more robust function and old_password() was created to facilitate backwards compatibility. Since I don’t require backwards compatibility, and 4.1 was some time ago, I had hoped that old_password() wouldn’t be active in my current MySQL install. I was wrong.

So, the usual song-and-dance. SSH on to the DV server and fire up mysql.

Issue this command.

SHOW GLOBAL VARIABLES LIKE 'old_passwords';

If your DV server is like mine, you’ll see that old_passwords=ON. This most be changed.

Side note: issue this command.

SHOW SESSION VARIABLES LIKE 'old_passwords';

Note that old_passwords=OFF. Curious, but mostly irrelevant. When signed into MySQL the GLOBAL setting dominates, so that must be changed.

Unfortunately, it can’t be changed from within MySQL. I needed to edit the my.cnf configuration file that instructs MySQL when it boots. I found this file at /etc/my.cnf. Crack it open in vi.

vi /etc/my.cnf

Remember for vi: type ‘i’ to begin inserting (or editing), hit ‘escape’ to stop editing, and then hit ‘:wq’ to save and quit (yes, that’s a colon, then a w, then a q, then enter).

Find the line that says old_passwords=1 and change it old_passwords=0.

Exit vi and reboot MySQL.

/etc/init.d/mysqld restart

Open MySQL back up. It’s time to edit some users.

Verify that old_passwords is off with the following SQL statement.

SHOW GLOBAL VARIABLES LIKE 'old_passwords';

Off, right? Good.

Issue this statement to get an overview of all the users in the system.

SELECT user, host, password FROM mysql.user;

You should see at least 3 users you didn’t create. One named ‘admin’, one named ‘horde’ and one named ‘pma_XX’ where XX is a long string of gibberish. These are the MySQL full privileges user, the horde webmail database user and the phpMyAdmin database user. Beside each of their names is their host, and they should all be ‘localhost’, and beside that is the hash of their password.

Note that each of their hashed passwords is 16 characters long and does not begin with an asterisk. This is what old passwords look like. User passwords in the new style always begin with an asterisk (*) and are much longer. Let’s create a new user now to demonstrate.

We’re going to create a new full privileges user named ‘superadmin’. Make sure you generate a strong password for this user.

GRANT ALL PRIVILEGES ON *.* TO 'superadmin'@'localhost' IDENTIFIED BY '{password}' WITH GRANT OPTION;
FLUSH PRIVILEGES;

Now issue the select again to view users and their passwords.

SELECT user, host, password FROM mysql.user;

See the difference? Note, that if you hadn’t flipped that switch in my.cnf and created this superadmin user, their hashed password would be in the old style.

IMPORTANT NOTE: You just created a new full privileges user and you may be thinking to yourself ‘I should kill the admin user, since admin is the known MT root user for MySQL and using a different user, such as superadmin, would be moderately more secure’. While this is true, trashing the admin user will totally pooch your Plesk install. DO NOT DO THIS. Trust me. It takes an hour to fix.

The problem, as it stands, is that you have a superadmin that is using the new password format but the rest of your users are not. They need to be brought up to speed. Why? Because at the end of this blog post we are going to activate the secure_auth flag and force the database to hash credentials in the new way instead of the old. Since new hashes don’t match old, those users will begin to fail.

So, first thing, let’s get admin up to speed. You know the admin password, it’s the one you use to sign into Plesk. Issue the following SQL.

SELECT old_password('{known_admin_password}');

See the output? That’s the same value that’s in the mysql.user table for the admin password. Now issue the following SQL.

SELECT password('{known_admin_password}');

Now this output represents the same password but run through the more robust hashing algorithm. Copy the output because you are going to issue an update against the user table.

UPDATE mysql.user SET password='{new_admin_hash}' WHERE user='admin' LIMIT 1;

This will update the password hash to the new style, and everything should work just dandy. Try signing out and signing in as admin. If it doesn’t work, don’t panic, that’s why we created superadmin. Sign back in as superadmin and review to see if you made any typos.

You might also try rebooting the MySQL server again to set things right.

/etc/init.d/mysqld restart

Right, so your admin is up to date, you have superadmin squared away. What about the phpMyAdmin user? Well, to update the password you need to locate the password that phpMyAdmin uses. It’s in a file known as config.default.php. But where is phpMyAdmins stored anyway? I’ve never seen it kicking around the server.

Here it is.

cd /usr/local/psa/admin/htdocs/domains/databases/phpMyAdmin/libraries

Yes, it’s part of the Plesk install, those tricky dicks. Issue a more command to view the config file.

more config.default.php

And keep hitting the space bar until you see the line that says

$cfg['Servers'][$i]['controlpass'] = "XXX";

Where XXX is your super secret phpMyAdmin password.

Back in MySQL you can verify that, yes, the string you found is actually the password.

SELECT old_password('{phpmyadmin_password');

Does the output match the hash for the phpMyAdmin user? Thought so. Generate the new hash.

SELECT password('{phpmyadmin_password}');

And update the user.

UPDATE mysql.user SET password='{new_phpmyadmin_hash}' WHERE user='{phpmyadmin_user_name}' LIMIT 1;

There, it’s up-to-date.

Of course, whenever you crack open phpMyAdmin through Plesk it always using the user created through the Plesk panel. Looking at the mysql.user list, and if you follow my naming convention, that user is typically named db_{something}_full. You know this password (since you created the user) so you can update it the same way. Also, as an aside, users created through Plesk have the wildcard ‘%’ as their host. I don’t like this so I change the full user to be ‘localhost’ like this.

UPDATE mysql.user SET host='localhost' WHERE user='{plesk_created_user}' LIMIT 1;

Of course, if you are using that Plesk created user to access your website through PHP localhost won’t do. In that case you should set the host to be the IP address of your server (which is the same as the IP address that Plesk is on). If you follow my user conventions, the full user can stay as localhost but the limited user needs to be changed to the IP address.

UPDATE mysql.user SET host='204.XX.XXX.XXX' WHERE user='{limited_mysql_user}' LIMIT 1;

So that should be everyone, except the horde user. I hunted around for the horde users password and ultimately I found it socked away in a shadow file. Issue the following command to see it.

more /etc/psa/.webmail.shadow

The result is the password. Back in MySQL you can run to satisfy yourself that the password is correct.

SELECT old_password('{horde_password}');

Then it’s the same old song and dance to update the password.

SELECT password('{horde_password}');

To get the hash and then feed it into the following.

UPDATE mysql.user SET password='{new_horde_hash}' WHERE user='horde' LIMIT 1;

Now all the users are up to snuff.

The final thing to do is go back into the my.cnf and activate the variable secure_auth. Before you do, issue the following SQL statement.

SHOW GLOBAL VARIABLES LIKE 'secure_auth';

It must say that it’s OFF because if it didn’t none of the steps in this post would have worked.

Edit your my.cnf file again in vi.

vi /etc/my.cnf

Cursor down to one line below the old_passwords=0 line from before and begin editing (‘i’). Add the following line.

secure_auth=1

Escape, save and quit (:wq).

For good measure, reboot the server one last time.

/etc/init.d/mysql restart

Because of this change MySQL will now refuse to recognize all passwords in the old style. Luckily, you updated everyone to new style. The database is now more secure. Huzzah.

Posted in Web Development | Tagged , | Leave a comment

MySQL SQL for Generating Canada and USA Province/State Tables

I often need Canadian and American provinces and states as a table in my databases. Here’s the MySQL SQL that I use to create them. I just copy and paste it into phpMyAdmin. Note that it first creates a countries table, populates it with Canada and USA and then inserts the provinces and states. Canada as country_id 1 and USA as country_id 2 is hardcoded since that’s the way it all shakes out if you run the script.

CREATE TABLE `countries` (
  `id` smallint(5) unsigned NOT NULL auto_increment,
  `name` varchar(40) NOT NULL,
  `abbreviation` varchar(4) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

CREATE TABLE `states` (
  `id` smallint(5) unsigned NOT NULL auto_increment,
  `country_id` smallint(5) unsigned NOT NULL,
  `name` varchar(100) NOT NULL,
  `abbreviation` varchar(3) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

INSERT INTO countries (name, abbreviation) VALUES ('Canada', 'CAN');
INSERT INTO countries (name, abbreviation) VALUES ('United States of America', 'USA');

INSERT INTO states (country_id, name, abbreviation) VALUES ('1', 'Alberta', 'AB');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1', 'British Columbia', 'BC');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Manitoba', 'MB');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'New Brunswick', 'NB');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Newfoundland and Labrador', 'NL');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Northwest Territories', 'NT');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Nova Scotia', 'NS');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1', 'Nunavut', 'NU');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Ontario', 'ON');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Prince Edward Island', 'PE');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Québec', 'QC');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Saskatchewan', 'SK');
INSERT INTO states (country_id, name, abbreviation) VALUES ('1',  'Yukon Territory', 'YT');

INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Alaska', 'AK');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Alabama', 'AL');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Arizona', 'AZ');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Arkansas', 'AR');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'California', 'CA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Colorado', 'CO');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Connecticut', 'CT');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Delaware', 'DE');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'District of Columbia', 'DC');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Florida', 'FL');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Georgia', 'GA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Hawaii', 'HI');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Idaho', 'ID');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Illinois', 'IL');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Indiana', 'IN');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Iowa', 'IA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Kansas', 'KS');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Kentucky', 'KY');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Louisiana', 'LA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Maine', 'ME');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Maryland', 'MD');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Massachusetts', 'MA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Michigan', 'MI');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Minnesota', 'MN');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Mississippi', 'MS');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Missouri', 'MO');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Montana', 'MT');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Nebraska', 'NE');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Nevada', 'NV');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'New Hampshire', 'NH');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'New Jersey', 'NJ');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'New Mexico', 'NM');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'New York', 'NY');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'North Carolina', 'NC');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'North Dakota', 'ND');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Ohio', 'OH');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Oklahoma', 'OK');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Oregon', 'OR');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Pennsylvania', 'PA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Puerto Rico', 'PR');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Rhode Island', 'RI');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'South Carolina', 'SC');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'South Dakota', 'SD');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Tennessee', 'TN');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Texas', 'TX');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Utah', 'UT');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Vermont', 'VT');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Virginia', 'VA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Washington', 'WA');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'West Virginia', 'WV');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Wisconsin', 'WI');
INSERT INTO states (country_id, name, abbreviation) VALUES ('2', 'Wyoming', 'WY');
Posted in Web Development | Tagged | Leave a comment

Securing MySQL on a Media Temple DV Server

I’m no pro at securing MySQL databases but I know that you shouldn’t use the full access user from within PHP. The following is a description of how I create a second, limited MySQL user for PHP applications hosted on a Media Temple DV server.

The default database user is created through Plesk in the traditional way. Sign into Plesk, select the “Domain” menu option, click the domain name, click the “Databases” icon, and click the “Add New Database” icon. Fill in the database name and click save.

Now click the “Add New Database User” icon to create the first user for the database. The form that appears asks for a user name and a password. For the username I follow the form “db_{project_name}_full” where {project_name} is a short word that represents the project. The key element is the use of “_full” in the name to act as an identifier that reminds me “this user has full privileges”.

Also crucial, at this point, and always, is the creation of a strong password. I like to use the Strong Password Generator to generate a nice 20-character string of nonsense to use as a password. Of course I can’t remember it so I save it in a password manager on my desktop. The chances of someone breaking into my computer and stealing the password is far less than someone guessing a shorter, more memorable password from the comfort of their computer room. Create a good one and use it.

It’s important to have a user with full permissions because such a user is needed to access phpMyAdmin and create and manage the database’s tables. Within the context of Plesk this is perfectly acceptable but within the context of PHP scripts full access can be dangerous. This is because, despite our best efforts, we may accidentally write some code that permits an SQL injection. By creating a user with limited MySQL access, no “DROP” permissions for example, we can partially mitigate the danger of an SQL injection.

To create the limited user you’ll need command line access. Media Temple has a wiki for that.

Sign on to the server and start MySQL.

mysql -u admin -p

Media Temple has set the default MySQL user to be “admin” instead of “root” and the password for “admin” is the same password used to sign into Plesk.

Once in to MySQL (you’ll see the mysql> command prompt) issue the following command.

GRANT SELECT ON {database_name}.* TO '{database_username}' IDENTIFIED BY '{username_password}';

Let’s break this down. {database_name} is the name of the database that was created in Plesk. {database_username} is the name of the new user being created (the act of granting permissions creates the user if they don’t already exist). Following the naming pattern above I use the form “db_{project_name}_limited” where {project_name} is the same short word and the presence of “_limited” tells me that this is the user with limited access. Create another strong password and drop it into {username_password}.

Clearly this command only allows the new user to SELECT. Perhaps that’s all you need. If so, you’ve created the most secure database user ever since they can only read your database. However, you probably need a bit more functionality. Issue the following two commands to add INSERT and UPDATE permissions.

GRANT INSERT ON {database_name}.* TO '{database_username}' IDENTIFIED BY '{username_password}';
GRANT UPDATE ON {database_name}.* TO '{database_username}' IDENTIFIED BY '{username_password}';

Now the limited user has SELECT, INSERT, and UPDATE. That’s all I ever give my limited user. That’s right, no DELETE. When I delete something in the CMS I set a “deleted” column to the current unix epoch time stamp and manage my SELECTs around on that column. This means that no one can inject a “DELETE from important_table” command and wipe out my important table. Of course, an UPDATE can be nearly as destructive but UPDATEs are necessary. Security is a balance after all.

If you want DELETE, issue the following command.

GRANT DELETE ON {database_name}.* TO '{database_username]' IDENTIFIED BY '{username_password}';

Finally, no matter what, issue this command to make sure all the privileges stick.

FLUSH PRIVILEGES;

You can also do a SELECT to see your new user and his permissions.

SELECT * FROM mysql.user;

There are a lot of Ys and Ns in that output but you can sort through it.

One note, it is possible to combine all the GRANT commands into one like this:

GRANT SELECT, INSERT, UPDATE ON {database_name}.* TO '{database_username}' IDENTIFIED BY '{username_password}';

But I usually do them one at a time just to be fastidious.

Finally, the obvious bit, use the new limited user and password as the user that accesses the MySQL database in PHP. Using these credentials will ensure that your PHP can’t do anything to the database that you haven’t explicitly given it permission to do.

Posted in Web Development | Tagged , , | 1 Comment

ImageMagick, Imagick, and Opticrop on Dreamhost’s Shared Hosting

This is a long one.

We were developing a site for a client that required that thumbnails be generated from user uploaded images. We started using vanilla GD to do it but the thumbs were pretty lousy since they seldom focus on the subject of the image. That’s when I found Jue Wang’s Opticrop script. Visit that link and check out the demo, the code is amazing. It’s one of those seemingly magical things that just works. We wanted it on the client site but we were going to have to work for it.

The problem was that the client was on a shared Dreamhost server package and they weren’t interested in upgrading to a more expensive package since their traffic would never justify it. The problem with that is that Dreamhost’s shared hosting comes with its own limitations related to a user’s ability to modify the server environment to their liking. Since it is a shared environment we obviously can’t have root access to install libraries and if we want to install any packages they have to be installed in the user folder. This was an issue because Opticrop uses ImageMagick and the PECL library Imagick to find the most interesting parts of an image and intelligently crop it. Dreamhost provides ImageMagick as part of their server environment but they do not provide Imagick. This post is about how I was able to get Imagick installed WITHOUT installing a custom version of PHP, which was something I really wanted to avoid doing.

In order to get Opticrop working it is necessary to do some command-line work with a Dreamhost account. If you aren’t familiar with SSH and how to get your DH user account to allow to work with the command-line try Google and the many, many tutorials will set you straight.

The first thing to do once you SSH into your account is create a directory named “custom”.

mkdir ~/custom

Now make three folders within custom.

mkdir ~/custom/imagemagick
mkdir ~/custom/imagick
mkdir ~/custom/autoconf  

mkdir ~/bin
mkdir ~/bin/php5
mkidr ~/bin/php5/extensions

You might be wondering why I created an ImageMagick folder. DH has ImageMagick, why does it need to be installed again? The short answer is that I didn’t find that the DH ImageMagick had all the libraries the IMagick installer needed (such as Magic Wand) and it was much easier to just install ImagickMagic in my home folder to get everything working.

With the folders in place it was time to begin installing. A big thanks to Paul Robsinson for the following steps. I found this blog post of Paul’s that showed me how to install each of the three programs I created folders for.

To begin, move into the newly created ImageMagick folder and issue the following commands.

cd ~/custom/imagemagick
wget ftp://ftp.imagemagick.org/pub/ImageMagick/ImageMagick.tar.gz
tar zxvf ImageMagick.tar.gz
cd ImageMagick-<version>
./configure --prefix=$HOME/local -with-gslib --with-gs-font-dir=/usr/share/fonts/type1/gsfonts/ --without-perl
make
make install

Now it’s time to install autoconf.

cd ~/custom/autoconf
wget ftp://ftp.gnu.org/gnu/autoconf/autoconf-2.63.tar.gz
tar xzf autoconf-2.63.tar.gz
cd autoconf-2.63
./configure --prefix=$HOME/custom/autoconf
nice -n 19 make
make install
export PATH=/home/{username}/custom/autoconf/bin:$PATH

Note the presence of {username} in there, sub in your own.

Finally, install Imagick

cd ~/custom/imagick
wget http://pecl.php.net/get/imagick
tar zxvf imagick-<version>.tgz
cd imagick-<version>
phpize
./configure -prefix=$HOME/imagick --with-imagemagick=$HOME/custom/ImageMagick

Note the “version” identifier. Fill that in properly with whatever version you’re grabbing.

The Imagick install required autoconf, which is why I installed it. Without it the “phpize” call fails. Also note that the I changed the –with-imagick flag to be –with-imagemagick to point to the location of the newly installed ImageMagick. I think this may have been a typo on Paul’s site because without that change, the install won’t complete.

Before I proceeded to the next step I needed to make a change to Makefile for Imagick in order for it to install its extension in a suitable place. Following the guidance of Adrian (way down in part 4) I used the text editor nano to make some changes to the Makefile.

First, I found the variable ‘phpincludedir and changed it from /usr/local/php/include/php to /home/{username}/bin/include

I also located “EXTENSION_DIR” and changed it to /home/{username}/bin/php5/extensions

I then saved and exited the file.

I then issued the final commands, still in the Imagick directory.

make
make install

After these commands the three programs should be installed. However, since we are using DH’s PHP installation, they are not available to us. In order to fix that we need to create a custom PHP.ini. For this part of the work I followed the excellent PHP.ini instructions on the DH wiki.

First, I created a cgi-bin directory in my domain:

mkdir ~/{domain_name}/cgi-bin

Fill in your proper domain.

Then I copied the default php.ini to the cgi-bin folder:

cp /etc/php5/cgi/php.ini $HOME/{domain_name}/cgi-bin/php.ini

Then I created a script wrapper for the ini:

cat << EOF > $HOME/{domain_name}/cgi-bin/php-wrapper.fcgi
#!/bin/sh
exec /dh/cgi-system/php5.cgi $*
EOF

Then I set permissions:

chmod 755 $HOME/{domain_name}/cgi-bin
chmod 755 $HOME/{domain_name}/cgi-bin/php-wrapper.fcgi
chmod 640 $HOME/{domain_name}/cgi-bin/php.ini

Then I created an .htaccess file for the domain with the following contents:

Options +ExecCGI
AddHandler php5-cgi .php
Action php-cgi /cgi-bin/php-wrapper.fcgi
Action php5-cgi /cgi-bin/php-wrapper.fcgi

The final thing to do is edit that newly copied php.ini file so that it takes note of the new Imagick installation.

For this last part of the job I relied on Adrian Hopebailie’s post on how he installed PECL extensions on his Site5 shared hosting.

Move into the directory with the custom php.ini file and edit with an editor (I used nano again)

cd ~/{domain_name}/cgi-bin
nano php.ini

Find the line:

extension_dir = "./"

and change it to:

extension_dir = "/home/{username}/bin/php/extensions"

Finally, scroll down a ways until you see all the commented out extension commands (commented out with a semicolon) and add the following (with no semicolon):

extension=imagick.so

The imagick.so file, in case you are interested, is in the ~/bin/php5/extensions folder that you created a while back.

With this, everything should be ready to go. One thing I found that was quircky was that I couldn’t restart my DH apache server. I created a phpinfo file to show me if imagick was installed, and for a while it wasn’t. But, after about half an hour or so (I left the computer), I came back and refreshed the phpinfo page and there it was. You can tell if PHP is using your php.ini file if the “Loaded Configuration File” variable in the phpinfo() output is pointing to your custom php.ini file. Imagick will show up further down the phpinfo() output as its own section if everything went as planned.

Finally, with all that done, you can grab the opticrop.php source file from Jue Wang’s site and ftp the file to your server. If you add a test image and visit the url as outlined in Jue’s demo you should see the successful “smart” cropping of the image.

Phew.

Posted in Web Development | Tagged , | 3 Comments

Parsing YouTube Video IDs from Submitted URLs

The following PHP function takes any of several common YouTube URL formats and returns the YouTube video ID.

function youtube_id($youtube) {
  $youtube_id = "";
  $pattern = '@http://www.youtube.com/v/([A-Za-z0-9\-_]+).+?|embed\/([0-9A-Za-z-_]{11})|watch\?v\=([0-9A-Za-z-_]{11})|#.*/([0-9A-Za-z-_]{11})@si';
  preg_match($pattern, $youtube, $matches);
  for ($i=1;$i<=4;$i++) {
    if (strlen($matches[$i])==11) {
      $youtube_id = $matches[$i];
      break;
    }
  }
  return $youtube_id;
}

The following URL formats are supported:

As an added bonus, if the user pastes in the full YouTube embed code this function will still find the YouTube video ID since the embed URL is contained within the rest of the embed code.

Posted in Web Development | Tagged , | Leave a comment

Storing Files Above the Web Root on Media Temple DV

Updated: I have written an update for this post for Media Temple’s DV 4 offering.

We developed a web app recently that allowed users to upload JPEG images to the server. Best practise for uploads says that uploaded files should be stored above the web root so that they cannot be directly addressed. The site was being hosted on a Media Temple DV server and we discovered that it was not straight forward to store files above the web root. This is how we solved the problem.

This solution requires that you have command line access to your Media Temple account. They have a wiki for that.

The first obstacle is that open_basedir is in effect on MT’s servers and as a result PHP has no programatic access to any directory besides /tmp and the web root of the current domain. To rectify this we followed MT’s community wiki article about open_basedir and created a vhost.conf file in our /var/www/vhosts/example.com/conf/ directory (where example.com needs to be swapped out for the actual domain). The contents of our vhost.conf file was:

<Directory "/var/www/vhosts/example.com/httpdocs">
php_admin_value open_basedir "/var/www/vhosts/example.com/httpdocs/:/tmp/:/var/www/vhosts/example.com/storage/"
</Directory>

The key thing, as pointed out in the wiki article, is that you add the path to the storage directory to the end of the open_basedir list of directories. The list has the main web root directory, tmp, and the new storage path. Again, swap in your domain for example.com (I’m not going to mention this anymore). Note that our file differs from Media Temple’s in that we dropped their include_path line since we aren’t PHP including any files.

Of course, we haven’t created the /storage directory yet and it can’t be done through the FTP client since the FTP user does not have permission to create folders above the httpdocs directory. Therefore, use the command line to move to the /example.com directory and create the directory (we named ours storage).

mkdir storage

This is where the tricky part comes in. This newly created directory does not have the same uid as the code that is uploaded to the web server through the FTP client. This is a big deal because even though open_basedir is satisfied, PHP safe_mode is not. A script can only access a directory that has the same uid as the executing script so the /storage directory, as it stands, is off limits.

But, this can changed by issuing the following command:

chown ftp_user_name:psaserv storage

Where ftp_user_name is the username used in the FTP client to log on to the web server. By chowning the directory you are telling the OS that the directory belongs to the FTP user and since all the files that are uploaded through FTP belong to the FTP user as well, they all have the same uid and safe_mode is satisfied.

Make sure that you also issue a

chmod 775 storage

since, even if the directory belongs to the FTP user, it won’t be writeable unless it’s made writable.

Finally, restart apache to make sure everything takes effect.

/etc/init.d/httpd restart

Now we can store files above the web root via PHP.

Posted in Server Stuff | Tagged , , , | 4 Comments

A Dollar Counter Made in PHP and Javascript

Prion Interactive recently completed a site for a client that required a counter that showed a progressive dollar increase from $0 to one billion dollars over a period of six weeks. The solution involved a combination of PHP and Javascript.

Before we began coding we needed to calculate how fast the counter should increment in order to move through one billion dollars in 6 weeks. The promotion period of 6 weeks is 60 seconds x 60 minutes x 24 hours x 7 days x 6 weeks = 3,628,800 seconds long. Dividing one billion dollars by 3,628,800 seconds means that the counter has to increase 275.57 dollars every second.

The promotion the counter was for began on January 1, 2011 at midnight and this time was designated the zero point. In order to make the work easier we converted the start time to the Unix epoch using PHP’s mktime() command, which provides an integer value of the time in seconds.

$start_time = mktime(0,0,0,1,11,2011);

Since the dollar value was increasing throughout the promotion the counter would only be zero at the zero point and that meant that every time the page was loaded we needed to determine the current time as a Unix epoch timestamp and calculate how many seconds had elapsed since the zero point.

$current_time = time();
$elapsed_time = $current_time - $start_time;

Knowing how many seconds had elapsed and knowing the dollar per second rate we could calculate the current dollar value of the counter, formatted as a dollar amount.

$rate = 275.57;
$counter_value = number_format($elapsed_time*$rate, '2', '.', ',');

Printing this result to the screen achieved the goal of having an increasing counter but the value would only be updated when the page was reloaded. The client also wanted the counter to increase after the page had finished loading. This required the use of some Javascript.

The following piece of Javascript was included in the head of the counter page. It uses jQuery to replace the HTML of the div element that holds the dollar value.

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
function addCommas(nStr) {
    nStr += '';
    x = nStr.split('.');
    x1 = x[0];
    x2 = x.length > 1 ? '.' + x[1] : '';
    var rgx = /(\d+)(\d{3})/;
    while (rgx.test(x1)) {
        x1 = x1.replace(rgx, '$1' + ',' + '$2');
    }
    return x1 + x2;
}

var x = <?php print $counter_value;?>;
var interval = setInterval(
  function(){
    if (x < 1000000000){
      x = parseInt(x) + 275.57;
      if (x > 1000000000) {x = 1000000000}
      $("#counter").html("$" + addCommas(x));
    }
  },
1000);
</script>

The first function is for formatting the counter value before it is output.

The meat of the Javascript begins with the var x declaration. This line sets the initial value of the counter and PHP is used to output the calculated counter value at load time.

The next line of code sets an interval that is called every 1000 milliseconds (or 1 second). The function in the interval takes the current value of x and adds the dollar per second value to it. It also checks to see if the maximum value of one billion has been reached and if it has it sets x to be one billion dollars. Finally, the div with id=”counter” has its HTML swapped out for the new dollar value (after it has been passed through the formatting function).

Now we have a page where PHP calculates the current counter value and sets that value in the Javascript and once the page loads the Javascript takes over and increments the counter every second by the proper dollar amount.

A final wrinkle was that the client didn’t like that the counter incremented 275.57 dollars every second and wished that the counter would display a rounder number. We settled on $500 increments and adjusted the code accordingly.

On the PHP side we decide to simply round to the nearest $500. To do this we manipulated the $counter_value in the following way.

$counter_value = ceil($counter_value/500)*500;

To make sure that the Javascript incremented the counter by $500 at a time and that the Javascript value didn’t race ahead of the value PHP would show if the page was reloaded it was necessary to modify the interval time for the setInterval() call. If the Javascript incremented $275.57 in 1000 milliseconds it should take ($500 x 1000 milliseconds)/$275.57 = 1814.42 milliseconds to increment $500 dollars. Knowing this we changed the Javascript interval code to the following (in the interest of reasonableness the millisecond value was rounded down to 1814).

var interval = setInterval(
  function(){
    if (x < 1000000000){
      x = parseInt(x) + 500;
      if (x > 1000000000) {x = 1000000000}
      $("#counter").html("$" + addCommas(x));
    }
  },
1814);

Note that the interval time has been updated and so has the increment value ($500) that is added to x each time the interval fires.

The final result was a counter that counted by $500 and steadily progressed to the maximum one billion dollars at the end of the promotion period.

Posted in Web Development | Tagged , | Leave a comment