CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 331 Figure 9-17. Expanding the validate month section of the date string Notice that the month and date sections are identical: a hyphen followed by two digits. This means you can simply repeat the month-matching pattern to validate the day using a repetition operator after the group: /^(\d{4}(-\d{2}){2})/ (see Figure 9-18). Figure 9-18. Adding the day part of the date string to the pattern Now match a single space and the hour section: /^(\d{4}(-\d{2}){2} (\d{2}))/ (see Figure 9-19). ■ Note Make sure that you include the space character. The shorthand class (\s) shouldn’t be used because new lines and tabs should not match in this instance. CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 332 Figure 9-19. Validating the hour section of the date string To validate the minutes, you match a colon and exactly two digits: /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}))/ (see Figure 9-20). Figure 9-20. Validating the minutes section of the date string Finally, repeat the pattern for the minutes to match the seconds, and then use the dollar sign modifier to match the end of the string: /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/ (see Figure 9-21). Figure 9-21. Validating the seconds section of the date string and completing the pattern CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 333 Armed with this regex pattern, you can now validate the date input in your application. Adding a Validation Method to the Calendar Class To validate the date string, you will add a new private method to the Calendar class called _validDate(). This method will accept the date string to be validated, then compare it to the validation pattern using preg_match(), which returns the number of matches found in the given string. Because this particular pattern will only match if the entire string conforms to the pattern, a valid date will return 1, while an invalid date will return 0. If the date is valid, the method will return TRUE; otherwise, it will return FALSE. Add this method to the Calendar class by inserting the following bold code into class.calendar.inc.php: <?php class Calendar extends DB_Connect { private $_useDate; private $_m; private $_y; private $_daysInMonth; private $_startDay; public function __construct($dbo=NULL, $useDate=NULL) { } public function buildCalendar() { } public function displayEvent($id) { } public function displayForm() { } public function processForm() { } public function confirmDelete($id) { } /** * Validates a date string * * @param string $date the date string to validate * @return bool TRUE on success, FALSE on failure */ private function _validDate($date) { /* * Define a regex pattern to check the date format CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 334 */ $pattern = '/^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/'; /* * If a match is found, return TRUE. FALSE otherwise. */ return preg_match($pattern, $date)==1 ? TRUE : FALSE; } private function _loadEventData($id=NULL) { } private function _createEventObj() { } private function _loadEventById($id) { } private function _adminGeneralOptions() { } private function _adminEntryOptions($id) { } } ?> Returning an Error if the Dates Don’t Validate Your next step is to modify the processForm() method so it calls the _validDate() method on both the start and end times for new entries. If the validation fails, simply return an error message. Add the following bold code to processForm() to implement the validation: <?php class Calendar extends DB_Connect { private $_useDate; private $_m; private $_y; private $_daysInMonth; private $_startDay; public function __construct($dbo=NULL, $useDate=NULL) { } public function buildCalendar() { } public function displayEvent($id) { } CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 335 public function displayForm() { } /** * Validates the form and saves/edits the event * * @return mixed TRUE on success, an error message on failure */ public function processForm() { /* * Exit if the action isn't set properly */ if ( $_POST['action']!='event_edit' ) { return "The method processForm was accessed incorrectly"; } /* * Escape data from the form */ $title = htmlentities($_POST['event_title'], ENT_QUOTES); $desc = htmlentities($_POST['event_description'], ENT_QUOTES); $start = htmlentities($_POST['event_start'], ENT_QUOTES); $end = htmlentities($_POST['event_end'], ENT_QUOTES); /* * If the start or end dates aren't in a valid format, exit * the script with an error */ if ( !$this->_validDate($start) || !$this->_validDate($end) ) { return "Invalid date format! Use YYYY-MM-DD HH:MM:SS"; } /* * If no event ID passed, create a new event */ if ( empty($_POST['event_id']) ) { $sql = "INSERT INTO `events` (`event_title`, `event_desc`, `event_start`, `event_end`) VALUES (:title, :description, :start, :end)"; } /* * Update the event if it's being edited */ else CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 336 { /* * Cast the event ID as an integer for security */ $id = (int) $_POST['event_id']; $sql = "UPDATE `events` SET `event_title`=:title, `event_desc`=:description, `event_start`=:start, `event_end`=:end WHERE `event_id`=$id"; } /* * Execute the create or edit query after binding the data */ try { $stmt = $this->db->prepare($sql); $stmt->bindParam(":title", $title, PDO::PARAM_STR); $stmt->bindParam(":description", $desc, PDO::PARAM_STR); $stmt->bindParam(":start", $start, PDO::PARAM_STR); $stmt->bindParam(":end", $end, PDO::PARAM_STR); $stmt->execute(); $stmt->closeCursor(); /* * Returns the ID of the event */ return $this->db->lastInsertId(); } catch ( Exception $e ) { return $e->getMessage(); } } public function confirmDelete($id) { } private function _validDate($date) { } private function _loadEventData($id=NULL) { } private function _createEventObj() { } private function _loadEventById($id) { } private function _adminGeneralOptions() { } private function _adminEntryOptions($id) { } CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 337 } ?> You can test the validation by entering a bad entry into the form at http://localhost/admin.php (see Figure 9-22). Figure 9-22. An entry with bad date values that should fail validation ■ Note You use http://localhost/admin.php because the only reason your server-side validation will be invoked is if the user has JavaScript disabled. In that case, the modal windows would not function, and the user would be brought to this form. In situations where JavaScript is enabled, the server-side acts as a double-check and an additional security measure against mischievous users. After this form is submitted, the app will simply output the error message and die (see Figure 9-23). The calendar application is designed for users with JavaScript enabled; you use this approach to prevent the app from displaying errors. CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 338 Figure 9-23. The error message displayed when invalid dates are supplied Adding Client-Side Date Validation For most users, JavaScript will be enabled. It’s far more convenient as a user to get instant feedback on the form, so you will add new jQuery functionality to validate date strings on the client side. Creating a New JavaScript File to Validate the Date String Because you’re going to continue to work with this script in the next chapter, you should put it in a separate file in the js folder called valid-date.js. This file will contain a function that is functionally equivalent to the _validDate() method in the Calendar class. It will accept a date to validate, check it against the date-matching regex pattern you wrote previously using match(), and then return true if a match is found or false if match() returns null. You build this function by inserting the following code into valid-date.js: // Checks for a valid date string (YYYY-MM-DD HH:MM:SS) function validDate(date) { // Define the regex pattern to validate the format var pattern = /^(\d{4}(-\d{2}){2} (\d{2})(:\d{2}){2})$/; // Returns true if the date matches, false if it doesn't return date.match(pattern)!=null; } ■ Note The regex pattern is not enclosed in quotes. If you used quotes, the pattern would be stored as a string and interpreted accordingly—this would result in the script looking for an exact character match, rather than interpreting the regex pattern properly. CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 339 Including the New File in the Footer To use the validDate() function, you’ll need to include the new JavaScript file before init.js, so that the function is available to be called. Do this by opening footer.inc.php in the common folder and inserting the following bold code: <script type="text/javascript" <script type="text/javascript"> google.load("jquery", "1"); </script> <script type="text/javascript" src="assets/js/valid-date.js"></script> <script type="text/javascript" src="assets/js/init.js"></script> </body> </html> Preventing the Form Submission if Validation Fails Now that validDate() is available in init.js, you need to add date validation before the form can be submitted. Store the start and end dates in variables (start and end, respectively), then check them using validDate() before allowing the form to be submitted. Next, modify the click handler to the Submit button on the form that edits or creates events, and then trigger an alert with a helpful error message if either date input has an invalid value. You need to prevent the form from being submitted as well, so the user doesn’t have to repopulate the other form fields. You accomplish this by inserting the following bold code into init.js: // Makes sure the document is ready before executing scripts jQuery(function($){ var processFile = "assets/inc/ajax.inc.php", fx = { } $("li a").live("click", function(event){ }); $(".admin-options form,.admin") .live("click", function(event){ }); // Edits events without reloading $(".edit-form input[type=submit]").live("click", function(event){ // Prevents the default form action from executing event.preventDefault(); // Serializes the form data for use with $.ajax() var formData = $(this).parents("form").serialize(), CHAPTER 9 ■ PERFORMING FORM VALIDATION WITH REGULAR EXPRESSIONS 340 // Stores the value of the submit button submitVal = $(this).val(), // Determines if the event should be removed remove = false, // Saves the start date input string start = $(this).siblings("[name=event_start]").val(), // Saves the end date input string end = $(this).siblings("[name=event_end]").val(); // If this is the deletion form, appends an action if ( $(this).attr("name")=="confirm_delete" ) { // Adds necessary info to the query string formData += "&action=confirm_delete" + "&confirm_delete="+submitVal; // If the event is really being deleted, sets // a flag to remove it from the markup if ( submitVal=="Yes, Delete It" ) { remove = true; } } // If creating/editing an event, checks for valid dates if ( $(this).siblings("[name=action]").val()=="event_edit" ) { if ( !validDate(start) || !validDate(end) ) { alert("Valid dates only! (YYYY-MM-DD HH:MM:SS)"); return false; } } // Sends the data to the processing file $.ajax({ type: "POST", url: processFile, data: formData, success: function(data) { // If this is a deleted event, removes // it from the markup if ( remove===true ) { fx.removeevent(); } . simply repeat the month-matching pattern to validate the day using a repetition operator after the group: /^(d{4} (- d{2}){2})/ (see Figure 9-1 8). Figure 9-1 8. Adding the day part of the date string. $stmt = $this->db->prepare($sql); $stmt->bindParam(":title", $title, PDO::PARAM_STR); $stmt->bindParam(":description", $desc, PDO::PARAM_STR); $stmt->bindParam(":start",. PDO::PARAM_STR); $stmt->bindParam(":end", $end, PDO::PARAM_STR); $stmt->execute(); $stmt->closeCursor(); /* * Returns the ID of the event */ return $this->db->lastInsertId();