From 68f481e61c2e0ff9c0b79548e2bebcfc6f5af2cd Mon Sep 17 00:00:00 2001 From: Seamus Lee Date: Sat, 5 Sep 2020 08:43:32 +1000 Subject: [PATCH] Change eWAY processor to use guzzle, fix style issues and update info.xml and add in test of eWAY single Currency payment processor Update README --- CRM/Upgrade/Incremental/php/FiveThirtyOne.php | 4 +- ext/ewaysingle/CRM/Core/Payment/eWAY.php | 75 +- ext/ewaysingle/README.md | 28 +- ext/ewaysingle/info.xml | 14 +- ext/ewaysingle/lib/XML/Util.php | 1532 +++++++++-------- .../lib/eWAY/eWAY_GatewayRequest.php | 458 +++-- .../lib/eWAY/eWAY_GatewayResponse.php | 321 ++-- ext/ewaysingle/phpunit.xml.dist | 18 + .../phpunit/CRM/Core/Payment/EwayTest.php | 212 +++ ext/ewaysingle/tests/phpunit/bootstrap.php | 63 + 10 files changed, 1472 insertions(+), 1253 deletions(-) create mode 100644 ext/ewaysingle/phpunit.xml.dist create mode 100644 ext/ewaysingle/tests/phpunit/CRM/Core/Payment/EwayTest.php create mode 100644 ext/ewaysingle/tests/phpunit/bootstrap.php diff --git a/CRM/Upgrade/Incremental/php/FiveThirtyOne.php b/CRM/Upgrade/Incremental/php/FiveThirtyOne.php index b85942cd3fce..efc87e6e14e0 100644 --- a/CRM/Upgrade/Incremental/php/FiveThirtyOne.php +++ b/CRM/Upgrade/Incremental/php/FiveThirtyOne.php @@ -71,8 +71,8 @@ public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) { * @param string $rev */ public function upgrade_5_31_alpha1($rev) { - $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev); - $this->addTask('enableeWAYSingleCurrencyExtension', 'enableEwaySingleExtension'); + $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev); + $this->addTask('enableeWAYSingleCurrencyExtension', 'enableEwaySingleExtension'); } public static function enableEwaySingleExtension(CRM_Queue_TaskContext $ctx) { diff --git a/ext/ewaysingle/CRM/Core/Payment/eWAY.php b/ext/ewaysingle/CRM/Core/Payment/eWAY.php index 59411cd5d0cf..a50389ffd33b 100644 --- a/ext/ewaysingle/CRM/Core/Payment/eWAY.php +++ b/ext/ewaysingle/CRM/Core/Payment/eWAY.php @@ -88,6 +88,11 @@ */ class CRM_Core_Payment_eWAY extends CRM_Core_Payment { + /** + * @var GuzzleHttp\Client + */ + protected $guzzleClient; + /** * ******************************************************* * Constructor @@ -106,6 +111,20 @@ public function __construct($mode, &$paymentProcessor) { $this->_paymentProcessor = $paymentProcessor; } + /** + * @return \GuzzleHttp\Client + */ + public function getGuzzleClient(): \GuzzleHttp\Client { + return $this->guzzleClient ?? new \GuzzleHttp\Client(); + } + + /** + * @param \GuzzleHttp\Client $guzzleClient + */ + public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) { + $this->guzzleClient = $guzzleClient; + } + /** * Sends request and receive response from eWAY payment process. * @@ -246,50 +265,13 @@ public function doDirectPayment(&$params) { //---------------------------------------------------------------------------------------------------- $requestxml = $eWAYRequest->ToXML(); - $submit = curl_init($gateway_URL); - - if (!$submit) { - throw new PaymentProcessorException('Could not initiate connection to payment gateway', 9004); - } - - curl_setopt($submit, CURLOPT_POST, TRUE); - // return the result on success, FALSE on failure - curl_setopt($submit, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($submit, CURLOPT_POSTFIELDS, $requestxml); - curl_setopt($submit, CURLOPT_TIMEOUT, 36000); - // if open_basedir or safe_mode are enabled in PHP settings CURLOPT_FOLLOWLOCATION won't work so don't apply it - // it's not really required CRM-5841 - if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) { - // ensures any Location headers are followed - curl_setopt($submit, CURLOPT_FOLLOWLOCATION, 1); - } - - // Send the data out over the wire - //-------------------------------- - $responseData = curl_exec($submit); - - //---------------------------------------------------------------------------------------------------- - // See if we had a curl error - if so tell 'em and bail out - // - // NOTE: curl_error does not return a logical value (see its documentation), but - // a string, which is empty when there was no error. - //---------------------------------------------------------------------------------------------------- - if ((curl_errno($submit) > 0) || (strlen(curl_error($submit)) > 0)) { - $errorNum = curl_errno($submit); - $errorDesc = curl_error($submit); - - // Paranoia - in the unlikley event that 'curl' errno fails - if ($errorNum == 0) { - $errorNum = 9005; - } - - // Paranoia - in the unlikley event that 'curl' error fails - if (strlen($errorDesc) == 0) { - $errorDesc = 'Connection to eWAY payment gateway failed'; - } - - throw new PaymentProcessorException($errorDesc, $errorNum); - } + $responseData = (string) $this->getGuzzleClient()->post($this->_paymentProcessor['url_site'], [ + 'body' => $requestxml, + 'curl' => [ + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'), + ], + ])->getBody(); //---------------------------------------------------------------------------------------------------- // If null data returned - tell 'em and bail out @@ -308,11 +290,6 @@ public function doDirectPayment(&$params) { throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007); } - //---------------------------------------------------------------------------------------------------- - // Success so far - close the curl and check the data - //---------------------------------------------------------------------------------------------------- - curl_close($submit); - //---------------------------------------------------------------------------------------------------- // Payment successfully sent to gateway - process the response now //---------------------------------------------------------------------------------------------------- diff --git a/ext/ewaysingle/README.md b/ext/ewaysingle/README.md index 52c94dafc116..81ddd1e7a6ba 100644 --- a/ext/ewaysingle/README.md +++ b/ext/ewaysingle/README.md @@ -2,43 +2,31 @@ ![Screenshot](/images/screenshot.png) -(*FIXME: In one or two paragraphs, describe what the extension does and why one would download it. *) +This extension is aimed at containing the original Core eWAY (Single Currency) Payment Processor Type that is legacy. See known issues below The extension is licensed under [AGPL-3.0](LICENSE.txt). ## Requirements -* PHP v7.0+ -* CiviCRM (*FIXME: Version number*) +* PHP v7.1+ +* CiviCRM 5.31 ## Installation (Web UI) -This extension has not yet been published for installation via the web UI. +Navigate to the Extensions Page and install the extension. -## Installation (CLI, Zip) +## Installation (CLI) -Sysadmins and developers may download the `.zip` file for this extension and -install it with the command-line tool [cv](https://github.com/civicrm/cv). +To enable this extension in the CLI do the following ```bash -cd -cv dl ewaysingle@https://github.com/FIXME/ewaysingle/archive/master.zip -``` - -## Installation (CLI, Git) - -Sysadmins and developers may clone the [Git](https://en.wikipedia.org/wiki/Git) repo for this extension and -install it with the command-line tool [cv](https://github.com/civicrm/cv). - -```bash -git clone https://github.com/FIXME/ewaysingle.git cv en ewaysingle ``` ## Usage -(* FIXME: Where would a new user navigate to get started? What changes would they see? *) +The eWAY (Single Currency) Payment Processor Type will show up as one of the options when your adding in a PaymentProcessor. ## Known Issues -(* FIXME *) +This Payment Processor does not do any kind of recurring payments at all for that you would need another extension e.g. [Agileware Eway Recurring](https://github.com/agileware/au.com.agileware.ewayrecurring) diff --git a/ext/ewaysingle/info.xml b/ext/ewaysingle/info.xml index bd99f5c4bd29..d69c7cd3627d 100644 --- a/ext/ewaysingle/info.xml +++ b/ext/ewaysingle/info.xml @@ -9,18 +9,18 @@ seamuslee001@gmail.com - http://FIXME - http://FIXME - http://FIXME + https://github.com/civicrm/civicrm-core/blob/master/ext/ewayrecurring + https://github.com/civicrm/civicrm-core/blob/master/ext/ewayrecurring + https://github.com/civicrm/civicrm-core/blob/master/ext/ewayrecurring http://www.gnu.org/licenses/agpl-3.0.html - 2020-09-04 + 2020-10-07 1.0 - alpha + stable - 5.0 + 5.31 - This is a new, undeveloped module + This is an extension to contain the eWAY Single Currency Payment Processor diff --git a/ext/ewaysingle/lib/XML/Util.php b/ext/ewaysingle/lib/XML/Util.php index f5927b16cfd4..cbef96ac831c 100644 --- a/ext/ewaysingle/lib/XML/Util.php +++ b/ext/ewaysingle/lib/XML/Util.php @@ -6,7 +6,7 @@ * XML_Util * * XML Utilities package - * + * * PHP versions 4 and 5 * * LICENSE: @@ -113,7 +113,7 @@ /** * utility class for working with XML documents * - + * * @category XML * @package XML_Util * @author Stephan Schmidt @@ -122,790 +122,800 @@ * @version Release: 1.2.1 * @link http://pear.php.net/package/XML_Util */ -class XML_Util -{ - /** - * return API version - * - * @return string $version API version - * @access public - * @static - */ - function apiVersion() - { - return '1.1'; - } - - /** - * replace XML entities - * - * With the optional second parameter, you may select, which - * entities should be replaced. - * - * - * require_once 'XML/Util.php'; - * - * // replace XML entites: - * $string = XML_Util::replaceEntities('This string contains < & >.'); - * - * - * With the optional third parameter, you may pass the character encoding - * - * require_once 'XML/Util.php'; - * - * // replace XML entites in UTF-8: - * $string = XML_Util::replaceEntities( - * 'This string contains < & > as well as ä, ö, ß, à and ê', - * XML_UTIL_ENTITIES_HTML, - * 'UTF-8' - * ); - * - * - * @param string $string string where XML special chars - * should be replaced - * @param int $replaceEntities setting for entities in attribute values - * (one of XML_UTIL_ENTITIES_XML, - * XML_UTIL_ENTITIES_XML_REQUIRED, - * XML_UTIL_ENTITIES_HTML) - * @param string $encoding encoding value (if any)... - * must be a valid encoding as determined - * by the htmlentities() function - * - * @return string string with replaced chars - * @access public - * @static - * @see reverseEntities() - */ - function replaceEntities($string, $replaceEntities = XML_UTIL_ENTITIES_XML, - $encoding = 'ISO-8859-1') - { - switch ($replaceEntities) { - case XML_UTIL_ENTITIES_XML: - return strtr($string, array( - '&' => '&', - '>' => '>', - '<' => '<', - '"' => '"', - '\'' => ''' )); - break; - case XML_UTIL_ENTITIES_XML_REQUIRED: - return strtr($string, array( - '&' => '&', - '<' => '<', - '"' => '"' )); - break; - case XML_UTIL_ENTITIES_HTML: - return htmlentities($string, ENT_COMPAT, $encoding); - break; - } - return $string; - } - - /** - * reverse XML entities - * - * With the optional second parameter, you may select, which - * entities should be reversed. - * - * - * require_once 'XML/Util.php'; - * - * // reverse XML entites: - * $string = XML_Util::reverseEntities('This string contains < & >.'); - * - * - * With the optional third parameter, you may pass the character encoding - * - * require_once 'XML/Util.php'; - * - * // reverse XML entites in UTF-8: - * $string = XML_Util::reverseEntities( - * 'This string contains < & > as well as' - * . ' ä, ö, ß, à and ê', - * XML_UTIL_ENTITIES_HTML, - * 'UTF-8' - * ); - * - * - * @param string $string string where XML special chars - * should be replaced - * @param int $replaceEntities setting for entities in attribute values - * (one of XML_UTIL_ENTITIES_XML, - * XML_UTIL_ENTITIES_XML_REQUIRED, - * XML_UTIL_ENTITIES_HTML) - * @param string $encoding encoding value (if any)... - * must be a valid encoding as determined - * by the html_entity_decode() function - * - * @return string string with replaced chars - * @access public - * @static - * @see replaceEntities() - */ - function reverseEntities($string, $replaceEntities = XML_UTIL_ENTITIES_XML, - $encoding = 'ISO-8859-1') - { - switch ($replaceEntities) { - case XML_UTIL_ENTITIES_XML: - return strtr($string, array( - '&' => '&', - '>' => '>', - '<' => '<', - '"' => '"', - ''' => '\'' )); - break; - case XML_UTIL_ENTITIES_XML_REQUIRED: - return strtr($string, array( - '&' => '&', - '<' => '<', - '"' => '"' )); - break; - case XML_UTIL_ENTITIES_HTML: - return html_entity_decode($string, ENT_COMPAT, $encoding); - break; - } - return $string; - } - - /** - * build an xml declaration - * - * - * require_once 'XML/Util.php'; - * - * // get an XML declaration: - * $xmlDecl = XML_Util::getXMLDeclaration('1.0', 'UTF-8', true); - * - * - * @param string $version xml version - * @param string $encoding character encoding - * @param bool $standalone document is standalone (or not) - * - * @return string xml declaration - * @access public - * @static - * @uses attributesToString() to serialize the attributes of the XML declaration - */ - function getXMLDeclaration($version = '1.0', $encoding = null, - $standalone = null) - { - $attributes = array( - 'version' => $version, - ); - // add encoding - if ($encoding !== null) { - $attributes['encoding'] = $encoding; - } - // add standalone, if specified - if ($standalone !== null) { - $attributes['standalone'] = $standalone ? 'yes' : 'no'; - } - - return sprintf('', - XML_Util::attributesToString($attributes, false)); - } - - /** - * build a document type declaration - * - * - * require_once 'XML/Util.php'; - * - * // get a doctype declaration: - * $xmlDecl = XML_Util::getDocTypeDeclaration('rootTag','myDocType.dtd'); - * - * - * @param string $root name of the root tag - * @param string $uri uri of the doctype definition - * (or array with uri and public id) - * @param string $internalDtd internal dtd entries - * - * @return string doctype declaration - * @access public - * @static - * @since 0.2 - */ - function getDocTypeDeclaration($root, $uri = null, $internalDtd = null) - { - if (is_array($uri)) { - $ref = sprintf(' PUBLIC "%s" "%s"', $uri['id'], $uri['uri']); - } elseif (!empty($uri)) { - $ref = sprintf(' SYSTEM "%s"', $uri); - } else { - $ref = ''; - } +class XML_Util { + + /** + * return API version + * + * @return string $version API version + * @access public + * @static + */ + public function apiVersion() { + return '1.1'; + } + + /** + * replace XML entities + * + * With the optional second parameter, you may select, which + * entities should be replaced. + * + * + * require_once 'XML/Util.php'; + * + * // replace XML entites: + * $string = XML_Util::replaceEntities('This string contains < & >.'); + * + * + * With the optional third parameter, you may pass the character encoding + * + * require_once 'XML/Util.php'; + * + * // replace XML entites in UTF-8: + * $string = XML_Util::replaceEntities( + * 'This string contains < & > as well as ä, ö, ß, à and ê', + * XML_UTIL_ENTITIES_HTML, + * 'UTF-8' + * ); + * + * + * @param string $string string where XML special chars + * should be replaced + * @param int $replaceEntities + * (one of XML_UTIL_ENTITIES_XML, + * XML_UTIL_ENTITIES_XML_REQUIRED, + * XML_UTIL_ENTITIES_HTML) + * @param string $encoding encoding value (if any)... + * must be a valid encoding as determined + * by the htmlentities() function + * + * @return string string with replaced chars + * @access public + * @static + * @see reverseEntities() + */ + public function replaceEntities($string, $replaceEntities = XML_UTIL_ENTITIES_XML, + $encoding = 'ISO-8859-1') { + switch ($replaceEntities) { + case XML_UTIL_ENTITIES_XML: + return strtr($string, array( + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + '\'' => ''', + )); + + break; + case XML_UTIL_ENTITIES_XML_REQUIRED: + return strtr($string, array( + '&' => '&', + '<' => '<', + '"' => '"', + )); + + break; + case XML_UTIL_ENTITIES_HTML: + return htmlentities($string, ENT_COMPAT, $encoding); + + break; + } + return $string; + } + + /** + * reverse XML entities + * + * With the optional second parameter, you may select, which + * entities should be reversed. + * + * + * require_once 'XML/Util.php'; + * + * // reverse XML entites: + * $string = XML_Util::reverseEntities('This string contains < & >.'); + * + * + * With the optional third parameter, you may pass the character encoding + * + * require_once 'XML/Util.php'; + * + * // reverse XML entites in UTF-8: + * $string = XML_Util::reverseEntities( + * 'This string contains < & > as well as' + * . ' ä, ö, ß, à and ê', + * XML_UTIL_ENTITIES_HTML, + * 'UTF-8' + * ); + * + * + * @param string $string string where XML special chars + * should be replaced + * @param int $replaceEntities + * (one of XML_UTIL_ENTITIES_XML, + * XML_UTIL_ENTITIES_XML_REQUIRED, + * XML_UTIL_ENTITIES_HTML) + * @param string $encoding encoding value (if any)... + * must be a valid encoding as determined + * by the html_entity_decode() function + * + * @return string string with replaced chars + * @access public + * @static + * @see replaceEntities() + */ + public function reverseEntities($string, $replaceEntities = XML_UTIL_ENTITIES_XML, + $encoding = 'ISO-8859-1') { + switch ($replaceEntities) { + case XML_UTIL_ENTITIES_XML: + return strtr($string, array( + '&' => '&', + '>' => '>', + '<' => '<', + '"' => '"', + ''' => '\'', + )); + + break; + case XML_UTIL_ENTITIES_XML_REQUIRED: + return strtr($string, array( + '&' => '&', + '<' => '<', + '"' => '"', + )); + + break; + case XML_UTIL_ENTITIES_HTML: + return html_entity_decode($string, ENT_COMPAT, $encoding); + + break; + } + return $string; + } + + /** + * build an xml declaration + * + * + * require_once 'XML/Util.php'; + * + * // get an XML declaration: + * $xmlDecl = XML_Util::getXMLDeclaration('1.0', 'UTF-8', true); + * + * + * @param string $version xml version + * @param string $encoding character encoding + * @param bool $standalone + * + * @return string xml declaration + * @access public + * @static + * @uses attributesToString() to serialize the attributes of the XML declaration + */ + public function getXMLDeclaration($version = '1.0', $encoding = NULL, + $standalone = NULL) { + $attributes = array( + 'version' => $version, + ); + // add encoding + if ($encoding !== NULL) { + $attributes['encoding'] = $encoding; + } + // add standalone, if specified + if ($standalone !== NULL) { + $attributes['standalone'] = $standalone ? 'yes' : 'no'; + } - if (empty($internalDtd)) { - return sprintf('', $root, $ref); - } else { - return sprintf("", $root, $ref, $internalDtd); - } + return sprintf('', + XML_Util::attributesToString($attributes, FALSE)); + } + + /** + * build a document type declaration + * + * + * require_once 'XML/Util.php'; + * + * // get a doctype declaration: + * $xmlDecl = XML_Util::getDocTypeDeclaration('rootTag','myDocType.dtd'); + * + * + * @param string $root name of the root tag + * @param string $uri uri of the doctype definition + * (or array with uri and public id) + * @param string $internalDtd internal dtd entries + * + * @return string doctype declaration + * @access public + * @static + * @since 0.2 + */ + public function getDocTypeDeclaration($root, $uri = NULL, $internalDtd = NULL) { + if (is_array($uri)) { + $ref = sprintf(' PUBLIC "%s" "%s"', $uri['id'], $uri['uri']); + } + elseif (!empty($uri)) { + $ref = sprintf(' SYSTEM "%s"', $uri); + } + else { + $ref = ''; } - /** - * create string representation of an attribute list - * - * - * require_once 'XML/Util.php'; - * - * // build an attribute string - * $att = array( - * 'foo' => 'bar', - * 'argh' => 'tomato' - * ); - * - * $attList = XML_Util::attributesToString($att); - * - * - * @param array $attributes attribute array - * @param bool|array $sort sort attribute list alphabetically, - * may also be an assoc array containing - * the keys 'sort', 'multiline', 'indent', - * 'linebreak' and 'entities' - * @param bool $multiline use linebreaks, if more than - * one attribute is given - * @param string $indent string used for indentation of - * multiline attributes - * @param string $linebreak string used for linebreaks of - * multiline attributes - * @param int $entities setting for entities in attribute values - * (one of XML_UTIL_ENTITIES_NONE, - * XML_UTIL_ENTITIES_XML, - * XML_UTIL_ENTITIES_XML_REQUIRED, - * XML_UTIL_ENTITIES_HTML) - * - * @return string string representation of the attributes - * @access public - * @static - * @uses replaceEntities() to replace XML entities in attribute values - * @todo allow sort also to be an options array + if (empty($internalDtd)) { + return sprintf('', $root, $ref); + } + else { + return sprintf("", $root, $ref, $internalDtd); + } + } + + /** + * create string representation of an attribute list + * + * + * require_once 'XML/Util.php'; + * + * // build an attribute string + * $att = array( + * 'foo' => 'bar', + * 'argh' => 'tomato' + * ); + * + * $attList = XML_Util::attributesToString($att); + * + * + * @param array $attributes + * @param bool|array $sort sort attribute list alphabetically, + * may also be an assoc array containing + * the keys 'sort', 'multiline', 'indent', + * 'linebreak' and 'entities' + * @param bool $multiline + * one attribute is given + * @param string $indent + * multiline attributes + * @param string $linebreak + * multiline attributes + * @param int $entities + * (one of XML_UTIL_ENTITIES_NONE, + * XML_UTIL_ENTITIES_XML, + * XML_UTIL_ENTITIES_XML_REQUIRED, + * XML_UTIL_ENTITIES_HTML) + * + * @return string string representation of the attributes + * @access public + * @static + * @uses replaceEntities() to replace XML entities in attribute values + * @todo allow sort also to be an options array + */ + public function attributesToString($attributes, $sort = TRUE, $multiline = FALSE, + $indent = ' ', $linebreak = "\n", $entities = XML_UTIL_ENTITIES_XML) { + /* + * second parameter may be an array */ - function attributesToString($attributes, $sort = true, $multiline = false, - $indent = ' ', $linebreak = "\n", $entities = XML_UTIL_ENTITIES_XML) - { - /* - * second parameter may be an array - */ - if (is_array($sort)) { - if (isset($sort['multiline'])) { - $multiline = $sort['multiline']; - } - if (isset($sort['indent'])) { - $indent = $sort['indent']; - } - if (isset($sort['linebreak'])) { - $multiline = $sort['linebreak']; - } - if (isset($sort['entities'])) { - $entities = $sort['entities']; - } - if (isset($sort['sort'])) { - $sort = $sort['sort']; - } else { - $sort = true; - } - } - $string = ''; - if (is_array($attributes) && !empty($attributes)) { - if ($sort) { - ksort($attributes); - } - if ( !$multiline || count($attributes) == 1) { - foreach ($attributes as $key => $value) { - if ($entities != XML_UTIL_ENTITIES_NONE) { - if ($entities === XML_UTIL_CDATA_SECTION) { - $entities = XML_UTIL_ENTITIES_XML; - } - $value = XML_Util::replaceEntities($value, $entities); - } - $string .= ' ' . $key . '="' . $value . '"'; - } - } else { - $first = true; - foreach ($attributes as $key => $value) { - if ($entities != XML_UTIL_ENTITIES_NONE) { - $value = XML_Util::replaceEntities($value, $entities); - } - if ($first) { - $string .= ' ' . $key . '="' . $value . '"'; - $first = false; - } else { - $string .= $linebreak . $indent . $key . '="' . $value . '"'; - } - } + if (is_array($sort)) { + if (isset($sort['multiline'])) { + $multiline = $sort['multiline']; + } + if (isset($sort['indent'])) { + $indent = $sort['indent']; + } + if (isset($sort['linebreak'])) { + $multiline = $sort['linebreak']; + } + if (isset($sort['entities'])) { + $entities = $sort['entities']; + } + if (isset($sort['sort'])) { + $sort = $sort['sort']; + } + else { + $sort = TRUE; + } + } + $string = ''; + if (is_array($attributes) && !empty($attributes)) { + if ($sort) { + ksort($attributes); + } + if (!$multiline || count($attributes) == 1) { + foreach ($attributes as $key => $value) { + if ($entities != XML_UTIL_ENTITIES_NONE) { + if ($entities === XML_UTIL_CDATA_SECTION) { + $entities = XML_UTIL_ENTITIES_XML; } + $value = XML_Util::replaceEntities($value, $entities); + } + $string .= ' ' . $key . '="' . $value . '"'; } - return $string; - } - - /** - * Collapses empty tags. - * - * @param string $xml XML - * @param int $mode Whether to collapse all empty tags (XML_UTIL_COLLAPSE_ALL) - * or only XHTML (XML_UTIL_COLLAPSE_XHTML_ONLY) ones. - * - * @return string XML - * @access public - * @static - * @todo PEAR CS - unable to avoid "space after open parens" error - * in the IF branch - */ - function collapseEmptyTags($xml, $mode = XML_UTIL_COLLAPSE_ALL) - { - if ($mode == XML_UTIL_COLLAPSE_XHTML_ONLY) { - return preg_replace( - '/<(area|base(?:font)?|br|col|frame|hr|img|input|isindex|link|meta|' - . 'param)([^>]*)><\/\\1>/s', - '<\\1\\2 />', - $xml); - } else { - return preg_replace('/<(\w+)([^>]*)><\/\\1>/s', '<\\1\\2 />', $xml); + } + else { + $first = TRUE; + foreach ($attributes as $key => $value) { + if ($entities != XML_UTIL_ENTITIES_NONE) { + $value = XML_Util::replaceEntities($value, $entities); + } + if ($first) { + $string .= ' ' . $key . '="' . $value . '"'; + $first = FALSE; + } + else { + $string .= $linebreak . $indent . $key . '="' . $value . '"'; + } } + } + } + return $string; + } + + /** + * Collapses empty tags. + * + * @param string $xml XML + * @param int $mode + * or only XHTML (XML_UTIL_COLLAPSE_XHTML_ONLY) ones. + * + * @return string XML + * @access public + * @static + * @todo PEAR CS - unable to avoid "space after open parens" error + * in the IF branch + */ + public function collapseEmptyTags($xml, $mode = XML_UTIL_COLLAPSE_ALL) { + if ($mode == XML_UTIL_COLLAPSE_XHTML_ONLY) { + return preg_replace( + '/<(area|base(?:font)?|br|col|frame|hr|img|input|isindex|link|meta|' + . 'param)([^>]*)><\/\\1>/s', + '<\\1\\2 />', + $xml); + } + else { + return preg_replace('/<(\w+)([^>]*)><\/\\1>/s', '<\\1\\2 />', $xml); + } + } + + /** + * create a tag + * + * This method will call XML_Util::createTagFromArray(), which + * is more flexible. + * + * + * require_once 'XML/Util.php'; + * + * // create an XML tag: + * $tag = XML_Util::createTag('myNs:myTag', + * array('foo' => 'bar'), + * 'This is inside the tag', + * 'http://www.w3c.org/myNs#'); + * + * + * @param string $qname qualified tagname (including namespace) + * @param array $attributes + * @param mixed $content + * @param string $namespaceUri URI of the namespace + * @param int $replaceEntities + * content, embedd it in a CData section + * or none of both + * @param bool $multiline + * each attribute gets written to a single line + * @param string $indent string used to indent attributes + * (_auto indents attributes so they start + * at the same column) + * @param string $linebreak string used for linebreaks + * @param bool $sortAttributes + * + * @return string XML tag + * @access public + * @static + * @see createTagFromArray() + * @uses createTagFromArray() to create the tag + */ + public function createTag($qname, $attributes = array(), $content = NULL, + $namespaceUri = NULL, $replaceEntities = XML_UTIL_REPLACE_ENTITIES, + $multiline = FALSE, $indent = '_auto', $linebreak = "\n", + $sortAttributes = TRUE) { + $tag = array( + 'qname' => $qname, + 'attributes' => $attributes, + ); + + // add tag content + if ($content !== NULL) { + $tag['content'] = $content; } - /** - * create a tag - * - * This method will call XML_Util::createTagFromArray(), which - * is more flexible. - * - * - * require_once 'XML/Util.php'; - * - * // create an XML tag: - * $tag = XML_Util::createTag('myNs:myTag', - * array('foo' => 'bar'), - * 'This is inside the tag', - * 'http://www.w3c.org/myNs#'); - * - * - * @param string $qname qualified tagname (including namespace) - * @param array $attributes array containg attributes - * @param mixed $content the content - * @param string $namespaceUri URI of the namespace - * @param int $replaceEntities whether to replace XML special chars in - * content, embedd it in a CData section - * or none of both - * @param bool $multiline whether to create a multiline tag where - * each attribute gets written to a single line - * @param string $indent string used to indent attributes - * (_auto indents attributes so they start - * at the same column) - * @param string $linebreak string used for linebreaks - * @param bool $sortAttributes Whether to sort the attributes or not - * - * @return string XML tag - * @access public - * @static - * @see createTagFromArray() - * @uses createTagFromArray() to create the tag - */ - function createTag($qname, $attributes = array(), $content = null, - $namespaceUri = null, $replaceEntities = XML_UTIL_REPLACE_ENTITIES, - $multiline = false, $indent = '_auto', $linebreak = "\n", - $sortAttributes = true) - { - $tag = array( - 'qname' => $qname, - 'attributes' => $attributes - ); - - // add tag content - if ($content !== null) { - $tag['content'] = $content; - } - - // add namespace Uri - if ($namespaceUri !== null) { - $tag['namespaceUri'] = $namespaceUri; - } + // add namespace Uri + if ($namespaceUri !== NULL) { + $tag['namespaceUri'] = $namespaceUri; + } - return XML_Util::createTagFromArray($tag, $replaceEntities, $multiline, - $indent, $linebreak, $sortAttributes); - } - - /** - * create a tag from an array - * this method awaits an array in the following format - *
-     * array(
-     *     // qualified name of the tag
-     *     'qname' => $qname        
-     *
-     *     // namespace prefix (optional, if qname is specified or no namespace)
-     *     'namespace' => $namespace    
-     *
-     *     // local part of the tagname (optional, if qname is specified)
-     *     'localpart' => $localpart,   
-     *
-     *     // array containing all attributes (optional)
-     *     'attributes' => array(),      
-     *
-     *     // tag content (optional)
-     *     'content' => $content,     
-     *
-     *     // namespaceUri for the given namespace (optional)
-     *     'namespaceUri' => $namespaceUri 
-     * )
-     * 
- * - * - * require_once 'XML/Util.php'; - * - * $tag = array( - * 'qname' => 'foo:bar', - * 'namespaceUri' => 'http://foo.com', - * 'attributes' => array('key' => 'value', 'argh' => 'fruit&vegetable'), - * 'content' => 'I\'m inside the tag', - * ); - * // creating a tag with qualified name and namespaceUri - * $string = XML_Util::createTagFromArray($tag); - * - * - * @param array $tag tag definition - * @param int $replaceEntities whether to replace XML special chars in - * content, embedd it in a CData section - * or none of both - * @param bool $multiline whether to create a multiline tag where each - * attribute gets written to a single line - * @param string $indent string used to indent attributes - * (_auto indents attributes so they start - * at the same column) - * @param string $linebreak string used for linebreaks - * @param bool $sortAttributes Whether to sort the attributes or not - * - * @return string XML tag - * @access public - * @static - * @see createTag() - * @uses attributesToString() to serialize the attributes of the tag - * @uses splitQualifiedName() to get local part and namespace of a qualified name - * @uses createCDataSection() - * @uses raiseError() - */ - function createTagFromArray($tag, $replaceEntities = XML_UTIL_REPLACE_ENTITIES, - $multiline = false, $indent = '_auto', $linebreak = "\n", - $sortAttributes = true) - { - if (isset($tag['content']) && !is_scalar($tag['content'])) { - return XML_Util::raiseError('Supplied non-scalar value as tag content', - XML_UTIL_ERROR_NON_SCALAR_CONTENT); - } + return XML_Util::createTagFromArray($tag, $replaceEntities, $multiline, + $indent, $linebreak, $sortAttributes); + } + + /** + * create a tag from an array + * this method awaits an array in the following format + *
+   * array(
+   *     // qualified name of the tag
+   *     'qname' => $qname
+   *
+   *     // namespace prefix (optional, if qname is specified or no namespace)
+   *     'namespace' => $namespace
+   *
+   *     // local part of the tagname (optional, if qname is specified)
+   *     'localpart' => $localpart,
+   *
+   *     // array containing all attributes (optional)
+   *     'attributes' => array(),
+   *
+   *     // tag content (optional)
+   *     'content' => $content,
+   *
+   *     // namespaceUri for the given namespace (optional)
+   *     'namespaceUri' => $namespaceUri
+   * )
+   * 
+ * + * + * require_once 'XML/Util.php'; + * + * $tag = array( + * 'qname' => 'foo:bar', + * 'namespaceUri' => 'http://foo.com', + * 'attributes' => array('key' => 'value', 'argh' => 'fruit&vegetable'), + * 'content' => 'I\'m inside the tag', + * ); + * // creating a tag with qualified name and namespaceUri + * $string = XML_Util::createTagFromArray($tag); + * + * + * @param array $tag + * @param int $replaceEntities + * content, embedd it in a CData section + * or none of both + * @param bool $multiline + * attribute gets written to a single line + * @param string $indent string used to indent attributes + * (_auto indents attributes so they start + * at the same column) + * @param string $linebreak string used for linebreaks + * @param bool $sortAttributes + * + * @return string XML tag + * @access public + * @static + * @see createTag() + * @uses attributesToString() to serialize the attributes of the tag + * @uses splitQualifiedName() to get local part and namespace of a qualified name + * @uses createCDataSection() + * @uses raiseError() + */ + public function createTagFromArray($tag, $replaceEntities = XML_UTIL_REPLACE_ENTITIES, + $multiline = FALSE, $indent = '_auto', $linebreak = "\n", + $sortAttributes = TRUE) { + if (isset($tag['content']) && !is_scalar($tag['content'])) { + return XML_Util::raiseError('Supplied non-scalar value as tag content', + XML_UTIL_ERROR_NON_SCALAR_CONTENT); + } - if (!isset($tag['qname']) && !isset($tag['localPart'])) { - return XML_Util::raiseError('You must either supply a qualified name ' - . '(qname) or local tag name (localPart).', - XML_UTIL_ERROR_NO_TAG_NAME); - } + if (!isset($tag['qname']) && !isset($tag['localPart'])) { + return XML_Util::raiseError('You must either supply a qualified name ' + . '(qname) or local tag name (localPart).', + XML_UTIL_ERROR_NO_TAG_NAME); + } - // if no attributes hav been set, use empty attributes - if (!isset($tag['attributes']) || !is_array($tag['attributes'])) { - $tag['attributes'] = array(); - } + // if no attributes hav been set, use empty attributes + if (!isset($tag['attributes']) || !is_array($tag['attributes'])) { + $tag['attributes'] = array(); + } - if (isset($tag['namespaces'])) { - foreach ($tag['namespaces'] as $ns => $uri) { - $tag['attributes']['xmlns:' . $ns] = $uri; - } - } + if (isset($tag['namespaces'])) { + foreach ($tag['namespaces'] as $ns => $uri) { + $tag['attributes']['xmlns:' . $ns] = $uri; + } + } - if (!isset($tag['qname'])) { - // qualified name is not given + if (!isset($tag['qname'])) { + // qualified name is not given - // check for namespace - if (isset($tag['namespace']) && !empty($tag['namespace'])) { - $tag['qname'] = $tag['namespace'] . ':' . $tag['localPart']; - } else { - $tag['qname'] = $tag['localPart']; - } - } elseif (isset($tag['namespaceUri']) && !isset($tag['namespace'])) { - // namespace URI is set, but no namespace + // check for namespace + if (isset($tag['namespace']) && !empty($tag['namespace'])) { + $tag['qname'] = $tag['namespace'] . ':' . $tag['localPart']; + } + else { + $tag['qname'] = $tag['localPart']; + } + } + elseif (isset($tag['namespaceUri']) && !isset($tag['namespace'])) { + // namespace URI is set, but no namespace - $parts = XML_Util::splitQualifiedName($tag['qname']); + $parts = XML_Util::splitQualifiedName($tag['qname']); - $tag['localPart'] = $parts['localPart']; - if (isset($parts['namespace'])) { - $tag['namespace'] = $parts['namespace']; - } - } + $tag['localPart'] = $parts['localPart']; + if (isset($parts['namespace'])) { + $tag['namespace'] = $parts['namespace']; + } + } - if (isset($tag['namespaceUri']) && !empty($tag['namespaceUri'])) { - // is a namespace given - if (isset($tag['namespace']) && !empty($tag['namespace'])) { - $tag['attributes']['xmlns:' . $tag['namespace']] = + if (isset($tag['namespaceUri']) && !empty($tag['namespaceUri'])) { + // is a namespace given + if (isset($tag['namespace']) && !empty($tag['namespace'])) { + $tag['attributes']['xmlns:' . $tag['namespace']] = $tag['namespaceUri']; - } else { - // define this Uri as the default namespace - $tag['attributes']['xmlns'] = $tag['namespaceUri']; - } - } + } + else { + // define this Uri as the default namespace + $tag['attributes']['xmlns'] = $tag['namespaceUri']; + } + } - // check for multiline attributes - if ($multiline === true) { - if ($indent === '_auto') { - $indent = str_repeat(' ', (strlen($tag['qname'])+2)); - } - } + // check for multiline attributes + if ($multiline === TRUE) { + if ($indent === '_auto') { + $indent = str_repeat(' ', (strlen($tag['qname']) + 2)); + } + } - // create attribute list - $attList = XML_Util::attributesToString($tag['attributes'], - $sortAttributes, $multiline, $indent, $linebreak, $replaceEntities); - if (!isset($tag['content']) || (string)$tag['content'] == '') { - $tag = sprintf('<%s%s />', $tag['qname'], $attList); - } else { - switch ($replaceEntities) { - case XML_UTIL_ENTITIES_NONE: - break; - case XML_UTIL_CDATA_SECTION: - $tag['content'] = XML_Util::createCDataSection($tag['content']); - break; - default: - $tag['content'] = XML_Util::replaceEntities($tag['content'], - $replaceEntities); - break; - } - $tag = sprintf('<%s%s>%s', $tag['qname'], $attList, $tag['content'], - $tag['qname']); - } - return $tag; - } - - /** - * create a start element - * - * - * require_once 'XML/Util.php'; - * - * // create an XML start element: - * $tag = XML_Util::createStartElement('myNs:myTag', - * array('foo' => 'bar') ,'http://www.w3c.org/myNs#'); - * - * - * @param string $qname qualified tagname (including namespace) - * @param array $attributes array containg attributes - * @param string $namespaceUri URI of the namespace - * @param bool $multiline whether to create a multiline tag where each - * attribute gets written to a single line - * @param string $indent string used to indent attributes (_auto indents - * attributes so they start at the same column) - * @param string $linebreak string used for linebreaks - * @param bool $sortAttributes Whether to sort the attributes or not - * - * @return string XML start element - * @access public - * @static - * @see createEndElement(), createTag() - */ - function createStartElement($qname, $attributes = array(), $namespaceUri = null, - $multiline = false, $indent = '_auto', $linebreak = "\n", - $sortAttributes = true) - { - // if no attributes hav been set, use empty attributes - if (!isset($attributes) || !is_array($attributes)) { - $attributes = array(); - } + // create attribute list + $attList = XML_Util::attributesToString($tag['attributes'], + $sortAttributes, $multiline, $indent, $linebreak, $replaceEntities); + if (!isset($tag['content']) || (string) $tag['content'] == '') { + $tag = sprintf('<%s%s />', $tag['qname'], $attList); + } + else { + switch ($replaceEntities) { + case XML_UTIL_ENTITIES_NONE: + break; + + case XML_UTIL_CDATA_SECTION: + $tag['content'] = XML_Util::createCDataSection($tag['content']); + break; + + default: + $tag['content'] = XML_Util::replaceEntities($tag['content'], + $replaceEntities); + break; + } + $tag = sprintf('<%s%s>%s', $tag['qname'], $attList, $tag['content'], + $tag['qname']); + } + return $tag; + } + + /** + * create a start element + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = XML_Util::createStartElement('myNs:myTag', + * array('foo' => 'bar') ,'http://www.w3c.org/myNs#'); + * + * + * @param string $qname qualified tagname (including namespace) + * @param array $attributes + * @param string $namespaceUri URI of the namespace + * @param bool $multiline + * attribute gets written to a single line + * @param string $indent string used to indent attributes (_auto indents + * attributes so they start at the same column) + * @param string $linebreak string used for linebreaks + * @param bool $sortAttributes + * + * @return string XML start element + * @access public + * @static + * @see createEndElement() + * @see createTag() + */ + public function createStartElement($qname, $attributes = array(), $namespaceUri = NULL, + $multiline = FALSE, $indent = '_auto', $linebreak = "\n", + $sortAttributes = TRUE) { + // if no attributes hav been set, use empty attributes + if (!isset($attributes) || !is_array($attributes)) { + $attributes = array(); + } - if ($namespaceUri != null) { - $parts = XML_Util::splitQualifiedName($qname); - } + if ($namespaceUri != NULL) { + $parts = XML_Util::splitQualifiedName($qname); + } - // check for multiline attributes - if ($multiline === true) { - if ($indent === '_auto') { - $indent = str_repeat(' ', (strlen($qname)+2)); - } - } + // check for multiline attributes + if ($multiline === TRUE) { + if ($indent === '_auto') { + $indent = str_repeat(' ', (strlen($qname) + 2)); + } + } - if ($namespaceUri != null) { - // is a namespace given - if (isset($parts['namespace']) && !empty($parts['namespace'])) { - $attributes['xmlns:' . $parts['namespace']] = $namespaceUri; - } else { - // define this Uri as the default namespace - $attributes['xmlns'] = $namespaceUri; - } - } + if ($namespaceUri != NULL) { + // is a namespace given + if (isset($parts['namespace']) && !empty($parts['namespace'])) { + $attributes['xmlns:' . $parts['namespace']] = $namespaceUri; + } + else { + // define this Uri as the default namespace + $attributes['xmlns'] = $namespaceUri; + } + } - // create attribute list - $attList = XML_Util::attributesToString($attributes, $sortAttributes, - $multiline, $indent, $linebreak); - $element = sprintf('<%s%s>', $qname, $attList); - return $element; - } - - /** - * create an end element - * - * - * require_once 'XML/Util.php'; - * - * // create an XML start element: - * $tag = XML_Util::createEndElement('myNs:myTag'); - * - * - * @param string $qname qualified tagname (including namespace) - * - * @return string XML end element - * @access public - * @static - * @see createStartElement(), createTag() - */ - function createEndElement($qname) - { - $element = sprintf('', $qname); - return $element; - } - - /** - * create an XML comment - * - * - * require_once 'XML/Util.php'; - * - * // create an XML start element: - * $tag = XML_Util::createComment('I am a comment'); - * - * - * @param string $content content of the comment - * - * @return string XML comment - * @access public - * @static - */ - function createComment($content) - { - $comment = sprintf('', $content); - return $comment; - } - - /** - * create a CData section - * - * - * require_once 'XML/Util.php'; - * - * // create a CData section - * $tag = XML_Util::createCDataSection('I am content.'); - * - * - * @param string $data data of the CData section - * - * @return string CData section with content - * @access public - * @static - */ - function createCDataSection($data) - { - return sprintf('', - preg_replace('/\]\]>/', ']]]]>', strval($data))); - - } - - /** - * split qualified name and return namespace and local part - * - * - * require_once 'XML/Util.php'; - * - * // split qualified tag - * $parts = XML_Util::splitQualifiedName('xslt:stylesheet'); - * - * the returned array will contain two elements: - *
-     * array(
-     *     'namespace' => 'xslt',
-     *     'localPart' => 'stylesheet'
-     * );
-     * 
- * - * @param string $qname qualified tag name - * @param string $defaultNs default namespace (optional) - * - * @return array array containing namespace and local part - * @access public - * @static - */ - function splitQualifiedName($qname, $defaultNs = null) - { - if (strstr($qname, ':')) { - $tmp = explode(':', $qname); - return array( - 'namespace' => $tmp[0], - 'localPart' => $tmp[1] - ); - } - return array( - 'namespace' => $defaultNs, - 'localPart' => $qname - ); - } - - /** - * check, whether string is valid XML name - * - *

XML names are used for tagname, attribute names and various - * other, lesser known entities.

- *

An XML name may only consist of alphanumeric characters, - * dashes, undescores and periods, and has to start with a letter - * or an underscore.

- * - * - * require_once 'XML/Util.php'; - * - * // verify tag name - * $result = XML_Util::isValidName('invalidTag?'); - * if (is_a($result, 'PEAR_Error')) { - * print 'Invalid XML name: ' . $result->getMessage(); - * } - * - * - * @param string $string string that should be checked - * - * @return mixed true, if string is a valid XML name, PEAR error otherwise - * @access public - * @static - * @todo support for other charsets - * @todo PEAR CS - unable to avoid 85-char limit on second preg_match - */ - function isValidName($string) - { - // check for invalid chars - if (!preg_match('/^[[:alpha:]_]$/', $string{0})) { - return XML_Util::raiseError('XML names may only start with letter ' - . 'or underscore', XML_UTIL_ERROR_INVALID_START); - } + // create attribute list + $attList = XML_Util::attributesToString($attributes, $sortAttributes, + $multiline, $indent, $linebreak); + $element = sprintf('<%s%s>', $qname, $attList); + return $element; + } + + /** + * create an end element + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = XML_Util::createEndElement('myNs:myTag'); + * + * + * @param string $qname qualified tagname (including namespace) + * + * @return string XML end element + * @access public + * @static + * @see createStartElement() + * @see createTag() + */ + public function createEndElement($qname) { + $element = sprintf('', $qname); + return $element; + } + + /** + * create an XML comment + * + * + * require_once 'XML/Util.php'; + * + * // create an XML start element: + * $tag = XML_Util::createComment('I am a comment'); + * + * + * @param string $content content of the comment + * + * @return string XML comment + * @access public + * @static + */ + public function createComment($content) { + $comment = sprintf('', $content); + return $comment; + } + + /** + * create a CData section + * + * + * require_once 'XML/Util.php'; + * + * // create a CData section + * $tag = XML_Util::createCDataSection('I am content.'); + * + * + * @param string $data data of the CData section + * + * @return string CData section with content + * @access public + * @static + */ + public function createCDataSection($data) { + return sprintf('', + preg_replace('/\]\]>/', ']]]]>', strval($data))); + + } + + /** + * split qualified name and return namespace and local part + * + * + * require_once 'XML/Util.php'; + * + * // split qualified tag + * $parts = XML_Util::splitQualifiedName('xslt:stylesheet'); + * + * the returned array will contain two elements: + *
+   * array(
+   *     'namespace' => 'xslt',
+   *     'localPart' => 'stylesheet'
+   * );
+   * 
+ * + * @param string $qname qualified tag name + * @param string $defaultNs default namespace (optional) + * + * @return array array containing namespace and local part + * @access public + * @static + */ + public function splitQualifiedName($qname, $defaultNs = NULL) { + if (strstr($qname, ':')) { + $tmp = explode(':', $qname); + return array( + 'namespace' => $tmp[0], + 'localPart' => $tmp[1], + ); + } + return array( + 'namespace' => $defaultNs, + 'localPart' => $qname, + ); + } + + /** + * check, whether string is valid XML name + * + *

XML names are used for tagname, attribute names and various + * other, lesser known entities.

+ *

An XML name may only consist of alphanumeric characters, + * dashes, undescores and periods, and has to start with a letter + * or an underscore.

+ * + * + * require_once 'XML/Util.php'; + * + * // verify tag name + * $result = XML_Util::isValidName('invalidTag?'); + * if (is_a($result, 'PEAR_Error')) { + * print 'Invalid XML name: ' . $result->getMessage(); + * } + * + * + * @param string $string string that should be checked + * + * @return mixed true, if string is a valid XML name, PEAR error otherwise + * @access public + * @static + * @todo support for other charsets + * @todo PEAR CS - unable to avoid 85-char limit on second preg_match + */ + public function isValidName($string) { + // check for invalid chars + if (!preg_match('/^[[:alpha:]_]$/', $string{0})) { + return XML_Util::raiseError('XML names may only start with letter ' + . 'or underscore', XML_UTIL_ERROR_INVALID_START); + } - // check for invalid chars - if (!preg_match('/^([[:alpha:]_]([[:alnum:]\-\.]*)?:)?[[:alpha:]_]([[:alnum:]\_\-\.]+)?$/', - $string) - ) { - return XML_Util::raiseError('XML names may only contain alphanumeric ' - . 'chars, period, hyphen, colon and underscores', - XML_UTIL_ERROR_INVALID_CHARS); - } - // XML name is valid - return true; - } - - /** - * replacement for XML_Util::raiseError - * - * Avoids the necessity to always require - * PEAR.php - * - * @param string $msg error message - * @param int $code error code - * - * @return PEAR_Error - * @access public - * @static - * @todo PEAR CS - should this use include_once instead? - */ - function raiseError($msg, $code) - { - require_once 'PEAR.php'; - return PEAR::raiseError($msg, $code); + // check for invalid chars + if (!preg_match('/^([[:alpha:]_]([[:alnum:]\-\.]*)?:)?[[:alpha:]_]([[:alnum:]\_\-\.]+)?$/', + $string) + ) { + return XML_Util::raiseError('XML names may only contain alphanumeric ' + . 'chars, period, hyphen, colon and underscores', + XML_UTIL_ERROR_INVALID_CHARS); } + // XML name is valid + return TRUE; + } + + /** + * replacement for XML_Util::raiseError + * + * Avoids the necessity to always require + * PEAR.php + * + * @param string $msg error message + * @param int $code + * + * @return PEAR_Error + * @access public + * @static + * @todo PEAR CS - should this use include_once instead? + */ + public function raiseError($msg, $code) { + require_once 'PEAR.php'; + return PEAR::raiseError($msg, $code); + } + } -?> diff --git a/ext/ewaysingle/lib/eWAY/eWAY_GatewayRequest.php b/ext/ewaysingle/lib/eWAY/eWAY_GatewayRequest.php index 2d26e59a3142..a3f5984e4b83 100644 --- a/ext/ewaysingle/lib/eWAY/eWAY_GatewayRequest.php +++ b/ext/ewaysingle/lib/eWAY/eWAY_GatewayRequest.php @@ -1,243 +1,215 @@ -txTransactionNumber; - } - - function EwayCustomerID($value) - { - $this->txCustomerID=$value; - } - - function InvoiceAmount($value) - { - $this->txAmount=$value; - } - - function CardHolderName($value) - { - $this->txCardholderName=$value; - } - - function CardExpiryMonth($value) - { - $this->txCardExpiryMonth=$value; - } - - function CardExpiryYear($value) - { - $this->txCardExpiryYear=$value; - } - - function TransactionNumber($value) - { - $this->txTransactionNumber=$value; - } - - function PurchaserFirstName($value) - { - $this->txCardholderFirstName=$value; - } - - function PurchaserLastName($value) - { - $this->txCardholderLastName=$value; - } - - function CardNumber($value) - { - $this->txCardNumber=$value; - } - - function PurchaserAddress($value) - { - $this->txCardholderAddress=$value; - } - - function PurchaserPostalCode($value) - { - $this->txCardholderPostalCode=$value; - } - - function PurchaserEmailAddress($value) - { - $this->txCardholderEmailAddress=$value; - } - - function InvoiceReference($value) - { - $this->txInvoiceReference=$value; - } - - function InvoiceDescription($value) - { - $this->txInvoiceDescription=$value; - } - - function CVN($value) - { - $this->txCVN=$value; - } - - function EwayOption1($value) - { - $this->txOption1=$value; - } - - function EwayOption2($value) - { - $this->txOption2=$value; - } - - function EwayOption3($value) - { - $this->txOption3=$value; - } - - function CustomerBillingCountry($value) - { - $this->txCustomerBillingCountry=$value; - } - - function CustomerIPAddress($value) - { - $this->txCustomerIPAddress=$value; - } - - function ToXml() - { - // We don't really need the overhead of creating an XML DOM object - // to really just concatenate a string together. - - $xml = ""; - $xml .= $this->CreateNode("ewayCustomerID", $this->txCustomerID); - $xml .= $this->CreateNode("ewayTotalAmount", $this->txAmount); - $xml .= $this->CreateNode("ewayCardHoldersName", $this->txCardholderName); - $xml .= $this->CreateNode("ewayCardNumber", $this->txCardNumber); - $xml .= $this->CreateNode("ewayCardExpiryMonth", $this->txCardExpiryMonth); - $xml .= $this->CreateNode("ewayCardExpiryYear", $this->txCardExpiryYear); - $xml .= $this->CreateNode("ewayTrxnNumber", $this->txTransactionNumber); - $xml .= $this->CreateNode("ewayCustomerInvoiceDescription", $this->txInvoiceDescription); - $xml .= $this->CreateNode("ewayCustomerFirstName", $this->txCardholderFirstName); - $xml .= $this->CreateNode("ewayCustomerLastName", $this->txCardholderLastName); - $xml .= $this->CreateNode("ewayCustomerEmail", $this->txCardholderEmailAddress); - $xml .= $this->CreateNode("ewayCustomerAddress", $this->txCardholderAddress); - $xml .= $this->CreateNode("ewayCustomerPostcode", $this->txCardholderPostalCode); - $xml .= $this->CreateNode("ewayCustomerInvoiceRef", $this->txInvoiceReference); - $xml .= $this->CreateNode("ewayCVN", $this->txCVN); - $xml .= $this->CreateNode("ewayOption1", $this->txOption1); - $xml .= $this->CreateNode("ewayOption2", $this->txOption2); - $xml .= $this->CreateNode("ewayOption3", $this->txOption3); - $xml .= $this->CreateNode("ewayCustomerIPAddress", $this->txCustomerIPAddress); - $xml .= $this->CreateNode("ewayCustomerBillingCountry", $this->txCustomerBillingCountry); - $xml .= ""; - - return $xml; - } - - - /******************************************************** - * Builds a simple XML Node - * - * 'NodeName' is the anem of the node being created. - * 'NodeValue' is its value - * - ********************************************************/ - function CreateNode($NodeName, $NodeValue) - { - require_once E::path('lib/XML/Util.php'); - - $xml = new XML_Util(); - $node = "<" . $NodeName . ">" . $xml->replaceEntities($NodeValue) . ""; - return $node; - } - -} // class GatewayRequest - -?> +txTransactionNumber; + } + + public function EwayCustomerID($value) { + $this->txCustomerID = $value; + } + + public function InvoiceAmount($value) { + $this->txAmount = $value; + } + + public function CardHolderName($value) { + $this->txCardholderName = $value; + } + + public function CardExpiryMonth($value) { + $this->txCardExpiryMonth = $value; + } + + public function CardExpiryYear($value) { + $this->txCardExpiryYear = $value; + } + + public function TransactionNumber($value) { + $this->txTransactionNumber = $value; + } + + public function PurchaserFirstName($value) { + $this->txCardholderFirstName = $value; + } + + public function PurchaserLastName($value) { + $this->txCardholderLastName = $value; + } + + public function CardNumber($value) { + $this->txCardNumber = $value; + } + + public function PurchaserAddress($value) { + $this->txCardholderAddress = $value; + } + + public function PurchaserPostalCode($value) { + $this->txCardholderPostalCode = $value; + } + + public function PurchaserEmailAddress($value) { + $this->txCardholderEmailAddress = $value; + } + + public function InvoiceReference($value) { + $this->txInvoiceReference = $value; + } + + public function InvoiceDescription($value) { + $this->txInvoiceDescription = $value; + } + + public function CVN($value) { + $this->txCVN = $value; + } + + public function EwayOption1($value) { + $this->txOption1 = $value; + } + + public function EwayOption2($value) { + $this->txOption2 = $value; + } + + public function EwayOption3($value) { + $this->txOption3 = $value; + } + + public function CustomerBillingCountry($value) { + $this->txCustomerBillingCountry = $value; + } + + public function CustomerIPAddress($value) { + $this->txCustomerIPAddress = $value; + } + + public function ToXml() { + // We don't really need the overhead of creating an XML DOM object + // to really just concatenate a string together. + + $xml = ""; + $xml .= $this->CreateNode("ewayCustomerID", $this->txCustomerID); + $xml .= $this->CreateNode("ewayTotalAmount", $this->txAmount); + $xml .= $this->CreateNode("ewayCardHoldersName", $this->txCardholderName); + $xml .= $this->CreateNode("ewayCardNumber", $this->txCardNumber); + $xml .= $this->CreateNode("ewayCardExpiryMonth", $this->txCardExpiryMonth); + $xml .= $this->CreateNode("ewayCardExpiryYear", $this->txCardExpiryYear); + $xml .= $this->CreateNode("ewayTrxnNumber", $this->txTransactionNumber); + $xml .= $this->CreateNode("ewayCustomerInvoiceDescription", $this->txInvoiceDescription); + $xml .= $this->CreateNode("ewayCustomerFirstName", $this->txCardholderFirstName); + $xml .= $this->CreateNode("ewayCustomerLastName", $this->txCardholderLastName); + $xml .= $this->CreateNode("ewayCustomerEmail", $this->txCardholderEmailAddress); + $xml .= $this->CreateNode("ewayCustomerAddress", $this->txCardholderAddress); + $xml .= $this->CreateNode("ewayCustomerPostcode", $this->txCardholderPostalCode); + $xml .= $this->CreateNode("ewayCustomerInvoiceRef", $this->txInvoiceReference); + $xml .= $this->CreateNode("ewayCVN", $this->txCVN); + $xml .= $this->CreateNode("ewayOption1", $this->txOption1); + $xml .= $this->CreateNode("ewayOption2", $this->txOption2); + $xml .= $this->CreateNode("ewayOption3", $this->txOption3); + $xml .= $this->CreateNode("ewayCustomerIPAddress", $this->txCustomerIPAddress); + $xml .= $this->CreateNode("ewayCustomerBillingCountry", $this->txCustomerBillingCountry); + $xml .= ""; + + return $xml; + } + + /** + * Builds a simple XML Node + * + * 'NodeName' is the anem of the node being created. + * 'NodeValue' is its value + * + */ + public function CreateNode($NodeName, $NodeValue) { + require_once E::path('lib/XML/Util.php'); + + $xml = new XML_Util(); + $node = "<" . $NodeName . ">" . $xml->replaceEntities($NodeValue) . ""; + return $node; + } + +} diff --git a/ext/ewaysingle/lib/eWAY/eWAY_GatewayResponse.php b/ext/ewaysingle/lib/eWAY/eWAY_GatewayResponse.php index 100bfb3fdcf4..08c16b284dc7 100644 --- a/ext/ewaysingle/lib/eWAY/eWAY_GatewayResponse.php +++ b/ext/ewaysingle/lib/eWAY/eWAY_GatewayResponse.php @@ -1,171 +1,150 @@ -txError = $xtr->ewayTrxnError; # -# $this->txStatus = $xtr->ewayTrxnStatus; # -# $this->txTransactionNumber = $xtr->ewayTrxnNumber; # -# $this->txOption1 = $xtr->ewayTrxnOption1; # -# $this->txOption2 = $xtr->ewayTrxnOption2; # -# $this->txOption3 = $xtr->ewayTrxnOption3; # -# $this->txAmount = $xtr->ewayReturnAmount; # -# $this->txAuthCode = $xtr->ewayAuthCode; # -# $this->txInvoiceReference = $xtr->ewayTrxnReference; # -# # -##################################################################################### - - $this->txError = self::GetNodeValue("ewayTrxnError", $Xml); - $this->txStatus = self::GetNodeValue("ewayTrxnStatus", $Xml); - $this->txTransactionNumber = self::GetNodeValue("ewayTrxnNumber", $Xml); - $this->txOption1 = self::GetNodeValue("ewayTrxnOption1", $Xml); - $this->txOption2 = self::GetNodeValue("ewayTrxnOption2", $Xml); - $this->txOption3 = self::GetNodeValue("ewayTrxnOption3", $Xml); - $amount = self::GetNodeValue("ewayReturnAmount", $Xml); - $this->txAuthCode = self::GetNodeValue("ewayAuthCode", $Xml); - $this->txInvoiceReference = self::GetNodeValue("ewayTrxnReference", $Xml); - $this->txBeagleScore = self::GetNodeValue("ewayBeagleScore", $Xml); - $this->txAmount = (int) $amount; - } - - - /************************************************************************ - * Simple function to use in place of the 'simplexml_load_string' call. - * - * It returns the NodeValue for a given NodeName - * or returns and empty string. - ************************************************************************/ - function GetNodeValue($NodeName, &$strXML) - { - $OpeningNodeName = "<" . $NodeName . ">"; - $ClosingNodeName = ""; - - $pos1 = stripos($strXML, $OpeningNodeName); - $pos2 = stripos($strXML, $ClosingNodeName); - - if ( ($pos1 === false) || ($pos2 === false) ) - return ""; - - $pos1 += strlen($OpeningNodeName); - $len = $pos2 - $pos1; - - $return = substr($strXML, $pos1, $len); - - return ($return); - } - - - function TransactionNumber() - { - return $this->txTransactionNumber; - } - - function InvoiceReference() - { - return $this->txInvoiceReference; - } - - function Option1() - { - return $this->txOption1; - } - - function Option2() - { - return $this->txOption2; - } - - function Option3() - { - return $this->txOption3; - } - - function AuthorisationCode() - { - return $this->txAuthCode; - } - - function Error() - { - return $this->txError; - } - - function Amount() - { - return $this->txAmount; - } - - function Status() - { - return $this->txStatus; - } - - function BeagleScore () - { - return $this->txBeagleScore ; - } -} - -?> +txError = $xtr->ewayTrxnError; + //$this->txStatus = $xtr->ewayTrxnStatus; + //$this->txTransactionNumber = $xtr->ewayTrxnNumber; + //$this->txOption1 = $xtr->ewayTrxnOption1; + //$this->txOption2 = $xtr->ewayTrxnOption2; + //$this->txOption3 = $xtr->ewayTrxnOption3; + //$this->txAmount = $xtr->ewayReturnAmount; + //$this->txAuthCode = $xtr->ewayAuthCode; + //$this->txInvoiceReference = $xtr->ewayTrxnReference; + + $this->txError = self::GetNodeValue("ewayTrxnError", $Xml); + $this->txStatus = self::GetNodeValue("ewayTrxnStatus", $Xml); + $this->txTransactionNumber = self::GetNodeValue("ewayTrxnNumber", $Xml); + $this->txOption1 = self::GetNodeValue("ewayTrxnOption1", $Xml); + $this->txOption2 = self::GetNodeValue("ewayTrxnOption2", $Xml); + $this->txOption3 = self::GetNodeValue("ewayTrxnOption3", $Xml); + $amount = self::GetNodeValue("ewayReturnAmount", $Xml); + $this->txAuthCode = self::GetNodeValue("ewayAuthCode", $Xml); + $this->txInvoiceReference = self::GetNodeValue("ewayTrxnReference", $Xml); + $this->txBeagleScore = self::GetNodeValue("ewayBeagleScore", $Xml); + $this->txAmount = (int) $amount; + } + + /** + * Simple function to use in place of the 'simplexml_load_string' call. + * + * It returns the NodeValue for a given NodeName + * or returns and empty string. + */ + public function GetNodeValue($NodeName, &$strXML) { + $OpeningNodeName = "<" . $NodeName . ">"; + $ClosingNodeName = ""; + + $pos1 = stripos($strXML, $OpeningNodeName); + $pos2 = stripos($strXML, $ClosingNodeName); + + if (($pos1 === FALSE) || ($pos2 === FALSE)) { + return ""; + } + + $pos1 += strlen($OpeningNodeName); + $len = $pos2 - $pos1; + + $return = substr($strXML, $pos1, $len); + + return ($return); + } + + public function TransactionNumber() { + return $this->txTransactionNumber; + } + + public function InvoiceReference() { + return $this->txInvoiceReference; + } + + public function Option1() { + return $this->txOption1; + } + + public function Option2() { + return $this->txOption2; + } + + public function Option3() { + return $this->txOption3; + } + + public function AuthorisationCode() { + return $this->txAuthCode; + } + + public function Error() { + return $this->txError; + } + + public function Amount() { + return $this->txAmount; + } + + public function Status() { + return $this->txStatus; + } + + public function BeagleScore () { + return $this->txBeagleScore; + } + +} diff --git a/ext/ewaysingle/phpunit.xml.dist b/ext/ewaysingle/phpunit.xml.dist new file mode 100644 index 000000000000..fc8f870b723b --- /dev/null +++ b/ext/ewaysingle/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + ./tests/phpunit + + + + + ./ + + + + + + + + diff --git a/ext/ewaysingle/tests/phpunit/CRM/Core/Payment/EwayTest.php b/ext/ewaysingle/tests/phpunit/CRM/Core/Payment/EwayTest.php new file mode 100644 index 000000000000..6591c5bb1676 --- /dev/null +++ b/ext/ewaysingle/tests/phpunit/CRM/Core/Payment/EwayTest.php @@ -0,0 +1,212 @@ +installMe(__DIR__) + ->apply(); + } + + public function setUp() { + $this->setUpEwayProcessor(); + $this->processor = \Civi\Payment\System::singleton()->getById($this->ids['PaymentProcessor']['eWAY']); + parent::setUp(); + } + + public function tearDown() { + $this->callAPISuccess('PaymentProcessor', 'delete', ['id' => $this->ids['PaymentProcessor']['eWAY']]); + parent::tearDown(); + } + + /** + * Test making a once off payment + */ + public function testSinglePayment() { + $this->setupMockHandler(); + $params = $this->getBillingParams(); + $params['amount'] = 10.00; + $params['currency'] = 'AUD'; + $params['description'] = 'Test Contribution'; + $params['invoiceID'] = 'xyz'; + $params['email'] = 'unittesteway@civicrm.org'; + $params['ip_address'] = '127.0.0.1'; + foreach ($params as $key => $value) { + // Paypal is super special and requires this. Leaving out of the more generic + // get billing params for now to make it more obvious. + // When/if PropertyBag supports all the params paypal needs we can convert & simplify this. + $params[str_replace('-5', '', str_replace('billing_', '', $key))] = $value; + } + $params['state_province'] = 'NSW'; + $params['country'] = 'AUS'; + $this->processor->doPayment($params); + $this->assertEquals($this->getExpectedSinglePaymentRequests(), $this->getRequestBodies()); + } + + /** + * Test making a failed once off payment + */ + public function testErrorSinglePayment() { + $this->setupMockHandler(NULL, TRUE); + $params = $this->getBillingParams(); + $params['amount'] = 5.24; + $params['currency'] = 'AUD'; + $params['description'] = 'Test Contribution'; + $params['invoiceID'] = 'xyz'; + $params['email'] = 'unittesteway@civicrm.org'; + $params['ip_address'] = '127.0.0.1'; + foreach ($params as $key => $value) { + // Paypal is super special and requires this. Leaving out of the more generic + // get billing params for now to make it more obvious. + // When/if PropertyBag supports all the params paypal needs we can convert & simplify this. + $params[str_replace('-5', '', str_replace('billing_', '', $key))] = $value; + } + $params['state_province'] = 'NSW'; + $params['country'] = 'AUS'; + try { + $this->processor->doPayment($params); + $this->fail('Test was meant to throw an exception'); + } + catch (\Civi\Payment\Exception\PaymentProcessorException $e) { + $this->assertEquals('Error: [24] - Do Not Honour(Test Gateway).', $e->getMessage()); + $this->assertEquals(9008, $e->getErrorCode()); + } + } + + /** + * Get some basic billing parameters. + * + * These are what are entered by the form-filler. + * + * @return array + */ + protected function getBillingParams(): array { + return [ + 'billing_first_name' => 'John', + 'billing_middle_name' => '', + 'billing_last_name' => "O'Connor", + 'billing_street_address-5' => '8 Hobbitton Road', + 'billing_city-5' => 'The Shire', + 'billing_state_province_id-5' => 1012, + 'billing_postal_code-5' => 5010, + 'billing_country_id-5' => 1228, + 'credit_card_number' => '4444333322221111', + 'cvv2' => 123, + 'credit_card_exp_date' => [ + 'M' => 9, + 'Y' => 2025, + ], + 'credit_card_type' => 'Visa', + 'year' => 2022, + 'month' => 10, + ]; + } + + public function setUpEwayProcessor() { + $params = [ + 'name' => 'demo', + 'domain_id' => CRM_Core_Config::domainID(), + 'payment_processor_type_id' => 'eWAY', + 'is_active' => 1, + 'is_default' => 0, + 'is_test' => 0, + 'user_name' => '87654321', + 'url_site' => 'https://www.eway.com.au/gateway/xmltest/testpage.asp', + 'class_name' => 'Payment_eWAY', + 'billing_mode' => 1, + 'financial_type_id' => 1, + 'financial_account_id' => 12, + // Credit card = 1 so can pass 'by accident'. + 'payment_instrument_id' => 'Debit Card', + ]; + if (!is_numeric($params['payment_processor_type_id'])) { + // really the api should handle this through getoptions but it's not exactly api call so lets just sort it + //here + $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [ + 'name' => $params['payment_processor_type_id'], + 'return' => 'id', + ], 'integer'); + } + $result = $this->callAPISuccess('payment_processor', 'create', $params); + $processorID = $result['id']; + $this->setupMockHandler($processorID); + $this->ids['PaymentProcessor']['eWAY'] = $processorID; + } + + /** + * Add a mock handler to the paypal Pro processor for testing. + * + * @param int|null $id + * @param bool $error + * + * @throws \CiviCRM_API3_Exception + */ + protected function setupMockHandler($id = NULL, $error = FALSE) { + if ($id) { + $this->processor = Civi\Payment\System::singleton()->getById($id); + } + $responses = $error ? $this->getExpectedSinglePaymentErrorResponses() : $this->getExpectedSinglePaymentResponses(); + // Comment the next line out when trying to capture the response. + // see https://github.com/civicrm/civicrm-core/pull/18350 + $this->createMockHandler($responses); + $this->setUpClientWithHistoryContainer(); + $this->processor->setGuzzleClient($this->getGuzzleClient()); + } + + /** + * Get the expected response from eWAY for a single payment. + * + * @return array + */ + public function getExpectedSinglePaymentResponses() { + return [ + 'True10002xyz123456100000,Transaction Approved(Test Gateway)', + ]; + } + + /** + * Get the expected request from eWAY. + * + * @return array + */ + public function getExpectedSinglePaymentRequests() { + return [ + '876543211000John O'Connor44443333222211111022xyzTest ContributionJohnO'Connorunittesteway@civicrm.org8 Hobbitton Road, The Shire, NSW.5010xyz123127.0.0.1AUS', + ]; + } + + /** + * Get the expected response from eWAY for a single payment. + * + * @return array + */ + public function getExpectedSinglePaymentErrorResponses() { + return [ + 'False10003xyz12345652424,Do Not Honour(Test Gateway)', + ]; + } + +} diff --git a/ext/ewaysingle/tests/phpunit/bootstrap.php b/ext/ewaysingle/tests/phpunit/bootstrap.php new file mode 100644 index 000000000000..a5b49253c819 --- /dev/null +++ b/ext/ewaysingle/tests/phpunit/bootstrap.php @@ -0,0 +1,63 @@ +add('CRM_', __DIR__); +$loader->add('Civi\\', __DIR__); +$loader->add('api_', __DIR__); +$loader->add('api\\', __DIR__); +$loader->register(); + +/** + * Call the "cv" command. + * + * @param string $cmd + * The rest of the command to send. + * @param string $decode + * Ex: 'json' or 'phpcode'. + * @return string + * Response output (if the command executed normally). + * @throws \RuntimeException + * If the command terminates abnormally. + */ +function cv($cmd, $decode = 'json') { + $cmd = 'cv ' . $cmd; + $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR); + $oldOutput = getenv('CV_OUTPUT'); + putenv("CV_OUTPUT=json"); + + // Execute `cv` in the original folder. This is a work-around for + // phpunit/codeception, which seem to manipulate PWD. + $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); + + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, 1); + + default: + throw new RuntimeException("Bad decoder format ($decode)"); + } +}