WebToPay   F
last analyzed

Complexity

Total Complexity 162

Size/Duplication

Total Lines 1136
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 1136
ccs 0
cts 679
cp 0
rs 1.0559
c 0
b 0
f 0
wmc 162
lcom 2
cbo 1

35 Methods

Rating   Name   Duplication   Size   Complexity  
A toggleSS2() 0 3 1
A throwResponseError() 0 27 2
A getRequestSpec() 0 41 1
A getRepeatRequestSpec() 0 20 1
A getMakroResponseSpec() 0 35 1
A getMikroResponseSpec() 0 24 1
C checkRequestData() 0 39 12
B signRequest() 0 18 7
A buildRequest() 0 8 1
A buildRepeatRequest() 0 9 1
A getCert() 0 3 1
A checkResponseCert() 0 25 5
C checkResponseData() 0 69 16
A useSS2() 0 3 1
A checkSS1v2() 0 23 5
A getPaymentMethods() 0 16 3
A getXML() 0 12 2
A getPaymentUrl() 0 7 2
A loadXML() 0 13 2
C parseXML() 0 56 13
A toBaseCurrency() 0 16 5
A checkMinAmount() 0 13 4
A checkMaxAmount() 0 13 4
D filterPayMethods() 0 59 20
A getSpecsForResponse() 0 18 5
A getPrefixed() 0 10 5
B getUrlContent() 0 42 6
C checkResponse() 0 72 15
A responseToLog() 0 8 2
A mikroResponseToLog() 0 8 2
A makroResponseToLog() 0 8 2
A mikroAnswerToLog() 0 8 2
B log() 0 43 5
A smsAnswer() 0 31 5
A _() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like WebToPay 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 WebToPay, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * BoxBilling
4
 *
