Passed
Pull Request — master (#373)
by Nic
05:52
created

FoxyStripeController   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 130
dl 0
loc 325
ccs 0
cts 133
cp 0
rs 8.8
c 0
b 0
f 0
wmc 45

14 Methods

Rating   Name   Duplication   Size   Complexity  
B parseOrderCustomer() 0 33 6
A sso() 0 29 3
A handleDataFeed() 0 20 4
A orderDetailFromProduct() 0 16 2
A getProductPage() 0 10 3
A getEncryption() 0 14 5
A getURLSegment() 0 3 1
A parseOrderInfo() 0 12 2
A index() 0 18 4
A modifyOrderDetailPrice() 0 22 5
A parseOrderDetails() 0 11 4
A getTransactionOptions() 0 4 2
A parseOrder() 0 7 3
A updatePasswordFromData() 0 12 1

How to fix   Complexity   

Complex Class

Complex classes like FoxyStripeController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FoxyStripeController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Dynamic\FoxyStripe\Controller;
4
5
use Dynamic\FoxyStripe\Model\FoxyCart;
6
use Dynamic\FoxyStripe\Model\FoxyStripeClient;
7
use Dynamic\FoxyStripe\Model\FoxyStripeSetting;
8
use Dynamic\FoxyStripe\Model\OptionItem;
9
use Dynamic\FoxyStripe\Model\Order;
10
use Dynamic\FoxyStripe\Model\OrderDetail;
11
use Dynamic\FoxyStripe\Page\ProductPage;
12
use SilverStripe\ORM\Queries\SQLUpdate;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\Security;
15
16
class FoxyStripeController extends \PageController
17
{
18
    /**
19
     *
20
     */
21
    const URLSEGMENT = 'foxystripe';
22
23
    /**
24
     * @var array
25
     */
26
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
27
        'index',
28
        'sso',
29
    ];
30
31
    /**
32
     * @return string
33
     */
34
    public function getURLSegment()
35
    {
36
        return self::URLSEGMENT;
37
    }
38
39
    /**
40
     * @return string
41
     *
42
     * @throws \SilverStripe\ORM\ValidationException
43
     */
44
    public function index()
45
    {
46
        // handle POST from FoxyCart API transaction
47
        if ((isset($_POST['FoxyData']) or isset($_POST['FoxySubscriptionData']))) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
48
            $FoxyData_encrypted = (isset($_POST['FoxyData'])) ?
49
                urldecode($_POST['FoxyData']) :
50
                urldecode($_POST['FoxySubscriptionData']);
51
            $FoxyData_decrypted = \rc4crypt::decrypt(FoxyCart::getStoreKey(), $FoxyData_encrypted);
0 ignored issues
show
Bug introduced by
It seems like Dynamic\FoxyStripe\Model\FoxyCart::getStoreKey() can also be of type false; however, parameter $pwd of rc4crypt::decrypt() 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

51
            $FoxyData_decrypted = \rc4crypt::decrypt(/** @scrutinizer ignore-type */ FoxyCart::getStoreKey(), $FoxyData_encrypted);
Loading history...
52
53
            // parse the response and save the order
54
            self::handleDataFeed($FoxyData_encrypted, $FoxyData_decrypted);
0 ignored issues
show
Bug Best Practice introduced by
The method Dynamic\FoxyStripe\Contr...oller::handleDataFeed() is not static, but was called statically. ( Ignorable by Annotation )

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

54
            self::/** @scrutinizer ignore-call */ 
55
                  handleDataFeed($FoxyData_encrypted, $FoxyData_decrypted);
Loading history...
55
56
            // extend to allow for additional integrations with Datafeed
57
            $this->extend('addIntegrations', $FoxyData_encrypted);
58
59
            return 'foxy';
60
        } else {
61
            return 'No FoxyData or FoxySubscriptionData received.';
62
        }
63
    }
64
65
    /**
66
     * @param $encrypted
67
     * @param $decrypted
68
     *
69
     * @throws \SilverStripe\ORM\ValidationException
70
     */
71
    public function handleDataFeed($encrypted, $decrypted)
