Explorar o código

更新腾讯云存储

吴昊天 %!s(int64=2) %!d(string=hai) anos
pai
achega
b34bcd2606

+ 575 - 0
crmeb/crmeb/services/upload/extend/cos/Client.php

@@ -0,0 +1,575 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+use crmeb\exceptions\UploadException;
+
+/**
+ * Class Client
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2022/9/29
+ * @package crmeb\services\upload\extend\cos
+ */
+class Client
+{
+
+    /**
+     * @var string
+     */
+    protected $accessKey;
+
+    /**
+     * @var string
+     */
+    protected $secretKey;
+
+    /**
+     * @var string
+     */
+    protected $appid;
+
+    /**
+     * @var mixed|string
+     */
+    protected $bucket;
+
+    /**
+     * @var mixed|string
+     */
+    protected $region;
+
+    /**
+     * @var mixed|string
+     */
+    protected $uploadUrl;
+
+    /**
+     * @var string
+     */
+    protected $action = '';
+
+    /**
+     * @var array
+     */
+    protected $response = ['content' => null, 'code' => 200, 'header' => []];
+
+    /**
+     * @var array
+     */
+    protected $request = ['header' => [], 'body' => [], 'host' => ''];
+
+    /**
+     * @var string
+     */
+    protected $cosacl = 'public-read';
+
+    /**
+     * Client constructor.
+     * @param array $config
+     */
+    public function __construct(array $config)
+    {
+        $this->accessKey = $config['accessKey'] ?? '';
+        $this->secretKey = $config['secretKey'] ?? '';
+        $this->appid = $config['appid'] ?? '';
+        $this->bucket = $config['bucket'] ?? '';
+        $this->region = $config['region'] ?? 'ap-chengdu';
+        $this->uploadUrl = $config['uploadUrl'] ?? '';
+    }
+
+    /**
+     * 获取实际请求
+     * @return array
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function getResponse()
+    {
+        $response = $this->response;
+
+        $this->response = ['content' => null, 'http_code' => 200, 'header' => []];
+
+        return $response;
+    }
+
+    /**
+     * @return array
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function getRequest()
+    {
+        $request = $this->request;
+
+        $this->request = ['header' => [], 'body' => [], 'host' => ''];
+
+        return $request;
+    }
+
+    /**
+     * 拼接请求地址
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     */
+    protected function makeUpUrl()
+    {
+        return $this->bucket . '.cos.' . $this->region . '.myqcloud.com';
+    }
+
+    /**
+     * @return bool
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     */
+    protected function ssl()
+    {
+        return strstr($this->uploadUrl, 'https://') !== false;
+    }
+
+    /**
+     * 检查参数
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     */
+    protected function checkOptions()
+    {
+        if (!$this->bucket) {
+            throw new UploadException('请传入桶名');
+        }
+        if (!$this->region) {
+            throw new UploadException('请传入所属地域');
+        }
+        if (!$this->accessKey) {
+            throw new UploadException('请传入SecretId');
+        }
+        if (!$this->secretKey) {
+            throw new UploadException('请传入SecretKey');
+        }
+    }
+
+    /**
+     * 上传文件
+     * @param string $key
+     * @param $body
+     * @return string[]
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     */
+    public function putObject(string $key, $body)
+    {
+
+        $this->checkOptions();
+
+        $url = $this->makeUpUrl();
+
+        $header = [
+            'Content-Type' => 'image/jpeg',
+            'x-cos-acl' => $this->cosacl,
+            'Content-MD5' => base64_encode(md5($body, true)),
+            'Host' => $url
+        ];
+
+        $imageUrl = ($this->ssl() ? 'https://' : 'http://') . $url . '/' . $key;
+
+        $res = $this->request($imageUrl, 'PUT', ['body' => $body], $header);
+
+        if ($res && !empty($res['Message'])) {
+            throw new UploadException($res['Message']);
+        }
+
+        return [
+            'name' => $key,
+            'path' => $imageUrl
+        ];
+    }
+
+    /**
+     * 删除文件
+     * @param string $bucket
+     * @param string $key
+     * @return array|false
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/19
+     */
+    public function deleteObject(string $bucket, string $key)
+    {
+        $url = $this->getRequestHost($bucket);
+
+        $header = [
+            'Host' => $url
+        ];
+
+        $res = $this->request('https://' . $url . '/' . $key, 'delete', [], $header);
+
+        if ($res && !empty($res['Message'])) {
+            throw new UploadException($res['Message']);
+        }
+
+        return $res;
+    }
+
+    /**
+     * 获取桶列表
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/19
+     */
+    public function listBuckets()
+    {
+        $url = 'service.cos.myqcloud.com';
+
+        $header = [
+            'Host' => $url
+        ];
+
+        $res = $this->request('https://' . $url . '/', 'get', [], $header);
+
+        if ($res && !empty($res['Message'])) {
+            throw new UploadException($res['Message']);
+        }
+
+        return $res;
+    }
+
+    /**
+     * 检测桶,不存在返回true
+     * @param string $bucket
+     * @param string $region
+     * @return bool
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function headBucket(string $bucket, string $region = '')
+    {
+        $url = $this->getRequestHost($bucket, $region);
+
+        $header = [
+            'Host' => $url
+        ];
+
+        $this->request('https://' . $url, 'head', [], $header);
+
+        $response = $this->getResponse();
+
+        return $response['code'] == 404;
+    }
+
+    /**
+     * 创建桶
+     * @param string $bucket
+     * @param string $region
+     * @param string $acl
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function createBucket(string $bucket, string $region = '', string $acl = 'public-read')
+    {
+        return $this->noBodyRequest('put', $bucket, $region, $acl);
+    }
+
+    /**
+     * 组合成xml
+     * @param array $data
+     * @param string $root
+     * @param string $itemKey
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    protected function xmlBuild(array $xmlAttr, string $root = 'xml', string $itemKey = 'item')
+    {
+        $xml = '<' . $root . '>';
+        $xml .= '<' . $itemKey . '>';
+
+        foreach ($xmlAttr as $kk => $vv) {
+            if (is_array($vv)) {
+                foreach ($vv as $v) {
+                    $xml .= '<' . $kk . '>' . $v . '</' . $kk . '>';
+                }
+            } else {
+                $xml .= '<' . $kk . '>' . $vv . '</' . $kk . '>';
+            }
+        }
+        $xml .= '</' . $itemKey . '>';
+        $xml .= '</' . $root . '>';
+
+        return $xml;
+    }
+
+    /**
+     * 设置跨域
+     * @param string $bucket
+     * @param string $region
+     * @param array $data
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function putBucketCors(string $bucket, array $data, string $region = '')
+    {
+        $url = $this->getRequestHost($bucket, $region);
+
+        $xml = $this->xmlBuild($data, 'CORSConfiguration', 'CORSRule');
+
+        $header = [
+            'Host' => $url,
+            'Content-Type' => 'application/xml',
+            'Content-Length' => strlen($xml),
+            'Content-MD5' => base64_encode(md5($xml, true))
+        ];
+
+        $res = $this->request('https://' . $url . '/?cors', 'put', ['xml' => $xml], $header);
+
+        if ($res && !empty($res['Message'])) {
+            throw new UploadException($res['Message']);
+        }
+
+        return $res;
+    }
+
+    /**
+     * 删除
+     * @param string $name
+     * @param string $region
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function deleteBucket(string $name, string $region = '')
+    {
+        return $this->noBodyRequest('delete', $name, $region);
+    }
+
+    /**
+     * 获取桶下的
+     * @param string $name
+     * @param string $region
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function getBucketDomain(string $name, string $region = '')
+    {
+        $this->action = 'domain';
+        return $this->noBodyRequest('get', $name, $region);
+    }
+
+    /**
+     * 绑定域名
+     * @param string $bucket
+     * @param string $region
+     * @param array $data
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/19
+     */
+    public function putBucketDomain(string $bucket, string $region, array $data)
+    {
+        $url = $this->getRequestHost($bucket, $region);
+
+        $xml = $this->xmlBuild($data, 'DomainConfiguration', 'DomainRule');
+
+        $header = [
+            'Host' => $url,
+            'Content-Type' => 'application/xml',
+            'Content-Length' => strlen($xml),
+            'Content-MD5' => base64_encode(md5($xml, true))
+        ];
+
+        $res = $this->request('https://' . $url . '/?domain', 'put', ['xml' => $xml], $header);
+
+        if ($res && !empty($res['Message'])) {
+            throw new UploadException($res['Message']);
+        }
+
+        return $res;
+    }
+
+    /**
+     * @param string $bucket
+     * @param string $region
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    protected function getRequestHost(string $bucket, string $region = '')
+    {
+        if (!$this->accessKey) {
+            throw new UploadException('请传入SecretId');
+        }
+        if (!$this->secretKey) {
+            throw new UploadException('请传入SecretKey');
+        }
+
+        if (strstr($bucket, '-') === false) {
+            $bucket = $bucket . '-' . $this->appid;
+        }
+
+        return $bucket . '.cos.' . ($region ?: $this->region) . '.myqcloud.com';
+    }
+
+    /**
+     * @param string $method
+     * @param string $bucket
+     * @param string $region
+     * @param string|null $acl
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/10/17
+     */
+    public function noBodyRequest(string $method, string $bucket, string $region = '', string $acl = null, bool $isExc = true)
+    {
+
+        $url = $this->getRequestHost($bucket, $region);
+
+        $header = [
+            'Host' => $url
+        ];
+
+        if ($acl) {
+            $header['x-cos-acl'] = $acl;
+        }
+
+        if (in_array($method, ['put', 'post'])) {
+            $header['Content-Length'] = 0;
+        }
+
+        $res = $this->request('https://' . $url . '/' . ($this->action ? '?' . $this->action : ''), $method, [], $header);
+        $this->action = '';
+
+        if ($isExc) {
+            if ($res && !empty($res['Message'])) {
+                throw new UploadException($res['Message']);
+            }
+        }
+
+        return $res;
+    }
+
+    /**
+     * 发起请求
+     * @param string $url
+     * @param string $method
+     * @param array $data
+     * @param array $header
+     * @param int $timeout
+     * @return array|false|\SimpleXMLElement|string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     */
+    public function request(string $url, string $method, array $data, array $header = [], int $timeout = 5)
+    {
+
+        $this->request['body'] = $data;
+        $this->request['host'] = $url;
+
+
+        $urlAttr = parse_url($url);
+        $curl = curl_init($url);
+        $method = strtoupper($method);
+        //请求方式
+        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
+
+        //超时时间
+        curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+        //设置header头
+
+        $header = array_merge($header, $this->getSign($url, $method, $urlAttr['path'] ?? '', [], $header));
+
+        $this->request['header'] = $header;
+
+        $clientHeader = [];
+        foreach ($header as $key => $item) {
+            $clientHeader[] = $key . ':' . $item;
+        }
+
+        curl_setopt($curl, CURLOPT_HTTPHEADER, $clientHeader);
+
+
+        curl_setopt($curl, CURLOPT_FAILONERROR, false);
+        //返回抓取数据
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        //输出header头信息
+        curl_setopt($curl, CURLOPT_HEADER, true);
+        //TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
+        curl_setopt($curl, CURLINFO_HEADER_OUT, true);
+        //https请求
+        if (1 == strpos("$" . $url, "https://")) {
+            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        }
+
+        //post请求
+        if ($method == 'PUT' && !empty($data['body'])) {
+            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+            // 注意这里的'file'是上传地址指定的key名
+            curl_setopt($curl, CURLOPT_POSTFIELDS, $data['body']);
+        }
+
+        if (!empty($data['xml'])) {
+            curl_setopt($curl, CURLOPT_POSTFIELDS, $data['xml']);
+        }
+
+        list($content, $status) = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)];
+
+        $content = trim(substr($content, $status['header_size']));
+
+        $this->response['content'] = $content;
+        $this->response['code'] = $status['http_code'];
+        $this->response['header'] = $status;
+
+        $res = XML::parse($content);
+        if ($res) {
+            return $res;
+        }
+        return (intval($status["http_code"]) === 200) ? $content : false;
+    }
+
+    /**
+     * 获取签名
+     * @param string $method
+     * @param string $urlPath
+     * @param array $query
+     * @param array $headers
+     * @return array
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/27
+     */
+    public function getSign(string $url, string $method, string $urlPath, array $query = [], array $headers = [])
+    {
+        return (new Signature($this->accessKey, $this->secretKey, ['signHost' => $url]))->signRequest($method, $urlPath, $query, $headers);
+    }
+}

