FoxyCart_Helper::getSecret()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

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 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
3
use Dynamic\FoxyStripe\Model\FoxyCart;
4
5
/**
6
 * FoxyCart_Helper.
7
 *
8
 * @author FoxyCart.com
9
 * @copyright FoxyCart.com LLC, 2011
10
 *
11
 * @version 0.7.2.20111013
12
 *
13
 * @license MIT http://opensource.org/licenses/MIT
14
 *
15
 * @example http://wiki.foxycart.com/docs/cart/validation
16
 *
17
 * Requirements:
18
 *   - Form "code" values should not have leading or trailing whitespace.
19
 *   - Cannot use double-pipes in an input's name
20
 *   - Empty textareas are assumed to be "open"
21
 */
22
class FoxyCart_Helper
23
{
24
    /**
25
     * API Key (Secret).
26
     *
27
     * @var string
28
     **/
29
    private static $secret;
30
31
/**
32
 * Cart URL.
33
 *
34
 * @var string
35
 *             Notes: Could be 'https://yourdomain.foxycart.com/cart' or 'https://secure.yourdomain.com/cart'
36
 **/
37
    // protected static $cart_url = 'https://yourdomain.foxycart.com/cart';
38
    protected static $cart_url;
39
40
    public static function setCartURL($storeName = null)
41
    {
42
        self::$cart_url = 'https://'.$storeName.'.faxycart.com/cart';
43
    }
44
45
    public static function setSecret($secret = null)
46
    {
47
        self::$secret = $secret;
48
    }
49
50
    public function __construct()
51
    {
52
        self::setCartURL(FoxyCart::getFoxyCartStoreName());
53
        self::setSecret(FoxyCart::getStoreKey());
54
    }
55
56
    public static function getSecret()
57
    {
58
        return FoxyCart::getStoreKey();
59
    }
60
61
    /**
62
     * Cart Excludes.
63
     *
64
     * Arrays of values and prefixes that should be ignored when signing links and forms.
65
     *
66
     * @var array
67
     */
68
    protected static $cart_excludes = array(
69
        // Cart values
70
        'cart', 'fcsid', 'empty', 'coupon', 'output', 'sub_token', 'redirect', 'callback', '_',
71
        // Checkout pre-population values
72
        'customer_email', 'customer_first_name', 'customer_last_name', 'customer_address1', 'customer_address2',
73
        'customer_city', 'customer_state', 'customer_postal_code', 'customer_country', 'customer_phone',
74
        'customer_company', 'shipping_first_name', 'shipping_last_name', 'shipping_address1', 'shipping_address2',
75
        'shipping_city', 'shipping_state', 'shipping_postal_code', 'shipping_country', 'shipping_phone',
76
        'shipping_company',
77
    );
78
    protected static $cart_excludes_prefixes = array(
79
        'h:', 'x:', '__',
80
    );
81
82
    /**
83
     * Debugging.
84
     *
85
     * Set to $debug to TRUE to enable debug logging.
86
     */
87
    protected static $debug = false;
88
    protected static $log = array();
89
90
    /**
91
     * "Link Method": Generate HMAC SHA256 for GET Query Strings.
92
     *
93
     * Notes: Can't parse_str because PHP doesn't support non-alphanumeric characters as array keys.
94
     *
95
     * @return string
96
     **/
97
    public static function fc_hash_querystring($qs, $output = true)
98
    {
99
        self::$log[] = '<strong>Signing link</strong> with data: '
100
            .htmlspecialchars(substr($qs, 0, 150)).'...';
101
        $fail = self::$cart_url.'?'.$qs;
102
103
        // If the link appears to be hashed already, don't bother
104
        if (strpos($qs, '||')) {
105
            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...
106
107
            return $fail;
108
        }
109
110
        // Stick an ampersand on the beginning of the querystring to make matching the first element a little easier
111
        $qs = '&'.urldecode($qs);
112
113
        // Get all the prefixes, codes, and name=value pairs
114
        preg_match_all(
115
            '%(?P<amp>&(?:amp;)?)(?P<prefix>[a-z0-9]{1,3}:)?(?P<name>[^=]+)=(?P<value>[^&]+)%',
116
            $qs,
117
            $pairs,
118
            PREG_SET_ORDER
119
        );
120
        self::$log[] = 'Found the following pairs to sign:<pre>'.htmlspecialchars(print_r($pairs, true)).'</pre>';
0 ignored issues
show
Bug introduced by
It seems like print_r($pairs, true) can also be of type true; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
        self::$log[] = 'Found the following pairs to sign:<pre>'.htmlspecialchars(/** @scrutinizer ignore-type */ print_r($pairs, true)).'</pre>';
Loading history...
121
122
        // Get all the "code" values, set the matches in $codes
123
        $codes = array();
124
        foreach ($pairs as $pair) {
125
            if ($pair['name'] == 'code') {
126
                $codes[$pair['prefix']] = $pair['value'];
127
            }
128
        }
129
        if (!count($codes)) {
130
            self::$log[] = '<strong style="color:#600;">No code found</strong> for the above link.';
131
132
            return $fail;
133
        }
134
        self::$log[] = '<strong style="color:orange;">CODES found:</strong> '
135
            .htmlspecialchars(print_r($codes, true));
136
137
        // Sign the name/value pairs
138
        foreach ($pairs as $pair) {
139
            // Skip the cart excludes
140
            if (in_array($pair['name'], self::$cart_excludes)
141
                || in_array($pair['prefix'], self::$cart_excludes_prefixes)) {
142
                self::$log[] = '<strong style="color:purple;">Skipping</strong> the reserved parameter or prefix "'
143
                    .$pair['prefix'].$pair['name'].'" = '.$pair['value'];
144
                continue;
145
            }
146
147
            // Continue to sign the value and replace the name=value in the querystring with name=value||hash
148
            $value = self::fc_hash_value(
149
                $codes[$pair['prefix']],
150
                $pair['name'],
151
                $pair['value'],
152
                'value',
153
                false,
154
                'urlencode'
155
            );
156
            $replacement = $pair['amp'].$pair['prefix'].urlencode($pair['name']).'='.$value;
157
            $qs = str_replace($pair[0], $replacement, $qs);
158
            self::$log[] = 'Signed <strong>'.$pair['name'].'</strong> = <strong>'.$pair['value'].'</strong> with '
159
                .$replacement.'.<br />Replacing: '.$pair[0].'<br />With... '.$replacement;
160
        }
161
        $qs = ltrim($qs, '&'); // Get rid of that leading ampersand we added earlier
162
163
        if ($output) {
164
            echo self::$cart_url.'?'.$qs;
165
        } else {
166
            return self::$cart_url.'?'.$qs;
167
        }
168
    }
169
170
    /**
171
     * "Form Method": Generate HMAC SHA256 for form elements or individual <input />s.
172
     *
173
     * @return string
174
     **/
175
    public static function fc_hash_value(
176
        $product_code,
177
        $option_name,
178
        $option_value = '',
179
        $method = 'name',
180
        $output = true,
181
        $urlencode = false
182
    ) {
183
        if (!$product_code || !$option_name) {
184
            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...
185
        }
186
        if ($option_value == '--OPEN--') {
187
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
0 ignored issues
show
Bug introduced by
It seems like self::getSecret() can also be of type false; however, parameter $key of hash_hmac() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, /** @scrutinizer ignore-type */ self::getSecret());
Loading history...
188
            $value = ($urlencode) ? urlencode($option_name).'||'.$hash.'||open' : $option_name.'||'.$hash.'||open';
189
        } else {
190
            $hash = hash_hmac('sha256', $product_code.$option_name.$option_value, self::getSecret());
191
            if ($method == 'name') {
192
                $value = ($urlencode) ? urlencode($option_name).'||'.$hash : $option_name.'||'.$hash;
193
            } else {
194
                $value = ($urlencode) ? urlencode($option_value).'||'.$hash : $option_value.'||'.$hash;
195
            }
196
        }
197
198
        if ($output) {
199
            echo $value;
200
        } else {
201
            return $value;
202
        }
203
    }
