2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 17:17:13 +08:00

特性:修复一堆问题

This commit is contained in:
耗子
2022-11-21 23:22:39 +08:00
parent e9ffcac3b1
commit 397d96d0ed
8305 changed files with 1996 additions and 1005109 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea
.DS_Store
.vendor

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Console\Commands;
use App\Models\Monitor as MonitorModel;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class Monitor extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitor';
/**
* The console command description.
*
* @var string
*/
protected $description = '耗子Linux面板 - 系统监控';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (Setting::query()->where('name', 'monitor')->value('value')) {
$info = self::getNowMonitor();
MonitorModel::query()->create(['info' => json_encode($info)]);
// 删除过期的记录
$days = Setting::query()->where('name', 'monitor_days')->value('value');
MonitorModel::query()->where('created_at', '<', Carbon::now()->subDays($days))->delete();
$this->info(time().' 监控完成');
} else {
$this->info('监控未开启');
}
return Command::SUCCESS;
}
/**
* 系统资源统计
* @return array
*/
private function getNowMonitor(): array
{
// 第一次获取网络信息
$net_info1 = $this->getNetInfo();
// 卡它一秒钟
sleep(1);
// 第二次获取网络信息
$net_info2 = $this->getNetInfo();
// CPU统计信息及负载
$cpu_info = file_get_contents('/proc/cpuinfo');
$physical_list = array();
$physical_sum = 0;
$siblings_sum = 0;
preg_match("/(\d+\.\d+), (\d+\.\d+), (\d+\.\d+)/", exec('uptime'), $uptime);
$uptime_1 = $uptime[1] ?? 'No';
$p_list = explode("\nprocessor", $cpu_info);
foreach ($p_list as $key => $val) {
preg_match("/physical id\s*:(.*)/", $val, $physical);
preg_match("/cpu cores\s*:(.*)/", $val, $cores);
preg_match("/siblings\s*:(.*)/", $val, $siblings);
if (isset($physical[1])) {
if (!in_array($physical[1], $physical_list)) {
$physical_sum += 1;
if (isset($siblings[1])) {
$siblings_sum += $siblings[1];
}
}
$physical_list[] = $physical[1];
}
}
// CPU使用率
$cpu_use = 0.1;
$result = explode("\n", shell_exec('ps aux'));
foreach ($result as $key => $val) {
$val = preg_replace("/\s+/", " ", $val);
$val = (explode(' ', $val));
$cpu_use += isset($val[2]) ? (float) $val[2] : 0;
}
$cpu_use = $siblings_sum > 0 ? ($cpu_use / $siblings_sum) : $cpu_use;
$cpu_use = round($cpu_use, 2);
$cpu_use = min($cpu_use, 100);
// 内存使用率
$result = explode("\n", shell_exec('free -m'));
foreach ($result as $key => $val) {
if (str_contains($val, 'Mem')) {
$mem_list = preg_replace("/\s+/", " ", $val);
} elseif (str_contains($val, 'Swap')) {
$swap_list = preg_replace("/\s+/", " ", $val);
}
}
$mem_arr = explode(' ', $mem_list);
$swap_arr = explode(' ', $swap_list);
// 内存大小MB
$mem_total = $mem_arr[1];
// Swap大小MB
$swap_total = $swap_arr[1];
// 使用中MB
$mem_use = (str_contains($result[0], 'buff/cache')) ? $mem_arr[2] : ($mem_arr[2] - $mem_arr[5] - $mem_arr[6]);
// Swap使用中MB
$swap_use = $swap_arr[2];
// 使用中%
$mem_use_p = round($mem_use / $mem_total, 2) * 100;
// Swap使用中%
$swap_use_p = round($swap_use / $swap_total, 2) * 100;
// 1分钟负载%
$uptime_1_p = $uptime_1 * 10;
$uptime_1_p = min($uptime_1_p, 100);
// 构建返回数组
$res['cpu_use'] = $cpu_use;
$res['uptime'] = $uptime_1;
$res['uptime_p'] = $uptime_1_p;
$res['mem_total'] = $mem_total;
$res['mem_use'] = $mem_use;
$res['mem_use_p'] = $mem_use_p;
$res['swap_total'] = $swap_total;
$res['swap_use'] = $swap_use;
$res['swap_use_p'] = $swap_use_p;
$res['tx_now'] = $net_info2['tx'] - $net_info1['tx'];
$res['rx_now'] = $net_info2['rx'] - $net_info1['rx'];
return $res;
}
/**
* 获取网络统计信息
* @return array
*/
private function getNetInfo(): array
{
$net_result = file_get_contents('/proc/net/dev');
$net_result = explode("\n", $net_result);
foreach ($net_result as $key => $val) {
if ($key < 2) {
continue;
}
$val = str_replace(':', ' ', trim($val));
$val = preg_replace("/[ ]+/", " ", $val);
$arr = explode(' ', $val);
if (!empty($arr[0])) {
$arr = array($arr[0], $arr[1], $arr[9]);
$all_rs[$arr[0].$key] = $arr;
}
}
ksort($all_rs);
$tx = 0;
$rx = 0;
foreach ($all_rs as $key => $val) {
// 排除本地lo
if (str_contains($key, 'lo')) {
continue;
}
$tx += $val[2];
$rx += $val[1];
}
$res['tx'] = $tx;
$res['rx'] = $rx;
return $res;
}
}

View File

@@ -34,6 +34,9 @@ class Panel extends Command
{
$action = $this->argument('action');
switch ($action) {
case 'init':
$this->init();
break;
case 'update':
$this->update();
break;
@@ -46,9 +49,6 @@ class Panel extends Command
case 'writePluginUnInstall':
$this->writePluginUnInstall();
break;
case 'writePluginUpdate':
$this->writePluginUpdate();
break;
default:
$this->error('错误的操作');
break;
@@ -56,6 +56,19 @@ class Panel extends Command
return Command::SUCCESS;
}
/**
* 初始化
* @return void
*/
private function init(): void
{
Setting::query()->updateOrCreate(['name' => 'name'], ['value' => '耗子Linux面板']);
Setting::query()->updateOrCreate(['name' => 'monitor'], ['value' => '1']);
Setting::query()->updateOrCreate(['name' => 'monitor_days'], ['value' => '30']);
Setting::query()->updateOrCreate(['name' => 'mysql_root_password'], ['value' => '']);
Setting::query()->updateOrCreate(['name' => 'postgresql_root_password'], ['value' => '']);
}
/**
* 更新面板
* @return void
@@ -83,10 +96,10 @@ class Panel extends Command
$this->info(shell_exec('rm -rf /tmp/panel.zip'));
$this->info(shell_exec('rm -rf /tmp/database.sqlite'));
$this->info(shell_exec('rm -rf /tmp/plugins'));
$this->info('正在更新数据库...');
$this->info('正在更新面板数据库...');
$this->info(shell_exec('cd /www/panel && php-panel artisan migrate'));
$this->info('正在重启面板服务...');
$this->info(shell_exec('systemctl reload panel.service'));
$this->info(shell_exec('systemctl restart panel.service'));
$this->info('更新完成');
}
@@ -97,21 +110,26 @@ class Panel extends Command
private function getInfo(): void
{
$user = User::query()->where('id', 1);
// 判空
if (empty($user)) {
$this->error('获取失败');
return;
}
// 生成唯一信息
$username = Str::random(6);
$password = Str::random(12);
// 入库
$user->update([
'username' => $username,
'password' => Hash::make($password),
]);
$this->info('面板用户名:' . $username);
$this->info('面板密码:' . $password);
// 判空
if (empty($user)) {
User::query()->create([
'id' => 1,
'username' => $username,
'password' => Hash::make($password),
]);
} else {
// 入库
$user->update([
'username' => $username,
'password' => Hash::make($password),
]);
}
$this->info('面板用户名:'.$username);
$this->info('面板密码:'.$password);
$this->info('访问地址http://IP:8888');
}
@@ -122,19 +140,15 @@ class Panel extends Command
private function writePluginInstall(): void
{
$pluginSlug = $this->argument('a1');
$pluginName = $this->argument('a2');
$pluginVersion = $this->argument('a3');
// 判空
if (empty($pluginSlug) || empty($pluginName) || empty($pluginVersion)) {
if (empty($pluginSlug)) {
$this->error('参数错误');
return;
}
// 入库
Plugin::query()->create([
'slug' => $pluginSlug,
'name' => $pluginName,
'version' => $pluginVersion,
'show' => 0,
]);
$this->info('成功');
@@ -161,25 +175,4 @@ class Panel extends Command
Plugin::query()->where('slug', $pluginSlug)->delete();
$this->info('成功');
}
/**
* 写入插件更新状态
* @return void
*/
private function writePluginUpdate(): void
{
$pluginSlug = $this->argument('a1');
$pluginVersion = $this->argument('a2');
// 判空
if (empty($pluginSlug) || empty($pluginVersion)) {
$this->error('参数错误');
return;
}
// 入库
Plugin::query()->where('slug', $pluginSlug)->update([
'version' => $pluginVersion,
]);
$this->info('成功');
}
}

View File

