Api.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <?php
  2. namespace app\controller;
  3. use app\BaseController;
  4. use app\model\FileModel;
  5. use app\model\LinkModel;
  6. use app\model\SettingModel;
  7. use DOMDocument;
  8. use GuzzleHttp\Client;
  9. use GuzzleHttp\Exception\RequestException;
  10. use think\facade\Cache;
  11. use think\facade\Filesystem;
  12. use think\facade\View;
  13. use think\helper\Str;
  14. class Api extends BaseController
  15. {
  16. public function site(): \think\response\Json
  17. {
  18. return $this->success("ok", [
  19. 'email' => $this->systemSetting('email', ''),
  20. 'qqGroup' => $this->systemSetting("qqGroup", ''),
  21. 'beianMps' => $this->systemSetting("beianMps", ''),
  22. 'copyright' => $this->systemSetting("copyright", ''),
  23. "recordNumber" => $this->systemSetting("recordNumber", ''),
  24. "mobileRecordNumber" => $this->systemSetting('mobileRecordNumber', '0'),
  25. "auth" => $this->auth,
  26. "logo" => $this->systemSetting('logo', ''),
  27. "qq_login" => $this->systemSetting('qq_login', '0'),
  28. "loginCloseRecordNumber" => $this->systemSetting('loginCloseRecordNumber', '0'),
  29. "is_push_link_store" => $this->auth ? $this->systemSetting('is_push_link_store', '0') : '0',
  30. "is_push_link_store_tips" => $this->systemSetting('is_push_link_store_tips', '0'),
  31. "is_push_link_status" => $this->systemSetting("is_push_link_status", '0'),
  32. 'google_ext_link' => $this->systemSetting("google_ext_link", ''),
  33. 'edge_ext_link' => $this->systemSetting("edge_ext_link", ''),
  34. 'local_ext_link' => $this->systemSetting("local_ext_link", ''),
  35. "customAbout" => $this->systemSetting("customAbout", ''),
  36. "user_register" => $this->systemSetting("user_register", '0', true),
  37. "tip" => [
  38. "ds_status" => $this->systemSetting('ds_status', '0', true),
  39. "ds_template" => $this->systemSetting('ds_template', 'org', true),
  40. "ds_alipay_img" => $this->systemSetting('ds_alipay_img', '', true),
  41. "ds_wx_img" => $this->systemSetting('ds_wx_img', '', true),
  42. "ds_custom_url" => $this->systemSetting("ds_custom_url", '', true),
  43. 'ds_title' => $this->systemSetting('ds_title', '', true),
  44. 'ds_tips' => $this->systemSetting('ds_tips', '', true)
  45. ]
  46. ]);
  47. }
  48. public function background(): \think\response\File
  49. {
  50. return download('static/background.jpeg', 'background.jpeg')->mimeType(\PluginStaticSystem::mimeType('static/background.jpeg'))->force(false)->expire(60 * 60 * 24 * 3);
  51. }
  52. //获取默认壁纸
  53. function DefBg(): \think\response\Json
  54. {
  55. $config = $this->systemSetting('defaultTab', 'static/defaultTab.json', true);
  56. if ($config) {
  57. $fp = public_path() . $config;
  58. if (file_exists($fp)) {
  59. $file = file_get_contents($fp);
  60. $json = json_decode($file, true);
  61. if (isset($json['config']['theme']['backgroundImage'])) {
  62. $bg = $json['config']['theme']['backgroundImage'];
  63. $bgMime = $json['config']['theme']['backgroundMime'] ?? 0;
  64. return $this->success("ok", ['background' => $bg, "mime" => $bgMime]);
  65. }
  66. }
  67. }
  68. return $this->success("ok", ['background' => "static/background.jpeg", "mime" => 0]);
  69. }
  70. function globalNotify(): \think\response\Json
  71. {
  72. $info = SettingModel::Config("globalNotify", false);
  73. if ($info) {
  74. $info = json_decode($info, true);
  75. $info['html'] = modifyImageUrls($info['html'], request()->root(true));
  76. if (isset($info['status']) && $info['status'] == 1) {
  77. return $this->success('ok', json_encode($info, JSON_UNESCAPED_UNICODE));
  78. }
  79. }
  80. return $this->error('empty');
  81. }
  82. //获取邮件验证码
  83. function getMailCode(): \think\response\Json
  84. {
  85. $mail = $this->request->post("mail", false);
  86. $code = rand(100000, 999999);
  87. if ($mail) {
  88. if (Cache::get('code' . $mail)) {
  89. return $this->success("请勿频繁获取验证码");
  90. }
  91. $k = SettingModel::Config('smtp_code_template', false);
  92. if ($k === false || mb_strlen(trim($k)) == 0) {
  93. $k = '
  94. <div style="border:1px #DEDEDE solid;border-top:3px #009944 solid;padding:25px;background-color:#FFF;">
  95. <div style="font-size:17px;font-weight:bold;">邮箱验证码</div>
  96. <div style="font-size:14px;line-height:36px;padding-top:15px;padding-bottom:15px;">
  97. 尊敬的用户,您好!<br>
  98. 您的验证码是:<b style="color: #1e9fff">{$code}</b>。5分钟内有效,请尽快验证。
  99. </div>
  100. <div style="line-height:15px;">
  101. 此致
  102. </div>
  103. </div>
  104. ';
  105. }
  106. $html = View::display($k, ['time' => date('Y-m-d H:i:s'), 'code' => $code]);
  107. try {
  108. $status = \Mail::send($mail, $html);
  109. if ($status) {
  110. Cache::set('code' . $mail, $code, 300);
  111. return $this->success('发送成功');
  112. }
  113. } catch (\Exception $e) {
  114. return $this->error($e->getMessage());
  115. }
  116. }
  117. return $this->error('发送失败');
  118. }
  119. private function addHttpProtocolRemovePath($url): string
  120. {
  121. // 解析URL
  122. $parsedUrl = parse_url($url);
  123. // 检查是否已经有协议,如果没有则添加http://
  124. if (!isset($parsedUrl['scheme'])) {
  125. // 检查是否以 // 开头,如果是,则转换为相对协议
  126. if (isset($parsedUrl['host']) && strpos($url, '//') === 0) {
  127. $url = 'http:' . $url;
  128. } else {
  129. $url = 'http://' . $url;
  130. }
  131. } else {
  132. // 如果有协议但没有路径,保留原样
  133. $url = $parsedUrl['scheme'] . '://';
  134. // 如果有主机,则添加主机部分
  135. if (isset($parsedUrl['host'])) {
  136. $url .= $parsedUrl['host'];
  137. // 如果有端口号,则添加端口号
  138. if (isset($parsedUrl['port'])) {
  139. $url .= ':' . $parsedUrl['port'];
  140. }
  141. }
  142. }
  143. return $url;
  144. }
  145. private function addHttpProtocol($url)
  146. {
  147. // 检查是否已经有协议,如果没有则添加http://
  148. if (!parse_url($url, PHP_URL_SCHEME)) {
  149. // 检查是否以 // 开头,如果是,则转换为相对协议
  150. if (strpos($url, '//') === 0) {
  151. $url = 'https:' . $url;
  152. } else {
  153. $url = 'http://' . $url;
  154. }
  155. }
  156. return $url;
  157. }
  158. private function hasOnlyPath($url): bool
  159. {
  160. if (!$url) {
  161. return false;
  162. }
  163. $parsedUrl = parse_url($url);
  164. // 检查是否存在路径但不存在域名和协议
  165. if (isset($parsedUrl['path']) && !isset($parsedUrl['host']) && !isset($parsedUrl['scheme'])) {
  166. return true;
  167. }
  168. return false;
  169. }
  170. function getIcon(): \think\response\Json
  171. {
  172. $avatar = $this->request->post('avatar');
  173. if ($avatar) {
  174. $remote_avatar = $this->systemSetting('remote_avatar', 'https://avatar.mtab.cc/6.x/icons/png?seed=', true);
  175. $str = $this->downloadFile($remote_avatar . $avatar, md5($avatar) . '.png');
  176. return $this->success(['src' => $str]);
  177. }
  178. $url = $this->request->post('url', false);
  179. if (!$url) {
  180. return $this->error('没有抓取到图标');
  181. }
  182. $realUrl = $this->addHttpProtocolRemovePath($url);
  183. $cdn = $this->systemSetting('assets_host', '');
  184. $icon = '';
  185. $title = '';
  186. try {
  187. $client = \Axios::http();
  188. $response = $client->get($realUrl, [
  189. 'headers' => [
  190. 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  191. ]
  192. ]);
  193. $status = $response->getStatusCode();
  194. if ($status === 200) {
  195. $contentType = $response->getHeaderLine('Content-Type');
  196. if (stripos($contentType, 'text/html') === false) {
  197. return $this->error('没有抓取到图标');
  198. }
  199. $body = $response->getBody()->getContents();
  200. $dom = new DOMDocument();
  201. @$dom->loadHTML($body);
  202. // 获取页面标题
  203. $titles = $dom->getElementsByTagName('title');
  204. if ($titles->length > 0) {
  205. $title = $titles->item(0)->textContent;
  206. }
  207. // 查找常见的图标类型
  208. $iconTags = $this->findIcons($dom, $realUrl);
  209. if (!empty($iconTags)) {
  210. // 处理第一个找到的图标
  211. $iconHref = $iconTags[0]['href'];
  212. $icon = $this->processIcon($iconHref, $realUrl, $cdn);
  213. }
  214. }
  215. // 如果没有找到图标或抓取失败,则尝试获取 favicon.ico
  216. if (empty($icon)) {
  217. $icon = $this->fetchFavicon($realUrl, $cdn);
  218. }
  219. if ($icon) {
  220. return $this->success(['src' => $icon, 'name' => $title]);
  221. }
  222. } catch (\Exception $e) {
  223. return $this->error('没有抓取到图标');
  224. }
  225. return $this->error('没有抓取到图标');
  226. }
  227. private function findIcons($dom, $baseUrl): array
  228. {
  229. $icons = [];
  230. $iconSelectors = [
  231. 'link[rel=icon]',
  232. 'link[rel=shortcut icon]',
  233. 'link[rel=apple-touch-icon]',
  234. 'link[rel=apple-touch-icon-precomposed]',
  235. 'link[rel=mask-icon]'
  236. ];
  237. foreach ($iconSelectors as $selector) {
  238. foreach ($dom->getElementsByTagName('link') as $icon) {
  239. if (in_array($icon->getAttribute('rel'), array_map('trim', $iconSelectors))) {
  240. $href = $icon->getAttribute('href');
  241. if ($this->hasOnlyPath($href)) {
  242. $href = rtrim($baseUrl, '/') . '/' . ltrim($href, '/');
  243. }
  244. $icons[] = ['href' => $href];
  245. }
  246. }
  247. }
  248. return $icons;
  249. }
  250. private function processIcon($iconHref, $realUrl, $cdn): string
  251. {
  252. try {
  253. $client = \Axios::http();
  254. $response = $client->get($iconHref, [
  255. 'headers' => [
  256. 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  257. ]
  258. ]);
  259. $contentType = $response->getHeaderLine('Content-Type');
  260. // 根据 content-type 确定文件格式
  261. if (preg_match('/(png|jpg|jpeg|x-icon|svg\+xml)$/i', $contentType, $matches)) {
  262. $fileFormats = [
  263. 'png' => 'png',
  264. 'jpg' => 'jpg',
  265. 'jpeg' => 'jpeg',
  266. 'x-icon' => 'ico',
  267. 'svg+xml' => 'svg',
  268. ];
  269. $fileFormat = strtolower($matches[1]);
  270. $iconPath = $this->downloadFile($iconHref, md5($realUrl) . '.' . $fileFormats[$fileFormat]);
  271. return $cdn . $iconPath;
  272. }
  273. } catch (\Exception $e) {
  274. // 直接返回失败
  275. return '';
  276. }
  277. return '';
  278. }
  279. private function fetchFavicon($realUrl, $cdn): string
  280. {
  281. try {
  282. $client = \Axios::http();
  283. $faviconUrl = rtrim($realUrl, '/') . '/favicon.ico';
  284. $response = $client->get($faviconUrl, [
  285. 'headers' => [
  286. 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  287. ]
  288. ]);
  289. $status = $response->getStatusCode();
  290. if ($status === 200) {
  291. $iconPath = $this->downloadFile($faviconUrl, md5($realUrl) . '.ico');
  292. return $cdn . $iconPath;
  293. }
  294. } catch (\Exception $e) {
  295. // 直接返回失败
  296. return '';
  297. }
  298. return '';
  299. }
  300. private function downloadFile($url, $name)
  301. {
  302. $user = $this->getUser();
  303. $client = \Axios::http();
  304. $path = '/images/' . date('Y/m/d/');
  305. $remotePath = public_path() . $path;
  306. $downloadPath = $remotePath . $name;
  307. if (!is_dir($remotePath)) {
  308. mkdir($remotePath, 0755, true);
  309. }
  310. try {
  311. $response = $client->request('GET', $url, [
  312. 'sink' => $downloadPath,
  313. 'headers' => [
  314. 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  315. ]
  316. ]);
  317. $realPath = joinPath(public_path(), $path . $name);
  318. $hash = hash_file('md5', $realPath);
  319. $check = FileModel::where('hash', $hash)->find();
  320. if ($check) {
  321. try {
  322. if (joinPath(public_path(), $check['path']) !== $realPath) {
  323. unlink($realPath);
  324. }
  325. } catch (\Exception $e) {
  326. }
  327. return $check['path'];
  328. }
  329. FileModel::addFile($path . $name, $user['user_id'] ?? null);
  330. return $path . $name;
  331. } catch (RequestException $e) {
  332. }
  333. return false;
  334. }
  335. function renderIco(): \think\Response
  336. {
  337. $send = $this->request->get('seed');
  338. $client = new Client();
  339. $remote_avatar = $this->systemSetting('remote_avatar', 'https://avatar.mtab.cc/6.x/icons/png?seed=', true);
  340. $response = $client->get($remote_avatar . urlencode($send), [
  341. 'stream' => true,
  342. 'timeout' => 10,
  343. ]);
  344. return response($response->getBody(), 200, ['Content-Type' => $response->getHeader("content-type")[0]]);
  345. }
  346. function upload(): \think\response\Json
  347. {
  348. $user = $this->getUser();
  349. if (!$user) {
  350. if ($this->systemSetting('touristUpload') !== '1') {
  351. //如果没有开启游客上传
  352. return $this->error('管理员已关闭游客上传!请登录后使用');
  353. }
  354. }
  355. $type = $this->request->header("Up-Type", '');
  356. $file = $this->request->file('file');
  357. if (empty($file)) {
  358. return $this->error('not File');
  359. }
  360. $maxSize = (double)$this->systemSetting('upload_size', '2');
  361. if ($file->getSize() > 1024 * 1024 * $maxSize) {
  362. $limit = $maxSize < 1 ? ($maxSize * 1000) . 'KB' : ($maxSize) . 'MB';
  363. return $this->error("文件最大$limit,请压缩后再试");
  364. }
  365. if (in_array(strtolower($file->getOriginalExtension()), ['png', 'jpg', 'jpeg', 'webp', 'ico', 'svg'])) {
  366. // 验证文件并保存
  367. try {
  368. $cdn = $this->systemSetting('assets_host', '/', true);
  369. // 构建保存路径
  370. $savePath = '/images/' . date('Y/m/d');
  371. $hash = Str::random(32);
  372. $fileName = $hash . '.' . $file->getOriginalExtension();
  373. $hash = $file->hash('md5');
  374. $check = FileModel::where('hash', $hash)->find();
  375. if ($check) {
  376. try {
  377. unlink($file->getRealPath());
  378. } catch (\Exception $e) {
  379. }
  380. $dt = ['url' => joinPath($cdn, $check['path'])];
  381. return $this->success($dt);
  382. }
  383. $filePath = Filesystem::disk('images')->putFileAs($savePath, $file, $fileName);
  384. $minPath = '';
  385. if ($type == 'icon' || $type == 'avatar') {
  386. $fp = joinPath(public_path(), $filePath);
  387. $image = new \ImageBack($fp);
  388. $image->resize(144, 0)->save($fp);
  389. } else if ($type == 'AdminBackground') {
  390. $minPath = joinPath($savePath, "/min_$fileName");
  391. $fp = joinPath(public_path(), $filePath);
  392. $image = new \ImageBack($fp);
  393. $image->resize(400, 0)->save(joinPath(public_path(), $minPath));
  394. FileModel::addFile($minPath, $user['user_id'] ?? null);
  395. }
  396. FileModel::addFile($filePath, $user['user_id'] ?? null);
  397. return $this->success(['url' => $cdn . $filePath, "minUrl" => joinPath($cdn, $minPath)]);
  398. } catch (\think\exception\ValidateException $e) {
  399. return $this->error($e->getMessage());
  400. // 验证失败,给出错误提示
  401. // ...
  402. }
  403. }
  404. return $this->error('上传失败');
  405. }
  406. function AdminUpload(): \think\response\Json
  407. {
  408. $user = $this->getAdmin();
  409. $file = $this->request->file('file');
  410. if (empty($file)) {
  411. return $this->error('not File');
  412. }
  413. if ($file->getSize() > 1024 * 1024 * 8) {
  414. return $this->error('文件最大8MB,请压缩后再试');
  415. }
  416. // 验证文件并保存
  417. try {
  418. // 构建保存路径
  419. $savePath = '/images/' . date('Y/m/d');
  420. $hash = Str::random(32);
  421. $fileName = $hash . '.' . $file->getOriginalExtension();
  422. $filePath = Filesystem::disk('images')->putFileAs($savePath, $file, $fileName);
  423. $cdn = $this->systemSetting('assets_host', '/', true);
  424. FileModel::addFile($filePath, $user['user_id'] ?? null);
  425. return $this->success(['url' => $cdn . $filePath]);
  426. } catch (\think\exception\ValidateException $e) {
  427. // 验证失败,给出错误提示
  428. // ...
  429. }
  430. return $this->error('上传失败');
  431. }
  432. function refresh(): \think\response\Json
  433. {
  434. $user = $this->getUser();
  435. if ($user) {
  436. $data = [];
  437. $data['link_update_time'] = LinkModel::where("user_id", $user['user_id'])->value("update_time");
  438. return $this->success("ok", $data);
  439. }
  440. return $this->error("not login");
  441. }
  442. }