204
205
    /**
206
     * Raw HTML Signing: Sign all links and form elements in a block of HTML.
207
     *
208
     * Accepts a string of HTML and signs all links and forms.
209
     * Requires link 'href' and form 'action' attributes to use 'https' and not 'http'.
210
     * Requires a 'code' to be set in every form.
211
     *
212
     * @return string
213
     **/
214
    public static function fc_hash_html($html)
215
    {
216
        // Initialize some counting
217
        $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...
218
        $count['links'] = 0;
219
        $count['forms'] = 0;
220
        $count['inputs'] = 0;
221
        $count['lists'] = 0;
222
        $count['textareas'] = 0;
223
224
        // Find and sign all the links
225
        preg_match_all(
226
            '%<a .*?href=[\'"]'.preg_quote(self::$cart_url).'(?:\.php)?\?(.+?)[\'"].*?>%i',
227
            $html,
228
            $querystrings
229
        );
230
        // print_r($querystrings);
231
        foreach ($querystrings[1] as $querystring) {
232
            // If it's already signed, skip it.
233
            if (preg_match('%&(?:amp;)?hash=%i', $querystring)) {
234
                continue;
235
            }
236
            $pattern = '%(href=[\'"])'.preg_quote(self::$cart_url, '%').'(?:\.php)?\?'
237
                .preg_quote($querystring, '%').'([\'"])%i';
238
            $signed = self::fc_hash_querystring($querystring, false);
239
            $html = preg_replace($pattern, '$1'.$signed.'$2', $html, -1, $count['temp']);
240
            $count['links'] += $count['temp'];
241
        }
242
        unset($querystrings);
243
244
        // Find and sign all form values
245
        preg_match_all(
246
            '%<form [^>]*?action=[\'"]'.preg_quote(self::$cart_url).'?[\'"].*?>(.+?)</form>%is',
247
            $html,
248
            $forms
249
        );
250
        foreach ($forms[1] as $form) {
251
            ++$count['forms'];
252
            self::$log[] = '<strong>Signing form</strong> with data: '.htmlspecialchars(substr(
253
                $form,
254
                0,
255
                150
256
            )).'...';
257
258
            // Store the original form so we can replace it when we're done
259
            $form_original = $form;
260
261
            // Check for the "code" input, set the matches in $codes
262
            if (!preg_match_all(
263
                '%<[^>]*?name=([\'"])([0-9]{1,3}:)?code\1[^>]*?>%i',
264
                $form,
265
                $codes,
266
                PREG_SET_ORDER
267
            )) {
268
                self::$log[] = '<strong style="color:#600;">No code found</strong> for the above form.';
269
                continue;
270
            }
271
            // For each code found, sign the appropriate inputs
272
            foreach ($codes as $code) {
273
                // If the form appears to be hashed already, don't bother
274
                if (strpos($code[0], '||')) {
275
                    self::$log[] = '<strong>Form appears to be signed already</strong>: '.htmlspecialchars($code[0]);
276
                    continue;
277
                }
278
                // Get the code and the prefix
279
                $prefix = (isset($code[2])) ? $code[2] : '';
280
                preg_match('%<[^>]*?value=([\'"])(.+?)\1[^>]*?>%i', $code[0], $code);
281
                $code = trim($code[2]);
282
                self::$log[] = '<strong>Prefix for '.htmlspecialchars($code).'</strong>: '.htmlspecialchars($prefix);
283
                if (!$code) { // If the code is empty, skip this form or specific prefixed elements
284
                    continue;
285
                }
286
287
                // Sign all <input /> elements with matching prefix
288
                preg_match_all(
289
                    '%<input [^>]*?name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(?:.+?)\1[^>]*>%i',
290
                    $form,
291
                    $inputs
292
                );
293
                foreach ($inputs[0] as $input) {
294
                    ++$count['inputs'];
295
                    // Test to make sure both name and value attributes are found
296
                    if (preg_match(
297
                        '%name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1%i',
298
                        $input,
299
                        $name
300
                    ) > 0) {
301
                        preg_match('%value=([\'"])(.*?)\1%i', $input, $value);
302
                        $value = (count($value) > 0) ? $value : array('', '', '');
303
                        preg_match('%type=([\'"])(.*?)\1%i', $input, $type);
304
                        $type = (count($type) > 0) ? $type : array('', '', '');
305
                        // Skip the cart excludes
306
                        if (in_array(
307
                            $prefix.$name[2],
308
                            self::$cart_excludes
309
                        ) || in_array(substr(
310
                            $prefix.$name[2],
311
                            0,
312
                            2
313
                        ), self::$cart_excludes_prefixes)) {
314
                            self::$log[] = '<strong style="color:purple;">Skipping</strong> 
315
                                the reserved parameter or prefix "'.$prefix.$name[2].'" = '.$value[2];
316
                            continue;
317
                        }
318
                        self::$log[] = '<strong>INPUT['.$type[2].']:</strong> Name: <strong>'
319
                            .$prefix.htmlspecialchars(preg_quote($name[2])).'</strong>';
320
                        self::$log[] = '<strong>Replacement Pattern:</strong> ([\'"])'
321
                            .$prefix.preg_quote($name[2]).'\1';
322
                        $value[2] = ($value[2] == '') ? '--OPEN--' : $value[2];
323
                        if ($type[2] == 'radio') {
324
                            $input_signed = preg_replace('%([\'"])'
325
                                .preg_quote($value[2]).'\1%', '${1}'
326
                                .self::fc_hash_value($code, $name[2], $value[2], 'value', false)
327
                                .'$1', $input);
328
                        } else {
329
                            $input_signed = preg_replace('%([\'"])'.$prefix.preg_quote($name[2])
330
                                .'\1%', '${1}'.$prefix
331
                                .self::fc_hash_value($code, $name[2], $value[2], 'name', false)
332
                                .'$1', $input);
333
                        }
334
                        self::$log[] = '<strong>INPUT:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
335
                           '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$name[2]).
336
                           '</strong> :: Value: <strong>'.htmlspecialchars($value[2]).
337
                           '</strong><br />Initial input: '.htmlspecialchars($input).
338
                           '<br />Signed: <span style="color:#060;">'.htmlspecialchars($input_signed).'</span>';
339
                        $form = str_replace($input, $input_signed, $form);
340
                    }
341
                }
342
                self::$log[] = '<strong>FORM after INPUTS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
343
344
                // Sign all <option /> elements
345
                preg_match_all(
346
                    '%<select [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.+?)</select>%is',
347
                    $form,
348
                    $lists,
349
                    PREG_SET_ORDER
350
                );
351
                foreach ($lists as $list) {
352
                    ++$count['lists'];
353
                    preg_match_all(
354
                        '%<option [^>]*value=([\'"])(.+?)\1[^>]*>(?:.*?)</option>%i',
355
                        $list[0],
356
                        $options,
357
                        PREG_SET_ORDER
358
                    );
359
                    self::$log[] = '<strong>Options:</strong> <pre>'.htmlspecialchars(print_r($options, true))
0 ignored issues
show
Bug introduced by
It seems like print_r($options, true) can also be of type true; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

359
                    self::$log[] = '<strong>Options:</strong> <pre>'.htmlspecialchars(/** @scrutinizer ignore-type */ print_r($options, true))
Loading history...
360
                        .'</pre>';
361
                    unset($form_part_signed);
362
                    foreach ($options as $option) {
363
                        if (!isset($form_part_signed)) {
364
                            $form_part_signed = $list[0];
365
                        }
366
                        $option_signed = preg_replace(
367
                            '%'.preg_quote($option[1]).preg_quote($option[2]).preg_quote($option[1]).'%',
368
                            $option[1].self::fc_hash_value(
369
                                $code,
370
                                $list[2],
371
                                $option[2],
372
                                'value',
373
                                false
374
                            ).$option[1],
375
                            $option[0]
376
                        );
377
                        $form_part_signed = str_replace($option[0], $option_signed, $form_part_signed);
378
                        self::$log[] = '<strong>OPTION:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
379
                           '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$list[2]).
380
                           '</strong> :: Value: <strong>'.htmlspecialchars($option[2]).
381
                           '</strong><br />Initial option: '.htmlspecialchars($option[0]).
382
                           '<br />Signed: <span style="color:#060;">'.htmlspecialchars($option_signed).'</span>';
383
                    }
384
                    $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...
385
                }
386
                self::$log[] = '<strong>FORM after OPTIONS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
387
388
                // Sign all <textarea /> elements
389
                preg_match_all(
390
                    '%<textarea [^>]*name=([\'"])'.preg_quote($prefix).'(?![0-9]{1,3})(.+?)\1[^>]*>(.*?)</textarea>%is',
391
                    $form,
392
                    $textareas,
393
                    PREG_SET_ORDER
394
                );
395
                // echo "\n\nTextareas: ".print_r($textareas, true);
396
                foreach ($textareas as $textarea) {
397
                    ++$count['textareas'];
398
                    // Tackle implied "--OPEN--" first, if textarea is empty
399
                    $textarea[3] = ($textarea[3] == '') ? '--OPEN--' : $textarea[3];
400
                    $textarea_signed = preg_replace(
401
                        '%([\'"])'.preg_quote($prefix.$textarea[2]).'\1%',
402
                        '$1'.self::fc_hash_value(
403
                            $code,
404
                            $textarea[2],
405
                            $textarea[3],
406
                            'name',
407
                            false
408
                        ).'$1',
409
                        $textarea[0]
410
                    );
411
                    $form = str_replace($textarea[0], $textarea_signed, $form);
412
                    self::$log[] = '<strong>TEXTAREA:</strong> Code: <strong>'.htmlspecialchars($prefix.$code).
413
                       '</strong> :: Name: <strong>'.htmlspecialchars($prefix.$textarea[2]).
414
                       '</strong> :: Value: <strong>'.htmlspecialchars($textarea[3]).
415
                       '</strong><br />Initial textarea: '.htmlspecialchars($textarea[0]).
416
                       '<br />Signed: <span style="color:#060;">'.htmlspecialchars($textarea_signed).'</span>';
417
                }
418
                self::$log[] = '<strong>FORM after TEXTAREAS:</strong> <pre>'.htmlspecialchars($form).'</pre>';
419
420
                // Exclude all <button> elements
421
                $form = preg_replace(
422
                    '%<button ([^>]*)name=([\'"])(.*?)\1([^>]*>.*?</button>)%i',
423
                    '<button $1name=$2x:$3$4',
424
                    $form
425
                );
426
            }
427
            // Replace the entire form
428
            self::$log[] = '<strong>FORM after ALL:</strong> <pre>'.htmlspecialchars($form).'</pre>'
429
                .'replacing <pre>'.htmlspecialchars($form_original).'</pre>';
430
            $html = str_replace($form_original, $form, $html);
431
            self::$log[] = '<strong>FORM end</strong><hr />';
432
        }
433
434
        // Return the signed output
435
        $output = '';
436
        if (self::$debug) {
437
            self::$log['Summary'] = $count['links'].' links signed. '.$count['forms'].' forms signed. '
438
                .$count['inputs'].' inputs signed. '.$count['lists'].' lists signed. '.$count['textareas']
439
                .' textareas signed.';
440
            $output .= '<h3>FoxyCart HMAC Debugging:</h3><ul>';
441
            foreach (self::$log as $name => $value) {
442
                $output .= '<li><strong>'.$name.':</strong> '.$value.'</li>';
443
            }
444
            $output .= '</ul><hr />';
445
        }
446
447
        return $output.$html;
448
    }
449
}
450