My last post was all about Second Life's Linden Scriptng Language (LSL) and how to add LSL scripts to our objects. We built a simple elevating platform that moved along the z-axis when touched by an avatar. This week I will continue building on this example, introducing new LSL concepts along the way.
Inter-Object Communication
Currently, our elevator starts moving after being touched by an avatar but this is not very realistic. Most elevators move when someone presses a call button and I want to replicate this behavior on my virtual counterpart. The first thing I need is a call button. Since the aim of this blog is to demonstrate LSL rather than creating beautifully crafted objects a simple prim will do as a call button. I need to add a script to my call button to notify my elevator that it should start moving. One way of doing this is to broadcast a message over a specific channel and set the elevator to listen to messages coming from that channel. Here's the button's script:
Pretty simple. All the script does is broadcast the "elev-mov" message over channel 1360 whenever the object is touched. We now need to configure our elevator to listen for this message:
The first line in the state_entry script for the default state sets the object to listen to any messages coming in on channel 1360 from an object named "es1" ("es1" is the name I gave the call button). I could also configure the object to listen for messages coming from another object that has a specific Universally Unique Identifier (UUID)but I must admit I haven't yet found a way to do this reliably. Our elevator is now set to listen for messages coming from the call button. We now need to take action on those messages. To do this we add a 'listen' event to our default state script and add the required code. In this case the elevator checks the channel (1360), source (call button) and message and starts moving. You might notice that using this technique, your object could be configured to listen for messaged from multiple objects over multiple channels and react accordingly. I'm not too sure about performance though.
Linking Objects
So now we have a platform that moves when the call button is 'pressed', but it's only a platform. What if we wanted to add 'walls' to this platform to make it look more like an actual elevator. Simply placing these walls on top of the platform will not make them move with it when the call button is pressed! Here's where linking objects comes in handy. To link the walls to the floor hold down the shift key and click on the walls and floor in succession to select all the objects. It is important to select the floor last and I will explain why in a minute. After selecting all the objects click the 'Link' button in the "Build Toolbox" to link the objects together. Now when the call button is pressed, the walls will move together with the platform for a more realistic looking elevator. This will not work unless the floor was selected last when linking the objects because the floor's script would not be set as the parent script of the group.
Final Thoughts
That's it for this week and for my coverage of Second Life. There's much more to Second Life and Linden Scripting Language than I have covered over the past couple of posts, in fact I've just barely scratched the surface. The fact that there's much to discover in Second Life is, for me, beside the point. The real issue for me is how willing am I to scratch below the surface and dig deeper into this virtual world. The answer I'm afraid is I'm not. Although I can appreciate the effort it took to create such a world and admire the vision behind it I was sincerely let down by the whole experience. Second life looks dated (most places I've been to anyway) and feels sluggish. I'm running this on a Core i5 processor and a GeForce GTS 250 graphics card over a 10 megabit connection but it still feels slow. I don't want to sound too negative here, as I said I do admire the vision behind it and what can be achieved within it but I'm afraid that I will not log-back into it any time soon.
Welcome back. Today’s post is all about the Linden Scripting Language and how we can use LSL to make objects respond to simple commands. As with any form of programming, the best way to explain LSL concepts is through practical examples, so I've chosen to build a simple elevator. The elevator will respond to “Touch” events by move vertically between pre-defined minimum and maximum z-axis values.
In order to focus on the actual scripting, I will not spend too much time building and fiddling with the design of the elevator. A simple rectangular platform will do nicely for this example.
Adding a Script
The first thing to do is to add an actual script to our platform. To do this, go to the “Content” tab of the build toolbox and click the “New Script” button. Double-click the newly added script object to open the script editor which should contain the following default script:
LSL is an event-driven language based on finite state machines. This means that every object in second life can have a pre-defined number of states and can transition from one state to another by reacting to events. All objects must have at least one state, the default state which is was we can see in the listing above – defined by the ‘default’ keyword. This script also contains two event handlers: ‘state_entry’ and ‘touch_start’. ‘State_entry’ is called whenever the object enters the current (in this case default) state, which in turn calls the LSL pre-defined ‘llSay’ function which basically broadcasts the “Hello Avatar!” message on channel 0, the public channel. On the other hand, ‘touch_start’ is triggered whenever the object is touched while in the default state, broadcasting “Touched” on the public channel.
Building our Elevator script
Going back to our example, our elevator can be in either one of two states: stationary (default) or moving. We want our elevator to start moving when it’s touched and we want it to stop moving when it gets to the target position. Here’s the basic skeleton for our script:
default
{
state_entry()
{
//Determine the next Target position based on the current position
}
touch_start(integer total_number)
{
//Go to the moving state
}
}
state moving
{
state_entry()
{
//Start Moving and stop when target is reached
}
}
We need four global variables to store values for the minimum and maximum Z-axis positions, the speed at which the elevator should move and the target position (up or down). These variables are placed at the very top of the script and given their default values. When entering the default state we want to set the target position according to the current position. In other words, if the elevator is currently at the bottom, the target will be the top and vice-versa. We also want the elevator to enter the ‘moving’ state when an avatar touches it:
integer MinZ = 23; // Minimum Z position (bottom)
integer MaxZ = 27; // Maximum Z position (top)
integer DeltaZ = 1; // Rate of change of Z (speed)
integer TargetZ; // Target Z position
default
{
state_entry()
{
vector position = llGetPos();
if (position.z >= MaxZ)
{
TargetZ = MinZ;
}
else
{
TargetZ = MaxZ;
}
}
touch_start(integer total_number)
{
//Go to the moving state
state moving;
}
}
state moving
{
state_entry()
{
//Start Moving and stop when target is reached
}
}
At line 10 the script calls the llGetPos() function that returns the current position of our object as a vector. A variable of type vector holds three floating point numbers for X, Y and Z values which in this case are our object’s position in 3D space (in metres). The current Z position is then used to determine the elevator’s next target position.
Animating Movement
In the moving state we could of course set the elevator’s position to the current target but that would not be very realistic. An elevator does not simply jump from one floor to another, it gradually moves between floors until the target is reached. To replicate this effect we need some sort of animation to smoothly transition the elevator from the current position to the target. There are multiple ways of doing this but I decided to use a timer event to control movement. Let’s have a look at the code for the moving state:
state moving
{
state_entry()
{
llSetTimerEvent(0.1);
}
timer()
{
vector position = llGetPos();
if (position.z < TargetZ) //Moving Up
{
if (position.z + DeltaZ > TargetZ)
{
position.z = TargetZ;
}
else
{
position.z = position.z + DeltaZ;
}
}
else // Moving down
{
if (position.z - DeltaZ < TargetZ)
{
position.z = TargetZ;
}
else
{
position.z = position.z - DeltaZ;
}
}
llSetPos (position); //Set the new position
if (position.z == TargetZ) // Target reached
{
llSetTimerEvent(0); //Cancel the timer
state default; // Goto the default state
}
}
}
This code might look slightly complex but in actual fact it’s fairly straight forward. When the object enters the moving state it calls the ‘llSetTimerEvent()’ function to trigger a timer event every tenth of a second. The timer event then does the actual movement. First it gets the object’s current position with the ‘llGetPos()’ function which is then compared to the target. If the current Z position is less than the target it means that the elevator is moving upwards and vice versa. Depending on the direction of movement, the value of ‘DeltaZ’ – the rate of change of vertical position (speed) – is added or subtracted from the current position. The script also makes sure that the maximum and minimum positions are not exceeded. After calculating the new position, the ‘llSetPos()’ function is called to ‘move’ the elevator to the new position. When the elevator reaches its target, the timer is cancelled and the object is moved back to the default state. And that’s it. Now, when the elevator is touched it will gradually move to its destination where it will stop until it is touched again to bring it back.
Further Improvements
Of course there are lots of ways to improve the script. For instance floating point numbers could be used for DeltaZ to make the animation smoother. The elevator itself could be designed much better rather than a simple platform by linking various prims together and use messaging to make them move as a single unit. I’ll be looking into messaging and linking in a future post where we’ll see how we can create a call button for our elevator.
Conclusion
Although building and scripting is one of the most enjoyable things in Second Life (for me at least) it can also be very, very frustrating at times. Given that scripts are compiled and run on the server, each time you modify a script it could take a while for the change to take effect depending on the amount of lag being experienced. When testing a script you usually need to continuously correct or tweak the code and having a bad response time can make this a rather frustrating experience. Also, the risk of being disconnected from second life without warning is also there (happened to me a couple of times) so I had to resort to writing code on a local text-editor and pasting it into Second Life to make sure I always had a backup I could fall back to. Although I have only begun to scratch the surface, it is already apparent that scripting in second life can be pretty powerful.
This week we shall have a look at how to build (very basic) objects in Second Life and how we can use scripts to add interactivity to these objects.
So let’s get straight down to business…
Building Basics
Second Life provides tools that enable you to create whatever your imagination conjures up. The only limits are practice and your own imagination! The building blocks for most everything in second life are known as Primitives or ‘Prims’ for short. Prims are basic shapes, such as cubes and spheres, provided by the building tools that can be combined together to create the larger more complex shapes in your creation. In total, there are around 15 prims of different shapes at your disposal.
To start building, you must be in an area in which you are allowed to build. This could be your own land, someone else’s land (if the owner gives you permission) or a sandbox, a public area where people can build stuff to their heart’s content. One thing to note about sandboxes is that the objects built in them are only temporary since sandboxes are periodically cleaned by their owners. To avoid losing your creation, make sure to save it in your inventory!
Editing Objects
So, we know where to build and with what to build but how do we actually build stuff? To start building, right-click anywhere on the ground and select “Build” from the context menu. This brings up the build toolbox shown below:
With the magic wand tool selected, choose the desired prim shape and click anywhere on the ground to create the object, a process known as “rezzing” a prim. Now that we have created our basic object we can go ahead and change pretty much anything about it. An object can be moved, rotated, resized, hollowed out, twisted, tapered… the list goes on. You can also change the texture of the object (the default texture is wood).
To move an object, select “Move” from the toolbox and use the arrows provided to move the object in the desired direction. Similarly selecting “Rotate” and “Stretch” from the toolbox will allow you to rotate and resize an object in 3 dimensions. The X, Y and Z axis’ in Second Life are colour-coded Red, Green and Blue respectively and the control surfaces provided to manipulate the object follow this standard as illustrated below:
Moving an object
Rotating an object
Resizing an object
You can also manually set numerical values for moving/rotating/re-sizing objects rather than using the control surfaces for more precise control.
Building a Chair
A chair is a good candidate to demonstrate how to use the build tools to create something that is actually useful. My chair will have four wooden legs and fabric-covered seat and back, nothing too fancy. Here’s the finished product:
The finished product
The first step is to create all the basic components of the chair independently of one-another starting with the seat. The seat is made from a cube prim which I flattened into the correct shape. I then took a copy of the seat and rotated it around the x-axis to make the back. I also re-sized the back to make it slightly narrower than the width of the seat. Finally I used another cube prim to create the first of the legs which I copied 3 times to create the set. At this point I should mention some pretty handy shortcuts I found for copying and rotating objects which make the process much quicker than having to go back to the build toolbox each time. To copy an object make sure it is selected, then hold down the ‘shift’ key and click and drag away from the object. This will create an exact copy of the selected object. To rotate an object, simply hold the ‘Ctrl’ key and use the rotation control surfaces to rotate along the desired axis. Here’s what the chair looks like before assembly:
The chair components
Before assembling the chair however I wanted to assign different textures to the various components. I wanted my chair to have a fabric seat and wooden legs. To change the texture of the currently selected part(s), goto the “Textures” tab in the build toolbox and click on the wooden texture thumbnail. This will open up the “Pick Texture” dialog box from where you can browse through all the available textures. I chose to go with the standard “Fabric Linen” texture found under the “Fabrics” folder.
Applying a texture
Similarly, a wooden texture is applied to the chair's legs.
The final step is assembling the chair, starting from the legs. To make it easier to see what I was doing, I opted to position the legs while the seat was still on the floor. I also made use of the “Snap To Grid” option to help me better position the legs relative to the seat. Snapping to the grid also made it much easier to equally space the legs.
Snapping to grid makes it easier to align objects
When I was happy with the position of the legs, I simply slid the seat along the Z-axis to bring it above them and correctly positioned (and slightly tapered) the back. Finally I selected all the parts and linked them together to make them behave like one object. Job done.
Introduction to Scripting
All scripts in Second Life are written in Linden Scripting Language, LSL for short, which is syntactically similar to C and Java. LSL scripts are interpreted and executed on the Second Life servers which send the results back to the viewer. LSL is state and event driven, meaning that an object can be in one of many states and transitions from one state to another by reacting to events. For instance a door can be open or closed (states) and can transition from one state to the other after being touched (event).
In my next post we will have a closer look at LSL and how we can add scripts to our objects. We shall also see how we can add interactivity to our objects by making them respond to commands.
This week’s post is all about Second Life, an online virtual world created by a company called Linden Lab. Good or bad, I had never heard about Second Life before starting this course. Considering that as of 2011 Second Life has more than 20 million registered user accounts (Wikipedia) I had to ask myself how I could have missed it. I was never a fan of Massively Multiplayer Online Role-Playing Games (MMORPG) so that could explain why, but that’s beside the point. More importantly Second Life is more than just an MMORPG, firstly it’s not really a game and secondly it’s much more massive… from all angles. Its name says it all, Second Life allows you to lead a second, virtual life online.
I must admit that I found the prospect quite scary (for lack of a better word), and this was before I downloaded the client software or created an account. I mean scary in the sense that there is so much one can do and see in Second Life that it’s quite frankly overwhelming. But it only really hits you once you get in.
Creating An Account
I started off by creating an account, which is a task in itself. With more than 20 million registered users, trying to find a good user id is like looking for a particular pixel on the Second Life Grid (a needle in a haystack is easy in comparison). I have an obsession with my user/character names, I want them to be different from my real name, sound cool (to me at least) and make some sort of sense. Putting these constraints to the already limited possibilities was not helping. Each user name I tried was taken and what I found rather annoying was the fact that the website did not suggest alternatives. Those of you who follow my blog will know that I’m a stickler when it comes to User Experience (UX) and will understand my annoyance at such things. I finally settled for the name “Wyder” which stands for “Wayne” (my real name) “derivative” and which was available. Next I downloaded and installed the Second Life Viewer and I was good to go.
Virtually There
After logging in I found my virtual self (Avatar) on Welcome Island, a region on the Grid aimed at getting newbies like me up and running in Second Life within a claimed 10 minutes. Welcome Island lived up to its promises and I got a hang of the basic controls well within those 10 minutes, including walking/flying around, chatting, basic camera controls and object interaction. With the pleasantries out of the way it was time to take the plunge into the Grid proper. I popped out the destination guide and I was surprised at the sheer number of locations available. I spent the good part of an hour teleporting between locations, spending a few minutes in each just to get a feel for the interface and to assess performance across locations. It was immediately apparent that a good internet connection and even better hardware is required to experience this virtual world at its best.
Customising the Avatar
I also attempted to change the look of my avatar and was impressed with the level of customization that is available to you. It did take some trial and error to decipher what each setting did (I still cannot figure out how to change the eye colour) and to realize that your hair can be changed only by acquiring new hair styles (at least that’s what I concluded). I also found some preset ‘looks’ in the inventory but to my horror, the presets re-set the facial features I had painstakingly tweaked to my liking just before. Saying I was frustrated is an understatement, so I set everything back to default and let the whole customization thing be for a while. I’ll try again another time.
Conclusion
I have to say that I did feel quite a bit disorientated, out of place even, while logged into Second Life. There’s so much to take in that it will take me several other sessions to feel comfortable enough. What does Second Life tell me? Well, I don’t want to pass judgment just yet as I still have to get over my baby blues J so I’ll leave all philosophical considerations for another post. I can say however that I am looking forward to building stuff in this virtual world and take a look at Linden Scripting Language to see what I can accomplish. In the meantime, I’ll concentrate on getting a better hang of the interface, the places, the people and the life!
As promised, in this fourth (and final) post of the series I will explain how I went about building the Web Space Manager itself.
I must admit that when I first read the requirements for this assignment I thought that it would be quite challenging to do. I came to realise however that the real challenge was learning how to structure a PHP application in such a way as to achieve as much sepraration as possible between the presentation and application layers while keeping the code easy to follow (without overloading pages with PHP includes). Once I got the hang of it though, the rest was pretty easy, including the Space Manager page.
The Web Space Manager
As stated in my last post, I wanted the user to be able to access all the application's functionality from one 'main' page. This includes the ability to browse through the uploaded files, create folders, upload new files and delete existing files. I also wanted users to be able to see at a glance how much free space they had left. Here's the end result:
Preventing unauthorised access
The first thing the page does is verify that the user is authenticated by checking the appropriate session variables set were by the login page. If they are not, the user is re-directed to the login page. There are 2 such variables, a flag that signals if the user is authenticated and the serialised user class itself. For this project I opted to check just the flag and ignore the user class. For added security I could if need be, re-validate the username and password (using the class) against the database but that would mean a query being executed on the database each time the page was loaded which could affect performance. I figured checking the flag would suffice for this project.
The SpaceManager Class
The SpaceManager class is the 'core' of this whole project and is responsible for all operations performed on the user's web space. Whenever the class is instantiated, it sets the path to the user's home directory, the quota and calculates the used space. If the user has logged-in for the first time, it creates the user's space (home folder) beforehand. I'll be explaining how this class works in more detail while going through each piece of functionality.
The File Browser
The file browser initially displays the files and folders in the user's home directory. These 'Home' folders are created under a folder called 'vault' located in the website's root. To prevent users from browsing to the contents of this folder (through the browser's address bar) I created a '.htaccess' file within the 'vault' folder that contains a 'deny from all' rule. Simple, but effective.
The list shows the file/folder name, the size (in the case of files) and last modification date. Folder names can be clicked to display the files within that folder. While browsing, breadcrumb-style links are shown in the toolbar, from the home folder to the current directory, to serve as useful shortcuts. Different icons are used for the different file types and to distinguish between folders that are empty, compressed and those that contain files. The file browser also allows users to delete selected files and create new folders. Let's look at each of these bits of functionality individually.
The file browser
Listing Files & Folders
The 'listFiles()' function of the SpaceManager class iterates through the contents of the current directory (initially the user's home directory) and returns a multidimensional array describing the files and folders contained within. Here's the code for the function:
function listFiles(){
$fileList = array();
$files = array();
$path = $this->_home.DS;
if ($_SESSION['currentdir'] !== "") $path .= $_SESSION['currentdir'].DS;
if (is_dir($path)) {
if ($dirref = opendir($path)) {
while (($file = readdir($dirref)) !== false) {
if ($file !== '.' && $file !== '..') {
$files['name'] = $file;
$files['type'] = filetype ($path.$file);
$files['size'] = filesize ($path.$file);
$files['time'] = date("F d Y H:i:s.", filemtime($path.$file));
if ($files['type'] == 'dir') {
$files['count'] = count(glob($path.$file.DS."*"));
$files['ext'] = "";
$files['icon'] = 'dir';
if ($files['count'] == 0) $files['icon'] .= "-empty";
} else {
$files['count'] = 1;
$files['ext'] = pathinfo ($path.$file, PATHINFO_EXTENSION);
$files['icon'] = $this->getIconForExtension($files['ext']);
}
array_push($fileList, $files);
}
}
closedir($dirref);
}
}
// Sort the list by type (folders followed by files) and name
if (sizeof($fileList) > 0 ) {
foreach ($fileList as $key => $row) {
$name[$key] = $row['name'];
$type[$key] = $row['type'];
$size[$key] = $row['size'];
}
array_multisort($type, SORT_ASC, $name, SORT_ASC, $fileList);
}
return $fileList;
}
The currently selected directory is stored as a session variable. The 'opendir' PHP function is used to open this directory and a while loop is used to iterate through the contents. For each file/folder found, the '$files' array is populated with its properties including the name, type (file or folder) size, last modification date etc. PHP functions are used to get this information, including 'filetype', 'filesize', 'filemtime' and 'pathinfo' which in this case is used to determine a file's extension. Certain properties (such as the icon) are set depending on whether the file is in fact a file or a folder. The SpaceManager class' getIconForExtension function returns the icon to use (basically a CSS class name) based on a simple 'switch' statement.
Although it might sound complex, it's actually quite straight forward. What's interesting however is how to sort the resulting multidimensional array '$fileList'. I wanted to sort the files by type - folders first - and name. To do this, I iterate through each file, extracting the name and type into separate arrays. I then pass these arrays to the 'array_multisort' PHP function in the order I need to sort my '$fileList' array which then is returned back to the main page. At this point it's just a question of building an HTML table while iterating through this array to display the files and related information:
...
// Get list of files from SpaceManager Class ($Space)
$files = $Space->listFiles();
$index = 0;
$class = "";
$path = "";
if ($_SESSION['currentdir'] !== "") $path = $_SESSION['currentdir'].DS;
foreach ($files as $file) {
if ($file['type'] == 'dir'){
$text = "".$file['name']." (" .$file['count']." files)";
} else {
// create download link
$text = "".$file['name']."";
}
if ($index == 1) {
$class = " class = 'alt' ";
} else {
$class = "";
}
echo "<tr".$class.">";
echo "<td><input type='checkbox' name='chk_".$file['type']."[]' value='".$file['name']."'/>";
echo "$lt;td class='icon16 icon16-".$file['icon']."'></td>";
echo "<td>".$text."</td>";
echo "<td>".$file['type']."</td>";
echo "<td class='text-right'>".$Space->formatBytes($file['size'],1)."</td>";
echo "<td>".$file['time']."</td>";
echo "</tr>";
$index = 1-$index;
}
As you might have noticed from the code, folder names and file names are created as links (HTML anchors) such that users are able to 'open' or browse the former and download the latter. Folder links point to the 'goto.php' script while file names point to the 'download.php' script. I will be explaining these two scripts in detail in the next sections.
Browsing Through Folders
As explained above, folder names are rendered as HTML anchors that link to the goto.php script, passing the location as a query string. The script simply sets the current directory session variable to the location in the query string and re-directs the browser to the main.php page which shows the content of the selected folder, as explained above. While browsing through the folders, a "breadcrumb trail"-style navigator is built to allow users to see where they are in the folder structure and 'hop' directly to any folder in the trail. These links work exactly like the folder links in the file browser, using the goto.php script. The 'getBreadCrumbs()' function of the SpaceManager function is responsible for building this trail of links. It uses the 'explode' PHP function to split the current directory path (by the directory delimiter) into an array of folder names. It then iterates through this array creating a string of links which it returns to the calling script.
Breadcrumbs in the file browser toolbar
function getBreadCrumbs() {
$homeFolder = $_SESSION['user']->getUserName();
$path = '';
$delimiter = "";
$out = "";
$index = 1;
// Set the current directory.
if (!isset($_SESSION['currentdir']) || $_SESSION['currentdir'] == '') {
$_SESSION['currentdir'] = '';
$this->_uploadPath = VAULT.DS.$this->_home.DS;
} else {
$this->_uploadPath = VAULT.DS.$this->_home.DS.$_SESSION['currentdir'].DS;
// Parse the current directory to obtain the trail
$folders = explode (DS, $_SESSION['currentdir']);
// Iterate through the folder names and create breadcrumbs for all except the home and current folders.
foreach ($folders as $folder) {
if ($folder !== $homeFolder) {
if ($index !== sizeof($folders)) {
$index ++;
if ($path == '') {
$path = $folder;
} else {
$path .= DS.$folder;
}
$out .= $delimiter." ".$folder."";
} else { // Last folder name in the trail should not be a link
$out .= $delimiter." ".$folder;
}
}
}
}
return $out;
}
Downloading Files
Similarly to folders, file names are rendered as links to allow the user to download the files:
// create download link
$text = "<a href='download.php?x=".$path.$file['name']."'>".$file['name']."</a>";
The link points to the 'download.php' script that expects the path to the file as an argument in the query string. The script makes sure the user is logged in, checks that the file exists and initiates the download:
Users can only download files from their own 'space' since the path received by the script is relative to the user's home directory. At this point I would like to mention that setting the names of home folders to the registered usernames is not such a good idea. In a real project, I would probably generate some sort of GUID or a hash based on the username to set the name of the various home folders.
Creating Folders
To create a folder, a user enters the new folder name and clicks the "New Folder" button. The text-box and button are actually part of an HTML form that is set to post to the 'newfolder.php' script. This script gets the name of the folder from the '$_POST' asscociative array and creates an instance of the SpaceManager class. It then appends the new folder name to the current directory path and calls the 'createFolder()' function of the spaceManager class which in turn simply calls the mkdir PHP fucntion to create the folder. The script then re-directs the browser to the 'main.php' page which displays the new folder in the file browser.
Deleting Files and Folders
You may have noticed that there is a check-box next to each file and folder being listed (see File Browser further up this post). This check-box is used to mark the files and folders the user wishes to delete. Here's the code that's generating these check-boxes (taken from the file browser listing above):
echo "";
The file list is contained within an HTML form whose action is set to 'delete.php'. This form is submitted when the user clicks the delete button in the file browser toolbar. As can be seen from the code-snipped above, the name of a check-box is set to either 'chk_file' or 'chk_dir' depending on whether the associated item is a file or a folder. Furthermore, the check-box value is set to the file/folder name. By using identical names for the check-boxes (per type) we can obtain an array of folder/file names when posting to the 'delete.php' script as shown below:
<?php
include_once("./lib/shared.php");
$SpaceMan = new SpaceManager($_SESSION['user']->getUserName());
// Loop through the list of files (if any) and delete.
if (isset($_POST['chk_file'])) {
$files = $_POST['chk_file'];
for ($i = 0; $i< count($files); $i++) {
$SpaceMan->deleteFile($files[$i]);
}
}
// Loop through the list of folders (if any) and delete.
if (isset($_POST['chk_dir'])){
$folders = $_POST['chk_dir'];
for ($i = 0; $i<count($folders); $i++) {
//Build the full folder path
$folder = VAULT.DS.$_SESSION['user']->getUserName().DS.$_SESSION['currentdir'].$folders[$i];
$SpaceMan->deleteFolder($folder);
}
}
// Redirect to the main page when finished.
header ("Location: ".HOME."main.php");
die();
The 'delete.php' script is quite straight forward. It simply loops through the lists of check-boxes and calls the 'deleteFile' and 'deleteFolder' functions of the SpaceManager class.
The 'deleteFile' function simply checks that the file exists and calls the PHP 'unlink()' function to delete the file. 'deleteFolder' on the other hand is slightly more complex. A folder can only be deleted if it is empty, so if it's not we first need to delete its contents which might also be folders...recursion anyone? Here's the code:
The function recursively calls itself until all the contents of a directory (and any sub directories) are deleted.
And that's about it, all the main bits of functionality explained (understandably I hope). There are other bits an pieces which I have not mentioned, such as handling the quota and how the 'Space Utilisation Meter' was rendered, but since these bits are rather simple, I didn't want to bloat an already lengthy post. Having said that, if anyone is interested in learning more, drop a comment and I will be more than happy to explain further in another post.
Conclusion
I have to say that overall, this project was qiute enjoyable. As with any first encounter with a language, the start was quite frustrating especially until I settled on a way to structure the code which made sense to me and with which I was comfortable. Once past that hurdle it's just a matter of learning the new syntax. This does not mean however that the code is perfect (it never is) or that it is what you might call best-practise. There are lots of things to improve especially with respect to error/exception handling and security, which when pressed for time, are always the first things to drop off the list (unfortunately). Having said that, I plan to improve on these aspects (especially security) in the coming days. Things like implementing stricter validation of POSTed values and query string encryption are good learning opportunities. The project also implements features to make it as platform independent as possible such as using the directory separator global variable when building paths and URLs. Another nice feature to add would be to make the project database-independent. This could be achieved by encapsulating all database specific calls (such as mysql functions) into a separate classes (per database) each inheriting from a common abstract class that serves as an interface. The list goes on.
That's it for this series, I hope you enjoyed reading it as much as I did learning what's written in it!
Last week I mentioned that I wasn't to happy with the way the code was structured so far. It was untidy to say the least and hard to maintain, even at this early stage. I also said that I was looking into MVC and OOP to instill some sense of structure and order to the project. We have a saying in Maltese that translates to: "the sauce will cost you more than the fish it's meant to garnish". Writing even a simple MVC framework for this project would have been massive overkill and would have taken too much time and effort for something that is not meant for production. Of course there are PHP frameworks out there that support MVC but using existing frameworks is beyond the scope of this blog.
However, I wasn't giving up. I still wanted to improve the way the code was written so I turned to good old Object Oriented Principles (OOP) and some other nifty tricks I picked up on the net while doing my research to help me turn my spaghetti code into neatly layered lasagne!
I wanted to spend just a little bit of time on how I re-structured the code I had so far before going further, as all the new stuff I added is obviously based on these changes. You will also notice changes in the look and feel of the app (being a UX buff I couldn't resist :) and the name (I've changed everything else so why not!).
1. Introducing Objects
Up to now we have functionality that allows users to register and log-into our system, and this sentence alone screams "UserAccount Class". My UserAccount class is structured as follows:
Properties
username
password
email
errors() - an array that stores any errors thrown by the class
salt - a random set of characters that are appended to the user's password before hashing it to improve security
Methods
login - accepts a username and password as arguments and handles authentication
register - attempts to register the user
isUsernameAvailable - private method used during registration to check whether the chosen username is unique or not.
The code in the UserAccount class methods was extracted from the login and registration pages. This code remained largely intact, with only minor modifications required to suit the class.
I also added a small FormValidator class to handle common validations such as comparing strings (used in the registration form to confirm the user's password choice).
2. Re-structuring the Pages
With the authentication and registration code safely encapsulated in the UserAccount class, the login and registration pages became simpler. So much so that I decided to merge them into one page. The login page now contains two forms: the login form and the registration form. These forms post to the "processlogin.php" and "processreg.php" scripts respectively. These scripts contain all the calls to the UserAccount class that handle authentication and registration. Placing this code in these script files separates server-side code from the HTML which is exactly what I want. I also placed php code that is common to all pages (such as session management and database connection) in another script called "shared.php". Here's the code for the "shared.php" and "processlogin.php" scripts:
The shared.php script defines constants for the root folder of our website and the directory separator. The latter is useful because of the fact that Windows and Linux have different directory separators (backslash and forwardslash) so it makes our code slightly more platform independent which is nice. The scripts also makes use of the __autoload function in PHP to make sure that class files (such as our UserAccount.php) are "automagically" loaded whenever they are needed, which is pretty handy. It also connects to the database and starts the session. I've stripped all the error handling code from the script for simplicity's sake, but obviously there is some to handle those annoying exceptions.
The "processlogin.php" script below is called when posting the login form. In a nutshell, it reads the values for the username and password from the $_POST associative array and after making sure the values are not blank, it creates an instance of the UserAccount class and attempts authentication. If all is successful the script sets session variables to indicate that the user has been authenticated and re-directs to the main application page. If not, the login page is included so that the error messages can be displayed to the user.
<?php // processlogin.php
include_once ('./lib/shared.php');
$errMsg = "";
if ($_POST['txtUname'] != "" && $_POST['txtPassword'] != "") {
$User = new UserAccount ();
if (!$User->login($_POST['txtUname'], $_POST['txtPassword'])) {
$errMsg = 'Incorrect Username or password';
}
} else {
$errMsg = "Please enter a username and password";
}
if ($errMsg != "") {
include 'login.php';
} else {
$_SESSION['loggedin'] = 1;
$_SESSION['user'] = $User;
header('Location: .' . DS . 'space' . DS . 'main.php');
die();
}
You may notice that in both these scripts, since they solely contain PHP code, the closing PHP tag "?>" was left out. This is purposely done to prevent any whitespace from being unintentionally sent into the response.
Here's what the combined login and registration page looks like:
3. The Web Space Manager
Past the login and into the 'private' area of the site which I refer to as the 'Web Space Manager'. I wanted a single interface through which users could browse through the files and folders of their web space, upload new files and see how much free space they had left. I also wanted to allow users to create folders in their space and delete unwanted files. Quite a lot to do, so I want to dedicate the next post to describe how I went about it, in the meantime here's a little screenshot of the space manager page (still needs polishing up).
4. Final Thoughts
Although going back and restructuring the pages took some time and effort, the end result was definitely worth it. The entire exercise helped me understand more how PHP works and how to get more out of it which after all is the whole point of this project. I also picked up some nifty tips & tricks while going through countless articles on the web, which is always good.
Welcome to part 2 of this build. In part 1 we created a simple login form that authenticates users using server side validations and saw how to redirect the browser to the private area of the website. We also saw how to use sessions and session variables to check whether a user is logged in.
Till now, our valid username and password were hard-coded in our PHP script. We will now see how to store credentials on a MySql database and how we can query this database using PHP. We will build a simple registration page that will allow us to register new users, and modify the login page such that authentication is done against the database.
I have already created a database called "db_webspace" and a table within it (called "users") to store my user data.
1. The Regisration Page
We first need a page where users can register for our website. To keep it simple we will only ask for a username, a password and an email address. Here's what the form looks like:
We obviously want to validate the user input to make sure that the username is unique and that the passwords match. Since I have already covered server-side validation in my previous post I will not go into too much detail, however I will discuss how to validate against the database. Before that however, we need to register our first user and to do that we need a connection to our database:
<?php
$conn = mysql_connect("localhost", "root", "mydbpassword");
if (!$conn) {
die ("Unable to connect to database server".mysql_error());
}
//Connection successful;
?>
To connect to our database we first declare a variable ('$conn' in this case) and assign it the return value of the "mysql_connect" function. This function takes the MySQL server name, username and password as arguments and opens a connection to that database. It then returns a reference to that connection which we are storing in "$conn". If the connection fails, "$conn" is set to FALSE.
I'm not very comfortable having my database password stored in plain text in the script. In the real world I would probably store all connection properties within another file and encrypt them. However, it must be said that if someone gains access to your php scripts, you're already in trouble no matter where and how you store your settings!
We now have a connection to our database. Assuming that the user input is valid, here's how to register a new user:
$Result = mysql_select_db("db_webspace",$conn);
if (!$Result){
$Out.="Unable to select DB: ".mysql_error()."
";
}
else
{
$Uname = mysql_real_escape_string($_POST['txtUname']);
$Pass = mysql_real_escape_string($_POST['txtPassword']);
$Conf = mysql_real_escape_string($_POST['txtConfirm']);
$Email = mysql_real_escape_string($_POST['txtEmail']);
//Validate...
//Register the user
$SQL = "INSERT INTO users Values ('$Uname', '$Pass', '$Email')";
$Success = mysql_query($SQL, $conn);
if ($Success)
{// Show Thank-you message}
else
{// Show Error}
}
After successfully connecting to our database server, we use the 'mysql_select_db' function to select the actual database we want to work with which in our case is "db_webspace". In actual fact we are setting "db_webspace" as the currently active database for our connection ($conn) and every subsequent SQL query made using "$conn" will be executed on this database. Next, we use the "mysql_real_escape_string" function to sanitize user input. This is very important to make sure that the user input can be safely used in an SQL query. After performing some validations and assuming that our data is valid we construct an SQL statement to insert the values into the "users" table and execute the query using the 'mysql_query' function. Our user is now registered but the password is stored in plain text on the database. Here's a screenshot from "phpmyadmin" showing my registered users and their passwords in all their glory!
To fix this I will use the SHA1 hashing algorithm to hash the user's password before storing it into the database:
...
//Hash the password
$Pass = sha1($Pass);
$SQL = "INSERT INTO users Values ('$Uname', '$Pass', '$Email')";
...
Here's what the hashed password looks like for the 'secadmin' user:
We now have a way of registering our users and storing their passwords securely on our database.
2. Authenticating against the Database
Let's turn our attention now back to the login page and add the necessary code to authenticate our newly registered users against the database:
...
$conn = mysql_connect("localhost", "root", "mydbpassword");
if (!$conn){
die ("Unable to connect to database server".mysql_error());
}
$Result = mysql_select_db("db_webspace",$conn);
if (!$Result){
//Show error and terminate script execution
}
else {
// Sanitize user input }
$User = mysql_real_escape_string($_POST['txtUname']);
$Pass = mysql_real_escape_string($_POST['txtPassword']);
//Hash the password to compare it to hash in db
$Pass = sha1($Pass);
$Sql = "Select Count(*) As myCount
From users
Where userid='$User'
And password='$Pass'";
$Result = mysql_query($Sql, $conn);
$Row = mysql_fetch_array($Result);
if ($Row['myCount'] > 0){
//Set session variables and redirect to private page
}
else{
//Incorrect username or password
}
}
...
Just like our registration page, we first need to create a connection to and select our database. Next the user input is sanitized and the password is hashed so that it can be compared to the hashed value in the database. Finally an SQL select statement is executed to return the number of matching records for our username and password combination. Unlikely as it may seem, the next part is what confused me the most: how to read the result back from the query. If the query is successful, "mysql_query" returns a "resource" ($Result) which I then passed to the "mysql_fetch_array" function which in turn returns an array to the fetched row, which in this case is made up of just one column "myCount" (an alias for the count(*) sql expression). I can then access the value of "myCount" through this associative array and grant/deny access accordingly.
What next?
With the authentication mechanism out of the way I will shift my focus on the application functionality itself: uploading and managing files, quota management and eMail notifications.
I must say however that even at this relatively early stage, the code is starting to look untidy and not as modular as I would like so I've been looking into the Model View Controller (MVC) pattern for PHP. Even from the outset it is clear that MVC would make the code much more maintainable by achieving a clear separation between application logic and the presentation layer (HTML). Also, using Object Oriented Principles (OOP) will add further modularity (separation of concerns) of the code. The downside to MVC is that it takes some getting used to and unless using an existing framework, it takes some effort to implement from scratch. Having said that, I have found some useful resources online to get me started so I might implement MVC on this project in time for my next post.