common.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <?php
  2. // 应用公共文件
  3. use think\App;
  4. use ba\Filesystem;
  5. use think\Response;
  6. use think\facade\Db;
  7. use think\facade\Lang;
  8. use think\facade\Event;
  9. use think\facade\Config;
  10. use voku\helper\AntiXSS;
  11. use app\admin\model\Config as configModel;
  12. use think\exception\HttpResponseException;
  13. use Symfony\Component\HttpFoundation\IpUtils;
  14. if (!function_exists('__')) {
  15. /**
  16. * 语言翻译
  17. * @param string $name 被翻译字符
  18. * @param array $vars 替换字符数组
  19. * @param string $lang 翻译语言
  20. * @return mixed
  21. */
  22. function __(string $name, array $vars = [], string $lang = ''): mixed
  23. {
  24. if (is_numeric($name) || !$name) {
  25. return $name;
  26. }
  27. return Lang::get($name, $vars, $lang);
  28. }
  29. }
  30. if (!function_exists('filter')) {
  31. /**
  32. * 输入过滤
  33. * 富文本反XSS请使用 clean_xss,也就不需要及不能再 filter 了
  34. * @param string $string 要过滤的字符串
  35. * @return string
  36. */
  37. function filter(string $string): string
  38. {
  39. // 去除字符串两端空格(对防代码注入有一定作用)
  40. $string = trim($string);
  41. // 过滤html和php标签
  42. $string = strip_tags($string);
  43. // 特殊字符转实体
  44. return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
  45. }
  46. }
  47. if (!function_exists('clean_xss')) {
  48. /**
  49. * 清理XSS
  50. * 通常只用于富文本,比 filter 慢
  51. * @param string $string
  52. * @return string
  53. */
  54. function clean_xss(string $string): string
  55. {
  56. $antiXss = new AntiXSS();
  57. // 允许 style 属性(style="list-style-image: url(javascript:alert(0))" 任然可被正确过滤)
  58. $antiXss->removeEvilAttributes(['style']);
  59. // 检查到 xss 代码之后使用 cleanXss 替换它
  60. $antiXss->setReplacement('cleanXss');
  61. return $antiXss->xss_clean($string);
  62. }
  63. }
  64. if (!function_exists('htmlspecialchars_decode_improve')) {
  65. /**
  66. * html解码增强
  67. * 被 filter函数 内的 htmlspecialchars 编码的字符串,需要用此函数才能完全解码
  68. * @param string $string
  69. * @param int $flags
  70. * @return string
  71. */
  72. function htmlspecialchars_decode_improve(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401): string
  73. {
  74. return htmlspecialchars_decode($string, $flags);
  75. }
  76. }
  77. if (!function_exists('get_sys_config')) {
  78. /**
  79. * 获取站点的系统配置,不传递参数则获取所有配置项
  80. * @param string $name 变量名
  81. * @param string $group 变量分组,传递此参数来获取某个分组的所有配置项
  82. * @param bool $concise 是否开启简洁模式,简洁模式下,获取多项配置时只返回配置的键值对
  83. * @return mixed
  84. * @throws Throwable
  85. */
  86. function get_sys_config(string $name = '', string $group = '', bool $concise = true): mixed
  87. {
  88. if ($name) {
  89. // 直接使用->value('value')不能使用到模型的类型格式化
  90. $config = configModel::cache($name, null, configModel::$cacheTag)->where('name', $name)->find();
  91. if ($config) $config = $config['value'];
  92. } else {
  93. if ($group) {
  94. $temp = configModel::cache('group' . $group, null, configModel::$cacheTag)->where('group', $group)->select()->toArray();
  95. } else {
  96. $temp = configModel::cache('sys_config_all', null, configModel::$cacheTag)->order('weigh desc')->select()->toArray();
  97. }
  98. if ($concise) {
  99. $config = [];
  100. foreach ($temp as $item) {
  101. $config[$item['name']] = $item['value'];
  102. }
  103. } else {
  104. $config = $temp;
  105. }
  106. }
  107. return $config;
  108. }
  109. }
  110. if (!function_exists('get_route_remark')) {
  111. /**
  112. * 获取当前路由后台菜单规则的备注信息
  113. * @return string
  114. */
  115. function get_route_remark(): string
  116. {
  117. $controllerName = request()->controller(true);
  118. $actionName = request()->action(true);
  119. $path = str_replace('.', '/', $controllerName);
  120. $remark = Db::name('admin_rule')
  121. ->where('name', $path)
  122. ->whereOr('name', $path . '/' . $actionName)
  123. ->value('remark');
  124. return __((string)$remark);
  125. }
  126. }
  127. if (!function_exists('full_url')) {
  128. /**
  129. * 获取资源完整url地址;若安装了云存储或 config/buildadmin.php 配置了CdnUrl,则自动使用对应的CdnUrl
  130. * @param string $relativeUrl 资源相对地址 不传入则获取域名
  131. * @param string|bool $domain 是否携带域名 或者直接传入域名
  132. * @param string $default 默认值
  133. * @return string
  134. */
  135. function full_url(string $relativeUrl = '', string|bool $domain = true, string $default = ''): string
  136. {
  137. // 存储/上传资料配置
  138. Event::trigger('uploadConfigInit', App::getInstance());
  139. $cdnUrl = Config::get('buildadmin.cdn_url');
  140. if (!$cdnUrl) {
  141. $cdnUrl = request()->upload['cdn'] ?? '//' . request()->host();
  142. }
  143. if ($domain === true) {
  144. $domain = $cdnUrl;
  145. } elseif ($domain === false) {
  146. $domain = '';
  147. }
  148. $relativeUrl = $relativeUrl ?: $default;
  149. if (!$relativeUrl) return $domain;
  150. $regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i";
  151. if (preg_match('/^http(s)?:\/\//', $relativeUrl) || preg_match($regex, $relativeUrl) || $domain === false) {
  152. return $relativeUrl;
  153. }
  154. $url = $domain . $relativeUrl;
  155. $cdnUrlParams = Config::get('buildadmin.cdn_url_params');
  156. if ($domain === $cdnUrl && $cdnUrlParams) {
  157. $separator = str_contains($url, '?') ? '&' : '?';
  158. $url .= $separator . $cdnUrlParams;
  159. }
  160. return $url;
  161. }
  162. }
  163. if (!function_exists('encrypt_password')) {
  164. /**
  165. * 加密密码
  166. * @deprecated 使用 hash_password 代替
  167. */
  168. function encrypt_password($password, $salt = '', $encrypt = 'md5')
  169. {
  170. return $encrypt($encrypt($password) . $salt);
  171. }
  172. }
  173. if (!function_exists('hash_password')) {
  174. /**
  175. * 创建密码散列(hash)
  176. */
  177. function hash_password(string $password): string
  178. {
  179. return password_hash($password, PASSWORD_DEFAULT);
  180. }
  181. }
  182. if (!function_exists('verify_password')) {
  183. /**
  184. * 验证密码是否和散列值匹配
  185. * @param string $password 密码
  186. * @param string $hash 散列值
  187. * @param array $extend 扩展数据
  188. */
  189. function verify_password(string $password, string $hash, array $extend = []): bool
  190. {
  191. // 第一个表达式直接检查是否为 password_hash 函数创建的 hash 的典型格式,即:$algo$cost$salt.hash
  192. if (str_starts_with($hash, '$') || password_get_info($hash)['algoName'] != 'unknown') {
  193. return password_verify($password, $hash);
  194. } else {
  195. // 兼容旧版 md5 加密的密码
  196. return encrypt_password($password, $extend['salt'] ?? '') === $hash;
  197. }
  198. }
  199. }
  200. if (!function_exists('str_attr_to_array')) {
  201. /**
  202. * 将字符串属性列表转为数组
  203. * @param string $attr 属性,一行一个,无需引号,比如:class=input-class
  204. * @return array
  205. */
  206. function str_attr_to_array(string $attr): array
  207. {
  208. if (!$attr) return [];
  209. $attr = explode("\n", trim(str_replace("\r\n", "\n", $attr)));
  210. $attrTemp = [];
  211. foreach ($attr as $item) {
  212. $item = explode('=', $item);
  213. if (isset($item[0]) && isset($item[1])) {
  214. $attrVal = $item[1];
  215. if ($item[1] === 'false' || $item[1] === 'true') {
  216. $attrVal = !($item[1] === 'false');
  217. } elseif (is_numeric($item[1])) {
  218. $attrVal = (float)$item[1];
  219. }
  220. if (strpos($item[0], '.')) {
  221. $attrKey = explode('.', $item[0]);
  222. if (isset($attrKey[0]) && isset($attrKey[1])) {
  223. $attrTemp[$attrKey[0]][$attrKey[1]] = $attrVal;
  224. continue;
  225. }
  226. }
  227. $attrTemp[$item[0]] = $attrVal;
  228. }
  229. }
  230. return $attrTemp;
  231. }
  232. }
  233. if (!function_exists('action_in_arr')) {
  234. /**
  235. * 检测一个方法是否在传递的数组内
  236. * @param array $arr
  237. * @return bool
  238. */
  239. function action_in_arr(array $arr = []): bool
  240. {
  241. $arr = is_array($arr) ? $arr : explode(',', $arr);
  242. if (!$arr) {
  243. return false;
  244. }
  245. $arr = array_map('strtolower', $arr);
  246. if (in_array(strtolower(request()->action()), $arr) || in_array('*', $arr)) {
  247. return true;
  248. }
  249. return false;
  250. }
  251. }
  252. if (!function_exists('build_suffix_svg')) {
  253. /**
  254. * 构建文件后缀的svg图片
  255. * @param string $suffix 文件后缀
  256. * @param ?string $background 背景颜色,如:rgb(255,255,255)
  257. * @return string
  258. */
  259. function build_suffix_svg(string $suffix = 'file', string $background = null): string
  260. {
  261. $suffix = mb_substr(strtoupper($suffix), 0, 4);
  262. $total = unpack('L', hash('adler32', $suffix, true))[1];
  263. $hue = $total % 360;
  264. [$r, $g, $b] = hsv2rgb($hue / 360, 0.3, 0.9);
  265. $background = $background ?: "rgb($r,$g,$b)";
  266. return '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
  267. <path style="fill:#E2E5E7;" d="M128,0c-17.6,0-32,14.4-32,32v448c0,17.6,14.4,32,32,32h320c17.6,0,32-14.4,32-32V128L352,0H128z"/>
  268. <path style="fill:#B0B7BD;" d="M384,128h96L352,0v96C352,113.6,366.4,128,384,128z"/>
  269. <polygon style="fill:#CAD1D8;" points="480,224 384,128 480,128 "/>
  270. <path style="fill:' . $background . ';" d="M416,416c0,8.8-7.2,16-16,16H48c-8.8,0-16-7.2-16-16V256c0-8.8,7.2-16,16-16h352c8.8,0,16,7.2,16,16 V416z"/>
  271. <path style="fill:#CAD1D8;" d="M400,432H96v16h304c8.8,0,16-7.2,16-16v-16C416,424.8,408.8,432,400,432z"/>
  272. <g><text><tspan x="220" y="380" font-size="124" font-family="Verdana, Helvetica, Arial, sans-serif" fill="white" text-anchor="middle">' . $suffix . '</tspan></text></g>
  273. </svg>';
  274. }
  275. }
  276. if (!function_exists('get_area')) {
  277. /**
  278. * 获取省份地区数据
  279. * @throws Throwable
  280. */
  281. function get_area(): array
  282. {
  283. $province = request()->get('province', '');
  284. $city = request()->get('city', '');
  285. $where = ['pid' => 0, 'level' => 1];
  286. if ($province !== '') {
  287. $where['pid'] = $province;
  288. $where['level'] = 2;
  289. if ($city !== '') {
  290. $where['pid'] = $city;
  291. $where['level'] = 3;
  292. }
  293. }
  294. return Db::name('area')
  295. ->where($where)
  296. ->field('id as value,name as label')
  297. ->select()
  298. ->toArray();
  299. }
  300. }
  301. if (!function_exists('hsv2rgb')) {
  302. function hsv2rgb($h, $s, $v): array
  303. {
  304. $r = $g = $b = 0;
  305. $i = floor($h * 6);
  306. $f = $h * 6 - $i;
  307. $p = $v * (1 - $s);
  308. $q = $v * (1 - $f * $s);
  309. $t = $v * (1 - (1 - $f) * $s);
  310. switch ($i % 6) {
  311. case 0:
  312. $r = $v;
  313. $g = $t;
  314. $b = $p;
  315. break;
  316. case 1:
  317. $r = $q;
  318. $g = $v;
  319. $b = $p;
  320. break;
  321. case 2:
  322. $r = $p;
  323. $g = $v;
  324. $b = $t;
  325. break;
  326. case 3:
  327. $r = $p;
  328. $g = $q;
  329. $b = $v;
  330. break;
  331. case 4:
  332. $r = $t;
  333. $g = $p;
  334. $b = $v;
  335. break;
  336. case 5:
  337. $r = $v;
  338. $g = $p;
  339. $b = $q;
  340. break;
  341. }
  342. return [
  343. floor($r * 255),
  344. floor($g * 255),
  345. floor($b * 255)
  346. ];
  347. }
  348. }
  349. if (!function_exists('ip_check')) {
  350. /**
  351. * IP检查
  352. * @throws Throwable
  353. */
  354. function ip_check($ip = null): void
  355. {
  356. $ip = is_null($ip) ? request()->ip() : $ip;
  357. $noAccess = get_sys_config('no_access_ip');
  358. $noAccess = !$noAccess ? [] : array_filter(explode("\n", str_replace("\r\n", "\n", $noAccess)));
  359. if ($noAccess && IpUtils::checkIp($ip, $noAccess)) {
  360. $response = Response::create(['msg' => 'No permission request'], 'json', 403);
  361. throw new HttpResponseException($response);
  362. }
  363. }
  364. }
  365. if (!function_exists('set_timezone')) {
  366. /**
  367. * 设置时区
  368. * @throws Throwable
  369. */
  370. function set_timezone($timezone = null): void
  371. {
  372. $defaultTimezone = Config::get('app.default_timezone');
  373. $timezone = is_null($timezone) ? get_sys_config('time_zone') : $timezone;
  374. if ($timezone && $defaultTimezone != $timezone) {
  375. Config::set([
  376. 'app.default_timezone' => $timezone
  377. ]);
  378. date_default_timezone_set($timezone);
  379. }
  380. }
  381. }
  382. if (!function_exists('get_upload_config')) {
  383. /**
  384. * 获取上传配置
  385. * @return array
  386. */
  387. function get_upload_config(): array
  388. {
  389. // 存储/上传资料配置
  390. Event::trigger('uploadConfigInit', App::getInstance());
  391. $uploadConfig = Config::get('upload');
  392. $uploadConfig['max_size'] = Filesystem::fileUnitToByte($uploadConfig['max_size']);
  393. $upload = request()->upload;
  394. if (!$upload) {
  395. $uploadConfig['mode'] = 'local';
  396. return $uploadConfig;
  397. }
  398. unset($upload['cdn']);
  399. return array_merge($upload, $uploadConfig);
  400. }
  401. }
  402. if (!function_exists('get_auth_token')) {
  403. /**
  404. * 获取鉴权 token
  405. * @param array $names
  406. * @return string
  407. */
  408. function get_auth_token(array $names = ['ba', 'token']): string
  409. {
  410. $separators = [
  411. 'header' => ['', '-'], // batoken、ba-token【ba_token 不在 header 的接受列表内因为兼容性不高,改用 http_ba_token】
  412. 'param' => ['', '-', '_'], // batoken、ba-token、ba_token
  413. 'server' => ['_'], // http_ba_token
  414. ];
  415. $tokens = [];
  416. $request = request();
  417. foreach ($separators as $fun => $sps) {
  418. foreach ($sps as $sp) {
  419. $tokens[] = $request->$fun(($fun == 'server' ? 'http_' : '') . implode($sp, $names));
  420. }
  421. }
  422. $tokens = array_filter($tokens);
  423. return array_values($tokens)[0] ?? '';
  424. }
  425. }
  426. if (!function_exists('keys_to_camel_case')) {
  427. /**
  428. * 将数组 key 的命名方式转换为小写驼峰
  429. * @param array $array 被转换的数组
  430. * @param array $keys 要转换的 key,默认所有
  431. * @return array
  432. */
  433. function keys_to_camel_case(array $array, array $keys = []): array
  434. {
  435. $result = [];
  436. foreach ($array as $key => $value) {
  437. // 将键名转换为驼峰命名
  438. $camelCaseKey = $keys && in_array($key, $keys) ? parse_name($key, 1, false) : $key;
  439. if (is_array($value)) {
  440. // 如果值是数组,递归转换
  441. $result[$camelCaseKey] = keys_to_camel_case($value);
  442. } else {
  443. $result[$camelCaseKey] = $value;
  444. }
  445. }
  446. return $result;
  447. }
  448. }