Once upon a time there was a reasonably popular
web-based chat room called Star Trekker chat. I
happened into this chat thanks to a friend and
even though
Star Trek
fans were hardly my favourite group of people I
found that for the most part people in there were
friendly and fun. But when Star Trekker shut down,
thanks to its Perl backend eating server resources
for lunch, these happy and kindly people were left
with nowhere to go. It was fortunate that at that
time I opened my own similar chat room and managed
to attract much of the homeless traffic from Trekker.
Wary of the resource problems caused by Perl, I
was pleased when a friend introduced me to PHP.
This particular design of web-based chat uses
variables posted from a form, processes them into
HTML and writes them to a file. Put the form and
the message file in a frameset and you have something
that looks reasonably like a
BeSeen
chat room. Of course the advantage is, our chat room
can be a little more clever than it's BeSeen cousin.
<form action="chat.php3" method="post">
Name : <input type="text" name="name"><br>
Message : <input type="text" name="message"><br>
<input type="submit" value="Send">
</form>
This is your basic form input. You'll probably want
to pretty it up more than that, but to all intents
and purposes, this is that you're dealing with. It
sends two variables through to chat.php3
called $name and $message.
Before we deal with those variables, however, we
need to extract the current contents of the message
file, otherwise we'd only see one message at a time.
Hardly a way to conduct a conversation. Being
familiar as I am with the structure of my own message
file, I know that each message is terminated by a
newline character. This means I can use the
file() function to read the message
file into an array.
The message file is 12 lines long. Of those 12 lines,
the line 1 is a set of headers, lines 2-11 are old
messages and line 12 contains my footers.
All I am interested in is obtaining a string that
contains most of those old messages.
<?php
// Read file into an array
$message_array = file("messages.html");
// Compile the string
for ($counter = 1; $counter < 10; $counter++) {
$old_messages .= $message_array[$counter];
}
?>
When compiling the string, I initiated the
for loop with $counter = 1
not $counter = 0 as is common. This
is because I know that element 0 of
$message_array contains my headers and
I don't want those. Also, by setting the loop
condition to $counter < 10 means that
only elements 1 thru 9 of the array are read into
the string. Of the other two elements, 11 contains
my footers and 10 contains the oldest message.
Both of which I want to remove so I only ever
have 10 messages on screen at any given time.
Altering the $counter < 10
expression allows you to vary the amount of
messages retained.
Now I have my old messages I want to make the new
message. We have our two variables $name
and $message so writing a new message
string is easy.
<?php
$new_message = "$name : $message<br>\n";
?>
We're nearly ready write our message file. All we
need are headers and footers. Start simple with
the headers:
<?php
// It's important that there are no newline
// characters except at the end of the string.
// This keeps all the headers together.
$header = "<html><body bgcolor=\"#000000\" text=\"#ffffff\">\n";
?>
We want the message screen to auto refresh so people
viewing the site can see new posts. In preference
to using JavaScript, I use an META refresh,
principally because it's more likely to be supported
client-side. I also don't want the search engines
indexing my message file. So we refine $header
to :
<?php
$header = "<html><head><meta http-equiv=\"refresh\" content=\"8\">".
"<meta name=\"robots\" content=\"noindex\"></head>".
"<body bgcolor=\"#000000\" text=\"#ffffff\">\n";
?>
In the file footer I tend to put a little copyright
information as well as close the tags I opened in the
header.
<?php
$footer = "<p align=\"center\"><font color=\"#000000\">".
"© Mike Hall 2000</font></p></body></html>";
?>
Wrapping the copyright in <font color="#000000">
means that unless selected it'll be invisible against
the equally #000000 background. This just
stops it being intrusive.
Now we finally have all we need to write the new file :
<?php
// Opens file for writing and truncates file length to zero.
$open_file = fopen("messages.html", "w");
// write file header...
fputs($open_file, $header);
// ... new line...
// (stripSlashes because we don't want all
// our escape characters appearing in the
// message file)
fputs($open_file, stripslashes($new_message));
// ... old lines ...
fputs($open_file, $old_messages);
// ... and footer.
fputs($open_file, $footer);
// Close the file when you're done. Don't forget to wash your hands
fclose($open_file);
?>
So we now have a very very basic web chat. Let's look
at some of the features.
<form action="chat.php3">
Name : <input type="text" name="name"> Color: <input type="text" name="color"><br>
Message : <input type="text" name="message"><br>
<input type="submit" name="Send">
</form>
We've added a new input to the form, meaning we get
a nice new variable to play with in the script. We
read the old messages as before, but in compiling
the new one, we use a little more HTML.
<?php
$new_message = "<font color=\"$color\">$name : $message</font><br>\n";
?>
And while we're thinking about it, we'll add a
few more bells and whistles.
<?php
$time = date("H:i");
$new_message = "<font color=\"$color\"><b><i>$name</i></b>".
" <font size=\"1\">($time)</font> : $message</font><br>\n";
?>
Now we're getting somewhere in terms of design. Another
feature the regulars at my chat room enjoy is the
ability to display email and URL link icons in their message.
Two more form inputs were incorporated and the links processed
thus :
<?php
if($url)
$link_html .= " <a href=\"$url\" target=\"_new\">".
"<font face=\"wingdings\">2</font></a>";
if($mail)
$link_html .= " <a href=\"$mail\" target=\"_new\">".
"<font face=\"wingdings\">*</font></a>";
$new_message = "<font color=\"$color\"><b><i>$name</i></b>".
" $link_html <font size=\"1\">($time)</font> : $message</font><br>\n";
?>
Again, we could just could just leave things at
that, but there are certain security issues.
What is to stop someone entering nasty HTML
into the message box? A little JavaScript?
A little VBScript? Even something as simple as a
5,000k JPEG image can do harm. Refreshing every
eight seconds on the screens of heaven-knows how
many people across the globe. Could be murder
on your bandwidth - not something we want.
We could remove all HTML and PHP elements using
the strip_tags() function, but I
want the chatters to be able to use basic HTML
in their posts. Basic elements like <i>,
<b> and <font> that can be
used to spruce up a message.
For almost two years I used a complicated series
of regex statements to screen out the nasty HTML.
However I found that I was more or less constantly
adding to this filter, until it was taking up most
of my code! Frustrated by inefficient code I was
again rescued by a friend who suggested approaching
the problem from the other direction. Instead of
telling the script what HTML it can use, tell it
what it can't.
htmlspecialchars() is a much
under-used PHP function. It replaces certain
characters with their HTML entities.
So " becomes ",
& becomes &,
< becomes <
and > becomes >.
By running the $new_message variable
through htmlspecialchars() I turn ...
<iframe src="http://www.microsoft.com">
... into ...
<iframe src="http://www.microsoft.com">
... rendering it useless. A series of string
replace functions can then re-enable certain
tags. Then comes the clever part. We use
str_replace() to undo some of
what htmlspecialchars() did.
<?php
$message = htmlspecialchars($message);
$message = str_replace(">", ">", $message);
$message = str_replace("<b>", "<b>", $message);
$message = str_replace("</b>", "</b>", $message);
$message = str_replace("<i>", "<i>", $message);
$message = str_replace("</i>", "</i>", $message);
$message = str_replace("<font ", "<font ", $message);
$message = str_replace("</font>", "</font>", $message);
?>
And so on. There are cleverer ways of doing this using
eregi_replace() but I don't want to
complicate matters.
We have to make sure we run the $name,
$color, $url
and $mail through this filter too, otherwise the
malicious users can enter code that way. Save yourself work and bundle
the filter off in a function.
<?php
$name = filterHTML($name);
$message = filterHTML($message);
$color = filterHTML($color);
$url = filterHTML($url);
$mail = filterHTML($mail);
?>
One last thing we should address is how to deal with
troublemakers. This is a particular problem if you
end up with a popular chat. It's a sad fact we have
to face up to - people are frequently jerks. And
because of this we have to make sure that only the
right kind of people get into our chat room.
One idea is a login system. Store usernames and
passwords in a MySQL database and make users register
before they can access your chat. The other idea is
to log the IP of troublemakers and prevent that
IP posting.
This second system is flawed to a certain extent, in
that malicious users can switch between any number of
proxies to change their IP. And as most ISP's assign
dynamic IP addresses, even the stupid ones can just
reconnect and get access to the chat.
Most "casual" troublemakers won't be bothered about
going to all that effort just to put the wind up a
handful of individials. Once "banned" they'll never
bother coming back.
So our "banned" IPs are logged in a file called
banned.ban. Each IP is terminated by
a newline character so as before we can use the
file() function to read the file into
an array.
$banned_array = file("banned.ban");
Now we have the file we need to cross-reference it
with the $REMOTE_ADDR variable so we
can tell if the user trying to post a message is
banned or not. Simplicity itself :
<?php
for ($counter=0;$counter<sizeof($banned_array);$counter++) {
if ($banned_array[$counter] == $REMOTE_ADDR) {
print("<font color=\"red\" face=\"arial\" align=\"center\">".
"You have been banned from this chat</font>");
exit;
}
}
?>
The exit command will stop immediately
the execution of the script. Place your ban checks
before you start performing operations on the POSTed
variables and your banned user can't use the chat.
With a mind to accounting in some way for the problem
of dynamic IP addresses, it's probably an idea to
check the IP block the IP belongs to. A simple
function makes makes this easy.
<?php
function makeMask($ip) {
// remember to escape the . so PHP doesn't think it's a concatenation
$ip_array = explode("\.", $ip);
$ip_mask = "$ip_array[0]\.$ip_array[1]\.$ip_array[2]";
return $ip_mask;
}
?>
Then we replace the looped if with:
<?php
for ($counter=0;$counter<sizeof($banned_array);$counter++) {
if (makeMask($REMOTE_ADDR) == makeMask($banned_array[$counter])) {
print("<font color=\"red\" face=\"arial\" align=\"center\">".
"You have been banned from this chat</font>");
exit;
}
}
?>
... we have some protection against dynamic IPs.
Finally we need a way to get the troublemaker's IP in
the first place. I do this by logging $name
and $REMOTE_ADDR in a file called
iplist.html. At a separate, secret
URL I can view the message and monitor the IP
addresses at the same time. This has the added
bonus of being able to spot impersonators - a
common crime in these places.
iplist.html is created in much the same
way as messages.html. First we extract
the current values from iplist.html,
we strip out the header, footer and oldest IP record
and then create a new record, new header and new
footer. To make the layout more clear, I used
a table.
<?php
$header = "<html><body bgcolor=\"#000000\" text=\"#ffffff\"><table border=\"0\">\n";
$footer = "</table></body></html>";
$new_ip = "<tr><td>$name</td><td>$REMOTE_ADDR</td></tr>\n";
$ip_array = file("iplist.html");
for ($counter = 1; $counter < 20; $counter++)
$old_ips.= $ip_array[$counter];
?>
Simply write that to the disk the same way as we
did the message file and there we have it. A simple
web-based chat room. Better cross platform
compatibility than Java and no need for anything
more than a web browser - I'm told that even the
Dreamcast
works with this!
Somethings you might want to try yourself include
combining common pieces of code into functions,
writing a script that will automatically add troublemakers
to the banned list and writing a regex expression
that scans a message text for URL's and e-mail
addresses and automatically turning them into
likes (as Outlook Express and ICQ do).
Play around, have fun, experiment. I did. This is
how I started in PHP and now I've made a career of it.
Happy Chatting.
--Mike