How to build your own Multilingual PHP MVC CMS from scratch - Part 5 - Frontend (part 3): User Interface

Tuesday, December 29, 2015

Welcome to the fifth settlement of our tutorial series on how to build your own Multilingual MVC CMS from scratch and in which we will be building an interface for the user to sign in and update its personal data.

During the last settlements, we have created a page module that allows the user to display a specific page based on URL input and aided by some third-party scripts, we have also created a customizable menu display system that comes in two layout flavors, vertical and horizontal. In a future settlement, I will show you how to create the back-end feature that will allow you to completely edit the menus stored in the database.

But for now, we will set our interest onto building a user interface that will allow for signing into the system as well as updating personal data. Now, since in reality, the user’s experience on this project encompasses at least 6 whole modules, which are: registration, sign in, forgotten password, account, profile, settings, password changing, plus the use of authentication model as well and that we obviously cannot review all of them at once, we will only be reviewing some of them, which are the registration, signin and profile modules. And this will still be a lot, so I will be focusing only on the parts that are of interest. This makes sense since there is no point in explaining how a controller works over and over again.

Anyway, the table named ‘users’ represents the natural entry point through which we will start digging our user interface.
 --  
 -- Table structure for table `users`  
 --  
   
 CREATE TABLE `users` (  
  `id_user` int(11) NOT NULL,  
  `user_login` varchar(12) NOT NULL,  
  `user_password` text NOT NULL,  
  `user_email` varchar(72) NOT NULL,  
  `user_country` varchar(255) NOT NULL DEFAULT 'uk',  
  `user_gender` enum('else','female','male') NOT NULL,  
  `user_birthdate` varchar(100) NOT NULL,  
  `user_type` enum('0','1','2') NOT NULL,  
  `user_ip` varchar(16) NOT NULL,  
  `user_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `users`  
 --  
   
 INSERT INTO `users` (`id_user`, `user_login`, `user_password`, `user_email`, `user_country`, `user_gender`, `user_birthdate`, `user_type`, `user_ip`, `user_timestamp`) VALUES  
 (1, 'test', 'c08ac56ae1145566f2ce54cbbea35fa3', 'test@mail.com', 'fidji', 'male', '08/02/1985', '1', '127.0.0.1', '2015-10-02 00:55:41');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `users`  
 --  
 ALTER TABLE `users`  
  ADD PRIMARY KEY (`id_user`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `users`  
 --  
 ALTER TABLE `users`  
  MODIFY `id_user` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;  
Relying on the users table is the table named user_settings, which I will expose while discussing the profile module. Note the user_type field will not be of much help since permission levels will not be implemented, at least not until the end of the series.

Without any transition whatsoever, let’s jump right in the registration form. There’s no point in providing a user with an interface to sign in if that user was never given the opportunity to register beforehand, right?
 /* themes/your_style/registration.php */  
   
 <div align="center">  
 <?php  
 // we first take care off displaying feedback, if it contains error messages  
 if(isset($feedback) && !is_string($feedback)){  
 ?>  
      <table border="0" cellpadding="2" cellspacing="2" align="center">  
      <ul>  
      <?php  
      foreach($feedback as $err){  
           foreach($err as $er){  
                $msgs = '';  
                $exp_msg = explode("#",$er[1]);  
                $cx = 0;  
                foreach($exp_msg as $em){  
                     if($cx < sizeof($exp_msg)-1){$trailer = '; ';}else{$trailer = '';}  
                     $msgs .= $GLOBALS['t_'.$em].$trailer;$cx++;  
                }  
                ?>  
                <tr><td><div class="response_negative" style="font-size:12px;"><li><?php echo ucfirst($GLOBALS['t_'.$er[0]]); ?> -> <?php echo $msgs; ?></div></li></td></tr>  
      <?php   
           }  
      }  
 ?>  
      </ul>  
      </table>  
      <br />  
      <?php  
 }  
 if(isset($feedback) && $feedback == 'failed'){ ?><div class="failed"><?php echo $GLOBALS['t_problem_registering_you']; ?></div><?php }  
   
 //now preparing for action urls to deploy  
 if(CLEAN_URLS == true){$url = CLEAN_PATH.'/'.CURRENT_RLINK.'/'.CURRENT_LANG;}  
 else{$url = $_SERVER['PHP_SELF'].'?'.RLINK.'='.CURRENT_RLINK.'&'.LN.'='.CURRENT_LANG;}  
   
 $utils = new UtilsModel();  
 $user_ip = $utils->get_user_ip();  
 ?>  
 <br /><br />  
 <p>&nbsp;</p>  
 </div>  
   
 <!-- the actual registration form -->  
 <form id="frm_registration" name="frm_registration" enctype="multipart/form-data" method="post" action="<?php echo $url; ?>" onsubmit="return Check_registration(this,'off');">  
       
 <table border="0" cellpadding="2" cellspacing="0" align="center">  
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_login']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="login" name="login" value="<?php echo isset($_POST['login'])?htmlspecialchars($_POST['login']):""; ?>" maxlength="<?php echo MAX_CHARS_LOGIN; ?>" onkeyup="check_login_exists();" /></td>  
           <!-- pay attention to the ternary statements - if post_login is set we use it,   
           //otherwise we leave the field empty - this allows for field rememberance,   
           //so that if for any reason the form gets reloaded, the user won't have to type everything over again -->  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_password']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="password" id="password" name="password" value="<?php echo isset($_POST['password'])?htmlspecialchars($_POST['password']):""; ?>" maxlength="<?php echo MAX_CHARS_PWD; ?>" /></td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_email']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="email" name="email" value="<?php echo isset($_POST['email']) ? htmlspecialchars($_POST['email']):""; ?>" maxlength="<?php echo EMAIL_MAX_LENGTH; ?>" onkeyup="check_email_exists();" /></td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_country']; ?>*</td>  
           <td align="left" class="input">  
           <select name="country">  
           <?php   
           require('lists/countries/countries.php'); //for clarity, we use a country list  
   
                if(isset($country_list) && !empty($country_list)){  
   
                foreach($country_list as $country){                      
                     if(isset($_POST['country']) && $_POST['country'] == $country[1]){$sel = ' selected=selected';}else{$sel = '';}  
                ?>  
                     <option value="<?php echo $country[1]; ?>"<?php echo $sel; ?>><?php echo $country[0]; ?></option>  
                     <?php  
                     }  
                }   
           ?>  
           </select>  
           </td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_gender']; ?>*</td>  
           <?php  
 // if(isset($_POST['gender'])){echo $_POST['gender'];$checked = ' checked';}else{$checked = '';}   
 ?>  
           <td align="left" class="input">  
                <table border="0" cellpadding="2" cellspacing="0" width="100%">  
                     <tr>  
                          <td><input type="radio" id="gender" name="gender" value="male" checked />&nbsp;&nbsp;<?php echo $GLOBALS['t_male']; ?></td>  
 </tr><tr>  
   
                     <td><input type="radio" id="gender" name="gender" value="female"<?php if(isset($_POST['gender']) && $_POST['gender'] == 'female'){echo ' checked';} ?> />&nbsp;&nbsp;<?php echo $GLOBALS['t_female']; ?></td>  
                     </tr>  
                </table>  
           </td>  
      </tr>  
   
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_birthdate']; ?>*</td>  
           <td align="left" class="input"><input class="text" type="text" id="birthdate" name="birthdate" value="<?php echo isset($_POST['birthdate'])?htmlspecialchars($_POST['birthdate']):""; ?>" maxlength="10" onkeyup = "displayDatePicker('birthdate');" onclick = "displayDatePicker('birthdate');" /></td>  
      </tr>  
        
      <tr>  
           <td align="left" class="std"><?php echo $GLOBALS['t_newsletter']; ?></td>  
           <td align="left" class="input"><input type="checkbox" name="newsletter" value=""<?php if(isset($_POST['newsletter'])){$checked = ' checked';}else{$checked = '';} echo $checked; ?> /></td>  
      </tr>  
   
      <tr align="right">  
           <td align="center" valign="middle" class="std" valign="bottom"><?php echo $GLOBALS['t_verification_code']; ?></td>  
           <td align="center">  
                <table border="0" cellpadding="0" cellspacing="0" width="100%">  
                     <tr align="center">  
                          <td valign="middle"><img src="<?php echo CLEAN_PATH.'/'.PATH_TO_THEMES.'/'.CURRENT_THEME.'/vf_image_u.php'; ?>" />&nbsp;</td>  
                          <td><input name="vercode_u" type="text" id="vercode_u" autocomplete="off" maxlength="5" style="height:;width:155px;font-size:px;background-color:#;color:#;" /></td>  
                     </tr>  
                </table>  
           </td>  
      </tr>  
   
      <tr>  
           <td><input type="hidden" name="type" value="1" /></td>  
           <td><input type="hidden" name="ip" value="<?php echo $user_ip; ?>" /></td>  
      </tr>  
   
      <tr>  
           <td class="std" colspan="2" align="center">  
           <br />  
           <input type="submit" id="submit" name="submit" class="submit" value="<?php echo $GLOBALS['t_submit']; ?>" /></td>  
      </tr>  
 </table>  
 </form>  
There really isn’t much to the above code. It starts off displaying feedback messages, if those are containing registration errors. Then the actual registration form begins.

Ternary syntax is used for field remembering. Eg:
 <input class="text" type="text" id="login" name="login" value="<?php echo isset($_POST['login'])?htmlspecialchars($_POST['login']):""; ?>" maxlength="<?php echo MAX_CHARS_LOGIN; ?>" />  
This tells that if a $post login is set, let’s use it, otherwise let’s leave empty.

Client-side Javascript validation is operated by a call to the Javascript Check_registration() function that can be found in public/js/main.php. In the event that the user disables Javascript, server-side validation is triggered via the check_form_errors() method as discussed further bellow.

During both onkeyup and onclick events, a dynamic Javascript/CSS-written date picker library is instantiated and appears below the birthdate field. This library will provided be as an attachment along with the corresponding CSS file. You may choose not to use this library and it will make no difference in terms of validation, as the birthdate field is validated client-side as well as server side. This third-party open-source date picker is here to operate only as an aesthetically pleasant date picker, enhancing the overall visual aspect.

A country list file and script is used for country selection.
 <?php  
   
 /* lists/countries/coutries.php */  
   
 require('array_countries.php');  
 $country_list = array();  
 $line = array();  
   
 foreach($array_country_list as $c){  
      array_push($line, $GLOBALS['t_'.strtolower($c)], strtolower($c));  
      array_push($country_list, $line);  
      $line = array();  
 }  
 asort($country_list);  
 ?>  
This snippet is pretty much self-explanatory and draws from the array bellow to create a new array containing the internationalized name of each country along with the all-lowercase formatted name of that same country.

Note the alphabetical sorting operated by the asort() function does not work too well with languages like Russian, or with html ascii codes because as of now, that’s as far as PHP’s support in that matter goes.
 <?php  
   
 /* lists/coutries/array_countries.php */  
   
 $array_country_list = array(  
 'Afghanistan',  
 'Albania',  
 'Algeria',  
  ...,  
 'Zimbabwe'  
 );  
 ?>  
The full array is available as attachment, along with the English translations, to be added to languages/en/en.php

An image containing a verification code is created by vf_image_u.php using GD, as follows
 <?php  
   
 /* themes/your_style/vf_image_u.php */  
   
 session_name('general_purpose');  
 session_start(); //we will store our random number inside a session variable   
      //so we need to declare a session here too.  
   
 $text = rand(10000,99999);   
 $_SESSION["vercode_u"] = $text;   
 $height = 15;   
 $width = 45;   
    
 $image_p = imagecreate($width, $height); // check about.php in case the image does not display  
 $black = imagecolorallocate($image_p, 165, 174, 112);   
 $white = imagecolorallocate($image_p, 68, 68, 68);   
 $font_size = 14;   
    
 imagestring($image_p, $font_size, 0, 0, $text, $white);   
 imagejpeg($image_p, null, 80);   
 ?>  
This simple script is using the imagecreate() function from the GD library. If the image does not display, load the about.php script that resides at root, to find out if you have GD enabled + jpeg support. We also fabricate a random number that we store inside $_SESSION[“vercode_u”].

Of course other fields can be added, some removed, that’s up to you and your project’s business logic of course, but then you need to make sure the proper fields are correctly retrieved while processing the sent data inside RegModel.php.

But from a controller point of view, this has no importance whatsoever since we will fetch our form variables programatically. Here’s how
 <?php  
   
 /* application/controllers/RegController.php */  
   
 class RegController extends BaseController{  
        
      private $module_name;  
      private $form_name;  
      private $feedback;  
                  
      public function initialize(){  
             
           $this->registry->template->module_name = $this->getModuleName();  
           $this->registry->template->form_name = $this->getFormName();  
             
           // new in our initialize() method, the intercept_data() method  
           // which will catch all form variables   
           // and send it to the relevant model method  
   
           $this->registry->template->feedback = $this->intercept_data();  
           $this->registry->template->assign_theme();  
      }  
   
      public function getModuleName(){  
           $model = RegModel::getInstance();  
           return $model->get_module_name();  
      }  
        
      public function getFormName(){  
           $model = RegModel::getInstance();  
           return $model->get_form_name();  
      }  
        
      public function intercept_data(){  
   
           // this method is where we intercept form variables  
           // to be then sent to the relevant model's method  
   
           $this->dol_posts = array();       
   
           foreach($_POST as $key => $val){//for each form variable  
   
                     // if there's a $post set of that variable,  
                     // we stack its value in the dol_posts array  
                  
                if(isset($_POST[$key])){array_push($this->dol_posts,$key);}  
           }  
             
           $model = RegModel::getInstance(); //we call an instance of the reg model  
           return $model->get_form_feedback($this->dol_posts); // and invoke the method that will process our form variables, passing them as parameter.  
      }  
 }  
 ?>  
So, this controller brings us new elements that weren’t in the page controller because the logic there was different, there was no form and thus no data to be intercepted. The intercept_data() method is where we intercept form variables to be then sent to the relevant model’s method, in this case, the get_form_feedback() method belonging to the RegModel class (Singleton).
 <?php   
   
 /* from application/models/RegModel.php */  
   
 public function get_form_feedback($parms){  
             
           $util = new UtilsModel();  
   
           if(!empty($parms)){ //if we did receive form variables  
             
                $error_list = array();  
   
                // we check for form errors  
                $error_check_reg = self::check_reg_form_errors($parms);  
                //and whether login and email are unique  
                $is_unique_login = $util->check_if_unique('users', 'user_login', $_POST[$parms[0]], array('login','already_in_use'));  
                $is_unique_email = $util->check_if_unique('users', 'user_email', $_POST[$parms[2]], array('email','already_in_use'));  
   
                //if login is not unique, we stack that info into the error array  
                if (sizeof($is_unique_login) > 0){array_push($error_list,$is_unique_login);}  
   
                //same for email  
                if (CHECK_EMAIL_UNIQUE == true && sizeof($is_unique_email) == 1){array_push($error_list,$is_unique_email);}  
   
                //and for general form errors  
                if (sizeof($error_check_reg) > 0){array_push($error_list,$error_check_reg);}  
   
                //if there's at least one error found, no matter what type            
                if(sizeof($error_list) > 0){  
                     //we return our array of errors  
                     $this->form_feedback = $error_list;            
                }  
                else{  
                     //otherwise, no form errors, so we start processing the form   
                       
                     // that's the place where you make sure the form's HTML element   
                     // names you need to pull back correspond to what's in the list just bellow  
                     $field_list = array('login','password','email','country','gender','birthdate','newsletter','vercode_u','type','ip');  
                  
                     foreach($field_list as $fl){ //and for each for variable we have fetched,   
                          //if it can be found in the array above and there's a set $post variable of that name, then  
                          //we set a variable that will bare that same name, using the variable of variable ($$) syntax  
                          if(in_array($fl,$parms) && isset($_POST[$fl])){$$fl = $_POST[$fl];}  
                     }  
   
                     //building a query to insert the new user  
                     $q_insert_user = "INSERT INTO users (user_login, user_password, user_email, user_country, user_gender, user_birthdate, user_type, user_ip) VALUES ('$login', md5('$password'), '$email', '$country', '$gender', '$birthdate', '$type', '$ip')";  
   
                     // if the query executes well  
                     if(MySQLModel::get_mysql_instance()->executeQuery($q_insert_user) == 0){  
                          //we get the ID of the newly created user  
                          $q_id = "SELECT id_user, user_login,user_password FROM users WHERE user_login = '".$login."' AND user_password = md5('".$password."')";  
                          MySQLModel::get_mysql_instance()->executeQuery($q_id);  
                          $my_id = MySQLModel::get_mysql_instance()->getRows($q_id);  
   
                          if($my_id != ''){  
                               // and if everything went well and we have that ID we wanted  
                               $id = $my_id['id_user'];  
                       
                               //let's create a settings entry in db  
                               if(CURRENT_LANG){$this->lng = CURRENT_LANG;}  
                               //do not worry about the newsletter parameter because newsletters have not really been implemented  
                               //on the project. This is here only to give an effect of form completeness  
                               if(isset($newsletter)){$this->nl = 'yes';}else{$this->nl = '';}  
                            
                               //we build the query to insert a settings row for that new user  
                               $q_insert_user_settings = "INSERT INTO user_settings(user_settings_id_user, user_settings_language, user_settings_newsletter) VALUES ('$id','$this->lng','$this->nl')";  
                                      
                               if(MySQLModel::get_mysql_instance()->executeQuery($q_insert_user_settings) == 0){  
   
                                         //and if everything went according to plan, we authentify the new user                      
                                         $auth = AuthModel::getInstance();  
                                         $auth->stay_in($id,$login,$password);  
   
                                         //and then we take care of sending a 'thanks for registering - here are your credentials...' email  
                                         // we will discuss email sending via PHPMailer in a future settlement   
                                         //so you can safely skip that part for now.  
                                         require_once('languages/templates/'.CURRENT_LANG.'/'.strtolower($this->module_name).'_'.CURRENT_LANG.'.php');  
   
                                         if($regmail = $util->send_mail(CONTACT_FORM_EMAIL,$email,$GLOBALS['t_reg_email_subject'],$GLOBALS['t_reg_email_alt'],$content)){$this->form_feedback = 0 ;}  
   
                                         //In any case, our business logic requires us to relaod the page if registration turns out successful   
                                         //simply because we want the new user to automatically see a thanks for   
                                         //registering message,  
                                         //so we build a header location link and redirect the new user to that  
                                         if(DEFAULT_PLINK != ''){  
                                              if(CLEAN_URLS == true){$plink = '/'.DEFAULT_PLINK;}  
                                              else {$plink = '&'.PLINK.'='.DEFAULT_PLINK;}  
                                         }  
                                         else{$plink = '';}  
   
                                         if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.$plink.'/'.CURRENT_LANG.'/regs';}  
                                         else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$plink.'&'.LN.'='.CURRENT_LANG.'&reg=success';}  
                                         header('Location: '.$location);exit();  
                                    }  
                               }  
                               else{$this->form_feedback = 'failed';} //self-explanatory  
                          }  
                               else{$this->form_feedback = 'failed';}  
                     }  
                }   
           return $this->form_feedback;  
      }  
 ?>  
Very simply, what that method does is, if we did intercept our form variables, we subject them to an error checking method (that we will review further bellow), we also check for the uniqueness of the login and email fields. If errors are found we stack them into an array we return to the controller, otherwise that means all came to us clean and we then start processing that data.

We insert the new user into the database and if the query went okay, we fetch its ID and insert a row into the user_settings table for that user. We log the user in, using our AuthModel class. We do send a confirmation email, but since we will discuss email sending in a future settlement only, we leave out that part for now. And since we want the new user to get a welcome message and everything, we build a location URL through which we will reload the page.

We will take a moment to review three things though, the first one being the check_reg_form_errors() method, useful to the get_form_feedback() method, as discussed above
 <?php   
   
 /* from application/models/RegModel.php */  
   
 public function check_reg_form_errors($mr){  
           $this->reg_form_errors = array();  
   
           $line = array();            
             
           foreach($mr as $r){  
   
                if(preg_match('/login/',$r)){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_LOGIN.','.MAX_CHARS_LOGIN.'}$/',$_POST[$r])){  
                     array_push($line,'login','not_valid');array_push($this->reg_form_errors,$line);}  
                     // as an example, 'login' and 'not_valid' are the strings we will match against   
                     //while in registration.php to help us know the encountered format for the login field is not valid  
                }  
   
                if(preg_match('/password/',$r)){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_PWD.','.MAX_CHARS_PWD.'}$/',$_POST[$r])){  
                          array_push($line,'password','not_valid');array_push($this->reg_form_errors,$line);  
                     }       
                }  
   
                if(preg_match('/email/',$r)){  
                     if (!filter_var($_POST[$r], FILTER_VALIDATE_EMAIL)){array_push($line,'email','not_valid');array_push($this->reg_form_errors,$line);}  
                     if($_POST[$r] != '' && mb_strlen($_POST[$r]) > EMAIL_MAX_LENGTH){array_push($line,'email','not_valid');array_push($this->reg_form_errors,$line);}   
                }  
   
                if(preg_match('/birthdate/',$r)){  
                     if(!empty($_POST[$r])){  
                          if(preg_match('/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/',$_POST[$r])){$exp_d = explode('/',$_POST[$r]);  
                          //var_dump($exp_d);  
                          if(checkdate($exp_d[0], $exp_d[1], $exp_d[2]) != 1 || !preg_match('/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/',$_POST[$r])){array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}                                     
                          }  
                          else{array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}  
                     }  
                     else{array_push($line,'birthdate','not_valid');array_push($this->reg_form_errors,$line);}  
                }  
   
                if(preg_match('/vercode_u/',$r)){  
                     if(!preg_match('/^'.$_SESSION["vercode_u"].'$/',$_POST[$r])){array_push($line,'verification_code','not_valid');array_push($this->reg_form_errors,$line);}  
                }  
   
      $line = array();  
           }  
                //var_dump($this->reg_form_errors);  
           return $this->reg_form_errors;  
      }  
 ?>  
Nothing special on that one, as it’s only about basic preg_match verification of field format. Not however that, the arrays we send back in case of found errors, correspond to the expected arrays we expect and parse in registration.php, before we display the registration form.

Second, to be able log the new user in following successful registration, we have used the stay_in() method of yet another Singleton class, the authentication class called AuthModel
 <?php   
   
 /* from application/models/AuthModel.php */  
   
 public function stay_in($id, $user, $pwd, $lang = NULL){  
      $_SESSION['c_id'] = $id;  
      $_SESSION['c_login'] = $user;  
      $_SESSION['c_pwd'] = md5($pwd);  
      $_SESSION['c_lang'] = $lang;  
 }  
 ?>  
There, we simply store obviously needed credentials into session variables, so the user stays logged in as long as it still hasn’t logged out.

Third, the get_user_ip() method from UtilsModel.php that’s being called in the registration form from registration.php
 <?php  
   
 /* from application/models/UtilsModel.php */  
   
 public function get_user_ip(){  
             
      if (!empty($_SERVER["HTTP_CLIENT_IP"])){$this->real_ip = $_SERVER["HTTP_CLIENT_IP"];}  
      elseif(!empty($_SERVER["HTTP_X_FORWARDED_FOR"])){$this->real_ip = $_SERVER["HTTP_X_FORWARDED_FOR"];}  
      else{$this->real_ip = $_SERVER["REMOTE_ADDR"];}  
             
       return $this->real_ip;  
 }  
 ?>  
Nothing complicated here, this snippet will go as far as it can to fetch the real IP of the user. Fetching someone’s IP is no perfect science, so bare with me and with this code until someone comes up with something even more efficient. Although, this here should get a fair share of users hiding behind proxies. But you got my point, it won’t be perfect in any case.

So anyway, our user can already register and upon successful registration gets logged in automatically. Now what if that same user has logged out but needs to log in again? Our system must offer that user a way to manually log in to the system. This is where the Signin module comes in handy. But again and for the sake of clarity, I will only be showing the relevant pieces of code. Indeed, the SignIn controller is identical to the RegController, except for a few occurrences where ‘Reg’ should be replaced by ‘SignIn’ and that’s all there’s is to it. So we will now get straight into the signin model’s get_form_feedback() method.
 <?php  
   
 /* from application/models/SignInModel.php */  
   
 public function get_form_feedback($parms){  
             
      if(!empty($parms)){ //if we did get our form values  
             
           $error_list = array();  
   
           //we check them against the check_signin_form_errors() method  
           $error_check_pwdc = self::check_signin_form_errors($parms);  
   
           if (sizeof($error_check_pwdc) > 0){array_push($error_list,$error_check_pwdc);}  
             
           if(sizeof($error_list) > 0){$this->form_feedback = $error_list;} // if errors were found, we return them  
           //otherwise, we authenticate the user, invoking AuthModel  
           else{$this->form_feedback = self::authentify($_POST[$parms[0]],$_POST[$parms[1]]);}  
           //0 for login and 1 for password  
      }  
   
      return $this->form_feedback;  
 }  
 ?>  
Basically here, we validate our form data against the check_signin_form_errors() model, which looks as follows
 <?php  
   
 /* from application/models/SignInModel.php */  
   
 public function check_signin_form_errors($mr){  
           $this->signin_form_errors = array();  
   
           $line = array();            
           //var_dump($mr);  
             
           foreach($mr as $r){  
           //var_dump($r);  
                if($r == 'login'){  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_LOGIN.','.MAX_CHARS_LOGIN.'}$/',$_POST[$r])){  
                          array_push($line,'login','not_valid');array_push($this->signin_form_errors,$line);  
                     }       
                }  
   
                if($r == 'password'){//echo $_POST[$r];  
                     if(!preg_match('/^[a-z0-9_]{'.MIN_CHARS_PWD.','.MAX_CHARS_PWD.'}$/',$_POST[$r])){  
                          array_push($line,'password','not_valid');array_push($this->signin_form_errors,$line);  
                     }       
                }  
   
      $line = array();  
           }  
   
           return $this->signin_form_errors;  
      }  
 ?>  
This method acts exactly the same way as check_reg_form_errors(). But if everything goes okay, we authenticate the user by calling the authentify() method from the same class.
 <?php   
   
 /* from application/models/SignInModel.php */  
   
 public function authentify($login,$password){  
      $auth = AuthModel::getInstance();  
      $this->authentified = $auth->identify($login,$password);  
   
      return $this->authentified;  
 }  
 ?>  
This method called for the identify() method kept in AuthModel.php, which looks just like this bellow
 <?php  
   
 /* from application/models/AuthModel.php */  
   
 public function identify($user,$pwd){  
             
           $default_plink = '';  
   
           if(DEFAULT_PLINK){  
                if(CLEAN_URLS == true){$default_plink = DEFAULT_PLINK.'/';}  
                else{$default_plink = '&'.PLINK.'='.DEFAULT_PLINK;}  
           }  
             
          $q_auth = "SELECT id_user,user_login,user_password   
                     FROM users   
                     WHERE user_login = '".$user."'   
                     AND user_password = md5('".$pwd."')  
                ";  
   
           MySQLModel::get_mysql_instance()->executeQuery($q_auth);  
           $my_auth = MySQLModel::get_mysql_instance()->getRows($q_auth);  
             
           if($my_auth != ''){       
                  
                $id = $my_auth['id_user'];  
                $this->identified = 'yes';  
                  
                //once the user has been authenticated, we go fetch its private language settings  
                $user_lang = "SELECT id_user_settings, user_settings_id_user, user_settings_language   
                        FROM user_settings   
                        WHERE user_settings_id_user = '".$id."'  
                        ";  
                  
                MySQLModel::get_mysql_instance()->executeQuery($user_lang);  
                  
                $userlang = MySQLModel::get_mysql_instance()->getRows($user_lang);  
   
                if($userlang != ''){$this->user_lang = $userlang['user_settings_language'];}  
   
                //and write the user to a session                 
                $this->stay_in($id, $user, $pwd, $this->user_lang);  
                  
                // IF LAST URL REMEMBERED  
                if(isset($_SESSION['last_url']) && $_SESSION['last_url'] != ''){  
                     $exp_url = explode("&",$_SESSION['last_url']);  
                     $xpr = LN.'=';  
                     $parts = '';  
                     $rep = LN.'='.CURRENT_LANG.'&';  
                       
                     foreach($exp_url as $xpurl){       
                          if(preg_match('/'.$xpr.'/',$xpurl)){  
                               if($xpurl_new = preg_replace('/'.$xpr.'/',$rep,$xpurl)){$parts .= $rep;}  
                          }  
                     else{$parts .= $xpurl.'&';}  
                     }  
                       
                     if(preg_match('/signin/',$_SESSION['last_url']) || preg_match('/fpwd/',$_SESSION['last_url'])){  
                          if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.$default_plink.'/'.CURRENT_LANG;}  
                          else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$default_plink.'&'.LN.'='.CURRENT_LANG;}                           
                     }  
                     else{$location = rtrim($parts,"&");}  
                }  
                else{  
                     if(CLEAN_URLS == true){$location = CLEAN_PATH.'/'.DEFAULT_RLINK.'/'.$default_plink.CURRENT_LANG;}  
                     else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'='.DEFAULT_RLINK.$default_plink.'&'.LN.'='.CURRENT_LANG;}  
                }  
                header('Location: '.$location);exit();  
           }  
           else{$this->identified = 'wrong_credentials';}  
             
           return $this->identified;  
      }  
 ?>  
If you have survived these series up until now, you should not need an explanation for this last snippet, but just in case though… : once we have identified the user we go fetch its ID and language than determine the location to which we will redirect that user, based on whether a last URL was recorded and whether clean URLS are enabled or not.

Finally, now that our user has been able to log in manually, that same user may be wanting to review and update its personal data. This is just what profile module is all about. The profile module, which is alike the settings one, only differs from let’s say the signin module in that we need to get the user’s data in order to present it in the profile form.
 <?php  
      /* from application/controllers/ProfileController.php */  
   
      ...  
      // while in properties  
      private $user_data;  
      ...  
   
      ...  
      //while inside the initialize() method, after the intercept_data() method, and before assign_theme()  
      $this->registry->template->user_data = $this->getProfileFormData(); //we call the method right bellow  
      ...  
        
      private function getProfileFormData(){  
           $model = ProfileModel::getInstance();  
           return $model->getProfileFormData(); //we call and returns the method we need  
      }  
      ...  
 ?>  
and so, in the profile model we have
 <?php  
   
      /* from application/controllers/ProfileModel.php */  
   
      public function getProfileFormData(){  
             
           $this->user_profile=array();  
        
           //data for the profile module resides in the users table       
           $my_query_profile = "SELECT * FROM users WHERE id_user = '".$_SESSION['c_id']."' AND user_login = '".$_SESSION['c_login']."'";  
             
           MySQLModel::get_mysql_instance()->executeQuery($my_query_profile);  
             
                $this->up = MySQLModel::get_mysql_instance()->getRows($my_query_profile);  
                  
                if($this->up != ''){  
                     array_push($this->user_profile, $this->up['user_login'],$this->up['user_email'],$this->up['user_country'],$this->up['user_birthdate']);  
                }  
                  
                return $this->user_profile;  
      }  
 ?>  
It also differs in that once we have intercepted our form data, we need to update the database with it, and here is how we will do it
 <?php  
   
      /* from application/controllers/ProfileModel.php */  
   
      public function get_form_feedback($parms){  
             
           if(!empty($parms)){  
             
                $error_list = array();  
   
                $error_check_profile = self::check_profile_form_errors($parms);  
   
                if (sizeof($error_check_profile) > 0){array_push($error_list,$error_check_profile);}  
             
                if(sizeof($error_list) > 0){  
                     $this->form_feedback = $error_list;            
                }  
                else{            
                     $field_list = array('login','password','email','country','birthdate');  
                  
                          foreach($field_list as $fl){  
                               if(in_array($fl,$parms) && isset($_POST[$fl])){$$fl = $_POST[$fl];}  
                          }  
                  
                     $now = date("Y-m-d H:i:s");  
   
                     $q_update_profile = "UPDATE users   
                          SET user_email = '".$email."',  
                          user_country = '".$country."',  
                          user_birthdate = '".$birthdate."',  
                          user_timestamp = '".$now."'  
                          WHERE user_login = '".$login."'  
                          AND user_password = md5('".$password."')  
                          AND user_timestamp != '".$now."'  
                     ";                 
   
                     if(MySQLModel::get_mysql_instance()->executeQuery($q_update_profile) == 0){  
                          if(MySQLModel::get_mysql_instance()->affectedRows() == 1) {$this->form_feedback = 'success';}  
                          else{echo MySQLModel::get_mysql_instance()->mysql_fetch_errors();$this->form_feedback = 'wrong_password';}  
                     }  
                     else{$this->form_feedback = 'failed';}  
                }  
             
           }            
                return $this->form_feedback;  
      }  
 ?>  
You can see it’s very similar to any other get_form_feedback() methods from other models, only here we update the data. Note the use of the field user_timestamp which exists only to force MySQL to affect something if one of the values has then changed. Otherwise, if the update is performed and no rows were affected, there’s no programatic way to know it worked and so our code could only return a failed state, or nothing at best.

Regarding the profile form in profile.php, its structure appears to be no different than the registration form. You will only have to make sure your HTML form field names correspond to what is being processed in the model. On the model side, the check_profile_form_errors() methods is similar to check_reg_form_errors() so there’s no point repeating the same code over and over and besides, this will be a great exercise for you to test the knowledge you have acquired so far from these series.

By the way, let’s take a look at the user_settings table, because you will need it too
 --  
 -- Table structure for table `user_settings`  
 --  
   
 CREATE TABLE `user_settings` (  
  `id_user_settings` int(11) NOT NULL,  
  `user_settings_id_user` int(11) NOT NULL,  
  `user_settings_language` enum('de','es','fr','nl','ru','se','uk','en') NOT NULL DEFAULT 'en',  
  `user_settings_nipp` enum('10','15','20','50','75','100','150') NOT NULL DEFAULT '15',  
  `user_settings_newsletter` varchar(3) NOT NULL,  
  `user_settings_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;  
   
 --  
 -- Dumping data for table `user_settings`  
 --  
   
 INSERT INTO `user_settings` (`id_user_settings`, `user_settings_id_user`, `user_settings_language`, `user_settings_nipp`, `user_settings_newsletter`, `user_settings_timestamp`) VALUES  
 (1, 1, 'fr', '20', '', '2015-11-19 08:05:46');  
   
 --  
 -- Indexes for dumped tables  
 --  
   
 --  
 -- Indexes for table `user_settings`  
 --  
 ALTER TABLE `user_settings`  
  ADD PRIMARY KEY (`id_user_settings`);  
   
 --  
 -- AUTO_INCREMENT for dumped tables  
 --  
   
 --  
 -- AUTO_INCREMENT for table `user_settings`  
 --  
 ALTER TABLE `user_settings`  
  MODIFY `id_user_settings` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;  
Note the user_settings_nipp and newsletter fields are only here for demonstration, so you may remove them but as long as you modify your settings model accordingly of course.

Finally and regarding the forgotten password module or the change password module, there is nothing magical about them, the fpwd module is somehow very identical to the signin module and the pwdc module is relatively similar to the signin module too. Knowing that should get you on track faster while you attempt at building those new modules. Note that regarding the fpwd module, an email is sent with a new password, you may use the generate_new_pwd() method from UtilsModel.php.

Now we could not be coming full circle if we did not get in the subject of authentication again. Why? Simply because users should not be allowed to enter their profile data unless they are already logged in, which makes sense. So that, the first call inside the initialize() method of your profile controller will be to the get_module_auth() method, as described bellow
 <?php  
      /* from application/controllers/ProfileController/php */  
        
      public function initialize(){  
   
           $utils = new UtilsModel();            
           $this->registry->template->module_auth = $utils->get_module_auth();  
           ...  
      }       
 ?>  
Now the function get_module_auth() method itself, pretty much and explicit one.
 <?php  
   
      /* from application/models/UtilsModel/php */  
   
      public function get_module_auth(){  
        
           if(CURRENT_PLINK){  
                if(CLEAN_URLS == true){$pg = '/'.CURRENT_PLINK;} else{$pg = '&'.PLINK.'='.CURRENT_PLINK;}  
           }  
           else{$pg = '';}       
                       
           if(isset($_SESSION['c_login']) && isset($_SESSION['c_pwd']) && $_SESSION['c_login']!='' && $_SESSION['c_pwd']!=''){  
                $this->module_auth = 0;  
           }  
           else{  
                $this->module_auth = 1;  
                if(CLEAN_URLS == true){$location = CLEAN_PATH.'/signin/'.CURRENT_LANG;}  
                else{$location = $_SERVER['PHP_SELF'].'?'.RLINK.'=signin&'.LN.'='.CURRENT_LANG;}  
                header('Location: '.$location);exit();  
           }  
      }  
 ?>  
And that wraps up this fifth settlement dedicated to building a user interface that allows someone to register, sign in, review and update profile. From this, you can now easily extend what you have already learned to build a settings module, same as a profile module but that will rely on the user_settings table, a fpwd module that will handle cases of forgotten passwords and even a pwdc module, that will allow users to change password, first checking of course on the validity of the password they enter, then checking that the new and confirm new passwords are of correct format and match each other too.

Next time I will get into full detail as to the subject of theme and language handling, reviewing all aspects of what makes the visual identity of this project and also discuss a method for localizing strings.

This article first appeared Thursday the 7th of January 2016 on RolandC.net.