Completed
Push — master ( f8f77e...d2a4ab )
by Jason
05:01
created

FoxyCart_Helper::fc_hash_html()   F

Complexity

Conditions 24
Paths 3204

Size

Total Lines 234
Code Lines 171

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 600

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 234
ccs 0
cts 175
cp 0
rs 2
cc 24
eloc 171
nc 3204
nop 1
crap 600

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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',
71
        'customer_company', 'shipping_first_name', 'shipping_last_name', 'shipping_address1', 'shipping_address2',
72
        'shipping_city', 'shipping_state', 'shipping_postal_code', 'shipping_country', 'shipping_phone',
73
        'shipping_company',
74
    );
75
    protected static $cart_excludes_prefixes = array(
76
        'h:', 'x:', '__',
77
    );
78
79
    /**
80
     * Debugging.
81
     *
82
     * Set to $debug to TRUE to enable debug logging.
83
     */
84
    protected static $debug = false;
85
    protected static $log = array();
86
87
    /**
88
     * "Link Method": Generate HMAC SHA256 for GET Query Strings.
89
     *
90
     * Notes: Can't parse_str because PHP doesn't support non-alphanumeric characters as array keys.
91
     *
92
     * @return string
93
     **/
94
    public static function fc_hash_querystring($qs, $output = true)
95
    {
96
        self::$log[] = '<strong>Signing link</strong> with data: '
97
            .htmlspecialchars(substr($qs, 0, 150)).'...';
98
        $fail = self::$cart_url.'?'.$qs;
99
100
        // If the link appears to be hashed already, don't bother
101
        if (strpos($qs, '||')) {
102
            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...
103
104
            return $fail;
105
        }
106
107
        // Stick an ampersand on the beginning of the querystring to make matching the first element a little easier
108
        $qs = '&'.urldecode($qs);
109
110
        // Get all the prefixes, codes, and name=value pairs
111
        preg_match_all(
112
            '%(?P<amp>&(?:amp;)?)(?P<prefix>[a-z0-9]{1,3}:)?(?P<name>[^=]+)=(?P<value>[^&]+)%',
113
            $qs,
114
            $pairs,
115
            PREG_SET_ORDER
116
        );
117
        self::$log[] = 'Found the following pairs to sign:<pre>'.htmlspecialchars(print_r($pairs, true)).'</pre>';
118
119
        // Get all the "code" values, set the matches in $codes
120
        $codes = array();
121
        foreach ($pairs as $pair) {
122
            if ($pair['name'] == 'code') {
123
                $codes[$pair['prefix']] = $pair['value'];
124
            }
125
        }
126
        if (!count($codes)) {
127
            self::$log[] = '<strong style="color:#600;">No code found</strong> for the above link.';
128
129
            return $fail;
130
        }
131
        self::$log[] = '<strong style="color:orange;">CODES found:</strong> '
132
            .htmlspecialchars(print_r($codes, true));
133
134
        // Sign the name/value pairs
135
        foreach ($pairs as $pair) {
136
            // Skip the cart excludes
137
            if (in_array($pair['name'], self::$cart_excludes)
138
                || in_array($pair['prefix'], self::$cart_excludes_prefixes)) {
139
                self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'
140
                    .$pair['prefix'].$pair['name'].'" = '.$pair['value'];
141
                continue;
142
            }
143
144
            // Continue to sign the value and replace the name=value in the querystring with name=value||hash
145
            $value = self::fc_hash_value(
146
                $codes[$pair['prefix']],
147
                $pair['name'],
148
                $pair['value'],
149
                'value',
150
                false,
151
                'urlencode'
152
            );
153
            $replacement = $pair['amp'].$pair['prefix'].urlencode($pair['name']).'='.$value;
154
            $qs = str_replace($pair[0], $replacement, $qs);
155
            self::$log[] = 'Signed <strong>'.$pair['name'].'</strong> = <strong>'.$pair['value'].'</strong> with '
156
                .$replacement.'.<br />Replacing: '.$pair[0].'<br />With... '.$replacement;
157
        }
158
        $qs = ltrim($qs, '&'); // Get rid of that leading ampersand we added earlier
159
160
        if ($output) {
161
            echo self::$cart_url.'?'.$qs;
162
        } else {
163
            return self::$cart_url.'?'.$qs;
164
        }
165
    }
