common.php 16 KB

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