You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

512 lines
17 KiB

<?php
/**
* Nexmo Client Library for PHP
*
* @copyright Copyright (c) 2016 Nexmo, Inc. (http://nexmo.com)
* @license https://github.com/Nexmo/nexmo-php/blob/master/LICENSE.txt MIT License
*/
namespace Nexmo;
use Http\Client\HttpClient;
use Nexmo\Client\Credentials\Basic;
use Nexmo\Client\Credentials\Container;
use Nexmo\Client\Credentials\CredentialsInterface;
use Nexmo\Client\Credentials\Keypair;
use Nexmo\Client\Credentials\OAuth;
use Nexmo\Client\Credentials\SignatureSecret;
use Nexmo\Client\Exception\Exception;
use Nexmo\Client\Factory\FactoryInterface;
use Nexmo\Client\Factory\MapFactory;
use Nexmo\Client\Response\Response;
use Nexmo\Client\Signature;
use Nexmo\Entity\EntityInterface;
use Nexmo\Verify\Verification;
use Psr\Http\Message\RequestInterface;
use Zend\Diactoros\Uri;
use Zend\Diactoros\Request;
/**
* Nexmo API Client, allows access to the API from PHP.
*
* @property \Nexmo\Message\Client $message
* @property \Nexmo\Call\Collection|\Nexmo\Call\Call[] $calls
*
* @method \Nexmo\Message\Client message()
* @method \Nexmo\Verify\Client verify()
* @method \Nexmo\Application\Client applications()
* @method \Nexmo\Call\Collection calls()
* @method \Nexmo\Numbers\Client numbers()
*/
class Client
{
const VERSION = '1.2.0';
const BASE_API = 'https://api.nexmo.com';
const BASE_REST = 'https://rest.nexmo.com';
/**
* API Credentials
* @var CredentialsInterface
*/
protected $credentials;
/**
* Http Client
* @var HttpClient
*/
protected $client;
/**
* @var FactoryInterface
*/
protected $factory;
/**
* @var array
*/
protected $options = [];
/**
* Create a new API client using the provided credentials.
*/
public function __construct(CredentialsInterface $credentials, $options = array(), HttpClient $client = null)
{
if(is_null($client)){
$client = new \Http\Adapter\Guzzle6\Client();
}
$this->setHttpClient($client);
//make sure we know how to use the credentials
if(!($credentials instanceof Container) && !($credentials instanceof Basic) && !($credentials instanceof SignatureSecret) && !($credentials instanceof OAuth) && !($credentials instanceof Keypair)){
throw new \RuntimeException('unknown credentials type: ' . get_class($credentials));
}
$this->credentials = $credentials;
$this->options = $options;
// If they've provided an app name, validate it
if (isset($options['app'])) {
$this->validateAppOptions($options['app']);
}
// Set the default URLs. Keep the constants for
// backwards compatibility
$this->apiUrl = static::BASE_API;
$this->restUrl = static::BASE_REST;
// If they've provided alternative URLs, use that instead
// of the defaults
if (isset($options['base_rest_url'])) {
$this->restUrl = $options['base_rest_url'];
}
if (isset($options['base_api_url'])) {
$this->apiUrl = $options['base_api_url'];
}
$this->setFactory(new MapFactory([
'account' => 'Nexmo\Account\Client',
'insights' => 'Nexmo\Insights\Client',
'message' => 'Nexmo\Message\Client',
'verify' => 'Nexmo\Verify\Client',
'applications' => 'Nexmo\Application\Client',
'numbers' => 'Nexmo\Numbers\Client',
'calls' => 'Nexmo\Call\Collection',
'conversion' => 'Nexmo\Conversion\Client',
'conversation' => 'Nexmo\Conversations\Collection',
'user' => 'Nexmo\User\Collection',
'redact' => 'Nexmo\Redact\Client',
], $this));
}
public function getRestUrl() {
return $this->restUrl;
}
public function getApiUrl() {
return $this->apiUrl;
}
/**
* Set the Http Client to used to make API requests.
*
* This allows the default http client to be swapped out for a HTTPlug compatible
* replacement.
*
* @param HttpClient $client
* @return $this
*/
public function setHttpClient(HttpClient $client)
{
$this->client = $client;
return $this;
}
/**
* Get the Http Client used to make API requests.
*
* @return HttpClient
*/
public function getHttpClient()
{
return $this->client;
}
/**
* Set the factory used to create API specific clients.
*
* @param FactoryInterface $factory
* @return $this
*/
public function setFactory(FactoryInterface $factory)
{
$this->factory = $factory;
return $this;
}
/**
* @param RequestInterface $request
* @param Signature $signature
* @return RequestInterface
*/
public static function signRequest(RequestInterface $request, SignatureSecret $credentials)
{
switch($request->getHeaderLine('content-type')){
case 'application/json':
$body = $request->getBody();
$body->rewind();
$content = $body->getContents();
$params = json_decode($content, true);
$params['api_key'] = $credentials['api_key'];
$signature = new Signature($params, $credentials['signature_secret'], $credentials['signature_method']);
$body->rewind();
$body->write(json_encode($signature->getSignedParams()));
break;
case 'application/x-www-form-urlencoded':
$body = $request->getBody();
$body->rewind();
$content = $body->getContents();
$params = [];
parse_str($content, $params);
$params['api_key'] = $credentials['api_key'];
$signature = new Signature($params, $credentials['signature_secret'], $credentials['signature_method']);
$params = $signature->getSignedParams();
$body->rewind();
$body->write(http_build_query($params, null, '&'));
break;
default:
$query = [];
parse_str($request->getUri()->getQuery(), $query);
$query['api_key'] = $credentials['api_key'];
$signature = new Signature($query, $credentials['signature_secret'], $credentials['signature_method']);
$request = $request->withUri($request->getUri()->withQuery(http_build_query($signature->getSignedParams())));
break;
}
return $request;
}
public static function authRequest(RequestInterface $request, Basic $credentials)
{
switch($request->getHeaderLine('content-type')) {
case 'application/json':
if (static::requiresBasicAuth($request)) {
$c = $credentials->asArray();
$request = $request->withHeader('Authorization', 'Basic ' . base64_encode($c['api_key'] . ':' . $c['api_secret']));
} else if (static::requiresAuthInUrlNotBody($request)) {
$query = [];
parse_str($request->getUri()->getQuery(), $query);
$query = array_merge($query, $credentials->asArray());
$request = $request->withUri($request->getUri()->withQuery(http_build_query($query)));
} else {
$body = $request->getBody();
$body->rewind();
$content = $body->getContents();
$params = json_decode($content, true);
if (!$params) { $params = []; }
$params = array_merge($params, $credentials->asArray());
$body->rewind();
$body->write(json_encode($params));
}
break;
case 'application/x-www-form-urlencoded':
$body = $request->getBody();
$body->rewind();
$content = $body->getContents();
$params = [];
parse_str($content, $params);
$params = array_merge($params, $credentials->asArray());
$body->rewind();
$body->write(http_build_query($params, null, '&'));
break;
default:
$query = [];
parse_str($request->getUri()->getQuery(), $query);
$query = array_merge($query, $credentials->asArray());
$request = $request->withUri($request->getUri()->withQuery(http_build_query($query)));
break;
}
return $request;
}
/**
* @param array $claims
* @return \Lcobucci\JWT\Token
*/
public function generateJwt($claims = [])
{
if (method_exists($this->credentials, "generateJwt")) {
return $this->credentials->generateJwt($claims);
}
throw new Exception(get_class($this->credentials).' does not support JWT generation');
}
/**
* Takes a URL and a key=>value array to generate a GET PSR-7 request object
*
* @param string $url The URL to make a request to
* @param array $params Key=>Value array of data to use as the query string
* @return \Psr\Http\Message\ResponseInterface
*/
public function get($url, array $params = [])
{
$queryString = '?' . http_build_query($params);
$url = $url . $queryString;
$request = new Request(
$url,
'GET'
);
return $this->send($request);
}
/**
* Takes a URL and a key=>value array to generate a POST PSR-7 request object
*
* @param string $url The URL to make a request to
* @param array $params Key=>Value array of data to send
* @return \Psr\Http\Message\ResponseInterface
*/
public function post($url, array $params)
{
$request = new Request(
$url,
'POST',
'php://temp',
['content-type' => 'application/json']
);
$request->getBody()->write(json_encode($params));
return $this->send($request);
}
/**
* Takes a URL and a key=>value array to generate a POST PSR-7 request object
*
* @param string $url The URL to make a request to
* @param array $params Key=>Value array of data to send
* @return \Psr\Http\Message\ResponseInterface
*/
public function postUrlEncoded($url, array $params)
{
$request = new Request(
$url,
'POST',
'php://temp',
['content-type' => 'application/x-www-form-urlencoded']
);
$request->getBody()->write(http_build_query($params));
return $this->send($request);
}
/**
* Takes a URL and a key=>value array to generate a PUT PSR-7 request object
*
* @param string $url The URL to make a request to
* @param array $params Key=>Value array of data to send
* @return \Psr\Http\Message\ResponseInterface
*/
public function put($url, array $params)
{
$request = new Request(
$url,
'PUT',
'php://temp',
['content-type' => 'application/json']
);
$request->getBody()->write(json_encode($params));
return $this->send($request);
}
/**
* Takes a URL and a key=>value array to generate a DELETE PSR-7 request object
*
* @param string $url The URL to make a request to
* @return \Psr\Http\Message\ResponseInterface
*/
public function delete($url)
{
$request = new Request(
$url,
'DELETE'
);
return $this->send($request);
}
/**
* Wraps the HTTP Client, creates a new PSR-7 request adding authentication, signatures, etc.
*
* @param \Psr\Http\Message\RequestInterface $request
* @return \Psr\Http\Message\ResponseInterface
*/
public function send(\Psr\Http\Message\RequestInterface $request)
{
if($this->credentials instanceof Container) {
if ($this->needsKeypairAuthentication($request)) {
$request = $request->withHeader('Authorization', 'Bearer ' . $this->credentials->get(Keypair::class)->generateJwt());
} else {
$request = self::authRequest($request, $this->credentials->get(Basic::class));
}
} elseif($this->credentials instanceof Keypair){
$request = $request->withHeader('Authorization', 'Bearer ' . $this->credentials->generateJwt());
} elseif($this->credentials instanceof SignatureSecret){
$request = self::signRequest($request, $this->credentials);
} elseif($this->credentials instanceof Basic){
$request = self::authRequest($request, $this->credentials);
}
//todo: add oauth support
//allow any part of the URI to be replaced with a simple search
if(isset($this->options['url'])){
foreach($this->options['url'] as $search => $replace){
$uri = (string) $request->getUri();
$new = str_replace($search, $replace, $uri);
if($uri !== $new){
$request = $request->withUri(new Uri($new));
}
}
}
// The user agent must be in the following format:
// LIBRARY-NAME/LIBRARY-VERSION LANGUAGE-NAME/LANGUAGE-VERSION [APP-NAME/APP-VERSION]
// See https://github.com/Nexmo/client-library-specification/blob/master/SPECIFICATION.md#reporting
$userAgent = [];
// Library name
$userAgent[] = 'nexmo-php/'.self::VERSION;
// Language name
$userAgent[] = 'php/'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;
// If we have an app set, add that to the UA
if (isset($this->options['app'])) {
$app = $this->options['app'];
$userAgent[] = $app['name'].'/'.$app['version'];
}
// Set the header. Build by joining all the parts we have with a space
$request = $request->withHeader('User-Agent', implode(" ", $userAgent));
$response = $this->client->sendRequest($request);
return $response;
}
protected function validateAppOptions($app) {
$disallowedCharacters = ['/', ' ', "\t", "\n"];
foreach (['name', 'version'] as $key) {
if (!isset($app[$key])) {
throw new \InvalidArgumentException('app.'.$key.' has not been set');
}
foreach ($disallowedCharacters as $char) {
if (strpos($app[$key], $char) !== false) {
throw new \InvalidArgumentException('app.'.$key.' cannot contain the '.$char.' character');
}
}
}
}
public function serialize(EntityInterface $entity)
{
if($entity instanceof Verification){
return $this->verify()->serialize($entity);
}
throw new \RuntimeException('unknown class `' . get_class($entity) . '``');
}
public function unserialize($entity)
{
if(is_string($entity)){
$entity = unserialize($entity);
}
if($entity instanceof Verification){
return $this->verify()->unserialize($entity);
}
throw new \RuntimeException('unknown class `' . get_class($entity) . '``');
}
public function __call($name, $args)
{
if(!$this->factory->hasApi($name)){
throw new \RuntimeException('no api namespace found: ' . $name);
}
$collection = $this->factory->getApi($name);
if(empty($args)){
return $collection;
}
return call_user_func_array($collection, $args);
}
public function __get($name)
{
if(!$this->factory->hasApi($name)){
throw new \RuntimeException('no api namespace found: ' . $name);
}
return $this->factory->getApi($name);
}
protected static function requiresBasicAuth(\Psr\Http\Message\RequestInterface $request)
{
$path = $request->getUri()->getPath();
$isSecretManagementEndpoint = strpos($path, '/accounts') === 0 && strpos($path, '/secrets') !== false;
$isApplicationV2 = strpos($path, '/v2/applications') === 0;
return $isSecretManagementEndpoint || $isApplicationV2;
}
protected static function requiresAuthInUrlNotBody(\Psr\Http\Message\RequestInterface $request)
{
$path = $request->getUri()->getPath();
$isRedactEndpoint = strpos($path, '/v1/redact') === 0;
return $isRedactEndpoint;
}
protected function needsKeypairAuthentication(\Psr\Http\Message\RequestInterface $request)
{
$path = $request->getUri()->getPath();
$isCallEndpoint = strpos($path, '/v1/calls') === 0;
$isRecordingUrl = strpos($path, '/v1/files') === 0;
$isStitchEndpoint = strpos($path, '/beta/conversation') === 0;
$isUserEndpoint = strpos($path, '/beta/users') === 0;
return $isCallEndpoint || $isRecordingUrl || $isStitchEndpoint || $isUserEndpoint;
}
}