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

FoxyStripeController::getURLSegment()   A

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 0
Metric Value
c 0
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
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\Security\Member;
13
use SilverStripe\Security\Security;
14
15
class FoxyStripeController extends \PageController
16
{
17
    /**
18
     *
19
     */
20
    const URLSEGMENT = 'foxystripe';
21
22
    /**
23
     * @var array
24
     */
25
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
26
        'index',
27
        'sso',
28
    ];
29
30
    /**
31
     * @return string
32
     */
33
    public function getURLSegment()
34
    {
35
        return self::URLSEGMENT;
36
    }
37
38
    /**
39
     * @return string
40
     *
41
     * @throws \SilverStripe\ORM\ValidationException
42
     */
43
    public function index()
44
    {
45
        // handle POST from FoxyCart API transaction
46
        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...
47
            $FoxyData_encrypted = (isset($_POST['FoxyData'])) ?
48
                urldecode($_POST['FoxyData']) :
49
                urldecode($_POST['FoxySubscriptionData']);
50
            $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

50
            $FoxyData_decrypted = \rc4crypt::decrypt(/** @scrutinizer ignore-type */ FoxyCart::getStoreKey(), $FoxyData_encrypted);
Loading history...
51
52
            // parse the response and save the order
53
            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

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

88
                $this->parseOrder(/** @scrutinizer ignore-type */ $orders, $order);
Loading history...
89
                $order->write();
90
            }
91
        }
92
    }
93
94
    /**
95
     * @param array $transactions
96
     * @param Order $order
97
     */
98
    public function parseOrder($transactions, $order)
99
    {
100
        $this->parseOrderInfo($transactions, $order);
101
        if (FoxyStripeSetting::current_foxystripe_setting()->UseSingleSignOn && FoxyStripeClient::is_valid()) {
102
            $this->parseOrderCustomer($transactions, $order);
103
        }
104
        $this->parseOrderDetails($transactions, $order);
105
    }
106
107
    /**
108
     * @param array $orders
109
     * @param Order $transaction
110
     */
111
    public function parseOrderInfo($orders, $transaction)
112
    {
113
        foreach ($orders->transactions->transaction as $order) {
114
            // Record transaction data from FoxyCart Datafeed:
115
            $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...
116
            $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...
117
            $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...
118
            $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...
119
            $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...
120
            $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...
121
            $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...
122
            $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...
123
        }
124
    }
125
126
    /**
127
     * @param array $orders
128
     * @param Order $transaction
129
     * @throws \SilverStripe\ORM\ValidationException
130
     */
131
    public function parseOrderCustomer($orders, $transaction)
132
    {
133
        foreach ($orders->transactions->transaction as $order) {
134
            if (!isset($order->customer_email) || $order->is_anonymous != 0) {
135
                continue;
136
            }
137
138
            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...
139
                $customer = Member::create();
140
                $customer->Customer_ID = (int)$order->customer_id;
141
                $customer->FirstName = (string)$order->customer_first_name;
142
                $customer->Surname = (string)$order->customer_last_name;
143
                $customer->Email = (string)$order->customer_email;
144
            }
145
146
            // if Customer is existing member, associate with current order
147
            if (Member::get()->filter('Email', $order->customer_email)->First()) {
148
                $customer = Member::get()->filter('Email', $order->customer_email)->First();
149
                /* todo: make sure local password is updated if changed on FoxyCart
150
                $this->updatePasswordFromData($customer, $order);
151
                */
152
            } else {
153
                // create new Member, set password info from FoxyCart
154
                $customer = Member::create();
155
                $customer->Customer_ID = (int)$order->customer_id;
156
                $customer->FirstName = (string)$order->customer_first_name;
157
                $customer->Surname = (string)$order->customer_last_name;
158
                $customer->Email = (string)$order->customer_email;
159
                $customer = $this->updatePasswordFromData($customer, $order);
160
            }
161
            $customer->write();
162
            // set Order MemberID
163
            $transaction->MemberID = $customer->ID;
164
        }
165
    }
166
167
    /**
168
     * Updates a customer's password. Sets password encryption to 'none' to avoid encryting it again.
169
     *
170
     * @param Member $customer
171
     * @param $order
172
     */
173
    public function updatePasswordFromData($customer, $order)
174
    {
175
        $password_encryption_algorithm = Security::config()->get('password_encryption_algorithm');
176
        Security::config()->update('password_encryption_algorithm', 'none');
177
178
        $customer->PasswordEncryption = 'none';
179
        $customer->Password = (string)$order->customer_password;
180
        $customer->write();
181
182
        $customer->PasswordEncryption = $this->getEncryption((string)$order->customer_password_hash_type);
183
        $customer->Salt = (string)$order->customer_password_salt;
184
185
        $customer->write();
186
187
        Security::config()->update('password_encryption_algorithm', $password_encryption_algorithm);
188
189
        return $customer;
190
    }
191
192
    /**
193
     * @param string $hashType
194
     * @return string
195
     */
196
    private function getEncryption($hashType)
