Browse Source

新增日志过滤
新增安装未知来源插件开关
新增插件纯净模式
修复分片合并未创建文件夹BUG
优化插件管理逻辑

Karson 5 years ago
parent
commit
acb42e8eb8

+ 2 - 1
application/admin/behavior/AdminLog.php

@@ -6,7 +6,8 @@ class AdminLog
 {
     public function run(&$params)
     {
-        if (request()->isPost()) {
+        //只记录POST请求的日志
+        if (request()->isPost() && config('fastadmin.auto_record_log')) {
             \app\admin\model\AdminLog::record();
         }
     }

+ 2 - 2
application/admin/command/Addon.php

@@ -88,7 +88,7 @@ class Addon extends Command
                     'name'               => $name,
                     'addon'              => $name,
                     'addonClassName'     => ucfirst($name),
-                    'addonInstallMenu'   => $createMenu ? "\$menu = " . var_export_short($createMenu, "\t") . ";\n\tMenu::create(\$menu);" : '',
+                    'addonInstallMenu'   => $createMenu ? "\$menu = " . var_export_short($createMenu) . ";\n\tMenu::create(\$menu);" : '',
                     'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
                     'addonEnableMenu'    => $menuList ? 'Menu::enable("' . $name . '");' : '',
                     'addonDisableMenu'   => $menuList ? 'Menu::disable("' . $name . '");' : '',
@@ -379,5 +379,5 @@ class Addon extends Command
     {
         return __DIR__ . '/Addon/stubs/' . $name . '.stub';
     }
-    
+
 }

+ 1 - 1
application/admin/command/Install.php

@@ -245,7 +245,7 @@ class Install extends Command
                 $config[$value['name']] = $value['value'];
             }
             $config['name'] = $siteName;
-            file_put_contents($configFile, '<?php' . "\n\nreturn " . var_export($config, true) . ";");
+            file_put_contents($configFile, '<?php' . "\n\nreturn " . var_export_short($config) . ";\n");
         }
 
         $installLockFile = INSTALL_PATH . "install.lock";

+ 4 - 4
application/admin/command/Install/install.html

@@ -9,7 +9,7 @@
 
     <style>
         body {
-            background: #fff;
+            background: #f1f6fd;
             margin: 0;
             padding: 0;
             line-height: 1.5;
@@ -71,8 +71,8 @@
         }
 
         .form-field input {
-            background: #EDF2F7;
-            margin: 0 0 1px;
+            background: #fff;
+            margin: 0 0 2px;
             border: 2px solid transparent;
             transition: background 0.2s, border-color 0.2s, color 0.2s;
             width: 100%;
@@ -313,4 +313,4 @@
     </div>
 </div>
 </body>
-</html>
+</html>

+ 32 - 76
application/admin/controller/Addon.php

@@ -116,6 +116,8 @@ class Addon extends Backend
         if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
             $this->error(__('Addon name incorrect'));
         }
+
+        $info = [];
         try {
             $uid = $this->request->post("uid");
             $token = $this->request->post("token");
@@ -127,16 +129,13 @@ class Addon extends Backend
                 'version'   => $version,
                 'faversion' => $faversion
             ];
-            Service::install($name, $force, $extend);
-            $info = get_addon_info($name);
-            $info['config'] = get_addon_config($name) ? 1 : 0;
-            $info['state'] = 1;
-            $this->success(__('Install successful'), null, ['addon' => $info]);
+            $info = Service::install($name, $force, $extend);
         } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
         } catch (Exception $e) {
             $this->error(__($e->getMessage()), $e->getCode());
         }
