Posted on

Easier PHP forms, part 2

In part 1 I showed you my method for simplifying form building and maintenance. Now I’d like to show you a few of the things that can be simplified by using this system. Included are:

  • prices
  • passwords
  • emails
  • textareas
  • selects
  • yes/no (booleans)
  • files
  • hiddens
  • cancel buttons
  • submit buttons
  • “are you sure you want to do that?” submit buttons
  • addresses

I use this code for 95% of the forms I build and then modify the CSS to get the effects a customer wants.

In part 3 I’ll show you how to use jQuery to automatically validate user data.

//------------------------------------------------------------------------------
function create_form_price($name,$value=null,$label=null,$hint=null) {
  return create_form_row("\$<input type='text' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'>",$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_password($name,$value=null,$label=null,$hint=null) {
  return create_form_row("<input type='password' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'>",$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_password_confirm($name,$value=null,$label=null,$hint=null) {
  add_form_test_password_confirm("pass",$label);  // we'll cover this in part 3

  return create_form_row("<input type='password' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'>",$label,$name,"").
  create_form_row("<input type='password' name='{$name}_confirm' value=''>","&nbsp;",$name."_confirm",$hint);
}

//------------------------------------------------------------------------------
function create_form_email($name,$value=null,$label=null,$hint=null) {
 // @TODO: add email validation.
 $test="    regex_{$name}=/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*(\.[a-zA-Z]{2,4})$/\n"
 ."    compare_to=f.{$name}.value.replace(/^\s+|\s+$/g,'');\n"
 ."    if(compare_to!='' && !compare_to.match(regex_{$name})) {\n"
 .add_form_error($name,"$label does not appear to be a valid email.\n")
 ."    }\n";
 add_form_test($test,$name);

 return create_form_row("<input type='text' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'>",$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_textarea($name,$value=null,$label=null,$hint=null) {
 return create_form_row("<textarea name='$name' rows='10' cols='80'>".htmlspecialchars($value,ENT_QUOTES)."</textarea>",$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_select_internal($name,$options,$value=null,$multiple=0,$extra='') {
 $sel="<select id='$name' name='$name' $extra";
 if($multiple>0) {
 $sel.=" multiple='yes' size='$multiple'";
 }
 $sel.=">\n";
 if(is_array($options)) {
 foreach($options as $k=>$v) {
 $k=htmlspecialchars($k,ENT_QUOTES);
 $v=htmlspecialchars($v,ENT_QUOTES);
 $sel.="  <option value='$k'>$v</option>\n";
 }
 }
 $sel.="</select>";
 $v=isset($value)?htmlspecialchars($value,ENT_QUOTES):"";
 return str_replace("value='$v'","value='$v' selected",$sel);
}

//------------------------------------------------------------------------------
function create_form_select($name,$options,$value=null,$label=null,$hint=null,$multiple=0,$extra='') {
 $sel=create_form_select_internal($name,$options,$value,$multiple,$extra);
 return create_form_row($sel,$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_bool($name,$value=null,$label=null,$hint=null,$extra='') {
 $sel="<select name='$name' $extra>\n";
 $sel.="  <option value='yes'>"._t("Yes")."</option>\n";
 $sel.="  <option value='no'>"._t("No")."</option>\n";
 $sel.="</select>";
 $v=isset($value)?htmlspecialchars($value,ENT_QUOTES):"";
 $sel=str_replace("value='$v'","value='$v' selected",$sel);
 return create_form_row($sel,$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_file($name,$label='',$hint='') {
 $inner="<input name='$name' type='file'>";

 return create_form_row($inner,$label,$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_address($name,$value=null,$label=null,$hint=null) {
 if(!isset($value['address1'])) $value['address1']="";
 if(!isset($value['address2'])) $value['address2']="";
 if(!isset($value['city'])) $value['city']="";
 if(!isset($value['region'])) $value['region']="";
 if(!isset($value['country'])) $value['country']="";
 if(!isset($value['postalcode'])) $value['postalcode']="";

 $str =create_form_text($name."_address1",$value['address1'],"Street");
 $str.=create_form_text($name."_address2",$value['address2'],"");
 $str.=create_form_text($name."_city",$value['city'],"City");
 $str.=create_form_text($name."_region",$value['region'],"State/Province");
 $str.=create_form_text($name."_country",$value['country'],"Country");
 $str.=create_form_text($name."_postalcode",$value['postalcode'],"Postal Code");

 return create_form_row($str,$label,null,null);
}

//------------------------------------------------------------------------------
function create_form_hidden($name,$value=null) {
 return "  <input type='hidden' id='$name' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'>\n";
}

//------------------------------------------------------------------------------
function create_form_submit($name,$value,$extra='') {
 global $last_form_name;  // we'll cover this is in part 3

 return "  <input type='submit' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."' onclick='javascript:return validate_$last_form_name(this.form);' $extra>\n";
}

//------------------------------------------------------------------------------
function create_form_cancel($name,$value,$redirect) {
 return "  <input type='button' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."' onclick='javascript:window.location=\"".htmlspecialchars($redirect,ENT_QUOTES)."\";'>\n";
}

//------------------------------------------------------------------------------
function create_form_submit_confirm($name,$value,$confirm="Are you sure?",$validate=0) {
 global $last_form_name;  // we'll cover this is in part 3

 $str="  <input type='submit' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."' onclick='javascript:return (confirm(\"".str_replace(array("'","\n"),array("\'","\\n"),$confirm)."\")";
 if($validate!=0) {
 $str.=" & validate_$last_form_name(this.form)";
 }
 $str.=");'>\n";
 return $str;
}
Posted on

Easier PHP forms, part 1

Building and maintaining forms in PHP is one of the most time-consuming parts of the job. Put another way, it’s one of the biggest bottlenecks for fast prototyping and development. Getting rid of bottlenecks speeds up work and increases overall joy. So how do we do this?

The first thing I did was look at all the forms I had been writing. In general they all follow a pattern:

<DOCTYPE ...>
<html ...>
<head>
  ...
  // javascript tests to make sure user fills form correctly
</head>
<body>
...
<form name='blah' action='#' method='post'>
...

  <div class='form_item'>
    <label for='ABC'>A human-readable name for the input</label>
    <div class='input_item'>
      <input name='ABC'>  <?/* or select/textarea/etc */?>
      <div class='help'>Some explanation text here.</div>
    </div>
    <div class='clear'></div>
  </div>
...
</form>
...
</body>
</html>

It seemed that cutting and pasting this was rife with errors. Even worse is trying to change the layout of the page – a major (non-CSS) alteration could really ruin my day! I had to come up with somethin better.

function create_form_row($input,$label_name=null,$label_for=null,$hint=null) {
                         $str ="  <div class='form_item'>\n";
  if(isset($label_name)) $str.="    <label for='$label_for'>$label_name</label>\n";
                         $str.= "    <div class='input_item'>\n";
                         $str.= "      $input\n";
  if(isset($hint))       $str.= "      <div class='help'>$hint</div>\n";
                         $str.= "    </div>\n";
                         $str.= "    <div class='clear'></div>\n";
                         $str.= "  </div>\n";
  return $str;
}

function create_form_start($name,$classname='form',$action='',$method='post',$target='') {
  return "<form enctype='multipart/form-data' id='$name' name='$name' class='$classname' action='$action' method='$method'>\n";
}

function create_form_end() {
  echo '</form>\n';
}

At first this might seem like a lot of work for not much gain. Think of this as the center of the onion. If we build layers on top of it we start to see some big big benefits.

function create_form_check_inner($name,$value=null,$checked=false) {
  return "<input type='checkbox' id='$name' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."'".($checked?" checked":"").">";
}

//------------------------------------------------------------------------------
function create_form_check($name,$value=null,$label=null,$hint=null,$checked=false) {
  return create_form_row($label,create_form_check_inner($name,$value,$checked),$name,$hint);
}

//------------------------------------------------------------------------------
function create_form_text_inner($name,$value=null,$extra='') {
  return "<input type='text' id='$name' name='$name' value='".htmlspecialchars($value,ENT_QUOTES)."' $extra>";
}

//------------------------------------------------------------------------------
function create_form_text($name,$value=null,$label=null,$hint=null,$extra='') {
  return create_form_row(create_form_text_inner($name,$value,$extra),$label,$name,$hint);
}

So now when I want to create a new form all I do is

echo create_form_start('form1');
echo create_form_text('user_name','','User Name','what is your name?');
echo create_form_check('love_this','yes','Love','Do you love this new form system?',true);
echo create_form_end();

In Part II I’ll show you how to use create_form_ elements for passwords, selects, textareas, and more.

In Part III I’ll show you how you can use this system and jQuery to add checks that make sure users put in valid emails, required fields, and so on.

Posted on

PHP login handling tutorial – sessions & cookies included

I see a lot of people trying to write code to authenticate users logging into a PHP website. This is some code I cobbled together in december of 2008 and it has worked problem free since then.

// I've already sanitized all GET, POST, and COOKIE data at this point.
function check_login() {
  global $DB;

  $login_justnow=false;

  // if the user isn't logged in and they're POSTing a login request, process it
  if(!get_session('user/id') && isset($_POST['login']) {
    $name=$_POST['login_name'];  $remember_me=isset($_POST['remember_me'])?1:0;
    $pass=md5($_POST['login_pass']);
    $user_id=$DB->QueryXY("SELECT id FROM `users` WHERE name='$name' AND pass='$pass' AND confirmed='1' LIMIT 1");
    if(isset($user_id)) {
      account_login($user_id,$remember_me);
      $login_justnow=true;
      $name=get_session("user/given_name")?', '.get_session("user/given_name"):'';
      add_notice("Welcome$name!");
    } else {
      add_error("Login failed.");
      account_logout();
    }
  }

  // if the user isn't logged in but has a COOKIE, process it
  if(!get_session("user/id") && isset($_COOKIE["remember_me"]) ) {
    list($user_id,$cookie_code)=@unserialize(stripslashes($_COOKIE["remember_me"]));
    if(isset($user_id) && isset($cookie_code)) {
      $cookie=md5($cookie_code);
      $result=$DB->QueryArray("SELECT * FROM `users` WHERE id='$user_id' AND cookie='$cookie' AND confirmed='1' LIMIT 1");
      if(count($result)) {
        account_login($user_id,true);
        $login_justnow=true;
        $name=get_session("user/given_name")?', '.get_session("user/given_name"):'';
        add_notice("Welcome$name!");
      }
    }
  }

  // if the user's session says they're logged in, process it
  if(get_session("user/id") && $login_justnow===false) {
    $user_id=get_session("user/id");
    $cookie=get_session("user/cookie");
    $ip=get_session("user/ip");
    $session=session_id();

    $query="SELECT * FROM `users` WHERE id='$user_id' AND ip='$ip' AND session='$session' AND cookie='$cookie' AND confirmed='1' LIMIT 1";
    $result=$DB->DoQuery($query);

    if($DB->NumRows($result)) {
      account_login($user_id,false);
    } else {
      add_error("Session security failed.");
      account_logout();
    }
    $DB->EndQuery($result);
  }

  // check if the user actually has rights to this part of the site - your implementation may vary
}

function account_login($user_id,$remember_me) {
  global $DB;

  if(user_is_logged_in())
    return;

  // update cookie
  if($remember_me==true) {
    $cookie_code=generate_random_string();
    $cookie_str=serialize(array($user_id, $cookie_code));
    setcookie('remember_me', $cookie_str, time() + 60*60*24*30, '/');
    add_session("user/cookie",$cookie_code);
  } else {
    remove_session("user/cookie");
  }

  // update session security
  $ip=$_SERVER['REMOTE_ADDR'];
  $session=session_id();
  $cookie_code=get_session("user/cookie");
  $cookie=md5($cookie_code);
  $DB->DoQuery("UPDATE `users` SET last_on=NOW(), session='$session'"
    .(($cookie_code!='')?", cookie='".$cookie."'":"")
    .", ip='$ip' WHERE id='$user_id' LIMIT 1");

  // update session info
  $result=$DB->DoQuery("SELECT * FROM `users` WHERE id='$user_id' LIMIT 1");
  $row=$DB->FetchAssoc($result);
  foreach($row as $k=>$v) {
    add_session("user/".$k,$v);
  }
  $DB->EndQuery($result);

  // check if any other part of your system needs to know about a user logging in.
}

function user_is_logged_in() {
 return get_session("user/id")!=0;
}

function generate_random_string($length=32) {
 $random="";
 srand((double)microtime()*1000000);
 $char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 $char_list.= "abcdefghijklmnopqrstuvwxyz";
 $char_list.= "1234567890";

 for($i=0;$i<$length;++$i) {
 $random.=substr($char_list,(rand()%(strlen($char_list))), 1);
 }

 return $random;
}
Posted on

Updating PHP cookies without breaking the experience

PHP cookies are a great idea for storing data temporarily. Sure, they have their security problems but (arguably) they do more good than bad. The trickiest part about cookies is when you update your PHP and it forces you to change the contents of your cookies. You don’t want your faithful site visitors to see an error message! Consider the following naive implementation:

function save() {
  setcookie("username","foo",60*60*24*30);
}

function load() {
  return isset($_COOKIE["username"])?$_COOKIE["username"]:'';
}

It doesn’t take a lot of imagination to see how this could break. Let’s start by making it easier to store lots of info in a single cookie.

function save() {
  $data=array('username'=>'foo', 'something else'=>'bar');
  $str=serialize($data);
  setcookie("userdata",$str,60*60*24*30);
}

function load() {
  $data=array('username'=>'','something else'=>'');
  if(isset($_COOKIE['userdata'])) {
     $data=unserialize($_COOKIE['userdata']);
  }
  return $data;
}

Ok, but what about if we need to add more data to our cookies later? Everyone with an old cookie will be missing parts of $data and we’ll have to catch that.

function save() {
  $data=array('version'='1.0', 'username'=>'foo', 'something else'=>'bar');
  $str=serialize($data);
  setcookie("userdata",$str,60*60*24*30);
}

function load() {
  $data=array('username'=>'','something else'=>'');
  $old=false;
  if(isset($_COOKIE['userdata'])) {
    try {
      $data=unserialize($_COOKIE['userdata']);
    }
    catch(Exception $e) {
      $old=true;  // this version is so old & busted we can't read it at all.
    }
    if(!$old) {
      // Use the $data['version'] to help us figure out what pieces of $data need to be filled in.
    }
  }
  return $data;
}

And… voila!