Search This Blog

Monday, June 21, 2010

Building a Text Editor

With the basics of PHP's file and directory handling capabilities under your belt, let's build a simple text file editor. It will take a filename as an argument, edit the file, and then save it. If it's not given an existing filename, the editor will create a new file.


First, think what the script will look like when finished. The picture in your mind eventually translates to the end user interface. Because it's an editor, it needs a place where it stores text and lets the user manipulate it. You won't have to worry about implementing specific editing features such as typing in characters, deleting them, moving the cursor, and so on, because the HTML tag <textarea> can handle all the editing features you'll need. All you have to do is throw in a form that displays a scrolling text box, an edit field for typing in a file name, and a button to save the text.



Next to consider are how to alert the user upon errors and how to get his confirmation about possibly disastrous actions such as overwriting an existing file. Some simple JavaScript tricks can achieve the goal. You can put a snippet of JavaScript into an HTML page by enclosing the code within the following tag combination:


<script> JavaScript code goes here </script>

For example, if you want to alert the user about an error, you can use the JavaScript's alert() method:


<script> alert("Warning! An error occurred!"); </script>

That opens a small window displaying the specified error message. You can use the history.go(-1) method to tell the browser to return to the previous page upon error:


<script>
alert("Warning! An error occurred! Let's get back to the previous page!");
history.go(-1);
</script>

Getting confirmation from the user is just as easy. The confirm() method does the trick:


<script>
result = confirm("Warning! Are you sure?");
if(!result) history.go(-1);
</script>

The confirm() method returns the user's decision. If he presses the YES button, True is returned and the specified action is executed. If he presses CANCEL, False is assigned to the variable result, causing the browser to fall back to the previous page. You'll use some of these tricks in the sample script.


You start by putting global variables and commonly used functions in the common.inc include file. As previously shown, it's good practice to put common elements that are shared and reusable by related scripts into a common include file. Name the revised include file common_php5_02.inc.php. First, the script presents a function to process error messages. This function provides a message to the user, and also sends him back to the previous page:


<?php
function error_message($msg) {
   echo "<script>alert(\"$msg\"); history.go(-1)</script>";
   exit;
}

The next function is just a copy of the date_str() function that you created earlier:


function date_str($timestamp) {
  $date_str = getdate($timestamp);
  $year = $date_str["year"];
  $mon = $date_str["mon"];
  $mday = $date_str["mday"];
  $hours = $date_str["hours"];
  $minutes = $date_str["minutes"];
  $seconds = $date_str["seconds"];

  return "$hours:$minutes:$seconds $mday/$mon/$year";
}

The last function for the common_php5_02.inc.php file is file_info():


function file_info($file) {
   global $text_file_array;
   $file_info_array["filesize"] = number_format(filesize($file)) . " bytes.";
   $file_info_array["filectime"] = date_str(filectime($file));
   $file_info_array["filemtime"] = date_str(filemtime($file));
   if(!isset($_ENV['WINDIR'])) {
   $file_info_array["fileatime"] = date_str(fileatime($file));
   $file_info_array["filegroup"] = filegroup($file);
   $file_info_array["fileowner"] = fileowner($file);
} else {
   $file_info_array["fileatime"] = "not available";
   $file_info_array["filegroup"] = "not available"
   $file_info_array["fileowner"] = "not available";
}

   $extension = array_pop(explode(".", $file));

   if (in_array($extension, $text_file_array)) {
    $file_info_array["filetype"] = "text";
   } else {
    $file_info_array["filetype"] = "binary";
   }
   return $file_info_array;
}

Note how you extract the extension from a given file with explode(".", $file). This breaks up the filename using the dot (.) as a delimiter, and returns the resulting pieces as an array. The last element of the array is extracted using the array_pop() function, and is stored in $extension. For this example, the extension is not the second element.


One final section in common_php5_02.inc.php creates an array of filename extensions to look for:


//specify the file extensions to handle
$text_file_array = array( "txt", "htm", "html", "php", "inc", "dat" );

Now you're ready to create the structure of the main processing file, editor_php5.php. Start by creating the header for an ordinary HTML Web page:


<html>
<head><title>Welcome to Web Text Editor</title></head>
<body>

<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>">
<input type="hidden" name="posted" value="true">

Next, begin the PHP code and pull in (using the require() function) the common_php5.inc.php file. Then set the directory to use:


require("common_php5_02.inc.php");
//specify the default directory
$dir = "/Inetpub/wwwroot/beginning_php5/ch07/docs";

Check whether the user has clicked a button, and if so, set the proper value for the eventual select..case statement:


