136 changed files with 12805 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] |
|||
patreon: overtrue |
|||
open_collective: # Replace with a single Open Collective username |
|||
ko_fi: # Replace with a single Ko-fi username |
|||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel |
|||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry |
|||
custom: # Replace with a single custom sponsorship URL |
|||
@ -0,0 +1,9 @@ |
|||
/vendor |
|||
composer.phar |
|||
composer.lock |
|||
.DS_Store |
|||
/.idea |
|||
Thumbs.db |
|||
/*.php |
|||
sftp-config.json |
|||
.php_cs.cache |
|||
@ -0,0 +1,28 @@ |
|||
<?php |
|||
$header = <<<EOF |
|||
This file is part of the overtrue/socialite. |
|||
|
|||
(c) overtrue <i@overtrue.me> |
|||
|
|||
This source file is subject to the MIT license that is bundled |
|||
with this source code in the file LICENSE. |
|||
EOF; |
|||
|
|||
return PhpCsFixer\Config::create() |
|||
->setRiskyAllowed(true) |
|||
->setRules(array( |
|||
'@Symfony' => true, |
|||
'header_comment' => array('header' => $header), |
|||
'array_syntax' => array('syntax' => 'short'), |
|||
'ordered_imports' => true, |
|||
'no_useless_else' => true, |
|||
'no_useless_return' => true, |
|||
'php_unit_construct' => true, |
|||
'php_unit_strict' => true, |
|||
)) |
|||
->setFinder( |
|||
PhpCsFixer\Finder::create() |
|||
->exclude('vendor') |
|||
->in(__DIR__) |
|||
) |
|||
; |
|||
@ -0,0 +1,13 @@ |
|||
language: php |
|||
|
|||
php: |
|||
- 7.0 |
|||
- 7.1 |
|||
- 7.2 |
|||
|
|||
sudo: false |
|||
dist: trusty |
|||
|
|||
install: travis_retry composer install --no-interaction --prefer-source |
|||
|
|||
script: vendor/bin/phpunit --verbose |
|||
@ -0,0 +1,21 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) overtrue <i@overtrue.me> |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
|||
@ -0,0 +1,266 @@ |
|||
<h1 align="center"> Socialite</h1> |
|||
<p align="center"> |
|||
<a href="https://travis-ci.org/overtrue/socialite"><img src="https://travis-ci.org/overtrue/socialite.svg?branch=master" alt="Build Status"></a> |
|||
<a href="https://packagist.org/packages/overtrue/socialite"><img src="https://poser.pugx.org/overtrue/socialite/v/stable.svg" alt="Latest Stable Version"></a> |
|||
<a href="https://packagist.org/packages/overtrue/socialite"><img src="https://poser.pugx.org/overtrue/socialite/v/unstable.svg" alt="Latest Unstable Version"></a> |
|||
<a href="https://scrutinizer-ci.com/g/overtrue/socialite/build-status/master"><img src="https://scrutinizer-ci.com/g/overtrue/socialite/badges/build.png?b=master" alt="Build Status"></a> |
|||
<a href="https://scrutinizer-ci.com/g/overtrue/socialite/?branch=master"><img src="https://scrutinizer-ci.com/g/overtrue/socialite/badges/quality-score.png?b=master" alt="Scrutinizer Code Quality"></a> |
|||
<a href="https://scrutinizer-ci.com/g/overtrue/socialite/?branch=master"><img src="https://scrutinizer-ci.com/g/overtrue/socialite/badges/coverage.png?b=master" alt="Code Coverage"></a> |
|||
<a href="https://packagist.org/packages/overtrue/socialite"><img src="https://poser.pugx.org/overtrue/socialite/downloads" alt="Total Downloads"></a> |
|||
<a href="https://packagist.org/packages/overtrue/socialite"><img src="https://poser.pugx.org/overtrue/socialite/license" alt="License"></a> |
|||
</p> |
|||
|
|||
|
|||
<p align="center">Socialite is an OAuth2 Authentication tool. It is inspired by <a href="https://github.com/laravel/socialite">laravel/socialite</a>, You can easily use it in any PHP project.</p> |
|||
|
|||
# Requirement |
|||
|
|||
``` |
|||
PHP >= 7.0 |
|||
``` |
|||
# Installation |
|||
|
|||
```shell |
|||
$ composer require "overtrue/socialite" -vvv |
|||
``` |
|||
|
|||
# Usage |
|||
|
|||
For Laravel 5: [overtrue/laravel-socialite](https://github.com/overtrue/laravel-socialite) |
|||
|
|||
`authorize.php`: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Overtrue\Socialite\SocialiteManager; |
|||
|
|||
$config = [ |
|||
'github' => [ |
|||
'client_id' => 'your-app-id', |
|||
'client_secret' => 'your-app-secret', |
|||
'redirect' => 'http://localhost/socialite/callback.php', |
|||
], |
|||
]; |
|||
|
|||
$socialite = new SocialiteManager($config); |
|||
|
|||
$response = $socialite->driver('github')->redirect(); |
|||
|
|||
echo $response;// or $response->send(); |
|||
``` |
|||
|
|||
`callback.php`: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Overtrue\Socialite\SocialiteManager; |
|||
|
|||
$config = [ |
|||
'github' => [ |
|||
'client_id' => 'your-app-id', |
|||
'client_secret' => 'your-app-secret', |
|||
'redirect' => 'http://localhost/socialite/callback.php', |
|||
], |
|||
]; |
|||
|
|||
$socialite = new SocialiteManager($config); |
|||
|
|||
$user = $socialite->driver('github')->user(); |
|||
|
|||
$user->getId(); // 1472352 |
|||
$user->getNickname(); // "overtrue" |
|||
$user->getUsername(); // "overtrue" |
|||
$user->getName(); // "安正超" |
|||
$user->getEmail(); // "anzhengchao@gmail.com" |
|||
$user->getProviderName(); // GitHub |
|||
... |
|||
``` |
|||
|
|||
### Configuration |
|||
|
|||
Now we support the following sites: |
|||
|
|||
`facebook`, `github`, `google`, `linkedin`, `outlook`, `weibo`, `taobao`, `qq`, `wechat`, `douyin`, and `douban`. |
|||
|
|||
Each driver uses the same configuration keys: `client_id`, `client_secret`, `redirect`. |
|||
|
|||
Example: |
|||
``` |
|||
... |
|||
'weibo' => [ |
|||
'client_id' => 'your-app-id', |
|||
'client_secret' => 'your-app-secret', |
|||
'redirect' => 'http://localhost/socialite/callback.php', |
|||
], |
|||
... |
|||
``` |
|||
|
|||
### Scope |
|||
|
|||
Before redirecting the user, you may also set "scopes" on the request using the scope method. This method will overwrite all existing scopes: |
|||
|
|||
```php |
|||
$response = $socialite->driver('github') |
|||
->scopes(['scope1', 'scope2'])->redirect(); |
|||
|
|||
``` |
|||
|
|||
### Redirect URL |
|||
|
|||
You may also want to dynamicly set `redirect`,you can use the following methods to change the `redirect` URL: |
|||
|
|||
```php |
|||
$socialite->redirect($url); |
|||
// or |
|||
$socialite->withRedirectUrl($url)->redirect(); |
|||
// or |
|||
$socialite->setRedirectUrl($url)->redirect(); |
|||
``` |
|||
|
|||
> WeChat scopes: |
|||
- `snsapi_base`, `snsapi_userinfo` - Used to Media Platform Authentication. |
|||
- `snsapi_login` - Used to web Authentication. |
|||
|
|||
### Additional parameters |
|||
|
|||
To include any optional parameters in the request, call the with method with an associative array: |
|||
|
|||
```php |
|||
$response = $socialite->driver('google') |
|||
->with(['hd' => 'example.com'])->redirect(); |
|||
``` |
|||
|
|||
### User interface |
|||
|
|||
#### Standard user api: |
|||
|
|||
```php |
|||
|
|||
$user = $socialite->driver('weibo')->user(); |
|||
``` |
|||
|
|||
```json |
|||
{ |
|||
"id": 1472352, |
|||
"nickname": "overtrue", |
|||
"name": "安正超", |
|||
"email": "anzhengchao@gmail.com", |
|||
"avatar": "https://avatars.githubusercontent.com/u/1472352?v=3", |
|||
"original": { |
|||
"login": "overtrue", |
|||
"id": 1472352, |
|||
"avatar_url": "https://avatars.githubusercontent.com/u/1472352?v=3", |
|||
"gravatar_id": "", |
|||
"url": "https://api.github.com/users/overtrue", |
|||
"html_url": "https://github.com/overtrue", |
|||
... |
|||
}, |
|||
"token": { |
|||
"access_token": "5b1dc56d64fffbd052359f032716cc4e0a1cb9a0", |
|||
"token_type": "bearer", |
|||
"scope": "user:email" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
You can fetch the user attribute as a array keys like these: |
|||
|
|||
```php |
|||
$user['id']; // 1472352 |
|||
$user['nickname']; // "overtrue" |
|||
$user['name']; // "安正超" |
|||
$user['email']; // "anzhengchao@gmail.com" |
|||
... |
|||
``` |
|||
|
|||
Or using the method: |
|||
|
|||
```php |
|||
$user->getId(); |
|||
$user->getNickname(); |
|||
$user->getName(); |
|||
$user->getEmail(); |
|||
$user->getAvatar(); |
|||
$user->getOriginal(); |
|||
$user->getToken();// or $user->getAccessToken() |
|||
$user->getProviderName(); // GitHub/Google/Facebook... |
|||
``` |
|||
|
|||
#### Get original response from OAuth API |
|||
|
|||
The `$user->getOriginal()` method will return an array of the API raw response. |
|||
|
|||
#### Get access token Object |
|||
|
|||
You can get the access token instance of current session by call `$user->getToken()` or `$user->getAccessToken()` or `$user['token']` . |
|||
|
|||
|
|||
### Get user with access token |
|||
|
|||
```php |
|||
$accessToken = new AccessToken(['access_token' => $accessToken]); |
|||
$user = $socialite->user($accessToken); |
|||
``` |
|||
|
|||
|
|||
### Custom Session or Request instance. |
|||
|
|||
You can set the request with your custom `Request` instance which instanceof `Symfony\Component\HttpFoundation\Request` before you call `driver` method. |
|||
|
|||
|
|||
```php |
|||
|
|||
$request = new Request(); // or use AnotherCustomRequest. |
|||
|
|||
$socialite = new SocialiteManager($config, $request); |
|||
``` |
|||
|
|||
Or set request to `SocialiteManager` instance: |
|||
|
|||
```php |
|||
$socialite->setRequest($request); |
|||
``` |
|||
|
|||
You can get the request from the `SocialiteManager` instance by `getRequest()`: |
|||
|
|||
```php |
|||
$request = $socialite->getRequest(); |
|||
``` |
|||
|
|||
#### Set custom session manager. |
|||
|
|||
By default, the `SocialiteManager` uses the `Symfony\Component\HttpFoundation\Session\Session` instance as session manager, you can change it as follows: |
|||
|
|||
```php |
|||
$session = new YourCustomSessionManager(); |
|||
$socialite->getRequest()->setSession($session); |
|||
``` |
|||
|
|||
> Your custom session manager must be implement the [`Symfony\Component\HttpFoundation\Session\SessionInterface`](http://api.symfony.com/3.0/Symfony/Component/HttpFoundation/Session/SessionInterface.html). |
|||
|
|||
Enjoy it! :heart: |
|||
|
|||
# Reference |
|||
|
|||
- [Google - OpenID Connect](https://developers.google.com/identity/protocols/OpenIDConnect) |
|||
- [Facebook - Graph API](https://developers.facebook.com/docs/graph-api) |
|||
- [Linkedin - Authenticating with OAuth 2.0](https://developer.linkedin.com/docs/oauth2) |
|||
- [微博 - OAuth 2.0 授权机制说明](http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E) |
|||
- [QQ - OAuth 2.0 登录QQ](http://wiki.connect.qq.com/oauth2-0%E7%AE%80%E4%BB%8B) |
|||
- [微信公众平台 - OAuth文档](http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html) |
|||
- [微信开放平台 - 网站应用微信登录开发指南](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN) |
|||
- [微信开放平台 - 代公众号发起网页授权](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318590&token=&lang=zh_CN) |
|||
- [豆瓣 - OAuth 2.0 授权机制说明](http://developers.douban.com/wiki/?title=oauth2) |
|||
- [抖音 - 网站应用开发指南](http://open.douyin.com/platform/doc) |
|||
|
|||
## PHP 扩展包开发 |
|||
|
|||
> 想知道如何从零开始构建 PHP 扩展包? |
|||
> |
|||
> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) |
|||
|
|||
# License |
|||
|
|||
MIT |
|||
@ -0,0 +1,26 @@ |
|||
{ |
|||
"name": "overtrue/socialite", |
|||
"description": "A collection of OAuth 2 packages that extracts from laravel/socialite.", |
|||
"keywords": ["OAuth", "social", "login", "Weibo", "WeChat", "QQ"], |
|||
"autoload": { |
|||
"psr-4": { |
|||
"Overtrue\\Socialite\\": "src/" |
|||
} |
|||
}, |
|||
"require": { |
|||
"php": ">=7.0", |
|||
"guzzlehttp/guzzle": "~5.0|~6.0", |
|||
"symfony/http-foundation": "^2.7|^3.0|^4.0" |
|||
}, |
|||
"require-dev": { |
|||
"mockery/mockery": "~1.2", |
|||
"phpunit/phpunit": "~6" |
|||
}, |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "overtrue", |
|||
"email": "anzhengchao@gmail.com" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<phpunit backupGlobals="false" |
|||
backupStaticAttributes="false" |
|||
bootstrap="vendor/autoload.php" |
|||
colors="true" |
|||
convertErrorsToExceptions="true" |
|||
convertNoticesToExceptions="true" |
|||
convertWarningsToExceptions="true" |
|||
processIsolation="false" |
|||
stopOnFailure="false" |
|||
syntaxCheck="false" |
|||
> |
|||
<testsuites> |
|||
<testsuite name="Package Test Suite"> |
|||
<directory suffix=".php">./tests/</directory> |
|||
</testsuite> |
|||
</testsuites> |
|||
</phpunit> |
|||
@ -0,0 +1,64 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
use ArrayAccess; |
|||
use InvalidArgumentException; |
|||
use JsonSerializable; |
|||
|
|||
/** |
|||
* Class AccessToken. |
|||
*/ |
|||
class AccessToken implements AccessTokenInterface, ArrayAccess, JsonSerializable |
|||
{ |
|||
use HasAttributes; |
|||
|
|||
/** |
|||
* AccessToken constructor. |
|||
* |
|||
* @param array $attributes |
|||
*/ |
|||
public function __construct(array $attributes) |
|||
{ |
|||
if (empty($attributes['access_token'])) { |
|||
throw new InvalidArgumentException('The key "access_token" could not be empty.'); |
|||
} |
|||
|
|||
$this->attributes = $attributes; |
|||
} |
|||
|
|||
/** |
|||
* Return the access token string. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getToken() |
|||
{ |
|||
return $this->getAttribute('access_token'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
return strval($this->getAttribute('access_token', '')); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function jsonSerialize() |
|||
{ |
|||
return $this->getToken(); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
/** |
|||
* Interface AccessTokenInterface. |
|||
*/ |
|||
interface AccessTokenInterface |
|||
{ |
|||
/** |
|||
* Return the access token string. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getToken(); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
class AuthorizeFailedException extends \RuntimeException |
|||
{ |
|||
/** |
|||
* Response body. |
|||
* |
|||
* @var array |
|||
*/ |
|||
public $body; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* @param string $message |
|||
* @param array $body |
|||
*/ |
|||
public function __construct($message, $body) |
|||
{ |
|||
parent::__construct($message, -1); |
|||
|
|||
$this->body = $body; |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
use ArrayAccess; |
|||
use InvalidArgumentException; |
|||
|
|||
/** |
|||
* Class Config. |
|||
*/ |
|||
class Config implements ArrayAccess |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $config; |
|||
|
|||
/** |
|||
* Config constructor. |
|||
* |
|||
* @param array $config |
|||
*/ |
|||
public function __construct(array $config) |
|||
{ |
|||
$this->config = $config; |
|||
} |
|||
|
|||
/** |
|||
* Get an item from an array using "dot" notation. |
|||
* |
|||
* @param string $key |
|||
* @param mixed $default |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function get($key, $default = null) |
|||
{ |
|||
$config = $this->config; |
|||
|
|||
if (is_null($key)) { |
|||
return $config; |
|||
} |
|||
if (isset($config[$key])) { |
|||
return $config[$key]; |
|||
} |
|||
foreach (explode('.', $key) as $segment) { |
|||
if (!is_array($config) || !array_key_exists($segment, $config)) { |
|||
return $default; |
|||
} |
|||
$config = $config[$segment]; |
|||
} |
|||
|
|||
return $config; |
|||
} |
|||
|
|||
/** |
|||
* Set an array item to a given value using "dot" notation. |
|||
* |
|||
* @param string $key |
|||
* @param mixed $value |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function set($key, $value) |
|||
{ |
|||
if (is_null($key)) { |
|||
throw new InvalidArgumentException('Invalid config key.'); |
|||
} |
|||
|
|||
$keys = explode('.', $key); |
|||
$config = &$this->config; |
|||
|
|||
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; |
|||
|
|||
return $config; |
|||
} |
|||
|
|||
/** |
|||
* Determine if the given configuration value exists. |
|||
* |
|||
* @param string $key |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function has($key) |
|||
{ |
|||
return (bool) $this->get($key); |
|||
} |
|||
|
|||
/** |
|||
* Whether a offset exists. |
|||
* |
|||
* @see http://php.net/manual/en/arrayaccess.offsetexists.php |
|||
* |
|||
* @param mixed $offset <p> |
|||
* An offset to check for. |
|||
* </p> |
|||
* |
|||
* @return bool true on success or false on failure. |
|||
* </p> |
|||
* <p> |
|||
* The return value will be casted to boolean if non-boolean was returned |
|||
* |
|||
* @since 5.0.0 |
|||
*/ |
|||
public function offsetExists($offset) |
|||
{ |
|||
return array_key_exists($offset, $this->config); |
|||
} |
|||
|
|||
/** |
|||
* Offset to retrieve. |
|||
* |
|||
* @see http://php.net/manual/en/arrayaccess.offsetget.php |
|||
* |
|||
* @param mixed $offset <p> |
|||
* The offset to retrieve. |
|||
* </p> |
|||
* |
|||
* @return mixed Can return all value types |
|||
* |
|||
* @since 5.0.0 |
|||
*/ |
|||
public function offsetGet($offset) |
|||
{ |
|||
return $this->get($offset); |
|||
} |
|||
|
|||
/** |
|||
* Offset to set. |
|||
* |
|||
* @see http://php.net/manual/en/arrayaccess.offsetset.php |
|||
* |
|||
* @param mixed $offset <p> |
|||
* The offset to assign the value to. |
|||
* </p> |
|||
* @param mixed $value <p> |
|||
* The value to set. |
|||
* </p> |
|||
* |
|||
* @since 5.0.0 |
|||
*/ |
|||
public function offsetSet($offset, $value) |
|||
{ |
|||
$this->set($offset, $value); |
|||
} |
|||
|
|||
/** |
|||
* Offset to unset. |
|||
* |
|||
* @see http://php.net/manual/en/arrayaccess.offsetunset.php |
|||
* |
|||
* @param mixed $offset <p> |
|||
* The offset to unset. |
|||
* </p> |
|||
* |
|||
* @since 5.0.0 |
|||
*/ |
|||
public function offsetUnset($offset) |
|||
{ |
|||
$this->set($offset, null); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
/** |
|||
* Interface FactoryInterface. |
|||
*/ |
|||
interface FactoryInterface |
|||
{ |
|||
/** |
|||
* Get an OAuth provider implementation. |
|||
* |
|||
* @param string $driver |
|||
* |
|||
* @return \Overtrue\Socialite\ProviderInterface |
|||
*/ |
|||
public function driver($driver); |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
/** |
|||
* Trait HasAttributes. |
|||
*/ |
|||
trait HasAttributes |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $attributes = []; |
|||
|
|||
/** |
|||
* Return the attributes. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getAttributes() |
|||
{ |
|||
return $this->attributes; |
|||
} |
|||
|
|||
/** |
|||
* Return the extra attribute. |
|||
* |
|||
* @param string $name |
|||
* @param string $default |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function getAttribute($name, $default = null) |
|||
{ |
|||
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default; |
|||
} |
|||
|
|||
/** |
|||
* Set extra attributes. |
|||
* |
|||
* @param string $name |
|||
* @param mixed $value |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setAttribute($name, $value) |
|||
{ |
|||
$this->attributes[$name] = $value; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Map the given array onto the user's properties. |
|||
* |
|||
* @param array $attributes |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function merge(array $attributes) |
|||
{ |
|||
$this->attributes = array_merge($this->attributes, $attributes); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function offsetExists($offset) |
|||
{ |
|||
return array_key_exists($offset, $this->attributes); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function offsetGet($offset) |
|||
{ |
|||
return $this->getAttribute($offset); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function offsetSet($offset, $value) |
|||
{ |
|||
$this->setAttribute($offset, $value); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function offsetUnset($offset) |
|||
{ |
|||
unset($this->attributes[$offset]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function __get($property) |
|||
{ |
|||
return $this->getAttribute($property); |
|||
} |
|||
|
|||
/** |
|||
* Return array. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function toArray() |
|||
{ |
|||
return $this->getAttributes(); |
|||
} |
|||
|
|||
/** |
|||
* Return JSON. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function toJSON() |
|||
{ |
|||
return json_encode($this->getAttributes(), JSON_UNESCAPED_UNICODE); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
class InvalidArgumentException extends \InvalidArgumentException |
|||
{ |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
class InvalidStateException extends \InvalidArgumentException |
|||
{ |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
interface ProviderInterface |
|||
{ |
|||
/** |
|||
* Redirect the user to the authentication page for the provider. |
|||
* |
|||
* @return \Symfony\Component\HttpFoundation\RedirectResponse |
|||
*/ |
|||
public function redirect(); |
|||
|
|||
/** |
|||
* Get the User instance for the authenticated user. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
public function user(AccessTokenInterface $token = null); |
|||
} |
|||
@ -0,0 +1,560 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use GuzzleHttp\Client; |
|||
use GuzzleHttp\ClientInterface; |
|||
use Overtrue\Socialite\AccessToken; |
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\AuthorizeFailedException; |
|||
use Overtrue\Socialite\InvalidStateException; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Symfony\Component\HttpFoundation\RedirectResponse; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
|
|||
/** |
|||
* Class AbstractProvider. |
|||
*/ |
|||
abstract class AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* Provider name. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $name; |
|||
|
|||
/** |
|||
* The HTTP request instance. |
|||
* |
|||
* @var \Symfony\Component\HttpFoundation\Request |
|||
*/ |
|||
protected $request; |
|||
|
|||
/** |
|||
* The client ID. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $clientId; |
|||
|
|||
/** |
|||
* The client secret. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $clientSecret; |
|||
|
|||
/** |
|||
* @var \Overtrue\Socialite\AccessTokenInterface |
|||
*/ |
|||
protected $accessToken; |
|||
|
|||
/** |
|||
* The redirect URL. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $redirectUrl; |
|||
|
|||
/** |
|||
* The custom parameters to be sent with the request. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $parameters = []; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = []; |
|||
|
|||
/** |
|||
* The separating character for the requested scopes. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $scopeSeparator = ','; |
|||
|
|||
/** |
|||
* The type of the encoding in the query. |
|||
* |
|||
* @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738 |
|||
*/ |
|||
protected $encodingType = PHP_QUERY_RFC1738; |
|||
|
|||
/** |
|||
* Indicates if the session state should be utilized. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $stateless = false; |
|||
|
|||
/** |
|||
* The options for guzzle\client. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected static $guzzleOptions = ['http_errors' => false]; |
|||
|
|||
/** |
|||
* Create a new provider instance. |
|||
* |
|||
* @param \Symfony\Component\HttpFoundation\Request $request |
|||
* @param string $clientId |
|||
* @param string $clientSecret |
|||
* @param string|null $redirectUrl |
|||
*/ |
|||
public function __construct(Request $request, $clientId, $clientSecret, $redirectUrl = null) |
|||
{ |
|||
$this->request = $request; |
|||
$this->clientId = $clientId; |
|||
$this->clientSecret = $clientSecret; |
|||
$this->redirectUrl = $redirectUrl; |
|||
} |
|||
|
|||
/** |
|||
* Get the authentication URL for the provider. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
abstract protected function getAuthUrl($state); |
|||
|
|||
/** |
|||
* Get the token URL for the provider. |
|||
* |
|||
* @return string |
|||
*/ |
|||
abstract protected function getTokenUrl(); |
|||
|
|||
/** |
|||
* Get the raw user for the given access token. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
abstract protected function getUserByToken(AccessTokenInterface $token); |
|||
|
|||
/** |
|||
* Map the raw user array to a Socialite User instance. |
|||
* |
|||
* @param array $user |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
abstract protected function mapUserToObject(array $user); |
|||
|
|||
/** |
|||
* Redirect the user of the application to the provider's authentication screen. |
|||
* |
|||
* @param string $redirectUrl |
|||
* |
|||
* @return \Symfony\Component\HttpFoundation\RedirectResponse |
|||
*/ |
|||
public function redirect($redirectUrl = null) |
|||
{ |
|||
$state = null; |
|||
|
|||
if (!is_null($redirectUrl)) { |
|||
$this->redirectUrl = $redirectUrl; |
|||
} |
|||
|
|||
if ($this->usesState()) { |
|||
$state = $this->makeState(); |
|||
} |
|||
|
|||
return new RedirectResponse($this->getAuthUrl($state)); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function user(AccessTokenInterface $token = null) |
|||
{ |
|||
if (is_null($token) && $this->hasInvalidState()) { |
|||
throw new InvalidStateException(); |
|||
} |
|||
|
|||
$token = $token ?: $this->getAccessToken($this->getCode()); |
|||
|
|||
$user = $this->getUserByToken($token); |
|||
|
|||
$user = $this->mapUserToObject($user)->merge(['original' => $user]); |
|||
|
|||
return $user->setToken($token)->setProviderName($this->getName()); |
|||
} |
|||
|
|||
/** |
|||
* Set redirect url. |
|||
* |
|||
* @param string $redirectUrl |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setRedirectUrl($redirectUrl) |
|||
{ |
|||
$this->redirectUrl = $redirectUrl; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Set redirect url. |
|||
* |
|||
* @param string $redirectUrl |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function withRedirectUrl($redirectUrl) |
|||
{ |
|||
$this->redirectUrl = $redirectUrl; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Return the redirect url. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getRedirectUrl() |
|||
{ |
|||
return $this->redirectUrl; |
|||
} |
|||
|
|||
/** |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $accessToken |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setAccessToken(AccessTokenInterface $accessToken) |
|||
{ |
|||
$this->accessToken = $accessToken; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessTokenInterface |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
if ($this->accessToken) { |
|||
return $this->accessToken; |
|||
} |
|||
|
|||
$postKey = (1 === version_compare(ClientInterface::VERSION, '6')) ? 'form_params' : 'body'; |
|||
|
|||
$response = $this->getHttpClient()->post($this->getTokenUrl(), [ |
|||
'headers' => ['Accept' => 'application/json'], |
|||
$postKey => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()); |
|||
} |
|||
|
|||
/** |
|||
* Set the scopes of the requested access. |
|||
* |
|||
* @param array $scopes |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function scopes(array $scopes) |
|||
{ |
|||
$this->scopes = $scopes; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Set the request instance. |
|||
* |
|||
* @param Request $request |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setRequest(Request $request) |
|||
{ |
|||
$this->request = $request; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get the request instance. |
|||
* |
|||
* @return \Symfony\Component\HttpFoundation\Request |
|||
*/ |
|||
public function getRequest() |
|||
{ |
|||
return $this->request; |
|||
} |
|||
|
|||
/** |
|||
* Indicates that the provider should operate as stateless. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function stateless() |
|||
{ |
|||
$this->stateless = true; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Set the custom parameters of the request. |
|||
* |
|||
* @param array $parameters |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function with(array $parameters) |
|||
{ |
|||
$this->parameters = $parameters; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @throws \ReflectionException |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
if (empty($this->name)) { |
|||
$this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true); |
|||
} |
|||
|
|||
return $this->name; |
|||
} |
|||
|
|||
/** |
|||
* Get the authentication URL for the provider. |
|||
* |
|||
* @param string $url |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function buildAuthUrlFromBase($url, $state) |
|||
{ |
|||
return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType); |
|||
} |
|||
|
|||
/** |
|||
* Get the GET parameters for the code request. |
|||
* |
|||
* @param string|null $state |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getCodeFields($state = null) |
|||
{ |
|||
$fields = array_merge([ |
|||
'client_id' => $this->clientId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), |
|||
'response_type' => 'code', |
|||
], $this->parameters); |
|||
|
|||
if ($this->usesState()) { |
|||
$fields['state'] = $state; |
|||
} |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* Format the given scopes. |
|||
* |
|||
* @param array $scopes |
|||
* @param string $scopeSeparator |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function formatScopes(array $scopes, $scopeSeparator) |
|||
{ |
|||
return implode($scopeSeparator, $scopes); |
|||
} |
|||
|
|||
/** |
|||
* Determine if the current request / session has a mismatching "state". |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function hasInvalidState() |
|||
{ |
|||
if ($this->isStateless()) { |
|||
return false; |
|||
} |
|||
|
|||
$state = $this->request->getSession()->get('state'); |
|||
|
|||
return !(strlen($state) > 0 && $this->request->get('state') === $state); |
|||
} |
|||
|
|||
/** |
|||
* Get the POST fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return [ |
|||
'client_id' => $this->clientId, |
|||
'client_secret' => $this->clientSecret, |
|||
'code' => $code, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token from the token response body. |
|||
* |
|||
* @param \Psr\Http\Message\StreamInterface|array $body |
|||
* |
|||
* @return \Overtrue\Socialite\AccessTokenInterface |
|||
*/ |
|||
protected function parseAccessToken($body) |
|||
{ |
|||
if (!is_array($body)) { |
|||
$body = json_decode($body, true); |
|||
} |
|||
|
|||
if (empty($body['access_token'])) { |
|||
throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body); |
|||
} |
|||
|
|||
return new AccessToken($body); |
|||
} |
|||
|
|||
/** |
|||
* Get the code from the request. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getCode() |
|||
{ |
|||
return $this->request->get('code'); |
|||
} |
|||
|
|||
/** |
|||
* Get a fresh instance of the Guzzle HTTP client. |
|||
* |
|||
* @return \GuzzleHttp\Client |
|||
*/ |
|||
protected function getHttpClient() |
|||
{ |
|||
return new Client(self::$guzzleOptions); |
|||
} |
|||
|
|||
/** |
|||
* Set options for Guzzle HTTP client. |
|||
* |
|||
* @param array $config |
|||
* |
|||
* @return array |
|||
*/ |
|||
public static function setGuzzleOptions($config = []) |
|||
{ |
|||
return self::$guzzleOptions = $config; |
|||
} |
|||
|
|||
/** |
|||
* Determine if the provider is operating with state. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function usesState() |
|||
{ |
|||
return !$this->stateless; |
|||
} |
|||
|
|||
/** |
|||
* Determine if the provider is operating as stateless. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function isStateless() |
|||
{ |
|||
return !$this->request->hasSession() || $this->stateless; |
|||
} |
|||
|
|||
/** |
|||
* Return array item by key. |
|||
* |
|||
* @param array $array |
|||
* @param string $key |
|||
* @param mixed $default |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function arrayItem(array $array, $key, $default = null) |
|||
{ |
|||
if (is_null($key)) { |
|||
return $array; |
|||
} |
|||
|
|||
if (isset($array[$key])) { |
|||
return $array[$key]; |
|||
} |
|||
|
|||
foreach (explode('.', $key) as $segment) { |
|||
if (!is_array($array) || !array_key_exists($segment, $array)) { |
|||
return $default; |
|||
} |
|||
|
|||
$array = $array[$segment]; |
|||
} |
|||
|
|||
return $array; |
|||
} |
|||
|
|||
/** |
|||
* Put state to session storage and return it. |
|||
* |
|||
* @return string|bool |
|||
*/ |
|||
protected function makeState() |
|||
{ |
|||
if (!$this->request->hasSession()) { |
|||
return false; |
|||
} |
|||
|
|||
$state = sha1(uniqid(mt_rand(1, 1000000), true)); |
|||
$session = $this->request->getSession(); |
|||
|
|||
if (is_callable([$session, 'put'])) { |
|||
$session->put('state', $state); |
|||
} elseif (is_callable([$session, 'set'])) { |
|||
$session->set('state', $state); |
|||
} else { |
|||
return false; |
|||
} |
|||
|
|||
return $state; |
|||
} |
|||
} |
|||
@ -0,0 +1,168 @@ |
|||
<?php |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessToken; |
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class DouYinProvider. |
|||
* |
|||
* @author haoliang@qiyuankeji.vip |
|||
* |
|||
* @see http://open.douyin.com/platform |
|||
*/ |
|||
class DouYinProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* 抖音接口域名. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $baseUrl = 'https://open.douyin.com'; |
|||
|
|||
/** |
|||
* 应用授权作用域. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['user_info']; |
|||
|
|||
/** |
|||
* 获取登录页面地址. |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase($this->baseUrl.'/platform/oauth/connect', $state); |
|||
} |
|||
|
|||
/** |
|||
* 获取授权码接口参数. |
|||
* |
|||
* @param string|null $state |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getCodeFields($state = null) |
|||
{ |
|||
$fields = [ |
|||
'client_key' => $this->clientId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), |
|||
'response_type' => 'code', |
|||
]; |
|||
|
|||
if ($this->usesState()) { |
|||
$fields['state'] = $state; |
|||
} |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* 获取access_token地址. |
|||
* |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return $this->baseUrl.'/oauth/access_token'; |
|||
} |
|||
|
|||
/** |
|||
* 通过code获取access_token. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->get($this->getTokenUrl(), [ |
|||
'query' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()->getContents()); |
|||
} |
|||
|
|||
/** |
|||
* 获取access_token接口参数. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return [ |
|||
'client_key' => $this->clientId, |
|||
'client_secret' => $this->clientSecret, |
|||
'code' => $code, |
|||
'grant_type' => 'authorization_code', |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* 格式化token. |
|||
* |
|||
* @param \Psr\Http\Message\StreamInterface|array $body |
|||
* |
|||
* @return \Overtrue\Socialite\AccessTokenInterface |
|||
*/ |
|||
protected function parseAccessToken($body) |
|||
{ |
|||
if (!is_array($body)) { |
|||
$body = json_decode($body, true); |
|||
} |
|||
|
|||
if (empty($body['data']['access_token'])) { |
|||
throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body); |
|||
} |
|||
|
|||
return new AccessToken($body['data']); |
|||
} |
|||
|
|||
/** |
|||
* 通过token 获取用户信息. |
|||
* |
|||
* @param AccessTokenInterface $token |
|||
* |
|||
* @return array|mixed |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$userUrl = $this->baseUrl.'/oauth/userinfo/'; |
|||
|
|||
$response = $this->getHttpClient()->get( |
|||
$userUrl, [ |
|||
'query' => [ |
|||
'access_token' => $token->getToken(), |
|||
'open_id' => $token['open_id'], |
|||
], |
|||
] |
|||
); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* 格式化用户信息. |
|||
* |
|||
* @param array $user |
|||
* |
|||
* @return User |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'open_id'), |
|||
'username' => $this->arrayItem($user, 'nickname'), |
|||
'nickname' => $this->arrayItem($user, 'nickname'), |
|||
'avatar' => $this->arrayItem($user, 'avatar'), |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class DoubanProvider. |
|||
* |
|||
* @see http://developers.douban.com/wiki/?title=oauth2 [使用 OAuth 2.0 访问豆瓣 API] |
|||
*/ |
|||
class DoubanProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://www.douban.com/service/auth2/auth', $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'https://www.douban.com/service/auth2/token'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->get('https://api.douban.com/v2/user/~me', [ |
|||
'headers' => [ |
|||
'Authorization' => 'Bearer '.$token->getToken(), |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody()->getContents(), true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'nickname' => $this->arrayItem($user, 'name'), |
|||
'name' => $this->arrayItem($user, 'name'), |
|||
'avatar' => $this->arrayItem($user, 'large_avatar'), |
|||
'email' => null, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->post($this->getTokenUrl(), [ |
|||
'form_params' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()->getContents()); |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class FacebookProvider. |
|||
* |
|||
* @see https://developers.facebook.com/docs/graph-api [Facebook - Graph API] |
|||
*/ |
|||
class FacebookProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The base Facebook Graph URL. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $graphUrl = 'https://graph.facebook.com'; |
|||
|
|||
/** |
|||
* The Graph API version for the request. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $version = 'v3.3'; |
|||
|
|||
/** |
|||
* The user fields being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $fields = ['first_name', 'last_name', 'email', 'gender', 'verified']; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['email']; |
|||
|
|||
/** |
|||
* Display the dialog in a popup view. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $popup = false; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://www.facebook.com/'.$this->version.'/dialog/oauth', $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return $this->graphUrl.'/oauth/access_token'; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->get($this->getTokenUrl(), [ |
|||
'query' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$appSecretProof = hash_hmac('sha256', $token->getToken(), $this->clientSecret); |
|||
|
|||
$response = $this->getHttpClient()->get($this->graphUrl.'/'.$this->version.'/me?access_token='.$token.'&appsecret_proof='.$appSecretProof.'&fields='.implode(',', $this->fields), [ |
|||
'headers' => [ |
|||
'Accept' => 'application/json', |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
$avatarUrl = $this->graphUrl.'/'.$this->version.'/'.$user['id'].'/picture'; |
|||
|
|||
$firstName = $this->arrayItem($user, 'first_name'); |
|||
$lastName = $this->arrayItem($user, 'last_name'); |
|||
|
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'nickname' => null, |
|||
'name' => $firstName.' '.$lastName, |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
'avatar' => $avatarUrl.'?type=normal', |
|||
'avatar_original' => $avatarUrl.'?width=1920', |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getCodeFields($state = null) |
|||
{ |
|||
$fields = parent::getCodeFields($state); |
|||
|
|||
if ($this->popup) { |
|||
$fields['display'] = 'popup'; |
|||
} |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* Set the user fields to request from Facebook. |
|||
* |
|||
* @param array $fields |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function fields(array $fields) |
|||
{ |
|||
$this->fields = $fields; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Set the dialog to be displayed as a popup. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function asPopup() |
|||
{ |
|||
$this->popup = true; |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Exception; |
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class GitHubProvider. |
|||
*/ |
|||
class GitHubProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['user:email']; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://github.com/login/oauth/authorize', $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'https://github.com/login/oauth/access_token'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$userUrl = 'https://api.github.com/user?access_token='.$token->getToken(); |
|||
|
|||
$response = $this->getHttpClient()->get( |
|||
$userUrl, $this->getRequestOptions() |
|||
); |
|||
|
|||
$user = json_decode($response->getBody(), true); |
|||
|
|||
if (in_array('user:email', $this->scopes)) { |
|||
$user['email'] = $this->getEmailByToken($token); |
|||
} |
|||
|
|||
return $user; |
|||
} |
|||
|
|||
/** |
|||
* Get the email for the given access token. |
|||
* |
|||
* @param string $token |
|||
* |
|||
* @return string|null |
|||
*/ |
|||
protected function getEmailByToken($token) |
|||
{ |
|||
$emailsUrl = 'https://api.github.com/user/emails?access_token='.$token->getToken(); |
|||
|
|||
try { |
|||
$response = $this->getHttpClient()->get( |
|||
$emailsUrl, $this->getRequestOptions() |
|||
); |
|||
} catch (Exception $e) { |
|||
return; |
|||
} |
|||
|
|||
foreach (json_decode($response->getBody(), true) as $email) { |
|||
if ($email['primary'] && $email['verified']) { |
|||
return $email['email']; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'username' => $this->arrayItem($user, 'login'), |
|||
'nickname' => $this->arrayItem($user, 'login'), |
|||
'name' => $this->arrayItem($user, 'name'), |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
'avatar' => $this->arrayItem($user, 'avatar_url'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Get the default options for an HTTP request. |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getRequestOptions() |
|||
{ |
|||
return [ |
|||
'headers' => [ |
|||
'Accept' => 'application/vnd.github.v3+json', |
|||
], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use GuzzleHttp\ClientInterface; |
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class GoogleProvider. |
|||
* |
|||
* @see https://developers.google.com/identity/protocols/OpenIDConnect [OpenID Connect] |
|||
*/ |
|||
class GoogleProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The separating character for the requested scopes. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $scopeSeparator = ' '; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = [ |
|||
'https://www.googleapis.com/auth/userinfo.email', |
|||
'https://www.googleapis.com/auth/userinfo.profile', |
|||
]; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://accounts.google.com/o/oauth2/v2/auth', $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'https://www.googleapis.com/oauth2/v4/token'; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$postKey = (1 === version_compare(ClientInterface::VERSION, '6')) ? 'form_params' : 'body'; |
|||
|
|||
$response = $this->getHttpClient()->post($this->getTokenUrl(), [ |
|||
$postKey => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()); |
|||
} |
|||
|
|||
/** |
|||
* Get the POST fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->get('https://www.googleapis.com/userinfo/v2/me', [ |
|||
'headers' => [ |
|||
'Accept' => 'application/json', |
|||
'Authorization' => 'Bearer '.$token->getToken(), |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'username' => $this->arrayItem($user, 'email'), |
|||
'nickname' => $this->arrayItem($user, 'name'), |
|||
'name' => $this->arrayItem($user, 'name'), |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
'avatar' => $this->arrayItem($user, 'picture'), |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,181 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class LinkedinProvider. |
|||
* |
|||
* @see https://developer.linkedin.com/docs/oauth2 [Authenticating with OAuth 2.0] |
|||
*/ |
|||
class LinkedinProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['r_liteprofile', 'r_emailaddress']; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state); |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient() |
|||
->post($this->getTokenUrl(), ['form_params' => $this->getTokenFields($code)]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'https://www.linkedin.com/oauth/v2/accessToken'; |
|||
} |
|||
|
|||
/** |
|||
* Get the POST fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$basicProfile = $this->getBasicProfile($token); |
|||
$emailAddress = $this->getEmailAddress($token); |
|||
|
|||
return array_merge($basicProfile, $emailAddress); |
|||
} |
|||
|
|||
/** |
|||
* Get the basic profile fields for the user. |
|||
* |
|||
* @param string $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getBasicProfile($token) |
|||
{ |
|||
$url = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))'; |
|||
|
|||
$response = $this->getHttpClient()->get($url, [ |
|||
'headers' => [ |
|||
'Authorization' => 'Bearer '.$token, |
|||
'X-RestLi-Protocol-Version' => '2.0.0', |
|||
], |
|||
]); |
|||
|
|||
return (array) json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* Get the email address for the user. |
|||
* |
|||
* @param string $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getEmailAddress($token) |
|||
{ |
|||
$url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))'; |
|||
|
|||
$response = $this->getHttpClient()->get($url, [ |
|||
'headers' => [ |
|||
'Authorization' => 'Bearer '.$token, |
|||
'X-RestLi-Protocol-Version' => '2.0.0', |
|||
], |
|||
]); |
|||
|
|||
return (array) $this->arrayItem(json_decode($response->getBody(), true), 'elements.0.handle~'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
$preferredLocale = $this->arrayItem($user, 'firstName.preferredLocale.language').'_'.$this->arrayItem($user, 'firstName.preferredLocale.country'); |
|||
$firstName = $this->arrayItem($user, 'firstName.localized.'.$preferredLocale); |
|||
$lastName = $this->arrayItem($user, 'lastName.localized.'.$preferredLocale); |
|||
$name = $firstName.' '.$lastName; |
|||
|
|||
$images = (array) $this->arrayItem($user, 'profilePicture.displayImage~.elements', []); |
|||
$avatars = array_filter($images, function ($image) { |
|||
return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 100; |
|||
}); |
|||
$avatar = array_shift($avatars); |
|||
$originalAvatars = array_filter($images, function ($image) { |
|||
return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 800; |
|||
}); |
|||
$originalAvatar = array_shift($originalAvatars); |
|||
|
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'nickname' => $name, |
|||
'name' => $name, |
|||
'email' => $this->arrayItem($user, 'emailAddress'), |
|||
'avatar' => $avatar ? $this->arrayItem($avatar, 'identifiers.0.identifier') : null, |
|||
'avatar_original' => $originalAvatar ? $this->arrayItem($originalAvatar, 'identifiers.0.identifier') : null, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Set the user fields to request from LinkedIn. |
|||
* |
|||
* @param array $fields |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function fields(array $fields) |
|||
{ |
|||
$this->fields = $fields; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Determine if the provider is operating as stateless. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function isStateless() |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class OutlookProvider. |
|||
*/ |
|||
class OutlookProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected $scopes = ['User.Read']; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected $scopeSeparator = ' '; |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase('https://login.microsoftonline.com/common/oauth2/v2.0/authorize', $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->get( |
|||
'https://graph.microsoft.com/v1.0/me', |
|||
['headers' => [ |
|||
'Accept' => 'application/json', |
|||
'Authorization' => 'Bearer '.$token->getToken(), |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody()->getContents(), true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'nickname' => null, |
|||
'name' => $this->arrayItem($user, 'displayName'), |
|||
'email' => $this->arrayItem($user, 'userPrincipalName'), |
|||
'avatar' => null, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return array_merge(parent::getTokenFields($code), [ |
|||
'grant_type' => 'authorization_code', |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,206 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class QQProvider. |
|||
* |
|||
* @see http://wiki.connect.qq.com/oauth2-0%E7%AE%80%E4%BB%8B [QQ - OAuth 2.0 登录QQ] |
|||
*/ |
|||
class QQProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The base url of QQ API. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $baseUrl = 'https://graph.qq.com'; |
|||
|
|||
/** |
|||
* User openid. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $openId; |
|||
|
|||
/** |
|||
* get token(openid) with unionid. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $withUnionId = false; |
|||
|
|||
/** |
|||
* User unionid. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $unionId; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['get_user_info']; |
|||
|
|||
/** |
|||
* The uid of user authorized. |
|||
* |
|||
* @var int |
|||
*/ |
|||
protected $uid; |
|||
|
|||
/** |
|||
* Get the authentication URL for the provider. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2.0/authorize', $state); |
|||
} |
|||
|
|||
/** |
|||
* Get the token URL for the provider. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return $this->baseUrl.'/oauth2.0/token'; |
|||
} |
|||
|
|||
/** |
|||
* Get the Post fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->get($this->getTokenUrl(), [ |
|||
'query' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()->getContents()); |
|||
} |
|||
|
|||
/** |
|||
* Get the access token from the token response body. |
|||
* |
|||
* @param string $body |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function parseAccessToken($body) |
|||
{ |
|||
parse_str($body, $token); |
|||
|
|||
return parent::parseAccessToken($token); |
|||
} |
|||
|
|||
/** |
|||
* @return self |
|||
*/ |
|||
public function withUnionId() |
|||
{ |
|||
$this->withUnionId = true; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get the raw user for the given access token. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$url = $this->baseUrl.'/oauth2.0/me?access_token='.$token->getToken(); |
|||
$this->withUnionId && $url .= '&unionid=1'; |
|||
|
|||
$response = $this->getHttpClient()->get($url); |
|||
|
|||
$me = json_decode($this->removeCallback($response->getBody()->getContents()), true); |
|||
$this->openId = $me['openid']; |
|||
$this->unionId = isset($me['unionid']) ? $me['unionid'] : ''; |
|||
|
|||
$queries = [ |
|||
'access_token' => $token->getToken(), |
|||
'openid' => $this->openId, |
|||
'oauth_consumer_key' => $this->clientId, |
|||
]; |
|||
|
|||
$response = $this->getHttpClient()->get($this->baseUrl.'/user/get_user_info?'.http_build_query($queries)); |
|||
|
|||
return json_decode($this->removeCallback($response->getBody()->getContents()), true); |
|||
} |
|||
|
|||
/** |
|||
* Map the raw user array to a Socialite User instance. |
|||
* |
|||
* @param array $user |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->openId, |
|||
'unionid' => $this->unionId, |
|||
'nickname' => $this->arrayItem($user, 'nickname'), |
|||
'name' => $this->arrayItem($user, 'nickname'), |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
'avatar' => $this->arrayItem($user, 'figureurl_qq_2'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Remove the fucking callback parentheses. |
|||
* |
|||
* @param string $response |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function removeCallback($response) |
|||
{ |
|||
if (false !== strpos($response, 'callback')) { |
|||
$lpos = strpos($response, '('); |
|||
$rpos = strrpos($response, ')'); |
|||
$response = substr($response, $lpos + 1, $rpos - $lpos - 1); |
|||
} |
|||
|
|||
return $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,242 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class TaobaoProvider. |
|||
* |
|||
* @author mechono <haodouliu@gmail.com> |
|||
* |
|||
* @see https://open.taobao.com/doc.htm?docId=102635&docType=1&source=search [Taobao - OAuth 2.0 授权登录] |
|||
*/ |
|||
class TaobaoProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The base url of Taobao API. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $baseUrl = 'https://oauth.taobao.com'; |
|||
|
|||
/** |
|||
* Taobao API service URL address. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $gatewayUrl = 'https://eco.taobao.com/router/rest'; |
|||
|
|||
/** |
|||
* The API version for the request. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $version = '2.0'; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $format = 'json'; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $signMethod = 'md5'; |
|||
|
|||
/** |
|||
* Web 对应 PC 端(淘宝 logo )浏览器页面样式;Tmall 对应天猫的浏览器页面样式;Wap 对应无线端的浏览器页面样式。 |
|||
*/ |
|||
protected $view = 'web'; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['user_info']; |
|||
|
|||
/** |
|||
* Get the authentication URL for the provider. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase($this->baseUrl.'/authorize', $state); |
|||
} |
|||
|
|||
/** |
|||
* 获取授权码接口参数. |
|||
* |
|||
* @param string|null $state |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getCodeFields($state = null) |
|||
{ |
|||
$fields = [ |
|||
'client_id' => $this->clientId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'view' => $this->view, |
|||
'response_type' => 'code', |
|||
]; |
|||
|
|||
if ($this->usesState()) { |
|||
$fields['state'] = $state; |
|||
} |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* Get the token URL for the provider. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return $this->baseUrl.'/token'; |
|||
} |
|||
|
|||
/** |
|||
* Get the Post fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code', 'view' => $this->view]; |
|||
} |
|||
|
|||
/** |
|||
* Get the access token for the given code. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->post($this->getTokenUrl(), [ |
|||
'query' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()->getContents()); |
|||
} |
|||
|
|||
/** |
|||
* Get the access token from the token response body. |
|||
* |
|||
* @param string $body |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function parseAccessToken($body) |
|||
{ |
|||
return parent::parseAccessToken($body); |
|||
} |
|||
|
|||
/** |
|||
* Get the raw user for the given access token. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->post($this->getUserInfoUrl($this->gatewayUrl, $token)); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* Map the raw user array to a Socialite User instance. |
|||
* |
|||
* @param array $user |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'open_id'), |
|||
'nickname' => $this->arrayItem($user, 'nick'), |
|||
'name' => $this->arrayItem($user, 'nick'), |
|||
'avatar' => $this->arrayItem($user, 'avatar'), |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* @param $params |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateSign($params) |
|||
{ |
|||
ksort($params); |
|||
|
|||
$stringToBeSigned = $this->clientSecret; |
|||
|
|||
foreach ($params as $k => $v) { |
|||
if (!is_array($v) && '@' != substr($v, 0, 1)) { |
|||
$stringToBeSigned .= "$k$v"; |
|||
} |
|||
} |
|||
|
|||
$stringToBeSigned .= $this->clientSecret; |
|||
|
|||
return strtoupper(md5($stringToBeSigned)); |
|||
} |
|||
|
|||
/** |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* @param array $apiFields |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getPublicFields(AccessTokenInterface $token, array $apiFields = []) |
|||
{ |
|||
$fields = [ |
|||
'app_key' => $this->clientId, |
|||
'sign_method' => $this->signMethod, |
|||
'session' => $token->getToken(), |
|||
'timestamp' => date('Y-m-d H:i:s'), |
|||
'v' => $this->version, |
|||
'format' => $this->format, |
|||
]; |
|||
|
|||
$fields = array_merge($apiFields, $fields); |
|||
$fields['sign'] = $this->generateSign($fields); |
|||
|
|||
return $fields; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getUserInfoUrl($url, AccessTokenInterface $token) |
|||
{ |
|||
$apiFields = ['method' => 'taobao.miniapp.userInfo.get']; |
|||
|
|||
$query = http_build_query($this->getPublicFields($token, $apiFields), '', '&', $this->encodingType); |
|||
|
|||
return $url.'?'.$query; |
|||
} |
|||
} |
|||
@ -0,0 +1,234 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\InvalidArgumentException; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
use Overtrue\Socialite\WeChatComponentInterface; |
|||
|
|||
/** |
|||
* Class WeChatProvider. |
|||
* |
|||
* @see http://mp.weixin.qq.com/wiki/9/01f711493b5a02f24b04365ac5d8fd95.html [WeChat - 公众平台OAuth文档] |
|||
* @see https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN [网站应用微信登录开发指南] |
|||
*/ |
|||
class WeChatProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The base url of WeChat API. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $baseUrl = 'https://api.weixin.qq.com/sns'; |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected $openId; |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected $scopes = ['snsapi_login']; |
|||
|
|||
/** |
|||
* Indicates if the session state should be utilized. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $stateless = true; |
|||
|
|||
/** |
|||
* Return country code instead of country name. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
protected $withCountryCode = false; |
|||
|
|||
/** |
|||
* @var WeChatComponentInterface |
|||
*/ |
|||
protected $component; |
|||
|
|||
/** |
|||
* Return country code instead of country name. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function withCountryCode() |
|||
{ |
|||
$this->withCountryCode = true; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* WeChat OpenPlatform 3rd component. |
|||
* |
|||
* @param WeChatComponentInterface $component |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function component(WeChatComponentInterface $component) |
|||
{ |
|||
$this->scopes = ['snsapi_base']; |
|||
|
|||
$this->component = $component; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function getAccessToken($code) |
|||
{ |
|||
$response = $this->getHttpClient()->get($this->getTokenUrl(), [ |
|||
'headers' => ['Accept' => 'application/json'], |
|||
'query' => $this->getTokenFields($code), |
|||
]); |
|||
|
|||
return $this->parseAccessToken($response->getBody()); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
$path = 'oauth2/authorize'; |
|||
|
|||
if (in_array('snsapi_login', $this->scopes)) { |
|||
$path = 'qrconnect'; |
|||
} |
|||
|
|||
return $this->buildAuthUrlFromBase("https://open.weixin.qq.com/connect/{$path}", $state); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function buildAuthUrlFromBase($url, $state) |
|||
{ |
|||
$query = http_build_query($this->getCodeFields($state), '', '&', $this->encodingType); |
|||
|
|||
return $url.'?'.$query.'#wechat_redirect'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getCodeFields($state = null) |
|||
{ |
|||
if ($this->component) { |
|||
$this->with(['component_appid' => $this->component->getAppId()]); |
|||
} |
|||
|
|||
return array_merge([ |
|||
'appid' => $this->clientId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'response_type' => 'code', |
|||
'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), |
|||
'state' => $state ?: md5(time()), |
|||
'connect_redirect' => 1, |
|||
], $this->parameters); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
if ($this->component) { |
|||
return $this->baseUrl.'/oauth2/component/access_token'; |
|||
} |
|||
|
|||
return $this->baseUrl.'/oauth2/access_token'; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$scopes = explode(',', $token->getAttribute('scope', '')); |
|||
|
|||
if (in_array('snsapi_base', $scopes)) { |
|||
return $token->toArray(); |
|||
} |
|||
|
|||
if (empty($token['openid'])) { |
|||
throw new InvalidArgumentException('openid of AccessToken is required.'); |
|||
} |
|||
|
|||
$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->getToken(), |
|||
'openid' => $token['openid'], |
|||
'lang' => $language, |
|||
]), |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'openid'), |
|||
'name' => $this->arrayItem($user, 'nickname'), |
|||
'nickname' => $this->arrayItem($user, 'nickname'), |
|||
'avatar' => $this->arrayItem($user, 'headimgurl'), |
|||
'email' => null, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return array_filter([ |
|||
'appid' => $this->clientId, |
|||
'secret' => $this->clientSecret, |
|||
'component_appid' => $this->component ? $this->component->getAppId() : null, |
|||
'component_access_token' => $this->component ? $this->component->getToken() : null, |
|||
'code' => $code, |
|||
'grant_type' => 'authorization_code', |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Remove the fucking callback parentheses. |
|||
* |
|||
* @param mixed $response |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function removeCallback($response) |
|||
{ |
|||
if (false !== strpos($response, 'callback')) { |
|||
$lpos = strpos($response, '('); |
|||
$rpos = strrpos($response, ')'); |
|||
$response = substr($response, $lpos + 1, $rpos - $lpos - 1); |
|||
} |
|||
|
|||
return $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class WeWorkProvider. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class WeWorkProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $agentId; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $detailed = false; |
|||
|
|||
/** |
|||
* Set agent id. |
|||
* |
|||
* @param string $agentId |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setAgentId($agentId) |
|||
{ |
|||
$this->agentId = $agentId; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param string $agentId |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function agent($agentId) |
|||
{ |
|||
return $this->setAgentId($agentId); |
|||
} |
|||
|
|||
/** |
|||
* Return user details. |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function detailed() |
|||
{ |
|||
$this->detailed = true; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
// 网页授权登录 |
|||
if (!empty($this->scopes)) { |
|||
return $this->getOAuthUrl($state); |
|||
} |
|||
|
|||
// 第三方网页应用登录(扫码登录) |
|||
return $this->getQrConnectUrl($state); |
|||
} |
|||
|
|||
/** |
|||
* OAuth url. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getOAuthUrl($state) |
|||
{ |
|||
$queries = [ |
|||
'appid' => $this->clientId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'response_type' => 'code', |
|||
'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator), |
|||
'agentid' => $this->agentId, |
|||
'state' => $state, |
|||
]; |
|||
|
|||
return sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', http_build_query($queries)); |
|||
} |
|||
|
|||
/** |
|||
* Qr connect url. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getQrConnectUrl($state) |
|||
{ |
|||
$queries = [ |
|||
'appid' => $this->clientId, |
|||
'agentid' => $this->agentId, |
|||
'redirect_uri' => $this->redirectUrl, |
|||
'state' => $state, |
|||
]; |
|||
|
|||
return 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect?'.http_build_query($queries); |
|||
} |
|||
|
|||
protected function getTokenUrl() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$userInfo = $this->getUserInfo($token); |
|||
|
|||
if ($this->detailed && isset($userInfo['user_ticket'])) { |
|||
return $this->getUserDetail($token, $userInfo['user_ticket']); |
|||
} |
|||
|
|||
$this->detailed = false; |
|||
|
|||
return $userInfo; |
|||
} |
|||
|
|||
/** |
|||
* Get user base info. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function getUserInfo(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->get('https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo', [ |
|||
'query' => array_filter([ |
|||
'access_token' => $token->getToken(), |
|||
'code' => $this->getCode(), |
|||
]), |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* Get user detail info. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* @param $ticket |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function getUserDetail(AccessTokenInterface $token, $ticket) |
|||
{ |
|||
$response = $this->getHttpClient()->post('https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail', [ |
|||
'query' => [ |
|||
'access_token' => $token->getToken(), |
|||
], |
|||
'json' => [ |
|||
'user_ticket' => $ticket, |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* @param array $user |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
if ($this->detailed) { |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'userid'), |
|||
'name' => $this->arrayItem($user, 'name'), |
|||
'avatar' => $this->arrayItem($user, 'avatar'), |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
]); |
|||
} |
|||
|
|||
return new User(array_filter([ |
|||
'id' => $this->arrayItem($user, 'UserId') ?: $this->arrayItem($user, 'OpenId'), |
|||
'userId' => $this->arrayItem($user, 'UserId'), |
|||
'openid' => $this->arrayItem($user, 'OpenId'), |
|||
'deviceId' => $this->arrayItem($user, 'DeviceId'), |
|||
])); |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite\Providers; |
|||
|
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\ProviderInterface; |
|||
use Overtrue\Socialite\User; |
|||
|
|||
/** |
|||
* Class WeiboProvider. |
|||
* |
|||
* @see http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E [OAuth 2.0 授权机制说明] |
|||
*/ |
|||
class WeiboProvider extends AbstractProvider implements ProviderInterface |
|||
{ |
|||
/** |
|||
* The base url of Weibo API. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $baseUrl = 'https://api.weibo.com'; |
|||
|
|||
/** |
|||
* The API version for the request. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $version = '2'; |
|||
|
|||
/** |
|||
* The scopes being requested. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $scopes = ['email']; |
|||
|
|||
/** |
|||
* The uid of user authorized. |
|||
* |
|||
* @var int |
|||
*/ |
|||
protected $uid; |
|||
|
|||
/** |
|||
* Get the authentication URL for the provider. |
|||
* |
|||
* @param string $state |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return $this->buildAuthUrlFromBase($this->baseUrl.'/oauth2/authorize', $state); |
|||
} |
|||
|
|||
/** |
|||
* Get the token URL for the provider. |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getTokenUrl() |
|||
{ |
|||
return $this->baseUrl.'/'.$this->version.'/oauth2/access_token'; |
|||
} |
|||
|
|||
/** |
|||
* Get the Post fields for the token request. |
|||
* |
|||
* @param string $code |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getTokenFields($code) |
|||
{ |
|||
return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; |
|||
} |
|||
|
|||
/** |
|||
* Get the raw user for the given access token. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
$response = $this->getHttpClient()->get($this->baseUrl.'/'.$this->version.'/users/show.json', [ |
|||
'query' => [ |
|||
'uid' => $token['uid'], |
|||
'access_token' => $token->getToken(), |
|||
], |
|||
'headers' => [ |
|||
'Accept' => 'application/json', |
|||
], |
|||
]); |
|||
|
|||
return json_decode($response->getBody(), true); |
|||
} |
|||
|
|||
/** |
|||
* Map the raw user array to a Socialite User instance. |
|||
* |
|||
* @param array $user |
|||
* |
|||
* @return \Overtrue\Socialite\User |
|||
*/ |
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User([ |
|||
'id' => $this->arrayItem($user, 'id'), |
|||
'nickname' => $this->arrayItem($user, 'screen_name'), |
|||
'name' => $this->arrayItem($user, 'name'), |
|||
'email' => $this->arrayItem($user, 'email'), |
|||
'avatar' => $this->arrayItem($user, 'avatar_large'), |
|||
]); |
|||
} |
|||
} |
|||
@ -0,0 +1,251 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
use Closure; |
|||
use InvalidArgumentException; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
use Symfony\Component\HttpFoundation\Session\Session; |
|||
|
|||
/** |
|||
* Class SocialiteManager. |
|||
*/ |
|||
class SocialiteManager implements FactoryInterface |
|||
{ |
|||
/** |
|||
* The configuration. |
|||
* |
|||
* @var \Overtrue\Socialite\Config |
|||
*/ |
|||
protected $config; |
|||
|
|||
/** |
|||
* The request instance. |
|||
* |
|||
* @var Request |
|||
*/ |
|||
protected $request; |
|||
|
|||
/** |
|||
* The registered custom driver creators. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $customCreators = []; |
|||
|
|||
/** |
|||
* The initial drivers. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $initialDrivers = [ |
|||
'facebook' => 'Facebook', |
|||
'github' => 'GitHub', |
|||
'google' => 'Google', |
|||
'linkedin' => 'Linkedin', |
|||
'weibo' => 'Weibo', |
|||
'qq' => 'QQ', |
|||
'wechat' => 'WeChat', |
|||
'douban' => 'Douban', |
|||
'wework' => 'WeWork', |
|||
'outlook' => 'Outlook', |
|||
'douyin' => 'DouYin', |
|||
'taobao' => 'Taobao', |
|||
]; |
|||
|
|||
/** |
|||
* The array of created "drivers". |
|||
* |
|||
* @var ProviderInterface[] |
|||
*/ |
|||
protected $drivers = []; |
|||
|
|||
/** |
|||
* SocialiteManager constructor. |
|||
* |
|||
* @param array $config |
|||
* @param Request|null $request |
|||
*/ |
|||
public function __construct(array $config, Request $request = null) |
|||
{ |
|||
$this->config = new Config($config); |
|||
|
|||
if ($this->config->has('guzzle')) { |
|||
Providers\AbstractProvider::setGuzzleOptions($this->config->get('guzzle')); |
|||
} |
|||
|
|||
if ($request) { |
|||
$this->setRequest($request); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Set config instance. |
|||
* |
|||
* @param \Overtrue\Socialite\Config $config |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function config(Config $config) |
|||
{ |
|||
$this->config = $config; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get a driver instance. |
|||
* |
|||
* @param string $driver |
|||
* |
|||
* @return ProviderInterface |
|||
*/ |
|||
public function driver($driver) |
|||
{ |
|||
if (!isset($this->drivers[$driver])) { |
|||
$this->drivers[$driver] = $this->createDriver($driver); |
|||
} |
|||
|
|||
return $this->drivers[$driver]; |
|||
} |
|||
|
|||
/** |
|||
* @param \Symfony\Component\HttpFoundation\Request $request |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setRequest(Request $request) |
|||
{ |
|||
$this->request = $request; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return \Symfony\Component\HttpFoundation\Request |
|||
*/ |
|||
public function getRequest() |
|||
{ |
|||
return $this->request ?: $this->createDefaultRequest(); |
|||
} |
|||
|
|||
/** |
|||
* Create a new driver instance. |
|||
* |
|||
* @param string $driver |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
* |
|||
* @return ProviderInterface |
|||
*/ |
|||
protected function createDriver($driver) |
|||
{ |
|||
if (isset($this->initialDrivers[$driver])) { |
|||
$provider = $this->initialDrivers[$driver]; |
|||
$provider = __NAMESPACE__.'\\Providers\\'.$provider.'Provider'; |
|||
|
|||
return $this->buildProvider($provider, $this->formatConfig($this->config->get($driver))); |
|||
} |
|||
|
|||
if (isset($this->customCreators[$driver])) { |
|||
return $this->callCustomCreator($driver); |
|||
} |
|||
|
|||
throw new InvalidArgumentException("Driver [$driver] not supported."); |
|||
} |
|||
|
|||
/** |
|||
* Call a custom driver creator. |
|||
* |
|||
* @param string $driver |
|||
* |
|||
* @return ProviderInterface |
|||
*/ |
|||
protected function callCustomCreator($driver) |
|||
{ |
|||
return $this->customCreators[$driver]($this->config); |
|||
} |
|||
|
|||
/** |
|||
* Create default request instance. |
|||
* |
|||
* @return Request |
|||
*/ |
|||
protected function createDefaultRequest() |
|||
{ |
|||
$request = Request::createFromGlobals(); |
|||
$session = new Session(); |
|||
|
|||
$request->setSession($session); |
|||
|
|||
return $request; |
|||
} |
|||
|
|||
/** |
|||
* Register a custom driver creator Closure. |
|||
* |
|||
* @param string $driver |
|||
* @param \Closure $callback |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function extend($driver, Closure $callback) |
|||
{ |
|||
$this->customCreators[$driver] = $callback; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get all of the created "drivers". |
|||
* |
|||
* @return ProviderInterface[] |
|||
*/ |
|||
public function getDrivers() |
|||
{ |
|||
return $this->drivers; |
|||
} |
|||
|
|||
/** |
|||
* Build an OAuth 2 provider instance. |
|||
* |
|||
* @param string $provider |
|||
* @param array $config |
|||
* |
|||
* @return ProviderInterface |
|||
*/ |
|||
public function buildProvider($provider, $config) |
|||
{ |
|||
return new $provider( |
|||
$this->getRequest(), |
|||
$config['client_id'], |
|||
$config['client_secret'], |
|||
$config['redirect'] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Format the server configuration. |
|||
* |
|||
* @param array $config |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function formatConfig(array $config) |
|||
{ |
|||
return array_merge([ |
|||
'identifier' => $config['client_id'], |
|||
'secret' => $config['client_secret'], |
|||
'callback_uri' => $config['redirect'], |
|||
], $config); |
|||
} |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
use ArrayAccess; |
|||
use JsonSerializable; |
|||
|
|||
/** |
|||
* Class User. |
|||
*/ |
|||
class User implements ArrayAccess, UserInterface, JsonSerializable, \Serializable |
|||
{ |
|||
use HasAttributes; |
|||
|
|||
/** |
|||
* User constructor. |
|||
* |
|||
* @param array $attributes |
|||
*/ |
|||
public function __construct(array $attributes) |
|||
{ |
|||
$this->attributes = $attributes; |
|||
} |
|||
|
|||
/** |
|||
* Get the unique identifier for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getId() |
|||
{ |
|||
return $this->getAttribute('id'); |
|||
} |
|||
|
|||
/** |
|||
* Get the username for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getUsername() |
|||
{ |
|||
return $this->getAttribute('username', $this->getId()); |
|||
} |
|||
|
|||
/** |
|||
* Get the nickname / username for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getNickname() |
|||
{ |
|||
return $this->getAttribute('nickname'); |
|||
} |
|||
|
|||
/** |
|||
* Get the full name of the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return $this->getAttribute('name'); |
|||
} |
|||
|
|||
/** |
|||
* Get the e-mail address of the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getEmail() |
|||
{ |
|||
return $this->getAttribute('email'); |
|||
} |
|||
|
|||
/** |
|||
* Get the avatar / image URL for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getAvatar() |
|||
{ |
|||
return $this->getAttribute('avatar'); |
|||
} |
|||
|
|||
/** |
|||
* Set the token on the user. |
|||
* |
|||
* @param \Overtrue\Socialite\AccessTokenInterface $token |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setToken(AccessTokenInterface $token) |
|||
{ |
|||
$this->setAttribute('token', $token->getToken()); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param string $provider |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setProviderName($provider) |
|||
{ |
|||
$this->setAttribute('provider', $provider); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getProviderName() |
|||
{ |
|||
return $this->getAttribute('provider'); |
|||
} |
|||
|
|||
/** |
|||
* Get the authorized token. |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getToken() |
|||
{ |
|||
return new AccessToken(['access_token' => $this->getAttribute('token')]); |
|||
} |
|||
|
|||
/** |
|||
* Alias of getToken(). |
|||
* |
|||
* @return \Overtrue\Socialite\AccessToken |
|||
*/ |
|||
public function getAccessToken() |
|||
{ |
|||
return $this->getToken(); |
|||
} |
|||
|
|||
/** |
|||
* Get the original attributes. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getOriginal() |
|||
{ |
|||
return $this->getAttribute('original'); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function jsonSerialize() |
|||
{ |
|||
return $this->attributes; |
|||
} |
|||
|
|||
public function serialize() |
|||
{ |
|||
return serialize($this->attributes); |
|||
} |
|||
|
|||
/** |
|||
* Constructs the object. |
|||
* |
|||
* @see https://php.net/manual/en/serializable.unserialize.php |
|||
* |
|||
* @param string $serialized <p> |
|||
* The string representation of the object. |
|||
* </p> |
|||
* |
|||
* @since 5.1.0 |
|||
*/ |
|||
public function unserialize($serialized) |
|||
{ |
|||
$this->attributes = \unserialize($serialized) ?? []; |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
/** |
|||
* Interface UserInterface. |
|||
*/ |
|||
interface UserInterface |
|||
{ |
|||
/** |
|||
* Get the unique identifier for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getId(); |
|||
|
|||
/** |
|||
* Get the nickname / username for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getNickname(); |
|||
|
|||
/** |
|||
* Get the full name of the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName(); |
|||
|
|||
/** |
|||
* Get the e-mail address of the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getEmail(); |
|||
|
|||
/** |
|||
* Get the avatar / image URL for the user. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getAvatar(); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\Socialite; |
|||
|
|||
/** |
|||
* Interface WeChatComponentInterface. |
|||
*/ |
|||
interface WeChatComponentInterface |
|||
{ |
|||
/** |
|||
* Return the open-platform component app id. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getAppId(); |
|||
|
|||
/** |
|||
* Return the open-platform component access token string. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getToken(); |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
use Mockery as m; |
|||
use Overtrue\Socialite\AccessTokenInterface; |
|||
use Overtrue\Socialite\Providers\AbstractProvider; |
|||
use Overtrue\Socialite\User; |
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
|
|||
class OAuthTest extends TestCase |
|||
{ |
|||
public function tearDown() |
|||
{ |
|||
m::close(); |
|||
} |
|||
|
|||
public function testRedirectGeneratesTheProperSymfonyRedirectResponse() |
|||
{ |
|||
$request = Request::create('foo'); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
$session->shouldReceive('put')->once(); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect'); |
|||
$response = $provider->redirect(); |
|||
|
|||
$this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); |
|||
$this->assertSame('http://auth.url', $response->getTargetUrl()); |
|||
} |
|||
|
|||
public function testRedirectUrl() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
|
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret'); |
|||
$this->assertNull($provider->getRedirectUrl()); |
|||
|
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect_uri'); |
|||
$this->assertSame('redirect_uri', $provider->getRedirectUrl()); |
|||
$provider->setRedirectUrl('overtrue.me'); |
|||
$this->assertSame('overtrue.me', $provider->getRedirectUrl()); |
|||
|
|||
$provider->withRedirectUrl('http://overtrue.me'); |
|||
$this->assertSame('http://overtrue.me', $provider->getRedirectUrl()); |
|||
} |
|||
|
|||
public function testUserReturnsAUserInstanceForTheAuthenticatedRequest() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
|
|||
$session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect_uri'); |
|||
$provider->http = m::mock('StdClass'); |
|||
$provider->http->shouldReceive('post')->once()->with('http://token.url', [ |
|||
'headers' => ['Accept' => 'application/json'], 'form_params' => ['client_id' => 'client_id', 'client_secret' => 'client_secret', 'code' => 'code', 'redirect_uri' => 'redirect_uri'], |
|||
])->andReturn($response = m::mock('StdClass')); |
|||
$response->shouldReceive('getBody')->once()->andReturn('{"access_token":"access_token"}'); |
|||
$user = $provider->user(); |
|||
|
|||
$this->assertInstanceOf('Overtrue\Socialite\User', $user); |
|||
$this->assertSame('foo', $user->getId()); |
|||
} |
|||
|
|||
/** |
|||
* @expectedException \Overtrue\Socialite\InvalidStateException |
|||
*/ |
|||
public function testExceptionIsThrownIfStateIsInvalid() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => str_repeat('B', 40), 'code' => 'code']); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
$session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect'); |
|||
$user = $provider->user(); |
|||
} |
|||
|
|||
/** |
|||
* @expectedException \Overtrue\Socialite\AuthorizeFailedException |
|||
* @expectedExceptionMessage Authorize Failed: {"error":"scope is invalid"} |
|||
*/ |
|||
public function testExceptionisThrownIfAuthorizeFailed() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => str_repeat('A', 40), 'code' => 'code']); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
$session->shouldReceive('get')->once()->with('state')->andReturn(str_repeat('A', 40)); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect_uri'); |
|||
$provider->http = m::mock('StdClass'); |
|||
$provider->http->shouldReceive('post')->once()->with('http://token.url', [ |
|||
'headers' => ['Accept' => 'application/json'], 'form_params' => ['client_id' => 'client_id', 'client_secret' => 'client_secret', 'code' => 'code', 'redirect_uri' => 'redirect_uri'], |
|||
])->andReturn($response = m::mock('StdClass')); |
|||
$response->shouldReceive('getBody')->once()->andReturn('{"error":"scope is invalid"}'); |
|||
$user = $provider->user(); |
|||
} |
|||
|
|||
/** |
|||
* @expectedException \Overtrue\Socialite\InvalidStateException |
|||
*/ |
|||
public function testExceptionIsThrownIfStateIsNotSet() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => 'state', 'code' => 'code']); |
|||
$request->setSession($session = m::mock('Symfony\Component\HttpFoundation\Session\SessionInterface')); |
|||
$session->shouldReceive('get')->once()->with('state'); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect'); |
|||
$user = $provider->user(); |
|||
} |
|||
|
|||
public function testDriverName() |
|||
{ |
|||
$request = Request::create('foo', 'GET', ['state' => 'state', 'code' => 'code']); |
|||
$provider = new OAuthTwoTestProviderStub($request, 'client_id', 'client_secret', 'redirect'); |
|||
|
|||
$this->assertSame('OAuthTwoTest', $provider->getName()); |
|||
} |
|||
} |
|||
|
|||
class OAuthTwoTestProviderStub extends AbstractProvider |
|||
{ |
|||
public $http; |
|||
|
|||
protected function getAuthUrl($state) |
|||
{ |
|||
return 'http://auth.url'; |
|||
} |
|||
|
|||
protected function getTokenUrl() |
|||
{ |
|||
return 'http://token.url'; |
|||
} |
|||
|
|||
protected function getUserByToken(AccessTokenInterface $token) |
|||
{ |
|||
return ['id' => 'foo']; |
|||
} |
|||
|
|||
protected function mapUserToObject(array $user) |
|||
{ |
|||
return new User(['id' => $user['id']]); |
|||
} |
|||
|
|||
/** |
|||
* Get a fresh instance of the Guzzle HTTP client. |
|||
* |
|||
* @return \GuzzleHttp\Client |
|||
*/ |
|||
protected function getHttpClient() |
|||
{ |
|||
if ($this->http) { |
|||
return $this->http; |
|||
} |
|||
|
|||
return $this->http = m::mock('StdClass'); |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
use Overtrue\Socialite\Providers\WeWorkProvider; |
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
|
|||
class WeWorkProviderTest extends TestCase |
|||
{ |
|||
public function testQrConnect() |
|||
{ |
|||
$response = (new WeWorkProvider(Request::create('foo'), 'ww100000a5f2191', 'client_secret', 'http://www.oa.com')) |
|||
->setAgentId('1000000') |
|||
->stateless() |
|||
->redirect(); |
|||
|
|||
$this->assertSame('https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=ww100000a5f2191&agentid=1000000&redirect_uri=http%3A%2F%2Fwww.oa.com', $response->getTargetUrl()); |
|||
} |
|||
|
|||
public function testOAuthWithAgentId() |
|||
{ |
|||
$response = (new WeWorkProvider(Request::create('foo'), 'CORPID', 'client_secret', 'REDIRECT_URI')) |
|||
->scopes(['snsapi_base']) |
|||
->setAgentId('1000000') |
|||
->stateless() |
|||
->redirect(); |
|||
|
|||
$this->assertSame('https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&agentid=1000000#wechat_redirect', $response->getTargetUrl()); |
|||
} |
|||
|
|||
public function testOAuthWithoutAgentId() |
|||
{ |
|||
$response = (new WeWorkProvider(Request::create('foo'), 'CORPID', 'client_secret', 'REDIRECT_URI')) |
|||
->scopes(['snsapi_base']) |
|||
->stateless() |
|||
->redirect(); |
|||
|
|||
$this->assertSame('https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base#wechat_redirect', $response->getTargetUrl()); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
use Overtrue\Socialite\AccessToken; |
|||
use Overtrue\Socialite\User; |
|||
use PHPUnit\Framework\TestCase; |
|||
|
|||
class UserTest extends TestCase |
|||
{ |
|||
public function testJsonserialize() |
|||
{ |
|||
$this->assertSame('[]', json_encode(new User([]))); |
|||
|
|||
$this->assertSame('{"token":"mock-token"}', json_encode(new User(['token' => new AccessToken(['access_token' => 'mock-token'])]))); |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/socialite. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
use Overtrue\Socialite\Providers\WeChatProvider as RealWeChatProvider; |
|||
use PHPUnit\Framework\TestCase; |
|||
use Symfony\Component\HttpFoundation\Request; |
|||
|
|||
class WechatProviderTest extends TestCase |
|||
{ |
|||
public function testWeChatProviderHasCorrectlyRedirectResponse() |
|||
{ |
|||
$response = (new WeChatProvider(Request::create('foo'), 'client_id', 'client_secret', 'http://localhost/socialite/callback.php'))->redirect(); |
|||
|
|||
$this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); |
|||
$this->assertStringStartsWith('https://open.weixin.qq.com/connect/qrconnect', $response->getTargetUrl()); |
|||
$this->assertRegExp('/redirect_uri=http%3A%2F%2Flocalhost%2Fsocialite%2Fcallback.php/', $response->getTargetUrl()); |
|||
} |
|||
|
|||
public function testWeChatProviderTokenUrlAndRequestFields() |
|||
{ |
|||
$provider = new WeChatProvider(Request::create('foo'), 'client_id', 'client_secret', 'http://localhost/socialite/callback.php'); |
|||
|
|||
$this->assertSame('https://api.weixin.qq.com/sns/oauth2/access_token', $provider->tokenUrl()); |
|||
$this->assertSame([ |
|||
'appid' => 'client_id', |
|||
'secret' => 'client_secret', |
|||
'code' => 'iloveyou', |
|||
'grant_type' => 'authorization_code', |
|||
], $provider->tokenFields('iloveyou')); |
|||
|
|||
$this->assertSame([ |
|||
'appid' => 'client_id', |
|||
'redirect_uri' => 'http://localhost/socialite/callback.php', |
|||
'response_type' => 'code', |
|||
'scope' => 'snsapi_login', |
|||
'state' => 'wechat-state', |
|||
'connect_redirect' => 1, |
|||
], $provider->codeFields('wechat-state')); |
|||
} |
|||
|
|||
public function testOpenPlatformComponent() |
|||
{ |
|||
$provider = new WeChatProvider(Request::create('foo'), 'client_id', null, 'redirect-url'); |
|||
$provider->component(new WeChatComponent()); |
|||
$this->assertSame([ |
|||
'appid' => 'client_id', |
|||
'redirect_uri' => 'redirect-url', |
|||
'response_type' => 'code', |
|||
'scope' => 'snsapi_base', |
|||
'state' => 'state', |
|||
'connect_redirect' => 1, |
|||
'component_appid' => 'component-app-id', |
|||
], $provider->codeFields('state')); |
|||
|
|||
$this->assertSame([ |
|||
'appid' => 'client_id', |
|||
'component_appid' => 'component-app-id', |
|||
'component_access_token' => 'token', |
|||
'code' => 'simcode', |
|||
'grant_type' => 'authorization_code', |
|||
], $provider->tokenFields('simcode')); |
|||
|
|||
$this->assertSame('https://api.weixin.qq.com/sns/oauth2/component/access_token', $provider->tokenUrl()); |
|||
} |
|||
} |
|||
|
|||
trait ProviderTrait |
|||
{ |
|||
public function tokenUrl() |
|||
{ |
|||
return $this->getTokenUrl(); |
|||
} |
|||
|
|||
public function tokenFields($code) |
|||
{ |
|||
return $this->getTokenFields($code); |
|||
} |
|||
|
|||
public function codeFields($state = null) |
|||
{ |
|||
return $this->getCodeFields($state); |
|||
} |
|||
} |
|||
|
|||
class WeChatProvider extends RealWeChatProvider |
|||
{ |
|||
use ProviderTrait; |
|||
} |
|||
|
|||
class WeChatComponent implements \Overtrue\Socialite\WeChatComponentInterface |
|||
{ |
|||
public function getAppId() |
|||
{ |
|||
return 'component-app-id'; |
|||
} |
|||
|
|||
public function getToken() |
|||
{ |
|||
return 'token'; |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService; |
|||
|
|||
use EasyWeChat\Kernel\ServiceContainer; |
|||
|
|||
/** |
|||
* Class Application. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
* |
|||
* @property \EasyWeChat\BasicService\Jssdk\Client $jssdk |
|||
* @property \EasyWeChat\BasicService\Media\Client $media |
|||
* @property \EasyWeChat\BasicService\QrCode\Client $qrcode |
|||
* @property \EasyWeChat\BasicService\Url\Client $url |
|||
* @property \EasyWeChat\BasicService\ContentSecurity\Client $content_security |
|||
*/ |
|||
class Application extends ServiceContainer |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $providers = [ |
|||
Jssdk\ServiceProvider::class, |
|||
QrCode\ServiceProvider::class, |
|||
Media\ServiceProvider::class, |
|||
Url\ServiceProvider::class, |
|||
ContentSecurity\ServiceProvider::class, |
|||
]; |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\ContentSecurity; |
|||
|
|||
use EasyWeChat\Kernel\BaseClient; |
|||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; |
|||
|
|||
/** |
|||
* Class Client. |
|||
* |
|||
* @author tianyong90 <412039588@qq.com> |
|||
*/ |
|||
class Client extends BaseClient |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $baseUri = 'https://api.weixin.qq.com/wxa/'; |
|||
|
|||
/** |
|||
* Text content security check. |
|||
* |
|||
* @param string $text |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function checkText(string $text) |
|||
{ |
|||
$params = [ |
|||
'content' => $text, |
|||
]; |
|||
|
|||
return $this->httpPostJson('msg_sec_check', $params); |
|||
} |
|||
|
|||
/** |
|||
* Image security check. |
|||
* |
|||
* @param string $path |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function checkImage(string $path) |
|||
{ |
|||
return $this->httpUpload('img_sec_check', ['media' => $path]); |
|||
} |
|||
|
|||
/** |
|||
* Media security check. |
|||
* |
|||
* @param string $mediaUrl |
|||
* @param int $mediaType |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function checkMediaAsync(string $mediaUrl, int $mediaType) |
|||
{ |
|||
/* |
|||
* 1:音频;2:图片 |
|||
*/ |
|||
$mediaTypes = [1, 2]; |
|||
|
|||
if (!in_array($mediaType, $mediaTypes, true)) { |
|||
throw new InvalidArgumentException('media type must be 1 or 2'); |
|||
} |
|||
|
|||
$params = [ |
|||
'media_url' => $mediaUrl, |
|||
'media_type' => $mediaType, |
|||
]; |
|||
|
|||
return $this->httpPostJson('media_check_async', $params); |
|||
} |
|||
|
|||
/** |
|||
* Image security check async. |
|||
* |
|||
* @param string $mediaUrl |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function checkImageAsync(string $mediaUrl) |
|||
{ |
|||
return $this->checkMediaAsync($mediaUrl, 2); |
|||
} |
|||
|
|||
/** |
|||
* Audio security check async. |
|||
* |
|||
* @param string $mediaUrl |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function checkAudioAsync(string $mediaUrl) |
|||
{ |
|||
return $this->checkMediaAsync($mediaUrl, 1); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\ContentSecurity; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ServiceProvider. |
|||
*/ |
|||
class ServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function register(Container $app) |
|||
{ |
|||
$app['content_security'] = function ($app) { |
|||
return new Client($app); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Jssdk; |
|||
|
|||
use EasyWeChat\Kernel\BaseClient; |
|||
use EasyWeChat\Kernel\Exceptions\RuntimeException; |
|||
use EasyWeChat\Kernel\Support; |
|||
use EasyWeChat\Kernel\Traits\InteractsWithCache; |
|||
|
|||
/** |
|||
* Class Client. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Client extends BaseClient |
|||
{ |
|||
use InteractsWithCache; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $ticketEndpoint = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'; |
|||
|
|||
/** |
|||
* Current URI. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $url; |
|||
|
|||
/** |
|||
* Get config json for jsapi. |
|||
* |
|||
* @param array $jsApiList |
|||
* @param bool $debug |
|||
* @param bool $beta |
|||
* @param bool $json |
|||
* |
|||
* @return array|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function buildConfig(array $jsApiList, bool $debug = false, bool $beta = false, bool $json = true) |
|||
{ |
|||
$config = array_merge(compact('debug', 'beta', 'jsApiList'), $this->configSignature()); |
|||
|
|||
return $json ? json_encode($config) : $config; |
|||
} |
|||
|
|||
/** |
|||
* Return jsapi config as a PHP array. |
|||
* |
|||
* @param array $apis |
|||
* @param bool $debug |
|||
* @param bool $beta |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function getConfigArray(array $apis, bool $debug = false, bool $beta = false) |
|||
{ |
|||
return $this->buildConfig($apis, $debug, $beta, false); |
|||
} |
|||
|
|||
/** |
|||
* Get js ticket. |
|||
* |
|||
* @param bool $refresh |
|||
* @param string $type |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
*/ |
|||
public function getTicket(bool $refresh = false, string $type = 'jsapi'): array |
|||
{ |
|||
$cacheKey = sprintf('easywechat.basic_service.jssdk.ticket.%s.%s', $type, $this->getAppId()); |
|||
|
|||
if (!$refresh && $this->getCache()->has($cacheKey)) { |
|||
return $this->getCache()->get($cacheKey); |
|||
} |
|||
|
|||
/** @var array<string, mixed> $result */ |
|||
$result = $this->castResponseToType( |
|||
$this->requestRaw($this->ticketEndpoint, 'GET', ['query' => ['type' => $type]]), |
|||
'array' |
|||
); |
|||
|
|||
$this->getCache()->set($cacheKey, $result, $result['expires_in'] - 500); |
|||
|
|||
if (!$this->getCache()->has($cacheKey)) { |
|||
throw new RuntimeException('Failed to cache jssdk ticket.'); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Build signature. |
|||
* |
|||
* @param string|null $url |
|||
* @param string|null $nonce |
|||
* @param int|null $timestamp |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
*/ |
|||
protected function configSignature(string $url = null, string $nonce = null, $timestamp = null): array |
|||
{ |
|||
$url = $url ?: $this->getUrl(); |
|||
$nonce = $nonce ?: Support\Str::quickRandom(10); |
|||
$timestamp = $timestamp ?: time(); |
|||
|
|||
return [ |
|||
'appId' => $this->getAppId(), |
|||
'nonceStr' => $nonce, |
|||
'timestamp' => $timestamp, |
|||
'url' => $url, |
|||
'signature' => $this->getTicketSignature($this->getTicket()['ticket'], $nonce, $timestamp, $url), |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Sign the params. |
|||
* |
|||
* @param string $ticket |
|||
* @param string $nonce |
|||
* @param int $timestamp |
|||
* @param string $url |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getTicketSignature($ticket, $nonce, $timestamp, $url): string |
|||
{ |
|||
return sha1(sprintf('jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s', $ticket, $nonce, $timestamp, $url)); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function dictionaryOrderSignature() |
|||
{ |
|||
$params = func_get_args(); |
|||
|
|||
sort($params, SORT_STRING); |
|||
|
|||
return sha1(implode('', $params)); |
|||
} |
|||
|
|||
/** |
|||
* Set current url. |
|||
* |
|||
* @param string $url |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setUrl(string $url) |
|||
{ |
|||
$this->url = $url; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Get current url. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getUrl(): string |
|||
{ |
|||
if ($this->url) { |
|||
return $this->url; |
|||
} |
|||
|
|||
return Support\current_url(); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
protected function getAppId() |
|||
{ |
|||
return $this->app['config']->get('app_id'); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Jssdk; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ServiceProvider. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class ServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function register(Container $app) |
|||
{ |
|||
$app['jssdk'] = function ($app) { |
|||
return new Client($app); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,212 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Media; |
|||
|
|||
use EasyWeChat\Kernel\BaseClient; |
|||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; |
|||
use EasyWeChat\Kernel\Http\StreamResponse; |
|||
|
|||
/** |
|||
* Class Client. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Client extends BaseClient |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $baseUri = 'https://api.weixin.qq.com/cgi-bin/'; |
|||
|
|||
/** |
|||
* Allow media type. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $allowTypes = ['image', 'voice', 'video', 'thumb']; |
|||
|
|||
/** |
|||
* Upload image. |
|||
* |
|||
* @param string $path |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function uploadImage($path) |
|||
{ |
|||
return $this->upload('image', $path); |
|||
} |
|||
|
|||
/** |
|||
* Upload video. |
|||
* |
|||
* @param string $path |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function uploadVideo($path) |
|||
{ |
|||
return $this->upload('video', $path); |
|||
} |
|||
|
|||
/** |
|||
* @param string $path |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function uploadVoice($path) |
|||
{ |
|||
return $this->upload('voice', $path); |
|||
} |
|||
|
|||
/** |
|||
* @param string $path |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function uploadThumb($path) |
|||
{ |
|||
return $this->upload('thumb', $path); |
|||
} |
|||
|
|||
/** |
|||
* Upload temporary material. |
|||
* |
|||
* @param string $type |
|||
* @param string $path |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function upload(string $type, string $path) |
|||
{ |
|||
if (!file_exists($path) || !is_readable($path)) { |
|||
throw new InvalidArgumentException(sprintf("File does not exist, or the file is unreadable: '%s'", $path)); |
|||
} |
|||
|
|||
if (!in_array($type, $this->allowTypes, true)) { |
|||
throw new InvalidArgumentException(sprintf("Unsupported media type: '%s'", $type)); |
|||
} |
|||
|
|||
return $this->httpUpload('media/upload', ['media' => $path], ['type' => $type]); |
|||
} |
|||
|
|||
/** |
|||
* @param string $path |
|||
* @param string $title |
|||
* @param string $description |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function uploadVideoForBroadcasting(string $path, string $title, string $description) |
|||
{ |
|||
$response = $this->uploadVideo($path); |
|||
/** @var array $arrayResponse */ |
|||
$arrayResponse = $this->detectAndCastResponseToType($response, 'array'); |
|||
|
|||
if (!empty($arrayResponse['media_id'])) { |
|||
return $this->createVideoForBroadcasting($arrayResponse['media_id'], $title, $description); |
|||
} |
|||
|
|||
return $response; |
|||
} |
|||
|
|||
/** |
|||
* @param string $mediaId |
|||
* @param string $title |
|||
* @param string $description |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function createVideoForBroadcasting(string $mediaId, string $title, string $description) |
|||
{ |
|||
return $this->httpPostJson('media/uploadvideo', [ |
|||
'media_id' => $mediaId, |
|||
'title' => $title, |
|||
'description' => $description, |
|||
]); |
|||
} |
|||
|
|||
/** |
|||
* Fetch item from WeChat server. |
|||
* |
|||
* @param string $mediaId |
|||
* |
|||
* @return \EasyWeChat\Kernel\Http\StreamResponse|\Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function get(string $mediaId) |
|||
{ |
|||
$response = $this->requestRaw('media/get', 'GET', [ |
|||
'query' => [ |
|||
'media_id' => $mediaId, |
|||
], |
|||
]); |
|||
|
|||
if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { |
|||
return StreamResponse::buildFromPsrResponse($response); |
|||
} |
|||
|
|||
return $this->castResponseToType($response, $this->app['config']->get('response_type')); |
|||
} |
|||
|
|||
/** |
|||
* @param string $mediaId |
|||
* |
|||
* @return array|\EasyWeChat\Kernel\Http\Response|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function getJssdkMedia(string $mediaId) |
|||
{ |
|||
$response = $this->requestRaw('media/get/jssdk', 'GET', [ |
|||
'query' => [ |
|||
'media_id' => $mediaId, |
|||
], |
|||
]); |
|||
|
|||
if (false !== stripos($response->getHeaderLine('Content-disposition'), 'attachment')) { |
|||
return StreamResponse::buildFromPsrResponse($response); |
|||
} |
|||
|
|||
return $this->castResponseToType($response, $this->app['config']->get('response_type')); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
/** |
|||
* ServiceProvider.php. |
|||
* |
|||
* This file is part of the wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Media; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ServiceProvider. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class ServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function register(Container $app) |
|||
{ |
|||
$app['media'] = function ($app) { |
|||
return new Client($app); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\QrCode; |
|||
|
|||
use EasyWeChat\Kernel\BaseClient; |
|||
|
|||
/** |
|||
* Class Client. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Client extends BaseClient |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $baseUri = 'https://api.weixin.qq.com/cgi-bin/'; |
|||
|
|||
const DAY = 86400; |
|||
const SCENE_MAX_VALUE = 100000; |
|||
const SCENE_QR_CARD = 'QR_CARD'; |
|||
const SCENE_QR_TEMPORARY = 'QR_SCENE'; |
|||
const SCENE_QR_TEMPORARY_STR = 'QR_STR_SCENE'; |
|||
const SCENE_QR_FOREVER = 'QR_LIMIT_SCENE'; |
|||
const SCENE_QR_FOREVER_STR = 'QR_LIMIT_STR_SCENE'; |
|||
|
|||
/** |
|||
* Create forever QR code. |
|||
* |
|||
* @param string|int $sceneValue |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
*/ |
|||
public function forever($sceneValue) |
|||
{ |
|||
if (is_int($sceneValue) && $sceneValue > 0 && $sceneValue < self::SCENE_MAX_VALUE) { |
|||
$type = self::SCENE_QR_FOREVER; |
|||
$sceneKey = 'scene_id'; |
|||
} else { |
|||
$type = self::SCENE_QR_FOREVER_STR; |
|||
$sceneKey = 'scene_str'; |
|||
} |
|||
$scene = [$sceneKey => $sceneValue]; |
|||
|
|||
return $this->create($type, $scene, false); |
|||
} |
|||
|
|||
/** |
|||
* Create temporary QR code. |
|||
* |
|||
* @param string|int $sceneValue |
|||
* @param int|null $expireSeconds |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
*/ |
|||
public function temporary($sceneValue, $expireSeconds = null) |
|||
{ |
|||
if (is_int($sceneValue) && $sceneValue > 0) { |
|||
$type = self::SCENE_QR_TEMPORARY; |
|||
$sceneKey = 'scene_id'; |
|||
} else { |
|||
$type = self::SCENE_QR_TEMPORARY_STR; |
|||
$sceneKey = 'scene_str'; |
|||
} |
|||
$scene = [$sceneKey => $sceneValue]; |
|||
|
|||
return $this->create($type, $scene, true, $expireSeconds); |
|||
} |
|||
|
|||
/** |
|||
* Return url for ticket. |
|||
* Detail: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1443433542 . |
|||
* |
|||
* @param string $ticket |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function url($ticket) |
|||
{ |
|||
return sprintf('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s', urlencode($ticket)); |
|||
} |
|||
|
|||
/** |
|||
* Create a QrCode. |
|||
* |
|||
* @param string $actionName |
|||
* @param array $actionInfo |
|||
* @param bool $temporary |
|||
* @param int $expireSeconds |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
protected function create($actionName, $actionInfo, $temporary = true, $expireSeconds = null) |
|||
{ |
|||
null !== $expireSeconds || $expireSeconds = 7 * self::DAY; |
|||
|
|||
$params = [ |
|||
'action_name' => $actionName, |
|||
'action_info' => ['scene' => $actionInfo], |
|||
]; |
|||
|
|||
if ($temporary) { |
|||
$params['expire_seconds'] = min($expireSeconds, 30 * self::DAY); |
|||
} |
|||
|
|||
return $this->httpPostJson('qrcode/create', $params); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\QrCode; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ServiceProvider. |
|||
*/ |
|||
class ServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function register(Container $app) |
|||
{ |
|||
$app['qrcode'] = function ($app) { |
|||
return new Client($app); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Url; |
|||
|
|||
use EasyWeChat\Kernel\BaseClient; |
|||
|
|||
/** |
|||
* Class Client. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Client extends BaseClient |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $baseUri = 'https://api.weixin.qq.com/'; |
|||
|
|||
/** |
|||
* Shorten the url. |
|||
* |
|||
* @param string $url |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function shorten(string $url) |
|||
{ |
|||
$params = [ |
|||
'action' => 'long2short', |
|||
'long_url' => $url, |
|||
]; |
|||
|
|||
return $this->httpPostJson('cgi-bin/shorturl', $params); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\BasicService\Url; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ServiceProvider. |
|||
*/ |
|||
class ServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* {@inheritdoc}. |
|||
*/ |
|||
public function register(Container $app) |
|||
{ |
|||
$app['url'] = function ($app) { |
|||
return new Client($app); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,282 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel; |
|||
|
|||
use EasyWeChat\Kernel\Contracts\AccessTokenInterface; |
|||
use EasyWeChat\Kernel\Exceptions\HttpException; |
|||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; |
|||
use EasyWeChat\Kernel\Exceptions\RuntimeException; |
|||
use EasyWeChat\Kernel\Traits\HasHttpRequests; |
|||
use EasyWeChat\Kernel\Traits\InteractsWithCache; |
|||
use Psr\Http\Message\RequestInterface; |
|||
use Psr\Http\Message\ResponseInterface; |
|||
|
|||
/** |
|||
* Class AccessToken. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
abstract class AccessToken implements AccessTokenInterface |
|||
{ |
|||
use HasHttpRequests; |
|||
use InteractsWithCache; |
|||
|
|||
/** |
|||
* @var \EasyWeChat\Kernel\ServiceContainer |
|||
*/ |
|||
protected $app; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $requestMethod = 'GET'; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $endpointToGetToken; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $queryName; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $token; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $safeSeconds = 500; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $tokenKey = 'access_token'; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $cachePrefix = 'easywechat.kernel.access_token.'; |
|||
|
|||
/** |
|||
* AccessToken constructor. |
|||
* |
|||
* @param \EasyWeChat\Kernel\ServiceContainer $app |
|||
*/ |
|||
public function __construct(ServiceContainer $app) |
|||
{ |
|||
$this->app = $app; |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function getRefreshedToken(): array |
|||
{ |
|||
return $this->getToken(true); |
|||
} |
|||
|
|||
/** |
|||
* @param bool $refresh |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function getToken(bool $refresh = false): array |
|||
{ |
|||
$cacheKey = $this->getCacheKey(); |
|||
$cache = $this->getCache(); |
|||
|
|||
if (!$refresh && $cache->has($cacheKey)) { |
|||
return $cache->get($cacheKey); |
|||
} |
|||
|
|||
/** @var array $token */ |
|||
$token = $this->requestToken($this->getCredentials(), true); |
|||
|
|||
$this->setToken($token[$this->tokenKey], $token['expires_in'] ?? 7200); |
|||
|
|||
$this->app->events->dispatch(new Events\AccessTokenRefreshed($this)); |
|||
|
|||
return $token; |
|||
} |
|||
|
|||
/** |
|||
* @param string $token |
|||
* @param int $lifetime |
|||
* |
|||
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
*/ |
|||
public function setToken(string $token, int $lifetime = 7200): AccessTokenInterface |
|||
{ |
|||
$this->getCache()->set($this->getCacheKey(), [ |
|||
$this->tokenKey => $token, |
|||
'expires_in' => $lifetime, |
|||
], $lifetime - $this->safeSeconds); |
|||
|
|||
if (!$this->getCache()->has($this->getCacheKey())) { |
|||
throw new RuntimeException('Failed to cache access token.'); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function refresh(): AccessTokenInterface |
|||
{ |
|||
$this->getToken(true); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param array $credentials |
|||
* @param bool $toArray |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function requestToken(array $credentials, $toArray = false) |
|||
{ |
|||
$response = $this->sendRequest($credentials); |
|||
$result = json_decode($response->getBody()->getContents(), true); |
|||
$formatted = $this->castResponseToType($response, $this->app['config']->get('response_type')); |
|||
|
|||
if (empty($result[$this->tokenKey])) { |
|||
throw new HttpException('Request access_token fail: '.json_encode($result, JSON_UNESCAPED_UNICODE), $response, $formatted); |
|||
} |
|||
|
|||
return $toArray ? $result : $formatted; |
|||
} |
|||
|
|||
/** |
|||
* @param \Psr\Http\Message\RequestInterface $request |
|||
* @param array $requestOptions |
|||
* |
|||
* @return \Psr\Http\Message\RequestInterface |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface |
|||
{ |
|||
parse_str($request->getUri()->getQuery(), $query); |
|||
|
|||
$query = http_build_query(array_merge($this->getQuery(), $query)); |
|||
|
|||
return $request->withUri($request->getUri()->withQuery($query)); |
|||
} |
|||
|
|||
/** |
|||
* Send http request. |
|||
* |
|||
* @param array $credentials |
|||
* |
|||
* @return ResponseInterface |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
protected function sendRequest(array $credentials): ResponseInterface |
|||
{ |
|||
$options = [ |
|||
('GET' === $this->requestMethod) ? 'query' : 'json' => $credentials, |
|||
]; |
|||
|
|||
return $this->setHttpClient($this->app['http_client'])->request($this->getEndpoint(), $this->requestMethod, $options); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
protected function getCacheKey() |
|||
{ |
|||
return $this->cachePrefix.md5(json_encode($this->getCredentials())); |
|||
} |
|||
|
|||
/** |
|||
* The request query will be used to add to the request. |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\HttpException |
|||
* @throws \Psr\SimpleCache\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
protected function getQuery(): array |
|||
{ |
|||
return [$this->queryName ?? $this->tokenKey => $this->getToken()[$this->tokenKey]]; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function getEndpoint(): string |
|||
{ |
|||
if (empty($this->endpointToGetToken)) { |
|||
throw new InvalidArgumentException('No endpoint for access token request.'); |
|||
} |
|||
|
|||
return $this->endpointToGetToken; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getTokenKey() |
|||
{ |
|||
return $this->tokenKey; |
|||
} |
|||
|
|||
/** |
|||
* Credential for get token. |
|||
* |
|||
* @return array |
|||
*/ |
|||
abstract protected function getCredentials(): array; |
|||
} |
|||
@ -0,0 +1,271 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel; |
|||
|
|||
use EasyWeChat\Kernel\Contracts\AccessTokenInterface; |
|||
use EasyWeChat\Kernel\Http\Response; |
|||
use EasyWeChat\Kernel\Traits\HasHttpRequests; |
|||
use GuzzleHttp\MessageFormatter; |
|||
use GuzzleHttp\Middleware; |
|||
use Psr\Http\Message\RequestInterface; |
|||
use Psr\Http\Message\ResponseInterface; |
|||
use Psr\Log\LogLevel; |
|||
|
|||
/** |
|||
* Class BaseClient. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class BaseClient |
|||
{ |
|||
use HasHttpRequests { request as performRequest; } |
|||
|
|||
/** |
|||
* @var \EasyWeChat\Kernel\ServiceContainer |
|||
*/ |
|||
protected $app; |
|||
|
|||
/** |
|||
* @var \EasyWeChat\Kernel\Contracts\AccessTokenInterface |
|||
*/ |
|||
protected $accessToken; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $baseUri; |
|||
|
|||
/** |
|||
* BaseClient constructor. |
|||
* |
|||
* @param \EasyWeChat\Kernel\ServiceContainer $app |
|||
* @param \EasyWeChat\Kernel\Contracts\AccessTokenInterface|null $accessToken |
|||
*/ |
|||
public function __construct(ServiceContainer $app, AccessTokenInterface $accessToken = null) |
|||
{ |
|||
$this->app = $app; |
|||
$this->accessToken = $accessToken ?? $this->app['access_token']; |
|||
} |
|||
|
|||
/** |
|||
* GET request. |
|||
* |
|||
* @param string $url |
|||
* @param array $query |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function httpGet(string $url, array $query = []) |
|||
{ |
|||
return $this->request($url, 'GET', ['query' => $query]); |
|||
} |
|||
|
|||
/** |
|||
* POST request. |
|||
* |
|||
* @param string $url |
|||
* @param array $data |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function httpPost(string $url, array $data = []) |
|||
{ |
|||
return $this->request($url, 'POST', ['form_params' => $data]); |
|||
} |
|||
|
|||
/** |
|||
* JSON request. |
|||
* |
|||
* @param string $url |
|||
* @param array $data |
|||
* @param array $query |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function httpPostJson(string $url, array $data = [], array $query = []) |
|||
{ |
|||
return $this->request($url, 'POST', ['query' => $query, 'json' => $data]); |
|||
} |
|||
|
|||
/** |
|||
* Upload file. |
|||
* |
|||
* @param string $url |
|||
* @param array $files |
|||
* @param array $form |
|||
* @param array $query |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function httpUpload(string $url, array $files = [], array $form = [], array $query = []) |
|||
{ |
|||
$multipart = []; |
|||
|
|||
foreach ($files as $name => $path) { |
|||
$multipart[] = [ |
|||
'name' => $name, |
|||
'contents' => fopen($path, 'r'), |
|||
]; |
|||
} |
|||
|
|||
foreach ($form as $name => $contents) { |
|||
$multipart[] = compact('name', 'contents'); |
|||
} |
|||
|
|||
return $this->request($url, 'POST', ['query' => $query, 'multipart' => $multipart, 'connect_timeout' => 30, 'timeout' => 30, 'read_timeout' => 30]); |
|||
} |
|||
|
|||
/** |
|||
* @return AccessTokenInterface |
|||
*/ |
|||
public function getAccessToken(): AccessTokenInterface |
|||
{ |
|||
return $this->accessToken; |
|||
} |
|||
|
|||
/** |
|||
* @param \EasyWeChat\Kernel\Contracts\AccessTokenInterface $accessToken |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setAccessToken(AccessTokenInterface $accessToken) |
|||
{ |
|||
$this->accessToken = $accessToken; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param string $url |
|||
* @param string $method |
|||
* @param array $options |
|||
* @param bool $returnRaw |
|||
* |
|||
* @return \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function request(string $url, string $method = 'GET', array $options = [], $returnRaw = false) |
|||
{ |
|||
if (empty($this->middlewares)) { |
|||
$this->registerHttpMiddlewares(); |
|||
} |
|||
|
|||
$response = $this->performRequest($url, $method, $options); |
|||
|
|||
$this->app->events->dispatch(new Events\HttpResponseCreated($response)); |
|||
|
|||
return $returnRaw ? $response : $this->castResponseToType($response, $this->app->config->get('response_type')); |
|||
} |
|||
|
|||
/** |
|||
* @param string $url |
|||
* @param string $method |
|||
* @param array $options |
|||
* |
|||
* @return \EasyWeChat\Kernel\Http\Response |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
|||
* @throws \GuzzleHttp\Exception\GuzzleException |
|||
*/ |
|||
public function requestRaw(string $url, string $method = 'GET', array $options = []) |
|||
{ |
|||
return Response::buildFromPsrResponse($this->request($url, $method, $options, true)); |
|||
} |
|||
|
|||
/** |
|||
* Register Guzzle middlewares. |
|||
*/ |
|||
protected function registerHttpMiddlewares() |
|||
{ |
|||
// retry |
|||
$this->pushMiddleware($this->retryMiddleware(), 'retry'); |
|||
// access token |
|||
$this->pushMiddleware($this->accessTokenMiddleware(), 'access_token'); |
|||
// log |
|||
$this->pushMiddleware($this->logMiddleware(), 'log'); |
|||
} |
|||
|
|||
/** |
|||
* Attache access token to request query. |
|||
* |
|||
* @return \Closure |
|||
*/ |
|||
protected function accessTokenMiddleware() |
|||
{ |
|||
return function (callable $handler) { |
|||
return function (RequestInterface $request, array $options) use ($handler) { |
|||
if ($this->accessToken) { |
|||
$request = $this->accessToken->applyToRequest($request, $options); |
|||
} |
|||
|
|||
return $handler($request, $options); |
|||
}; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Log the request. |
|||
* |
|||
* @return \Closure |
|||
*/ |
|||
protected function logMiddleware() |
|||
{ |
|||
$formatter = new MessageFormatter($this->app['config']['http.log_template'] ?? MessageFormatter::DEBUG); |
|||
|
|||
return Middleware::log($this->app['logger'], $formatter, LogLevel::DEBUG); |
|||
} |
|||
|
|||
/** |
|||
* Return retry middleware. |
|||
* |
|||
* @return \Closure |
|||
*/ |
|||
protected function retryMiddleware() |
|||
{ |
|||
return Middleware::retry(function ( |
|||
$retries, |
|||
RequestInterface $request, |
|||
ResponseInterface $response = null |
|||
) { |
|||
// Limit the number of retries to 2 |
|||
if ($retries < $this->app->config->get('http.max_retries', 1) && $response && $body = $response->getBody()) { |
|||
// Retry on server errors |
|||
$response = json_decode($body, true); |
|||
|
|||
if (!empty($response['errcode']) && in_array(abs($response['errcode']), [40001, 40014, 42001], true)) { |
|||
$this->accessToken->refresh(); |
|||
$this->app['logger']->debug('Retrying with refreshed access token.'); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
}, function () { |
|||
return abs($this->app->config->get('http.retry_delay', 500)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Clauses; |
|||
|
|||
/** |
|||
* Class Clause. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class Clause |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $clauses = [ |
|||
'where' => [], |
|||
]; |
|||
|
|||
/** |
|||
* @param mixed ...$args |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function where(...$args) |
|||
{ |
|||
array_push($this->clauses['where'], $args); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param mixed $payload |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function intercepted($payload) |
|||
{ |
|||
return (bool) $this->interceptWhereClause($payload); |
|||
} |
|||
|
|||
/** |
|||
* @param mixed $payload |
|||
* |
|||
* @return bool |
|||
*/ |
|||
protected function interceptWhereClause($payload) |
|||
{ |
|||
foreach ($this->clauses['where'] as $item) { |
|||
list($key, $value) = $item; |
|||
if (isset($payload[$key]) && $payload[$key] !== $value) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel; |
|||
|
|||
use EasyWeChat\Kernel\Support\Collection; |
|||
|
|||
/** |
|||
* Class Config. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Config extends Collection |
|||
{ |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Contracts; |
|||
|
|||
use Psr\Http\Message\RequestInterface; |
|||
|
|||
/** |
|||
* Interface AuthorizerAccessToken. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
interface AccessTokenInterface |
|||
{ |
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function getToken(): array; |
|||
|
|||
/** |
|||
* @return \EasyWeChat\Kernel\Contracts\AccessTokenInterface |
|||
*/ |
|||
public function refresh(): self; |
|||
|
|||
/** |
|||
* @param \Psr\Http\Message\RequestInterface $request |
|||
* @param array $requestOptions |
|||
* |
|||
* @return \Psr\Http\Message\RequestInterface |
|||
*/ |
|||
public function applyToRequest(RequestInterface $request, array $requestOptions = []): RequestInterface; |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Contracts; |
|||
|
|||
use ArrayAccess; |
|||
|
|||
/** |
|||
* Interface Arrayable. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
interface Arrayable extends ArrayAccess |
|||
{ |
|||
/** |
|||
* Get the instance as an array. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function toArray(); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Contracts; |
|||
|
|||
/** |
|||
* Interface EventHandlerInterface. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
interface EventHandlerInterface |
|||
{ |
|||
/** |
|||
* @param mixed $payload |
|||
*/ |
|||
public function handle($payload = null); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Contracts; |
|||
|
|||
/** |
|||
* Interface MediaInterface. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
interface MediaInterface extends MessageInterface |
|||
{ |
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getMediaId(): string; |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Contracts; |
|||
|
|||
/** |
|||
* Interface MessageInterface. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
interface MessageInterface |
|||
{ |
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getType(): string; |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function transformForJsonRequest(): array; |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function transformToXml(): string; |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Decorators; |
|||
|
|||
/** |
|||
* Class FinallyResult. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class FinallyResult |
|||
{ |
|||
/** |
|||
* @var mixed |
|||
*/ |
|||
public $content; |
|||
|
|||
/** |
|||
* FinallyResult constructor. |
|||
* |
|||
* @param mixed $content |
|||
*/ |
|||
public function __construct($content) |
|||
{ |
|||
$this->content = $content; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Decorators; |
|||
|
|||
/** |
|||
* Class TerminateResult. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class TerminateResult |
|||
{ |
|||
/** |
|||
* @var mixed |
|||
*/ |
|||
public $content; |
|||
|
|||
/** |
|||
* FinallyResult constructor. |
|||
* |
|||
* @param mixed $content |
|||
*/ |
|||
public function __construct($content) |
|||
{ |
|||
$this->content = $content; |
|||
} |
|||
} |
|||
@ -0,0 +1,219 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel; |
|||
|
|||
use EasyWeChat\Kernel\Exceptions\RuntimeException; |
|||
use EasyWeChat\Kernel\Support\AES; |
|||
use function EasyWeChat\Kernel\Support\str_random; |
|||
use EasyWeChat\Kernel\Support\XML; |
|||
use Throwable; |
|||
|
|||
/** |
|||
* Class Encryptor. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Encryptor |
|||
{ |
|||
const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed |
|||
const ERROR_PARSE_XML = -40002; // Parse XML failed |
|||
const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed |
|||
const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey |
|||
const ERROR_INVALID_APP_ID = -40005; // Check AppID failed |
|||
const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed |
|||
const ERROR_DECRYPT_AES = -40007; // AES decryption failed |
|||
const ERROR_INVALID_XML = -40008; // Invalid XML |
|||
const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed |
|||
const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed |
|||
const ERROR_XML_BUILD = -40011; // XML build failed |
|||
const ILLEGAL_BUFFER = -41003; // Illegal buffer |
|||
|
|||
/** |
|||
* App id. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $appId; |
|||
|
|||
/** |
|||
* App token. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $token; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $aesKey; |
|||
|
|||
/** |
|||
* Block size. |
|||
* |
|||
* @var int |
|||
*/ |
|||
protected $blockSize = 32; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* @param string $appId |
|||
* @param string|null $token |
|||
* @param string|null $aesKey |
|||
*/ |
|||
public function __construct(string $appId, string $token = null, string $aesKey = null) |
|||
{ |
|||
$this->appId = $appId; |
|||
$this->token = $token; |
|||
$this->aesKey = base64_decode($aesKey.'=', true); |
|||
} |
|||
|
|||
/** |
|||
* Get the app token. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getToken(): string |
|||
{ |
|||
return $this->token; |
|||
} |
|||
|
|||
/** |
|||
* Encrypt the message and return XML. |
|||
* |
|||
* @param string $xml |
|||
* @param string $nonce |
|||
* @param int $timestamp |
|||
* |
|||
* @return string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function encrypt($xml, $nonce = null, $timestamp = null): string |
|||
{ |
|||
try { |
|||
$xml = $this->pkcs7Pad(str_random(16).pack('N', strlen($xml)).$xml.$this->appId, $this->blockSize); |
|||
|
|||
$encrypted = base64_encode(AES::encrypt( |
|||
$xml, |
|||
$this->aesKey, |
|||
substr($this->aesKey, 0, 16), |
|||
OPENSSL_NO_PADDING |
|||
)); |
|||
// @codeCoverageIgnoreStart |
|||
} catch (Throwable $e) { |
|||
throw new RuntimeException($e->getMessage(), self::ERROR_ENCRYPT_AES); |
|||
} |
|||
// @codeCoverageIgnoreEnd |
|||
|
|||
!is_null($nonce) || $nonce = substr($this->appId, 0, 10); |
|||
!is_null($timestamp) || $timestamp = time(); |
|||
|
|||
$response = [ |
|||
'Encrypt' => $encrypted, |
|||
'MsgSignature' => $this->signature($this->token, $timestamp, $nonce, $encrypted), |
|||
'TimeStamp' => $timestamp, |
|||
'Nonce' => $nonce, |
|||
]; |
|||
|
|||
//生成响应xml |
|||
return XML::build($response); |
|||
} |
|||
|
|||
/** |
|||
* Decrypt message. |
|||
* |
|||
* @param string $content |
|||
* @param string $msgSignature |
|||
* @param string $nonce |
|||
* @param string $timestamp |
|||
* |
|||
* @return string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function decrypt($content, $msgSignature, $nonce, $timestamp): string |
|||
{ |
|||
$signature = $this->signature($this->token, $timestamp, $nonce, $content); |
|||
|
|||
if ($signature !== $msgSignature) { |
|||
throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE); |
|||
} |
|||
|
|||
$decrypted = AES::decrypt( |
|||
base64_decode($content, true), |
|||
$this->aesKey, |
|||
substr($this->aesKey, 0, 16), |
|||
OPENSSL_NO_PADDING |
|||
); |
|||
$result = $this->pkcs7Unpad($decrypted); |
|||
$content = substr($result, 16, strlen($result)); |
|||
$contentLen = unpack('N', substr($content, 0, 4))[1]; |
|||
|
|||
if (trim(substr($content, $contentLen + 4)) !== $this->appId) { |
|||
throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID); |
|||
} |
|||
|
|||
return substr($content, 4, $contentLen); |
|||
} |
|||
|
|||
/** |
|||
* Get SHA1. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function signature(): string |
|||
{ |
|||
$array = func_get_args(); |
|||
sort($array, SORT_STRING); |
|||
|
|||
return sha1(implode($array)); |
|||
} |
|||
|
|||
/** |
|||
* PKCS#7 pad. |
|||
* |
|||
* @param string $text |
|||
* @param int $blockSize |
|||
* |
|||
* @return string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function pkcs7Pad(string $text, int $blockSize): string |
|||
{ |
|||
if ($blockSize > 256) { |
|||
throw new RuntimeException('$blockSize may not be more than 256'); |
|||
} |
|||
$padding = $blockSize - (strlen($text) % $blockSize); |
|||
$pattern = chr($padding); |
|||
|
|||
return $text.str_repeat($pattern, $padding); |
|||
} |
|||
|
|||
/** |
|||
* PKCS#7 unpad. |
|||
* |
|||
* @param string $text |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function pkcs7Unpad(string $text): string |
|||
{ |
|||
$pad = ord(substr($text, -1)); |
|||
if ($pad < 1 || $pad > $this->blockSize) { |
|||
$pad = 0; |
|||
} |
|||
|
|||
return substr($text, 0, (strlen($text) - $pad)); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Events; |
|||
|
|||
use EasyWeChat\Kernel\AccessToken; |
|||
|
|||
/** |
|||
* Class AccessTokenRefreshed. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class AccessTokenRefreshed |
|||
{ |
|||
/** |
|||
* @var \EasyWeChat\Kernel\AccessToken |
|||
*/ |
|||
public $accessToken; |
|||
|
|||
/** |
|||
* @param \EasyWeChat\Kernel\AccessToken $accessToken |
|||
*/ |
|||
public function __construct(AccessToken $accessToken) |
|||
{ |
|||
$this->accessToken = $accessToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Events; |
|||
|
|||
use EasyWeChat\Kernel\ServiceContainer; |
|||
|
|||
/** |
|||
* Class ApplicationInitialized. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class ApplicationInitialized |
|||
{ |
|||
/** |
|||
* @var \EasyWeChat\Kernel\ServiceContainer |
|||
*/ |
|||
public $app; |
|||
|
|||
/** |
|||
* @param \EasyWeChat\Kernel\ServiceContainer $app |
|||
*/ |
|||
public function __construct(ServiceContainer $app) |
|||
{ |
|||
$this->app = $app; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Events; |
|||
|
|||
use Psr\Http\Message\ResponseInterface; |
|||
|
|||
/** |
|||
* Class HttpResponseCreated. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class HttpResponseCreated |
|||
{ |
|||
/** |
|||
* @var \Psr\Http\Message\ResponseInterface |
|||
*/ |
|||
public $response; |
|||
|
|||
/** |
|||
* @param \Psr\Http\Message\ResponseInterface $response |
|||
*/ |
|||
public function __construct(ResponseInterface $response) |
|||
{ |
|||
$this->response = $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Events; |
|||
|
|||
use Symfony\Component\HttpFoundation\Response; |
|||
|
|||
/** |
|||
* Class ServerGuardResponseCreated. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class ServerGuardResponseCreated |
|||
{ |
|||
/** |
|||
* @var \Symfony\Component\HttpFoundation\Response |
|||
*/ |
|||
public $response; |
|||
|
|||
/** |
|||
* @param \Symfony\Component\HttpFoundation\Response $response |
|||
*/ |
|||
public function __construct(Response $response) |
|||
{ |
|||
$this->response = $response; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
/** |
|||
* Class BadRequestException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class BadRequestException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
class DecryptException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
use Exception as BaseException; |
|||
|
|||
/** |
|||
* Class Exception. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Exception extends BaseException |
|||
{ |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
use Psr\Http\Message\ResponseInterface; |
|||
|
|||
/** |
|||
* Class HttpException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class HttpException extends Exception |
|||
{ |
|||
/** |
|||
* @var \Psr\Http\Message\ResponseInterface|null |
|||
*/ |
|||
public $response; |
|||
|
|||
/** |
|||
* @var \Psr\Http\Message\ResponseInterface|\EasyWeChat\Kernel\Support\Collection|array|object|string|null |
|||
*/ |
|||
public $formattedResponse; |
|||
|
|||
/** |
|||
* HttpException constructor. |
|||
* |
|||
* @param string $message |
|||
* @param \Psr\Http\Message\ResponseInterface|null $response |
|||
* @param null $formattedResponse |
|||
* @param int|null $code |
|||
*/ |
|||
public function __construct($message, ResponseInterface $response = null, $formattedResponse = null, $code = null) |
|||
{ |
|||
parent::__construct($message, $code); |
|||
|
|||
$this->response = $response; |
|||
$this->formattedResponse = $formattedResponse; |
|||
|
|||
if ($response) { |
|||
$response->getBody()->rewind(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
/** |
|||
* Class InvalidArgumentException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class InvalidArgumentException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
/** |
|||
* Class InvalidConfigException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class InvalidConfigException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
/** |
|||
* Class RuntimeException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class RuntimeException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Exceptions; |
|||
|
|||
/** |
|||
* Class InvalidConfigException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class UnboundServiceException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel; |
|||
|
|||
use EasyWeChat\Kernel\Contracts\Arrayable; |
|||
use EasyWeChat\Kernel\Exceptions\RuntimeException; |
|||
use EasyWeChat\Kernel\Support\Arr; |
|||
use EasyWeChat\Kernel\Support\Collection; |
|||
|
|||
function data_get($data, $key, $default = null) |
|||
{ |
|||
switch (true) { |
|||
case is_array($data): |
|||
return Arr::get($data, $key, $default); |
|||
case $data instanceof Collection: |
|||
return $data->get($key, $default); |
|||
case $data instanceof Arrayable: |
|||
return Arr::get($data->toArray(), $key, $default); |
|||
case $data instanceof \ArrayIterator: |
|||
return $data->getArrayCopy()[$key] ?? $default; |
|||
case $data instanceof \ArrayAccess: |
|||
return $data[$key] ?? $default; |
|||
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator: |
|||
return $data->getIterator()->getArrayCopy()[$key] ?? $default; |
|||
case is_object($data): |
|||
return $data->{$key} ?? $default; |
|||
default: |
|||
throw new RuntimeException(sprintf('Can\'t access data with key "%s"', $key)); |
|||
} |
|||
} |
|||
|
|||
function data_to_array($data) |
|||
{ |
|||
switch (true) { |
|||
case is_array($data): |
|||
return $data; |
|||
case $data instanceof Collection: |
|||
return $data->all(); |
|||
case $data instanceof Arrayable: |
|||
return $data->toArray(); |
|||
case $data instanceof \IteratorAggregate && $data->getIterator() instanceof \ArrayIterator: |
|||
return $data->getIterator()->getArrayCopy(); |
|||
case $data instanceof \ArrayIterator: |
|||
return $data->getArrayCopy(); |
|||
default: |
|||
throw new RuntimeException(sprintf('Can\'t transform data to array')); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Http; |
|||
|
|||
use EasyWeChat\Kernel\Support\Collection; |
|||
use EasyWeChat\Kernel\Support\XML; |
|||
use GuzzleHttp\Psr7\Response as GuzzleResponse; |
|||
use Psr\Http\Message\ResponseInterface; |
|||
|
|||
/** |
|||
* Class Response. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Response extends GuzzleResponse |
|||
{ |
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getBodyContents() |
|||
{ |
|||
$this->getBody()->rewind(); |
|||
$contents = $this->getBody()->getContents(); |
|||
$this->getBody()->rewind(); |
|||
|
|||
return $contents; |
|||
} |
|||
|
|||
/** |
|||
* @param \Psr\Http\Message\ResponseInterface $response |
|||
* |
|||
* @return \EasyWeChat\Kernel\Http\Response |
|||
*/ |
|||
public static function buildFromPsrResponse(ResponseInterface $response) |
|||
{ |
|||
return new static( |
|||
$response->getStatusCode(), |
|||
$response->getHeaders(), |
|||
$response->getBody(), |
|||
$response->getProtocolVersion(), |
|||
$response->getReasonPhrase() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Build to json. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function toJson() |
|||
{ |
|||
return json_encode($this->toArray()); |
|||
} |
|||
|
|||
/** |
|||
* Build to array. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function toArray() |
|||
{ |
|||
$content = $this->removeControlCharacters($this->getBodyContents()); |
|||
|
|||
if (false !== stripos($this->getHeaderLine('Content-Type'), 'xml') || 0 === stripos($content, '<xml')) { |
|||
return XML::parse($content); |
|||
} |
|||
|
|||
$array = json_decode($content, true, 512, JSON_BIGINT_AS_STRING); |
|||
|
|||
if (JSON_ERROR_NONE === json_last_error()) { |
|||
return (array) $array; |
|||
} |
|||
|
|||
return []; |
|||
} |
|||
|
|||
/** |
|||
* Get collection data. |
|||
* |
|||
* @return \EasyWeChat\Kernel\Support\Collection |
|||
*/ |
|||
public function toCollection() |
|||
{ |
|||
return new Collection($this->toArray()); |
|||
} |
|||
|
|||
/** |
|||
* @return object |
|||
*/ |
|||
public function toObject() |
|||
{ |
|||
return json_decode($this->toJson()); |
|||
} |
|||
|
|||
/** |
|||
* @return bool|string |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
return $this->getBodyContents(); |
|||
} |
|||
|
|||
/** |
|||
* @param string $content |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function removeControlCharacters(string $content) |
|||
{ |
|||
return \preg_replace('/[\x00-\x1F\x80-\x9F]/u', '', $content); |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Http; |
|||
|
|||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; |
|||
use EasyWeChat\Kernel\Exceptions\RuntimeException; |
|||
use EasyWeChat\Kernel\Support\File; |
|||
|
|||
/** |
|||
* Class StreamResponse. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class StreamResponse extends Response |
|||
{ |
|||
/** |
|||
* @param string $directory |
|||
* @param string $filename |
|||
* @param bool $appendSuffix |
|||
* |
|||
* @return bool|int |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function save(string $directory, string $filename = '', bool $appendSuffix = true) |
|||
{ |
|||
$this->getBody()->rewind(); |
|||
|
|||
$directory = rtrim($directory, '/'); |
|||
|
|||
if (!is_dir($directory)) { |
|||
mkdir($directory, 0755, true); // @codeCoverageIgnore |
|||
} |
|||
|
|||
if (!is_writable($directory)) { |
|||
throw new InvalidArgumentException(sprintf("'%s' is not writable.", $directory)); |
|||
} |
|||
|
|||
$contents = $this->getBody()->getContents(); |
|||
|
|||
if (empty($contents) || '{' === $contents[0]) { |
|||
throw new RuntimeException('Invalid media response content.'); |
|||
} |
|||
|
|||
if (empty($filename)) { |
|||
if (preg_match('/filename="(?<filename>.*?)"/', $this->getHeaderLine('Content-Disposition'), $match)) { |
|||
$filename = $match['filename']; |
|||
} else { |
|||
$filename = md5($contents); |
|||
} |
|||
} |
|||
|
|||
if ($appendSuffix && empty(pathinfo($filename, PATHINFO_EXTENSION))) { |
|||
$filename .= File::getStreamExt($contents); |
|||
} |
|||
|
|||
file_put_contents($directory.'/'.$filename, $contents); |
|||
|
|||
return $filename; |
|||
} |
|||
|
|||
/** |
|||
* @param string $directory |
|||
* @param string $filename |
|||
* @param bool $appendSuffix |
|||
* |
|||
* @return bool|int |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException |
|||
*/ |
|||
public function saveAs(string $directory, string $filename, bool $appendSuffix = true) |
|||
{ |
|||
return $this->save($directory, $filename, $appendSuffix); |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Article. |
|||
*/ |
|||
class Article extends Message |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $type = 'mpnews'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'thumb_media_id', |
|||
'author', |
|||
'title', |
|||
'content', |
|||
'digest', |
|||
'source_url', |
|||
'show_cover', |
|||
]; |
|||
|
|||
/** |
|||
* Aliases of attribute. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $jsonAliases = [ |
|||
'content_source_url' => 'source_url', |
|||
'show_cover_pic' => 'show_cover', |
|||
]; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $required = [ |
|||
'thumb_media_id', |
|||
'title', |
|||
'content', |
|||
'show_cover', |
|||
]; |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
/** |
|||
* Card.php. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
* @copyright 2015 overtrue <i@overtrue.me> |
|||
* |
|||
* @see https://github.com/overtrue |
|||
* @see http://overtrue.me |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Card. |
|||
*/ |
|||
class Card extends Message |
|||
{ |
|||
/** |
|||
* Message type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'wxcard'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = ['card_id']; |
|||
|
|||
/** |
|||
* Media constructor. |
|||
* |
|||
* @param string $cardId |
|||
*/ |
|||
public function __construct(string $cardId) |
|||
{ |
|||
parent::__construct(['card_id' => $cardId]); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class DeviceEvent. |
|||
* |
|||
* @property string $media_id |
|||
*/ |
|||
class DeviceEvent extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'device_event'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'device_type', |
|||
'device_id', |
|||
'content', |
|||
'session_id', |
|||
'open_id', |
|||
]; |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class DeviceText. |
|||
* |
|||
* @property string $content |
|||
*/ |
|||
class DeviceText extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'device_text'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'device_type', |
|||
'device_id', |
|||
'content', |
|||
'session_id', |
|||
'open_id', |
|||
]; |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
return [ |
|||
'DeviceType' => $this->get('device_type'), |
|||
'DeviceID' => $this->get('device_id'), |
|||
'SessionID' => $this->get('session_id'), |
|||
'Content' => base64_encode($this->get('content')), |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Image. |
|||
* |
|||
* @property string $media_id |
|||
*/ |
|||
class File extends Media |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $type = 'file'; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Image. |
|||
* |
|||
* @property string $media_id |
|||
*/ |
|||
class Image extends Media |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'image'; |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Link. |
|||
*/ |
|||
class Link extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'link'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'url', |
|||
]; |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Location. |
|||
*/ |
|||
class Location extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'location'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'latitude', |
|||
'longitude', |
|||
'scale', |
|||
'label', |
|||
'precision', |
|||
]; |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
use EasyWeChat\Kernel\Contracts\MediaInterface; |
|||
use EasyWeChat\Kernel\Support\Str; |
|||
|
|||
/** |
|||
* Class Media. |
|||
*/ |
|||
class Media extends Message implements MediaInterface |
|||
{ |
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = ['media_id']; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $required = [ |
|||
'media_id', |
|||
]; |
|||
|
|||
/** |
|||
* MaterialClient constructor. |
|||
* |
|||
* @param string $mediaId |
|||
* @param string $type |
|||
* @param array $attributes |
|||
*/ |
|||
public function __construct(string $mediaId, $type = null, array $attributes = []) |
|||
{ |
|||
parent::__construct(array_merge(['media_id' => $mediaId], $attributes)); |
|||
|
|||
!empty($type) && $this->setType($type); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function getMediaId(): string |
|||
{ |
|||
$this->checkRequiredAttributes(); |
|||
|
|||
return $this->get('media_id'); |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
return [ |
|||
Str::studly($this->getType()) => [ |
|||
'MediaId' => $this->get('media_id'), |
|||
], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
use EasyWeChat\Kernel\Contracts\MessageInterface; |
|||
use EasyWeChat\Kernel\Support\XML; |
|||
use EasyWeChat\Kernel\Traits\HasAttributes; |
|||
use Mockery\Exception\BadMethodCallException; |
|||
|
|||
/** |
|||
* Class Messages. |
|||
*/ |
|||
abstract class Message implements MessageInterface |
|||
{ |
|||
use HasAttributes; |
|||
|
|||
const TEXT = 2; |
|||
const IMAGE = 4; |
|||
const VOICE = 8; |
|||
const VIDEO = 16; |
|||
const SHORT_VIDEO = 32; |
|||
const LOCATION = 64; |
|||
const LINK = 128; |
|||
const DEVICE_EVENT = 256; |
|||
const DEVICE_TEXT = 512; |
|||
const FILE = 1024; |
|||
const TEXT_CARD = 2048; |
|||
const TRANSFER = 4096; |
|||
const EVENT = 1048576; |
|||
const MINIPROGRAM_PAGE = 2097152; |
|||
const ALL = self::TEXT | self::IMAGE | self::VOICE | self::VIDEO | self::SHORT_VIDEO | self::LOCATION | self::LINK |
|||
| self::DEVICE_EVENT | self::DEVICE_TEXT | self::FILE | self::TEXT_CARD | self::TRANSFER | self::EVENT | self::MINIPROGRAM_PAGE; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $type; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
protected $id; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $to; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $from; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $properties = []; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $jsonAliases = []; |
|||
|
|||
/** |
|||
* Message constructor. |
|||
* |
|||
* @param array $attributes |
|||
*/ |
|||
public function __construct(array $attributes = []) |
|||
{ |
|||
$this->setAttributes($attributes); |
|||
} |
|||
|
|||
/** |
|||
* Return type name message. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getType(): string |
|||
{ |
|||
return $this->type; |
|||
} |
|||
|
|||
/** |
|||
* @param string $type |
|||
*/ |
|||
public function setType(string $type) |
|||
{ |
|||
$this->type = $type; |
|||
} |
|||
|
|||
/** |
|||
* Magic getter. |
|||
* |
|||
* @param string $property |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function __get($property) |
|||
{ |
|||
if (property_exists($this, $property)) { |
|||
return $this->$property; |
|||
} |
|||
|
|||
return $this->getAttribute($property); |
|||
} |
|||
|
|||
/** |
|||
* Magic setter. |
|||
* |
|||
* @param string $property |
|||
* @param mixed $value |
|||
* |
|||
* @return Message |
|||
*/ |
|||
public function __set($property, $value) |
|||
{ |
|||
if (property_exists($this, $property)) { |
|||
$this->$property = $value; |
|||
} else { |
|||
$this->setAttribute($property, $value); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param array $appends |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function transformForJsonRequestWithoutType(array $appends = []) |
|||
{ |
|||
return $this->transformForJsonRequest($appends, false); |
|||
} |
|||
|
|||
/** |
|||
* @param array $appends |
|||
* @param bool $withType |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function transformForJsonRequest(array $appends = [], $withType = true): array |
|||
{ |
|||
if (!$withType) { |
|||
return $this->propertiesToArray([], $this->jsonAliases); |
|||
} |
|||
$messageType = $this->getType(); |
|||
$data = array_merge(['msgtype' => $messageType], $appends); |
|||
|
|||
$data[$messageType] = array_merge($data[$messageType] ?? [], $this->propertiesToArray([], $this->jsonAliases)); |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
/** |
|||
* @param array $appends |
|||
* @param bool $returnAsArray |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function transformToXml(array $appends = [], bool $returnAsArray = false): string |
|||
{ |
|||
$data = array_merge(['MsgType' => $this->getType()], $this->toXmlArray(), $appends); |
|||
|
|||
return $returnAsArray ? $data : XML::build($data); |
|||
} |
|||
|
|||
/** |
|||
* @param array $data |
|||
* @param array $aliases |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
|||
*/ |
|||
protected function propertiesToArray(array $data, array $aliases = []): array |
|||
{ |
|||
$this->checkRequiredAttributes(); |
|||
|
|||
foreach ($this->attributes as $property => $value) { |
|||
if (is_null($value) && !$this->isRequired($property)) { |
|||
continue; |
|||
} |
|||
$alias = array_search($property, $aliases, true); |
|||
|
|||
$data[$alias ?: $property] = $this->get($property); |
|||
} |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
throw new BadMethodCallException(sprintf('Class "%s" cannot support transform to XML message.', __CLASS__)); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class MiniProgramPage. |
|||
*/ |
|||
class MiniProgramPage extends Message |
|||
{ |
|||
protected $type = 'miniprogrampage'; |
|||
|
|||
protected $properties = [ |
|||
'title', |
|||
'appid', |
|||
'pagepath', |
|||
'thumb_media_id', |
|||
]; |
|||
|
|||
protected $required = [ |
|||
'thumb_media_id', 'appid', 'pagepath', |
|||
]; |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Music. |
|||
* |
|||
* @property string $url |
|||
* @property string $hq_url |
|||
* @property string $title |
|||
* @property string $description |
|||
* @property string $thumb_media_id |
|||
* @property string $format |
|||
*/ |
|||
class Music extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'music'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'url', |
|||
'hq_url', |
|||
'thumb_media_id', |
|||
'format', |
|||
]; |
|||
|
|||
/** |
|||
* Aliases of attribute. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $jsonAliases = [ |
|||
'musicurl' => 'url', |
|||
'hqmusicurl' => 'hq_url', |
|||
]; |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
$music = [ |
|||
'Music' => [ |
|||
'Title' => $this->get('title'), |
|||
'Description' => $this->get('description'), |
|||
'MusicUrl' => $this->get('url'), |
|||
'HQMusicUrl' => $this->get('hq_url'), |
|||
], |
|||
]; |
|||
if ($thumbMediaId = $this->get('thumb_media_id')) { |
|||
$music['Music']['ThumbMediaId'] = $thumbMediaId; |
|||
} |
|||
|
|||
return $music; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class News. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class News extends Message |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $type = 'news'; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'items', |
|||
]; |
|||
|
|||
/** |
|||
* News constructor. |
|||
* |
|||
* @param array $items |
|||
*/ |
|||
public function __construct(array $items = []) |
|||
{ |
|||
parent::__construct(compact('items')); |
|||
} |
|||
|
|||
/** |
|||
* @param array $data |
|||
* @param array $aliases |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function propertiesToArray(array $data, array $aliases = []): array |
|||
{ |
|||
return ['articles' => array_map(function ($item) { |
|||
if ($item instanceof NewsItem) { |
|||
return $item->toJsonArray(); |
|||
} |
|||
}, $this->get('items'))]; |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
$items = []; |
|||
|
|||
foreach ($this->get('items') as $item) { |
|||
if ($item instanceof NewsItem) { |
|||
$items[] = $item->toXmlArray(); |
|||
} |
|||
} |
|||
|
|||
return [ |
|||
'ArticleCount' => count($items), |
|||
'Articles' => $items, |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class NewsItem. |
|||
*/ |
|||
class NewsItem extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'news'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'url', |
|||
'image', |
|||
]; |
|||
|
|||
public function toJsonArray() |
|||
{ |
|||
return [ |
|||
'title' => $this->get('title'), |
|||
'description' => $this->get('description'), |
|||
'url' => $this->get('url'), |
|||
'picurl' => $this->get('image'), |
|||
]; |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
return [ |
|||
'Title' => $this->get('title'), |
|||
'Description' => $this->get('description'), |
|||
'Url' => $this->get('url'), |
|||
'PicUrl' => $this->get('image'), |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Raw. |
|||
*/ |
|||
class Raw extends Message |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $type = 'raw'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = ['content']; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* @param string $content |
|||
*/ |
|||
public function __construct(string $content) |
|||
{ |
|||
parent::__construct(['content' => strval($content)]); |
|||
} |
|||
|
|||
/** |
|||
* @param array $appends |
|||
* @param bool $withType |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function transformForJsonRequest(array $appends = [], $withType = true): array |
|||
{ |
|||
return json_decode($this->content, true) ?? []; |
|||
} |
|||
|
|||
public function __toString() |
|||
{ |
|||
return $this->get('content') ?? ''; |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class ShortVideo. |
|||
* |
|||
* @property string $title |
|||
* @property string $media_id |
|||
* @property string $description |
|||
* @property string $thumb_media_id |
|||
*/ |
|||
class ShortVideo extends Video |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'shortvideo'; |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class TaskCard. |
|||
* |
|||
* @property string $title |
|||
* @property string $description |
|||
* @property string $url |
|||
* @property string $task_id |
|||
* @property array $btn |
|||
*/ |
|||
class TaskCard extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'taskcard'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'url', |
|||
'task_id', |
|||
'btn', |
|||
]; |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Text. |
|||
* |
|||
* @property string $content |
|||
*/ |
|||
class Text extends Message |
|||
{ |
|||
/** |
|||
* Message type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'text'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = ['content']; |
|||
|
|||
/** |
|||
* Text constructor. |
|||
* |
|||
* @param string $content |
|||
*/ |
|||
public function __construct(string $content) |
|||
{ |
|||
parent::__construct(compact('content')); |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function toXmlArray() |
|||
{ |
|||
return [ |
|||
'Content' => $this->get('content'), |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Text. |
|||
* |
|||
* @property string $title |
|||
* @property string $description |
|||
* @property string $url |
|||
*/ |
|||
class TextCard extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'textcard'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'url', |
|||
]; |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Transfer. |
|||
* |
|||
* @property string $to |
|||
* @property string $account |
|||
*/ |
|||
class Transfer extends Message |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'transfer_customer_service'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'account', |
|||
]; |
|||
|
|||
/** |
|||
* Transfer constructor. |
|||
* |
|||
* @param string|null $account |
|||
*/ |
|||
public function __construct(string $account = null) |
|||
{ |
|||
parent::__construct(compact('account')); |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
return empty($this->get('account')) ? [] : [ |
|||
'TransInfo' => [ |
|||
'KfAccount' => $this->get('account'), |
|||
], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Video. |
|||
* |
|||
* @property string $video |
|||
* @property string $title |
|||
* @property string $media_id |
|||
* @property string $description |
|||
* @property string $thumb_media_id |
|||
*/ |
|||
class Video extends Media |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'video'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'title', |
|||
'description', |
|||
'media_id', |
|||
'thumb_media_id', |
|||
]; |
|||
|
|||
/** |
|||
* Video constructor. |
|||
* |
|||
* @param string $mediaId |
|||
* @param array $attributes |
|||
*/ |
|||
public function __construct(string $mediaId, array $attributes = []) |
|||
{ |
|||
parent::__construct($mediaId, 'video', $attributes); |
|||
} |
|||
|
|||
public function toXmlArray() |
|||
{ |
|||
return [ |
|||
'Video' => [ |
|||
'MediaId' => $this->get('media_id'), |
|||
'Title' => $this->get('title'), |
|||
'Description' => $this->get('description'), |
|||
], |
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Messages; |
|||
|
|||
/** |
|||
* Class Voice. |
|||
* |
|||
* @property string $media_id |
|||
*/ |
|||
class Voice extends Media |
|||
{ |
|||
/** |
|||
* Messages type. |
|||
* |
|||
* @var string |
|||
*/ |
|||
protected $type = 'voice'; |
|||
|
|||
/** |
|||
* Properties. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected $properties = [ |
|||
'media_id', |
|||
'recognition', |
|||
]; |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Providers; |
|||
|
|||
use EasyWeChat\Kernel\Config; |
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
|
|||
/** |
|||
* Class ConfigServiceProvider. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class ConfigServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* Registers services on the given container. |
|||
* |
|||
* This method should only be used to configure services and parameters. |
|||
* It should not get services. |
|||
* |
|||
* @param Container $pimple A container instance |
|||
*/ |
|||
public function register(Container $pimple) |
|||
{ |
|||
$pimple['config'] = function ($app) { |
|||
return new Config($app->getConfig()); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/wechat. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace EasyWeChat\Kernel\Providers; |
|||
|
|||
use Pimple\Container; |
|||
use Pimple\ServiceProviderInterface; |
|||
use Symfony\Component\EventDispatcher\EventDispatcher; |
|||
|
|||
/** |
|||
* Class EventDispatcherServiceProvider. |
|||
* |
|||
* @author mingyoung <mingyoungcheung@gmail.com> |
|||
*/ |
|||
class EventDispatcherServiceProvider implements ServiceProviderInterface |
|||
{ |
|||
/** |
|||
* Registers services on the given container. |
|||
* |
|||
* This method should only be used to configure services and parameters. |
|||
* It should not get services. |
|||
* |
|||
* @param Container $pimple A container instance |
|||
*/ |
|||
public function register(Container $pimple) |
|||
{ |
|||
$pimple['events'] = function ($app) { |
|||
$dispatcher = new EventDispatcher(); |
|||
|
|||
foreach ($app->config->get('events.listen', []) as $event => $listeners) { |
|||
foreach ($listeners as $listener) { |
|||
$dispatcher->addListener($event, $listener); |
|||
} |
|||
} |
|||
|
|||
return $dispatcher; |
|||
}; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue