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.
 
 

504 lines
13 KiB

<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\authclient;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidParamException;
use Yii;
use yii\helpers\Json;
/**
* BaseClient is a base class for the OAuth clients.
*
* @see http://oauth.net/
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
abstract class BaseOAuth extends Component
{
const CONTENT_TYPE_JSON = 'json'; // JSON format
const CONTENT_TYPE_URLENCODED = 'urlencoded'; // urlencoded query string, like name1=value1&name2=value2
const CONTENT_TYPE_XML = 'xml'; // XML format
const CONTENT_TYPE_AUTO = 'auto'; // attempts to determine format automatically
/**
* @var string protocol version.
*/
public $version = '1.0';
/**
* @var string URL, which user will be redirected after authentication at the OAuth provider web site.
* Note: this should be absolute URL (with http:// or https:// leading).
* By default current URL will be used.
*/
private $_returnUrl = '';
/**
* @var string API base URL.
*/
public $apiBaseUrl = '';
/**
* @var string authorize URL.
*/
public $authUrl = '';
/**
* @var string auth request scope.
*/
public $scope = '';
/**
* @var array cURL request options. Option values from this field will overwrite corresponding
* values from {@link defaultCurlOptions()}.
*/
private $_curlOptions = [];
/**
* @var OAuthToken|array access token instance or its array configuration.
*/
private $_accessToken = null;
/**
* @var signature\BaseMethod|array signature method instance or its array configuration.
*/
private $_signatureMethod = [];
/**
* @param string $returnUrl return URL
*/
public function setReturnUrl($returnUrl)
{
$this->_returnUrl = $returnUrl;
}
/**
* @return string return URL.
*/
public function getReturnUrl()
{
if (empty($this->_returnUrl)) {
$this->_returnUrl = $this->defaultReturnUrl();
}
return $this->_returnUrl;
}
/**
* @param array $curlOptions cURL options.
*/
public function setCurlOptions(array $curlOptions)
{
$this->_curlOptions = $curlOptions;
}
/**
* @return array cURL options.
*/
public function getCurlOptions()
{
return $this->_curlOptions;
}
/**
* @param array|OAuthToken $token
*/
public function setAccessToken($token)
{
if (!is_object($token)) {
$token = $this->createToken($token);
}
$this->_accessToken = $token;
$this->saveAccessToken($token);
}
/**
* @return OAuthToken auth token instance.
*/
public function getAccessToken()
{
if (!is_object($this->_accessToken)) {
$this->_accessToken = $this->restoreAccessToken();
}
return $this->_accessToken;
}
/**
* @param array|signature\BaseMethod $signatureMethod signature method instance or its array configuration.
* @throws InvalidParamException on wrong argument.
*/
public function setSignatureMethod($signatureMethod)
{
if (!is_object($signatureMethod) && !is_array($signatureMethod)) {
throw new InvalidParamException('"' . get_class($this) . '::signatureMethod" should be instance of "\yii\autclient\signature\BaseMethod" or its array configuration. "' . gettype($signatureMethod) . '" has been given.');
}
$this->_signatureMethod = $signatureMethod;
}
/**
* @return signature\BaseMethod signature method instance.
*/
public function getSignatureMethod()
{
if (!is_object($this->_signatureMethod)) {
$this->_signatureMethod = $this->createSignatureMethod($this->_signatureMethod);
}
return $this->_signatureMethod;
}
/**
* Composes default {@link returnUrl} value.
* @return string return URL.
*/
protected function defaultReturnUrl()
{
return Yii::$app->getRequest()->getAbsoluteUrl();
}
/**
* Sends HTTP request.
* @param string $method request type.
* @param string $url request URL.
* @param array $params request params.
* @return array response.
* @throws Exception on failure.
*/
protected function sendRequest($method, $url, array $params = [])
{
$curlOptions = $this->mergeCurlOptions(
$this->defaultCurlOptions(),
$this->getCurlOptions(),
array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_URL => $url,
),
$this->composeRequestCurlOptions(strtoupper($method), $url, $params)
);
$curlResource = curl_init();
foreach ($curlOptions as $option => $value) {
curl_setopt($curlResource, $option, $value);
}
$response = curl_exec($curlResource);
$responseHeaders = curl_getinfo($curlResource);
// check cURL error
$errorNumber = curl_errno($curlResource);
$errorMessage = curl_error($curlResource);
curl_close($curlResource);
if ($errorNumber > 0) {
throw new Exception('Curl error requesting "' . $url . '": #' . $errorNumber . ' - ' . $errorMessage);
}
if ($responseHeaders['http_code'] != 200) {
throw new Exception('Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response);
}
return $this->processResponse($response, $this->determineContentTypeByHeaders($responseHeaders));
}
/**
* Merge CUrl options.
* If each options array has an element with the same key value, the latter
* will overwrite the former.
* @param array $options1 options to be merged to.
* @param array $options2 options to be merged from. You can specify additional
* arrays via third argument, fourth argument etc.
* @return array merged options (the original options are not changed.)
*/
protected function mergeCurlOptions($options1, $options2)
{
$args = func_get_args();
$res = array_shift($args);
while (!empty($args)) {
$next = array_shift($args);
foreach ($next as $k => $v) {
$res[$k]=$v;
}
}
return $res;
}
/**
* Returns default cURL options.
* @return array cURL options.
*/
protected function defaultCurlOptions()
{
return [
CURLOPT_USERAGENT => Yii::$app->name . ' OAuth ' . $this->version . ' Client',
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false,
];
}
/**
* Processes raw response converting it to actual data.
* @param string $rawResponse raw response.
* @param string $contentType response content type.
* @throws Exception on failure.
* @return array actual response.
*/
protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO)
{
if (empty($rawResponse)) {
return [];
}
switch ($contentType) {
case self::CONTENT_TYPE_AUTO: {
$contentType = $this->determineContentTypeByRaw($rawResponse);
if ($contentType == self::CONTENT_TYPE_AUTO) {
throw new Exception('Unable to determine response content type automatically.');
}
$response = $this->processResponse($rawResponse, $contentType);
break;
}
case self::CONTENT_TYPE_JSON: {
$response = Json::decode($rawResponse, true);
if (isset($response['error'])) {
throw new Exception('Response error: ' . $response['error']);
}
break;
}
case self::CONTENT_TYPE_URLENCODED: {
$response = [];
parse_url($rawResponse, $response);
break;
}
case self::CONTENT_TYPE_XML: {
$response = $this->convertXmlToArray($rawResponse);
break;
}
default: {
throw new Exception('Unknown response type "' . $contentType . '".');
}
}
return $response;
}
/**
* Converts XML document to array.
* @param string|\SimpleXMLElement $xml xml to process.
* @return array XML array representation.
*/
protected function convertXmlToArray($xml)
{
if (!is_object($xml)) {
$xml = simplexml_load_string($xml);
}
$result = (array)$xml;
foreach ($result as $key => $value) {
if (is_object($value)) {
$result[$key] = $this->convertXmlToArray($value);
}
}
return $result;
}
/**
* Attempts to determine HTTP request content type by headers.
* @param array $headers request headers.
* @return string content type.
*/
protected function determineContentTypeByHeaders(array $headers)
{
if (isset($headers['content_type'])) {
if (stripos($headers['content_type'], 'json') !== false) {
return self::CONTENT_TYPE_JSON;
}
if (stripos($headers['content_type'], 'urlencoded') !== false) {
return self::CONTENT_TYPE_URLENCODED;
}
if (stripos($headers['content_type'], 'xml') !== false) {
return self::CONTENT_TYPE_XML;
}
}
return self::CONTENT_TYPE_AUTO;
}
/**
* Attempts to determine the content type from raw content.
* @param string $rawContent raw response content.
* @return string response type.
*/
protected function determineContentTypeByRaw($rawContent)
{
if (preg_match('/^\\{.*\\}$/is', $rawContent)) {
return self::CONTENT_TYPE_JSON;
}
if (preg_match('/^[^=|^&]+=[^=|^&]+(&[^=|^&]+=[^=|^&]+)*$/is', $rawContent)) {
return self::CONTENT_TYPE_URLENCODED;
}
if (preg_match('/^<.*>$/is', $rawContent)) {
return self::CONTENT_TYPE_XML;
}
return self::CONTENT_TYPE_AUTO;
}
/**
* Creates signature method instance from its configuration.
* @param array $signatureMethodConfig signature method configuration.
* @return signature\BaseMethod signature method instance.
*/
protected function createSignatureMethod(array $signatureMethodConfig)
{
if (!array_key_exists('class', $signatureMethodConfig)) {
$signatureMethodConfig['class'] = signature\HmacSha1::className();
}
return Yii::createObject($signatureMethodConfig);
}
/**
* Creates token from its configuration.
* @param array $tokenConfig token configuration.
* @return OAuthToken token instance.
*/
protected function createToken(array $tokenConfig = [])
{
if (!array_key_exists('class', $tokenConfig)) {
$tokenConfig['class'] = OAuthToken::className();
}
return Yii::createObject($tokenConfig);
}
/**
* Composes URL from base URL and GET params.
* @param string $url base URL.
* @param array $params GET params.
* @return string composed URL.
*/
protected function composeUrl($url, array $params = [])
{
if (strpos($url, '?') === false) {
$url .= '?';
} else {
$url .= '&';
}
$url .= http_build_query($params, '', '&', PHP_QUERY_RFC3986);
return $url;
}
/**
* Saves token as persistent state.
* @param OAuthToken $token auth token
* @return static self reference.
*/
protected function saveAccessToken(OAuthToken $token)
{
return $this->setState('token', $token);
}
/**
* Restores access token.
* @return OAuthToken auth token.
*/
protected function restoreAccessToken()
{
$token = $this->getState('token');
if (is_object($token)) {
/* @var $token OAuthToken */
if ($token->getIsExpired()) {
$token = $this->refreshAccessToken($token);
}
}
return $token;
}
/**
* Sets persistent state.
* @param string $key state key.
* @param mixed $value state value
* @return static self reference.
*/
protected function setState($key, $value)
{
$session = Yii::$app->getSession();
$key = $this->getStateKeyPrefix() . $key;
$session->set($key, $value);
return $this;
}
/**
* Returns persistent state value.
* @param string $key state key.
* @return mixed state value.
*/
protected function getState($key)
{
$session = Yii::$app->getSession();
$key = $this->getStateKeyPrefix() . $key;
$value = $session->get($key);
return $value;
}
/**
* Removes persistent state value.
* @param string $key state key.
* @return boolean success.
*/
protected function removeState($key)
{
$session = Yii::$app->getSession();
$key = $this->getStateKeyPrefix() . $key;
$session->remove($key);
return true;
}
/**
* Returns session key prefix, which is used to store internal states.
* @return string session key prefix.
*/
protected function getStateKeyPrefix()
{
return get_class($this) . '_' . sha1($this->authUrl) . '_';
}
/**
* Performs request to the OAuth API.
* @param string $apiSubUrl API sub URL, which will be append to [[apiBaseUrl]], or absolute API URL.
* @param string $method request method.
* @param array $params request parameters.
* @return array API response
* @throws Exception on failure.
*/
public function api($apiSubUrl, $method = 'GET', array $params = [])
{
if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) {
$url = $apiSubUrl;
} else {
$url = $this->apiBaseUrl . '/' . $apiSubUrl;
}
$accessToken = $this->getAccessToken();
if (!is_object($accessToken) || !$accessToken->getIsValid()) {
throw new Exception('Invalid access token.');
}
return $this->apiInternal($accessToken, $url, $method, $params);
}
/**
* Composes HTTP request CUrl options, which will be merged with the default ones.
* @param string $method request type.
* @param string $url request URL.
* @param array $params request params.
* @return array CUrl options.
* @throws Exception on failure.
*/
abstract protected function composeRequestCurlOptions($method, $url, array $params);
/**
* Gets new auth token to replace expired one.
* @param OAuthToken $token expired auth token.
* @return OAuthToken new auth token.
*/
abstract public function refreshAccessToken(OAuthToken $token);
/**
* Performs request to the OAuth API.
* @param OAuthToken $accessToken actual access token.
* @param string $url absolute API URL.
* @param string $method request method.
* @param array $params request parameters.
* @return array API response.
* @throws Exception on failure.
*/
abstract protected function apiInternal($accessToken, $url, $method, array $params);
}