+ 77 - 0
crmeb/crmeb/services/upload/extend/cos/Scope.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+
+class Scope
+{
+    protected $action;
+    protected $bucket;
+    protected $region;
+    protected $resourcePrefix;
+    protected $effect = 'allow';
+
+    public function __construct($action, $bucket, $region, $resourcePrefix)
+    {
+        $this->action = $action;
+        $this->bucket = $bucket;
+        $this->region = $region;
+        $this->resourcePrefix = $resourcePrefix;
+    }
+
+    public function set_effect($isAllow)
+    {
+        if ($isAllow) {
+            $this->effect = 'allow';
+        } else {
+            $this->effect = 'deny';
+        }
+    }
+
+    public function get_action()
+    {
+        if ($this->action == null) {
+            throw new \Exception("action == null");
+        }
+        return $this->action;
+    }
+
+    public function get_resource()
+    {
+        if ($this->bucket == null) {
+            throw new \Exception("bucket == null");
+        }
+        if ($this->region == null) {
+            throw new \Exception("region == null");
+        }
+        if ($this->resourcePrefix == null) {
+            throw new \Exception("resourcePrefix == null");
+        }
+        $index = strripos($this->bucket, '-');
+        if ($index < 0) {
+            throw new Exception("bucket is invalid: " . $this->bucket);
+        }
+        $appid = substr($this->bucket, $index + 1);
+        if (!(strpos($this->resourcePrefix, '/') === 0)) {
+            $this->resourcePrefix = '/' . $this->resourcePrefix;
+        }
+        return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':' . $this->bucket . $this->resourcePrefix;
+    }
+
+    public function get_effect()
+    {
+        return $this->effect;
+    }
+
+}

