Addon.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. namespace app\admin\command;
  3. use think\addons\AddonException;
  4. use think\addons\Service;
  5. use think\Config;
  6. use think\console\Command;
  7. use think\console\Input;
  8. use think\console\input\Option;
  9. use think\console\Output;
  10. use think\Db;
  11. use think\Exception;
  12. use think\exception\PDOException;
  13. class Addon extends Command
  14. {
  15. protected function configure()
  16. {
  17. $this
  18. ->setName('addon')
  19. ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
  20. ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh/upgrade/package/move)', 'create')
  21. ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
  22. ->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
  23. ->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
  24. ->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
  25. ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local package', null)
  26. ->setDescription('Addon manager');
  27. }
  28. protected function execute(Input $input, Output $output)
  29. {
  30. $name = $input->getOption('name') ?: '';
  31. $action = $input->getOption('action') ?: '';
  32. if (stripos($name, 'addons' . DS) !== false) {
  33. $name = explode(DS, $name)[1];
  34. }
  35. //强制覆盖
  36. $force = $input->getOption('force');
  37. //版本
  38. $release = $input->getOption('release') ?: '';
  39. //uid
  40. $uid = $input->getOption('uid') ?: '';
  41. //token
  42. $token = $input->getOption('token') ?: '';
  43. include dirname(__DIR__) . DS . 'common.php';
  44. if (!$name) {
  45. throw new Exception('Addon name could not be empty');
  46. }
  47. if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package', 'move'])) {
  48. throw new Exception('Please input correct action name');
  49. }
  50. // 查询一次SQL,判断连接是否正常
  51. Db::execute("SELECT 1");
  52. $addonDir = ADDON_PATH . $name . DS;
  53. switch ($action) {
  54. case 'create':
  55. //非覆盖模式时如果存在则报错
  56. if (is_dir($addonDir) && !$force) {
  57. throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
  58. }
  59. //如果存在先移除
  60. if (is_dir($addonDir)) {
  61. rmdirs($addonDir);
  62. }
  63. mkdir($addonDir, 0755, true);
  64. mkdir($addonDir . DS . 'controller', 0755, true);
  65. $menuList = \app\common\library\Menu::export($name);
  66. $createMenu = $this->getCreateMenu($menuList);
  67. $prefix = Config::get('database.prefix');
  68. $createTableSql = '';
  69. try {
  70. $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
  71. if (isset($result[0]) && isset($result[0]['Create Table'])) {
  72. $createTableSql = $result[0]['Create Table'];
  73. }
  74. } catch (PDOException $e) {
  75. }
  76. $data = [
  77. 'name' => $name,
  78. 'addon' => $name,
  79. 'addonClassName' => ucfirst($name),
  80. 'addonInstallMenu' => $createMenu ? "\$menu = " . var_export_short($createMenu, "\t") . ";\n\tMenu::create(\$menu);" : '',
  81. 'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
  82. 'addonEnableMenu' => $menuList ? 'Menu::enable("' . $name . '");' : '',
  83. 'addonDisableMenu' => $menuList ? 'Menu::disable("' . $name . '");' : '',
  84. ];
  85. $this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php');
  86. $this->writeToFile("config", $data, $addonDir . 'config.php');
  87. $this->writeToFile("info", $data, $addonDir . 'info.ini');
  88. $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
  89. if ($createTableSql) {
  90. $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
  91. file_put_contents($addonDir . 'install.sql', $createTableSql);
  92. }
  93. $output->info("Create Successed!");
  94. break;
  95. case 'disable':
  96. case 'enable':
  97. try {
  98. //调用启用、禁用的方法
  99. Service::$action($name, 0);
  100. } catch (AddonException $e) {
  101. if ($e->getCode() != -3) {
  102. throw new Exception($e->getMessage());
  103. }
  104. if (!$force) {
  105. //如果有冲突文件则提醒
  106. $data = $e->getData();
  107. foreach ($data['conflictlist'] as $k => $v) {
  108. $output->warning($v);
  109. }
  110. $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files? Type 'yes' to continue: ");
  111. $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  112. if (trim($line) != 'yes') {
  113. throw new Exception("Operation is aborted!");
  114. }
  115. }
  116. //调用启用、禁用的方法
  117. Service::$action($name, 1);
  118. } catch (Exception $e) {
  119. throw new Exception($e->getMessage());
  120. }
  121. $output->info(ucfirst($action) . " Successed!");
  122. break;
  123. case 'install':
  124. //非覆盖模式时如果存在则报错
  125. if (is_dir($addonDir) && !$force) {
  126. throw new Exception("addon already exists!\nIf you need to install again, use the parameter --force=true ");
  127. }
  128. //如果存在先移除
  129. if (is_dir($addonDir)) {
  130. rmdirs($addonDir);
  131. }
  132. // 获取本地路径
  133. $local = $input->getOption('local');
  134. try {
  135. Service::install($name, 0, ['version' => $release], $local);
  136. } catch (AddonException $e) {
  137. if ($e->getCode() != -3) {
  138. throw new Exception($e->getMessage());
  139. }
  140. if (!$force) {
  141. //如果有冲突文件则提醒
  142. $data = $e->getData();
  143. foreach ($data['conflictlist'] as $k => $v) {
  144. $output->warning($v);
  145. }
  146. $output->info("Are you sure you want to override all those files? Type 'yes' to continue: ");
  147. $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  148. if (trim($line) != 'yes') {
  149. throw new Exception("Operation is aborted!");
  150. }
  151. }
  152. Service::install($name, 1, ['version' => $release, 'uid' => $uid, 'token' => $token], $local);
  153. } catch (Exception $e) {
  154. throw new Exception($e->getMessage());
  155. }
  156. $output->info("Install Successed!");
  157. break;
  158. case 'uninstall':
  159. //非覆盖模式时如果存在则报错
  160. if (!$force) {
  161. throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
  162. }
  163. try {
  164. Service::uninstall($name, 0);
  165. } catch (AddonException $e) {
  166. if ($e->getCode() != -3) {
  167. throw new Exception($e->getMessage());
  168. }
  169. if (!$force) {
  170. //如果有冲突文件则提醒
  171. $data = $e->getData();
  172. foreach ($data['conflictlist'] as $k => $v) {
  173. $output->warning($v);
  174. }
  175. $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: ");
  176. $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  177. if (trim($line) != 'yes') {
  178. throw new Exception("Operation is aborted!");
  179. }
  180. }
  181. Service::uninstall($name, 1);
  182. } catch (Exception $e) {
  183. throw new Exception($e->getMessage());
  184. }
  185. $output->info("Uninstall Successed!");
  186. break;
  187. case 'refresh':
  188. Service::refresh();
  189. $output->info("Refresh Successed!");
  190. break;
  191. case 'upgrade':
  192. Service::upgrade($name, ['version' => $release, 'uid' => $uid, 'token' => $token]);
  193. $output->info("Upgrade Successed!");
  194. break;
  195. case 'package':
  196. $infoFile = $addonDir . 'info.ini';
  197. if (!is_file($infoFile)) {
  198. throw new Exception(__('Addon info file was not found'));
  199. }
  200. $info = get_addon_info($name);
  201. if (!$info) {
  202. throw new Exception(__('Addon info file data incorrect'));
  203. }
  204. $infoname = isset($info['name']) ? $info['name'] : '';
  205. if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) {
  206. throw new Exception(__('Addon info name incorrect'));
  207. }
  208. $infoversion = isset($info['version']) ? $info['version'] : '';
  209. if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) {
  210. throw new Exception(__('Addon info version incorrect'));
  211. }
  212. $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
  213. if (!is_dir($addonTmpDir)) {
  214. @mkdir($addonTmpDir, 0755, true);
  215. }
  216. $addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip';
  217. if (!class_exists('ZipArchive')) {
  218. throw new Exception(__('ZinArchive not install'));
  219. }
  220. $zip = new \ZipArchive;
  221. $zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
  222. $files = new \RecursiveIteratorIterator(
  223. new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
  224. );
  225. foreach ($files as $name => $file) {
  226. if (!$file->isDir()) {
  227. $filePath = $file->getRealPath();
  228. $relativePath = str_replace(DS, '/', substr($filePath, strlen($addonDir)));
  229. if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db'])) {
  230. $zip->addFile($filePath, $relativePath);
  231. }
  232. }
  233. }
  234. $zip->close();
  235. $output->info("Package Successed!");
  236. break;
  237. case 'move':
  238. $movePath = [
  239. 'adminOnlySelfDir' => ['admin/behavior', 'admin/controller', 'admin/library', 'admin/model', 'admin/validate', 'admin/view'],
  240. 'adminAllSubDir' => ['admin/lang'],
  241. 'publicDir' => ['public/assets/addons', 'public/assets/js/backend']
  242. ];
  243. $paths = [];
  244. $appPath = str_replace('/', DS, APP_PATH);
  245. $rootPath = str_replace('/', DS, ROOT_PATH);
  246. foreach ($movePath as $k => $items) {
  247. switch ($k) {
  248. case 'adminOnlySelfDir':
  249. foreach ($items as $v) {
  250. $v = str_replace('/', DS, $v);
  251. $oldPath = $appPath . $v . DS . $name;
  252. $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $name;
  253. $paths[$oldPath] = $newPath;
  254. }
  255. break;
  256. case 'adminAllSubDir':
  257. foreach ($items as $v) {
  258. $v = str_replace('/', DS, $v);
  259. $vPath = $appPath . $v;
  260. $list = scandir($vPath);
  261. foreach ($list as $_v) {
  262. if (!in_array($_v, ['.', '..']) && is_dir($vPath . DS . $_v)) {
  263. $oldPath = $appPath . $v . DS . $_v . DS . $name;
  264. $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $_v . DS . $name;
  265. $paths[$oldPath] = $newPath;
  266. }
  267. }
  268. }
  269. break;
  270. case 'publicDir':
  271. foreach ($items as $v) {
  272. $v = str_replace('/', DS, $v);
  273. $oldPath = $rootPath . $v . DS . $name;
  274. $newPath = $rootPath . 'addons' . DS . $name . DS . $v . DS . $name;
  275. $paths[$oldPath] = $newPath;
  276. }
  277. break;
  278. }
  279. }
  280. foreach ($paths as $oldPath => $newPath) {
  281. if (is_dir($oldPath)) {
  282. if ($force) {
  283. if (is_dir($newPath)) {
  284. $list = scandir($newPath);
  285. foreach ($list as $_v) {
  286. if (!in_array($_v, ['.', '..'])) {
  287. $file = $newPath . DS . $_v;
  288. @chmod($file, 0777);
  289. @unlink($file);
  290. }
  291. }
  292. @rmdir($newPath);
  293. }
  294. }
  295. copydirs($oldPath, $newPath);
  296. }
  297. }
  298. break;
  299. default:
  300. break;
  301. }
  302. }
  303. /**
  304. * 获取创建菜单的数组
  305. * @param array $menu
  306. * @return array
  307. */
  308. protected function getCreateMenu($menu)
  309. {
  310. $result = [];
  311. foreach ($menu as $k => & $v) {
  312. $arr = [
  313. 'name' => $v['name'],
  314. 'title' => $v['title'],
  315. ];
  316. if ($v['icon'] != 'fa fa-circle-o') {
  317. $arr['icon'] = $v['icon'];
  318. }
  319. if ($v['ismenu']) {
  320. $arr['ismenu'] = $v['ismenu'];
  321. }
  322. if (isset($v['childlist']) && $v['childlist']) {
  323. $arr['sublist'] = $this->getCreateMenu($v['childlist']);
  324. }
  325. $result[] = $arr;
  326. }
  327. return $result;
  328. }
  329. /**
  330. * 写入到文件
  331. * @param string $name
  332. * @param array $data
  333. * @param string $pathname
  334. * @return mixed
  335. */
  336. protected function writeToFile($name, $data, $pathname)
  337. {
  338. $search = $replace = [];
  339. foreach ($data as $k => $v) {
  340. $search[] = "{%{$k}%}";
  341. $replace[] = $v;
  342. }
  343. $stub = file_get_contents($this->getStub($name));
  344. $content = str_replace($search, $replace, $stub);
  345. if (!is_dir(dirname($pathname))) {
  346. mkdir(strtolower(dirname($pathname)), 0755, true);
  347. }
  348. return file_put_contents($pathname, $content);
  349. }
  350. /**
  351. * 获取基础模板
  352. * @param string $name
  353. * @return string
  354. */
  355. protected function getStub($name)
  356. {
  357. return __DIR__ . '/Addon/stubs/' . $name . '.stub';
  358. }
  359. }