php|architect (September 2004)

VOLUME III - ISSUE 9 SEPTEMBER 2004 TM www.phparch.com The Magazine For PHP Professionals Plus: Tips & Tricks, Prod...

33 downloads 844 Views 3MB Size Report

This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!

Report copyright / DMCA form

’; 13 } 14 15 function selectPulldown($addressfieldid, $addressfieldlabel, $options) { 16 $return=’
<select name=”’.$addressfieldid.’” id=”’.$addressfieldid.’”>’; 17 foreach($options as $key=>$value) { 18 $return.=’’; 21 } 22 $return.=’
’; 23 return $return; 24 } 25 26 function getFinalizeValue($label, $value) { 27 return ‘
<span class=”cartfinalizelabel”>’.$label.’: <span class=”cartfinalizevalue”>$’.number_format($value, 2).’
’; 28 } 29 30 function getTotal() { 31 $return=getFinalizeValue($GLOBALS[‘shipping’]->shipmethods[$_POST[‘ServiceCode’]].’ shipping’, $GLOBALS[‘shipping’]>shippingDetails[$_POST[‘ServiceCode’]]->MonetaryValue); 32 if($GLOBALS[‘taxstate’]==$_POST[‘billingstate’]) $return.=getFinalizeValue(‘Tax’, $GLOBALS[‘taxcost’]); 33 $return.=getFinalizeValue(‘Total’, $GLOBALS[‘total’]); 34 return $return; 35 } 36 37 function digitsOnly(&$string) { 38 $string=preg_replace(‘/\D/’, ‘’, $string); 39 } 40 41 function confirmUserData() { 42 global $error; 43 foreach(array(‘cc’, ‘shippingzip’, ‘billingzip’, ‘telephone’) as $val) digitsOnly($_POST[$val]); 44 45 $requiredtextfields=array(‘full name’, ‘address 1’, ‘city’); 46 foreach($GLOBALS[‘addresstypes’] as $addresstype) foreach($requiredtextfields as $field) { 47 if(strlen($_POST[$addresstype.str_replace(‘ ‘, ‘’, $field)])<3) $error[]=ucfirst($addresstype).’ ‘.$field.’ must be filled out’; 48 } 49 if(strlen($_POST[‘billingzip’])!=5) $error[]=’Billing zip is invalid’; 50 if(strlen($_POST[‘telephone’])!=10) $error[]=’Invalid telephone number’; 51 if(!preg_match(‘/([\w!#$%”*+\/=?`{}|~^-]+)@(([\w-]+\.)+[\w-]+)/’, $_POST[‘email’])) $error[]=’Invalid email address’;

Continued on page 43 September 2004



PHP Architect



www.phparch.com

42

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

our cc class (Listing 1) by setting the mod10digits property to the following array: 0=>0, 1=>2, 2=>4, 3=>6, 4=>8, 5=>1, 6=>3, 7=>5, 8=>7, 9=>9

Notice the distinct pattern that the values take; one through four map to the even numbers, 2, 4, 6, 8 and five through nine are the odd numbers of 1, 3, 5, 7 and 9. Next, we create the mod10valid method, which starts by calculating the string length of the credit card. All common credit cards have between 13 and 16 numeric digits, so the length is validated on line 8 before any calculations begin. Next, we determine if there is an odd or even amount of digits on the card by checking for a remainder when dividing the card length by two and saving the result to the startbit variable (line 11). We then loop through each digit of the credit card. The currentbit variable is a Boolean that alternates

value at each iteration of the loop (line 13). When the currentbit equates to the startbit (line 14), we replace the original digit with the value from the mod10digits array—this will, of course, occur for every other number. The digit is then added on line 15 to the digitsum variable that maintains a running sum of the digits. On line 17, we check if dividing the digitsum by 10 yields a remainder. If it does, the card is not valid and in such a case we want to return false, while if it is we want to return true—so, we negate the results of digitsum %10 to return true for a valid card and false for an invalid card. Taking the Customers Money The charge method attempts to charge a credit card through the AuthorizeNet gateway. There are three arguments that need to be passed in order to make the transaction: the amount, the credit card number and the expiration date.

Listing 2: Continued from page 42 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106

$cc=new cc; if(!$cc->mod10valid($_POST[‘cc’])) $error[]=’Invalid credit card number’; if(date(‘ym’)>$_POST[‘expirationyear’].$_POST[‘expirationmonth’]) $error[]=’Credit card is expired’; } function mysql_escape_string_by_reference(&$string) { $string=mysql_escape_string($string); } function trim_by_reference(&$string) { $string=trim($string); } function stripslashes_by_reference(&$string) { $string=stripslashes($string); } array_walk($_POST, ‘trim_by_reference’); if(get_magic_quotes_gpc()) array_walk($_POST, ‘stripslashes_by_reference’); if(isset($_SESSION[‘shippingzip’])) { $shipping=new shipping; $shipping->xmlaccesskey=$upsxmlaccesskey; $shipping->userid=$upsuserid; $shipping->password=$upspassword; $shipping->originzip=$upsoriginzip; $shipping->totalweight=$weight; $shipping->shippingzip=$_SESSION[‘shippingzip’]; if(!$shipping->getRates()) $error[]=$shipping->errormessage; } if(isset($_POST[‘ServiceCode’])) { $servicecode=$_POST[‘ServiceCode’]; $shippingcost=(Float)$shipping->shippingDetails[$servicecode]->MonetaryValue; $total=($subtotal+$shippingcost); $taxcost=($taxstate==$_POST[‘billingstate’]?round($total*($taxrate/100), 2):0); $total+=$taxcost; } $action=’showform’; if(!isset($_POST[‘adjustinfo’])) { if(isset($_POST[‘orderconfirmed’])) { confirmUserData(); if(!isset($error)) { $cc=new cc; $cc->login=$authnetlogin; $cc->password=$authnetpassword; $authcode=$cc->charge($total, $_POST[‘cc’], $_POST[‘expirationmonth’].$_POST[‘expirationyear’]); if($authcode!=’1’) $error[]=’Credit card did not clear. ‘.$cc->responsetext; } if(!isset($error)) $action=’checkoutconfirmed’; } else if(isset($_POST[‘shippingaddress1’])) { confirmUserData(); if(!isset($error)) $action=’showconfirm’;

Continued on page 44 September 2004



PHP Architect



www.phparch.com

43

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

AuthorizeNet expects the data as a POST string of name/value pairs. We start by creating an array containing the AuthorizeNet login and password, the transaction type (authorization and capture), the customer’s IP address for security and the three arguments required for the transaction. We then use the new PHP5 function http_build_query() on line 33 to create a query string from the postfields array. Next, we define the header that will initialize the socket. This is basically the same header as used to connect to UPS, but with a URL and path pointing to https://secure.authorize.net/gateway/transact.dll

instead of the UPS application. The socket stream connection begins the same as when connecting to UPS, but varies in getting the response data (line 43) in two ways. The first is that we know the response will be on only one line, but that line could be quite long, so we simply call fgets() once without putting it into a while loop and we do not set a maximum length of charac-

ters to read per line. In addition, the AuthorizeNet transaction gateway is apparently an IIS-based server—and IIS is known to not close SSL connections in the expected manner. While this does not effect the data transmission, it does lead to a PHP warning message, so we suppress the error notification by preceding the fgets() call with an at sign. By default, the string returned is made up of a series of comma-delimited values. We explode this string on line 46 into a variable aptly named response. We then get the data that we need out of the response variable—a description of the transaction’s result, the approval code and the transaction ID—from positions 4, 5 and 7 of the array (since the array is zero-based, we actually get the data from the array positions 3, 4 and 6). We return the response code, which is in the initial position in the response (array key 0). A response code value of 1 indicates an approved transaction, while 2

Listing 2: Continued from page 43 107 } else if(!isset($_SESSION[‘shippingzip’])||isset($error)||isset($_GET[‘restart’])) { 108 $action=’adjustshippingzip’; 109 } 110 } 111 112 if(isset($error)) foreach($error as $errormessage) echo ‘
’.$errormessage.’
’; 113 114 switch($action) { 115 case ‘showform’: 116 echo ‘
Checkout
Shipping options’; 117 if(!isset($_POST[‘ServiceCode’])) $_POST[‘ServiceCode’]=key($shipping->shippingDetails); 118 foreach($shipping->shippingDetails as $ServiceCode=>$obj) { 119 echo ‘
’; 128 } 129 echo ‘
’; 130 131 $constantData=new constantData; 132 if(!isset($_POST[‘billingzip’])) $_POST[‘billingzip’]=$_SESSION[‘shippingzip’]; 133 foreach($addresstypes as $addresstype) { 134 echo ‘

’.ucfirst($addresstype).’ Address

’; 135 if($addresstype==’billing’) echo ‘<script type=”text/JavaScript” src=”datacopy.js”>’; 136 foreach($addressfields as $addressfield) { 137 if($addressfield==’state’) { 138 echo selectPulldown($addresstype.’state’, ‘State’, $constantData->states); 139 continue; 140 } 141 142 if($addressfield==’zip’&&$addresstype==’shipping’) { 143 echo ‘
<span class=”checkoutuserdata”>’.$_SESSION[‘shippingzip’].’ [adjust]
’; 144 continue; 145 } 146 echo inputText($addresstype.str_replace(‘ ‘, ‘’, $addressfield), ucfirst($addressfield)); 147 } 148 } 149 150 echo ‘

Contact Information

’; 151 foreach($contactfields as $contactfield) echo inputText($contactfield, ucfirst($contactfield)); 152 153 echo ‘

Billing Information

’; 154 echo inputText(‘cc’, ‘Credit card number’); 155 156 echo selectPulldown(‘expirationmonth’, ‘Expiration month’, $constantData->months); 157 $thisyear=date(‘Y’); 158 for($year=$thisyear;$year<($thisyear+10);$year++) $years[substr($year, 2)]=$year;

Continued on page 45 September 2004



PHP Architect



www.phparch.com

44

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

means that it was declined, 3 indicates an error and 4 means that the transaction is being held for merchant review. For a detailed description of the other fields, you can check the AuthorizeNet documentation. Putting it all together The checkout.php file (Listing 2) handles the entire checkout process. Before the user can reach the checkout page, he would have to have entered his shipping ZIP code into the form generated by the getAddToCart method of the htmlOutput class. Once they do that and submit the form, they will be presented with shipping options and a form to enter their shipping, billing, contact and payment information. Upon submitting their information, they will be brought to a confirmation page to verify their data. From that page, the user has the choice to submit the order or go back and adjust their information. Should they submit the order and their data is valid, their credit card will be charged, the

order will be saved in the database, the cart session variable will be unset and a thank you message will be displayed. The initial data that a user enters to get to the checkout page is the shipping ZIP code. On line 2, we check to see if the ZIP code provided by the user exists in the $_POST array and if it does, we save it to a session variable of the same name. We do so in case the user just wants to see the shipping rates and then decides to continue to shop intending to return to the checkout at a later time. Upon return, the shipping ZIP code form input will be populated with the previously entered value. This makes it easier for the customer to pay and that in turn increases the likelihood that the merchant will make a sale. Since this is where all the personal and billing information will be handled, we will always want to encrypt the data transmitted between the browser and server. And so line 3 is basically the same code that is in glob-

Listing 2: Continued from page 44 159 echo selectPulldown(‘expirationyear’, ‘Expiration year’, $years); 160 161 echo ‘
’; 162 163 break; 164 case ‘showconfirm’: 165 echo getTotal(); 166 167 echo ‘
’; 168 $constantData=new constantData; 169 $_POST[‘shippingzip’]=$_SESSION[‘shippingzip’]; 170 foreach($addresstypes as $addresstype) { 171 echo ‘

’.ucfirst($addresstype).’

’; 172 foreach($addressfields as $addressfield) { 173 $userdata=$_POST[$addresstype.str_replace(‘ ‘, ‘’, $addressfield)]; 174 if($addressfield==’state’) $userdata=$constantData->states[$userdata]; 175 echo ‘
<span class=”checkoutuserdata”>’.htmlentities($userdata).’
’; 176 } 177 } 178 179 echo ‘

Contact Information

’; 180 foreach($contactfields as $contactfield) echo ‘
<span class=”checkoutuserdata”>’.$_POST[$contactfield].’
’; 181 182 echo ‘

Billing Information

’; 183 echo ‘
<span class=”checkoutuserdata”>’.$_POST[‘cc’].’
’; 184 echo ‘
<span class=”checkoutuserdata”>’.$constantData>months[$_POST[‘expirationmonth’]].’
’; 185 echo ‘
<span class=”checkoutuserdata”>20’.$_POST[‘expirationyear’].’
’; 186 echo ‘
’; 187 188 echo ‘
Checkout
’; 189 foreach($_POST as $key=>$val) echo ‘’; 190 echo ‘
’; 191 192 break; 193 case ‘checkoutconfirmed’: 194 echo getTotal(); 195 196 array_walk($_POST, ‘mysql_escape_string_by_reference’); 197 198 $sql=’select (max(orderid)+1) from orders’; 199 $res=mysql_query($sql); 200 $row=mysql_fetch_row($res); 201 mysql_free_result($res); 202 $orderid=$row[0]; 203 204 $approvalcode=$cc->approvalcode; 205 $transactionid=$cc->transactionid; 206 207 $orderspecifics=array(‘orderid’, ‘subtotal’, ‘shippingcost’, ‘taxcost’, ‘total’, ‘servicecode’, ‘approvalcode’, ‘transactionid’); 208 209 foreach($orderspecifics as $orderspecific) $orderfields[$orderspecific]=$$orderspecific;

Continued on page 46 September 2004



PHP Architect



www.phparch.com

45

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

al.php, only in this situation the logic of the code is inverted to assure that the browser is connecting securely. Next, we check that the cart has something in it. We do not want to show a checkout form to users with an empty cart, so if the cart is empty we forward them out of the checkout page and into the showcart.php cart contents displayer page that will, in turn, display the empty cart notification. Next, we include showcart.php, so that the checkout page will always start with a confirmation of the products being ordered. Thanks to the logic that we built into the cart display manager earlier, the display of the order is static text, so the products can no longer be removed, nor can their quantities be adjusted without exiting the checkout page and navigating to showcart.php directly. We will want to gather the telephone number and email address where the customer can be contacted, along with name and address at both the customer’s billing and shipping locations. This equates to quite a few fields and we do not want to type all that HTML to create them, so we will let PHP generate it for us. On lines 7 through 9, we create an array for the address types, billing and shipping, then another array of the address fields such as city, state and zip and yet another array for telephone and email. The arrays contain all lowercase values containing spaces. When these are used as the input name, the spaces will be stripped out, while when they are used as the humanly visible input label, the first letter will be capitalized for aesthetics. We will need to define some functions before dipping into the logic of the code: • The inputText function on line 11 will return the HTML to produce a form input box along with the appropriate label. If a POST value with the same name as the input box exists, it will be used as the default value for

the input box. • The selectPulldown function does the same thing, only generating a drop-down menu instead of an input text box. Population of the pull down menu options comes from an associative array passed in as an argument; the array key becomes the option’s value (line 18) and the array value becomes the text that is displayed to the user (line 20). • The getFinalizeValue function wraps and returns each line of the finalizing parts of the order (shipping, tax rate and total) with the appropriate HTML. • The getTotal function calls getFinalizeValue to display the shipping cost and the total. The tax is also displayed if the order is billed to the same state that the merchant does business in (line 32). Fields such as telephone number and zip code must only contain numeric data so for that purpose we create a digitsOnly function that uses regex to strip out non-numeric characters. The next function, confirmUserData(), checks the validity of submitted data in each of the mandatory fields. First, the credit card number, the telephone number along with the shipping and billing zip codes get stripped of non-numeric characters via a call to digitsOnly() on line 43. Then the full name, first line of the address field and city for both the shipping and billing locations are checked to contain at least three characters of data (lines 46 through 48). If they do not pass the validation, an error message is stored in the error array. We also check that the telephone number

Listing 2: Continued from page 45 210 foreach($contactfields as $contactfield) $orderfields[$contactfield]=$_POST[$contactfield]; 211 foreach($addresstypes as $addresstype) foreach($addressfields as $addressfield) { 212 $fieldid=$addresstype.str_replace(‘ ‘, ‘’, $addressfield); 213 $orderfields[$fieldid]=$_POST[$fieldid]; 214 } 215 216 $sql=”insert into orders (“.implode(‘,’, array_keys($orderfields)).”) values (‘“.implode(“‘,’”, array_values($orderfields)).”’)”; 217 mysql_query($sql); 218 219 foreach($_SESSION[‘cart’] as $productid=>$quantity) $orderproducts[]=”(‘“.$orderid.”’,’”.$productid.”’,’”.$productDetails[$productid]>price.”’,’”.$quantity.”’)”; 220 $sql=”insert into orderproducts (orderid, productid, price, quantity) values “.implode(‘,’, $orderproducts); 221 mysql_query($sql); 222 223 unset($_SESSION[‘cart’]); 224 echo ‘

Thank you, your order has been received.

’; 225 break; 226 case ‘adjustshippingzip’: 227 echo $htmlOutput->getCheckoutForm(); 228 break; 229 } 230 ?>

September 2004



PHP Architect



www.phparch.com

46

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

has 10 digits and the billing ZIP code has 5. The reason that we only check the billing ZIP code and not the shipping zip code is that if the shipping ZIP code were invalid, UPS will not accept it and an error will be thrown stating that there is no service available for it. We then use regex to check for a valid email address on line 51. Following that, we validate the credit card number with the mod10 method of the cc class that we created earlier. We also verify on line 55 that the expiration date has not yet passed by concatenating the current year and month into a single four-digit number, doing the same to the submitted year and month and checking which value is greater. Later in the code, we will want to escape any characters in the user data that cannot be directly entered into a SQL string. We will be using array_walk() to pass each element in the $_POST array through the mysql_escape_string() PHP function. In order to do that, we will need to utilize the mysql_escape_string function by reference, so we make a function called mysql_escape_string_by_reference() that does exactly that. We will also need to trim the white space off the data, so we will create a trim_by_reference function as well. We immediately pass the entire $_POST array through trim_by_reference() via array_walk(). This gets rid of the heading and trailing white space on all the submitted fields in one easy step. If the magic_quotes_gpc setting in the php.ini file is set to On, the POST array will have the escapeslashes function applied to them at each phase of the shopping cart, leading to some very messy values. To counteract this, we create a stripslashes_by_reference function and then on line 71 we check if magic_quotes_gpc is enabled with the get_magic_quotes_gpc PHP function; if it is, we pass the $_POST array through stripslashes_by_reference() via array_walk().

We will need to retrieve the shipping rates from UPS regardless of which phase of the checkout process the customer is in, as long as the shipping ZIP code has been set, so we create a new instance of the shipping class on line 74 and set the basic properties that are defined in the global.php file: xmlaccesskey, userid, password and originzip. The weight variable comes from the inclusion of showcart.php that calculates the total weight for the order. We set the shippingzip property to the value in the shippingzip session and then we attempt to get the rates via the getRates method. If we get a return value of false (line 81), we add the UPS error message string stored in the errormessage property to our error array. If the user has already chosen a shipping rate, signifying that they are at least in the second step of checkout, the shipping method would be set in the ServiceCode variable of the $_POST array. If we know which shipping method has been chosen, we can calculate the tax and total for the order. First we create local variables for the service code and shipping cost (lines 85 and 86), which we will need later. Then, we add the shipping rate to the subtotal and save the result to a variable suitably named total. On line 88, we check if the billing state is the same as the state that the merchant operates out of; if it is, we must charge sales tax, so we divide the tax rate by 100 to get the percent value, multiply by the total and round the number to two digits following the decimal point and save the results to the taxcost variable. If it is an out-ofstate sale, then taxcost is set to zero. Finally, taxcost is added to the total. Taking Action The action variable is set to default to showform on line 92; this variable will be used shortly to determine the course of action to take. Before we are sure of what to

Listing 3 function datacopy() { if(document.getElementById(‘samedata’).checked) { var inputtags=document.body.getElementsByTagName(‘input’); var inputtagslength=inputtags.length; var billingobject; for(var x=0; x
document.getElementById(‘billingstate’).selectedIndex=document.getElementById(‘shippingstate’).selectedIndex; } } if(typeof document.getElementById!=’undefined’) { document.write(‘
’); document.getElementById(‘samedata’).onclick=datacopy; }

September 2004



PHP Architect



www.phparch.com

47

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

do, we must validate the data submitted by the user. If the user clicked the button on the confirm page to adjust their information, we do not need to confirm the integrity of their data, so in that case we can just bypass the whole validation. If the user confirmed their data and submitted the order, we call the confirmUserData function on line 95 and then check if there are any errors. If there were no errors then we proceed to charge the credit card via the charge method of the cc class (line 100). If we receive a response code other than one then the charge did not clear and the responsetext property of the cc class would contain the error message. In such a case we add the value of the responsetext property to the error array (line 101). If no errors occurred, the default action gets overridden and set to checkoutconfirmed (line 103). This action, in turn, will store the order in the database, unset the cart session and display a thank you notice, as you will see later. If the order has not yet been confirmed then we check for the existence of the shippingaddress1 element in the $_POST array on line 104. If it exists, we know that the user submitted the data already, so we call confirmUserData() if there are no errors we set the action to show the user a page confirming their data. If none of the above apply and either an error was captured or the shippingzip does not exist in the session array, it is most likely that the user is navigating to the cart page without using the checkout form or the shipping ZIP code was rejected by UPS. In such a case, the action is set to adjustshippingzip, which will display the shipping ZIP form the same as the showcart.php page does. By this point, we have completed capturing any errors. If we found any, they are echoed out on line 112. Now, we move on to handle each action. Show Form The first step in showing the checkout form is to start the HTML form tag within a fieldset tag that will wrap around the entire check out form. The first set of fields will be radio buttons, one for each of the available shipping options (line 118). These will be contained within their own ‘Shipping options’ fieldset, which, in turn is part of the main fieldset. If the user has chosen to adjust their data, he would have already selected a shipping option, thus that radio button will be selected by default. If they have not, we set the first radio button as the default on line 117 by using the key PHP function to get the first array key from the shippingDetails property of the shipping method. Not all the shipping methods guarantee a delivery date and time, but if they do we want to pass that information on to the customer. We echo out the GuaranteedDaysToDelivery property along with some descriptive text. If GuaranteedDaysToDelivery is more September 2004



PHP Architect



www.phparch.com

Listing 4 * { font-family: arial, helvetica; } h2 { font-size: medium; } #cartrowheader, .cartfinalizelabel, .errormessage, #checkoutbutton { font-weight: bold; } .errormessage { color: red; } .cartname, .cartpriceperunit, .cartquantity, .cartpricewrapper, #cart a, .cartfinalizevalue, .cartfinalizelabel, #checkout label, #checkout input, #checkout select, .checkoutuserdata, #checkout h2, #cartconfirm h2, #cartconfirm label { float: left; } h2, .errormessage, .cartname, .cartfinalizelabel, #checkout, #checkout div, #checkoutsubmit, #cartconfirm div, #cartconfirm label, noscript { clear: left; } .cartqauntitytextbox { width: 30px; } #shippingzip { width: 60px; } .cartquantity { width: 100px; text-align: right; } .cartpriceperunit, .cartpricewrapper, .cartfinalizevalue { width: 140px; text-align: right; } #cartconfirm label, #checkout label width: 150px; }

{

.cartname { width: 290px; } .cartfinalizelabel { width: 530px; text-align: right; } #cartfinalizesubtotal span, #checkoutsubmit { margin-top: 10px; } #cart a { margin-left: 10px; } legend, .cartrow label { display: none; } fieldset { border: 0px solid; } #checkoutbutton { margin-left: 50px; } #checkout label { padding-right: 5px; text-align: right; } #checkout .checkoutservicecode label { width: auto; text-align: left; }

48

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

than one, on line 124 we append the letter “s” to the end of the text making the word “day” into “days,” assuring that the text will be grammatically correct (it might seem like a small and unimportant thing, but, in my opinion, it shows that you programmed your system with care). If there is a scheduled delivery time, that is echoed out as well. Next, we create an instance of the constantData class (line 131), which we will need it very shortly. If the user has chosen to adjust their data, the $_POST array would contain his previously entered data, which would be used as default values for the form. If this is the first time that he is seeing the form, the $_POST array would not contain values for anything other than shippingzip. In such a case, we do not know what the billing ZIP code will be, although for a large percentage of users it will be the same as the shipping ZIP code, so on line 132 we set the $_POST[‘billingzip’] to the shipping ZIP value saved in the session. The fields in the $_POST array will be used shortly as our default values in the form, so this would cause the shipping ZIP to be the default value of the billing ZIP input box. Now we loop through the address types, shipping and then billing, echoing a second-level header (line 134) followed by a loop that prints each of the address field text input boxes via the inputText function (line 146). When the loop reaches the state field (line 137), a state pull down menu is echoed out by calling the selectPulldown function, after which the loop will continue to the next iteration. Since the user has already set the shipping ZIP code, rather then shown it in an input box as the other fields are, the shipping ZIP is only echoed to them along with a link to change it (line 143). If they change the shipping ZIP, the cost of shipping may change, so instead of misleading the customer by altering the shipping cost after they have already submitted their billing information, the customer will be required to start the check out process over again. This minor inconvenience will prevent customers from complaining of being incorrectly charged “behind their backs.” After the loop, we display the fields for telephone, email and credit card number, as well as pull down menus for the month and year of the credit card expiration date (lines 150 through 159). The month field is populated via the months property of the constantData class. The year field is generated dynamically starting with options for the current year until 9 years from now. For added usability, we want to give the customer a checkbox that, when clicked, copies the values from the shipping fields into the billing fields. We do this on line 135 by echoing a JavaScript include of the datacopy.js file between the shipping and billing fields. Including this file will generate a checkbox that, when clicked, triggers the datacopy JavaScript function.

September 2004



PHP Architect



www.phparch.com

Billing Info is the Same as Shipping The datacopy JavaScript function in the datacopy.js file (Listing 3) copies the data that the user entered into the shipping fields to the billing fields. Before we get into explaining how this function works, let’s skip down the file to line 16, where we display the checkbox that the user can click to execute it. Since datacopy() relies on the browser having the JavaScript capability to utilize the document.getElementById function, if the user has an incapable browser, they should not see the checkbox. If they have a capable browser, the checkbox is written to the screen by calling document.write(). JavaScript is then set to monitor the checkbox for a click ‘event’ and to execute datacopy() should one occur. A click event on a checkbox occurs when the checkbox is either clicked with the mouse or toggled via the keyboard. Since the checkbox is clicked both when it is checked and unchecked, in the beginning of the datacopy function (line 2) the status of the checkbox is determined; if it is unchecked, the remainder of the function is bypassed so, essentially, nothing happens. In the event that the checkbox is checked, all of the HTML input tags on the page are loaded into the inputtags variable on line 3 and then looped through on line 7, seeking only the form inputs where the name begins with the word shipping. When one is found, we get the input tag with the exact same name, only with the word shipping substituted for the word billing, and set the value of that tag to the value of the shipping input tag. Setting the value property in JavaScript does not have any effect on drop-down menus, so we need to copy the selected index property of the shipping state selector to the selected index of the billing state selector (line 11). Show Confirmation By this stage we know what shipping option the customer wants and what state their billing address is in and, therefore, we have what we need to calculate the shipping and tax cost, which, in turn, allows us to calculate a grand total. All this can be calculated and echoed out by calling the getTotal function (line 165 of listing 2). We then proceed to echo the submitted data in a fashion very similar to how we displayed the input boxes. So similar, in fact, that it may be tempting to overlap the functionality of both checkout phases into one action, but separating them into individual steps as we have done greatly improves the code legibility. Since the state was passed along as an abbreviation, we will want to map it back to the full state name before echoing it out. This is done via the states array property of the constantData class on line 174. After the confirmation data, we start the form to dis-

49

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

play two buttons to the user, one to confirm the order and checkout and the other to adjust the information. We also create a hidden form input for each entry in the $_POST array to pass the data along to the next page (line 189). If the customer chooses to adjust their information, they will be taken back to the showform phase of checkout and the POST data will be used to populate the form. Checkout Confirmed By the time the user gets to the check out confirmed stage of the check out process, their data has been verified and their credit card has been successfully charged. The getTotal function is echoed one last time to confirm what the customer had just purchased. Since we have previously counteracted any magic_quotes_gpc functionality that may have been enabled, we need to pass the data through the mysql_escape_string function before entering it into the database. We do so on line 196 by ‘array walking’ each value of the $_POST array through the mysql_escape_string_by_reference function. Next, we query the orders table for the next available order ID and save it to a local orderid variable (lines 198 through 202). We also save the approval code and transaction ID to local variables named approvalcode and transactionid. We then create the orderspecifics array and populate with the names of local variables that contain order specifics (line 207). For each variable in the orderspecifics array, there is also a column in the orders table that shares the same name. This next bit may be a bit confusing; on line 209 we loop through the orderspecifics array populating a new array called orderfields with the name of the orderspecifics fields being used as the array key. The values of the orderfields elements, on the other hand, are obtained by retrieving the value of global variables whose names correspond to the values stored in the orderspecifics array. We then continue to load the orderfields array with the user data from the $_POST array where the key matches the names of fields used previously to display the form inputs. Once the orderfields array is loaded, we use it to create a SQL INSERT statement on line 216 by imploding the array keys to fill the column names and the array values to fill the data. We then create a partial SQL insert statement for each item in the cart on line 219 and save it to the orderproducts array. The array is then used on the next line to construct a single SQL statement that inserts all the products of the order into the orderproducts database table. After the SQL statement is executed, the cart session variable is unset on line 223 and then a thank you message is echoed to the customer. By unsetting the session variable, if the page gets reloaded the cart would be empty and instead of double charging the September 2004



PHP Architect



www.phparch.com

credit card, the user will be sent to the showcart.php page and shown an error message stating that the cart is empty. CSS At this point, the ultimate shopping cart is fully functional and chock full of features but it is not exactly winning any beauty contests. When we created an instance of htmlOutput in the showcart.php file, we set the css property to include the cart.css file. We begin the cart.css file (Listing 4) with some basic document formatting, then follow that with floating most of the elements in the shopping cart and marking the others as set to clear. When two sequential objects are floating, they will appear on the same line if space permits, while when an object is set to clear, it will always break to the next line. When you set an object to float, you can set a fixed height or width that the object consumes and no other floating object will enter into that space. As you will see soon, using a combination of span and div containers, it is possible to position elements with CSS so that they appear as if they are in a table. Back in our CSS file, we proceed to set fixed widths for each type of data in the cart contents displayer (quantity, price per unit, etc). and align all the currency values to the right margin of their allotted space. Next, we position the cart finalization fields, such as the tax and total, using some basic arithmetic. We want the text labels to appear as though they are in a column under the quantity field and their dollar values in the next column over, directly under the price column. We do this by adding the width of the columns for item name (290px), price per unit (140px) and quantity (100px), and set the sum, 530px, as the width of the cart finalize label and align the text to the right margin. The dollar value for the cart finalizing rows then needs to be the same width as the column for the product row prices. Since we want a little visual separation between the end of the order breakdown and the cart finalization numbers, so we add a 10px margin above the subtotal. We also need a little margin between the remove from cart link and the prices, as well as between the adjust shipping zip link in the checkout form and the display of the shipping zip, so we set a 10px left margin to all the links in the cart. The labels before each element in the cart contents displayer as well as the fieldsets along with their legends throughout the cart are primarily for the benefit of browsers that do not support or disable style sheets and for users of assistive technologies that may not be able to see a tabular view of the cart. In all other cases, we can hide the row labels, the fieldset borders and legend texts. All the input text boxes have label text to the left of

50

FEATURE

The Ultimate PHP 5 Shopping Cart: Part II

them. We have previously set the width of the labels to 150px and set them to float along with the input box next to them. We now align the text to the right with 5px of padding. This causes all the text boxes to appear in a uniform vertical stack down the page 155 pixels from the left edge (150px label width + 5px padding) with the label text neatly flushed against them. While all the text fields look good now, the radio buttons that are used to display the shipping options have become a mess, since their input and label are in opposite positions; the input is to the left of the label. In this case, we override the previous setting and set the alignment to the left and the width to auto, letting the browser determine the maximum available space to give to each label. Limitations Like everything in life, there are limitations. The maximum weight that UPS will give shipping rates for in the US is 150 pounds. If you have packages heavier than that, you will need to research other shipping methods. If you have more than one shipping distribution center, you will also need to add some logic in the code to send UPS the correct origin zip code and account for the possibility that a single order may be shipped in multiple packages originating from different locations. If you plan to sell to customers outside the US, there are a few areas that need adjustment. First, you need to make sure that your merchant account accepts international credit cards and find out what the rates are— you’ll find that they are often much higher then the domestic rates. Next, you will need to adjust the database columns, as well as the checkout system, to add a country column, extend the length of the telephone and ZIP code columns, change the ZIP code columns to varchar types and make the state field optional. You will also need to adjust the UPS script as well—that is, if UPS even services the country that you intend on selling to. Additional Features You have now completed the creation of the ultimate shipping cart. If you would like to really spice up your cart even further, there a few more steps you can take. The CVV2 numbers on a credit card are those three digits that are printed, rather then embossed, on the back of the credit card (in the case of American Express, it is four digits on the front) and are not left on the imprinted credit card receipt when a purchase is made, thus making the card number less susceptible to theft. You could beef up credit card security by requesting and validating the CVV2 field, as well as using an address verification system (AVS) to verify the billing address. To prevent a malicious user from packet hijacking, AuthorizeNet creates a string combination of a password that is set in the AuthorizeNet merchant control panel with data that is specific to the order. The string is encrypted with MD5 sent back in position 38 of the September 2004



PHP Architect



www.phparch.com

authorization response. By generating your own MD5 string based on the same criteria and verifying that it is identical to the one in the response, you can be fairly certain that the response data was actually sent from the AuthorizeNet server and not be a malicious impersonator. For additional security, you can authenticate that the email address actually points to a real email account by doing an MX lookup, although the performance hit of doing this may negate its benefit. In addition, to save on transaction fees, you could use the initial digits on the credit card to determine the card type (Visa, MasterCard, etc.) and be sure that your merchant account handles that type before processing the transaction. Since credit cards span in length from 13 to 16 digits you could also verify the number of digits matches the card type, for instance, an American Express card must have 15 digits, while a VISA can have either 13 or 16, and so on. For added user-friendliness, or by this point we might as well call it pampering, by adding a zip code database you could map the zip code entered by the user to the city and state, saving a couple of keystrokes from the user during check out. To avoid the same user trying an invalid card multiple times at your expense, you could also keep a database of invalid cards and prevent repeat transactions from a failed card within a set time span. A simple convenience that should be added to every cart is to make the item name of each item in the cart contents displayer a link to the details of that item. Since the customer’s card is being charged for the purchase immediately, you can be fairly certain that once an order is placed the product has been sold. This means that adding a live product inventory to the cart would be easy. In closing If you have any additional ideas how to further improve the ultimate shopping cart then send me an email; I would love to hear from you!

About the Author

?>

Eric David Wiener applies his web solutions expertise at IPRO where he is the Webmaster. IPRO is New York State's healthcare quality improvement organization and the leading QIO in the US. Eric's consulting experience focuses on business management and technology, providing clients with interactive solutions in the areas of e-commerce, e-learning, and accessibility. You may see his work at www.dynamicink.com and contact him at [email protected]

To Discuss this article: http://forums.phparch.com/174

51

Cheap Manpower Calling External Programs from PHP Scripts

F E A T U R E

by Michal Wojciechowski

The ability to launch external programs from PHP scripts enables the developer to extend the functionality of an application beyond the capabilities of PHP functions and extensions—all this with little effort and in a short time.

O

ne of the biggest advantages of PHP is its flexibility, achieved through the wide range of builtin functions, extensions, and PEAR packages available to the developer. The functionality provided by these mechanisms makes PHP suitable for many different needs. Most common tasks can be accomplished with basic functions and extensions, while other applications may require additional packages. However, from time to time all developers find themselves faced with a problem for which existing and readily available solutions are not sufficient. In some cases, necessity becomes the mother of invention and the developer comes up with a new solution (which he will hopefully share with the community). Sometimes, however, there’s no time (or money) for invention—for example, when a project you’re working on is due the next day and a major feature is still missing. You need a working solution, and you need it immediately. At least in some cases like this, existing software may come to the rescue. Many tasks that are still hard to accomplish directly from within PHP are easily taken care of by applications that have been around for years. Piggybacking on these programs is usually much simpler than attempting to implement the same functionality in PHP (which, in many cases, may seem like reinventing the wheel). PHP provides several core functions for executing programs on the server’s system. The developer’s task is reduced to using these functions to create a kind of interface to call the desired application and collect the results—pretty much what Unix applications have been

September 2004



PHP Architect



www.phparch.com

doing for years. This can be usually achieved with a few lines of code, while a solution implemented directly in PHP may require hundreds. Program Execution Functions Program execution functions are part of the PHP core, so no extensions are required to use them (which is great if you’re running on a shared host). Be aware, however, that safe mode severely limits which programs you can execute, as we’ll see later on. The functions can be divided into two groups. The first contains functions that execute a program and return (or display) its output and exit code: • exec() - executes a command and returns the last line of its output • passthru() - displays the raw output of the command • shell_exec() - returns the output as a string (equivalent to the backtick operator) • system() - displays the output, does a flush after each line. These functions can be described as “fire and forget”—you execute a program and wait until it finishes

REQUIREMENTS PHP: 4.3.x OS: Unix, Linux Other: txt2html, wvHtml, unrtf Code Directory: manpower

52

FEATURE

Cheap Manpower

its operation, not worrying much about what the program does in the meantime. There’s no interaction between your PHP code and the running program while it is being executed. When the function returns, the program has already terminated. The following example illustrates a typical usage of one of these functions: system(“uname”, $ret); if ($ret != 0) { print “uname failed\n”; }

The system() function executes the uname system command, which, in a Unix-like environment, displays the server’s operating system type (e.g.:. Linux or FreeBSD). The exit code is stored in the $ret variable— a value other than zero here indicates an error. There is also a group of program execution functions that operate in a slightly different way: they make it possible for a script to “spawn” a process and communicate with it: • popen() - starts a program and opens its input or output as a pipe • pclose() - closes a pipe opened with popen() • proc_open() - starts a program and opens file pointers for its input and output • proc_close() - closes a process started with proc_open() and returns its exit code • proc_terminate() - kills a process started with proc_open() (introduced in PHP 5) The popen() and proc_open() functions start a new process that runs in parallel with the PHP code. You can interact with the running program by writing to its

Listing 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

array(“pipe”, “r”), 1 => array(“pipe”, “w”), 2 => array(“pipe”, “w”)); // launch gnuplot $proc = proc_open(“/usr/bin/gnuplot”, $descs, $pipes); if (is_resource($proc)) { // commands to plot the image on standard output fwrite($pipes[0], “set terminal png\n”); fwrite($pipes[0], “plot sin(x)\n”); fwrite($pipes[0], “show output\n”); fclose($pipes[0]); header(“Content-Type: image/png”); // read image data and send it to client while (!feof($pipes[1])) { echo fread($pipes[1], 1024); } // close gnuplot process proc_close($proc); } ?>

September 2004



PHP Architect



www.phparch.com

input and reading from its output. The ability to communicate with a running program that these functions provide makes them especially useful for working with interactive applications that expect user input. Listing 1 shows an example of PHP code that launches gnuplot, a function plotting program, and uses it to create a graph of the sine function in PNG format. In the example, we need bi-directional communication with gnuplot—we’ll be writing commands to its input and reading results from its output—so we use the proc_open() function (ppopen() only allows for either reading or writing to the process, but not both). The function sets up the $pipes array for input and output. Commands can then be sent to $pipes[0] (which points to gnuplot’s standard input) with the fwrite() function. In turn, gnuplot writes back the image data to its standard output, which we read using fread() and pass to the client’s browser with the appropriate HTTP header (CContent-Type: image/png). This example is slightly inadequate with respect to the unique functionality provided by external programs, since it’s not particularly hard to create a graph in PHP and using gnuplot for this task doesn’t give any bonus—the point was only to show interaction between PHP and a running program. We will now present a more interesting and practical example of using external programs to make a hard task easy. Story of an On-line Story Archive Imagine the following scenario: our mission is to build an on-line story archive, where amateur writers could publish their stories, making them available for the visitors’ reading pleasure. The stories will be uploaded using a standard HTML form-based file upload mechanism. To make the archive more user-friendly, we have decided to support a number of file formats for the submitted files, that is, TXT, DOC and RTF, so that the author isn’t required to convert his story to a specific format before sending it in. On the other hand, we would also like to make the archive accessible for any visitor, regardless of whether he or she can open a document saved in a specific format or not. Therefore, we need to convert each file to a format readable by every browser, which is,of course, HTML. While converting a text file to HTML is a trivial task (simply placing the file contents inside ... would do), converting DOCs and RTFs isn’t. So far, there are no extensions or packages that are capable of performing such conversion. Doing it from scratch would be a complex and time-consuming task. DOC, RTF, and several other file formats, widely used by Microsoft Windows users, have been a thorn in the side of Unix user community for years. Until recently, few or no applications were able to handle these for-

53

FEATURE

Cheap Manpower

mats in a usable way. However, as a half solution, programmers have created many conversion programs capable of turning DOC/RTF documents into TXT, POD, HTML, TeX and other “Unix-friendly” formats. Examples of such utilities include antiword, catdoc, word2x, unrtf just to name a few. Thanks to the existence of program execution functions and conversion utilities, our task can be greatly simplified to just launching the appropriate conversion program. First, we need to choose the software to use. As we have already said, we want to support the TXT, DOC and RTF formats (we could extend this list to other document types, but, for the sake of simplicity, we’ll stick with these three). Each conversion program must be capable of producing HTML output, as that’s how we want the stories to be kept in the archive. A quick search, followed by several tests, enabled me to come up with the following selections: • txt2html - the name says it all—it is a textto-HTML converter • wvHtml - one of the scripts of the wv utilities suite, it converts DOCs to HTML • unrtf - an RTF file converter, capable of producing HTML output. To keep our work simple, we will not implement any user account management—no registration or login will be required to submit a story. We’ll keep the stories in a basic MySQL database; the database creation script is shown in Listing 2. We’ll use a single table, which will hold information about the story’s author, its title, and the HTML file containing the story itself. What we need now is a form that enables the user to upload his story. We’ll provide a very simple form with two text input boxes and a file selection element, laid out nicely in a table as shown in Listing 3. Two important parts of the form are the enctype attribute and the MAX_FILE_SIZE hidden input element. The former determines the mechanism used to encode the form’s contents—we use multipart/form-data, as that is the appropriate enctype for file upload. The latter sets the maximum size of uploaded file that will be accepted by the PHP script. We set it to 128 kilobytes Listing 2 CREATE DATABASE stories; USE stories; GRANT ALL ON stories.* TO 'stories'@'localhost' IDENTIFIED BY 'rocket43'; CREATE TABLE stories ( author TEXT NOT NULL, title TEXT NOT NULL, file TEXT NOT NULL );

September 2004



PHP Architect



www.phparch.com

(1131072 bytes). The finished form is shown in Figure 1. If the uploaded file is larger than the maximum size we specified, an error occurs. Therefore, the first thing we do before processing the file is check for errors: if ($_FILES[‘file’][‘error’] == 1 || $_FILES[‘file’][‘error’] == 2) { $error = “File too big”; }

An error value of 1 means that the file size exceeds the upload_max_filesize directive specified in php.ini, while a value of 2 indicates that the size exceeds our MAX_FILE_SIZE set in the form. The $error variable will be used in our script to track and report errors. We will now build the target HTML filename, based on the author and title values provided by the user. To avoid spaces in the filename, we replace them with underscores. The resulting filename will have the form author_title.html; for example, if the author introduced himself as John Foo, and his story was titled “Story of the Quick Brown Fox,” then the file will be named John_Foo_Story_of_the_Quick_Brown_Fox.html. Since the filename is based on user input and will be used in a shell command, we need to make sure it will be safe. For that purpose, we use escapeshellarg() to escape any “dangerous” character away: $author = str_replace(‘ ‘, ‘_’, $_POST[‘author’]); $title = str_replace(‘ ‘, ‘_’, $_POST[‘title’]); $target = escapeshellarg($author.’_’.$title.’.html’);

We now need to determine the format of the uploaded file. There are several methods for doing this, such as using a “magic” file recognition utility (for example file) or checking the MIME type. The former is most accurate, but requires some additional work, while the latter only requires that the appropriate Listing 3 1
” method=”post”> 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 22 23
Author:
Title:
File:
20 21
24


54

FEATURE

Cheap Manpower

extension be installed. For the sake of simplicity (again), we’ll do it the easy way, just checking the extension of the file. We retrieve it with a regular expression: preg_match(“/\.([^.]+)$/”, $_FILES[‘file’][‘name’], $matches); $ext = strtolower($matches[1]);

The variable $ext now contains the file extension (converted to lowercase, so that .txt and .TXT are treated the same way) that will be used to determine the appropriate conversion program. We’ll create an array that maps each supported file extension to the command that converts that specific format to HTML. The $file variable holds the temporary name of the uploaded file to be used as the command line argument. $file = $_FILES[‘file’][‘tmp_name’]; $commands ‘txt’ => ‘doc’ => ‘rtf’ =>

= array( “/usr/bin/txt2html $file > $target”, “/usr/bin/wvHtml $file $target”, “/usr/bin/unrtf $file > $target”);

The command that we’ll execute would be the value corresponding to the file extension, ie. $commands[$ext]. Now, we need to choose a suitable program execution function. The program output is supposed to be saved in a file, so there’s no need to display it—this limits our choice to exec() and shell_exec(). Actually, we need no output at all, but we have to retrieve the command’s exit code in order to know if it succeeded. We’ll use exec(), which, called with three arguments, returns both the program output (which we will silently ignore) and the exit code: $dir = getcwd(); chdir(‘submitted’); exec($commands[$ext], $out, $ret); chdir($dir);

We will place the uploaded stories in a separate directory, named submitted. Before performing the conversion, we chdir() into that directory. This could be done in a simpler way, just by adding submitted/ as a prefix to $target, but, for some reason, the version of wvHtml that I use refuses to create the output file in a directory other than its current working one. Hence, this workaround is necessary. When the program execution is finished, we have to make sure everything went fine. if ($ret != 0) { $error = “File conversion failed”; }

September 2004



PHP Architect



www.phparch.com

A return value of zero indicates that the conversion was successful. We can finish the whole operation by registering the newly uploaded story in the database: mysql_query(“INSERT INTO stories VALUES(‘“ . mysql_escape_string($_POST[‘author’]) . “‘, ‘“ . mysql_escape_string($_POST[‘title’]) . “‘, ‘submitted/” . mysql_escape_string(substr($target, 1, -1)) . “‘)”);

The target filename ($$target) was “quoted” when we used escapeshellarg() and we need to unquote it before inserting into an SQL query—we use the substr() function to strip the leading and trailing quote characters. When the query is executed, the story is added to the database and our job is done. The complete code is shown in Listing 4. Creating a basic interface between the script and the executed program is simple and can be done in a couple of minutes. However, there are several things that you should be aware of when calling external programs.

“For a higher level of security, you might want to enable PHP safe mode on your web server.” Execution Environment External programs inherit their execution environment from the script that called them, which, in turn, usually inherits the web server’s environment. This means that the program is called with the privileges of the web server (ie. the www or nobody user) and has the same set of shell environment variables. Be careful when relying on environment variables, though. The HTTP server may have a different set of variables than your shell. You might be surprised that the same program works fine when executed from the command line, but mysteriously fails when called with exec(). This could happen if the PATH variable was not set to the appropriate directory where the program executable is stored. In most of our examples we were using absolute paths to the executed programs—this way we didn’t have to worry about the PATH setting. Another possible solution consists of setting PATH to the appropriate value before calling the external program (this makes it easier to port the code to another system that may

55

FEATURE

Cheap Manpower

Listing 4 1 “/usr/bin/txt2html $file > $target”, 31 ‘doc’ => “/usr/bin/wvHtml $file $target”, 32 ‘rtf’ => “/usr/bin/unrtf $file > $target”); 33 34 // check if the extension is recognized 35 if (!array_key_exists($ext, $commands)) { 36 $error = “Unsupported file format”; 37 } 38 else { 39 // go to the target directory 40 $dir = getcwd(); 41 chdir(‘submitted’); 42 43 exec($commands[$ext], $out, $ret); 44 45 // go back 46 chdir($dir); 47 48 // return value other than zero indicates an error 49 if ($ret != 0) { 50 $error = “File conversion failed”; 51 } 52 } 53 54 // if no error occurred, put the story in the database 55 if (!isset($error)) { 56 mysql_query(“INSERT INTO stories VALUES(‘“ . 57 mysql_escape_string($_POST[‘author’]) . 58 “‘, ‘“ . mysql_escape_string($_POST[‘title’]) . 59 “‘, ‘submitted/” . 60 mysql_escape_string(substr($target, 1, -1)) . “‘)”); 61 } 62 } 63 } 64 65 // retrieve the list of stories 66 $results = mysql_query(“SELECT * FROM stories”); 67 68 mysql_close($link); 69 70 ?> 71 73 74 75 Online Stories Archive 76 77 78 79
80 81
82

Online Stories Archive

83
84 85
86 There are stories in our archive. 87 88 89 90 91

September 2004



PHP Architect



www.phparch.com

keep its binaries in another directory, e.g.: in /usr/local/bin instead of /usr/bin). The HOME and LC_X variables could also cause confusion when executing external programs. HOME is set to the user’s home directory, which is sometimes used by applications to store temporary files. The web server’s HOME might be set to a directory that the program has no write access to, which could cause it to fail. The LC_X variables determine the locale settings (i.e.: character handling, date/time format, etc.) and are used by many programs with locale support. For example, if you call a program and it displays the date in some strange format, it’s probably because of an incorrect LC_TIME setting. When in doubt, you can check the value of an environment variable from the script’s perspective with the getenv() function—you can also list all environment Listing 4: Continued... 92 93 94 95 96 97 98 99
AuthorTitle
”>
100
101
102 103
104 105 106
107 ERROR: 108
109
110 111 112
113 Submit your story:
114 <small>(TXT, DOC, and RTF format is accepted. 115 File size limit is 128K.) 116 117
” method=”post”> 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 138 139
Author:
Title:
File:
136 137
140
141
142 143
144 145 146

56

FEATURE

Cheap Manpower

variables with the good old phpinfo(). If you need to change the value of a variable, use the putenv() function. The variable will be restored to its original state when the script exits, so you don’t need to reset it explicitly. Security Considerations Security is a major issue when dealing with program execution functions. The functions operate directly on the server’s filesystem and can be used to perform actions that the web server is normally not allowed to. Improper usage of these functions may easily expose the web server to various kinds of attacks. Be extremely careful, in particular, when executing commands that take user-supplied values as arguments. A malicious user could supply a specially crafted argument that confuses the executed application or tricks the function into executing another program. Take a look at the following example:

This code uses the cpp2html utility to display the specified C source file ($$file) as HTML. The author assumes that all source files are stored in the sources directory. The files can be accessed simply using an URL like source.php?file=helloworld.c . Besides being very useful, however, the script is also very dangerous. It doesn’t take a genius to discover that this script can be used to read virtually any file on the server’s filesystem—actually, any file that the HTTP server has read access to. A malicious user could type an URL like source.php?file=../../../../etc/passwd and see the contents of the /etc/passwd file. The script assumes that the accessed file is located in the sources directory, but does nothing to ensure that it really is. Scary? Wait, there’s more to come. The Unix shell has the ability to execute a sequence of commands separated by semicolons. Since program execution functions make use of the shell, this method works for them as well. An attacker could supply an URL of the form source.php?file=;/sbin/ifconfig, which expands to -d < the command line /usr/bin/cpp2html sources/;/sbin/ifconfig. The shell interprets it as two subsequent commands, and first executes cpp2html, then ifconfig. The attacker gets the results of both commands—in our case, he manages to view the configuration of system network interfaces returned by ifconfig. The ability to execute arbitrary commands is the first step for an intruder to successfully compromise your system. For a skilled attacker, gaining more privileges September 2004



PHP Architect



www.phparch.com

could be just a matter of time. The problem here is caused by the fact that certain symbols, like .. or ;, have special meaning to the shell and should be considered dangerous when executing commands from PHP code. Allowing them in parameters provided by users is asking for trouble. A rule of thumb is to avoid relying on user-supplied values used as command line arguments or path elements. If you really have to, be double sure that there are no dangerous characters in the command line or that they have been properly escaped—the escapeshellcmd() and escapeshellarg() functions will help you with that.

“External programs inherit their execution environment from the script that called them...” Safe Mode For a higher level of security, you might want to enable PHP safe mode on your web server. Safe mode places several restrictions on program execution functions. First of all, when it is enabled, the functions can only execute programs located in the directory specified in the safe_mode_exec_dir configuration directive. Also, the shell_exec() function is disabled (as well as its equivalent backtick operator). Other commands are restricted to have only one argument passed to the executed program. If more than one word follows the program name in the command line, they are treated as one quoted argument, eg. cat foo; cat bar becomes cat ‘foo; cat bar’. There are also restrictions with respect to environment variables. The script is allowed to set only those variables whose names begin with prefixes specified in the safe_mode_allowed_env_vars configuration directive. In addition, safe_mode_protected_env_vars contains a list of variables that cannot be changed at all. Chroot Environment If you are even more concerned about the security of your web server when calling external programs, you might consider running it in a chroot environment. Chroot creates a separate directory structure for an application, preventing it from accessing the root filesystem. Even if an attacker succeeded in compromising the web server, the impact of his actions would be minimized to the chroot environment.

57

FEATURE

PHP 5: Beyond the Objects

Setting up a chroot environment requires some effort from the system administrator. The environment needs to be properly configured to run the web server software as well as the additional programs. The administrator must ensure that the programs can be successfully launched, are able to access the required libraries, can create temporary files (if necessary), and so forth. External Programs Security As a final note on security, be aware that in addition to the security of the web server and the PHP code, you need to take care of the external programs security as well. A vulnerability in an application executed from PHP code might be even more dangerous to the server’s security than an insecure script. This puts a higher level of responsibility on both the developer and the server administrator. Always make sure that you are running the latest (secure) versions of the programs called from your PHP scripts. Summary Piggybacking on existing programs is a very effective method of implementing functionality that is hard to accomplish the “traditional” way by using standard PHP functions and extensions. It makes developing a

working solution in short time and with little effort possible. One of the greatest advantages of using existing software is its maturity—programs that have been around for years are less likely to cause any trouble than freshly developed solutions. This can save hours (or even days) of testing to ensure that some component of your application actually works the way you want. However, it’s important to keep in mind that this technique should be used only when it is really necessary. Do not use an external program when there is a function or extension that can do the job as well. Using external applications usually reduces the portability of your code (especially if you use system-specific programs) and requires more attention to system security.

About the Author

?>

Michal Wojciechowski is a student at the Warsaw University of Technology in Poland and a freelance PHP developer. You can contact him at [email protected].

To Discuss this article: http://forums.phparch.com/172

FavorHosting.com offers reliable and cost effective web hosting... SETUP FEES WAIVED AND FIRST 30 DAYS FREE! So if you're worried about an unreliable hosting provider who won't be around in another month, or available to answer your PHP specific support questions. Contact us and we'll switch your information and servers to one of our reliable hosting facilities and you'll enjoy no installation fees plus your first month of service is free!* - Strong support team - Focused on developer needs - Full Managed Backup Services Included Our support team consists of knowledgable and experienced professionals who understand the requirements of installing and supporting PHP based applications. Please visit http://www.favorhosting.com/phpa/ call 1-866-4FAVOR1 now for information.

September 2004



PHP Architect



www.phparch.com

58

SECURITY CORNER

S E C U R I T Y

C O R N E R

Security Corner

Secure Design by Chris Shiflett Welcome to another edition of Security Corner. This month’s topic is secure design, the application architecture that provides the foundation for secure development. May’s column on data filtering touched on this topic a bit, and it’s something that is sure to appear in this column again. Design has always been a controversial topic, but only because developers tend to be very loyal to their own discoveries, ideas, and approaches. Thus, discussing software design can spawn debates rivaled only by coding standards discussions, text editor opinions, and programming language choices. With this in mind, please feel free to contact me to suggest different approaches than the ones written here. Like any other developer, I’m always interested in learning new techniques, and I’d be happy to do a few case studies of any particularly sound designs. In order to demonstrate some common approaches, I describe two different overall methods of organizing your applications: the dispatch method and the include method.

Dispatch Method A good software design should help developers ensure that data filtering cannot be bypassed, guarantee that tainted data cannot be mistaken for clean data and identify the origin of data. Without these characteristics, a developer’s task is more difficult and prone to errors. Just as complexity in an application leads to more bugs, the lack of a sound design leads to more security vulnerabilities. One popular design that embodies each of these characteristics is the dispatch method. The approach is to have a single PHP script available directly from the Web (via URL). Everything else is a module included with include or require as needed. This method usually requires that a GET variable be passed along with every URL, identifying the task. This GET variable can be considered the replacement for the script name that would be used in a more simplistic design. September 2004



PHP Architect



www.phparch.com

For example: http://example.org/dispatch.php?task=print_form

The file dispatch.php is the only file within document root. This allows a developer to do two important things: 1. Implement some global security measures at the top of dispatch.php and be assured that these measures cannot be bypassed, and 2. Easily see that data filtering takes place when necessary by focusing on the control flow of a specific task. I have developed applications using this approach with great success. As a developer, I especially appreci-

59

SECURITY CORNER ate its simplicity. Over-architected applications tend to solve problems that don’t exist, and the added complexity is rarely worth it. To further illustrate this approach, consider the example dispatch.php script given in Listing 1. Keeping in mind that dispatch.php is the only resource available from the Web, it should be clear that the design of this application ensures that any global security measures taken at the top cannot be bypassed. It also lets a developer easily see the control flow for a specific task. For example, instead of glancing through a lot of code, it is easy to see that end.inc is only displayed to a user when $form_valid is True, and because it is initialized as False just before process.inc is included, it is clear that the logic within process.inc must set it to Ttrue, otherwise the form is displayed again (presumably with appropriate error messages). It is also impossible to access end.inc otherwise, because it is not available from the Web (it is not within the docuListing 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29



Listing 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21



September 2004



PHP Architect



www.phparch.com

Secure Design

ment root). In order to keep dispatch.php as simple as possible, I recommend only adding logic that is important to the control flow of the application and putting everything else in modules. It can also help your module organization to classify the latter in three categories: display, logic, and database queries. These two things make dispatch.php very easy to follow. Note: If you use a directory index file index.php such as (instead of dispatch.php), you can use URLs such as http://example.org/?task=print_form . You can also use the Apache ForceType directive or mod_rewrite to accommodate URLs such as http://example.org/app-name/print-form .

“If you’re generating random passwords, then it’s still better to use a loop that selects random uppercase and lowercase letters...” Include Method An almost opposite approach is to have a single module that is included at the top of every public script (those within the document root). This module is responsible for all global security measures, such as ensuring that data filtering cannot be bypassed. Listing 2 gives a simplistic example of such a script, security.inc. This example demonstrates the handling of form submissions, although this is only one of the types of tasks that can be performed here. The process.inc script is where the data filtering takes place, and security.inc makes sure that it always executes when a form meets the minimum criteria for testing, (which is that it only contains expected data). This is done by adding a hidden form variable to every form that identifies it (or using any approach that can be used to distinguish forms) and then comparing form fields with what is expected. Listing 3 shows an example of a form that identifies itself as login and adheres to the checks from the exam-

60

SECURITY CORNER ple security.inc script shown in Listing 2. Note: Use the auto_prepend_file directive to ensure that security.inc is not accidentally left out.

Naming Conventions A topic worth revisiting here is that of naming conventions. However you decide to name your variables, make sure that you choose a method that will not make it easy to mistakenly use tainted data. One approach is to rename any variable that passes through data filtering to something that distinguishes it as being clean. For example, Listing 4 demonstrates testing the validity of an email address. The $clean[‘email’] variable will either not exist, or it will contain a valid email address. With this approach, you can safely use any variable within the $clean array in your programming logic, and the worst-case scenario is that you reference an undefined variable. You’ll catch these types of errors with your error reporting (a future topic for Security Corner), and the impact is much less severe anyway.

Note: If you place your data filtering in a separate module (such as process.inc as mentioned in Listing 2), it is important to initialize your $clean array in the parent script and to be sure that no path through your logic bypasses this initialization.

Listing 3 1 2 3 4 5 6 7 8

Username:

Password:



Secure Design

Until Next Time... Of the two general approaches I discussed, dispatch method is my favourite. The main reason for my preference is that it leverages existing mechanisms that have been proven reliable, such as the fact that only files within document root can be accessed from the Web. Another benefit is that it relies less on the developer remembering something (although the auto_prepend_file directive can assure us that a file such as security.inc is always prepended). Again, if you have a particularly secure design that you wouldn’t mind sharing with your fellow readers, please let me know. I’ll be happy to have a look and provide a thorough analysis in the hopes that everyone benefits. You should now have the tools you need to add security precautions to your next application design. Just be sure that your approach satisfies the three important characteristics I mentioned at the beginning: help developers to ensure that data filtering cannot be bypassed, ensure that tainted data cannot be mistaken for clean data and identify the origin of data. These things help as security-conscious developer focus on the important issues. Until next month, be safe.

About the Author

?>

Chris Shiflett is a frequent contributor to the PHP community and one of the leading security experts in the field. His solutions to security problems are often used as points of reference, and these solutions are showcased in his talks at conferences such as ApacheCon and the O’Reilly Open Source Convention, and in his articles in publications such as PHP Magazine and php|architect. “Security Corner,” his monthly column for php|architect, is the industry’s first and foremost PHP security column. Chris is the author of the HTTP Developer’s Handbook (Sams), a coauthor of the Zend PHP Certification Study Guide (Sams), and is currently writing PHP Security (O’Reilly). As a member of the Zend PHP Education Advisory Board, he is one of the authors of the Zend PHP Certification. He is also leading an effort to create a PHP community site at PHPCommunity.org. You can contact him at [email protected] or visit his Web site at http://shiflett.org/.

Listing 4 1 2 3 4 5 6 7 8 9 10 11 12 13



September 2004



PHP Architect



www.phparch.com

61

T I P S

&

T R I C K S

Tips & Tricks By John W. Holmes

RANDOM LINES I recently came across the need to grab random lines from a file for a project I was working on. Okay, I thought, I’ve done this before. A simple call to file(), array_rand() and a small loop will get me the lines I need, right? I soon realized that I couldn’t have been more wrong. So, to start off, let’s say that for small files, the file/array_rand method isn’t bad. If you’ve got a file of 10 lines, then it’s twice as fast as the second method I’ll soon get to. Using file() fails to scale very well, though, as you can imagine, when the files start getting larger. This is because you’re reading the entire file into memory when you may only want to grab a couple lines from it. Not only is this slower than other methods, but it’s now eating up your memory by the handful. To put this into perspective, the file I was looking to grab lines from is a word list consisting of around 113,000 words, each on their own line. You can see that this isn’t something that you want to be loading up into memory with file(). This is especially the case if the script is going on any server that’ll actually see some traffic, versus your test rig at home. The other limitation that you’ll eventually run into, besides memory, is that array_rand() has an upper limit of 32,767. This is also the upper limit of rand(), as shown by the getrandmax(). So, out of my 113,000 lines, two-thirds of the times the function would just return 32,767, which doesn’t make for a very random line selector. Of course, I could use mt_rand(), which

has an upper limit of 2147483647, but then there would still be the memory issues to deal with. Obviously, these functions weren’t meant for files or arrays of this size, so I went on to search for a faster algorithm to get me my random lines. I knew I’d be opening the file with fopen() and using fseek() to move around looking for lines, but wasn’t sure on the exact sequence. Luckily I stumbled across an algorithm on a web site (it was actually an ASP site, but don’t tell anyone), that gave me what I needed. The code in Listing 1 shows how you can open the file and search for and return a certain number of random lines. This code was developed to return a two word code that could be used as a secret invitation code to participate in a survey, but can be easily adapted for any use. The function is actually quite simple in how it works (and, as you know, if it’s simple and it works, it must be brilliant). It opens the file and determines its size. It then picks a random number between zero and the file size and uses ffseek() to get to that position. Then, it begins looping and seeking backwards until it hits a newline character or the beginning of the file. Once it reaches either one, it uses fgets() to grab the current line and return that as a random word. Depending upon how many lines (or words) you need from the file, the process repeats and joins everything together before it’s returned. One thing to note is how I said this is to generate a “code”—and didn’t say “password”. Even though there are 113,000 words to choose from in my case,

“Using file() fails to scale very well, though, as you can imagine, when the files start getting larger.”

September 2004



PHP Architect



www.phparch.com

62

TIPS & TRICKS this method would still be a bad password generator. It would be trivial for a malicious user to loop through all of the word combinations looking for matches. If you’re generating random passwords, then it’s still better to use a loop that selects random uppercase and lowercase letters, numbers, and special characters of a good length. WORD LIST SOURCE Anyone looking to create a similar code generator based on a list of words can get an incredible word list from Project Gutenberg at www.gutenberg.net/etext/3201. The Moby Word Lists file available for download contains public domain files that have a variety of words separated into different categories. There are over 354,000 single words, 256,000 compound words, 113,000 words legal for crossword puzzles (such as Scrabble™, the page says, so you can finally code that official Scrabble word checker!), 21,000 names, 10,000 place names in the U.S., the complete works of Shakespeare and the entire U.S. Constitution, plus a few more files. These files may not help speed up the payroll system you’re designing, but if you ever come across the need for a large list of words and names to select from, you’ll be thankful that these are in the public domain.

make this a short tip. We all have our favorite editors, no doubt, and if it does what you want, then continue to use it. The editor I want to mention is something that fills a small requirement that I had recently. I’m often switching between computers at work that I don’t have any kind of administrator access to, so I can’t just install my favorite editor on each computer I sit down to. So, I went out in search of a small editor that I could carry on my USB thumb drive and that didn’t require any installation. I figured I’d have to settle for something with all of the features of Notepad in order to meet my requirements, but, luckily, I was pointed to an editor named Scite. You can check it out yourself at http://www.scintilla.org/SciTE.html, but I’m going to tell you a little about it as well. First of all, it met my requirements exactly. The editor is offered in two versions, one version a “full” download of 600K that comes with everything in separate files and a second version that has everything available in a single 360K executable. It doesn’t require any installation, so it’s simply double-click and go. What about the features, you ask? It blows notepad out of the water and matches many of the multi-megabyte-downloadrequiring-admin-installation editors out there. It’s got syntax highlighting, which is one of the first things to look for. Search and replace with regular expressions is something that always comes in handy and is implemented along with “find in files”. Set a buffer value in the configuration file and you can open multiple files in tabs like many other browsers.

“If you’re generating random passwords, then it’s still better to use a loop that selects random uppercase and lowercase letters...”

PHP EDITOR REVIEW I know—I can hear the collective moans over another editor review. In fact, I heard them from my editor (the human kind) when I brought the subject up, so I’ll Listing 1 1

define(‘WORDCODE_NUMWORDS’,2); define(‘WORDCODE_SEPERATOR’,’-’); function _getWordCode($file) { $chosenwords = array(); $fp = fopen($file,’r’); $fsize = filesize($file); for($x=0;$x<WORDCODE_NUMWORDS;$x++) { $pos = mt_rand(0,$fsize); fseek($fp,$pos); while(fgetc($fp) != “\n” && $pos != 0) { fseek($fp,—$pos); } $chosenwords[] = trim(fgets($fp)); } return implode(WORDCODE_SEPERATOR,$chosenwords); }

September 2004



PHP Architect



www.phparch.com

63

TIPS & TRICKS It certainly doesn’t end there, either. Other features include brace and parenthesis matching, code folding (allowing you to hide the code within functions or methods, for example), auto-complete and code hinting (although those two will require some work on your own to enable and get set up). You can configure the path to the CLI version of PHP and run your PHP scripts from Scite to check for errors, also. The program will take you to the file and line number of any standard errors that are reported, also. Want to export your magic function to a PDF file and share it with others? Exporting to PDF, HTML, RTF, Latex and XML is only a couple of clicks away. Using the configuration file options, you can color single and double quotes string different colors as a reminder of which one you’re working with. Variables within single quoted strings are not highlighted like variables in double quoted strings, so you have a visual cue that the variable will not be evaluated. There are a ton of different configuration options you can set that are described in great detail in the documentation(http://scintilla.sourceforge.net/SciTEDoc.ht ml). You can essentially customize any part of the program to your liking. Looking back, I think I said something about keeping this short in the beginning, so I had better cut myself off and let you see the rest for yourself. If you already

have a favorite editor, I’m looking to change it. The Scite editor fills the niche of a small, powerful, featurerich editor that’ll match almost any other editor out there. It also fits easily on a USB thumb drive, which is how I take it everywhere I go now. Go check it out for yourself. GOT SOMETHING BETTER? We’ve done the editor comparisons to death across the PHP community, I’m sure, but if you have a better suggestion for a small powerful editor, visit the php|architect forums at http://www.phparch.com/discuss/ and let me know in the Tips & Tricks forum. Or, if you have a better algorithm for getting random lines out of a file or a better algorithm for anything in general, come let the community know in the forums where we can discuss and learn from it. As alway

About the Author

Have you had your PHP today?

ER FF LO IA EC SP

Subscribe to the print

?>

John Holmes is a Captain in the U.S. Army and a freelance PHP and MySQL programmer. He has been programming in PHP for over 4 years and loves every minute of it. He is currently serving at Ft. Gordon, Georgia as a Company Commander with his wife and two sons.

http://www.phparch.com

edition and get a copy of Lumen's LightBulb--a $499 value absolutely FREE*!

In collaboration with:

NEW COMBO NOW AVAILABLE: PDF + PRINT The Magazine For PHP Professionals

* Offer valid until 12/31/2004 on the purchase of a 12-month print subscription

September 2004



PHP Architect



www.phparch.com

64

Exam Under the Microscope

e x i t ( 0 ) ;

by Andi Gutmans and Marco Tabini

Marco’s Braindump The recent introduction of the Zend Certification Exam has been the topic of several news items, as well as posts on our forums and on the mailing lists. Although I must say that, as far as I’ve been able to see, the reception has, for the most part, been good, some people just seem to think that the certification program is nothing more than a power (and money) grab from Zend. As one of the people who helped write the certification exam, I can hardly promise you that my opinion on the matter is entirely unbiased. I could, however, tell you that of the ten new things that are proposed to me every day, the certification is one of the few that I actually decided to pick up and help with—not because I consider my time so precious to the world-at-large that Zend and the community should feel honoured that I did, but because my time is limited and I did consider the certification program a worthy project that I could support wholeheartedly, and my interest goes beyond its purely financial aspects—you’ve been warned, and you be the judge. September 2004



PHP Architect



In my opinion, it is extremely important that we all understand one thing: the certification is not conceived as a tool for technical people. I don’t know how many times I’ve heard someone say (or write) something to the effect of “why should I get certified? I know PHP, and the fact that someone is certified or not wouldn’t affect my hiring him or not—I would simply judge his or her technical abilities for myself.” That’s great, except that it is a very deeply flawed way of thinking—slightly self-centered, if you ask me. In the real world—the one in which most people live and have to pay their mortgage and eat their meals—things are a bit different. The person who makes decisions is often not someone who has the technical knowledge required to make them—as sad as it may be, it’s a fact that the average business person does not know PHP. Like it or hate it, this means that that person lacks the tools required to make a good technical decision, including whether or not to hire a programmer, either as an employee or as a contractor.

www.phparch.com

From their point of view, experience alone cannot be a reference point. I have been taking pictures— semiprofessionally—for almost ten years now, and yet I doubt that any professional photographer would hire me given the opportunity to examine my work (the words “don’t quit your day job” come to mind). I could, however, go to someone who does not have the experience to judge my work technically, show them my ten best pictures and he would have to make a decision based on my ten years of experience—without being able to tell whether I can produce output of consistent quality. Much like a degree, a certification tells a prospective employer that you have been able to overcome a set of hurdles—seventy questions in ninety minutes in our case—and successfully meet a specific set of requirements. Naturally, a certificate is not on the same level as a degree—much like a degree is not on the same level of actually knowing what you’re doing. Still, they both have the intrinsic value given them by the level of knowledge that is required to obtain them, and

65

EXIT(0);

Exam Under the Microscope

while obtaining a degree is undoubtedly more difficult than getting Zend-certified, the latter exam is much more specific to PHP than anything you’re likely to do in college. Besides this, I have to admit that I took a sort-of perverse pleasure in seeing people come by the Zend booth (where I was a guest) at a recent conference, claiming that they “knew PHP inside out” and then failing to answer a reasonably simple question. My pleasure, just so that we understand each other, is not of the “told you so” kind— everybody makes mistakes and God know I wake up in the morning wondering about what I’ll do wrong each day. The pleasure is in seeing reality dawn on the faces of these people when they start wondering whether there is any value in the certification program after all. It’s too bad that, while they had an opportunity to face the problem in first person, so many others will simply dismiss the certification program as yet another attempt at commercializing PHP. Andi’s Thoughts Such an interesting topic! As I do share some of the reasoning Marco mentioned, I hope my part of the column won’t be too repetitive. If it is, then I apologize in advance. Like Marco, I might be a bit biased, being co-Founder of Zend and also a member of the Zend Certification Committee, but I never write something I don’t believe in, so, again, you be the judge. There has always been a lot of talk about what is good for PHP and whether the involvement of commercial companies such as Zend benefits the PHP community. The way I see it, at the end of the day, most non-hobbyist developers in the PHP community have to make a living—preferably, not “just a living,” but a decent one, too. I have met many PHP enthusiasts who have had to develop JSP or ASP to make a living. Now why is that? Because, historically, there have September 2004



PHP Architect



been more jobs—better paying jobs—in these technologies. If we try and understand why this is the case, I am sure most people reading php|architect will agree that it’s not due to PHP’s inferiority as a technology. On the contrary, we hear of far more success stories about sites moving from JSP/ASP to PHP than the other way around. So what is the reason behind this inequality? I think there are many, but probably a major one is the current perception of PHP in the commercial market. Many decision makers have seen PHP as a hobbyist language that is not ready for prime-time, or even a great language but just not a safe choice because, without strong commercial backing, betting on such a technology might be risky. The end result is that large employers are still hesitant in choosing PHP and/or they see PHP developers as mere “scripters” and not real software engineers and architects. This leads to fewer available PHP jobs and lower salaries for the PHP developer. For this reason and others, is it so important to have companies like Zend who help evangelize PHP in the commercial market. The first step is to convince these companies that PHP is a great technology that easily competes with JSP/ASP and is up to prime-time, enterprise-grade applications. Such market education is difficult and requires many efforts to reach the places where decision makers get their information. Although the Netcraft numbers of PHP’s install base are amazing, most such decision makers don’t hang out on their site or visit php.net to see our impressive graphs. If employers actually have decided to look at PHP in a serious manner, they start asking themselves what anybody would qualify as very reasonable questions. Who will support us and hold our hands in this endeavor? Where do we get commercially-backed development and production tools from? Most importantly, not only how do we

www.phparch.com

find PHP developers, but also how do we know they are actually up to the job? I think it is clear that the Zend Certification Exam is up to this job. Having such a certification is the key to make employers feel comfortable in hiring PHP developers and ultimately pushing the wider proliferation of PHP. As Zend recognized the strategic importance of having a certification program for PHP as a technology, we called upon a selection of the leaders of the PHP community to help us make the certification into an exam whose rationale doesn’t only sound good in writing (like the *cough* Microsoft certifications), but an exam which truly tests the skills a successful PHP developer requires. In my opinion, having a high quality certification backed by PHP experts is something PHP has needed for a long time. The logistics required to set up such a certification exam, which include market education, availability of three thousand test centers around the world and other important tasks can only be successfully performed by a commercial company that can invest the amount of resources needed to get such an endeavor up and running. There is no doubt that Zend’s motivation is the ongoing proliferation of PHP and, therefore, advancing Zend’s business. However, it all comes back to the fact that PHP’s ongoing success is the success for every person that makes (or would like to make) a living developing or working with the best web scripting language available today. Looking back at that last year, with the growing adoption of PHP in larger commercial companies and the growing number of available PHP jobs, I think all of this hard work seems to be making a change in the market.

php|a

66

Recommend Documents

SEPTEMBER 2004 WWW.SCIAM.COM COPYRIGHT 2004 SCIENTIFIC AMERICAN, INC. september 2004 SCIENTIFIC AMERICAN Volume 291 N...

In This Issue September 2004 In This Issue Click article title to open Reviews BONUS REVIEW: Line 6 Vetta II & Variax ...

World Economic and Financial Sur veys Global Financial Stability Report Market Developments and Issues September 2004...

SEPTEMBER 2004 · € 6,90 (D/A) DEUTSCHE AUSGABE DES > Mikroben steuerten Klima > Das Sterben der Sonne > Chips im Gehir...