├── cache
├── .gitignore
└── README.md
├── .gitignore
├── example
├── example.png
└── cli.php
├── phpunit.xml
├── src
├── Exception
│ ├── IOException.php
│ ├── ImageUploaderException.php
│ ├── RequestException.php
│ ├── RequirePinException.php
│ ├── BadResponseException.php
│ └── RuntimeException.php
├── functions_include.php
├── functions.php
└── Client.php
├── composer.json
├── LICENSE
└── README.md
/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !README.md
4 |
--------------------------------------------------------------------------------
/cache/README.md:
--------------------------------------------------------------------------------
1 | Default FileSystem cache store directory.
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor
3 | tags
4 | .DS_Store
5 | .*.sw*
6 | .*.un~
7 |
--------------------------------------------------------------------------------
/example/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/consatan/weibo_image_uploader/HEAD/example/example.png
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests/
5 |
6 |
7 |
8 | src/
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Exception/IOException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | class IOException extends RuntimeException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/ImageUploaderException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | interface ImageUploaderException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/RequestException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | class RequestException extends RuntimeException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/RequirePinException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | class RequirePinException extends RuntimeException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/BadResponseException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | class BadResponseException extends RuntimeException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader\Exception;
13 |
14 | class RuntimeException extends \RuntimeException implements ImageUploaderException
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/functions_include.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | // Don't redefine the functions if included multiple times.
13 | if (!function_exists('Consatan\Weibo\ImageUploader\rsa_pkey')) {
14 | require __DIR__ . '/functions.php';
15 | }
16 |
--------------------------------------------------------------------------------
/example/cli.php:
--------------------------------------------------------------------------------
1 | upload('./example.png', $username, $password);
15 | break;
16 | } catch (Consatan\Weibo\ImageUploader\Exception\RequirePinException $e) {
17 | echo '验证码图片位置:' . $e->getMessage() . PHP_EOL . '输入验证码以继续:';
18 | if (!$client->login($username, $password, stream_get_line(STDIN, 1024, PHP_EOL))) {
19 | echo '登入失败';
20 | break;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "consatan/weibo_image_uploader",
3 | "description": "Weibo image uploader.",
4 | "keywords": ["sina", "weibo", "image", "photo", "picture", "upload", "uploader"],
5 | "homepage": "https://chopin.im/weibo_puploader",
6 | "type": "library",
7 | "license": "BSD-3-Clause",
8 | "authors": [
9 | {
10 | "name": "Chopin Ngo",
11 | "email": "consatan@gmail.com",
12 | "homepage": "https://chopin.im"
13 | }
14 | ],
15 | "require": {
16 | "php": ">=7.0",
17 | "ext-json": "*",
18 | "ext-openssl": "*",
19 | "guzzlehttp/guzzle": "^6.2",
20 | "symfony/cache": "^3.2"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "^5.7",
24 | "mockery/mockery": "^0.9.7"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Consatan\\Weibo\\ImageUploader\\": "src/"
29 | },
30 | "files": ["src/functions_include.php"]
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Consatan\\Weibo\\ImageUploader\\Tests\\": "tests/"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Chopin Ngo
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader;
13 |
14 | /**
15 | * asn1_length
16 | *
17 | * @param int $length
18 | * @return string
19 | * @link https://github.com/yangyuan/weibo-publisher/blob/520dbc24f775db8caa3b48ea6dbbc838e5142850/weibo.php#L92
20 | */
21 | function asn1_length(int $length): string
22 | {
23 | if ($length <= 0x7f) {
24 | return chr($length);
25 | }
26 |
27 | $tmp = ltrim(pack('N', $length), chr(0));
28 | return pack('Ca*', 0x80 | strlen($tmp), $tmp);
29 | }
30 |
31 | /**
32 | * rsa_pkey
33 | *
34 | * @param string $exponent
35 | * @param string $modulus
36 | * @return string
37 | * @link https://github.com/yangyuan/weibo-publisher/blob/520dbc24f775db8caa3b48ea6dbbc838e5142850/weibo.php#L99
38 | */
39 | function rsa_pkey(string $exponent, string $modulus): string
40 | {
41 | $pkey = pack('Ca*a*', 0x02, asn1_length(strlen($modulus)), $modulus)
42 | . pack('Ca*a*', 0x02, asn1_length(strlen($exponent)), $exponent);
43 |
44 | $pkey = pack('Ca*a*', 0x30, asn1_length(strlen($pkey)), $pkey);
45 | $pkey = pack('Ca*', 0x00, $pkey);
46 | $pkey = pack('Ca*a*', 0x03, asn1_length(strlen($pkey)), $pkey);
47 | $pkey = pack('H*', '300d06092a864886f70d0101010500') . $pkey;
48 | $pkey = pack('Ca*a*', 0x30, asn1_length(strlen($pkey)), $pkey);
49 |
50 | return "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(base64_encode($pkey)) . '-----END PUBLIC KEY-----';
51 | }
52 |
53 | /**
54 | * rsa_encrypt
55 | *
56 | * @param string $message
57 | * @param string $exponent
58 | * @param string $pubkey
59 | * @return string
60 | * @link https://github.com/yangyuan/weibo-publisher/blob/520dbc24f775db8caa3b48ea6dbbc838e5142850/weibo.php#L114
61 | */
62 | function rsa_encrypt(string $message, string $exponent, string $pubkey): string
63 | {
64 | openssl_public_encrypt($message, $result, rsa_pkey(hex2bin($exponent), hex2bin($pubkey)), OPENSSL_PKCS1_PADDING);
65 | return $result;
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### PHP 实现的微博图床上传轮子
2 |
3 | #### 安装
4 |
5 | ##### 要求
6 |
7 | - PHP 7.0 以上版本
8 | - json 扩展
9 | - openssl 扩展
10 |
11 | PHP **5.5, 5.6** 版本请使用 [0.5 版本](https://github.com/consatan/weibo_image_uploader/tree/0.5)
12 |
13 | 使用 composer (推荐)
14 |
15 | ```shell
16 | composer require consatan/weibo_image_uploader
17 | ```
18 |
19 | 从 Github 上下载
20 |
21 | ```shell
22 | git clone https://consatan.github.com/weibo_image_uploader.git
23 | ```
24 |
25 | #### 使用示例
26 |
27 | ```php
28 | useHttps(false);
36 |
37 | // 上传示例图片
38 | $url = $weibo->upload('./example.jpg', '微博帐号', '微博帐号密码');
39 |
40 | // 输出新浪图床 URL
41 | echo $url . PHP_EOL;
42 | ```
43 |
44 | #### 使用说明
45 |
46 | 构造函数可传递 `\Psr\Cache\CacheItemPoolInterface` 和 `\GuzzleHttp\ClientInterface`,默认情况下使用文件缓存 cookie 信息,存储在项目根目录的 cache/weibo 文件夹下,缓存的 `key` 使用 `md5` 后的微博用户名,可根据需求将缓存保存到其他适配器中,具体参见 `\Symfony\Cache\Adapter`。
47 |
48 | > 关于验证码问题([issue #3](https://github.com/consatan/weibo_image_uploader/issues/3)),可查看 [example/cli.php](https://github.com/consatan/weibo_image_uploader/tree/master/example/cli.php) 示例代码
49 |
50 | `Client::upload` 方法的第四个参数允许传递 `Guzzle request` 的参数数组,具体见 [Request Options](http://docs.guzzlephp.org/en/latest/request-options.html),通过该参数可实现切换代理等操作,如下例:
51 |
52 | ```php
53 | upload('./example.jpg', '微博帐号1', '密码');
57 | // 同一用户名只有第一次上传需要登入,之后使用缓存的登入 cookie 进行上传
58 | // 如果使用 cookie 上传失败,将尝试重新登入一次,还是失败的话抛出异常
59 | // 除非使用的是无法持久化保存的缓存适配器(如 ArrayAdapter)
60 | // 否则以后同一用户名都将使用缓存的 cookie 进行登入
61 | // echo $weibo->upload('./example.jpg', '微博帐号1', '密码');
62 |
63 | // resource
64 | $url2 = $weibo->upload(fopen('./example.jpg', 'r'), '微博帐号2', '密码', [
65 | 'proxy' => 'http://192.168.1.100:8080'
66 | ]);
67 |
68 | // 字符串
69 | $url3 = $weibo->upload(file_get_contents('./example.jpg'), '微博帐号3', '密码', [
70 | 'proxy' => 'http://192.168.1.200:8090'
71 | ]);
72 |
73 | // \Psr\Http\Message\StreamInterface
74 | $url4 = $weibo->upload(\GuzzleHttp\Psr7\stream_for(file_get_contents('./example.jpg')), '微博帐号4', '密码', [
75 | 'proxy' => 'http://192.168.1.250:9080'
76 | ]);
77 | ```
78 |
79 | ##### 水印选项
80 | ```php
81 | // 开启水印
82 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', ['mark' => true]);
83 |
84 | // 水印位置
85 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', [
86 | 'mark' => true,
87 | 'markpos' => Consatan\Weibo\ImageUploader\Client::MARKPOS_BOTTOM_CENTER,
88 | ]);
89 |
90 | // 可使用任意的水印暱称(也许以后的版本就会和谐了)
91 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', ['mark' => true, 'nickname' => '任意暱称']);
92 | ```
93 |
94 | ##### 获取其他尺寸的图片链接
95 | ```php
96 | // 默认使用 large (原始)尺寸,此处使用 thumbnail (缩略图) 尺寸
97 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', [
98 | 'size' => Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_THUMBNAIL
99 | ]);
100 |
101 | // 获取多个尺寸的图片链接
102 | $urls = $weibo->upload('./example.jpg', '微博帐号', '密码', ['size' => [
103 | Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_SMALL,
104 | Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_LARGE,
105 | Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_THUMBNAIL,
106 | ]]);
107 | // 返回
108 | // array (
109 | // 'small' => 'https://ws2.sinaimg.cn/small/0068M0xKgy1fetd4l7x6vj30bo0bximx.jpg',
110 | // 'large' => 'https://ws2.sinaimg.cn/large/0068M0xKgy1fetd4l7x6vj30bo0bximx.jpg',
111 | // 'thumbnail' => 'https://ws2.sinaimg.cn/thumbnail/0068M0xKgy1fetd4l7x6vj30bo0bximx.jpg',
112 | // )
113 | ```
114 |
115 | ##### upload 方法中,$config 和 $option 参数位置可调换
116 | ```php
117 | // 以下 2 种传参顺序最终效果是一致的
118 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', [
119 | 'size' => Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_THUMBNAIL
120 | ], [
121 | 'proxy' => 'http://192.168.1.250:9080'
122 | ]);
123 |
124 | $url = $weibo->upload('./example.jpg', '微博帐号', '密码', [
125 | 'proxy' => 'http://192.168.1.250:9080'
126 | ], [
127 | 'size' => Consatan\Weibo\ImageUploader\Client::IMAGE_SIZE_THUMBNAIL
128 | ]);
129 | ```
130 |
131 | 抛出的所有异常都可通过 `\Consatan\Weibo\ImageUploader\Exception\ImageUploaderException` 接口捕获, 实现该接口的异常都在 [src/Exception](https://github.com/consatan/weibo_image_uploader/tree/master/src/Exception) 目录下。
132 |
133 | #### Todo
134 |
135 | - [ ] 单元测试
136 | - [x] 获取其他规格的图片 URL(如,small, thumbnail...)
137 | - [x] 添加水印选项
138 | - [x] 实现验证码输入(用户输入)
139 |
140 | #### 参考
141 |
142 | - [微博官方简易发布器](http://weibo.com/minipublish)
143 | - [微博官方图片上传js](http://js.t.sinajs.cn/t5/home/js/page/content/simplePublish.js)
144 | - [WeiboPicBed](https://github.com/Suxiaogang/WeiboPicBed/blob/master/js/popup.js)
145 | - [超详细的Python实现新浪微博模拟登陆(小白都能懂)](http://www.jianshu.com/p/816594c83c74)
146 | - [调用网页接口实现发微博(PHP实现)](http://andrewyang.cn/post.php?id=1034)
147 | - [weibo-publisher](https://github.com/yangyuan/weibo-publisher)
148 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Consatan\Weibo\ImageUploader;
13 |
14 | use GuzzleHttp\Middleware;
15 | use GuzzleHttp\HandlerStack;
16 | use GuzzleHttp\ClientInterface;
17 | use GuzzleHttp\Cookie\CookieJar;
18 | use GuzzleHttp\Handler\CurlHandler;
19 | use GuzzleHttp\Client as HttpClient;
20 | use GuzzleHttp\Cookie\CookieJarInterface;
21 | use GuzzleHttp\Exception\GuzzleException;
22 | use Psr\Cache\CacheItemPoolInterface;
23 | use Symfony\Component\Cache\Adapter\FilesystemAdapter;
24 | use Psr\Http\Message\StreamInterface;
25 | use Consatan\Weibo\ImageUploader\Exception\IOException;
26 | use Consatan\Weibo\ImageUploader\Exception\RequestException;
27 | use Consatan\Weibo\ImageUploader\Exception\BadResponseException;
28 | use Consatan\Weibo\ImageUploader\Exception\RuntimeException;
29 | use Consatan\Weibo\ImageUploader\Exception\RequirePinException;
30 | use Consatan\Weibo\ImageUploader\Exception\ImageUploaderException;
31 |
32 | /**
33 | * Class Client
34 | *
35 | * @static string getImageUrl(
36 | * string $pid,
37 | * string $size = self::IMAGE_SIZE_ORIGNAL,
38 | * bool $https = true
39 | * )
40 | * @method self __construct(
41 | * \Psr\Cache\CacheItemPoolInterface $cache = null,
42 | * \GuzzleHttp\ClientInterface $http = null
43 | * )
44 | * @method self setNickname(string $nickname = '')
45 | * @method string getNickname()
46 | * @method self setMark(bool $mark)
47 | * @method bool getMark()
48 | * @method self setMarkPos(int $pos)
49 | * @method int getMarkPos()
50 | * @method self setImageSizes(string|string[] $sizes)
51 | * @method string[] getImageSizes()
52 | * @method self useHttps(bool $https = true)
53 | * @method self setHttps(bool $https = true)
54 | * @method bool login(string $username, string $password, bool|string $cache = true)
55 | * @method string upload(
56 | * string|resource|\Psr\Http\Message\StreamInterface $file,
57 | * string $username = '',
58 | * string $password = '',
59 | * array $config = [],
60 | * array $option = []
61 | * )
62 | */
63 | class Client
64 | {
65 | const IMAGE_SIZE_LARGE = 'large';
66 |
67 | const IMAGE_SIZE_SMALL = 'small';
68 |
69 | const IMAGE_SIZE_SQUARE = 'square';
70 |
71 | const IMAGE_SIZE_MIDDLE = 'bmiddle';
72 |
73 | const IMAGE_SIZE_ORIGNAL = 'large';
74 |
75 | const IMAGE_SIZE_BMIDDLE = 'bmiddle';
76 |
77 | const IMAGE_SIZE_THUMBNAIL = 'thumbnail';
78 |
79 | const IMAGE_SIZE_THUMB180 = 'thumb180';
80 |
81 | const IMAGE_SIZE_MW690 = 'mw690';
82 |
83 | const IMAGE_SIZE_MW1024 = 'mw1024';
84 |
85 | /**
86 | * 水印位置,图片右下角位置
87 | *
88 | * @var int
89 | */
90 | const MARKPOS_BOTTOM_RIGHT = 1;
91 |
92 | /**
93 | * 水印位置,图片底部中间位置
94 | *
95 | * @var int
96 | */
97 | const MARKPOS_BOTTOM_CENTER = 2;
98 |
99 | /**
100 | * 水印位置,图片中心位置
101 | *
102 | * @var int
103 | */
104 | const MARKPOS_CENTER = 3;
105 |
106 | /**
107 | * 允许的图片尺寸
108 | *
109 | * @var string[]
110 | */
111 | public static $imageSize = [
112 | 'mw690' => 'mw690',
113 | 'large' => 'large',
114 | 'small' => 'small',
115 | 'square' => 'square',
116 | 'mw1024' => 'mw1024',
117 | 'middle' => 'bmiddle',
118 | 'orignal' => 'large',
119 | 'bmiddle' => 'bmiddle',
120 | 'thumb180' => 'thumb180',
121 | 'thumbnail' => 'thumbnail',
122 | ];
123 |
124 | /**
125 | * http 实例
126 | *
127 | * @var \GuzzleHttp\ClientInterface
128 | */
129 | protected $http;
130 |
131 | /**
132 | * cache 实例
133 | *
134 | * @var \Psr\Cache\CacheItemPoolInterface
135 | */
136 | protected $cache;
137 |
138 | /**
139 | * cookie 实例
140 | *
141 | * @var \GuzzleHttp\Cookie\CookieJarInterface
142 | */
143 | protected $cookie;
144 |
145 | /**
146 | * 返回的图片 URL 协议,https 或 http
147 | *
148 | * @var string
149 | */
150 | protected $protocol = 'https';
151 |
152 | /**
153 | * User-Agent
154 | *
155 | * @var string
156 | */
157 | protected $ua = '';
158 |
159 | /**
160 | * 微博帐号
161 | *
162 | * @var string
163 | */
164 | protected $username = '';
165 |
166 | /**
167 | * 微博密码
168 | *
169 | * @var string
170 | */
171 | protected $password = '';
172 |
173 | /**
174 | * 是否添加水印
175 | *
176 | * @var bool
177 | */
178 | protected $mark = false;
179 |
180 | /**
181 | * 水印位置
182 | *
183 | * @var int
184 | */
185 | protected $markpos = self::MARKPOS_BOTTOM_RIGHT;
186 |
187 | /**
188 | * 微博暱称
189 | *
190 | * @var string
191 | */
192 | protected $nickname = '';
193 |
194 | /**
195 | * 要获取的图片尺寸
196 | *
197 | * @var string[]
198 | */
199 | protected $imageSizes = [self::IMAGE_SIZE_LARGE];
200 |
201 | /**
202 | * 微博暱称缓存,由于微博暱称允许更改,所以该缓存不持久化
203 | *
204 | * @var array
205 | */
206 | protected $nicknames = [];
207 |
208 | /**
209 | * @param \Psr\Cache\CacheItemPoolInterface $cache (null) Cache 实例
210 | * 未设置默认使用文件缓存,保存在项目根路径的 cache/weibo 目录。
211 | * @param \GuzzleHttp\ClientInterface $http (null) Guzzle client 实例
212 | *
213 | * @return self
214 | */
215 | public function __construct(CacheItemPoolInterface $cache = null, ClientInterface $http = null)
216 | {
217 | $this->cookie = new CookieJar();
218 | $this->cache = null !== $cache ? $cache : new FilesystemAdapter('weibo', 0, __DIR__ . '/../cache');
219 |
220 | $ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:45.0) Gecko/20100101 Firefox/45.0';
221 | if (null !== $http) {
222 | $this->http = $http;
223 | $header = $http->getConfig('headers');
224 | // 如果是默认 UA 替换成模拟的 UA
225 | if (0 === strpos($header['User-Agent'], 'GuzzleHttp')) {
226 | $this->ua = $ua;
227 | }
228 |
229 | if (($cookie = $http->getConfig('cookies')) instanceof CookieJarInterface) {
230 | $this->cookie = $cookie;
231 | }
232 | } else {
233 | $this->http = new HttpClient(['headers' => ['User-Agent' => $ua]]);
234 | }
235 | }
236 |
237 | /**
238 | * 获取图片链接
239 | *
240 | * @param string $pid 微博图床pid,或者微博图床链接。传递的是链接的话,
241 | * 仅是将链接的尺寸更改为目标尺寸而已。
242 | * @param string $size (self::IMAGE_SIZE_LARGE) 图片尺寸
243 | * @param bool $https (true) 是否使用 https 协议
244 | *
245 | * @return string 图片链接
246 | *
247 | * @throws Consatan\Weibo\ImageUploader\Exception\RuntimeException
248 | * 当 $pid 既不是 pid 也不是合法的微博图床链接时
249 | */
250 | public static function getImageUrl(string $pid, string $size = self::IMAGE_SIZE_ORIGNAL, bool $https = true)
251 | {
252 | $pid = trim($pid);
253 | $size = strtolower($size);
254 | $size = isset(self::$imageSize[$size]) ? $size : self::IMAGE_SIZE_ORIGNAL;
255 |
256 | // 传递 pid
257 | if (preg_match('/^[a-zA-Z0-9]{32}$/', $pid) === 1) {
258 | return ($https ? 'https' : 'http') . '://' . ($https ? 'ws' : 'ww')
259 | . ((crc32($pid) & 3) + 1) . ".sinaimg.cn/" . self::$imageSize[$size]
260 | . "/$pid." . ($pid[21] === 'g' ? 'gif' : 'jpg');
261 | }
262 |
263 | // 传递 url
264 | $url = $pid;
265 | $imgUrl = preg_replace_callback('/^(https?:\/\/[a-z]{2}\d\.sinaimg\.cn\/)'
266 | . '(large|bmiddle|mw1024|mw690|small|square|thumb180|thumbnail)'
267 | . '(\/[a-z0-9]{32}\.(jpg|gif))$/i', function ($match) use ($size) {
268 | return $match[1] . self::$imageSize[$size] . $match[3];
269 | }, $url, -1, $count);
270 |
271 | if ($count === 0) {
272 | throw new RuntimeException('Invalid URL: ' . $url);
273 | }
274 | return $imgUrl;
275 | }
276 |
277 | /**
278 | * 设置(水印中的)微博用户暱称,当前版本允许自定义水印微博用户暱称,
279 | * 以后版本该功能可能会被和谐。
280 | *
281 | * @param string $nickname ('') 微博用户暱称
282 | * @return self
283 | */
284 | public function setNickname(string $nickname = '')
285 | {
286 | $nickname = trim($nickname);
287 | if ($nickname === '') {
288 | if (isset($this->nicknames[$this->username])) {
289 | $nickname = $this->nicknames[$this->username];
290 | } else {
291 | $this->request('http://weibo.com/minipublish', function (string $content) use (&$nickname) {
292 | if (preg_match('/\$CONFIG\[\'nick\'\]\s*=\s*\'(.*)\'\s*;/m', $content, $match) === 1) {
293 | $nickname = trim($match[1]);
294 | $this->nicknames[$this->username] = $nickname;
295 | }
296 | });
297 | }
298 | }
299 | $this->nickname = $nickname;
300 | return $this;
301 | }
302 |
303 | /**
304 | * 获取微博用户暱称
305 | *
306 | * @return string 微博用户暱称
307 | */
308 | public function getNickname()
309 | {
310 | return $this->nickname;
311 | }
312 |
313 | /**
314 | * 设置水印开关
315 | *
316 | * @param bool $mark true 开启水印,false 关闭水印
317 | *
318 | * @return self
319 | */
320 | public function setMark(bool $mark)
321 | {
322 | $this->mark = $mark;
323 | return $this;
324 | }
325 |
326 | /**
327 | * 获取水印开关
328 | *
329 | * @return bool
330 | */
331 | public function getMark()
332 | {
333 | return $this->mark;
334 | }
335 |
336 | /**
337 | * 设置水印位置
338 | *
339 | * @param int $pos 水印位置
340 | *
341 | * @return self
342 | */
343 | public function setMarkPos(int $pos)
344 | {
345 | $this->markpos = $pos >= 1 && $pos <= 3 ? $pos : self::MARKPOS_BOTTOM_RIGHT;
346 | return $this;
347 | }
348 |
349 | /**
350 | * 获取水印位置
351 | *
352 | * @return int
353 | */
354 | public function getMarkPos()
355 | {
356 | return $this->markpos;
357 | }
358 |
359 | /**
360 | * 设置图片尺寸
361 | *
362 | * @param string[]|string 图片尺寸
363 | *
364 | * @return self
365 | */
366 | public function setImageSizes($sizes)
367 | {
368 | if (is_string($sizes)) {
369 | $sizes = [$sizes];
370 | }
371 |
372 | $this->imageSizes = [];
373 | if (is_array($sizes)) {
374 | foreach ($sizes as $size) {
375 | if (isset(self::imageSize[$size])) {
376 | $this->imageSizes[] = $size;
377 | }
378 | }
379 | }
380 |
381 | if (sizeof($this->imageSizes) === 0) {
382 | $this->imageSizes = [self::IMAGE_SIZE_LARGE];
383 | }
384 | return $this;
385 | }
386 |
387 | /**
388 | * 获取图片尺寸
389 | *
390 | * @return array
391 | */
392 | public function getImageSizes()
393 | {
394 | return $this->imageSizes;
395 | }
396 |
397 | /**
398 | * 图床 URL 使用 https 协议
399 | *
400 | * @param bool $https (true) 默认使用 https,设置为 false 使用 http
401 | *
402 | * @return self
403 | */
404 | public function useHttps(bool $https = true): self
405 | {
406 | $this->protocol = $https ? 'https' : 'http';
407 | return $this;
408 | }
409 |
410 | /**
411 | * $this->useHttps() 的别名
412 | *
413 | * @see $this->useHttps()
414 | */
415 | public function setHttps(bool $https = true): self
416 | {
417 | return $this->useHttps($https);
418 | }
419 |
420 | /**
421 | * 上传图片
422 | *
423 | * @param mixed $file 要上传的文件,可以是文件路径、文件内容(字符串)、文件资源句柄
424 | * 或者实现了 \Psr\Http\Message\StreamInterface 接口的实体类。
425 | * @param string $username ('') 微博帐号
426 | * @param string $password ('') 微博密码
427 | * @param array $config ([]) 图片上传参数,该参数和 $option 参数位置可调换
428 | * - mark: (bool, default=false) 图片水印开关
429 | * - markpos: (int, default=1) 图片水印位置,仅开启图片水印时有效
430 | * 1: 图片右下角
431 | * 2: 图片底部居中位置
432 | * 3: 图片垂直和水平居中位置
433 | * - nickname: (string) 水印上的暱称,默认使用上传的微博帐号暱称,当前该参数
434 | * 允许自定义暱称,但不保证以后版本中会被微博屏蔽掉。仅开启图片水印时有效
435 | * - size: (string[], default=['large']) 获取不同尺寸的图片链接。当仅需要一个
436 | * 尺寸时,返回图片 URL;当需要多个尺寸时,返回索引数组,key 为尺寸,
437 | * value 为对应尺寸的图片 URL
438 | * @param array $option ([]) 具体见 Guzzle request 的请求参数说明
439 | *
440 | * @return string|array 上传成功返回对应的图片 URL,上传失败返回空字符串。
441 | * 当 $config['size'] 需要多个尺寸的图片 URL 时,返回索引数组,key 为尺寸,
442 | * value 为对应尺寸的图片 URL,当上传失败时返回空数组。
443 | *
444 | * @throws \Consatan\Weibo\ImageUploader\Exception\IOException 读取上传文件失败时
445 | * @throws \Consatan\Weibo\ImageUploader\Exception\RuntimeException 参数类型错误时
446 | * @throws \Consatan\Weibo\ImageUploader\Exception\BadResponseException 登入失败时
447 | * @throws \Consatan\Weibo\ImageUploader\Exception\RequestException 请求失败时
448 | *
449 | * @see http://docs.guzzlephp.org/en/latest/request-options.html
450 | */
451 | public function upload(
452 | $file,
453 | string $username = '',
454 | string $password = '',
455 | array $config = [],
456 | array $option = []
457 | ) {
458 | $img = $file;
459 | $imgUrl = '';
460 |
461 | if (is_string($file)) {
462 | // 如果是文件路径,根据文件路径获取文件句柄
463 | if (file_exists($file) && false === ($img = @fopen($file, 'r'))) {
464 | throw new IOException("无法读取文件 $file.");
465 | }
466 | } else {
467 | if (!is_resource($file) && !($file instanceof StreamInterface)) {
468 | throw new RuntimeException('Upload `$file` MUST a type of string or resource '
469 | . 'or instance of \Psr\Http\Message\StreamInterface, '
470 | . gettype($file) . ' given.');
471 | }
472 | }
473 |
474 | // 如果有提供用户名密码的话,从缓存中获取登入 cookie
475 | if ('' !== $username && '' !== $password && !$this->login($username, $password, true)) {
476 | // 登入失败
477 | throw new BadResponseException('登入失败,请检查用户名或密码是否正确');
478 | }
479 |
480 | $header = [
481 | 'Referer' => 'http://weibo.com/minipublish',
482 | 'Accept' => 'text/html, application/xhtml+xml, image/jxr, */*',
483 | ];
484 |
485 | // 允许 $config 和 $option 参数位置调换
486 | if (!empty($config) && !isset($config['mark']) && !isset($config['markpos'])
487 | && !isset($config['nickname']) && !isset($config['size'])) {
488 | $tmp = $config;
489 | $config = $option;
490 | $option = $tmp;
491 | }
492 |
493 | if (!empty($option) && (isset($option['mark']) || isset($option['markpos'])
494 | || isset($option['nickname']) || isset($option['size']))) {
495 | $tmp = $config;
496 | $config = $option;
497 | $option = $tmp;
498 | }
499 |
500 | if (!empty($option)) {
501 | if (isset($option['headers'])) {
502 | foreach ($option['headers'] as $key => $val) {
503 | $name = strtolower($key);
504 | // 删除 headers 中用户自定义的必须参数
505 | if ('referer' === $name || 'accept' === $name) {
506 | unset($option['headers'][$key]);
507 | }
508 | $header[$key] = $val;
509 | }
510 | }
511 |
512 | // 删除不允许修改的参数 或 不能和 multipart 一起使用的参数
513 | unset($option['json'], $option['body'], $option['form_params'], $option['handler']);
514 | unset($option['query'], $option['allow_redirects'], $option['multipart'], $option['headers']);
515 | }
516 |
517 | // 创建重试中间件
518 | $stack = HandlerStack::create(new CurlHandler());
519 | $stack->push(Middleware::retry(function ($retries, $req, $rsp, $error) use (&$imgUrl, &$config) {
520 | $imgUrl = '';
521 | if ($rsp !== null) {
522 | $statusCode = $rsp->getStatusCode();
523 |
524 | if (300 <= $statusCode && 303 >= $statusCode && !empty(($url = $rsp->getHeader('Location')))) {
525 | $url = $url[0];
526 | if (false !== ($query = parse_url($url, PHP_URL_QUERY))) {
527 | parse_str($query, $pid);
528 | if (isset($pid['pid'])) {
529 | $pid = $pid['pid'];
530 | /**
531 | * pid 相关信息查看下面链接,可通过搜索 crc32 查看相关代码
532 | * @link http://js.t.sinajs.cn/t5/home/js/page/content/simplePublish.js
533 | *
534 | * 根据上面 js 文件代码来看,cdn 的编号应该由以下代码来决定
535 | * (($pid[9] === 'w' ? (crc32($pid) & 3) : (hexdec(substr($pid, 19, 2)) & 0xf)) + 1)
536 | * 然而当前能访问的 cdn 编号只有 1 ~ 4,而且基本上任意的
537 | * cdn 编号都能访问到同一资源,所以根据 pid 来判断 cdn 编号
538 | * 当前实际上没啥意义了,有些实现甚至直接写死 cdn 编号
539 | */
540 | $imgUrl = self::getImageUrl($pid, $config['size'][0], $this->protocol === 'https');
541 |
542 | // 停止重试
543 | return false;
544 | }
545 | }
546 | }
547 | }
548 |
549 | // 上传失败,进行重试判断,$retries 参数由 0 开始
550 | if ($retries === 0) {
551 | // 进行非缓存登入
552 | if (!$this->login($this->username, $this->password, false)) {
553 | // 如果非缓存登入失败,抛出异常
554 | throw new BadResponseException('登入失败,请检查用户名或密码是否正确');
555 | }
556 |
557 | // 重试上传
558 | return true;
559 | } else {
560 | // 已是第二次上传失败,停止重试
561 | return false;
562 | }
563 | }));
564 |
565 | $config = array_merge([
566 | 'mark' => $this->mark,
567 | 'markpos' => $this->markpos,
568 | 'nickname' => $this->nickname,
569 | 'size' => $this->imageSizes,
570 | ], $config);
571 | if (is_string($config['size'])) {
572 | $config['size'] = [$config['size']];
573 | }
574 |
575 | $option = array_merge($option, [
576 | 'handler' => $stack,
577 | 'query' => [
578 | 'ori' => '1',
579 | 'marks' => '1',
580 | 'app' => 'miniblog',
581 | 's' => 'rdxt',
582 | 'markpos' => $config['mark'] ? $config['markpos'] : '',
583 | 'logo' => '',
584 | 'nick' => '@' . $config['nickname'],
585 | 'url' => '',
586 | 'cb' => 'http://weibo.com/aj/static/upimgback.html?_wv=5&callback=STK_ijax_'
587 | . substr(strval(microtime(true) * 1000), 0, 13) . '1',
588 | ],
589 | 'multipart' => [[
590 | 'name' => 'pic1',
591 | 'contents' => $img,
592 | ]],
593 | 'headers' => $header,
594 | // 使用常规上传,将重定向到 query 里的 cb URL
595 | // pid 已包含在 URL 里,故毋须进行重定向
596 | 'allow_redirects' => false,
597 | ]);
598 |
599 | $this->applyOption($option);
600 |
601 | try {
602 | $this->http->request('POST', 'http://picupload.service.weibo.com/interface/pic_upload.php', $option);
603 | } catch (GuzzleException $e) {
604 | throw new RequestException('请求失败. ' . $e->getMessage(), $e->getCode(), $e);
605 | }
606 |
607 | if (sizeof($config['size']) > 1) {
608 | $imgs = [];
609 |
610 | foreach ($config['size'] as $size) {
611 | $imgs[$size] = self::getImageUrl($imgUrl, $size);
612 | }
613 | return $imgs;
614 | } else {
615 | return $imgUrl;
616 | }
617 | }
618 |
619 | /**
620 | * 模拟登入微博,以获取登入信息 cookie。
621 | *
622 | * @param string $username 微博帐号,微博帐号的 md5 值将作为缓存 key
623 | * @param string $password 微博密码
624 | * @param bool|string $cache (true) 是否使用缓存的cookie进行登入,如果缓存不存在则创建;
625 | * 当传入的是字符串时,该参数为验证码,不使用缓存登入。
626 | *
627 | * @return bool 登入成功与否
628 | *
629 | * @throws \Consatan\Weibo\ImageUploader\Exception\RequirePinException 需要输入验证码时,
630 | * Exception message 为验证码图片的本地路径
631 | * @throws \Consatan\Weibo\ImageUploader\Exception\IOException 缓存持久化失败时
632 | */
633 | public function login(string $username, string $password, $cache = true): bool
634 | {
635 | $this->password = $password;
636 | $this->username = trim($username);
637 | $cacheKey = md5($this->username);
638 |
639 | if (is_string($cache)) {
640 | $pin = $cache;
641 | $cache = false;
642 | } else {
643 | $pin = '';
644 | $cache = (bool)$cache;
645 | }
646 |
647 | // 如果使用缓存登入且缓存里有对应用户名的缓存cookie的话,则不需要登入操作
648 | if ($cache && ($cookie = $this->cache->getItem($cacheKey)->get()) instanceof CookieJarInterface) {
649 | $this->cookie = $cookie;
650 | $this->setNickname();
651 | return true;
652 | }
653 |
654 | return $this->request($this->ssoLogin($pin), function (string $content) use ($cacheKey) {
655 | if (1 === preg_match('/"\s*result\s*["\']\s*:\s*true\s*/i', $content)) {
656 | $this->persistenceCache($cacheKey, $this->cookie);
657 | $this->setNickname();
658 | return true;
659 | }
660 |
661 | return false;
662 | }, [
663 | // 该请求会返回 302 重定向,所以开启 allow_redirects
664 | 'allow_redirects' => true,
665 | 'headers' => [
666 | 'Referer' => 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)',
667 | ],
668 | ]);
669 | }
670 |
671 | /**
672 | * 获取 SSO 登入信息
673 | *
674 | * @param string $pin ('') 验证码
675 | *
676 | * @return string 返回登入结果的重定向的 URL
677 | *
678 | * @throws \Consatan\Weibo\ImageUploader\Exception\RequirePinException 需要输入验证码时,
679 | * Exception message 为验证码图片的本地路径
680 | * @throws \Consatan\Weibo\ImageUploader\Exception\BadResponseException 响应非预期或未输入验证码时
681 | */
682 | protected function ssoLogin(string $pin = ''): string
683 | {
684 | $params = [];
685 | $pin = trim($pin);
686 | $cacheKey = md5($this->username) . '_preLogin';
687 | if ($this->cache->hasItem($cacheKey)) {
688 | // 从缓存中获取上次 preLogin 的数据
689 | $data = $this->cache->getItem($cacheKey)->get();
690 | if (is_array($data) && isset(
691 | $data['pcid'],
692 | $data['servertime'],
693 | $data['nonce'],
694 | $data['pubkey'],
695 | $data['rsakv'],
696 | $data['pinImgPath']
697 | )) {
698 | if ($pin !== '') {
699 | $params['pcid'] = $data['pcid'];
700 | $params['door'] = $pin;
701 | } else {
702 | if (file_exists($data['pinImgPath'])) {
703 | // 如果已经缓存过验证码图片,就不需要重复获取
704 | throw new RequirePinException($data['pinImgPath']);
705 | }
706 | }
707 | // 删除本地验证码图片
708 | @unlink($data['pinImgPath']);
709 | }
710 | // 删除 prelogin 缓存,如果提供了验证码,则验证码都是一次性的,
711 | // 不管验证成功与否,都没有必要继续缓存;如果没提供验证码,则会
712 | // 抛出 RequirePinException 异常,也就不会执行删除缓存的代码。
713 | $this->cache->deleteItem($cacheKey);
714 | }
715 |
716 | if (empty($params)) {
717 | $data = $this->preLogin();
718 | if (isset($data['showpin']) && (int)$data['showpin']) {
719 | // 要求输入验证码
720 | throw new RequirePinException($this->getPin($data));
721 | }
722 | }
723 |
724 | $msg = "{$data['servertime']}\t{$data['nonce']}\n{$this->password}";
725 |
726 | return $this->request(
727 | 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)',
728 | function (string $content) {
729 | if (1 === preg_match('/location\.replace\s*\(\s*[\'"](.*?)[\'"]\s*\)\s*;/', $content, $match)) {
730 | // 返回重定向URL
731 | if (false !== stripos(($url = trim($match[1])), 'retcode=4049')) {
732 | throw new BadResponseException('登入失败,要求输入验证码');
733 | }
734 | return $url;
735 | } else {
736 | throw new BadResponseException("登入响应非预期结果: $content");
737 | }
738 | },
739 | [
740 | 'headers' => ['Referer' => 'http://weibo.com/login.php'],
741 | 'form_params' => $params + [
742 | 'entry' => 'weibo',
743 | 'gateway' => '1',
744 | 'from' => '',
745 | 'savestate' => '7',
746 | 'useticket' => '1',
747 | 'pagerefer' => '',
748 | 'vsnf' => '1',
749 | 'su' => base64_encode(urlencode($this->username)),
750 | 'service' => 'miniblog',
751 | 'servertime' => $data['servertime'],
752 | 'nonce' => $data['nonce'],
753 | 'pwencode' => 'rsa2',
754 | 'rsakv' => $data['rsakv'],
755 | // 加密用户登入密码
756 | 'sp' => bin2hex(rsa_encrypt($msg, '010001', $data['pubkey'])),
757 | 'sr' => '1440*900',
758 | 'encoding' => 'UTF-8',
759 | // 该参数为加载 preLogin 页面到提交登入表单的间隔时间
760 | // 此处使用 float 是为了兼容 32 位系统
761 | 'prelt' => (int)round((microtime(true) - $data['preloginTime']) * 1000),
762 | 'url' => 'http://weibo.com/ajaxlogin.php?'
763 | . 'framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack',
764 | 'returntype' => 'META'
765 | ],
766 | ],
767 | 'POST'
768 | );
769 | }
770 |
771 | /**
772 | * 登入前获取相关信息操作
773 | *
774 | * @return array 返回登入前信息数组
775 | *
776 | * @throws \Consatan\Weibo\ImageUploader\Exception\BadResponseException 响应非预期时
777 | */
778 | protected function preLogin(): array
779 | {
780 | $ts = microtime(true);
781 | return $this->request(
782 | 'http://login.sina.com.cn/sso/prelogin.php?entry=weibo&callback=sinaSSOController.preloginCallBack&su='
783 | . urlencode(base64_encode(urlencode($this->username)))
784 | . '&rsakt=mod&checkpin=1&client=ssologin.js(v1.4.18)&_='
785 | . substr(strval($ts * 1000), 0, 13),
786 | function (string $content) use ($ts) {
787 | if (1 === preg_match('/^sinaSSOController.preloginCallBack\s*\((.*)\)\s*$/', $content, $match)) {
788 | $json = json_decode($match[1], true);
789 | if (isset($json['nonce'], $json['rsakv'], $json['servertime'], $json['pubkey'])) {
790 | // 记录访问时间戳,登入时 prelt 参数需要用到
791 | $json['preloginTime'] = $ts;
792 | return $json;
793 | }
794 | throw new BadResponseException("PreLogin 响应非预期结果: $match[1]");
795 | } else {
796 | throw new BadResponseException("PreLogin 响应非预期结果: $content");
797 | }
798 | },
799 | ['headers' => ['Referer' => 'http://weibo.com/login.php']]
800 | );
801 | }
802 |
803 | /**
804 | * 获取验证码图片
805 | *
806 | * @param string $pcid preLogin 阶段获取到的 pcid
807 | *
808 | * @return string 验证码图片的本地路径
809 | *
810 | * @throws \Consatan\Weibo\ImageUploader\Exception\IOException 创建或保存验证码图片失败时,
811 | * 或持久化缓存失败时
812 | */
813 | protected function getPin(array $data): string
814 | {
815 | $url = 'http://login.sina.com.cn/cgi/pin.php?r=' . rand(100000000, 99999999) . '&s=0&p=' . $data['pcid'];
816 | $this->request($url, function ($content) use (&$data) {
817 | if (false === ($path = tempnam(sys_get_temp_dir(), 'WEIBO'))) {
818 | throw new IOException('创建验证码图片文件失败');
819 | }
820 |
821 | if (false === file_put_contents($path, $content)) {
822 | throw new IOException('保存验证码图片失败');
823 | }
824 | $data['pinImgPath'] = $path;
825 | }, ['headers' => [
826 | 'Accept' => 'image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5',
827 | 'Referer' => 'http://www.weibo.com/login.php',
828 | ]]);
829 |
830 | $cacheKey = md5($this->username);
831 | // 持久化 preLogin 获取的数据
832 | $this->persistenceCache($cacheKey . '_preLogin', $data);
833 | // 持久化 cookie 保存当前状态
834 | $this->persistenceCache($cacheKey, $this->cookie);
835 |
836 | return $data['pinImgPath'];
837 | }
838 |
839 | /**
840 | * 封装的 HTTP 请求方法
841 | *
842 | * @param string $url 请求 URL
843 | * @param callable $fn 回调函数
844 | * @param array $option ([]) 请求参数,具体见 Guzzle request 的请求参数说明
845 | * @param string $method ('GET') 请求方法
846 | *
847 | * @return mixed 返回 `$fn` 回调函数的调用结果
848 | *
849 | * @throws \Consatan\Weibo\ImageUploader\Exception\RequestException 请求失败时
850 | * @throws \Consatan\Weibo\ImageUploader\Exception\RuntimeException 获取响应内容失败时
851 | *
852 | * @see http://docs.guzzlephp.org/en/latest/request-options.html
853 | */
854 | protected function request(string $url, callable $fn, array $option = [], string $method = 'GET')
855 | {
856 | $this->applyOption($option);
857 |
858 | try {
859 | $rsp = $this->http->request($method, $url, $option);
860 | if (200 === ($statusCode = $rsp->getStatusCode())) {
861 | try {
862 | $content = $rsp->getBody()->getContents();
863 | } catch (\RuntimeException $e) {
864 | throw new RuntimeException('获取响应内容失败 :' . $e->getMessage());
865 | }
866 | return $fn($content);
867 | } elseif (300 <= $statusCode && 303 >= $statusCode) {
868 | // 如果禁止重定向(只有禁止重定向才会捕获到300 ~ 303代码)
869 | // 则把重定向 URL 当参数传递
870 | return $fn(empty(($rsp = $rsp->getHeader('Location'))) ? '' : $rsp[0]);
871 | } else {
872 | throw new RequestException("请求失败. HTTP code: $statusCode " . $rsp->getReasonPhrase());
873 | }
874 | } catch (GuzzleException $e) {
875 | throw new RequestException('请求失败. ' . $e->getMessage(), $e->getCode(), $e);
876 | }
877 | }
878 |
879 | /**
880 | * 填充必须的 http header
881 | *
882 | * @param array &$option
883 | *
884 | * @return void
885 | */
886 | private function applyOption(array &$option)
887 | {
888 | if ('' !== $this->ua && !isset($option['headers']['User-Agent'])) {
889 | $option['headers']['User-Agent'] = $this->ua;
890 | }
891 |
892 | if (!isset($option['cookies'])) {
893 | $option['cookies'] = $this->cookie;
894 | }
895 | }
896 |
897 | /**
898 | * 持久化缓存
899 | *
900 | * @param string $key 缓存key
901 | * @param mixed $value 缓存数据
902 | *
903 | * @return void
904 | *
905 | * @throws Consatan\Weibo\ImageUploader\Exception\IOException 持久化失败时
906 | */
907 | private function persistenceCache(string $key, $value)
908 | {
909 | $this->cache->deleteItem($key);
910 | // 新建 或 获取 CacheItemInterface 实例
911 | $cache = $this->cache->getItem($key);
912 | // 设置 cookie 信息
913 | $cache->set($value);
914 | // 缓存持久化
915 | if (!$this->cache->save($cache)) {
916 | throw new IOException('持久化缓存失败');
917 | }
918 | }
919 | }
920 |
--------------------------------------------------------------------------------