. * * @package WebToPay * @author EVP International * @license http://www.gnu.org/licenses/lgpl.html * @version 1.6 * @link http://www.webtopay.com/ */ /** * Contains static methods for most used scenarios. */ class WebToPay { /** * WebToPay Library version. */ const VERSION = '1.6'; /** * Server URL where all requests should go. */ const PAY_URL = 'https://bank.paysera.com/pay/'; /** * Server URL where all non-lithuanian language requests should go. */ const PAYSERA_PAY_URL = 'https://bank.paysera.com/pay/'; /** * Server URL where we can get XML with payment method data. */ const XML_URL = 'https://www.paysera.com/new/api/paymentMethods/'; /** * SMS answer url. */ const SMS_ANSWER_URL = 'https://bank.paysera.com/psms/respond/'; /** * Builds request data array. * * This method checks all given data and generates correct request data * array or raises WebToPayException on failure. * * Possible keys: * https://developers.paysera.com/en/checkout/integrations/integration-specification * * @param array $data Information about current payment request * * @return array * * @throws WebToPayException on data validation error */ 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); } /** * Builds request and redirects user to payment window with generated request data * * Possible array keys are described here: * https://developers.paysera.com/en/checkout/integrations/integration-specification * * @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 redirectToPayment($data, $exit = false) { 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)); $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(); } } /** * Builds repeat request data array. * * This method checks all given data and generates correct request data * array or raises WebToPayException on failure. * * Method accepts single parameter $data of array type. All possible array * keys are described here: * https://developers.paysera.com/en/checkout/integrations/integration-specification * * @param array $data Information about current payment request * * @return array * * @throws WebToPayException on data validation error */ 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 payment url. Argument is same as lang parameter in request data * * @param string $language * @return string $url */ public static function getPaymentUrl($language = 'LIT') { return (in_array($language, array('lt', 'lit', 'LIT'))) ? self::PAY_URL : self::PAYSERA_PAY_URL; } /** * 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://developers.paysera.com/en/checkout/integrations/integration-specification * mikro: https://developers.paysera.com/en/sms-keywords/current#sms_keywords_specification * * If response is not correct, WebToPayException will be raised. * * @param array $query Response array * @param array $userData * * @return array * * @throws WebToPayException * @deprecated use validateAndParseData() and check status code yourself */ 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; 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 ($logFile) { self::log('OK', http_build_query($data, '', '&'), $logFile); } return $data; } catch (WebToPayException $exception) { if ($logFile && $exception->getCode() != WebToPayException::E_DEPRECATED_USAGE) { self::log('ERR', $exception . "\nQuery: " . http_build_query($query, '', '&'), $logFile); } throw $exception; } } /** * Parses request (query) data and validates its signature. * * @param array $query usually $_GET * @param integer $projectId * @param string $password * * @return array * * @throws WebToPayException */ 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; } /** * Sends SMS answer * * @param array $userData * * @throws WebToPayException * @throws WebToPay_Exception_Validation */ 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; } } /** * Gets available payment methods for project. Gets methods min and max amounts in specified currency. * * @param integer $projectId * @param float $amount * @param string $currency * * @return WebToPay_PaymentMethodList * * @throws WebToPayException */ public static function getPaymentMethodList($projectId, $amount, $currency = 'EUR') { $factory = new WebToPay_Factory(array('projectId' => $projectId)); return $factory->getPaymentMethodListProvider()->getPaymentMethodList($amount, $currency); } /** * Logs to file. Just skips logging if file is not writeable * * @param string $type * @param string $msg * @param string $logfile */ protected static function log($type, $msg, $logfile) { $fp = @fopen($logfile, 'a'); if (!$fp) { return; } $logline = array( $type, isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-', date('[Y-m-d H:i:s O]'), 'v' . self::VERSION . ':', $msg ); $logline = implode(' ', $logline)."\n"; fwrite($fp, $logline); fclose($fp); // 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 { /** * Missing field. */ const E_MISSING = 1; /** * Invalid field value. */ const E_INVALID = 2; /** * Max length exceeded. */ const E_MAXLEN = 3; /** * Regexp for field value doesn't match. */ const E_REGEXP = 4; /** * Missing or invalid user given parameters. */ const E_USER_PARAMS = 5; /** * Logging errors */ const E_LOG = 6; /** * SMS answer errors */ const E_SMS_ANSWER = 7; /** * Macro answer errors */ const E_STATUS = 8; /** * Library errors - if this happens, bug-report should be sent; also you can check for newer version */ const E_LIBRARY = 9; /** * Errors in remote service - it returns some invalid data */ const E_SERVICE = 10; /** * Deprecated usage errors */ const E_DEPRECATED_USAGE = 11; /** * @var string|boolean */ protected $fieldName = false; /** * Sets field which failed * * @param string $fieldName */ public function setField($fieldName) { $this->fieldName = $fieldName; } /** * Gets field which failed * * @return string|boolean false */ public function getField() { return $this->fieldName; } } /** * Class to hold information about payment method */ class WebToPay_PaymentMethod { /** * Assigned key for this payment method * * @var string */ protected $key; /** * Logo url list by language. Usually logo is same for all languages, but exceptions exist * * @var array */ protected $logoList; /** * Title list by language * * @var array */ protected $titleTranslations; /** * Default language to use for titles * * @var string */ protected $defaultLanguage; /** * @var boolean */ protected $isIban; /** * @var string */ protected $baseCurrency; /** * 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 */ 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; } /** * Sets default language for titles. * Returns itself for fluent interface * * @param string $language * * @return WebToPay_PaymentMethod */ public function setDefaultLanguage($language) { $this->defaultLanguage = $language; return $this; } /** * Gets default language for titles * * @return string */ public function getDefaultLanguage() { return $this->defaultLanguage; } /** * Get assigned payment method key * * @return string */ public function getKey() { return $this->key; } /** * 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 */ 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; } } /** * Gets title for this payment method. Uses specified language or default one. * * @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->key; } } /** * 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 */ 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); } /** * Set if this method returns IBAN number after payment * * @param boolean $isIban */ public function setIsIban($isIban) { $this->isIban = $isIban == 1; } /** * Get if this method returns IBAN number after payment * * @return bool */ public function isIban() { return $this->isIban; } /** * Setter of BaseCurrency * * @param string $baseCurrency */ public function setBaseCurrency($baseCurrency) { $this->baseCurrency = $baseCurrency; } /** * Getter of BaseCurrency * * @return string */ public function getBaseCurrency() { return $this->baseCurrency; } } /** * 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' => 'https://www.paysera.com/download/public.key', 'payment' => 'https://bank.paysera.com/pay/', 'paymentMethodList' => 'https://www.paysera.com/new/api/paymentMethods/', 'smsAnswer' => 'https://bank.paysera.com/psms/respond/', ), self::ENV_SANDBOX => array( 'publicKey' => 'https://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(), isset($this->configuration['password']) ? $this->configuration['password'] : null ); } 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; } } /** * 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; /** * Translations array for this group. Holds associative array of group title by country codes. * * @var array */ protected $translations; /** * Holds actual payment methods * * @var WebToPay_PaymentMethod[] */ protected $paymentMethods; /** * Default language for titles * * @var string */ protected $defaultLanguage; /** * 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(); } /** * 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; } /** * Gets default language for titles * * @return string */ public function getDefaultLanguage() { return $this->defaultLanguage; } /** * 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; } } /** * Returns group key * * @return string */ public function getKey() { return $this->groupKey; } /** * Returns available payment methods for this group * * @return WebToPay_PaymentMethod[] */ public function getPaymentMethods() { return $this->paymentMethods; } /** * 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; } /** * 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; } /** * 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; } /** * 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; } /** * Returns whether this group has no payment methods * * @return boolean */ public function isEmpty() { return count($this->paymentMethods) === 0; } /** * 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; } 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 )); } } /** * 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 ); } } /** * Parses and validates callbacks */ class WebToPay_CallbackValidator { /** * @var WebToPay_Sign_SignCheckerInterface */ protected $signer; /** * @var WebToPay_Util */ protected $util; /** * @var integer */ protected $projectId; /** * @var string|null */ protected $password; /** * Constructs object * * @param integer $projectId * @param WebToPay_Sign_SignCheckerInterface $signer * @param WebToPay_Util $util * @param string|null $password */ public function __construct( $projectId, WebToPay_Sign_SignCheckerInterface $signer, WebToPay_Util $util, $password = null ) { $this->signer = $signer; $this->util = $util; $this->projectId = $projectId; $this->password = $password; } /** * 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 * * @return array Parsed callback parameters * * @throws WebToPayException * @throws WebToPay_Exception_Callback */ public function validateAndParseData(array $requestData) { if (!isset($requestData['data'])) { throw new WebToPay_Exception_Callback('"data" parameter not found'); } $data = $requestData['data']; if (isset($requestData['ss1']) || isset($requestData['ss2'])) { if (!$this->signer->checkSign($requestData)) { throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit'); } $queryString = $this->util->decodeSafeUrlBase64($data); } else { if (null === $this->password) { throw new WebToPay_Exception_Configuration('You have to provide project password'); } $queryString = $this->util->decryptGCM( $this->util->decodeSafeUrlBase64($data), $this->password ); if (null === $queryString) { throw new WebToPay_Exception_Callback('Callback data decryption failed'); } } $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 ); } 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 $request; } /** * Checks data to have all the same parameters provided in expected array * * @param array $data * @param array $expected * * @throws WebToPayException */ 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( sprintf('Field %s is not as expected (expected %s, got %s)', $key, $value, $passedValue) ); } } } } /** * 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); } } /** * 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, '', '&'); } $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); } } /** * 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, '', '&')); 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', 255, 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}$/'), ); } } /** * Utility class */ class WebToPay_Util { const GCM_CIPHER = 'aes-256-gcm'; const GCM_AUTH_KEY_LENGTH = 16; /** * 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('+' => '-', '/' => '_')); } /** * Decrypts string with aes-256-gcm algorithm * * @param string $stringToDecrypt * @param string $key * * @return string|null */ function decryptGCM($stringToDecrypt, $key) { $ivLength = openssl_cipher_iv_length(self::GCM_CIPHER); $iv = substr($stringToDecrypt, 0, $ivLength); $ciphertext = substr($stringToDecrypt, $ivLength, -self::GCM_AUTH_KEY_LENGTH); $tag = substr($stringToDecrypt, -self::GCM_AUTH_KEY_LENGTH); $decryptedText = openssl_decrypt( $ciphertext, self::GCM_CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag ); return $decryptedText === false ? null : $decryptedText; } /** * Parses HTTP query to array * * @param string $query * * @return array */ public function parseHttpQuery($query) { $params = array(); parse_str($query, $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); } } } /** * 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 ); } } } /** * 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 { /** * @var integer */ protected $projectId; /** * @var WebToPay_WebClient */ protected $webClient; /** * Holds constructed method lists by currency * * @var WebToPay_PaymentMethodList[] */ protected $methodListCache = array(); /** * Builds various request URLs * * @var WebToPay_UrlBuilder $urlBuilder */ protected $urlBuilder; /** * Constructs object * * @param integer $projectId * @param WebToPay_WebClient $webClient * @param WebToPay_UrlBuilder $urlBuilder * * @throws WebToPayException if SimpleXML is not available */ 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'); } } /** * Gets payment method list for specified currency * * @param float $amount * @param string $currency * * @return WebToPay_PaymentMethodList * * @throws WebToPayException */ public function getPaymentMethodList($amount, $currency) { if (!isset($this->methodListCache[$currency])) { $xmlAsString = $this->webClient->get($this->urlBuilder->buildForPaymentsMethodList($this->projectId, $amount, $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]; } } /** * Raised if configuration is incorrect */ class WebToPay_Exception_Configuration extends WebToPayException { } /** * Raised on validation error in passed data when building the request */ class WebToPay_Exception_Validation extends WebToPayException { public function __construct($message, $code = 0, $field = null, Exception $previousException = null) { parent::__construct($message, $code, $previousException); if ($field) { $this->setField($field); } } } /** * Raised on error in callback */ class WebToPay_Exception_Callback extends WebToPayException { } /** * 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); } /** * 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 $amount * @param string $currency * @return string */ public function buildForPaymentsMethodList($projectId, $amount, $currency) { $route = $this->environmentSettings['paymentMethodList']; return $route . $projectId . '/currency:' . $currency . '/amount:' . $amount; } /** * 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, '', '&'); return preg_replace('/[\r\n]+/is', '', $url); } /** * Returns payment url. Argument is same as lang parameter in request data * * @return string $url */ public function getPaymentUrl() { $route = $this->environmentSettings['payment']; return $route; } } /** * 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); } }