作者 Karson

新增日志过滤

新增安装未知来源插件开关
新增插件纯净模式
修复分片合并未创建文件夹BUG
优化插件管理逻辑
... ... @@ -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();
}
}
... ...
... ... @@ -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';
}
}
... ...
... ... @@ -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";
... ...
... ... @@ -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>
\ No newline at end of file
</html>
... ...
... ... @@ -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) {
... ...
... ... @@ -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"
);
}
... ...
... ... @@ -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!' => '如有重要数据请备份后再操作!!!',
... ...
... ... @@ -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);
... ...
... ... @@ -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>
... ...
<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">
... ...
... ... @@ -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>
... ...
... ... @@ -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);
}
}
... ...
... ... @@ -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] . '.';
... ...
... ... @@ -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']);
}
}
}
... ...
... ... @@ -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);
... ...
... ... @@ -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',
//版本号
... ...
<?php
return array (
'autoload' => false,
'hooks' =>
array (
),
'route' =>
array (
),
'priority' =>
array (
),
);
\ No newline at end of file
return [
'autoload' => false,
'hooks' => [],
'route' => [],
'priority' => [],
];
... ...
<?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',
);
\ No newline at end of file
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',
];
... ...
... ... @@ -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": "*"
},
... ...
... ... @@ -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 {
... ...
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'
}
}
});
});
\ No newline at end of file
... ...
... ... @@ -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");
}
}
}
};
... ...
... ... @@ -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);
... ...
... ... @@ -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 {
... ...