+        $this->success(__('Install successful'), '', ['addon' => $info]);
     }
 
     /**
@@ -171,12 +170,12 @@ class Addon extends Backend
                     Db::execute("DROP TABLE IF EXISTS `{$table}`");
                 }
             }
-            $this->success(__('Uninstall successful'));
         } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
         } catch (Exception $e) {
             $this->error(__($e->getMessage()));
         }
+        $this->success(__('Uninstall successful'));
     }
 
     /**
@@ -198,12 +197,12 @@ class Addon extends Backend
             //调用启用、禁用的方法
             Service::$action($name, $force);
             Cache::rm('__menu__');
-            $this->success(__('Operate successful'));
         } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
         } catch (Exception $e) {
             $this->error(__($e->getMessage()));
         }
+        $this->success(__('Operate successful'));
     }
 
     /**
@@ -213,75 +212,27 @@ class Addon extends Backend
     {
         Config::set('default_return_type', 'json');
 
+        $info = [];
         $file = $this->request->file('file');
-        $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
-        if (!is_dir($addonTmpDir)) {
-            @mkdir($addonTmpDir, 0755, true);
-        }
-        $info = $file->rule('uniqid')->validate(['size' => 10240000, 'ext' => 'zip'])->move($addonTmpDir);
-        if ($info) {
-            $tmpName = substr($info->getFilename(), 0, stripos($info->getFilename(), '.'));
-            $tmpAddonDir = ADDON_PATH . $tmpName . DS;
-            $tmpFile = $addonTmpDir . $info->getSaveName();
-            try {
-                Service::unzip($tmpName);
-                unset($info);
-                @unlink($tmpFile);
-                $infoFile = $tmpAddonDir . 'info.ini';
-                if (!is_file($infoFile)) {
-                    throw new Exception(__('Addon info file was not found'));
-                }
-
-                $config = Config::parse($infoFile, '', $tmpName);
-                $name = isset($config['name']) ? $config['name'] : '';
-                if (!$name) {
-                    throw new Exception(__('Addon info file data incorrect'));
-                }
-                if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
-                    throw new Exception(__('Addon name incorrect'));
-                }
-
-                $newAddonDir = ADDON_PATH . $name . DS;
-                if (is_dir($newAddonDir)) {
-                    throw new Exception(__('Addon already exists'));
-                }
-
-                //重命名插件文件夹
-                rename($tmpAddonDir, $newAddonDir);
-                try {
-                    //默认禁用该插件
-                    $info = get_addon_info($name);
-                    if ($info['state']) {
-                        $info['state'] = 0;
-                        set_addon_info($name, $info);
-                    }
-
-                    //执行插件的安装方法
-                    $class = get_addon_class($name);
-                    if (class_exists($class)) {
-                        $addon = new $class();
-                        $addon->install();
-                    }
-
-                    //导入SQL
-                    Service::importsql($name);
-
-                    $info['config'] = get_addon_config($name) ? 1 : 0;
-                    $this->success(__('Offline installed tips'), null, ['addon' => $info]);
-                } catch (Exception $e) {
-                    @rmdirs($newAddonDir);
-                    throw new Exception(__($e->getMessage()));
-                }
-            } catch (Exception $e) {
-                unset($info);
-                @unlink($tmpFile);
-                @rmdirs($tmpAddonDir);
-                $this->error(__($e->getMessage()));
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $faversion = $this->request->post("faversion");
+            if (!$uid || !$token) {
+                throw new Exception(__('Please login and try to install'));
             }
-        } else {
-            // 上传失败获取错误信息
-            $this->error(__($file->getError()));
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'faversion' => $faversion
+            ];
+            $info = Service::local($file, $extend);
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
         }
+        $this->success(__('Offline installed tips'), '', ['addon' => $info]);
     }
 
     /**
@@ -300,6 +251,8 @@ class Addon extends Backend
         if (!is_dir($addonTmpDir)) {
             @mkdir($addonTmpDir, 0755, true);
         }
+
+        $info = [];
         try {
             $uid = $this->request->post("uid");
             $token = $this->request->post("token");
@@ -312,14 +265,14 @@ class Addon extends Backend
                 'faversion' => $faversion
             ];
             //调用更新的方法
-            Service::upgrade($name, $extend);
+            $info = Service::upgrade($name, $extend);
             Cache::rm('__menu__');
-            $this->success(__('Operate successful'));
         } catch (AddonException $e) {
             $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
         } catch (Exception $e) {
             $this->error(__($e->getMessage()));
         }
+        $this->success(__('Operate successful'), '', ['addon' => $info]);
     }
 
     /**
@@ -352,7 +305,7 @@ class Addon extends Backend
         $addons = get_addon_list();
         $list = [];
         foreach ($addons as $k => $v) {
-            if ($search && stripos($v['name'], $search) === false && stripos($v['intro'], $search) === false) {
+            if ($search && stripos($v['name'], $search) === false && stripos($v['title'], $search) === false && stripos($v['intro'], $search) === false) {
                 continue;
             }
 
@@ -393,6 +346,9 @@ class Addon extends Backend
     public function get_table_list()
     {
         $name = $this->request->post("name");
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
         $tables = get_addon_tables($name);
         $prefix = Config::get('database.prefix');
         foreach ($tables as $index => $table) {

+ 1 - 1
application/admin/controller/general/Config.php

@@ -190,7 +190,7 @@ class Config extends Backend
         }
         file_put_contents(
             APP_PATH . 'extra' . DS . 'site.php',
-            '<?php' . "\n\nreturn " . var_export($config, true) . ";"
+            '<?php' . "\n\nreturn " . var_export_short($config) . ";\n"
         );
     }
 

+ 16 - 7
application/admin/lang/zh-cn/addon.php

@@ -21,19 +21,21 @@ return [
     'Pay tips'                                                => '扫码支付后如果仍然无法立即下载,请不要重复支付,请稍后再重试安装!',
     'Pay click tips'                                          => '请点击这里在新窗口中进行支付!',
     'Pay new window tips'                                     => '请在新弹出的窗口中进行支付,支付完成后再重新点击安装按钮进行安装!',
-    'Upgrade tips'                                            => '确认升级<b>[%s]</b>?<p class="text-danger">升级后可能出现部分冗余数据记录,请根据需要移除即可!!!</p>如有重要数据请备份后再操作!',
-    'Offline installed tips'                                  => '插件安装成功!清除浏览器缓存和框架缓存后生效!',
-    'Online installed tips'                                   => '插件安装成功!清除浏览器缓存和框架缓存后生效!',
+    'Upgrade tips'                                            => '确认升级<b>《%s》</b>?<p class="text-danger">1、请务必做好代码和数据库备份!备份!备份!<br>2、升级后如出现冗余数据,请根据需要移除即可!<br>3、不建议在生产环境升级,请在本地完成升级测试</p>如有重要数据请备份后再操作!',
+    'Offline installed tips'                                  => '安装成功!清除浏览器缓存和框架缓存后生效!',
+    'Online installed tips'                                   => '安装成功!清除浏览器缓存和框架缓存后生效!',
     'Not login tips'                                          => '你当前未登录FastAdmin,登录后将同步已购买的记录,下载时无需二次付费!',
+    'Please login and try to install'                         => '请登录后再进行离线安装!',
     'Not installed tips'                                      => '请安装后再访问插件前台页面!',
     'Not enabled tips'                                        => '插件已经禁用,请启用后再访问插件前台页面!',
     'New version tips'                                        => '发现新版本:%s 点击查看更新日志',
     'Store now available tips'                                => '插件市场暂不可用,是否切换到本地插件?',
     'Switch to the local'                                     => '切换到本地插件',
     'try to reload'                                           => '重新尝试加载',
-    'Please disable addon first'                              => '请先禁用插件再进行升级',
+    'Please disable the add before trying to upgrade'         => '请先禁用插件再进行升级',
+    'Please disable the add before trying to uninstall'       => '请先禁用插件再进行卸载',
     'Login now'                                               => '立即登录',
-    'Continue install'                                        => '不登录,继续安装',
+    'Continue install'                                        => '继续安装',
     'View addon home page'                                    => '查看插件介绍和帮助',
     'View addon index page'                                   => '查看插件前台首页',
     'View addon screenshots'                                  => '点击查看插件截图',
@@ -85,10 +87,17 @@ return [
     'Addon name incorrect'                                    => '插件名称不正确',
     'Addon info file was not found'                           => '插件配置文件未找到',
     'Addon info file data incorrect'                          => '插件配置信息不正确',
-    'Addon already exists'                                    => '上传的插件已经存在',
+    'Addon already exists'                                    => '插件已经存在',
+    'Addon package download failed'                           => '插件下载失败',
+    'Conflicting file found'                                  => '发现冲突文件',
+    'Invalid addon package'                                   => '未验证的插件',
+    'No permission to write temporary files'                  => '没有权限写入临时文件',
+    'The addon file does not exist'                           => '插件主启动程序不存在',
+    'The configuration file content is incorrect'             => '配置文件不完整',
     'Unable to open the zip file'                             => '无法打开ZIP文件',
     'Unable to extract the file'                              => '无法解压ZIP文件',
-    'Are you sure you want to unstall %s?'                    => '确认卸载插件《%s》?',
+    'Unable to open file \'%s\' for writing'                  => '文件(%s)没有写入权限',
+    'Are you sure you want to unstall %s?'                    => '确认卸载<b>《%s》</b>?',
     'Delete all the addon file and cannot be recovered!'      => '卸载将会删除所有插件文件且不可找回!!!',
     'Delete all the addon database and cannot be recovered!'  => '删除所有插件相关数据表且不可找回!!!',
     'Please backup important data manually before uninstall!' => '如有重要数据请备份后再操作!!!',

+ 53 - 12
application/admin/model/AdminLog.php

@@ -18,6 +18,10 @@ class AdminLog extends Model
     protected static $title = '';
     //自定义日志内容
     protected static $content = '';
+    //忽略的链接正则列表
+    protected static $ignoreRegex = [
+        '/^(.*)\/(selectpage|index)$/i',
+    ];
 
     public static function setTitle($title)
     {
@@ -29,26 +33,41 @@ class AdminLog extends Model
         self::$content = $content;
     }
 
-    public static function record($title = '')
+    public static function setIgnoreRegex($regex = [])
+    {
+        $regex = is_array($regex) ? $regex : [$regex];
+        self::$ignoreRegex = array_merge(self::$ignoreRegex, $regex);
+    }
+
+    /**
+     * 记录日志
+     * @param string $title
+     * @param string $content
+     */
+    public static function record($title = '', $content = '')
     {
         $auth = Auth::instance();
         $admin_id = $auth->isLogin() ? $auth->id : 0;
         $username = $auth->isLogin() ? $auth->username : __('Unknown');
-        $content = self::$content;
-        if (!$content) {
-            $content = request()->param('', null, 'trim,strip_tags,htmlspecialchars');
-            foreach ($content as $k => $v) {
-                if (is_string($v) && strlen($v) > 200 || stripos($k, 'password') !== false) {
-                    unset($content[$k]);
+
+        $controllername = Loader::parseName(request()->controller());
+        $actionname = strtolower(request()->action());
+        $path = str_replace('.', '/', $controllername) . '/' . $actionname;
+        if (self::$ignoreRegex) {
+            foreach (self::$ignoreRegex as $index => $item) {
+                if (preg_match($item, $path)) {
+                    return;
                 }
             }
         }
-        $title = self::$title;
+        $content = $content ? $content : self::$content;
+        if (!$content) {
+            $content = request()->param('', null, 'trim,strip_tags,htmlspecialchars');
+            $content = self::getPureContent($content);
+        }
+        $title = $title ? $title : self::$title;
         if (!$title) {
             $title = [];
-            $controllername = Loader::parseName(request()->controller());
-            $actionname = strtolower(request()->action());
-            $path = str_replace('.', '/', $controllername) . '/' . $actionname;
             $breadcrumb = Auth::instance()->getBreadcrumb($path);
             foreach ($breadcrumb as $k => $v) {
                 $title[] = $v['title'];
@@ -57,7 +76,7 @@ class AdminLog extends Model
         }
         self::create([
             'title'     => $title,
-            'content'   => !is_scalar($content) ? json_encode($content) : $content,
+            'content'   => !is_scalar($content) ? json_encode($content, JSON_UNESCAPED_UNICODE) : $content,
             'url'       => substr(request()->url(), 0, 1500),
             'admin_id'  => $admin_id,
             'username'  => $username,
@@ -66,6 +85,28 @@ class AdminLog extends Model
         ]);
     }
 
+    /**
+     * 获取已屏蔽关键信息的数据
+     * @param $content
+     * @return false|string
+     */
+    protected static function getPureContent($content)
+    {
+        if (!is_array($content)) {
+            return $content;
+        }
+        foreach ($content as $index => &$item) {
+            if (preg_match("/(password|salt|token)/i", $index)) {
+                $item = "***";
+            } else {
+                if (is_array($item)) {
+                    $item = self::getPureContent($item);
+                }
+            }
+        }
+        return $content;
+    }
+
     public function admin()
     {
         return $this->belongsTo('Admin', 'admin_id')->setEagerlyType(0);

+ 9 - 9
application/admin/view/addon/index.html

@@ -78,18 +78,18 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        {:build_toolbar('refresh')}
-                        <button type="button" id="faupload-addon" class="btn btn-danger faupload" data-url="addon/local" data-chunking="false" data-mimetype="zip" data-multiple="false"><i class="fa fa-upload"></i>
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" data-force-refresh="false"><i class="fa fa-refresh"></i> </a>
+                        {if $Think.config.fastadmin.api_url}
+                        <button type="button" id="faupload-addon" class="btn btn-danger faupload btn-mini-xs" data-url="addon/local" data-chunking="false" data-mimetype="zip,fastaddon" data-multiple="false"><i class="fa fa-upload"></i>
                             {:__('Offline install')}
                         </button>
-                        {if $Think.config.fastadmin.api_url}
                         <div class="btn-group">
-                            <a href="#" class="btn btn-info btn-switch active" data-type="all"><i class="fa fa-list"></i> {:__('All')}</a>
-                            <a href="#" class="btn btn-info btn-switch" data-type="free"><i class="fa fa-gift"></i> {:__('Free')}</a>
-                            <a href="#" class="btn btn-info btn-switch" data-type="price"><i class="fa fa-rmb"></i> {:__('Paying')}</a>
-                            <a href="#" class="btn btn-info btn-switch" data-type="local" data-url="addon/downloaded"><i class="fa fa-laptop"></i> {:__('Local addon')}</a>
+                            <a href="#" class="btn btn-info btn-switch active btn-mini-xs" data-type="all"><i class="fa fa-list"></i> {:__('All')}</a>
+                            <a href="#" class="btn btn-info btn-switch btn-mini-xs" data-type="free"><i class="fa fa-gift"></i> {:__('Free')}</a>
+                            <a href="#" class="btn btn-info btn-switch btn-mini-xs" data-type="price"><i class="fa fa-rmb"></i> {:__('Paying')}</a>
+                            <a href="#" class="btn btn-info btn-switch btn-mini-xs" data-type="local" data-url="addon/downloaded"><i class="fa fa-laptop"></i> {:__('Local addon')}</a>
                         </div>
-                        <a class="btn btn-primary btn-userinfo" href="javascript:;"><i class="fa fa-user"></i> {:__('Userinfo')}</a>
+                        <a class="btn btn-primary btn-userinfo btn-mini-xs" href="javascript:;"><i class="fa fa-user"></i> {:__('Userinfo')}</a>
                         {/if}
                     </div>
                     <table id="table" class="table table-striped table-bordered table-hover" width="100%">
@@ -208,7 +208,7 @@
 </script>
 <script id="uninstalltpl" type="text/html">
     <div class="">
-        <div class=""><%=__("Are you sure you want to unstall %s?", addon['title'])%>
+        <div class=""><%=#__("Are you sure you want to unstall %s?", addon['title'])%>
             <p class="text-danger">{:__('Delete all the addon file and cannot be recovered!')} </p>
             {if config('app_debug')}
             <p class="text-danger"><input type="checkbox" name="droptables" id="droptables" data-name="<%=addon['name']%>"/> {:__('Delete all the addon database and cannot be recovered!')} </p>

+ 16 - 11
application/admin/view/auth/adminlog/detail.html

@@ -1,17 +1,22 @@
-<table class="table table-striped">
+<style>
+    .table-adminlog tr td {
+        word-break: break-all;
+    }
+</style>
+<table class="table table-striped table-adminlog">
     <thead>
-        <tr>
-            <th>{:__('Title')}</th>
-            <th>{:__('Content')}</th>
-        </tr>
+    <tr>
+        <th width="100">{:__('Title')}</th>
+        <th>{:__('Content')}</th>
+    </tr>
     </thead>
     <tbody>
-        {volist name="row" id="vo"  }
-            <tr>
-                <td>{:__($key)}</td>
-                <td>{if $key=='createtime'}{$vo|datetime}{else/}{$vo|htmlentities}{/if}</td>
-            </tr>
-        {/volist}
+    {volist name="row" id="vo" }
+    <tr>
+        <td>{:__($key)}</td>
+        <td>{if $key=='createtime'}{$vo|datetime}{else/}{$vo|htmlentities}{/if}</td>
+    </tr>
+    {/volist}
     </tbody>
 </table>
 <div class="hide layer-footer">

+ 4 - 4
application/admin/view/auth/rule/index.html

@@ -9,7 +9,7 @@
             <div class="tab-pane fade active in" id="one">
                 <div class="widget-body no-padding">
                     <div id="toolbar" class="toolbar">
-                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" data-force-refresh="false"><i class="fa fa-refresh"></i> </a>
                         <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('auth/rule/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
                         <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('auth/rule/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
                         <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('auth/rule/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
@@ -22,9 +22,9 @@
                         </div>
                         <a href="javascript:;" class="btn btn-danger btn-toggle-all"><i class="fa fa-plus"></i> {:__('Toggle all')}</a>
                     </div>
-                    <table id="table" class="table table-bordered table-hover" 
-                           data-operate-edit="{:$auth->check('auth/rule/edit')}" 
-                           data-operate-del="{:$auth->check('auth/rule/del')}" 
+                    <table id="table" class="table table-bordered table-hover"
+                           data-operate-edit="{:$auth->check('auth/rule/edit')}"
+                           data-operate-del="{:$auth->check('auth/rule/del')}"
                            width="100%">
                     </table>
                 </div>

+ 5 - 20
application/common.php

@@ -2,6 +2,8 @@
 
 // 公共助手函数
 
+use Symfony\Component\VarExporter\VarExporter;
+
 if (!function_exists('__')) {
 
     /**
@@ -264,29 +266,12 @@ if (!function_exists('var_export_short')) {
 
     /**
      * 返回打印数组结构
-     * @param string $var    数组
-     * @param string $indent 缩进字符
+     * @param string $var 数组
      * @return string
      */
-    function var_export_short($var, $indent = "")
+    function var_export_short($var)
     {
-        switch (gettype($var)) {
-            case "string":
-                return '"' . addcslashes($var, "\\\$\"\r\n\t\v\f") . '"';
-            case "array":
-                $indexed = array_keys($var) === range(0, count($var) - 1);
-                $r = [];
-                foreach ($var as $key => $value) {
-                    $r[] = "$indent    "
-                        . ($indexed ? "" : var_export_short($key) . " => ")
-                        . var_export_short($value, "$indent    ");
-                }
-                return "[\n" . implode(",\n", $r) . "\n" . $indent . "]";
-            case "boolean":
-                return $var ? "TRUE" : "FALSE";
-            default:
-                return var_export($var, true);
-        }
+        return VarExporter::export($var);
     }
 }
 

+ 1 - 1
application/common/controller/Backend.php

@@ -277,7 +277,7 @@ class Backend extends Controller
         $bind = [];
         $name = '';
         $aliasName = '';
-        if (!empty($this->model)) {
+        if (!empty($this->model) && $this->relationSearch) {
             $name = $this->model->getTable();
             $alias[$name] = Loader::parseName(basename(str_replace('\\', '/', get_class($this->model))));
             $aliasName = $alias[$name] . '.';

+ 80 - 38
application/common/library/Menu.php

@@ -4,6 +4,8 @@ namespace app\common\library;
 
 use app\admin\model\AuthRule;
 use fast\Tree;
+use think\addons\Service;
+use think\Db;
 use think\Exception;
 use think\exception\PDOException;
 
@@ -15,32 +17,16 @@ class Menu
      * @param array $menu
      * @param mixed $parent 父类的name或pid
      */
-    public static function create($menu, $parent = 0)
+    public static function create($menu = [], $parent = 0)
     {
-        if (!is_numeric($parent)) {
-            $parentRule = AuthRule::getByName($parent);
-            $pid = $parentRule ? $parentRule['id'] : 0;
-        } else {
-            $pid = $parent;
-        }
-        $allow = array_flip(['file', 'name', 'title', 'icon', 'condition', 'remark', 'ismenu', 'weigh']);
-        foreach ($menu as $k => $v) {
-            $hasChild = isset($v['sublist']) && $v['sublist'] ? true : false;
+        $old = [];
+        self::menuUpdate($menu, $old, $parent);
 
-            $data = array_intersect_key($v, $allow);
-
-            $data['ismenu'] = isset($data['ismenu']) ? $data['ismenu'] : ($hasChild ? 1 : 0);
-            $data['icon'] = isset($data['icon']) ? $data['icon'] : ($hasChild ? 'fa fa-list' : 'fa fa-circle-o');
-            $data['pid'] = $pid;
-            $data['status'] = 'normal';
-            try {
-                $menu = AuthRule::create($data);
-                if ($hasChild) {
-                    self::create($v['sublist'], $menu->id);
-                }
-            } catch (PDOException $e) {
-                throw new Exception($e->getMessage());
-            }
+        //菜单刷新处理
+        $info = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
+        preg_match('/addons\\\\([a-z0-9]+)\\\\/i', $info['class'], $matches);
+        if ($matches && isset($matches[1])) {
+            Menu::refresh($matches[1], $menu);
         }
     }
 
@@ -97,14 +83,71 @@ class Menu
      */
     public static function upgrade($name, $menu)
     {
-        $old = AuthRule::where('name', 'like', "{$name}%")->select();
+        $ids = self::getAuthRuleIdsByName($name);
+        $old = AuthRule::where('id', 'in', $ids)->select();
         $old = collection($old)->toArray();
         $old = array_column($old, null, 'name');
-        self::menuUpdate($menu, $old);
+
+        Db::startTrans();
+        try {
+            self::menuUpdate($menu, $old);
+            $ids = [];
+            foreach ($old as $index => $item) {
+                if (!isset($item['keep'])) {
+                    $ids[] = $item['id'];
+                }
+            }
+            if ($ids) {
+                //旧版本的菜单需要做删除处理
+                $config = Service::config($name);
+                $menus = isset($config['menus']) ? $config['menus'] : [];
+                $where = ['id' => ['in', $ids]];
+                if ($menus) {
+                    //必须是旧版本中的菜单,可排除用户自主创建的菜单
+                    $where['name'] = ['in', $menus];
+                }
+                AuthRule::where($where)->delete();
+            }
+
+            Db::commit();
+        } catch (PDOException $e) {
+            Db::rollback();
+            return false;
+        }
+
+        Menu::refresh($name, $menu);
         return true;
     }
 
     /**
+     * 刷新插件菜单配置缓存
+     * @param string $name
+     * @param array  $menu
+     */
+    public static function refresh($name, $menu = [])
+    {
+        if (!$menu) {
+            // $menu为空时表示首次安装,首次安装需刷新插件菜单标识缓存
+            $menuIds = Menu::getAuthRuleIdsByName($name);
+            $menus = Db::name("auth_rule")->where('id', 'in', $menuIds)->column('name');
+        } else {
+            // 刷新新的菜单缓存
+            $getMenus = function ($menu) use (&$getMenus) {
+                $result = [];
+                foreach ($menu as $index => $item) {
+                    $result[] = $item['name'];
+                    $result = array_merge($result, isset($item['sublist']) && is_array($item['sublist']) ? $getMenus($item['sublist']) : []);
+                }
+                return $result;
+            };
+            $menus = $getMenus($menu);
+        }
+
+        //刷新新的插件核心菜单缓存
+        Service::config($name, ['menus' => $menus]);
+    }
+
+    /**
      * 导出指定名称的菜单规则
      * @param string $name
      * @return array
@@ -131,7 +174,7 @@ class Menu
      * @param int   $parent
      * @throws Exception
      */
-    private static function menuUpdate($newMenu, $oldMenu, $parent = 0)
+    private static function menuUpdate($newMenu, &$oldMenu, $parent = 0)
     {
         if (!is_numeric($parent)) {
             $parentRule = AuthRule::getByName($parent);
@@ -147,17 +190,16 @@ class Menu
             $data['icon'] = isset($data['icon']) ? $data['icon'] : ($hasChild ? 'fa fa-list' : 'fa fa-circle-o');
             $data['pid'] = $pid;
             $data['status'] = 'normal';
-            try {
-                if (!isset($oldMenu[$data['name']])) {
-                    $menu = AuthRule::create($data);
-                } else {
-                    $menu = $oldMenu[$data['name']];
-                }
-                if ($hasChild) {
-                    self::menuUpdate($v['sublist'], $oldMenu, $menu['id']);
-                }
-            } catch (PDOException $e) {
-                throw new Exception($e->getMessage());
+            if (!isset($oldMenu[$data['name']])) {
+                $menu = AuthRule::create($data);
+            } else {
+                $menu = $oldMenu[$data['name']];
+                //更新旧菜单
+                AuthRule::update($data, ['id' => $menu['id']]);
+                $oldMenu[$data['name']]['keep'] = true;
+            }
+            if ($hasChild) {
+                self::menuUpdate($v['sublist'], $oldMenu, $menu['id']);
             }
         }
     }

+ 3 - 0
application/common/library/Upload.php

@@ -311,6 +311,9 @@ class Upload
             $sourceFile = $this->file->getRealPath() ?: $this->file->getPathname();
             $info = $this->file->getInfo();
             $this->file = null;
+            if (!is_dir($destDir)) {
+                @mkdir($destDir, 0755, true);
+            }
             rename($sourceFile, $destFile);
             $file = new File($destFile);
             $file->setSaveName($fileName)->setUploadInfo($info);

+ 8 - 0
application/config.php

@@ -279,6 +279,14 @@ return [
         'adminskin'             => '',
         //后台是否启用面包屑
         'breadcrumb'            => false,
+        //是否允许未知来源的插件压缩包
+        'unknownsources'        => true,
+        //插件启用禁用时是否备份对应的全局文件
+        'backup_global_files'   => true,
+        //是否开启后台自动日志记录
+        'auto_record_log'       => true,
+        //插件纯净模式,插件启用后是否删除插件目录的application、public和assets文件夹
+        'addon_pure_mode'       => true,
         //允许跨域的域名,多个以,分隔
         'cors_request_domain'   => 'localhost,127.0.0.1',
         //版本号

+ 6 - 12
application/extra/addons.php

@@ -1,14 +1,8 @@
 <?php
 
-return array (
-  'autoload' => false,
-  'hooks' => 
-  array (
-  ),
-  'route' => 
-  array (
-  ),
-  'priority' => 
-  array (
-  ),
-);
+return [
+    'autoload' => false,
+    'hooks' => [],
+    'route' => [],
+    'priority' => [],
+];

+ 33 - 36
application/extra/site.php

@@ -1,38 +1,35 @@
 <?php
 
-return array (
-  'name' => '我的网站',
-  'beian' => '',
-  'cdnurl' => '',
-  'version' => '1.0.1',
-  'timezone' => 'Asia/Shanghai',
-  'forbiddenip' => '',
-  'languages' => 
-  array (
-    'backend' => 'zh-cn',
-    'frontend' => 'zh-cn',
-  ),
-  'fixedpage' => 'dashboard',
-  'categorytype' => 
-  array (
-    'default' => 'Default',
-    'page' => 'Page',
-    'article' => 'Article',
-    'test' => 'Test',
-  ),
-  'configgroup' => 
-  array (
-    'basic' => 'Basic',
-    'email' => 'Email',
-    'dictionary' => 'Dictionary',
-    'user' => 'User',
-    'example' => 'Example',
-  ),
-  'mail_type' => '1',
-  'mail_smtp_host' => 'smtp.qq.com',
-  'mail_smtp_port' => '465',
-  'mail_smtp_user' => '10000',
-  'mail_smtp_pass' => 'password',
-  'mail_verify_type' => '2',
-  'mail_from' => '10000@qq.com',
-);
+return [
+    'name' => '我的网站',
+    'beian' => '',
+    'cdnurl' => '',
+    'version' => '1.0.1',
+    'timezone' => 'Asia/Shanghai',
+    'forbiddenip' => '',
+    'languages' => [
+        'backend' => 'zh-cn',
+        'frontend' => 'zh-cn',
+    ],
+    'fixedpage' => 'dashboard',
+    'categorytype' => [
+        'default' => 'Default',
+        'page' => 'Page',
+        'article' => 'Article',
+        'test' => 'Test',
+    ],
+    'configgroup' => [
+        'basic' => 'Basic',
+        'email' => 'Email',
+        'dictionary' => 'Dictionary',
+        'user' => 'User',
+        'example' => 'Example',
+    ],
+    'mail_type' => '1',
+    'mail_smtp_host' => 'smtp.qq.com',
+    'mail_smtp_port' => '465',
+    'mail_smtp_user' => '10000',
+    'mail_smtp_pass' => 'password',
+    'mail_verify_type' => '2',
+    'mail_from' => '10000@qq.com',
+];

+ 3 - 1
composer.json

@@ -19,10 +19,12 @@
         "topthink/framework": "~5.0.24",
         "topthink/think-captcha": "^1.0",
         "phpmailer/phpmailer": "~6.1.6",
-        "karsonzhang/fastadmin-addons": "~1.2.0",
+        "karsonzhang/fastadmin-addons": "~1.2.4",
         "overtrue/pinyin": "~3.0",
         "phpoffice/phpspreadsheet": "^1.2",
         "overtrue/wechat": "4.2.11",
+        "nelexa/zip": "^3.3",
+        "symfony/var-exporter ": "^4.4.13",
         "ext-json": "*",
         "ext-curl": "*"
     },

+ 2 - 2
public/assets/css/backend.css

@@ -1008,7 +1008,7 @@ table.table-nowrap thead > tr > th {
   .fixed-table-toolbar .toolbar a.btn-import,
   .fixed-table-toolbar .toolbar a.btn-more,
   .fixed-table-toolbar .toolbar a.btn-recyclebin,
-  .fixed-table-toolbar .toolbar a.btn-mini-xs {
+  .fixed-table-toolbar .toolbar .btn-mini-xs {
     font-size: 0;
   }
   .fixed-table-toolbar .toolbar a.btn-refresh .fa,
@@ -1018,7 +1018,7 @@ table.table-nowrap thead > tr > th {
   .fixed-table-toolbar .toolbar a.btn-import .fa,
   .fixed-table-toolbar .toolbar a.btn-more .fa,
   .fixed-table-toolbar .toolbar a.btn-recyclebin .fa,
-  .fixed-table-toolbar .toolbar a.btn-mini-xs .fa {
+  .fixed-table-toolbar .toolbar .btn-mini-xs .fa {
     font-size: initial;
   }
   .fixed-table-toolbar .search {

File diff suppressed because it is too large
+ 1 - 1
public/assets/css/backend.min.css


+ 1 - 36
public/assets/js/addons.js

@@ -1,38 +1,3 @@
 define([], function () {
-    require.config({
-    paths: {
-        'jquery-colorpicker': '../addons/cms/js/jquery.colorpicker.min',
-        'jquery-autocomplete': '../addons/cms/js/jquery.autocomplete',
-        'jquery-tagsinput': '../addons/cms/js/jquery.tagsinput',
-        'clipboard': '../addons/cms/js/clipboard.min',
-    },
-    shim: {
-        'jquery-colorpicker': {
-            deps: ['jquery'],
-            exports: '$.fn.extend'
-        },
-        'jquery-autocomplete': {
-            deps: ['jquery'],
-            exports: '$.fn.extend'
-        },
-        'jquery-tagsinput': {
-            deps: ['jquery', 'jquery-autocomplete', 'css!../addons/cms/css/jquery.tagsinput.min.css'],
-            exports: '$.fn.extend'
-        }
-    }
-});
-
-require.config({
-    paths: {
-        'async': '../addons/example/js/async',
-        'BMap': ['//api.map.baidu.com/api?v=2.0&ak=mXijumfojHnAaN2VxpBGoqHM'],
-    },
-    shim: {
-        'BMap': {
-            deps: ['jquery'],
-            exports: 'BMap'
-        }
-    }
-});
-
+    
 });

+ 35 - 12
public/assets/js/backend/addon.js

@@ -31,6 +31,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                         btn: [__('Switch to the local'), __('Try to reload')]
                     }, function (index) {
                         layer.close(index);
+                        $(".panel .nav-tabs").hide();
+                        $(".toolbar > *:not(:first)").hide();
                         $(".btn-switch[data-type='local']").trigger("click");
                     }, function (index) {
                         layer.close(index);
@@ -62,6 +64,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             Template.helper("Moment", Moment);
             Template.helper("addons", Config['addons']);
 
+            $("#faupload-addon").data("params", function () {
+                var userinfo = Controller.api.userinfo.get();
+                return {
+                    uid: userinfo ? userinfo.id : '',
+                    token: userinfo ? userinfo.token : '',
+                    version: Config.faversion
+                };
+            });
+
             // 初始化表格
             table.bootstrapTable({
                 url: $.fn.bootstrapTable.defaults.extend.index_url,
@@ -168,6 +179,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     Config['addons'][data.addon.name] = data.addon;
                     Toastr.success(ret.msg);
                     operate(data.addon.name, 'enable', false);
+                    return false;
                 });
             });
 
@@ -220,12 +232,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             // 会员信息
             $(document).on("click", ".btn-userinfo", function () {
                 var that = this;
+                var area = [$(window).width() > 800 ? '500px' : '95%', $(window).height() > 600 ? '400px' : '95%'];
                 var userinfo = Controller.api.userinfo.get();
                 if (!userinfo) {
                     Layer.open({
                         content: Template("logintpl", {}),
                         zIndex: 99,
-                        area: [$(window).width() > 800 ? '500px' : '95%', $(window).height() > 600 ? '400px' : '95%'],
+                        area: area,
                         title: __('Login FastAdmin'),
                         resize: false,
                         btn: [__('Login'), __('Register')],
@@ -315,8 +328,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                         title: __('Warning'),
                         icon: 1
                     });
-                    $('.btn-refresh').trigger('click');
-                    Fast.api.refreshmenu();
+                    Controller.api.refresh(table, name);
                 }, function (data, ret) {
                     //如果是需要购买的插件则弹出二维码提示
                     if (ret && ret.code === -1) {
@@ -377,8 +389,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                 }, function (data, ret) {
                     delete Config['addons'][name];
                     Layer.closeAll();
-                    $('.btn-refresh').trigger('click');
-                    Fast.api.refreshmenu();
+                    Controller.api.refresh(table, name);
                 }, function (data, ret) {
                     if (ret && ret.code === -3) {
                         //插件目录发现影响全局的文件
@@ -411,8 +422,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     var addon = Config['addons'][name];
                     addon.state = action === 'enable' ? 1 : 0;
                     Layer.closeAll();
-                    $('.btn-refresh').trigger('click');
-                    Fast.api.refreshmenu();
+                    Controller.api.refresh(table, name);
                 }, function (data, ret) {
                     if (ret && ret.code === -3) {
                         //插件目录发现影响全局的文件
@@ -445,10 +455,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                     url: 'addon/upgrade',
                     data: {name: name, uid: uid, token: token, version: version, faversion: Config.faversion}
                 }, function (data, ret) {
-                    Config['addons'][name].version = version;
+                    Config['addons'][name] = data.addon;
                     Layer.closeAll();
-                    $('.btn-refresh').trigger('click');
-                    Fast.api.refreshmenu();
+                    Controller.api.refresh(table, name);
                 }, function (data, ret) {
                     Layer.alert(ret.msg);
                     return false;
@@ -483,7 +492,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             $(document).on("click", ".btn-uninstall", function () {
                 var name = $(this).closest(".operate").data('name');
                 if (Config['addons'][name].state == 1) {
-                    Layer.alert(__('Please disable addon first'), {icon: 7});
+                    Layer.alert(__('Please disable the add before trying to uninstall'), {icon: 7});
                     return false;
                 }
                 Template.helper("__", __);
@@ -509,7 +518,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
             $(document).on("click", ".btn-upgrade", function () {
                 var name = $(this).closest(".operate").data('name');
                 if (Config['addons'][name].state == 1) {
-                    Layer.alert(__('Please disable addon first'), {icon: 7});
+                    Layer.alert(__('Please disable the add before trying to upgrade'), {icon: 7});
                     return false;
                 }
                 var version = $(this).data("version");
@@ -602,6 +611,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
                         localStorage.removeItem("fastadmin_userinfo");
                     }
                 }
+            },
+            refresh: function (table, name) {
+                //刷新左侧边栏
+                Fast.api.refreshmenu();
+
+                //刷新行数据
+                if ($(".operate[data-name='" + name + "']").length > 0) {
+                    var index = $(".operate[data-name='" + name + "']").closest("tr[data-index]").data("index");
+                    var row = Table.api.getrowbyindex(table, index);
+                    row.addon = typeof Config['addons'][name] !== 'undefined' ? Config['addons'][name] : undefined;
+                    table.bootstrapTable("updateRow", {index: index, row: row});
+                } else if ($(".btn-switch.active").data("type") == "local") {
+                    $(".btn-refresh").trigger("click");
+                }
             }
         }
     };

+ 2 - 1
public/assets/js/backend/index.js

@@ -116,7 +116,8 @@ define(['jquery', 'bootstrap', 'backend', 'addtabs', 'adminlte', 'form'], functi
             $(document).on('refresh', '.sidebar-menu', function () {
                 Fast.api.ajax({
                     url: 'index/index',
-                    data: {action: 'refreshmenu'}
+                    data: {action: 'refreshmenu'},
+                    loading: false
                 }, function (data) {
                     $(".sidebar-menu li:not([data-rel='external'])").remove();
                     $(".sidebar-menu").prepend(data.menulist);

File diff suppressed because it is too large
+ 2 - 2
public/assets/js/require-backend.min.js


+ 1 - 1
public/assets/less/backend.less

@@ -1243,7 +1243,7 @@ table.table-nowrap {
         }
 
         .toolbar {
-            a.btn-refresh, a.btn-del, a.btn-add, a.btn-edit, a.btn-import, a.btn-more, a.btn-recyclebin, a.btn-mini-xs {
+            a.btn-refresh, a.btn-del, a.btn-add, a.btn-edit, a.btn-import, a.btn-more, a.btn-recyclebin, .btn-mini-xs {
                 font-size: 0;
 
                 .fa {