Google as last-resort backup
25th of May, 2007 0 comments WWW , WordPress , Google ,
Not long ago, at work, we were painfully close to losing 2.5TB of data belonging to our users due to two faulty disks in the RAID. With two faulty disks in the array, you've basically lost everything and with that amount of data, the problem is not having a recent enough backup (which we did), but rather restoring it -- 2.5TB of data would take days to restore. Fortunately, some of my co-workers were able to keep one of the disks alive long enough to restore redundancy. The first thing I did when I came home that day, was to make an additional backup of all my valuable data and stored it in a safe place.
A few days later, a blog post was deleted by accident from one of the blogs I'm administrating. "No worries, this is why we do backups", I said, in good faith restoring the post from backup would take me about five minutes or less. The blog was running the bundled WordPress Database Backup-plugin and WP-cron to ensure that backups were taken on a regular basis and would require no manual labour from my part. The daily backups would be sent to two different e-mail accounts hosted on two different servers, in case of a disk crash. Needless to say, I didn't worry much when I was told about the blog post had been deleted.
As it turned out, every single backup taken since the last time I updated WordPress was corrupted. Three or four months worth of backup turned out to be of absolutely no use whatsoever. What's worse, is that I had been living in the illusion that everything was working as it should, and that I had a daily snapshot in case something should happen, like a blog post being deleted by accident...
What saved me was Google and its Cache-function. Some cut-and-paste magic and everything was back to normal, the post even got its original timestamp. Comments had been turned off, but if there had been any I could've restored them manually as well.
This approach will work in most cases, given that a search engine has cached your site recently. If you've just posted the entry, chances are that it hasn't been indexed yet and thus no cache has been created. This is likely to be the case for recently posted comments.
Although losing a blog post isn't the worst thing that could happen, it provides a good example of what might go wrong with a backup. With information that's published on the web, there's usually a cached copy of it somewhere, so you're not entirely lost at sea. That's not the case with backup of local files and even though the backup program does its job without any warnings, it being Backup Exec or any home made script, it doesn't necessarily mean that the quality of the backup is good enough.
In school we performed fire drills quarterly, which always seemed like a hassle, but maybe it's not such a bad idea after all and it's certainly something I'll start doing with my backups. Like the saying goes "he who forgets history is destined to repeat it".
Sessions on (mt) Media Temple’s (gs) Grid-Server
24th of April, 2007 5 comments Hosting , php ,
Not long ago, I uploaded latest version of a project I'm working on to my production server at (mt) Media Temple, only to discover that users were kicked out of the system no less than five seconds after logging in.
At first I didn't know what was causing this, so I went through most of my code, searching every tidbit of it in an attempt to find out what was causing this unexpected behaviour. I searched for possible reasons to such an "time out", but couldn't find anything that even seemed remotely plausible. After staring at my code for quite some time, it dawned on me; because of the sheer number of sites hosted at (mt) Media Temple and their (gs) Grid-Server cluster, even with the default settings the sessions initiated would be cleared out and deleted before the users would be able to do anything.
I tried to set the session.gc_probability higher, which didn't work. The same with the lifetime of each session, using the session.gc_maxlifetime, which controls when a session is to be deleted.
The solution
After some more research, I found some rather massive scripts that kept track of sessions using tables and some other really paranoid measures. The solution, however, is actually quite simple, and is stated in the manual as well:
Note: If different scripts have different values of
session.gc_maxlifetimebut share the same place for storing the session data then the script with the minimum value will be cleaning the data. In this case, use this directive together with session.save_path.1
Here is what I did
- Created a directory called
sessionsone level above myhtml-catalogue - Added the following lines to my scripts
< ?php
ini_set('session.gc_maxlifetime', 3600); // Valid for an hour
session_save_path('/sessions/'); // Relative to file system, not URI
session_start();
?>
Anyone know of a better way to solve this issue?
1PHP: Session Handling Functions - gc_maxlifetime
I want out - now
30th of March, 2007 1 comment WWW , Hosting ,
I like to think that 37Signals are a couple of guys who've understood the essence of how to do business and how to make stuff that just works. These guys practically (and literally) wrote the book on how to make a product and service user-centric and customer-centric. They even wrote a book on how they had achieved what they have, so that others could follow their lead. I know of a company that should read that book - and one chapter in particular.
In chapter 12, "Easy On, Easy Off", in their book "Getting Real" one can read the following:
Make signup and cancellation a painless process
Make it as easy as possible to get in -- and get out -- of your app.[...]
You never want to "trap" people inside your product. While we're sorry when people decide to cancel their Basecamp account, we never make that process intimidating or confusing. "Cancel my account" is a link that's clear as day on a person's account page. There shouldn't be any email to send, special form to fill out, or questions to answer. 1
In short; if the customer wants to cancel his or her subscription for whatever reason, they should be able to do so without any hassle. Sounds pretty fair and square to me. How come there are some people who then who apparently thinks it's a good idea to trap their customers in the big company spider web, making it difficult to cancel a subscription.
Back in 2005 I bought a domain and a web service from B-One, a well-known and cheap web host. The plan was to use the domain for a student driven newsletter, powered by a WordPress blog to make it easy to publish news on the fly. Initially, we wanted a different content management system which B-One wasn't capable of powering, but that's a different story. Due to a whole variety of circumstances, the newsletter was closed after a year; we no longer needed the domain or the web service provided by B-One.
Come summer of 2006, I got an invoice from B-One for the domain and hosting plan. I wrote an e-mail (in Norwegian) explaining how we no longer needed the domain and hosting service as the project had ended and I no longer had anything to do with the school. The following day, they reply (in Danish):
If you wish to cancel your service with us, you are requested to send us a confirmation, in writing, 45 days in advance of termination (if you fail to do so, you'll have to pay for a full year). Cancellation can only be done in writing and should contain the following information: domain name, order number, name and address, as well as the owners signature.2
Needless to say, but I was quite baffled by this reply, not quite understanding what the problem was. I no longer needed nor wanted the service I bought a year ago and wanted to cancel my subscription so that I wouldn't have to pay for yet another year. Long story short; I ended up paying for yet another year of total inactivity.
Ever since, I've been thinking about getting around to write a short statement canceling my subscription, signing it and fax it to B-One (who has a fax anyway?). Mid February I finally got around doing it. However, it turns out that the fax-number that's posted on their homepage doesn't work. I tried three-four times. No dice. So I go on a treasure-hunt in my apartment to find my old flat bed scanner; I search high and low on the internet after a Mac OS X-driver for it, which turns out to be a Mac OS 9 plugin for Adobe Photoshop - but it works; I scan a neatly written document with the aforementioned information and my signature; save it as a PDF and uploads it to One.com's (former B-One) contact form with a short text stating that I would like to cancel my subscription with them.
This is what happened next:
- Submit cancellation at One.com
- I receive an e-mail with a confirmation link in
- Fill in e-mail and password (which I had to dig up from an old e-mail)
- Receive confirmation that my cancellation has been recorded
Finally! I thought, but no...
We have received your notice of cancellation of your web space for
domain istezine.com.Your web space will be terminated on 8/16/07.3
The web space won't be terminated for yet another six months, but this time I'm just happy it'll happen.
Now, I wonder; what's so difficult with making my web space disappear that it'll take six months? Why make your customers go through all this hassle only to cancel their subscription?
137 Signals. "Easy On, Easy Off." Getting Real. 25 Oct 2006. 37 Signals, 22 Mar 2007 <http://gettingreal.37signals.com/ch12_Easy_On_Easy_Off.php>.
2My translation, the reply is shortened from its original form.
3Again my translation and shortened from its original form.
Simple PHP-login script using session and MySQL
11th of March, 2007 71 comments php ,
I came across a short tutorial on how to make a session based login script, using PHP and MySQL. However, I felt that the tutorial did have several shortcomings in terms of its code, and so I felt like writing a short post on how I would go about doing the same thing, elaborating the things I would do differently.
This post has been updated since it first was posted
You can download all the files from this tutorial by visiting http://hvassing.com/wp-content/uploads/simple-login/
PHP can do several nifty things, one of them is talking to a MySQL database server, another is using sessions. Session are useful when we want to keep track of users as they roam through a site. Using session, we can make sure that only those who have a valid username and password can gain access to a specific part of a site, or in the case of any online store; keep track of what is in the shopping chart at any given time.
Session support in PHP consists of a way to preserve certain data across subsequent accesses. This enables you to build more customized applications and increase the appeal of your web site.
A visitor accessing your web site is assigned a unique id, the so-called session id. This is either stored in a cookie on the user side or is propagated in the URL.1
A session can not be written to after there has been any form of output on screen. This means that all session-related code should be above all HTML-code, or any code that'll produce any output. Any error messages from any other script before the session is set, will result in the session not being set.
We'll create six files, which will do all the hard work for us
- db.php
- functions.php
- manage-check.php
- index.php
- logout.php
- members-only.php
The MySQL-table
CREATE TABLE IF NOT EXISTS `members` (
`ID` mediumint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL DEFAULT "",
`user_password` char(40) NOT NULL DEFAULT "",
PRIMARY KEY (`ID`, `username`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
Using mediumint(5) UNSIGNED we allow up to 16 777 215 entries, which is probably way too much. As a rule of thumb; it is always wise to use the smallest applicable type (tinyint rather than int if the value would never go beyond 255 unsigned).
| Name | Information |
|---|---|
| BIT[(M)] | M indicates the number of bits per value, from 1 to 64. The default is 1 if M is omitted. Before 5.0.3, BIT is a synonym for TINYINT(1). |
| TINYINT | The signed range is -128 to 127. The unsigned range is 0 to 255. |
| SMALLINT | The signed range is -32 768 to 32 767. The unsigned range is 0 to 65 535. |
| MEDIUMINT | The signed range is -8 388 608 to 8 388 607. The unsigned range is 0 to 16 777 215. |
| INT / INTEGER | The signed range is -2 147 483 648 to 2 147 483 647. The unsigned range is 0 to 4 294 967 295. |
| BIGINT | The signed range is -9223372036854775808 to 9223372036854775807. The unsigned range is 0 to 18446744073709551615. |
The table above can be found in its entirety at MySQL Developer Zone. Chose the type that is works best for your application, remember that scaling is usually not a problem, and when it is; you'll have the resources to deal with it.
You don't have a scaling problem yet
"Will my app scale when millions of people start using it?"Ya know what? Wait until that actually happens. If you've got a huge number of people overloading your system then huzzah! That's one swell problem to have.2
Moving along; we'll use Sha1 to encrypt the password, we know that it'll be exactly 40 characters long, which means that char will be the best choice. varchar uses less space when the data has a variable length , as is the case with the username, but char is faster and there's no space to save in this case due to the constant length of the password hash. Using Sha1, we get that "Apples and diamonds" becomes a 40 character long string: "45d4e61ff429b6b61a52204a89d304f625e303d4".
The ID and the username are the two columns that we'll access most frequently, so it's a good idea to make indexes of the two of them.
We'll call the password field "user_password" instead of "password", which is a field type and thus is easily confused.
The HTML-form
index.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Member Login</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body>
<form name="login-form" id="login-form" method="post" action="manage-check.php">
<fieldset>
<legend>Member Login</legend>
<dl>
<dt><label title="Username">Username: <input tabindex="1" accesskey="u" name="username" type="text" maxlength="100" id="username" /></label></dt>
</dl>
<dl>
<dt><label title="Password">Password: <input tabindex="2" accesskey="p" name="password" type="password" maxlength="14" id="password" /></label></dt>
</dl>
<dl>
<dt><label title="Submit"><input tabindex="3" accesskey="l" type="submit" name="submit" value="Login" /></label></dt>
</dl>
</fieldset>
</form>
</body>
</html>
That should do the trick.
The actual login
manage-check.php
< ?php
session_start();
include('db.php');
if(isset($_POST['submit'])) :
// Username and password sent from signup form
// First we remove all HTML-tags and PHP-tags, then we create a sha1-hash
$username = strip_tags($_POST['username']);
$password = sha1(strip_tags($_POST['password']));
// Make the query a wee-bit safer
$query = sprintf("SELECT ID FROM members WHERE username = '%s' AND user_password = '%s' LIMIT 1;", mysql_real_escape_string($username), mysql_real_escape_string($password));
$result = mysql_query($query);
if(1 != mysql_num_rows($result)) :
// MySQL returned zero rows (or there's something wrong with the query)
header('Location: index.php?msg=login_failed');
else :
// We found the row that we were looking for
$row = mysql_fetch_assoc($result);
// Register the user ID for further use
$_SESSION['member_ID'] = $row['ID'];
header('Location: members-only.php');
endif;
endif;
?>
The rest...
db.php
Before we can do any calls to your database and retrieve any information from our tables, we'll need to connect to the database. This is best done in a separate script, with the database username and password defined as constants, which will make sure that they are not easily hijacked and that they won't be echoed on screen in the unlikely event of the database connection failing. This is what the aforementioned db.php should look like.
< ?php
define('SQL_USER', 'username');
define('SQL_PASS', 'password');
define('SQL_DB', 'database');
// Create a link to the database server
$link = mysql_connect('localhost', SQL_USER, SQL_PASS);
if(!$link) :
die('Could not connect: ' . mysql_error());
endif;
// Select a database where our member tables are stored
$db = mysql_select_db(SQL_DB, $link);
if(!$db) :
die ('Can\'t connect to database : ' . mysql_error());
endif;
?>
For each document that's a part of your secure site, we'll need to check whether or not the user is logged in. This little snippet will ensure that users who are not logged in, won't get to see this particular page. That in mind; do not put this in top of your login page.
< ?php
session_start();
if(!session_is_registered('member_ID')) :
header('Location: index.php');
endif;
?>
members-only.php
This is the page which only members get to see
< ?php
// Start a session
session_start();
// Sends the user to the login-page if not logged in
if(!session_is_registered('member_ID')) :
header('Location: index.php?msg=requires_login');
endif;
// Include database information and connectivity
include 'db.php';
// We store all our functions in one file
include 'functions.php';
?>
< !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
Welcome < ?php print user_info('username'); ?>
logout.php
Making sure that our users can securely log out when they are done, we need to invoke session_unregister().
< ?php
session_start();
if(true === session_unregister('member_ID')) :
header('Location: index.php?msg=logout_complete');
else :
unset($_SESSION['member_ID']);
sleep(3);
header('Location: index.php?msg=logout_complete');
endif;
?>
functions.php
functions.php will only serve as a place to store functions, which, after a short while with developing, tend to grow quite big.
< ?php
function user_info($field='') {
// If $field is empty
if(empty($field))
return false;
// Check to see if we're allowed to query the requested field.
// If we add other fields, such as name, e-mail etc, this array
// will have to be extended to include those fields.
$accepted = array('username', 'user_password');
if(!in_array($field, $accepted))
return false;
// Poll the database
$result = mysql_query("SELECT ". $field ." FROM members WHERE ID = ". $_SESSION['member_ID'] .";");
// If we don't find any rows
if(1 != mysql_num_rows($result)) :
return false;
else :
// We found the row that we were looking for
$row = mysql_fetch_assoc($result);
// Return the field
return $row[$field];
endif;
} // end user_info
?>
That's about it! Of course, this could be taken even further, using tables to keep track of all session ID's to safeguard against session hijacking and so on, so forth.
Let me know if you find any mistakes or don't get it to work.
1Achour, Mehdi et al. "Session Handling Functions." PHP Manual. 28 Feb 2007. PHP, 11 Mar 2007 <http://php.net/manual/en/ref.session.php>.
237 Signals. "Scale later." Getting Real. 25 Oct 2006. 37 Signals, 11 Mar 2007 <http://gettingreal.37signals.com/ch04_Scale_Later.php>.