diff --git a/crmeb/crmeb/services/upload/extend/cos/Client.php b/crmeb/crmeb/services/upload/extend/cos/Client.php new file mode 100644 index 000000000..1cda502fc --- /dev/null +++ b/crmeb/crmeb/services/upload/extend/cos/Client.php @@ -0,0 +1,575 @@ + + * +---------------------------------------------------------------------- + */ + +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 . ''; + } + } else { + $xml .= '<' . $kk . '>' . $vv . ''; + } + } + $xml .= ''; + $xml .= ''; + + 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); + } +} diff --git a/crmeb/crmeb/services/upload/extend/cos/Scope.php b/crmeb/crmeb/services/upload/extend/cos/Scope.php new file mode 100644 index 000000000..717417273 --- /dev/null +++ b/crmeb/crmeb/services/upload/extend/cos/Scope.php @@ -0,0 +1,77 @@ + + * +---------------------------------------------------------------------- + */ + +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; + } + +} diff --git a/crmeb/crmeb/services/upload/extend/cos/Signature.php b/crmeb/crmeb/services/upload/extend/cos/Signature.php new file mode 100644 index 000000000..61dacf2e5 --- /dev/null +++ b/crmeb/crmeb/services/upload/extend/cos/Signature.php @@ -0,0 +1,213 @@ + + * +---------------------------------------------------------------------- + */ + +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]; + } +} diff --git a/crmeb/crmeb/services/upload/extend/cos/Sts.php b/crmeb/crmeb/services/upload/extend/cos/Sts.php new file mode 100644 index 000000000..fee8733f5 --- /dev/null +++ b/crmeb/crmeb/services/upload/extend/cos/Sts.php @@ -0,0 +1,321 @@ + + * +---------------------------------------------------------------------- + */ + +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; + } +} diff --git a/crmeb/crmeb/services/upload/extend/cos/XML.php b/crmeb/crmeb/services/upload/extend/cos/XML.php new file mode 100644 index 000000000..dd2e14a61 --- /dev/null +++ b/crmeb/crmeb/services/upload/extend/cos/XML.php @@ -0,0 +1,154 @@ + $value) { + $_attr[] = "{$key}=\"{$value}\""; + } + + $attr = implode(' ', $_attr); + } + + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = "<{$root}{$attr}>"; + $xml .= self::data2Xml($data, $item, $id); + $xml .= ""; + + return $xml; + } + + /** + * Build CDATA. + * + * @param string $string + * + * @return string + */ + public static function cdata($string) + { + return sprintf('', $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 .= ""; + } + + 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); + } +} \ No newline at end of file diff --git a/crmeb/crmeb/services/upload/storage/Cos.php b/crmeb/crmeb/services/upload/storage/Cos.php index 4fb29c52a..f8e427614 100644 --- a/crmeb/crmeb/services/upload/storage/Cos.php +++ b/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;