From be554a1798c8f42de1c1e716c6a32460b9134d1b Mon Sep 17 00:00:00 2001 From: James ZHANG <1631685+TheNorthMemory@users.noreply.github.com> Date: Fri, 8 Jul 2022 00:21:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E9=87=8F=E4=BB=A3=E7=A0=81=E4=BF=AE?= =?UTF-8?q?=E5=9E=8B=E5=8F=8A=E5=8F=AF=E8=83=BD=E7=9A=84bug=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3:=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 遵循`RFC6749`规范,引入命名空间常量`RFC6749_ABNF_*`定义,从代码层面即可直观感受各`Providers`对规范的遵循度; 2. 在抽象`User`类,引入命名空间常量`ABNF_*`定义,直观感受对各厂商的授权信息抽象; 3. 在工厂抽象类中,引入命名空间常量`ABNF_*`定义,直观感受本LIB对`RFC6749`及厂商实现的抽象; 4. 调优`Psr\Http\Message\ResponseInterface::getBody()`获取返回值可能引发的流偏移而拿不到返回文本的潜在问题; 5. 使用相对路径的命名空间导入功能,对代码做了一点点修型; 6. 扔掉了下游包抛送的异常注释,代码注释仅专注于本LIB抛送的异常定义; 7. 支付宝&钉钉的`scope`拼接修型; 8. BC: 大小写`Exceptions\Feishu`调整为`Exceptions\FeiShu`; 9. BC: 部分抛送的异常调整为恰当类型; --- src/Config.php | 19 ++- src/Contracts/FactoryInterface.php | 10 +- src/Contracts/ProviderInterface.php | 46 ++++- src/Contracts/UserInterface.php | 20 +++ .../FeiShu/InvalidTicketException.php | 9 + .../Feishu/InvalidTicketException.php | 9 - src/Exceptions/InvalidArgumentException.php | 2 +- src/Providers/Alipay.php | 113 ++++++------- src/Providers/Azure.php | 35 ++-- src/Providers/Baidu.php | 53 +++--- src/Providers/Base.php | 126 +++++++------- src/Providers/DingTalk.php | 96 ++++++----- src/Providers/DouYin.php | 74 ++++---- src/Providers/Douban.php | 42 ++--- src/Providers/Facebook.php | 71 ++++---- src/Providers/FeiShu.php | 158 ++++++++---------- src/Providers/Figma.php | 37 ++-- src/Providers/GitHub.php | 34 ++-- src/Providers/Gitee.php | 53 +++--- src/Providers/Google.php | 37 ++-- src/Providers/Line.php | 51 +++--- src/Providers/Linkedin.php | 54 +++--- src/Providers/OpenWeWork.php | 96 +++++------ src/Providers/Outlook.php | 35 ++-- src/Providers/QCloud.php | 77 ++++----- src/Providers/QQ.php | 73 ++++---- src/Providers/Taobao.php | 81 +++++---- src/Providers/Tapd.php | 87 +++++----- src/Providers/WeChat.php | 123 ++++++-------- src/Providers/WeWork.php | 127 ++++++-------- src/Providers/Weibo.php | 61 +++---- src/SocialiteManager.php | 67 ++++---- src/Traits/HasAttributes.php | 8 +- src/User.php | 43 +++-- tests/Providers/FeiShuTest.php | 12 +- tests/Providers/WechatTest.php | 3 + 36 files changed, 977 insertions(+), 1065 deletions(-) create mode 100644 src/Exceptions/FeiShu/InvalidTicketException.php delete mode 100644 src/Exceptions/Feishu/InvalidTicketException.php diff --git a/src/Config.php b/src/Config.php index 3dd76a0..f6516c0 100644 --- a/src/Config.php +++ b/src/Config.php @@ -3,8 +3,9 @@ namespace Overtrue\Socialite; use ArrayAccess; +use JsonSerializable; -class Config implements ArrayAccess, \JsonSerializable +class Config implements ArrayAccess, JsonSerializable { protected array $config; @@ -24,8 +25,8 @@ public function get(string $key, mixed $default = null): mixed return $config[$key]; } - foreach (explode('.', $key) as $segment) { - if (!is_array($config) || !array_key_exists($segment, $config)) { + foreach (\explode('.', $key) as $segment) { + if (!\is_array($config) || !\array_key_exists($segment, $config)) { return $default; } $config = $config[$segment]; @@ -36,18 +37,18 @@ public function get(string $key, mixed $default = null): mixed public function set(string $key, $value): mixed { - $keys = explode('.', $key); + $keys = \explode('.', $key); $config = &$this->config; - while (count($keys) > 1) { - $key = array_shift($keys); - if (!isset($config[$key]) || !is_array($config[$key])) { + while (\count($keys) > 1) { + $key = \array_shift($keys); + if (!isset($config[$key]) || !\is_array($config[$key])) { $config[$key] = []; } $config = &$config[$key]; } - $config[array_shift($keys)] = $value; + $config[\array_shift($keys)] = $value; return $config; } @@ -59,7 +60,7 @@ public function has(string $key): bool public function offsetExists(mixed $offset): bool { - return array_key_exists($offset, $this->config); + return \array_key_exists($offset, $this->config); } public function offsetGet(mixed $offset): mixed diff --git a/src/Contracts/FactoryInterface.php b/src/Contracts/FactoryInterface.php index 943c11c..b0491f9 100644 --- a/src/Contracts/FactoryInterface.php +++ b/src/Contracts/FactoryInterface.php @@ -2,12 +2,12 @@ namespace Overtrue\Socialite\Contracts; +const ABNF_APP_ID = 'app_id'; +const ABNF_APP_SECRET = 'app_secret'; +const ABNF_OPEN_ID = 'open_id'; +const ABNF_TOKEN = 'token'; + interface FactoryInterface { - /** - * @param string $driver - * - * @return \Overtrue\Socialite\Contracts\ProviderInterface - */ public function create(string $driver): ProviderInterface; } diff --git a/src/Contracts/ProviderInterface.php b/src/Contracts/ProviderInterface.php index d02df93..28deefb 100644 --- a/src/Contracts/ProviderInterface.php +++ b/src/Contracts/ProviderInterface.php @@ -2,6 +2,45 @@ namespace Overtrue\Socialite\Contracts; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.1 */ +const RFC6749_ABNF_CLIENT_ID = 'client_id'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.2 */ +const RFC6749_ABNF_CLIENT_SECRET = 'client_secret'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.3 */ +const RFC6749_ABNF_RESPONSE_TYPE = 'response_type'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.4 */ +const RFC6749_ABNF_SCOPE = 'scope'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.5 */ +const RFC6749_ABNF_STATE = 'state'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.6 */ +const RFC6749_ABNF_REDIRECT_URI = 'redirect_uri'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.7 */ +const RFC6749_ABNF_ERROR = 'error'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.8 */ +const RFC6749_ABNF_ERROR_DESCRIPTION = 'error_description'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.9 */ +const RFC6749_ABNF_ERROR_URI = 'error_uri'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.10 */ +const RFC6749_ABNF_GRANT_TYPE = 'grant_type'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.11 */ +const RFC6749_ABNF_CODE = 'code'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.12 */ +const RFC6749_ABNF_ACCESS_TOKEN = 'access_token'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.13 */ +const RFC6749_ABNF_TOKEN_TYPE = 'token_type'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.14 */ +const RFC6749_ABNF_EXPIRES_IN = 'expires_in'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.15 */ +const RFC6749_ABNF_USERNAME = 'username'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.16 */ +const RFC6749_ABNF_PASSWORD = 'password'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#appendix-A.17 */ +const RFC6749_ABNF_REFRESH_TOKEN = 'refresh_token'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 */ +const RFC6749_ABNF_AUTHORATION_CODE = 'authorization_code'; +/** @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.2 */ +const RFC6749_ABNF_CLIENT_CREDENTIALS = 'client_credentials'; + interface ProviderInterface { public function redirect(?string $redirectUrl = null): string; @@ -10,7 +49,10 @@ public function userFromCode(string $code): UserInterface; public function userFromToken(string $token): UserInterface; - public function withState(string $state): ProviderInterface; + public function withState(string $state): self; - public function scopes(array $scopes): ProviderInterface; + /** + * @param string[] $scopes + */ + public function scopes(array $scopes): self; } diff --git a/src/Contracts/UserInterface.php b/src/Contracts/UserInterface.php index c6278cc..de8336b 100644 --- a/src/Contracts/UserInterface.php +++ b/src/Contracts/UserInterface.php @@ -2,6 +2,12 @@ namespace Overtrue\Socialite\Contracts; +const ABNF_ID = 'id'; +const ABNF_NAME = 'name'; +const ABNF_NICKNAME = 'nickname'; +const ABNF_EMAIL = 'email'; +const ABNF_AVATAR = 'avatar'; + interface UserInterface { public function getId(): mixed; @@ -19,4 +25,18 @@ public function getAccessToken(): ?string; public function getRefreshToken(): ?string; public function getExpiresIn(): ?int; + + public function getProvider(): ProviderInterface; + + public function setRefreshToken(?string $refreshToken): self; + + public function setExpiresIn(int $expiresIn): self; + + public function setTokenResponse(array $response): self; + + public function setProvider(ProviderInterface $provider): self; + + public function setRaw(array $user): self; + + public function setAccessToken(string $token): self; } diff --git a/src/Exceptions/FeiShu/InvalidTicketException.php b/src/Exceptions/FeiShu/InvalidTicketException.php new file mode 100644 index 0000000..1704318 --- /dev/null +++ b/src/Exceptions/FeiShu/InvalidTicketException.php @@ -0,0 +1,9 @@ + $token]; $params['sign'] = $this->generateSign($params); - $response = $this->getHttpClient()->post( + $responseInstance = $this->getHttpClient()->post( $this->baseUrl, [ 'form_params' => $params, 'headers' => [ - "content-type" => "application/x-www-form-urlencoded;charset=utf-8", + 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8', ], ] ); - $response = json_decode($response->getBody()->getContents(), true); + $response = $this->fromJsonBody($responseInstance); - if (!empty($response['error_response']) || empty($response['alipay_user_info_share_response'])) { - throw new \InvalidArgumentException('You have error! ' . \json_encode($response, JSON_UNESCAPED_UNICODE)); + if (!empty($response['error_response'] ?? null) || empty($response['alipay_user_info_share_response'] ?? [])) { + throw new Exceptions\BadRequestException((string) $responseInstance->getBody()); } - return $response['alipay_user_info_share_response']; + return $response['alipay_user_info_share_response'] ?? []; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $user['user_id'] ?? null, - 'name' => $user['nick_name'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => $user['email'] ?? null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user['user_id'] ?? null, + Contracts\ABNF_NAME => $user['nick_name'] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + ]); } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @throws Exceptions\BadRequestException */ public function tokenFromCode(string $code): array { - $response = $this->getHttpClient()->post( + $responseInstance = $this->getHttpClient()->post( $this->getTokenUrl(), [ 'form_params' => $this->getTokenFields($code), 'headers' => [ - "content-type" => "application/x-www-form-urlencoded;charset=utf-8", + 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8', ], ] ); - $response = json_decode($response->getBody()->getContents(), true); + $response = $this->fromJsonBody($responseInstance); if (!empty($response['error_response'])) { - throw new \InvalidArgumentException('You have error! ' . json_encode($response, JSON_UNESCAPED_UNICODE)); + throw new Exceptions\BadRequestException((string)$responseInstance->getBody()); } return $this->normalizeAccessTokenResponse($response['alipay_system_oauth_token_response']); } /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @throws Exceptions\InvalidArgumentException */ protected function getCodeFields(): array { if (empty($this->redirectUrl)) { - throw new InvalidArgumentException('Please set same redirect URL like your Alipay Official Admin'); + throw new Exceptions\InvalidArgumentException('Please set the correct redirect URL refer which was on the Alipay Official Admin pannel.'); } - $fields = array_merge( + $fields = \array_merge( [ - 'app_id' => $this->getConfig()->get('client_id') ?? $this->getConfig()->get('app_id'), - 'scope' => implode(',', $this->scopes), - 'redirect_uri' => $this->redirectUrl, + Contracts\ABNF_APP_ID => $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_ID) ?? $this->getConfig()->get(Contracts\ABNF_APP_ID), + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, ], $this->parameters ); @@ -131,21 +127,19 @@ protected function getCodeFields(): array return $fields; } - /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException - */ #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { $params = $this->getPublicFields('alipay.system.oauth.token'); $params += [ - 'code' => $code, - 'grant_type' => 'authorization_code', + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, ]; $params['sign'] = $this->generateSign($params); @@ -153,68 +147,67 @@ protected function getTokenFields(string $code): array } #[ArrayShape([ - 'app_id' => "array|mixed", - 'format' => "string", - 'charset' => "string", - 'sign_type' => "string", - 'method' => "string", - 'timestamp' => "string", - 'version' => "string" + Contracts\ABNF_APP_ID => 'string', + 'format' => 'string', + 'charset' => 'string', + 'sign_type' => 'string', + 'method' => 'string', + 'timestamp' => 'string', + 'version' => 'string' ])] public function getPublicFields(string $method): array { return [ - 'app_id' => $this->getConfig()->get('client_id') ?? $this->getConfig()->get('app_id'), + Contracts\ABNF_APP_ID => $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_ID) ?? $this->getConfig()->get(Contracts\ABNF_APP_ID), 'format' => $this->format, 'charset' => $this->postCharset, 'sign_type' => $this->signType, 'method' => $method, - 'timestamp' => date('Y-m-d H:m:s'), + 'timestamp' => (new \DateTime('now', new \DateTimeZone('Asia/Shanghai')))->format('Y-m-d H:i:s'), 'version' => $this->apiVersion, ]; } /** - * @throws InvalidArgumentException * @see https://opendocs.alipay.com/open/289/105656 */ protected function generateSign($params): string { - ksort($params); + \ksort($params); return $this->signWithSHA256RSA($this->buildParams($params), $this->getConfig()->get('rsa_private_key')); } /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @throws Exceptions\InvalidArgumentException */ protected function signWithSHA256RSA(string $signContent, string $key): string { if (empty($key)) { - throw new InvalidArgumentException('no RSA private key set.'); + throw new Exceptions\InvalidArgumentException('no RSA private key set.'); } $key = "-----BEGIN RSA PRIVATE KEY-----\n" . - chunk_split($key, 64, "\n") . + \chunk_split($key, 64, "\n") . "-----END RSA PRIVATE KEY-----"; - openssl_sign($signContent, $signValue, $key, OPENSSL_ALGO_SHA256); + \openssl_sign($signContent, $signValue, $key, \OPENSSL_ALGO_SHA256); - return base64_encode($signValue); + return \base64_encode($signValue); } public static function buildParams(array $params, bool $urlencode = false, array $except = ['sign']): string { $param_str = ''; foreach ($params as $k => $v) { - if (in_array($k, $except)) { + if (\in_array($k, $except)) { continue; } $param_str .= $k . '='; - $param_str .= $urlencode ? rawurlencode($v) : $v; + $param_str .= $urlencode ? \rawurlencode($v) : $v; $param_str .= '&'; } - return rtrim($param_str, '&'); + return \rtrim($param_str, '&'); } } diff --git a/src/Providers/Azure.php b/src/Providers/Azure.php index 29df0f3..26bee1f 100644 --- a/src/Providers/Azure.php +++ b/src/Providers/Azure.php @@ -4,12 +4,13 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; class Azure extends Base { public const NAME = 'azure'; + protected array $scopes = ['User.Read']; protected string $scopeSeparator = ' '; @@ -20,7 +21,7 @@ protected function getAuthUrl(): string protected function getBaseUrl(): string { - return 'https://login.microsoftonline.com/'.$this->config["tenant"]; + return 'https://login.microsoftonline.com/' . $this->config['tenant']; } protected function getTokenUrl(): string @@ -28,45 +29,43 @@ protected function getTokenUrl(): string return $this->getBaseUrl() . '/oauth2/v2.0/token'; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->get( 'https://graph.microsoft.com/v1.0/me', ['headers' => [ 'Accept' => 'application/json', - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, ], ] ); - return \json_decode($response->getBody()->getContents(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => null, - 'name' => $user['displayName'] ?? null, - 'email' => $user['userPrincipalName'] ?? null, - 'avatar' => null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => null, + Contracts\ABNF_NAME => $user['displayName'] ?? null, + Contracts\ABNF_EMAIL => $user['userPrincipalName'] ?? null, + Contracts\ABNF_AVATAR => null, ]); } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { return parent::getTokenFields($code) + [ - 'grant_type' => 'authorization_code', + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, ]; } } diff --git a/src/Providers/Baidu.php b/src/Providers/Baidu.php index c183d1f..4b0d4b7 100644 --- a/src/Providers/Baidu.php +++ b/src/Providers/Baidu.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,6 +13,7 @@ class Baidu extends Base { public const NAME = 'baidu'; + protected string $baseUrl = 'https://openapi.baidu.com'; protected string $version = '2.0'; protected array $scopes = ['basic']; @@ -40,12 +41,12 @@ protected function getAuthUrl(): string protected function getCodeFields(): array { return [ - 'response_type' => 'code', - 'client_id' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'display' => $this->display, - ] + $this->parameters; + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, + Contracts\RFC6749_ABNF_CLIENT_ID => $this->getClientId(), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + 'display' => $this->display, + ] + $this->parameters; } protected function getTokenUrl(): string @@ -54,26 +55,26 @@ protected function getTokenUrl(): string } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [ + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + ]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { $response = $this->getHttpClient()->get( $this->baseUrl . '/rest/' . $this->version . '/passport/users/getInfo', [ 'query' => [ - 'access_token' => $token, + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, ], 'headers' => [ 'Accept' => 'application/json', @@ -81,20 +82,18 @@ protected function getUserByToken(string $token): array ] ); - return json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $user['userid'] ?? null, - 'nickname' => $user['realname'] ?? null, - 'name' => $user['username'] ?? null, - 'email' => '', - 'avatar' => $user['portrait'] ? 'http://tb.himg.baidu.com/sys/portraitn/item/' . $user['portrait'] : null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user['userid'] ?? null, + Contracts\ABNF_NICKNAME => $user['realname'] ?? null, + Contracts\ABNF_NAME => $user['username'] ?? null, + Contracts\ABNF_EMAIL => '', + Contracts\ABNF_AVATAR => $user['portrait'] ? 'http://tb.himg.baidu.com/sys/portraitn/item/' . $user['portrait'] : null, + ]); } } diff --git a/src/Providers/Base.php b/src/Providers/Base.php index 57c46ba..5279ff3 100644 --- a/src/Providers/Base.php +++ b/src/Providers/Base.php @@ -3,15 +3,15 @@ namespace Overtrue\Socialite\Providers; use GuzzleHttp\Client as GuzzleClient; -use GuzzleHttp\Psr7\Stream; +use GuzzleHttp\Utils; +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\StreamInterface; use JetBrains\PhpStorm\ArrayShape; use Overtrue\Socialite\Config; -use Overtrue\Socialite\Contracts\ProviderInterface; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\MethodDoesNotSupportException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; -abstract class Base implements ProviderInterface +abstract class Base implements Contracts\ProviderInterface { public const NAME = null; @@ -24,9 +24,9 @@ abstract class Base implements ProviderInterface protected GuzzleClient $httpClient; protected array $guzzleOptions = []; protected int $encodingType = PHP_QUERY_RFC1738; - protected string $expiresInKey = 'expires_in'; - protected string $accessTokenKey = 'access_token'; - protected string $refreshTokenKey = 'refresh_token'; + protected string $expiresInKey = Contracts\RFC6749_ABNF_EXPIRES_IN; + protected string $accessTokenKey = Contracts\RFC6749_ABNF_ACCESS_TOKEN; + protected string $refreshTokenKey = Contracts\RFC6749_ABNF_REFRESH_TOKEN; public function __construct(array $config) { @@ -35,23 +35,23 @@ public function __construct(array $config) // set scopes if ($this->config->has('scopes') && is_array($this->config->get('scopes'))) { $this->scopes = $this->getConfig()->get('scopes'); - } elseif ($this->config->has('scope') && is_string($this->getConfig()->get('scope'))) { - $this->scopes = array($this->getConfig()->get('scope')); + } elseif ($this->config->has(Contracts\RFC6749_ABNF_SCOPE) && is_string($this->getConfig()->get(Contracts\RFC6749_ABNF_SCOPE))) { + $this->scopes = array($this->getConfig()->get(Contracts\RFC6749_ABNF_SCOPE)); } - // normalize 'client_id' - if (!$this->config->has('client_id')) { - $id = $this->config->get('app_id'); + // normalize Contracts\RFC6749_ABNF_CLIENT_ID + if (!$this->config->has(Contracts\RFC6749_ABNF_CLIENT_ID)) { + $id = $this->config->get(Contracts\ABNF_APP_ID); if (null != $id) { - $this->config->set('client_id', $id); + $this->config->set(Contracts\RFC6749_ABNF_CLIENT_ID, $id); } } - // normalize 'client_secret' - if (!$this->config->has('client_secret')) { - $secret = $this->config->get('app_secret'); + // normalize Contracts\RFC6749_ABNF_CLIENT_SECRET + if (!$this->config->has(Contracts\RFC6749_ABNF_CLIENT_SECRET)) { + $secret = $this->config->get(Contracts\ABNF_APP_SECRET); if (null != $secret) { - $this->config->set('client_secret', $secret); + $this->config->set(Contracts\RFC6749_ABNF_CLIENT_SECRET, $secret); } } @@ -68,7 +68,7 @@ abstract protected function getTokenUrl(): string; abstract protected function getUserByToken(string $token): array; - abstract protected function mapUserToObject(array $user): UserInterface; + abstract protected function mapUserToObject(array $user): Contracts\UserInterface; public function redirect(?string $redirectUrl = null): string { @@ -79,11 +79,7 @@ public function redirect(?string $redirectUrl = null): string return $this->getAuthUrl(); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ - public function userFromCode(string $code): UserInterface + public function userFromCode(string $code): Contracts\UserInterface { $tokenResponse = $this->tokenFromCode($code); $user = $this->userFromToken($tokenResponse[$this->accessTokenKey]); @@ -93,16 +89,13 @@ public function userFromCode(string $code): UserInterface ->setTokenResponse($tokenResponse); } - public function userFromToken(string $token): UserInterface + public function userFromToken(string $token): Contracts\UserInterface { $user = $this->getUserByToken($token); return $this->mapUserToObject($user)->setProvider($this)->setRaw($user)->setAccessToken($token); } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException|\GuzzleHttp\Exception\GuzzleException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->post( @@ -119,35 +112,35 @@ public function tokenFromCode(string $code): array } /** - * @throws \Overtrue\Socialite\Exceptions\MethodDoesNotSupportException + * @throws Exceptions\MethodDoesNotSupportException */ public function refreshToken(string $refreshToken) { - throw new MethodDoesNotSupportException('refreshToken does not support.'); + throw new Exceptions\MethodDoesNotSupportException('refreshToken does not support.'); } - public function withRedirectUrl(string $redirectUrl): ProviderInterface + public function withRedirectUrl(string $redirectUrl): Contracts\ProviderInterface { $this->redirectUrl = $redirectUrl; return $this; } - public function withState(string $state): ProviderInterface + public function withState(string $state): Contracts\ProviderInterface { $this->state = $state; return $this; } - public function scopes(array $scopes): ProviderInterface + public function scopes(array $scopes): Contracts\ProviderInterface { $this->scopes = $scopes; return $this; } - public function with(array $parameters): ProviderInterface + public function with(array $parameters): Contracts\ProviderInterface { $this->parameters = $parameters; @@ -159,7 +152,7 @@ public function getConfig(): Config return $this->config; } - public function withScopeSeparator(string $scopeSeparator): ProviderInterface + public function withScopeSeparator(string $scopeSeparator): Contracts\ProviderInterface { $this->scopeSeparator = $scopeSeparator; @@ -168,12 +161,12 @@ public function withScopeSeparator(string $scopeSeparator): ProviderInterface public function getClientId(): ?string { - return $this->config->get('client_id'); + return $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID); } public function getClientSecret(): ?string { - return $this->config->get('client_secret'); + return $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET); } public function getHttpClient(): GuzzleClient @@ -181,7 +174,7 @@ public function getHttpClient(): GuzzleClient return $this->httpClient ?? new GuzzleClient($this->guzzleOptions); } - public function setGuzzleOptions(array $config): ProviderInterface + public function setGuzzleOptions(array $config): Contracts\ProviderInterface { $this->guzzleOptions = $config; @@ -195,28 +188,28 @@ public function getGuzzleOptions(): array protected function formatScopes(array $scopes, string $scopeSeparator): string { - return implode($scopeSeparator, $scopes); + return \implode($scopeSeparator, $scopes); } #[ArrayShape([ - 'client_id' => "null|string", - 'client_secret' => "null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string' ])] protected function getTokenFields(string $code): array { return [ - 'client_id' => $this->getClientId(), - 'client_secret' => $this->getClientSecret(), - 'code' => $code, - 'redirect_uri' => $this->redirectUrl, + Contracts\RFC6749_ABNF_CLIENT_ID => $this->getClientId(), + Contracts\RFC6749_ABNF_CLIENT_SECRET => $this->getClientSecret(), + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, ]; } protected function buildAuthUrlFromBase(string $url): string { - $query = $this->getCodeFields() + ($this->state ? ['state' => $this->state] : []); + $query = $this->getCodeFields() + ($this->state ? [Contracts\RFC6749_ABNF_STATE => $this->state] : []); return $url . '?' . \http_build_query($query, '', '&', $this->encodingType); } @@ -225,29 +218,29 @@ protected function getCodeFields(): array { $fields = array_merge( [ - 'client_id' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'response_type' => 'code', + Contracts\RFC6749_ABNF_CLIENT_ID => $this->getClientId(), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, ], $this->parameters ); if ($this->state) { - $fields['state'] = $this->state; + $fields[Contracts\RFC6749_ABNF_STATE] = $this->state; } return $fields; } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function normalizeAccessTokenResponse($response): array { - if ($response instanceof Stream) { - $response->rewind(); - $response = $response->getContents(); + if ($response instanceof StreamInterface) { + $response->tell() && $response->rewind(); + $response = (string) $response; } if (\is_string($response)) { @@ -255,17 +248,22 @@ protected function normalizeAccessTokenResponse($response): array } if (!\is_array($response)) { - throw new AuthorizeFailedException('Invalid token response', [$response]); + throw new Exceptions\AuthorizeFailedException('Invalid token response', [$response]); } if (empty($response[$this->accessTokenKey])) { - throw new AuthorizeFailedException('Authorize Failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE), $response); + throw new Exceptions\AuthorizeFailedException('Authorize Failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE), $response); } return $response + [ - 'access_token' => $response[$this->accessTokenKey], - 'refresh_token' => $response[$this->refreshTokenKey] ?? null, - 'expires_in' => \intval($response[$this->expiresInKey] ?? 0), - ]; + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $response[$this->accessTokenKey], + Contracts\RFC6749_ABNF_REFRESH_TOKEN => $response[$this->refreshTokenKey] ?? null, + Contracts\RFC6749_ABNF_EXPIRES_IN => \intval($response[$this->expiresInKey] ?? 0), + ]; + } + + protected function fromJsonBody(MessageInterface $response): array + { + return Utils::jsonDecode((string) $response->getBody(), true); } } diff --git a/src/Providers/DingTalk.php b/src/Providers/DingTalk.php index 2d7bc4b..4664a2f 100644 --- a/src/Providers/DingTalk.php +++ b/src/Providers/DingTalk.php @@ -3,7 +3,8 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -17,36 +18,42 @@ class DingTalk extends Base { public const NAME = 'dingtalk'; + protected string $getUserByCode = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'; protected array $scopes = ['snsapi_login']; + protected string $scopeSeparator = ' '; protected function getAuthUrl(): string { return $this->buildAuthUrlFromBase('https://oapi.dingtalk.com/connect/qrconnect'); } + /** + * @throws Exceptions\InvalidArgumentException + */ protected function getTokenUrl(): string { - throw new \InvalidArgumentException('not supported to get access token.'); + throw new Exceptions\InvalidArgumentException('not supported to get access token.'); } + /** + * @throws Exceptions\InvalidArgumentException + */ protected function getUserByToken(string $token): array { - throw new \InvalidArgumentException('Unable to use token get User.'); + throw new Exceptions\InvalidArgumentException('Unable to use token get User.'); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'name' => $user['nick'] ?? null, - 'nickname' => $user['nick'] ?? null, - 'id' => $user['openid'] ?? null, - 'email' => null, - 'avatar' => null, - ] - ); + return new User([ + Contracts\ABNF_NAME => $user['nick'] ?? null, + Contracts\ABNF_NICKNAME => $user['nick'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_OPEN_ID] ?? null, + Contracts\ABNF_EMAIL => null, + Contracts\ABNF_AVATAR => null, + ]); } protected function getCodeFields(): array @@ -54,9 +61,9 @@ protected function getCodeFields(): array return array_merge( [ 'appid' => $this->getClientId(), - 'response_type' => 'code', - 'scope' => implode($this->scopes), - 'redirect_uri' => $this->redirectUrl, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + Contracts\RFC6749_ABNF_CODE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, ], $this->parameters ); @@ -64,52 +71,51 @@ protected function getCodeFields(): array public function getClientId(): ?string { - return $this->getConfig()->get('app_id') ?? $this->getConfig()->get('appid') ?? $this->getConfig()->get('appId') - ?? $this->getConfig()->get('client_id'); + return $this->getConfig()->get(Contracts\ABNF_APP_ID) + ?? $this->getConfig()->get('appid') + ?? $this->getConfig()->get('appId') + ?? $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_ID); } public function getClientSecret(): ?string { - return $this->getConfig()->get('app_secret') ?? $this->getConfig()->get('appSecret') - ?? $this->getConfig()->get('client_secret'); + return $this->getConfig()->get(Contracts\ABNF_APP_SECRET) + ?? $this->getConfig()->get('appSecret') + ?? $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_SECRET); } protected function createSignature(int $time): string { - return base64_encode(hash_hmac('sha256', $time, $this->getClientSecret(), true)); + return \base64_encode(\hash_hmac('sha256', $time, $this->getClientSecret(), true)); } /** - * @throws \GuzzleHttp\Exception\GuzzleException * @see https://ding-doc.dingtalk.com/doc#/personnal/tmudue + * + * @throws Exceptions\BadRequestException */ - public function userFromCode(string $code): UserInterface + public function userFromCode(string $code): Contracts\UserInterface { - $time = (int)microtime(true) * 1000; - $queryParams = [ - 'accessKey' => $this->getClientId(), - 'timestamp' => $time, - 'signature' => $this->createSignature($time), - ]; - - $response = $this->getHttpClient()->post( - $this->getUserByCode . '?' . http_build_query($queryParams), - [ - 'json' => ['tmp_auth_code' => $code], - ] - ); - $response = \json_decode($response->getBody()->getContents(), true); + $time = (int)\microtime(true) * 1000; + + $responseInstance = $this->getHttpClient()->post($this->getUserByCode, [ + 'query' => [ + 'accessKey' => $this->getClientId(), + 'timestamp' => $time, + 'signature' => $this->createSignature($time), + ], + 'json' => ['tmp_auth_code' => $code], + ]); + $response = $this->fromJsonBody($responseInstance); if (0 != $response['errcode'] ?? 1) { - throw new \InvalidArgumentException('You get error: ' . json_encode($response, JSON_UNESCAPED_UNICODE)); + throw new Exceptions\BadRequestException((string)$responseInstance->getBody()); } - return new User( - [ - 'name' => $response['user_info']['nick'], - 'nickname' => $response['user_info']['nick'], - 'id' => $response['user_info']['openid'], - ] - ); + return new User([ + Contracts\ABNF_NAME => $response['user_info']['nick'], + Contracts\ABNF_NICKNAME => $response['user_info']['nick'], + Contracts\ABNF_ID => $response['user_info'][Contracts\ABNF_OPEN_ID], + ]); } } diff --git a/src/Providers/DouYin.php b/src/Providers/DouYin.php index 314de28..f5c9c6d 100644 --- a/src/Providers/DouYin.php +++ b/src/Providers/DouYin.php @@ -4,9 +4,8 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\InvalidArgumentException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -16,6 +15,7 @@ class DouYin extends Base { public const NAME = 'douyin'; + protected string $baseUrl = 'https://open.douyin.com'; protected array $scopes = ['user_info']; protected ?string $openId; @@ -26,18 +26,18 @@ protected function getAuthUrl(): string } #[ArrayShape([ - 'client_key' => "null|string", - 'redirect_uri' => "mixed", - 'scope' => "string", - 'response_type' => "string" + 'client_key' => 'null|string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_SCOPE => 'string', + Contracts\RFC6749_ABNF_RESPONSE_TYPE => 'string', ])] public function getCodeFields(): array { return [ 'client_key' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'response_type' => 'code', + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, ]; } @@ -47,8 +47,7 @@ protected function getTokenUrl(): string } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\AuthorizeFailedException * */ public function tokenFromCode(string $code): array @@ -60,72 +59,69 @@ public function tokenFromCode(string $code): array ] ); - $body = \json_decode($response->getBody()->getContents(), true) ?? []; + $body = $this->fromJsonBody($response); - if (empty($body['data']) || $body['data']['error_code'] != 0) { - throw new AuthorizeFailedException('Invalid token response', $body); + if (empty($body['data'] ?? null) || ($body['data']['error_code'] ?? -1) != 0) { + throw new Exceptions\AuthorizeFailedException('Invalid token response', $body); } - $this->withOpenId($body['data']['open_id']); + $this->withOpenId($body['data'][Contracts\ABNF_OPEN_ID]); return $this->normalizeAccessTokenResponse($body['data']); } #[ArrayShape([ - 'client_key' => "null|string", - 'client_secret' => "null|string", - 'code' => "string", - 'grant_type' => "string" + 'client_key' => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { return [ 'client_key' => $this->getClientId(), - 'client_secret' => $this->getClientSecret(), - 'code' => $code, - 'grant_type' => 'authorization_code', + Contracts\RFC6749_ABNF_CLIENT_SECRET => $this->getClientSecret(), + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, ]; } /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\InvalidArgumentException */ protected function getUserByToken(string $token): array { $userUrl = $this->baseUrl . '/oauth/userinfo/'; if (empty($this->openId)) { - throw new InvalidArgumentException('please set open_id before your query.'); + throw new Exceptions\InvalidArgumentException('please set the `open_id` before issue the API request.'); } $response = $this->getHttpClient()->get( $userUrl, [ 'query' => [ - 'access_token' => $token, - 'open_id' => $this->openId, + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, + Contracts\ABNF_OPEN_ID => $this->openId, ], ] ); - $body = \json_decode($response->getBody()->getContents(), true); + $body = $this->fromJsonBody($response); return $body['data'] ?? []; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $user['open_id'] ?? null, - 'name' => $user['nickname'] ?? null, - 'nickname' => $user['nickname'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => $user['email'] ?? null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user[Contracts\ABNF_OPEN_ID] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NICKNAME] ?? null, + Contracts\ABNF_NICKNAME => $user[Contracts\ABNF_NICKNAME] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + ]); } public function withOpenId(string $openId): self diff --git a/src/Providers/Douban.php b/src/Providers/Douban.php index 8270e89..72a0ef5 100644 --- a/src/Providers/Douban.php +++ b/src/Providers/Douban.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -25,56 +25,50 @@ protected function getTokenUrl(): string } /** - * @param string $token - * @param array|null $query - * - * @return array - * @throws \GuzzleHttp\Exception\GuzzleException + * @param string $token + * @param ?array $query */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->get('https://api.douban.com/v2/user/~me', [ 'headers' => [ - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, ], ]); - return json_decode($response->getBody()->getContents(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => $user['name'] ?? null, - 'name' => $user['name'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => null, ]); } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 'form_params' => $this->getTokenFields($code), ]); - return $this->normalizeAccessTokenResponse($response->getBody()->getContents()); + return $this->normalizeAccessTokenResponse($response->getBody()); } } diff --git a/src/Providers/Facebook.php b/src/Providers/Facebook.php index cac60d5..112baf1 100644 --- a/src/Providers/Facebook.php +++ b/src/Providers/Facebook.php @@ -3,7 +3,7 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -12,6 +12,7 @@ class Facebook extends Base { public const NAME = 'facebook'; + protected string $graphUrl = 'https://graph.facebook.com'; protected string $version = 'v3.3'; protected array $fields = ['first_name', 'last_name', 'email', 'gender', 'verified']; @@ -28,62 +29,52 @@ protected function getTokenUrl(): string return $this->graphUrl . '/oauth/access_token'; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ public function tokenFromCode(string $code): array { - $response = $this->getHttpClient()->get( - $this->getTokenUrl(), - [ - 'query' => $this->getTokenFields($code), - ] - ); + $response = $this->getHttpClient()->get($this->getTokenUrl(), [ + 'query' => $this->getTokenFields($code), + ]); return $this->normalizeAccessTokenResponse($response->getBody()); } /** - * @throws \GuzzleHttp\Exception\GuzzleException */ protected function getUserByToken(string $token, ?array $query = []): array { - $appSecretProof = hash_hmac('sha256', $token, $this->getConfig()->get('client_secret')); - $endpoint = $this->graphUrl . '/' . $this->version . '/me?access_token=' . $token . '&appsecret_proof=' . $appSecretProof . '&fields=' . - implode(',', $this->fields); - - $response = $this->getHttpClient()->get( - $endpoint, - [ - 'headers' => [ - 'Accept' => 'application/json', - ], - ] - ); - - return \json_decode($response->getBody(), true) ?? []; + $appSecretProof = \hash_hmac('sha256', $token, $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_SECRET)); + + $response = $this->getHttpClient()->get($this->graphUrl . '/' . $this->version . '/me', [ + 'query' => [ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, + 'appsecret_proof' => $appSecretProof, + 'fields' => $this->formatScopes($this->fields, $this->scopeSeparator) + ], + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - $userId = $user['id'] ?? null; + $userId = $user[Contracts\ABNF_ID] ?? null; $avatarUrl = $this->graphUrl . '/' . $this->version . '/' . $userId . '/picture'; $firstName = $user['first_name'] ?? null; $lastName = $user['last_name'] ?? null; - return new User( - [ - 'id' => $user['id'] ?? null, - 'nickname' => null, - 'name' => $firstName . ' ' . $lastName, - 'email' => $user['email'] ?? null, - 'avatar' => $userId ? $avatarUrl . '?type=normal' : null, - 'avatar_original' => $userId ? $avatarUrl . '?width=1920' : null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => null, + Contracts\ABNF_NAME => $firstName . ' ' . $lastName, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $userId ? $avatarUrl . '?type=normal' : null, + 'avatar_original' => $userId ? $avatarUrl . '?width=1920' : null, + ]); } protected function getCodeFields(): array @@ -97,14 +88,14 @@ protected function getCodeFields(): array return $fields; } - public function fields(array $fields): static + public function fields(array $fields): self { $this->fields = $fields; return $this; } - public function asPopup(): static + public function asPopup(): self { $this->popup = true; diff --git a/src/Providers/FeiShu.php b/src/Providers/FeiShu.php index 3644922..a65b8be 100644 --- a/src/Providers/FeiShu.php +++ b/src/Providers/FeiShu.php @@ -2,14 +2,10 @@ namespace Overtrue\Socialite\Providers; -use GuzzleHttp\Exception\GuzzleException; use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\BadRequestException; -use Overtrue\Socialite\Exceptions\Feishu\InvalidTicketException; -use Overtrue\Socialite\Exceptions\InvalidTokenException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -18,6 +14,8 @@ class FeiShu extends Base { public const NAME = 'feishu'; + private const APP_TICKET = 'app_ticket'; + protected string $baseUrl = 'https://open.feishu.cn/open-apis'; protected string $expiresInKey = 'refresh_expires_in'; protected bool $isInternalApp = false; @@ -33,12 +31,12 @@ protected function getAuthUrl(): string return $this->buildAuthUrlFromBase($this->baseUrl . '/authen/v1/index'); } - #[ArrayShape(['redirect_uri' => "mixed", 'app_id' => "null|string"])] + #[ArrayShape([Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', Contracts\ABNF_APP_ID => 'null|string'])] protected function getCodeFields(): array { return [ - 'redirect_uri' => $this->redirectUrl, - 'app_id' => $this->getClientId(), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\ABNF_APP_ID => $this->getClientId(), ]; } @@ -47,91 +45,79 @@ protected function getTokenUrl(): string return $this->baseUrl . '/authen/v1/access_token'; } - /** - * @throws AuthorizeFailedException - * @throws GuzzleException - */ public function tokenFromCode(string $code): array { return $this->normalizeAccessTokenResponse($this->getTokenFromCode($code)); } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \Overtrue\Socialite\Exceptions\Feishu\InvalidTicketException - * @throws \Overtrue\Socialite\Exceptions\InvalidTokenException + * @throws Exceptions\AuthorizeFailedException */ protected function getTokenFromCode(string $code): array { $this->configAppAccessToken(); - $response = $this->getHttpClient()->post( - $this->getTokenUrl(), - [ - 'json' => [ - 'app_access_token' => $this->config->get('app_access_token'), - 'code' => $code, - 'grant_type' => 'authorization_code', - ], - ] - ); - $response = \json_decode($response->getBody(), true) ?? []; + $responseInstance = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'json' => [ + 'app_access_token' => $this->config->get('app_access_token'), + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + ], + ]); + $response = $this->fromJsonBody($responseInstance); - if (empty($response['data'])) { - throw new AuthorizeFailedException('Invalid token response', $response); + if (empty($response['data'] ?? null)) { + throw new Exceptions\AuthorizeFailedException('Invalid token response', $response); } return $this->normalizeAccessTokenResponse($response['data']); } /** - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\BadRequestException */ protected function getUserByToken(string $token): array { - $response = $this->getHttpClient()->get( - $this->baseUrl . '/authen/v1/user_info', - [ - 'headers' => ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $token], - 'query' => array_filter( - [ - 'user_access_token' => $token, - ] - ), - ] - ); - - $response = \json_decode($response->getBody(), true) ?? []; - - if (empty($response['data'])) { - throw new \InvalidArgumentException('You have error! ' . json_encode($response, JSON_UNESCAPED_UNICODE)); + $responseInstance = $this->getHttpClient()->get($this->baseUrl . '/authen/v1/user_info', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $token + ], + 'query' => \array_filter( + [ + 'user_access_token' => $token, + ] + ), + ]); + + $response = $this->fromJsonBody($responseInstance); + + if (empty($response['data'] ?? null)) { + throw new Exceptions\BadRequestException((string)$responseInstance->getBody()); } return $response['data']; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $user['user_id'] ?? null, - 'name' => $user['name'] ?? null, - 'nickname' => $user['name'] ?? null, - 'avatar' => $user['avatar_url'] ?? null, - 'email' => $user['email'] ?? null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user['user_id'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_NICKNAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_AVATAR => $user['avatar_url'] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + ]); } - public function withInternalAppMode(): static + public function withInternalAppMode(): self { $this->isInternalApp = true; return $this; } - public function withDefaultMode(): static + public function withDefaultMode(): self { $this->isInternalApp = false; @@ -139,11 +125,11 @@ public function withDefaultMode(): static } /** - * set 'app_ticket' in config attribute + * set self::APP_TICKET in config attribute */ - public function withAppTicket(string $appTicket): static + public function withAppTicket(string $appTicket): self { - $this->config->set('app_ticket', $appTicket); + $this->config->set(self::APP_TICKET, $appTicket); return $this; } @@ -153,17 +139,17 @@ public function withAppTicket(string $appTicket): static * 应用维度授权凭证,开放平台可据此识别调用方的应用身份 * 分内建和自建 * - * @throws \Overtrue\Socialite\Exceptions\Feishu\InvalidTicketException - * @throws \Overtrue\Socialite\Exceptions\InvalidTokenException + * @throws Exceptions\FeiShu\InvalidTicketException + * @throws Exceptions\InvalidTokenException */ protected function configAppAccessToken() { $url = $this->baseUrl . '/auth/v3/app_access_token/'; $params = [ 'json' => [ - 'app_id' => $this->config->get('client_id'), - 'app_secret' => $this->config->get('client_secret'), - 'app_ticket' => $this->config->get('app_ticket'), + Contracts\ABNF_APP_ID => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID), + Contracts\ABNF_APP_SECRET => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET), + self::APP_TICKET => $this->config->get(self::APP_TICKET), ], ]; @@ -171,21 +157,21 @@ protected function configAppAccessToken() $url = $this->baseUrl . '/auth/v3/app_access_token/internal/'; $params = [ 'json' => [ - 'app_id' => $this->config->get('client_id'), - 'app_secret' => $this->config->get('client_secret'), + Contracts\ABNF_APP_ID => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID), + Contracts\ABNF_APP_SECRET => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET), ], ]; } - if (!$this->isInternalApp && !$this->config->has('app_ticket')) { - throw new InvalidTicketException('You are using default mode, please config \'app_ticket\' first'); + if (!$this->isInternalApp && !$this->config->has(self::APP_TICKET)) { + throw new Exceptions\FeiShu\InvalidTicketException('You are using default mode, please config \'app_ticket\' first'); } - $response = $this->getHttpClient()->post($url, $params); - $response = \json_decode($response->getBody(), true) ?? []; + $responseInstance = $this->getHttpClient()->post($url, $params); + $response = $this->fromJsonBody($responseInstance); - if (empty($response['app_access_token'])) { - throw new InvalidTokenException('Invalid \'app_access_token\' response', json_encode($response)); + if (empty($response['app_access_token'] ?? null)) { + throw new Exceptions\InvalidTokenException('Invalid \'app_access_token\' response', (string) $responseInstance->getBody()); } $this->config->set('app_access_token', $response['app_access_token']); @@ -196,17 +182,17 @@ protected function configAppAccessToken() * 应用的企业授权凭证,开放平台据此识别调用方的应用身份和企业身份 * 分内建和自建 * - * @throws \Overtrue\Socialite\Exceptions\BadRequestException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\BadRequestException + * @throws Exceptions\AuthorizeFailedException */ protected function configTenantAccessToken() { $url = $this->baseUrl . '/auth/v3/tenant_access_token/'; $params = [ 'json' => [ - 'app_id' => $this->config->get('client_id'), - 'app_secret' => $this->config->get('client_secret'), - 'app_ticket' => $this->config->get('app_ticket'), + Contracts\ABNF_APP_ID => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID), + Contracts\ABNF_APP_SECRET => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET), + self::APP_TICKET => $this->config->get(self::APP_TICKET), ], ]; @@ -214,20 +200,20 @@ protected function configTenantAccessToken() $url = $this->baseUrl . '/auth/v3/tenant_access_token/internal/'; $params = [ 'json' => [ - 'app_id' => $this->config->get('client_id'), - 'app_secret' => $this->config->get('client_secret'), + Contracts\ABNF_APP_ID => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID), + Contracts\ABNF_APP_SECRET => $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET), ], ]; } - if (!$this->isInternalApp && !$this->config->has('app_ticket')) { - throw new BadRequestException('You are using default mode, please config \'app_ticket\' first'); + if (!$this->isInternalApp && !$this->config->has(self::APP_TICKET)) { + throw new Exceptions\BadRequestException('You are using default mode, please config \'app_ticket\' first'); } $response = $this->getHttpClient()->post($url, $params); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($response); if (empty($response['tenant_access_token'])) { - throw new AuthorizeFailedException('Invalid tenant_access_token response', $response); + throw new Exceptions\AuthorizeFailedException('Invalid tenant_access_token response', $response); } $this->config->set('tenant_access_token', $response['tenant_access_token']); diff --git a/src/Providers/Figma.php b/src/Providers/Figma.php index 6f097c9..9843842 100644 --- a/src/Providers/Figma.php +++ b/src/Providers/Figma.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,6 +13,7 @@ class Figma extends Base { public const NAME = 'figma'; + protected string $scopeSeparator = ''; protected array $scopes = ['file_read']; @@ -40,46 +41,44 @@ public function tokenFromCode(string $code): array } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => "null|string", + Contracts\RFC6749_ABNF_CLIENT_SECRET => "null|string", + Contracts\RFC6749_ABNF_CODE => "string", + Contracts\RFC6749_ABNF_REDIRECT_URI => "null|string", + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string' ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } protected function getCodeFields(): array { - return parent::getCodeFields() + ['state' => \md5(\uniqid('state_', true))]; + return parent::getCodeFields() + [Contracts\RFC6749_ABNF_STATE => \md5(\uniqid('state_', true))]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->get('https://api.figma.com/v1/me', [ 'headers' => [ 'Accept' => 'application/json', - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, ], ]); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'username' => $user['email'] ?? null, - 'nickname' => $user['handle'] ?? null, - 'name' => $user['handle'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['img_url'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + 'username' => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_NICKNAME => $user['handle'] ?? null, + Contracts\ABNF_NAME => $user['handle'] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['img_url'] ?? null, ]); } } diff --git a/src/Providers/GitHub.php b/src/Providers/GitHub.php index 950d4a9..f9a340f 100644 --- a/src/Providers/GitHub.php +++ b/src/Providers/GitHub.php @@ -4,14 +4,15 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; class GitHub extends Base { public const NAME = 'github'; - protected array $scopes = ['read:user']; - protected string $scopeSeparator = ' '; + + protected array $scopes = ['read:user']; + protected string $scopeSeparator = ' '; protected function getAuthUrl(): string { @@ -23,9 +24,6 @@ protected function getTokenUrl(): string return 'https://github.com/login/oauth/access_token'; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { $userUrl = 'https://api.github.com/user'; @@ -35,10 +33,10 @@ protected function getUserByToken(string $token): array $this->createAuthorizationHeaders($token) ); - $user = json_decode($response->getBody(), true); + $user = $this->fromJsonBody($response); - if (in_array('user:email', $this->scopes)) { - $user['email'] = $this->getEmailByToken($token); + if (\in_array('user:email', $this->scopes)) { + $user[Contracts\ABNF_EMAIL] = $this->getEmailByToken($token); } return $user; @@ -57,9 +55,9 @@ protected function getEmailByToken(string $token): string return ''; } - foreach (json_decode($response->getBody(), true) as $email) { + foreach ($this->fromJsonBody($response) as $email) { if ($email['primary'] && $email['verified']) { - return $email['email']; + return $email[Contracts\ABNF_EMAIL]; } } @@ -67,14 +65,14 @@ protected function getEmailByToken(string $token): string } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => $user['login'] ?? null, - 'name' => $user['name'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['avatar_url'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $user['login'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['avatar_url'] ?? null, ]); } @@ -84,7 +82,7 @@ protected function createAuthorizationHeaders(string $token): array return [ 'headers' => [ 'Accept' => 'application/vnd.github.v3+json', - 'Authorization' => sprintf('token %s', $token), + 'Authorization' => \sprintf('token %s', $token), ], ]; } diff --git a/src/Providers/Gitee.php b/src/Providers/Gitee.php index d8598fe..42f729e 100644 --- a/src/Providers/Gitee.php +++ b/src/Providers/Gitee.php @@ -4,17 +4,14 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; class Gitee extends Base { public const NAME = 'gitee'; - protected string $expiresInKey = 'expires_in'; - protected string $accessTokenKey = 'access_token'; - protected string $refreshTokenKey = 'refresh_token'; - protected array $scopes = ['user_info']; + protected array $scopes = ['user_info']; protected function getAuthUrl(): string { @@ -29,42 +26,42 @@ protected function getTokenUrl(): string protected function getUserByToken(string $token): array { $userUrl = 'https://gitee.com/api/v5/user'; - $response = $this->getHttpClient()->get( - $userUrl, - [ - 'query' => ['access_token' => $token], - ] - ); - return \json_decode($response->getBody()->getContents(), true) ?? []; + $response = $this->getHttpClient()->get($userUrl, [ + 'query' => [ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token + ], + ]); + + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => $user['login'] ?? null, - 'name' => $user['name'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['avatar_url'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $user['login'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['avatar_url'] ?? null, ]); } #[ArrayShape([ - 'client_id' => "null|string", - 'client_secret' => "null|string", - 'code' => "string", - 'redirect_uri' => "mixed", - 'grant_type' => "string" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { return [ - 'client_id' => $this->getClientId(), - 'client_secret' => $this->getClientSecret(), - 'code' => $code, - 'redirect_uri' => $this->redirectUrl, - 'grant_type' => 'authorization_code', + Contracts\RFC6749_ABNF_CLIENT_ID => $this->getClientId(), + Contracts\RFC6749_ABNF_CLIENT_SECRET => $this->getClientSecret(), + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, ]; } } diff --git a/src/Providers/Google.php b/src/Providers/Google.php index 0c9a231..7eae2ca 100644 --- a/src/Providers/Google.php +++ b/src/Providers/Google.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,6 +13,7 @@ class Google extends Base { public const NAME = 'google'; + protected string $scopeSeparator = ' '; protected array $scopes = [ 'https://www.googleapis.com/auth/userinfo.email', @@ -29,10 +30,6 @@ protected function getTokenUrl(): string return 'https://www.googleapis.com/oauth2/v4/token'; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->post($this->getTokenUrl(), [ @@ -43,19 +40,17 @@ public function tokenFromCode(string $code): array } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->get('https://www.googleapis.com/userinfo/v2/me', [ @@ -65,19 +60,19 @@ protected function getUserByToken(string $token, ?array $query = []): array ], ]); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'username' => $user['email'] ?? null, - 'nickname' => $user['name'] ?? null, - 'name' => $user['name'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['picture'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + 'username' => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_NICKNAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['picture'] ?? null, ]); } } diff --git a/src/Providers/Line.php b/src/Providers/Line.php index c15461b..2e391ce 100644 --- a/src/Providers/Line.php +++ b/src/Providers/Line.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,14 +13,16 @@ class Line extends Base { public const NAME = 'line'; + protected string $baseUrl = 'https://api.line.me/oauth2/'; protected string $version = 'v2.1'; protected array $scopes = ['profile']; protected function getAuthUrl(): string { - $this->state = $this->state ?: \md5(\uniqid('state', true)); - return $this->buildAuthUrlFromBase('https://access.line.me/oauth2/'.$this->version.'/authorize'); + $this->state = $this->state ?: \md5(\uniqid(Contracts\RFC6749_ABNF_STATE, true)); + + return $this->buildAuthUrlFromBase('https://access.line.me/oauth2/' . $this->version . '/authorize'); } protected function getTokenUrl(): string @@ -29,26 +31,17 @@ protected function getTokenUrl(): string } #[ArrayShape([ - 'client_id' => "null|string", - 'client_secret' => "null|string", - 'code' => "string", - 'grant_type' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return [ - 'client_id' => $this->getClientId(), - 'client_secret' => $this->getClientSecret(), - 'code' => $code, - 'grant_type' => 'authorization_code', - 'redirect_uri' => $this->redirectUrl, - ]; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { $response = $this->getHttpClient()->get( @@ -56,25 +49,23 @@ protected function getUserByToken(string $token): array [ 'headers' => [ 'Accept' => 'application/json', - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, ], ] ); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $user['userId'] ?? null, - 'name' => $user['displayName'] ?? null, - 'nickname' => $user['displayName'] ?? null, - 'avatar' => $user['pictureUrl'] ?? null, - 'email' => null, - ] - ); + return new User([ + Contracts\ABNF_ID => $user['userId'] ?? null, + Contracts\ABNF_NAME => $user['displayName'] ?? null, + Contracts\ABNF_NICKNAME => $user['displayName'] ?? null, + Contracts\ABNF_AVATAR => $user['pictureUrl'] ?? null, + Contracts\ABNF_EMAIL => null, + ]); } } diff --git a/src/Providers/Linkedin.php b/src/Providers/Linkedin.php index b1cd167..3d6f005 100644 --- a/src/Providers/Linkedin.php +++ b/src/Providers/Linkedin.php @@ -3,7 +3,7 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\ArrayShape; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -12,6 +12,7 @@ class Linkedin extends Base { public const NAME = 'linkedin'; + protected array $scopes = ['r_liteprofile', 'r_emailaddress']; protected function getAuthUrl(): string @@ -25,25 +26,23 @@ protected function getTokenUrl(): string } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields($code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $basicProfile = $this->getBasicProfile($token); $emailAddress = $this->getEmailAddress($token); - return array_merge($basicProfile, $emailAddress); + return \array_merge($basicProfile, $emailAddress); } /** @@ -55,32 +54,29 @@ protected function getBasicProfile(string $token): array $response = $this->getHttpClient()->get($url, [ 'headers' => [ - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, 'X-RestLi-Protocol-Version' => '2.0.0', ], ]); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getEmailAddress(string $token): array { $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))'; $response = $this->getHttpClient()->get($url, [ 'headers' => [ - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, 'X-RestLi-Protocol-Version' => '2.0.0', ], ]); - return \json_decode($response->getBody(), true)['elements.0.handle~'] ?? []; + return $this->fromJsonBody($response)['elements.0.handle~'] ?? []; } - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { $preferredLocale = ($user['firstName.preferredLocale.language'] ?? null).'_'.($user['firstName.preferredLocale.country']) ?? null; $firstName = $user['firstName.localized.'.$preferredLocale] ?? null; @@ -88,21 +84,17 @@ protected function mapUserToObject(array $user): UserInterface $name = $firstName.' '.$lastName; $images = $user['profilePicture.displayImage~.elements'] ?? []; - $avatars = array_filter($images, function ($image) { - return ($image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 0) === 100; - }); - $avatar = array_shift($avatars); - $originalAvatars = array_filter($images, function ($image) { - return ($image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 0) === 800; - }); - $originalAvatar = array_shift($originalAvatars); + $avatars = \array_filter($images, static fn ($image) => ($image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 0) === 100); + $avatar = \array_shift($avatars); + $originalAvatars = \array_filter($images, static fn ($image) => ($image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 0) === 800); + $originalAvatar = \array_shift($originalAvatars); return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => $name, - 'name' => $name, - 'email' => $user['emailAddress'] ?? null, - 'avatar' => $avatar['identifiers.0.identifier'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $name, + Contracts\ABNF_NAME => $name, + Contracts\ABNF_EMAIL => $user['emailAddress'] ?? null, + Contracts\ABNF_AVATAR => $avatar['identifiers.0.identifier'] ?? null, 'avatar_original' => $originalAvatar['identifiers.0.identifier'] ?? null, ]); } diff --git a/src/Providers/OpenWeWork.php b/src/Providers/OpenWeWork.php index 71f4a11..3c3b361 100644 --- a/src/Providers/OpenWeWork.php +++ b/src/Providers/OpenWeWork.php @@ -3,10 +3,8 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\InvalidArgumentException; -use Overtrue\Socialite\Exceptions\MethodDoesNotSupportException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -15,6 +13,7 @@ class OpenWeWork extends Base { public const NAME = 'open-wework'; + protected bool $detailed = false; protected ?string $suiteTicket = null; protected ?int $agentId = null; @@ -30,32 +29,32 @@ public function __construct(array $config) } } - public function withAgentId(int $agentId): static + public function withAgentId(int $agentId): self { $this->agentId = $agentId; return $this; } - public function userFromCode(string $code): UserInterface + public function userFromCode(string $code): Contracts\UserInterface { $user = $this->getUser($this->getSuiteAccessToken(), $code); if ($this->detailed) { - $user = array_merge($user, $this->getUserByTicket($user['user_ticket'])); + $user = \array_merge($user, $this->getUserByTicket($user['user_ticket'])); } return $this->mapUserToObject($user)->setProvider($this)->setRaw($user); } - public function withSuiteTicket(string $suiteTicket): OpenWeWork + public function withSuiteTicket(string $suiteTicket): self { $this->suiteTicket = $suiteTicket; return $this; } - public function withSuiteAccessToken(string $suiteAccessToken): OpenWeWork + public function withSuiteAccessToken(string $suiteAccessToken): self { $this->suiteAccessToken = $suiteAccessToken; @@ -63,77 +62,71 @@ public function withSuiteAccessToken(string $suiteAccessToken): OpenWeWork } /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @throws Exceptions\InvalidArgumentException */ public function getAuthUrl(): string { $queries = \array_filter([ 'appid' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'response_type' => 'code', - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'state' => $this->state, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_STATE => $this->state, 'agentid' => $this->agentId, ]); if ((\in_array('snsapi_userinfo', $this->scopes) || \in_array('snsapi_privateinfo', $this->scopes)) && empty($this->agentId)) { - throw new InvalidArgumentException('agentid is required when scopes is snsapi_userinfo or snsapi_privateinfo.'); + throw new Exceptions\InvalidArgumentException('agentid is required when scopes is snsapi_userinfo or snsapi_privateinfo.'); } - return sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', http_build_query($queries)); + return \sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', \http_build_query($queries)); } /** - * @throws \Overtrue\Socialite\Exceptions\MethodDoesNotSupportException + * @throws Exceptions\MethodDoesNotSupportException */ protected function getUserByToken(string $token): array { - throw new MethodDoesNotSupportException('Open WeWork doesn\'t support access_token mode'); + throw new Exceptions\MethodDoesNotSupportException('Open WeWork doesn\'t support access_token mode'); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ protected function getSuiteAccessToken(): string { return $this->suiteAccessToken ?? $this->suiteAccessToken = $this->requestSuiteAccessToken(); } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\AuthorizeFailedException */ protected function getUser(string $token, string $code): array { - $response = $this->getHttpClient()->get( + $responseInstance = $this->getHttpClient()->get( $this->baseUrl . '/cgi-bin/service/getuserinfo3rd', [ - 'query' => array_filter( + 'query' => \array_filter( [ 'suite_access_token' => $token, - 'code' => $code, + Contracts\RFC6749_ABNF_CODE => $code, ] ), ] ); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (($response['errcode'] ?? 1) > 0 || (empty($response['UserId']) && empty($response['open_userid']))) { - throw new AuthorizeFailedException('Failed to get user openid:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } return $response; } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\AuthorizeFailedException */ protected function getUserByTicket(string $userTicket): array { - $response = $this->getHttpClient()->post( + $responseInstance = $this->getHttpClient()->post( $this->baseUrl . '/cgi-bin/user/get', [ 'query' => [ @@ -143,43 +136,34 @@ protected function getUserByTicket(string $userTicket): array ] ); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (($response['errcode'] ?? 1) > 0 || empty($response['userid'])) { - throw new AuthorizeFailedException('Failed to get user:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } return $response; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - if ($this->detailed) { - return new User( - [ - 'id' => $user['userid'] ?? null, - 'name' => $user['name'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => $user['email'] ?? null, - ] - ); - } - - return new User( - [ - 'id' => $user['UserId'] ?? null ?: $user['OpenId'] ?? null, - ] - ); + return new User($this->detailed ? [ + Contracts\ABNF_ID => $user['userid'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + ] : [ + Contracts\ABNF_ID => $user['UserId'] ?? null ?: $user['OpenId'] ?? null, + ]); } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\AuthorizeFailedException */ protected function requestSuiteAccessToken(): string { - $response = $this->getHttpClient()->post( + $responseInstance = $this->getHttpClient()->post( $this->baseUrl . '/cgi-bin/service/get_suite_token', [ 'json' => @@ -191,10 +175,10 @@ protected function requestSuiteAccessToken(): string ] ); - $response = \json_decode($response->getBody()->getContents(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (isset($response['errcode']) && $response['errcode'] > 0) { - throw new AuthorizeFailedException('Failed to get api access_token:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } return $response['suite_access_token']; diff --git a/src/Providers/Outlook.php b/src/Providers/Outlook.php index 55f1d5b..91495f8 100644 --- a/src/Providers/Outlook.php +++ b/src/Providers/Outlook.php @@ -4,12 +4,13 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; class Outlook extends Base { public const NAME = 'outlook'; + protected array $scopes = ['User.Read']; protected string $scopeSeparator = ' '; @@ -23,45 +24,41 @@ protected function getTokenUrl(): string return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->get( 'https://graph.microsoft.com/v1.0/me', ['headers' => [ 'Accept' => 'application/json', - 'Authorization' => 'Bearer '.$token, + 'Authorization' => 'Bearer ' . $token, ], ] ); - return \json_decode($response->getBody()->getContents(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => null, - 'name' => $user['displayName'] ?? null, - 'email' => $user['userPrincipalName'] ?? null, - 'avatar' => null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => null, + Contracts\ABNF_NAME => $user['displayName'] ?? null, + Contracts\ABNF_EMAIL => $user['userPrincipalName'] ?? null, + Contracts\ABNF_AVATAR => null, ]); } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + [ - 'grant_type' => 'authorization_code', - ]; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } } diff --git a/src/Providers/QCloud.php b/src/Providers/QCloud.php index a17d0f6..9463e46 100644 --- a/src/Providers/QCloud.php +++ b/src/Providers/QCloud.php @@ -3,14 +3,14 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\ProviderInterface; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; -class QCloud extends Base implements ProviderInterface +class QCloud extends Base { public const NAME = 'qcloud'; + protected array $scopes = ['login']; protected string $accessTokenKey = 'UserAccessToken'; protected string $refreshTokenKey = 'UserRefreshToken'; @@ -30,7 +30,7 @@ protected function getTokenUrl(): string protected function getAppId(): string { - return $this->config->get('app_id') ?? $this->getClientId(); + return $this->config->get(Contracts\ABNF_APP_ID) ?? $this->getClientId(); } protected function getSecretId(): string @@ -43,10 +43,6 @@ protected function getSecretKey(): string return $this->config->get('secret_key'); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ public function TokenFromCode(string $code): array { $response = $this->performRequest( @@ -65,8 +61,7 @@ public function TokenFromCode(string $code): array } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function getUserByToken(string $token): array { @@ -88,20 +83,17 @@ protected function getUserByToken(string $token): array } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - return new User( - [ - 'id' => $this->openId ?? null, - 'name' => $user['Nickname'] ?? null, - 'nickname' => $user['Nickname'] ?? null, - ] - ); + return new User([ + Contracts\ABNF_ID => $this->openId ?? null, + Contracts\ABNF_NAME => $user['Nickname'] ?? null, + Contracts\ABNF_NICKNAME => $user['Nickname'] ?? null, + ]); } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ public function performRequest(string $method, string $host, string $action, string $version, array $options = [], ?string $secretId = null, ?string $secretKey = null): array { @@ -128,10 +120,10 @@ public function performRequest(string $method, string $host, string $action, str ); $response = $this->getHttpClient()->get("https://{$host}/", $options); - $response = json_decode($response->getBody()->getContents(), true) ?? []; + $response = $this->fromJsonBody($response); if (!empty($response['Response']['Error'])) { - throw new AuthorizeFailedException( + throw new Exceptions\AuthorizeFailedException( \sprintf('%s: %s', $response['Response']['Error']['Code'], $response['Response']['Error']['Message']), $response ); @@ -150,7 +142,7 @@ protected function sign(string $requestMethod, string $host, array $query, strin \http_build_query($query), "content-type:{$headers['Content-Type']}\nhost:{$host}\n", "content-type;host", - hash('SHA256', $payload), + \hash('SHA256', $payload), ] ); @@ -160,29 +152,29 @@ protected function sign(string $requestMethod, string $host, array $query, strin 'TC3-HMAC-SHA256', $headers['X-TC-Timestamp'], $credential, - hash('SHA256', $canonicalRequestString), + \hash('SHA256', $canonicalRequestString), ] ); $secretKey = $secretKey ?? $this->getSecretKey(); - $secretDate = hash_hmac('SHA256', \gmdate('Y-m-d', $headers['X-TC-Timestamp']), "TC3{$secretKey}", true); - $secretService = hash_hmac('SHA256', $this->getServiceFromHost($host), $secretDate, true); - $secretSigning = hash_hmac('SHA256', "tc3_request", $secretService, true); + $secretDate = \hash_hmac('SHA256', \gmdate('Y-m-d', $headers['X-TC-Timestamp']), "TC3{$secretKey}", true); + $secretService = \hash_hmac('SHA256', $this->getServiceFromHost($host), $secretDate, true); + $secretSigning = \hash_hmac('SHA256', "tc3_request", $secretService, true); - return hash_hmac('SHA256', $signString, $secretSigning); + return \hash_hmac('SHA256', $signString, $secretSigning); } /** - * @throws AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function parseAccessToken(array | string $body) { - if (!is_array($body)) { - $body = json_decode($body, true); + if (!\is_array($body)) { + $body = \json_decode($body, true); } if (empty($body['UserOpenId'])) { - throw new AuthorizeFailedException('Authorize Failed: ' . json_encode($body, JSON_UNESCAPED_UNICODE), $body); + throw new Exceptions\AuthorizeFailedException('Authorize Failed: ' . \json_encode($body, JSON_UNESCAPED_UNICODE), $body); } $this->openId = $body['UserOpenId'] ?? null; @@ -192,8 +184,7 @@ protected function parseAccessToken(array | string $body) } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function getFederationToken(string $accessToken): array { @@ -215,7 +206,7 @@ protected function getFederationToken(string $accessToken): array ); if (empty($response['Credentials'])) { - throw new AuthorizeFailedException('Get Federation Token failed.', $response); + throw new Exceptions\AuthorizeFailedException('Get Federation Token failed.', $response); } return $response['Credentials']; @@ -223,18 +214,18 @@ protected function getFederationToken(string $accessToken): array protected function getCodeFields(): array { - $fields = array_merge( + $fields = \array_merge( [ - 'app_id' => $this->getAppId(), - 'redirect_url' => $this->redirectUrl, - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'response_type' => 'code', + Contracts\ABNF_APP_ID => $this->getAppId(), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, ], $this->parameters ); if ($this->state) { - $fields['state'] = $this->state; + $fields[Contracts\RFC6749_ABNF_STATE] = $this->state; } return $fields; @@ -242,6 +233,6 @@ protected function getCodeFields(): array protected function getServiceFromHost(string $host): string { - return explode('.', $host)[0] ?? ''; + return \explode('.', $host)[0] ?? ''; } } diff --git a/src/Providers/QQ.php b/src/Providers/QQ.php index d0f6291..977c87c 100644 --- a/src/Providers/QQ.php +++ b/src/Providers/QQ.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,91 +13,86 @@ class QQ extends Base { public const NAME = 'qq'; + protected string $baseUrl = 'https://graph.qq.com'; protected array $scopes = ['get_user_info']; protected bool $withUnionId = false; protected function getAuthUrl(): string { - return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2.0/authorize'); + return $this->buildAuthUrlFromBase($this->baseUrl . '/oauth2.0/authorize'); } protected function getTokenUrl(): string { - return $this->baseUrl.'/oauth2.0/token'; + return $this->baseUrl . '/oauth2.0/token'; } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + [ - 'grant_type' => 'authorization_code', - ]; + return parent::getTokenFields($code) + [Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE]; } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->get($this->getTokenUrl(), [ 'query' => $this->getTokenFields($code), ]); - \parse_str($response->getBody()->getContents(), $token); + \parse_str((string) $response->getBody(), $token); return $this->normalizeAccessTokenResponse($token); } - public function withUnionId(): static + public function withUnionId(): self { $this->withUnionId = true; return $this; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { - $url = $this->baseUrl.'/oauth2.0/me?fmt=json&access_token='.$token; - $this->withUnionId && $url .= '&unionid=1'; - - $response = $this->getHttpClient()->get($url); - - $me = \json_decode($response->getBody()->getContents(), true); + $response = $this->getHttpClient()->get($this->baseUrl . '/oauth2.0/me', [ + 'query' => [ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, + 'fmt' => 'json', + ] + $this->withUnionId ? ['unionid' => 1] : [] + ]); - $queries = [ - 'access_token' => $token, - 'fmt' => 'json', - 'openid' => $me['openid'], - 'oauth_consumer_key' => $this->getClientId(), - ]; + $me = $this->fromJsonBody($response); - $response = $this->getHttpClient()->get($this->baseUrl.'/user/get_user_info?'.http_build_query($queries)); + $response = $this->getHttpClient()->get($this->baseUrl . '/user/get_user_info', [ + 'query' => [ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, + 'fmt' => 'json', + 'openid' => $me['openid'], + 'oauth_consumer_key' => $this->getClientId(), + ] + ]); - return (\json_decode($response->getBody()->getContents(), true) ?? []) + [ + return ($this->fromJsonBody($response) ?? []) + [ 'unionid' => $me['unionid'] ?? null, 'openid' => $me['openid'] ?? null, ]; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['openid'] ?? null, - 'name' => $user['nickname'] ?? null, - 'nickname' => $user['nickname'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['figureurl_qq_2'] ?? null, + Contracts\ABNF_ID => $user['openid'] ?? null, + Contracts\ABNF_NAME => $user['nickname'] ?? null, + Contracts\ABNF_NICKNAME => $user['nickname'] ?? null, + Contracts\ABNF_EMAIL => $user['email'] ?? null, + Contracts\ABNF_AVATAR => $user['figureurl_qq_2'] ?? null, ]); } } diff --git a/src/Providers/Taobao.php b/src/Providers/Taobao.php index 95acd6a..a504b2c 100644 --- a/src/Providers/Taobao.php +++ b/src/Providers/Taobao.php @@ -4,7 +4,7 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; +use Overtrue\Socialite\Contracts; use Overtrue\Socialite\User; /** @@ -13,12 +13,13 @@ class Taobao extends Base { public const NAME = 'taobao'; + protected string $baseUrl = 'https://oauth.taobao.com'; protected string $gatewayUrl = 'https://eco.taobao.com/router/rest'; protected string $view = 'web'; protected array $scopes = ['user_info']; - public function withView(string $view): static + public function withView(string $view): self { $this->view = $view; @@ -27,91 +28,89 @@ public function withView(string $view): static protected function getAuthUrl(): string { - return $this->buildAuthUrlFromBase($this->baseUrl.'/authorize'); + return $this->buildAuthUrlFromBase($this->baseUrl . '/authorize'); } #[ArrayShape([ - 'client_id' => "null|string", - 'redirect_uri' => "mixed", - 'view' => "string", - 'response_type' => "string" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + 'view' => 'string', + Contracts\RFC6749_ABNF_RESPONSE_TYPE => 'string' ])] public function getCodeFields(): array { return [ - 'client_id' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, + Contracts\RFC6749_ABNF_CLIENT_ID => $this->getClientId(), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, 'view' => $this->view, - 'response_type' => 'code', + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, ]; } protected function getTokenUrl(): string { - return $this->baseUrl.'/token'; + return $this->baseUrl . '/token'; } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', + 'view' => 'string', ])] - protected function getTokenFields($code): array + protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code', 'view' => $this->view]; + return parent::getTokenFields($code) + [ + Contracts\RFC6749_ABNF_GRANT_TYPE =>Contracts\RFC6749_ABNF_AUTHORATION_CODE, + 'view' => $this->view + ]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 'query' => $this->getTokenFields($code), ]); - return $this->normalizeAccessTokenResponse($response->getBody()->getContents()); + return $this->normalizeAccessTokenResponse($response->getBody()); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token, ?array $query = []): array { $response = $this->getHttpClient()->post($this->getUserInfoUrl($this->gatewayUrl, $token)); - return \json_decode($response->getBody()->getContents(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['open_id'] ?? null, - 'nickname' => $user['nick'] ?? null, - 'name' => $user['nick'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => $user['email'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_OPEN_ID] ?? null, + Contracts\ABNF_NICKNAME => $user['nick'] ?? null, + Contracts\ABNF_NAME => $user['nick'] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, ]); } protected function generateSign(array $params): string { - ksort($params); + \ksort($params); - $stringToBeSigned = $this->getConfig()->get('client_secret'); + $stringToBeSigned = $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_SECRET); foreach ($params as $k => $v) { - if (!is_array($v) && !str_starts_with($v, '@')) { + if (!\is_array($v) && !\str_starts_with($v, '@')) { $stringToBeSigned .= "$k$v"; } } - $stringToBeSigned .= $this->getConfig()->get('client_secret'); + $stringToBeSigned .= $this->getConfig()->get(Contracts\RFC6749_ABNF_CLIENT_SECRET); - return strtoupper(md5($stringToBeSigned)); + return \strtoupper(\md5($stringToBeSigned)); } protected function getPublicFields(string $token, array $apiFields = []): array @@ -120,12 +119,12 @@ protected function getPublicFields(string $token, array $apiFields = []): array 'app_key' => $this->getClientId(), 'sign_method' => 'md5', 'session' => $token, - 'timestamp' => \date('Y-m-d H:i:s'), + 'timestamp' => (new \DateTime('now', new \DateTimeZone('Asia/Shanghai')))->format('Y-m-d H:i:s'), 'v' => '2.0', 'format' => 'json', ]; - $fields = array_merge($apiFields, $fields); + $fields = \array_merge($apiFields, $fields); $fields['sign'] = $this->generateSign($fields); return $fields; @@ -135,8 +134,8 @@ protected function getUserInfoUrl(string $url, string $token): string { $apiFields = ['method' => 'taobao.miniapp.userInfo.get']; - $query = http_build_query($this->getPublicFields($token, $apiFields), '', '&', $this->encodingType); + $query = \http_build_query($this->getPublicFields($token, $apiFields), '', '&', $this->encodingType); - return $url.'?'.$query; + return $url . '?' . $query; } } diff --git a/src/Providers/Tapd.php b/src/Providers/Tapd.php index 4c4f512..617019c 100644 --- a/src/Providers/Tapd.php +++ b/src/Providers/Tapd.php @@ -2,11 +2,10 @@ namespace Overtrue\Socialite\Providers; -use GuzzleHttp\Psr7\Stream; +use Psr\Http\Message\StreamInterface; use JetBrains\PhpStorm\ArrayShape; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\BadRequestException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -15,6 +14,7 @@ class Tapd extends Base { public const NAME = 'tapd'; + protected string $baseUrl = 'https://api.tapd.cn'; protected function getAuthUrl(): string @@ -32,10 +32,6 @@ protected function getRefreshTokenUrl(): string return $this->baseUrl . '/tokens/refresh_token'; } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException - */ public function tokenFromCode(string $code): array { $response = $this->getHttpClient()->post($this->getTokenUrl(), [ @@ -46,33 +42,37 @@ public function tokenFromCode(string $code): array 'form_params' => $this->getTokenFields($code), ]); - return $this->normalizeAccessTokenResponse($response->getBody()->getContents()); + return $this->normalizeAccessTokenResponse($response->getBody()); } - #[ArrayShape(['grant_type' => "string", 'redirect_uri' => "mixed", 'code' => "string"])] + #[ArrayShape([ + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string' + ])] protected function getTokenFields(string $code): array { return [ - 'grant_type' => 'authorization_code', - 'redirect_uri' => $this->redirectUrl, - 'code' => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_CODE => $code, ]; } - #[ArrayShape(['grant_type' => "string", 'redirect_uri' => "mixed", 'refresh_token' => ""])] + #[ArrayShape([ + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_REFRESH_TOKEN => 'string' + ])] protected function getRefreshTokenFields(string $refreshToken): array { return [ - 'grant_type' => 'refresh_token', - 'redirect_uri' => $this->redirectUrl, - 'refresh_token' => $refreshToken, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_REFRESH_TOKEN, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_REFRESH_TOKEN => $refreshToken, ]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ public function tokenFromRefreshToken(string $refreshToken): array { $response = $this->getHttpClient()->post($this->getRefreshTokenUrl(), [ @@ -83,12 +83,9 @@ public function tokenFromRefreshToken(string $refreshToken): array 'form_params' => $this->getRefreshTokenFields($refreshToken), ]); - return $this->normalizeAccessTokenResponse($response->getBody()->getContents()); + return $this->normalizeAccessTokenResponse((string)$response->getBody()); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { $response = $this->getHttpClient()->get($this->baseUrl . '/users/info', [ @@ -98,53 +95,53 @@ protected function getUserByToken(string $token): array ], ]); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } /** - * @throws \Overtrue\Socialite\Exceptions\BadRequestException + * @throws Exceptions\BadRequestException */ - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { if (!isset($user['status']) && $user['status'] != 1) { - throw new BadRequestException("用户信息获取失败"); + throw new Exceptions\BadRequestException('用户信息获取失败'); } return new User([ - 'id' => $user['data']['id'] ?? null, - 'nickname' => $user['data']['nick'] ?? null, - 'name' => $user['data']['name'] ?? null, - 'email' => $user['data']['email'] ?? null, - 'avatar' => $user['data']['avatar'] ?? null, + Contracts\ABNF_ID => $user['data'][Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $user['data']['nick'] ?? null, + Contracts\ABNF_NAME => $user['data'][Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_EMAIL => $user['data'][Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['data'][Contracts\ABNF_AVATAR] ?? null, ]); } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function normalizeAccessTokenResponse($response): array { - if ($response instanceof Stream) { + if ($response instanceof StreamInterface) { $response->rewind(); - $response = $response->getContents(); + $response = (string)$response; } if (\is_string($response)) { - $response = json_decode($response, true) ?? []; + $response = \json_decode($response, true) ?? []; } if (!\is_array($response)) { - throw new AuthorizeFailedException('Invalid token response', [$response]); + throw new Exceptions\AuthorizeFailedException('Invalid token response', [$response]); } - if (empty($response['data'][$this->accessTokenKey])) { - throw new AuthorizeFailedException('Authorize Failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE), $response); + if (empty($response['data'][$this->accessTokenKey] ?? null)) { + throw new Exceptions\AuthorizeFailedException('Authorize Failed: ' . \json_encode($response, JSON_UNESCAPED_UNICODE), $response); } return $response + [ - 'access_token' => $response['data'][$this->accessTokenKey], - 'refresh_token' => $response['data'][$this->refreshTokenKey] ?? null, - 'expires_in' => \intval($response['data'][$this->expiresInKey] ?? 0), - ]; + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $response['data'][$this->accessTokenKey], + Contracts\RFC6749_ABNF_REFRESH_TOKEN => $response['data'][$this->refreshTokenKey] ?? null, + Contracts\RFC6749_ABNF_EXPIRES_IN => \intval($response['data'][$this->expiresInKey] ?? 0), + ]; } } diff --git a/src/Providers/WeChat.php b/src/Providers/WeChat.php index c837068..2fc8572 100644 --- a/src/Providers/WeChat.php +++ b/src/Providers/WeChat.php @@ -3,8 +3,8 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\InvalidArgumentException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; use Psr\Http\Message\ResponseInterface; @@ -16,15 +16,13 @@ class WeChat extends Base { public const NAME = 'wechat'; + protected string $baseUrl = 'https://api.weixin.qq.com/sns'; protected array $scopes = ['snsapi_login']; protected bool $withCountryCode = false; protected ?array $component = null; protected ?string $openid = null; - /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException - */ public function __construct(array $config) { parent::__construct($config); @@ -34,37 +32,31 @@ public function __construct(array $config) } } - public function withOpenid(string $openid): static + public function withOpenid(string $openid): self { $this->openid = $openid; return $this; } - public function withCountryCode(): static + public function withCountryCode(): self { $this->withCountryCode = true; return $this; } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException|\GuzzleHttp\Exception\GuzzleException - */ public function tokenFromCode(string $code): array { $response = $this->getTokenFromCode($code); - return $this->normalizeAccessTokenResponse($response->getBody()->getContents()); + return $this->normalizeAccessTokenResponse($response->getBody()); } /** - * @param array $componentConfig ['id' => xxx, 'token' => xxx] - * - * @return \Overtrue\Socialite\Providers\WeChat - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @param array $componentConfig [Contracts\ABNF_ID => xxx, Contracts\ABNF_TOKEN => xxx] */ - public function withComponent(array $componentConfig): static + public function withComponent(array $componentConfig): self { $this->prepareForComponent($componentConfig); @@ -80,7 +72,7 @@ protected function getAuthUrl(): string { $path = 'oauth2/authorize'; - if (in_array('snsapi_login', $this->scopes)) { + if (\in_array('snsapi_login', $this->scopes)) { $path = 'qrconnect'; } @@ -89,7 +81,7 @@ protected function getAuthUrl(): string protected function buildAuthUrlFromBase(string $url): string { - $query = http_build_query($this->getCodeFields(), '', '&', $this->encodingType); + $query = \http_build_query($this->getCodeFields(), '', '&', $this->encodingType); return $url . '?' . $query . '#wechat_redirect'; } @@ -97,35 +89,28 @@ protected function buildAuthUrlFromBase(string $url): string protected function getCodeFields(): array { if (!empty($this->component)) { - $this->with(array_merge($this->parameters, ['component_appid' => $this->component['id']])); + $this->with(\array_merge($this->parameters, ['component_appid' => $this->component[Contracts\ABNF_ID]])); } - return array_merge([ + return \array_merge([ 'appid' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'response_type' => 'code', - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'state' => $this->state ?: \md5(\uniqid('state', true)), + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_STATE => $this->state ?: \md5(\uniqid(Contracts\RFC6749_ABNF_STATE, true)), 'connect_redirect' => 1, ], $this->parameters); } protected function getTokenUrl(): string { - if (!empty($this->component)) { - return $this->baseUrl . '/oauth2/component/access_token'; - } - - return $this->baseUrl . '/oauth2/access_token'; + return \sprintf($this->baseUrl . '/oauth2%s/access_token', empty($this->component) ? '' : '/component'); } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException|\GuzzleHttp\Exception\GuzzleException - */ public function userFromCode(string $code): User { - if (in_array('snsapi_base', $this->scopes)) { - return $this->mapUserToObject(\json_decode($this->getTokenFromCode($code)->getBody()->getContents(), true) ?? []); + if (\in_array('snsapi_base', $this->scopes)) { + return $this->mapUserToObject($this->fromJsonBody($this->getTokenFromCode($code))); } $token = $this->tokenFromCode($code); @@ -134,64 +119,54 @@ public function userFromCode(string $code): User $user = $this->userFromToken($token[$this->accessTokenKey]); - return $user->setRefreshToken($token['refresh_token']) - ->setExpiresIn($token['expires_in']) + return $user->setRefreshToken($token[Contracts\RFC6749_ABNF_REFRESH_TOKEN]) + ->setExpiresIn($token[Contracts\RFC6749_ABNF_EXPIRES_IN]) ->setTokenResponse($token); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getUserByToken(string $token): array { $language = $this->withCountryCode ? null : (isset($this->parameters['lang']) ? $this->parameters['lang'] : 'zh_CN'); $response = $this->getHttpClient()->get($this->baseUrl . '/userinfo', [ - 'query' => array_filter([ - 'access_token' => $token, + 'query' => \array_filter([ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, 'openid' => $this->openid, 'lang' => $language, ]), ]); - return \json_decode($response->getBody()->getContents(), true) ?? []; + return $this->fromJsonBody($response); } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['openid'] ?? null, - 'name' => $user['nickname'] ?? null, - 'nickname' => $user['nickname'] ?? null, - 'avatar' => $user['headimgurl'] ?? null, - 'email' => null, + Contracts\ABNF_ID => $user['openid'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NICKNAME] ?? null, + Contracts\ABNF_NICKNAME => $user[Contracts\ABNF_NICKNAME] ?? null, + Contracts\ABNF_AVATAR => $user['headimgurl'] ?? null, + Contracts\ABNF_EMAIL => null, ]); } protected function getTokenFields(string $code): array { - if (!empty($this->component)) { - return [ - 'appid' => $this->getClientId(), - 'component_appid' => $this->component['id'], - 'component_access_token' => $this->component['token'], - 'code' => $code, - 'grant_type' => 'authorization_code', - ]; - } - - return [ + return empty($this->component) ? [ 'appid' => $this->getClientId(), 'secret' => $this->getClientSecret(), - 'code' => $code, - 'grant_type' => 'authorization_code', + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + ] : [ + 'appid' => $this->getClientId(), + 'component_appid' => $this->component[Contracts\ABNF_ID], + 'component_access_token' => $this->component[Contracts\ABNF_TOKEN], + Contracts\RFC6749_ABNF_CODE => $code, + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, ]; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function getTokenFromCode(string $code): ResponseInterface { return $this->getHttpClient()->get($this->getTokenUrl(), [ @@ -201,7 +176,7 @@ protected function getTokenFromCode(string $code): ResponseInterface } /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException + * @throws Exceptions\InvalidArgumentException */ protected function prepareForComponent(array $component) { @@ -212,25 +187,25 @@ protected function prepareForComponent(array $component) } switch ($key) { - case 'id': - case 'app_id': + case Contracts\ABNF_ID: + case Contracts\ABNF_APP_ID: case 'component_app_id': - $config['id'] = $value; + $config[Contracts\ABNF_ID] = $value; break; - case 'token': + case Contracts\ABNF_TOKEN: + case Contracts\RFC6749_ABNF_ACCESS_TOKEN: case 'app_token': - case 'access_token': case 'component_access_token': - $config['token'] = $value; + $config[Contracts\ABNF_TOKEN] = $value; break; } } - if (2 !== count($config)) { - throw new InvalidArgumentException('Please check your config arguments is available.'); + if (2 !== \count($config)) { + throw new Exceptions\InvalidArgumentException('Please check your config arguments were available.'); } - if (1 === count($this->scopes) && in_array('snsapi_login', $this->scopes)) { + if (1 === \count($this->scopes) && \in_array('snsapi_login', $this->scopes)) { $this->scopes = ['snsapi_base']; } diff --git a/src/Providers/WeWork.php b/src/Providers/WeWork.php index 94e8cc5..17e8de9 100644 --- a/src/Providers/WeWork.php +++ b/src/Providers/WeWork.php @@ -3,9 +3,8 @@ namespace Overtrue\Socialite\Providers; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\AuthorizeFailedException; -use Overtrue\Socialite\Exceptions\MethodDoesNotSupportException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -14,6 +13,7 @@ class WeWork extends Base { public const NAME = 'wework'; + protected bool $detailed = false; protected ?string $apiAccessToken; protected string $baseUrl = 'https://qyapi.weixin.qq.com'; @@ -32,7 +32,7 @@ public function getBaseUrl() return $this->baseUrl; } - public function userFromCode(string $code): UserInterface + public function userFromCode(string $code): Contracts\UserInterface { $token = $this->getApiAccessToken(); $user = $this->getUser($token, $code); @@ -44,42 +44,39 @@ public function userFromCode(string $code): UserInterface return $this->mapUserToObject($user)->setProvider($this)->setRaw($user); } - public function detailed(): static + public function detailed(): self { $this->detailed = true; return $this; } - public function withApiAccessToken(string $apiAccessToken): static + public function withApiAccessToken(string $apiAccessToken): self { $this->apiAccessToken = $apiAccessToken; return $this; } - /** - * @throws \Overtrue\Socialite\Exceptions\InvalidArgumentException - */ public function getAuthUrl(): string { $queries = [ 'appid' => $this->getClientId(), - 'redirect_uri' => $this->redirectUrl, - 'response_type' => 'code', - 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), - 'state' => $this->state, + Contracts\RFC6749_ABNF_REDIRECT_URI => $this->redirectUrl, + Contracts\RFC6749_ABNF_RESPONSE_TYPE => Contracts\RFC6749_ABNF_CODE, + Contracts\RFC6749_ABNF_SCOPE => $this->formatScopes($this->scopes, $this->scopeSeparator), + Contracts\RFC6749_ABNF_STATE => $this->state, ]; - return sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', http_build_query($queries)); + return \sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', \http_build_query($queries)); } /** - * @throws \Overtrue\Socialite\Exceptions\MethodDoesNotSupportException + * @throws Exceptions\MethodDoesNotSupportException */ protected function getUserByToken(string $token): array { - throw new MethodDoesNotSupportException('WeWork doesn\'t support access_token mode'); + throw new Exceptions\MethodDoesNotSupportException('WeWork doesn\'t support access_token mode'); } protected function getApiAccessToken(): string @@ -87,29 +84,24 @@ protected function getApiAccessToken(): string return $this->apiAccessToken ?? $this->apiAccessToken = $this->requestApiAccessToken(); } - /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - */ protected function getUser(string $token, string $code): array { - $response = $this->getHttpClient()->get( + $responseInstance = $this->getHttpClient()->get( $this->baseUrl . '/cgi-bin/user/getuserinfo', [ - 'query' => array_filter( + 'query' => \array_filter( [ - 'access_token' => $token, - 'code' => $code, + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, + Contracts\RFC6749_ABNF_CODE => $code, ] ), ] ); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (($response['errcode'] ?? 1) > 0 || (empty($response['UserId']) && empty($response['OpenId']))) { - throw new AuthorizeFailedException('Failed to get user openid:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } elseif (empty($response['UserId'])) { $this->detailed = false; } @@ -118,77 +110,64 @@ protected function getUser(string $token, string $code): array } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException + * @throws Exceptions\AuthorizeFailedException */ protected function getUserById(string $userId): array { - $response = $this->getHttpClient()->post( - $this->baseUrl . '/cgi-bin/user/get', - [ - 'query' => [ - 'access_token' => $this->getApiAccessToken(), - 'userid' => $userId, - ], - ] - ); + $responseInstance = $this->getHttpClient()->post($this->baseUrl . '/cgi-bin/user/get', [ + 'query' => [ + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $this->getApiAccessToken(), + 'userid' => $userId, + ], + ]); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (($response['errcode'] ?? 1) > 0 || empty($response['userid'])) { - throw new AuthorizeFailedException('Failed to get user:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } return $response; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { - if ($this->detailed) { - return new User( - [ - 'id' => $user['userid'] ?? null, - 'name' => $user['name'] ?? null, - 'avatar' => $user['avatar'] ?? null, - 'email' => $user['email'] ?? null, - ] - ); - } - - return new User( - [ - 'id' => $user['UserId'] ?? null ?: $user['OpenId'] ?? null, - ] - ); + return new User($this->detailed ? [ + Contracts\ABNF_ID => $user['userid'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_AVATAR => $user[Contracts\ABNF_AVATAR] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + ] : [ + Contracts\ABNF_ID => $user['UserId'] ?? null ?: $user['OpenId'] ?? null, + ]); } /** - * @throws \Overtrue\Socialite\Exceptions\AuthorizeFailedException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws Exceptions\AuthorizeFailedException */ protected function requestApiAccessToken(): string { - $response = $this->getHttpClient()->get( - $this->baseUrl . '/cgi-bin/gettoken', - [ - 'query' => array_filter( - [ - 'corpid' => $this->config->get('corp_id') ?? $this->config->get('corpid') ?? $this->config->get('client_id'), - 'corpsecret' => $this->config->get('corp_secret') ?? $this->config->get('corpsecret') ?? $this->config->get('client_secret'), - ] - ), - ] - ); + $responseInstance = $this->getHttpClient()->get($this->baseUrl . '/cgi-bin/gettoken', [ + 'query' => \array_filter( + [ + 'corpid' => $this->config->get('corp_id') + ?? $this->config->get('corpid') + ?? $this->config->get(Contracts\RFC6749_ABNF_CLIENT_ID), + 'corpsecret' => $this->config->get('corp_secret') + ?? $this->config->get('corpsecret') + ?? $this->config->get(Contracts\RFC6749_ABNF_CLIENT_SECRET), + ] + ), + ]); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($responseInstance); if (($response['errcode'] ?? 1) > 0) { - throw new AuthorizeFailedException('Failed to get api access_token:' . $response['errmsg'] ?? 'Unknown.', $response); + throw new Exceptions\AuthorizeFailedException((string)$responseInstance->getBody(), $response); } - return $response['access_token']; + return $response[Contracts\RFC6749_ABNF_ACCESS_TOKEN]; } protected function getTokenUrl(): string diff --git a/src/Providers/Weibo.php b/src/Providers/Weibo.php index c16d58f..572013e 100644 --- a/src/Providers/Weibo.php +++ b/src/Providers/Weibo.php @@ -4,8 +4,8 @@ use JetBrains\PhpStorm\ArrayShape; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Exceptions\InvalidTokenException; +use Overtrue\Socialite\Contracts; +use Overtrue\Socialite\Exceptions; use Overtrue\Socialite\User; /** @@ -14,89 +14,90 @@ class Weibo extends Base { public const NAME = 'weibo'; + protected string $baseUrl = 'https://api.weibo.com'; - protected array $scopes = ['email']; + protected array $scopes = [Contracts\ABNF_EMAIL]; protected function getAuthUrl(): string { - return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2/authorize'); + return $this->buildAuthUrlFromBase($this->baseUrl . '/oauth2/authorize'); } protected function getTokenUrl(): string { - return $this->baseUrl.'/2/oauth2/access_token'; + return $this->baseUrl . '/2/oauth2/access_token'; } #[ArrayShape([ - 'client_id' => "\null|string", - 'client_secret' => "\null|string", - 'code' => "string", - 'redirect_uri' => "mixed" + Contracts\RFC6749_ABNF_CLIENT_ID => 'null|string', + Contracts\RFC6749_ABNF_CLIENT_SECRET => 'null|string', + Contracts\RFC6749_ABNF_CODE => 'string', + Contracts\RFC6749_ABNF_REDIRECT_URI => 'null|string', + Contracts\RFC6749_ABNF_GRANT_TYPE => 'string', ])] protected function getTokenFields(string $code): array { - return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; + return parent::getTokenFields($code) + [ + Contracts\RFC6749_ABNF_GRANT_TYPE => Contracts\RFC6749_ABNF_AUTHORATION_CODE, + ]; } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * - * @throws \Overtrue\Socialite\Exceptions\InvalidTokenException + * @throws Exceptions\InvalidTokenException */ protected function getUserByToken(string $token): array { $uid = $this->getTokenPayload($token)['uid'] ?? null; if (empty($uid)) { - throw new InvalidTokenException('Invalid token.', $token); + throw new Exceptions\InvalidTokenException('Invalid token.', $token); } - $response = $this->getHttpClient()->get($this->baseUrl.'/2/users/show.json', [ + $response = $this->getHttpClient()->get($this->baseUrl . '/2/users/show.json', [ 'query' => [ 'uid' => $uid, - 'access_token' => $token, + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, ], 'headers' => [ 'Accept' => 'application/json', ], ]); - return \json_decode($response->getBody(), true) ?? []; + return $this->fromJsonBody($response); } /** - * @throws \GuzzleHttp\Exception\GuzzleException - * @throws \Overtrue\Socialite\Exceptions\InvalidTokenException + * @throws Exceptions\InvalidTokenException */ protected function getTokenPayload(string $token): array { - $response = $this->getHttpClient()->post($this->baseUrl.'/oauth2/get_token_info', [ + $response = $this->getHttpClient()->post($this->baseUrl . '/oauth2/get_token_info', [ 'query' => [ - 'access_token' => $token, + Contracts\RFC6749_ABNF_ACCESS_TOKEN => $token, ], 'headers' => [ 'Accept' => 'application/json', ], ]); - $response = \json_decode($response->getBody(), true) ?? []; + $response = $this->fromJsonBody($response); - if (empty($response['uid'])) { - throw new InvalidTokenException(\sprintf('Invalid token %s', $token), $token); + if (empty($response['uid'] ?? null)) { + throw new Exceptions\InvalidTokenException(\sprintf('Invalid token %s', $token), $token); } return $response; } #[Pure] - protected function mapUserToObject(array $user): UserInterface + protected function mapUserToObject(array $user): Contracts\UserInterface { return new User([ - 'id' => $user['id'] ?? null, - 'nickname' => $user['screen_name'] ?? null, - 'name' => $user['name'] ?? null, - 'email' => $user['email'] ?? null, - 'avatar' => $user['avatar_large'] ?? null, + Contracts\ABNF_ID => $user[Contracts\ABNF_ID] ?? null, + Contracts\ABNF_NICKNAME => $user['screen_name'] ?? null, + Contracts\ABNF_NAME => $user[Contracts\ABNF_NAME] ?? null, + Contracts\ABNF_EMAIL => $user[Contracts\ABNF_EMAIL] ?? null, + Contracts\ABNF_AVATAR => $user['avatar_large'] ?? null, ]); } } diff --git a/src/SocialiteManager.php b/src/SocialiteManager.php index 0d40241..cf0c15b 100644 --- a/src/SocialiteManager.php +++ b/src/SocialiteManager.php @@ -3,39 +3,36 @@ namespace Overtrue\Socialite; use Closure; -use InvalidArgumentException; use JetBrains\PhpStorm\Pure; -use Overtrue\Socialite\Contracts\FactoryInterface; -use Overtrue\Socialite\Contracts\ProviderInterface; -class SocialiteManager implements FactoryInterface +class SocialiteManager implements Contracts\FactoryInterface { protected Config $config; protected array $resolved = []; - protected array $customCreators = []; - protected array $providers = [ - Providers\QQ::NAME => Providers\QQ::class, - Providers\Tapd::NAME => Providers\Tapd::class, - Providers\Weibo::NAME => Providers\Weibo::class, + protected static array $customCreators = []; + protected const PROVIDERS = [ Providers\Alipay::NAME => Providers\Alipay::class, - Providers\QCloud::NAME => Providers\QCloud::class, - Providers\GitHub::NAME => Providers\GitHub::class, - Providers\Google::NAME => Providers\Google::class, - Providers\Figma::NAME => Providers\Figma::class, - Providers\WeChat::NAME => Providers\WeChat::class, - Providers\Douban::NAME => Providers\Douban::class, - Providers\WeWork::NAME => Providers\WeWork::class, + Providers\Azure::NAME => Providers\Azure::class, + Providers\DingTalk::NAME => Providers\DingTalk::class, Providers\DouYin::NAME => Providers\DouYin::class, - Providers\Taobao::NAME => Providers\Taobao::class, + Providers\Douban::NAME => Providers\Douban::class, + Providers\Facebook::NAME => Providers\Facebook::class, Providers\FeiShu::NAME => Providers\FeiShu::class, - Providers\Outlook::NAME => Providers\Outlook::class, - Providers\Azure::NAME => Providers\Azure::class, + Providers\Figma::NAME => Providers\Figma::class, + Providers\GitHub::NAME => Providers\GitHub::class, + Providers\Gitee::NAME => Providers\Gitee::class, + Providers\Google::NAME => Providers\Google::class, + Providers\Line::NAME => Providers\Line::class, Providers\Linkedin::NAME => Providers\Linkedin::class, - Providers\Facebook::NAME => Providers\Facebook::class, - Providers\DingTalk::NAME => Providers\DingTalk::class, Providers\OpenWeWork::NAME => Providers\OpenWeWork::class, - Providers\Line::NAME => Providers\Line::class, - Providers\Gitee::NAME => Providers\Gitee::class, + Providers\Outlook::NAME => Providers\Outlook::class, + Providers\QCloud::NAME => Providers\QCloud::class, + Providers\QQ::NAME => Providers\QQ::class, + Providers\Taobao::NAME => Providers\Taobao::class, + Providers\Tapd::NAME => Providers\Tapd::class, + Providers\WeChat::NAME => Providers\WeChat::class, + Providers\WeWork::NAME => Providers\WeWork::class, + Providers\Weibo::NAME => Providers\Weibo::class, ]; #[Pure] @@ -51,9 +48,9 @@ public function config(Config $config): static return $this; } - public function create(string $name): ProviderInterface + public function create(string $name): Contracts\ProviderInterface { - $name = strtolower($name); + $name = \strtolower($name); if (!isset($this->resolved[$name])) { $this->resolved[$name] = $this->createProvider($name); @@ -64,7 +61,7 @@ public function create(string $name): ProviderInterface public function extend(string $name, Closure $callback): self { - $this->customCreators[strtolower($name)] = $callback; + self::$customCreators[\strtolower($name)] = $callback; return $this; } @@ -74,37 +71,37 @@ public function getResolvedProviders(): array return $this->resolved; } - public function buildProvider(string $provider, array $config): ProviderInterface + public function buildProvider(string $provider, array $config): Contracts\ProviderInterface { return new $provider($config); } /** - * @throws \InvalidArgumentException + * @throws Exceptions\InvalidArgumentException */ - protected function createProvider(string $name): ProviderInterface + protected function createProvider(string $name): Contracts\ProviderInterface { $config = $this->config->get($name, []); $provider = $config['provider'] ?? $name; - if (isset($this->customCreators[$provider])) { + if (isset(self::$customCreators[$provider])) { return $this->callCustomCreator($provider, $config); } if (!$this->isValidProvider($provider)) { - throw new InvalidArgumentException("Provider [$provider] not supported."); + throw new Exceptions\InvalidArgumentException("Provider [{$name}] not supported."); } - return $this->buildProvider($this->providers[$provider] ?? $provider, $config); + return $this->buildProvider(self::PROVIDERS[$provider] ?? $provider, $config); } - protected function callCustomCreator(string $name, array $config): ProviderInterface + protected function callCustomCreator(string $name, array $config): Contracts\ProviderInterface { - return $this->customCreators[$name]($config); + return self::$customCreators[$name]($config); } protected function isValidProvider(string $provider): bool { - return isset($this->providers[$provider]) || is_subclass_of($provider, ProviderInterface::class); + return isset(self::PROVIDERS[$provider]) || \is_subclass_of($provider, Contracts\ProviderInterface::class); } } diff --git a/src/Traits/HasAttributes.php b/src/Traits/HasAttributes.php index d4e2133..bbf3dd0 100644 --- a/src/Traits/HasAttributes.php +++ b/src/Traits/HasAttributes.php @@ -18,23 +18,23 @@ public function getAttribute(string $name, mixed $default = null) return $this->attributes[$name] ?? $default; } - public function setAttribute(string $name, mixed $value): static + public function setAttribute(string $name, mixed $value): self { $this->attributes[$name] = $value; return $this; } - public function merge(array $attributes): static + public function merge(array $attributes): self { - $this->attributes = array_merge($this->attributes, $attributes); + $this->attributes = \array_merge($this->attributes, $attributes); return $this; } public function offsetExists(mixed $offset): bool { - return array_key_exists($offset, $this->attributes); + return \array_key_exists($offset, $this->attributes); } public function offsetGet(mixed $offset): mixed diff --git a/src/User.php b/src/User.php index 3786aa7..8747edf 100644 --- a/src/User.php +++ b/src/User.php @@ -4,78 +4,75 @@ use ArrayAccess; use JsonSerializable; -use Overtrue\Socialite\Contracts\ProviderInterface; -use Overtrue\Socialite\Contracts\UserInterface; -use Overtrue\Socialite\Traits\HasAttributes; -class User implements ArrayAccess, UserInterface, JsonSerializable +class User implements ArrayAccess, Contracts\UserInterface, JsonSerializable { - use HasAttributes; + use Traits\HasAttributes; - public function __construct(array $attributes, protected ?ProviderInterface $provider = null) + public function __construct(array $attributes, protected ?Contracts\ProviderInterface $provider = null) { $this->attributes = $attributes; } public function getId(): mixed { - return $this->getAttribute('id') ?? $this->getEmail(); + return $this->getAttribute(Contracts\ABNF_ID) ?? $this->getEmail(); } public function getNickname(): ?string { - return $this->getAttribute('nickname') ?? $this->getName(); + return $this->getAttribute(Contracts\ABNF_NICKNAME) ?? $this->getName(); } public function getName(): ?string { - return $this->getAttribute('name'); + return $this->getAttribute(Contracts\ABNF_NAME); } public function getEmail(): ?string { - return $this->getAttribute('email'); + return $this->getAttribute(Contracts\ABNF_EMAIL); } public function getAvatar(): ?string { - return $this->getAttribute('avatar'); + return $this->getAttribute(Contracts\ABNF_AVATAR); } - public function setAccessToken(string $token): self + public function setAccessToken(string $value): self { - $this->setAttribute('access_token', $token); + $this->setAttribute(Contracts\RFC6749_ABNF_ACCESS_TOKEN, $value); return $this; } public function getAccessToken(): ?string { - return $this->getAttribute('access_token'); + return $this->getAttribute(Contracts\RFC6749_ABNF_ACCESS_TOKEN); } - public function setRefreshToken(?string $refreshToken): self + public function setRefreshToken(?string $value): self { - $this->setAttribute('refresh_token', $refreshToken); + $this->setAttribute(Contracts\RFC6749_ABNF_REFRESH_TOKEN, $value); return $this; } public function getRefreshToken(): ?string { - return $this->getAttribute('refresh_token'); + return $this->getAttribute(Contracts\RFC6749_ABNF_REFRESH_TOKEN); } - public function setExpiresIn(int $expiresIn): self + public function setExpiresIn(int $value): self { - $this->setAttribute('expires_in', $expiresIn); + $this->setAttribute(Contracts\RFC6749_ABNF_EXPIRES_IN, $value); return $this; } public function getExpiresIn(): ?int { - return $this->getAttribute('expires_in'); + return $this->getAttribute(Contracts\RFC6749_ABNF_EXPIRES_IN); } public function setRaw(array $user): self @@ -90,7 +87,7 @@ public function getRaw(): array return $this->getAttribute('raw'); } - public function setTokenResponse(array $response): static + public function setTokenResponse(array $response): self { $this->setAttribute('token_response', $response); @@ -117,12 +114,12 @@ public function __unserialize(array $serialized) $this->attributes = $serialized ?: []; } - public function getProvider(): \Overtrue\Socialite\Contracts\ProviderInterface + public function getProvider(): Contracts\ProviderInterface { return $this->provider; } - public function setProvider(\Overtrue\Socialite\Contracts\ProviderInterface $provider): static + public function setProvider(Contracts\ProviderInterface $provider): self { $this->provider = $provider; diff --git a/tests/Providers/FeiShuTest.php b/tests/Providers/FeiShuTest.php index 59cc82f..8739e65 100644 --- a/tests/Providers/FeiShuTest.php +++ b/tests/Providers/FeiShuTest.php @@ -6,7 +6,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Client; -use Overtrue\Socialite\Exceptions\Feishu\InvalidTicketException; +use Overtrue\Socialite\Exceptions\FeiShu\InvalidTicketException; use Overtrue\Socialite\Exceptions\InvalidTokenException; use Overtrue\Socialite\Providers\FeiShu; use PHPUnit\Framework\TestCase; @@ -110,7 +110,7 @@ public function testConfigAppAccessTokenWithDefaultModeNoAppTicketWork() $mock = new MockHandler([ new Response(403, []), - new Response(200, [], json_encode([ + new Response(200, [], \json_encode([ 'app_access_token' => 'app_access_token' ])) ]); @@ -146,7 +146,7 @@ public function testConfigAppAccessTokenWithDefaultModeAndAppTicketWorkInBadResp $ff = new \ReflectionMethod(FeiShu::class, 'configAppAccessToken'); $mock = new MockHandler([ - new Response(200, []), + new Response(200, [], '{}'), ]); $handler = HandlerStack::create($mock); @@ -172,7 +172,7 @@ public function testConfigAppAccessTokenWithDefaultModeAndAppTicketWorkInGoodRes $ff = new \ReflectionMethod(FeiShu::class, 'configAppAccessToken'); $mock = new MockHandler([ - new Response(200, [], json_encode([ + new Response(200, [], \json_encode([ 'app_access_token' => 'app_access_token' ])) ]); @@ -202,7 +202,7 @@ public function testConfigAppAccessTokenWithInternalInBadResponse() $ff = new \ReflectionMethod(FeiShu::class, 'configAppAccessToken'); $mock = new MockHandler([ - new Response(200, []), + new Response(200, [], '{}'), ]); $handler = HandlerStack::create($mock); @@ -229,7 +229,7 @@ public function testConfigAppAccessTokenWithInternalInGoodResponse() $ff = new \ReflectionMethod(FeiShu::class, 'configAppAccessToken'); $mock = new MockHandler([ - new Response(200, [], json_encode([ + new Response(200, [], \json_encode([ 'app_access_token' => 'app_access_token' ])) ]); diff --git a/tests/Providers/WechatTest.php b/tests/Providers/WechatTest.php index 2d95eb0..6b29759 100644 --- a/tests/Providers/WechatTest.php +++ b/tests/Providers/WechatTest.php @@ -3,6 +3,9 @@ use PHPUnit\Framework\TestCase; use Overtrue\Socialite\Providers\WeChat; +// here we need loaded the symbols first. +\class_exists(\Overtrue\Socialite\Contracts\FactoryInterface::class); + class WechatTest extends TestCase { public function testWeChatProviderHasCorrectlyRedirectResponse()