@@ -15,7 +15,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->command('monitor')->everyMinute();
}
/**

View File

@@ -241,7 +241,7 @@ class InfosController extends Controller
*/
public function getHomePlugins(): JsonResponse
{
$plugins = Plugin::where('show', 1)->get();
$plugins = Plugin::query()->where('show', 1)->get();
// 判空
if ($plugins->isEmpty()) {
$res['code'] = 0;
@@ -310,17 +310,29 @@ class InfosController extends Controller
/**
* 获取已安装的数据库和PHP版本
*/
public function getInstalledDbAndPhp()
public function getInstalledDbAndPhp(): JsonResponse
{
// 判断mysql插件目录是否存在
if (is_dir('/www/panel/plugins/mysql')) {
$mysql_version = 80;
$dbVersions = [];
// 判断mysql插件是否安装
if (isset(PLUGINS['mysql'])) {
$dbVersions['mysql'] = PLUGINS['mysql']['version'];
} else {
$mysql_version = false;
$dbVersions['mysql'] = false;
}
/**
* TODO: PostgreSQL版本
*/
// 判断postgresql插件是否安装
if (isset(PLUGINS['postgresql15'])) {
$dbVersions['postgresql15'] = PLUGINS['postgresql15']['version'];
} else {
$dbVersions['postgresql15'] = false;
}
// 循环获取已安装的PHP版本
$php_versions = Plugin::query()->where('slug', 'like', 'php%')->get();
$php_versions = $php_versions->toArray();
$php_versions = array_column($php_versions, 'slug');
$php_versions = array_map(function ($item) {
return str_replace('php', '', $item);
}, $php_versions);
$php_version = shell_exec('ls /www/server/php');
$php_version = trim($php_version);
@@ -333,10 +345,7 @@ class InfosController extends Controller
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = array(
'db_version' => [
'mysql' => $mysql_version,
'postgresql' => false
],
'db_version' => $dbVersions,
'php_version' => $php_versions
);
return response()->json($res);

View File

@@ -0,0 +1,102 @@
<?php
/**
* 耗子Linux面板 - 监控控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Monitor;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class MonitorsController extends Controller
{
/**
* 修改监控开关
*/
public function setMonitorSwitch(Request $request): JsonResponse
{
$switch = $request->input('switch');
if ($switch) {
$status = true;
} else {
$status = false;
}
Setting::query()->where('name', 'monitor')->update(['value' => $status]);
return response()->json(['code' => 0, 'msg' => '修改成功']);
}
/**
* 修改保存天数
*/
public function setMonitorSaveDays(Request $request): JsonResponse
{
$days = $request->input('days');
Setting::query()->where('name', 'monitor_days')->update(['value' => $days]);
return response()->json(['code' => 0, 'msg' => '修改成功']);
}
/**
* 清空监控数据
*/
public function clearMonitorData(): JsonResponse
{
Monitor::query()->truncate();
return response()->json(['code' => 0, 'msg' => '清空成功']);
}
/**
* 获取监控开关和保存天数
*/
public function getMonitorSwitchAndDays(): JsonResponse
{
$monitor = Setting::query()->where('name', 'monitor')->first();
$monitor_days = Setting::query()->where('name', 'monitor_days')->first();
return response()->json([
'code' => 0, 'msg' => '获取成功',
'data' => ['monitor' => $monitor->value, 'monitor_days' => $monitor_days->value]
]);
}
/**
* 获取监控数据
*/
public function getMonitorData(Request $request): JsonResponse
{
$start = $request->input('start') ?? now();
$end = $request->input('end') ?? now();
$start = Carbon::create($start)->startOfDay();
$end = Carbon::create($end)->endOfDay();
$data = Monitor::query()->where('created_at', '>=', $start)->where('created_at', '<=', $end)->get()->toArray();
$res['code'] = 0;
$res['msg'] = 'success';
if (empty($data)) {
$res['data']['times'] = [];
$res['data']['uptime'] = [];
$res['data']['cpu']['use'] = [];
$res['data']['memory']['mem_use'] = [];
$res['data']['memory']['mem_use_p'] = [];
$res['data']['memory']['swap_use'] = [];
$res['data']['memory']['swap_use_p'] = [];
$res['data']['network']['tx_now'] = [];
$res['data']['network']['rx_now'] = [];
}
foreach ($data as $key => $value) {
$info = json_decode($value['info'], true);
$res['data']['times'][] = Carbon::create($value['created_at'])->tz(config('app.timezone', 'PRC'))->isoFormat('MM-DD HH:mm');
$res['data']['uptime']['uptime'][] = round($info['uptime'], 2);
$res['data']['cpu']['use'][] = round($info['cpu_use'], 2);
$res['data']['memory']['mem_use'][] = round($info['mem_use'], 2);
$res['data']['memory']['mem_use_p'][] = round($info['mem_use_p'], 2);
$res['data']['memory']['swap_use'][] = round($info['swap_use'], 2);
$res['data']['memory']['swap_use_p'][] = round($info['swap_use_p'], 2);
$res['data']['network']['tx_now'][] = round($info['tx_now'] / 1024, 2);
$res['data']['network']['rx_now'][] = round($info['rx_now'] / 1024, 2);
}
return response()->json($res);
}
}

View File

@@ -30,20 +30,20 @@ class PluginsController extends Controller
$data['msg'] = 'success';
$data['data'] = $this->pluginList(false);
foreach ($data['data'] as $k => $v) {
// 获取已装版本
$installVersion = Plugin::query()->where('slug', $v['slug'])->first();
// 判空
if ($installVersion) {
$data['data'][$k]['install_version'] = $installVersion->version;
} else {
$data['data'][$k]['install_version'] = '';
}
// 获取首页显示状态
$shows = Plugin::query()->pluck('show', 'slug');
// 如果本地已安装,则显示本地名称
$data['data'][$k]['name'] = PLUGINS[$v['slug']]['name'] ?? $data['data'][$k]['name'];
// 已装版本
$data['data'][$k]['install_version'] = PLUGINS[$v['slug']]['version'] ?? '';
// 首页显示
$data['data'][$k]['show'] = $shows[$v['slug']] ?? 0;
// 去除不需要的字段
unset($data['data'][$k]['url']);
unset($data['data'][$k]['install']);
unset($data['data'][$k]['uninstall']);
unset($data['data'][$k]['update']);
if (!empty(Plugin::query()->where('slug', $v['slug'])->first())) {
if (isset(PLUGINS[$v['slug']])) {
$data['data'][$k]['control']['installed'] = true;
$data['data'][$k]['control']['allow_uninstall'] = true;
// 判断是否有更新
@@ -109,7 +109,7 @@ class PluginsController extends Controller
// 入库等待安装
$task = new Task();
$task->name = '安装' . $plugin_data['name'];
$task->shell = $plugin_data['install_shell'];
$task->shell = $plugin_data['install'];
$task->status = 'waiting';
$task->log = '/tmp/' . $plugin_data['slug'] . '.log';
$task->save();
@@ -124,7 +124,8 @@ class PluginsController extends Controller
/**
* 卸载插件
* @return
* @param Request $request
* @return JsonResponse
*/
public function uninstall(Request $request): JsonResponse
{
@@ -162,12 +163,12 @@ class PluginsController extends Controller
return response()->json($data);
}
// 判断插件是否未安装
$installed = Task::query()->where('slug', $slug)->first();
/*$installed = Task::query()->where('slug', $slug)->first();
if (!$installed) {
$data['code'] = 1;
$data['msg'] = '请不要重复卸载!';
$data['msg'] = '插件未安装,无需卸载!';
return response()->json($data);
}
}*/
// 判断是否是操作openresty
if ($slug == 'openresty') {
@@ -179,7 +180,7 @@ class PluginsController extends Controller
// 入库等待卸载
$task = new Task();
$task->name = '卸载' . $plugin_data['name'];
$task->shell = $plugin_data['uninstall_shell'];
$task->shell = $plugin_data['uninstall'];
$task->status = 'waiting';
$task->log = '/tmp/' . $plugin_data['slug'] . '.log';
$task->save();

View File

@@ -0,0 +1,248 @@
<?php
/**
* 耗子Linux面板 - 安全控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SafesController extends Controller
{
/**
* 获取防火墙状态
* @return JsonResponse
*/
public function getFirewallStatus(): JsonResponse
{
$firewallStatus = trim(shell_exec("systemctl status firewalld | grep Active | awk '{print $3}'"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($firewallStatus == '(running)') {
$res['data'] = 1;
} else {
$res['data'] = 0;
}
return response()->json($res);
}
/**
* 设置防火墙状态
* @param Request $request
* @return JsonResponse
*/
public function setFirewallStatus(Request $request): JsonResponse
{
$status = $request->input('status');
if ($status) {
shell_exec("systemctl enable firewalld");
shell_exec("systemctl start firewalld");
} else {
shell_exec("systemctl stop firewalld");
shell_exec("systemctl disable firewalld");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取SSH状态
* @return JsonResponse
*/
public function getSshStatus(): JsonResponse
{
$sshStatus = trim(shell_exec("systemctl status sshd | grep Active | awk '{print $3}'"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($sshStatus == '(running)') {
$res['data'] = 1;
} else {
$res['data'] = 0;
}
return response()->json($res);
}
/**
* 设置SSH状态
* @param Request $request
* @return JsonResponse
*/
public function setSshStatus(Request $request): JsonResponse
{
$status = $request->input('status');
if ($status) {
shell_exec("systemctl enable sshd");
shell_exec("systemctl start sshd");
} else {
shell_exec("systemctl stop sshd");
shell_exec("systemctl disable sshd");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取SSH端口
* @return JsonResponse
*/
public function getSshPort(): JsonResponse
{
$sshPort = trim(shell_exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'"));
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $sshPort;
return response()->json($res);
}
/**
* 设置SSH端口
* @param Request $request
* @return JsonResponse
*/
public function setSshPort(Request $request): JsonResponse
{
$port = $request->input('port');
$oldPort = trim(shell_exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'"));
shell_exec("sed -i 's/#Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config");
shell_exec("sed -i 's/Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config");
// 判断ssh是否开启
$sshStatus = trim(shell_exec("systemctl status sshd | grep Active | awk '{print $3}'"));
if ($sshStatus == '(running)') {
shell_exec("systemctl restart sshd");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取ping状态
* @return JsonResponse
*/
public function getPingStatus(): JsonResponse
{
$pingStatus = trim(shell_exec("cat /etc/sysctl.conf | grep 'net.ipv4.icmp_echo_ignore_all = 1'"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($pingStatus && !str_starts_with($pingStatus, '#')) {
$res['data'] = 0;
} else {
$res['data'] = 1;
}
return response()->json($res);
}
/**
* 设置ping状态
* @param Request $request
* @return JsonResponse
*/
public function setPingStatus(Request $request): JsonResponse
{
$status = $request->input('status');
shell_exec("sed -i '/net.ipv4.icmp_echo_ignore_all/d' /etc/sysctl.conf");
if (!$status) {
// 禁止ping
shell_exec("echo 'net.ipv4.icmp_echo_ignore_all = 1' >> /etc/sysctl.conf");
}
shell_exec("sysctl -p");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取防火墙规则
* @return JsonResponse
*/
public function getFirewallRules(): JsonResponse
{
$firewallRules = trim(shell_exec("firewall-cmd --list-all 2>&1"));
// 判断是否开启
if (str_contains($firewallRules, 'not running')) {
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = [];
return response()->json($res);
}
// 正则匹配出ports
preg_match('/ports: (.*)/', $firewallRules, $matches);
$rawPorts = $matches[1];
// 22/tcp 80/tcp 443/tcp 8888/tcp 5432/tcp
$ports = explode(' ', $rawPorts);
// 对ports进行分割为port=>protocol形式
$rules = [];
foreach ($ports as $port) {
$rule = explode('/', $port);
$rules[] = [
'port' => $rule[0],
'protocol' => $rule[1],
];
}
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $rules;
return response()->json($res);
}
/**
* 添加防火墙规则
* @param Request $request
* @return JsonResponse
*/
public function addFirewallRule(Request $request): JsonResponse
{
$port = $request->input('port');
$protocol = $request->input('protocol');
// 判断是否开启
$firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1"));
if ($firewallStatus != 'running') {
$res['code'] = 1;
$res['msg'] = '防火墙未开启';
return response()->json($res);
}
// 清空当前规则
shell_exec("firewall-cmd --remove-port=".$port."/".$protocol." --permanent");
// 添加新的防火墙规则
shell_exec("firewall-cmd --add-port=".$port."/".$protocol." --permanent");
// 重启防火墙
shell_exec("firewall-cmd --reload");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 删除防火墙规则
* @param Request $request
* @return JsonResponse
*/
public function deleteFirewallRule(Request $request): JsonResponse
{
$port = $request->input('port');
$protocol = $request->input('protocol');
// 判断是否开启
$firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1"));
if ($firewallStatus != 'running') {
$res['code'] = 1;
$res['msg'] = '防火墙未开启';
return response()->json($res);
}
// 清空当前规则
shell_exec("firewall-cmd --remove-port=".$port."/".$protocol." --permanent");
// 重启防火墙
shell_exec("firewall-cmd --reload");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
}

View File

@@ -3,69 +3,64 @@
* 耗子Linux面板 - 设置控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SettingsController extends Controller
{
/**
* 获取面板设置
* @return
* @param Request $request
* @return JsonResponse
*/
public function get_settings(Request $request)
public function get(Request $request)
{
$settings = Db::table('setting')->select()->toArray();
$settings = Setting::query()->get()->toArray();
foreach ($settings as $setting) {
$res['data'][$setting['name']] = $setting['value'];
}
$user_password = Db::table('user')->where('username', $request->username)->value('password');
$res['data']['username'] = $request->username;
$res['data']['password'] = $user_password;
if (!empty($settings)) {
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
} else {
$res['code'] = 1;
$res['msg'] = '面板设置获取失败';
$res['data'] = null;
return response()->json($res);
}
return response()->json($res);
}
/**
* 保存面板设置
* @return
* @param Request $request
* @return JsonResponse
*/
public function save_settings(Request $request)
public function save(Request $request): JsonResponse
{
// 获取前端传递过来的数据
$settings = Request::post();
$settings = $request->all();
// 将数据入库
foreach ($settings as $key => $value) {
if ($key == 'access_token' || $key == 'username' || $key == 'password') {
continue;
}
if ($key == 'mysql_root_password') {
$old_mysql_password = Db::table('setting')->where('name', 'mysql_root_password')->value('value');
$old_mysql_password = Setting::query()->where('name', 'mysql_root_password')->value('value');
if ($old_mysql_password != $value) {
shell_exec('/www/server/mysql/bin/mysqladmin -uroot -p' . $old_mysql_password . ' password ' . $value);
shell_exec('mysql -uroot -p'.$old_mysql_password.' -e "ALTER USER \'root\'@\'localhost\' IDENTIFIED BY \''.$value.'\';"');
shell_exec('mysql -uroot -p'.$old_mysql_password.' -e "flush privileges;"');
}
}
Db::table('setting')->where('name', $key)->update(['value' => $value]);
Setting::query()->where('name', $key)->update(['value' => $value]);
}
$res['code'] = 0;
$res['msg'] = 'success';
$old_user_info = Db::table('user')->where('username', $request->username)->select()->toArray();
if ($old_user_info[0]['username'] != $settings['username'] || $old_user_info[0]['password'] != $settings['password']) {
$res['msg'] = 'change';
Db::table('user')->where('username', $request->username)->update(['username' => $settings['username']]);
Db::table('user')->where('username', $settings['username'])->update(['password' => $settings['password']]);
}
return response()->json($res);
}
}

View File

@@ -19,16 +19,6 @@ class UsersController extends Controller
*/
public function login(Request $request)
{
/*$user = User::create([
'id' => '',
'username' => 'haozi',
'password' => Hash::make('haozi'),
]);
return response()->json([
'code' => 200,
'message' => '注册成功',
'data' => $user,
]);*/
// 消毒数据
try {
$credentials = $this->validate($request, [
@@ -38,7 +28,8 @@ class UsersController extends Controller
]);
} catch (ValidationException $e) {
return response()->json([
'message' => '参数错误',
'code' => 1,
'msg' => '参数错误',
'errors' => $e->errors()
], 422);
}
@@ -56,7 +47,7 @@ class UsersController extends Controller
$user = $request->user();
$res['code'] = 0;
$res['msg'] = 'success';
$res['data']['username'] = 'haozi';
$res['data']['username'] = $user->username;
return response()->json($res);
}
}

View File

@@ -3,6 +3,7 @@
* 耗子Linux面板 - 网站控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
@@ -11,6 +12,7 @@ use App\Models\Setting;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
class WebsitesController extends Controller
@@ -53,20 +55,21 @@ class WebsitesController extends Controller
'php' => 'required|integer',
'note' => 'string|nullable|max:255',
'db' => 'required|boolean',
'db_type' => 'required_if:db,true|string|max:10',
'db_name' => 'required_if:db,true|string|max:255',
'db_username' => 'required_if:db,true|string|max:255',
'db_password' => 'required_if:db,true|string|max:255',
'db_type' => 'required_if:db,true|max:10',
'db_name' => 'required_if:db,true|max:255',
'db_username' => 'required_if:db,true|max:255',
'db_password' => 'required_if:db,true|max:255',
]);
} catch (ValidationException $e) {
return response()->json([
'message' => '参数错误',
'code' => 1,
'msg' => '参数错误',
'errors' => $e->errors()
], 422);
], 200);
}
// path为空时设置默认值
if (empty($credentials['path'])) {
$credentials['path'] = '/www/wwwroot/' . $credentials['name'];
$credentials['path'] = '/www/wwwroot/'.$credentials['name'];
}
// ssl默认设置为0
$credentials['ssl'] = 0;
@@ -76,9 +79,9 @@ class WebsitesController extends Controller
// 入库
Website::query()->create($credentials);
// 创建网站目录
shell_exec("mkdir -p " . $credentials['path']);
shell_exec("mkdir -p ".$credentials['path']);
// 创建index.html
shell_exec("touch " . $credentials['path'] . "/index.html");
shell_exec("touch ".$credentials['path']."/index.html");
// 写入到index.html
$index_html = <<<EOF
<!DOCTYPE html>
@@ -95,7 +98,7 @@ class WebsitesController extends Controller
</html>
EOF;
file_put_contents($credentials['path'] . "/index.html", $index_html);
file_put_contents($credentials['path']."/index.html", $index_html);
// 创建nginx配置
$port_list = "";
@@ -103,18 +106,18 @@ EOF;
$domain_arr = explode(PHP_EOL, $domain);
foreach ($domain_arr as $key => $value) {
$temp = explode(":", $value);
$domain_list .= " " . $temp[0];
$domain_list .= " ".$temp[0];
if (!isset($temp[1])) {
if ($key == count($domain_arr) - 1) {
$port_list .= " listen 80;";
} else {
$port_list .= " listen 80;" . PHP_EOL;
$port_list .= " listen 80;".PHP_EOL;
}
} else {
if ($key == count($domain_arr) - 1) {
$port_list .= " listen " . $temp[1] . ";";
$port_list .= " listen ".$temp[1].";";
} else {
$port_list .= " listen " . $temp[1] . ";" . PHP_EOL;
$port_list .= " listen ".$temp[1].";".PHP_EOL;
}
}
@@ -143,7 +146,7 @@ $port_list
# php标记位开始
include enable-php-$credentials[php].conf;
# php标记位结束
# waf标记位开始
waf on;
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
@@ -176,20 +179,20 @@ $port_list
}
EOF;
// 写入nginx配置
file_put_contents('/www/server/vhost/' . $credentials['name'] . '.conf', $nginx_config);
shell_exec('echo "" > /www/server/vhost/rewrite/' . $credentials['name'] . '.conf');
shell_exec('echo "" > /www/server/vhost/ssl/' . $credentials['name'] . '.pem');
shell_exec('echo "" > /www/server/vhost/ssl/' . $credentials['name'] . '.key');
file_put_contents('/www/server/vhost/'.$credentials['name'].'.conf', $nginx_config);
shell_exec('echo "" > /www/server/vhost/rewrite/'.$credentials['name'].'.conf');
shell_exec('echo "" > /www/server/vhost/ssl/'.$credentials['name'].'.pem');
shell_exec('echo "" > /www/server/vhost/ssl/'.$credentials['name'].'.key');
shell_exec("systemctl reload nginx");
// 创建数据库
if ($credentials['db']) {
if ($credentials['db_type'] == 'mysql') {
$password = Setting::query()->where('name', 'mysql_root_password')->value('value');
shell_exec("/www/server/mysql/bin/mysql -u root -p" . $password . " -e \"CREATE DATABASE IF NOT EXISTS " . $credentials['db_name'] . " DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;\" 2>&1");
shell_exec("/www/server/mysql/bin/mysql -u root -p" . $password . " -e \"CREATE USER '" . $credentials['db_username'] . "'@'localhost' IDENTIFIED BY '" . $credentials['db_password'] . "';\"");
shell_exec("/www/server/mysql/bin/mysql -u root -p" . $password . " -e \"GRANT ALL PRIVILEGES ON " . $credentials['db_name'] . ".* TO '" . $credentials['db_username'] . "'@'localhost';\"");
shell_exec("/www/server/mysql/bin/mysql -u root -p" . $password . " -e \"flush privileges;\"");
shell_exec("/www/server/mysql/bin/mysql -u root -p".$password." -e \"CREATE DATABASE IF NOT EXISTS ".$credentials['db_name']." DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;\" 2>&1");
shell_exec("/www/server/mysql/bin/mysql -u root -p".$password." -e \"CREATE USER '".$credentials['db_username']."'@'localhost' IDENTIFIED BY '".$credentials['db_password']."';\"");
shell_exec("/www/server/mysql/bin/mysql -u root -p".$password." -e \"GRANT ALL PRIVILEGES ON ".$credentials['db_name'].".* TO '".$credentials['db_username']."'@'localhost';\"");
shell_exec("/www/server/mysql/bin/mysql -u root -p".$password." -e \"flush privileges;\"");
}
}
$res['code'] = 0;
@@ -251,7 +254,7 @@ EOF;
$name = $request->input('name');
$website = Website::query()->where('name', $name)->first();
// 通过name读取相应的nginx配置
$nginx_config = file_get_contents('/www/server/vhost/' . $name . '.conf');
$nginx_config = file_get_contents('/www/server/vhost/'.$name.'.conf');
// 从nginx配置中port标记位提取全部端口
$port_raw = $this->cut('# port标记位开始', '# port标记位结束', $nginx_config);
preg_match_all('/listen\s+(.*);/', $port_raw, $matches);
@@ -259,7 +262,7 @@ EOF;
if ($k == 0) {
$website['port'] = $v;
} else {
$website['port'] .= PHP_EOL . $v;
$website['port'] .= PHP_EOL.$v;
}
}
// 从nginx配置中server_name标记位提取全部域名
@@ -270,18 +273,30 @@ EOF;
if ($k == 0) {
$website['domain'] = $v;
} else {
$website['domain'] .= PHP_EOL . $v;
$website['domain'] .= PHP_EOL.$v;
}
}
// 从nginx配置中root标记位提取全部根目录
$root_raw = $this->cut('# root标记位开始', '# root标记位结束', $nginx_config);
preg_match_all('/root\s+(.+);/', $root_raw, $matches2);
$website['root'] = $matches2[1][0];
$website['path'] = $matches2[1][0];
// 从nginx配置中index标记位提取全部默认文件
$index_raw = $this->cut('# index标记位开始', '# index标记位结束', $nginx_config);
preg_match_all('/index\s+(.+);/', $index_raw, $matches3);
$website['index'] = $matches3[1][0];
// 检查网站目录下是否存在.user.ini文件且设置了open_basedir
if (file_exists($website['path'].'/.user.ini')) {
$user_ini = file_get_contents($website['path'].'/.user.ini');
if (str_contains($user_ini, 'open_basedir')) {
$website['open_basedir'] = 1;
} else {
$website['open_basedir'] = 0;
}
} else {
$website['open_basedir'] = 0;
}
if ($website['ssl'] == '1') {
$ssl_certificate_raw = $this->cut('# ssl标记位开始', '# ssl标记位结束', $nginx_config);
// 从nginx配置中ssl_certificate标记位提取全部证书路径
@@ -291,15 +306,36 @@ EOF;
preg_match_all('/ssl_certificate_key\s+(.+);/', $ssl_certificate_raw, $matches5);
$website['ssl_certificate_key'] = file_get_contents($matches5[1][0]);
$website['http_redirect'] = str_contains($nginx_config, '# http重定向标记位');
$website['hsts'] = str_contains($nginx_config, '# hsts标记位');
} else {
$website['ssl_certificate'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.pem');
$website['ssl_certificate_key'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.key');
$website['http_redirect'] = 0;
$website['hsts'] = 0;
}
// 从nginx配置中ssl标记位提取waf配置
$waf_raw = $this->cut('# waf标记位开始', '# waf标记位结束', $nginx_config);
if (str_contains($waf_raw, 'waf on;')) {
$website['waf'] = 1;
} else {
$website['waf'] = 0;
}
preg_match_all('/waf_mode\s+(.+);/', $waf_raw, $matches6);
$website['waf_mode'] = $matches6[1][0];
preg_match_all('/waf_cc_deny\s+(.+);/', $waf_raw, $matches7);
$website['waf_cc_deny'] = $matches7[1][0];
preg_match_all('/waf_cache\s+(.+);/', $waf_raw, $matches8);
$website['waf_cache'] = $matches8[1][0];
// 读取伪静态文件的内容
$website['rewrite'] = file_get_contents('/www/server/vhost/rewrite/' . $name . '.conf');
$website['rewrite'] = file_get_contents('/www/server/vhost/rewrite/'.$name.'.conf');
// 读取配置原文
$website['config_raw'] = file_get_contents('/www/server/vhost/' . $name . '.conf');
$website['config_raw'] = file_get_contents('/www/server/vhost/'.$name.'.conf');
// 读取访问日志
$website['log'] = shell_exec('tail -n 100 /www/wwwlogs/' . $name . '.log');
$website['log'] = shell_exec('tail -n 100 /www/wwwlogs/'.$name.'.log');
$res['code'] = 0;
$res['msg'] = 'success';
@@ -315,15 +351,23 @@ EOF;
public function saveSiteSettings(Request $request): JsonResponse
{
// 获取前端传递过来的数据
$name = $request->input('name');
$config = $request->input('config');
$res['code'] = 0;
$res['msg'] = 'success';
// 如果config_raw与本地配置文件不一致则更新配置文件然后返回
$config_raw = shell_exec('cat /www/server/vhost/' . $config['name'] . '.conf');
if (trim($config_raw) != trim($config['config_raw'])) {
file_put_contents('/www/server/vhost/' . $config['name'] . '.conf', $config['config_raw']);
// 如果config_raw与本地配置文件不一致则更新配置文件然后直接返回
$configRaw = shell_exec('cat /www/server/vhost/'.$name.'.conf');
if (trim($configRaw) != trim($config['config_raw'])) {
file_put_contents('/www/server/vhost/'.$name.'.conf', $config['config_raw']);
return response()->json($res);
}
// 检查网站目录是否存在
if (!is_dir($config['path'])) {
$res['code'] = 1;
$res['msg'] = '网站目录不存在';
return response()->json($res);
}
@@ -331,40 +375,112 @@ EOF;
$domain = "server_name";
$domain_arr = explode(PHP_EOL, $config['domain']);
foreach ($domain_arr as $v) {
$domain .= " " . $v;
$domain .= " ".$v;
}
$domain .= ';';
$domain_config_old = $this->cut('# server_name标记位开始', '# server_name标记位结束', $config_raw);
$domain_config_old = $this->cut('# server_name标记位开始', '# server_name标记位结束', $configRaw);
if (!empty(trim($domain_config_old)) && $domain_config_old != PHP_EOL) {
$config_raw = str_replace($domain_config_old, PHP_EOL . " " . $domain . PHP_EOL . ' ', $config_raw);
$configRaw = str_replace($domain_config_old, PHP_EOL." ".$domain.PHP_EOL.' ', $configRaw);
}
// 端口
$port = "";
$port_arr = explode(PHP_EOL, $config['port']);
foreach ($port_arr as $k => $v) {
if ($k != count($port_arr) - 1) {
$port .= " listen " . $v . ';' . PHP_EOL;
$portArr = explode(PHP_EOL, $config['port']);
foreach ($portArr as $k => $v) {
// 检查端口是否均为数字
if (!is_numeric($v) && $v != '443 ssl http2') {
$res['code'] = 1;
$res['msg'] = '端口必须为数字';
return response()->json($res);
}
// 检查是否443端口
if ($v == '443' && $config['ssl'] == '1') {
$v = '443 ssl http2';
}
if ($k != count($portArr) - 1) {
$port .= " listen ".$v.';'.PHP_EOL;
} else {
$port .= " listen " . $v . ';';
$port .= " listen ".$v.';';
}
}
$port_config_old = $this->cut('# port标记位开始', '# port标记位结束', $config_raw);
$port_config_old = $this->cut('# port标记位开始', '# port标记位结束', $configRaw);
if (!empty(trim($port_config_old)) && $port_config_old != PHP_EOL) {
$config_raw = str_replace($port_config_old, PHP_EOL . $port . PHP_EOL . ' ', $config_raw);
$configRaw = str_replace($port_config_old, PHP_EOL.$port.PHP_EOL.' ', $configRaw);
}
// 网站目录
$pathConfig = $this->cut('# root标记位开始', '# root标记位结束', $configRaw);
preg_match_all('/root\s+(.+);/', $pathConfig, $matches1);
$pathConfigOld = $matches1[1][0];
if (!empty(trim($pathConfigOld)) && $pathConfigOld != PHP_EOL) {
$pathConfigNew = str_replace($pathConfigOld, $config['path'], $pathConfig);
$configRaw = str_replace($pathConfig, $pathConfigNew, $configRaw);
}
// 如果开启ssl则更新nginx配置文件
// 默认文件
$indexConfig = $this->cut('# index标记位开始', '# index标记位结束', $configRaw);
preg_match_all('/index\s+(.+);/', $indexConfig, $matches2);
$indexConfigOld = $matches2[1][0];
if (!empty(trim($indexConfigOld)) && $indexConfigOld != PHP_EOL) {
$indexConfigNew = str_replace($indexConfigOld, $config['index'], $indexConfig);
$configRaw = str_replace($indexConfig, $indexConfigNew, $configRaw);
}
// open_basedir
if ($config['open_basedir'] == 1) {
// 判断$config['path']是否为'/'结尾
if (str_ends_with($config['path'], '/')) {
$open_basedir = "open_basedir=".$config['path'].":/tmp/";
} else {
$open_basedir = "open_basedir=".$config['path']."/:/tmp/";
}
// 写入open_basedir配置到.user.ini文件
if (is_dir($config['path'])) {
file_put_contents($config['path'].'/.user.ini', $open_basedir);
// 为.user.ini文件添加i权限
shell_exec('chattr +i '.$config['path'].'/.user.ini');
}
} else {
// 移除.user.ini文件的i权限
shell_exec('chattr -i '.$config['path'].'/.user.ini');
// 删除.user.ini文件
if (file_exists($config['path'].'/.user.ini')) {
unlink($config['path'].'/.user.ini');
}
}
// waf
$waf = $config['waf'] == 1 ? 'on' : 'off';
$wafMode = empty($config['waf_mode']) ? 'DYNAMIC' : $config['waf_mode'];
$wafCcDeny = empty($config['waf_cc_deny']) ? 'rate=1000r/m duration=60m' : $config['waf_cc_deny'];
$wafCache = empty($config['waf_cache']) ? 'capacity=50' : $config['waf_cache'];
$wafConfig = <<<EOF
# waf标记位开始
waf $waf;
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
waf_mode $wafMode;
waf_cc_deny $wafCcDeny;
waf_cache $wafCache;
EOF;
$wafConfig .= PHP_EOL.' ';
$wafConfigOld = $this->cut('# waf标记位开始', '# waf标记位结束', $configRaw);
if (!empty(trim($wafConfigOld)) && $wafConfigOld != PHP_EOL) {
$configRawClean = str_replace($wafConfigOld, "", $configRaw);
} else {
$configRawClean = $configRaw;
}
$configRaw = str_replace('# waf标记位开始', $wafConfig, $configRawClean);
// ssl
if ($config['ssl'] == '1') {
// 写入证书
file_put_contents("/www/server/vhost/ssl/" . $config['name'] . '.pem', $config['ssl_certificate']);
file_put_contents("/www/server/vhost/ssl/" . $config['name'] . '.key', $config['ssl_certificate_key']);
file_put_contents("/www/server/vhost/ssl/".$name.'.pem', $config['ssl_certificate']);
file_put_contents("/www/server/vhost/ssl/".$name.'.key', $config['ssl_certificate_key']);
$ssl_config = <<<EOF
# ssl标记位开始
ssl_certificate /www/server/vhost/ssl/$config[name].pem;
ssl_certificate_key /www/server/vhost/ssl/$config[name].key;
ssl_certificate /www/server/vhost/ssl/$name.pem;
ssl_certificate_key /www/server/vhost/ssl/$name.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
@@ -380,52 +496,55 @@ EOF;
if (\$server_port !~ 443){
return 301 https://\$host\$request_uri;
}
if (\$server_port ~ 443){
add_header Strict-Transport-Security "max-age=63072000" always;
}
error_page 497 https://\$host\$request_uri;
# http重定向标记位结束
EOF;
}
$ssl_config .= PHP_EOL . ' ';
$ssl_config_old = $this->cut('# ssl标记位开始', '# ssl标记位结束', $config_raw);
if (!empty(trim($ssl_config_old)) && $ssl_config_old != PHP_EOL) {
$config_raw_clean = str_replace($ssl_config_old, "", $config_raw);
} else {
$config_raw_clean = $config_raw;
if ($config['hsts'] == '1') {
$ssl_config .= PHP_EOL;
$ssl_config .= <<<EOF
# hsts标记位开始
add_header Strict-Transport-Security "max-age=63072000" always;
# hsts标记位结束
EOF;
}
$config_raw = str_replace('# ssl标记位开始', $ssl_config, $config_raw_clean);
$ssl_config .= PHP_EOL.' ';
$ssl_config_old = $this->cut('# ssl标记位开始', '# ssl标记位结束', $configRaw);
if (!empty(trim($ssl_config_old)) && $ssl_config_old != PHP_EOL) {
$configRaw_clean = str_replace($ssl_config_old, "", $configRaw);
} else {
$configRaw_clean = $configRaw;
}
$configRaw = str_replace('# ssl标记位开始', $ssl_config, $configRaw_clean);
} else {
// 更新nginx配置文件
$ssl_config_old = $this->cut('# ssl标记位开始', '# ssl标记位结束', $config_raw);
$ssl_config_old = $this->cut('# ssl标记位开始', '# ssl标记位结束', $configRaw);
if (!empty(trim($ssl_config_old)) && $ssl_config_old != PHP_EOL) {
$config_raw = str_replace($ssl_config_old, PHP_EOL . ' ', $config_raw);
$configRaw = str_replace($ssl_config_old, PHP_EOL.' ', $configRaw);
}
}
// 如果PHP版本不一致则更新PHP版本
$php_old = Website::query()->where('name', $config['name'])->value('php');
$php_old = Website::query()->where('name', $name)->value('php');
if ($config['php'] != $php_old) {
$php_config_old = $this->cut('# php标记位开始', '# php标记位结束', $config_raw);
$php_config_old = $this->cut('# php标记位开始', '# php标记位结束', $configRaw);
$php_config_new = PHP_EOL;
$php_config_new .= <<<EOL
include enable-php-$config[php].conf;
EOL;
$php_config_new .= PHP_EOL . ' ';
$php_config_new .= PHP_EOL.' ';
if (!empty(trim($php_config_old)) && $php_config_old != PHP_EOL) {
$config_raw = str_replace($php_config_old, $php_config_new, $config_raw);
$configRaw = str_replace($php_config_old, $php_config_new, $configRaw);
}
}
// 将数据入库
Website::query()->where('name', $config['name'])->update(['php' => $config['php']]);
Website::query()->where('name', $config['name'])->update(['ssl' => $config['ssl']]);
file_put_contents('/www/server/vhost/' . $config['name'] . '.conf', $config_raw);
file_put_contents('/www/server/vhost/rewrite/' . $config['name'] . '.conf', $config['rewrite_raw']);
Website::query()->where('name', $name)->update(['php' => $config['php']]);
Website::query()->where('name', $name)->update(['ssl' => $config['ssl']]);
file_put_contents('/www/server/vhost/'.$name.'.conf', $configRaw);
file_put_contents('/www/server/vhost/rewrite/'.$name.'.conf', $config['rewrite']);
shell_exec('systemctl reload nginx');
return response()->json($res);
}
@@ -435,15 +554,29 @@ EOL;
* @param Request $request
* @return JsonResponse
*/
public function cleanSiteLog(Request $request): JsonResponse
public function clearSiteLog(Request $request): JsonResponse
{
$name = $request->input('name');
shell_exec('echo "" > /www/wwwlogs/' . $name . '.log');
shell_exec('echo "" > /www/wwwlogs/'.$name.'.log');
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 修改网站备注
* @param Request $request
* @return JsonResponse
*/
public function updateSiteNote(Request $request): JsonResponse
{
$name = $request->input('name');
$note = $request->input('note');
Website::query()->where('name', $name)->update(['note' => $note]);
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
// 裁剪字符串
private function cut($begin, $end, $str): string

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Http;
use Alexusmai\LaravelFileManager\Services\ACLService\ACLRepository;
class FilesACLRepository implements ACLRepository
{
/**
* Get user ID
*
* @return mixed
*/
public function getUserID()
{
return auth('sanctum')->id();
}
/**
* Get ACL rules list for user
*
* @return array
*/
public function getRules(): array
{
if (auth('sanctum')->check()) {
return [
['disk' => 'www', 'path' => '*', 'access' => 2],
];
} else {
return [
['disk' => 'www', 'path' => '*', 'access' => 0],
];
}
}
}

View File

@@ -10,21 +10,6 @@ use Illuminate\Http\Response;
class Authenticate extends Middleware
{
/**
* Handle an incoming request.
*/
public function handle($request, Closure $next, ...$guards)
{
/* // 获取请求头中的token
$token = $request->header('access_token') ?? $request->input('access_token');
// 将token放入请求中
$request->headers->set('Authorization', 'Bearer ' . $token);
// 验证token
$this->authenticate($request, $guards);*/
// 验证通过
return $next($request);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
@@ -36,7 +21,7 @@ class Authenticate extends Middleware
{
abort(response()->json([
'code' => 1001,
'message' => '登录状态失效'
'msg' => '登录状态失效'
]));
}
}

View File

@@ -53,13 +53,13 @@ class ProcessShell implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
// 查询任务
$task = Task::query()->where('id', $this->task_id)->get();
$task = Task::query()->where('id', $this->task_id)->first();
echo $task->name . "开始执行".PHP_EOL;
// 更新任务状态为running
$task->job_id = $this->job->getJobId();
$task->status = 'running';
$task->save();
//shell_exec($task->shell.' > '.$task->log.' 2>&1 &');
shell_exec($task->shell);
// 更新任务状态
$task->status = 'finished';
$task->save();

18
app/Models/Monitor.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Monitor extends Model
{
use HasFactory;
// 白名单
protected $fillable = [
'info',
'created_at',
'updated_at',
];
}

View File

@@ -6,6 +6,7 @@ use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use App\Services\Plugin;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class PluginServiceProvider extends ServiceProvider
@@ -28,10 +29,10 @@ class PluginServiceProvider extends ServiceProvider
foreach ($plugins->getPlugins() as $plugin) {
// 加载视图路径
$finder->addNamespace($plugin['name'], $plugin['path']."/views");
$finder->addNamespace($plugin['slug'], $plugin['path']."/views");
// 加载语言包
$loader->addNamespace($plugin['name'], $plugin['path']."/lang");
$loader->addNamespace($plugin['slug'], $plugin['path']."/lang");
}
// 加载插件Composer装载文件
@@ -61,5 +62,8 @@ class PluginServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->singleton('plugins', Plugin::class);
// 设置面板名称
$name = DB::table('settings')->where('name', 'name')->value('value');
$this->app['config']['panel.name'] = !empty($name) ? $name : config('panel.name');
}
}

View File

@@ -45,6 +45,8 @@ class Plugin
}
/**
* 读取所有插件
*
* @return Collection|null
* @throws FileNotFoundException
* @throws Exception
@@ -88,15 +90,18 @@ class Plugin
// 初始化插件信息
$plugin = [];
$plugin['name'] = (Arr::get($package, 'name'));
$plugin['slug'] = (Arr::get($package, 'slug'));
$plugin['version'] = (Arr::get($package, 'version'));
$plugin['path'] = $this->getPluginsDir().DIRECTORY_SEPARATOR.$dirname;
if ($plugins->has($plugin['name'])) {
if ($plugins->has($plugin['slug'])) {
continue;
}
$plugins->put($plugin['name'], $plugin);
$plugins->put($plugin['slug'], $plugin);
}
define('PLUGINS', $plugins->toArray());
$this->plugins = $plugins;
}

View File

@@ -6,7 +6,6 @@
"license": "MIT",
"require": {
"php": "^8.1.9",
"alexusmai/laravel-file-manager": "^3.0",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^9.19",
"laravel/octane": "^1.3",

278
composer.lock generated
View File

@@ -4,68 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "38a554bd11fc94b74f3d7073ea061d23",
"content-hash": "e04515c415a76e849e40b5659911865a",
"packages": [
{
"name": "alexusmai/laravel-file-manager",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/alexusmai/laravel-file-manager.git",
"reference": "41d8e1d6020ccb67003f87beb1e3f56b5fa51165"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alexusmai/laravel-file-manager/zipball/41d8e1d6020ccb67003f87beb1e3f56b5fa51165",
"reference": "41d8e1d6020ccb67003f87beb1e3f56b5fa51165",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"intervention/image": "^2.7",
"intervention/imagecache": "^2.5",
"laravel/framework": "^9.0",
"league/flysystem": "^3.0",
"php": "^8.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Alexusmai\\LaravelFileManager\\FileManagerServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Alexusmai\\LaravelFileManager\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Aleksandr Manekin",
"email": "alexusmai@gmail.com",
"role": "Developer"
}
],
"description": "File manager for Laravel",
"homepage": "https://github.com/alexusami/laravel-file-manager",
"keywords": [
"file",
"laravel",
"manager"
],
"support": {
"issues": "https://github.com/alexusmai/laravel-file-manager/issues",
"source": "https://github.com/alexusmai/laravel-file-manager/tree/v3.0.3"
},
"time": "2022-07-15T12:31:26+00:00"
},
{
"name": "brick/math",
"version": "0.10.2",
@@ -957,157 +897,6 @@
],
"time": "2022-10-26T14:07:24+00:00"
},
{
"name": "intervention/image",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "04be355f8d6734c826045d02a1079ad658322dad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
"reference": "04be355f8d6734c826045d02a1079ad658322dad",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
},
"laravel": {
"providers": [
"Intervention\\Image\\ImageServiceProvider"
],
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
}
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "intervention/imagecache",
"version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/imagecache.git",
"reference": "270d1e72ddff2fc0a6d3c7e6cbc9d23c9ec1e3e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/imagecache/zipball/270d1e72ddff2fc0a6d3c7e6cbc9d23c9ec1e3e4",
"reference": "270d1e72ddff2fc0a6d3c7e6cbc9d23c9ec1e3e4",
"shasum": ""
},
"require": {
"illuminate/cache": "^5.5|~6|~7|~8|~9",
"illuminate/filesystem": "^5.5|~6|~7|~8|~9",
"intervention/image": "~2.2",
"nesbot/carbon": "^2.39",
"opis/closure": "^3.5",
"php": "~7.2|~8"
},
"require-dev": {
"phpunit/phpunit": "^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "http://intervention.io/"
}
],
"description": "Caching extension for the Intervention Image Class",
"homepage": "https://image.intervention.io",
"keywords": [
"cache",
"gd",
"image",
"imagick",
"laravel"
],
"support": {
"issues": "https://github.com/Intervention/imagecache/issues",
"source": "https://github.com/Intervention/imagecache/tree/2.5.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2022-01-22T11:14:47+00:00"
},
{
"name": "laminas/laminas-diactoros",
"version": "2.20.0",
@@ -2566,71 +2355,6 @@
],
"time": "2022-10-28T22:51:32+00:00"
},
{
"name": "opis/closure",
"version": "3.6.3",
"source": {
"type": "git",
"url": "https://github.com/opis/closure.git",
"reference": "3d81e4309d2a927abbe66df935f4bb60082805ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opis/closure/zipball/3d81e4309d2a927abbe66df935f4bb60082805ad",
"reference": "3d81e4309d2a927abbe66df935f4bb60082805ad",
"shasum": ""
},
"require": {
"php": "^5.4 || ^7.0 || ^8.0"
},
"require-dev": {
"jeremeamia/superclosure": "^2.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.6.x-dev"
}
},
"autoload": {
"files": [
"functions.php"
],
"psr-4": {
"Opis\\Closure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marius Sarca",
"email": "marius.sarca@gmail.com"
},
{
"name": "Sorin Sarca",
"email": "sarca_sorin@hotmail.com"
}
],
"description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.",
"homepage": "https://opis.io/closure",
"keywords": [
"anonymous functions",
"closure",
"function",
"serializable",
"serialization",
"serialize"
],
"support": {
"issues": "https://github.com/opis/closure/issues",
"source": "https://github.com/opis/closure/tree/3.6.3"
},
"time": "2022-01-27T09:35:39+00:00"
},
{
"name": "overtrue/laravel-lang",
"version": "6.0.3",

View File

@@ -1,174 +0,0 @@
<?php
use Alexusmai\LaravelFileManager\Services\ConfigService\DefaultConfigRepository;
use Alexusmai\LaravelFileManager\Services\ACLService\ConfigACLRepository;
return [
/**
* Set Config repository
*
* Default - DefaultConfigRepository get config from this file
*/
'configRepository' => DefaultConfigRepository::class,
/**
* ACL rules repository
*
* Default - ConfigACLRepository (see rules in - aclRules)
*/
'aclRepository' => \App\Http\FilesACLRepository::class,
//********* Default configuration for DefaultConfigRepository **************
/**
* LFM Route prefix
* !!! WARNING - if you change it, you should compile frontend with new prefix(baseUrl) !!!
*/
'routePrefix' => 'file-manager',
/**
* List of disk names that you want to use
* (from config/filesystems)
*/
'diskList' => ['www'],
/**
* Default disk for left manager
*
* null - auto select the first disk in the disk list
*/
'leftDisk' => null,
/**
* Default disk for right manager
*
* null - auto select the first disk in the disk list
*/
'rightDisk' => null,
/**
* Default path for left manager
*
* null - root directory
*/
'leftPath' => null,
/**
* Default path for right manager
*
* null - root directory
*/
'rightPath' => null,
/**
* Image cache ( Intervention Image Cache )
*
* set null, 0 - if you don't need cache (default)
* if you want use cache - set the number of minutes for which the value should be cached
*/
'cache' => null,
/**
* File manager modules configuration
*
* 1 - only one file manager window
* 2 - one file manager window with directories tree module
* 3 - two file manager windows
*/
'windowsConfig' => 1,
/**
* File upload - Max file size in KB
*
* null - no restrictions
*/
'maxUploadFileSize' => null,
/**
* File upload - Allow these file types
*
* [] - no restrictions
*/
'allowFileTypes' => [],
/**
* Show / Hide system files and folders
*/
'hiddenFiles' => true,
/***************************************************************************
* Middleware
*
* Add your middleware name to array -> ['web', 'auth', 'admin']
* !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!!
*/
'middleware' => ['web'],
/***************************************************************************
* ACL mechanism ON/OFF
*
* default - false(OFF)
*/
'acl' => true,
/**
* Hide files and folders from file-manager if user doesn't have access
*
* ACL access level = 0
*/
'aclHideFromFM' => true,
/**
* ACL strategy
*
* blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
*
* whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
*/
'aclStrategy' => 'whitelist',
/**
* ACL Rules cache
*
* null or value in minutes
*/
'aclRulesCache' => null,
//********* Default configuration for DefaultConfigRepository END **********
/***************************************************************************
* ACL rules list - used for default ACL repository (ConfigACLRepository)
*
* 1 it's user ID
* null - for not authenticated user
*
* 'disk' => 'disk-name'
*
* 'path' => 'folder-name'
* 'path' => 'folder1*' - select folder1, folder12, folder1/sub-folder, ...
* 'path' => 'folder2/*' - select folder2/sub-folder,... but not select folder2 !!!
* 'path' => 'folder-name/file-name.jpg'
* 'path' => 'folder-name/*.jpg'
*
* * - wildcard
*
* access: 0 - deny, 1 - read, 2 - read/write
*/
'aclRules' => [
null => [
//['disk' => 'public', 'path' => '/', 'access' => 2],
],
1 => [
//['disk' => 'public', 'path' => 'images/arch*.jpg', 'access' => 2],
//['disk' => 'public', 'path' => 'files/*', 'access' => 1],
],
],
/**
* Enable slugification of filenames of uploaded files.
*
*/
'slugifyNames' => false,
];

View File

@@ -3,6 +3,6 @@ use Illuminate\Support\Facades\Facade;
return [
'name' => '耗子Linux面板',
'version' => '20221102',
'version' => '20221120',
'plugin_dir' => '/www/panel/plugins',
];

View File

@@ -17,7 +17,6 @@ return new class extends Migration
$table->id();
$table->string('slug')->unique()->comment('插件标识');
$table->string('name')->comment('插件名称');
$table->string('version')->comment('插件版本');
$table->boolean('show')->comment('是否首页显示')->nullable();
$table->timestamps();
});

View File

@@ -15,7 +15,7 @@ return new class extends Migration
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->integer('job_id')->comment('任务ID');
$table->integer('job_id')->nullable()->comment('任务ID');
$table->string('name')->comment('任务名');
$table->string('status')->default('waiting')->comment('任务状态');
$table->string('shell')->nullable()->comment('任务脚本');

View File

@@ -13,8 +13,9 @@ return new class extends Migration
*/
public function up()
{
Schema::create('{{ table }}', function (Blueprint $table) {
Schema::create('monitors', function (Blueprint $table) {
$table->id();
$table->json('info')->comment('监控记录');
$table->timestamps();
});
}
@@ -26,6 +27,6 @@ return new class extends Migration
*/
public function down()
{
Schema::dropIfExists('{{ table }}');
Schema::dropIfExists('monitors');
}
};

View File

@@ -2,7 +2,7 @@
/**
* Name: OpenResty插件控制器
* Author:耗子
* Date: 2022-11-02
* Date: 2022-11-21
*/
namespace Plugins\Openresty\Controllers;

View File

@@ -1,4 +1,5 @@
{
"name": "openresty",
"version": "20221102"
"name": "OpenResty-1.21.4",
"slug": "openresty",
"version": "1.21.4.1"
}

View File

@@ -2,7 +2,7 @@
/**
* Name: OpenResty插件
* Author: 耗子
* Date: 2022-11-02
* Date: 2022-11-21
*/
use Illuminate\Support\Facades\Route;

View File

@@ -1,7 +1,7 @@
<!--
Name: Openresty管理器
Author: 耗子
Date: 2022-11-02
Date: 2022-11-21
-->
<title>OpenResty</title>
<div class="layui-fluid" id="component-tabs">

View File

@@ -44,7 +44,7 @@ layui.define(['laytpl', 'layer'], function(exports){
});
//跳转到登入页
location.hash = '/user/login';
location.hash = '/login';
};
//Ajax请求

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
<title>数据库 - PostgreSQL</title>
<h1 style="text-align: center; padding-top: 20px;">暂不支持</h1>
<h1 style="text-align: center; padding-top: 20px;">管理正在开发中</h1>

View File

@@ -1,25 +1,2 @@
<title>文件</title>
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">文件Beta版</div>
<div class="layui-card-body">
<iframe id="panel_fm" src="{{ asset('../../api/fm') }}" style="width: 100%; height: 800px; border: none;"></iframe>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['jquery'], function () {
var $ = layui.jquery;
// 获取iframe的src
var src = $('#panel_fm').attr('src');
// src后面加上access_token
if (layui.data('haozi_panel').access_token !== undefined) {
$('#panel_fm').attr('src', src + '?access_token=' + layui.data('haozi_panel').access_token);
}
});
</script>
<h1 style="text-align: center; padding-top: 20px;">管理正在开发中!</h1>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<link rel="stylesheet" href="https://cdnjs.cdn.wepublish.cn/bootstrap/5.1.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cdn.wepublish.cn/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css">
<div id="fm" style="height: 800px;"></div>
<link rel="stylesheet" href="{{ asset('../../vendor/file-manager/css/file-manager.css') }}">
<script src="{{ asset('../../vendor/file-manager/js/file-manager.js') }}"></script>
</body>
</html>

View File

@@ -6,19 +6,24 @@ Date: 2022-10-14
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div id="address1" class="layui-col-md12">
<div style="background: #fff;" class="layui-collapse" lay-filter="home_ad">
<div class="layui-colla-content layui-show">
<div class="layui-collapse">
<div style="background: #fff;" class="layui-colla-content layui-show">
<div class="text" style="overflow: hidden;height: 22px;">
<ul style="margin-top: -2px;">
<li>
<a href="https://hzbk.net"
<div class="layui-carousel" id="home_ad" lay-filter="home_ad">
<div carousel-item="">
<a style="background: #fff;" href="https://hzbk.net"
title="耗子博客" target="_blank"><i class="layui-icon layui-icon-release"></i> 耗子博客</a>
</li>
</ul>
<a style="background: #fff;" href="https://weavatar.com"
title="WeAvatar" target="_blank"><i class="layui-icon layui-icon-release"></i> WeAvatar - 互联网公共头像服务</a>
<a style="background: #fff;" href="https://wepublish.cn"
title="WePublish" target="_blank"><i class="layui-icon layui-icon-release"></i> WePublish - WordPress的本土化版本</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="monitor1" class="layui-col-md6">
@@ -190,7 +195,7 @@ Date: 2022-10-14
element.render('progress');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
@@ -202,10 +207,19 @@ Date: 2022-10-14
clearInterval(home_timer);
home_timer = setInterval(refresh_home_info, 3000);
// 获取系统信息,这部分信息无需更新。
layui.use(['index', 'jquery', 'admin'], function () {
layui.use(['index', 'jquery', 'admin', 'carousel'], function () {
let $ = layui.jquery
, admin = layui.admin
, element = layui.element;
, element = layui.element
, carousel = layui.carousel;
carousel.render({
elem: '#home_ad'
, width: '100%'
, height: '200px'
, anim: 'fade'
, arrow: 'none'
, indicator: 'none'
});
admin.req({
url: "/api/panel/info/getSystemInfo"
, method: 'get'
@@ -217,10 +231,10 @@ Date: 2022-10-14
}
$('#home_os_name').text(result.data.os_name);
$('#home_panel_version').text(result.data.panel_version);
$('#home_uptime').text('已不间断运行' + result.data.uptime + '天');
$('#home_uptime').text('已不间断运行 ' + result.data.uptime + ' 天');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
@@ -254,7 +268,7 @@ Date: 2022-10-14
,
skin: 'layui-anim layui-anim-upbit'
,
content: '请在SSH执行<span class="layui-badge-rim">cd /www/panel && php-panel artisan panel update</span>以更新面板!'
content: '请在SSH执行<span class="layui-badge-rim">panel update</span>以更新面板!'
});
});
});

View File

@@ -20,7 +20,7 @@
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无Bug 永不宕机 //
// Name耗子Linux面板 Author耗子 Date2022-10-14 //
// Name耗子Linux面板 Author耗子 Date2022-11-21 //
////////////////////////////////////////////////////////////////////
-->
@@ -45,17 +45,14 @@
base: 'panel/'
, version: {{config('panel.version')}}
}).use('index', function () {
var layer = layui.layer, admin = layui.admin, $ = layui.jquery;
let layer = layui.layer, admin = layui.admin, $ = layui.jquery;
layer.ready(function () {
/**
* TODO: 因为开发临时注释了测试版tips需要取消注释
*/
/*admin.popup({
content: '当前面板为测试版本,如遇到问题请联系耗子反馈!</br>QQ: 823374000'
admin.popup({
content: '当前面板为公测版本,如遇到问题请联系耗子反馈!</br>QQ: 823374000'
, area: '380px'
, shade: false
, offset: 't'
});*/
});
});
});
</script>

View File

@@ -50,6 +50,14 @@
, router = layui.router()
, search = router.search;
// 判断并清除定时器
if (typeof home_timer !== 'undefined') {
clearInterval(home_timer);
}
if (typeof install_plugin_timer !== 'undefined') {
clearInterval(install_plugin_timer);
}
form.render();
//提交

View File

@@ -1,2 +1,334 @@
<title>监控</title>
<h1 style="text-align: center; padding-top: 20px;">功能开发中!</h1>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-inline">
<span style="margin-right: 10px;">开启监控</span><input type="checkbox" id="monitor-switch"
lay-filter="monitor" lay-skin="switch"
lay-text="ON|OFF">
<span style="margin-left: 40px; margin-right: 10px;">保存天数</span>
<div class="layui-input-inline"><input type="number" name="monitor-save-days" class="layui-input"
style="height: 30px; margin-top: 5px;" min=0 max=30 disabled>
</div>
<div class="layui-input-inline">
<button id="save_monitor_date" class="layui-btn layui-btn-sm" style="margin-left: 10px;">确定
</button>
</div>
</div>
<div style="float: right;">
<button id="clear_monitor_record" class="layui-btn layui-btn-sm layui-btn-danger">清空监控记录
</button>
</div>
</div>
</div>
<div class="layui-row layui-col-space10">
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>负载</span>
<div style="float: right;">
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">昨天
</button>--}}
<button class="layui-btn layui-btn-xs">今天</button>
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">
最近七天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">
最近30天
</button>
<button id="test" class="layui-btn layui-btn-xs layui-btn-primary">自定义时间
</button>--}}
</div>
</div>
<div class="layui-card-body">
<div id="load_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>CPU</span>
<div style="float: right;">
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">昨天
</button>--}}
<button class="layui-btn layui-btn-xs">今天</button>
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">
最近七天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">最近30天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">自定义时间
</button>--}}
</div>
</div>
<div class="layui-card-body">
<div id="cpu_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="layui-row layui-col-space10">
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>内存</span>
<div style="float: right;">
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">昨天
</button>--}}
<button class="layui-btn layui-btn-xs">今天</button>
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">
最近七天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">
最近30天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">
自定义时间
</button>--}}
</div>
</div>
<div class="layui-card-body">
<div id="memory_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>网络</span>
<div style="float: right;">
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">昨天
</button>--}}
<button class="layui-btn layui-btn-xs">今天</button>
{{--<button class="layui-btn layui-btn-xs layui-btn-primary">最近七天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">最近30天
</button>
<button class="layui-btn layui-btn-xs layui-btn-primary">自定义时间
</button>--}}
</div>
</div>
<div class="layui-card-body">
<div id="network_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['admin', 'view', 'form', 'echarts', 'element', 'carousel'], function () {
var admin = layui.admin;
var view = layui.view;
var $ = layui.jquery;
var form = layui.form;
// 获取监控开关和保存天数
admin.req({
url: '/api/panel/monitor/getMonitorSwitchAndDays',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
if (res.data.monitor == 1) {
$('#monitor-switch').attr('checked', true);
} else {
$('#monitor-switch').attr('checked', false);
}
$('input[name="monitor-save-days"]').val(res.data.monitor_days);
// 移除禁用
$('input[name="monitor-save-days"]').removeAttr('disabled');
form.render();
}
}
});
// 监听switch开关是否开启监控
form.on('switch(monitor)', function (data) {
admin.req({
url: '/api/panel/monitor/setMonitorSwitch',
type: 'post',
dataType: 'json',
data: {switch: data.elem.checked},
success: function (res) {
if (res.code === 0) {
layer.msg('修改成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
// 监听保存天数按钮
$('#save_monitor_date').click(function () {
var days = $('input[name="monitor-save-days"]').val();
if (days == '') {
layer.msg('请输入保存天数', {icon: 2});
return false;
}
admin.req({
url: '/api/panel/monitor/setMonitorSaveDays',
type: 'post',
dataType: 'json',
data: {days: days},
success: function (res) {
if (res.code === 0) {
layer.msg('修改成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
// 监听清除监控数据按钮
$('#clear_monitor_record').click(function () {
layer.confirm('确定要清除监控数据吗?', function (index) {
admin.req({
url: '/api/panel/monitor/clearMonitorData',
type: 'post',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
layer.msg('清除成功', {icon: 1});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
layer.close(index);
});
});
// 获取监控数据
admin.req({
url: '/api/panel/monitor/getMonitorData',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.code !== 0) {
layer.msg(res.msg, {icon: 2});
return false;
}
renderEcharts('load_monitor', '负载监控', undefined, res.data.times, [{
name: '负载',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.uptime.uptime,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}]);
renderEcharts('cpu_monitor', 'CPU监控', undefined, res.data.times, [{
name: '使用率',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.cpu.use,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], '{value} %', '单位 %');
renderEcharts('memory_monitor', '内存', {
x: 'left',
data: ["内存", "Swap"]
}, res.data.times, [{
name: '内存',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.memory.mem_use,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: 'Swap',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.memory.swap_use,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], '{value} M', '单位 MB');
renderEcharts('network_monitor', '网络', {
x: 'left',
data: ["", ""]
}, res.data.times, [{
name: '出',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.network.tx_now,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '入',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.network.rx_now,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], '{value} Kb', '单位 Kb/s');
}, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
// 渲染图表
function renderEcharts(element_id, title, legend = undefined, data_xAxis, series, formatter = '{value}', yName = '') {
var Chart = echarts.init(document.getElementById(element_id), layui.echartsTheme);
var option = {
title: {text: title, x: 'center', textStyle: {fontSize: 20}},
tooltip: {trigger: 'axis'},
legend: legend,
xAxis: [{type: 'category', boundaryGap: false, data: data_xAxis}],
yAxis: [{
name: yName,
type: 'value',
axisLabel: {
formatter: formatter
}
}],
dataZoom: {
show: true,
realtime: true,
start: 0,
end: 100
},
series: series
};
Chart.setOption(option);
window.onresize = Chart.resize;
}
</script>

View File

@@ -1,4 +1,4 @@
<title>应用</title>
<title>插件</title>
<div class="layui-fluid">
<div class="layui-card">
@@ -23,16 +23,16 @@
<!-- 操作按钮模板 -->
<script type="text/html" id="store-control-tpl">
@{{# if(d.control.installed == true && d.control.allow_uninstall == true){ }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
<a class="layui-btn layui-btn-xs" lay-event="open">管理</a>
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
@{{# } else{ }}
@{{# if(d.control.installed == true && d.control.allow_uninstall == false){ }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
<a class="layui-btn layui-btn-xs" lay-event="open">管理</a>
@{{# } else{ }}
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
@@ -44,8 +44,8 @@
<input type="checkbox" name="plugin-show-home" lay-skin="switch" lay-text="ON|OFF"
lay-filter="plugin-show-home"
value="@{{ d.show }}" data-plugin-slug="@{{ d.slug }}"
@{{ d.run==
1 ? 'checked' : '' }} />
@{{ d.show==
1 ? 'checked' : '' }} />
</script>
</div>
</div>
@@ -86,14 +86,17 @@
layer.confirm('确定安装该插件吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/install',
url: '/api/panel/plugin/install',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code == 0) {
layer.msg('安装:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000}, function () {
if (res.code === 0) {
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
}, function () {
location.reload();
});
} else {
@@ -101,7 +104,7 @@
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
@@ -109,13 +112,13 @@
layer.confirm('确定卸载该插件吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/uninstall',
url: '/api/panel/plugin/uninstall',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code == 0) {
if (res.code === 0) {
layer.msg('卸载:' + data.name + ' 成功!', {icon: 1, time: 1000}, function () {
location.reload();
});
@@ -124,7 +127,7 @@
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
@@ -132,14 +135,17 @@
layer.confirm('确定升级该插件吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/update',
url: '/api/panel/plugin/update',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code == 0) {
layer.msg('安装:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000}, function () {
if (res.code === 0) {
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
}, function () {
location.reload();
});
} else {
@@ -147,7 +153,7 @@
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
@@ -160,6 +166,7 @@
let show = obj.elem.checked ? 1 : 0;
console.log(plugin_slug); //当前行数据
});
/*form.render(null, 'store-form');

View File

@@ -1,2 +1,305 @@
<title>安全</title>
<h1 style="text-align: center; padding-top: 20px;">功能开发中!</h1>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-inline">
<span style="margin-right: 10px;">防火墙</span>
<input type="checkbox" id="safe_firewall" lay-filter="safe_firewall" lay-skin="switch"
lay-text="ON|OFF"/>
<span style="margin: 0px 10px;">启用SSH</span>
<input type="checkbox" id="safe_ssh" lay-filter="safe_ssh" lay-skin="switch" lay-text="ON|OFF"/>
<span style="margin: 0px 10px 0px 20px;">SSH端口</span>
<div class="layui-input-inline" style="width: 80px;">
<input type="number" id="safe_ssh_port" class="layui-input" style="height: 30px; margin-top: 5px;"
min=1
max=65535 disabled/>
</div>
<div class="layui-input-inline">
<button id="safe_ssh_port_save" class="layui-btn layui-btn-sm layui-btn-primary">确定
</button>
</div>
<span style="margin: 0px 10px 0px 20px;">允许Ping</span>
<input type="checkbox" id="switch_ping" lay-filter="safe_ping" lay-skin="switch" lay-text="ON|OFF"/>
</div>
<div class="layui-inline" style="float: right;">
{{--<button class="layui-btn layui-btn-sm layui-btn-danger">清空 OpenResty 日志
</button>--}}
</div>
</div>
</div>
<div id="vm_security">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-inline">
<span style="margin-right: 10px;">端口控制</span>
<div class="layui-input-inline">
<input id="safe_add_firewall_rule_port" type="text" name="safe_add_firewall_rule_port" class="layui-input"
placeholder="端口号3306"
min=1 max=65535/>
</div>
<div class="layui-input-inline">
<select id="safe_add_firewall_rule_protocol" lay-filter="safe_add_firewall_rule_protocol"
style="height: 30px; margin-top: 5px;">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
</div>
<div class="layui-input-inline">
<button id="safe_add_firewall_rule" class="layui-btn layui-btn-sm" style="margin-top: -4px;">放行
</button>
</div>
</div>
</div>
<div class="layui-card-body">
<table class="layui-hide" id="safe-port" lay-filter="safe-port"></table>
<!-- 右侧删除端口 -->
<script type="text/html" id="safe-port-setting">
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
</div>
</div>
</div>
<script>
layui.use(['layer', 'admin', 'form', 'laypage', 'table'], function () {
var $ = layui.$;
var admin = layui.admin;
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
// 获取防火墙状态
admin.req({
url: '/api/panel/safe/getFirewallStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// 防火墙
if (res.data) {
$('#safe_firewall').attr('checked', true);
} else {
$('#safe_firewall').attr('checked', false);
}
form.render();
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
// 获取SSH状态
admin.req({
url: '/api/panel/safe/getSshStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// SSH
if (res.data) {
$('#safe_ssh').attr('checked', true);
} else {
$('#safe_ssh').attr('checked', false);
}
form.render();
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
// 获取SSH端口
admin.req({
url: '/api/panel/safe/getSshPort'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// SSH端口
$('#safe_ssh_port').val(res.data);
$('#safe_ssh_port').attr('disabled', false);
form.render();
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
// 获取ping状态
admin.req({
url: '/api/panel/safe/getPingStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// ping
if (res.data) {
$('#switch_ping').attr('checked', true);
} else {
$('#switch_ping').attr('checked', false);
}
form.render();
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
// 设置防火墙开关
form.on('switch(safe_firewall)', function (data) {
admin.req({
url: '/api/panel/safe/setFirewallStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked === true ? 1 : 0
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
// 设置SSH开关
form.on('switch(safe_ssh)', function (data) {
admin.req({
url: '/api/panel/safe/setSshStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked === true ? 1 : 0
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
// 设置ping开关
form.on('switch(safe_ping)', function (data) {
admin.req({
url: '/api/panel/safe/setPingStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked === true ? 1 : 0
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
table.render({
elem: '#safe-port'
, url: '/api/panel/safe/getFirewallRules'
//, toolbar: '#website-list-bar'
, title: '网站列表'
, cols: [[
{field: 'port', title: '端口', width: 200}
, {field: 'protocol', title: '协议'}
, {fixed: 'right', title: '操作', toolbar: '#safe-port-setting', width: 150}
]]
/**
* TODO: 分页
*/
//, page: true
});
table.on('tool(safe-port)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除 <b style="color: red;">' + data.protocol + '</b> 端口 <b style="color: red;">' + data.port + '</b> 吗?', function (index) {
admin.req({
url: "/api/panel/safe/deleteFirewallRule"
, method: 'post'
, data: data
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板端口删除失败接口返回' + result);
layer.msg('网站删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('<b style="color: red;">' + data.protocol + '</b> 端口 <b style="color: red;">' + data.port + '</b> 删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
layer.close(index);
});
}
});
// 监听ssh端口保存
$('#safe_ssh_port_save').click(function () {
var port = Number($('#safe_ssh_port').val());
// 判断端口是否合法
if (isNaN(port) || port < 1 || port > 65535) {
layer.msg('端口号不合法', {icon: 2});
return false;
}
var index = layer.load(2);
admin.req({
url: '/api/panel/safe/setSshPort'
, type: 'post'
, dataType: 'json'
, data: {
port: port
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.msg('设置成功', {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
// 监听添加端口保存
$('#safe_add_firewall_rule').click(function () {
var port = Number($('#safe_add_firewall_rule_port').val());
var protocol = $('#safe_add_firewall_rule_protocol').val();
// 判断端口是否合法
if (isNaN(port) || port < 1 || port > 65535) {
layer.msg('端口号不合法', {icon: 2});
return false;
}
var index = layer.load(2);
admin.req({
url: '/api/panel/safe/addFirewallRule'
, type: 'post'
, dataType: 'json'
, data: {
port: port,
protocol: protocol
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.msg('设置成功', {icon: 1});
table.reload('safe-port');
} else {
layer.msg(res.msg, {icon: 2});
}
}
});
});
});
</script>

View File

@@ -15,31 +15,17 @@ Date: 2022-10-14
<div class="layui-form-item">
<label class="layui-form-label">面板名称</label>
<div class="layui-input-inline">
<input type="text" name="name" value="获取中ing..." class="layui-input">
<input type="text" name="name" value="获取中ing..." class="layui-input" disabled/>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的显示名称</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="font-size: 13px;">MySQL密码</label>
<div class="layui-input-inline">
<input type="text" name="mysql_root_password" value="获取中ing..." class="layui-input">
<input type="text" name="mysql_root_password" value="获取中ing..." class="layui-input" disabled/>
</div>
<div class="layui-form-mid layui-word-aux">修改MySQL的root密码</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板账号</label>
<div class="layui-input-inline">
<input type="text" name="username" value="获取中ing..." class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">修改面板账号</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板密码</label>
<div class="layui-input-inline">
<input type="password" name="password" value="获取中ing..." class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">修改面板密码</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="panel_setting_submit">确认修改</button>
@@ -69,7 +55,7 @@ Date: 2022-10-14
// ajax获取设置项并赋值
admin.req({
url: "/api/panel/get_settings"
url: "/api/panel/setting/get"
, method: 'get'
, success: function (result) {
if (result.code !== 0) {
@@ -80,9 +66,10 @@ Date: 2022-10-14
form.val("panel_setting",
result.data
);
$('input').attr('disabled', false);
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
@@ -116,7 +103,7 @@ Date: 2022-10-14
form.on('submit(panel_setting_submit)', function (obj) {
// 提交修改
admin.req({
url: "/api/panel/save_settings"
url: "/api/panel/setting/save"
, method: 'post'
, data: obj.field
, success: function (result) {
@@ -126,15 +113,9 @@ Date: 2022-10-14
return false;
}
layer.msg('面板设置保存成功!');
if (result.msg == 'change') {
layer.msg('您已修改用户名/密码,请重新登录!');
setTimeout(function () {
location.hash = '/logout';
}, 2000);
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
return false;

View File

@@ -78,7 +78,7 @@ Date: 2022-10-09
});
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
})
}
@@ -136,11 +136,11 @@ Date: 2022-10-09
table.on('tool(panel-task-finished)', function (obj) {
let data = obj.data;
if (obj.event === 'remove') {
layer.confirm('确定卸载该插件安装记录吗?', function (index) {
layer.confirm('确定移除该记录吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/task/deleteTask',
type: 'GET',
type: 'post',
data: {
name: data.name
}
@@ -155,7 +155,7 @@ Date: 2022-10-09
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});

View File

@@ -1,7 +1,7 @@
<!--
Name: 网站 - 添加
Author: 耗子
Date: 2022-10-14
Date: 2022-11-21
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-website-form">
@@ -39,7 +39,7 @@ Date: 2022-10-14
<label class="layui-form-label">数据库</label>
<div class="layui-input-block">
<select name="db_type" lay-filter="add-website-db">
<option value="" selected=""></option>
<option value="" selected="">不使用</option>
@{{# layui.each(d.params.db_version, function(index, item){ }}
@{{# if(item){ }}
<option value="@{{ index }}">@{{ index }}</option>
@@ -53,19 +53,19 @@ Date: 2022-10-14
<div class="layui-inline">
<label class="layui-form-label">数据库名</label>
<div class="layui-input-inline">
<input type="text" name="db_name" lay-verify="required" autocomplete="off" class="layui-input">
<input type="text" name="db_name" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">数据库用户</label>
<div class="layui-input-inline">
<input type="text" name="db_username" lay-verify="required" autocomplete="off" class="layui-input">
<input type="text" name="db_username" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">数据库密码</label>
<div class="layui-input-inline">
<input type="text" name="db_password" lay-verify="required" autocomplete="off" class="layui-input">
<input type="text" name="db_password" autocomplete="off" class="layui-input">
</div>
</div>
</div>
@@ -117,7 +117,12 @@ Date: 2022-10-14
if (data.value === 'mysql') {
$("#add-website-db-info").show();
$('input[name="db_name"]').val($('input[name="name"]').val() + '_mysql');
$('input[name="db_user"]').val($('input[name="name"]').val() + '_mysql');
$('input[name="db_username"]').val($('input[name="name"]').val() + '_mysql');
$('input[name="db_password"]').val($('input[name="name"]').val() + '_password');
}else if(data.value === 'postgresql15') {
$("#add-website-db-info").show();
$('input[name="db_name"]').val($('input[name="name"]').val() + '_postgresql');
$('input[name="db_username"]').val($('input[name="name"]').val() + '_postgresql');
$('input[name="db_password"]').val($('input[name="name"]').val() + '_password');
}
});
@@ -129,7 +134,6 @@ Date: 2022-10-14
}else{
data.field.db = 1;
}
console.log(data.field);
admin.req({
url: "/api/panel/website/add"
, method: 'post'
@@ -151,7 +155,7 @@ Date: 2022-10-14
});
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
return false;

View File

@@ -0,0 +1,306 @@
<!--
Name: 网站 - 编辑
Author: 耗子
Date: 2022-11-21
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">域名端口</li>
<li>基本设置</li>
<li>防火墙</li>
<li>SSL</li>
<li>伪静态</li>
<li>配置原文</li>
<li>访问日志</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<!-- 域名绑定 -->
<div class="layui-form layui-form-pane">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">域名</label>
<div class="layui-input-block">
<textarea name="domain" lay-verify="required"
placeholder="请输入域名,一行一个支持泛域名"
class="layui-textarea">@{{ d.params.config.domain }}</textarea>
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<textarea name="port" lay-verify="required"
placeholder="请输入访问端口,一行一个"
class="layui-textarea">@{{ d.params.config.port }}</textarea>
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<!-- 基本设置 -->
<div class="layui-form layui-form-pane">
<div class="layui-form-item">
<label class="layui-form-label">网站目录</label>
<div class="layui-input-block">
<input type="text" name="path" autocomplete="off" placeholder="请输入网站目录"
class="layui-input" value="@{{ d.params.config.path }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">默认文档</label>
<div class="layui-input-block">
<input type="text" name="index" autocomplete="off" placeholder="请输入默认文档,以空格隔开"
class="layui-input" value="@{{ d.params.config.index }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">PHP版本</label>
<div class="layui-input-block">
<select name="php" lay-filter="website-php">
@{{# layui.each(d.params.php_version, function(index, item){ }}
@{{# if(index == "00"){ }}
<option value="@{{ item }}" selected="">@{{ item }}</option>
@{{# }else{ }}
<option value="@{{ item }}">@{{ item }}</option>
@{{# } }}
@{{# }); }}
</select>
</div>
</div>
<div class="layui-form-item" pane="">
<label class="layui-form-label">防跨站攻击</label>
<div class="layui-input-block">
<input type="checkbox" name="open_basedir" lay-skin="switch" lay-text="ON|OFF"
@{{ d.params.config.open_basedir== 1 ? 'checked' : '' }} />
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<!-- 防火墙 -->
<blockquote class="layui-elem-quote layui-quote-nm">
面板自带开源的 ngx_waf 防火墙<br>文档参考:<a
href="https://docs.addesp.com/ngx_waf/zh-cn/advance/directive.html"
target="_blank">https://docs.addesp.com/ngx_waf/zh-cn/advance/directive.html</a>
</blockquote>
<div class="layui-form layui-form-pane">
<div class="layui-form-item" pane="">
<label class="layui-form-label">总开关</label>
<div class="layui-input-inline">
<input type="checkbox" name="waf" lay-skin="switch" lay-text="ON|OFF"
@{{ d.params.config.waf== 1 ? 'checked' : '' }} />
</div>
<div class="layui-form-mid layui-word-aux">只有打开了总开关,下面的设置才会生效!</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">模式</label>
<div class="layui-input-block">
<input type="text" name="waf_mode" autocomplete="off" placeholder="DYNAMIC"
class="layui-input" value="@{{ d.params.config.waf_mode }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">CC</label>
<div class="layui-input-block">
<input type="text" name="waf_cc_deny" autocomplete="off"
placeholder="rate=1000r/m duration=60m"
class="layui-input" value="@{{ d.params.config.waf_cc_deny }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">缓存</label>
<div class="layui-input-block">
<input type="text" name="waf_cache" autocomplete="off" placeholder="capacity=50"
class="layui-input" value="@{{ d.params.config.waf_cache }}">
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<!-- SSL -->
<div class="layui-form layui-form-pane">
<div class="layui-form-item" pane="">
<label class="layui-form-label">总开关</label>
<div class="layui-input-inline">
<input type="checkbox" name="ssl" lay-skin="switch" lay-text="ON|OFF"
@{{ d.params.config.ssl== 1 ? 'checked' : '' }} />
</div>
<div class="layui-form-mid layui-word-aux">只有打开了总开关,下面的设置才会生效!</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">HTTP跳转</label>
<div class="layui-input-block">
<input type="checkbox" name="http_redirect" lay-skin="switch" lay-text="ON|OFF"
@{{ d.params.config.http_redirect== 1 ? 'checked' : '' }} />
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">HSTS</label>
<div class="layui-input-inline">
<input type="checkbox" name="hsts" lay-skin="switch" lay-text="ON|OFF"
@{{ d.params.config.hsts== 1 ? 'checked' : '' }} />
</div>
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">证书</label>
<div class="layui-input-block">
<textarea name="ssl_certificate" placeholder="请输入pem证书文件的内容"
class="layui-textarea">@{{ d.params.config.ssl_certificate }}</textarea>
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">私钥</label>
<div class="layui-input-block">
<textarea name="ssl_certificate_key" placeholder="请输入key私钥文件的内容"
class="layui-textarea">@{{ d.params.config.ssl_certificate_key }}</textarea>
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<!-- 伪静态 -->
<blockquote class="layui-elem-quote layui-quote-nm">
设置伪静态规则,填入 <code>location</code> 部分即可
</blockquote>
<div id="rewrite-editor" style="height: -webkit-fill-available;">@{{ d.params.config.rewrite }}</div>
</div>
<div class="layui-tab-item">
<!-- 配置原文 -->
<blockquote class="layui-elem-quote layui-quote-nm">
如果您不了解配置规则,请勿随意修改,否则可能会导致网站无法访问或面板功能异常!如果已经遇到问题,可尝试:
<button id="site-config-restore" class="layui-btn layui-btn-xs">重置配置</button>
<br>
如果你修改了原文,那么点击保存后,其余的修改将不会生效!
</blockquote>
<div id="config-editor" style="height: -webkit-fill-available;">@{{ d.params.config.config_raw }}</div>
</div>
<div class="layui-tab-item">
<!-- 访问日志 -->
<button id="clean-site-log" class="layui-btn">清空日志</button>
<pre class="layui-code" lay-options="{about: '@{{ d.params.config.name }}.log'}">@{{ d.params.config.log }}</pre>
</div>
</div>
</div>
<div class="layui-footer">
<button id="save-site-config" class="layui-btn">保存设置</button>
</div>
</script>
<script>
let rewriteEditor = '';
let configEditor = '';
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, element = layui.element
, layer = layui.layer
, laydate = layui.laydate
, table = layui.table
, form = layui.form;
console.log(params);
form.render();
rewriteEditor = ace.edit("rewrite-editor", {
mode: "ace/mode/nginx",
selectionStyle: "text"
});
configEditor = ace.edit("config-editor", {
mode: "ace/mode/nginx",
selectionStyle: "text"
});
layui.code({
encode: true
, about: false
});
$("#clean-site-log").click(function () {
layer.confirm('确定要清空日志吗?', function (index) {
layer.close(index);
layer.load(2);
admin.req({
url: '/api/panel/website/clearSiteLog'
, type: 'post'
, data: {name: params.config.name}
, success: function (res) {
layer.closeAll('loading');
if (res.code === 0) {
layer.msg('已清空', {icon: 1});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.msg(res.msg, {icon: 2});
}
}
, error: function (xhr, status, error) {
layer.closeAll('loading');
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
});
$('#save-site-config').click(function () {
layer.load(2);
var port = $('textarea[name="port"]').val();
var reg = new RegExp(/\n443.*\n?/);
// 如果开启了https就自动添加443端口
if ($('input[name="ssl"]').prop('checked') && !reg.test(port)) {
console.log(port);
port = port + '\n443';
}
// 如果关闭了https就自动删除443端口
if (!$('input[name="ssl"]').prop('checked') && reg.test(port)) {
// 正则替换
port = port.replace(/443.*\n?/, '');
console.log(port);
}
admin.req({
url: '/api/panel/website/saveSiteSettings'
, type: 'post'
, data: {
name: params.config.name,
config: {
domain: $('textarea[name="domain"]').val(),
port: port,
ssl: $('input[name="ssl"]').prop('checked') ? 1 : 0,
http_redirect: $('input[name="http_redirect"]').prop('checked') ? 1 : 0,
hsts: $('input[name="hsts"]').prop('checked') ? 1 : 0,
ssl_certificate: $('textarea[name="ssl_certificate"]').val(),
ssl_certificate_key: $('textarea[name="ssl_certificate_key"]').val(),
path: $('input[name="path"]').val(),
index: $('input[name="index"]').val(),
php: $('select[name="php"]').val(),
open_basedir: $('input[name="open_basedir"]').prop('checked') ? 1 : 0,
waf: $('input[name="waf"]').prop('checked') ? 1 : 0,
waf_mode: $('input[name="waf_mode"]').val(),
waf_cc_deny: $('input[name="waf_cc_deny"]').val(),
waf_cache: $('input[name="waf_cache"]').val(),
rewrite: rewriteEditor.getValue(),
config_raw: configEditor.getValue()
}
}
, success: function (res) {
layer.closeAll('loading');
if (res.code === 0) {
layer.msg('保存成功', {icon: 1});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.msg(res.msg, {icon: 2});
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
});
};
</script>

View File

@@ -1,7 +1,7 @@
<!--
Name: 网站
Name: 网站 - 列表
Author: 耗子
Date: 2022-10-14
Date: 2022-11-21
-->
<title>网站</title>
<div class="layui-fluid">
@@ -28,9 +28,8 @@ Date: 2022-10-14
<script type="text/html" id="website-run">
<input type="checkbox" name="run" lay-skin="switch" lay-text="ON|OFF"
lay-filter="website-run-checkbox"
value="@{{ d.run }}" data-website-name="@{{ d.name }}"
@{{ d.run==
1 ? 'checked' : '' }} />
value="@{{ d.status }}" data-website-name="@{{ d.name }}"
@{{ d.status== 1 ? 'checked' : '' }} />
</script>
</div>
</div>
@@ -65,7 +64,7 @@ Date: 2022-10-14
, icon: 2
, content: '已安装的PHP和DB版本获取失败接口返回' + xhr.status + ' ' + xhr.statusText
});
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
table.render({
@@ -75,8 +74,8 @@ Date: 2022-10-14
, title: '网站列表'
, cols: [[
{field: 'name', title: '网站名', width: 200, fixed: 'left', unresize: true, sort: true, edit: 'text'}
, {field: 'run', title: '运行', width: 90, templet: '#website-run', unresize: true}
, {field: 'directory', title: '目录', width: 250}
, {field: 'run', title: '运行', width: 100, templet: '#website-run', unresize: true}
, {field: 'path', title: '目录', width: 250}
, {field: 'php', title: 'PHP', width: 60}
, {field: 'ssl', title: 'SSL', width: 110}
, {field: 'note', title: '备注', edit: 'textarea'}
@@ -140,7 +139,7 @@ Date: 2022-10-14
layer.alert('网站' + data.name + '删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error)
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
layer.close(index);
@@ -149,7 +148,7 @@ Date: 2022-10-14
let config;
admin.req({
url: "/api/panel/get_website_settings?name=" + data.name
url: "/api/panel/website/getSiteSettings?name=" + data.name
, method: 'get'
, beforeSend: function (request) {
layer.load();
@@ -186,13 +185,28 @@ Date: 2022-10-14
}
});
// 单元格编辑
table.on('edit(website-run)', function (obj) {
var value = obj.value //得到修改后的值
, data = obj.data //得到所在行所有键值
, field = obj.field; //得到字段
layer.msg('[ID: ' + data.id + '] ' + field + ' 字段更改为:' + value, {
offset: '15px'
// 网站备注编辑
table.on('edit(website-list)', function (obj) {
var value = obj.value // 得到修改后的值
, data = obj.data; // 得到行数据
admin.req({
url: "/api/panel/website/updateSiteNote"
, method: 'post'
, data: {
name: data.name,
note: value
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板网站备注更新失败接口返回' + result);
layer.msg('网站备注更新失败,请刷新重试!')
return false;
}
layer.alert('网站 ' + data.name + ' 备注更新成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});

View File

@@ -1,6 +1,9 @@
<?php
use App\Http\Controllers\Api\MonitorsController;
use App\Http\Controllers\Api\PluginsController;
use App\Http\Controllers\Api\SafesController;
use App\Http\Controllers\Api\SettingsController;
use App\Http\Controllers\Api\TasksController;
use App\Http\Controllers\Api\UsersController;
use App\Http\Controllers\Api\WebsitesController;
@@ -23,9 +26,6 @@ use App\Http\Controllers\Api\InfosController;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware('auth:sanctum')->get('/fm', function () {
return view('fm');
});
Route::prefix('panel')->group(function () {
Route::prefix('user')->group(function () {
// 登录
@@ -73,13 +73,63 @@ Route::prefix('panel')->group(function () {
// 获取网站列表
Route::get('getList', [WebsitesController::class, 'getList']);
Route::post('add', [WebsitesController::class, 'add']);
Route::post('delete', [WebsitesController::class, 'delete']);
Route::get('getSiteSettings', [WebsitesController::class, 'getSiteSettings']);
Route::post('saveSiteSettings', [WebsitesController::class, 'saveSiteSettings']);
Route::post('clearSiteLog', [WebsitesController::class, 'clearSiteLog']);
Route::post('updateSiteNote', [WebsitesController::class, 'updateSiteNote']);
});
// 监控
Route::middleware('auth:sanctum')->prefix('monitor')->group(function () {
// 获取监控数据
Route::get('getMonitorData', [MonitorsController::class, 'getMonitorData']);
// 获取监控开关和保存天数
Route::get('getMonitorSwitchAndDays', [MonitorsController::class, 'getMonitorSwitchAndDays']);
// 设置监控开关
Route::post('setMonitorSwitch', [MonitorsController::class, 'setMonitorSwitch']);
// 设置保存天数
Route::post('setMonitorSaveDays', [MonitorsController::class, 'setMonitorSaveDays']);
// 清空监控数据
Route::post('clearMonitorData', [MonitorsController::class, 'clearMonitorData']);
});
// 安全
Route::middleware('auth:sanctum')->prefix('safe')->group(function () {
// 获取防火墙状态
Route::get('getFirewallStatus', [SafesController::class, 'getFirewallStatus']);
// 设置防火墙状态
Route::post('setFirewallStatus', [SafesController::class, 'setFirewallStatus']);
// 获取SSH状态
Route::get('getSshStatus', [SafesController::class, 'getSshStatus']);
// 设置SSH状态
Route::post('setSshStatus', [SafesController::class, 'setSshStatus']);
// 获取SSH端口
Route::get('getSshPort', [SafesController::class, 'getSshPort']);
// 设置SSH端口
Route::post('setSshPort', [SafesController::class, 'setSshPort']);
// 获取ping状态
Route::get('getPingStatus', [SafesController::class, 'getPingStatus']);
// 设置ping状态
Route::post('setPingStatus', [SafesController::class, 'setPingStatus']);
// 获取防火墙规则
Route::get('getFirewallRules', [SafesController::class, 'getFirewallRules']);
// 添加防火墙规则
Route::post('addFirewallRule', [SafesController::class, 'addFirewallRule']);
// 删除防火墙规则
Route::post('deleteFirewallRule', [SafesController::class, 'deleteFirewallRule']);
});
// 插件
Route::middleware('auth:sanctum')->prefix('plugin')->group(function () {
// 获取插件列表
Route::get('getList', [PluginsController::class, 'getList']);
Route::get('install', [PluginsController::class, 'install']);
Route::get('uninstall', [PluginsController::class, 'uninstall']);
Route::post('install', [PluginsController::class, 'install']);
Route::post('uninstall', [PluginsController::class, 'uninstall']);
});
// 设置
Route::middleware('auth:sanctum')->prefix('setting')->group(function () {
// 获取设置
Route::get('get', [SettingsController::class, 'get']);
// 保存设置
Route::post('save', [SettingsController::class, 'save']);
});
});

View File

@@ -1,5 +0,0 @@
vendor/
node_modules/
npm-debug.log
.idea

View File

@@ -1,7 +0,0 @@
# Contributing
> Contributions are welcome, and are accepted via pull requests.
## Pull requests
Please ensure all pull requests are made against the `develop` branch on GitHub.

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) Aleksandr Manekin alexusmai@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,74 +0,0 @@
# Laravel File Manager
[![Latest Stable Version](http://poser.pugx.org/alexusmai/laravel-file-manager/v)](https://packagist.org/packages/alexusmai/laravel-file-manager)
[![Total Downloads](http://poser.pugx.org/alexusmai/laravel-file-manager/downloads)](https://packagist.org/packages/alexusmai/laravel-file-manager)
[![Latest Unstable Version](http://poser.pugx.org/alexusmai/laravel-file-manager/v/unstable)](https://packagist.org/packages/alexusmai/laravel-file-manager)
[![License](http://poser.pugx.org/alexusmai/laravel-file-manager/license)](https://packagist.org/packages/alexusmai/laravel-file-manager)
[![PHP Version Require](http://poser.pugx.org/alexusmai/laravel-file-manager/require/php)](https://packagist.org/packages/alexusmai/laravel-file-manager)
![Laravel File Manager](https://raw.github.com/alexusmai/vue-laravel-file-manager/master/src/assets/laravel-file-manager.gif?raw=true)
**DEMO:** [Laravel File Manager](http://file-manager.webmai.ru/)
**Vue.js Frontend:** [alexusmai/vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
## Documentation
[Laravel File Manager Docs](./docs/index.md)
* [Installation](./docs/installation.md)
* [Configuration](./docs/configuration.md)
* [Integration](./docs/integration.md)
* [ACL](./docs/acl.md)
* [Events](./docs/events.md)
* [Update](./docs/update.md)
## Features
* Frontend on Vue.js - [vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
* Work with the file system is organized by the standard means Laravel Flysystem:
* Local, FTP, S3, Dropbox ...
* The ability to work only with the selected disks
* Several options for displaying the file manager:
* One-panel view
* One-panel + Directory tree
* Two-panel
* The minimum required set of operations:
* Creating files
* Creating folders
* Copying / Cutting Folders and Files
* Renaming
* Uploading files (multi-upload)
* Downloading files
* Two modes of displaying elements - table and grid
* Preview for images
* Viewing images
* Full screen mode
* More operations (v.2):
* Audio player (mp3, ogg, wav, aac), Video player (webm, mp4) - ([Plyr](https://github.com/sampotts/plyr))
* Code editor - ([Code Mirror](https://github.com/codemirror/codemirror))
* Image cropper - ([Cropper.js](https://github.com/fengyuanchen/cropperjs))
* Zip / Unzip - only for local disks
* Integration with WYSIWYG Editors:
* CKEditor 4
* TinyMCE 4
* TinyMCE 5
* SummerNote
* Standalone button
* ACL - access control list
* delimiting access to files and folders
* two work strategies:
* blacklist - Allow everything that is not forbidden by the ACL rules list
* whitelist - Deny everything, that not allowed by the ACL rules list
* You can use different repositories for the rules - an array (configuration file), a database (there is an example implementation), or you can add your own.
* You can hide files and folders that are not accessible.
* Events (v2.2)
* Thumbnails lazy load
* Dynamic configuration (v2.4)
* Supported locales : ru, en, ar, sr, cs, de, es, nl, zh-CN, fa, it, tr, fr, pt-BR, zh-TW, pl
## In a new version 3
- **Version 3 only works with Laravel 9!**
- Vue.js 3
- Bootstrap 5
- Bootstrap Icons

View File

@@ -1,40 +0,0 @@
{
"name": "alexusmai/laravel-file-manager",
"description": "File manager for Laravel",
"keywords": [
"laravel",
"file",
"manager"
],
"authors": [
{
"name": "Aleksandr Manekin",
"email": "alexusmai@gmail.com",
"role": "Developer"
}
],
"homepage": "https://github.com/alexusami/laravel-file-manager",
"license": "MIT",
"minimum-stability": "dev",
"require": {
"php": "^8.0",
"ext-zip": "*",
"ext-json": "*",
"laravel/framework": "^9.0",
"league/flysystem": "^3.0",
"intervention/image": "^2.7",
"intervention/imagecache": "^2.5"
},
"autoload": {
"psr-4": {
"Alexusmai\\LaravelFileManager\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Alexusmai\\LaravelFileManager\\FileManagerServiceProvider"
]
}
}
}

View File

@@ -1,174 +0,0 @@
<?php
use Alexusmai\LaravelFileManager\Services\ConfigService\DefaultConfigRepository;
use Alexusmai\LaravelFileManager\Services\ACLService\ConfigACLRepository;
return [
/**
* Set Config repository
*
* Default - DefaultConfigRepository get config from this file
*/
'configRepository' => DefaultConfigRepository::class,
/**
* ACL rules repository
*
* Default - ConfigACLRepository (see rules in - aclRules)
*/
'aclRepository' => ConfigACLRepository::class,
//********* Default configuration for DefaultConfigRepository **************
/**
* LFM Route prefix
* !!! WARNING - if you change it, you should compile frontend with new prefix(baseUrl) !!!
*/
'routePrefix' => 'file-manager',
/**
* List of disk names that you want to use
* (from config/filesystems)
*/
'diskList' => ['public'],
/**
* Default disk for left manager
*
* null - auto select the first disk in the disk list
*/
'leftDisk' => null,
/**
* Default disk for right manager
*
* null - auto select the first disk in the disk list
*/
'rightDisk' => null,
/**
* Default path for left manager
*
* null - root directory
*/
'leftPath' => null,
/**
* Default path for right manager
*
* null - root directory
*/
'rightPath' => null,
/**
* Image cache ( Intervention Image Cache )
*
* set null, 0 - if you don't need cache (default)
* if you want use cache - set the number of minutes for which the value should be cached
*/
'cache' => null,
/**
* File manager modules configuration
*
* 1 - only one file manager window
* 2 - one file manager window with directories tree module
* 3 - two file manager windows
*/
'windowsConfig' => 2,
/**
* File upload - Max file size in KB
*
* null - no restrictions
*/
'maxUploadFileSize' => null,
/**
* File upload - Allow these file types
*
* [] - no restrictions
*/
'allowFileTypes' => [],
/**
* Show / Hide system files and folders
*/
'hiddenFiles' => true,
/***************************************************************************
* Middleware
*
* Add your middleware name to array -> ['web', 'auth', 'admin']
* !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!!
*/
'middleware' => ['web'],
/***************************************************************************
* ACL mechanism ON/OFF
*
* default - false(OFF)
*/
'acl' => false,
/**
* Hide files and folders from file-manager if user doesn't have access
*
* ACL access level = 0
*/
'aclHideFromFM' => true,
/**
* ACL strategy
*
* blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
*
* whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
*/
'aclStrategy' => 'blacklist',
/**
* ACL Rules cache
*
* null or value in minutes
*/
'aclRulesCache' => null,
//********* Default configuration for DefaultConfigRepository END **********
/***************************************************************************
* ACL rules list - used for default ACL repository (ConfigACLRepository)
*
* 1 it's user ID
* null - for not authenticated user
*
* 'disk' => 'disk-name'
*
* 'path' => 'folder-name'
* 'path' => 'folder1*' - select folder1, folder12, folder1/sub-folder, ...
* 'path' => 'folder2/*' - select folder2/sub-folder,... but not select folder2 !!!
* 'path' => 'folder-name/file-name.jpg'
* 'path' => 'folder-name/*.jpg'
*
* * - wildcard
*
* access: 0 - deny, 1 - read, 2 - read/write
*/
'aclRules' => [
null => [
//['disk' => 'public', 'path' => '/', 'access' => 2],
],
1 => [
//['disk' => 'public', 'path' => 'images/arch*.jpg', 'access' => 2],
//['disk' => 'public', 'path' => 'files/*', 'access' => 1],
],
],
/**
* Enable slugification of filenames of uploaded files.
*
*/
'slugifyNames' => false,
];

View File

@@ -1,185 +0,0 @@
# ACL
You can use the access control system to differentiate access to files and folders for different users.
For this you need to make the following settings.
Open configuration file - config/file-manager.php
1. Turn ON ACL system (fm-acl middleware will turn ON automatically)
```php
// set true
'acl' => true,
```
2. You can hide files and folders to which the user does not have access(access = 0).
```php
'aclHideFromFM' => true,
```
3. ACL system operation strategies:
```php
/**
* ACL strategy
*
* blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
*
* whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
*/
'aclStrategy' => 'blacklist',
```
4. Set the rule repository, the default is the configuration file.
```php
/**
* ACL rules repository
*
* default - config file(ConfigACLRepository)
*/
'aclRepository' => \Alexusmai\LaravelFileManager\Services\ACLService\ConfigACLRepository::class,
```
Now you can add your rules in 'aclRules' array. But if you want to store your rules in another place, such as a database, you need to create your own class, and implements two functions from ACLRepository.
I have already made a similar class for an example, and if it suits you, you can use it. You only need to replace the repository name in the configuration file. And add a new migration to the database.
```php
php artisan vendor:publish --tag=fm-migrations
```
See [/src/Services/ACLService/DBACLRepository.php](../src/Services/ACLService/DBACLRepository.php) and [/migrations/2019_02_06_174631_make_acl_rules_table.php](./../migrations/2019_02_06_174631_make_acl_rules_table.php)
## Example 1
I have disk 'images' in /config/filesystems.php for folder /public/images
```php
'disks' => [
'images' => [
'driver' => 'local',
'root' => public_path('images'),
'url' => env('APP_URL').'/images/',
],
]
```
This disk contain:
```php
/ // disk root folder
|-- nature // folder
|-- cars // folder
|-- icons
|-- image1.jpg // file
|-- image2.jpg
|-- avatar.png
```
I add this disk to file-manager config file
```php
'diskList' => ['images'],
'aclStrategy' => 'blacklist',
// now it's a black list
'aclRules' => [
// null - for not authenticated users
null => [
['disk' => 'images', 'path' => 'nature', 'access' => 0], // guest don't have access for this folder
['disk' => 'images', 'path' => 'icons', 'access' => 1], // only read - guest can't change folder - rename, delete
['disk' => 'images', 'path' => 'icons/*', 'access' => 1], // only read all files and foders in this folder
['disk' => 'images', 'path' => 'image*.jpg', 'access' => 0], // can't read and write (preview, rename, delete..)
['disk' => 'images', 'path' => 'avatar.png', 'access' => 1], // only read (view)
],
// for user with ID = 1
1 => [
['disk' => 'images', 'path' => 'cars', 'access' => 0], // don't have access
['disk' => 'public', 'path' => 'image*.jpg', 'access' => 1], // only read (view)
],
],
```
## Example 2
> Task: For each registered user, a new folder is created with his name(in folder /users). You want to allow users access only to their folders. But for an administrator with ID = 1, allow access to all folders.
- You need to create a new repository for ACL rules, for example, in the / app / Http folder
```php
<?php
namespace App\Http;
use Alexusmai\LaravelFileManager\Services\ACLService\ACLRepository;
class UsersACLRepository implements ACLRepository
{
/**
* Get user ID
*
* @return mixed
*/
public function getUserID()
{
return \Auth::id();
}
/**
* Get ACL rules list for user
*
* @return array
*/
public function getRules(): array
{
if (\Auth::id() === 1) {
return [
['disk' => 'disk-name', 'path' => '*', 'access' => 2],
];
}
return [
['disk' => 'disk-name', 'path' => '/', 'access' => 1], // main folder - read
['disk' => 'disk-name', 'path' => 'users', 'access' => 1], // only read
['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name, 'access' => 1], // only read
['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name .'/*', 'access' => 2], // read and write
];
}
}
```
- disk-name - you need to replace for your disk name
- now in the config file we will change the repository to a new one, and set aclStrategy in whitelist - we will deny everything that is not allowed by the rules. You can also hide folders and files that are not available.
```php
/**
* Hide files and folders from file-manager if user doesn't have access
* ACL access level = 0
*/
'aclHideFromFM' => true,
/**
* ACL strategy
*
* blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list
*
* whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list
*/
'aclStrategy' => 'whitelist',
/**
* ACL rules repository
*
* default - config file(ConfigACLRepository)
*/
'aclRepository' => \App\Http\UsersACLRepository::class,
```
## What's next
[Events](./events.md)

View File

@@ -1,161 +0,0 @@
# Configuration
Open configuration file - config/file-manager.php
- fill the disk list from config/filesystem.php (select the desired drive names)
- set cache
- select file manager windows configuration
**!!! Be sure to add your middleware to restrict access to the application !!!**
**Don't forget to configure your php and Nginx**
```
// PHP
upload_max_filesize,
post_max_size
// Nginx
client_max_body_size
```
### You can set default disk and default path
You have two variants for how to do it:
1. Add this params to the config file (config/file-manager.php)
```php
/**
* Default disk for left manager
* null - auto select the first disk in the disk list
*/
'leftDisk' => 'public',
/**
* Default disk for right manager
* null - auto select the first disk in the disk list
*/
'rightDisk' => null,
/**
* Default path for left manager
* null - root directory
*/
'leftPath' => 'directory/sub-directory',
/**
* Default path for right manager
* null - root directory
*/
'rightPath' => null,
```
2 Or you can add this params in URL
```
http://site.name/?leftDisk=public
http://site.name/?leftDisk=public&rightDisk=images
http://site.name/?leftDisk=public&leftPath=directory/sub-directory
http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars/vw/golf
// %2F - /, %20 - space
http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars%2Fvw%2Fgolf
```
leftDisk and leftPath is default for the file manager windows configuration - 1,2
**You can't add a disk that does not exist in the diskList array !**
**! Params in URL have more weight than params in config file. It means that URL params can overwrite your config params. !**
## Disk settings example
- config/filesystems.php
```php
// Filesystem Disks
'disks' => [
// images folder in public path
'images' => [
'driver' => 'local',
'root' => public_path('images'),
'url' => env('APP_URL').'/images',
],
// public folder in storage/app/public
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage', // https://laravel.com/docs/5.7/filesystem#file-urls
'visibility' => 'public',
],
// ftp
'dd-wrt' => [
'driver' => 'ftp',
'host' => 'ftp.dd-wrt.com',
'username' => 'anonymous',
'passive' => true,
'timeout' => 30,
],
],
```
- config/file-manager.php
```php
// You need to enter the disks you want to use in the file manager
'diskList' => ['images', 'public'],
```
## Normalization of uploaded file names
If you expect to work with files that may have filenames that are not considered *normal* `f.e.: "DCIM_2021 - čšč& (1).jpg.jpg"` so basically any time you give the option to upload files to users, you can set `slugifyNames` to `true` and have the names ran through `Str::slug()` before saving it so you file will look something like `dcim-2021-csc-1.jpg`
## Dynamic configuration
You can create your own configuration, for example for different users or their roles.
Create new class - example - TestConfigRepository
```php
namespace App\Http;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
class TestConfigRepository implements ConfigRepository
{
// implement all methods from interface
/**
* Get disk list
*
* ['public', 'local', 's3']
*
* @return array
*/
public function getDiskList(): array
{
if (\Auth::id() === 1) {
return [
['public', 'local', 's3'],
];
}
return ['public'];
}
...
}
```
For example see [src/Services/ConfigService/DefaultConfigRepository.php](https://github.com/alexusmai/laravel-file-manager/blob/master/src/Services/ConfigService/DefaultConfigRepository.php)
## What's next
[ACL](./acl.md)
[Integration](./integration.md)

View File

@@ -1,321 +0,0 @@
# Events
### BeforeInitialization
> Alexusmai\LaravelFileManager\Events\BeforeInitialization
Example:
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\BeforeInitialization',
function ($event) {
}
);
```
### DiskSelected
> Alexusmai\LaravelFileManager\Events\DiskSelected
Example:
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\DiskSelected',
function ($event) {
\Log::info('DiskSelected:', [$event->disk()]);
}
);
```
### FilesUploading
> Alexusmai\LaravelFileManager\Events\FilesUploading
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploading',
function ($event) {
\Log::info('FilesUploading:', [
$event->disk(),
$event->path(),
$event->files(),
$event->overwrite(),
]);
}
);
```
### FilesUploaded
> Alexusmai\LaravelFileManager\Events\FilesUploaded
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploaded',
function ($event) {
\Log::info('FilesUploaded:', [
$event->disk(),
$event->path(),
$event->files(),
$event->overwrite(),
]);
}
);
```
### Deleting
> Alexusmai\LaravelFileManager\Events\Deleting
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Deleting',
function ($event) {
\Log::info('Deleting:', [
$event->disk(),
$event->items(),
]);
}
);
```
### Deleted
> Alexusmai\LaravelFileManager\Events\Deleted
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Deleted',
function ($event) {
\Log::info('Deleted:', [
$event->disk(),
$event->items(),
]);
}
);
```
### Paste
> Alexusmai\LaravelFileManager\Events\Paste
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Paste',
function ($event) {
\Log::info('Paste:', [
$event->disk(),
$event->path(),
$event->clipboard(),
]);
}
);
```
### Rename
> Alexusmai\LaravelFileManager\Events\Rename
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Rename',
function ($event) {
\Log::info('Rename:', [
$event->disk(),
$event->newName(),
$event->oldName(),
$event->type(), // 'file' or 'dir'
]);
}
);
```
### Download
> Alexusmai\LaravelFileManager\Events\Download
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Download',
function ($event) {
\Log::info('Download:', [
$event->disk(),
$event->path(),
]);
}
);
```
*When using a text editor, the file you are editing is also downloaded! And this event is triggered!*
### DirectoryCreating
> Alexusmai\LaravelFileManager\Events\DirectoryCreating
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreating',
function ($event) {
\Log::info('DirectoryCreating:', [
$event->disk(),
$event->path(),
$event->name(),
]);
}
);
```
### DirectoryCreated
> Alexusmai\LaravelFileManager\Events\DirectoryCreated
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreated',
function ($event) {
\Log::info('DirectoryCreated:', [
$event->disk(),
$event->path(),
$event->name(),
]);
}
);
```
### FileCreating
> Alexusmai\LaravelFileManager\Events\FileCreating
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\FileCreating',
function ($event) {
\Log::info('FileCreating:', [
$event->disk(),
$event->path(),
$event->name(),
]);
}
);
```
### FileCreated
> Alexusmai\LaravelFileManager\Events\FileCreated
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\FileCreated',
function ($event) {
\Log::info('FileCreated:', [
$event->disk(),
$event->path(),
$event->name(),
]);
}
);
```
### FileUpdate
> Alexusmai\LaravelFileManager\Events\FileUpdate
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\FileUpdate',
function ($event) {
\Log::info('FileUpdate:', [
$event->disk(),
$event->path(),
]);
}
);
```
### Zip
> Alexusmai\LaravelFileManager\Events\Zip
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Zip',
function ($event) {
\Log::info('Zip:', [
$event->disk(),
$event->path(),
$event->name(),
$event->elements(),
]);
}
);
```
### ZipCreated
> Alexusmai\LaravelFileManager\Events\ZipCreated
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\ZipCreated',
function ($event) {
\Log::info('ZipCreated:', [
$event->disk(),
$event->path(),
$event->name(),
$event->elements(),
]);
}
);
```
### ZipFailed
> Alexusmai\LaravelFileManager\Events\ZipCreated
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\ZipFailed',
function ($event) {
\Log::info('ZipFailed:', [
$event->disk(),
$event->path(),
$event->name(),
$event->elements(),
]);
}
);
```
### Unzip
> Alexusmai\LaravelFileManager\Events\Unzip
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\Unzip',
function ($event) {
\Log::info('Unzip:', [
$event->disk(),
$event->path(),
$event->folder(),
]);
}
);
```
### UnzipCreated
> Alexusmai\LaravelFileManager\Events\UnzipCreated
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\UnzipCreated',
function ($event) {
\Log::info('UnzipCreated:', [
$event->disk(),
$event->path(),
$event->folder(),
]);
}
);
```
### UnzipFailed
> Alexusmai\LaravelFileManager\Events\UnzipFailed
```php
\Event::listen('Alexusmai\LaravelFileManager\Events\UnzipFailed',
function ($event) {
\Log::info('UnzipFailed:', [
$event->disk(),
$event->path(),
$event->folder(),
]);
}
);
```

View File

@@ -1,18 +0,0 @@
# Laravel File Manager
## Docs
* [Installation](./installation.md)
* [Configuration](./configuration.md)
* [Integration](./integration.md)
* [ACL](./acl.md)
* [Events](./events.md)
* [Update](./update.md)
## Requirements
* PHP >= 8.0
* ext-zip - for zip and unzip functions
* Laravel 9 or higher
* GD Library or Imagick for [intervention/image](https://github.com/Intervention/image)
* requires [intervention/image](https://github.com/Intervention/image) and [intervention/imagecache](https://github.com/Intervention/imagecache)
* Bootstrap 5 and Bootstrap Icons v1.8.0 and higher

View File

@@ -1,91 +0,0 @@
# Installation
1. Install package - using composer
```
composer require alexusmai/laravel-file-manager
```
For Laravel 5 - 8 use v2.5.4
```
composer require alexusmai/laravel-file-manager "2.5.4"
```
2. If you use Laravel 5.4, then add service provider to config/app.php (for the Laravel 5.5 and higher skip this step):
```php
Alexusmai\LaravelFileManager\FileManagerServiceProvider::class,
```
3. Publish configuration file
```bash
php artisan vendor:publish --tag=fm-config
```
4. You can install npm package directly and use it in your vue application - more information about it -
[vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager)
> OR
Publish compiled and minimized js and css files
```
php artisan vendor:publish --tag=fm-assets
```
Open the view file where you want to place the application block, and add:
* add a csrf token to head block if you did not do it before
```
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
```
* For version 3 and higher - the frontend package uses **Bootstrap 5** and **Bootstrap Icons** styles, if you already use it,
then you do not need to connect any styles. Otherwise, add -
```
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
```
* For old versions - the frontend package uses **Bootstrap 4** and **Font Awesome 5** styles, if you already use it,
then you do not need to connect any styles. Otherwise, add -
```
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
```
* add file manager styles
```
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
```
* add file manager js
```
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
```
* For version 3 and higher - add div for application (set application height!)
```
<div id="fm" style="height: 600px;"></div>
```
* For old versions - add div for application (set application height!)
```
<div style="height: 600px;">
<div id="fm"></div>
</div>
```
## What's next
[Configuration](./configuration.md)

View File

@@ -1,229 +0,0 @@
# Integration
> See examples in [examples](./../examples) folder
### CKEditor 4
Add to CKEditor config
```js
CKEDITOR.replace('editor-id', {filebrowserImageBrowseUrl: '/file-manager/ckeditor'});
```
OR in to the config.js
```js
CKEDITOR.editorConfig = function( config ) {
//...
// Upload image
config.filebrowserImageBrowseUrl = '/file-manager/ckeditor';
};
```
After these actions, you will be able to call the file manager from the CKEditor editor menu (Image -> Selection on the server).
The file manager will appear in a new window.
### TinyMCE 4
Add to TinyMCE configuration
```js
tinymce.init({
selector: '#my-textarea',
// ...
file_browser_callback: function(field_name, url, type, win) {
tinyMCE.activeEditor.windowManager.open({
file: '/file-manager/tinymce',
title: 'Laravel File Manager',
width: window.innerWidth * 0.8,
height: window.innerHeight * 0.8,
resizable: 'yes',
close_previous: 'no',
}, {
setUrl: function(url) {
win.document.getElementById(field_name).value = url;
},
});
},
});
```
### TinyMCE 5
Add to TinyMCE 5 configuration
```js
tinymce.init({
selector: '#my-textarea',
// ...
file_picker_callback (callback, value, meta) {
let x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth
let y = window.innerHeight|| document.documentElement.clientHeight|| document.getElementsByTagName('body')[0].clientHeight
tinymce.activeEditor.windowManager.openUrl({
url : '/file-manager/tinymce5',
title : 'Laravel File manager',
width : x * 0.8,
height : y * 0.8,
onMessage: (api, message) => {
callback(message.content, { text: message.text })
}
})
}
});
```
### SummerNote
Create and add new button
```js
// File manager button (image icon)
const FMButton = function(context) {
const ui = $.summernote.ui;
const button = ui.button({
contents: '<i class="note-icon-picture"></i> ',
tooltip: 'File Manager',
click: function() {
window.open('/file-manager/summernote', 'fm', 'width=1400,height=800');
}
});
return button.render();
};
$('#summernote').summernote({
toolbar: [
// [groupName, [list of button]]
// your settings
['fm-button', ['fm']],
],
buttons: {
fm: FMButton
}
});
```
And add this function
```js
// set file link
function fmSetLink(url) {
$('#summernote').summernote('insertImage', url);
}
```
See [example](./../examples/wysiwyg/summernote.blade.php)
### Standalone button
Add button
```html
<div class="input-group">
<input type="text" id="image_label" class="form-control" name="image"
aria-label="Image" aria-describedby="button-image">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-image">Select</button>
</div>
</div>
```
and js script
```js
document.addEventListener("DOMContentLoaded", function() {
document.getElementById('button-image').addEventListener('click', (event) => {
event.preventDefault();
window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800');
});
});
// set file link
function fmSetLink($url) {
document.getElementById('image_label').value = $url;
}
```
### Multiple standalone buttons
```html
<!-- HTML -->
<div class="container">
<div class="form-row">
<div class="form-group col-md-6">
<label for="image_label">Image</label>
<div class="input-group">
<input type="text" id="image1" class="form-control" name="image"
aria-label="Image" aria-describedby="button-image">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-image">Select</button>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="image_label">Image2</label>
<div class="input-group">
<input type="text" id="image2" class="form-control" name="image"
aria-label="Image" aria-describedby="button-image">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-image2">Select</button>
</div>
</div>
</div>
</div>
</div>
<!-- JS -->
<script>
document.addEventListener("DOMContentLoaded", function() {
document.getElementById('button-image').addEventListener('click', (event) => {
event.preventDefault();
inputId = 'image1';
window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800');
});
// second button
document.getElementById('button-image2').addEventListener('click', (event) => {
event.preventDefault();
inputId = 'image2';
window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800');
});
});
// input
let inputId = '';
// set file link
function fmSetLink($url) {
document.getElementById(inputId).value = $url;
}
</script>
```
### Modifications
To change standard views(with file manager), publish them.
```bash
php artisan vendor:publish --tag=fm-views
```
You will get:
```
resources/views/vendor/file-manager/ckeditor.blade.php
resources/views/vendor/file-manager/tinymce.blade.php
resources/views/vendor/file-manager/summernote.blade.php
resources/views/vendor/file-manager/fmButton.blade.php
```
Now you can change styles and any more..

View File

@@ -1,23 +0,0 @@
# How to update to the latest version?
- Backup your settings - config/file-manager.php
- Will be better if you delete configuration file from "config" folder before updating
- download a new version
```
composer update alexusmai/laravel-file-manager
```
- Update config file and assets(js)
```
// config
php artisan vendor:publish --tag=fm-config --force
// js, css
php artisan vendor:publish --tag=fm-assets --force
```
- set your settings in to the config/file-manager.php
- if you implementing "ConfigRepository" and you see a new settings in
the config file - don't forget to add new functions in to your class

View File

@@ -1,24 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<title>File manager and CKeditor</title>
</head>
<body>
<div class="container">
<textarea name="editor"></textarea>
</div>
<script src="https://cdn.ckeditor.com/4.11.2/standard/ckeditor.js"></script>
<script>
CKEDITOR.replace( 'editor', {filebrowserImageBrowseUrl: '/file-manager/ckeditor'});
</script>
</body>
</html>

View File

@@ -1,46 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<title>File Manager and standalone button</title>
</head>
<body>
<div class="container">
<div class="form-row">
<div class="form-group col-md-6">
<label for="image_label">Image</label>
<div class="input-group">
<input type="text" id="image_label" class="form-control" name="image"
aria-label="Image" aria-describedby="button-image">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-image">Select</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.getElementById('button-image').addEventListener('click', (event) => {
event.preventDefault();
window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800');
});
});
// set file link
function fmSetLink($url) {
document.getElementById('image_label').value = $url;
}
</script>
</body>
</html>

View File

@@ -1,66 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<!-- SummerNote -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.11/summernote-bs4.css" rel="stylesheet">
<title>File Manager and SummerNote</title>
</head>
<body>
<div class="container">
<div id="summernote"></div>
</div>
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<!-- SummerNote js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.11/summernote-bs4.js"></script>
<script>
$(document).ready(function(){
// File manager button (image icon)
const FMButton = function(context) {
const ui = $.summernote.ui;
const button = ui.button({
contents: '<i class="note-icon-picture"></i> ',
tooltip: 'File Manager',
click: function() {
window.open('/file-manager/summernote', 'fm', 'width=1400,height=800');
}
});
return button.render();
};
$('#summernote').summernote({
toolbar: [
// [groupName, [list of button]]
['style', ['bold', 'italic', 'underline', 'clear']],
['font', ['strikethrough', 'superscript', 'subscript']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['height', ['height']],
['fm-button', ['fm']],
],
buttons: {
fm: FMButton
}
});
});
// set file link
function fmSetLink(url) {
$('#summernote').summernote('insertImage', url);
}
</script>
</body>
</html>

View File

@@ -1,53 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<title>File manager and TinyMCE</title>
</head>
<body>
<div class="container">
<form method="post">
<textarea id="my-textarea"></textarea>
</form>
</div>
<!-- TinyMCE -->
<script src='https://cloud.tinymce.com/stable/tinymce.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
tinymce.init({
selector: '#my-textarea',
plugins: [
'advlist autolink lists link image charmap print preview hr anchor pagebreak',
'searchreplace wordcount visualblocks visualchars code fullscreen',
'insertdatetime media nonbreaking save table contextmenu directionality',
'emoticons template paste textcolor colorpicker textpattern',
],
toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media',
relative_urls: false,
file_browser_callback: function(field_name, url, type, win) {
tinyMCE.activeEditor.windowManager.open({
file: '/file-manager/tinymce',
title: 'Laravel File Manager',
width: window.innerWidth * 0.8,
height: window.innerHeight * 0.8,
resizable: 'yes',
close_previous: 'no',
}, {
setUrl: function(url) {
win.document.getElementById(field_name).value = url;
},
});
},
});
});
</script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MakeAclRulesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('acl_rules', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id')->nullable();
$table->string('disk');
$table->string('path');
$table->tinyInteger('access');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('acl_rules');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'File Manager') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" id="fm-main-block">
<div id="fm"></div>
</div>
</div>
</div>
<!-- File manager -->
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// set fm height
document.getElementById('fm-main-block').setAttribute('style', 'height:' + window.innerHeight + 'px');
// Helper function to get parameters from the query string.
function getUrlParam(paramName) {
const reParam = new RegExp('(?:[\?&]|&)' + paramName + '=([^&]+)', 'i');
const match = window.location.search.match(reParam);
return (match && match.length > 1) ? match[1] : null;
}
// Add callback to file manager
fm.$store.commit('fm/setFileCallBack', function(fileUrl) {
const funcNum = getUrlParam('CKEditorFuncNum');
window.opener.CKEDITOR.tools.callFunction(funcNum, fileUrl);
window.close();
});
});
</script>
</body>
</html>

View File

@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'File Manager') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" id="fm-main-block">
<div id="fm"></div>
</div>
</div>
</div>
<!-- File manager -->
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// set fm height
document.getElementById('fm-main-block').setAttribute('style', 'height:' + window.innerHeight + 'px');
// Add callback to file manager
fm.$store.commit('fm/setFileCallBack', function(fileUrl) {
window.opener.fmSetLink(fileUrl);
window.close();
});
});
</script>
</body>
</html>

View File

@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'File Manager') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" id="fm-main-block">
<div id="fm"></div>
</div>
</div>
</div>
<!-- File manager -->
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// set fm height
document.getElementById('fm-main-block').setAttribute('style', 'height:' + window.innerHeight + 'px');
// Add callback to file manager
fm.$store.commit('fm/setFileCallBack', function(fileUrl) {
window.opener.fmSetLink(fileUrl);
window.close();
});
});
</script>
</body>
</html>

View File

@@ -1,52 +0,0 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'File Manager') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" id="fm-main-block">
<div id="fm"></div>
</div>
</div>
</div>
<!-- File manager -->
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// set fm height
document.getElementById('fm-main-block').setAttribute('style', 'height:' + window.innerHeight + 'px');
const FileBrowserDialogue = {
init: function() {
// Here goes your code for setting your custom things onLoad.
},
mySubmit: function (URL) {
// pass selected file path to TinyMCE
parent.tinymce.activeEditor.windowManager.getParams().setUrl(URL);
// close popup window
parent.tinymce.activeEditor.windowManager.close();
}
};
// Add callback to file manager
fm.$store.commit('fm/setFileCallBack', function(fileUrl) {
FileBrowserDialogue.mySubmit(fileUrl);
});
});
</script>
</body>
</html>

View File

@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'File Manager') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ asset('vendor/file-manager/css/file-manager.css') }}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" id="fm-main-block">
<div id="fm"></div>
</div>
</div>
</div>
<!-- File manager -->
<script src="{{ asset('vendor/file-manager/js/file-manager.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// set fm height
document.getElementById('fm-main-block').setAttribute('style', 'height:' + window.innerHeight + 'px');
const FileBrowserDialogue = {
init: function() {
// Here goes your code for setting your custom things onLoad.
},
mySubmit: function (URL) {
// pass selected file path to TinyMCE
parent.postMessage({
mceAction: 'insert',
content: URL,
text: URL.split('/').pop()
})
// close popup window
parent.postMessage({ mceAction: 'close' });
}
};
// Add callback to file manager
fm.$store.commit('fm/setFileCallBack', function(fileUrl) {
FileBrowserDialogue.mySubmit(fileUrl);
});
});
</script>
</body>
</html>

View File

@@ -1,427 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Controllers;
use Alexusmai\LaravelFileManager\Events\BeforeInitialization;
use Alexusmai\LaravelFileManager\Events\Deleting;
use Alexusmai\LaravelFileManager\Events\DirectoryCreated;
use Alexusmai\LaravelFileManager\Events\DirectoryCreating;
use Alexusmai\LaravelFileManager\Events\DiskSelected;
use Alexusmai\LaravelFileManager\Events\Download;
use Alexusmai\LaravelFileManager\Events\FileCreated;
use Alexusmai\LaravelFileManager\Events\FileCreating;
use Alexusmai\LaravelFileManager\Events\FilesUploaded;
use Alexusmai\LaravelFileManager\Events\FilesUploading;
use Alexusmai\LaravelFileManager\Events\FileUpdate;
use Alexusmai\LaravelFileManager\Events\Paste;
use Alexusmai\LaravelFileManager\Events\Rename;
use Alexusmai\LaravelFileManager\Events\Zip as ZipEvent;
use Alexusmai\LaravelFileManager\Events\Unzip as UnzipEvent;
use Alexusmai\LaravelFileManager\Requests\RequestValidator;
use Alexusmai\LaravelFileManager\FileManager;
use Alexusmai\LaravelFileManager\Services\Zip;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\View\View;
use League\Flysystem\FilesystemException;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FileManagerController extends Controller
{
/**
* @var FileManager
*/
public $fm;
/**
* FileManagerController constructor.
*
* @param FileManager $fm
*/
public function __construct(FileManager $fm)
{
$this->fm = $fm;
}
/**
* Initialize file manager
*
* @return JsonResponse
*/
public function initialize(): JsonResponse
{
event(new BeforeInitialization());
return response()->json(
$this->fm->initialize()
);
}
/**
* Get files and directories for the selected path and disk
*
* @param RequestValidator $request
*
* @return JsonResponse
* @throws FilesystemException
*/
public function content(RequestValidator $request): JsonResponse
{
return response()->json(
$this->fm->content(
$request->input('disk'),
$request->input('path')
)
);
}
/**
* Directory tree
*
* @param RequestValidator $request
*
* @return JsonResponse
* @throws FilesystemException
*/
public function tree(RequestValidator $request): JsonResponse
{
return response()->json(
$this->fm->tree(
$request->input('disk'),
$request->input('path')
)
);
}
/**
* Check the selected disk
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function selectDisk(RequestValidator $request): JsonResponse
{
event(new DiskSelected($request->input('disk')));
return response()->json([
'result' => [
'status' => 'success',
'message' => 'diskSelected',
],
]);
}
/**
* Upload files
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function upload(RequestValidator $request): JsonResponse
{
event(new FilesUploading($request));
$uploadResponse = $this->fm->upload(
$request->input('disk'),
$request->input('path'),
$request->file('files'),
$request->input('overwrite')
);
event(new FilesUploaded($request));
return response()->json($uploadResponse);
}
/**
* Delete files and folders
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function delete(RequestValidator $request): JsonResponse
{
event(new Deleting($request));
$deleteResponse = $this->fm->delete(
$request->input('disk'),
$request->input('items')
);
return response()->json($deleteResponse);
}
/**
* Copy / Cut files and folders
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function paste(RequestValidator $request): JsonResponse
{
event(new Paste($request));
return response()->json(
$this->fm->paste(
$request->input('disk'),
$request->input('path'),
$request->input('clipboard')
)
);
}
/**
* Rename
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function rename(RequestValidator $request): JsonResponse
{
event(new Rename($request));
return response()->json(
$this->fm->rename(
$request->input('disk'),
$request->input('newName'),
$request->input('oldName')
)
);
}
/**
* Download file
*
* @param RequestValidator $request
*
* @return StreamedResponse
*/
public function download(RequestValidator $request): StreamedResponse
{
event(new Download($request));
return $this->fm->download(
$request->input('disk'),
$request->input('path')
);
}
/**
* Create thumbnails
*
* @param RequestValidator $request
*
* @return Response|mixed
* @throws BindingResolutionException
*/
public function thumbnails(RequestValidator $request): mixed
{
return $this->fm->thumbnails(
$request->input('disk'),
$request->input('path')
);
}
/**
* Image preview
*
* @param RequestValidator $request
*
* @return mixed
*/
public function preview(RequestValidator $request): mixed
{
return $this->fm->preview(
$request->input('disk'),
$request->input('path')
);
}
/**
* File url
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function url(RequestValidator $request): JsonResponse
{
return response()->json(
$this->fm->url(
$request->input('disk'),
$request->input('path')
)
);
}
/**
* Create new directory
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function createDirectory(RequestValidator $request): JsonResponse
{
event(new DirectoryCreating($request));
$createDirectoryResponse = $this->fm->createDirectory(
$request->input('disk'),
$request->input('path'),
$request->input('name')
);
if ($createDirectoryResponse['result']['status'] === 'success') {
event(new DirectoryCreated($request));
}
return response()->json($createDirectoryResponse);
}
/**
* Create new file
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function createFile(RequestValidator $request): JsonResponse
{
event(new FileCreating($request));
$createFileResponse = $this->fm->createFile(
$request->input('disk'),
$request->input('path'),
$request->input('name')
);
if ($createFileResponse['result']['status'] === 'success') {
event(new FileCreated($request));
}
return response()->json($createFileResponse);
}
/**
* Update file
*
* @param RequestValidator $request
*
* @return JsonResponse
*/
public function updateFile(RequestValidator $request): JsonResponse
{
event(new FileUpdate($request));
return response()->json(
$this->fm->updateFile(
$request->input('disk'),
$request->input('path'),
$request->file('file')
)
);
}
/**
* Stream file
*
* @param RequestValidator $request
*
* @return mixed
*/
public function streamFile(RequestValidator $request): mixed
{
return $this->fm->streamFile(
$request->input('disk'),
$request->input('path')
);
}
/**
* Create zip archive
*
* @param RequestValidator $request
* @param Zip $zip
*
* @return array
*/
public function zip(RequestValidator $request, Zip $zip)
{
event(new ZipEvent($request));
return $zip->create();
}
/**
* Extract zip archive
*
* @param RequestValidator $request
* @param Zip $zip
*
* @return array
*/
public function unzip(RequestValidator $request, Zip $zip)
{
event(new UnzipEvent($request));
return $zip->extract();
}
/**
* Integration with ckeditor 4
*
* @return Factory|View
*/
public function ckeditor(): Factory|View
{
return view('file-manager::ckeditor');
}
/**
* Integration with TinyMCE v4
*
* @return Factory|View
*/
public function tinymce(): Factory|View
{
return view('file-manager::tinymce');
}
/**
* Integration with TinyMCE v5
*
* @return Factory|View
*/
public function tinymce5(): Factory|View
{
return view('file-manager::tinymce5');
}
/**
* Integration with SummerNote
*
* @return Factory|View
*/
public function summernote(): Factory|View
{
return view('file-manager::summernote');
}
/**
* Simple integration with input field
*
* @return Factory|View
*/
public function fmButton(): Factory|View
{
return view('file-manager::fmButton');
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
class BeforeInitialization
{
/**
* BeforeInitialization constructor.
*/
public function __construct()
{
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Deleted
{
/**
* @var string
*/
private $disk;
/**
* @var array
*/
private $items;
/**
* Deleted constructor.
*
* @param Request $request
*/
public function __construct($disk, $items)
{
$this->disk = $disk;
$this->items = $items;
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return array
*/
public function items()
{
return $this->items;
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Deleting
{
/**
* @var string
*/
private $disk;
/**
* @var array
*/
private $items;
/**
* Deleting constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->items = $request->input('items');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return array
*/
public function items()
{
return $this->items;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class DirectoryCreated
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* DirectoryCreated constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class DirectoryCreating
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* DirectoryCreating constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
class DiskSelected
{
/**
* @var string
*/
private $disk;
/**
* DiskSelected constructor.
*
* @param $disk
*/
public function __construct($disk)
{
$this->disk = $disk;
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
}

View File

@@ -1,45 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Download
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* Download constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class FileCreated
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* FileCreated constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class FileCreating
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* FileCreating constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
}

View File

@@ -1,55 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class FileUpdate
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var \Illuminate\Http\UploadedFile
*/
private $file;
/**
* FileUpdate constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->file = $request->file('file');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
if ($this->path) {
return $this->path.'/'.$this->file->getClientOriginalName();
}
return $this->file->getClientOriginalName();
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class FilesUploaded
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var \Illuminate\Http\UploadedFile
*/
private $files;
/**
* @var string|null
*/
private $overwrite;
/**
* FilesUploaded constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->files = $request->file('files');
$this->overwrite = $request->input('overwrite');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return array
*/
public function files()
{
return array_map(function ($file) {
return [
'name' => $file->getClientOriginalName(),
'path' => $this->path.'/'.$file->getClientOriginalName(),
'extension' => $file->extension(),
];
}, $this->files);
}
/**
* @return bool
*/
public function overwrite()
{
return !!$this->overwrite;
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class FilesUploading
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var \Illuminate\Http\UploadedFile
*/
private $files;
/**
* @var string|null
*/
private $overwrite;
/**
* FilesUploading constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->files = $request->file('files');
$this->overwrite = $request->input('overwrite');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return array
*/
public function files()
{
return array_map(function ($file) {
return [
'name' => $file->getClientOriginalName(),
'path' => $this->path.'/'.$file->getClientOriginalName(),
'extension' => $file->extension(),
];
}, $this->files);
}
/**
* @return bool
*/
public function overwrite()
{
return !!$this->overwrite;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Paste
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var array
*/
private $clipboard;
/**
* Paste constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->clipboard = $request->input('clipboard');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return array
*/
public function clipboard()
{
return $this->clipboard;
}
}

View File

@@ -1,78 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Rename
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $newName;
/**
* @var string
*/
private $oldName;
/**
* @var string
*/
private $type;
/**
* Rename constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->newName = $request->input('newName');
$this->oldName = $request->input('oldName');
$this->type = $request->input('type');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function newName()
{
return $this->newName;
}
/**
* @return string
*/
public function oldName()
{
return $this->oldName;
}
/**
* @return string
*/
public function type()
{
/*
* $info = Storage::disk($this->disk)->getMetadata($this->oldName);
* return $info['type'];
*/
return $this->type;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Unzip
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $folder;
/**
* Unzip constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->folder = $request->input('folder');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function folder()
{
return $this->folder;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class UnzipCreated
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $folder;
/**
* Unzip constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->folder = $request->input('folder');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function folder()
{
return $this->folder;
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class UnzipFailed
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $folder;
/**
* UnzipFailed constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->folder = $request->input('folder');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function folder()
{
return $this->folder;
}
}

View File

@@ -1,73 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class Zip
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* @var array|string|null
*/
private $elements;
/**
* Zip constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
$this->elements = $request->input('elements');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
/**
* @return array|string|null
*/
public function elements()
{
return $this->elements;
}
}

View File

@@ -1,73 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class ZipCreated
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* @var array|string|null
*/
private $elements;
/**
* ZipCreated constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
$this->elements = $request->input('elements');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
/**
* @return array|string|null
*/
public function elements()
{
return $this->elements;
}
}

View File

@@ -1,73 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Events;
use Illuminate\Http\Request;
class ZipFailed
{
/**
* @var string
*/
private $disk;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $name;
/**
* @var array|string|null
*/
private $elements;
/**
* ZipFailed constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
$this->disk = $request->input('disk');
$this->path = $request->input('path');
$this->name = $request->input('name');
$this->elements = $request->input('elements');
}
/**
* @return string
*/
public function disk()
{
return $this->disk;
}
/**
* @return string
*/
public function path()
{
return $this->path;
}
/**
* @return string
*/
public function name()
{
return $this->name;
}
/**
* @return array|string|null
*/
public function elements()
{
return $this->elements;
}
}

View File

@@ -1,493 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager;
use Alexusmai\LaravelFileManager\Events\Deleted;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
use Alexusmai\LaravelFileManager\Services\TransferService\TransferFactory;
use Alexusmai\LaravelFileManager\Traits\CheckTrait;
use Alexusmai\LaravelFileManager\Traits\ContentTrait;
use Alexusmai\LaravelFileManager\Traits\PathTrait;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
use League\Flysystem\FilesystemException;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FileManager
{
use PathTrait, ContentTrait, CheckTrait;
/**
* @var ConfigRepository
*/
public ConfigRepository $configRepository;
/**
* FileManager constructor.
*
* @param ConfigRepository $configRepository
*/
public function __construct(ConfigRepository $configRepository)
{
$this->configRepository = $configRepository;
}
/**
* Initialize App
*
* @return array
*/
public function initialize(): array
{
if (!config()->has('file-manager')) {
return [
'result' => [
'status' => 'danger',
'message' => 'noConfig',
],
];
}
$config = [
'acl' => $this->configRepository->getAcl(),
'leftDisk' => $this->configRepository->getLeftDisk(),
'rightDisk' => $this->configRepository->getRightDisk(),
'leftPath' => $this->configRepository->getLeftPath(),
'rightPath' => $this->configRepository->getRightPath(),
'windowsConfig' => $this->configRepository->getWindowsConfig(),
'hiddenFiles' => $this->configRepository->getHiddenFiles(),
];
// disk list
foreach ($this->configRepository->getDiskList() as $disk) {
if (array_key_exists($disk, config('filesystems.disks'))) {
$config['disks'][$disk] = Arr::only(
config('filesystems.disks')[$disk], ['driver']
);
}
}
// get language
$config['lang'] = 'zh-CN';
return [
'result' => [
'status' => 'success',
'message' => null,
],
'config' => $config,
];
}
/**
* Get files and directories for the selected path and disk
*
* @param $disk
* @param $path
*
* @return array
* @throws FilesystemException
*/
public function content($disk, $path): array
{
$content = $this->getContent($disk, $path);
return [
'result' => [
'status' => 'success',
'message' => null,
],
'directories' => $content['directories'],
'files' => $content['files'],
];
}
/**
* Get part of the directory tree
*
* @param $disk
* @param $path
*
* @return array
* @throws FilesystemException
*/
public function tree($disk, $path): array
{
$directories = $this->getDirectoriesTree($disk, $path);
return [
'result' => [
'status' => 'success',
'message' => null,
],
'directories' => $directories,
];
}
/**
* Upload files
*
* @param string|null $disk
* @param string|null $path
* @param array|null $files
* @param bool $overwrite
*
* @return array
*/
public function upload($disk, $path, $files, $overwrite): array
{
$fileNotUploaded = false;
foreach ($files as $file) {
// skip or overwrite files
if (!$overwrite && Storage::disk($disk)->exists($path . '/' . $file->getClientOriginalName())) {
continue;
}
// check file size
if ($this->configRepository->getMaxUploadFileSize()
&& $file->getSize() / 1024 > $this->configRepository->getMaxUploadFileSize()
) {
$fileNotUploaded = true;
continue;
}
// check file type
if ($this->configRepository->getAllowFileTypes()
&& !in_array(
$file->getClientOriginalExtension(),
$this->configRepository->getAllowFileTypes()
)
) {
$fileNotUploaded = true;
continue;
}
$name = $file->getClientOriginalName();
if ($this->configRepository->getSlugifyNames()) {
$name = Str::slug(
Str::replace(
'.' . $file->getClientOriginalExtension(),
'',
$name
)
) . '.' . $file->getClientOriginalExtension();
}
// overwrite or save file
Storage::disk($disk)->putFileAs(
$path,
$file,
$name
);
}
if ($fileNotUploaded) {
return [
'result' => [
'status' => 'warning',
'message' => 'notAllUploaded',
],
];
}
return [
'result' => [
'status' => 'success',
'message' => 'uploaded',
],
];
}
/**
* Delete files and folders
*
* @param $disk
* @param $items
*
* @return array
*/
public function delete($disk, $items): array
{
$deletedItems = [];
foreach ($items as $item) {
if (!Storage::disk($disk)->exists($item['path'])) {
continue;
} else {
if ($item['type'] === 'dir') {
Storage::disk($disk)->deleteDirectory($item['path']);
} else {
Storage::disk($disk)->delete($item['path']);
}
}
$deletedItems[] = $item;
}
event(new Deleted($disk, $deletedItems));
return [
'result' => [
'status' => 'success',
'message' => 'deleted',
],
];
}
/**
* Copy / Cut - Files and Directories
*
* @param $disk
* @param $path
* @param $clipboard
*
* @return array
*/
public function paste($disk, $path, $clipboard): array
{
// compare disk names
if ($disk !== $clipboard['disk']) {
if (!$this->checkDisk($clipboard['disk'])) {
return $this->notFoundMessage();
}
}
$transferService = TransferFactory::build($disk, $path, $clipboard);
return $transferService->filesTransfer();
}
/**
* Rename file or folder
*
* @param $disk
* @param $newName
* @param $oldName
*
* @return array
*/
public function rename($disk, $newName, $oldName): array
{
Storage::disk($disk)->move($oldName, $newName);
return [
'result' => [
'status' => 'success',
'message' => 'renamed',
],
];
}
/**
* Download selected file
*
* @param $disk
* @param $path
*
* @return StreamedResponse
*/
public function download($disk, $path): StreamedResponse
{
// if file name not in ASCII format
if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) {
$filename = Str::ascii(basename($path));
} else {
$filename = basename($path);
}
return Storage::disk($disk)->download($path, $filename);
}
/**
* Create thumbnails
*
* @param $disk
* @param $path
*
* @return Response|mixed
* @throws BindingResolutionException
*/
public function thumbnails($disk, $path): mixed
{
if ($this->configRepository->getCache()) {
$thumbnail = Image::cache(function ($image) use ($disk, $path) {
$image->make(Storage::disk($disk)->get($path))->fit(80);
}, $this->configRepository->getCache());
// output
return response()->make(
$thumbnail,
200,
['Content-Type' => Storage::disk($disk)->mimeType($path)]
);
}
$thumbnail = Image::make(Storage::disk($disk)->get($path))->fit(80);
return $thumbnail->response();
}
/**
* Image preview
*
* @param $disk
* @param $path
*
* @return mixed
*/
public function preview($disk, $path): mixed
{
$preview = Image::make(Storage::disk($disk)->get($path));
return $preview->response();
}
/**
* Get file URL
*
* @param $disk
* @param $path
*
* @return array
*/
public function url($disk, $path): array
{
return [
'result' => [
'status' => 'success',
'message' => null,
],
'url' => Storage::disk($disk)->url($path),
];
}
/**
* Create new directory
*
* @param $disk
* @param $path
* @param $name
*
* @return array
*/
public function createDirectory($disk, $path, $name)
{
$directoryName = $this->newPath($path, $name);
if (Storage::disk($disk)->exists($directoryName)) {
return [
'result' => [
'status' => 'warning',
'message' => 'dirExist',
],
];
}
Storage::disk($disk)->makeDirectory($directoryName);
$directoryProperties = $this->directoryProperties(
$disk,
$directoryName
);
// add directory properties for the tree module
$tree = $directoryProperties;
$tree['props'] = ['hasSubdirectories' => false];
return [
'result' => [
'status' => 'success',
'message' => 'dirCreated',
],
'directory' => $directoryProperties,
'tree' => [$tree],
];
}
/**
* Create new file
*
* @param $disk
* @param $path
* @param $name
*
* @return array
*/
public function createFile($disk, $path, $name): array
{
$path = $this->newPath($path, $name);
if (Storage::disk($disk)->exists($path)) {
return [
'result' => [
'status' => 'warning',
'message' => 'fileExist',
],
];
}
Storage::disk($disk)->put($path, '');
$fileProperties = $this->fileProperties($disk, $path);
return [
'result' => [
'status' => 'success',
'message' => 'fileCreated',
],
'file' => $fileProperties,
];
}
/**
* Update file
*
* @param $disk
* @param $path
* @param $file
*
* @return array
*/
public function updateFile($disk, $path, $file): array
{
Storage::disk($disk)->putFileAs(
$path,
$file,
$file->getClientOriginalName()
);
$filePath = $this->newPath($path, $file->getClientOriginalName());
$fileProperties = $this->fileProperties($disk, $filePath);
return [
'result' => [
'status' => 'success',
'message' => 'fileUpdated',
],
'file' => $fileProperties,
];
}
/**
* Stream file - for audio and video
*
* @param $disk
* @param $path
*
* @return StreamedResponse
*/
public function streamFile($disk, $path): StreamedResponse
{
// if file name not in ASCII format
if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) {
$filename = Str::ascii(basename($path));
} else {
$filename = basename($path);
}
return Storage::disk($disk)->response($path, $filename, ['Accept-Ranges' => 'bytes']);
}
}

View File

@@ -1,77 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager;
use Alexusmai\LaravelFileManager\Middleware\FileManagerACL;
use Alexusmai\LaravelFileManager\Services\ACLService\ACLRepository;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
use Illuminate\Support\ServiceProvider;
class FileManagerServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
// routes
$this->loadRoutesFrom(__DIR__.'/routes.php');
// views
$this->loadViewsFrom(__DIR__.'/../resources/views', 'file-manager');
// publish config
$this->publishes([
__DIR__
.'/../config/file-manager.php' => config_path('file-manager.php'),
], 'fm-config');
// publish views
$this->publishes([
__DIR__
.'/../resources/views' => resource_path('views/vendor/file-manager'),
], 'fm-views');
// publish js and css files - vue-file-manager module
$this->publishes([
__DIR__
.'/../resources/assets' => public_path('vendor/file-manager'),
], 'fm-assets');
// publish migrations
$this->publishes([
__DIR__
.'/../migrations' => database_path('migrations'),
], 'fm-migrations');
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(
__DIR__.'/../config/file-manager.php',
'file-manager'
);
// Config Repository
$this->app->bind(
ConfigRepository::class,
$this->app['config']['file-manager.configRepository']
);
// ACL Repository
$this->app->bind(
ACLRepository::class,
$this->app->make(ConfigRepository::class)->getAclRepository()
);
// register ACL middleware
$this->app['router']->aliasMiddleware('fm-acl', FileManagerACL::class);
}
}

View File

@@ -1,328 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Middleware;
use Alexusmai\LaravelFileManager\Services\ACLService\ACL;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
use Alexusmai\LaravelFileManager\Traits\PathTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Closure;
class FileManagerACL
{
use PathTrait;
/**
* Check method names
*/
const CHECKERS = [
'fm.tree' => 'checkContent',
'fm.content' => 'checkContent',
'fm.preview' => 'checkContent',
'fm.thumbnails' => 'checkContent',
'fm.url' => 'checkContent',
'fm.stream-file' => 'checkContent',
'fm.download' => 'checkDownload',
'fm.create-file' => 'checkCreate',
'fm.create-directory' => 'checkCreate',
'fm.update-file' => 'checkUpdate',
'fm.upload' => 'checkUpload',
'fm.delete' => 'checkDelete',
'fm.paste' => 'checkPaste',
'fm.rename' => 'checkRename',
'fm.zip' => 'checkZip',
'fm.unzip' => 'checkUnzip',
];
/**
* @var string|null
*/
protected $disk;
/**
* @var string|null
*/
protected $path;
/**
* @var ACL|mixed
*/
protected $acl;
/**
* @var Request
*/
protected $request;
/**
* FileManagerACL constructor.
*
* @param Request $request
* @param ACL $acl
*/
public function __construct(Request $request, ACL $acl)
{
$this->disk = $request->has('disk') ? $request->input('disk') : null;
$this->path = $request->has('path') ? $request->input('path') : '/';
$this->acl = $acl;
$this->request = $request;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$routeName = $request->route()->getName();
// if ACL is OFF or route name wasn't found
if ( ! resolve(ConfigRepository::class)->getAcl()
|| ! array_key_exists($routeName, self::CHECKERS)
) {
return $next($request);
}
if ( ! call_user_func([$this, self::CHECKERS[$routeName]])) {
return $this->errorMessage();
}
// return request
return $next($request);
}
/**
* ACL Error message
*
* @return \Illuminate\Http\JsonResponse
*/
protected function errorMessage()
{
return response()->json([
'result' => [
'status' => 'error',
'message' => 'aclError',
],
]);
}
/**
* Check content actions
*
* @return bool
*/
protected function checkContent()
{
// need r access
return ! ($this->acl->getAccessLevel($this->disk, $this->path) === 0);
}
/**
* Check download actions
*/
protected function checkDownload()
{
// need r access
abort_if(
$this->acl->getAccessLevel($this->disk, $this->path) === 0,
403
);
return true;
}
/**
* Check create actions
*
* @return bool
*/
protected function checkCreate()
{
$name = $this->request->input('name');
$pathToWrite = $this->request->input('path')
? $this->request->input('path').'/' : '';
// need r/w access
return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2);
}
/**
* Check update actions
*
* @return bool
*/
protected function checkUpdate()
{
$pathToWrite = $this->request->input('path')
? $this->request->input('path').'/' : '';
$name = $this->request->file('file')->getClientOriginalName();
// need r/w access
return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2);
}
/**
* Check upload actions
*
* @return bool
*/
protected function checkUpload()
{
$pathToWrite = $this->request->input('path')
? $this->request->input('path').'/' : '';
// filter
$firstFall = Arr::first($this->request->file('files'),
function ($value) use ($pathToWrite) {
// need r/w access
return $this->acl->getAccessLevel(
$this->disk,
$pathToWrite.$value->getClientOriginalName()
) !== 2;
}, null);
// if founded one access error
if ($firstFall) {
return false;
}
return true;
}
/**
* Check delete actions
*
* @return bool
*/
protected function checkDelete()
{
$firstFall = Arr::first($this->request->input('items'),
function ($value) {
// need r/w access
return $this->acl->getAccessLevel($this->disk, $value['path']) !== 2;
}, null);
if ($firstFall) {
return false;
}
return true;
}
/**
* Check paste action
*
* @return bool
*/
protected function checkPaste()
{
// get clipboard data
$clipboard = $this->request->input('clipboard');
// copy - r, cut - rw
$getLevel = $clipboard['type'] === 'copy' ? 1 : 2;
// can user copy or cut selected files and folders
$checkDirs = Arr::first($clipboard['directories'],
function ($value) use ($clipboard, $getLevel) {
return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel;
}, null);
$checkFiles = Arr::first($clipboard['files'],
function ($value) use ($clipboard, $getLevel) {
return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel;
}, null);
// can user write to selected folder?
$writeToFolder = $this->acl->getAccessLevel($this->disk, $this->path);
return ! ($checkDirs || $checkFiles || $writeToFolder !== 2);
}
/**
* Check rename actions
*
* @return bool
*/
protected function checkRename()
{
// old path
$oldPath = $this->request->input('oldName');
// new path
$newPath = $this->request->input('newName');
// need r/w access
return ! ($this->acl->getAccessLevel($this->disk, $oldPath) !== 2
|| $this->acl->getAccessLevel($this->disk, $newPath) !== 2);
}
/**
* Check zip actions
*
* @return bool
*/
protected function checkZip()
{
// can user write to selected folder?
$writeToFolder = $this->acl->getAccessLevel(
$this->disk,
$this->newPath(
$this->request->input('path'),
$this->request->input('name')
)
);
// need r/w access
if ($writeToFolder !== 2) {
return false;
}
// data to zip
$elements = $this->request->input('elements');
// can user read selected files and folders?
$checkDirs = Arr::first($elements['directories'],
function ($value) {
// need r access
return $this->acl->getAccessLevel($this->disk, $value) === 0;
}, null);
$checkFiles = Arr::first($elements['files'],
function ($value) {
// need r access
return $this->acl->getAccessLevel($this->disk, $value) === 0;
}, null);
return ! ($checkDirs || $checkFiles);
}
/**
* Check unzip actions
*
* @return bool
*/
protected function checkUnzip()
{
if ($this->request->input('folder')) {
$dirname = dirname($this->path) === '.' ? ''
: dirname($this->path).'/';
$pathToWrite = $dirname.$this->request->input('folder');
} else {
$pathToWrite = dirname($this->path) === '.' ? '/'
: dirname($this->path);
}
return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite) !== 2
|| $this->acl->getAccessLevel($this->disk, $this->path) === 0);
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
trait CustomErrorMessage
{
/**
* Validation error response
*
* @param Validator $validator
*/
protected function failedValidation(Validator $validator)
{
$message = (method_exists($this, 'message'))
? $this->container->call([$this, 'message'])
: 'The given data was invalid.';
throw new HttpResponseException(response()->json([
'errors' => $validator->errors(),
'message' => $message,
], 422));
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Requests;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Storage;
class RequestValidator extends FormRequest
{
use CustomErrorMessage;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$config = resolve(ConfigRepository::class);
return [
'disk' => [
'sometimes',
'string',
function ($attribute, $value, $fail) use($config) {
if (!in_array($value, $config->getDiskList()) ||
!array_key_exists($value, config('filesystems.disks'))
) {
return $fail('diskNotFound');
}
},
],
'path' => [
'sometimes',
'string',
'nullable',
function ($attribute, $value, $fail) {
if ($value && !Storage::disk($this->input('disk'))->exists($value)
) {
return $fail('pathNotFound');
}
},
],
];
}
/**
* Not found message
*
* @return string
*/
public function message()
{
return 'notFound';
}
}

View File

@@ -1,93 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Services\ACLService;
use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
class ACL
{
/**
* @var ACLRepository
*/
public $aclRepository;
/**
* @var ConfigRepository
*/
public $configRepository;
/**
* ACL constructor.
*
* @param ACLRepository $aclRepository
* @param ConfigRepository $configRepository
*/
public function __construct(
ACLRepository $aclRepository,
ConfigRepository $configRepository
) {
$this->aclRepository = $aclRepository;
$this->configRepository = $configRepository;
}
/**
* Get access level for selected path
*
* @param $disk
* @param string $path
*
* @return int
*/
public function getAccessLevel($disk, $path = '/'): int
{
$rules = $this->rulesForDisk($disk);
// find the first rule where the paths are equal
$firstRule = Arr::first($rules, function ($value) use ($path) {
return $value['path'] === '*' || $value['path'] === $path;
});
if ($firstRule) {
return $firstRule['access'];
}
// blacklist or whitelist (ACL strategy)
return $this->configRepository->getAclStrategy() === 'blacklist' ? 2 : 0;
}
/**
* Select rules for disk
*
* @param $disk
*
* @return array
*/
protected function rulesForDisk($disk): array
{
return Arr::where($this->rulesList(),
function ($value) use ($disk) {
return $value['disk'] === $disk;
});
}
/**
* Get rules list from ACL Repository
*
* @return array|mixed
*/
protected function rulesList(): mixed
{
// if cache on
if ($minutes = $this->configRepository->getAclRulesCache()) {
$cacheName = get_class($this->aclRepository) . '_' .$this->aclRepository->getUserID();
return Cache::remember($cacheName, $minutes, function () {
return $this->aclRepository->getRules();
});
}
return $this->aclRepository->getRules();
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace Alexusmai\LaravelFileManager\Services\ACLService;
/**
* Interface ACLRepository
*
* @package Alexusmai\LaravelFileManager\Services\ACLService
*/
interface ACLRepository
{
/**
* Get user ID
*
* @return mixed
*/
public function getUserID();
/**
* Get ACL rules list for user
*
* You need to return an array, like this:
*
* 0 => [
* "disk" => "public"
* "path" => "music"
* "access" => 0
* ],
* 1 => [
* "disk" => "public"
* "path" => "images"
* "access" => 1
* ]
*
* OR [] - if no results for selected user
*
* @return array
*/
public function getRules(): array;
}

Some files were not shown because too many files have changed in this diff Show More