166
167
    /**
168
     * "Form Method": Generate HMAC SHA256 for form elements or individual <input />s.
169
     *
170
     * @return string
171
     **/
172
    public static function fc_hash_value(
173
        $product_code,
174
        $option_name,
175
        $option_value = '',
176
        $method = 'name',
177
        $output = true,
178
        $urlencode = false
179
    ) {
180
        if (!$product_code || !$option_name) {
181
            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...
182
        }
183
        if ($option_value == '--OPEN--') {
184
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
185
            $value = ($urlencode) ? urlencode($option_name).'||'.$hash.'||open' : $option_name.'||'.$hash.'||open';
186
        } else {
187
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
188
            if ($method == 'name') {
189
                $value = ($urlencode) ? urlencode($option_name).'||'.$hash : $option_name.'||'.$hash;
190
            } else {
191
                $value = ($urlencode) ? urlencode($option_value).'||'.$hash : $option_value.'||'.$hash;
192
            }
193
        }
194
195
        if ($output) {
196
            echo $value;
197
        } else {
198
            return $value;
199
        }
200
    }
201
202
    /**
203
     * Raw HTML Signing: Sign all links and form elements in a block of HTML.
204
     *
205
     * Accepts a string of HTML and signs all links and forms.
206
     * Requires link 'href' and form 'action' attributes to use 'https' and not 'http'.
207
     * Requires a 'code' to be set in every form.
208
     *
209
     * @return string
210
     **/
211
    public static function fc_hash_html($html)
