Pages

Banner 468

Thursday 5 May 2011

Building a Web Space Management System - 4

0 comments
 
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:

<?php
   include_once("./inc/shared.php");
   include_once ('.'.DS.'inc'.DS.'checklogin.php');
 
   if (isset($_GET['x']) && $_GET['x'] !== "") {
  
      $path = VAULT.DS.$_SESSION['user']->getUserName().DS.$_GET['x'];
  
      if (file_exists($path)) {
         header('Content-type: application/force-download');
         header('Content-Disposition: attachment; filename="'.basename ($path).'"');

         readfile($path);
        
      } else {
         header ("Location: ".HOME."main.php");
         die();
      }
  
   } else {
      header ("Location: ".HOME."main.php");
      die(); 
   } 


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:

function deleteFolder($folderPath) {
   
   if(!file_exists($folderPath) || !is_dir($folderPath)) { 
      return false; 
   } elseif(!is_readable($folderPath)) { 
      return false; 
   } else { 
      $directoryHandle = opendir($folderPath); 
        
      while ($contents = readdir($directoryHandle)) { 
         if($contents != '.' && $contents != '..') { 
            $path= $folderPath . DS . $contents; 
            if(is_dir($path)) { 
               $this->deleteFolder($path); 
            } else { 
               unlink($path); 
            } 
         } 
      } 
        
      closedir($directoryHandle); 
  
      if(!rmdir($folderPath)) {           
         return false; 
      } 
        
      return true;
   } 
 }  
 

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!

Leave a Reply