197
    {
198
        // TODO - update this with new/correct types
199
        switch (true) {
0 ignored issues
show
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, '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, '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, '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...
200
            case stristr($hashType, 'sha1'):
201
                return 'sha1_v2.4';
202
            case stristr($hashType, 'sha256'):
203
                return 'sha256';
204
            case stristr($hashType, 'md5'):
205
                return 'md5';
206
            case stristr($hashType, 'bcrypt'):
207
                return 'blowfish';
208
            default:
209
                return 'none';
210
        }
211
    }
212
213
    /**
214
     * @param array $orders
215
     * @param Order $transaction
216
     */
217
    public function parseOrderDetails($orders, $transaction)
218
    {
219
        // remove previous OrderDetails so we don't end up with duplicates
220
        foreach ($transaction->Details() as $detail) {
221
            $detail->delete();
222
        }
223
224
        foreach ($orders->transactions->transaction as $order) {
225
            // Associate ProductPages, Options, Quanity with Order
226
            foreach ($order->transaction_details->transaction_detail as $product) {
227
                $this->orderDetailFromProduct($product, $transaction);
228
            }
229
        }
230
    }
231
232
    /**
233
     * @param $product
234
     * @param $transaction
235
     */
236
    public function orderDetailFromProduct($product, $transaction)
237
    {
238
        $OrderDetail = OrderDetail::create();
239
        $OrderDetail->Quantity = (int)$product->product_quantity;
240
        $OrderDetail->Price = (float)$product->product_price;
241
        // Find product via product_id custom variable
242
243
        foreach ($this->getTransactionOptions($product) as $productID) {
244
            $productPage = $this->getProductPage($product);
245
            $this->modifyOrderDetailPrice($productPage, $OrderDetail, $product);
246
            // associate with this order
247
            $OrderDetail->OrderID = $transaction->ID;
248
            // extend OrderDetail parsing, allowing for recording custom fields from FoxyCart
249
            $this->extend('handleOrderItem', $decrypted, $product, $OrderDetail);
250
            // write
251
            $OrderDetail->write();
252
        }
253
    }
254
255
    /**
256
     * @param $product
257
     * @return \Generator
258
     */
259
    public function getTransactionOptions($product)
260
    {
261
        foreach ($product->transaction_detail_options->transaction_detail_option as $productOption) {
262
            yield $productOption;
263
        }
264
    }
265
266
    /**
267
     * @param $product
268
     * @return bool|ProductPage
269
     */
270
    public function getProductPage($product)
271
    {
272
        foreach ($this->getTransactionOptions($product) as $productOptions) {
273
            if ($productOptions->product_option_name != 'product_id') {
274
                continue;
275
            }
276
277
            return ProductPage::get()
278
                ->filter('ID', (int)$productOptions->product_option_value)
279
                ->First();
280
        }
281
    }
282
283
    /**
284
     * @param bool|ProductPage $OrderProduct
285
     * @param OrderDetail $OrderDetail
286
     */
287
    public function modifyOrderDetailPrice($OrderProduct, $OrderDetail, $product)
288
    {
289
        if (!$OrderProduct) {
290
            return;
291
        }
292
293
        $OrderDetail->ProductID = $OrderProduct->ID;
294
295
        foreach ($this->getTransactionOptions($product) as $option) {
296
            $OptionItem = OptionItem::get()->filter([
297
                'ProductID' => (string)$OrderProduct->ID,
298
                'Title' => (string)$option->product_option_value,
299
            ])->First();
300
301
            if (!$OptionItem) {
302
                continue;
303
            }
304
305
            $OrderDetail->OptionItems()->add($OptionItem);
306
            // modify product price
307
            if ($priceMod = $option->price_mod) {
308
                $OrderDetail->Price += $priceMod;
309
            }
310
        }
311
    }
312
313
    /**
314
     * Single Sign on integration with FoxyCart.
315
     */
316
    public function sso()
317
    {
318
        // GET variables from FoxyCart Request
319
        $fcsid = $this->request->getVar('fcsid');
320
        $timestampNew = strtotime('+30 days');
321
322
        // get current member if logged in. If not, create a 'fake' user with Customer_ID = 0
323
        // fake user will redirect to FC checkout, ask customer to log in
324
        // to do: consider a login/registration form here if not logged in
325
        if ($Member = Security::getCurrentUser()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $Member is dead and can be removed.
Loading history...
326
            $Member = Security::getCurrentUser();
327
        } else {
328
            $Member = new Member();
329
            $Member->Customer_ID = 0;
330
        }
331
332
        $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

332
        $auth_token = sha1($Member->Customer_ID . '|' . $timestampNew . '|' . /** @scrutinizer ignore-type */ FoxyCart::getStoreKey());
Loading history...
333
334
        $config = FoxyStripeSetting::current_foxystripe_setting();
335
        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...
336
            $link = FoxyCart::getFoxyCartStoreName();
337
        } else {
338
            $link = FoxyCart::getFoxyCartStoreName() . '.foxycart.com';
339
        }
340
341
        $redirect_complete = 'https://' . $link . '/checkout?fc_auth_token=' . $auth_token . '&fcsid=' . $fcsid .
342
            '&fc_customer_id=' . $Member->Customer_ID . '&timestamp=' . $timestampNew;
343
344
        $this->redirect($redirect_complete);
345
    }
346
}
347