212
    {
213
        // Initialize some counting
214
        $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...
215
        $count['links'] = 0;
216
        $count['forms'] = 0;
217
        $count['inputs'] = 0;
218
        $count['lists'] = 0;
219
        $count['textareas'] = 0;
220
221
        // Find and sign all the links
222
        preg_match_all(
223
            '%<a .*?href=[\'"]'.preg_quote(self::$cart_url).'(?:\.php)?\?(.+?)[\'"].*?>%i',
224
            $html,
225
            $querystrings
226
        );
227
        // 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...
228
        foreach ($querystrings[1] as $querystring) {
229
            // If it's already signed, skip it.
230
            if (preg_match('%&(?:amp;)?hash=%i', $querystring)) {
231
                continue;
232
            }
233
            $pattern = '%(href=[\'"])'.preg_quote(self::$cart_url, '%').'(?:\.php)?\?'
234
                .preg_quote($querystring, '%').'([\'"])%i';
235
            $signed = self::fc_hash_querystring($querystring, false);
236
            $html = preg_replace($pattern, '$1'.$signed.'$2', $html, -1, $count['temp']);
237
            $count['links'] += $count['temp'];
238
        }
239
        unset($querystrings);
240
241
        // Find and sign all form values
242
        preg_match_all(
243
            '%<form [^>]*?action=[\'"]'.preg_quote(self::$cart_url).'?[\'"].*?>(.+?)</form>%is',
244
            $html,
245
            $forms
246
        );
247
        foreach ($forms[1] as $form) {
248
            ++$count['forms'];
249
            self::$log[] = '<strong>Signing form</strong> with data: '.htmlspecialchars(substr(
250
                $form,
251
                0,
252
                150
253
            )).'...';
254
255
            // Store the original form so we can replace it when we're done
256
            $form_original = $form;
257
258
            // Check for the "code" input, set the matches in $codes
259
            if (!preg_match_all(
260
                '%<[^>]*?name=([\'"])([0-9]{1,3}:)?code\1[^>]*?>%i',
261
                $form,
262
                $codes,
263
                PREG_SET_ORDER
264
            )) {
265
                self::$log[] = '<strong style="color:#600;">No code found</strong> for the above form.';
266
                continue;
267
            }
268
            // For each code found, sign the appropriate inputs
269
            foreach ($codes as $code) {
270
                // If the form appears to be hashed already, don't bother
271
                if (strpos($code[0], '||')) {
272
                    self::$log[] = '<strong>Form appears to be signed already</strong>: '.htmlspecialchars($code[0]);
273
                    continue;
274
                }
275
                // Get the code and the prefix
276
                $prefix = (isset($code[2])) ? $code[2] : '';
277
                preg_match('%<[^>]*?value=([\'"])(.+?)\1[^>]*?>%i', $code[0], $code);
278
                $code = trim($code[2]);
279
                self::$log[] = '<strong>Prefix for '.htmlspecialchars($code).'</strong>: '.htmlspecialchars($prefix);
280
                if (!$code) { // If the code is empty, skip this form or specific prefixed elements
281
                    continue;
282
                }
283
284
                // Sign all <input /> elements with matching prefix
285
                preg_match_all(
286
                    '%<input [^>]*?name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(?:.+?)\1[^>]*>%i',
287
                    $form,
288
                    $inputs
289
                );
290
                foreach ($inputs[0] as $input) {
291
                    ++$count['inputs'];
292
                    // Test to make sure both name and value attributes are found
293
                    if (preg_match(
294
                        '%name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1%i',
295
                        $input,
296
                        $name
297
                    ) > 0) {
298
                        preg_match('%value=([\'"])(.*?)\1%i', $input, $value);
299
                        $value = (count($value) > 0) ? $value : array('', '', '');
300
                        preg_match('%type=([\'"])(.*?)\1%i', $input, $type);
301
                        $type = (count($type) > 0) ? $type : array('', '', '');
302
                        // Skip the cart excludes
303
                        if (in_array(
304
                            $prefix.$name[2],
305
                            self::$cart_excludes
306
                        ) || in_array(substr(
307
                            $prefix.$name[2],
308
                            0,
309
                            2
310
                        ), self::$cart_excludes_prefixes)) {
311
                            self::$log[] = '<strong style="color:purple;">Skipping</strong> 
312
                                the reserved parameter or prefix "'.$prefix.$name[2].'" = '.$value[2];
313
                            continue;
314
                        }
315
                        self::$log[] = '<strong>INPUT['.$type[2].']:</strong> Name: <strong>'
316
                            .$prefix.htmlspecialchars(preg_quote($name[2])).'</strong>';
317
                        self::$log[] = '<strong>Replacement Pattern:</strong> ([\'"])'
318
                            .$prefix.preg_quote($name[2]).'\1';
319
                        $value[2] = ($value[2] == '') ? '--OPEN--' : $value[2];
320
                        if ($type[2] == 'radio') {
321
                            $input_signed = preg_replace('%([\'"])'
322
                                .preg_quote($value[2]).'\1%', '${1}'
323
                                .self::fc_hash_value($code, $name[2], $value[2], 'value', false)
324
                                .'$1', $input);
325
                        } else {
326
                            $input_signed = preg_replace('%([\'"])'.$prefix.preg_quote($name[2])
327
                                .'\1%', '${1}'.$prefix
328
                                .self::fc_hash_value($code, $name[2], $value[2], 'name', false)
329
                                .'$1', $input);
330
                        }
331
                        self::$log[] = '<strong>INPUT:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
332
                           '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$name[2]).
333
                           '</strong> :: Value: <strong>'.htmlspecialchars($value[2]).
334
                           '</strong><br />Initial input: '.htmlspecialchars($input).
335
                           '<br />Signed: <span style="color:#060;">'.htmlspecialchars($input_signed).'</span>';
336
                        $form = str_replace($input, $input_signed, $form);
337
                    }
338
                }
339
                self::$log[] = '<strong>FORM after INPUTS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
340
341
                // Sign all <option /> elements
342
                preg_match_all(
343
                    '%<select [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.+?)</select>%is',
344
                    $form,
345
                    $lists,
346
                    PREG_SET_ORDER
347
                );
348
                foreach ($lists as $list) {
349
                    ++$count['lists'];
350
                    preg_match_all(
351
                        '%<option [^>]*value=([\'"])(.+?)\1[^>]*>(?:.*?)</option>%i',
352
                        $list[0],
353
                        $options,
354
                        PREG_SET_ORDER
355
                    );
356
                    self::$log[] = '<strong>Options:</strong> <pre>'.htmlspecialchars(print_r($options, true))
357
                        .'</pre>';
358
                    unset($form_part_signed);
359
                    foreach ($options as $option) {
360
                        if (!isset($form_part_signed)) {
361
                            $form_part_signed = $list[0];
362
                        }
363
                        $option_signed = preg_replace(
364
                            '%'.preg_quote($option[1]).preg_quote($option[2]).preg_quote($option[1]).'%',
365
                            $option[1].self::fc_hash_value(
366
                                $code,
367
                                $list[2],
368
                                $option[2],
369
                                'value',
370
                                false
371
                            ).$option[1],
372
                            $option[0]
373
                        );
374
                        $form_part_signed = str_replace($option[0], $option_signed, $form_part_signed);
375
                        self::$log[] = '<strong>OPTION:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
376
                           '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$list[2]).
377
                           '</strong> :: Value: <strong>'.htmlspecialchars($option[2]).
378
                           '</strong><br />Initial option: '.htmlspecialchars($option[0]).
379
                           '<br />Signed: <span style="color:#060;">'.htmlspecialchars($option_signed).'</span>';
380
                    }
381
                    $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...
382
                }
383
                self::$log[] = '<strong>FORM after OPTIONS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
384
385
                // Sign all <textarea /> elements
386
                preg_match_all(
387
                    '%<textarea [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.*?)</textarea>%is',
388
                    $form,
389
                    $textareas,
390
                    PREG_SET_ORDER
391
                );
392
                // 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...
393
                foreach ($textareas as $textarea) {
394
                    ++$count['textareas'];
395
                    // Tackle implied "--OPEN--" first, if textarea is empty
396
                    $textarea[3] = ($textarea[3] == '') ? '--OPEN--' : $textarea[3];
397
                    $textarea_signed = preg_replace(
398
                        '%([\'"])'.preg_quote($prefix.$textarea[2]).'\1%',
399
                        '$1'.self::fc_hash_value(
400
                            $code,
401
                            $textarea[2],
402
                            $textarea[3],
403
                            'name',
404
                            false
405
                        ).'$1',
406
                        $textarea[0]
407
                    );
408
                    $form = str_replace($textarea[0], $textarea_signed, $form);
409
                    self::$log[] = '<strong>TEXTAREA:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
410
                       '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$textarea[2]).
411
                       '</strong> :: Value: <strong>'.htmlspecialchars($textarea[3]).
412
                       '</strong><br />Initial textarea: '.htmlspecialchars($textarea[0]).
413
                       '<br />Signed: <span style="color:#060;">'.htmlspecialchars($textarea_signed).'</span>';
414
                }
415
                self::$log[] = '<strong>FORM after TEXTAREAS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
416
417
                // Exclude all <button> elements
418
                $form = preg_replace(
419
                    '%<button ([^>]*)name=([\'"])(.*?)\1([^>]*>.*?</button>)%i',
420
                    '<button $1name=$2x:$3$4',
421
                    $form
422
                );
423
            }
424
            // Replace the entire form
425
            self::$log[] = '<strong>FORM after ALL:</strong> <pre>'.htmlspecialchars($form).'</pre>'
426
                .'replacing <pre>'.htmlspecialchars($form_original).'</pre>';
427
            $html = str_replace($form_original, $form, $html);
428
            self::$log[] = '<strong>FORM end</strong><hr />';
429
        }
430
431
        // Return the signed output
432
        $output = '';
433
        if (self::$debug) {
434
            self::$log['Summary'] = $count['links'].' links signed. '.$count['forms'].' forms signed. '
435
                .$count['inputs'].' inputs signed. '.$count['lists'].' lists signed. '.$count['textareas']
436
                .' textareas signed.';
437
            $output .= '<h3>FoxyCart HMAC Debugging:</h3><ul>';
438
            foreach (self::$log as $name => $value) {
439
                $output .= '<li><strong>'.$name.':</strong> '.$value.'</li>';
440
            }
441
            $output .= '</ul><hr />';
442
        }
443
444
        return $output.$html;
445
    }
446
}
447