Index.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. namespace addons\cos\controller;
  3. use addons\cos\library\Auth;
  4. use app\common\exception\UploadException;
  5. use app\common\library\Upload;
  6. use app\common\model\Attachment;
  7. use Qcloud\Cos\Client;
  8. use Qcloud\Cos\Signature;
  9. use think\addons\Controller;
  10. use think\Config;
  11. /**
  12. * COS云储存
  13. *
  14. */
  15. class Index extends Controller
  16. {
  17. protected $cosConfig = [];
  18. public function _initialize()
  19. {
  20. //跨域检测
  21. check_cors_request();
  22. parent::_initialize();
  23. Config::set('default_return_type', 'json');
  24. $config = get_addon_config('cos');
  25. $this->cosConfig = array(
  26. 'region' => $config['region'],
  27. 'schema' => 'https', //协议头部,默认为http
  28. 'credentials' => array(
  29. 'secretId' => $config['secretId'],
  30. 'secretKey' => $config['secretKey']
  31. )
  32. );
  33. }
  34. public function index()
  35. {
  36. Config::set('default_return_type', 'html');
  37. $this->error("当前插件暂无前台页面");
  38. }
  39. public function params()
  40. {
  41. $this->check();
  42. $config = get_addon_config('cos');
  43. $name = $this->request->post('name');
  44. $md5 = $this->request->post('md5');
  45. $chunk = $this->request->post('chunk');
  46. $name = xss_clean($name);
  47. // 检查文件后缀
  48. $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  49. $allowedExtensions = explode(',', strtolower($config['mimetype']));
  50. if (!in_array($extension, $allowedExtensions) || in_array($extension, ['php', 'html', 'htm', 'phar', 'phtml']) || preg_match("/^php(.*)/i", $extension)) {
  51. $this->error('不允许的文件类型');
  52. }
  53. // 检查文件大小
  54. $fileSize = $this->request->post('size');
  55. preg_match('/([0-9\.]+)(\w+)/', $config['maxsize'], $matches);
  56. $configMaxSize = $matches ? $matches[1] : $config['maxsize'];
  57. $type = $matches ? strtolower($matches[2]) : 'b';
  58. $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
  59. $maxSize = (int)($configMaxSize * pow(1024, $typeDict[$type] ?? 0));
  60. if ($fileSize > $maxSize) {
  61. $this->error('文件大小超出限制');
  62. }
  63. $key = (new Upload())->getSavekey($config['savekey'], $name, $md5);
  64. $key = ltrim($key, "/");
  65. $params = [
  66. 'key' => $key,
  67. 'md5' => $md5
  68. ];
  69. if ($chunk) {
  70. $fileSize = $this->request->post('size');
  71. $oss = new Client($this->cosConfig);
  72. $result = $oss->createMultipartUpload(array(
  73. 'Bucket' => $config['bucket'],
  74. 'Key' => $key,
  75. ));
  76. $uploadId = $result['UploadId'];
  77. $sig = new Signature($config['secretId'], $config['secretKey'], ['signHost' => true]);
  78. $partSize = $this->request->post("chunksize");
  79. $i = 0;
  80. $size_count = $fileSize;
  81. $values = array();
  82. while ($size_count > 0) {
  83. $size_count -= $partSize;
  84. $values[] = array(
  85. $partSize * $i,
  86. ($size_count > 0) ? $partSize : ($size_count + $partSize),
  87. );
  88. $i++;
  89. }
  90. $params['key'] = $key;
  91. $params['uploadId'] = $uploadId;
  92. $params['partsAuthorization'] = [];
  93. $date = gmdate('D, d M Y H:i:s \G\M\T');
  94. foreach ($values as $index => $part) {
  95. $partNumber = $index + 1;
  96. $options = array(
  97. 'Bucket' => $config['bucket'],
  98. 'Key' => $key,
  99. 'UploadId' => $uploadId,
  100. 'PartNumber' => $partNumber,
  101. 'Body' => ''
  102. );
  103. $command = $oss->getCommand('uploadPart', $options);
  104. $request = $oss->commandToRequestTransformer($command);
  105. $authorization = $sig->createAuthorization($request);
  106. $params['partsAuthorization'][$index] = $authorization;
  107. }
  108. $params['date'] = $date;
  109. } else {
  110. if ($config['uploadmode'] == 'client') {
  111. $expiretime = time() + $config['expire'];
  112. $expiration = gmdate("Y-m-d\TH:i:s.414\Z", $expiretime);
  113. $keytime = (time() - 60) . ';' . $expiretime;
  114. $policy = json_encode([
  115. 'expiration' => $expiration,
  116. 'conditions' => [
  117. ['q-sign-algorithm' => 'sha1'],
  118. ['q-ak' => $config['secretId']],
  119. ['q-sign-time' => $keytime],
  120. ['content-length-range', 1, $maxSize]
  121. ]
  122. ]);
  123. $signature = hash_hmac('sha1', sha1($policy), hash_hmac('sha1', $keytime, $config['secretKey']));
  124. $params = [
  125. 'key' => $key,
  126. 'policy' => base64_encode($policy),
  127. 'q-sign-algorithm' => 'sha1',
  128. 'q-ak' => $config['secretId'],
  129. 'q-key-time' => $keytime,
  130. 'q-sign-time' => $keytime,
  131. 'q-signature' => $signature
  132. ];
  133. }
  134. }
  135. $this->success('', null, $params);
  136. }
  137. /**
  138. * 服务器中转上传文件
  139. * 上传分片
  140. * 合并分片
  141. * @param bool $isApi
  142. */
  143. public function upload($isApi = false)
  144. {
  145. if ($isApi === true) {
  146. if (!Auth::isModuleAllow()) {
  147. $this->error("请登录后再进行操作");
  148. }
  149. } else {
  150. $this->check();
  151. }
  152. $config = get_addon_config('cos');
  153. $oss = new Client($this->cosConfig);
  154. //检测删除文件或附件
  155. $checkDeleteFile = function ($attachment, $upload, $force = false) use ($config) {
  156. //如果设定为不备份则删除文件和记录 或 强制删除
  157. if ((isset($config['serverbackup']) && !$config['serverbackup']) || $force) {
  158. if ($attachment && !empty($attachment['id'])) {
  159. $attachment->delete();
  160. }
  161. if ($upload) {
  162. //文件绝对路径
  163. $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
  164. @unlink($filePath);
  165. }
  166. }
  167. };
  168. $chunkid = $this->request->post("chunkid");
  169. if ($chunkid) {
  170. $action = $this->request->post("action");
  171. $chunkindex = $this->request->post("chunkindex/d");
  172. $chunkcount = $this->request->post("chunkcount/d");
  173. $filesize = $this->request->post("filesize");
  174. $filename = $this->request->post("filename");
  175. $method = $this->request->method(true);
  176. $key = $this->request->post("key");
  177. $uploadId = $this->request->post("uploadId");
  178. if ($action == 'merge') {
  179. $attachment = null;
  180. $upload = null;
  181. //合并分片
  182. if ($config['uploadmode'] == 'server') {
  183. //合并分片文件
  184. try {
  185. $upload = new Upload();
  186. $attachment = $upload->merge($chunkid, $chunkcount, $filename);
  187. } catch (UploadException $e) {
  188. $this->error($e->getMessage());
  189. }
  190. }
  191. $etags = $this->request->post("etags/a", []);
  192. if (count($etags) != $chunkcount) {
  193. $checkDeleteFile($attachment, $upload, true);
  194. $this->error("分片数据错误");
  195. }
  196. $listParts = [];
  197. for ($i = 0; $i < $chunkcount; $i++) {
  198. $listParts[] = array("PartNumber" => $i + 1, "ETag" => $etags[$i]);
  199. }
  200. try {
  201. $result = $oss->completeMultipartUpload(
  202. array(
  203. 'Bucket' => $config['bucket'],
  204. 'Key' => $key,
  205. 'UploadId' => $uploadId,
  206. 'Parts' => $listParts
  207. )
  208. );
  209. } catch (\Exception $e) {
  210. $checkDeleteFile($attachment, $upload, true);
  211. $this->error($e->getMessage());
  212. }
  213. if (!isset($result['Key'])) {
  214. $checkDeleteFile($attachment, $upload, true);
  215. $this->error("上传失败(1001)");
  216. } else {
  217. $checkDeleteFile($attachment, $upload);
  218. $this->success("上传成功", '', ['url' => "/" . $key, 'fullurl' => cdnurl("/" . $key, true)]);
  219. }
  220. } else {
  221. //默认普通上传文件
  222. $file = $this->request->file('file');
  223. try {
  224. $upload = new Upload($file);
  225. $file = $upload->chunk($chunkid, $chunkindex, $chunkcount);
  226. } catch (UploadException $e) {
  227. $this->error($e->getMessage());
  228. }
  229. try {
  230. $params = array(
  231. 'Bucket' => $config['bucket'],
  232. 'Key' => $key,
  233. 'UploadId' => $uploadId,
  234. 'PartNumber' => $chunkindex + 1,
  235. 'Body' => $file->fread($file->getSize())
  236. );
  237. $ret = $oss->uploadPart($params);
  238. $etag = $ret['ETag'];
  239. } catch (\Exception $e) {
  240. $this->error($e->getMessage());
  241. }
  242. $this->success("上传成功", "", [], 3, ['ETag' => $etag]);
  243. }
  244. } else {
  245. $attachment = null;
  246. //默认普通上传文件
  247. $file = $this->request->file('file');
  248. try {
  249. $upload = new Upload($file);
  250. $attachment = $upload->upload();
  251. } catch (UploadException $e) {
  252. $this->error($e->getMessage());
  253. }
  254. //文件绝对路径
  255. $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
  256. $url = $attachment->url;
  257. try {
  258. $ret = $oss->upload($config['bucket'], ltrim($attachment->url, "/"), $upload->getFile());
  259. //成功不做任何操作
  260. } catch (\Exception $e) {
  261. $checkDeleteFile($attachment, $upload, true);
  262. \think\Log::write($e->getMessage());
  263. $this->error("上传失败(1002)");
  264. }
  265. $checkDeleteFile($attachment, $upload);
  266. // 记录云存储记录
  267. $data = $attachment->toArray();
  268. unset($data['id']);
  269. $data['storage'] = 'cos';
  270. Attachment::create($data, true);
  271. $this->success("上传成功", '', ['url' => $url, 'fullurl' => cdnurl($url, true)]);
  272. }
  273. return;
  274. }
  275. /**
  276. * 回调
  277. */
  278. public function notify()
  279. {
  280. $this->check();
  281. $config = get_addon_config('cos');
  282. if ($config['uploadmode'] != 'client') {
  283. $this->error("无需执行该操作");
  284. }
  285. $this->request->filter('trim,strip_tags,htmlspecialchars,xss_clean');
  286. $size = $this->request->post('size/d');
  287. $name = $this->request->post('name', '');
  288. $md5 = $this->request->post('md5', '');
  289. $type = $this->request->post('type', '');
  290. $url = $this->request->post('url', '');
  291. $width = $this->request->post('width/d');
  292. $height = $this->request->post('height/d');
  293. $category = $this->request->post('category', '');
  294. $category = array_key_exists($category, config('site.attachmentcategory') ?? []) ? $category : '';
  295. $suffix = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  296. $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
  297. $attachment = Attachment::where('url', $url)->where('storage', 'cos')->find();
  298. if (!$attachment) {
  299. $params = array(
  300. 'category' => $category,
  301. 'admin_id' => (int)session('admin.id'),
  302. 'user_id' => (int)$this->auth->id,
  303. 'filesize' => $size,
  304. 'filename' => $name,
  305. 'imagewidth' => $width,
  306. 'imageheight' => $height,
  307. 'imagetype' => $suffix,
  308. 'imageframes' => 0,
  309. 'mimetype' => $type,
  310. 'url' => $url,
  311. 'uploadtime' => time(),
  312. 'storage' => 'cos',
  313. 'sha1' => $md5,
  314. );
  315. Attachment::create($params, true);
  316. }
  317. $this->success();
  318. }
  319. /**
  320. * 检查签名是否正确或过期
  321. */
  322. protected function check()
  323. {
  324. $costoken = $this->request->post('costoken', '', 'trim');
  325. if (!$costoken) {
  326. $this->error("参数不正确(code:1)");
  327. }
  328. $config = get_addon_config('cos');
  329. list($appId, $sign, $data) = explode(':', $costoken);
  330. if (!$appId || !$sign || !$data) {
  331. $this->error("参数不正确(code:2)");
  332. }
  333. if ($appId !== $config['appId']) {
  334. $this->error("参数不正确(code:3)");
  335. }
  336. if ($sign !== base64_encode(hash_hmac('sha1', base64_decode($data), $config['secretKey'], true))) {
  337. $this->error("签名不正确");
  338. }
  339. $json = json_decode(base64_decode($data), true);
  340. if ($json['deadline'] < time()) {
  341. $this->error("请求已经超时");
  342. }
  343. }
  344. }