72
    {
73
        $orders = new \SimpleXMLElement($decrypted);
74
75
        // loop over each transaction to find FoxyCart Order ID
76
        foreach ($orders->transactions->transaction as $transaction) {
77
            // if FoxyCart order id, then parse order
78
            if (isset($transaction->id)) {
79
                $order = Order::get()->filter('Order_ID', (int)$transaction->id)->First();
80
                if (!$order) {
81
                    $order = Order::create();
82
                }
83
84
                // save base order info
85
                $order->Order_ID = (int)$transaction->id;
86
                $order->Response = urlencode($encrypted);
87
                // first write needed otherwise it creates a duplicates
88
                $order->write();
89
                $this->parseOrder($orders, $order);
0 ignored issues
show
Bug introduced by
$orders of type SimpleXMLElement is incompatible with the type array expected by parameter $transactions of Dynamic\FoxyStripe\Contr...ontroller::parseOrder(). ( Ignorable by Annotation )

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

89
                $this->parseOrder(/** @scrutinizer ignore-type */ $orders, $order);
Loading history...
90
                $order->write();
91
            }
92
        }
93
    }
94
95
    /**
96
     * @param array $transactions
97
     * @param Order $order
98
     */
99
    public function parseOrder($transactions, $order)
100
    {
101
        $this->parseOrderInfo($transactions, $order);
102
        if (FoxyStripeSetting::current_foxystripe_setting()->UseSingleSignOn && FoxyStripeClient::is_valid()) {
103
            $this->parseOrderCustomer($transactions, $order);
104
        }
105
        $this->parseOrderDetails($transactions, $order);
106
    }
107
108
    /**
109
     * @param array $orders
110
     * @param Order $transaction
111
     */
112
    public function parseOrderInfo($orders, $transaction)
113
    {
114
        foreach ($orders->transactions->transaction as $order) {
115
            // Record transaction data from FoxyCart Datafeed:
116
            $transaction->Store_ID = (int)$order->store_id;
0 ignored issues
show
Bug Best Practice introduced by
The property Store_ID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
117
            $transaction->TransactionDate = (string)$order->transaction_date;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string)$order->transaction_date of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBDatetime of property $TransactionDate.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
118
            $transaction->ProductTotal = (float)$order->product_total;
0 ignored issues
show
Documentation Bug introduced by
It seems like (double)$order->product_total of type double is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $ProductTotal.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
119
            $transaction->TaxTotal = (float)$order->tax_total;
0 ignored issues
show
Documentation Bug introduced by
It seems like (double)$order->tax_total of type double is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $TaxTotal.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120
            $transaction->ShippingTotal = (float)$order->shipping_total;
0 ignored issues
show
Documentation Bug introduced by
It seems like (double)$order->shipping_total of type double is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $ShippingTotal.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
121
            $transaction->OrderTotal = (float)$order->order_total;
0 ignored issues
show
Documentation Bug introduced by
It seems like (double)$order->order_total of type double is incompatible with the declared type SilverStripe\ORM\FieldType\DBCurrency of property $OrderTotal.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
122
            $transaction->ReceiptURL = (string)$order->receipt_url;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string)$order->receipt_url of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBVarchar of property $ReceiptURL.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
123
            $transaction->OrderStatus = (string)$order->status;
0 ignored issues
show
Documentation Bug introduced by
It seems like (string)$order->status of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBVarchar of property $OrderStatus.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
124
        }
125
    }
126
127
    /**
128
     * @param array $orders
129
     * @param Order $transaction
130
     * @throws \SilverStripe\ORM\ValidationException
131
     */
132
    public function parseOrderCustomer($orders, $transaction)
