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.
		
		
		
		
		
			
		
			
				
					
					
						
							503 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
	
	
							503 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\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 BaseClient implements ClientInterface | |
| { | |
| 	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; | |
| 	/** | |
| 	 * @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 ($this->_returnUrl === null) { | |
| 			$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_str($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); | |
| } |