From 520f2f0ae9fdba85c05c2072492532f1864ca1b5 Mon Sep 17 00:00:00 2001 From: Ed Mark Date: Fri, 6 May 2016 16:12:42 +0300 Subject: [PATCH] update --- DIEGIMAS.txt | 12 +- README.md | 15 +- README.txt | 12 +- .../libwebtopay/WebToPay.php | 2777 +++++++++++++---- .../payment_module_wtp_cc_api.php | 60 +- data.sql | 18 +- 6 files changed, 2159 insertions(+), 735 deletions(-) diff --git a/DIEGIMAS.txt b/DIEGIMAS.txt index 4ab60ce..403b48c 100644 --- a/DIEGIMAS.txt +++ b/DIEGIMAS.txt @@ -1,16 +1,14 @@ ===================DIEGIMAS================= 1. Padarykite atsargine kopija[nebutina]. -2. Nukopijuokite avactis-system ir avactis-themess katalogus i Jusu avactis kataloga. -3. Eikite i avactis-system/cache ir istrinkite visus failus. -Perkraukite mokejimo moduliu puslapi administravime ir turi atsirasti mokejimo budas +2. Nukopijuokite avactis-system ir avactis-themes katalogus i Jusu avactis kataloga. +3. Eikite i avactis-conf/cache ir istrinkite visus failus. 4. pridekite eilute i /avactis-system/shortname2path faila PM_WTP = "avactis-system/admin/templates/resources/payment-module-wtp-messages-eng.ini" 5. Importuokite data.sql faila i Jusu duomenu baze. -6. Dar karta eikite i avactis-system/cache ir istrinkite visus failus. -Perkraukite mokejimo moduliu sarasa administravime, Paysera.com mokejimu modulis turetu pasikeist -i aktyvu bei parinkta. -Pereinant i Paysera.com nebuvo atliekami testavimai, todel kilus kazkokiems nesklandumams prasome pranesti i pagalba@mokejimai.lt \ No newline at end of file +Perkraukite mokejimo moduliu sarasa administravime, Paysera.com mokejimu modulis turetu atsirasti kaip aktyvu bei parinktas. + +Kilus kazkokiems nesklandumams prasome pranesti i pagalba@paysera.lt \ No newline at end of file diff --git a/README.md b/README.md index 8be8617..dd39a3b 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,17 @@ Installation ------------ -1. Download this repository as zip. -2. Extract avactis-system and avactis-themess folder to yout Avactis directory. -3. Go to avactis-system/cache and delete all files. -Refresh payment module list in Admin area, new module should appear. -4. add following line to /avactis-system/shortname2path file. +1. BACKUP EVERYTHING. +2. Copy avactis-system and avactis-themes folder to yout Avactis directory. +3. Go to avactis-conf/cache and delete all files. +4. add following line to /avactis-system/shortname2path file PM_WTP = "avactis-system/admin/templates/resources/payment-module-wtp-messages-eng.ini" 5. Import data.sql file to your database. -6. Once again go to avactis-system/cache and delete all files. -Refresh payment module list in Admin area, Paysera.com payment module should appear -as active and selected. -When migrating to Paysera.com no testings were made, so if any problems occur please report to support@paysera.com +Refresh payment module list in Admin area, Paysera.com payment module should appear as active and selected. + Contacts -------- diff --git a/README.txt b/README.txt index d644f6e..3bef278 100644 --- a/README.txt +++ b/README.txt @@ -1,16 +1,14 @@ ===================INSTALATION================= 1. BACKUP EVERYTHING. -2. Copy avactis-system and avactis-themess folder to yout Avactis directory. -3. Go to avactis-system/cache and delete all files. -Refresh payment module list in Admin area, new module should appear. +2. Copy avactis-system and avactis-themes folder to yout Avactis directory. +3. Go to avactis-conf/cache and delete all files. 4. add following line to /avactis-system/shortname2path file PM_WTP = "avactis-system/admin/templates/resources/payment-module-wtp-messages-eng.ini" 5. Import data.sql file to your database. -6. Once again go to avactis-system/cache and delete all files. -Refresh payment module list in Admin area, Paysera.com payment module should appear -as active and selected. -When migrating to Paysera.com no testings were made, so if any problems occur please report to support@paysera.com \ No newline at end of file +Refresh payment module list in Admin area, Paysera.com payment module should appear as active and selected. + +If any problems occur please report to support@paysera.com \ No newline at end of file diff --git a/avactis-system/modules/payment_module_wtp_cc/libwebtopay/WebToPay.php b/avactis-system/modules/payment_module_wtp_cc/libwebtopay/WebToPay.php index 64cf2fc..7720682 100644 --- a/avactis-system/modules/payment_module_wtp_cc/libwebtopay/WebToPay.php +++ b/avactis-system/modules/payment_module_wtp_cc/libwebtopay/WebToPay.php @@ -1,7 +1,10 @@ . * * @package WebToPay - * @author EVP International + * @author EVP International * @license http://www.gnu.org/licenses/lgpl.html - * @version 1.3.3 + * @version 1.6 * @link http://www.webtopay.com/ */ + +/** + * Contains static methods for most used scenarios. + */ class WebToPay { /** * WebToPay Library version. */ - const VERSION = '1.3.3'; - + const VERSION = '1.6'; /** * Server URL where all requests should go. */ - const PAY_URL = 'https://www.mokejimai.lt/pay/'; - - - /** - * SMS answer url. - */ - const SMS_ANSWER_URL = 'https://www.mokejimai.lt/psms/respond/'; - + const PAY_URL = 'https://www.paysera.com/pay/'; /** - * Prefix for callback data. + * Server URL where all non-lithuanian language requests should go. */ - const PREFIX = 'wp_'; + const PAYSERA_PAY_URL = 'https://www.paysera.com/pay/'; /** - * Identifies what verification method was used. - * - * Values can be: - * - false not verified - * - RESPONSE only response parameters are verified - * - SS1 SS1 verification - * - SS2 SS2 verification + * Server URL where we can get XML with payment method data. */ - public static $verified = false; - + const XML_URL = 'https://www.paysera.com/new/api/paymentMethods/'; /** - * If true, check SS2 if false, skip to SS1 + * SMS answer url. */ - private static $SS2 = true; - + const SMS_ANSWER_URL = 'https://www.paysera.com/psms/respond/'; /** - * Toggle SS2 checking. Usualy you don't need to use this method, because - * by default first SS2 support are checked and if it doesn't work, - * fallback to SS1. + * Builds request data array. * - * Use this method if your server supports SS2, but you want to use SS1. + * This method checks all given data and generates correct request data + * array or raises WebToPayException on failure. + * + * Possible keys: + * https://www.paysera.com/makro_specifikacija.html + * + * @param array $data Information about current payment request + * + * @return array + * + * @throws WebToPayException on data validation error */ - public static function toggleSS2($value) { - self::$SS2 = (bool) $value; + public static function buildRequest($data) { + if (!isset($data['sign_password']) || !isset($data['projectid'])) { + throw new WebToPayException('sign_password or projectid is not provided'); + } + $password = $data['sign_password']; + $projectId = $data['projectid']; + unset($data['sign_password']); + unset($data['projectid']); + + $factory = new WebToPay_Factory(array('projectId' => $projectId, 'password' => $password)); + $requestBuilder = $factory->getRequestBuilder(); + return $requestBuilder->buildRequest($data); } /** - * Throw exception. + * Builds request and redirects user to payment window with generated request data + * + * Possible array keys are described here: + * https://www.paysera.com/makro_specifikacija.html * - * @param string $code - * @return void + * @param array $data Information about current payment request. + * @param boolean $exit if true, exits after sending Location header; default false + * + * @throws WebToPayException on data validation error */ - public static function throwResponseError($code) { - $errors = array( - '0x1' => self::_('mokėjimo suma per maža'), - '0x2' => self::_('mokėjimo suma per didelė'), - '0x3' => self::_('nurodyta valiuta neaptarnaujama'), - '0x4' => self::_('nėra sumos arba valiutos'), - '0x6' => self::_('klaidos kodas nebenaudojamas'), - '0x7' => self::_('išjungtas testavimo režimas'), - '0x8' => self::_('jūs uždraudėte šį mokėjimo būdą'), - '0x9' => self::_('blogas "paytext" kintamojo kodavimas (turi būti utf-8)'), - '0x10' => self::_('tuščias arba neteisingai užpildytas "orderid"'), - '0x11' => self::_('mokėjimas negalimas, kol projektas nepatvirtintas arba jeigu jis yra blokuotas'), - '0x12' => self::_('negautas "projectid" parametras, nors jis yra privalomas'), - '0x13' => self::_('"accepturl", "cancellurl" arba "callbacurl" skiriasi nuo projekte patvirtintų adresų'), - '0x14' => self::_('blogai sugeneruotas paraštas ("sign" parametras)'), - '0x15' => self::_('klaidingi kai kurie iš perduotų parametrų'), - '0x15x0' => self::_('neteisingas vienas iš šių parametrų: cancelurl, accepturl, callbackurl'), - ); - - if (isset($errors[$code])) { - $msg = $errors[$code]; + public static function redirectToPayment($data, $exit = false) { + if (!isset($data['sign_password']) || !isset($data['projectid'])) { + throw new WebToPayException('sign_password or projectid is not provided'); } - else { - $msg = self::_('Nenumatyta klaida'); + $password = $data['sign_password']; + $projectId = $data['projectid']; + unset($data['sign_password']); + unset($data['projectid']); + + $factory = new WebToPay_Factory(array('projectId' => $projectId, 'password' => $password)); + $url = $factory->getRequestBuilder() + ->buildRequestUrlFromData($data); + + if (headers_sent()) { + echo ''; + } else { + header("Location: $url", true); + } + printf( + 'Redirecting to %s. Please wait.', + htmlentities($url, ENT_QUOTES, 'UTF-8'), + htmlentities($url, ENT_QUOTES, 'UTF-8') + ); + if ($exit) { + exit(); } - - throw new WebToPayException($msg); } - /** - * Returns specification array for request. + * Builds repeat request data array. * - * @return array - */ - public static function getRequestSpec() { - // Array structure: - // * name – request item name. - // * maxlen – max allowed value for item. - // * required – is this item is required. - // * user – if true, user can set value of this item, if false - // item value is generated. - // * isrequest – if true, item will be included in request array, if - // false, item only be used internaly and will not be - // included in outgoing request array. - // * regexp – regexp to test item value. - return array( - array('projectid', 11, true, true, true, '/^\d+$/'), - array('orderid', 40, true, true, true, ''), - array('lang', 3, false, true, true, '/^[a-z]{3}$/i'), - array('amount', 11, false, true, true, '/^\d+$/'), - array('currency', 3, false, true, true, '/^[a-z]{3}$/i'), - array('accepturl', 255, true, true, true, ''), - array('cancelurl', 255, true, true, true, ''), - array('callbackurl', 255, true, true, true, ''), - array('payment', 20, false, true, true, ''), - array('country', 2, false, true, true, '/^[a-z_]{2}$/i'), - array('paytext', 255, false, true, true, ''), - array('p_firstname', 255, false, true, true, ''), - array('p_lastname', 255, false, true, true, ''), - array('p_email', 255, false, true, true, ''), - array('p_street', 255, false, true, true, ''), - array('p_city', 255, false, true, true, ''), - array('p_state', 20, false, true, true, ''), - array('p_zip', 20, false, true, true, ''), - array('p_countrycode', 2, false, true, true, '/^[a-z]{2}$/i'), - array('sign', 255, true, false, true, ''), - array('sign_password', 255, true, true, false, ''), - array('only_payments', 0, false, true, true, ''), - array('disalow_payments', 0, false, true, true, ''), - array('repeat_request', 1, false, false, true, '/^[01]$/'), - array('test', 1, false, true, true, '/^[01]$/'), - array('version', 9, true, false, true, '/^\d+\.\d+$/'), - ); - } - - - /** - * Returns specification array for repeat request. + * This method checks all given data and generates correct request data + * array or raises WebToPayException on failure. * - * @return array - */ - public static function getRepeatRequestSpec() { - // Array structure: - // * name – request item name. - // * maxlen – max allowed value for item. - // * required – is this item is required. - // * user – if true, user can set value of this item, if false - // item value is generated. - // * isrequest – if true, item will be included in request array, if - // false, item only be used internaly and will not be - // included in outgoing request array. - // * regexp – regexp to test item value. - return array( - array('projectid', 11, true, true, true, '/^\d+$/'), - array('orderid', 40, true, true, true, ''), - array('sign', 255, true, false, true, ''), - array('sign_password', 255, true, true, false, ''), - array('repeat_request', 1, true, false, true, '/^1$/'), - array('version', 9, true, false, true, '/^\d+\.\d+$/'), - ); - } - - - /** - * Returns specification array for makro response. + * Method accepts single parameter $data of array type. All possible array + * keys are described here: + * https://www.paysera.com/makro_specifikacija.html + * + * @param array $data Information about current payment request * * @return array + * + * @throws WebToPayException on data validation error */ - public static function getMakroResponseSpec() { - // Array structure: - // * name – request item name. - // * maxlen – max allowed value for item. - // * required – is this item is required in response. - // * mustcheck – this item must be checked by user. - // * isresponse – if false, item must not be included in response array. - // * regexp – regexp to test item value. - return array( - 'projectid' => array(11, true, true, true, '/^\d+$/'), - 'orderid' => array(40, false, false, true, ''), - 'lang' => array(3, false, false, true, '/^[a-z]{3}$/i'), - 'amount' => array(11, false, false, true, '/^\d+$/'), - 'currency' => array(3, false, false, true, '/^[a-z]{3}$/i'), - 'payment' => array(20, false, false, true, ''), - 'country' => array(2, false, false, true, '/^[a-z_]{2}$/i'), - 'paytext' => array(0, false, false, true, ''), - '_ss2' => array(0, true, false, true, ''), - '_ss1' => array(0, false, false, true, ''), - 'name' => array(255, false, false, true, ''), - 'surename' => array(255, false, false, true, ''), - 'status' => array(255, false, false, true, ''), - 'error' => array(20, false, false, true, ''), - 'test' => array(1, false, false, true, '/^[01]$/'), - - 'p_email' => array(0, false, false, true, ''), - 'payamount' => array(0, false, false, true, ''), - 'paycurrency' => array(0, false, false, true, ''), - - 'version' => array(9, true, false, true, '/^\d+\.\d+$/'), - - 'sign_password' => array(255, false, true, false, ''), - ); - } - + public static function buildRepeatRequest($data) { + if (!isset($data['sign_password']) || !isset($data['projectid']) || !isset($data['orderid'])) { + throw new WebToPayException('sign_password, projectid or orderid is not provided'); + } + $password = $data['sign_password']; + $projectId = $data['projectid']; + $orderId = $data['orderid']; + $factory = new WebToPay_Factory(array('projectId' => $projectId, 'password' => $password)); + $requestBuilder = $factory->getRequestBuilder(); + return $requestBuilder->buildRepeatRequest($orderId); + } /** - * Returns specification array for mikro response. + * Returns payment url. Argument is same as lang parameter in request data * - * @return array + * @param string $language + * @return string $url */ - public static function getMikroResponseSpec() { - // Array structure: - // * name – request item name. - // * maxlen – max allowed value for item. - // * required – is this item is required in response. - // * mustcheck – this item must be checked by user. - // * isresponse – if false, item must not be included in response array. - // * regexp – regexp to test item value. - return array( - 'to' => array(0, true, false, true, ''), - 'sms' => array(0, true, false, true, ''), - 'from' => array(0, true, false, true, ''), - 'operator' => array(0, true, false, true, ''), - 'amount' => array(0, true, false, true, ''), - 'currency' => array(0, true, false, true, ''), - 'country' => array(0, true, false, true, ''), - 'id' => array(0, true, false, true, ''), - '_ss2' => array(0, true, false, true, ''), - '_ss1' => array(0, true, false, true, ''), - 'test' => array(0, true, false, true, ''), - 'key' => array(0, true, false, true, ''), - //'version' => array(9, true, false, true, '/^\d+\.\d+$/'), - ); + public static function getPaymentUrl($language = 'LIT') { + return (in_array($language, array('lt', 'lit', 'LIT'))) + ? self::PAY_URL + : self::PAYSERA_PAY_URL; } /** - * Checks user given request data array. + * Parses response from WebToPay server and validates signs. + * + * This function accepts both micro and macro responses. + * + * First parameter usualy should be $_GET array. + * + * Description about response can be found here: + * makro: https://www.paysera.com/makro_specifikacija.html + * mikro: https://www.paysera.com/mikro_mokejimu_specifikacija_SMS.html * - * If any errors occurs, WebToPayException will be raised. + * If response is not correct, WebToPayException will be raised. * - * This method returns validated request array. Returned array contains - * only those items from $data, that are needed. + * @param array $query Response array + * @param array $userData * - * @param array $data * @return array + * + * @throws WebToPayException + * @deprecated use validateAndParseData() and check status code yourself */ - public static function checkRequestData($data, $specs) { - $request = array(); - foreach ($specs as $spec) { - list($name, $maxlen, $required, $user, $isrequest, $regexp) = $spec; - if (!$user) continue; - if ($required && !isset($data[$name])) { - $e = new WebToPayException( - self::_("'%s' is required but missing.", $name), - WebToPayException::E_MISSING); - $e->setField($name); - throw $e; - } + public static function checkResponse($query, $userData = array()) { + $projectId = isset($userData['projectid']) ? $userData['projectid'] : null; + $password = isset($userData['sign_password']) ? $userData['sign_password'] : null; + $logFile = isset($userData['log']) ? $userData['log'] : null; - if (!empty($data[$name])) { - if ($maxlen && strlen($data[$name]) > $maxlen) { - $e = new WebToPayException( - self::_("'%s' value '%s' is too long, %d characters allowed.", - $name, $data[$name], $maxlen), - WebToPayException::E_MAXLEN); - $e->setField($name); - throw $e; - } + try { + $data = self::validateAndParseData($query, $projectId, $password); + if ($data['type'] == 'macro' && $data['status'] != 1) { + throw new WebToPayException('Expected status code 1', WebToPayException::E_DEPRECATED_USAGE); + } - if ('' != $regexp && !preg_match($regexp, $data[$name])) { - $e = new WebToPayException( - self::_("'%s' value '%s' is invalid.", $name, $data[$name]), - WebToPayException::E_REGEXP); - $e->setField($name); - throw $e; - } + if ($logFile) { + self::log('OK', http_build_query($data, null, '&'), $logFile); } + return $data; - if ($isrequest && isset($data[$name])) { - $request[$name] = $data[$name]; + } catch (WebToPayException $exception) { + if ($logFile && $exception->getCode() != WebToPayException::E_DEPRECATED_USAGE) { + self::log('ERR', $exception . "\nQuery: " . http_build_query($query, null, '&'), $logFile); } + throw $exception; } - - return $request; } - /** - * Puts signature on request data array. + * Parses request (query) data and validates its signature. * - * @param string $request - * @param string $password - * @return string + * @param array $query usually $_GET + * @param integer $projectId + * @param string $password + * + * @return array + * + * @throws WebToPayException */ - public static function signRequest($request, $password) { - $fields = array( - 'projectid', 'orderid', 'lang', 'amount', 'currency', - 'accepturl', 'cancelurl', 'callbackurl', 'payment', 'country', - 'p_firstname', 'p_lastname', 'p_email', 'p_street', - 'p_city', 'p_state', 'p_zip', 'p_countrycode', 'test', - 'version' - ); - $data = ''; - foreach ($fields as $key) { - if (isset($request[$key]) && trim($request[$key]) != '') { - $data .= $request[$key]; - } - } - $request['sign'] = md5($data . $password); - - return $request; + public static function validateAndParseData(array $query, $projectId, $password) { + $factory = new WebToPay_Factory(array('projectId' => $projectId, 'password' => $password)); + $validator = $factory->getCallbackValidator(); + $data = $validator->validateAndParseData($query); + return $data; } - /** - * Builds request data array. - * - * This method checks all given data and generates correct request data - * array or raises WebToPayException on failure. + * Sends SMS answer * - * Method accepts single parameter $data of array type. All possible array - * keys are described here: - * https://www.mokejimai.lt/makro_specifikacija.html + * @param array $userData * - * @param array $data Information about current payment request. - * @return array + * @throws WebToPayException + * @throws WebToPay_Exception_Validation */ - public static function buildRequest($data) { - $specs = self::getRequestSpec(); - $request = self::checkRequestData($data, $specs); - $version = explode('.', self::VERSION); - $request['version'] = $version[0].'.'.$version[1]; - $request = self::signRequest($request, $data['sign_password']); - return $request; + public static function smsAnswer($userData) { + if (!isset($userData['id']) || !isset($userData['msg']) || !isset($userData['sign_password'])) { + throw new WebToPay_Exception_Validation('id, msg and sign_password are required'); + } + + $smsId = $userData['id']; + $text = $userData['msg']; + $password = $userData['sign_password']; + $logFile = isset($userData['log']) ? $userData['log'] : null; + + try { + + $factory = new WebToPay_Factory(array('password' => $password)); + $factory->getSmsAnswerSender()->sendAnswer($smsId, $text); + + if ($logFile) { + self::log('OK', 'SMS ANSWER ' . $smsId . ' ' . $text, $logFile); + } + + } catch (WebToPayException $e) { + if ($logFile) { + self::log('ERR', 'SMS ANSWER ' . $e, $logFile); + } + throw $e; + } + } /** - * Builds repeat request data array. + * Gets available payment methods for project. Gets methods min and max amounts in specified currency. * - * This method checks all given data and generates correct request data - * array or raises WebToPayException on failure. + * @param integer $projectId + * @param string $currency * - * Method accepts single parameter $data of array type. All possible array - * keys are described here: - * https://www.mokejimai.lt/makro_specifikacija.html + * @return WebToPay_PaymentMethodList * - * @param array $data Information about current payment request. - * @return array + * @throws WebToPayException */ - public static function buildRepeatRequest($data) { - $specs = self::getRepeatRequestSpec(); - $request = self::checkRequestData($data, $specs); - $request['repeat_request'] = '1'; - $version = explode('.', self::VERSION); - $request['version'] = $version[0].'.'.$version[1]; - $request = self::signRequest($request, $data['sign_password']); - return $request; + public static function getPaymentMethodList($projectId, $currency = 'EUR') { + $factory = new WebToPay_Factory(array('projectId' => $projectId)); + return $factory->getPaymentMethodListProvider()->getPaymentMethodList($currency); } /** - * Download certificate from webtopay.com. + * Logs to file. Just skips logging if file is not writeable * - * @param string $cert - * @return string + * @param string $type + * @param string $msg + * @param string $logfile */ - public static function getCert($cert) { - $fp = fsockopen("downloads.webtopay.com", 80, $errno, $errstr, 30); + protected static function log($type, $msg, $logfile) { + $fp = @fopen($logfile, 'a'); if (!$fp) { - throw new WebToPayException( - self::_('Payment check: Can\'t get cert from '. - 'downloads.webtopay.com/download/%s', - $cert), - WebToPayException::E_INVALID); - return false; + return; } - $out = "GET /download/" . $cert . " HTTP/1.1\r\n"; - $out .= "Host: downloads.webtopay.com\r\n"; - $out .= "Connection: Close\r\n\r\n"; - - $content = ''; + $logline = array( + $type, + isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-', + date('[Y-m-d H:i:s O]'), + 'v' . self::VERSION . ':', + $msg + ); - fwrite($fp, $out); - while (!feof($fp)) $content .= fgets($fp, 8192); + $logline = implode(' ', $logline)."\n"; + fwrite($fp, $logline); fclose($fp); - list($header, $content) = explode("\r\n\r\n", $content, 2); - - return $content; + // clear big log file + if (filesize($logfile) > 1024 * 1024 * pi()) { + copy($logfile, $logfile.'.old'); + unlink($logfile); + } } +} + + + + +/** + * Base exception class for all exceptions in this library + */ +class WebToPayException extends Exception { /** - * Check is response certificate is valid - * - * @param string $response - * @param string $cert - * @return bool + * Missing field. */ - public static function checkResponseCert($response, $cert='public.key') { - $pKeyP = self::getCert($cert); - if (!$pKeyP) { - throw new WebToPayException( - self::_('Can\'t get openssl public key for %s', $cert), - WebToPayException::E_INVALID); - } + const E_MISSING = 1; - $_SS2 = ''; - foreach ($response as $key => $value) { - if ($key!='_ss2') $_SS2 .= "{$value}|"; - } - $ok = openssl_verify($_SS2, base64_decode($response['_ss2']), $pKeyP); + /** + * Invalid field value. + */ + const E_INVALID = 2; - if ($ok !== 1) { - throw new WebToPayException( - self::_('Can\'t verify SS2 for %s', $cert), - WebToPayException::E_INVALID); - } + /** + * Max length exceeded. + */ + const E_MAXLEN = 3; - return true; - } + /** + * Regexp for field value doesn't match. + */ + const E_REGEXP = 4; - public static function checkResponseData($response, $mustcheck_data, $specs) { - $resp_keys = array(); - foreach ($specs as $name => $spec) { - list($maxlen, $required, $mustcheck, $is_response, $regexp) = $spec; - if ($required && !isset($response[$name])) { - $e = new WebToPayException( - self::_("'%s' is required but missing.", $name), - WebToPayException::E_MISSING); - $e->setField($name); - throw $e; - } + /** + * Missing or invalid user given parameters. + */ + const E_USER_PARAMS = 5; - if ($mustcheck) { - if (!isset($mustcheck_data[$name])) { - $e = new WebToPayException( - self::_("'%s' must exists in array of second parameter ". - "of checkResponse() method.", $name), - WebToPayException::E_USER_PARAMS); - $e->setField($name); - throw $e; - } + /** + * Logging errors + */ + const E_LOG = 6; - if ($is_response) { - if ($response[$name] != $mustcheck_data[$name]) { - $e = new WebToPayException( - self::_("'%s' yours and requested value is not ". - "equal ('%s' != '%s') ", - $name, $mustcheck_data[$name], $response[$name]), - WebToPayException::E_INVALID); - $e->setField($name); - throw $e; - } - } - } + /** + * SMS answer errors + */ + const E_SMS_ANSWER = 7; - if (!empty($response[$name])) { - if ($maxlen && strlen($response[$name]) > $maxlen) { - $e = new WebToPayException( - self::_("'%s' value '%s' is too long, %d characters allowed.", - $name, $response[$name], $maxlen), - WebToPayException::E_MAXLEN); - $e->setField($name); - throw $e; - } + /** + * Macro answer errors + */ + const E_STATUS = 8; - if ('' != $regexp && !preg_match($regexp, $response[$name])) { - $e = new WebToPayException( - self::_("'%s' value '%s' is invalid.", $name, $response[$name]), - WebToPayException::E_REGEXP); - $e->setField($name); - throw $e; - } - } + /** + * Library errors - if this happens, bug-report should be sent; also you can check for newer version + */ + const E_LIBRARY = 9; - if (isset($response[$name])) { - $resp_keys[] = $name; - } - } + /** + * Errors in remote service - it returns some invalid data + */ + const E_SERVICE = 10; - // Filter only parameters passed from webtopay - $_response = array(); - foreach (array_keys($response) as $key) { - if (in_array($key, $resp_keys)) { - $_response[$key] = $response[$key]; - } - } + /** + * Deprecated usage errors + */ + const E_DEPRECATED_USAGE = 11; - return $_response; - } + /** + * @var string|boolean + */ + protected $fieldName = false; + /** + * Sets field which failed + * + * @param string $fieldName + */ + public function setField($fieldName) { + $this->fieldName = $fieldName; + } /** - * Check if SS2 checking is available and enabled. + * Gets field which failed * - * @return bool + * @return string|boolean false */ - public static function useSS2() { - if (!self::$SS2) return false; - if (!function_exists('openssl_pkey_get_public')) return false; - return true; + public function getField() { + return $this->fieldName; } +} +/** + * Parses and validates callbacks + */ +class WebToPay_CallbackValidator { /** - * Check for SS1, which is not depend on openssl functions. - * - * @param array $response - * @param string $passwd - * @param int $orderid - * @return bool + * @var WebToPay_Sign_SignCheckerInterface */ - public static function checkSS1($response, $passwd, $orderid) { - if (32 != strlen($passwd)) { - $passwd = md5($passwd); - } + protected $signer; - $_SS1 = array( - $passwd, - $orderid, - intval($response['test']), - 1 - ); + /** + * @var WebToPay_Util + */ + protected $util; - $_SS1 = implode('|', $_SS1); - if ($response['_ss1'] != md5($_SS1)) { - throw new WebToPayException( - self::_('Can\'t verify SS1'), - WebToPayException::E_INVALID); - } + /** + * @var integer + */ + protected $projectId; - return true; + /** + * Constructs object + * + * @param integer $projectId + * @param WebToPay_Sign_SignCheckerInterface $signer + * @param WebToPay_Util $util + */ + public function __construct($projectId, WebToPay_Sign_SignCheckerInterface $signer, WebToPay_Util $util) { + $this->signer = $signer; + $this->util = $util; + $this->projectId = $projectId; } - /** - * Return type and specification of given response array. + * Parses callback parameters from query parameters and checks if sign is correct. + * Request has parameter "data", which is signed and holds all callback parameters + * + * @param array $requestData * - * @param array $response - * @return array($type, $specs) + * @return array Parsed callback parameters + * + * @throws WebToPayException + * @throws WebToPay_Exception_Callback */ - public static function getSpecsForResponse($response) { - if ( - isset($response['to']) && - isset($response['from']) && - isset($response['sms']) && - !isset($response['projectid']) - ) - { - $type = 'mikro'; - $specs = self::getMikroResponseSpec(); + public function validateAndParseData(array $requestData) { + if (!$this->signer->checkSign($requestData)) { + throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit'); } - else { - $type = 'makro'; - $specs = self::getMakroResponseSpec(); + + if (!isset($requestData['data'])) { + throw new WebToPay_Exception_Callback('"data" parameter not found'); } + $data = $requestData['data']; - return array($type, $specs); - } + $queryString = $this->util->decodeSafeUrlBase64($data); + $request = $this->util->parseHttpQuery($queryString); + + if (!isset($request['projectid'])) { + throw new WebToPay_Exception_Callback( + 'Project ID not provided in callback', + WebToPayException::E_INVALID + ); + } + if ((string) $request['projectid'] !== (string) $this->projectId) { + throw new WebToPay_Exception_Callback( + sprintf('Bad projectid: %s, should be: %s', $request['projectid'], $this->projectId), + WebToPayException::E_INVALID + ); + } - public static function getPrefixed($data, $prefix) { - if (empty($prefix)) return $data; - $ret = array(); - foreach ($data as $key => $val) { - if (strpos($key, $prefix) === 0 && strlen($key) > 3) { - $ret[substr($key, 3)] = $val; - } + if (!isset($request['type']) || !in_array($request['type'], array('micro', 'macro'))) { + $micro = ( + isset($request['to']) + && isset($request['from']) + && isset($request['sms']) + ); + $request['type'] = $micro ? 'micro' : 'macro'; } - return $ret; - } + return $request; + } /** - * Checks and validates response from WebToPay server. - * - * This function accepts both mikro and makro responses. - * - * First parameter usualy should by $_GET array. - * - * Description about response can be found here: - * makro: https://www.mokejimai.lt/makro_specifikacija.html - * mikro: https://www.mokejimai.lt/mikro_mokejimu_specifikacija_SMS.html + * Checks data to have all the same parameters provided in expected array * - * If response is not correct, WebToPayException will be raised. + * @param array $data + * @param array $expected * - * @param array $response Response array. - * @param array $user_data - * @return void + * @throws WebToPayException */ - public static function checkResponse($response, $user_data=array()) { - self::$verified = false; - - $response = self::getPrefixed($response, self::PREFIX); - - // *get* response type (makro|mikro) - list($type, $specs) = self::getSpecsForResponse($response); - - try { - // *check* response - $version = explode('.', self::VERSION); - $version = $version[0].'.'.$version[1]; - if ('makro' == $type && $response['version'] != $version) { + public function checkExpectedFields(array $data, array $expected) { + foreach ($expected as $key => $value) { + $passedValue = isset($data[$key]) ? $data[$key] : null; + if ($passedValue != $value) { throw new WebToPayException( - self::_('Incompatible library and response versions: ' . - 'libwebtopay %s, response %s', self::VERSION, $response['version']), - WebToPayException::E_INVALID); + sprintf('Field %s is not as expected (expected %s, got %s)', $key, $value, $passedValue) + ); } + } + } +} - if ('makro' == $type && $response['projectid'] != $user_data['projectid']) { - throw new WebToPayException( - self::_('Bad projectid: ' . - 'libwebtopay %s, response %s', self::VERSION, $response['version']), - WebToPayException::E_INVALID); - } - - if ('makro' == $type) { - self::$verified = 'RESPONSE VERSION '.$response['version'].' OK'; - } - - $orderid = 'makro' == $type ? $response['orderid'] : $response['id']; - $password = $user_data['sign_password']; - - // *check* SS2 - if (self::useSS2()) { - $cert = 'public.key'; - if (self::checkResponseCert($response, $cert)) { - self::$verified = 'SS2 public.key'; - } - } +/** + * Wrapper class to group payment methods. Each country can have several payment method groups, each of them + * have one or more payment methods. + */ +class WebToPay_PaymentMethodGroup { + /** + * Some unique (in the scope of country) key for this group + * + * @var string + */ + protected $groupKey; - // *check* SS1 - else if (self::checkSS1($response, $password, $orderid)) { - self::$verified = 'SS1'; - } + /** + * Translations array for this group. Holds associative array of group title by country codes. + * + * @var array + */ + protected $translations; - // *check* status - if ('makro' == $type && '1' != $response['status']) { - throw new WebToPayException( - self::_('Returned transaction status is %d, successful status '. - 'should be 1.', $response['status']), - WebToPayException::E_INVALID); - } + /** + * Holds actual payment methods + * + * @var WebToPay_PaymentMethod[] + */ + protected $paymentMethods; - } + /** + * Default language for titles + * + * @var string + */ + protected $defaultLanguage; - catch (WebToPayException $e) { - if (isset($user_data['log'])) { - self::log('ERR', - self::responseToLog($type, $response) . - ' ('. get_class($e).': '. $e->getMessage().')', - $user_data['log']); - } - throw $e; - } + /** + * Constructs object + * + * @param string $groupKey + * @param array $translations + * @param string $defaultLanguage + */ + public function __construct($groupKey, array $translations = array(), $defaultLanguage = 'lt') { + $this->groupKey = $groupKey; + $this->translations = $translations; + $this->defaultLanguage = $defaultLanguage; + $this->paymentMethods = array(); + } - if (isset($user_data['log'])) { - self::log('OK', self::responseToLog($type, $response), $user_data['log']); + /** + * Sets default language for titles. + * Returns itself for fluent interface + * + * @param string $language + * + * @return WebToPay_PaymentMethodGroup + */ + public function setDefaultLanguage($language) { + $this->defaultLanguage = $language; + foreach ($this->paymentMethods as $paymentMethod) { + $paymentMethod->setDefaultLanguage($language); } + return $this; + } - return $response; + /** + * Gets default language for titles + * + * @return string + */ + public function getDefaultLanguage() { + return $this->defaultLanguage; } - public static function responseToLog($type, $req) { - if ('mikro' == $type) { - return self::mikroResponseToLog($req); - } - else { - return self::makroResponseToLog($req); + /** + * Gets title of the group. Tries to get title in specified language. If it is not found or if language is not + * specified, uses default language, given to constructor. + * + * @param string [Optional] $languageCode + * + * @return string + */ + public function getTitle($languageCode = null) { + if ($languageCode !== null && isset($this->translations[$languageCode])) { + return $this->translations[$languageCode]; + } elseif (isset($this->translations[$this->defaultLanguage])) { + return $this->translations[$this->defaultLanguage]; + } else { + return $this->groupKey; } } - public static function mikroResponseToLog($req) { - $ret = array(); - foreach (array('to', 'from', 'id', 'sms') as $key) { - $ret[] = $key.':"'.$req[$key].'"'; - } + /** + * Returns group key + * + * @return string + */ + public function getKey() { + return $this->groupKey; + } - return 'MIKRO '.implode(', ', $ret); + /** + * Returns available payment methods for this group + * + * @return WebToPay_PaymentMethod[] + */ + public function getPaymentMethods() { + return $this->paymentMethods; } - public static function makroResponseToLog($req) { - $ret = array(); - foreach (array('projectid', 'orderid', 'payment') as $key) { - $ret[] = $key.':"'.$req[$key].'"'; - } - return 'MAKRO '.implode(', ', $ret); + /** + * Adds new payment method for this group. + * If some other payment method with specified key was registered earlier, overwrites it. + * Returns given payment method + * + * @param WebToPay_PaymentMethod $paymentMethod + * + * @return WebToPay_PaymentMethod + */ + public function addPaymentMethod(WebToPay_PaymentMethod $paymentMethod) { + return $this->paymentMethods[$paymentMethod->getKey()] = $paymentMethod; } - public static function mikroAnswerToLog($answer) { - $ret = array(); - foreach (array('id', 'msg') as $key) { - $ret[] = $key.':"'.$answer[$key].'"'; - } - - return 'MIKRO [answer] '.implode(', ', $ret); + /** + * Gets payment method object with key. If no payment method with such key is found, returns null. + * + * @param string $key + * + * @return null|WebToPay_PaymentMethod + */ + public function getPaymentMethod($key) { + return isset($this->paymentMethods[$key]) ? $this->paymentMethods[$key] : null; } - public static function log($type, $msg, $logfile=null) { - if (!isset($logfile)) { - return false; + /** + * Returns new group instance with only those payment methods, which are available for provided amount. + * + * @param integer $amount + * @param string $currency + * + * @return WebToPay_PaymentMethodGroup + */ + public function filterForAmount($amount, $currency) { + $group = new WebToPay_PaymentMethodGroup($this->groupKey, $this->translations, $this->defaultLanguage); + foreach ($this->getPaymentMethods() as $paymentMethod) { + if ($paymentMethod->isAvailableForAmount($amount, $currency)) { + $group->addPaymentMethod($paymentMethod); + } } + return $group; + } - $fp = @fopen($logfile, 'a'); - if (!$fp) { - throw new WebToPayException( - self::_('Can\'t write to logfile: %s', $logfile), WebToPayException::E_LOG); + /** + * Returns new country instance with only those payment methods, which are returns or not iban number after payment + * + * @param boolean $isIban + * + * @return WebToPay_PaymentMethodGroup + */ + public function filterForIban($isIban = true) { + $group = new WebToPay_PaymentMethodGroup($this->groupKey, $this->translations, $this->defaultLanguage); + foreach ($this->getPaymentMethods() as $paymentMethod) { + if ($paymentMethod->isIban() == $isIban) { + $group->addPaymentMethod($paymentMethod); + } } + return $group; + } - $logline = array(); + /** + * Returns whether this group has no payment methods + * + * @return boolean + */ + public function isEmpty() { + return count($this->paymentMethods) === 0; + } - // *log* type - $logline[] = $type; + /** + * Loads payment methods from given XML node + * + * @param SimpleXMLElement $groupNode + */ + public function fromXmlNode($groupNode) { + foreach ($groupNode->payment_type as $paymentTypeNode) { + $key = (string) $paymentTypeNode->attributes()->key; + $titleTranslations = array(); + foreach ($paymentTypeNode->title as $titleNode) { + $titleTranslations[(string) $titleNode->attributes()->language] = (string) $titleNode; + } + $logoTranslations = array(); + foreach ($paymentTypeNode->logo_url as $logoNode) { + if ((string) $logoNode !== '') { + $logoTranslations[(string) $logoNode->attributes()->language] = (string) $logoNode; + } + } + $minAmount = null; + $maxAmount = null; + $currency = null; + $isIban = false; + $baseCurrency = null; + if (isset($paymentTypeNode->min)) { + $minAmount = (int) $paymentTypeNode->min->attributes()->amount; + $currency = (string) $paymentTypeNode->min->attributes()->currency; + } + if (isset($paymentTypeNode->max)) { + $maxAmount = (int) $paymentTypeNode->max->attributes()->amount; + $currency = (string) $paymentTypeNode->max->attributes()->currency; + } - // *log* REMOTE_ADDR - if (isset($_SERVER['REMOTE_ADDR'])) { - $logline[] = $_SERVER['REMOTE_ADDR']; - } - else { - $logline[] = '-'; + if (isset($paymentTypeNode->is_iban)) { + $isIban = (int) $paymentTypeNode->is_iban; + } + if (isset($paymentTypeNode->base_currency)) { + $baseCurrency = (string) $paymentTypeNode->base_currency; + } + $this->addPaymentMethod($this->createPaymentMethod( + $key, $minAmount, $maxAmount, $currency, $logoTranslations, $titleTranslations, $isIban, $baseCurrency + )); } + } - // *log* datetime - $logline[] = date('[Y-m-d H:i:s O]'); + /** + * Method to create new payment method instances. Overwrite if you have to use some other subclass. + * + * @param string $key + * @param integer $minAmount + * @param integer $maxAmount + * @param string $currency + * @param array $logoList + * @param array $titleTranslations + * @param bool $isIban + * @param null $baseCurrency + * + * @return WebToPay_PaymentMethod + */ + protected function createPaymentMethod( + $key, $minAmount, $maxAmount, $currency, array $logoList = array(), array $titleTranslations = array(), + $isIban = false, $baseCurrency = null + ) { + return new WebToPay_PaymentMethod( + $key, $minAmount, $maxAmount, $currency, $logoList, $titleTranslations, $this->defaultLanguage, + $isIban, $baseCurrency + ); + } +} - // *log* version - $logline[] = 'v'.self::VERSION.':'; +/** + * Loads data about payment methods and constructs payment method list object from that data + * You need SimpleXML support to use this feature + */ +class WebToPay_PaymentMethodListProvider { - // *log* message - $logline[] = $msg; + /** + * @var integer + */ + protected $projectId; - $logline = implode(' ', $logline)."\n"; - fwrite($fp, $logline); - fclose($fp); + /** + * @var WebToPay_WebClient + */ + protected $webClient; - // clear big log file - if (filesize($logfile) > 1024 * 1024 * pi()) { - copy($logfile, $logfile.'.old'); - unlink($logfile); - } - } + /** + * Holds constructed method lists by currency + * + * @var WebToPay_PaymentMethodList[] + */ + protected $methodListCache = array(); + /** + * Builds various request URLs + * + * @var WebToPay_UrlBuilder $urlBuilder + */ + protected $urlBuilder; /** - * Sends SMS answer. + * Constructs object * - * @param array $answer - * @return void + * @param integer $projectId + * @param WebToPay_WebClient $webClient + * @param WebToPay_UrlBuilder $urlBuilder + * + * @throws WebToPayException if SimpleXML is not available */ - public static function smsAnswer($answer) { - $url = parse_url(self::SMS_ANSWER_URL); - if ('https' == $url['scheme']) { - $host = 'ssl://'.$url['host']; - $port = 443; - } - else { - $host = $url['host']; - $port = 80; + public function __construct( + $projectId, + WebToPay_WebClient $webClient, + WebToPay_UrlBuilder $urlBuilder + ) + { + $this->projectId = $projectId; + $this->webClient = $webClient; + $this->urlBuilder = $urlBuilder; + + if (!function_exists('simplexml_load_string')) { + throw new WebToPayException('You have to install libxml to use payment methods API'); } + } - try { - $fp = fsockopen($host, $port, $errno, $errstr, 30); - if (!$fp) { - throw new WebToPayException( - self::_('Can\'t connect to %s', self::SMS_ANSWER_URL), - WebToPayException::E_SMS_ANSWER); + /** + * Gets payment method list for specified currency + * + * @param string $currency + * + * @return WebToPay_PaymentMethodList + * + * @throws WebToPayException + */ + public function getPaymentMethodList($currency) { + if (!isset($this->methodListCache[$currency])) { + $xmlAsString = $this->webClient->get($this->urlBuilder->buildForPaymentsMethodList($this->projectId, $currency)); + $useInternalErrors = libxml_use_internal_errors(false); + $rootNode = simplexml_load_string($xmlAsString); + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + if (!$rootNode) { + throw new WebToPayException('Unable to load XML from remote server'); } + $methodList = new WebToPay_PaymentMethodList($this->projectId, $currency); + $methodList->fromXmlNode($rootNode); + $this->methodListCache[$currency] = $methodList; + } + return $this->methodListCache[$currency]; + } +} - $data = array( - 'id' => $answer['id'], - 'msg' => $answer['msg'], - 'transaction' => md5($answer['sign_password'].'|'.$answer['id']), - ); - - $query = $url['path'].'?'.http_build_query($data); - - $out = "GET " . $query . " HTTP/1.1\r\n"; - $out .= "Host: ".$url['host']."\r\n"; - $out .= "Connection: Close\r\n\r\n"; +/** + * Raised on error in callback + */ +class WebToPay_Exception_Callback extends WebToPayException { - $content = ''; +} - fwrite($fp, $out); - while (!feof($fp)) $content .= fgets($fp, 8192); - fclose($fp); +/** + * Raised if configuration is incorrect + */ +class WebToPay_Exception_Configuration extends WebToPayException { - list($header, $content) = explode("\r\n\r\n", $content, 2); +} - $content = trim($content); - if (strpos($content, 'OK') !== 0) { - throw new WebToPayException( - self::_('Error: %s', $content), - WebToPayException::E_SMS_ANSWER); - } - } - catch (WebToPayException $e) { - if (isset($answer['log'])) { - self::log('ERR', - self::mikroAnswerToLog($answer). - ' ('. get_class($e).': '. $e->getMessage().')', - $answer['log']); - } - throw $e; - } +/** + * Raised on validation error in passed data when building the request + */ +class WebToPay_Exception_Validation extends WebToPayException { - if (isset($answer['log'])) { - self::log('OK', self::mikroAnswerToLog($answer), $answer['log']); + public function __construct($message, $code = 0, $field = null, Exception $previousException = null) { + parent::__construct($message, $code, $previousException); + if ($field) { + $this->setField($field); } - } +} +/** + * Class to hold information about payment method + */ +class WebToPay_PaymentMethod { + /** + * Assigned key for this payment method + * + * @var string + */ + protected $key; /** - * I18n support. + * Logo url list by language. Usually logo is same for all languages, but exceptions exist + * + * @var array */ - public static function _() { - $args = func_get_args(); - if (sizeof($args) > 1) { - return call_user_func_array('sprintf', $args); - } - else { - return $args[0]; - } - } + protected $logoList; -} + /** + * Title list by language + * + * @var array + */ + protected $titleTranslations; + /** + * Default language to use for titles + * + * @var string + */ + protected $defaultLanguage; + /** + * @var boolean + */ + protected $isIban; -class WebToPayException extends Exception { + /** + * @var string + */ + protected $baseCurrency; /** - * Missing field. + * Constructs object + * + * @param string $key + * @param integer $minAmount + * @param integer $maxAmount + * @param string $currency + * @param array $logoList + * @param array $titleTranslations + * @param string $defaultLanguage + * @param bool $isIban + * @param string $baseCurrency */ - const E_MISSING = 1; + public function __construct( + $key, $minAmount, $maxAmount, $currency, array $logoList = array(), array $titleTranslations = array(), + $defaultLanguage = 'lt', $isIban = false, $baseCurrency = null + ) { + $this->key = $key; + $this->minAmount = $minAmount; + $this->maxAmount = $maxAmount; + $this->currency = $currency; + $this->logoList = $logoList; + $this->titleTranslations = $titleTranslations; + $this->defaultLanguage = $defaultLanguage; + $this->isIban = $isIban; + $this->baseCurrency = $baseCurrency; + } /** - * Invalid field value. + * Sets default language for titles. + * Returns itself for fluent interface + * + * @param string $language + * + * @return WebToPay_PaymentMethod */ - const E_INVALID = 2; + public function setDefaultLanguage($language) { + $this->defaultLanguage = $language; + return $this; + } /** - * Max length exceeded. + * Gets default language for titles + * + * @return string */ - const E_MAXLEN = 3; + public function getDefaultLanguage() { + return $this->defaultLanguage; + } /** - * Regexp for field value doesn't match. + * Get assigned payment method key + * + * @return string */ - const E_REGEXP = 4; + public function getKey() { + return $this->key; + } /** - * Missing or invalid user given parameters. + * Gets logo url for this payment method. Uses specified language or default one. + * If logotype is not found for specified language, null is returned. + * + * @param string [Optional] $languageCode + * + * @return string|null */ - const E_USER_PARAMS = 5; + public function getLogoUrl($languageCode = null) { + if ($languageCode !== null && isset($this->logoList[$languageCode])) { + return $this->logoList[$languageCode]; + } elseif (isset($this->logoList[$this->defaultLanguage])) { + return $this->logoList[$this->defaultLanguage]; + } else { + return null; + } + } /** - * Logging errors + * Gets title for this payment method. Uses specified language or default one. + * + * @param string [Optional] $languageCode + * + * @return string */ - const E_LOG = 6; + public function getTitle($languageCode = null) { + if ($languageCode !== null && isset($this->titleTranslations[$languageCode])) { + return $this->titleTranslations[$languageCode]; + } elseif (isset($this->titleTranslations[$this->defaultLanguage])) { + return $this->titleTranslations[$this->defaultLanguage]; + } else { + return $this->key; + } + } /** - * SMS answer errors + * Checks if this payment method can be used for specified amount. + * Throws exception if currency checked is not the one, for which payment method list was downloaded. + * + * @param integer $amount + * @param string $currency + * + * @return boolean + * + * @throws WebToPayException */ - const E_SMS_ANSWER = 7; + public function isAvailableForAmount($amount, $currency) { + if ($this->currency !== $currency) { + throw new WebToPayException( + 'Currencies does not match. You have to get payment types for the currency you are checking. Given currency: ' + . $currency . ', available currency: ' . $this->currency + ); + } + return ( + ($this->minAmount === null || $amount >= $this->minAmount) + && ($this->maxAmount === null || $amount <= $this->maxAmount) + ); + } + /** + * Returns min amount for this payment method. If no min amount is specified, returns empty string. + * + * @return string + */ + public function getMinAmountAsString() { + return $this->minAmount === null ? '' : ($this->minAmount . ' ' . $this->currency); + } + /** + * Returns max amount for this payment method. If no max amount is specified, returns empty string. + * + * @return string + */ + public function getMaxAmountAsString() { + return $this->maxAmount === null ? '' : ($this->maxAmount . ' ' . $this->currency); + } - protected $field_name = false; + /** + * Set if this method returns IBAN number after payment + * + * @param boolean $isIban + */ + public function setIsIban($isIban) { + $this->isIban = $isIban == 1; + } - public function setField($field_name) { - $this->field_name = $field_name; + /** + * Get if this method returns IBAN number after payment + * + * @return bool + */ + public function isIban() { + return $this->isIban; } - public function getField() { - return $this->field_name; + /** + * Setter of BaseCurrency + * + * @param string $baseCurrency + */ + public function setBaseCurrency($baseCurrency) + { + $this->baseCurrency = $baseCurrency; } + /** + * Getter of BaseCurrency + * + * @return string + */ + public function getBaseCurrency() + { + return $this->baseCurrency; + } } + +/** + * Utility class + */ +class WebToPay_Util { + + /** + * Decodes url-safe-base64 encoded string + * Url-safe-base64 is same as base64, but + is replaced to - and / to _ + * + * @param string $encodedText + * + * @return string + */ + public function decodeSafeUrlBase64($encodedText) { + return base64_decode(strtr($encodedText, array('-' => '+', '_' => '/'))); + } + + /** + * Encodes string to url-safe-base64 + * Url-safe-base64 is same as base64, but + is replaced to - and / to _ + * + * @param string $text + * + * @return string + */ + public function encodeSafeUrlBase64($text) { + return strtr(base64_encode($text), array('+' => '-', '/' => '_')); + } + + /** + * Parses HTTP query to array + * + * @param string $query + * + * @return array + */ + public function parseHttpQuery($query) { + $params = array(); + parse_str($query, $params); + if (get_magic_quotes_gpc()) { + $params = $this->stripSlashesRecursively($params); + } + return $params; + } + + /** + * Strips slashes recursively, so this method can be used on arrays with more than one level + * + * @param mixed $data + * + * @return mixed + */ + protected function stripSlashesRecursively($data) { + if (is_array($data)) { + $result = array(); + foreach ($data as $key => $value) { + $result[stripslashes($key)] = $this->stripSlashesRecursively($value); + } + return $result; + } else { + return stripslashes($data); + } + } +} + +/** + * Class with all information about available payment methods for some project, optionally filtered by some amount. + */ +class WebToPay_PaymentMethodList { + /** + * Holds available payment countries + * + * @var WebToPay_PaymentMethodCountry[] + */ + protected $countries; + + /** + * Default language for titles + * + * @var string + */ + protected $defaultLanguage; + + /** + * Project ID, to which this method list is valid + * + * @var integer + */ + protected $projectId; + + /** + * Currency for min and max amounts in this list + * + * @var string + */ + protected $currency; + + /** + * If this list is filtered for some amount, this field defines it + * + * @var integer + */ + protected $amount; + + /** + * Constructs object + * + * @param integer $projectId + * @param string $currency currency for min and max amounts in this list + * @param string $defaultLanguage + * @param integer $amount null if this list is not filtered by amount + */ + public function __construct($projectId, $currency, $defaultLanguage = 'lt', $amount = null) { + $this->projectId = $projectId; + $this->countries = array(); + $this->defaultLanguage = $defaultLanguage; + $this->currency = $currency; + $this->amount = $amount; + } + + /** + * Sets default language for titles. + * Returns itself for fluent interface + * + * @param string $language + * + * @return WebToPay_PaymentMethodList + */ + public function setDefaultLanguage($language) { + $this->defaultLanguage = $language; + foreach ($this->countries as $country) { + $country->setDefaultLanguage($language); + } + return $this; + } + + /** + * Gets default language for titles + * + * @return string + */ + public function getDefaultLanguage() { + return $this->defaultLanguage; + } + + /** + * Gets project ID for this payment method list + * + * @return integer + */ + public function getProjectId() { + return $this->projectId; + } + + /** + * Gets currency for min and max amounts in this list + * + * @return string + */ + public function getCurrency() { + return $this->currency; + } + + /** + * Gets whether this list is already filtered for some amount + * + * @return boolean + */ + public function isFiltered() { + return $this->amount !== null; + } + + /** + * Returns available countries + * + * @return WebToPay_PaymentMethodCountry[] + */ + public function getCountries() { + return $this->countries; + } + + /** + * Adds new country to payment methods. If some other country with same code was registered earlier, overwrites it. + * Returns added country instance + * + * @param WebToPay_PaymentMethodCountry $country + * + * @return WebToPay_PaymentMethodCountry + */ + public function addCountry(WebToPay_PaymentMethodCountry $country) { + return $this->countries[$country->getCode()] = $country; + } + + /** + * Gets country object with specified country code. If no country with such country code is found, returns null. + * + * @param string $countryCode + * + * @return null|WebToPay_PaymentMethodCountry + */ + public function getCountry($countryCode) { + return isset($this->countries[$countryCode]) ? $this->countries[$countryCode] : null; + } + + /** + * Returns new payment method list instance with only those payment methods, which are available for provided + * amount. + * Returns itself, if list is already filtered and filter amount matches the given one. + * + * @param integer $amount + * @param string $currency + * + * @return WebToPay_PaymentMethodList + * + * @throws WebToPayException if this list is already filtered and not for provided amount + */ + public function filterForAmount($amount, $currency) { + if ($currency !== $this->currency) { + throw new WebToPayException( + 'Currencies do not match. Given currency: ' . $currency . ', currency in list: ' . $this->currency + ); + } + if ($this->isFiltered()) { + if ($this->amount === $amount) { + return $this; + } else { + throw new WebToPayException('This list is already filtered, use unfiltered list instead'); + } + } else { + $list = new WebToPay_PaymentMethodList($this->projectId, $currency, $this->defaultLanguage, $amount); + foreach ($this->getCountries() as $country) { + $country = $country->filterForAmount($amount, $currency); + if (!$country->isEmpty()) { + $list->addCountry($country); + } + } + return $list; + } + } + + /** + * Loads countries from given XML node + * + * @param SimpleXMLElement $xmlNode + */ + public function fromXmlNode($xmlNode) { + foreach ($xmlNode->country as $countryNode) { + $titleTranslations = array(); + foreach ($countryNode->title as $titleNode) { + $titleTranslations[(string) $titleNode->attributes()->language] = (string) $titleNode; + } + $this->addCountry($this->createCountry((string) $countryNode->attributes()->code, $titleTranslations)) + ->fromXmlNode($countryNode); + } + } + + /** + * Method to create new country instances. Overwrite if you have to use some other country subtype. + * + * @param string $countryCode + * @param array $titleTranslations + * + * @return WebToPay_PaymentMethodCountry + */ + protected function createCountry($countryCode, array $titleTranslations = array()) { + return new WebToPay_PaymentMethodCountry($countryCode, $titleTranslations, $this->defaultLanguage); + } +} + +/** + * Sends answer to SMS payment if it was not provided with response to callback + */ +class WebToPay_SmsAnswerSender { + + /** + * @var string + */ + protected $password; + + /** + * @var WebToPay_WebClient + */ + protected $webClient; + + /** + * @var WebToPay_UrlBuilder $urlBuilder + */ + protected $urlBuilder; + + /** + * Constructs object + * + * @param string $password + * @param WebToPay_WebClient $webClient + * @param WebToPay_UrlBuilder $urlBuilder + */ + public function __construct( + $password, + WebToPay_WebClient $webClient, + WebToPay_UrlBuilder $urlBuilder + ) { + $this->password = $password; + $this->webClient = $webClient; + $this->urlBuilder = $urlBuilder; + } + + /** + * Sends answer by sms ID get from callback. Answer can be send only if it was not provided + * when responding to callback + * + * @param integer $smsId + * @param string $text + * + * @throws WebToPayException + */ + public function sendAnswer($smsId, $text) { + $content = $this->webClient->get($this->urlBuilder->buildForSmsAnswer(), array( + 'id' => $smsId, + 'msg' => $text, + 'transaction' => md5($this->password . '|' . $smsId), + )); + if (strpos($content, 'OK') !== 0) { + throw new WebToPayException( + sprintf('Error: %s', $content), + WebToPayException::E_SMS_ANSWER + ); + } + } +} + + +/** + * Creates objects. Also caches to avoid creating several instances of same objects + */ +class WebToPay_Factory { + + const ENV_PRODUCTION = 'production'; + const ENV_SANDBOX = 'sandbox'; + + /** + * @var array + */ + protected static $defaultConfiguration = array( + 'routes' => array( + self::ENV_PRODUCTION => array( + 'publicKey' => 'http://www.paysera.com/download/public.key', + 'payment' => 'https://www.paysera.com/pay/', + 'paymentMethodList' => 'https://www.paysera.com/new/api/paymentMethods/', + 'smsAnswer' => 'https://www.paysera.com/psms/respond/', + ), + self::ENV_SANDBOX => array( + 'publicKey' => 'http://sandbox.paysera.com/download/public.key', + 'payment' => 'https://sandbox.paysera.com/pay/', + 'paymentMethodList' => 'https://sandbox.paysera.com/new/api/paymentMethods/', + 'smsAnswer' => 'https://sandbox.paysera.com/psms/respond/', + ), + ) + ); + + /** + * @var string + */ + protected $environment; + + /** + * @var array + */ + protected $configuration; + + /** + * @var WebToPay_WebClient + */ + protected $webClient = null; + + /** + * @var WebToPay_CallbackValidator + */ + protected $callbackValidator = null; + + /** + * @var WebToPay_RequestBuilder + */ + protected $requestBuilder = null; + + /** + * @var WebToPay_Sign_SignCheckerInterface + */ + protected $signer = null; + + /** + * @var WebToPay_SmsAnswerSender + */ + protected $smsAnswerSender = null; + + /** + * @var WebToPay_PaymentMethodListProvider + */ + protected $paymentMethodListProvider = null; + + /** + * @var WebToPay_Util + */ + protected $util = null; + + /** + * @var WebToPay_UrlBuilder + */ + protected $urlBuilder = null; + + + /** + * Constructs object. + * Configuration keys: projectId, password + * They are required only when some object being created needs them, + * if they are not found at that moment - exception is thrown + * + * @param array $configuration + */ + public function __construct(array $configuration = array()) { + + $this->configuration = array_merge(self::$defaultConfiguration, $configuration); + $this->environment = self::ENV_PRODUCTION; + } + + /** + * If passed true the factory will use sandbox when constructing URLs + * + * @param $enableSandbox + * @return self + */ + public function useSandbox($enableSandbox) + { + if ($enableSandbox) { + $this->environment = self::ENV_SANDBOX; + } else { + $this->environment = self::ENV_PRODUCTION; + } + return $this; + } + + /** + * Creates or gets callback validator instance + * + * @return WebToPay_CallbackValidator + * + * @throws WebToPay_Exception_Configuration + */ + public function getCallbackValidator() { + if ($this->callbackValidator === null) { + if (!isset($this->configuration['projectId'])) { + throw new WebToPay_Exception_Configuration('You have to provide project ID'); + } + $this->callbackValidator = new WebToPay_CallbackValidator( + $this->configuration['projectId'], + $this->getSigner(), + $this->getUtil() + ); + } + return $this->callbackValidator; + } + + /** + * Creates or gets request builder instance + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_RequestBuilder + */ + public function getRequestBuilder() { + if ($this->requestBuilder === null) { + if (!isset($this->configuration['password'])) { + throw new WebToPay_Exception_Configuration('You have to provide project password to sign request'); + } + if (!isset($this->configuration['projectId'])) { + throw new WebToPay_Exception_Configuration('You have to provide project ID'); + } + $this->requestBuilder = new WebToPay_RequestBuilder( + $this->configuration['projectId'], + $this->configuration['password'], + $this->getUtil(), + $this->getUrlBuilder() + ); + } + return $this->requestBuilder; + } + + /** + * @return WebToPay_UrlBuilder + */ + public function getUrlBuilder() { + if ($this->urlBuilder === null) { + $this->urlBuilder = new WebToPay_UrlBuilder( + $this->configuration, + $this->environment + ); + } + return $this->urlBuilder; + } + + /** + * Creates or gets SMS answer sender instance + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_SmsAnswerSender + */ + public function getSmsAnswerSender() { + if ($this->smsAnswerSender === null) { + if (!isset($this->configuration['password'])) { + throw new WebToPay_Exception_Configuration('You have to provide project password'); + } + $this->smsAnswerSender = new WebToPay_SmsAnswerSender( + $this->configuration['password'], + $this->getWebClient(), + $this->getUrlBuilder() + ); + } + return $this->smsAnswerSender; + } + + /** + * Creates or gets payment list provider instance + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_PaymentMethodListProvider + */ + public function getPaymentMethodListProvider() { + if ($this->paymentMethodListProvider === null) { + if (!isset($this->configuration['projectId'])) { + throw new WebToPay_Exception_Configuration('You have to provide project ID'); + } + $this->paymentMethodListProvider = new WebToPay_PaymentMethodListProvider( + $this->configuration['projectId'], + $this->getWebClient(), + $this->getUrlBuilder() + + ); + } + return $this->paymentMethodListProvider; + } + + /** + * Creates or gets signer instance. Chooses SS2 signer if openssl functions are available, SS1 in other case + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_Sign_SignCheckerInterface + * + * @throws WebToPayException + */ + protected function getSigner() { + if ($this->signer === null) { + if (function_exists('openssl_pkey_get_public')) { + $webClient = $this->getWebClient(); + $publicKey = $webClient->get($this->getUrlBuilder()->buildForPublicKey()); + if (!$publicKey) { + throw new WebToPayException('Cannot download public key from WebToPay website'); + } + $this->signer = new WebToPay_Sign_SS2SignChecker($publicKey, $this->getUtil()); + } else { + if (!isset($this->configuration['password'])) { + throw new WebToPay_Exception_Configuration( + 'You have to provide project password if OpenSSL is unavailable' + ); + } + $this->signer = new WebToPay_Sign_SS1SignChecker($this->configuration['password']); + } + } + return $this->signer; + } + + /** + * Creates or gets web client instance + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_WebClient + */ + protected function getWebClient() { + if ($this->webClient === null) { + $this->webClient = new WebToPay_WebClient(); + } + return $this->webClient; + } + + /** + * Creates or gets util instance + * + * @throws WebToPay_Exception_Configuration + * + * @return WebToPay_Util + */ + protected function getUtil() { + if ($this->util === null) { + $this->util = new WebToPay_Util(); + } + return $this->util; + } +} + + +/** + * Sign checker which checks SS1 signature. SS1 does not depend on SSL functions + */ +class WebToPay_Sign_SS1SignChecker implements WebToPay_Sign_SignCheckerInterface { + + /** + * @var string + */ + protected $projectPassword; + + /** + * Constructs object + * + * @param string $projectPassword + */ + public function __construct($projectPassword) { + $this->projectPassword = $projectPassword; + } + + /** + * Check for SS1, which is not depend on openssl functions. + * + * @param array $request + * + * @return boolean + * + * @throws WebToPay_Exception_Callback + */ + public function checkSign(array $request) { + if (!isset($request['data']) || !isset($request['ss1'])) { + throw new WebToPay_Exception_Callback('Not enough parameters in callback. Possible version mismatch'); + } + + return md5($request['data'] . $this->projectPassword) === $request['ss1']; + } +} + +/** + * Checks SS2 signature. Depends on SSL functions + */ +class WebToPay_Sign_SS2SignChecker implements WebToPay_Sign_SignCheckerInterface { + + /** + * @var string + */ + protected $publicKey; + + /** + * @var WebToPay_Util + */ + protected $util; + + /** + * Constructs object + * + * @param string $publicKey + * @param WebToPay_Util $util + */ + public function __construct($publicKey, WebToPay_Util $util) { + $this->publicKey = $publicKey; + $this->util = $util; + } + + /** + * Checks signature + * + * @param array $request + * + * @return boolean + * + * @throws WebToPay_Exception_Callback + */ + public function checkSign(array $request) { + if (!isset($request['data']) || !isset($request['ss2'])) { + throw new WebToPay_Exception_Callback('Not enough parameters in callback. Possible version mismatch'); + } + + $ss2 = $this->util->decodeSafeUrlBase64($request['ss2']); + $ok = openssl_verify($request['data'], $ss2, $this->publicKey); + return $ok === 1; + } +} + +/** + * Interface for sign checker + */ +interface WebToPay_Sign_SignCheckerInterface { + + /** + * Checks whether request is signed properly + * + * @param array $request + * + * @return boolean + */ + public function checkSign(array $request); +} + +/** + * Simple web client + */ +class WebToPay_WebClient { + + /** + * Gets page contents by specified URI. Adds query data if provided to the URI + * Ignores status code of the response and header fields + * + * @param string $uri + * @param array $queryData + * + * @return string + * + * @throws WebToPayException + */ + public function get($uri, array $queryData = array()) { + if (count($queryData) > 0) { + $uri .= strpos($uri, '?') === false ? '?' : '&'; + $uri .= http_build_query($queryData, null, '&'); + } + $url = parse_url($uri); + if ('https' == $url['scheme']) { + $host = 'ssl://'.$url['host']; + $port = 443; + } else { + $host = $url['host']; + $port = 80; + } + + $fp = fsockopen($host, $port, $errno, $errstr, 30); + if (!$fp) { + throw new WebToPayException(sprintf('Cannot connect to %s', $uri), WebToPayException::E_INVALID); + } + + if(isset($url['query'])) { + $data = $url['path'].'?'.$url['query']; + } else { + $data = $url['path']; + } + + $out = "GET " . $data . " HTTP/1.0\r\n"; + $out .= "Host: ".$url['host']."\r\n"; + $out .= "Connection: Close\r\n\r\n"; + + $content = ''; + + fwrite($fp, $out); + while (!feof($fp)) $content .= fgets($fp, 8192); + fclose($fp); + + list($header, $content) = explode("\r\n\r\n", $content, 2); + + return trim($content); + } +} + +/** + * Payment method configuration for some country + */ +class WebToPay_PaymentMethodCountry { + /** + * @var string + */ + protected $countryCode; + + /** + * Holds available payment types for this country + * + * @var WebToPay_PaymentMethodGroup[] + */ + protected $groups; + + /** + * Default language for titles + * + * @var string + */ + protected $defaultLanguage; + + /** + * Translations array for this country. Holds associative array of country title by language codes. + * + * @var array + */ + protected $titleTranslations; + + /** + * Constructs object + * + * @param string $countryCode + * @param array $titleTranslations + * @param string $defaultLanguage + */ + public function __construct($countryCode, $titleTranslations, $defaultLanguage = 'lt') { + $this->countryCode = $countryCode; + $this->defaultLanguage = $defaultLanguage; + $this->titleTranslations = $titleTranslations; + $this->groups = array(); + } + + /** + * Sets default language for titles. + * Returns itself for fluent interface + * + * @param string $language + * + * @return WebToPay_PaymentMethodCountry + */ + public function setDefaultLanguage($language) { + $this->defaultLanguage = $language; + foreach ($this->groups as $group) { + $group->setDefaultLanguage($language); + } + return $this; + } + + /** + * Gets title of the group. Tries to get title in specified language. If it is not found or if language is not + * specified, uses default language, given to constructor. + * + * @param string [Optional] $languageCode + * + * @return string + */ + public function getTitle($languageCode = null) { + if ($languageCode !== null && isset($this->titleTranslations[$languageCode])) { + return $this->titleTranslations[$languageCode]; + } elseif (isset($this->titleTranslations[$this->defaultLanguage])) { + return $this->titleTranslations[$this->defaultLanguage]; + } else { + return $this->countryCode; + } + } + + /** + * Gets default language for titles + * + * @return string + */ + public function getDefaultLanguage() { + return $this->defaultLanguage; + } + + /** + * Gets country code + * + * @return string + */ + public function getCode() { + return $this->countryCode; + } + + /** + * Adds new group to payment methods for this country. + * If some other group was registered earlier with same key, overwrites it. + * Returns given group + * + * @param WebToPay_PaymentMethodGroup $group + * + * @return WebToPay_PaymentMethodGroup + */ + public function addGroup(WebToPay_PaymentMethodGroup $group) { + return $this->groups[$group->getKey()] = $group; + } + + /** + * Gets group object with specified group key. If no group with such key is found, returns null. + * + * @param string $groupKey + * + * @return null|WebToPay_PaymentMethodGroup + */ + public function getGroup($groupKey) { + return isset($this->groups[$groupKey]) ? $this->groups[$groupKey] : null; + } + + /** + * Returns payment method groups registered for this country. + * + * @return WebToPay_PaymentMethodGroup[] + */ + public function getGroups() { + return $this->groups; + } + + /** + * Gets payment methods in all groups + * + * @return WebToPay_PaymentMethod[] + */ + public function getPaymentMethods() { + $paymentMethods = array(); + foreach ($this->groups as $group) { + $paymentMethods = array_merge($paymentMethods, $group->getPaymentMethods()); + } + return $paymentMethods; + } + + /** + * Returns new country instance with only those payment methods, which are available for provided amount. + * + * @param integer $amount + * @param string $currency + * + * @return WebToPay_PaymentMethodCountry + */ + public function filterForAmount($amount, $currency) { + $country = new WebToPay_PaymentMethodCountry($this->countryCode, $this->titleTranslations, $this->defaultLanguage); + foreach ($this->getGroups() as $group) { + $group = $group->filterForAmount($amount, $currency); + if (!$group->isEmpty()) { + $country->addGroup($group); + } + } + return $country; + } + + /** + * Returns new country instance with only those payment methods, which are returns or not iban number after payment + * + * @param boolean $isIban + * + * @return WebToPay_PaymentMethodCountry + */ + public function filterForIban($isIban = true) { + $country = new WebToPay_PaymentMethodCountry($this->countryCode, $this->titleTranslations, $this->defaultLanguage); + foreach ($this->getGroups() as $group) { + $group = $group->filterForIban($isIban); + if (!$group->isEmpty()) { + $country->addGroup($group); + } + } + return $country; + } + + /** + * Returns whether this country has no groups + * + * @return boolean + */ + public function isEmpty() { + return count($this->groups) === 0; + } + + /** + * Loads groups from given XML node + * + * @param SimpleXMLElement $countryNode + */ + public function fromXmlNode($countryNode) { + foreach ($countryNode->payment_group as $groupNode) { + $key = (string) $groupNode->attributes()->key; + $titleTranslations = array(); + foreach ($groupNode->title as $titleNode) { + $titleTranslations[(string) $titleNode->attributes()->language] = (string) $titleNode; + } + $this->addGroup($this->createGroup($key, $titleTranslations))->fromXmlNode($groupNode); + } + } + + /** + * Method to create new group instances. Overwrite if you have to use some other group subtype. + * + * @param string $groupKey + * @param array $translations + * + * @return WebToPay_PaymentMethodGroup + */ + protected function createGroup($groupKey, array $translations = array()) { + return new WebToPay_PaymentMethodGroup($groupKey, $translations, $this->defaultLanguage); + } +} + + +/** + * Used to build a complete request URL. + * + * Class WebToPay_UrlBuilder + */ +class WebToPay_UrlBuilder { + + const PLACEHOLDER_KEY = '[domain]'; + + /** + * @var array + */ + protected $configuration = array(); + + /** + * @var string + */ + protected $environment; + + /** + * @var array + */ + protected $environmentSettings; + + /** + * @param array $configuration + * @param string $environment + */ + function __construct($configuration, $environment) + { + $this->configuration = $configuration; + $this->environment = $environment; + $this->environmentSettings = $this->configuration['routes'][$this->environment]; + } + + /** + * Builds a complete request URL based on the provided parameters + * + * @param $request + * @param null $language + * @return string + */ + public function buildForRequest($request, $language = null) { + return $this->createUrlFromRequestAndLanguage($request); + } + + /** + * Builds a complete URL for payment list API + * + * @param int $projectId + * @param string $currency + * @return string + */ + public function buildForPaymentsMethodList($projectId, $currency) { + $route = $this->environmentSettings['paymentMethodList']; + return $route . $projectId . '/currency:' . $currency; + } + + /** + * Builds a complete URL for Sms Answer + * + * @return string + */ + public function buildForSmsAnswer() { + $route = $this->environmentSettings['smsAnswer']; + return $route; + } + + /** + * Build the url to the public key + * + * @return string + */ + public function buildForPublicKey() { + $route = $this->environmentSettings['publicKey']; + return $route; + } + + /** + * Creates an URL from the request and data provided. + * + * @param array $request + * @return string + */ + protected function createUrlFromRequestAndLanguage($request) { + $url = $this->getPaymentUrl() . '?' . http_build_query($request, null, '&'); + return preg_replace('/[\r\n]+/is', '', $url); + } + + /** + * Returns payment url. Argument is same as lang parameter in request data + * + * @return string $url + */ + protected function getPaymentUrl() { + $route = $this->environmentSettings['payment']; + return $route; + } +} + + +/** + * Builds and signs requests + */ +class WebToPay_RequestBuilder { + + /** + * @var string + */ + protected $projectPassword; + + /** + * @var WebToPay_Util + */ + protected $util; + + /** + * @var integer + */ + protected $projectId; + + + /** + * @var WebToPay_UrlBuilder $urlBuilder + */ + protected $urlBuilder; + + /** + * Constructs object + * + * @param integer $projectId + * @param string $projectPassword + * @param WebToPay_Util $util + * @param WebToPay_UrlBuilder $urlBuilder + */ + public function __construct( + $projectId, + $projectPassword, + WebToPay_Util $util, + WebToPay_UrlBuilder $urlBuilder + ) + { + $this->projectId = $projectId; + $this->projectPassword = $projectPassword; + $this->util = $util; + $this->urlBuilder = $urlBuilder; + } + + /** + * Builds request data array. + * + * This method checks all given data and generates correct request data + * array or raises WebToPayException on failure. + * + * @param array $data information about current payment request + * + * @return array + * + * @throws WebToPayException + */ + public function buildRequest($data) { + $this->validateRequest($data, self::getRequestSpec()); + $data['version'] = WebToPay::VERSION; + $data['projectid'] = $this->projectId; + unset($data['repeat_request']); + return $this->createRequest($data); + } + + /** + * Builds the full request url (including the protocol and the domain) + * + * @param array $data + * @return string + */ + public function buildRequestUrlFromData($data) { + $language = isset($data['lang']) ? $data['lang'] : null; + $request = $this->buildRequest($data); + return $this->urlBuilder->buildForRequest($request, $language); + } + + /** + * Builds repeat request data array. + * + * This method checks all given data and generates correct request data + * array or raises WebToPayException on failure. + * + * @param string $orderId order id of repeated request + * + * @return array + * + * @throws WebToPayException + */ + public function buildRepeatRequest($orderId) { + $data['orderid'] = $orderId; + $data['version'] = WebToPay::VERSION; + $data['projectid'] = $this->projectId; + $data['repeat_request'] = '1'; + return $this->createRequest($data); + } + + /** + * Builds the full request url for a repeated request (including the protocol and the domain) + * + * @param string $orderId order id of repeated request + * @return string + */ + public function buildRepeatRequestUrlFromOrderId($orderId) { + $request = $this->buildRepeatRequest($orderId); + return $this->urlBuilder->buildForRequest($request); + } + + /** + * Checks data to be valid by passed specification + * + * @param array $data + * @param array $specs + * + * @throws WebToPay_Exception_Validation + */ + protected function validateRequest($data, $specs) { + foreach ($specs as $spec) { + list($name, $maxlen, $required, $regexp) = $spec; + if ($required && !isset($data[$name])) { + throw new WebToPay_Exception_Validation( + sprintf("'%s' is required but missing.", $name), + WebToPayException::E_MISSING, + $name + ); + } + + if (!empty($data[$name])) { + if ($maxlen && strlen($data[$name]) > $maxlen) { + throw new WebToPay_Exception_Validation(sprintf( + "'%s' value is too long (%d), %d characters allowed.", + $name, + strlen($data[$name]), + $maxlen + ), WebToPayException::E_MAXLEN, $name); + } + + if ($regexp !== '' && !preg_match($regexp, $data[$name])) { + throw new WebToPay_Exception_Validation( + sprintf("'%s' value '%s' is invalid.", $name, $data[$name]), + WebToPayException::E_REGEXP, + $name + ); + } + } + } + } + + /** + * Makes request data array from parameters, also generates signature + * + * @param array $request + * + * @return array + */ + protected function createRequest(array $request) { + $data = $this->util->encodeSafeUrlBase64(http_build_query($request, null, '&')); + return array( + 'data' => $data, + 'sign' => md5($data . $this->projectPassword), + ); + } + + /** + * Returns specification of fields for request. + * + * Array structure: + * name – request item name + * maxlen – max allowed value for item + * required – is this item is required + * regexp – regexp to test item value + * + * @return array + */ + protected static function getRequestSpec() { + return array( + array('orderid', 40, true, ''), + array('accepturl', 255, true, ''), + array('cancelurl', 255, true, ''), + array('callbackurl', 255, true, ''), + array('lang', 3, false, '/^[a-z]{3}$/i'), + array('amount', 11, false, '/^\d+$/'), + array('currency', 3, false, '/^[a-z]{3}$/i'), + array('payment', 20, false, ''), + array('country', 2, false, '/^[a-z_]{2}$/i'), + array('paytext', 255, false, ''), + array('p_firstname', 255, false, ''), + array('p_lastname', 255, false, ''), + array('p_email', 255, false, ''), + array('p_street', 255, false, ''), + array('p_city', 255, false, ''), + array('p_state', 20, false, ''), + array('p_zip', 20, false, ''), + array('p_countrycode', 2, false, '/^[a-z]{2}$/i'), + array('test', 1, false, '/^[01]$/'), + array('time_limit', 19, false, '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'), + ); + } +} \ No newline at end of file diff --git a/avactis-system/modules/payment_module_wtp_cc/payment_module_wtp_cc_api.php b/avactis-system/modules/payment_module_wtp_cc/payment_module_wtp_cc_api.php index 265cbc5..771dbd4 100644 --- a/avactis-system/modules/payment_module_wtp_cc/payment_module_wtp_cc_api.php +++ b/avactis-system/modules/payment_module_wtp_cc/payment_module_wtp_cc_api.php @@ -306,40 +306,18 @@ function getConfirmationData($orderId){ catch (WebToPayException $e) { echo get_class($e).': '.$e->getMessage(); } - + $acceptURL = str_replace('&','&',$buildRequest['accepturl']); $cancelURL = str_replace('&','&',$buildRequest['cancelurl']); $callbackURL = str_replace('&','&',$buildRequest['callbackurl']); - - + + $confirmationData = array( "FormAction" => WebToPay::PAY_URL, "FormMethod" => "POST", "DataFields" => array( - 'projectid' => $buildRequest['projectid'], - 'orderid' => $buildRequest['orderid'], - - 'amount' => $buildRequest['amount'], - 'currency' => $buildRequest['currency'], - 'lang' => $buildRequest['lang'], - - 'accepturl' => $buildRequest['accepturl'], - 'cancelurl' => $buildRequest['cancelurl'], - 'callbackurl' => $buildRequest['callbackurl'], - - 'payment' => $buildRequest['payment'], - 'country' => $buildRequest['country'], - - 'p_firstname' => $buildRequest['p_firstname'], - 'p_lastname' => $buildRequest['p_lastname'], - 'p_email' => $buildRequest['p_email'], - 'p_street' => $buildRequest['p_street'], - 'p_city' => $buildRequest['p_city'], - 'p_zip' => $buildRequest['p_zip'], - - 'test' => $buildRequest['test'], - 'version' => $buildRequest['version'], - 'sign' => $buildRequest['sign'], + 'data' => $buildRequest['data'], + 'sign' => $buildRequest['sign'], )); //=========================== logging request ======================== @@ -369,9 +347,11 @@ function processData($data, $order_id){ // The checking on the delivery to the selected place might be // added here // - - if($_GET[WebToPay::PREFIX.'status'] == 1) { - $this->validateOrder($order_id); + + $moduleData = $this->getSettings(); + $data = WebToPay::validateAndParseData($_GET, $moduleData['MODULE_METHOD_ID'], $moduleData['MODULE_METHOD_PASS']); + if($data['status'] == 1){ + $this->validateOrder($data, $data['orderid']); } $EventType = "ConfirmationSuccess"; @@ -383,9 +363,9 @@ function processData($data, $order_id){ return array("EventType" => $EventType, "statusChanged" => $result); } - function validateOrder($orderID){ + function validateOrder($data, $orderID){ - if(ltrim($_GET[WebToPay::PREFIX.'orderid'], "0") != $orderID) { + if(ltrim($data['orderid'], "0") != $orderID) { exit('Order ID mismatch!'); } @@ -398,21 +378,11 @@ function validateOrder($orderID){ $query->addSelectField('*'); $query->WhereValue('order_id', DB_EQ, $orderID); $Order = $application->db->getDB_Result($query); - - try { - WebToPay::toggleSS2(true); - $response = WebToPay::checkResponse($_GET, array( - 'projectid' => $moduleData['MODULE_METHOD_ID'], - 'sign_password' => $moduleData['MODULE_METHOD_PASS'], - )); - } catch (Exception $e) { - exit(get_class($e).': '.$e->getMessage()); - } - - if (intval(number_format($Order[0]['order_total'],2,'','')) > $_GET[WebToPay::PREFIX.'amount']){ + + if (intval(number_format($Order[0]['order_total'],2,'','')) > $data['amount']){ exit('Bad amount!'); } - else if ($Order[0]['currency_code'] != $_GET[WebToPay::PREFIX.'currency']){ + else if ($Order[0]['currency_code'] != $data['currency']){ exit('Bad currency!'); } else { modApiFunc("Checkout", "UpdatePaymentStatusInDB", $orderID, 2, 'Payment accepted.'); diff --git a/data.sql b/data.sql index 487bc86..d3a152c 100644 --- a/data.sql +++ b/data.sql @@ -5,7 +5,7 @@ INSERT INTO `asc_module` (`module_name`, `module_groups`, `module_description`, ('Payment_Module_Wtp_CC', 'PaymentModule,OnlineCC', '"Paysera" method payment module. See payment documentation', '0.1', 'EVP International', '', '', '2011-04-27', '1', '1'); INSERT INTO `asc_module_class` (`module_class_name`, `module_class_file`, `module_class_type`, `module_class_active`) VALUES -('Payment_Module_Wtp_CC', 'payment_module_wtp_cc/payment_module_wtp_cc_api.php', 'api', '1'); +('Payment_Module_Wtp_CC', '/avactis-system/modules/payment_module_wtp_cc/payment_module_wtp_cc_api.php', 'api', '1'); INSERT INTO `asc_checkout_pm_sm_currency_acceptance_rules` (`checkout_pm_sm_currency_acceptance_rules_module_id`, `checkout_pm_sm_currency_acceptance_rules_rule_name`, `checkout_pm_sm_currency_acceptance_rules_rule_selected`) VALUES ('F2A8C192-7C29-7218-B135-285582382EF6', 'MAIN_STORE_CURRENCY', 'true'); @@ -42,4 +42,18 @@ INSERT INTO `asc_resource_labels` (`res_prefix`, `res_label`, `res_text`) VALUES ('PM_WTP', 'MODULE_STATUS_INACTIVE', 'Inactive'), ('PM_WTP', 'MODULE_STATUS_FIELD_NAME', 'Module status'), ('PM_WTP', 'MODULE_STATUS_FIELD_NAME_DESCR', 'There are two possible statuses for this module - active and inactive'), -('PM_WTP', 'MODULE_TYPE', 'Payment Module'); \ No newline at end of file +('PM_WTP', 'MODULE_TYPE', 'Payment Module'); + +CREATE TABLE IF NOT EXISTS `asc_pm_wtp_settings` ( + `pm_wtp_setting_id` int(11) NOT NULL AUTO_INCREMENT, + `pm_wtp_setting_key` varchar(50) DEFAULT NULL, + `pm_wtp_setting_value` varchar(255) DEFAULT NULL, + PRIMARY KEY (`pm_wtp_setting_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=5 ; + + +INSERT INTO `asc_pm_wtp_settings` (`pm_wtp_setting_id`, `pm_wtp_setting_key`, `pm_wtp_setting_value`) VALUES +(1, 'MODULE_NAME', 's:7:"Paysera";'), +(2, 'MODULE_MODE', 's:1:"1";'), +(3, 'MODULE_METHOD_ID', 's:5:"project_id";'), +(4, 'MODULE_METHOD_PASS', 's:32:"project_password";'); \ No newline at end of file