Completed
Push — master ( cfffae...fa5fd4 )
by Nic
05:44
created

src/Controller/FoxyStripeController.php (5 issues)

1
<?php
2
3
namespace Dynamic\FoxyStripe\Controller;
4
5
use Dynamic\FoxyStripe\Model\FoxyCart;
6
use Dynamic\FoxyStripe\Model\FoxyStripeSetting;
7
use Dynamic\FoxyStripe\Model\OptionItem;
8
use Dynamic\FoxyStripe\Model\Order;
9
use Dynamic\FoxyStripe\Model\OrderDetail;
10
use Dynamic\FoxyStripe\Page\ProductPage;
11
use SilverStripe\Security\Member;
12
use SilverStripe\Security\Security;
13
14
class FoxyStripeController extends \PageController
15
{
16
    /**
17
     *
18
     */
19
    const URLSEGMENT = 'foxystripe';
20
    /**
21
     * @var array
22
     */
23
    private static $allowed_actions = array(
24
        'index',
25
        'sso',
26
    );
27
28
    /**
29
     * @return string
30
     */
31
    public function getURLSegment()
32
    {
33
        return self::URLSEGMENT;
34
    }
35
36
    /**
37
     * @return string
38
     *
39
     * @throws \SilverStripe\ORM\ValidationException
40
     */
41
    public function index()
42
    {
43
        // handle POST from FoxyCart API transaction
44
        if ((isset($_POST['FoxyData']) or isset($_POST['FoxySubscriptionData']))) {
45
            $FoxyData_encrypted = (isset($_POST['FoxyData'])) ?
46
                urldecode($_POST['FoxyData']) :
47
                urldecode($_POST['FoxySubscriptionData']);
48
            $FoxyData_decrypted = \rc4crypt::decrypt(FoxyCart::getStoreKey(), $FoxyData_encrypted);
49
50
            // parse the response and save the order
51
            self::handleDataFeed($FoxyData_encrypted, $FoxyData_decrypted);
52
53
            // extend to allow for additional integrations with Datafeed
54
            $this->extend('addIntegrations', $FoxyData_encrypted);
55
56
            return 'foxy';
57
        } else {
58
            return 'No FoxyData or FoxySubscriptionData received.';
59
        }
60
    }
61
62
    /**
63
     * @param $encrypted
64
     * @param $decrypted
65
     *
66
     * @throws \SilverStripe\ORM\ValidationException
67
     */
68
    public function handleDataFeed($encrypted, $decrypted)
69
    {
70
        $orders = new \SimpleXMLElement($decrypted);
71
72
        // loop over each transaction to find FoxyCart Order ID
73
        foreach ($orders->transactions->transaction as $transaction) {
74
            // if FoxyCart order id, then parse order
75
            if (isset($transaction->id)) {
76
                $order = Order::get()->filter('Order_ID', (int)$transaction->id)->First();
77
                if (!$order) {
78
                    $order = Order::create();
79
                }
80
81
                // save base order info
82
                $order->Order_ID = (int)$transaction->id;
83
                $order->Response = urlencode($encrypted);
84
                // first write needed otherwise it creates a duplicates
85
                $order->write();
86
                $this->parseOrder($orders, $order);
87
                $order->write();
88
            }
89
        }
90
    }
91
92
    /**
93
     * @param array $transactions
94
     * @param Order $order
95
     */
96
    public function parseOrder($transactions, $order)
97
    {
98
        $this->parseOrderInfo($transactions, $order);
99
        $this->parseOrderCustomer($transactions, $order);
100
        $this->parseOrderDetails($transactions, $order);
101
    }
102
103
    /**
104
     * @param array $orders
105
     * @param Order $transaction
106
     */
107
    public function parseOrderInfo($orders, $transaction)
108
    {
109
        foreach ($orders->transactions->transaction as $order) {
110
            // Record transaction data from FoxyCart Datafeed:
111
            $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...
112
            $transaction->TransactionDate = (string)$order->transaction_date;
113
            $transaction->ProductTotal = (float)$order->product_total;
114
            $transaction->TaxTotal = (float)$order->tax_total;
115
            $transaction->ShippingTotal = (float)$order->shipping_total;
116
            $transaction->OrderTotal = (float)$order->order_total;
117
            $transaction->ReceiptURL = (string)$order->receipt_url;
118
            $transaction->OrderStatus = (string)$order->status;
119
        }
120
    }
121
122
    /**
123
     * @param array $orders
124
     * @param Order $transaction
125
     * @throws \SilverStripe\ORM\ValidationException
126
     */
127
    public function parseOrderCustomer($orders, $transaction)
128
    {
129
        foreach ($orders->transactions->transaction as $order) {
130
            if (!isset($order->customer_email) || $order->is_anonymous != 0) {
131
                continue;
132
            }
133
134
            // if Customer is existing member, associate with current order
135
            if (Member::get()->filter('Email', $order->customer_email)->First()) {
136
                $customer = Member::get()->filter('Email', $order->customer_email)->First();
137
                /* todo: make sure local password is updated if changed on FoxyCart
138
                $this->updatePasswordFromData($customer, $order);
139
                */
140
            } else {
141
                // create new Member, set password info from FoxyCart
142
                $customer = Member::create();
143
                $customer->Customer_ID = (int)$order->customer_id;
144
                $customer->FirstName = (string)$order->customer_first_name;
145
                $customer->Surname = (string)$order->customer_last_name;
146
                $customer->Email = (string)$order->customer_email;
147
                $this->updatePasswordFromData($customer, $order);
148
            }
149
            $customer->write();
150
            // set Order MemberID
151
            $transaction->MemberID = $customer->ID;
152
        }
153
    }
154
155
    /**
156
     * Updates a customer's password. Sets password encryption to 'none' to avoid encryting it again.
157
     *
158
     * @param Member $customer
159
     * @param $order
160
     */
161
    public function updatePasswordFromData($customer, $order)
162
    {
163
        $password_encryption_algorithm = Security::config()->get('password_encryption_algorithm');
164
        Security::config()->update('password_encryption_algorithm', 'none');
165
166
        $customer->PasswordEncryption = 'none';
167
        $customer->Password = (string) $order->customer_password;
168
        $customer->write();
169
170
        $customer->PasswordEncryption = $this->getEncryption((string) $order->customer_password_hash_type);
171
        $customer->Salt = (string) $order->customer_password_salt;
172
173
        Security::config()->update('password_encryption_algorithm', $password_encryption_algorithm);
174
    }
175
176
    /**
177
     * @param string $hashType
178
     * @return string
179
     */
180
    private function getEncryption($hashType)
181
    {
182
        // TODO - update this with new/correct types
183
        switch (true) {
0 ignored issues
show
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...
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...
184
            case stristr($hashType, 'sha1'):
185
                return 'sha1_v2.4';
186
            case stristr($hashType, 'sha256'):
187
                return 'sha256';
188
            case stristr($hashType, 'md5'):
189
                return 'md5';
190
            case stristr($hashType, 'bcrypt'):
191
                return 'bcrypt';
192
            default:
193
                return 'none';
194
        }
195
    }
196
197
    /**
198
     * @param array $orders
199
     * @param Order $transaction
200
     */
201
    public function parseOrderDetails($orders, $transaction)
202
    {
203
        // remove previous OrderDetails so we don't end up with duplicates
204
        foreach ($transaction->Details() as $detail) {
205
            $detail->delete();
206
        }
207
208
        foreach ($orders->transactions->transaction as $order) {
209
            // Associate ProductPages, Options, Quanity with Order
210
            foreach ($order->transaction_details->transaction_detail as $product) {
211
                $this->orderDetailFromProduct($product, $transaction);
212
            }
213
        }
214
    }
215
216
    /**
217
     * @param $product
218
     * @param $transaction
219
     */
220
    public function orderDetailFromProduct($product, $transaction)
221
    {
222
        $OrderDetail = OrderDetail::create();
223
        $OrderDetail->Quantity = (int)$product->product_quantity;
224
        $OrderDetail->Price = (float)$product->product_price;
225
        // Find product via product_id custom variable
226
227
        foreach ($this->getTransactionOptions($product) as $productID) {
228
            $productPage = $this->getProductPage($product);
229
            $this->modifyOrderDetailPrice($productPage, $OrderDetail, $product);
230
            // associate with this order
231
            $OrderDetail->OrderID = $transaction->ID;
232
            // extend OrderDetail parsing, allowing for recording custom fields from FoxyCart
233
            $this->extend('handleOrderItem', $decrypted, $product, $OrderDetail);
234
            // write
235
            $OrderDetail->write();
236
        }
237
    }
238
239
    /**
240
     * @param $product
241
     * @return \Generator
242
     */
243
    public function getTransactionOptions($product)
244
    {
245
        foreach ($product->transaction_detail_options->transaction_detail_option as $productOption) {
246
            yield $productOption;
247
        }
248
    }
249
250
    /**
251
     * @param $product
252
     * @return bool|ProductPage
253
     */
254
    public function getProductPage($product)
255
    {
256
        foreach ($this->getTransactionOptions($product) as $productOptions) {
257
            if ($productOptions->product_option_name != 'product_id') {
258
                continue;
259
            }
260
261
            return ProductPage::get()
262
                ->filter('ID', (int) $productOptions->product_option_value)
263
                ->First();
264
        }
265
    }
266
267
    /**
268
     * @param bool|ProductPage $OrderProduct
269
     * @param OrderDetail $OrderDetail
270
     */
271
    public function modifyOrderDetailPrice($OrderProduct, $OrderDetail, $product)
272
    {
273
        if (!$OrderProduct) {
274
            return;
275
        }
276
277
        $OrderDetail->ProductID = $OrderProduct->ID;
278
279
        foreach ($this->getTransactionOptions($product) as $option) {
280
            $OptionItem = OptionItem::get()->filter(array(
281
                'ProductID' => (string)$OrderProduct->ID,
282
                'Title' => (string)$option->product_option_value
283
            ))->First();
284
285
            if (!$OptionItem) {
286
                continue;
287
            }
288
289
            $OrderDetail->OptionItems()->add($OptionItem);
290
            // modify product price
291
            if ($priceMod = $option->price_mod) {
292
                $OrderDetail->Price += $priceMod;
293
            }
294
        }
295
    }
296
297
    /**
298
     * Single Sign on integration with FoxyCart.
299
     */
300
    public function sso()
301
    {
302
303
        // GET variables from FoxyCart Request
304
        $fcsid = $this->request->getVar('fcsid');
305
        $timestampNew = strtotime('+30 days');
306
307
        // get current member if logged in. If not, create a 'fake' user with Customer_ID = 0
308
        // fake user will redirect to FC checkout, ask customer to log in
309
        // to do: consider a login/registration form here if not logged in
310
        if ($Member = Security::getCurrentUser()) {
311
            $Member = Security::getCurrentUser();
312
        } else {
313
            $Member = new Member();
314
            $Member->Customer_ID = 0;
315
        }
316
317
        $auth_token = sha1($Member->Customer_ID . '|' . $timestampNew . '|' . FoxyCart::getStoreKey());
318
319
        $config = FoxyStripeSetting::current_foxystripe_setting();
320
        if ($config->CustomSSL) {
321
            $link = FoxyCart::getFoxyCartStoreName();
322
        } else {
323
            $link = FoxyCart::getFoxyCartStoreName() . '.foxycart.com';
324
        }
325
326
        $redirect_complete = 'https://'.$link.'/checkout?fc_auth_token='.$auth_token.'&fcsid='.$fcsid.
327
            '&fc_customer_id='.$Member->Customer_ID.'&timestamp='.$timestampNew;
328
329
        $this->redirect($redirect_complete);
330
    }
331
}
332