├── 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 | --------------------------------------------------------------------------------