+ 213 - 0
crmeb/crmeb/services/upload/extend/cos/Signature.php

@@ -0,0 +1,213 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+/**
+ * Class 生成签名
+ * @author 等风来
+ * @email 136327134@qq.com
+ * @date 2022/9/26
+ * @package crmeb\services\upload\extend\cos
+ */
+class Signature
+{
+    /**
+     * @var string
+     */
+    private $accessKey;
+
+    /**
+     * @var string
+     */
+    private $secretKey;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    /**
+     * Signature constructor.
+     * @param string $accessKey
+     * @param string $secretKey
+     * @param array $options
+     * @param string $token
+     */
+    public function __construct(string $accessKey, string $secretKey, array $options = [], string $token = '')
+    {
+        $this->accessKey = $accessKey;
+        $this->secretKey = $secretKey;
+        $this->options = $options;
+        $this->token = $token;
+        $this->signHeader = [
+            'cache-control',
+            'content-disposition',
+            'content-encoding',
+            'content-length',
+            'content-md5',
+            'content-type',
+            'expect',
+            'expires',
+            'host',
+            'if-match',
+            'if-modified-since',
+            'if-none-match',
+            'if-unmodified-since',
+            'origin',
+            'range',
+            'response-cache-control',
+            'response-content-disposition',
+            'response-content-encoding',
+            'response-content-language',
+            'response-content-type',
+            'response-expires',
+            'transfer-encoding',
+            'versionid',
+        ];
+        date_default_timezone_set('PRC');
+    }
+
+    public function needCheckHeader($header)
+    {
+        if ($this->startWith($header, 'x-cos-')) {
+            return true;
+        }
+        if (in_array($header, $this->signHeader)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/29
+     * @param $haystack
+     * @param $needle
+     * @return bool
+     */
+    protected function startWith($haystack, $needle)
+    {
+        $length = strlen($needle);
+        if ($length == 0) {
+            return true;
+        }
+        return (substr($haystack, 0, $length) === $needle);
+    }
+
+    /**
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @return string[]
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function signRequest(string $method, string $urlPath, array $querys = [], array $headers = [])
+    {
+        $authorization = $this->createAuthorization($method, $urlPath, $querys, $headers);
+        return ['Authorization' => $authorization];
+    }
+
+    /**
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @param string $expires
+     * @return string
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function createAuthorization(string $method, string $urlPath, array $querys = [], array $headers = [], $expires = '+30 minutes')
+    {
+        if (is_null($expires) || !strtotime($expires)) {
+            $expires = '+30 minutes';
+        }
+        $signTime = ( string )(time() - 60) . ';' . ( string )(strtotime($expires));
+        $urlParamListArray = [];
+        foreach ($querys as $query) {
+            if (!empty($query)) {
+                $tmpquery = explode('=', $query);
+                //为了保证CI的key中有=号的情况也能正常通过,ci在这层之前已经encode了,这里需要拆开重新encode,防止上方explode拆错
+                $key = strtolower(rawurlencode(urldecode($tmpquery[0])));
+                if (count($tmpquery) >= 2) {
+                    $value = $tmpquery[1];
+                } else {
+                    $value = "";
+                }
+                //host开关
+                if (!$this->options['signHost'] && $key == 'host') {
+                    continue;
+                }
+                $urlParamListArray[$key] = $key . '=' . $value;
+            }
+        }
+        ksort($urlParamListArray);
+        $urlParamList = join(';', array_keys($urlParamListArray));
+        $httpParameters = join('&', array_values($urlParamListArray));
+
+        $headerListArray = [];
+        foreach ($headers as $key => $value) {
+            $key = strtolower(urlencode($key));
+            $value = rawurlencode($value);
+            if (!$this->options['signHost'] && $key == 'host') {
+                continue;
+            }
+            if ($this->needCheckHeader($key)) {
+                $headerListArray[$key] = $key . '=' . $value;
+            }
+        }
+        ksort($headerListArray);
+        $headerList = join(';', array_keys($headerListArray));
+        $httpHeaders = join('&', array_values($headerListArray));
+        $httpString = strtolower($method) . "\n" . urldecode($urlPath) . "\n" . $httpParameters .
+            "\n" . $httpHeaders . "\n";
+
+        $sha1edHttpString = sha1($httpString);
+        $stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
+        $signKey = hash_hmac('sha1', $signTime, trim($this->secretKey));
+        $signature = hash_hmac('sha1', $stringToSign, $signKey);
+        $authorization = 'q-sign-algorithm=sha1&q-ak=' . trim($this->accessKey) .
+            "&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=$headerList&q-url-param-list=$urlParamList&" .
+            "q-signature=$signature";
+        return $authorization;
+    }
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param string $urlPath
+     * @param array $querys
+     * @param array $headers
+     * @param string $expires
+     * @return string[]
+     * @author 等风来
+     * @email 136327134@qq.com
+     * @date 2022/9/26
+     */
+    public function createPresignedUrl(string $url, string $method, string $urlPath, array $querys = [], array $headers = [], string $expires = '+30 minutes')
+    {
+        $authorization = $this->createAuthorization($method, $urlPath, $querys, $headers, $expires);
+        $uri = $url;
+        $query = 'sign=' . urlencode($authorization) . '&' . implode('&', $querys);
+        if ($this->token != null) {
+            $query = $query . '&x-cos-security-token=' . $this->token;
+        }
+        return [$uri, $query];
+    }
+}

+ 321 - 0
crmeb/crmeb/services/upload/extend/cos/Sts.php

@@ -0,0 +1,321 @@
+<?php
+/**
+ *  +----------------------------------------------------------------------
+ *  | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+ *  +----------------------------------------------------------------------
+ *  | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+ *  +----------------------------------------------------------------------
+ *  | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+ *  +----------------------------------------------------------------------
+ *  | Author: CRMEB Team <admin@crmeb.com>
+ *  +----------------------------------------------------------------------
+ */
+
+namespace crmeb\services\upload\extend\cos;
+
+
+class Sts
+{
+// 临时密钥计算样例
+    function _hex2bin($data)
+    {
+        $len = strlen($data);
+        return pack("H" . $len, $data);
+    }
+
+    // obj 转 query string
+    function json2str($obj, $notEncode = false)
+    {
+        ksort($obj);
+        $arr = array();
+        if (!is_array($obj)) {
+            throw new \Exception('$obj must be an array, the actual value is:' . json_encode($obj));
+        }
+        foreach ($obj as $key => $val) {
+            array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
+        }
+        return join('&', $arr);
+    }
+
+    // 计算临时密钥用的签名
+    function getSignature($opt, $key, $method, $config)
+    {
+        $host = "sts.tencentcloudapi.com";
+
+        if (array_key_exists('domain', $config)) {
+            $host = $config['domain'];
+        }
+
+        if (array_key_exists('endpoint', $config)) {
+            $host = "sts." . $config['endpoint'];
+        }
+
+        $formatString = $method . $host . '/?' . $this->json2str($opt, 1);
+        $sign = hash_hmac('sha1', $formatString, $key);
+        $sign = base64_encode($this->_hex2bin($sign));
+        return $sign;
+    }
+
+    // v2接口的key首字母小写,v3改成大写,此处做了向下兼容
+    function backwardCompat($result)
+    {
+        if (!is_array($result)) {
+            throw new \Exception('$result must be an array, the actual value is:' . json_encode($result));
+        }
+        $compat = array();
+        foreach ($result as $key => $value) {
+            if (is_array($value)) {
+                $compat[lcfirst($key)] = $this->backwardCompat($value);
+            } elseif ($key == 'Token') {
+                $compat['sessionToken'] = $value;
+            } else {
+                $compat[lcfirst($key)] = $value;
+            }
+        }
+        return $compat;
+    }
+
+    // 获取临时密钥
+    function getTempKeys($config)
+    {
+        $result = null;
+        try {
+            if (array_key_exists('policy', $config)) {
+                $policy = $config['policy'];
+            } else {
+                if (array_key_exists('bucket', $config)) {
+                    $ShortBucketName = substr($config['bucket'], 0, strripos($config['bucket'], '-'));
+                    $AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
+                } else {
+                    throw new \Exception("bucket== null");
+                }
+                if (array_key_exists('allowPrefix', $config)) {
+                    if (!(strpos($config['allowPrefix'], '/') === 0)) {
+                        $config['allowPrefix'] = '/' . $config['allowPrefix'];
+                    }
+                } else {
+                    throw new \Exception("allowPrefix == null");
+                }
+                if (!array_key_exists('region', $config)) {
+                    throw new \Exception("region == null");
+                }
+                $policy = array(
+                    'version' => '2.0',
+                    'statement' => array(
+                        array(
+                            'action' => $config['allowActions'],
+                            'effect' => 'allow',
+                            'resource' => array(
+                                'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
+                            )
+                        )
+                    )
+                );
+            }
+            $policyStr = str_replace('\\/', '/', json_encode($policy));
+            $Action = 'GetFederationToken';
+            $Nonce = rand(10000, 20000);
+            $Timestamp = time();
+            $Method = 'POST';
+            if (array_key_exists('durationSeconds', $config)) {
+                if (!(is_integer($config['durationSeconds']))) {
+                    throw new \Exception("durationSeconds must be a int type");
+                }
+            }
+            $params = array(
+                'SecretId' => $config['secretId'],
+                'Timestamp' => $Timestamp,
+                'Nonce' => $Nonce,
+                'Action' => $Action,
+                'DurationSeconds' => $config['durationSeconds'],
+                'Version' => '2018-08-13',
+                'Name' => 'cos',
+                'Region' => $config['region'],
+                'Policy' => urlencode($policyStr)
+            );
+            $params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
+            $url = 'https://sts.tencentcloudapi.com/';
+
+            if (array_key_exists('url', $config)) {
+                $url = $config['url'];
+            }
+
+            if (!array_key_exists('url', $config) && array_key_exists('domain', $config)) {
+                $url = 'https://sts.' . $config['domain'];
+            }
+
+            if (array_key_exists('endpoint', $config)) {
+                $url = 'https://sts.' . $config['endpoint'];
+            }
+
+            $ch = curl_init($url);
+            if (array_key_exists('proxy', $config)) {
+                $config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
+            }
+            curl_setopt($ch, CURLOPT_HEADER, 0);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
+            $result = curl_exec($ch);
+            if (curl_errno($ch)) $result = curl_error($ch);
+            curl_close($ch);
+            $result = json_decode($result, 1);
+            if (isset($result['Response'])) {
+                $result = $result['Response'];
+                if (isset($result['Error'])) {
+                    throw new \Exception("get cam failed");
+                }
+                $result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
+            }
+            $result = $this->backwardCompat($result);
+            return $result;
+        } catch (\Exception $e) {
+            if ($result == null) {
+                $result = "error: " . $e->getMessage();
+            } else {
+                $result = json_encode($result);
+            }
+            throw new \Exception($result);
+        }
+    }
+
+    //申请角色授权
+    function getRoleCredential($config)
+    {
+        $result = null;
+        try {
+            if (array_key_exists('policy', $config)) {
+                $policy = $config['policy'];
+            } else {
+                if (array_key_exists('bucket', $config)) {
+                    $ShortBucketName = substr($config['bucket'], 0, strripos($config['bucket'], '-'));
+                    $AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
+                } else {
+                    throw new \Exception("bucket== null");
+                }
+                if (array_key_exists('allowPrefix', $config)) {
+                    if (!(strpos($config['allowPrefix'], '/') === 0)) {
+                        $config['allowPrefix'] = '/' . $config['allowPrefix'];
+                    }
+                } else {
+                    throw new \Exception("allowPrefix == null");
+                }
+                if (!array_key_exists('region', $config)) {
+                    throw new \Exception("region == null");
+                }
+                $policy = array(
+                    'version' => '2.0',
+                    'statement' => array(
+                        array(
+                            'action' => $config['allowActions'],
+                            'effect' => 'allow',
+                            'resource' => array(
+                                'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
+                            )
+                        )
+                    )
+                );
+            }
+            if (array_key_exists('roleArn', $config)) {
+                $RoleArn = $config['roleArn'];
+            } else {
+                throw new \Exception("roleArn == null");
+            }
+            $policyStr = str_replace('\\/', '/', json_encode($policy));
+            $Action = 'AssumeRole';
+            $Nonce = rand(10000, 20000);
+            $Timestamp = time();
+            $Method = 'POST';
+            $ExternalId = "";
+            if (array_key_exists('externalId', $config)) {
+                $ExternalId = $config['externalId'];
+            }
+            if (array_key_exists('durationSeconds', $config)) {
+                if (!(is_integer($config['durationSeconds']))) {
+                    throw new \Exception("durationSeconds must be a int type");
+                }
+            }
+            $params = array(
+                'SecretId' => $config['secretId'],
+                'Timestamp' => $Timestamp,
+                'RoleArn' => $RoleArn,
+                'Action' => $Action,
+                'Nonce' => $Nonce,
+                'DurationSeconds' => $config['durationSeconds'],
+                'Version' => '2018-08-13',
+                'RoleSessionName' => 'cos',
+                'Region' => $config['region'],
+                'ExternalId' => $ExternalId,
+                'Policy' => urlencode($policyStr)
+            );
+            $params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
+            $url = 'https://sts.internal.tencentcloudapi.com/';
+
+            if (array_key_exists('endpoint', $config)) {
+                $url = 'https://sts.' . $config['endpoint'];
+            }
+            $ch = curl_init($url);
+            if (array_key_exists('proxy', $config)) {
+                $config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
+            }
+            curl_setopt($ch, CURLOPT_HEADER, 0);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
+            $result = curl_exec($ch);
+            if (curl_errno($ch)) $result = curl_error($ch);
+            curl_close($ch);
+            $result = json_decode($result, 1);
+            if (isset($result['Response'])) {
+                $result = $result['Response'];
+                if (isset($result['Error'])) {
+                    throw new \Exception("get cam failed");
+                }
+                $result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
+            }
+            $result = $this->backwardCompat($result);
+            return $result;
+        } catch (\Exception $e) {
+            if ($result == null) {
+                $result = "error: " . $e->getMessage();
+            } else {
+                $result = json_encode($result);
+            }
+            throw new \Exception($result);
+        }
+    }
+
+
+    // get policy
+    function getPolicy($scopes)
+    {
+        if (!is_array($scopes)) {
+            return null;
+        }
+        $statements = array();
+
+        for ($i = 0, $counts = count($scopes); $i < $counts; $i++) {
+            $actions = array();
+            $resources = array();
+            array_push($actions, $scopes[$i]->get_action());
+            array_push($resources, $scopes[$i]->get_resource());
+
+            $statement = array(
+                'action' => $actions,
+                'effect' => $scopes[$i]->get_effect(),
+                'resource' => $resources
+            );
+            array_push($statements, $statement);
+        }
+
+        $policy = array(
+            'version' => '2.0',
+            'statement' => $statements
+        );
+        return $policy;
+    }
+}

+ 154 - 0
crmeb/crmeb/services/upload/extend/cos/XML.php

@@ -0,0 +1,154 @@
+<?php
+
+namespace crmeb\services\upload\extend\cos;
+
+class XML
+{
+    /**
+     * XML to array.
+     *
+     * @param string $xml XML string
+     *
+     * @return array
+     */
+    public static function parse($xml)
+    {
+        $backup = PHP_MAJOR_VERSION < 8 ? libxml_disable_entity_loader(true) : null;
+
+        $result = self::normalize(simplexml_load_string(self::sanitize($xml), 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS));
+
+        PHP_MAJOR_VERSION < 8 && libxml_disable_entity_loader($backup);
+
+        return $result;
+    }
+
+    /**
+     * XML encode.
+     *
+     * @param mixed $data
+     * @param string $root
+     * @param string $item
+     * @param string $attr
+     * @param string $id
+     *
+     * @return string
+     */
+    public static function build(
+        $data,
+        $root = 'xml',
+        $item = 'item',
+        $attr = '',
+        $id = 'id'
+    )
+    {
+        if (is_array($attr)) {
+            $_attr = [];
+
+            foreach ($attr as $key => $value) {
+                $_attr[] = "{$key}=\"{$value}\"";
+            }
+
+            $attr = implode(' ', $_attr);
+        }
+
+        $attr = trim($attr);
+        $attr = empty($attr) ? '' : " {$attr}";
+        $xml = "<{$root}{$attr}>";
+        $xml .= self::data2Xml($data, $item, $id);
+        $xml .= "</{$root}>";
+
+        return $xml;
+    }
+
+    /**
+     * Build CDATA.
+     *
+     * @param string $string
+     *
+     * @return string
+     */
+    public static function cdata($string)
+    {
+        return sprintf('<![CDATA[%s]]>', $string);
+    }
+
+    /**
+     * Object to array.
+     *
+     *
+     * @param SimpleXMLElement $obj
+     *
+     * @return array
+     */
+    protected static function normalize($obj)
+    {
+        $result = null;
+
+        if (is_object($obj)) {
+            $obj = (array)$obj;
+        }
+
+        if (is_array($obj)) {
+            foreach ($obj as $key => $value) {
+                $res = self::normalize($value);
+                if (('@attributes' === $key) && ($key)) {
+                    $result = $res; // @codeCoverageIgnore
+                } else {
+                    $result[$key] = $res;
+                }
+            }
+        } else {
+            $result = $obj;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Array to XML.
+     *
+     * @param array $data
+     * @param string $item
+     * @param string $id
+     *
+     * @return string
+     */
+    protected static function data2Xml($data, $item = 'item', $id = 'id')
+    {
+        $xml = $attr = '';
+
+        foreach ($data as $key => $val) {
+            if (is_numeric($key)) {
+                $id && $attr = " {$id}=\"{$key}\"";
+                $key = $item;
+            }
+
+            $xml .= "<{$key}{$attr}>";
+
+            if ((is_array($val) || is_object($val))) {
+                $xml .= self::data2Xml((array)$val, $item, $id);
+            } else {
+                $xml .= is_numeric($val) ? $val : self::cdata($val);
+            }
+
+            $xml .= "</{$key}>";
+        }
+
+        return $xml;
+    }
+
+    /**
+     * Delete invalid characters in XML.
+     *
+     * @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range
+     * @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode
+     *
+     * @param string $xml
+     *
+     * @return string
+     */
+    public static function sanitize($xml)
+    {
+        return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml);
+    }
+}

+ 30 - 41
crmeb/crmeb/services/upload/storage/Cos.php

@@ -13,8 +13,10 @@ namespace crmeb\services\upload\storage;
 use crmeb\services\upload\BaseUpload;
 use crmeb\exceptions\AdminException;
 use crmeb\exceptions\UploadException;
+use GuzzleHttp\Psr7\Utils;
 use Qcloud\Cos\Client;
 use QCloud\COSSTS\Sts;
+use crmeb\services\upload\extend\cos\Client as CrmebClient;
 
 /**
  * 腾讯云COS文件上传
@@ -44,7 +46,7 @@ class Cos extends BaseUpload
 
     /**
      * 句柄
-     * @var Client
+     * @var CrmebClient
      */
     protected $handle;
 
@@ -101,16 +103,18 @@ class Cos extends BaseUpload
 
     /**
      * 实例化cos
-     * @return Client
+     * @return CrmebClient
      */
     protected function app()
     {
-        if (!$this->accessKey || !$this->secretKey) {
-            throw new UploadException(400721);
-        }
-        $this->handle = new Client(['region' => $this->storageRegion, 'credentials' => [
-            'secretId' => $this->accessKey, 'secretKey' => $this->secretKey
-        ]]);
+        $this->handle = new CrmebClient([
+            'accessKey' => $this->accessKey,
+            'secretKey' => $this->secretKey,
+            'region' => $this->storageRegion ?: 'ap-chengdu',
+            'bucket' => $this->storageName,
+            'appid' => $this->appid,
+            'uploadUrl' => $this->uploadUrl
+        ]);
         return $this->handle;
     }
 
@@ -141,17 +145,14 @@ class Cos extends BaseUpload
             }
             $key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
             $body = fopen($fileHandle->getRealPath(), 'rb');
+            $body = (string)Utils::streamFor($body);
         } else {
             $key = $file;
             $body = $fileContent;
         }
         try {
             $key = $this->getUploadPath($key);
-            $this->fileInfo->uploadInfo = $this->app()->putObject([
-                'Bucket' => $this->storageName,
-                'Key' => $key,
-                'Body' => $body
-            ]);
+            $this->fileInfo->uploadInfo = $this->app()->putObject($key, $body);
             $this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
             $this->fileInfo->realName = isset($fileHandle) ? $fileHandle->getOriginalName() : $key;
             $this->fileInfo->fileName = $key;
@@ -263,7 +264,7 @@ class Cos extends BaseUpload
     public function delete(string $filePath)
     {
         try {
-            return $this->app()->deleteObject(['Bucket' => $this->storageName, 'Key' => $filePath]);
+            return $this->app()->deleteObject($this->storageName, $filePath);
         } catch (\Exception $e) {
             return $this->setError($e->getMessage());
         }
@@ -403,7 +404,7 @@ class Cos extends BaseUpload
         $app = $this->app();
         //检测桶
         try {
-            $app->headBucket(['Bucket' => $name . '-' . $this->appid]);
+            $app->headBucket($name);
         } catch (\Throwable $e) {
             //桶不存在返回404
             if (strstr('404', $e->getMessage())) {
@@ -412,7 +413,7 @@ class Cos extends BaseUpload
         }
         //创建桶
         try {
-            $res = $app->createBucket(['Bucket' => $name . '-' . $this->appid, 'ACL' => $acl]);
+            $res = $app->createBucket($name . '-' . $this->appid, '', $acl);
         } catch (\Throwable $e) {
             if (strstr('[curl] 6', $e->getMessage())) {
                 return $this->setError('COS:无效的区域!!');
@@ -432,7 +433,7 @@ class Cos extends BaseUpload
     public function deleteBucket(string $name)
     {
         try {
-            $res = $this->app()->deleteBucket(['Bucket' => $name]);
+            $res = $this->app()->deleteBucket($name);
             if ($res->get('RequestId')) {
                 return true;
             }
@@ -451,9 +452,7 @@ class Cos extends BaseUpload
     {
         $this->storageRegion = $region;
         try {
-            $res = $this->app()->GetBucketDomain([
-                'Bucket' => $name,
-            ]);
+            $res = $this->app()->GetBucketDomain($name);
             $domainRules = $res->toArray()['DomainRules'];
             return array_column($domainRules, 'Name');
         } catch (\Throwable $e) {
@@ -473,16 +472,11 @@ class Cos extends BaseUpload
         $this->storageRegion = $region;
         $parseDomin = parse_url($domain);
         try {
-            $res = $this->app()->putBucketDomain([
-                'Bucket' => $name,
-                'DomainRules' => [
-                    [
-                        'Name' => $parseDomin['host'],
-                        'Status' => 'ENABLED',
-                        'Type' => 'REST',
-                        'ForcedReplacement' => 'CNAME'
-                    ]
-                ]
+            $res = $this->app()->putBucketDomain($name, '', [
+                'Name' => $parseDomin['host'],
+                'Status' => 'ENABLED',
+                'Type' => 'REST',
+                'ForcedReplacement' => 'CNAME'
             ]);
             $res = $res->toArray();
             if ($res['RequestId'] ?? null) {
@@ -530,17 +524,12 @@ class Cos extends BaseUpload
     {
         $this->storageRegion = $region;
         try {
-            $res = $this->app()->PutBucketCors([
-                'Bucket' => $name,
-                'CORSRules' => [
-                    [
-                        'AllowedHeaders' => ['*'],
-                        'AllowedMethods' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
-                        'AllowedOrigins' => ['*'],
-                        'ExposeHeaders' => ['ETag', 'Content-Length', 'x-cos-request-id'],
-                        'MaxAgeSeconds' => 12
-                    ]
-                ]
+            $res = $this->app()->PutBucketCors($name, [
+                'AllowedHeaders' => ['*'],
+                'AllowedMethods' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
+                'AllowedOrigins' => ['*'],
+                'ExposeHeaders' => ['ETag', 'Content-Length', 'x-cos-request-id'],
+                'MaxAgeSeconds' => 12
             ]);
             if (isset($res['RequestId'])) {
                 return true;