Source of file foxycart.cart_validation.php
Size: 21,157 Bytes - Last Modified: 2021-12-24T06:46:07+00:00
/var/www/docs.ssmods.com/process/src/thirdparty/foxycart/foxycart.cart_validation.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 | <?php /** * FoxyCart_Helper * * @author FoxyCart.com * @copyright FoxyCart.com LLC, 2011 * @version 2.0.0.20171024 * @license MIT http://opensource.org/licenses/MIT * @example http://wiki.foxycart.com/docs/cart/validation * * Requirements: * - Form "code" values should not have leading or trailing whitespace. * - Cannot use double-pipes in an input's name * - Empty textareas are assumed to be "open" */ class FoxyCart_Helper { /** * API Key (Secret) * * @var string **/ private static $secret = 'ENTER YOUR KEY HERE'; public static function setSecret($secret) { self::$secret = $secret; } public static function getSecret() { return self::$secret; } /** * Cart URL * * @var string * Notes: Could be 'https://yourdomain.foxycart.com/cart' or 'https://secure.yourdomain.com/cart' **/ protected static $cart_url = 'https://YOURDOMAIN.foxycart.com/cart'; public static function setCartUrl($cart_url) { self::$cart_url = $cart_url; } public static function getCartUrl() { return self::$cart_url; } /** * Cart Excludes * * Arrays of values and prefixes that should be ignored when signing links and forms. * @var array */ protected static $cart_excludes = array( // Analytics values '_', '_ga', '_ke', // Cart values 'cart', 'fcsid', 'empty', 'coupon', 'output', 'sub_token', 'redirect', 'callback', 'locale', 'template_set', // Checkout pre-population values 'customer_email', 'customer_first_name', 'customer_last_name', 'customer_address1', 'customer_address2', 'customer_city', 'customer_state', 'customer_postal_code', 'customer_country', 'customer_phone', 'customer_company', 'billing_first_name', 'billing_last_name', 'billing_address1', 'billing_address2', 'billing_city', 'billing_postal_code', 'billing_region', 'billing_phone', 'billing_company', 'shipping_first_name', 'shipping_last_name', 'shipping_address1', 'shipping_address2', 'shipping_city', 'shipping_state', 'shipping_country', 'shipping_postal_code', 'shipping_region', 'shipping_phone', 'shipping_company', ); protected static $cart_excludes_prefixes = array( 'h:', 'x:', '__', 'utm_' ); /** * Debugging * * Set to $debug to TRUE to enable debug logging. * */ protected static $debug = FALSE; protected static $log = array(); /** * "Link Method": Generate HMAC SHA256 for GET Query Strings * * Notes: Can't parse_str because PHP doesn't support non-alphanumeric characters as array keys. * @return string **/ public static function fc_hash_querystring($qs, $output = TRUE) { self::$log[] = '<strong>Signing link</strong> with data: '.htmlspecialchars(substr($qs, 0, 1500)).'...'; $fail = self::$cart_url.'?'.$qs; // If the link appears to be hashed already, don't bother if (strpos($qs, '||')) { self::$log[] = '<strong>Link appears to be signed already</strong>: '.htmlspecialchars($code[0]); return $fail; } // Stick an ampersand on the beginning of the querystring to make matching the first element a little easier $qs = '&'.$qs; // Get all the prefixes, codes, and name=value pairs preg_match_all('%(?P<amp>&(?:amp;)?)(?P<prefix>[a-z0-9]{1,3}:)?(?P<name>[^=]+)=(?P<value>[^&]+)%', $qs, $pairs, PREG_SET_ORDER); self::$log[] = 'Found the following pairs to sign:<pre>'.htmlspecialchars(print_r($pairs, true)).'</pre>'; // Get all the "code" values, set the matches in $codes $codes = array(); foreach ($pairs as $pair) { if ($pair['name'] == 'code') { $codes[$pair['prefix']] = $pair['value']; } } foreach ($pairs as $pair) { if ($pair['name'] == 'parent_code') { $codes[$pair['prefix']] .= $pair['value']; } } if ( ! count($codes)) { self::$log[] = '<strong style="color:#600;">No code found</strong> for the above link.'; return $fail; } self::$log[] = '<strong style="color:orange;">CODES found:</strong> '.htmlspecialchars(print_r($codes, true)); // Sign the name/value pairs foreach ($pairs as $pair) { // Skip the cart excludes $include_pair = true; if (in_array($pair['name'], self::$cart_excludes)) { $include_pair = false; } foreach (self::$cart_excludes_prefixes as $exclude_prefix) { if (substr(strtolower($pair['prefix'].$pair['name']), 0, strlen($exclude_prefix)) == $exclude_prefix) { $include_pair = false; } } if (!$include_pair) { self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$pair['prefix'].$pair['name'].'" = '.$pair['value']; continue; } // Continue to sign the value and replace the name=value in the querystring with name=value||hash $value = self::fc_hash_value($codes[$pair['prefix']], urldecode($pair['name']), urldecode($pair['value']), 'value', FALSE, 'urlencode'); if (urldecode($pair['value']) === '--OPEN--') { $replacement = $pair['amp'].$value.'='; } else { $replacement = $pair['amp'].$pair['prefix'].urlencode($pair['name']).'='.$value; } $qs = str_replace($pair[0], $replacement, $qs); self::$log[] = 'Signed <strong>'.$pair['name'].'</strong> = <strong>'.$pair['value'].'</strong> with '.$replacement.'.<br />Replacing: '.$pair[0].'<br />With... '.$replacement; } $qs = ltrim($qs, '&'); // Get rid of that leading ampersand we added earlier if ($output) { echo self::$cart_url.'?'.$qs; } else { return self::$cart_url.'?'.$qs; } } /** * "Form Method": Generate HMAC SHA256 for form elements or individual <input />s * * @param string $product_code The product code * @param string $option_name The field name to encode * @param string|double $option_value The field value to encode * @param string $method Choose to encode for the name or the value. Defaults to name. * @param bool $output Will echo when true and return when false. * @param bool $urlencode Output will be url encoded if true. Defaults to false. * * @return string|null **/ public static function fc_hash_value($product_code, $option_name, $option_value = '', $method = 'name', $output = TRUE, $urlencode = false) { if (!$product_code || !$option_name) { return FALSE; } $option_name = str_replace(' ', '_', $option_name); if ($option_value === '--OPEN--') { $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::$secret); $value = ($urlencode) ? urlencode($option_name).'||'.$hash.'||open' : $option_name.'||'.$hash.'||open'; } else { $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::$secret); self::$log[] = '<strong>Challenge: </strong><span style="font-family:monospace; color:blue"><code>'.$product_code.$option_name.$option_value.'</code></span>'; if ($method == 'name') { $value = ($urlencode) ? urlencode($option_name).'||'.$hash : $option_name.'||'.$hash; } else { $value = ($urlencode) ? urlencode($option_value).'||'.$hash : $option_value.'||'.$hash; } } if ($output) { echo $value; } else { return $value; } } /** * Raw HTML Signing: Sign all links and form elements in a block of HTML * * Accepts a string of HTML and signs all links and forms. * Requires link 'href' and form 'action' attributes to use 'https' and not 'http'. * Requires a 'code' to be set in every form. * * @return string **/ public static function fc_hash_html($html) { // Initialize some counting $count['temp'] = 0; // temp counter $count['links'] = 0; $count['forms'] = 0; $count['inputs'] = 0; $count['lists'] = 0; $count['textareas'] = 0; // Find and sign all the links preg_match_all('%<a .*?href=([\'"])'.preg_quote(self::$cart_url).'(?:\.php)?\?(.+?)\1.*?>%i', $html, $querystrings); self::$log[] = '<strong>Querystrings: </strong><pre>' . htmlspecialchars(print_r($querystrings, true)) . '</pre>'; // print_r($querystrings); foreach ($querystrings[2] as $querystring) { // If it's already signed, skip it. if (strpos($querystring, '||')) { continue; } $pattern = '%(href=([\'"]))'.preg_quote(self::$cart_url, '%').'(?:\.php)?\?'.preg_quote($querystring, '%').'\2%i'; $signed = self::fc_hash_querystring($querystring, FALSE); $html = preg_replace($pattern, '$1'.$signed.'$2', $html, -1, $count['temp']); $count['links'] += $count['temp']; } unset($querystrings); // Find and sign all form values preg_match_all('%<form [^>]*?action=[\'"]'.preg_quote(self::$cart_url).'(?:\.php)?[\'"].*?>(.+?)</form>%is', $html, $forms); foreach ($forms[1] as $form) { $count['forms']++; self::$log[] = '<strong>Signing form</strong> with data: '.htmlspecialchars(substr($form, 0, 150)).'...'; // Store the original form so we can replace it when we're done $form_original = $form; // Check for the "code" input, set the matches in $codes if (!preg_match_all('%<[^>]*?name=([\'"])([0-9]{1,3}:)?code\1[^>]*?>%i', $form, $codes, PREG_SET_ORDER)) { self::$log[] = '<strong style="color:#600;">No code found</strong> for the above form.'; continue; } // For each code found, sign the appropriate inputs foreach ($codes as $code) { // If the form appears to be hashed already, don't bother if (strpos($code[0], '||')) { self::$log[] = '<strong>Form appears to be signed already</strong>: '.htmlspecialchars($code[0]); continue; } // Get the code and the prefix $prefix = (isset($code[2])) ? $code[2] : ''; preg_match('%<[^>]*?value=([\'"])(.+?)\1[^>]*?>%i', $code[0], $code); $code = trim($code[2]); self::$log[] = '<strong>Prefix for '.htmlspecialchars($code).'</strong>: '.htmlspecialchars($prefix); if (!$code) { // If the code is empty, skip this form or specific prefixed elements continue; } // Sign all <input /> elements with matching prefix preg_match_all('%<input [^>]*?name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(?:.+?)\1[^>]*>%i', $form, $inputs); // get parent codes if they exist and append them to our code $parent_code_index = false; foreach ($inputs[0] as $key => $item) { if (strpos($item, 'parent_code') !== false) { $parent_code_index = $key; } } if ($parent_code_index !== false) { if (preg_match('%value=([\'"])(.*?)\1%i', $inputs[0][$parent_code_index], $value)) { $code .= $value[2]; } } foreach ($inputs[0] as $input) { $count['inputs']++; // Test to make sure both name and value attributes are found if (preg_match('%name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1%i', $input, $name) > 0) { preg_match('%value=([\'"])(.*?)\1%i', $input, $value); $value = (count($value) > 0) ? $value : array('', '', ''); preg_match('%type=([\'"])(.*?)\1%i', $input, $type); $type = (count($type) > 0) ? $type : array('', '', ''); // Skip the cart excludes $include_input = true; if (in_array($prefix.$name[2], self::$cart_excludes)) { $include_input = false; } foreach (self::$cart_excludes_prefixes as $exclude_prefix) { if (substr(strtolower($prefix.$name[2]), 0, strlen($exclude_prefix)) == $exclude_prefix) { $include_input = false; } } if (!$include_input) { self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$prefix.$name[2].'" = '.$value[2]; continue; } self::$log[] = '<strong>INPUT['.$type[2].']:</strong> Name: <strong>'.$prefix.htmlspecialchars(preg_quote($name[2])).'</strong>'; $value[2] = ($value[2] == '') ? '--OPEN--' : $value[2]; if ($type[2] == 'radio') { self::$log[] = '<strong>Replacement Pattern:</strong> ([\'"])'.$prefix.preg_quote($value[2]).'\1'; $input_signed = preg_replace('%([\'"])'.preg_quote($value[2]).'\1%', '${1}'.self::fc_hash_value($code, $name[2], $value[2], 'value', FALSE)."$1", $input); } else { self::$log[] = '<strong>Replacement Pattern:</strong> name=([\'"])'.$prefix.preg_quote($name[2]).'\1'; $input_signed = preg_replace('%name=([\'"])'.$prefix.preg_quote($name[2]).'\1%', 'name=${1}'.$prefix.self::fc_hash_value($code, $name[2], $value[2], 'name', FALSE)."$1", $input); } self::$log[] = '<strong>INPUT:</strong> Code: <strong>'.htmlspecialchars($prefix.$code). '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$name[2]). '</strong> :: Value: <strong>'.htmlspecialchars($value[2]). '</strong><br />Initial input: '.htmlspecialchars($input). '<br />Signed: <span style="color:#060;">'.htmlspecialchars($input_signed).'</span>'; $form = str_replace($input, $input_signed, $form); } } self::$log[] = '<strong>FORM after INPUTS:</strong> <pre>'.htmlspecialchars($form).'</pre>'; // Sign all <option /> elements preg_match_all('%<select [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.+?)</select>%is', $form, $lists, PREG_SET_ORDER); foreach ($lists as $list) { $count['lists']++; // Skip the cart excludes $include_input = true; if (in_array($prefix.$list[2], self::$cart_excludes)) { $include_input = false; } foreach (self::$cart_excludes_prefixes as $exclude_prefix) { if (substr(strtolower($prefix.$list[2]), 0, strlen($exclude_prefix)) == $exclude_prefix) { $include_input = false; } } if (!$include_input) { self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$prefix.$list[2]; continue; } preg_match_all('%<option [^>]*value=([\'"])(.+?)\1[^>]*>(?:.*?)</option>%i', $list[0], $options, PREG_SET_ORDER); self::$log[] = '<strong>Options:</strong> <pre>'.htmlspecialchars(print_r($options, true)).'</pre>'; unset( $form_part_signed ); foreach ($options as $option) { if( !isset($form_part_signed) ) $form_part_signed = $list[0]; $option_signed = preg_replace( '%'.preg_quote($option[1]).preg_quote($option[2]).preg_quote($option[1]).'%', $option[1].self::fc_hash_value($code, $list[2], $option[2], 'value', FALSE).$option[1], $option[0]); $form_part_signed = str_replace($option[0], $option_signed, $form_part_signed ); self::$log[] = '<strong>OPTION:</strong> Code: <strong>'.htmlspecialchars($prefix.$code). '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$list[2]). '</strong> :: Value: <strong>'.htmlspecialchars($option[2]). '</strong><br />Initial option: '.htmlspecialchars($option[0]). '<br />Signed: <span style="color:#060;">'.htmlspecialchars($option_signed).'</span>'; } $form = str_replace($list[0], $form_part_signed, $form); } self::$log[] = '<strong>FORM after OPTIONS:</strong> <pre>'.htmlspecialchars($form).'</pre>'; // Sign all <textarea /> elements preg_match_all('%<textarea [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.*?)</textarea>%is', $form, $textareas, PREG_SET_ORDER); // echo "\n\nTextareas: ".print_r($textareas, true); foreach ($textareas as $textarea) { $count['textareas']++; // Skip the cart excludes $include_input = true; if (in_array($prefix.$textarea[2], self::$cart_excludes)) { $include_input = false; } foreach (self::$cart_excludes_prefixes as $exclude_prefix) { if (substr(strtolower($prefix.$textarea[2]), 0, strlen($exclude_prefix)) == $exclude_prefix) { $include_input = false; } } if (!$include_input) { self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$prefix.$textarea[2]; continue; } // Tackle implied "--OPEN--" first, if textarea is empty $textarea[3] = ($textarea[3] == '') ? '--OPEN--' : $textarea[3]; $textarea_signed = preg_replace('%name=([\'"])'.preg_quote($prefix.$textarea[2]).'\1%', "name=$1".self::fc_hash_value($code, $textarea[2], $textarea[3], 'name', FALSE)."$1", $textarea[0]); $form = str_replace($textarea[0], $textarea_signed, $form); self::$log[] = '<strong>TEXTAREA:</strong> Code: <strong>'.htmlspecialchars($prefix.$code). '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$textarea[2]). '</strong> :: Value: <strong>'.htmlspecialchars($textarea[3]). '</strong><br />Initial textarea: '.htmlspecialchars($textarea[0]). '<br />Signed: <span style="color:#060;">'.htmlspecialchars($textarea_signed).'</span>'; } self::$log[] = '<strong>FORM after TEXTAREAS:</strong> <pre>'.htmlspecialchars($form).'</pre>'; // Exclude all <button> elements $form = preg_replace('%<button ([^>]*)name=([\'"])(.*?)\1([^>]*>.*?</button>)%i', "<button $1name=$2x:$3$4", $form); } // Replace the entire form self::$log[] = '<strong>FORM after ALL:</strong> <pre>'.htmlspecialchars($form).'</pre>'.'replacing <pre>'.htmlspecialchars($form_original).'</pre>'; $html = str_replace($form_original, $form, $html); self::$log[] = '<strong>FORM end</strong><hr />'; } // Return the signed output $output = ''; if (self::$debug) { self::$log['Summary'] = $count['links'].' links signed. '.$count['forms'].' forms signed. '.$count['inputs'].' inputs signed. '.$count['lists'].' lists signed. '.$count['textareas'].' textareas signed.'; $output .= '<div style="background:#fff;"><h3>FoxyCart HMAC Debugging:</h3><ul>'; foreach (self::$log as $name => $value) { $output .= '<li><strong>'.$name.':</strong> '.$value.'</li>'."\n"; } $output .= '</ul><hr />'; } return $output.$html; } } |