allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,37 @@
<?php
namespace FedaPay;
/**
* Class Account
*
* @property int $id
* @property string $name
* @property string $timezone
* @property string $country
* @property string $verify
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Account extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
public static function light($params = [], $headers = [])
{
$path = static::resourcePath('light');
$className = static::className();
list($response, $opts) = static::_staticRequest('get', $path, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->$className;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace FedaPay;
/**
* Class ApiKey
*
* @property int $id
* @property string $public_key
* @property string $private_key
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class ApiKey extends Resource
{
}

View File

@@ -0,0 +1,25 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait All
*/
trait All
{
/**
* Static method to retrive a list of resources
* @param array $params
* @param array $headers
*
* @return array FedaPay\FedaPayObject
*/
public static function all($params = [], $headers = [])
{
self::_validateParams($params);
$path = static::classPath();
list($response, $opts) = static::_staticRequest('get', $path, $params, $headers);
return \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Create
*/
trait Create
{
/**
* Static method to create a resources
* @param array $params
* @param array $headers
*
* @return Resource
*/
public static function create($params = [], $headers = [])
{
self::_validateParams($params);
$path = static::classPath();
$className = static::className();
list($response, $opts) = static::_staticRequest('post', $path, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->$className;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait CreateInBatch
*/
trait CreateInBatch
{
/**
* Create resources in batch
*
* @param array $params
* @param array $headers
* @return FedaPay\FedaPayObject
*/
public static function createInBatch($params = [], $headers = [])
{
$path = static::resourcePath('batch');
$className = static::className();
list($response, $opts) = static::_staticRequest('post', $path, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
$data = "{$className}_batch";
return $object->$data;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Update
*/
trait Delete
{
/**
* Send a detele request
* @param array $headers
*/
public function delete($headers = [])
{
$path = $this->instanceUrl();
static::_staticRequest('delete', $path, [], $headers);
return $this;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Request
*/
trait Request
{
/**
* Validate request params
* @param array $params
* @throws Error\InvalidRequest
*/
protected static function _validateParams($params = null)
{
if ($params && !is_array($params)) {
$message = 'You must pass an array as the first argument to FedaPay API '
. 'method calls. (HINT: an example call to create a customer '
. "would be: \"FedaPay\\Customer::create(array('firstname' => toto, "
. "'lastname' => 'zoro', 'email' => 'admin@gmail.com', 'phone' => '66666666'))\")";
throw new FedaPay\Error\InvalidRequest($message);
}
}
/**
* Static method to send request
* @param string $method
* @param string $path
* @param array $params
* @param array $headers
*
* @return array
*/
protected static function _staticRequest($method, $path, $params = [], $headers = [])
{
$requestor = self::getRequestor();
$response = $requestor->request($method, $path, $params, $headers);
$options = [
'apiVersion' => \FedaPay\FedaPay::getApiVersion(),
'environment' => \FedaPay\FedaPay::getEnvironment()
];
return [$response, $options];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Retrieve
*/
trait Retrieve
{
/**
* Static method to retrive a resource
* @param mixed $id
* @return FedaPay\FedaPayObject
*/
public static function retrieve($id, $params = [], $headers = [])
{
$url = static::resourcePath($id);
$className = static::className();
list($response, $opts) = static::_staticRequest('get', $url, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->$className;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Save
*/
trait Save
{
/**
* Update the resource
* @param array $headers the request headers
*
* @return Resource the updated API resource
*/
public function save($headers = [])
{
$params = $this->serializeParameters();
$className = static::className();
$path = $this->instanceUrl();
list($response, $opts) = static::_staticRequest('put', $path, $params, $headers);
$klass = $opts['apiVersion'] . '/' . $className;
$json = $response[$klass];
$this->refreshFrom($json, $opts);
return $this;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Search
*/
trait Search
{
/**
* Static method to search resources
* @param array $params
* @param array $headers
*
* @return array FedaPay\FedaPayObject
*/
public static function search($q = '*', $params = [], $headers = [])
{
$params['search'] = $q;
self::_validateParams($params);
$path = static::resourcePath('search');
list($response, $opts) = static::_staticRequest('get', $path, $params, $headers);
return \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace FedaPay\ApiOperations;
/**
* trait Update
*/
trait Update
{
/**
* Static method to update a resource
* @param string $id The ID of the API resource to update.
* @param array $params The request params
* @param array $headers the request headers
*
* @return Resource the updated API resource
*/
public static function update($id, $params = [], $headers = [])
{
self::_validateParams($params);
$path = static::resourcePath($id);
$className = static::className();
list($response, $opts) = static::_staticRequest('put', $path, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->$className;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace FedaPay;
/**
* Class Balance
*
* @property int $id
* @property int $currency_id
* @property int $account_id
* @property int $amount
* @property string $mode
* @property string $deleted_at
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Balance extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace FedaPay;
/**
* Class Currency
*
* @property int $id
* @property string $name
* @property string $iso
* @property int $code
* @property string $prefix
* @property string $suffix
* @property string $div
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Currency extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace FedaPay;
/**
* Class Customer
*
* @property int $id
* @property string $firstname
* @property string $lastname
* @property string $email
* @property string $phone
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Customer extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace FedaPay\Error;
/**
* Class ApiConnection
*
* @package FedaPay\Error
*/
class ApiConnection extends Base
{
}

View File

@@ -0,0 +1,91 @@
<?php
namespace FedaPay\Error;
use Exception;
/**
* Class Base
*
* @package FedaPay\Error
*/
class Base extends Exception
{
/**
* @var int
*/
private $httpStatus;
/**
* @var string
*/
private $httpBody;
/**
* @var json
*/
private $jsonBody;
/**
* @var string
*/
private $errorMessage;
/**
* @var Array
*/
private $errors;
public function __construct(
$message,
$httpStatus = null,
$httpBody = null,
$jsonBody = null,
$httpHeaders = null
) {
parent::__construct($message);
$this->httpStatus = $httpStatus;
$this->httpBody = $httpBody;
$this->jsonBody = $jsonBody;
$this->httpHeaders = $httpHeaders;
$this->errorMessage = isset($jsonBody["message"]) ? $jsonBody["message"]: null;
$this->errors = isset($jsonBody["errors"]) ? $jsonBody["errors"]: null;
}
public function getHttpStatus()
{
return $this->httpStatus;
}
public function getHttpBody()
{
return $this->httpBody;
}
public function getJsonBody()
{
return $this->jsonBody;
}
public function getHttpHeaders()
{
return $this->httpHeaders;
}
public function getErrorMessage()
{
return $this->errorMessage;
}
public function getErrors()
{
return $this->errors;
}
public function hasErrors()
{
return !empty($this->errors);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace FedaPay\Error;
/**
* Class InvalidRequest
*
* @package FedaPay\Error
*/
class InvalidRequest extends Base
{
}

View File

@@ -0,0 +1,27 @@
<?php
namespace FedaPay\Error;
/**
* Class SignatureVerification
*
* @package FedaPay\Error
*/
class SignatureVerification extends Base
{
private $sigHeader;
public function __construct(
$message,
$sigHeader,
$httpBody = null
) {
parent::__construct($message, null, $httpBody, null, null);
$this->sigHeader = $sigHeader;
}
public function getSigHeader()
{
return $this->sigHeader;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace FedaPay;
/**
* Class Event
*
* @property int $id
* @property string $type
* @property string $entity
* @property int $object_id
* @property int $account_id
* @property string $object
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Event extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
}

View File

@@ -0,0 +1,289 @@
<?php
namespace FedaPay;
/**
* Class FedaPay
*
* @package FedaPay
*/
class FedaPay
{
const VERSION = '0.1.7';
// @var string The FedaPay API key to be used for requests.
protected static $apiKey = null;
// @var string The FedaPay API base to be used for requests.
protected static $apiBase = null;
// @var string|null The FedaPay token to be used for oauth requests.
protected static $token = null;
// @var string|null The account ID for connected accounts requests.
public static $accountId = null;
// @var string|null The Oauth client Id.
public static $oauthClientId = null;
// @var string|null The Oauth client Secret.
public static $oauthClientSecret = null;
// @var string The environment for the FedaPay API.
protected static $environment = 'sandbox';
// @var string The environment for the FedaPay API.
protected static $locale = null;
// @var string The api version.
protected static $apiVersion = 'v1';
// @var bool verify ssl certs.
protected static $verifySslCerts = true;
// @var string|null the ca bundle path.
protected static $caBundlePath = null;
// @var int Maximum number of request retries
public static $maxNetworkRetries = 0;
// @var float Maximum delay between retries, in seconds
private static $maxNetworkRetryDelay = 2.0;
// @var float Initial delay between retries, in seconds
private static $initialNetworkRetryDelay = 0.5;
/**
* @return string The API key used for requests.
*/
public static function getApiKey()
{
return self::$apiKey;
}
/**
* Sets the API key to be used for requests.
*
* @param string $apiKey
* @return void
*/
public static function setApiKey($apiKey)
{
self::$apiKey = $apiKey;
self::$token = null;
}
/**
* @return string The API base used for requests.
*/
public static function getApiBase()
{
return self::$apiBase;
}
/**
* Sets the API base to be used for requests.
*
* @param string $apiBase
* @return void
*/
public static function setApiBase($apiBase)
{
self::$apiBase = $apiBase;
}
/**
* @return string The token used for requests.
*/
public static function getToken()
{
return self::$token;
}
/**
* Sets the token to be used for requests.
*
* @param string $token
* @return void
*/
public static function setToken($token)
{
self::$token = $token;
self::$apiKey = null;
}
/**
* @return string The account id used for connected account.
*/
public static function getAccountId()
{
return self::$accountId;
}
/**
* Sets the account id to be used for connected account.
*
* @param string $token
* @return void
*/
public static function setAccountId($accountId)
{
self::$accountId = $accountId;
}
/**
* @return string The Oauth client id.
*/
public static function getOauthClientId()
{
return self::$oauthClientId;
}
/**
* Sets the Oauth client id.
*
* @param string $oauthClientId
* @return void
*/
public static function setOauthClientId($oauthClientId)
{
self::$oauthClientId = $oauthClientId;
}
/**
* @return string The Oauth client secret.
*/
public static function getOauthClientSecret()
{
return self::$oauthClientSecret;
}
/**
* Sets the Oauth client id.
*
* @param string $oauthClientId
* @return void
*/
public static function setOauthClientSecret($oauthClientSecret)
{
self::$oauthClientSecret = $oauthClientSecret;
}
/**
* @return string The API version used for requests.
*/
public static function getApiVersion()
{
return self::$apiVersion;
}
/**
* @param string $apiVersion The API version.
* @return void
*/
public static function setApiVersion($apiVersion)
{
self::$apiVersion = $apiVersion;
}
/**
* @return bool Determine if the request should verify SSL certificate.
*/
public static function getVerifySslCerts()
{
return self::$verifySslCerts;
}
/**
* @param bool $verify The verify ssl certificates value.
* @return void
*/
public static function setVerifySslCerts($verify)
{
self::$verifySslCerts = $verify;
}
/**
* @return string Return the path of the certificate.
*/
public static function getCaBundlePath()
{
if (!self::$caBundlePath) {
self::$caBundlePath = dirname(__FILE__) . '/../data/ca-certificates.crt';
}
return self::$caBundlePath;
}
/**
* @param string $path The path of the certificate.
* @return void
*/
public static function setCaBundlePath($path)
{
self::$caBundlePath = $path;
}
/**
* @return string | null The FedaPay environment
*/
public static function getEnvironment()
{
return self::$environment;
}
/**
* @param string $environment The API environment.
* @return void
*/
public static function setEnvironment($environment)
{
self::$environment = $environment;
}
/**
* @return string | null The FedaPay locale
*/
public static function getLocale()
{
return self::$locale;
}
/**
* @param string $locale The API locale.
* @return void
*/
public static function setLocale($locale)
{
self::$locale = $locale;
}
/**
* @return int Maximum number of request retries
*/
public static function getMaxNetworkRetries()
{
return self::$maxNetworkRetries;
}
/**
* @param int $maxNetworkRetries Maximum number of request retries
*/
public static function setMaxNetworkRetries($maxNetworkRetries)
{
self::$maxNetworkRetries = $maxNetworkRetries;
}
/**
* @return float Maximum delay between retries, in seconds
*/
public static function getMaxNetworkRetryDelay()
{
return self::$maxNetworkRetryDelay;
}
/**
* @return float Initial delay between retries, in seconds
*/
public static function getInitialNetworkRetryDelay()
{
return self::$initialNetworkRetryDelay;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace FedaPay;
use FedaPay\Util\Util;
/**
* Class FedaPayObject
*
* @package FedaPay
*/
class FedaPayObject implements \ArrayAccess, \JsonSerializable
{
protected $_values;
public function __construct($id = null, $opts = null)
{
$this->_values = [];
if (is_array($id)) {
$this->refreshFrom($id, $opts);
} elseif ($id !== null) {
$this->id = $id;
}
}
// Standard accessor magic methods
public function __set($k, $v)
{
if ($v === '') {
throw new \InvalidArgumentException(
'You cannot set \''.$k.'\'to an empty string. '
.'We interpret empty strings as NULL in requests. '
.'You may set obj->'.$k.' = NULL to delete the property'
);
}
$this->_values[$k] = $v;
}
public function &__get($k)
{
// function should return a reference, using $nullval to return a reference to null
$nullval = null;
if (!empty($this->_values) && array_key_exists($k, $this->_values)) {
return $this->_values[$k];
} else {
$class = get_class($this);
error_log("FedaPay Notice: Undefined property of $class instance: $k");
return $nullval;
}
}
public function __isset($k)
{
return isset($this->_values[$k]);
}
public function __unset($k)
{
unset($this->_values[$k]);
}
// Magic method for var_dump output. Only works with PHP >= 5.6
public function __debugInfo()
{
return $this->_values;
}
// ArrayAccess methods
#[\ReturnTypeWillChange]
public function offsetSet($k, $v)
{
$this->$k = $v;
}
#[\ReturnTypeWillChange]
public function offsetExists($k)
{
return array_key_exists($k, $this->_values);
}
#[\ReturnTypeWillChange]
public function offsetUnset($k)
{
unset($this->$k);
}
#[\ReturnTypeWillChange]
public function offsetGet($k)
{
return array_key_exists($k, $this->_values) ? $this->_values[$k] : null;
}
public function keys()
{
return array_keys($this->_values);
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->_values;
}
public function __toJSON()
{
return json_encode($this->__toArray(true), JSON_PRETTY_PRINT);
}
public function __toString()
{
$class = get_class($this);
return $class . ' JSON: ' . $this->__toJSON();
}
public function __toArray($recursive = false)
{
if ($recursive) {
return Util::convertFedaPayObjectToArray($this->_values);
} else {
return $this->_values;
}
}
public function serializeParameters()
{
$params = [];
foreach ($this->_values as $key => $value) {
if ($key === 'id') {
continue;
}
if ($value instanceof FedaPayObject) {
$serialized = $value->serializeParameters();
if ($serialized) {
$params[$key] = $serialized;
}
} else {
$params[$key] = $value;
}
}
return $params;
}
public function refreshFrom($values, $opts)
{
if (!is_null($values)) {
if ($values instanceof FedaPayObject) {
$values = $values->__toArray(true);
}
foreach ($values as $k => $value) {
if (is_array($value)) {
$k = Util::stripApiVersion($k, $opts);
$this->_values[$k] = Util::arrayToFedaPayObject($value, $opts);
} else {
$this->_values[$k] = $value;
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace FedaPay\HttpClient;
interface ClientInterface
{
/**
* @param string $method The HTTP method being used
* @param string $absUrl The URL being requested, including domain and protocol
* @param array $headers Headers to be used in the request (full strings, not KV pairs)
* @param array $params KV pairs for parameters. Can be nested for arrays and hashes
*
* @throws \FedaPay\Error\InvalidRequest
* @throws \FedaPay\Error\ApiConnection
* @return array An array whose first element is raw request body, second
* element is HTTP status code and third array of HTTP headers.
*/
public function request($method, $absUrl, $headers, $params);
}

View File

@@ -0,0 +1,344 @@
<?php
namespace FedaPay\HttpClient;
use FedaPay\FedaPay;
use FedaPay\Error;
use FedaPay\Util;
// cURL constants are not defined in PHP < 5.5
// @codingStandardsIgnoreStart
// PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
// constants do not abide by those rules.
// Note the values 1 and 6 come from their position in the enum that
// defines them in cURL's source code.
if (!defined('CURL_SSLVERSION_TLSv1')) {
define('CURL_SSLVERSION_TLSv1', 1);
}
if (!defined('CURL_SSLVERSION_TLSv1_2')) {
define('CURL_SSLVERSION_TLSv1_2', 6);
}
// @codingStandardsIgnoreEnd
if (!defined('CURL_HTTP_VERSION_2TLS')) {
define('CURL_HTTP_VERSION_2TLS', 4);
}
/**
* CurlClient from https://github.com/stripe/stripe-php
*/
class CurlClient implements ClientInterface
{
private static $instance;
public static function instance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
protected $defaultOptions;
protected $userAgentInfo;
/**
* CurlClient constructor.
*
* Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
* off a request with, or an flat array with the same format used by curl_setopt_array() to
* provide a static set of options. Note that many options are overridden later in the request
* call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
*
* Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
* throw an exception if $defaultOptions returns a non-array value.
*
* @param array|callable|null $defaultOptions
*/
public function __construct($defaultOptions = null, $randomGenerator = null)
{
$this->defaultOptions = $defaultOptions;
$this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
$this->initUserAgentInfo();
}
public function initUserAgentInfo()
{
$curlVersion = curl_version();
$this->userAgentInfo = [
'httplib' => 'curl ' . $curlVersion['version'],
'ssllib' => $curlVersion['ssl_version'],
];
}
public function getDefaultOptions()
{
return $this->defaultOptions;
}
public function getUserAgentInfo()
{
return $this->userAgentInfo;
}
// USER DEFINED TIMEOUTS
const DEFAULT_TIMEOUT = 80;
const DEFAULT_CONNECT_TIMEOUT = 30;
private $timeout = self::DEFAULT_TIMEOUT;
private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
public function setTimeout($seconds)
{
$this->timeout = (int) max($seconds, 0);
return $this;
}
public function setConnectTimeout($seconds)
{
$this->connectTimeout = (int) max($seconds, 0);
return $this;
}
public function getTimeout()
{
return $this->timeout;
}
public function getConnectTimeout()
{
return $this->connectTimeout;
}
// END OF USER DEFINED TIMEOUTS
public function request($method, $absUrl, $params, $headers)
{
$method = strtolower($method);
$opts = [];
if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
$opts = call_user_func_array($this->defaultOptions, func_get_args());
if (!is_array($opts)) {
throw new Error\ApiConnection("Non-array value returned by defaultOptions CurlClient callback");
}
} elseif (is_array($this->defaultOptions)) { // set default curlopts from array
$opts = $this->defaultOptions;
}
switch ($method) {
case 'post':
$opts[CURLOPT_POST] = 1;
$opts[CURLOPT_POSTFIELDS] = Util\Util::encodeParameters($params);
break;
case 'put':
$opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
$opts[CURLOPT_POSTFIELDS] = Util\Util::encodeParameters($params);
break;
case 'get':
$opts[CURLOPT_HTTPGET] = 1;
if (count($params) > 0) {
$encoded = Util\Util::encodeParameters($params);
$absUrl = "$absUrl?$encoded";
}
break;
case 'delete':
$opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
if (count($params) > 0) {
$encoded = Util\Util::encodeParameters($params);
$absUrl = "$absUrl?$encoded";
}
break;
default:
throw new Error\InvalidRequest("Unrecognized method $method");
}
// It is only safe to retry network failures on POST requests if we
// add an Idempotency-Key header
if (($method == 'post') && (FedaPay::$maxNetworkRetries > 0)) {
if (!isset($headers['Idempotency-Key'])) {
array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid());
}
}
// Create a callback to capture HTTP headers for the response
$rheaders = [];
$headerCallback = function ($curl, $header_line) use (&$rheaders) {
// Ignore the HTTP request line (HTTP/1.1 200 OK)
if (strpos($header_line, ":") === false) {
return strlen($header_line);
}
list($key, $value) = explode(":", trim($header_line), 2);
$rheaders[trim($key)] = trim($value);
return strlen($header_line);
};
// By default for large request body sizes (> 1024 bytes), cURL will
// send a request without a body and with a `Expect: 100-continue`
// header, which gives the server a chance to respond with an error
// status code in cases where one can be determined right away (say
// on an authentication problem for example), and saves the "large"
// request body from being ever sent.
//
// Unfortunately, the bindings don't currently correctly handle the
// success case (in which the server sends back a 100 CONTINUE), so
// we'll error under that condition. To compensate for that problem
// for the time being, override cURL's behavior by simply always
// sending an empty `Expect:` header.
array_push($headers, 'Expect: ');
$opts[CURLOPT_URL] = $absUrl;
$opts[CURLOPT_RETURNTRANSFER] = true;
$opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
$opts[CURLOPT_TIMEOUT] = $this->timeout;
$opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
$opts[CURLOPT_HTTPHEADER] = $headers;
$opts[CURLOPT_CAINFO] = FedaPay::getCABundlePath();
if (!FedaPay::getVerifySslCerts()) {
$opts[CURLOPT_SSL_VERIFYPEER] = false;
}
// For HTTPS requests, enable HTTP/2, if supported
$opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS;
list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
return [$rbody, $rcode, $rheaders];
}
/**
* @param array $opts cURL options
*/
private function executeRequestWithRetries($opts, $absUrl)
{
$numRetries = 0;
while (true) {
$rcode = 0;
$errno = 0;
$curl = curl_init();
curl_setopt_array($curl, $opts);
$rbody = curl_exec($curl);
if ($rbody === false) {
$errno = curl_errno($curl);
$message = curl_error($curl);
} else {
$rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
}
curl_close($curl);
if ($this->shouldRetry($errno, $rcode, $numRetries)) {
$numRetries += 1;
$sleepSeconds = $this->sleepTime($numRetries);
usleep(intval($sleepSeconds * 1000000));
} else {
break;
}
}
if ($rbody === false) {
$this->handleCurlError($absUrl, $errno, $message, $numRetries);
}
return [$rbody, $rcode];
}
/**
* @param string $url
* @param int $errno
* @param string $message
* @param int $numRetries
* @throws Error\ApiConnection
*/
private function handleCurlError($url, $errno, $message, $numRetries)
{
switch ($errno) {
case CURLE_COULDNT_CONNECT:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_OPERATION_TIMEOUTED:
$msg = "Could not connect to FedaPay ($url). Please check your "
. "internet connection and try again. If this problem persists";
break;
case CURLE_SSL_CACERT:
case CURLE_SSL_PEER_CERTIFICATE:
$msg = "Could not verify FedaPay's SSL certificate. Please make sure "
. "that your network is not intercepting certificates. "
. "(Try going to $url in your browser.) "
. "If this problem persists,";
break;
default:
$msg = "Unexpected error communicating with FedaPay. "
. "If this problem persists,";
}
$msg .= " let us know at support@fedapay.com.";
$msg .= "\n\n(Network error [errno $errno]: $message)";
if ($numRetries > 0) {
$msg .= "\n\nRequest was retried $numRetries times.";
}
throw new Error\ApiConnection($msg);
}
/**
* Checks if an error is a problem that we should retry on. This includes both
* socket errors that may represent an intermittent problem and some special
* HTTP statuses.
* @param int $errno
* @param int $rcode
* @param int $numRetries
* @return bool
*/
private function shouldRetry($errno, $rcode, $numRetries)
{
if ($numRetries >= FedaPay::getMaxNetworkRetries()) {
return false;
}
// Retry on timeout-related problems (either on open or read).
if ($errno === CURLE_OPERATION_TIMEOUTED) {
return true;
}
// Destination refused the connection, the connection was reset, or a
// variety of other connection failures. This could occur from a single
// saturated server, so retry in case it's intermittent.
if ($errno === CURLE_COULDNT_CONNECT) {
return true;
}
// 409 conflict
if ($rcode === 409) {
return true;
}
return false;
}
private function sleepTime($numRetries)
{
// Apply exponential backoff with $initialNetworkRetryDelay on the
// number of $numRetries so far as inputs. Do not allow the number to exceed
// $maxNetworkRetryDelay.
$sleepSeconds = min(
FedaPay::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
FedaPay::getMaxNetworkRetryDelay()
);
// Apply some jitter by randomizing the value in the range of
// ($sleepSeconds / 2) to ($sleepSeconds).
$sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
// But never sleep less than the base sleep seconds.
$sleepSeconds = max(FedaPay::getInitialNetworkRetryDelay(), $sleepSeconds);
return $sleepSeconds;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace FedaPay;
/**
* Class Invoice
*
* @property int $id
* @property integer $number
* @property string $reference
* @property string $status
* @property integer $tax
* @property string $discount_type
* @property integer $discount_amount
* @property integer $ttc
* @property integer $sub_total
* @property integer $discount
* @property integer $before_tax
* @property integer $tax_amount
* @property integer $total_amount_paid
* @property string $notes
* @property integer $invoice_products_count
* @property string $due_at
* @property string $sent_at
* @property array $paid_at
* @property string $partially_paid_at
* @property int $customer_id
* @property int $currency_id
* @property int $account_id
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
*
* @package FedaPay
*/
class Invoice extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
public static function verify($reference, $params = [], $headers = [])
{
$base = static::resourcePath($reference);
$url = "$base/verify";
list($response, $opts) = static::_staticRequest('get', $url, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->invoice_verify;
}
}

28
vendor/fedapay/fedapay-php/lib/Log.php vendored Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace FedaPay;
/**
* Class Log
*
* @property int $id
* @property string $method
* @property string $url
* @property string $status
* @property string $ip_address
* @property string $version
* @property string $source
* @property string $query
* @property string $body
* @property string $response
* @property int $account_id
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Log extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace FedaPay\OAuth;
use FedaPay\Resource;
use FedaPay\FedaPay;
/**
* CurlClient from https://github.com/stripe/stripe-php
*/
class Client extends Resource
{
public static function grantClientCredentials()
{
list($response, $opts) = static::_staticRequest(
'post', '/oauth/token',
[
'grant_type' => 'client_credentials',
'client_id' => FedaPay::getOauthClientId(),
'client_secret' => FedaPay::getOauthClientSecret()
]
);
return \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
}
}

57
vendor/fedapay/fedapay-php/lib/Page.php vendored Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace FedaPay;
/**
* Class Page
*
* @property int $id
* @property string $name
* @property string $description
* @property string $reference
* @property string $published
* @property string $amount
* @property string $enable_phone_number
* @property string $callback_url
* @property array $custom_fields
* @property string $image_url
* @property int $account_id
* @property int $currency_id
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
*
* @package FedaPay
*/
class Page extends Resource
{
use ApiOperations\All;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
public static function verify($reference, $params = [], $headers = [])
{
$base = static::resourcePath($reference);
$url = "$base/verify";
list($response, $opts) = static::_staticRequest('get', $url, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->page_verify;
}
public static function light($reference, $params = [], $headers = [])
{
$base = static::resourcePath($reference);
$url = "$base/light";
$className = static::className();
list($response, $opts) = static::_staticRequest('get', $url, $params, $headers);
$object = \FedaPay\Util\Util::arrayToFedaPayObject($response, $opts);
return $object->$className;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace FedaPay;
use FedaPay\Util\Util;
/**
* Class Payout
*
* @property int $id
* @property string $reference
* @property string $amount
* @property string $status
* @property int $customer_id
* @property int $balance_id
* @property string $mode
* @property int $last_error_code
* @property string $last_error_message
* @property string $created_at
* @property string $updated_at
* @property string $scheduled_at
* @property string $sent_at
* @property string $failed_at
* @property string $deleted_at
*
* @package FedaPay
*/
class Payout extends Resource
{
use ApiOperations\All;
use ApiOperations\Search;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\CreateInBatch;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
/**
* @param array|string|null $params
* @param array|string|null $headers
*
* @return Payout.
*/
protected function _start($params, $headers)
{
$path = static::resourcePath('start');
list($response, $opts) = static::_staticRequest('put', $path, $params, $headers);
$object = Util::arrayToFedaPayObject($response, $opts);
$this->refreshFrom($object->payouts[0], $opts);
return $this;
}
/**
* @param array|string|null $params
* @param array|string|null $headers
*
* @return FedaPayObject.
*/
protected static function _startAll($params, $headers)
{
$path = static::resourcePath('start');
list($response, $opts) = static::_staticRequest('put', $path, $params, $headers);
return Util::arrayToFedaPayObject($response, $opts);
}
/**
* Start the payout
* @param string $scheduled_at
* @param array|string|null $params
*
* @return FedaPay\FedaPayObject
*/
public function schedule($scheduled_at, $params = [], $headers = [])
{
$scheduled_at = Util::toDateString($scheduled_at);
$payout_params = [ 'id' => $this->id, 'scheduled_at' => $scheduled_at ];
if (isset($params['phone_number'])) {
$payout_params['phone_number'] = $params['phone_number'];
unset($params['phone_number']); // Remove phone_number from params
}
$_params = [ 'payouts' => [ $payout_params ] ];
$params = array_merge($_params, $params);
return $this->_start($params, $headers);
}
/**
* Send the payout now
* @param array|string|null $params
* @param array|string|null $headers
*
* @return FedaPay\FedaPayObject
*/
public function sendNow($params = [], $headers = [])
{
$payout_params = [ 'id' => $this->id ];
if (isset($params['phone_number'])) {
$payout_params['phone_number'] = $params['phone_number'];
unset($params['phone_number']); // Remove phone_number from params
}
$_params = [ 'payouts' => [$payout_params] ];
$params = array_merge($_params, $params);
return $this->_start($params, $headers);
}
/**
* Start a scheduled payout
*
* @param array $payouts list of payouts id to start. One at least
* @param null|DateTime $scheduled_at If null, payouts should be send now.
* @param array $headers
* @return FedaPay\FedaPayObject
*/
public static function scheduleAll($payouts = [], $params = [], $headers = [])
{
$items = [];
foreach ($payouts as $key => $payout) {
$item = [];
if (!array_key_exists('id', $payout)) {
throw new \InvalidArgumentException(
'Invalid id argument. You must specify payout id.'
);
}
$item['id'] = $payout['id'];
if (array_key_exists('scheduled_at', $payout)) {
$item['scheduled_at'] = Util::toDateString($payout['scheduled_at']);
}
if (isset($params[$key]['phone_number'])) {
$item['phone_number'] = $params[$key]['phone_number'];
unset($params[$key]['phone_number']); // Remove phone_number from params
}
$items[] = $item;
}
$_params = [
'payouts' => $items
];
$params = array_merge($_params, $params);
return self::_startAll($params, $headers);
}
/**
* Send all payouts now
*
* @param array $payouts list of payouts id to start. One at least
* @param array $params If null, payouts should be send now.
* @param array $headers
* @return FedaPay\FedaPayObject
*/
public static function sendAllNow($payouts = [], $params = [], $headers = [])
{
$items = [];
foreach ($payouts as $key => $payout) {
$item = [];
if (!array_key_exists('id', $payout)) {
throw new \InvalidArgumentException(
'Invalid id argument. You must specify payout id.'
);
}
$item['id'] = $payout['id'];
if (isset($params[$key]['phone_number'])) {
$item['phone_number'] = $params[$key]['phone_number'];
unset($params[$key]['phone_number']); // Remove phone_number from params
}
$items[] = $item;
}
$_params = [
'payouts' => $items
];
$params = array_merge($_params, $params);
return self::_startAll($params, $headers);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace FedaPay;
/**
* Class PhoneNumber
*
* @property int $id
* @property string $number
* @property string $country
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class PhoneNumber extends Resource
{
}

View File

@@ -0,0 +1,215 @@
<?php
namespace FedaPay;
/**
* Class Requestor
*
* @package FedaPay
*/
class Requestor
{
const SANDBOX_BASE = 'https://sandbox-api.fedapay.com';
const PRODUCTION_BASE = 'https://api.fedapay.com';
const DEVELOPMENT_BASE = 'https://dev-api.fedapay.com';
/**
* Http Client
* @var GuzzleHttp\ClientInterface
*/
protected static $httpClient;
/**
* @static
*
* @param HttpClient\ClientInterface $client
*/
public static function setHttpClient($client)
{
self::$httpClient = $client;
}
/**
* @return HttpClient\ClientInterface
*/
private function httpClient()
{
if (!self::$httpClient) {
$options = [];
if (FedaPay::getVerifySslCerts()) {
$options['verify'] = FedaPay::getCaBundlePath();
}
self::$httpClient = HttpClient\CurlClient::instance();
}
return self::$httpClient;
}
/**
* @param string $method
* @param string $url
* @param array|null $params
* @param array|null $headers
*
* @return array An API response.
*/
public function request($method, $path, $params = null, $headers = null)
{
$params = $params ?: [];
$headers = $headers ?: [];
$params = array_merge($this->defaultParams(), $params);
$headers = array_merge($this->defaultHeaders(), $headers);
$url = $this->url($path);
$rawHeaders = [];
foreach ($headers as $h => $v) {
$rawHeaders[] = $h . ': ' . $v;
}
list($rbody, $rcode, $rheaders) = $this->httpClient()->request($method, $url, $params, $rawHeaders);
$json = $this->_interpretResponse($rbody, $rcode, $rheaders);
return $json;
}
/**
* Format http request error
* @param GuzzleHttp\Exception\RequestException $e
* @throws Error\ApiConnection
* @return void
*/
protected function handleRequestException($e)
{
$message = 'Request error: '. $e->getMessage();
$httpStatusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : null;
$httpRequest = $e->getRequest();
$httpResponse = $e->getResponse();
throw new Error\ApiConnection(
$message,
$httpStatusCode,
$httpRequest,
$httpResponse
);
}
/**
* Return the default request params
* @return array
*/
protected function defaultParams()
{
$params = [];
if (FedaPay::getLocale()) {
$params['locale'] = FedaPay::getLocale();
}
return $params;
}
/**
* Return the default request headers
* @return array
*/
protected function defaultHeaders()
{
$auth = FedaPay::getApiKey() ?: FedaPay::getToken();
$apiVersion = FedaPay::getApiVersion();
$accountId = FedaPay::getAccountId();
$default = [
'X-Version' => FedaPay::VERSION,
'X-Api-Version' => $apiVersion,
'X-Source' => 'FedaPay PhpLib',
'Authorization' => "Bearer $auth"
];
if ($accountId) {
$default['FedaPay-Account'] = $accountId;
}
return $default;
}
/**
* Return the base url of the requests
* @return string
*/
protected function baseUrl()
{
$apiBase = FedaPay::getApiBase();
$environment = FedaPay::getEnvironment();
if ($apiBase) {
return $apiBase;
}
switch ($environment) {
case 'development':
case 'dev':
return self::DEVELOPMENT_BASE;
case 'sandbox':
case 'test':
case null:
return self::SANDBOX_BASE;
case 'production':
case 'live':
return self::PRODUCTION_BASE;
}
}
/**
* Return the request url
* @return string
*/
protected function url($path)
{
return $this->baseUrl() . '/' . FedaPay::getApiVersion() . $path;
}
/**
* @param string $rbody
* @param int $rcode
* @param array $rheaders
*
* @return mixed
*/
private function _interpretResponse($rbody, $rcode, $rheaders)
{
$resp = json_decode($rbody, true);
$jsonError = json_last_error();
if ($resp === null && $jsonError !== JSON_ERROR_NONE) {
$msg = "Invalid response body from API: $rbody "
. "(HTTP response code was $rcode, json_last_error() was $jsonError)";
throw new Error\ApiConnection($msg, $rcode, $rbody);
}
if ($rcode < 200 || $rcode >= 300) {
$this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
}
return $resp;
}
/**
* @param string $rbody A JSON string.
* @param int $rcode
* @param array $rheaders
* @param array $resp
*
* @throws Error\InvalidRequest if the error is caused by the user.
*/
public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
{
$msg = isset($resp['message']) ? $resp['message'] : 'ApiConnection Error' ;
throw new Error\ApiConnection($msg, $rcode, $rbody, $resp, $rheaders);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace FedaPay;
use FedaPay\Util\Util;
use FedaPay\Util\Inflector;
/**
* Class Resource
*
* @package FedaPay
*/
abstract class Resource extends FedaPayObject
{
use ApiOperations\Request;
/**
* @var FedaPay\Requestor
*/
protected static $requestor;
/**
* Set requestor
* @param FedaPay\Requestor $requestor
*/
public static function setRequestor(Requestor $requestor)
{
self::$requestor = $requestor;
}
/**
* Return the requestor
* @return FedaPay\Requestor
*/
public static function getRequestor()
{
if (self::$requestor) {
return self::$requestor;
}
self::$requestor = new Requestor;
return self::$requestor;
}
public static function className()
{
$class = get_called_class();
// Useful for namespaces: Foo\Charge
if ($postfixNamespaces = strrchr($class, '\\')) {
$class = substr($postfixNamespaces, 1);
}
// Useful for underscored 'namespaces': Foo_Charge
if ($postfixFakeNamespaces = strrchr($class, '')) {
$class = $postfixFakeNamespaces;
}
if (substr($class, 0, strlen('FedaPay')) == 'FedaPay') {
$class = substr($class, strlen('FedaPay'));
}
$class = str_replace('_', '', $class);
$name = urlencode($class);
$name = strtolower($name);
return $name;
}
/**
* @return string The endpoint URL for the given class.
*/
public static function classPath()
{
$base = static::className();
$plurial = Inflector::pluralize($base);
return "/$plurial";
}
/**
* @return string The instance endpoint URL for the given class.
*/
public static function resourcePath($id)
{
if ($id === null) {
$class = get_called_class();
$message = 'Could not determine which URL to request: '
. "$class instance has invalid ID: $id";
throw new Error\InvalidRequest($message, null);
}
$base = static::classPath();
$extn = urlencode($id);
return "$base/$extn";
}
/**
* @return string The full API URL for this API resource.
*/
public function instanceUrl()
{
return static::resourcePath($this['id']);
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace FedaPay;
use FedaPay\Util\Util;
/**
* Class Transaction
*
* @property int $id
* @property string $reference
* @property string $description
* @property string $callback_url
* @property string $amount
* @property string $status
* @property int $transaction_id
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
class Transaction extends Resource
{
use ApiOperations\All;
use ApiOperations\Search;
use ApiOperations\Retrieve;
use ApiOperations\Create;
use ApiOperations\CreateInBatch;
use ApiOperations\Update;
use ApiOperations\Save;
use ApiOperations\Delete;
/**
* Paid status list
* @var array
*/
private static $PAID_STATUS = [
'approved', 'transferred', 'refunded',
'approved_partially_refunded', 'transferred_partially_refunded'
];
/**
* Check if the transaction was paid
*
* @return boolean
*/
public function wasPaid()
{
return in_array($this->status, self::$PAID_STATUS);
}
/**
* Check if the transacton was refunded. Status must include refunded.
*
* @return boolean
*/
public function wasRefunded()
{
return strpos($this->status, 'refunded') !== false;
}
/**
* Check if the transacton was partially refunded. Status must include partially_refunded.
*
* @return boolean
*/
public function wasPartiallyRefunded()
{
return strpos($this->status, 'partially_refunded') !== false;
}
/**
* Generate a payment token and url
* @return FedaPay\FedaPayObject
*/
public function generateToken($params = [], $headers = [])
{
$url = $this->instanceUrl() . '/token';
list($response, $opts) = static::_staticRequest('post', $url, $params, $headers);
return Util::arrayToFedaPayObject($response, $opts);
}
/**
* Generate a payment token and url from transaction id method
* @return FedaPay\FedaPayObject
*/
public static function generateTokenFromId($id, $params = [], $headers = [])
{
$path = static::resourcePath($id) . '/token';
list($response, $opts) = static::_staticRequest('post', $path, $params, $headers);
return Util::arrayToFedaPayObject($response, $opts);
}
/**
* Send Mobile Money request with token
* @param string $mode
* @param string $token
* @param array $params
* @param array $headers
*
* @return FedaPay\FedaPayObject
*/
public function sendNowWithToken($mode, $token, $params = [], $headers = [])
{
$url = '/' . $mode;
$params = array_merge(['token' => $token], $params);
list($response, $opts) = static::_staticRequest('post', $url, $params, $headers);
return Util::arrayToFedaPayObject($response, $opts);
}
/**
* Return transaction receipt URL
* @param array $force
* @param array $params
* @param array $headers
*
* @return string
*/
public function getReceiptURL($force = false, $params = [], $headers = [])
{
$receipt_url = $this->receipt_url;
if (is_null($receipt_url) || $force) {
$url = $this->instanceUrl() . '/receipt_url';
// Force Api to generate url
if ($force) {
$params['force'] = true;
}
list($response, $opts) = static::_staticRequest('post', $url, $params, $headers);
$urlObject = Util::arrayToFedaPayObject($response, $opts);
$receipt_url = $urlObject->url;
}
return $receipt_url;
}
/**
* Send Mobile Money request
* @param string $mode
* @param array $params
* @param array $headers
*
* @return FedaPay\FedaPayObject
*/
public function sendNow($mode, $params = [], $headers = [])
{
$tokenObject = $this->generateToken([], $headers);
return $this->sendNowWithToken($mode, $tokenObject->token, $params, $headers);
}
/**
* Get transactions fees details
* @param string $token
* @param string $mode
* @param array $params
* @param array $headers
*
* @return FedaPay\FedaPayObject
*/
public function getFees($token, $mode, $params = [], $headers = [])
{
$url = static::classPath() . '/fees';
$params = array_merge(['token' => $token, 'mode' => $mode], $params);
list($response, $opts) = static::_staticRequest('get', $url, $params, $headers);
return Util::arrayToFedaPayObject($response, $opts);
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace FedaPay\Util;
/**
* Class Inflector
*
* @package FedaPay\Util
*/
abstract class Inflector
{
/**
* Plural inflector rules
*
* @var array
*/
protected static $_plural = [
'/(s)tatus$/i' => '\1tatuses',
'/(quiz)$/i' => '\1zes',
'/^(ox)$/i' => '\1\2en',
'/([m|l])ouse$/i' => '\1ice',
'/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
'/(x|ch|ss|sh)$/i' => '\1es',
'/([^aeiouy]|qu)y$/i' => '\1ies',
'/(hive)$/i' => '\1s',
'/(chef)$/i' => '\1s',
'/(?:([^f])fe|([lre])f)$/i' => '\1\2ves',
'/sis$/i' => 'ses',
'/([ti])um$/i' => '\1a',
'/(p)erson$/i' => '\1eople',
'/(?<!u)(m)an$/i' => '\1en',
'/(c)hild$/i' => '\1hildren',
'/(buffal|tomat)o$/i' => '\1\2oes',
'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i',
'/us$/i' => 'uses',
'/(alias)$/i' => '\1es',
'/(ax|cris|test)is$/i' => '\1es',
'/s$/' => 's',
'/^$/' => '',
'/$/' => 's',
];
/**
* Singular inflector rules
*
* @var array
*/
protected static $_singular = [
'/(s)tatuses$/i' => '\1\2tatus',
'/^(.*)(menu)s$/i' => '\1\2',
'/(quiz)zes$/i' => '\\1',
'/(matr)ices$/i' => '\1ix',
'/(vert|ind)ices$/i' => '\1ex',
'/^(ox)en/i' => '\1',
'/(alias)(es)*$/i' => '\1',
'/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
'/([ftw]ax)es/i' => '\1',
'/(cris|ax|test)es$/i' => '\1is',
'/(shoe)s$/i' => '\1',
'/(o)es$/i' => '\1',
'/ouses$/' => 'ouse',
'/([^a])uses$/' => '\1us',
'/([m|l])ice$/i' => '\1ouse',
'/(x|ch|ss|sh)es$/i' => '\1',
'/(m)ovies$/i' => '\1\2ovie',
'/(s)eries$/i' => '\1\2eries',
'/([^aeiouy]|qu)ies$/i' => '\1y',
'/(tive)s$/i' => '\1',
'/(hive)s$/i' => '\1',
'/(drive)s$/i' => '\1',
'/([le])ves$/i' => '\1f',
'/([^rfoa])ves$/i' => '\1fe',
'/(^analy)ses$/i' => '\1sis',
'/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
'/([ti])a$/i' => '\1um',
'/(p)eople$/i' => '\1\2erson',
'/(m)en$/i' => '\1an',
'/(c)hildren$/i' => '\1\2hild',
'/(n)ews$/i' => '\1\2ews',
'/eaus$/' => 'eau',
'/^(.*us)$/' => '\\1',
'/s$/i' => ''
];
/**
* Irregular rules
*
* @var array
*/
protected static $_irregular = [
'atlas' => 'atlases',
'beef' => 'beefs',
'brief' => 'briefs',
'brother' => 'brothers',
'cafe' => 'cafes',
'child' => 'children',
'cookie' => 'cookies',
'corpus' => 'corpuses',
'cow' => 'cows',
'criterion' => 'criteria',
'ganglion' => 'ganglions',
'genie' => 'genies',
'genus' => 'genera',
'graffito' => 'graffiti',
'hoof' => 'hoofs',
'loaf' => 'loaves',
'man' => 'men',
'money' => 'monies',
'mongoose' => 'mongooses',
'move' => 'moves',
'mythos' => 'mythoi',
'niche' => 'niches',
'numen' => 'numina',
'occiput' => 'occiputs',
'octopus' => 'octopuses',
'opus' => 'opuses',
'ox' => 'oxen',
'penis' => 'penises',
'person' => 'people',
'sex' => 'sexes',
'soliloquy' => 'soliloquies',
'testis' => 'testes',
'trilby' => 'trilbys',
'turf' => 'turfs',
'potato' => 'potatoes',
'hero' => 'heroes',
'tooth' => 'teeth',
'goose' => 'geese',
'foot' => 'feet',
'foe' => 'foes',
'sieve' => 'sieves'
];
/**
* Words that should not be inflected
*
* @var array
*/
protected static $_uninflected = [
'.*[nrlm]ese', '.*data', '.*deer', '.*fish', '.*measles', '.*ois',
'.*pox', '.*sheep', 'people', 'feedback', 'stadia', '.*?media',
'chassis', 'clippers', 'debris', 'diabetes', 'equipment', 'gallows',
'graffiti', 'headquarters', 'information', 'innings', 'news', 'nexus',
'pokemon', 'proceedings', 'research', 'sea[- ]bass', 'series', 'species', 'weather'
];
/**
* Method cache array.
*
* @var array
*/
protected static $_cache = [];
/**
* Return $word in plural form.
*
* @param string $word Word in singular
* @return string Word in plural
*/
public static function pluralize($word)
{
if (isset(static::$_cache['pluralize'][$word])) {
return static::$_cache['pluralize'][$word];
}
if (!isset(static::$_cache['irregular']['pluralize'])) {
static::$_cache['irregular']['pluralize'] = '(?:' . implode('|', array_keys(static::$_irregular)) . ')';
}
if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) {
static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
substr(static::$_irregular[strtolower($regs[2])], 1);
return static::$_cache['pluralize'][$word];
}
if (!isset(static::$_cache['uninflected'])) {
static::$_cache['uninflected'] = '(?:' . implode('|', static::$_uninflected) . ')';
}
if (preg_match('/^(' . static::$_cache['uninflected'] . ')$/i', $word, $regs)) {
static::$_cache['pluralize'][$word] = $word;
return $word;
}
foreach (static::$_plural as $rule => $replacement) {
if (preg_match($rule, $word)) {
static::$_cache['pluralize'][$word] = preg_replace($rule, $replacement, $word);
return static::$_cache['pluralize'][$word];
}
}
}
/**
* Return $word in singular form.
*
* @param string $word Word in plural
* @return string Word in singular
* @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-plural-singular-forms
*/
public static function singularize($word)
{
if (isset(static::$_cache['singularize'][$word])) {
return static::$_cache['singularize'][$word];
}
if (!isset(static::$_cache['irregular']['singular'])) {
static::$_cache['irregular']['singular'] = '(?:' . implode('|', static::$_irregular) . ')';
}
if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) {
static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
substr(array_search(strtolower($regs[2]), static::$_irregular), 1);
return static::$_cache['singularize'][$word];
}
if (!isset(static::$_cache['uninflected'])) {
static::$_cache['uninflected'] = '(?:' . implode('|', static::$_uninflected) . ')';
}
if (preg_match('/^(' . static::$_cache['uninflected'] . ')$/i', $word, $regs)) {
static::$_cache['pluralize'][$word] = $word;
return $word;
}
foreach (static::$_singular as $rule => $replacement) {
if (preg_match($rule, $word)) {
static::$_cache['singularize'][$word] = preg_replace($rule, $replacement, $word);
return static::$_cache['singularize'][$word];
}
}
static::$_cache['singularize'][$word] = $word;
return $word;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace FedaPay\Util;
/**
* A basic random generator. This is in a separate class so we the generator
* can be injected as a dependency and replaced with a mock in tests.
*/
class RandomGenerator
{
/**
* Returns a random value between 0 and $max.
*
* @param float $max (optional)
* @return float
*/
public function randFloat($max = 1.0)
{
return mt_rand() / mt_getrandmax() * $max;
}
/**
* Returns a v4 UUID.
*
* @return string
*/
public function uuid()
{
$arr = array_values(unpack('N1a/n4b/N1c', openssl_random_pseudo_bytes(16)));
$arr[2] = ($arr[2] & 0x0fff) | 0x4000;
$arr[3] = ($arr[3] & 0x3fff) | 0x8000;
return vsprintf('%08x-%04x-%04x-%04x-%04x%08x', $arr);
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace FedaPay\Util;
use FedaPay\FedaPayObject;
/**
* Class Util
*
* @package FedaPay\Util
*/
abstract class Util
{
private static $isMbstringAvailable = null;
private static $isHashEqualsAvailable = null;
/**
* Whether the provided array (or other) is a list rather than a dictionary.
*
* @param array|mixed $array
* @return boolean True if the given object is a list.
*/
public static function isList($array)
{
if (!is_array($array)) {
return false;
}
if ($array === []) {
return true;
}
if (array_keys($array) !== range(0, count($array) - 1)) {
return false;
}
return true;
}
/**
* Convert a a response to fedapay object
* @param array $resp The response object
* @param array $opts Additional options.
*
* @return \FedaPay\FedaPayObject
*/
public static function convertToFedaPayObject($resp, $opts)
{
$types = [
'v1/api_key' => 'FedaPay\\ApiKey',
'v1/account' => 'FedaPay\\Account',
'v1/currency' => 'FedaPay\\Currency',
'v1/customer' => 'FedaPay\\Customer',
'v1/event' => 'FedaPay\\Event',
'v1/log' => 'FedaPay\\Log',
'v1/phone_number' => 'FedaPay\\PhoneNumber',
'v1/transaction' => 'FedaPay\\Transaction',
'v1/payout' => 'FedaPay\\Payout',
'v1/page' => 'FedaPay\\Page',
'v1/invoice' => 'FedaPay\\Invoice',
'v1/balance' => 'FedaPay\\Balance',
];
if (self::isList($resp)) {
$mapped = [];
foreach ($resp as $i) {
array_push($mapped, self::arrayToFedaPayObject($i, $opts));
}
return $mapped;
} elseif (is_array($resp)) {
if (isset($resp['klass']) && is_string($resp['klass']) && isset($types[$resp['klass']])) {
$class = $types[$resp['klass']];
} else {
$class = FedaPayObject::class;
}
$object = new $class;
$object->refreshFrom($resp, $opts);
return $object;
} else {
return $resp;
}
}
/**
* Converts an array to FedaPay object.
*
* @param array $array The PHP array to convert.
* @param array $opts Additional options.
*
* @return \FedaPay\FedaPayObject
*/
public static function arrayToFedaPayObject($array, $opts)
{
if (self::isList($array)) {
$mapped = array();
foreach ($array as $i) {
array_push($mapped, self::convertToFedaPayObject($i, $opts));
}
return $mapped;
} else {
return self::convertToFedaPayObject($array, $opts);
}
}
/**
* Recursively converts the PHP FedaPay object to an array.
*
* @param array $values The PHP FedaPay object to convert.
* @return array
*/
public static function convertFedaPayObjectToArray($values)
{
$results = [];
foreach ($values as $k => $v) {
if ($v instanceof FedaPayObject) {
$results[$k] = $v->__toArray(true);
} elseif (is_array($v)) {
$results[$k] = self::convertFedaPayObjectToArray($v);
} else {
$results[$k] = $v;
}
}
return $results;
}
/**
* Strip api version from key
* @param string $key
* @param array $opts
*
* @return string
*/
public static function stripApiVersion($key, $opts)
{
$apiPart = '';
if (is_array($opts) && isset($opts['apiVersion'])) {
$apiPart = $opts['apiVersion'] . '/';
}
return str_replace($apiPart, '', $key);
}
/**
* Check a date falue
* @param mixed $date
* @return mixed
*/
public static function toDateString($date)
{
if ($date instanceof \DateTime) {
return $date->format('Y-m-d H:i:s');
} else if (is_string($date) || is_int($date)) {
return $date;
} else {
throw new \InvalidArgumentException(
'Invalid datetime argument. Should be a date in string format, '
.' a timestamp or an instance of \DateTime.'
);
}
}
/**
* @param array $params
*
* @return string
*/
public static function encodeParameters($params)
{
$flattenedParams = [];
self::flattenParams($params, $flattenedParams);
$pieces = [];
foreach ($flattenedParams as $param) {
list($k, $v) = $param;
array_push($pieces, self::urlEncode($k) . '=' . self::urlEncode($v));
}
return implode('&', $pieces);
}
/**
* Flattens the array so that it can be used with curl.
*
* @param $arrays
* @param array $new
* @param null $prefix
*/
public static function flattenParams($arrays, &$new = array(), $prefix = null)
{
$isList = self::isList($arrays);
foreach ($arrays as $key => $value) {
if (isset($prefix) && $isList) {
$k = $prefix.'[]';
} elseif (isset($prefix)) {
$k = $prefix.'['.$key.']';
} else {
$k = $key;
}
if (is_array($value)) {
self::flattenParams($value, $new, $k);
} else {
array_push($new, [$k, $value]);
}
}
}
/**
* @param string $key A string to URL-encode.
*
* @return string The URL-encoded string.
*/
public static function urlEncode($key)
{
$s = urlencode($key);
// Don't use strict form encoding by changing the square bracket control
// characters back to their literals. This is fine by the server, and
// makes these parameter strings easier to read.
$s = str_replace('%5B', '[', $s);
$s = str_replace('%5D', ']', $s);
return $s;
}
/**
* Compares two strings for equality. The time taken is independent of the
* number of characters that match.
*
* @param string $a one of the strings to compare.
* @param string $b the other string to compare.
* @return bool true if the strings are equal, false otherwise.
*/
public static function secureCompare($a, $b)
{
if (self::$isHashEqualsAvailable === null) {
self::$isHashEqualsAvailable = function_exists('hash_equals');
}
if (self::$isHashEqualsAvailable) {
return hash_equals($a, $b);
} else {
if (strlen($a) != strlen($b)) {
return false;
}
$result = 0;
for ($i = 0; $i < strlen($a); $i++) {
$result |= ord($a[$i]) ^ ord($b[$i]);
}
return ($result == 0);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace FedaPay;
/**
* Class Webhook
*
* @property int $id
* @property string $public_key
* @property string $private_key
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
abstract class Webhook
{
const DEFAULT_TOLERANCE = 300;
/**
* Returns an Event instance using the provided JSON payload. Throws a
* \UnexpectedValueException if the payload is not valid JSON, and a
* \FedaPay\SignatureVerificationException if the signature verification
* fails for any reason.
*
* @param string $payload the payload sent by FedaPay.
* @param string $sigHeader the contents of the signature header sent by
* FedaPay.
* @param string $secret secret used to generate the signature.
* @param int $tolerance maximum difference allowed between the header's
* timestamp and the current time
* @return \FedaPay\Event the Event instance
* @throws \UnexpectedValueException if the payload is not valid JSON,
* @throws \FedaPay\Error\SignatureVerification if the verification fails.
*/
public static function constructEvent($payload, $sigHeader, $secret, $tolerance = self::DEFAULT_TOLERANCE)
{
WebhookSignature::verifyHeader($payload, $sigHeader, $secret, $tolerance);
$data = json_decode($payload, true);
$jsonError = json_last_error();
if ($data === null && $jsonError !== JSON_ERROR_NONE) {
$msg = "Invalid payload: $payload "
. "(json_last_error() was $jsonError)";
throw new \UnexpectedValueException($msg);
}
return new Event($data);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace FedaPay;
/**
* Class WebhookSignature
*
* @property int $id
* @property string $public_key
* @property string $private_key
* @property string $created_at
* @property string $updated_at
*
* @package FedaPay
*/
abstract class WebhookSignature
{
const EXPECTED_SCHEME = 's';
/**
* Verifies the signature header sent by FedaPay. Throws a
* SignatureVerification exception if the verification fails for any
* reason.
*
* @param string $payload the payload sent by FedaPay.
* @param string $header the contents of the signature header sent by
* FedaPay.
* @param string $secret secret used to generate the signature.
* @param int $tolerance maximum difference allowed between the header's
* timestamp and the current time
* @throws \FedaPay\Error\SignatureVerification if the verification fails.
* @return bool
*/
public static function verifyHeader($payload, $header, $secret, $tolerance = null)
{
// Extract timestamp and signatures from header
$timestamp = self::getTimestamp($header);
$signatures = self::getSignatures($header, self::EXPECTED_SCHEME);
if ($timestamp == -1) {
throw new Error\SignatureVerification(
"Unable to extract timestamp and signatures from header",
$header,
$payload
);
}
if (empty($signatures)) {
throw new Error\SignatureVerification(
"No signatures found with expected scheme",
$header,
$payload
);
}
// Check if expected signature is found in list of signatures from
// header
$signedPayload = "$timestamp.$payload";
$expectedSignature = self::computeSignature($signedPayload, $secret);
$signatureFound = false;
foreach ($signatures as $signature) {
if (Util\Util::secureCompare($expectedSignature, $signature)) {
$signatureFound = true;
break;
}
}
if (!$signatureFound) {
throw new Error\SignatureVerification(
"No signatures found matching the expected signature for payload",
$header,
$payload
);
}
// Check if timestamp is within tolerance
if (($tolerance > 0) && (abs(time() - $timestamp) > $tolerance)) {
throw new Error\SignatureVerification(
"Timestamp outside the tolerance zone",
$header,
$payload
);
}
return true;
}
/**
* Extracts the timestamp in a signature header.
*
* @param string $header the signature header
* @return int the timestamp contained in the header, or -1 if no valid
* timestamp is found
*/
private static function getTimestamp($header)
{
$items = explode(",", $header);
foreach ($items as $item) {
$itemParts = explode('=', $item, 2);
if ($itemParts[0] == 't') {
if (!is_numeric($itemParts[1])) {
return -1;
}
return intval($itemParts[1]);
}
}
return -1;
}
/**
* Extracts the signatures matching a given scheme in a signature header.
*
* @param string $header the signature header
* @param string $scheme the signature scheme to look for.
* @return array the list of signatures matching the provided scheme.
*/
private static function getSignatures($header, $scheme)
{
$signatures = [];
$items = explode(",", $header);
foreach ($items as $item) {
$itemParts = explode("=", $item, 2);
if ($itemParts[0] == $scheme) {
array_push($signatures, $itemParts[1]);
}
}
return $signatures;
}
/**
* Computes the signature for a given payload and secret.
*
* The current scheme used by FedaPay ("v1") is HMAC/SHA-256.
*
* @param string $payload the payload to sign.
* @param string $secret the secret used to generate the signature.
* @return string the signature as a string.
*/
private static function computeSignature($payload, $secret)
{
return hash_hmac('sha256', $payload, $secret);
}
}