5
 * @copyright BoxBilling, Inc (http://www.boxbilling.com)
6
 * @license   Apache-2.0
7
 *
8
 * Copyright BoxBilling, Inc
9
 * This source file is subject to the Apache-2.0 License that is bundled
10
 * with this source code in the file LICENSE
11
 */
12
13
class Payment_Adapter_WebToPay extends Payment_AdapterAbstract
14
{
15
    public function init()
16
    {
17
        if(!$this->getParam('projectid')) {
18
            throw new Payment_Exception('Payment gateway "WebToPay" is not configured properly. Please update configuration parameter "Project ID" at "Configuration -> Payments".');
19
        }
20
21
        if(!$this->getParam('sign_password')) {
22
            throw new Payment_Exception('Payment gateway "WebToPay" is not configured properly. Please update configuration parameter "Sign Password" at "Configuration -> Payments".');
23
        }
24
    }
25
26
	/**
27
	 * Return gateway type
28
	 *
29
	 * @return string
30
	*/
31
    public function getType()
32
    {
33
        return Payment_AdapterAbstract::TYPE_FORM;
34
    }
35
36
    public static function getConfig()
37
    {
38
        return array(
39
            'supports_one_time_payments'   =>  true,
40
            'supports_subscriptions'     =>  false,
41
            'description'     =>  'WebToPay gateway. More information at www.webtopay.com',
42
            'form'  => array(
43
                'projectid' => array('text', array(
44
                            'label' => 'Project ID',
45
                            'value' => '',
46
                    ),
47
                 ),
48
                'sign_password' => array('password', array(
49
                            'label' => 'Sign Password',
50
                            'value' => '',
51
                    ),
52
                 ),
53
            ),
54
        );
55
    }
56
57
	/**
58
	 * Return service call url
59
	 *
60
	 * @return string
61
	*/
62
    public function getServiceUrl()
63
    {
64
        return WebToPay::PAY_URL;
65
    }
66
67
	/**
68
	 * Init single payment call to webservice
69
	 * Invoice id is passed via notify_url
70
     *
71
	 * @return string
72
	*/
73
    public function singlePayment(Payment_Invoice $invoice)
74
    {
75
        $buyer = $invoice->getBuyer();
76
        return WebToPay::buildRequest(array(
77
                'projectid'     => $this->getParam('projectid'),
78
                'sign_password' => $this->getParam('sign_password'),
79
                'orderid'       => $invoice->getNumber(),
80
                'amount'        => $this->moneyFormat($invoice->getTotalWithTax()),
81
                'currency'      => $invoice->getCurrency(),
82
                'accepturl'     => $this->getParam('return_url'),
83
                'cancelurl'     => $this->getParam('cancel_url'),
84
                'callbackurl'   => $this->getParam('notify_url'),
85
                'paytext'       => $invoice->getTitle(),
86
87
                'p_firstname'   => $buyer->getFirstName(),
88
                'p_lastname'    => $buyer->getLastName(),
89
                'p_email'       => $buyer->getEmail(),
90
                'p_street'      => $buyer->getAddress(),
91
                'p_city'        => $buyer->getCity(),
92
                'p_state'       => $buyer->getState(),
93
                'p_zip'         => $buyer->getZip(),
94
                'p_countrycode' => $buyer->getCountry(),
95
96
                'lang'          => 'ENG',
97
                'test'          => $this->testMode,
98
            ));
99
    }
100
101
	/**
102
	 * Init recurrent payment call to webservice
103
	 *
104
	 * @return mixed
105
	*/
106
    public function recurrentPayment(Payment_Invoice $invoice)
107
    {
108
        throw new Payment_Exception('WebToPay payment gateway do not support recurrent payments');
109
    }
110
111
    /**
112
     * Handle IPN and return response object
113
     * @return Payment_Transaction
114
     */
115
    public function getTransaction($data, Payment_Invoice $invoice)
116
    {
117
        $ipn = $data['get'];
118
119
        $tx = new Payment_Transaction();
120
        $tx->setId($ipn['wp_requestid']);
121
        $tx->setAmount($ipn['wp_payamount'] / 100);
122
        $tx->setCurrency($ipn['wp_paycurrency']);
123
        $tx->setType(Payment_Transaction::TXTYPE_PAYMENT);
124
125
        if($ipn['wp_status'] == 1) {
126
            $tx->setStatus(Payment_Transaction::STATUS_COMPLETE);
127
        }
128
129
        if($ipn['wp_status'] == 2) {
130
            $tx->setStatus(Payment_Transaction::STATUS_PENDING);
131
        }
132
133
        return $tx;
134
    }
135
136
    public function isIpnValid($data, Payment_Invoice $invoice)
137
    {
138
        $ipn = $data['get'];
139
        try{
140
            WebToPay::checkResponse($ipn, array(
141
                'projectid'     => $this->getParam('projectid'),
142
                'sign_password' => $this->getParam('sign_password'),
143
            ));
144
            $this->setOutput('OK');
145
            return true;
146
        } catch (WebToPayException $e) {
147
            error_log($e->getMessage());
148
            $this->setOutput('ERR');
149
            return false;
150
        }
151
    }
152
153
    /**
154
     * @param double $amount
155
     */
156
    public function moneyFormat($amount, $currency = null)
157
    {
158
        return $amount * 100;
159
    }
160
}
161
162
/**
163
 * PHP Library for WebToPay provided services.
164
 * Copyright (C) 2010  http://www.webtopay.com/
165
 *
166
 * This program is free software: you can redistribute it and/or modify it
167
 * under the terms of the GNU Lesser General Public License as published by the
168
 * Free Software Foundation, either version 3 of the License, or (at your
169
 * option) any later version.
170
 *
171
 * This program is distributed in the hope that it will be useful, but WITHOUT
172
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
173
 * FITNESS FOR A PARTICULAR PURPOSE.
174
 * See the GNU Lesser General Public License for more details.
175
 *
176
 * You should have received a copy of the GNU Lesser General Public License
177
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
178
 *
179
 * @package    WebToPay
180
 * @author     EVP International
181
 * @license    http://www.gnu.org/licenses/lgpl.html
182
 * @version    1.5
183
 * @link       http://www.webtopay.com/
184
 */
185
class WebToPay {
186
187
    /**
188
     * WebToPay Library version.
189
     */
190
    const VERSION = '1.5';
191
192
    /**
193
     * Server URL where all requests should go.
194
     */
195
    const PAY_URL = 'https://www.mokejimai.lt/pay/';
196
197
    /**
198
     * Server URL where we can get XML with payment method data.
199
     */
200
    const XML_URL = 'https://www.mokejimai.lt/new/lt/lib_web_to_pays/api/';
201
202
    /**
203
     * SMS answer url.
204
     */
205
    const SMS_ANSWER_URL = 'https://www.mokejimai.lt/psms/respond/';
206
207
    /**
208
     * Prefix for callback data.
209
     */
210
    const PREFIX = 'wp_';
211
212
    /**
213
     * Cache file.
214
     */
215
    const CACHE_URL = 'cache.php';
216
217
218
    /**
219
     * Identifies what verification method was used.
220
     *
221
     * Values can be:
222
     *  - false     not verified
223
     *  - RESPONSE  only response parameters are verified
224
     *  - SS1v2     SS1 v2 verification
225
     *  - SS2       SS2 verification
226
     */
227
    public static $verified = false;
228
229
230
    /**
231
     * If true, check SS2 if false, skip to SS1
232
     *
233
     * @deprecated
234
     */
235
    private static $SS2 = true;
236
237
    /**
238
     * Toggle SS2 checking. Usualy you don't need to use this method, because
239
     * by default first SS2 support are checked and if it doesn't work,
240
     * fallback to SS1.
241
     *
242
     * Use this method if your server supports SS2, but you want to use SS1.
243
     *
244
     * @deprecated
245
     */
246
    public static function toggleSS2($value) {
247
        self::$SS2 = (bool) $value;
248
    }
249
250
251
    /**
252
     * Throw exception.
253
     *
254
     * @param  string $code
255
     * @return void
256
     */
257
    public static function throwResponseError($code) {
258
        $errors = array(
259
            '0x1'   => self::_('Payment amount is too small.'),
260
            '0x2'   => self::_('Payment amount is too big.'),
261
            '0x3'   => self::_('Selected currency is not available.'),
262
            '0x4'   => self::_('Amount or currency is missing.'),
263
            '0x6'   => self::_('projectId is missing or such ID does not exist.'),
264
            '0x7'   => self::_('Testing mode is turned off, but you have still tried to make a test payment.'),
265
            '0x8'   => self::_('You have banned this way of payment.'),
266
            '0x9'   => self::_('Coding of variable "paytext" is not suitable (has to be utf-8).'),
267
            '0x10'  => self::_('Empty or not correctly filled "orderID".'),
268
            '0x11'  => self::_('Project has not been checked by our administrator.'),
269
            '0x13'  => self::_('Accepturl, cancellurl, callbacurl or referer base address differs from the addresses confirmed in the project.'),
270
            '0x14'  => self::_('Invalid "sign" parameter.'),
271
            '0x15x0'  => self::_('At least one of these parameters is incorrect: cancelurl, accepturl, callbackurl.'),
272
            '0x15x1'  => self::_('Parameter time_limit is not valid (wrong format or not valid value)'),
273
        );
274
275
        if (isset($errors[$code])) {
276
            $msg = $errors[$code];
277
        }
278
        else {
279
            $msg = self::_('Unknown error');
280
        }
281
282
        throw new WebToPayException($msg);
283
    }
284
285
286
    /**
287
     * Returns specification array for request.
288
     *
289
     * @return array
290
     */
291
    public static function getRequestSpec() {
292
        // Array structure:
293
        //  * name      – request item name.
294
        //  * maxlen    – max allowed value for item.
295
        //  * required  – is this item is required.
296
        //  * user      – if true, user can set value of this item, if false
297
        //                item value is generated.
298
        //  * isrequest – if true, item will be included in request array, if
299
        //                false, item only be used internaly and will not be
300
        //                included in outgoing request array.
301
        //  * regexp    – regexp to test item value.
302
        return array(
303
            array('projectid',      11,     true,   true,   true,   '/^\d+$/'),
304
            array('orderid',        40,     true,   true,   true,   ''),
305
            array('lang',           3,      false,  true,   true,   '/^[a-z]{3}$/i'),
306
            array('amount',         11,     false,  true,   true,   '/^\d+$/'),
307
            array('currency',       3,      false,  true,   true,   '/^[a-z]{3}$/i'),
308
            array('accepturl',      255,    true,   true,   true,   ''),
309
            array('cancelurl',      255,    true,   true,   true,   ''),
310
            array('callbackurl',    255,    true,   true,   true,   ''),
311
            array('payment',        20,     false,  true,   true,   ''),
312
            array('country',        2,      false,  true,   true,   '/^[a-z_]{2}$/i'),
313
            array('paytext',        255,    false,  true,   true,   ''),
314
            array('p_firstname',    255,    false,  true,   true,   ''),
315
            array('p_lastname',     255,    false,  true,   true,   ''),
316
            array('p_email',        255,    false,  true,   true,   ''),
317
            array('p_street',       255,    false,  true,   true,   ''),
318
            array('p_city',         255,    false,  true,   true,   ''),
319
            array('p_state',        20,     false,  true,   true,   ''),
320
            array('p_zip',          20,     false,  true,   true,   ''),
321
            array('p_countrycode',  2,      false,  true,   true,   '/^[a-z]{2}$/i'),
322
            array('sign',           255,    true,   false,  true,   ''),
323
            array('sign_password',  255,    true,   true,   false,  ''),
324
            array('only_payments',  0,      false,  true,   true,   ''),
325
            array('disalow_payments', 0,    false,  true,   true,   ''),
326
            array('repeat_request', 1,      false,  false,  true,   '/^[01]$/'),
327
            array('test',           1,      false,  true,   true,   '/^[01]$/'),
328
            array('version',        9,      true,   false,  true,   '/^\d+\.\d+$/'),
329
            array('time_limit',     19,     false,  true,   true,   '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'),
330
        );
331
    }
332
333
334
    /**
335
     * Returns specification array for repeat request.
336
     *
337
     * @return array
338
     */
339
    public static function getRepeatRequestSpec() {
340
        // Array structure:
341
        //  * name      – request item name.
342
        //  * maxlen    – max allowed value for item.
343
        //  * required  – is this item is required.
344
        //  * user      – if true, user can set value of this item, if false
345
        //                item value is generated.
346
        //  * isrequest – if true, item will be included in request array, if
347
        //                false, item only be used internaly and will not be
348
        //                included in outgoing request array.
349
        //  * regexp    – regexp to test item value.
350
        return array(
351
            array('projectid',      11,     true,   true,   true,   '/^\d+$/'),
352
            array('requestid',      40,     true,   true,   true,   ''),
353
            array('sign',           255,    true,   false,  true,   ''),
354
            array('sign_password',  255,    true,   true,   false,  ''),
355
            array('repeat_request', 1,      true,   false,  true,   '/^1$/'),
356
            array('version',        9,      true,   false,  true,   '/^\d+\.\d+$/'),
357
        );
358
    }
359
360
361
    /**
362
     * Returns specification array for makro response.
363
     *
364
     * @return array
365
     */
366
    public static function getMakroResponseSpec() {
367
        // Array structure:
368
        //  * name       – request item name.
369
        //  * maxlen     – max allowed value for item.
370
        //  * required   – is this item is required in response.
371
        //  * mustcheck  – this item must be checked by user.
372
        //  * isresponse – if false, item must not be included in response array.
373
        //  * regexp     – regexp to test item value.
374
        return array(
375
            'projectid'     => array(11,     true,   true,   true,  '/^\d+$/'),
376
            'orderid'       => array(40,     false,  false,  true,  ''),
377
            'lang'          => array(3,      false,  false,  true,  '/^[a-z]{3}$/i'),
378
            'amount'        => array(11,     false,  false,  true,  '/^\d+$/'),
379
            'currency'      => array(3,      false,  false,  true,  '/^[a-z]{3}$/i'),
380
            'payment'       => array(20,     false,  false,  true,  ''),
381
            'country'       => array(2,      false,  false,  true,  '/^[a-z_]{2}$/i'),
382
            'paytext'       => array(0,      false,  false,  true,  ''),
383
            '_ss2'          => array(0,      true,   false,  true,  ''),
384
            '_ss1v2'        => array(0,      false,  false,  true,  ''),
385
            'name'          => array(255,    false,  false,  true,  ''),
386
            'surename'      => array(255,    false,  false,  true,  ''),
387
            'status'        => array(255,    false,  false,  true,  ''),
388
            'error'         => array(20,     false,  false,  true,  ''),
389
            'test'          => array(1,      false,  false,  true,  '/^[01]$/'),
390
391
            'p_email'       => array(0,      false,  false,  true,  ''),
392
            'requestid'     => array(40,     false,  false,  true,  ''),
393
            'payamount'     => array(0,      false,  false,  true,  ''),
394
            'paycurrency'   => array(0,      false,  false,  true,  ''),
395
396
            'version'       => array(9,      true,   false,  true,  '/^\d+\.\d+$/'),
397
398
            'sign_password' => array(255,    false,  true,   false, ''),
399
        );
400
    }
401
402
403
404
    /**
405
     * Returns specification array for mikro response.
406
     *
407
     * @return array
408
     */
409
    public static function getMikroResponseSpec() {
410
        // Array structure:
411
        //  * name       – request item name.
412
        //  * maxlen     – max allowed value for item.
413
        //  * required   – is this item is required in response.
414
        //  * mustcheck  – this item must be checked by user.
415
        //  * isresponse – if false, item must not be included in response array.
416
        //  * regexp     – regexp to test item value.
417
        return array(
418
            'to'            => array(0,      true,   false,  true,  ''),
419
            'sms'           => array(0,      true,   false,  true,  ''),
420
            'from'          => array(0,      true,   false,  true,  ''),
421
            'operator'      => array(0,      true,   false,  true,  ''),
422
            'amount'        => array(0,      true,   false,  true,  ''),
423
            'currency'      => array(0,      true,   false,  true,  ''),
424
            'country'       => array(0,      true,   false,  true,  ''),
425
            'id'            => array(0,      true,   false,  true,  ''),
426
            '_ss2'          => array(0,      true,   false,  true,  ''),
427
            '_ss1v2'        => array(0,      true,   false,  true,  ''),
428
            'test'          => array(0,      true,   false,  true,  ''),
429
            'key'           => array(0,      true,   false,  true,  ''),
430
            //'version'       => array(9,      true,   false,  true,  '/^\d+\.\d+$/'),
431
        );
432
    }
433
434
    /**
435
     * Checks user given request data array.
436
     *
437
     * If any errors occurs, WebToPayException will be raised.
438
     *
439
     * This method returns validated request array. Returned array contains
440
     * only those items from $data, that are needed.
441
     *
442
     * @param  array $data
443
     * @return array
444
     */
445
    public static function checkRequestData($data, $specs) {
446
        $request = array();
447
        foreach ($specs as $spec) {
448
            list($name, $maxlen, $required, $user, $isrequest, $regexp) = $spec;
449
            if (!$user) continue;
450
            if ($required && !isset($data[$name])) {
451
                $e = new WebToPayException(
452
                    self::_("'%s' is required but missing.", $name),
453
                    WebToPayException::E_MISSING);
454
                $e->setField($name);
455
                throw $e;
456
            }
457
458
            if (!empty($data[$name])) {
459
                if ($maxlen && strlen($data[$name]) > $maxlen) {
460
                    $e = new WebToPayException(
461
                        self::_("'%s' value '%s' is too long, %d characters allowed.",
462
                                $name, $data[$name], $maxlen),
463
                        WebToPayException::E_MAXLEN);
464
                    $e->setField($name);
465
                    throw $e;
466
                }
467
468
                if ('' != $regexp && !preg_match($regexp, $data[$name])) {
469
                    $e = new WebToPayException(
470
                        self::_("'%s' value '%s' is invalid.", $name, $data[$name]),
471
                        WebToPayException::E_REGEXP);
472
                    $e->setField($name);
473
                    throw $e;
474
                }
475
            }
476
477
            if ($isrequest && isset($data[$name])) {
478
                $request[$name] = $data[$name];
479
            }
480
        }
481
482
        return $request;
483
    }
484
485
486
    /**
487
     * Puts signature on request data array.
488
     *
489
     * @param  array   $specification
490
     * @param  string  $request
491
     * @param  string  $password
492
     * @return string
493
     */
494
    public static function signRequest($specification, $request, $password) {
495
        $fields = array();
496
        foreach ($specification as $field) {
497
            if ($field[4] && $field[0] != 'sign') {
498
                $fields[] = $field[0];
499
            }
500
        }
501
502
        $data = '';
503
        foreach ($fields as $key) {
504
            if (isset($request[$key]) && trim($request[$key]) != '') {
505
                $data .= $request[$key];
506
            }
507
        }
508
        $request['sign'] = md5($data . $password);
509
510
        return $request;
511
    }
512
513
514
    /**
515
     * Builds request data array.
516
     *
517
     * This method checks all given data and generates correct request data
518
     * array or raises WebToPayException on failure.
519
     *
520
     * Method accepts single parameter $data of array type. All possible array
521
     * keys are described here:
522
     * https://www.mokejimai.lt/makro_specifikacija.html
523
     *
524
     * @param  array $data Information about current payment request.
525
     * @return string
526
     */
527
    public static function buildRequest($data) {
528
        $specs = self::getRequestSpec();
529
        $request = self::checkRequestData($data, $specs);
530
        $version = explode('.', self::VERSION);
531
        $request['version'] = $version[0].'.'.$version[1];
532
        $request = self::signRequest($specs, $request, $data['sign_password']);
533
        return $request;
534
    }
535
536
537
    /**
538
     * Builds repeat request data array.
539
     *
540
     * This method checks all given data and generates correct request data
541
     * array or raises WebToPayException on failure.
542
     *
543
     * Method accepts single parameter $data of array type. All possible array
544
     * keys are described here:
545
     * https://www.mokejimai.lt/makro_specifikacija.html
546
     *
547
     * @param  array $data Information about current payment request.
548
     * @return string
549
     */
550
    public static function buildRepeatRequest($data) {
551
        $specs = self::getRepeatRequestSpec();
552
        $request = self::checkRequestData($data, $specs);
553
        $request['repeat_request'] = '1';
554
        $version = explode('.', self::VERSION);
555
        $request['version'] = $version[0].'.'.$version[1];
556
        $request = self::signRequest($specs, $request, $data['sign_password']);
557
        return $request;
558
    }
559
560
    /**
561
     * Download certificate from webtopay.com.
562
     *
563
     * @param  string $cert
564
     * @return string
565
     */
566
    public static function getCert($cert) {
567
        return self::getUrlContent('http://downloads.webtopay.com/download/'.$cert);
568
    }
569
570
    /**
571
     * Check is response certificate is valid
572
     *
573
     * @param  array $response
574
     * @param  string $cert
575
     * @return bool
576
     */
577
    public static function checkResponseCert($response, $cert='public.key') {
578
        $pKeyP = self::getCert($cert);
579
        if (!$pKeyP) {
580
            throw new WebToPayException(
581
                self::_('Can\'t get openssl public key for %s', $cert),
582
                WebToPayException::E_INVALID);
583
        }
584
585
        $_SS2 = '';
586
        foreach ($response as $key => $value) {
587
            if (in_array($key, array('_ss1v2', '_ss2'))) {
588
                continue;
589
            }
590
            $_SS2 .= "{$value}|";
591
        }
592
        $ok = openssl_verify($_SS2, base64_decode($response['_ss2']), $pKeyP);
593
594
        if ($ok !== 1) {
595
            throw new WebToPayException(
596
                self::_('Can\'t verify SS2 for %s', $cert),
597
                WebToPayException::E_INVALID);
598
        }
599
600
        return true;
601
    }
602
603
    public static function checkResponseData($response, $mustcheck_data, $specs) {
604
        $resp_keys = array();
605
        foreach ($specs as $name => $spec) {
606
            list($maxlen, $required, $mustcheck, $is_response, $regexp) = $spec;
607
            if ($required && !isset($response[$name])) {
608
                $e = new WebToPayException(
609
                    self::_("'%s' is required but missing.", $name),
610
                    WebToPayException::E_MISSING);
611
                $e->setField($name);
612
                throw $e;
613
            }
614
615
            if ($mustcheck) {
616
                if (!isset($mustcheck_data[$name])) {
617
                    $e = new WebToPayException(
618
                        self::_("'%s' must exists in array of second parameter ".
619
                                "of checkResponse() method.", $name),
620
                        WebToPayException::E_USER_PARAMS);
621
                    $e->setField($name);
622
                    throw $e;
623
                }
624
625
                if ($is_response) {
626
                    if ($response[$name] != $mustcheck_data[$name]) {
627
                        $e = new WebToPayException(
628
                            self::_("'%s' yours and requested value is not ".
629
                                    "equal ('%s' != '%s') ",
630
                                    $name, $mustcheck_data[$name], $response[$name]),
631
                            WebToPayException::E_INVALID);
632
                        $e->setField($name);
633
                        throw $e;
634
                    }
635
                }
636
            }
637
638
            if (!empty($response[$name])) {
639
                if ($maxlen && strlen($response[$name]) > $maxlen) {
640
                    $e = new WebToPayException(
641
                        self::_("'%s' value '%s' is too long, %d characters allowed.",
642
                                $name, $response[$name], $maxlen),
643
                        WebToPayException::E_MAXLEN);
644
                    $e->setField($name);
645
                    throw $e;
646
                }
647
648
                if ('' != $regexp && !preg_match($regexp, $response[$name])) {
649
                    $e = new WebToPayException(
650
                        self::_("'%s' value '%s' is invalid.", $name, $response[$name]),
651
                        WebToPayException::E_REGEXP);
652
                    $e->setField($name);
653
                    throw $e;
654
                }
655
            }
656
657
            if (isset($response[$name])) {
658
                $resp_keys[] = $name;
659
            }
660
        }
661
662
        // Filter only parameters passed from webtopay
663
        $_response = array();
664
        foreach (array_keys($response) as $key) {
665
            if (in_array($key, $resp_keys)) {
666
                $_response[$key] = $response[$key];
667
            }
668
        }
669
670
        return $_response;
671
    }
672
673
674
    /**
675
     * Check if SS2 checking is available and enabled.
676
     *
677
     * @return bool
678
     */
679
    public static function useSS2() {
680
        return function_exists('openssl_pkey_get_public');
681
    }
682
683
684
    /**
685
     * Check for SS1, which is not depend on openssl functions.
686
     *
687
     * @param  array  $response
688
     * @param  string $password
689
     * @return bool
690
     */
691
    public static function checkSS1v2($response, $password) {
692
        if (32 != strlen($password)) {
693
            $password = md5($password);
694
        }
695
696
        $buffer = array($password);
697
        foreach ($response as $key => $value) {
698
            if (in_array($key, array('_ss1v2', '_ss2'))) {
699
                continue;
700
            }
701
            $buffer[] = $value;
702
        }
703
704
        $ss1v2 = md5(implode('|', $buffer));
705
706
        if ($response['_ss1v2'] != $ss1v2) {
707
            throw new WebToPayException(
708
                self::_('Can\'t verify SS1 v2'),
709
                WebToPayException::E_INVALID);
710
        }
711
712
        return true;
713
    }
714
715
    /**
716
     * Filters saved payment method cache by e-shop's order sum and language
717
     *
718
     * @param string    $payCurrency
719
     * @param int       $sum
720
     * @param array     $currency           array ( '0' => array ('iso' => 'USD', 'rate' => 0.417391, ),);
721
     * @param string    $lang
722
     * @param int       $projectID
723
     * @return array    $filtered
724
     */
725
    public static function getPaymentMethods($payCurrency, $sum, $currency, $lang, $projectID) {
726
727
        $filtered    = array();
728
        $data        = self::loadXML();
729
        $lang        = strtolower($lang);
730
731
        //jei xml senesnis nei para
732
        if((($data['ts']+3600*24) - time()) < 0 || $data == null) {
733
            self::getXML($projectID); //siunciam nauja
734
            $data = self::loadXML();  //vel uzloadinam
735
        }
736
737
        $filtered = self::filterPayMethods($data['data'], $payCurrency, $sum, $currency, $lang);
738
739
        return $filtered;
740
    }
741
742
743
    /**
744
     * Downloads xml data from webtopay.com
745
     *
746
     * @param int       $projectID
747
     * @return boolean
748
     */
749
    public static function getXML($projectID) {
750
        $response = self::getUrlContent(self::XML_URL.$projectID.'/');
751
        $feed     = simplexml_load_string($response);
752
753
        $feed = simplexml_load_string($response);
754
        if($feed === false){
755
            return false;
756
        } else {
757
            self::parseXML($feed);
758
            return true;
759
        }
760
    }
761
762
763
    /**
764
     * Returns payment url
765
     *
766
     * @param  string $language
767
     * @return string $url
768
     */
769
    public static function getPaymentUrl($language) {
770
        $url = self::PAY_URL;
771
        if($language != 'LT') {
772
           $url = str_replace('mokejimai.lt', 'webtopay.com', $url);
773
        }
774
        return $url;
775
    }
776
777
    /**
778
     * Loads and unserializes xml data from file
779
     *
780
     * @return array $data
781
     */
782
    private static function loadXML() {
783
784
        $data   = array();
785
786
        if (file_exists(self::CACHE_URL)) {
787
            $fh     = fopen(self::CACHE_URL, 'r');
788
            $data   = unserialize(fread($fh,filesize(self::CACHE_URL)));
789
            fclose($fh);
790
            return $data;
791
        } else {
792
            return null;
793
        }
794
    }
795
796
    /**
797
     * Parses xml to array, serializes it and writes it to file CACHE_URL
798
     *
799
     * @param SimpleXMLElement   $xml
800
     */
801
    public static function parseXML($xml){
802
803
        $paydata    = array();
804
        $parsed     = array();
805
        $language   = array();
806
        $logo       = array();
807
        $qouta      = array();
808
        $title      = array();
809
        $cache      = array();
810
811
        foreach($xml->country as $country){
812
            $countries  = strtolower(trim((string)$country->attributes()));
813
            foreach($country->payment_group as $group){
814
                $groups = strtolower(trim((string)$group->attributes()));
815
                foreach($group->title as $tit => $v){
816
                    $language[strtolower(trim((string)$v->attributes()))] = trim((string)$v);
817
                }
818
                $parsed[$countries][$groups]['translate'] = $language;
819
                foreach($group->payment_type as $type) {
820
                    $types = strtolower(trim((string)$type->attributes()));
821
                    foreach($type as $key => $value) {
822
                        if($key === 'logo_url'){
823
                            $logo[trim((string)$value->attributes())] = trim((string)$value);
824
                        }
825
                        if($key === 'title'){
826
                            $title[trim((string)$value->attributes())] = trim((string)$value);
827
                        }
828
                        if($key === 'max' || $key === 'min') {
829
                            foreach($value->attributes() as $k => $v){
830
                                $qouta[$key.'_amount'] = trim((string)$value->attributes());
831
                                $qouta[$key.'_amount_currency'] = trim((string)$v);
832
                            }
833
                        }
834
                        $paydata['logo']    = $logo;
835
                        $paydata['title']   = $title;
836
                        $paydata['amount']  = $qouta;
837
                        $parsed[$countries][$groups][$types] = $paydata;
838
                    }
839
                    //unset($qouta);
840
                    $qouta = null;
841
                }
842
            }
843
        }
844
845
        $cache['ts']    = time();
846
        $cache['data']  = $parsed;
847
848
        if (!is_writable(dirname(__FILE__))) {
849
            throw new WebToPayException(self::_('Directory '.dirname(__FILE__).' is not writable.',WebToPayException::E_INVALID));
850
        } else {
851
            $file   = serialize($cache);
852
            $fp     = fopen(self::CACHE_URL, 'w+') or die('error writing cache');
853
            fwrite($fp, $file);
854
            fclose($fp);
855
        };
856
    }
857
858
    /**
859
     * Converts money amount to e-shops base currency
860
     *
861
     * @param int       $sum
862
     * @param string    $payCurrency
863
     * @param string    $convertCurrency
864
     * @param array     $currency           array ( '0' => array ('iso' => USD, 'rate' => 0.417391, ),);
865
     * @return int      $amount
866
     */
867
    private static function toBaseCurrency($sum, $payCurrency, $convertCurrency, $currency){
868
        $amount = 0;
869
        foreach($currency as $entry) {
870
            if($payCurrency == $entry['iso']) {
871
                $amount = $sum/$entry['rate']; //turim viska BASE valiuta
872
                foreach($currency as $entry) {
873
                    if($convertCurrency == $entry['iso']) {
874
                        $amount *= $entry['rate'];
875
                        return $amount;
876
                        break;
877
                    }
878
                }
879
                break;
880
            }
881
        }
882
    }
883
884
885
    /**
886
     * Checks minimum amount of payment method
887
     *
888
     * @param array     $data
889
     * @param int       $sum
890
     * @param string    $payCurrency
891
     * @param array     $currency           array ( '0' => array ('iso' => USD, 'rate' => 0.417391, ),);
892
     * @return bool
893
     */
894
    private static function checkMinAmount($data, $sum, $payCurrency, $currency){
895
        //jei apribotas min_amount
896
        if (array_key_exists('min_amount', $data) && $data['min_amount'] != null) {
897
            if ($payCurrency == $data['min_amount_currency']) {//kai sutampa valiutos
898
                return ($sum >= $data['min_amount']);
899
            } else {
900
                //konvertuojam i base
901
                $amount = self::toBaseCurrency($sum, $payCurrency, $data['min_amount_currency'], $currency);
902
                return ($amount >= $data['min_amount']);
903
            }
904
        }
905
        return true;
906
    }
907
908
    /**
909
     * Checks maximum amount of payment method
910
     *
911
     * @param array     $data
912
     * @param int       $sum
913
     * @param string    $payCurrency
914
     * @param array     $currency           array ( '0' => array ('iso' => USD, 'rate' => 0.417391, ),);
915
     * @return bool
916
     */
917
    private static function checkMaxAmount($data, $sum, $payCurrency, $currency){
918
        //jei apribotas max_amount
919
        if (array_key_exists('max_amount', $data) && $data['max_amount'] != null) {
920
            if ($payCurrency == $data['max_amount_currency']) {//kai sutampa valiutos
921
                return ($data['max_amount'] >= $sum);
922
            } else {
923
                //konvertuojam i base
924
                $amount = self::toBaseCurrency($sum, $payCurrency, $data['max_amount_currency'], $currency);
925
                return ($data['max_amount'] >= $amount);
926
            }
927
        }
928
        return true;
929
    }
930
931
    /**
932
     * Checks maximum amount of payment method
933
     *
934
     * @param array     $payMethods     - unserialized array with pay method data
935
     * @param string    $payCurrency    -
936
     * @param int       $sum
937
     * @param string    $payCurrency
938
     * @param array     $currency           array ( '0' => array ('iso' => USD, 'rate' => 0.417391, ),);
939
     * @param string    $lang
940
     * @return array
941
     */
942
    public static function filterPayMethods($payMethods, $payCurrency, $sum, $currency, $lang) {
943
944
        $filtered   = array();
945
        $groupName  = array();
946
        $logo       = null;
947
        $name       = null;
948
949
        foreach($payMethods as $key1 => $value1) {
950
            foreach($value1 as $key2 => $value2){
951
                foreach($value2 as $key3 => $value3){
952
                    if($key3 === 'translate') {
953
                        foreach($value3 as $loc => $text) {
954
                            if($loc === 'en'){
955
                                $groupName = $text;
956
                            }
957
                            if($loc === $lang) {
958
                                $groupName = $text;
959
                                break;
960
                            }
961
                        }
962
                    } else {
963
                        foreach($value3 as $key4 => $value4) {
964
                            if($key4 === 'logo'){
965
                                foreach($value4 as $k => $v) {
966
                                    if($k === 'en'){ //statom anglu kalba default jei nerasta vertimu
967
                                        $logo = $v;
968
                                    }
969
                                    if($k === $lang) {
970
                                        $logo = $v;
971
                                        break;
972
                                    }
973
                                }
974
                            }
975
                            if($key4 === 'title'){
976
                                foreach($value4 as $k => $v) {
977
                                    if($k === 'en'){
978
                                        $name = $v;
979
                                    }
980
                                    if($k === $lang) {
981
                                        $name = $v;
982
                                        break;
983
                                    }
984
                                }
985
                            }
986
                            if($key4 === 'amount'){
987
                                $min = self::checkMinAmount($value4, $sum, $payCurrency, $currency);
988
                                $max = self::checkMaxAmount($value4, $sum, $payCurrency, $currency);
989
                                if($min && $max) { //jei praeina pagal min ir max irasom
990
                                    $filtered[$key1][$groupName][$name] = array('name' => $key3,'logo' => $logo);
991
                                }
992
                            }
993
                        }
994
                    }
995
                }
996
            }
997
        }
998
999
        return $filtered;
1000
    }
1001
1002
1003
    /**
1004
     * Return type and specification of given response array.
1005
     *
1006
     * @param array     $response
1007
     * @return array($type, $specs)
1008
     */
1009
    public static function getSpecsForResponse($response) {
1010
        if (
1011
                isset($response['to']) &&
1012
                isset($response['from']) &&
1013
                isset($response['sms']) &&
1014
                !isset($response['projectid'])
1015
            )
1016
        {
1017
            $type = 'mikro';
1018
            $specs = self::getMikroResponseSpec();
1019
        }
1020
        else {
1021
            $type = 'makro';
1022
            $specs = self::getMakroResponseSpec();
1023
        }
1024
1025
        return array($type, $specs);
1026
    }
1027
1028
1029
    /**
1030
     * @param string $prefix
1031
     */
1032
    public static function getPrefixed($data, $prefix) {
1033
        if (empty($prefix)) return $data;
1034
        $ret = array();
1035
        foreach ($data as $key => $val) {
1036
            if (strpos($key, $prefix) === 0 && strlen($key) > 3) {
1037
                $ret[substr($key, 3)] = $val;
1038
            }
1039
        }
1040
        return $ret;
1041
    }
1042
1043
    /**
1044
     * @param string $URL
1045
     */
1046
    private static function getUrlContent($URL){
1047
        $url = parse_url($URL);
1048
        if ('https' == $url['scheme']) {
1049
            $host = 'ssl://'.$url['host'];
1050
            $port = 443;
1051
        } else {
1052
            $host = $url['host'];
1053
            $port = 80;
1054
        }
1055
1056
        try {
1057
            $fp = fsockopen($host, $port, $errno, $errstr, 30);
1058
            if (!$fp) {
1059
                throw new WebToPayException(
1060
                    self::_('Can\'t connect to %s', $URL),
1061
                    WebToPayException::E_INVALID);
1062
            }
1063
1064
            if(isset($url['query'])) {
1065
                $data = $url['path'].'?'.$url['query'];
1066
            } else {
1067
                $data = $url['path'];
1068
            }
1069
1070
            $out = "GET " . $data . " HTTP/1.0\r\n";
1071
            $out .= "Host: ".$url['host']."\r\n";
1072
            $out .= "Connection: Close\r\n\r\n";
1073
1074
            $content = '';
1075
1076
            fwrite($fp, $out);
1077
            while (!feof($fp)) $content .= fgets($fp, 8192);
1078
            fclose($fp);
1079
1080
            list($header, $content) = explode("\r\n\r\n", $content, 2);
1081
1082
            return trim($content);
1083
1084
        } catch (WebToPayException $e) {
1085
            throw new WebToPayException(self::_('fsockopen fail!', WebToPayException::E_INVALID));
1086
        }
1087
    }
1088
1089
1090
    /**
1091
     * Checks and validates response from WebToPay server.
1092
     *
1093
     * This function accepts both mikro and makro responses.
1094
     *
1095
     * First parameter usualy should by $_GET array.
1096
     *
1097
     * Description about response can be found here:
1098
     * makro: https://www.mokejimai.lt/makro_specifikacija.html
1099
     * mikro: https://www.mokejimai.lt/mikro_mokejimu_specifikacija_SMS.html
1100
     *
1101
     * If response is not correct, WebToPayException will be raised.
1102
     *
1103
     * @param array     $response       Response array.
1104
     * @param array     $user_data
1105
     * @return array
1106
     */
1107
    public static function checkResponse($response, $user_data=array()) {
1108
        self::$verified = false;
1109
1110
        $response = self::getPrefixed($response, self::PREFIX);
1111
1112
        // *get* response type (makro|mikro)
1113
        list($type, $specs) = self::getSpecsForResponse($response);
1114
1115
        try {
1116
            // *check* response
1117
            $version = explode('.', self::VERSION);
1118
            $version = $version[0].'.'.$version[1];
1119
            if ('makro' == $type && $response['version'] != $version) {
1120
                throw new WebToPayException(
1121
                    self::_('Incompatible library and response versions: ' .
1122
                            'libwebtopay %s, response %s', self::VERSION, $response['version']),
1123
                    WebToPayException::E_INVALID);
1124
            }
1125
1126
            if ('makro' == $type && $response['projectid'] != $user_data['projectid']) {
1127
                throw new WebToPayException(
1128
                    self::_('Bad projectid: ' .
1129
                            'libwebtopay %s, response %s', self::VERSION, $response['version']),
1130
                    WebToPayException::E_INVALID);
1131
            }
1132
1133
            if ('makro' == $type) {
1134
                self::$verified = 'RESPONSE VERSION '.$response['version'].' OK';
1135
            }
1136
1137
            $orderid = 'makro' == $type ? $response['orderid'] : $response['id'];
1138
            $password = $user_data['sign_password'];
1139
1140
            // *check* SS2
1141
            if (self::useSS2()) {
1142
                $cert = 'public.key';
1143
                if (self::checkResponseCert($response, $cert)) {
1144
                    self::$verified = 'SS2 public.key';
1145
                }
1146
            }
1147
1148
            // *check* SS1 v2
1149
            else if (self::checkSS1v2($response, $password)) {
1150
                self::$verified = 'SS1v2';
1151
            }
1152
1153
            // *check* status
1154
            if ('makro' == $type && $response['status'] != '1') {
1155
                throw new WebToPayException(
1156
                    self::_('Returned transaction status is %d, successful status '.
1157
                            'should be 1.', $response['status']),
1158
                    WebToPayException::E_STATUS);
1159
            }
1160
1161
        }
1162
1163
        catch (WebToPayException $e) {
1164
            if (isset($user_data['log'])) {
1165
                self::log('ERR',
1166
                    self::responseToLog($type, $response) .
1167
                    ' ('. get_class($e).': '. $e->getMessage().')',
1168
                    $user_data['log']);
1169
            }
1170
            throw $e;
1171
        }
1172
1173
        if (isset($user_data['log'])) {
1174
            self::log('OK', self::responseToLog($type, $response), $user_data['log']);
1175
        }
1176
1177
        return $response;
1178
    }
1179
1180
    /**
1181
     * @param string $type
1182
     */
1183
    public static function responseToLog($type, $req) {
1184
        if ('mikro' == $type) {
1185
            return self::mikroResponseToLog($req);
1186
        }
1187
        else {
1188
            return self::makroResponseToLog($req);
1189
        }
1190
    }
1191
1192
    public static function mikroResponseToLog($req) {
1193
        $ret = array();
1194
        foreach (array('to', 'from', 'id', 'sms') as $key) {
1195
            $ret[] = $key.':"'.$req[$key].'"';
1196
        }
1197
1198
        return 'MIKRO '.implode(', ', $ret);
1199
    }
1200
1201
    public static function makroResponseToLog($req) {
1202
        $ret = array();
1203
        foreach (array('projectid', 'orderid', 'payment') as $key) {
1204
            $ret[] = $key.':"'.$req[$key].'"';
1205
        }
1206
1207
        return 'MAKRO '.implode(', ', $ret);
1208
    }
1209
1210
    public static function mikroAnswerToLog($answer) {
1211
        $ret = array();
1212
        foreach (array('id', 'msg') as $key) {
1213
            $ret[] = $key.':"'.$answer[$key].'"';
1214
        }
1215
1216
        return 'MIKRO [answer] '.implode(', ', $ret);
1217
    }
1218
1219
    /**
1220
     * @param string $type
1221
     * @param string $msg
1222
     */
1223
    public static function log($type, $msg, $logfile=null) {
1224
        if (!isset($logfile)) {
1225
            return false;
1226
        }
1227
1228
        $fp = @fopen($logfile, 'a');
1229
        if (!$fp) {
1230
            throw new WebToPayException(
1231
                self::_('Can\'t write to logfile: %s', $logfile), WebToPayException::E_LOG);
1232
        }
1233
1234
        $logline = array();
1235
1236
        // *log* type
1237
        $logline[] = $type;
1238
1239
        // *log* REMOTE_ADDR
1240
        if (isset($_SERVER['REMOTE_ADDR'])) {
1241
            $logline[] = $_SERVER['REMOTE_ADDR'];
1242
        }
1243
        else {
1244
            $logline[] = '-';
1245
        }
1246
1247
        // *log* datetime
1248
        $logline[] = date('[Y-m-d H:i:s O]');
1249
1250
        // *log* version
1251
        $logline[] = 'v'.self::VERSION.':';
1252
1253
        // *log* message
1254
        $logline[] = $msg;
1255
1256
        $logline = implode(' ', $logline)."\n";
1257
        fwrite($fp, $logline);
1258
        fclose($fp);
1259
1260
        // clear big log file
1261
        if (filesize($logfile) > 1024 * 1024 * pi()) {
1262
            copy($logfile, $logfile.'.old');
1263
            unlink($logfile);
1264
        }
1265
    }
1266
1267
1268
    /**
1269
     * Sends SMS answer.
1270
     *
1271
     * @param array     $answer
1272
     * @return void
1273
     */
1274
    public static function smsAnswer($answer) {
1275
1276
        $data = array(
1277
                'id'            => $answer['id'],
1278
                'msg'           => $answer['msg'],
1279
                'transaction'   => md5($answer['sign_password'].'|'.$answer['id']),
1280
            );
1281
1282
        $url = self::SMS_ANSWER_URL.'?'.http_build_query($data);
1283
        try {
1284
            $content = self::getUrlContent($url);
1285
            if (strpos($content, 'OK') !== 0) {
1286
                throw new WebToPayException(
1287
                    self::_('Error: %s', $content),
1288
                    WebToPayException::E_SMS_ANSWER);
1289
            }
1290
        } catch (WebToPayException $e) {
1291
            if (isset($answer['log'])) {
1292
                self::log('ERR',
1293
                    self::mikroAnswerToLog($answer).
1294
                    ' ('. get_class($e).': '. $e->getMessage().')',
1295
                    $answer['log']);
1296
            }
1297
            throw $e;
1298
        }
1299
1300
        if (isset($answer['log'])) {
1301
            self::log('OK', self::mikroAnswerToLog($answer), $answer['log']);
1302
        }
1303
1304
    }
1305
1306
1307
    /**
1308
     * I18n support.
1309
     */
1310
    public static function _() {
1311
        $args = func_get_args();
1312
        if (sizeof($args) > 1) {
1313
            return call_user_func_array('sprintf', $args);
1314
        }
1315
        else {
1316
            return $args[0];
1317
        }
1318
    }
1319
1320
}
1321
1322
1323
1324
class WebToPayException extends Exception {
1325
1326
    /**
1327
     * Missing field.
1328
     */
1329
    const E_MISSING = 1;
1330
1331
    /**
1332
     * Invalid field value.
1333
     */
1334
    const E_INVALID = 2;
1335
1336
    /**
1337
     * Max length exceeded.
1338
     */
1339
    const E_MAXLEN = 3;
1340
1341
    /**
1342
     * Regexp for field value doesn't match.
1343
     */
1344
    const E_REGEXP = 4;
1345
1346
    /**
1347
     * Missing or invalid user given parameters.
1348
     */
1349
    const E_USER_PARAMS = 5;
1350
1351
    /**
1352
     * Logging errors
1353
     */
1354
    const E_LOG = 6;
1355
1356
    /**
1357
     * SMS answer errors
1358
     */
1359
    const E_SMS_ANSWER = 7;
1360
1361
    /**
1362
     * Macro answer errors
1363
     */
1364
    const E_STATUS = 8;
1365
1366
    protected $field_name = false;
1367
1368
    public function setField($field_name) {
1369
        $this->field_name = $field_name;
1370
    }
1371
1372
    public function getField() {
1373
        return $this->field_name;
1374
    }
1375
}
1376