133
    {
134
        foreach ($orders->transactions->transaction as $order) {
135
            if (!isset($order->customer_email) || $order->is_anonymous != 0) {
136
                continue;
137
            }
138
139
            if (!$memeber = Member::get()->filter('Email', $order->customer_email)->first()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $memeber is dead and can be removed.
Loading history...
140
                $customer = Member::create();
141
                $customer->Customer_ID = (int)$order->customer_id;
142
                $customer->FirstName = (string)$order->customer_first_name;
143
                $customer->Surname = (string)$order->customer_last_name;
144
                $customer->Email = (string)$order->customer_email;
145
            }
146
147
            // if Customer is existing member, associate with current order
148
            if (Member::get()->filter('Email', $order->customer_email)->First()) {
149
                $customer = Member::get()->filter('Email', $order->customer_email)->First();
150
                /* todo: make sure local password is updated if changed on FoxyCart
151
                $this->updatePasswordFromData($customer, $order);
152
                */
153
            } else {
154
                // create new Member, set password info from FoxyCart
155
                $customer = Member::create();
156
                $customer->Customer_ID = (int)$order->customer_id;
157
                $customer->FirstName = (string)$order->customer_first_name;
158
                $customer->Surname = (string)$order->customer_last_name;
159
                $customer->Email = (string)$order->customer_email;
160
                $customer = $this->updatePasswordFromData($customer, $order);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $customer is correct as $this->updatePasswordFromData($customer, $order) targeting Dynamic\FoxyStripe\Contr...pdatePasswordFromData() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
161
            }
162
            $customer->write();
163
            // set Order MemberID
164
            $transaction->MemberID = $customer->ID;
165
        }
166
    }
167
168
    /**
169
     * Updates a customer's password. Sets password encryption to 'none' to avoid encryting it again.
170
     *
171
     * @param Member $customer
172
     * @param $order
173
     */
174
    public function updatePasswordFromData($customer, $order)
175
    {
176
        $query = SQLUpdate::create();
177
        $query->setTable('Member');
178
        $query->addAssignments([
179
            'PasswordEncryption' => $this->getEncryption((string)$order->customer_password_hash_type),
180
            'Password' => (string)$order->customer_password,
181
            'Salt' => (string)$order->customer_password_salt,
182
        ]);
183
        $query->addWhere(['ID != ?' => $customer->ID]);
184
185
        $query->execute();
186
    }
187
188
    /**
189
     * @param string $hashType
190
     * @return string
191
     */
192
    private function getEncryption($hashType)
193
    {
194
        // TODO - update this with new/correct types
195
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing stristr($hashType, 'sha256') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing stristr($hashType, 'bcrypt') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing stristr($hashType, 'sha1') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing stristr($hashType, 'md5') of type string to the boolean true. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
196
            case stristr($hashType, 'sha1'):
197
                return 'sha1_v2.4';
198
            case stristr($hashType, 'sha256'):
199
                return 'sha256';
200
            case stristr($hashType, 'md5'):
201
                return 'md5';
202
            case stristr($hashType, 'bcrypt'):
203
                return 'blowfish';
204
            default:
205
                return 'none';
206
        }
207
    }
208
209
    /**
210
     * @param array $orders
211
     * @param Order $transaction
212
     */
213
    public function parseOrderDetails($orders, $transaction)
214
    {
215
        // remove previous OrderDetails so we don't end up with duplicates
216
        foreach ($transaction->Details() as $detail) {
217
            $detail->delete();
218
        }
219
220
        foreach ($orders->transactions->transaction as $order) {
221
            // Associate ProductPages, Options, Quanity with Order
222
            foreach ($order->transaction_details->transaction_detail as $product) {
223
                $this->orderDetailFromProduct($product, $transaction);
224
            }
225
        }
226
    }
227
228
    /**
229
     * @param $product
230
     * @param $transaction
231
     */
232
    public function orderDetailFromProduct($product, $transaction)
233
    {
234
        $OrderDetail = OrderDetail::create();
235
        $OrderDetail->Quantity = (int)$product->product_quantity;
236
        $OrderDetail->Price = (float)$product->product_price;
237
        // Find product via product_id custom variable
238
239
        foreach ($this->getTransactionOptions($product) as $productID) {
240
            $productPage = $this->getProductPage($product);
241
            $this->modifyOrderDetailPrice($productPage, $OrderDetail, $product);
242
            // associate with this order
243
            $OrderDetail->OrderID = $transaction->ID;
244
            // extend OrderDetail parsing, allowing for recording custom fields from FoxyCart
245
            $this->extend('handleOrderItem', $decrypted, $product, $OrderDetail);
246
            // write
247
            $OrderDetail->write();
248
        }
249
    }
