Upload.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php
  2. namespace Widget;
  3. use Typecho\Common;
  4. use Typecho\Date;
  5. use Typecho\Db\Exception;
  6. use Typecho\Plugin;
  7. use Typecho\Widget;
  8. use Widget\Base\Contents;
  9. if (!defined('__TYPECHO_ROOT_DIR__')) {
  10. exit;
  11. }
  12. /**
  13. * 上传组件
  14. *
  15. * @author qining
  16. * @category typecho
  17. * @package Widget
  18. */
  19. class Upload extends Contents implements ActionInterface
  20. {
  21. //上传文件目录
  22. public const UPLOAD_DIR = '/usr/uploads';
  23. /**
  24. * 删除文件
  25. *
  26. * @param array $content 文件相关信息
  27. * @return bool
  28. */
  29. public static function deleteHandle(array $content): bool
  30. {
  31. $result = Plugin::factory(Upload::class)->trigger($hasDeleted)->deleteHandle($content);
  32. if ($hasDeleted) {
  33. return $result;
  34. }
  35. return @unlink(__TYPECHO_ROOT_DIR__ . '/' . $content['attachment']->path);
  36. }
  37. /**
  38. * 获取实际文件绝对访问路径
  39. *
  40. * @param array $content 文件相关信息
  41. * @return string
  42. */
  43. public static function attachmentHandle(array $content): string
  44. {
  45. $result = Plugin::factory(Upload::class)->trigger($hasPlugged)->attachmentHandle($content);
  46. if ($hasPlugged) {
  47. return $result;
  48. }
  49. $options = Options::alloc();
  50. return Common::url(
  51. $content['attachment']->path,
  52. defined('__TYPECHO_UPLOAD_URL__') ? __TYPECHO_UPLOAD_URL__ : $options->siteUrl
  53. );
  54. }
  55. /**
  56. * 获取实际文件数据
  57. *
  58. * @param array $content
  59. * @return string
  60. */
  61. public static function attachmentDataHandle(array $content): string
  62. {
  63. $result = Plugin::factory(Upload::class)->trigger($hasPlugged)->attachmentDataHandle($content);
  64. if ($hasPlugged) {
  65. return $result;
  66. }
  67. return file_get_contents(
  68. Common::url(
  69. $content['attachment']->path,
  70. defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
  71. )
  72. );
  73. }
  74. /**
  75. * 初始化函数
  76. */
  77. public function action()
  78. {
  79. if ($this->user->pass('contributor', true) && $this->request->isPost()) {
  80. $this->security->protect();
  81. if ($this->request->is('do=modify&cid')) {
  82. $this->modify();
  83. } else {
  84. $this->upload();
  85. }
  86. } else {
  87. $this->response->setStatus(403);
  88. }
  89. }
  90. /**
  91. * 执行升级程序
  92. *
  93. * @throws Exception
  94. */
  95. public function modify()
  96. {
  97. if (!empty($_FILES)) {
  98. $file = array_pop($_FILES);
  99. if (0 == $file['error'] && is_uploaded_file($file['tmp_name'])) {
  100. $this->db->fetchRow($this->select()->where('table.contents.cid = ?', $this->request->filter('int')->cid)
  101. ->where('table.contents.type = ?', 'attachment'), [$this, 'push']);
  102. if (!$this->have()) {
  103. $this->response->setStatus(404);
  104. exit;
  105. }
  106. if (!$this->allow('edit')) {
  107. $this->response->setStatus(403);
  108. exit;
  109. }
  110. // xhr的send无法支持utf8
  111. if ($this->request->isAjax()) {
  112. $file['name'] = urldecode($file['name']);
  113. }
  114. $result = self::modifyHandle($this->row, $file);
  115. if (false !== $result) {
  116. self::pluginHandle()->beforeModify($result);
  117. $this->update([
  118. 'text' => serialize($result)
  119. ], $this->db->sql()->where('cid = ?', $this->cid));
  120. $this->db->fetchRow($this->select()->where('table.contents.cid = ?', $this->cid)
  121. ->where('table.contents.type = ?', 'attachment'), [$this, 'push']);
  122. /** 增加插件接口 */
  123. self::pluginHandle()->modify($this);
  124. $this->response->throwJson([$this->attachment->url, [
  125. 'cid' => $this->cid,
  126. 'title' => $this->attachment->name,
  127. 'type' => $this->attachment->type,
  128. 'size' => $this->attachment->size,
  129. 'bytes' => number_format(ceil($this->attachment->size / 1024)) . ' Kb',
  130. 'isImage' => $this->attachment->isImage,
  131. 'url' => $this->attachment->url,
  132. 'permalink' => $this->permalink
  133. ]]);
  134. }
  135. }
  136. }
  137. $this->response->throwJson(false);
  138. }
  139. /**
  140. * 修改文件处理函数,如果需要实现自己的文件哈希或者特殊的文件系统,请在options表里把modifyHandle改成自己的函数
  141. *
  142. * @param array $content 老文件
  143. * @param array $file 新上传的文件
  144. * @return mixed
  145. */
  146. public static function modifyHandle(array $content, array $file)
  147. {
  148. if (empty($file['name'])) {
  149. return false;
  150. }
  151. $result = self::pluginHandle()->trigger($hasModified)->modifyHandle($content, $file);
  152. if ($hasModified) {
  153. return $result;
  154. }
  155. $ext = self::getSafeName($file['name']);
  156. if ($content['attachment']->type != $ext) {
  157. return false;
  158. }
  159. $path = Common::url(
  160. $content['attachment']->path,
  161. defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
  162. );
  163. $dir = dirname($path);
  164. //创建上传目录
  165. if (!is_dir($dir)) {
  166. if (!self::makeUploadDir($dir)) {
  167. return false;
  168. }
  169. }
  170. if (isset($file['tmp_name'])) {
  171. @unlink($path);
  172. //移动上传文件
  173. if (!@move_uploaded_file($file['tmp_name'], $path)) {
  174. return false;
  175. }
  176. } elseif (isset($file['bytes'])) {
  177. @unlink($path);
  178. //直接写入文件
  179. if (!file_put_contents($path, $file['bytes'])) {
  180. return false;
  181. }
  182. } elseif (isset($file['bits'])) {
  183. @unlink($path);
  184. //直接写入文件
  185. if (!file_put_contents($path, $file['bits'])) {
  186. return false;
  187. }
  188. } else {
  189. return false;
  190. }
  191. if (!isset($file['size'])) {
  192. $file['size'] = filesize($path);
  193. }
  194. //返回相对存储路径
  195. return [
  196. 'name' => $content['attachment']->name,
  197. 'path' => $content['attachment']->path,
  198. 'size' => $file['size'],
  199. 'type' => $content['attachment']->type,
  200. 'mime' => $content['attachment']->mime
  201. ];
  202. }
  203. /**
  204. * 获取安全的文件名
  205. *
  206. * @param string $name
  207. * @return string
  208. */
  209. private static function getSafeName(string &$name): string
  210. {
  211. $name = str_replace(['"', '<', '>'], '', $name);
  212. $name = str_replace('\\', '/', $name);
  213. $name = false === strpos($name, '/') ? ('a' . $name) : str_replace('/', '/a', $name);
  214. $info = pathinfo($name);
  215. $name = substr($info['basename'], 1);
  216. return isset($info['extension']) ? strtolower($info['extension']) : '';
  217. }
  218. /**
  219. * 创建上传路径
  220. *
  221. * @param string $path 路径
  222. * @return boolean
  223. */
  224. private static function makeUploadDir(string $path): bool
  225. {
  226. $path = preg_replace("/\\\+/", '/', $path);
  227. $current = rtrim($path, '/');
  228. $last = $current;
  229. while (!is_dir($current) && false !== strpos($path, '/')) {
  230. $last = $current;
  231. $current = dirname($current);
  232. }
  233. if ($last == $current) {
  234. return true;
  235. }
  236. if (!@mkdir($last, 0755)) {
  237. return false;
  238. }
  239. return self::makeUploadDir($path);
  240. }
  241. /**
  242. * 执行升级程序
  243. *
  244. * @throws Exception
  245. */
  246. public function upload()
  247. {
  248. if (!empty($_FILES)) {
  249. $file = array_pop($_FILES);
  250. if (0 == $file['error'] && is_uploaded_file($file['tmp_name'])) {
  251. // xhr的send无法支持utf8
  252. if ($this->request->isAjax()) {
  253. $file['name'] = urldecode($file['name']);
  254. }
  255. $result = self::uploadHandle($file);
  256. if (false !== $result) {
  257. self::pluginHandle()->beforeUpload($result);
  258. $struct = [
  259. 'title' => $result['name'],
  260. 'slug' => $result['name'],
  261. 'type' => 'attachment',
  262. 'status' => 'publish',
  263. 'text' => serialize($result),
  264. 'allowComment' => 1,
  265. 'allowPing' => 0,
  266. 'allowFeed' => 1
  267. ];
  268. if (isset($this->request->cid)) {
  269. $cid = $this->request->filter('int')->cid;
  270. if ($this->isWriteable($this->db->sql()->where('cid = ?', $cid))) {
  271. $struct['parent'] = $cid;
  272. }
  273. }
  274. $insertId = $this->insert($struct);
  275. $this->db->fetchRow($this->select()->where('table.contents.cid = ?', $insertId)
  276. ->where('table.contents.type = ?', 'attachment'), [$this, 'push']);
  277. /** 增加插件接口 */
  278. self::pluginHandle()->upload($this);
  279. $this->response->throwJson([$this->attachment->url, [
  280. 'cid' => $insertId,
  281. 'title' => $this->attachment->name,
  282. 'type' => $this->attachment->type,
  283. 'size' => $this->attachment->size,
  284. 'bytes' => number_format(ceil($this->attachment->size / 1024)) . ' Kb',
  285. 'isImage' => $this->attachment->isImage,
  286. 'url' => $this->attachment->url,
  287. 'permalink' => $this->permalink
  288. ]]);
  289. }
  290. }
  291. }
  292. $this->response->throwJson(false);
  293. }
  294. /**
  295. * 上传文件处理函数,如果需要实现自己的文件哈希或者特殊的文件系统,请在options表里把uploadHandle改成自己的函数
  296. *
  297. * @param array $file 上传的文件
  298. * @return mixed
  299. */
  300. public static function uploadHandle(array $file)
  301. {
  302. if (empty($file['name'])) {
  303. return false;
  304. }
  305. $result = self::pluginHandle()->trigger($hasUploaded)->uploadHandle($file);
  306. if ($hasUploaded) {
  307. return $result;
  308. }
  309. $ext = self::getSafeName($file['name']);
  310. if (!self::checkFileType($ext)) {
  311. return false;
  312. }
  313. $date = new Date();
  314. $path = Common::url(
  315. defined('__TYPECHO_UPLOAD_DIR__') ? __TYPECHO_UPLOAD_DIR__ : self::UPLOAD_DIR,
  316. defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
  317. ) . '/' . $date->year . '/' . $date->month;
  318. //创建上传目录
  319. if (!is_dir($path)) {
  320. if (!self::makeUploadDir($path)) {
  321. return false;
  322. }
  323. }
  324. //获取文件名
  325. $fileName = sprintf('%u', crc32(uniqid())) . '.' . $ext;
  326. $path = $path . '/' . $fileName;
  327. if (isset($file['tmp_name'])) {
  328. //移动上传文件
  329. if (!@move_uploaded_file($file['tmp_name'], $path)) {
  330. return false;
  331. }
  332. } elseif (isset($file['bytes'])) {
  333. //直接写入文件
  334. if (!file_put_contents($path, $file['bytes'])) {
  335. return false;
  336. }
  337. } elseif (isset($file['bits'])) {
  338. //直接写入文件
  339. if (!file_put_contents($path, $file['bits'])) {
  340. return false;
  341. }
  342. } else {
  343. return false;
  344. }
  345. if (!isset($file['size'])) {
  346. $file['size'] = filesize($path);
  347. }
  348. //返回相对存储路径
  349. return [
  350. 'name' => $file['name'],
  351. 'path' => (defined('__TYPECHO_UPLOAD_DIR__') ? __TYPECHO_UPLOAD_DIR__ : self::UPLOAD_DIR)
  352. . '/' . $date->year . '/' . $date->month . '/' . $fileName,
  353. 'size' => $file['size'],
  354. 'type' => $ext,
  355. 'mime' => Common::mimeContentType($path)
  356. ];
  357. }
  358. /**
  359. * 检查文件名
  360. *
  361. * @access private
  362. * @param string $ext 扩展名
  363. * @return boolean
  364. */
  365. public static function checkFileType(string $ext): bool
  366. {
  367. $options = Options::alloc();
  368. return in_array($ext, $options->allowedAttachmentTypes);
  369. }
  370. }