Passed
Pull Request — master (#309)
by Jason
06:15 queued 01:51
created

FoxyCart_Helper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * FoxyCart_Helper.
4
 *
5
 * @author FoxyCart.com
6
 * @copyright FoxyCart.com LLC, 2011
7
 *
8
 * @version 0.7.2.20111013
9
 *
10
 * @license MIT http://opensource.org/licenses/MIT
11
 *
12
 * @example http://wiki.foxycart.com/docs/cart/validation
13
 *
14
 * Requirements:
15
 *   - Form "code" values should not have leading or trailing whitespace.
16
 *   - Cannot use double-pipes in an input's name
17
 *   - Empty textareas are assumed to be "open"
18
 */
19
class FoxyCart_Helper
20
{
21
    /**
22
     * API Key (Secret).
23
     *
24
     * @var string
25
     **/
26
    private static $secret;
27
28
/**
29
 * Cart URL.
30
 *
31
 * @var string
32
 *             Notes: Could be 'https://yourdomain.foxycart.com/cart' or 'https://secure.yourdomain.com/cart'
33
 **/
34
    // protected static $cart_url = 'https://yourdomain.foxycart.com/cart';
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
35
    protected static $cart_url;
36
37
    public static function setCartURL($storeName = null)
38
    {
39
        self::$cart_url = 'https://'.$storeName.'.faxycart.com/cart';
40
    }
41
42
    public static function setSecret($secret = null)
43
    {
44
        self::$secret = $secret;
45
    }
46
47
    public function __construct()
48
    {
49
        self::setCartURL(FoxyCart::getFoxyCartStoreName());
50
        self::setSecret(FoxyCart::getStoreKey());
51
    }
52
53
    public static function getSecret()
54
    {
55
        return FoxyCart::getStoreKey();
56
    }
57
58
    /**
59
     * Cart Excludes.
60
     *
61
     * Arrays of values and prefixes that should be ignored when signing links and forms.
62
     *
63
     * @var array
64
     */
65
    protected static $cart_excludes = array(
66
        // Cart values
67
        'cart', 'fcsid', 'empty', 'coupon', 'output', 'sub_token', 'redirect', 'callback', '_',
68
        // Checkout pre-population values
69
        'customer_email', 'customer_first_name', 'customer_last_name', 'customer_address1', 'customer_address2',
70
        'customer_city', 'customer_state', 'customer_postal_code', 'customer_country', 'customer_phone', 'customer_company',
71
        'shipping_first_name', 'shipping_last_name', 'shipping_address1', 'shipping_address2',
72
        'shipping_city', 'shipping_state', 'shipping_postal_code', 'shipping_country', 'shipping_phone', 'shipping_company',
73
    );
74
    protected static $cart_excludes_prefixes = array(
75
        'h:', 'x:', '__',
76
    );
77
78
    /**
79
     * Debugging.
80
     *
81
     * Set to $debug to TRUE to enable debug logging.
82
     */
83
    protected static $debug = false;
84
    protected static $log = array();
85
86
    /**
87
     * "Link Method": Generate HMAC SHA256 for GET Query Strings.
88
     *
89
     * Notes: Can't parse_str because PHP doesn't support non-alphanumeric characters as array keys.
90
     *
91
     * @return string
92
     **/
93
    public static function fc_hash_querystring($qs, $output = true)
94
    {
95
        self::$log[] = '<strong>Signing link</strong> with data: '.htmlspecialchars(substr($qs, 0, 150)).'...';
96
        $fail = self::$cart_url.'?'.$qs;
97
98
        // If the link appears to be hashed already, don't bother
99
        if (strpos($qs, '||')) {
100
            self::$log[] = '<strong>Link appears to be signed already</strong>: '.htmlspecialchars($code[0]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $code does not exist. Did you maybe mean $codes?
Loading history...
101
102
            return $fail;
103
        }
104
105
        // Stick an ampersand on the beginning of the querystring to make matching the first element a little easier
106
        $qs = '&'.urldecode($qs);
107
108
        // Get all the prefixes, codes, and name=value pairs
109
        preg_match_all('%(?P<amp>&(?:amp;)?)(?P<prefix>[a-z0-9]{1,3}:)?(?P<name>[^=]+)=(?P<value>[^&]+)%', $qs, $pairs, PREG_SET_ORDER);
110
        self::$log[] = 'Found the following pairs to sign:<pre>'.htmlspecialchars(print_r($pairs, true)).'</pre>';
111
112
        // Get all the "code" values, set the matches in $codes
113
        $codes = array();
114
        foreach ($pairs as $pair) {
115
            if ($pair['name'] == 'code') {
116
                $codes[$pair['prefix']] = $pair['value'];
117
            }
118
        }
119
        if (!count($codes)) {
120
            self::$log[] = '<strong style="color:#600;">No code found</strong> for the above link.';
121
122
            return $fail;
123
        }
124
        self::$log[] = '<strong style="color:orange;">CODES found:</strong> '.htmlspecialchars(print_r($codes, true));
125
126
        // Sign the name/value pairs
127
        foreach ($pairs as $pair) {
128
            // Skip the cart excludes
129
            if (in_array($pair['name'], self::$cart_excludes) || in_array($pair['prefix'], self::$cart_excludes_prefixes)) {
130
                self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$pair['prefix'].$pair['name'].'" = '.$pair['value'];
131
                continue;
132
            }
133
134
            // Continue to sign the value and replace the name=value in the querystring with name=value||hash
135
            $value = self::fc_hash_value($codes[$pair['prefix']], $pair['name'], $pair['value'], 'value', false, 'urlencode');
136
            $replacement = $pair['amp'].$pair['prefix'].urlencode($pair['name']).'='.$value;
137
            $qs = str_replace($pair[0], $replacement, $qs);
138
            self::$log[] = 'Signed <strong>'.$pair['name'].'</strong> = <strong>'.$pair['value'].'</strong> with '.$replacement.'.<br />Replacing: '.$pair[0].'<br />With... '.$replacement;
139
        }
140
        $qs = ltrim($qs, '&'); // Get rid of that leading ampersand we added earlier
141
142
        if ($output) {
143
            echo self::$cart_url.'?'.$qs;
144
        } else {
145
            return self::$cart_url.'?'.$qs;
146
        }
147
    }
148
149
    /**
150
     * "Form Method": Generate HMAC SHA256 for form elements or individual <input />s.
151
     *
152
     * @return string
153
     **/
154
    public static function fc_hash_value($product_code, $option_name, $option_value = '', $method = 'name', $output = true, $urlencode = false)
155
    {
156
        if (!$product_code || !$option_name) {
157
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
158
        }
159
        if ($option_value == '--OPEN--') {
160
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
161
            $value = ($urlencode) ? urlencode($option_name).'||'.$hash.'||open' : $option_name.'||'.$hash.'||open';
162
        } else {
163
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
164
            if ($method == 'name') {
165
                $value = ($urlencode) ? urlencode($option_name).'||'.$hash : $option_name.'||'.$hash;
166
            } else {
167
                $value = ($urlencode) ? urlencode($option_value).'||'.$hash : $option_value.'||'.$hash;
168
            }
169
        }
170
171
        if ($output) {
172
            echo $value;
173
        } else {
174
            return $value;
175
        }
176
    }
177
178
    /**
179
     * Raw HTML Signing: Sign all links and form elements in a block of HTML.
180
     *
181
     * Accepts a string of HTML and signs all links and forms.
182
     * Requires link 'href' and form 'action' attributes to use 'https' and not 'http'.
183
     * Requires a 'code' to be set in every form.
184
     *
185
     * @return string
186
     **/
187
    public static function fc_hash_html($html)
188
    {
189
        // Initialize some counting
190
        $count['temp'] = 0; // temp counter
0 ignored issues
show
Comprehensibility Best Practice introduced by
$count was never initialized. Although not strictly required by PHP, it is generally a good practice to add $count = array(); before regardless.
Loading history...
191
        $count['links'] = 0;
192
        $count['forms'] = 0;
193
        $count['inputs'] = 0;
194
        $count['lists'] = 0;
195
        $count['textareas'] = 0;
196
197
        // Find and sign all the links
198
        preg_match_all('%<a .*?href=[\'"]'.preg_quote(self::$cart_url).'(?:\.php)?\?(.+?)[\'"].*?>%i', $html, $querystrings);
199
        // print_r($querystrings);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
200
        foreach ($querystrings[1] as $querystring) {
201
            // If it's already signed, skip it.
202
            if (preg_match('%&(?:amp;)?hash=%i', $querystring)) {
203
                continue;
204
            }
205
            $pattern = '%(href=[\'"])'.preg_quote(self::$cart_url, '%').'(?:\.php)?\?'.preg_quote($querystring, '%').'([\'"])%i';
206
            $signed = self::fc_hash_querystring($querystring, false);
207
            $html = preg_replace($pattern, '$1'.$signed.'$2', $html, -1, $count['temp']);
208
            $count['links'] += $count['temp'];
209
        }
210
        unset($querystrings);
211
212
        // Find and sign all form values
213
        preg_match_all('%<form [^>]*?action=[\'"]'.preg_quote(self::$cart_url).'?[\'"].*?>(.+?)</form>%is', $html, $forms);
214
        foreach ($forms[1] as $form) {
215
            ++$count['forms'];
216
            self::$log[] = '<strong>Signing form</strong> with data: '.htmlspecialchars(substr($form, 0, 150)).'...';
217
218
            // Store the original form so we can replace it when we're done
219
            $form_original = $form;
220
221
            // Check for the "code" input, set the matches in $codes
222
            if (!preg_match_all('%<[^>]*?name=([\'"])([0-9]{1,3}:)?code\1[^>]*?>%i', $form, $codes, PREG_SET_ORDER)) {
223
                self::$log[] = '<strong style="color:#600;">No code found</strong> for the above form.';
224
                continue;
225
            }
226
            // For each code found, sign the appropriate inputs
227
            foreach ($codes as $code) {
228
                // If the form appears to be hashed already, don't bother
229
                if (strpos($code[0], '||')) {
230
                    self::$log[] = '<strong>Form appears to be signed already</strong>: '.htmlspecialchars($code[0]);
231
                    continue;
232
                }
233
                // Get the code and the prefix
234
                $prefix = (isset($code[2])) ? $code[2] : '';
235
                preg_match('%<[^>]*?value=([\'"])(.+?)\1[^>]*?>%i', $code[0], $code);
236
                $code = trim($code[2]);
237
                self::$log[] = '<strong>Prefix for '.htmlspecialchars($code).'</strong>: '.htmlspecialchars($prefix);
238
                if (!$code) { // If the code is empty, skip this form or specific prefixed elements
239
                    continue;
240
                }
241
242
                // Sign all <input /> elements with matching prefix
243
                preg_match_all('%<input [^>]*?name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(?:.+?)\1[^>]*>%i', $form, $inputs);
244
                foreach ($inputs[0] as $input) {
245
                    ++$count['inputs'];
246
                    // Test to make sure both name and value attributes are found
247
                    if (preg_match('%name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1%i', $input, $name) > 0) {
248
                        preg_match('%value=([\'"])(.*?)\1%i', $input, $value);
249
                        $value = (count($value) > 0) ? $value : array('', '', '');
250
                        preg_match('%type=([\'"])(.*?)\1%i', $input, $type);
251
                        $type = (count($type) > 0) ? $type : array('', '', '');
252
                        // Skip the cart excludes
253
                        if (in_array($prefix.$name[2], self::$cart_excludes) || in_array(substr($prefix.$name[2], 0, 2), self::$cart_excludes_prefixes)) {
254
                            self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'.$prefix.$name[2].'" = '.$value[2];
255
                            continue;
256
                        }
257
                        self::$log[] = '<strong>INPUT['.$type[2].']:</strong> Name: <strong>'.$prefix.htmlspecialchars(preg_quote($name[2])).'</strong>';
258
                        self::$log[] = '<strong>Replacement Pattern:</strong> ([\'"])'.$prefix.preg_quote($name[2]).'\1';
259
                        $value[2] = ($value[2] == '') ? '--OPEN--' : $value[2];
260
                        if ($type[2] == 'radio') {
261
                            $input_signed = preg_replace('%([\'"])'.preg_quote($value[2]).'\1%', '${1}'.self::fc_hash_value($code, $name[2], $value[2], 'value', false).'$1', $input);
262
                        } else {
263
                            $input_signed = preg_replace('%([\'"])'.$prefix.preg_quote($name[2]).'\1%', '${1}'.$prefix.self::fc_hash_value($code, $name[2], $value[2], 'name', false).'$1', $input);
264
                        }
265
                        self::$log[] = '<strong>INPUT:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
266
                                       '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$name[2]).
267
                                       '</strong> :: Value: <strong>'.htmlspecialchars($value[2]).
268
                                       '</strong><br />Initial input: '.htmlspecialchars($input).
269
                                       '<br />Signed: <span style="color:#060;">'.htmlspecialchars($input_signed).'</span>';
270
                        $form = str_replace($input, $input_signed, $form);
271
                    }
272
                }
273
                self::$log[] = '<strong>FORM after INPUTS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
274
275
                // Sign all <option /> elements
276
                preg_match_all('%<select [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.+?)</select>%is', $form, $lists, PREG_SET_ORDER);
277
                foreach ($lists as $list) {
278
                    ++$count['lists'];
279
                    preg_match_all('%<option [^>]*value=([\'"])(.+?)\1[^>]*>(?:.*?)</option>%i', $list[0], $options, PREG_SET_ORDER);
280
                    self::$log[] = '<strong>Options:</strong> <pre>'.htmlspecialchars(print_r($options, true)).'</pre>';
281
                    unset($form_part_signed);
282
                    foreach ($options as $option) {
283
                        if (!isset($form_part_signed)) {
284
                            $form_part_signed = $list[0];
285
                        }
286
                        $option_signed = preg_replace(
287
                            '%'.preg_quote($option[1]).preg_quote($option[2]).preg_quote($option[1]).'%',
288
                            $option[1].self::fc_hash_value($code, $list[2], $option[2], 'value', false).$option[1],
289
                            $option[0]);
290
                        $form_part_signed = str_replace($option[0], $option_signed, $form_part_signed);
291
                        self::$log[] = '<strong>OPTION:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
292
                                       '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$list[2]).
293
                                       '</strong> :: Value: <strong>'.htmlspecialchars($option[2]).
294
                                       '</strong><br />Initial option: '.htmlspecialchars($option[0]).
295
                                       '<br />Signed: <span style="color:#060;">'.htmlspecialchars($option_signed).'</span>';
296
                    }
297
                    $form = str_replace($list[0], $form_part_signed, $form);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $form_part_signed does not seem to be defined for all execution paths leading up to this point.
Loading history...
298
                }
299
                self::$log[] = '<strong>FORM after OPTIONS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
300
301
                // Sign all <textarea /> elements
302
                preg_match_all('%<textarea [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.*?)</textarea>%is', $form, $textareas, PREG_SET_ORDER);
303
                // echo "\n\nTextareas: ".print_r($textareas, true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
304
                foreach ($textareas as $textarea) {
305
                    ++$count['textareas'];
306
                    // Tackle implied "--OPEN--" first, if textarea is empty
307
                    $textarea[3] = ($textarea[3] == '') ? '--OPEN--' : $textarea[3];
308
                    $textarea_signed = preg_replace('%([\'"])'.preg_quote($prefix.$textarea[2]).'\1%', '$1'.self::fc_hash_value($code, $textarea[2], $textarea[3], 'name', false).'$1', $textarea[0]);
309
                    $form = str_replace($textarea[0], $textarea_signed, $form);
310
                    self::$log[] = '<strong>TEXTAREA:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
311
                                   '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$textarea[2]).
312
                                   '</strong> :: Value: <strong>'.htmlspecialchars($textarea[3]).
313
                                   '</strong><br />Initial textarea: '.htmlspecialchars($textarea[0]).
314
                                   '<br />Signed: <span style="color:#060;">'.htmlspecialchars($textarea_signed).'</span>';
315
                }
316
                self::$log[] = '<strong>FORM after TEXTAREAS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
317
318
                // Exclude all <button> elements
319
                $form = preg_replace('%<button ([^>]*)name=([\'"])(.*?)\1([^>]*>.*?</button>)%i', '<button $1name=$2x:$3$4', $form);
320
            }
321
            // Replace the entire form
322
            self::$log[] = '<strong>FORM after ALL:</strong> <pre>'.htmlspecialchars($form).'</pre>'.'replacing <pre>'.htmlspecialchars($form_original).'</pre>';
323
            $html = str_replace($form_original, $form, $html);
324
            self::$log[] = '<strong>FORM end</strong><hr />';
325
        }
326
327
        // Return the signed output
328
        $output = '';
329
        if (self::$debug) {
330
            self::$log['Summary'] = $count['links'].' links signed. '.$count['forms'].' forms signed. '.$count['inputs'].' inputs signed. '.$count['lists'].' lists signed. '.$count['textareas'].' textareas signed.';
331
            $output .= '<h3>FoxyCart HMAC Debugging:</h3><ul>';
332
            foreach (self::$log as $name => $value) {
333
                $output .= '<li><strong>'.$name.':</strong> '.$value.'</li>';
334
            }
335
            $output .= '</ul><hr />';
336
        }
337
338
        return $output.$html;
339
    }
340
}
341