common.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <?php
  2. use Symfony\Component\VarExporter\VarExporter;
  3. use think\addons\Service;
  4. use think\App;
  5. use think\Cache;
  6. use think\Config;
  7. use think\Exception;
  8. use think\exception\HttpException;
  9. use think\exception\HttpResponseException;
  10. use think\Hook;
  11. use think\Loader;
  12. use think\Response;
  13. use think\Route;
  14. // 插件目录
  15. define('ADDON_PATH', ROOT_PATH . 'addons' . DS);
  16. // 定义路由
  17. Route::any('addons/:addon/[:controller]/[:action]', "\\think\\addons\\Route@execute");
  18. // 如果插件目录不存在则创建
  19. if (!is_dir(ADDON_PATH)) {
  20. @mkdir(ADDON_PATH, 0755, true);
  21. }
  22. // 注册类的根命名空间
  23. Loader::addNamespace('addons', ADDON_PATH);
  24. // 监听addon_init
  25. Hook::listen('addon_init');
  26. // 闭包自动识别插件目录配置
  27. Hook::add('app_init', function () {
  28. // 获取开关
  29. $autoload = (bool)Config::get('addons.autoload', false);
  30. // 非正是返回
  31. if (!$autoload) {
  32. return;
  33. }
  34. // 当debug时不缓存配置
  35. $config = App::$debug ? [] : Cache::get('addons', []);
  36. if (empty($config)) {
  37. $config = get_addon_autoload_config();
  38. Cache::set('addons', $config);
  39. }
  40. });
  41. // 闭包初始化行为
  42. Hook::add('app_init', function () {
  43. //注册路由
  44. $routeArr = (array)Config::get('addons.route');
  45. $domains = [];
  46. $rules = [];
  47. $execute = "\\think\\addons\\Route@execute?addon=%s&controller=%s&action=%s";
  48. foreach ($routeArr as $k => $v) {
  49. if (is_array($v)) {
  50. $addon = $v['addon'];
  51. $domain = $v['domain'];
  52. $drules = [];
  53. foreach ($v['rule'] as $m => $n) {
  54. $urlArr = explode('/', $n);
  55. if (count($urlArr) < 3) {
  56. continue;
  57. }
  58. list($addon, $controller, $action) = $urlArr;
  59. $drules[$m] = sprintf($execute . '&indomain=1', $addon, $controller, $action);
  60. }
  61. //$domains[$domain] = $drules ? $drules : "\\addons\\{$k}\\controller";
  62. $domains[$domain] = $drules ? $drules : [];
  63. $domains[$domain][':controller/[:action]'] = sprintf($execute . '&indomain=1', $addon, ":controller", ":action");
  64. } else {
  65. if (!$v) {
  66. continue;
  67. }
  68. $urlArr = explode('/', $v);
  69. if (count($urlArr) < 3) {
  70. continue;
  71. }
  72. list($addon, $controller, $action) = $urlArr;
  73. $rules[$k] = sprintf($execute, $addon, $controller, $action);
  74. }
  75. }
  76. Route::rule($rules);
  77. if ($domains) {
  78. Route::domain($domains);
  79. }
  80. // 获取系统配置
  81. $hooks = App::$debug ? [] : Cache::get('hooks', []);
  82. if (empty($hooks)) {
  83. $hooks = (array)Config::get('addons.hooks');
  84. // 初始化钩子
  85. foreach ($hooks as $key => $values) {
  86. $values = is_string($values) ? explode(',', $values) : (array)$values;
  87. $values = array_filter($values);
  88. $hooks[$key] = array_filter(array_map('get_addon_class', $values));
  89. }
  90. Cache::set('hooks', $hooks);
  91. }
  92. //如果在插件中有定义app_init,则直接执行
  93. if (isset($hooks['app_init'])) {
  94. foreach ($hooks['app_init'] as $k => $v) {
  95. Hook::exec($v, 'app_init');
  96. }
  97. }
  98. Hook::import($hooks, true);
  99. });
  100. /**
  101. * 处理插件钩子
  102. * @param string $hook 钩子名称
  103. * @param mixed $params 传入参数
  104. * @return void
  105. */
  106. function hook($hook, $params = [])
  107. {
  108. Hook::listen($hook, $params);
  109. }
  110. /**
  111. * 移除空目录
  112. * @param string $dir 目录
  113. */
  114. function remove_empty_folder($dir)
  115. {
  116. try {
  117. $isDirEmpty = !(new \FilesystemIterator($dir))->valid();
  118. if ($isDirEmpty) {
  119. @rmdir($dir);
  120. remove_empty_folder(dirname($dir));
  121. }
  122. } catch (\UnexpectedValueException $e) {
  123. } catch (\Exception $e) {
  124. }
  125. }
  126. /**
  127. * 获得插件列表
  128. * @return array
  129. */
  130. function get_addon_list()
  131. {
  132. $results = scandir(ADDON_PATH);
  133. $list = [];
  134. foreach ($results as $name) {
  135. if ($name === '.' or $name === '..') {
  136. continue;
  137. }
  138. if (is_file(ADDON_PATH . $name)) {
  139. continue;
  140. }
  141. $addonDir = ADDON_PATH . $name . DS;
  142. if (!is_dir($addonDir)) {
  143. continue;
  144. }
  145. if (!is_file($addonDir . ucfirst($name) . '.php')) {
  146. continue;
  147. }
  148. //这里不采用get_addon_info是因为会有缓存
  149. //$info = get_addon_info($name);
  150. $info_file = $addonDir . 'info.ini';
  151. if (!is_file($info_file)) {
  152. continue;
  153. }
  154. $info = Config::parse($info_file, '', "addon-info-{$name}");
  155. if (!isset($info['name'])) {
  156. continue;
  157. }
  158. $info['url'] = addon_url($name);
  159. $list[$name] = $info;
  160. }
  161. return $list;
  162. }
  163. /**
  164. * 获得插件自动加载的配置
  165. * @param bool $truncate 是否清除手动配置的钩子
  166. * @return array
  167. */
  168. function get_addon_autoload_config($truncate = false)
  169. {
  170. // 读取addons的配置
  171. $config = (array)Config::get('addons');
  172. if ($truncate) {
  173. // 清空手动配置的钩子
  174. $config['hooks'] = [];
  175. }
  176. // 伪静态优先级
  177. $priority = isset($config['priority']) && $config['priority'] ? is_array($config['priority']) ? $config['priority'] : explode(',', $config['priority']) : [];
  178. $route = [];
  179. // 读取插件目录及钩子列表
  180. $base = get_class_methods("\\think\\Addons");
  181. $base = array_merge($base, ['install', 'uninstall', 'enable', 'disable']);
  182. $url_domain_deploy = Config::get('url_domain_deploy');
  183. $addons = get_addon_list();
  184. $domain = [];
  185. $priority = array_merge($priority, array_keys($addons));
  186. $orderedAddons = array();
  187. foreach ($priority as $key) {
  188. if (!isset($addons[$key])) {
  189. continue;
  190. }
  191. $orderedAddons[$key] = $addons[$key];
  192. }
  193. foreach ($orderedAddons as $name => $addon) {
  194. if (!$addon['state']) {
  195. continue;
  196. }
  197. // 读取出所有公共方法
  198. $methods = (array)get_class_methods("\\addons\\" . $name . "\\" . ucfirst($name));
  199. // 跟插件基类方法做比对,得到差异结果
  200. $hooks = array_diff($methods, $base);
  201. // 循环将钩子方法写入配置中
  202. foreach ($hooks as $hook) {
  203. $hook = Loader::parseName($hook, 0, false);
  204. if (!isset($config['hooks'][$hook])) {
  205. $config['hooks'][$hook] = [];
  206. }
  207. // 兼容手动配置项
  208. if (is_string($config['hooks'][$hook])) {
  209. $config['hooks'][$hook] = explode(',', $config['hooks'][$hook]);
  210. }
  211. if (!in_array($name, $config['hooks'][$hook])) {
  212. $config['hooks'][$hook][] = $name;
  213. }
  214. }
  215. $conf = get_addon_config($addon['name']);
  216. if ($conf) {
  217. $conf['rewrite'] = isset($conf['rewrite']) && is_array($conf['rewrite']) ? $conf['rewrite'] : [];
  218. $rule = array_map(function ($value) use ($addon) {
  219. return "{$addon['name']}/{$value}";
  220. }, array_flip($conf['rewrite']));
  221. if ($url_domain_deploy && isset($conf['domain']) && $conf['domain']) {
  222. $domain[] = [
  223. 'addon' => $addon['name'],
  224. 'domain' => $conf['domain'],
  225. 'rule' => $rule
  226. ];
  227. } else {
  228. $route = array_merge($route, $rule);
  229. }
  230. }
  231. }
  232. $config['route'] = $route;
  233. $config['route'] = array_merge($config['route'], $domain);
  234. return $config;
  235. }
  236. /**
  237. * 获取插件类的类名
  238. * @param string $name 插件名
  239. * @param string $type 返回命名空间类型
  240. * @param string $class 当前类名
  241. * @return string
  242. */
  243. function get_addon_class($name, $type = 'hook', $class = null)
  244. {
  245. $name = Loader::parseName($name);
  246. // 处理多级控制器情况
  247. if (!is_null($class) && strpos($class, '.')) {
  248. $class = explode('.', $class);
  249. $class[count($class) - 1] = Loader::parseName(end($class), 1);
  250. $class = implode('\\', $class);
  251. } else {
  252. $class = Loader::parseName(is_null($class) ? $name : $class, 1);
  253. }
  254. switch ($type) {
  255. case 'controller':
  256. $namespace = "\\addons\\" . $name . "\\controller\\" . $class;
  257. break;
  258. default:
  259. $namespace = "\\addons\\" . $name . "\\" . $class;
  260. }
  261. return class_exists($namespace) ? $namespace : '';
  262. }
  263. /**
  264. * 读取插件的基础信息
  265. * @param string $name 插件名
  266. * @return array
  267. */
  268. function get_addon_info($name)
  269. {
  270. $addon = get_addon_instance($name);
  271. if (!$addon) {
  272. return [];
  273. }
  274. return $addon->getInfo($name);
  275. }
  276. /**
  277. * 获取插件类的配置数组
  278. * @param string $name 插件名
  279. * @return array
  280. */
  281. function get_addon_fullconfig($name)
  282. {
  283. $addon = get_addon_instance($name);
  284. if (!$addon) {
  285. return [];
  286. }
  287. return $addon->getFullConfig($name);
  288. }
  289. /**
  290. * 获取插件类的配置值值
  291. * @param string $name 插件名
  292. * @return array
  293. */
  294. function get_addon_config($name)
  295. {
  296. $addon = get_addon_instance($name);
  297. if (!$addon) {
  298. return [];
  299. }
  300. return $addon->getConfig($name);
  301. }
  302. /**
  303. * 获取插件的单例
  304. * @param string $name 插件名
  305. * @return mixed|null
  306. */
  307. function get_addon_instance($name)
  308. {
  309. static $_addons = [];
  310. if (isset($_addons[$name])) {
  311. return $_addons[$name];
  312. }
  313. $class = get_addon_class($name);
  314. if (class_exists($class)) {
  315. $_addons[$name] = new $class();
  316. return $_addons[$name];
  317. } else {
  318. return null;
  319. }
  320. }
  321. /**
  322. * 获取插件创建的表
  323. * @param string $name 插件名
  324. * @return array
  325. */
  326. function get_addon_tables($name)
  327. {
  328. $addonInfo = get_addon_info($name);
  329. if (!$addonInfo) {
  330. return [];
  331. }
  332. $regex = "/^CREATE\s+TABLE\s+(IF\s+NOT\s+EXISTS\s+)?`?([a-zA-Z_]+)`?/mi";
  333. $sqlFile = ADDON_PATH . $name . DS . 'install.sql';
  334. $tables = [];
  335. if (is_file($sqlFile)) {
  336. preg_match_all($regex, file_get_contents($sqlFile), $matches);
  337. if ($matches && isset($matches[2]) && $matches[2]) {
  338. $prefix = config('database.prefix');
  339. $tables = array_map(function ($item) use ($prefix) {
  340. return str_replace("__PREFIX__", $prefix, $item);
  341. }, $matches[2]);
  342. }
  343. }
  344. return $tables;
  345. }
  346. /**
  347. * 插件显示内容里生成访问插件的url
  348. * @param string $url 地址 格式:插件名/控制器/方法
  349. * @param array $vars 变量参数
  350. * @param bool|string $suffix 生成的URL后缀
  351. * @param bool|string $domain 域名
  352. * @return bool|string
  353. */
  354. function addon_url($url, $vars = [], $suffix = true, $domain = false)
  355. {
  356. $url = ltrim($url, '/');
  357. $addon = substr($url, 0, stripos($url, '/'));
  358. if (!is_array($vars)) {
  359. parse_str($vars, $params);
  360. $vars = $params;
  361. }
  362. $params = [];
  363. foreach ($vars as $k => $v) {
  364. if (substr($k, 0, 1) === ':') {
  365. $params[$k] = $v;
  366. unset($vars[$k]);
  367. }
  368. }
  369. $val = "@addons/{$url}";
  370. $config = get_addon_config($addon);
  371. $dispatch = think\Request::instance()->dispatch();
  372. $indomain = isset($dispatch['var']['indomain']) && $dispatch['var']['indomain'] && $dispatch['var']['addon'] == $addon ? true : false;
  373. //优先取插件配置中的domain,没有的情况下取全局的域名前缀配置
  374. $domainprefix = $config && isset($config['domain']) && $config['domain'] ? $config['domain'] : Config::get('addons.domain');
  375. $domain = $domainprefix && Config::get('url_domain_deploy') ? $domainprefix : $domain;
  376. $rewrite = $config && isset($config['rewrite']) && $config['rewrite'] ? $config['rewrite'] : [];
  377. if ($rewrite) {
  378. $path = substr($url, stripos($url, '/') + 1);
  379. if (isset($rewrite[$path]) && $rewrite[$path]) {
  380. $val = $rewrite[$path];
  381. array_walk($params, function ($value, $key) use (&$val) {
  382. $val = str_replace("[{$key}]", $value, $val);
  383. });
  384. $val = str_replace(['^', '$'], '', $val);
  385. if (substr($val, -1) === '/') {
  386. $suffix = false;
  387. }
  388. } else {
  389. // 如果采用了域名部署,则需要去掉前两段
  390. if ($indomain && $domainprefix) {
  391. $arr = explode("/", $val);
  392. $val = implode("/", array_slice($arr, 2));
  393. }
  394. }
  395. } else {
  396. // 如果采用了域名部署,则需要去掉前两段
  397. if ($indomain && $domainprefix) {
  398. $arr = explode("/", $val);
  399. $val = implode("/", array_slice($arr, 2));
  400. }
  401. foreach ($params as $k => $v) {
  402. $vars[substr($k, 1)] = $v;
  403. }
  404. }
  405. $url = url($val, [], $suffix, $domain) . ($vars ? '?' . http_build_query($vars) : '');
  406. $url = preg_replace("/\/((?!index)[\w]+)\.php\//i", "/", $url);
  407. return $url;
  408. }
  409. /**
  410. * 设置基础配置信息
  411. * @param string $name 插件名
  412. * @param array $array 配置数据
  413. * @return boolean
  414. * @throws Exception
  415. */
  416. function set_addon_info($name, $array)
  417. {
  418. $file = ADDON_PATH . $name . DS . 'info.ini';
  419. $addon = get_addon_instance($name);
  420. $array = $addon->setInfo($name, $array);
  421. if (!isset($array['name']) || !isset($array['title']) || !isset($array['version'])) {
  422. throw new Exception("插件配置写入失败");
  423. }
  424. $res = array();
  425. foreach ($array as $key => $val) {
  426. if (is_array($val)) {
  427. $res[] = "[$key]";
  428. foreach ($val as $skey => $sval) {
  429. $res[] = "$skey = " . (is_numeric($sval) ? $sval : $sval);
  430. }
  431. } else {
  432. $res[] = "$key = " . (is_numeric($val) ? $val : $val);
  433. }
  434. }
  435. if (file_put_contents($file, implode("\n", $res) . "\n", LOCK_EX)) {
  436. //清空当前配置缓存
  437. Config::set($name, null, 'addoninfo');
  438. } else {
  439. throw new Exception("文件没有写入权限");
  440. }
  441. return true;
  442. }
  443. /**
  444. * 写入配置文件
  445. * @param string $name 插件名
  446. * @param array $config 配置数据
  447. * @param boolean $writefile 是否写入配置文件
  448. * @return bool
  449. * @throws Exception
  450. */
  451. function set_addon_config($name, $config, $writefile = true)
  452. {
  453. $addon = get_addon_instance($name);
  454. $addon->setConfig($name, $config);
  455. $fullconfig = get_addon_fullconfig($name);
  456. foreach ($fullconfig as $k => &$v) {
  457. if (isset($config[$v['name']])) {
  458. $value = $v['type'] !== 'array' && is_array($config[$v['name']]) ? implode(',', $config[$v['name']]) : $config[$v['name']];
  459. $v['value'] = $value;
  460. }
  461. }
  462. if ($writefile) {
  463. // 写入配置文件
  464. set_addon_fullconfig($name, $fullconfig);
  465. }
  466. return true;
  467. }
  468. /**
  469. * 写入配置文件
  470. *
  471. * @param string $name 插件名
  472. * @param array $array 配置数据
  473. * @return boolean
  474. * @throws Exception
  475. */
  476. function set_addon_fullconfig($name, $array)
  477. {
  478. $file = ADDON_PATH . $name . DS . 'config.php';
  479. $ret = file_put_contents($file, "<?php\n\n" . "return " . VarExporter::export($array) . ";\n", LOCK_EX);
  480. if (!$ret) {
  481. throw new Exception("配置写入失败");
  482. }
  483. return true;
  484. }