250
251
    /**
252
     * @param $product
253
     * @return \Generator
254
     */
255
    public function getTransactionOptions($product)
256
    {
257
        foreach ($product->transaction_detail_options->transaction_detail_option as $productOption) {
258
            yield $productOption;
259
        }
260
    }
261
262
    /**
263
     * @param $product
264
     * @return bool|ProductPage
265
     */
266
    public function getProductPage($product)
267
    {
268
        foreach ($this->getTransactionOptions($product) as $productOptions) {
269
            if ($productOptions->product_option_name != 'product_id') {
270
                continue;
271
            }
272
273
            return ProductPage::get()
274
                ->filter('ID', (int)$productOptions->product_option_value)
275
                ->First();
276
        }
277
    }
278
279
    /**
280
     * @param bool|ProductPage $OrderProduct
281
     * @param OrderDetail $OrderDetail
282
     */
283
    public function modifyOrderDetailPrice($OrderProduct, $OrderDetail, $product)
284
    {
285
        if (!$OrderProduct) {
286
            return;
287
        }
288
289
        $OrderDetail->ProductID = $OrderProduct->ID;
290
291
        foreach ($this->getTransactionOptions($product) as $option) {
292
            $OptionItem = OptionItem::get()->filter([
293
                'ProductID' => (string)$OrderProduct->ID,
294
                'Title' => (string)$option->product_option_value,
295
            ])->First();
296
297
            if (!$OptionItem) {
298
                continue;
299
            }
300
301
            $OrderDetail->OptionItems()->add($OptionItem);
302
            // modify product price
303
            if ($priceMod = $option->price_mod) {
304
                $OrderDetail->Price += $priceMod;
305
            }
306
        }
307
    }
308
309
    /**
310
     * Single Sign on integration with FoxyCart.
311
     */
312
    public function sso()
313
    {
314
        // GET variables from FoxyCart Request
315
        $fcsid = $this->request->getVar('fcsid');
316
        $timestampNew = strtotime('+30 days');
317
318
        // get current member if logged in. If not, create a 'fake' user with Customer_ID = 0
319
        // fake user will redirect to FC checkout, ask customer to log in
320
        // to do: consider a login/registration form here if not logged in
321
        if ($Member = Security::getCurrentUser()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $Member is dead and can be removed.
Loading history...
322
            $Member = Security::getCurrentUser();
323
        } else {
324
            $Member = new Member();
325
            $Member->Customer_ID = 0;
326
        }
327
328
        $auth_token = sha1($Member->Customer_ID . '|' . $timestampNew . '|' . FoxyCart::getStoreKey());
0 ignored issues
show
Bug introduced by
Are you sure Dynamic\FoxyStripe\Model\FoxyCart::getStoreKey() of type SilverStripe\ORM\FieldType\DBVarchar|false can be used in concatenation? ( Ignorable by Annotation )

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

328
        $auth_token = sha1($Member->Customer_ID . '|' . $timestampNew . '|' . /** @scrutinizer ignore-type */ FoxyCart::getStoreKey());
Loading history...
329
330
        $config = FoxyStripeSetting::current_foxystripe_setting();
331
        if ($config->CustomSSL) {
0 ignored issues
show
Bug Best Practice introduced by
The property CustomSSL does not exist on Dynamic\FoxyStripe\Model\FoxyStripeSetting. Since you implemented __get, consider adding a @property annotation.
Loading history...
332
            $link = FoxyCart::getFoxyCartStoreName();
333
        } else {
334
            $link = FoxyCart::getFoxyCartStoreName() . '.foxycart.com';
335
        }
336
337
        $redirect_complete = 'https://' . $link . '/checkout?fc_auth_token=' . $auth_token . '&fcsid=' . $fcsid .
338
            '&fc_customer_id=' . $Member->Customer_ID . '&timestamp=' . $timestampNew;
339
340
        $this->redirect($redirect_complete);
341
    }
342
}
343