*
* use OndraKoupil\Csob;
*
* $config = new Config(
* "Your Merchant ID",
* "Path to your private key file",
* "Path to bank's public key file",
* "Your e-shop name",
* "Some URL to return customers to",
* GatewayUrl::TEST_LATEST
* );
*
* $client = new Client($config);
*
* // Check if connection and signing works
* $client->testGetConnection();
* $client->testPostConnection();
*
* // Create new payment with some item in cart
* $payment = new Payment("12345");
* $payment->addCartItem("Some cool stuff", 1, 10000);
*
* $client->paymentInit($payment);
*
* // Check for payment status
* $client->paymentStatus($payment);
*
* // Get URL to send the customer to
* $url = $client->getPaymentProcessUrl($payment);
*
* // Or redirect him there right now
* $client->redirectToGateway($payment);
*
* // Cancel the payment
* $client->paymentReverse($payment);
*
* // Or approve the payment (if not set to do that automatically)
* $client->paymentClose($payment);
*
*
*
*/
class Client {
const DATE_FORMAT = "YmdHis";
/**
* Customer with given ID was not found
*/
const CUST_NOT_FOUND = 800;
/**
* Customer found, but has no saved cards
*/
const CUST_NO_CARDS = 810;
/**
* Customer found and has some cards
*/
const CUST_CARDS = 820;
/**
* @var Config
* @ignore
*/
protected $config;
/**
* @ignore
* @var string
*/
protected $logFile;
/**
* @ignore
* @var callable
*/
protected $logCallback;
/**
* @ignore
* @var string
*/
protected $traceLogFile;
/**
* @ignore
* @var callable
*/
protected $traceLogCallback;
// ------- BASICS --------
/**
* Create new client with given Config.
*
* @param Config $config
* @param callable|string $log Log for messages concerning payments
* at bussiness-logic level. Either a string filename or a callback
* that you can use to forward messages to your own logging system.
* @param callable|string $traceLog Log for technical messages with
* exact contents of communication.
*/
function __construct(Config $config, $log = null, $traceLog = null) {
$this->config = $config;
if ($log) {
$this->setLog($log);
}
if ($traceLog) {
$this->setTraceLog($traceLog);
}
}
/**
* @return Config
*/
function getConfig() {
return $this->config;
}
/**
* @param Config $config
* @return Client
*/
function setConfig(Config $config) {
$this->config = $config;
return $this;
}
// ------- API CALL METHODS --------
/**
* Performs payment/init API call.
*
* Use this to create new payment in payment gateway.
* After successful call, the $payment object will be updated by given PayID.
*
* @param Payment $payment Create and fill this object manually with real data.
* @param Extension[]|Extension $extensions Added extensions
*
* @return array Array with results of the call. You don't need to use
* any of this, PayID will be set to $payment automatically.
*/
function paymentInit(Payment $payment, $extensions = array()) {
$payment->checkAndPrepare($this->config);
$array = $payment->signAndExport($this);
$this->writeToLog("payment/init started for payment with orderNo " . $payment->orderNo);
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?authCode");
if($this->getConfig()->queryApiVersion('1.8')){
$returnDataNames = array_merge($returnDataNames, array("?customerCode","?statusDetail"));
}
try {
$ret = $this->sendRequest(
"payment/init",
$array,
"POST",
$returnDataNames,
null,
false,
false,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
if (!isset($ret["payId"]) or !$ret["payId"]) {
$this->writeToLog("Fail, no payId received.");
throw new Exception("Bank API did not return a payId value.");
}
$payment->setPayId($ret["payId"]);
$this->writeToLog("payment/init OK, got payId ".$ret["payId"]);
return $ret;
}
/**
* Generates URL to send customer's browser to after initiating the payment.
*
* Use this after you successfully called paymentInit() and redirect
* the customer's browser on the URL that this method returns manually,
* or use redirectToGateway().
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @return string
*
* @see redirectToGateway()
*/
function getPaymentProcessUrl($payment) {
$payId = $this->getPayId($payment);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM()
);
$payload["signature"] = $this->signRequest($payload);
$url = $this->sendRequest(
"payment/process",
$payload,
"GET",
array(),
array("merchantId", "payId", "dttm", "signature"),
true
);
$this->writeToLog("URL for processing payment ".$payId.": $url");
return $url;
}
/**
* Generates checkout URL to send customer's browser to after initiating
* the payment.
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @param int $oneClickPaymentCheckbox Flag to indicate whether to display
* the checkbox for saving card for future payments and to indicate whether
* it should be preselected or not.
* 0 - hidden, unchecked
* 1 - displayed, unchecked
* 2 - displayed, checked
* 3 - hidden, checked (you need to indicate to customer that this happens
* before initiating the payment)
*
* @param bool|null $displayOmnibox Flag to indicate whether to display
* omnibox in desktop's iframe version instead of card number, expiration
* and cvc fields
*
* @param string|null $returnCheckoutUrl URL for scenario when process needs
* to get back to checkout page
*
* @return string
*/
function getPaymentCheckoutUrl($payment, $oneClickPaymentCheckbox, $displayOmnibox = null, $returnCheckoutUrl = null) {
$payId = $this->getPayId($payment);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM(),
"oneclickPaymentCheckbox" => $oneClickPaymentCheckbox,
);
if ($displayOmnibox !== null) {
$payload["displayOmnibox"] = $displayOmnibox ? "true" : "false";
}
if ($returnCheckoutUrl !== null) {
$payload["returnCheckoutUrl"] = $returnCheckoutUrl;
}
$payload["signature"] = $this->signRequest($payload);
$url = $this->sendRequest(
"payment/checkout",
$payload,
"GET",
array(),
array("merchantId", "payId", "dttm", "oneclickPaymentCheckbox", "displayOmnibox", "returnCheckoutUrl", "signature"),
true
);
$this->writeToLog("URL for processing payment ".$payId.": $url");
return $url;
}
/**
* Redirect customer's browser to payment gateway.
*
* Use this after you successfully called paymentInit()
*
* Note that HTTP headers must not have been sent before.
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @throws Exception If headers has been already sent
*/
function redirectToGateway($payment) {
if (headers_sent($file, $line)) {
$this->writeToLog("Can't redirect, headers sent at $file, line $line");
throw new Exception("Can't redirect the browser, headers were already sent at $file line $line");
}
$url = $this->getPaymentProcessUrl($payment);
$this->writeToLog("Redirecting to payment gate...");
header("HTTP/1.1 302 Moved");
header("Location: $url");
header("Connection: close");
}
/**
* Check payment status by calling payment/status API method.
*
* Use this to check current status of some transaction.
* See ČSOB's wiki on Github for explanation of each status.
*
* Basically, they are:
*
* - 1 = new; after paymentInit() but before customer starts filling in his
* card number and authorising the transaction
* - 2 = in progress; during customer's stay at payment gateway
* - 4 = after successful authorisation but before it is approved by you by
* calling paymentClose. This state is skipped if you use
* Payment->closePayment = true or Config->closePayment = true.
* - 7 = waiting for being processed by bank. The payment will remain in this
* state for about one working day. You can call paymentReverse() during this
* time to stop it from being processed.
* - 5 = cancelled by you. Payment gets here after calling paymentReverse().
* - 8 = finished. This means money is already probably on your account.
* If you want to cancel the payment, you can only refund it by calling
* paymentRefund().
* - 9 = being refunded
* - 10 = refunded
* - 6 or 3 = payment was not authorised or was cancelled by customer
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @param bool $returnStatusOnly Leave on true if you want to return only
* status code. Set to false if you want more information as array.
*
* @param Extension[]|Extension $extensions
*
* @return array|number Number if $returnStatusOnly was true, array otherwise.
*/
function paymentStatus($payment, $returnStatusOnly = true, $extensions = array(), $nullIfPaymentNotFound = true) {
$payId = $this->getPayId($payment);
$this->writeToLog("payment/status started for payment $payId");
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM()
);
try {
$payload["signature"] = $this->signRequest($payload);
// Payment status is optional, bank doesn't include it in signature base if the payment is not found.
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?authCode");
if ($this->getConfig()->queryApiVersion('1.8')){
$returnDataNames = array_merge($returnDataNames, array("?customerCode","?statusDetail"));
}
if ($this->getConfig()->queryApiVersion('1.9')){
$returnDataNames[] = '?actions';
}
$ret = $this->sendRequest(
"payment/status",
$payload,
"GET",
$returnDataNames,
array(
"merchantId",
"payId",
"dttm",
"signature",
),
false,
false,
$extensions
);
} catch (Exception $e) {
if ($nullIfPaymentNotFound and $e->getCode() === 140) {
// Error 140 = payment not found
return null;
} else {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
}
$this->writeToLog("payment/status OK, status of payment $payId is ".$ret["paymentStatus"]);
if ($returnStatusOnly) {
return $ret["paymentStatus"];
}
return $ret;
}
/**
* Performs payment/reverse API call.
*
* Reversing payment means stopping payment that has not yet been processed
* by bank (usually about 1 working day after being created).
*
* Normally, payment must be in state 4 or 7 to be reversable.
* If the payment is not in an acceptable state, then the gateway
* returns an error code 150 and exception is thrown from here.
* Set $ignoreWrongPaymentStatusError to true if you are okay with that
* situation and you want to silently ignore it. Method then returns null.
*
* If some other type of error occurs, exception will be thrown anyway.
*
* @param string|array|Payment $payment Either PayID given during paymentInit(),
* or whole returned array from paymentInit or just the Payment object
* you used in paymentInit()
*
* @param bool $ignoreWrongPaymentStatusError
*
* @param Extension[]|Extension $extensions
*
* @return array|null Array with results of call or null if payment is not
* in correct state
*
*
* @throws Exception
*/
function paymentReverse($payment, $ignoreWrongPaymentStatusError = false, $extensions = array()) {
$payId = $this->getPayId($payment);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM()
);
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?authCode");
if($this->getConfig()->queryApiVersion('1.8')){
$returnDataNames = array_merge($returnDataNames, array("?customerCode","?statusDetail"));
}
$this->writeToLog("payment/reverse started for payment $payId");
try {
$payload["signature"] = $this->signRequest($payload);
try {
$ret = $this->sendRequest(
"payment/reverse",
$payload,
"PUT",
$returnDataNames,
array("merchantId", "payId", "dttm", "signature"),
false,
false,
$extensions
);
} catch (Exception $e) {
if ($e->getCode() != 150) { // Not just invalid state
throw $e;
}
if (!$ignoreWrongPaymentStatusError) {
throw $e;
}
$this->writeToLog("payment/reverse failed, payment is not in correct status");
return null;
}
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("payment/reverse OK");
return $ret;
}
/**
* Performs payment/close API call.
*
* If you want to accept (close) payments manually, set property $closePayment
* of Payment object or Config object to false. Then, payments will wait in
* state 4 for your approval by calling paymentClose().
*
* Normally, payment must be in state 4 to be eligible for this operation.
* If the payment is not in an acceptable state, then the gateway
* returns an error code 150 and exception is thrown from here.
* Set $ignoreWrongPaymentStatusError to true if you are okay with that
* situation and you want to silently ignore it. Method then returns null.
*
* If some other type of error occurs, exception will be thrown in all cases.
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @param bool $ignoreWrongPaymentStatusError
*
* @param int $amount Amount of finance to close (if different from originally authorized amount).
* Use hundreths of basic currency unit.
*
* @param Extension[]|Extension $extensions
*
* @return array|null Array with results of call or null if payment is not
* in correct state
*
*
* @throws Exception
*/
function paymentClose($payment, $ignoreWrongPaymentStatusError = false, $amount = null, $extensions = array()) {
$payId = $this->getPayId($payment);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM()
);
if ($amount !== null) {
$payload["totalAmount"] = $amount;
}
$this->writeToLog("payment/close started for payment $payId" . ($amount !== null ? ", amount $amount" : ""));
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?authCode");
if($this->getConfig()->queryApiVersion('1.8')){
$returnDataNames = array_merge($returnDataNames, array("?customerCode","?statusDetail"));
}
try {
$payload["signature"] = $this->signRequest($payload);
try {
$ret = $this->sendRequest(
"payment/close",
$payload,
"PUT",
$returnDataNames,
array("merchantId", "payId", "dttm", "totalAmount", "signature"),
false,
false,
$extensions
);
} catch (Exception $e) {
if ($e->getCode() != 150) { // Not just invalid state
throw $e;
}
if (!$ignoreWrongPaymentStatusError) {
throw $e;
}
$this->writeToLog("payment/close failed, payment is not in correct status");
return null;
}
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("payment/close OK");
return $ret;
}
/**
* Performs payment/refund API call.
*
* If you want to send money back to your customer after payment has been
* completely processed and money transferred, use this method.
*
* Normally, payment must be in state 8 to be eligible for this operation.
* If the payment is not in an acceptable state, then the gateway
* returns an error code 150 and exception is thrown from here.
* Set $ignoreWrongPaymentStatusError to true if you are okay with that
* situation and you want to silently ignore it. Method then returns null.
*
* If some other type of error occurs, exception will be thrown in all cases.
*
* Note: on testing environment, refunding often ends up with HTTP code 500
* and exception thrown. This is a bug in payment gateway - see
* https://github.com/csob/paymentgateway/issues/43, however it is still not resolved.
* On production environment, this should be OK.
*
* @param string|Payment $payment Either PayID given during paymentInit(),
* or just the Payment object you used in paymentInit()
*
* @param bool $ignoreWrongPaymentStatusError
*
* @param int $amount Optionally, an amount (in hundreths of basic money unit - beware!)
* can be passed, so that the payment will be refunded partially.
* Null means full refund.
*
* @param Extension[]|Extension $extensions
*
* @return array|null Array with results of call or null if payment is not
* in correct state
*/
function paymentRefund($payment, $ignoreWrongPaymentStatusError = false, $amount = null, $extensions = array()) {
$payId = $this->getPayId($payment);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"dttm" => $this->getDTTM()
);
if ($amount !== null) {
if (!is_numeric($amount)) {
throw new Exception("Amount for refunding must be a number.");
}
$payload["amount"] = $amount;
}
$this->writeToLog("payment/refund started for payment $payId, amount = " . ($amount !== null ? $amount : "null"));
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?authCode");
if($this->getConfig()->queryApiVersion('1.8')){
$returnDataNames = array_merge($returnDataNames, array("?customerCode","?statusDetail"));
}
try {
$payloadForSigning = $payload;
/*
if (isset($payloadForSigning["amount"])) {
unset($payloadForSigning["amount"]);
}
*/
$payload["signature"] = $this->signRequest($payloadForSigning);
try {
$ret = $this->sendRequest(
"payment/refund",
$payload,
"PUT",
$returnDataNames,
array("merchantId", "payId", "dttm", "amount", "signature"),
false,
false,
$extensions
);
} catch (Exception $e) {
if ($e->getCode() != 150) { // Not just invalid state
throw $e;
}
if (!$ignoreWrongPaymentStatusError) {
throw $e;
}
$this->writeToLog("payment/refund failed, payment is not in correct status");
return null;
}
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("payment/refund OK");
return $ret;
}
/**
* Performs payment/recurrent API call.
*
* Use this method to redo a payment that has already been marked as
* a template for recurring payments and approved by customer
* - see Payment::setRecurrentPayment()
*
* You need PayID of the original payment and a new Payment object.
* Only $orderNo, $totalAmount (sum of cart items added by addToCart), $currency
* and $description of $newPayment are used, others are ignored.
*
* Note that if $totalAmount is set, then also $currency must be set. If not,
* CZK is used as default value.
*
* $orderNo is the only mandatory value in $newPayment. Other properties
* can be left null to use original values from $origPayment.
*
* After successful call, received PayID will be set in $newPayment object.
*
* @param Payment|string $origPayment Either string PayID or a Payment object
* @param Payment $newPayment
* @return array Data with new values
* @throws Exception
*
* @deprecated Deprecated since eAPI 1.7, please use paymentOneClick() instead.
*
* @see Payment::setRecurrentPayment()
* @see paymentOneClickInit()
*/
function paymentRecurrent($origPayment, Payment $newPayment) {
trigger_error('paymentRecurrent() is deprecated now, please use paymentOneClick() instead.', E_USER_DEPRECATED);
$origPayId = $this->getPayId($origPayment);
$newOrderNo = $newPayment->orderNo;
if (!$newOrderNo or !preg_match('~^\d{1,10}$~', $newOrderNo)) {
throw new Exception("Given Payment object must have an \$orderNo property, numeric, max. 10 chars length.");
}
$newPaymentCart = $newPayment->getCart();
if ($newPaymentCart) {
$totalAmount = array_sum(Arrays::transform($newPaymentCart, true, "amount"));
} else {
$totalAmount = 0;
}
$newDescription = Strings::shorten($newPayment->description, 240, "...");
$payload = array(
"merchantId" => $this->config->merchantId,
"origPayId" => $origPayId,
"orderNo" => $newOrderNo,
"dttm" => $this->getDTTM(),
);
if ($totalAmount > 0) {
$payload["totalAmount"] = $totalAmount;
$payload["currency"] = $newPayment->currency ?: "CZK"; // Currency is mandatory since 2016-01-10
}
if ($newDescription) {
$payload["description"] = $newDescription;
}
$this->writeToLog("payment/recurrent started using orig payment $origPayId");
try {
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest(
"payment/recurrent",
$payload,
"POST",
array("payId", "dttm", "resultCode", "resultMessage", "paymentStatus", "?authCode"),
array("merchantId", "origPayId", "orderNo", "dttm", "totalAmount", "currency", "description", "signature")
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("payment/recurrent OK, new payment got payId " . $ret["payId"]);
$newPayment->setPayId($ret["payId"]);
return $ret;
}
/**
* Performs a payment/oneclick/init API call.
*
* Use this method to redo a payment that has already been marked as
* a template for recurring payments and approved by customer
* - see Payment::setOneClickPayment()
*
* You need PayID of the original payment and a new Payment object.
* Only $orderNo, $totalAmount (sum of cart items added by addToCart), $currency
* and $description of $newPayment are used, others are ignored.
*
* Note that if $totalAmount is set, then also $currency must be set. If not,
* CZK is used as default value.
*
* $orderNo is the only mandatory value in $newPayment. Other properties
* can be left null to use original values from $origPayment.
*
* After successful call, received PayID will be set in $newPayment object.
* Then, pass this object to paymentOneClickStart method.
*
* This method is a successor of now deprecated paymentRecurrent() method.
*
* Since API v 1.8, this method uses oneclick/init endpoint.
*
* @param Payment|string $origPayment Either string PayID or a Payment object
* @param Payment $newPayment
* @param Extension[]|Extension $extensions
* @param string $clientIp IP address of customer's browser
* @param bool $clientInitiated Indicates whether it is possible to payment authentication in the presence of the customer. New in API 1.9.
* Strongly recommend setting to false, or you might have to perform additional verification actions that are not quite supported on this library.
*
* @return array Data with new values
*
* @see Payment::setOneClickPayment()
* @see paymentOneClickStart()
*/
function paymentOneClickInit($origPayment, Payment $newPayment, $extensions = array(), $clientIp = '', $clientInitiated = false) {
$origPayId = $this->getPayId($origPayment);
$newOrderNo = $newPayment->orderNo;
$newPayment->origPayId = $origPayId;
if (!$newOrderNo or !preg_match('~^\d{1,10}$~', $newOrderNo)) {
throw new Exception("Given Payment object must have an \$orderNo property, numeric, max. 10 chars length.");
}
$newPaymentCart = $newPayment->getCart();
if ($newPaymentCart) {
$totalAmount = array_sum(Arrays::transform($newPaymentCart, true, "amount"));
} else {
$totalAmount = 0;
}
$newDescription = Strings::shorten($newPayment->description, 240, "...");
$version1_8 = $this->config->queryApiVersion('1.8');
$version1_9 = $this->config->queryApiVersion('1.9');
$payload = array(
"merchantId" => $this->config->merchantId,
"origPayId" => $origPayId,
"orderNo" => $newOrderNo,
"dttm" => $this->getDTTM(),
);
if ($version1_8) {
// A new parameter appeared in v 1.8
$payload['clientIp'] = $clientIp;
}
if ($totalAmount > 0) {
$payload["totalAmount"] = $totalAmount;
$payload["currency"] = $newPayment->currency ?: "CZK"; // Currency is mandatory since 2016-01-10
}
if ($version1_9) {
$payload['closePayment'] = !!$newPayment->closePayment;
$payload['returnUrl'] = $newPayment->returnUrl ?: $this->config->returnUrl;
$payload['returnMethod'] = $newPayment->returnMethod ?: $this->config->returnMethod;
if ($newPayment->getCustomer()) {
$payload['customer'] = $newPayment->getCustomer()->export();
}
if ($newPayment->getOrder()) {
$payload['order'] = $newPayment->getOrder()->export();
}
$payload['clientInitiated'] = !!$clientInitiated;
}
if ($newDescription and !$version1_8) {
// In v 1.8, there is no description anymore
$payload["description"] = $newDescription;
}
if ($version1_8) {
// A new parameter appeared in v 1.8
$payload['merchantData'] = $newPayment->getMerchantDataEncoded();
}
$endpointName = $this->config->queryApiVersion('1.8') ? 'oneclick/init' : 'payment/oneclick/init';
$signatureBase = Tools::linearizeForSigning($payload);
$payload["signature"] = $this->signRequest(array($signatureBase));
//$newPayment->checkAndPrepare($this->config);
//$payload = $newPayment->signAndExport($this);
//
//
//$this->writeToLog($endpointName . " started for payment with orderNo " . $newPayment->orderNo);
$returnDataNames = array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus");
if ($this->getConfig()->queryApiVersion('1.9')){
$returnDataNames = array_merge($returnDataNames, array("?statusDetail","?actions"));
}
try {
$ret = $this->sendRequest(
$endpointName,
$payload,
"POST",
$returnDataNames,
null,
false,
false,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("$endpointName OK, new payment got payId " . $ret["payId"]);
$newPayment->setPayId($ret["payId"]);
return $ret;
}
/**
* Performs a payment/oneclick/start (or oneclick/start) API call.
*
* Use this method to confirm a recurring one click payment
* that was previously initiated using paymentOneClickInit() method.
*
* @param Payment $newPayment
* @param Extension[]|Extension $extensions
*
* @deprecated Deprecated in API 1.9 - use paymentOneClickProcess now.
*
* @return array|string
*/
function paymentOneClickStart(Payment $newPayment, $extensions = array()) {
if ($this->config->queryApiVersion('1.9')) {
throw new Exception('paymentOneClickStart() is not available in API 1.9 anymore, use paymentOneClickProcess() instead.');
}
$newPayId = $newPayment->getPayId();
if (!$newPayId) {
throw new Exception('Given Payment object does not have a PayId. Please provide a Payment object that was returned from paymentOneClickInit() method.');
}
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $newPayId,
"dttm" => $this->getDTTM(),
);
$this->writeToLog("payment/oneclick/start started with PayId $newPayId");
$endpointName = $this->config->queryApiVersion('1.8') ? 'oneclick/start' : 'payment/oneclick/start';
try {
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest(
$endpointName,
$payload,
"POST",
array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus"),
array("merchantId", "payId", "dttm", "signature"),
false,
false,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("payment/oneclick/start OK");
return $ret;
}
/**
* Performs a oneclick/echo API call.
*
* Use this method to check whether a oneclick template is still ready to be used.
*
* CAUTION! The method returns a numeric code. 0 means success. Positive number means failure.
* Do not just `if ($client->paymentOneClickEcho($payId)) { success(); } else { fail(); }`!!
*
* See https://github.com/csob/paymentgateway/wiki/Vol%C3%A1n%C3%AD-rozhran%C3%AD-eAPI#operation-return-code
* for explanation.
*
* @param string $origPayId The PayID of original payment template.
*
* @return number 0 = template is OK, number 700-740 = template is not OK.
*
* @see https://github.com/csob/paymentgateway/wiki/Vol%C3%A1n%C3%AD-rozhran%C3%AD-eAPI#operation-return-code
*/
function paymentOneClickEcho($origPayId) {
$payload = array(
"merchantId" => $this->config->merchantId,
"origPayId" => $origPayId,
"dttm" => $this->getDTTM(),
);
$this->writeToLog("oneclick/echo started with \$origPayId $origPayId");
$resultCode = 0;
try {
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest(
"oneclick/echo",
$payload,
"POST",
array("origPayId", "dttm", "resultCode", "resultMessage", "?paymentStatus"),
array("merchantId", "origPayId", "dttm", "signature"),
false,
false
);
$resultCode = $ret['resultCode'];
} catch (Exception $e) {
if ($e->getCode() >= 700 and $e->getCode() <= 740) {
// This is the way bank responds for unusable payment templates (origPayIds).
$resultCode = $e->getCode();
} else {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
}
$this->writeToLog("oneclick/echo OK, result " . $resultCode);
return +$resultCode;
}
/**
* Performs a oneclick/process API call.
*
* Use this method to confirm a recurring one click payment after you initialised it using paymentOneClickInit().
*
*
* @param $newPayId
*
* @return array|string
*/
function paymentOneClickProcess($newPayId) {
if (!$this->config->queryApiVersion('1.9')) {
throw new Exception('paymentOneClickProcess() is only available since API 1.9.');
}
$newPayId = $this->getPayId($newPayId);
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $newPayId,
"dttm" => $this->getDTTM(),
);
$this->writeToLog("oneclick/process started with PayId $newPayId");
try {
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest(
'oneclick/process',
$payload,
"POST",
array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus", "?statusDetail","?actions"),
array("merchantId", "payId", "dttm", "signature"),
false,
false,
array()
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("oneclick/process OK");
return $ret;
}
/**
* Performs a payment/button API call in API <= 1.7
*
* You need a Payment object that was already processed via paymentInit() method
* (or was injected with a payId that you received from other source).
*
* In response, you'll receive an array with [redirect], which should be
* another array with [method] and [url] items. Redirect your user to that address
* to complete the payment.
* Do not use redirectToGateway(), just redirect to `$response[redirect][url]`.
*
*
* @param Payment $payment
* @param string $brand "csob" or "era"
* @param Extension[]|Extension $extensions
*
* @return array|string
*
* @deprecated Not available since API 1.8, use buttonInit() instead
*/
function paymentButton(Payment $payment, $brand = "csob", $extensions = array()) {
$payId = $payment->getPayId();
if (!$payId) {
throw new Exception('Given Payment object does not have a PayId. Please provide a Payment object that was returned from paymentInit() method.');
}
$payload = array(
"merchantId" => $this->config->merchantId,
"payId" => $payId,
"brand" => $brand,
"dttm" => $this->getDTTM(),
);
if ($this->config->queryApiVersion('1.8')) {
throw new Exception('paymentButton() is not available in API 1.8 and newer.');
}
$endpointName = 'payment/button';
$this->writeToLog("$endpointName started with PayId $payId");
try {
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest(
$endpointName,
$payload,
"POST",
array("payId", "dttm", "resultCode", "resultMessage", "redirect"),
array("merchantId", "payId", "brand", "dttm", "signature"),
false,
false,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("$endpointName OK");
return $ret;
}
/**
* Performs a button/init API call in API >= 1.8
*
* You need a Payment object, but DO NOT process it via paymentInit() method. You don't need its PayID.
* It is used only as source of data for calling API.
*
* Items in cart in Payment object are not used, only total sum of their prices.
*
* In response, you'll receive an array with [redirect], which should be
* another array with [method], [url] and possibly [params] items.
* Redirect your user to that address to complete the payment.
* Do not use redirectToGateway(), just redirect to `$response[redirect][url]`.
*
* @see https://github.com/csob/paymentgateway/wiki/Metody-pro-platebn%C3%AD-tla%C4%8D%C3%ADtko for details
*
* @param Payment $payment
* @param string $clientIp
* @param string $brand "csob" or "era"
* @param Extension[]|Extension $extensions
*
* @return array
*/
public function buttonInit(Payment $payment, $clientIp, $brand = 'csob', $extensions = array()) {
if (!$this->config->queryApiVersion('1.8')) {
throw new Exception('buttonInit() is not available before API 1.8.');
}
if ($brand !== 'csob' and $brand !== 'era') {
throw new Exception('Invalid $brand, must be "csob" or "era".');
}
$payment->checkAndPrepare($this->config);
$payload = array(
"merchantId" => $this->config->merchantId,
"orderNo" => $payment->orderNo,
"dttm" => $this->getDTTM(),
"clientIp" => $clientIp,
"totalAmount" => $payment->getTotalAmount(),
"currency" => $payment->currency,
"returnUrl" => $payment->returnUrl,
"returnMethod" => $payment->returnMethod,
"brand" => $brand,
"merchantData" => $payment->getMerchantDataEncoded(),
"language" => $payment->language,
);
$payload["signature"] = $this->signRequest($payload);
$this->writeToLog("button/init started for payment with orderNo " . $payment->orderNo);
try {
$ret = $this->sendRequest(
"button/init",
$payload,
"POST",
array("payId", "dttm", "resultCode", "resultMessage", "?paymentStatus","?redirect.method","?redirect.url","?redirect.params"),
null,
false,
false,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("button/init OK");
return $ret;
}
/**
* Sends an arbitrary request to bank's API with any parameters.
*
* Use this method to call various masterpass/* API methods or any methods of
* API versions that may come in future and are not implemented in this library yet.
*
* $inputPayload is an associative array with data in order in which they should be signed.
* You can leave *dttm* and *merchantId* empty or null, their values will be filled automatically,
* however you can't omit them completely, since they are required in the signature.
* Signature field will be added automatically.
*
* $expectedOutputFields should be ordinary array of field names in order they appear in the response.
* Their order in the array is important to verify response signature. You can leave this empty, the
* base string will be created on order as the keys appear in the response. However, it can't be guaranteed
* it is the correct order. If you want it to be more reliable, I recommend to define it.
*
* Example - testing post connection:
*
* ```php
* $client->customRequest(
* 'echo',
* array(
* 'merchantId' => null,
* 'dttm' => null
* )
* );
* ```
*
* @param string $methodUrl API method name, without leading slash, ie. "payment/init"
* @param array $inputPayload Input payload in form of associative array. Order of items is significant.
* @param array $expectedOutputFields Expected field names of response in order in which they should be returned.
* @param Extension[]|Extension $extensions
* @param string $method HTTP method
* @param bool $logOutput Should be the complete output logged into debug log?
* @param bool $ignoreInvalidReturnSignature If set to true, then in case of invalid signature of returned data,
* no exception will be thrown and method will return received data as usual. Then, you should handle the situation by yourself.
* Do not use this option on regular basis, it is intended only as workaround for cases when returned data or its signature is more complex
* and its verification fails for some reason.
*
* @return array|string
*/
function customRequest($methodUrl, $inputPayload, $expectedOutputFields = array(), $extensions = array(), $method = "POST", $logOutput = false, $ignoreInvalidReturnSignature = false) {
if (array_key_exists('dttm', $inputPayload) and !$inputPayload['dttm']) {
$inputPayload['dttm'] = $this->getDTTM();
}
if (array_key_exists('merchantId', $inputPayload) and !$inputPayload['merchantId']) {
$inputPayload['merchantId'] = $this->config->merchantId;
}
$signature = $this->signRequest($inputPayload);
$inputPayload['signature'] = $signature;
$this->writeToLog("custom request to $methodUrl - start");
try {
$ret = $this->sendRequest(
$methodUrl,
$inputPayload,
$method,
$expectedOutputFields,
array_keys($inputPayload),
false,
$ignoreInvalidReturnSignature,
$extensions
);
} catch (Exception $e) {
$this->writeToLog("Fail, got exception: " . $e->getCode().", " . $e->getMessage());
throw $e;
}
$this->writeToLog("custom request to $methodUrl - OK");
if ($logOutput) {
$this->writeToTraceLog(print_r($ret, true));
}
return $ret;
}
/**
* Test the connection using POST method.
*
* @return array Results of calling the method.
* @throw Exception If something goes wrong. Se exception's message for more.
*/
function testPostConnection() {
$payload = array(
"merchantId" => $this->config->merchantId,
"dttm" => $this->getDTTM()
);
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest("echo", $payload, true, array("dttm", "resultCode", "resultMessage"));
$this->writeToLog("Connection test POST successful.");
return $ret;
}
/**
* Test the connection using GET method.
*
* @return array Results of calling the method.
* @throw Exception If something goes wrong. Se exception's message for more.
*/
function testGetConnection() {
$payload = array(
"merchantId" => $this->config->merchantId,
"dttm" => $this->getDTTM()
);
$payload["signature"] = $this->signRequest($payload);
$ret = $this->sendRequest("echo", $payload, false, array("dttm", "resultCode", "resultMessage"), array("merchantId", "dttm", "signature"));
$this->writeToLog("Connection test GET successful.");
return $ret;
}
/**
* Performs customer/info (below v1.8) or echo/customer (>=v1.8) API call.
*
* Use this method to check if customer with given ID has any saved cards.
* If he does, you can show some icon or change default payment method in
* e-shop or do some other action. This is just an auxiliary method and
* is not neccessary at all.
*
* @param string|array|Payment $customerId Customer ID, Payment object or array
* as returned from paymentInit
* @param bool $returnIfHasCardsOnly
*
* @return bool|int If $returnIfHasCardsOnly is set to true, method returns
* boolean indicating whether given customerID has any saved cards. If it is
* set to false, then method returns one of CUSTOMER_*** constants which can
* be used to distinguish more precisely whether customer just hasn't saved
* any cards or was not found at all.
*/
function customerInfo($customerId, $returnIfHasCardsOnly = true) {
$customerId = $this->getCustomerId($customerId);
$this->writeToLog("customer/info started for customer $customerId");
if (!$customerId) {
$this->writeToLog("no customer Id give, aborting");
return null;
}
$payload = array(
"merchantId" => $this->config->merchantId,
"customerId" => $customerId,
"dttm" => $this->getDTTM()
);
$payload["signature"] = $this->signRequest($payload);
$result = 0;
$resMessage = "";
try {
$endpointName = 'customer/info';
if ($this->getConfig()->queryApiVersion('1.8')) {
$endpointName = 'echo/customer'; // API 1.8 renamed this method
}
$ret = $this->sendRequest(
$endpointName,
$payload,
"GET",
array("customerId", "dttm", "resultCode", "resultMessage"),
array("merchantId", "customerId", "dttm", "signature")
);
} catch (Exception $e) {
// Valid call returns non-0 resultCode, which leads to exception
$resMessage = $e->getMessage();
switch ($e->getCode()) {
// V 1.8 returns 404 for nonexistent user
case 404:
$result = self::CUST_NOT_FOUND;
break;
case self::CUST_CARDS:
case self::CUST_NO_CARDS:
case self::CUST_NOT_FOUND:
$result = $e->getCode();
break;
default:
throw $e;
// this is really some error
}
}
$this->writeToLog("Result: $result, $resMessage");
if ($returnIfHasCardsOnly) {
return ($result == self::CUST_CARDS);
}
return $result;
}
/**
* Processes the data that are sent together with customer when he
* returns back from payment gateway.
*
* Call this method on your returnUrl to extract all data from request,
* validate signature, decode merchant data from base64 and return
* it all as an array. Method automatically reads data from GET or POST.
*
*
* @param array|null $input If return data is not in GET or POST, supply
* your own array with accordingly named variables.
*
* @return array|null Array with received data or null if no data is present.
*/
function receiveReturningCustomer($input = null) {
$returnDataNames = array(
"payId",
"dttm",
"resultCode",
"resultMessage",
"?paymentStatus",
"?authCode",
"?merchantData",
"?statusDetail",
// "signature"
);
if (!$input) {
if (isset($_GET["payId"])) $input = $_GET;
elseif (isset($_POST["payId"])) $input = $_POST;
}
if (!$input) {
return null;
}
$this->writeToTraceLog("Received data from returning customer: ".str_replace("\n", " ", print_r($input, true)));
$nullFields = array_fill_keys($returnDataNames, null);
$input += $nullFields;
$signatureOk = $this->verifyResponseSignature($input, $input["signature"], $returnDataNames);
if (!$signatureOk) {
$this->writeToTraceLog("Signature is invalid.");
$this->writeToLog("Returning customer: payId $input[payId], has invalid signature.");
throw new Exception("Signature is invalid.");
}
$merch = @base64_decode($input["merchantData"]);
if ($merch) {
$input["merchantData"] = $merch;
}
$mess = "Returning customer: payId ".$input["payId"];
if (isset($input["authCode"]) and $input['authCode']) $mess .= ', authCode ' . $input["authCode"];
if (isset($input["paymentStatus"]) and $input['paymentStatus']) $mess .= ', paymentStatus ' . $input["paymentStatus"];
if (isset($input["merchantData"]) and $input['merchantData']) $mess .= ', merchantData ' . $input["merchantData"];
if (isset($input["statusDetail"]) and $input['statusDetail']) $mess .= ', statusDetail ' . $input["statusDetail"];
$this->writeToLog($mess);
return $input;
}
// ------ LOGGING -------
/**
* Sets logging for bussiness-logic level messages.
*
* @param string|callback $log String filename or callback that forwards
* messages to your own logging system.
*
* @return Client
*/
function setLog($log) {
if (!$log) {
$this->logFile = null;
$this->logCallback = null;
} elseif (is_callable($log)) {
$this->logFile = null;
$this->logCallback = $log;
} else {
Files::create($log);
$this->logFile = $log;
$this->logCallback = null;
}
return $this;
}
/**
* Sets logging for exact contents of communication
*
* @param string|callback $log String filename or callback that forwards
* messages to your own logging system.
*
* @return Client
*/
function setTraceLog($log) {
if (!$log) {
$this->traceLogFile = null;
$this->traceLogCallback = null;
} elseif (is_callable($log)) {
$this->traceLogFile = null;
$this->traceLogCallback = $log;
} else {
Files::create($log);
$this->traceLogFile = $log;
$this->traceLogCallback = null;
}
return $this;
}
/**
* @ignore
*
* @param string $message
*/
function writeToLog($message) {
if ($this->logFile) {
$timestamp = date("Y-m-d H:i:s");
$timestamp = str_pad($timestamp, 20);
if (isset($_SERVER["REMOTE_ADDR"])) {
$ip = $_SERVER["REMOTE_ADDR"];
} else {
$ip = "Unknown IP";
}
$ip = str_pad($ip, 15);
$taggedMessage = "$timestamp $ip $message\n";
file_put_contents($this->logFile, $taggedMessage, FILE_APPEND);
}
if ($this->logCallback) {
call_user_func_array($this->logCallback, array($message));
}
}
/**
* @ignore
*
* @param string $message
*/
function writeToTraceLog($message) {
if ($this->traceLogFile) {
$timestamp = date("Y-m-d H:i:s");
$timestamp = str_pad($timestamp, 20);
if (isset($_SERVER["REMOTE_ADDR"])) {
$ip = $_SERVER["REMOTE_ADDR"];
} else {
$ip = "Unknown IP";
}
$ip = str_pad($ip, 15);
$taggedMessage = "$timestamp $ip $message\n";
file_put_contents($this->traceLogFile, $taggedMessage, FILE_APPEND);
}
if ($this->traceLogCallback) {
call_user_func_array($this->traceLogCallback, array($message));
}
}
// ------ COMMUNICATION ------
/**
* Get payId as string and validate it.
* @ignore
* @param Payment|string|array $payment String, Payment object or array as returned from paymentInit call
* @return string
* @throws Exception
*/
protected function getPayId($payment) {
if (!is_string($payment) and $payment instanceof Payment) {
$payment = $payment->getPayId();
if (!$payment) {
throw new Exception("Given Payment object does not have payId. Please call paymentInit() first.");
}
}
if (is_array($payment) and isset($payment["payId"])) {
$payment = $payment["payId"];
}
if (!is_string($payment) or strlen($payment) != 15) {
throw new Exception("Given Payment ID is not valid - it should be a string with length 15 characters.");
}
return $payment;
}
/**
* Get customerId as string and validate it.
* @ignore
* @param Payment|string|array $payment String, Payment object or array as returned from paymentInit call
* @return string
* @throws Exception
*/
protected function getCustomerId($payment) {
if (!is_string($payment) and $payment instanceof Payment) {
$payment = $payment->customerId;
}
if (is_array($payment) and isset($payment["customerId"])) {
$payment = $payment["customerId"];
}
if (!is_string($payment)) {
throw new Exception("Given Customer ID is not valid.");
}
return $payment;
}
/**
* Get current timestamp in payment gate's format.
* @return string
* @ignore
*/
public function getDTTM() {
return date(self::DATE_FORMAT);
}
/**
* Signs array payload
* @param array $arrayToSign
* @return string Base64 encoded signature
* @ignore
*/
protected function signRequest($arrayToSign) {
$stringToSign = Crypto::createSignatureBaseFromArray($arrayToSign);
$keyFile = $this->config->privateKeyFile;
$signature = Crypto::signString(
$stringToSign,
$keyFile,
$this->config->privateKeyPassword,
$this->config->getHashMethod()
);
$this->writeToTraceLog("Signing string \"$stringToSign\" using key $keyFile, result: ".$signature);
return $signature;
}
/**
* Send prepared request.
*
* @param string $apiMethod
* @param array $payload
* @param bool|string $usePostMethod True = post, false = get, string = exact method
* @param array $responseFieldsOrder
* @param array $requestFieldsOrder
* @param bool $returnUrlOnly
* @param bool $allowInvalidReturnSignature Set to true if you want to ignore the fact
* that the signature of returned data was incorrect, so that you can receive the returned data anyway
* and handle the situation by yourself. If false, an exception will be thrown instead of returning the received data.
* @param Extension[]|Extension $extensions
*
* @return array|string
* @ignore
*/
protected function sendRequest(
$apiMethod,
$payload,
$usePostMethod = true,
$responseFieldsOrder = null,
$requestFieldsOrder = null,
$returnUrlOnly = false,
$allowInvalidReturnSignature = false,
$extensions = array()
) {
$url = $this->getApiMethodUrl($apiMethod);
$method = $usePostMethod;
$this->writeToTraceLog("Will send request to method $apiMethod");
if (!$usePostMethod or $usePostMethod === "GET") {
$method = "GET";
if (!$requestFieldsOrder) {
$requestFieldsOrder = $responseFieldsOrder;
}
$parametersToUrl = $requestFieldsOrder ? $requestFieldsOrder : array_keys($payload);
foreach($parametersToUrl as $param) {
if (isset($payload[$param])) {
$url .= "/" . urlencode($payload[$param]);
}
}
}
if ($method === true) {
$method = "POST";
}
if ($extensions) {
$extensions = Arrays::arrayize($extensions);
}
if ($extensions) {
$payload["extensions"] = array();
/** @var Extension $extension */
foreach ($extensions as $extension) {
if (!($extension instanceof Extension)) {
throw new Exception('Given argument is not Extension object.');
}
$extension->setHashMethod($this->config->getHashMethod());
$addedData = $extension->createRequestArray($this);
if ($addedData) {
$payload["extensions"][] = $addedData;
}
}
}
if ($returnUrlOnly) {
$this->writeToTraceLog("Returned final URL: " . $url);
return $url;
}
$ch = \curl_init($url);
$this->writeToTraceLog("URL to send request to: " . $url);
if ($method === "POST" or $method === "PUT") {
$encodedPayload = json_encode($payload);
if (json_last_error()) {
$msg = 'Request data could not be encoded to JSON: ' . json_last_error();
if (function_exists('json_last_error_msg')) {
$msg .= ' - ' . json_last_error_msg();
}
$this->writeToTraceLog($msg);
throw new Exception($msg, 1);
}
$this->writeToTraceLog("JSON payload: ".$encodedPayload);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedPayload);
}
if (!$this->config->sslCertificatePath) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
} else {
if (is_dir($this->config->sslCertificatePath)) {
\curl_setopt($ch, CURLOPT_CAPATH, $this->config->sslCertificatePath);
} else {
\curl_setopt($ch, CURLOPT_CAINFO, $this->config->sslCertificatePath);
}
}
if ($this->config->sslVersion) {
\curl_setopt($ch, CURLOPT_SSLVERSION, $this->config->sslVersion);
}
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Accept: application/json;charset=UTF-8'
));
$result = \curl_exec($ch);
if (\curl_errno($ch)) {
$this->writeToTraceLog("CURL failed: " . \curl_errno($ch) . " " . \curl_error($ch));
throw new Exception("Failed sending data to API: ".\curl_errno($ch)." ".\curl_error($ch));
}
$this->writeToTraceLog("API response: $result");
$httpCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode != 200) {
$this->writeToTraceLog("Failed: returned HTTP code $httpCode");
$errorMessage = '';
$decoded = @json_decode($result, true);
if ($decoded) {
if (isset($decoded['resultMessage']) and isset($decoded['resultCode'])) {
$errorMessage = $decoded['resultMessage'] . ' (resultCode ' . $decoded['resultCode'] . ')';
}
}
throw new Exception(
"API returned HTTP code $httpCode, which is not code 200."
. ($errorMessage ? (' ' . $errorMessage) : ''),
$httpCode
);
}
\curl_close($ch);
$decoded = @json_decode($result, true);
if ($decoded === null) {
$this->writeToTraceLog("Failed: returned value is not parsable JSON");
throw new Exception("API did not return a parseable JSON string: \"".$result."\"");
}
if (!isset($decoded["resultCode"])) {
$this->writeToTraceLog("Failed: API did not return response with resultCode");
throw new Exception("API did not return a response containing resultCode.");
}
if (!isset($decoded["signature"]) or !$decoded["signature"]) {
$this->writeToTraceLog("Failed: missing response signature");
throw new Exception("Result does not contain signature.");
}
$signature = $decoded["signature"];
try {
$verificationResult = $this->verifyResponseSignature($decoded, $signature, $responseFieldsOrder);
} catch (Exception $e) {
$this->writeToTraceLog("Failed: error occured when verifying signature.");
throw $e;
}
if (!$verificationResult) {
if (!$allowInvalidReturnSignature) {
$this->writeToTraceLog("Failed: signature is incorrect.");
throw new Exception("Result signature is incorrect. Please make sure that bank's public key in file specified in config is correct and up-to-date.");
} else {
$this->writeToTraceLog("Signature is incorrect, but method was called with \$allowInvalidReturnSignature = true, so we'll ignore it.");
}
}
if ($decoded["resultCode"] != "0") {
$this->writeToTraceLog("Failed: resultCode ".$decoded["resultCode"].", message ".$decoded["resultMessage"]);
throw new Exception("API returned an error: resultCode \"" . $decoded["resultCode"] . "\", resultMessage: ".$decoded["resultMessage"], $decoded["resultCode"]);
}
if ($extensions) {
$extensionsById = array();
foreach ($extensions as $extension) {
$extensionsById[$extension->getExtensionId()] = $extension;
}
$extensionsDataDecoded = isset($decoded["extensions"]) ? $decoded["extensions"] : array();
foreach ($extensionsDataDecoded as $extensionData) {
$extensionId = $extensionData['extension'];
if (isset($extensionsById[$extensionId])) {
/** @var Extension $extensionObject */
$extensionObject = $extensionsById[$extensionId];
$extensionObject->setResponseData($extensionData);
$extensionObject->setHashMethod($this->config->getHashMethod());
$signatureResult = $extensionObject->verifySignature($extensionData, $this);
if (!$signatureResult) {
$this->writeToTraceLog("Signature of extension $extensionId is incorrect.");
if ($extension->getStrictSignatureVerification()) {
throw new Exception("Result signature of extension $extensionId is incorrect. Please make sure that bank's public key in file specified in config is correct and up-to-date.");
} else {
$extension->setSignatureCorrect(false);
}
} else {
$extension->setSignatureCorrect(true);
}
}
}
}
$this->writeToTraceLog("OK");
return $decoded;
}
/**
* Gets the URL of API method
* @param string $apiMethod
* @return string
*/
function getApiMethodUrl($apiMethod) {
return $this->config->url . "/" . $apiMethod;
}
/**
* @param array $response
* @param string $signature in Base64
* @param array $responseFieldsOrder
* @return bool
* @ignore
*/
function verifyResponseSignature($response, $signature, $responseFieldsOrder = array()) {
$responseWithoutSignature = $response;
if (isset($responseWithoutSignature["signature"])) {
unset($responseWithoutSignature["signature"]);
}
if ($responseFieldsOrder) {
$string = Crypto::createSignatureBaseWithOrder($responseWithoutSignature, $responseFieldsOrder, false);
} else {
$string = Crypto::createSignatureBaseFromArray($responseWithoutSignature, false);
}
$this->writeToTraceLog("String for verifying signature: \"" . $string . "\", using key " . $this->config->bankPublicKeyFile);
return Crypto::verifySignature($string, $signature, $this->config->bankPublicKeyFile, $this->config->getHashMethod());
}
}
}
// src/Config.php
namespace OndraKoupil\Csob {
/**
* Configuration for integrating your app to bank gateway.
*/
class Config {
/**
* Bank API path. By default, this is the testing (playground) API.
* Change that when you are ready to go to live environment.
*
* @var string
*
* @see GatewayUrl
*/
public $url = GatewayUrl::TEST_LATEST;
/**
* API Version. Version 1.8 brings some BC breaks, so the library needs to know which version you want to call.
* Use this property to explicitly specify API version. Leave null to autodetect from endpoint URL.
*
* @var string
*/
public $apiVersion = null;
/**
* @var int|null One of OPENSSL_HASH_* constants or null for auto detection
*/
public $hashMethod = null;
/**
* Path to file where bank's public key is saved.
*
* You can obtain the key from bank's app
* https://iposman.iplatebnibrana.csob.cz/posmerchant
* or from their package on GitHub)
*
* @var string
*/
public $bankPublicKeyFile = "";
/**
* Your Merchant ID.
*
* You obtain that from the bank or from https://iplatebnibrana.csob.cz/keygen/
*
* @var string
*/
public $merchantId = "";
/**
* Path to file where your private key is saved.
*
* You obtain that key from https://iplatebnibrana.csob.cz/keygen/ - it is
* the .key file you download from the keygen.
*
* Careful - that file MUST NOT BE publicly accessible on webserver!
* @var string
*/
public $privateKeyFile = "";
/**
* Password for your private key.
*
* You need to specify this only if your private key was not generated
* using bank's keygen https://iplatebnibrana.csob.cz/keygen/
* @var string
*/
public $privateKeyPassword = null;
/**
* A URL of your e-shop to return your customers after the have paid.
*
* @var string
*/
public $returnUrl;
/**
* A method to return customers on $returnUrl.
*
* Right now (api v1) it is not much significant, since (according to their doc)
* you must support both GET and POST methods.
*
* @var string
*/
public $returnMethod = "POST";
/**
* Name of your e-shop or app - it will be used on some points of
* creating payments.
*
* @var string
*/
public $shopName;
/**
* Should payments be created with closePayment = true by default?
* See Wiki on ČSOB's github for more information.
*
* @var boolean
*/
public $closePayment = true;
/**
* Path to a CA certificate chain or a directory containing certificates to verify
* bank's certificate when initiating a HTTPS connection.
*
* Leave null to disable certificate validation.
*
* @see CURLOPT_SSL_VERIFYPEER, CURLOPT_CAINFO, CURLOPT_CAPATH
*
* @var string
*/
public $sslCertificatePath = null;
/**
* Force the client to use a specific SSL version.
*
* Leave null to use automatic selection (default).
*
* @var number Use one of CURL_SSLVERSION_* or CURL_SSLVERSION_MAX_* constants
*
* @see https://www.php.net/manual/en/function.curl-setopt.php
*/
public $sslVersion = null;
/**
* Create config with all mandatory values.
*
* See equally named properties of this class for more info.
*
* To specify $bankApiUrl, you can use constants of GatewayUrl class.
*
* @param string $merchantId
* @param string $privateKeyFile
* @param string $bankPublicKeyFile
* @param string $shopName
* @param string|null $returnUrl
* @param string|null $bankApiUrl
* @param string|null $privateKeyPassword
* @param string|null $sslCertificatePath
* @param string|null $apiVersion Leave null to autodetect from $bankApiUrl
* @param int|null $hashMethod One of OPENSSL_HASH_* constants, leave null for auto detection from given $bankApiUrl. Read via getHashMethod();
*/
function __construct(
$merchantId,
$privateKeyFile,
$bankPublicKeyFile,
$shopName,
$returnUrl = null,
$bankApiUrl = null,
$privateKeyPassword = null,
$sslCertificatePath = null,
$apiVersion = null,
$hashMethod = null
) {
if ($bankApiUrl) {
$this->url = $bankApiUrl;
}
if ($privateKeyPassword) {
$this->privateKeyPassword = $privateKeyPassword;
}
$this->merchantId = $merchantId;
$this->privateKeyFile = $privateKeyFile;
$this->bankPublicKeyFile = $bankPublicKeyFile;
$this->returnUrl = $returnUrl;
$this->shopName = $shopName;
$this->sslCertificatePath = $sslCertificatePath;
$this->hashMethod = $hashMethod;
$this->apiVersion = $apiVersion;
}
function getVersion() {
if (!$this->apiVersion) {
if (!$this->url) {
throw new Exception('You must specify bank API URL first.');
}
$match = preg_match('~\/api\/v([0-9.]+)$~', $this->url, $matches);
if ($match) {
$this->apiVersion = $matches[1];
} else {
throw new Exception('Can not deduce API version from URL: ' . $this->url);
}
}
return $this->apiVersion;
}
/**
* Return the set hashing method or deduce it from bank API's version.
*
* @return int
*/
function getHashMethod() {
if ($this->hashMethod) {
return $this->hashMethod;
}
if ($this->queryApiVersion('1.8')) {
return OPENSSL_ALGO_SHA256;
} else {
return OPENSSL_ALGO_SHA1;
}
}
/**
* Returns true if currently set API version is at least $version or greater.
*
* @param string $version
*
* @return boolean
*/
function queryApiVersion($version) {
return !!version_compare($this->getVersion(), $version, '>=');
}
}
}
// src/Payment.php
namespace OndraKoupil\Csob {
use OndraKoupil\Csob\Metadata\Customer;
use OndraKoupil\Csob\Metadata\Order;
use \OndraKoupil\Tools\Strings;
use \OndraKoupil\Tools\Arrays;
use DateTime;
/**
* A payment request.
*
* To init new payment, you need to create an instance
* of this class and fill its properties with real information
* from the order.
*/
class Payment {
/**
* Běžná platba
*/
const OPERATION_PAYMENT = "payment";
/**
* Opakovaná platba
*
* @deprecated Deprecated since eAPI 1.7 - use one click payments
*/
const OPERATION_RECURRENT = "recurrentPayment";
/**
* Platba na klik
*/
const OPERATION_ONE_CLICK = "oneclickPayment";
/**
* Custom platba
*/
const OPERATION_CUSTOM_PAYMENT = "customPayment";
/**
* @ignore
* @var string
*/
protected $merchantId;
/**
* Number of your order, a string of 1 to 10 numbers
* (this is basically the Variable symbol).
*
* This is the only one mandatory value you need to supply.
*
* @var string
*/
public $orderNo;
/**
* @ignore
* @var number
*/
protected $totalAmount = 0;
/**
* For oneclick payments use only
* @internal
* @var string
*/
public $origPayId;
/**
* Currency of the transaction. Default value is "CZK".
* @var string
*/
public $currency;
/**
* Should the payment be processed right on?
* See Wiki on ČSOB's github for more information.
*
* If not set, value from Config us used (true by default).
*
* @var bool|null
*/
public $closePayment = null;
/**
* Return URL to send your customers back to.
*
* You need to specify this only if you don't want to use the default
* URL from your Config. Leave empty to use the default one.
*
* @var string
*/
public $returnUrl;
/**
* Return method. Leave empty to use the default one.
* @var string
* @see returnUrl
*/
public $returnMethod;
/**
* @ignore
* @var array
*/
protected $cart = array();
/**
* Description of the order that will be shown to customer during payment
* process.
*
* Leave empty to use your e-shop's name as given in Config.
*
* @var string
*/
public $description;
/**
* @ignore
* @var string
*/
protected $merchantData;
/**
* Your customer's ID (e-mail, number, whatever...)
*
* Leave empty if you don't want to use some features relying on knowing
* customer ID.
*
* @var string
*/
public $customerId;
/**
* Language of the gateway. Default is "CZ".
*
* See wiki on ČSOB's Github for other values, they are not the same
* as standard ISO language codes.
*
* @see https://github.com/csob/paymentgateway/wiki/Basic-Methods
*
* @var string
*/
public $language;
/**
* @ignore
* @var string
*/
protected $dttm;
/**
* payOperation value. Leave empty to use the default
* (and the only one valid) value.
*
* Using API v1, you can ignore this.
*
* @var string
*/
public $payOperation;
/**
* payMethod value. Leave empty to use the default
* (and the only one valid) value.
*
* Using API v1, you can ignore this.
*
* @var string
*/
public $payMethod;
/**
* The PayID value that you will need fo call other methods.
* It is given to your payment by bank.
*
* @var string
* @see getPayId
*/
protected $foreignId;
/**
* Lifetime of the transaction in seconds. Number from 300 to 1800.
*
* @var int
*/
public $ttlSec;
/**
* Version of logo.
*
* @var int
*/
public $logoVersion;
/**
* Color version
*
* @var int
*/
public $colorSchemeVersion;
/**
* @var Customer
*/
protected $customer;
/**
* @var Order
*/
protected $order;
/**
* @var DateTime
*/
protected $customExpiry;
/**
* @var array
* @ignore
*/
private $fieldsInOrder = array(
"merchantId",
"*origPayId", // placeholder
"orderNo",
"dttm",
"payOperation",
"payMethod",
"totalAmount",
"currency",
"closePayment",
"returnUrl",
"returnMethod",
"cart",
"*customer", // placeholder
"*order", // placeholder
"description",
"merchantData",
"customerId",
"language",
"ttlSec",
);
private $auxFieldsInOrder = array(
"logoVersion",
"colorSchemeVersion",
"customExpiry"
);
/**
* @param string $orderNo
* @param mixed $merchantData
* @param string $customerId
* @param bool|null $oneClickPayment
*/
function __construct($orderNo = '', $merchantData = null, $customerId = null, $oneClickPayment = null) {
$this->orderNo = $orderNo;
if ($merchantData) {
$this->setMerchantData($merchantData);
}
if ($customerId) {
$this->customerId = $customerId;
}
if ($oneClickPayment !== null) {
$this->setOneClickPayment($oneClickPayment);
}
}
/**
* Add one cart item.
*
* You are required to add one or two cart items (at least on API v1).
*
* Remember that $totalAmount must be given in **hundredth of currency units**
* (cents for USD or EUR, "halíře" for CZK)
*
* @param string $name Name that customer will see
* (will be automatically trimmed to 20 characters)
* @param number $quantity
* @param number $totalAmount Total price (total sum for all $quantity),
* in **hundredths** of currency unit
* @param string $description Aux description (trimmed to 40 chars max)
*
* @return Payment Fluent interface
*
* @throws Exception When more than 2nd cart item is to be added or other argument is invalid
*/
function addCartItem($name, $quantity, $totalAmount, $description = "") {
if (count($this->cart) >= 2) {
throw new Exception("This version of banks's API supports only up to 2 cart items in single payment, you can't add any more items.");
}
if (!is_numeric($quantity) or $quantity < 1) {
throw new Exception("Invalid quantity: $quantity. It must be numeric and >= 1");
}
$name = trim(Strings::shorten($name, 20, "", true, true));
$description = trim(Strings::shorten($description, 40, "", true, true));
$this->cart[] = array(
"name" => $name,
"quantity" => $quantity,
"amount" => intval(round($totalAmount)),
"description" => $description
);
return $this;
}
/**
* @return Customer
*/
public function getCustomer() {
return $this->customer;
}
/**
* @param Customer $customer
*
* @return Payment
*/
public function setCustomer($customer) {
$this->customer = $customer;
return $this;
}
/**
* @return Order
*/
public function getOrder() {
return $this->order;
}
/**
* @param Order $order
*
* @return Payment
*/
public function setOrder($order) {
$this->order = $order;
return $this;
}
/**
* Set some arbitrary data you will receive back when customer returns
*
* @param string $data
* @param bool $alreadyEncoded True if given $data is already encoded to Base64
*
* @return Payment Fluent interface
*
* @throws Exception When the data is too long and can't be encoded.
*/
public function setMerchantData($data, $alreadyEncoded = false) {
if (!$alreadyEncoded) {
$data = base64_encode($data);
}
if (strlen($data) > 255) {
throw new Exception("Merchant data can not be longer than 255 characters after base64 encoding.");
}
$this->merchantData = $data;
return $this;
}
/**
* Get back merchantData, decoded to original value.
*
* @return string
*/
public function getMerchantData() {
if ($this->merchantData) {
return base64_decode($this->merchantData);
}
return "";
}
/**
* Get back MerchantData encoded as base64.
*
* @return string
*/
public function getMerchantDataEncoded() {
return $this->merchantData ?: '';
}
/**
* After the payment has been saved using payment/init, you can
* get PayID from here.
*
* @return string
*/
public function getPayId() {
return $this->foreignId;
}
/**
* Returns sum of all cart items in **hundreths** of base currency unit.
*
* @return number
*/
public function getTotalAmount() {
$sumOfItems = array_sum(Arrays::transform($this->cart, true, "amount"));
$this->totalAmount = $sumOfItems;
return $this->totalAmount;
}
/**
* Cart items as array.
* @return array
*/
function getCart() {
return $this->cart;
}
/**
* Do not call this on your own. Really.
*
* @param string $id
*/
public function setPayId($id) {
$this->foreignId = $id;
}
/**
* Mark this payment as a template for recurrent payments.
*
* Basically, this is a lazy method for setting $payOperation to OPERATION_RECURRENT.
*
* @param bool $recurrent
* @deprecated Deprecated and replaced by setOneClickPayment
*
* @return \OndraKoupil\Csob\Payment
*/
function setRecurrentPayment($recurrent = true) {
$this->payOperation = $recurrent ? self::OPERATION_RECURRENT : self::OPERATION_PAYMENT;
trigger_error('setRecurrentPayment() is deprecated, use setOneClickPayment() instead.', E_USER_DEPRECATED);
return $this;
}
/**
* Mark this payment as one-click payment template
*
* Basically, this is a lazy method for setting $payOperation to OPERATION_ONE_CLICK
*
* @param bool $oneClick
*
* @return $this
*/
function setOneClickPayment($oneClick = true) {
$this->payOperation = $oneClick ? self::OPERATION_ONE_CLICK : self::OPERATION_PAYMENT;
return $this;
}
function setCustomExpiry(DateTime $customExpiry) {
$this->customExpiry = $customExpiry->format('YmdHis');
}
/**
* Validate and initialise properties. This method is called
* automatically in proper time, you never have to call it on your own.
*
* @param Config $config
* @throws Exception
* @return Payment Fluent interface
*
* @ignore
*/
function checkAndPrepare(Config $config) {
$this->merchantId = $config->merchantId;
$this->dttm = date(Client::DATE_FORMAT);
if (!$this->payOperation) {
$this->payOperation = self::OPERATION_PAYMENT;
}
if (!$this->payMethod) {
$this->payMethod = "card";
}
if (!$this->currency) {
$this->currency = "CZK";
}
if (!$this->language) {
$this->language = "CZ";
}
if (!$this->ttlSec or !is_numeric($this->ttlSec)) {
$this->ttlSec = 1800;
}
if ($this->closePayment === null) {
$this->closePayment = $config->closePayment ? true : false;
}
if (!$this->returnUrl) {
$this->returnUrl = $config->returnUrl;
}
if (!$this->returnUrl) {
throw new Exception("A ReturnUrl must be set - either by setting \$returnUrl property, or by specifying it in Config.");
}
if (!$this->returnMethod) {
$this->returnMethod = $config->returnMethod;
}
if (!$this->description) {
$this->description = $config->shopName.", ".$this->orderNo;
}
$this->description = Strings::shorten($this->description, 240, "...");
$this->customerId = Strings::shorten($this->customerId, 50, "", true, true);
if (!$this->cart) {
throw new Exception("Cart is empty. Please add one or two items into cart using addCartItem() method.");
}
if (!$this->orderNo or !preg_match('~^[0-9]{1,10}$~', $this->orderNo)) {
throw new Exception("Invalid orderNo - it must be a non-empty numeric value, 10 characters max.");
}
$sumOfItems = array_sum(Arrays::transform($this->cart, true, "amount"));
$this->totalAmount = $sumOfItems;
return $this;
}
/**
* Add signature and export to array. This method is called automatically
* and you don't need to call is on your own.
*
* @param Client $client
* @return array
*
* @ignore
*/
function signAndExport(Client $client) {
$arr = array();
$config = $client->getConfig();
$fieldNames = $this->fieldsInOrder;
if ($client->getConfig()->queryApiVersion('1.8')) {
// Version 1.8 omitted $description parameter
$fieldNames = Arrays::deleteValue($fieldNames, 'description');
}
foreach($fieldNames as $f) {
if ($f[0] === '*') {
continue; // skip those beginning with asterisk - they are just placeholders
}
$val = $this->$f;
if ($val === null) {
$val = "";
}
$arr[$f] = $val;
}
foreach ($this->auxFieldsInOrder as $f) {
$val = $this->$f;
if ($val !== null) {
$arr[$f] = $val;
}
}
// Sice API 1.9, we add a complex customer and order objects to the payment data.
if ($client->getConfig()->queryApiVersion('1.9')) {
if ($this->customer) {
$arr['customer'] = $this->customer->export();
}
if ($this->order) {
$arr['order'] = $this->order->export();
}
if ($this->origPayId) {
$arr['origPayId'] = $this->origPayId;
}
}
$stringToSign = $this->getSignatureString($client);
$client->writeToTraceLog('Signing payment request, base for the signature:' . "\n" . $stringToSign);
$signed = Crypto::signString($stringToSign, $config->privateKeyFile, $config->privateKeyPassword, $client->getConfig()->getHashMethod());
$arr["signature"] = $signed;
return $arr;
}
/**
* Convert to string that serves as base for signing.
*
* @param Client $client
*
* @return string
* @ignore
*/
function getSignatureString(Client $client) {
$parts = array();
$fieldNames = $this->fieldsInOrder;
if ($client->getConfig()->queryApiVersion('1.8')) {
// Version 1.8 omitted $description parameter
$fieldNames = Arrays::deleteValue($fieldNames, 'description');
}
$partsToSign = array();
foreach($fieldNames as $f) {
if ($f[0] === '*') {
// These needs special treatment
if ($f === '*customer') {
if ($this->customer and $client->getConfig()->queryApiVersion('1.9')) {
$partsToSign[] = $this->customer->export();
}
}
if ($f === '*order') {
if ($this->order and $client->getConfig()->queryApiVersion('1.9')) {
$partsToSign[] = $this->order->export();
}
}
if ($f === '*origPayId' and $this->origPayId and $client->getConfig()->queryApiVersion('1.9')) {
$partsToSign[] = $this->origPayId;
}
continue;
}
$partsToSign[] = $this->$f;
}
foreach ($this->auxFieldsInOrder as $f) {
$val = $this->$f;
if ($val !== null) {
$partsToSign[] = $val;
}
}
return Tools::linearizeForSigning($partsToSign);
}
}
}
// src/Crypto.php
namespace OndraKoupil\Csob {
/**
* Helper class for signing and signature verification
*
* @see https://github.com/csob/paymentgateway/blob/master/eshop-integration/eAPI/v1/php/example/crypto.php
*/
class Crypto {
const DEFAULT_HASH_METHOD = OPENSSL_ALGO_SHA1;
const HASH_SHA1 = OPENSSL_ALGO_SHA1;
const HASH_SHA256 = OPENSSL_ALGO_SHA256;
/**
* Signs a string
*
* @param string $string
* @param string $privateKeyFile Path to file with your private key (the .key file from https://iplatebnibrana.csob.cz/keygen/ )
* @param string $privateKeyPassword Password to the key, if it was generated with one. Leave empty if you created the key at https://iplatebnibrana.csob.cz/keygen/
* @param int $hashMethod One of OPENSSL_HASH_* constants
* @return string Signature encoded with Base64
* @throws CryptoException When signing fails or key file path is not valid
*/
static function signString($string, $privateKeyFile, $privateKeyPassword = "", $hashMethod = self::DEFAULT_HASH_METHOD) {
if (!function_exists("openssl_get_privatekey")) {
throw new CryptoException("OpenSSL extension in PHP is required. Please install or enable it.");
}
if (!file_exists($privateKeyFile) or !is_readable($privateKeyFile)) {
throw new CryptoException("Private key file \"$privateKeyFile\" not found or not readable.");
}
$keyAsString = file_get_contents($privateKeyFile);
$privateKeyId = openssl_get_privatekey($keyAsString, $privateKeyPassword);
if (!$privateKeyId) {
throw new CryptoException("Private key could not be loaded from file \"$privateKeyFile\". Please make sure that the file contains valid private key in PEM format.");
}
$ok = openssl_sign($string, $signature, $privateKeyId, $hashMethod);
if (!$ok) {
throw new CryptoException("Signing failed.");
}
$signature = base64_encode ($signature);
if (version_compare(PHP_VERSION, '8.0', '<')) {
// https://github.com/ondrakoupil/csob/issues/33
openssl_free_key ($privateKeyId);
}
return $signature;
}
/**
* Verifies signature of a string
*
* @param string $textToVerify The text that was signed
* @param string $signatureInBase64 The signature encoded with Base64
* @param string $publicKeyFile Path to file where bank's public key is saved
* (you can obtain it from bank's app https://iposman.iplatebnibrana.csob.cz/posmerchant
* or from their package on GitHub)
* @param int $hashMethod One of OPENSSL_HASH_* constants
*
* @return bool True if signature is correct
*/
static function verifySignature($textToVerify, $signatureInBase64, $publicKeyFile, $hashMethod = self::DEFAULT_HASH_METHOD) {
if (!function_exists("openssl_get_privatekey")) {
throw new CryptoException("OpenSSL extension in PHP is required. Please install or enable it.");
}
if (!file_exists($publicKeyFile) or !is_readable($publicKeyFile)) {
throw new CryptoException("Public key file \"$publicKeyFile\" not found or not readable.");
}
$keyAsString = file_get_contents($publicKeyFile);
$publicKeyId = openssl_get_publickey($keyAsString);
$signature = base64_decode($signatureInBase64);
$res = openssl_verify($textToVerify, $signature, $publicKeyId, $hashMethod);
if (version_compare(PHP_VERSION, '8.0', '<')) {
// https://github.com/ondrakoupil/csob/issues/33
openssl_free_key($publicKeyId);
}
if ($res == -1) {
throw new CryptoException("Verification of signature failed: ".openssl_error_string());
}
return $res ? true : false;
}
/**
* Vytvoří z array (i víceúrovňového) string pro výpočet podpisu.
*
* @param array $array
* @param bool $returnAsArray
*
* @return string|array
*/
static function createSignatureBaseFromArray($array, $returnAsArray = false) {
$linearizedArray = self::createSignatureBaseRecursion($array);
if ($returnAsArray) {
return $linearizedArray;
}
return implode('|', $linearizedArray);
}
protected static function createSignatureBaseRecursion($array, $depthCheck = 0) {
if ($depthCheck > 10) {
return array();
}
$ret = array();
foreach ($array as $val) {
if (is_array($val)) {
$ret = array_merge(
$ret,
self::createSignatureBaseRecursion($val, $depthCheck + 1)
);
} elseif (is_bool($val)) {
$ret[] = $val ? 'true' : 'false';
} else {
$ret[] = $val;
}
}
return $ret;
}
/**
* Generická implementace linearizace pole s dopředu zadaným požadovaným pořadím.
*
* V $order by mělo být požadované pořadí položek formou stringových "keypath".
* Keypath je název klíče v poli $data, pokud je víceúrovňové, klíče jsou spojeny tečkou.
*
* Pokud keypath začíná znakem otazník, považuje se za nepovinnou a není-li taková
* položka nalezena, z výsledku se vynechá. V opačném případě se vloží prázdný řetězec.
*
* Pokud keypath odkazuje na další array, to se vloží postupně položka po položce.
*
* Příklad:
*
* ```php
* $data = array(
* 'foo' => 'bar',
* 'arr' => array(
* 'a' => 'A',
* 'b' => 'B'
* )
* );
*
* $order = array(
* 'foo',
* 'arr.a',
* 'somethingRequired',
* '?somethingOptional',
* 'foo',
* 'arr.x',
* 'foo',
* 'arr'
* );
*
* $result = Crypto::createSignatureBaseWithOrder($data, $order, false);
*
* $result == array('bar', 'A', '', 'bar', '', 'bar', 'A', 'B');
* ```
*
* @param array $data Pole s daty
* @param array $order Požadované pořadí položek.
* @param bool $returnAsArray
*
* @return array
*/
static function createSignatureBaseWithOrder($data, $order, $returnAsArray = false) {
$result = array();
foreach ($order as $key) {
$optional = false;
if ($key[0] == '?') {
$optional = true;
$key = substr($key, 1);
}
$keyPath = explode('.', $key);
$pos = $data;
$found = true;
foreach ($keyPath as $keyPathComponent) {
// NULLs are not included in signature as well
if (array_key_exists($keyPathComponent, $pos) and $pos[$keyPathComponent] !== null) {
$pos = $pos[$keyPathComponent];
} else {
$found = false;
break;
}
}
if ($found) {
if (is_array($pos)) {
$result = array_merge($result, self::createSignatureBaseFromArray($pos, true));
} else {
if (is_bool($pos)) {
$result[] = $pos ? 'true' : 'false';
} else {
$result[] = $pos;
}
}
} else {
if (!$optional) {
$result[] = '';
}
}
}
if ($returnAsArray) {
return $result;
}
return implode('|', $result);
}
}
}
// src/Exception.php
namespace OndraKoupil\Csob {
class Exception extends \RuntimeException {}
}
// src/CryptoException.php
namespace OndraKoupil\Csob {
class CryptoException extends Exception {}
}
// src/GatewayUrl.php
namespace OndraKoupil\Csob {
/**
* Class containing for CSOB gateway URLs
*/
class GatewayUrl {
const TEST_LATEST = self::TEST_1_9;
const PRODUCTION_LATEST = self::PRODUCTION_1_9;
const TEST_1_0 = "https://iapi.iplatebnibrana.csob.cz/api/v1";
const PRODUCTION_1_0 = "https://api.platebnibrana.csob.cz/api/v1";
const TEST_1_5 = "https://iapi.iplatebnibrana.csob.cz/api/v1.5";
const PRODUCTION_1_5 = "https://api.platebnibrana.csob.cz/api/v1.5";
const TEST_1_6 = "https://iapi.iplatebnibrana.csob.cz/api/v1.6";
const PRODUCTION_1_6 = "https://api.platebnibrana.csob.cz/api/v1.6";
const TEST_1_7 = "https://iapi.iplatebnibrana.csob.cz/api/v1.7";
const PRODUCTION_1_7 = "https://api.platebnibrana.csob.cz/api/v1.7";
const TEST_1_8 = "https://iapi.iplatebnibrana.csob.cz/api/v1.8";
const PRODUCTION_1_8 = "https://api.platebnibrana.csob.cz/api/v1.8";
const TEST_1_9 = "https://iapi.iplatebnibrana.csob.cz/api/v1.9";
const PRODUCTION_1_9 = "https://api.platebnibrana.csob.cz/api/v1.9";
}
}
// src/Extension.php
namespace OndraKoupil\Csob {
/**
* Represents additional data to be sent with a request to the API
* and also defines how to verify the response.
*
* Each method call can have several extensions.
*/
class Extension {
/**
* @var array
*/
protected $inputData;
/**
* @var array
*/
protected $responseData;
/**
* @var array
*/
protected $expectedResponseKeysOrder;
/**
* @var string
*/
protected $extensionId;
/**
* @var bool
*/
protected $strictSignatureVerification = true;
/**
* @var bool
*/
protected $signatureCorrect = false;
/**
* @var int
*/
protected $hashMethod = Crypto::DEFAULT_HASH_METHOD;
/**
* @param string $extensionId
*/
function __construct($extensionId) {
if (!$extensionId) {
throw new Exception('No Extension ID given!');
}
$this->extensionId = $extensionId;
}
/**
* @return array
*/
public function getInputData() {
return $this->inputData;
}
/**
* Sets the data that are sent with your request to API.
*
* Set this to falsey value to disable sending extension data with your request
* (this extension will affect only response).
*
* Order of keys is significant because of generating base for signature.
* Check CSOB wiki for correct order.
*
* If you need to insert "dttm" parameter, you can just set it to null, real valu ewill be added automatically.
* The same is for "extension" parameter, which can be filled with extensionId.
*
* If signature base is being generated incorrectly, reimplement getRequestSignatureBase()
* with a better one.
*
* @param array $inputData
*
* @return Extension
*/
public function setInputData($inputData) {
$this->inputData = $inputData;
return $this;
}
/**
* @return array
*/
public function getExpectedResponseKeysOrder() {
return $this->expectedResponseKeysOrder;
}
/**
* Use this method to hint in which order should the base string
* for verifying signature of response be generated.
* See Crypto::createSignatureBaseWithOrder() for options for specifying key paths.
*
* If response signatures are verifyed incorrectly because of wrong order of parts
* in base string, you can reimplement getResponseSignatureBase() method with a better one.
*
* Set to falsey value to disable parsing of the extension object in response,
* the extension will then affect only sending the request.
*
* @param array $expectedResponseKeysOrder
*
* @return Extension
*/
public function setExpectedResponseKeysOrder($expectedResponseKeysOrder) {
$this->expectedResponseKeysOrder = $expectedResponseKeysOrder;
return $this;
}
/**
* @return string
*/
public function getExtensionId() {
return $this->extensionId;
}
/**
* Creates array for request.
*
* If requests for your extension are signed incorrectly because of
* wrong order of parts of the base string, check that order of keys in
* your input data given to setInputData() is the same as in extension's documentation on CSOB wiki,
* or extend this class and reimplement getRequestSignatureBase() method.
*
* @param Client $client
*
* @return array
*/
public function createRequestArray(Client $client) {
$sourceArray = $this->getInputData();
if (!$sourceArray) {
return null;
}
$config = $client->getConfig();
/*
if (!array_key_exists('dttm', $sourceArray)) {
$sourceArray = array(
'dttm' => $this->getDTTM()
) + $sourceArray;
} elseif (!$sourceArray['dttm']) {
$sourceArray['dttm'] = $this->getDTTM();
}
if (!array_key_exists('extension', $sourceArray)) {
$sourceArray = array(
'extension' => $this->getExtensionId()
) + $sourceArray;
} elseif (!$sourceArray['extension']) {
$sourceArray['extension'] = $this->getExtensionId();
}*/
if (array_key_exists('dttm', $sourceArray) and !$sourceArray['dttm']) {
$sourceArray['dttm'] = $client->getDTTM();
}
if (array_key_exists('extension', $sourceArray) and !$sourceArray['extension']) {
$sourceArray['extension'] = $this->getExtensionId();
}
$baseString = $this->getRequestSignatureBase($sourceArray);
$client->writeToTraceLog('Signing request of extension ' . $this->extensionId . ', base string is:' . "\n" . $baseString);
$signature = Crypto::signString($baseString, $config->privateKeyFile, $config->privateKeyPassword, $this->hashMethod);
$sourceArray['signature'] = $signature;
return $sourceArray;
}
/**
* Returns string that is used as basis for signature.
*
* Default implementation uses Crypto::createSignatureBaseFromArray.
* This means that order of keys is significant. If the calculated signature
* for the extension is incorrect, extend the class with your own and reimplement
* this method with a better one.
*
* @param array $dataArray Including dttm and extension ID
*
* @return string
*/
public function getRequestSignatureBase($dataArray) {
return Crypto::createSignatureBaseFromArray($dataArray, false);
}
/**
* Verifies signature.
*
* @param array $receivedData
* @param Client $client
*
* @return bool
*
*/
public function verifySignature($receivedData, Client $client) {
$signature = isset($receivedData['signature']) ? $receivedData['signature'] : '';
if (!$signature) {
return false;
}
$responseWithoutSignature = $receivedData;
unset($responseWithoutSignature["signature"]);
$baseString = $this->getResponseSignatureBase($responseWithoutSignature);
$config = $client->getConfig();
$client->writeToTraceLog('Verifying signature of response of extension ' . $this->extensionId . ', base string is:' . "\n" . $baseString);
return Crypto::verifySignature($baseString, $signature, $config->bankPublicKeyFile, $this->hashMethod);
}
/**
* Returns base string for verifying signature of response.
*
* If verifying signature fails because its base string has parts
* in incorrect order, check that the order of keys given to
* setExpectedResponseKeysOrder() is the same as in CSOB wiki,
* or reimplement this method with a better one.
*
* @param array $responseWithoutSignature
*
* @return string
*/
public function getResponseSignatureBase($responseWithoutSignature) {
$keys = $this->getExpectedResponseKeysOrder();
if ($keys) {
$baseString = Crypto::createSignatureBaseWithOrder($responseWithoutSignature, $keys, false);
} else {
$baseString = Crypto::createSignatureBaseFromArray($responseWithoutSignature, false);
}
return $baseString;
}
/**
* @return bool
*/
public function getStrictSignatureVerification() {
return $this->strictSignatureVerification;
}
/**
* @param bool $strictSignatureVerification
*
* @return self
*/
public function setStrictSignatureVerification($strictSignatureVerification) {
$this->strictSignatureVerification = $strictSignatureVerification;
return $this;
}
/**
* @return bool
*/
public function isSignatureCorrect() {
return $this->signatureCorrect;
}
/**
* @param bool $signatureCorrect
*/
public function setSignatureCorrect($signatureCorrect) {
$this->signatureCorrect = $signatureCorrect;
return $this;
}
/**
* @return mixed
*/
public function getResponseData() {
return $this->responseData;
}
/**
* @param mixed $responseData
*/
public function setResponseData($responseData) {
$this->responseData = $responseData;
}
/**
* @return int
*/
public function getHashMethod() {
return $this->hashMethod;
}
/**
* @param int $hashMethod
*/
public function setHashMethod($hashMethod) {
$this->hashMethod = $hashMethod;
}
}
}
// src/Metadata/Account.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
use DateTime;
/**
* @see https://github.com/csob/paymentgateway/wiki/Purchase-metadata#customer
*/
class Account {
/**
* @var DateTime
*/
protected $createdAt;
/**
* @var DateTime
*/
protected $changedAt;
/**
* @var DateTime
*/
protected $changedPwdAt;
/**
* @var int
*/
public $orderHistory = 0;
/**
* @var int
*/
public $paymentsDay = 0;
/**
* @var int
*/
public $paymentsYear = 0;
/**
* @var int
*/
public $oneclickAdds = 0;
/**
* @var bool
*/
public $suspicious = false;
/**
* @return DateTime
*/
public function getCreatedAt() {
return $this->createdAt;
}
/**
* @param DateTime $createdAt
*
* @return Account
*/
public function setCreatedAt(DateTime $createdAt) {
$this->createdAt = $createdAt;
return $this;
}
/**
* @return DateTime
*/
public function getChangedAt() {
return $this->changedAt;
}
/**
* @param DateTime $changedAt
*
* @return Account
*/
public function setChangedAt(DateTime $changedAt) {
$this->changedAt = $changedAt;
return $this;
}
/**
* @return DateTime
*/
public function getChangedPwdAt() {
return $this->changedPwdAt;
}
/**
* @param DateTime $changedPwdAt
*
* @return Account
*/
public function setChangedPwdAt(DateTime $changedPwdAt) {
$this->changedPwdAt = $changedPwdAt;
return $this;
}
public function export() {
$a = array(
'createdAt' => $this->createdAt ? $this->createdAt->format('c') : null,
'changedAt' => $this->changedAt ? $this->changedAt->format('c') : null,
'changedPwdAt' => $this->changedPwdAt ? $this->changedPwdAt->format('c') : null,
'orderHistory' => +$this->orderHistory,
'paymentsDay' => +$this->paymentsDay,
'paymentsYear' => +$this->paymentsYear,
'oneclickAdds' => +$this->oneclickAdds,
'suspicious' => !!$this->suspicious,
);
$a = Tools::filterOutEmptyFields($a);
return $a;
}
}
}
// src/Metadata/Address.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
use OndraKoupil\Tools\Strings;
class Address {
/**
* @var string
*/
public $address1 = '';
/**
* @var string
*/
public $address2 = '';
/**
* @var string
*/
public $address3 = '';
/**
* @var string
*/
public $city = '';
/**
* @var string
*/
public $zip = '';
/**
* @var string
*/
public $state = '';
/**
* @var string
*/
public $country = '';
/**
* @param string $address1
* @param string $city
* @param string $zip
* @param string $country
*/
public function __construct($address1, $city, $zip, $country) {
$this->address1 = $address1;
$this->city = $city;
$this->zip = $zip;
$this->country = $country;
}
public function export() {
$a = array(
'address1' => Strings::shorten($this->address1, 50, '', true, true),
'address2' => Strings::shorten($this->address2, 50, '', true, true),
'address3' => Strings::shorten($this->address3, 50, '', true, true),
'city' => Strings::shorten($this->city, 50, '', true, true),
'zip' => Strings::shorten($this->zip, 16, '', true, true),
'state' => trim($this->state),
'country' => trim($this->country),
);
return Tools::filterOutEmptyFields($a);
}
}
}
// src/Metadata/Customer.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
use OndraKoupil\Tools\Strings;
/**
* @see https://github.com/csob/paymentgateway/wiki/Purchase-metadata#customer
*/
class Customer {
/**
* @var string
*/
public $name = '';
/**
* @var string
*/
public $email = '';
/**
* @var string
*/
public $homePhone = '';
/**
* @var string
*/
public $workPhone = '';
/**
* @var string
*/
public $mobilePhone = '';
/**
* @var Account
*/
protected $account;
/**
* @var Login
*/
protected $login;
/**
* @return Account
*/
public function getAccount() {
return $this->account;
}
/**
* @param Account $account
*
* @return Customer
*/
public function setAccount($account) {
$this->account = $account;
return $this;
}
/**
* @return Login
*/
public function getLogin() {
return $this->login;
}
/**
* @param Login $login
*
* @return Customer
*/
public function setLogin($login) {
$this->login = $login;
return $this;
}
function export() {
$a = array(
'name' => Strings::shorten(trim($this->name), 45, '', true, true),
'email' => Strings::shorten(trim($this->email), 100, '', true, true),
'homePhone' => trim($this->homePhone),
'workPhone' => trim($this->workPhone),
'mobilePhone' => trim($this->mobilePhone),
'account' => $this->account ? $this->account->export() : null,
'login' => $this->login ? $this->login->export() : null,
);
$a = Tools::filterOutEmptyFields($a);
return $a;
}
}
}
// src/Metadata/GiftCards.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
class GiftCards {
/**
* @var number
*/
public $totalAmount;
/**
* @var string
*/
public $currency;
/**
* @var number
*/
public $quantity;
public function export() {
$a = array(
'totalAmount' => $this->totalAmount,
'currency' => $this->currency,
'quantity' => $this->quantity,
);
return Tools::filterOutEmptyFields($a);
}
}
}
// src/Metadata/Login.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
use DateTime;
/**
* @see https://github.com/csob/paymentgateway/wiki/Purchase-metadata#customerlogin-data-
*/
class Login {
const AUTH_GUEST = 'guest';
const AUTH_ACCOUNT = 'account';
const AUTH_FEDERATED = 'federated';
const AUTH_ISSUER = 'issuer';
const AUTH_THIRDPARTY = 'thirdparty';
const AUTH_FIDO = 'fido';
const AUTH_FIDO_SIGNED = 'fido_signed';
const AUTH_API = 'api';
/**
* Use AUTH_* class constants
* @var string
*/
public $auth = '';
/**
* @var DateTime
*/
protected $authAt;
public $authData = '';
/**
* @return mixed
*/
public function getAuthAt() {
return $this->authAt;
}
/**
* @param mixed $authAt
*
* @return Login
*/
public function setAuthAt(DateTime $authAt) {
$this->authAt = $authAt;
return $this;
}
function export() {
$a = array(
'auth' => trim($this->auth),
'authAt' => $this->authAt ? $this->authAt->format('c') : null,
'authData' => trim($this->authData),
);
$a = Tools::filterOutEmptyFields($a);
return $a;
}
}
}
// src/Metadata/Order.php
namespace OndraKoupil\Csob\Metadata {
use OndraKoupil\Csob\Tools;
use OndraKoupil\Tools\Strings;
use DateTime;
class Order {
const TYPE_PURCHASE = 'purchase';
const TYPE_BALANCE = 'balance';
const TYPE_PREPAID = 'prepaid';
const TYPE_CASH = 'cash';
const TYPE_CHECK = 'check';
const AVAILABILITY_NOW = 'now';
const AVAILABILITY_PREORDER = 'preorder';
const DELIVERY_SHIPPING = 'shipping';
const DELIVERY_SHIPPING_VERIFIED = 'shipping_verified';
const DELIVERY_INSTORE = 'instore';
const DELIVERY_DIGITAL = 'digital';
const DELIVERY_TICKET = 'ticket';
const DELIVERY_OTHER = 'other';
const DELIVERY_MODE_ELECTRONIC = 0;
const DELIVERY_MODE_SAME_DAY = 1;
const DELIVERY_MODE_NEXT_DAY = 2;
const DELIVERY_MODE_LATER = 3;
/**
* @var string
*/
public $type = '';
/**
* @var string
*/
public $availability = '';
/**
* @var string
*/
public $delivery = '';
/**
* @var int
*/
public $deliveryMode = 0;
/**
* @var string
*/
public $deliveryEmail = '';
/**
* @var bool
*/
public $nameMatch;
/**
* @var bool
*/
public $addressMatch;
/**
* @var Address
*/
protected $billing;
/**
* @var Address
*/
protected $shipping;
/**
* @var DateTime
*/
protected $shippingAddedAt;
/**
* @var bool
*/
public $reorder;
/**
* @var GiftCards
*/
protected $giftCards;
/**
* @return Address
*/
public function getBilling() {
return $this->billing;
}
/**
* @param Address $billing
*
* @return Order
*/
public function setBilling($billing) {
$this->billing = $billing;
return $this;
}
/**
* @return Address
*/
public function getShipping() {
return $this->shipping;
}
/**
* @param Address $shipping
*
* @return Order
*/
public function setShipping($shipping) {
$this->shipping = $shipping;
return $this;
}
/**
* @return DateTime
*/
public function getShippingAddedAt() {
return $this->shippingAddedAt;
}
/**
* @param DateTime $shippingAddedAt
*
* @return Order
*/
public function setShippingAddedAt($shippingAddedAt) {
$this->shippingAddedAt = $shippingAddedAt;
return $this;
}
/**
* @return GiftCards
*/
public function getGiftCards() {
return $this->giftCards;
}
/**
* @param GiftCards $giftCards
*
* @return Order
*/
public function setGiftCards($giftCards) {
$this->giftCards = $giftCards;
return $this;
}
public function export() {
$a = array(
'type' => trim($this->type),
'availability' => trim($this->availability),
'delivery' => trim($this->delivery),
'deliveryMode' => +$this->deliveryMode,
'deliveryEmail' => Strings::shorten($this->deliveryEmail, 100, '', true, true),
'nameMatch' => !!$this->nameMatch,
'addressMatch' => !!$this->addressMatch,
'billing' => $this->billing ? $this->billing->export() : null,
'shipping' => $this->shipping ? $this->shipping->export() : null,
'shippingAddedAt' => $this->shippingAddedAt ? $this->shippingAddedAt->format('c') : null,
'reorder' => !!$this->reorder,
'giftcards' => $this->giftCards ? $this->giftCards->export() : null,
);
return Tools::filterOutEmptyFields($a);
}
}
}
// src/Tools.php
namespace OndraKoupil\Csob {
class Tools {
public static function linearizeForSigning($input) {
if ($input === null) {
return '';
}
if (is_bool($input)) {
return $input ? 'true' : 'false';
}
if (is_array($input)) {
$parts = array();
foreach ($input as $inputItem) {
$parts[] = self::linearizeForSigning($inputItem);
}
return implode('|', $parts);
}
return $input;
}
public static function filterOutEmptyFields(array $input) {
return array_filter(
$input,
function($value) {
return ($value !== null and $value !== '');
}
);
}
}
}
// src/Extensions/CardNumberExtension.php
namespace OndraKoupil\Csob\Extensions {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
/**
* 'maskClnRP' extension for payment/status
*/
class CardNumberExtension extends Extension {
/**
* @var string
*/
protected $maskedCln;
/**
* @var string
*/
protected $expiration;
/**
* @var string
*/
protected $longMaskedCln;
function __construct() {
parent::__construct('maskClnRP');
}
function getExpectedResponseKeysOrder() {
return array(
'extension',
'dttm',
'maskedCln',
'expiration',
'longMaskedCln'
);
}
function setInputData($inputData) {
throw new Exception('You cannot call this directly.');
}
function setExpectedResponseKeysOrder($inputData) {
throw new Exception('You cannot call this directly.');
}
function setResponseData($responseData) {
if (isset($responseData['maskedCln'])) {
$this->maskedCln = $responseData['maskedCln'];
}
if (isset($responseData['expiration'])) {
$this->expiration = $responseData['expiration'];
}
if (isset($responseData['longMaskedCln'])) {
$this->longMaskedCln = $responseData['longMaskedCln'];
}
}
/**
* Returns payment card number as ****XXXX
*
* @return string
*/
public function getMaskedCln() {
return $this->maskedCln;
}
/**
* Returns payment card expiration as MM/YY
*
* @return string
*/
public function getExpiration() {
return $this->expiration;
}
/**
* Returns payment card number as PPPPPP****XXXX
*
* @return string
*/
public function getLongMaskedCln() {
return $this->longMaskedCln;
}
}
}
// src/Extensions/DatesExtension.php
namespace OndraKoupil\Csob\Extensions {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
use DateTime;
/**
* 'trxDates' extension for use in payment/status method
*/
class DatesExtension extends Extension {
/**
* @var DateTime
*/
protected $createdDate;
/**
* @var DateTime
*/
protected $settlementDate;
/**
* @var DateTime
*/
protected $authDate;
function __construct() {
parent::__construct('trxDates');
}
function getExpectedResponseKeysOrder() {
return array(
'extension',
'dttm',
'?createdDate',
'?authDate',
'?settlementDate',
);
}
function setInputData($inputData) {
throw new Exception('You cannot call this directly.');
}
function setExpectedResponseKeysOrder($inputData) {
throw new Exception('You cannot call this directly.');
}
function setResponseData($responseData) {
if (isset($responseData['createdDate'])) {
$this->createdDate = new DateTime($responseData['createdDate']);
} else {
$this->createdDate = null;
}
if (isset($responseData['authDate'])) {
$ok = preg_match('~^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$~', $responseData['authDate'], $parts);
if ($ok) {
$authDateHinted = "$parts[1]-$parts[2]-$parts[3] $parts[4]:$parts[5]:$parts[6]";
$this->authDate = new DateTime($authDateHinted);
} else {
$this->authDate = null;
}
} else {
$this->authDate = null;
}
if (isset($responseData['settlementDate'])) {
$ok = preg_match('~^(\d{4})(\d{2})(\d{2})$~', $responseData['settlementDate'], $parts);
if ($ok) {
$settlementDateHinted = "$parts[1]-$parts[2]-$parts[3]";
$this->settlementDate = new DateTime($settlementDateHinted);
} else {
$this->settlementDate = null;
}
} else {
$this->settlementDate = null;
}
}
/**
* Returns createdDate as DateTime or null if it has not been in the response
*
* @return DateTime|null
*/
public function getCreatedDate() {
return $this->createdDate;
}
/**
* Returns createdDate as DateTime or null if it has not been in the response
*
* @return DateTime|null
*/
public function getSettlementDate() {
return $this->settlementDate;
}
/**
* Returns createdDate as DateTime or null if it has not been in the response
*
* @return DateTime|null
*/
public function getAuthDate() {
return $this->authDate;
}
}
}
// src/Extensions/EET/EETCloseExtension.php
namespace OndraKoupil\Csob\Extensions\EET {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
class EETCloseExtension extends Extension {
/**
* @var EETData
*/
protected $data;
/**
* @param EETData $data
*/
function __construct(EETData $data = null) {
parent::__construct('eetV3');
$this->data = $data;
$this->expectedResponseKeysOrder = null;
}
/**
* @return EETData
*/
public function getEETData() {
return $this->data;
}
/**
* @param EETData $data
*/
public function setData($data = null) {
$this->data = $data;
}
/**
* Builds input data array
*
* @return array
*/
public function getInputData() {
if ($this->data) {
return array(
'extension' => $this->extensionId,
'dttm' => null,
'data' => $this->data->asArray(),
);
}
return null;
}
/**
* Do not call this directly.
*
* @param array $inputData
* @throws Exception
*
* @return void
*/
public function setInputData($inputData) {
throw new Exception('You cannot call this method directly.');
}
}
}
// src/Extensions/EET/EETData.php
namespace OndraKoupil\Csob\Extensions\EET {
/**
* Data set for the EET extension.
*
* You need to create this object when calling paymentInit.
*
* Then, you will receive this object in responses of other methods.
*
* When creating the object, remember that $totalPrice is in CZK, not halers (as is in Payment object)
*
* See https://github.com/csob/paymentgateway/wiki/Specifikace-API-roz%C5%A1%C3%AD%C5%99en%C3%AD-pro-EET
* for details on meaning of each property.
*/
class EETData {
/**
* @var number Označení provozovny
*/
public $premiseId;
/**
* @var string Označení pokladního zařízení poplatníka
*/
public $cashRegisterId;
/**
* @var number Celková částka tržby, v případě nahlášení platby (volání v rámci payment/init, payment/oneclick/init a payment/close) musí být kladné číslo, v případě odhlášení platby (volání v rámci payment/refund) musí být záporné číslo.
*/
public $totalPrice;
/**
* @var string DIČ pověřujícího poplatníka
*/
public $delegatedVatId;
/**
* @var number Celková částka plnění osvobozených od DPH, ostatních plnění
*/
public $priceZeroVat;
/**
* @var number Celkový základ daně se základní sazbou DPH
*/
public $priceStandardVat;
/**
* @var number Celková DPH se základní sazbou
*/
public $vatStandard;
/**
* @var number Celkový základ daně s první sníženou sazbou DPH
*/
public $priceFirstReducedVat;
/**
* @var number Celková DPH s první sníženou sazbou
*/
public $vatFirstReduced;
/**
* @var number Celkový základ daně s druhou sníženou sazbou DPH
*/
public $priceSecondReducedVat;
/**
* @var number Celková DPH s druhou sníženou sazbou
*/
public $vatSecondReduced;
/**
* @var number Celková částka v režimu DPH pro cestovní službu
*/
public $priceTravelService;
/**
* @var number Celková částka v režimu DPH pro prodej použitého zboží se základní sazbou
*/
public $priceUsedGoodsStandardVat;
/**
* @var number Celková částka v režimu DPH pro prodej použitého zboží s první sníženou sazbou
*/
public $priceUsedGoodsFirstReduced;
/**
* @var number Celková částka v režimu DPH pro prodej použitého zboží s druhou sníženou sazbou
*/
public $priceUsedGoodsSecondReduced;
/**
* @var number Celková částka plateb určená k následnému čerpání nebo zúčtování
*/
public $priceSubsequentSettlement;
/**
* @var number Celková částka plateb, které jsou následným čerpáním nebo zúčtováním platby
*/
public $priceUsedSubsequentSettlement;
/**
* @var array Contains raw data as were received from API. Only for responses.
*/
public $rawData;
/**
* Constructor allows to set three mandatory properties
*
* @param number $premiseId
* @param string $cashRegisterId
* @param number $totalPrice
*/
public function __construct($premiseId = null, $cashRegisterId = null, $totalPrice = null) {
$this->premiseId = $premiseId;
$this->cashRegisterId = $cashRegisterId;
$this->totalPrice = $totalPrice;
}
/**
* Export as array
*
* @return array
*/
public function asArray() {
$a = array();
$a['premiseId'] = +$this->premiseId;
$a['cashRegisterId'] = $this->cashRegisterId;
$a['totalPrice'] = self::formatPriceValue($this->totalPrice);
if ($this->delegatedVatId) {
$a['delegatedVatId'] = $this->delegatedVatId;
}
if ($this->priceZeroVat) {
$a['priceZeroVat'] = self::formatPriceValue($this->priceZeroVat);
}
if ($this->priceStandardVat) {
$a['priceStandardVat'] = self::formatPriceValue($this->priceStandardVat);
}
if ($this->vatStandard) {
$a['vatStandard'] = self::formatPriceValue($this->vatStandard);
}
if ($this->priceFirstReducedVat) {
$a['priceFirstReducedVat'] = self::formatPriceValue($this->priceFirstReducedVat);
}
if ($this->vatFirstReduced) {
$a['vatFirstReduced'] = self::formatPriceValue($this->vatFirstReduced);
}
if ($this->priceSecondReducedVat) {
$a['priceSecondReducedVat'] = self::formatPriceValue($this->priceSecondReducedVat);
}
if ($this->vatSecondReduced) {
$a['vatSecondReduced'] = self::formatPriceValue($this->vatSecondReduced);
}
if ($this->priceTravelService) {
$a['priceTravelService'] = self::formatPriceValue($this->priceTravelService);
}
if ($this->priceUsedGoodsStandardVat) {
$a['priceUsedGoodsStandardVat'] = self::formatPriceValue($this->priceUsedGoodsStandardVat);
}
if ($this->priceUsedGoodsFirstReduced) {
$a['priceUsedGoodsFirstReduced'] = self::formatPriceValue($this->priceUsedGoodsFirstReduced);
}
if ($this->priceUsedGoodsSecondReduced) {
$a['priceUsedGoodsSecondReduced'] = self::formatPriceValue($this->priceUsedGoodsSecondReduced);
}
if ($this->priceSubsequentSettlement) {
$a['priceSubsequentSettlement'] = self::formatPriceValue($this->priceSubsequentSettlement);
}
if ($this->priceUsedSubsequentSettlement) {
$a['priceUsedSubsequentSettlement'] = self::formatPriceValue($this->priceUsedSubsequentSettlement);
}
return $a;
}
/**
* Format a numeric price for use in EET extension
*
* @param number $price
*
* @return number
*/
static function formatPriceValue($price) {
return number_format($price, 2, '.', '');
}
static protected $keyNames = array(
'premiseId',
'cashRegisterId',
'totalPrice',
'delegatedVatId',
'priceZeroVat',
'priceStandardVat',
'vatStandard',
'priceFirstReducedVat',
'vatFirstReduced',
'priceSecondReducedVat',
'vatSecondReduced',
'priceTravelService',
'priceUsedGoodsStandardVat',
'priceUsedGoodsFirstReduced',
'priceUsedGoodsSecondReduced',
'priceSubsequentSettlement',
'priceUsedSubsequentSettlement',
);
/**
* Creates EETData object from array received from API
*
* @param $array
*
* @return EETData
*/
static function fromArray($array) {
$data = new EETData();
foreach (self::$keyNames as $key) {
if (array_key_exists($key, $array)) {
$data->$key = $array[$key];
}
}
$data->rawData = $array;
return $data;
}
/**
* Return part of the string required for building the signature string.
*
* @return string
*/
public function getSignatureBase() {
$array = $this->asArray();
return implode('|', $this->asArray());
}
}
}
// src/Extensions/EET/EETError.php
namespace OndraKoupil\Csob\Extensions\EET {
/**
* Common base for error and warning in EET extension
*/
class EETError extends EETErrorMessage {
}
}
// src/Extensions/EET/EETErrorMessage.php
namespace OndraKoupil\Csob\Extensions\EET {
/**
* Represents an error message from EET extension
*/
abstract class EETErrorMessage {
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $desc;
/**
* The constructor
*
* @param string $code
* @param string $desc
*/
public function __construct($code, $desc) {
$this->code = $code;
$this->desc = $desc;
}
/**
* @return string
*/
public function getSignatureBase() {
return $this->code . '|' . $this->desc;
}
}
}
// src/Extensions/EET/EETInitExtension.php
namespace OndraKoupil\Csob\Extensions\EET {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
/**
* Extension for EET - payment/init and payment/oneclick/init operations
*/
class EETInitExtension extends Extension {
/**
* @var bool Ověřovací (testovací) režim?
*/
public $verificationMode;
/**
* @var EETData
*/
protected $data;
/**
* @param EETData $data Data for EET
* @param bool $verificationMode Should verification (testing) mode be used?
*/
function __construct(EETData $data, $verificationMode = false) {
parent::__construct('eetV3');
$this->data = $data;
$this->verificationMode = $verificationMode ? true : false;
$this->expectedResponseKeysOrder = null;
}
/**
* @return EETData
*/
public function getEETData() {
return $this->data;
}
/**
* Builds input data array
*
* @return array
*/
public function getInputData() {
$a = array(
'extension' => $this->extensionId,
'dttm' => null,
'data' => $this->data->asArray(),
'verificationMode' => $this->verificationMode ? 'true' : 'false'
);
return $a;
}
/**
* Do not call this directly.
*
* @param array $inputData
* @throws Exception
*
* @return void
*/
public function setInputData($inputData) {
throw new Exception('You cannot call this method directly.');
}
}
}
// src/Extensions/EET/EETRefundExtension.php
namespace OndraKoupil\Csob\Extensions\EET {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
/**
* Remember that in data object, the amount should be NEGATIVE!
*/
class EETRefundExtension extends Extension {
/**
* @var EETData
*/
protected $data;
/**
* @param EETData $data
*/
function __construct(EETData $data = null) {
parent::__construct('eetV3');
$this->data = $data;
$this->expectedResponseKeysOrder = null;
}
/**
* @return EETData
*/
public function getEETData() {
return $this->data;
}
/**
* @param EETData $data
*/
public function setData($data = null) {
$this->data = $data;
}
/**
* Builds input data array
*
* @return array
*/
public function getInputData() {
if ($this->data) {
return array(
'extension' => $this->extensionId,
'dttm' => null,
'data' => $this->data->asArray(),
);
}
return null;
}
/**
* Do not call this directly.
*
* @param array $inputData
* @throws Exception
*
* @return void
*/
public function setInputData($inputData) {
throw new Exception('You cannot call this method directly.');
}
}
}
// src/Extensions/EET/EETReport.php
namespace OndraKoupil\Csob\Extensions\EET {
use DateTime;
/**
* Represents a result of payment/status call with EET extension activated.
*
* You will receive this object from responses, but won't ever need to create it manually.
*/
class EETReport {
/**
* @var number stav nahlášení platby, viz životní cyklus tržby
* @see https://github.com/csob/paymentgateway/wiki/%C5%BDivotn%C3%AD-cyklus-tr%C5%BEby
*/
public $eetStatus;
/**
* @var EETData
*/
public $data;
/**
* @var boolean Příznak ověřovacího módu odesílání
*/
public $verificationMode;
/**
* @var string DIČ poplatníka
*/
public $vatId;
/**
* @var string Pořadové číslo účtenky, formát YYYYMMRXXXXXXXXXX, kde YYYY je rok, MM měsíc, R znak identifikující nahlášení platby, XXXXXXXXXX pořadové číslo účtenky, např. 201701R0000000004
*/
public $receiptNumber;
/**
* @var DateTime|null Datum a čas přijetí tržby
*/
public $receiptTime;
/**
* @var number Režim platby, platební brána podporuje pouze běžný režim, bude vrácena hodnota 0
*/
public $evidenceMode;
/**
* @var string UUID datové zprávy evidované tržby
*/
public $uuid;
/**
* @var DateTime|null Datum a čas odeslání zprávy z platební brány
*/
public $sendTime;
/**
* @var DateTime|null Datum a čas přijetí zprávy na FS
*/
public $acceptTime;
/**
* @var string Bezpečnostní kód poplatníka
*/
public $bkp;
/**
* @var string Podpisový kód poplatníka
*/
public $pkp;
/**
* @var string Fiskální identifikační kód
*/
public $fik;
/**
* @var DateTime|null Datum a čas odmítnutí zprávy na FS
*/
public $rejectTime;
/**
* @var EETError|null error zpracování na FS, viz popis objektu error
*/
public $error;
/**
* @var EETWarning[] Seznam případných varování z FS, viz popis objektu warning
*/
public $warning = array();
/**
* @var array
*/
public $rawData = array();
/**
* Creates an EETStatus object from received data array
*
* @param array $array
*
* @return EETReport
*/
static public function fromArray($array) {
$status = new EETReport();
$status->rawData = $array;
if (array_key_exists('eetStatus', $array)) {
$status->eetStatus = $array['eetStatus'];
}
if (array_key_exists('data', $array)) {
$status->data = EETData::fromArray($array['data']);
}
if (array_key_exists('verificationMode', $array)) {
$status->verificationMode = $array['verificationMode'] ? true : false;
}
if (array_key_exists('vatId', $array)) {
$status->vatId = $array['vatId'];
}
if (array_key_exists('receiptNumber', $array)) {
$status->receiptNumber = $array['receiptNumber'];
}
if (array_key_exists('receiptTime', $array)) {
$status->receiptTime = new DateTime($array['receiptTime']);
}
if (array_key_exists('evidenceMode', $array)) {
$status->evidenceMode = $array['evidenceMode'];
}
if (array_key_exists('uuid', $array)) {
$status->uuid = $array['uuid'];
}
if (array_key_exists('sendTime', $array)) {
$status->sendTime = new DateTime($array['sendTime']);
}
if (array_key_exists('acceptTime', $array)) {
$status->acceptTime = new DateTime($array['acceptTime']);
}
if (array_key_exists('bkp', $array)) {
$status->bkp = $array['bkp'];
}
if (array_key_exists('pkp', $array)) {
$status->pkp = $array['pkp'];
}
if (array_key_exists('fik', $array)) {
$status->fik = $array['fik'];
}
if (array_key_exists('rejectTime', $array)) {
$status->rejectTime = new DateTime($array['rejectTime']);
}
if (array_key_exists('error', $array) and $array['error']) {
$status->error = new EETError($array['error']['code'], $array['error']['desc']);
}
if (array_key_exists('warning', $array) and is_array($array['warning'])) {
foreach ($array['warning'] as $warningData) {
$status->warning[] = new EETWarning($warningData['code'], $warningData['desc']);
}
}
return $status;
}
/**
* @inheritdoc
*
* @return string
*/
public function getSignatureBase() {
$fields = array();
if ($this->eetStatus !== null) {
$fields[] = $this->eetStatus;
}
if ($this->data) {
$fields[] = $this->data->getSignatureBase();
}
if ($this->verificationMode !== null) {
$fields[] = $this->verificationMode ? 'true' : 'false';
}
if ($this->vatId) {
$fields[] = $this->vatId;
}
if ($this->receiptNumber) {
$fields[] = $this->receiptNumber;
}
if ($this->receiptTime) {
if (isset($this->rawData['receiptTime']) and $this->rawData['receiptTime']) {
$fields[] = $this->rawData['receiptTime'];
} else {
$fields[] = self::formatTime($this->receiptTime);
}
}
if ($this->evidenceMode !== null) {
$fields[] = $this->evidenceMode;
}
if ($this->uuid !== null) {
$fields[] = $this->uuid;
}
if ($this->sendTime) {
if (isset($this->rawData['sendTime']) and $this->rawData['sendTime']) {
$fields[] = $this->rawData['sendTime'];
} else {
$fields[] = self::formatTime($this->sendTime);
}
}
if ($this->acceptTime) {
if (isset($this->rawData['acceptTime']) and $this->rawData['acceptTime']) {
$fields[] = $this->rawData['acceptTime'];
} else {
$fields[] = self::formatTime($this->acceptTime);
}
}
if ($this->bkp) {
$fields[] = $this->bkp;
}
if ($this->pkp) {
$fields[] = $this->pkp;
}
if ($this->fik) {
$fields[] = $this->fik;
}
if ($this->rejectTime) {
if (isset($this->rawData['rejectTime']) and $this->rawData['rejectTime']) {
$fields[] = $this->rawData['rejectTime'];
} else {
$fields[] = self::formatTime($this->rejectTime);
}
}
if ($this->error) {
$fields[] = $this->error->getSignatureBase();
}
if ($this->warning) {
foreach ($this->warning as $w) {
$fields[] = $w->getSignatureBase();
}
}
return implode('|', $fields);
}
/**
* Formats DateTime to format used in API
*
* @param DateTime $dt
*
* @return string
*/
static function formatTime(DateTime $dt) {
return $dt->format('c');
}
}
}
// src/Extensions/EET/EETStatusExtension.php
namespace OndraKoupil\Csob\Extensions\EET {
use OndraKoupil\Csob\Exception;
use OndraKoupil\Csob\Extension;
/**
* Represents extension for EET for payment/status method.
*/
class EETStatusExtension extends Extension {
/**
* Data from "report" section of response
*
* @var EETReport
*/
protected $report;
/**
* Data from "cancel" section of response
*
* @var EETReport[]
*/
protected $cancels;
/**
* The constructor
*/
function __construct() {
parent::__construct('eetV3');
}
/**
* Builds input data array
*
* @return array
*/
public function getInputData() {
return null;
}
/**
* Do not call this directly.
*
* @param array $inputData
* @throws Exception
*
* @return void
*/
public function setInputData($inputData) {
throw new Exception('You cannot call this method directly.');
}
/**
* @inheritdoc
*
* @param array $data
*
* @return void
*/
public function setResponseData($data) {
$this->responseData = $data;
if (isset($data['report'])) {
$this->report = EETReport::fromArray($data['report']);
}
$this->cancels = array();
if (isset($data['cancel'])) {
foreach ($data['cancel'] as $cancel) {
$this->cancels[] = EETReport::fromArray($cancel);
}
}
}
/**
* @inheritdoc
*
* @param array $dataArray
*
* @return string
*/
public function getResponseSignatureBase($dataArray) {
$base = array();
$base[] = $dataArray['extension'];
$base[] = $dataArray['dttm'];
if ($this->report) {
$base[] = $this->report->getSignatureBase();
}
if ($this->cancels) {
foreach ($this->cancels as $cancel) {
$base[] = $cancel->getSignatureBase();
}
}
return implode('|', $base);
}
/**
* @return EETReport
*/
public function getReport() {
return $this->report;
}
/**
* @return EETReport[]
*/
public function getCancels() {
return $this->cancels;
}
/**
* Shortcut to get BKP from response's report
*
* @return string
*/
public function getBKP() {
if ($this->report) {
return $this->report->bkp;
}
return "";
}
/**
* Shortcut to get PKP from response's report
*
* @return string
*/
public function getPKP() {
if ($this->report) {
return $this->report->pkp;
}
return "";
}
/**
* Shortcut to get FIK from response's report
*
* @return string
*/
public function getFIK() {
if ($this->report) {
return $this->report->fik;
}
return "";
}
/**
* Shortcut to get EET status from response's report
*
* @return number|string
*/
public function getEETStatus() {
if ($this->report) {
return $this->report->eetStatus;
}
return "";
}
}
}
// src/Extensions/EET/EETWarning.php
namespace OndraKoupil\Csob\Extensions\EET {
class EETWarning extends EETErrorMessage {
}
}
// vendor/ondrakoupil/tools/src/Strings.php
namespace OndraKoupil\Tools {
use ArrayAccess;
class Strings {
/**
* Skloňuje řetězec dle českých pravidel řetězec
* @param number $amount
* @param string $one Lze použít dvě procenta - %% - pro nahrazení za $amount
* @param string $two
* @param string $five Vynechat nebo null = použít $two
* @param string $zero Vynechat nebo null = použít $five
* @return string
*/
static function plural($amount, $one, $two = null, $five = null, $zero = null) {
if ($two === null) $two = $one;
if ($five === null) $five = $two;
if ($zero === null) $zero = $five;
if ($amount==1) return str_replace("%%",$amount,$one);
if ($amount>1 and $amount<5) return str_replace("%%",$amount,$two);
if ($amount == 0) return str_replace("%%",$amount,$zero);
return str_replace("%%",$amount,$five);
}
/**
* strlen pro UTF-8
* @param string $input
* @return int
*/
static function length($input) {
return mb_strlen($input, "utf-8");
}
/**
* strlen pro UTF-8
* @param string $input
* @return int
*/
static function strlen($input) {
return self::length($input);
}
/**
* substr() pro UTF-8
*
* @param string $input
* @param int $start
* @param int $length
* @return string
*/
static function substring($input, $start, $length = null) {
return self::substr($input, $start, $length, "utf-8");
}
/**
* substr() pro UTF-8
*
* @param string $input
* @param int $start
* @param int $length
* @return string
*/
static function substr($input, $start, $length = null) {
if ($length === null) {
$length = self::length($input) - $start;
}
return mb_substr($input, $start, $length, "utf-8");
}
static function strpos($haystack, $needle, $offset = 0) {
return mb_strpos($haystack, $needle, $offset, "utf-8");
}
static function strToLower($string) {
return mb_strtolower($string, "utf-8");
}
static function lower($string) {
return self::strToLower($string);
}
static function strToUpper($string) {
return mb_strtoupper($string, "utf-8");
}
static function upper($string) {
return self::strToUpper($string);
}
/**
* Otestuje zda řetězec obsahuje hledaný výraz
* @param $haystack
* @param $needle
* @return bool
*/
public static function contains($haystack, $needle) {
return strpos($haystack, $needle) !== FALSE;
}
/**
* Otestuje zda řetězec obsahuje hledaný výraz, nedbá na velikost znaků
* @param $haystack
* @param $needle
* @return bool
*/
public static function icontains($haystack, $needle) {
return stripos($haystack, $needle) !== FALSE;
}
/**
* Funkce pro zkrácení dlouhého textu na menší délku.
* Ořezává tak, aby nerozdělovala slova, a případně umí i odstranit HTML znaky.
* @param string $text Původní (dlouhý) text
* @param int $length Požadovaná délka textu. Oříznutý text nebude mít přesně tuto délku, může být o nějaké znaky kratší nebo delší podle toho, kde končí slovo.
* @param string $ending Pokud dojde ke zkrácení textu, tak s na jeho konec přilepí tento řetězec. Defaultně trojtečka (jako HTML entita …). TRUE = … (nemusíš si pak pamatovat tu entitu)
* @param bool $stripHtml Mají se odstraňovat HTML tagy? True = odstranit. Zachovají se pouze
, a všechny konce řádků (\n i \r) budou nahrazeny za
.
* Odstraňování je důležité, jinak by mohlo dojít k ořezu uprostřed HTML tagu, anebo by nebyl nějaký tag správně ukončen.
* Pro ořezávání se zachováním html tagů je shortenHtml().
* @param bool $ignoreWords Ignorovat slova a rozdělit přesně.
* @return string Zkrácený text
*/
static function shorten($text, $length, $ending="…", $stripHtml=true, $ignoreWords = false) {
if ($stripHtml) {
$text=self::br2nl($text);
$text=strip_tags($text);
}
$text=trim($text);
if ($ending===true) $ending="…";
$text = trim($text);
$needsTrim = (self::strlen($text) > $length);
if (!$needsTrim) {
return $text;
}
$hardTrimmed = self::substr($text, 0, $length);
if (!$ignoreWords) {
$nextChar = self::substr($text, $length, 1);
if (!preg_match('~[\s.,/\-]~', $nextChar)) {
$endingRemains = preg_match('~[\s.,/\-]([^\s.,/\-]*)$~', $hardTrimmed, $foundParts);
if ($endingRemains) {
$endingLength = self::strlen($foundParts[1]);
$hardTrimmed = self::substr($hardTrimmed, 0, -1 * $endingLength - 1);
}
}
}
$hardTrimmed .= $ending;
$hardTrimmed = trim($hardTrimmed);
return $hardTrimmed;
}
/**
* Všechny tagy BR (ve formě <br> i <br />) nahradí za \n (LF)
* @param string $input
* @return string
*/
static function br2nl($input) {
return preg_replace('~
~i', "\n", $input ?: '');
}
/**
* Nahradí nové řádky za <br />, ale nezanechá je tam.
* @param string $input
* @return string
*/
static function nl2br($input) {
$input = str_replace("\r\n", "\n", $input ?: '');
return str_replace("\n", "
", $input ?: '');
}
/**
* Nahradí entity v řetězci hodnotami ze zadaného pole.
* @param string $string
* @param array|ArrayAccess $valuesArray
* @param callback $escapeFunction Funkce, ktrsou se prožene každá nahrazená entita (např. kvůli escapování paznaků). Defaultně Html::escape()
* @param string $entityDelimiter Jeden znak
* @param string $entityNameChars Rozsah povolených znaků v názvech entit
* @return string
*/
static function replaceEntities($string, $valuesArray, $escapeFunction = "!!default", $entityDelimiter = "%", $entityNameChars = 'a-z0-9_-') {
if ($escapeFunction === "!!default") {
$escapeFunction = "\\OndraKoupil\\Tools\\Html::escape";
}
$arrayMode = is_array($valuesArray);
$arrayAccessMode = (!is_array($valuesArray) and $valuesArray instanceof ArrayAccess);
$string = \preg_replace_callback('~'.preg_quote($entityDelimiter).'(['.$entityNameChars.']+)'.preg_quote($entityDelimiter).'~i', function($found) use ($valuesArray, $escapeFunction, $arrayMode, $arrayAccessMode) {
if ($arrayMode and key_exists($found[1], $valuesArray)) {
$v = $valuesArray[$found[1]];
if ($escapeFunction) {
$v = call_user_func_array($escapeFunction, array($v));
}
return $v;
}
if ($arrayAccessMode) {
if (isset($valuesArray[$found[1]])) {
$v = $valuesArray[$found[1]];
if ($escapeFunction) {
$v = call_user_func_array($escapeFunction, array($v));
}
return $v;
}
}
if (!$arrayAccessMode and !$arrayMode) {
if (property_exists($valuesArray, $found[1])) {
$v = $valuesArray->{$found[1]};
if ($escapeFunction) {
$v = call_user_func_array($escapeFunction, array($v));
}
return $v;
}
}
return $found[0];
}, $string);
return $string;
}
/**
* Převede číslo s lidsky čitelným násobitelem, jako to zadávané v php.ini (např. 100M jako 100 mega), na normální číslo
* @param string $number
* @return number|boolean False, pokud je vstup nepřevoditelný
*/
static function parsePhpNumber($number) {
$number = trim($number);
if (is_numeric($number)) {
return $number * 1;
}
if (preg_match('~^(-?)([0-9\.,]+)([kmgt]?)$~i', $number, $parts)) {
$base = self::number($parts[2]);
switch ($parts[3]) {
case "K": case "k":
$base *= 1024;
break;
case "M": case "m":
$base *= 1024 * 1024;
break;
case "G": case "g":
$base *= 1024 * 1024 * 1024;
break;
case "T": case "t":
$base *= 1024 * 1024 * 1024 * 1024;
break;
}
if ($parts[1]) {
$c = -1;
} else {
$c = 1;
}
return $base * $c;
}
return false;
}
/**
* Naformátuje telefonní číslo
* @param string $input
* @param bool $international Nechat/přidat mezinárodní předvolbu?
* @param bool|string $spaces Přidat mezery pro trojčíslí? True = mezery. False = žádné mezery. String = zadaný řetězec použít jako mezeru.
* @param string $internationalPrefix Prefix pro mezinárodní řpedvolbu, používá se většinou "+" nebo "00"
* @param string $defaultInternational Výchozí mezinárodní předvolba (je-li $international == true a $input je bez předvolby). Zadávej BEZ prefixu.
* @return string
*/
static function phoneNumberFormatter($input, $international = true, $spaces = false, $internationalPrefix = "+", $defaultInternational = "420") {
if (!trim($input)) {
return "";
}
if ($spaces === true) {
$spaces = " ";
}
$filteredInput = preg_replace('~\D~', '', $input);
$parsedInternational = "";
$parsedMain = "";
if (strlen($filteredInput) > 9) {
$parsedInternational = self::substr($filteredInput, 0, -9);
$parsedMain = self::substr($filteredInput, -9);
} else {
$parsedMain = $filteredInput;
}
if (self::startsWith($parsedInternational, $internationalPrefix)) {
$parsedInternational = self::substr($parsedInternational, self::strlen($internationalPrefix));
}
if ($spaces) {
$spacedMain = "";
$len = self::strlen($parsedMain);
for ($i = $len; $i > -3; $i-=3) {
$spacedMain = self::substr($parsedMain, ($i >= 0 ? $i : 0), ($i >= 0 ? 3 : (3 - $i * -1)))
.($spacedMain ? ($spaces.$spacedMain) : "");
}
} else {
$spacedMain = $parsedMain;
}
$output = "";
if ($international) {
if (!$parsedInternational) {
$parsedInternational = $defaultInternational;
}
$output .= $internationalPrefix.$parsedInternational;
if ($spaces) {
$output .= $spaces;
}
}
$output .= $spacedMain;
return $output;
}
/**
* Začíná $string na $startsWith?
* @param string $string
* @param string $startsWith
* @param bool $caseSensitive
* @return bool
*/
static function startsWith($string, $startsWith, $caseSensitive = true) {
$len = self::strlen($startsWith);
if ($caseSensitive) return self::substr($string, 0, $len) == $startsWith;
return self::strtolower(self::substr($string, 0, $len)) == self::strtolower($startsWith);
}
/**
* Končí $string na $endsWith?
* @param string $string
* @param string $endsWith
* @return string
*/
static function endsWith($string, $endsWith, $caseSensitive = true) {
$len = self::strlen($endsWith);
if ($caseSensitive) return self::substr($string, -1 * $len) == $endsWith;
return self::strtolower(self::substr($string, -1 * $len)) == self::strtolower($endsWith);
}
/**
* Ošetří zadanou hodnotu tak, aby z ní bylo číslo.
* (normalizuje desetinnou čárku na tečku a ověří is_numeric).
* @param mixed $string
* @param int|float $default Vrátí se, pokud $vstup není čílený řetězec ani číslo (tj. je array, object, bool nebo nenumerický řetězec)
* @param bool $positiveOnly Dáš-li true, tak se záporné číslo bude považovat za nepřijatelné a vrátí se $default (vhodné např. pro strtotime)
* @return int|float
*/
static function number($string, $default = 0, $positiveOnly = false) {
if (is_bool($string) or is_object($string) or is_array($string)) return $default;
$string = (string)$string;
$string=str_replace(array(","," "),array(".",""),trim($string));
if (!is_numeric($string)) return $default;
$string = $string * 1; // Convert to number
if ($positiveOnly and $string<0) return $default;
return $string;
}
/**
* Funkce zlikviduje z řetězce všechno kromě číselných znaků a vybraného desetinného oddělovače.
* @param string $string
* @param string $decimalPoint
* @param string $convertedDecimalPoint Takto lze normalizovat desetinný oddělovač.
* @return string
*/
static function numberOnly($string, $decimalPoint = ".", $convertedDecimalPoint = ".") {
$vystup="";
for ($i=0;$iAlias pro webalize()
* @param string $string
* @param bool $allowDot Povolit tečku?
* @return string
*/
static function safe($string, $allowDot = true) {
return self::webalize($string, $allowDot ? "." : "");
}
/**
* Converts to ASCII.
* @param string UTF-8 encoding
* @return string ASCII
* @author Nette Framework
*/
public static function toAscii($s)
{
$s = preg_replace('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s);
$s = strtr($s, '`\'"^~', "\x01\x02\x03\x04\x05");
if (ICONV_IMPL === 'glibc') {
$s = @iconv('UTF-8', 'WINDOWS-1250//TRANSLIT', $s); // intentionally @
$s = strtr($s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e"
. "\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3"
. "\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8"
. "\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96",
"ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt-");
} else {
$s = @iconv('UTF-8', 'ASCII//TRANSLIT', $s); // intentionally @
}
$s = str_replace(array('`', "'", '"', '^', '~'), '', $s);
return strtr($s, "\x01\x02\x03\x04\x05", '`\'"^~');
}
/**
* Převede řetězec na základní alfanumerické znaky a pomlčky [a-z0-9.-]
* @param string $s Řetězec, UTF-8 encoding
* @param string $charlist allowed characters jako regexp
* @param bool $lower Zmenšit na malá písmena?
* @return string
* @author Nette Framework
*/
public static function webalize($s, $charlist = NULL, $lower = TRUE)
{
$s = self::toAscii($s);
if ($lower) {
$s = strtolower($s);
}
if (!$charlist) {
$charlist = '';
}
$s = preg_replace('#[^a-z0-9' . preg_quote($charlist, '#') . ']+#i', '-', $s);
$s = trim($s, '-');
return $s;
}
/**
* Převede číselnou velikost na textové výjádření v jednotkách velikosti (KB,MB,...)
* @param $size
* @return string
*/
public static function formatSize($size, $decimalPrecision = 2) {
if ($size < 1024) return $size . ' B';
elseif ($size < 1048576) return round($size / 1024, $decimalPrecision) . ' kB';
elseif ($size < 1073741824) return round($size / 1048576, $decimalPrecision) . ' MB';
elseif ($size < 1099511627776) return round($size / 1073741824, $decimalPrecision) . ' GB';
elseif ($size < 1125899906842624) return round($size / 1099511627776, $decimalPrecision) . ' TB';
elseif ($size < 1152921504606846976) return round($size / 1125899906842624, $decimalPrecision) . ' PB';
else return round($size / 1152921504606846976, $decimalPrecision) . ' EB';
}
/**
* Ošetření paznaků v HTML kódu
*
* @param string $input
* @param bool $doubleEncode
*
* @return string
*/
public static function specChars($input, $doubleEncode = false) {
return htmlspecialchars($input, ENT_QUOTES, 'utf-8', $doubleEncode);
}
/**
* Vygeneruje náhodný alfanumerický řetězec zadané délky
*
* @param int $length
* @return string Skládá se z [a-zA-Z0-9] nebo [a-z0-9] při $lowercase === true
*/
public static function randomString($length, $lowercase = false) {
$bytesLength = ceil($length * 3/4) + 1;
$randomBytes = openssl_random_pseudo_bytes($bytesLength);
$hex = base64_encode($randomBytes);
$hex = preg_replace('~[/+=]~', '', $hex);
$len = strlen($hex);
if ($len > $length) {
$hex = substr($hex, 0, $length);
}
if ($len < $length) {
$hex .= self::randomString($length - $len);
}
if ($lowercase) {
$hex = strtolower($hex);
}
return $hex;
}
/**
* Převede excelovské značení sloupců (a, b, c, ..., aa, ab, ac, ...) na zero-based (0, 1, 2, ..., 26, 27, 28, ...) číslování.
* @param string $excelSloupec
* @return int
*/
static function excelToNumber($excelSloupec) {
$excelSloupec = strtolower(trim($excelSloupec));
$cislo = 0;
while ($excelSloupec) {
$pismenko = $excelSloupec[0];
$cislo *= 26;
$cislo += ord($pismenko) - 96;
$excelSloupec = substr($excelSloupec, 1);
}
return $cislo - 1;
}
}
}
// vendor/ondrakoupil/tools/src/Arrays.php
namespace OndraKoupil\Tools {
class Arrays {
/**
* Zajistí, aby zadaný argument byl array.
*
* Převede booly nebo nully na array(), pole nechá být, ArrayAccess a Traversable
* také, vše ostatní převede na array(0=>$hodnota)
*
* @param mixed $value
* @param bool $forceArrayFromObject True = Traversable objekty také převádět na array
* @return array|\ArrayAccess|\Traversable
*/
static function arrayize($value, $forceArrayFromObject = false) {
if (is_array($value)) return $value;
if (is_bool($value) or $value===null) return array();
if ($value instanceof \Traversable) {
if ($forceArrayFromObject) {
return iterator_to_array($value);
}
return $value;
}
if ($value instanceof \ArrayAccess) {
return $value;
}
return array(0=>$value);
}
/**
* Pokud je pole, převede na řetězec, jinak nechá být
* @param array|mixed $value
* @param string $glue
* @return mixed
*/
static function dearrayize($value,$glue=",") {
if (is_array($value)) return implode($glue, $value);
return $value;
}
/**
* Transformace dvoj(či více)-rozměrných polí či Traversable objektů
* @param array $input Vstupní pole.
* @param mixed $outputKeys Jak mají být tvořeny indexy výstupního pole?
*
False = numericky indexovat od 0.
*
True = zachovat původní indexy.
*
Cokoliv jiného - použít takto pojmenovanou hodnotu z druhého rozměru
* @param mixed $outputValue Jak mají být tvořeny hodnoty výstupního pole?
*
True = zachovat původní položky
*
String nebo array = vybrat pouze takto pojmenovanou položku nebo položky.
*
False = původní index. Může být zadán i jako prvek pole, pak bude daný prvek mít index [key].
* @return mixed
*/
static function transform($input,$outputKeys,$outputValue) {
$input=self::arrayize($input);
$output=array();
foreach($input as $inputI=>$inputR) {
if (is_array($outputValue)) {
$novaPolozka=array();
foreach($outputValue as $ov) {
if ($ov===false) {
$novaPolozka["key"]=$inputI;
} else {
if (isset($inputR[$ov])) {
$novaPolozka[$ov]=$inputR[$ov];
} else {
$novaPolozka[$ov]=null;
}
}
}
} else {
if ($outputValue===true) {
$novaPolozka=$inputR;
} elseif ($outputValue===false) {
$novaPolozka=$inputI;
} elseif (isset($inputR[$outputValue])) {
$novaPolozka=$inputR[$outputValue];
} else {
$novaPolozka=null;
}
}
if ($outputKeys===false) {
$output[]=$novaPolozka;
} elseif ($outputKeys===true) {
$output[$inputI]=$novaPolozka;
} else {
if (isset($inputR[$outputKeys])) {
$output[$inputR[$outputKeys]]=$novaPolozka;
} else {
$output[]=$novaPolozka;
}
}
}
return $output;
}
/**
* Seřadí prvky v jednom poli dle klíčů podle pořadí hodnot v jiném poli
* @param array $dataArray
* @param array $keysArray
* @return null
*/
static function sortByExternalKeys($dataArray, $keysArray) {
$returnArray = array();
foreach($keysArray as $k) {
if (isset($dataArray[$k])) {
$returnArray[$k] = $dataArray[$k];
} else {
$returnArray[$k] = null;
}
}
return $returnArray;
}
/**
* Vybere všechny možné hodnoty z dvourozměrného asociativního pole či Traversable objektu.
* Funkce iteruje po prvním rozměru pole $array a ve druhém rozměru hledá $hodnota. Ve druhém rozměru
* mohou být jak pole, tak objekty.
* Vrátí všechny různé nalezené hodnoty (bez duplikátů).
* @param array $array
* @param string $hodnota Index nebo jméno hodnoty, který chceme získat
* @param array $ignoredValues Volitelně lze doplnit hodnoty, které mají být ignorovány (pro porovnávání se
* používá striktní === ekvivalence)
* @return array
*/
static function valuePicker($array, $hodnota, $ignoredValues = null) {
$vrat=array();
foreach($array as $a) {
if ((is_array($a) or ($a instanceof \ArrayAccess)) and isset($a[$hodnota])) {
$vrat[]=$a[$hodnota];
} elseif (is_object($a) and isset($a->$hodnota)) {
$vrat[]=$a->$hodnota;
}
}
$vrat=array_values(array_unique($vrat));
if ($ignoredValues) {
$ignoredValues = self::arrayize($ignoredValues);
foreach($vrat as $i=>$r) {
if (in_array($r, $ignoredValues, true)) unset($vrat[$i]);
}
$vrat = array_values($vrat);
}
return $vrat;
}
/**
* Ze zadaného pole vybere jen ty položky, které mají klíč udaný v druhém poli.
* @param array|\ArrayAccess $array Asociativní pole
* @param array $requiredKeys Obyčejné pole klíčů
* @return array
*/
static function filterByKeys($array, $requiredKeys) {
if (is_array($array)) {
return array_intersect_key($array, array_fill_keys($requiredKeys, true));
}
if ($array instanceof \ArrayAccess) {
$ret = array();
foreach ($requiredKeys as $k) {
if (isset($array[$k])) {
$ret[$k] = $array[$k];
}
}
return $ret;
}
throw new \InvalidArgumentException("Argument must be an array or object with ArrayAccess");
}
/**
* Z klasického dvojrozměrného pole udělá trojrozměrné pole, kde první index bude sdružovat řádku dle nějaké z hodnot.
* @param array $data
* @param string $groupBy Název políčka v $data, podle něhož se má sdružovat
* @param bool|string $orderByKey False (def.) = nechat, jak to přišlo pod ruku. True = seřadit dle sdružované hodnoty. String "desc" = sestupně.
* @return array
*/
static public function group($data,$groupBy,$orderByKey=false) {
$vrat=array();
foreach($data as $index=>$radek) {
if (!isset($radek[$groupBy])) {
$radek[$groupBy]="0";
}
if (!isset($vrat[$radek[$groupBy]])) {
$vrat[$radek[$groupBy]]=array();
}
$vrat[$radek[$groupBy]][$index]=$radek;
}
if ($orderByKey) {
ksort($vrat);
}
if ($orderByKey==="desc") {
$vrat=array_reverse($vrat);
}
return $vrat;
}
/**
* Zadané dvourozměrné pole nebo traversable objekt přeindexuje tak, že jeho jednotlivé indexy
* budou tvořeny určitým prvkem nebo public vlastností z každého prvku.
*
* Pokud některý z prvků vstupního pole neobsahuje $keyName, zachová se jeho původní index.
*
* @param array|\Traversable $input Vstupní pole/objekt
* @param string $keyName Podle čeho indexovat
* @return array
*/
static public function indexByKey($input, $keyName) {
if (!is_array($input) and !($input instanceof \Traversable)) {
throw new \InvalidArgumentException("Given argument must be an array or traversable object.");
}
$returnedArray = array();
foreach($input as $index => $f) {
if (is_array($f)) {
$key = array_key_exists($keyName, $f) ? $f[$keyName] : $index;
$returnedArray[$key] = $f;
} elseif (is_object($f)) {
$key = property_exists($f, $keyName) ? $f->$keyName : $index;
$returnedArray[$key] = $f;
} else {
if (!isset($returnedArray[$index])) {
$returnedArray[$index] = $f;
}
}
}
return $returnedArray;
}
/**
* Zruší z pole všechny výskyty určité hodnoty.
* @param array $dataArray
* @param mixed $valueToDelete Nesmí být null!
* @param bool $keysInsignificant True = přečíslovat vrácené pole, indexy nejsou podstatné. False = nechat původní indexy.
* @param bool $strict == nebo ===
* @return array Upravené $dataArray
*/
static public function deleteValue($dataArray, $valueToDelete, $keysInsignificant = true, $strict = false) {
if ($valueToDelete === null) throw new \InvalidArgumentException("\$valueToDelete cannot be null.");
$keys = array_keys($dataArray, $valueToDelete, $strict);
if ($keys) {
foreach($keys as $k) {
unset($dataArray[$k]);
}
if ($keysInsignificant) {
$dataArray = array_values($dataArray);
}
}
return $dataArray;
}
/**
* Zruší z jednoho pole všechny hodnoty, které se vyskytují ve druhém poli.
* Ve druhém poli musí jít o skalární typy, objekty nebo array povedou k chybě.
* @param array $dataArray
* @param array $arrayOfValuesToDelete
* @param bool $keysInsignificant True = přečíslovat vrácené pole, indexy nejsou podstatné. False = nechat původní indexy.
* @return array Upravené $dataArray
*/
static public function deleteValues($dataArray, $arrayOfValuesToDelete, $keysInsignificant = true) {
$arrayOfValuesToDelete = self::arrayize($arrayOfValuesToDelete);
$invertedDeletes = array_fill_keys($arrayOfValuesToDelete, true);
foreach ($dataArray as $i=>$r) {
if (isset($invertedDeletes[$r])) {
unset($dataArray[$i]);
}
}
if ($keysInsignificant) {
$dataArray = array_values($dataArray);
}
return $dataArray;
}
/**
* Obohatí $mainArray o nějaké prvky z $mixinArray. Obě pole by měla být dvourozměrná pole, kde
* první rozměr je ID a další rozměr je asociativní pole s nějakými vlastnostmi.
*
Data z $mainArray se považují za prioritnější a správnější, a pokud již příslušný prvek obsahují,
* nepřepíší se tím z $mixinArray.
* @param array $mainArray
* @param array $mixinArray
* @param bool|array|string $fields True = obohatit vším, co v $mixinArray je. Jinak string/array stringů.
* @param array $changeIndexes Do $mainField lze použít jiné indexy, než v originále. Sem zadej "překladovou tabulku" ve tvaru array([original_key] => new_key).
* Ve $fields používej již indexy po přejmenování.
* @return array Obohacené $mainArray
*/
static public function enrich($mainArray, $mixinArray, $fields=true, $changeIndexes = array()) {
if ($fields!==true) $fields=self::arrayize($fields);
foreach($mixinArray as $mixinId=>$mixinData) {
if (!isset($mainArray[$mixinId])) continue;
if ($changeIndexes) {
foreach($changeIndexes as $fromI=>$toI) {
if (isset($mixinData[$fromI])) {
$mixinData[$toI] = $mixinData[$fromI];
unset($mixinData[$fromI]);
} else {
$mixinData[$toI] = null;
}
}
}
if ($fields===true) {
$mainArray[$mixinId]+=$mixinData;
} else {
foreach($fields as $field) {
if (!isset($mainArray[$mixinId][$field])) {
if (isset($mixinData[$field])) {
$mainArray[$mixinId][$field]=$mixinData[$field];
} else {
$mainArray[$mixinId][$field]=null;
}
}
}
}
}
return $mainArray;
}
/**
* Konverze asociativního pole na objekt třídy stdClass
* @param array|Traversable $array
* @return \stdClass
*/
static function toObject($array) {
if (!is_array($array) and !($array instanceof \Traversable)) {
throw new \InvalidArgumentException("You must give me an array!");
}
$obj = new \stdClass();
foreach ($array as $i=>$r) {
$obj->$i = $r;
}
return $obj;
}
/**
* Z dvourozměrného pole, které bylo sgrupované podle nějaké hodnoty, udělá zpět jednorozměrné, s výčtem jednotlivých hodnot.
* Funguje pouze za předpokladu, že jednotlivé hodnoty jsou obyčejné skalární typy. Objekty nebo array třetího rozměru povede k chybě.
* @param array $array
* @return array
*/
static public function flatten($array) {
$out=array();
foreach($array as $i=>$subArray) {
foreach($subArray as $value) {
$out[$value]=true;
}
}
return array_keys($out);
}
/**
* Normalizuje hodnoty v poli do rozsahu <0-1>
* @param array $array
* @return array
*/
static public function normaliseValues($array) {
$array=self::arrayize($array);
if (!$array) return $array;
$minValue=min($array);
$maxValue=max($array);
if ($maxValue==$minValue) {
$minValue-=1;
}
foreach($array as $index=>$value) {
$array[$index]=($value-$minValue)/($maxValue-$minValue);
}
return $array;
}
/**
* Rekurzivně převede traversable objekt na obyčejné array.
* @param \Traversable $traversable
* @param int $depth Interní, pro kontorlu nekonečné rekurze
* @return array
* @throws \RuntimeException
*/
static function traversableToArray($traversable, $depth = 0) {
$vrat = array();
if ($depth > 10) throw new \RuntimeException("Recursion is too deep.");
if (!is_array($traversable) and !($traversable instanceof \Traversable)) {
throw new \InvalidArgumentException("\$traversable must be an array or Traversable object.");
}
foreach ($traversable as $i=>$r) {
if (is_array($r) or ($r instanceof \Traversable)) {
$vrat[$i] = self::traversableToArray($r, $depth + 1);
} else {
$vrat[$i] = $r;
}
}
return $vrat;
}
/**
* Pomocná funkce zjednodušující práci s různými číselníky definovanými jako array v PHP. Umožňuje buď "lidsky" zformátovat jeden vybraný prvek z číselníku, nebo vrátit celé array hodnot.
* @param array $data Celé array se všemi položkami ve tvaru [index]=>$value
* @param string|int|bool $index False = vrať array se všemi. Jinak zadej index jedné konkrétní položky.
* @param string|bool $pattern False = vrať tak, jak to je v $data. String = naformátuj. Entity %index%, %value%, %i%. %i% označuje pořadí a vyplňuje se jen je-li $index false a je 0-based.
* @param string|int $default Pokud by snad v $data nebyla položka s indexem $indexPolozky, hledej index $default, pokud není, vrať $default.
* @param bool $reverse dej True, má-li se vrátit v opačném pořadí.
* @return array|string Array pokud $indexPolozky je false, jinak string.
*/
static function enumItem ($data,$index,$pattern=false,$default=0,$reverse=false) {
if ($index!==false) {
if (!isset($data[$index])) {
$index=$default;
if (!isset($data[$index])) return $default;
}
if ($pattern===false) return $data[$index];
return self::enumItemPattern($pattern,$index,$data[$index],"");
}
if ($pattern===false) {
if ($reverse) return array_reverse($data,true);
return $data;
}
$vrat=array();
$i=0;
foreach($data as $di=>$dr) {
$vrat[$di]=self::enumItemPattern($pattern,$di,$dr,$i);
$i++;
}
if ($reverse) return array_reverse($vrat,true);
return $vrat;
}
/**
* @ignore
*/
protected static function enumItemPattern($pattern,$index,$value,$i) {
return str_replace(
array("%index%","%i%","%value%"),
array($index,$i,$value),
$pattern
);
}
/**
* Porovná, zda jsou hodnoty ve dvou polích stejné. Nezáleží na indexech ani na pořadí prvků v poli.
* @param array $array1
* @param array $array2
* @param bool $strict Používat ===
* @return boolean True = stejné. False = rozdílné.
*/
static function compareValues($array1, $array2, $strict = false) {
if (count($array1) != count($array2)) return false;
$array1 = array_values($array1);
$array2 = array_values($array2);
sort($array1, SORT_STRING);
sort($array2, SORT_STRING);
foreach($array1 as $i=>$r) {
if ($array2[$i] != $r) return false;
if ($strict and $array2[$i] !== $r) return false;
}
return true;
}
/**
* Rekurzivní změna kódování libovolného typu proměnné (array, string, atd., kromě objektů).
* @param string $from Vstupní kódování
* @param string $to Výstupní kódování
* @param mixed $array Co překódovat
* @param bool $keys Mají se iconvovat i klíče? Def. false.
* @param int $checkDepth Tento parametr ignoruj, používá se jako pojistka proti nekonečné rekurzi.
* @return mixed
*/
static function iconv($from, $to, $array, $keys=false, $checkDepth = 0) {
if (is_object($array)) {
return $array;
}
if (!is_array($array)) {
if (is_string($array)) {
return iconv($from,$to,$array);
} else {
return $array;
}
}
if ($checkDepth>20) return $array;
$vrat=array();
foreach($array as $i=>$r) {
if ($keys) {
$i=iconv($from,$to,$i);
}
$vrat[$i]=self::iconv($from,$to,$r,$keys,$checkDepth+1);
}
return $vrat;
}
/**
* Vytvoří kartézský součin.
*
* $input = array(
* "barva" => array("red", "green"),
* "size" => array("small", "big")
* );
*
* $output = array(
* [0] => array("barva" => "red", "size" => "small"),
* [1] => array("barva" => "green", "size" => "small"),
* [2] => array("barva" => "red", "size" => "big"),
* [3] => array("barva" => "green", "size" => "big")
* );
*
*
* @param array $input
* @return array
* @see http://stackoverflow.com/questions/6311779/finding-cartesian-product-with-php-associative-arrays
*/
static function cartesian($input) {
$input = array_filter($input);
$result = array(array());
foreach ($input as $key => $values) {
$append = array();
foreach($result as $product) {
foreach($values as $item) {
$product[$key] = $item;
$append[] = $product;
}
}
$result = $append;
}
return $result;
}
/**
* Zjistí, zda má pole pouze číselné indexy
* @param array $array
* @return bool
* @author Michael Pavlista
*/
public static function isNumeric(array $array) {
return empty($array) ? TRUE : is_numeric(implode('', array_keys($array)));
}
/**
* Zjistí, zda je pole asociativní
* @param array $array
* @return bool
* @author Michael Pavlista
*/
public static function isAssoc(array $array) {
return empty($array) ? TRUE : !self::isNumeric($array);
}
/**
* @param array $old
* @param array $new
* @return array
*
* @author Paul's Simple Diff Algorithm v 0.1
* (C) Paul Butler 2007
* May be used and distributed under the zlib/libpng license.
*/
public static function diff($old, $new) {
$matrix = array();
$maxlen = 0;
foreach($old as $oindex => $ovalue){
$nkeys = array_keys($new, $ovalue);
foreach($nkeys as $nindex){
$matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
$matrix[$oindex - 1][$nindex - 1] + 1 : 1;
if($matrix[$oindex][$nindex] > $maxlen){
$maxlen = $matrix[$oindex][$nindex];
$omax = $oindex + 1 - $maxlen;
$nmax = $nindex + 1 - $maxlen;
}
}
}
if($maxlen == 0) return array(array('d'=>$old, 'i'=>$new));
return array_merge(
self::diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
array_slice($new, $nmax, $maxlen),
self::diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)));
}
}
}
// vendor/ondrakoupil/tools/src/Files.php
namespace OndraKoupil\Tools {
use OndraKoupil\Tools\Exceptions\FileException;
use OndraKoupil\Tools\Exceptions\FileAccessException;
/**
* Pár vylepšených nsátrojů pro práci se soubory
*/
class Files {
const LOWERCASE = "L";
const UPPERCASE = "U";
/**
* Vrací jen jméno souboru
*
* `/var/www/vhosts/somefile.txt` => `somefile.txt`
*
* @param string $in
* @return string
*/
static function filename($in) {
return basename($in);
}
/**
* Přípona souboru
*
* `/var/www/vhosts/somefile.txt` => `txt`
* @param string $in
* @param string $case self::LOWERCASE nebo self::UPPERCASE. Cokoliv jiného = neměnit velikost přípony.
* @return string
*/
static function extension($in,$case=false) {
$name=self::filename($in);
if (preg_match('~\.(\w{1,10})\s*$~',$name,$parts)) {
if (!$case) return $parts[1];
if (strtoupper($case)==self::LOWERCASE) return Strings::lower($parts[1]);
if (strtoupper($case)==self::UPPERCASE) return Strings::upper($parts[1]);
return $parts[1];
}
return "";
}
/**
* Jméno souboru, ale bez přípony.
*
* `/var/www/vhosts/somefile.txt` => `somefile`
* @param string $filename
* @return string
*/
static function filenameWithoutExtension($filename) {
$filename=self::filename($filename);
if (preg_match('~(.*)\.(\w{1,10})$~',$filename,$parts)) {
return $parts[1];
}
return $filename;
}
/**
* Vrátí jméno souboru, jako kdyby byl přejmenován, ale ve stejném adresáři
*
* `/var/www/vhosts/somefile.txt` => `/var/www/vhosts/anotherfile.txt`
*
* @param string $path Původní cesta k souboru
* @param string $to Nové jméno souboru
* @return string
*/
static function changedFilename($path, $newName) {
return self::dir($path)."/".$newName;
}
/**
* Jen cesta k adresáři.
*
* `/var/www/vhosts/somefile.txt` => `/var/www/vhosts`
*
* @param string $in
* @param bool $real True = použít realpath()
* @return string Pokud je $real==true a $in neexistuje, vrací empty string
*/
static function dir($in,$real=false) {
if ($real) {
$in=realpath($in);
if ($in and is_dir($in)) $in.="/file";
}
return dirname($in);
}
/**
* Přidá do jména souboru něco na konec, před příponu.
*
* `/var/www/vhosts/somefile.txt` => `/var/www/vhosts/somefile-affix.txt`
*
* @param string $filename
* @param string $addedString
* @param bool $withPath Vracet i s cestou? Anebo jen jméno souboru?
* @return string
*/
static function addBeforeExtension($filename,$addedString,$withPath=true) {
if ($withPath) {
$dir=self::dir($filename)."/";
} else {
$dir="";
}
if (!$dir or $dir=="./") $dir="";
$filenameWithoutExtension=self::filenameWithoutExtension($filename);
$extension=self::extension($filename);
if ($extension) $addExtension=".".$extension;
else $addExtension="";
return $dir.$filenameWithoutExtension.$addedString.$addExtension;
}
/**
* Nastaví práva, aby $filename bylo zapisovatelné, ať už je to soubor nebo adresář
* @param string $filename
* @return bool Dle úspěchu
* @throws Exceptions\FileException Pokud zadaná cesta není
* @throws Exceptions\FileAccessException Pokud změna selže
*/
static function perms($filename) {
if (!file_exists($filename)) {
throw new FileException("Missing: $filename");
}
if (!is_writeable($filename)) {
throw new FileException("Not writable: $filename");
}
if (is_dir($filename)) {
$ok=chmod($filename,0777);
} else {
$ok=chmod($filename,0666);
}
if (!$ok) {
throw new FileAccessException("Could not chmod $filename");
}
return $ok;
}
/**
* Přesune soubor i s adresářovou strukturou zpod jednoho do jiného.
* @param string $file Cílový soubor
* @param string $from Adresář, který brát jako základ
* @param string $to Clový adresář
* @param bool $copy True (default) = kopírovat, false = přesunout
* @return string Cesta k novému souboru
* @throws FileException Když $file není nalezeno nebo když selže kopírování
* @throws \InvalidArgumentException Když $file není umístěno v $from
*/
static function rebaseFile($file, $from, $to, $copy=false) {
if (!file_exists($file)) {
throw new FileException("Not found: $file");
}
if (!Strings::startsWith($file, $from)) {
throw new \InvalidArgumentException("File $file is not in directory $from");
}
$newPath=$to."/".Strings::substring($file, Strings::length($from));
$newDir=self::dir($newPath);
self::createDirectories($newDir);
if ($copy) {
$ok=copy($file,$newPath);
} else {
$ok=rename($file, $newPath);
}
if (!$ok) {
throw new FileException("Failed copying to $newPath");
}
self::perms($newPath);
return $newPath;
}
/**
* Vrátí cestu k souboru, jako kdyby byl umístěn do jiného adresáře i s cestou k sobě.
* @param string $file Jméno souboru
* @param string $from Cesta k němu
* @param string $to Adresář, kam ho chceš přesunout
* @return string
* @throws \InvalidArgumentException
*/
static function rebasedFilename($file,$from,$to) {
if (!Strings::startsWith($file, $from)) {
throw new \InvalidArgumentException("File $file is not in directory $from");
}
$secondPart=Strings::substring($file, Strings::length($from));
if ($secondPart[0]=="/") $secondPart=substr($secondPart,1);
$newPath=$to."/".$secondPart;
return $newPath;
}
/**
* Ověří, zda soubor je v zadaném adresáři.
* @param string $file
* @param string $dir
* @return bool
*/
static function isFileInDir($file,$dir) {
if (!Strings::endsWith($dir, "/")) $dir.="/";
return Strings::startsWith($file, $dir);
}
/**
* Vytvoří bezpečné jméno pro soubor
* @param string $filename
* @param array $unsafeExtensions
* @param string $safeExtension
* @return string
*/
static function safeName($filename,$unsafeExtensions=null,$safeExtension="txt") {
if ($unsafeExtensions===null) $unsafeExtensions=array("php","phtml","inc","php3","php4","php5");
if ($filename[0] == '.') {
$filename = substr($filename, 1);
}
$filename = str_replace(DIRECTORY_SEPARATOR, '-', $filename);
$extension=self::extension($filename, "l");
if (in_array($extension, $unsafeExtensions)) {
$extension=$safeExtension;
}
$name=self::filenameWithoutExtension($filename);
$name=Strings::safe($name, false);
if (preg_match('~^(.*)[-_]+$~',$name,$partsName)) {
$name=$partsName[1];
}
if (preg_match('~^[-_]+(.*)$~',$name,$partsName)) {
$name=$partsName[1];
}
$ret=$name;
if ($extension) $ret.=".".$extension;
return $ret;
}
/**
* Vytvoří soubor, pokud neexistuje, a udělá ho zapisovatelným
* @param string $filename
* @param bool $createDirectoriesIfNeeded
* @param string $content Pokud se má vytvořit nový soubor, naplní se tímto obsahem
* @return string Jméno vytvořného souboru (cesta k němu)
* @throws \InvalidArgumentException
* @throws FileException
* @throws FileAccessException
*/
static function create($filename, $createDirectoriesIfNeeded=true, $content="") {
if (!$filename) {
throw new \InvalidArgumentException("Completely missing argument!");
}
if (file_exists($filename) and is_dir($filename)) {
throw new FileException("$filename is directory!");
}
if (file_exists($filename)) {
self::perms($filename);
return $filename;
}
if ($createDirectoriesIfNeeded) self::createDirectories(self::dir($filename, false));
$ok=@touch($filename);
if (!$ok) {
throw new FileAccessException("Could not create file $filename");
}
self::perms($filename);
if ($content) {
file_put_contents($filename, $content);
}
return $filename;
}
/**
* Vrací práva k určitému souboru či afdresáři jako třímístný string.
* @param string $path
* @return string Např. "644" nebo "777"
* @throws FileException
*/
static function getPerms($path) {
//http://us3.php.net/manual/en/function.fileperms.php example #1
if (!file_exists($path)) {
throw new FileException("File '$path' is missing");
}
return substr(sprintf('%o', fileperms($path)), -3);
}
/**
* Pokusí se vytvořit strukturu adresářů v zadané cestě.
* @param string $path
* @return string Vytvořená cesta
* @throws FileException Když už takto pojmenovaný soubor existuje a jde o obyčejný soubor nebo když vytváření selže.
*/
static function createDirectories($path) {
if (!$path) throw new \InvalidArgumentException("\$path can not be empty.");
/*
$parts=explode("/",$path);
$pathPart="";
foreach($parts as $i=>$p) {
if ($i) $pathPart.="/";
$pathPart.=$p;
if ($pathPart) {
if (@file_exists($pathPart) and !is_dir($pathPart)) {
throw new FileException("\"$pathPart\" is a regular file!");
}
if (!(@file_exists($pathPart))) {
self::mkdir($pathPart,false);
}
}
}
return $pathPart;
*
*/
if (file_exists($path)) {
if (is_dir($path)) {
return $path;
}
throw new FileException("\"$path\" is a regular file!");
}
$ret = @mkdir($path, 0777, true);
if (!$ret) {
throw new FileException("Directory \"$path\ could not be created.");
}
return $path;
}
/**
* Vytvoří adresář, pokud neexistuje, a udělá ho obecně zapisovatelným
* @param string $filename
* @param bool $createDirectoriesIfNeeded
* @return string Jméno vytvořneého adresáře
* @throws \InvalidArgumentException
* @throws FileException
* @throws FileAccessException
*/
static function mkdir($filename, $createDirectoriesIfNeeded=true) {
if (!$filename) {
throw new \InvalidArgumentException("Completely missing argument!");
}
if (file_exists($filename) and !is_dir($filename)) {
throw new FileException("$filename is not a directory!");
}
if (file_exists($filename)) {
self::perms($filename);
return $filename;
}
if ($createDirectoriesIfNeeded) {
self::createDirectories($filename);
} else {
$ok=@mkdir($filename);
if (!$ok) {
throw new FileAccessException("Could not create directory $filename");
}
}
self::perms($filename);
return $filename;
}
/**
* Najde volné pojmenování pro soubor v určitém adresáři tak, aby bylo jméno volné.
*
Pokus je obsazené, pokouší se přidávat pomlčku a čísla až do 99, pak přejde na uniqid():
*
freeFilename("/files/somewhere","abc.txt");
*
Bude zkoušet: abc.txt, abc-2.txt, abc-3.txt atd.
*
* @param string $path Adresář
* @param string $filename Požadované jméno souboru
* @return string Jméno souboru (ne celá cesta, jen jméno souboru)
* @throws AccessException
*/
static function freeFilename($path,$filename) {
if (!file_exists($path) or !is_dir($path) or !is_writable($path)) {
throw new FileAccessException("Directory $path is missing or not writeble.");
}
if (!file_exists($path."/".$filename)) {
return $filename;
}
$maxTries=99;
$filenamePart=self::filenameWithoutExtension($filename);
$extension=self::extension($filename);
$addExtension=$extension?".$extension":"";
for ( $addedIndex=2 ; $addedIndex<$maxTries ; $addedIndex++ ) {
if (!file_exists($path."/".$filenamePart."-".$addedIndex.$addExtension)) {
break;
}
}
if ($addedIndex==$maxTries) {
return $filenamePart."-".uniqid("").$addExtension;
}
return $filenamePart."-".$addedIndex.$addExtension;
}
/**
* Vymaže obsah adresáře
* @param string $dir
* @return boolean Dle úspěchu
* @throws \InvalidArgumentException
*/
static function purgeDir($dir) {
if (!is_dir($dir)) {
throw new \InvalidArgumentException("$dir is not directory.");
}
$content=glob($dir."/*");
if ($content) {
foreach($content as $sub) {
if ($sub=="." or $sub=="..") continue;
self::remove($sub);
}
}
return true;
}
/**
* Smaže adresář a rekurzivně i jeho obsah
* @param string $dir
* @param int $depthLock Interní, ochrana proti nekonečné rekurzi
* @return boolean Dle úspěchu
* @throws \RuntimeException
* @throws \InvalidArgumentException
* @throws FileAccessException
*/
static function removeDir($dir,$depthLock=0) {
if ($depthLock > 15) {
throw new \RuntimeException("Recursion too deep at $dir");
}
if (!file_exists($dir)) {
return true;
}
if (!is_dir($dir)) {
throw new \InvalidArgumentException("$dir is not directory.");
}
$content=glob($dir."/*");
if ($content) {
foreach($content as $sub) {
if ($sub=="." or $sub=="..") continue;
if (is_dir($sub)) {
self::removeDir($sub,$depthLock+1);
} else {
if (is_writable($sub)) {
unlink($sub);
} else {
throw new FileAccessException("Could not delete file $sub");
}
}
}
}
$ok=rmdir($dir);
if (!$ok) {
throw new FileAccessException("Could not remove dir $dir");
}
return true;
}
/**
* Smaže $path, ať již je to adresář nebo soubor
* @param string $path
* @param bool $onlyFiles Zakáže mazání adresářů
* @return boolean Dle úspěchu
* @throws FileAccessException
* @throws FileException
*/
static function remove($path, $onlyFiles=false) {
if (!file_exists($path)) {
return true;
}
if (is_dir($path)) {
if ($onlyFiles) throw new FileException("$path is a directory!");
return self::removeDir($path);
}
else {
$ok=unlink($path);
if (!$ok) throw new FileAccessException("Could not delete file $path");
}
return true;
}
/**
* Stažení vzdáleného souboru pomocí cURL
* @param $url URL vzdáleného souboru
* @param $path Kam stažený soubor uložit?
* @param bool $stream
*/
public static function downloadFile($url, $path, $stream = TRUE) {
$curl = curl_init($url);
if(!$stream) {
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
file_put_contents($path, curl_exec($curl));
}
else {
$fp = fopen($path, 'w');
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_exec($curl);
fclose($fp);
}
curl_close($curl);
}
/**
* Vrací maximální nahratelnou velikost souboru.
*
* Bere menší z hodnot post_max_size a upload_max_filesize a převede je na obyčejné číslo.
* @return int Bytes
*/
static function maxUploadFileSize() {
$file_max = Strings::parsePhpNumber(ini_get("post_max_size"));
$post_max = Strings::parsePhpNumber(ini_get("upload_max_filesize"));
$php_max = min($file_max,$post_max);
return $php_max;
}
}
}
// vendor/ondrakoupil/tools/src/Exceptions/FileException.php
namespace OndraKoupil\Tools {
class FileException extends \RuntimeException {
}
}
// vendor/ondrakoupil/tools/src/Exceptions/FileAccessException.php
namespace OndraKoupil\Tools {
class FileAccessException extends \RuntimeException {
}
}