if (isset($_POST['save_edited_file'])) {
    $action_chosen = "save_file";
} elseif (isset($_POST['create_new_file'])) {
    $action_chosen = "save_file";
} elseif (isset($_POST['edit_existing_file])) {
    $action_chosen = "edit_existing_file";
}

Begin the select..case statement and lay out the save_file case. This code checks to see if the file exists, and if so, uses JavaScript to ask the user if he wants to overwrite the file:


switch ($action_chosen) {
 case "save_file";

       if (file_exists("$dir/$_POST[filename]")) {
          echo "<script>result = confirm(\"Overwrite
             '$dir/$_POST[filename]'?\"); if(!result) history.go
(-1);</script>";
     }

Try to open (or create) the file, and if it fails, use the error_message() function to send the user an error message:


      if ($file = fopen("$dir/$_POST[filename]", "w")) {
        fputs($file, $_POST[filebody]);
        fclose($file);
      } else {
        error_message("Can't save file $dir/$_POST[filename].");
     }

The last chunk of code in the save_file case uses HTML to display the primary buttons that allow the user to perform the available tasks in the application. Notice the variation of the directory listing code to fill the drop-down box. The same code is also used in the default case. Don't forget the break at the end of the case:


      //display the main buttons
      ?>
      <table border="1" align="center"><tr><td>
      <strong>Create (or Overwrite) New File or Edit Existing File</strong>
      </td></tr>
      <tr><td>
      <input type="submit" name="create_new_file" value="Create New File">
      <input type="text" name="filename" value="new.txt">
      </td></tr> <tr><td>
      <input type="submit" name="edit_existing_file" value="Edit Existing
File">
      <select name="existing_file">
      <?php

      if($dp = opendir($dir)) {
          while($file = readdir($dp)) {
              if($file !== '.' && $file != = '..' &&
                      is_file($dir."/".$file)) {
              ?>
              <option value="<?php echo $file; ?>">
                      <?php echo $file; ?></option>
              <?php
        }
      }
      } else {
             error_msg("Can't open directory $dir.");
           }

        closedir($dp);

        ?>
        </select>
        </td></tr></table>
        <?php

      break;

Now layout the edit_existing_file case. Set the file particulars, including the filebody, and put the results of the file_info() function into $file_info_array:


case "edit_existing_file";

        $filepath = "$dir/$_POST[existing_file]";
        $filebody = implode("",file($filepath));
        $file_info_array = file_info("$filepath");
Check to see if the file is a text file, and if so, reset the filebody so you'll know not to edit it:
       if ($file_info_array["filetype"] != "text") {
           $filebody = $filepath . " is not a text file. Can't edit.";
           $editable = 0;
       } else {
           $editable = 1;
     }

Display the file stats:


        ?>
        <table border="l" width="70%" align="center">
        <tr><th width="100%" colspan="2">
        <center><strong>Stats for Existing File named <?php echo
"$dir/$_POST[existing_file]"; ?>
        </td></tr>
        <?php
        $file_info_array = file_info("$dir/$_POST[existing_file]");
        foreach ($file_info_array as $key=>$val) {
            echo "<tr><th width=\"30%\">" . ucfirst($key)
            ."</td><td width=\"70%\">" . $val
            ."</td></tr>\n" ;
        }
        ?>
      </table>

The final chunk of code in the edit_existing_file case allows the user to edit the code and save the result:


        <br>
        <table border="1" align="center"><tr><td>
        <strong>Editing Existing File named <?php echo $_POST['existing_file'];
?></strong>
        </td></tr>
        <tr><td>
        <?php

        if ($editable == 0) {
            echo $filebody;
        } else {
            ?>
            <input type="hidden" name="filename" value="<?php echo
$_POST['existing_file]; ?>">
            <textarea rows="4" name="filebody" cols="40" wrap="soft">
            <?php
            echo "$filebody";
            ?>
            </textarea><br>
            <input type="submit" name="save_edited_file" value="Save Edited
File">
            <?php
       }
       ?>
       </td></tr></table>
       <?php

     break;

The default case is a copy of the last portion of the save_file case:


  default;
              //display the main buttons
              ?>
              <table border="1" align="center"><tr><td>
              <strong>Create (or Overwrite) New File or Edit Exist-
ing File</strong>
              </td></tr>
              <tr><td>
              <input type="submit" name="create_new_file" value="Create
New File">
              <input type="text" name="filename" value="new.txt">
              </td></tr>
              <tr><td>
              <input type="submit" name="edit_existing_file" value="Edit
Existing File">
              <select name="existing_file">
              <?php

              if($dp = opendir($dir))    {
                 while($file = readdir($dp))    {
                    if($file   !==   '.' && $file   != =   '..'   &&  is_file
($dir."/".$file)) {
                ?>
             <option value="<?php echo $file; ?>"><?php echo $file; ?></option>
             <?php
                 }
               }
             } else {
                error_msg("Can't open directory $dir.");
             }
             closedir($dp);

           ?>
           </select>
           </td></tr></table>
           <?php
          break;

The final code in the file ends the select..case statement and finishes off the HTML for the page, including the closing </form> tag.


}
?>
    </form>
   </body>
</html>

And that pretty well covers how to deal with the members of the server file system family.