Browse Source

update

tags/1.0 1.0
flucont 3 years ago
commit
23b52ec3f7
  1. 18
      .env
  2. 4
      .gitignore
  3. 21
      LICENSE
  4. 48
      README.md
  5. 1
      app/.htaccess
  6. 22
      app/AppService.php
  7. 115
      app/BaseController.php
  8. 58
      app/ExceptionHandle.php
  9. 8
      app/Request.php
  10. 115
      app/command/UpdateAll.php
  11. 176
      app/common.php
  12. 341
      app/controller/Admin.php
  13. 251
      app/controller/Api.php
  14. 24
      app/controller/Index.php
  15. 17
      app/event.php
  16. 203
      app/lib/Btapi.php
  17. 229
      app/lib/Plugins.php
  18. 12
      app/middleware.php
  19. 24
      app/middleware/AuthAdmin.php
  20. 19
      app/middleware/CheckAdmin.php
  21. 29
      app/middleware/LoadConfig.php
  22. 24
      app/middleware/RefererCheck.php
  23. 9
      app/provider.php
  24. 9
      app/service.php
  25. 59
      app/view/admin/index.html
  26. 67
      app/view/admin/layout.html
  27. 209
      app/view/admin/list.html
  28. 74
      app/view/admin/log.html
  29. 97
      app/view/admin/login.html
  30. 214
      app/view/admin/plugins.html
  31. 67
      app/view/admin/record.html
  32. 211
      app/view/admin/set.html
  33. 60
      app/view/dispatch_jump.html
  34. 159
      app/view/index/download.html
  35. 41
      composer.json
  36. 32
      config/app.php
  37. 40
      config/cache.php
  38. 39
      config/captcha.php
  39. 10
      config/console.php
  40. 20
      config/cookie.php
  41. 63
      config/database.php
  42. 24
      config/filesystem.php
  43. 27
      config/lang.php
  44. 45
      config/log.php
  45. 8
      config/middleware.php
  46. 45
      config/route.php
  47. 19
      config/session.php
  48. 10
      config/trace.php
  49. 25
      config/view.php
  50. 1
      data/config/plugin_list.json
  51. BIN
      data/plugins/other/image/20190401/169f9ef390f68c134c4b8b003ec1a412.png
  52. BIN
      data/plugins/other/image/20190416/ac0d8aa620c481be425ea5a008e33480.png
  53. BIN
      data/plugins/other/image/20190420/0237badbc85457149fc8772e11c39070.png
  54. BIN
      data/plugins/other/image/20190529/b7e5b51c52bc2ead12b338197456916e.png
  55. BIN
      data/plugins/other/image/20190615/e7372086b3e573497e612e1003bbe5b3.png
  56. BIN
      data/plugins/other/image/20190624/c6bfbb27b99058e14e9e424a4b66104b.png
  57. BIN
      data/plugins/other/image/20190802/cd56d03b758b930a85e127ae24a467ca.png
  58. BIN
      data/plugins/other/image/20190808/17e9b99a0a3649deb219968a9172688a.png
  59. BIN
      data/plugins/other/image/20190809/3f8f90fde77213080143e63b668d8c77.png
  60. BIN
      data/plugins/other/image/20190829/36a4514dda9ebfde9b6fa1c7277c67e8.png
  61. BIN
      data/plugins/other/image/20190918/85b94a174957d8c2d078f6e58fef02b8.png
  62. BIN
      data/plugins/other/image/20190921/1b28d8321def8455207b6482d78d6534.png
  63. BIN
      data/plugins/other/image/20190925/797ff13c9157f656c08a27a1674cbb86.png
  64. BIN
      data/plugins/other/image/20190925/987b50979ba8c2781c62d1ddbe773cef.png
  65. BIN
      data/plugins/other/image/20191005/2366e9d746b8e52bfb7f00498dd88d41.png
  66. BIN
      data/plugins/other/image/20191008/2c48dbb3f2e21fc2eb19d68e0826d162.png
  67. BIN
      data/plugins/other/image/20191010/12ae100cb6da09c45ebb64c383f17daa.png
  68. BIN
      data/plugins/other/image/20191014/a5a91d5b7503ac6fc40b587c18f884cb.png
  69. BIN
      data/plugins/other/image/20191015/3739007955dbad246abd1bc51a3ce9cc.png
  70. BIN
      data/plugins/other/image/20191019/57f47dfa77ad9b958fdb0682c44dd2dd.png
  71. BIN
      data/plugins/other/image/20191031/3044f0e8898553b27a3f8e3f39b49829.png
  72. BIN
      data/plugins/other/image/20191101/ffb9085b6c30168308e289be27f11486.png
  73. BIN
      data/plugins/other/image/20191106/1989b486726cdea6a62954fe3b345e3d.png
  74. BIN
      data/plugins/other/image/20191107/065808e927f4c37052049b56561fe1cd.png
  75. BIN
      data/plugins/other/image/20191108/d74b2c5e165c729f76ca94923835350c.png
  76. BIN
      data/plugins/other/image/20191113/087f6342714ae5d298e8a75b87a9cd1f.png
  77. BIN
      data/plugins/other/image/20191115/a6f31569f41a4a331d0a7d778b3972ab.png
  78. BIN
      data/plugins/other/image/20191118/331aa690f39edc9f15f64045294e853e.png
  79. BIN
      data/plugins/other/image/20191217/e651e53602b61836185802ba6ea261ab.png
  80. BIN
      data/plugins/other/image/20191230/0596328af186e6272f09889cfcca5843.png
  81. BIN
      data/plugins/other/image/20200106/00e5f4d74d5e8acb10a5b4b279c0bad0.png
  82. BIN
      data/plugins/other/image/20200106/d67109fd986b85ae6291515a7d6b0a61.png
  83. BIN
      data/plugins/other/image/20200108/fcfe197c6ddf9cc3f36629f72c1285d7.png
  84. BIN
      data/plugins/other/image/20200120/b388d6ebc2fe5dc1b568663db38b2d49.png
  85. BIN
      data/plugins/other/image/20200129/2588534a9f75e23127bd0e882ae1690f.png
  86. BIN
      data/plugins/other/image/20200210/50e8a3b77777038f72a3b1467ddca264.png
  87. BIN
      data/plugins/other/image/20200218/ba006d057f070951a7b1cbe75e50ec65.png
  88. BIN
      data/plugins/other/image/20200315/d92c6611e3678db4bf3f85e0cc9d37cf.png
  89. BIN
      data/plugins/other/image/20200315/eea7e9c23a22857b685ac7a0302a542e.png
  90. BIN
      data/plugins/other/image/20200325/06a8123060338addc6804259a2de83cf.png
  91. BIN
      data/plugins/other/image/20200325/4f20682fd226e85ea0e7e8618a4697f6.png
  92. BIN
      data/plugins/other/image/20200326/6f437e4f013b783cac17842f2b79478b.png
  93. BIN
      data/plugins/other/image/20200329/40d01d19b0c2bff6acd5b2c02931d39a.png
  94. BIN
      data/plugins/other/image/20200330/a558018b235958e380eb27385d296672.png
  95. BIN
      data/plugins/other/image/20200401/5ab52b348d46d2fd21fd6af5bd987955.png
  96. BIN
      data/plugins/other/image/20200407/fea4f518a447cde5cbc5c6df1e1f434d.png
  97. BIN
      data/plugins/other/image/20200410/891111b223fb30cb065d6a29410c6d70.png
  98. BIN
      data/plugins/other/image/20200427/44047cab050e6d1a4df7ee1c935cd8a2.png
  99. BIN
      data/plugins/other/image/20200427/c053de465ca87c22cb8b4518d5a0425c.png
  100. BIN
      data/plugins/other/image/20200502/b5f71be748935aedb7d2211ead1da3d6.png

18
.env

@ -0,0 +1,18 @@
APP_DEBUG = false
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = mysql
HOSTNAME = localhost
DATABASE = btcloud
USERNAME = btcloud
PASSWORD = 123456
HOSTPORT = 3306
CHARSET = utf8mb4
PREFIX = cloud_
DEBUG = false
[LANG]
default_lang = zh-cn

4
.gitignore

@ -0,0 +1,4 @@
/.idea
/.vscode
/vendor
*.log

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Flucont
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.

48
README.md

@ -0,0 +1,48 @@
# 宝塔面板第三方云端
这是一个用php开发的宝塔面板第三方云端站点程序。
你可以使用此程序搭建属于自己的宝塔面板第三方云端,实现最新版宝塔面板私有化部署,不与宝塔官方接口通信,满足隐私安全合规需求。同时还可以去除面板强制绑定账号,DIY面板功能等。
网站后台管理可一键同步宝塔官方的插件列表与增量更新插件包,还有云端使用记录、IP黑白名单、操作日志、定时任务等功能。
本项目自带的宝塔安装包和更新包是7.9.2最新版,已修改适配此第三方云端,并且全开源,无so等加密文件。
觉得该项目不错的可以给个Star~
## 声明
1.此项目只能以自用为目的,不得侵犯堡塔公司及其他第三方的知识产权和其他合法权利。
2.搭建使用此项目必须有一定的编程和Linux运维基础,纯小白不建议使用。
## 环境要求
* `PHP` >= 7.4
* `MySQL` >= 5.6
* `fileinfo`扩展
* `ZipArchive`扩展
## 部署方法
- [下载最新版的Release包](https://github.com/flucont/btcloud/releases)
- 如果是下载的源码包,需要执行 `composer install --no-dev` 安装依赖,如果是下载的Release包,则不需要
- 设置网站运行目录为`public`
- 设置伪静态为`ThinkPHP`
- 导入`install.sql`到数据库
- 在`.env`里面修改数据库信息,包括数据库地址(HOSTNAME)、数据库名(DATABASE)、用户名(USERNAME)、密码(PASSWORD)
- 访问`/admin`进入网站后台,默认管理员用户名密码:admin/123456
## 使用方法
- 在`系统基本设置`修改宝塔面板接口设置。你需要一个官方最新脚本安装并绑定账号的宝塔面板,用于获取最新插件列表及插件包。并根据界面提示安装好专用插件。
- 在`定时任务设置`执行所显示的命令从宝塔官方获取最新的插件列表并批量下载插件包(增量更新)。当然你也可以去插件列表,一个一个点击下载。
- 在public/install/src和update文件夹里面分别是bt安装包和更新包,解压后源码里面全部的 www.example.com 替换成你自己搭建的云端域名,然后重新打包。可使用VSCode等支持批量替换的软件。
- 将bt安装脚本public/install/install_6.0.sh和更新脚本update6.sh里面的 www.example.com 替换成你自己搭建的云端域名。
- 访问网站`/download`查看使用此第三方云端的一键安装脚本
## 其他
- [bt官方更新包修改记录](./wiki/update.md)

1
app/.htaccess

@ -0,0 +1 @@
deny from all

22
app/AppService.php

@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

115
app/BaseController.php

@ -0,0 +1,115 @@
<?php
declare (strict_types = 1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
use think\facade\View;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
protected $clientip;
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{
$this->clientip = real_ip();
}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
protected function alert($code, $msg = '', $url = null, $wait = 3)
{
if ($url) {
$url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : (string)$this->app->route->buildUrl($url);
}
if(empty($msg)) $msg = '未知错误';
View::assign([
'code' => $code,
'msg' => $msg,
'url' => $url,
'wait' => $wait,
]);
return View::fetch(app()->getAppPath().'view/dispatch_jump.html');
}
}

58
app/ExceptionHandle.php

@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

8
app/Request.php

@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

115
app/command/UpdateAll.php

@ -0,0 +1,115 @@
<?php
declare (strict_types = 1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Config;
use app\lib\Plugins;
class UpdateAll extends Command
{
protected function configure()
{
$this->setName('updateall')
->setDescription('the updateall command');
}
protected function execute(Input $input, Output $output)
{
$res = Db::name('config')->cache('configs',0)->column('value','key');
Config::set($res, 'sys');
//刷新插件列表
if(!$this->refresh_plugin_list($input, $output)){
return;
}
$count = 0;
$type = intval(config_get('updateall_type'));
$json_arr = Plugins::get_plugin_list();
//循环下载缺少的插件
foreach($json_arr['list'] as $plugin){
if($type == 0 && ($plugin['type']==8 || $plugin['type']==12) || $type == 1 && $plugin['type']==12 || $plugin['type']==10 || $plugin['type']==5) continue;
foreach($plugin['versions'] as $version){
$ver = $version['m_version'].'.'.$version['version'];
if(isset($version['download'])){
if(!file_exists(get_data_dir().'plugins/other/'.$version['download'])){
$this->download_plugin($input, $output, $plugin['name'], $ver);
sleep(1);
$count++;
}
}else{
if(!file_exists(get_data_dir().'plugins/package/'.$plugin['name'].'-'.$ver.'.zip')){
$this->download_plugin($input, $output, $plugin['name'], $ver);
sleep(1);
$count++;
}
}
}
}
$imgcount = 0;
//循环下载缺少的插件图片
foreach($json_arr['list'] as $plugin){
if(isset($plugin['min_image']) && strpos($plugin['min_image'], 'fname=')){
$fname = substr($plugin['min_image'], strpos($plugin['min_image'], '?fname=')+7);
if(!file_exists(get_data_dir().'plugins/other/'.$fname)){
$this->download_plugin_image($input, $output, $fname);
sleep(1);
$imgcount++;
}
}
}
$output->writeln('本次成功下载'.$count.'个插件'.($imgcount>0?','.$imgcount.'个图片':''));
config_set('runtime', date('Y-m-d H:i:s'));
}
private function refresh_plugin_list(Input $input, Output $output){
try{
Plugins::refresh_plugin_list();
Db::name('log')->insert(['uid' => 1, 'action' => '刷新插件列表', 'data' => '刷新插件列表成功', 'addtime' => date("Y-m-d H:i:s")]);
$output->writeln('刷新插件列表成功');
return true;
}catch(\Exception $e){
$output->writeln($e->getMessage());
errorlog($e->getMessage());
return false;
}
}
private function download_plugin(Input $input, Output $output, $plugin_name, $version){
$fullname = $plugin_name.'-'.$version;
try{
Plugins::download_plugin($plugin_name, $version);
Db::name('log')->insert(['uid' => 1, 'action' => '下载插件', 'data' => $fullname, 'addtime' => date("Y-m-d H:i:s")]);
$output->writeln('下载插件: '.$fullname.' 成功');
return true;
}catch(\Exception $e){
$output->writeln($fullname.' '.$e->getMessage());
errorlog($fullname.' '.$e->getMessage());
return false;
}
}
private function download_plugin_image(Input $input, Output $output, $fname){
try{
Plugins::download_plugin_other($fname);
$output->writeln('下载图片: '.$fname.' 成功');
return true;
}catch(\Exception $e){
$output->writeln($fname.' '.$e->getMessage());
errorlog($fname.' '.$e->getMessage());
return false;
}
}
}

176
app/common.php

@ -0,0 +1,176 @@
<?php
// 应用公共文件
use think\facade\Db;
function get_data_dir(){
return app()->getRootPath().'data/';
}
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($operation == 'DECODE') {
if(((int)substr($result, 0, 10) == 0 || (int)substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc.str_replace('=', '', base64_encode($result));
}
}
function get_curl($url, $post=0, $referer=0, $cookie=0, $header=0, $ua=0, $nobody=0, $addheader=0)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Encoding: gzip,deflate,sdch";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
if($addheader){
$httpheader = array_merge($httpheader, $addheader);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
if ($post) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
if ($header) {
curl_setopt($ch, CURLOPT_HEADER, true);
}
if ($cookie) {
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
if($referer){
curl_setopt($ch, CURLOPT_REFERER, $referer);
}
if ($ua) {
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
}
else {
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36");
}
if ($nobody) {
curl_setopt($ch, CURLOPT_NOBODY, 1);
}
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$ret = curl_exec($ch);
curl_close($ch);
return $ret;
}
function jsonp_decode($jsonp, $assoc = false)
{
$jsonp = trim($jsonp);
if(isset($jsonp[0]) && $jsonp[0] !== '[' && $jsonp[0] !== '{') {
$begin = strpos($jsonp, '(');
if(false !== $begin)
{
$end = strrpos($jsonp, ')');
if(false !== $end)
{
$jsonp = substr($jsonp, $begin + 1, $end - $begin - 1);
}
}
}
return json_decode($jsonp, $assoc);
}
function config_get($key, $default = null)
{
$value = config('sys.'.$key);
return $value!==null ? $value : $default;
}
function config_set($key, $value)
{
$res = Db::name('config')->replace()->insert(['key'=>$key, 'value'=>$value]);
return $res!==false;
}
function real_ip($type=0){
$ip = $_SERVER['REMOTE_ADDR'];
if($type<=0 && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (filter_var($xip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $xip;
break;
}
}
} elseif ($type<=0 && isset($_SERVER['HTTP_CLIENT_IP']) && filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif ($type<=1 && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif ($type<=1 && isset($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
return $ip;
}
function getSubstr($str, $leftStr, $rightStr)
{
$left = strpos($str, $leftStr);
$start = $left+strlen($leftStr);
$right = strpos($str, $rightStr, $start);
if($left < 0) return '';
if($right>0){
return substr($str, $start, $right-$start);
}else{
return substr($str, $start);
}
}
function checkRefererHost(){
if(!request()->header('referer'))return false;
$url_arr = parse_url(request()->header('referer'));
$http_host = request()->header('host');
if(strpos($http_host,':'))$http_host = substr($http_host, 0, strpos($http_host, ':'));
return $url_arr['host'] === $http_host;
}
function checkIfActive($string) {
$array=explode(',',$string);
$action = request()->action();
if (in_array($action,$array)){
return 'active';
}else
return null;
}
function errorlog($msg){
$handle = fopen(app()->getRootPath()."record.txt", 'a');
fwrite($handle, date('Y-m-d H:i:s')."\t".$msg."\r\n");
fclose($handle);
}

341
app/controller/Admin.php

@ -0,0 +1,341 @@
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
use app\lib\Btapi;
use app\lib\Plugins;
class Admin extends BaseController
{
public function verifycode()
{
return captcha();
}
public function login(){
if(request()->islogin){
return redirect('/admin');
}
if(request()->isAjax()){
$username = input('post.username',null,'trim');
$password = input('post.password',null,'trim');
$code = input('post.code',null,'trim');
if(empty($username) || empty($password)){
return json(['code'=>-1, 'msg'=>'用户名或密码不能为空']);
}
if(!captcha_check($code)){
return json(['code'=>-1, 'msg'=>'验证码错误']);
}
if($username == config_get('admin_username') && $password == config_get('admin_password')){
Db::name('log')->insert(['uid' => 0, 'action' => '登录后台', 'data' => 'IP:'.$this->clientip, 'addtime' => date("Y-m-d H:i:s")]);
$session = md5($username.config_get('admin_password'));
$expiretime = time()+2562000;
$token = authcode("{$username}\t{$session}\t{$expiretime}", 'ENCODE', config_get('syskey'));
cookie('admin_token', $token, ['expire' => $expiretime, 'httponly' => true]);
config_set('admin_lastlogin', date('Y-m-d H:i:s'));
return json(['code'=>0]);
}else{
return json(['code'=>-1, 'msg'=>'用户名或密码错误']);
}
}
return view();
}
public function logout()
{
cookie('admin_token', null);
return redirect('/admin/login');
}
public function index()
{
$stat = ['total'=>0, 'free'=>0, 'pro'=>0, 'ltd'=>0, 'third'=>0];
$json_arr = Plugins::get_plugin_list();
if($json_arr){
foreach($json_arr['list'] as $plugin){
$stat['total']++;
if($plugin['type']==10) $stat['third']++;
elseif($plugin['type']==12) $stat['ltd']++;
elseif($plugin['type']==8) $stat['pro']++;
elseif($plugin['type']==5 || $plugin['type']==6 || $plugin['type']==7) $stat['free']++;
}
}
$stat['runtime'] = Db::name('config')->where('key','runtime')->value('value') ?? '<font color="red">未运行</font>';
$stat['record_total'] = Db::name('record')->count();
$stat['record_isuse'] = Db::name('record')->whereTime('usetime', '>=', strtotime('-7 days'))->count();
View::assign('stat', $stat);
$tmp = 'version()';
$mysqlVersion = Db::query("select version()")[0][$tmp];
$info = [
'framework_version' => app()::VERSION,
'php_version' => PHP_VERSION,
'mysql_version' => $mysqlVersion,
'software' => $_SERVER['SERVER_SOFTWARE'],
'os' => php_uname(),
'date' => date("Y-m-d H:i:s"),
];
View::assign('info', $info);
return view();
}
public function set(){
if(request()->isAjax()){
$params = Request::param();
foreach ($params as $key => $value) {
config_set($key, $value);
}
cache('configs', NULL);
return json(['code'=>0]);
}
$mod = input('param.mod', 'sys');
View::assign('mod', $mod);
View::assign('conf', config('sys'));
$runtime = Db::name('config')->where('key','runtime')->value('value') ?? '<font color="red">未运行</font>';
View::assign('runtime', $runtime);
return view();
}
public function setaccount(){
$params = Request::param();
if(isset($params['username']))$params['username']=trim($params['username']);
if(isset($params['oldpwd']))$params['oldpwd']=trim($params['oldpwd']);
if(isset($params['newpwd']))$params['newpwd']=trim($params['newpwd']);
if(isset($params['newpwd2']))$params['newpwd2']=trim($params['newpwd2']);
if(empty($params['username'])) return json(['code'=>-1, 'msg'=>'用户名不能为空']);
config_set('admin_username', $params['username']);
if(!empty($params['oldpwd']) && !empty($params['newpwd']) && !empty($params['newpwd2'])){
if(config_get('admin_password') != $params['oldpwd']){
return json(['code'=>-1, 'msg'=>'旧密码不正确']);
}
if($params['newpwd'] != $params['newpwd2']){
return json(['code'=>-1, 'msg'=>'两次新密码输入不一致']);
}
config_set('admin_password', $params['newpwd']);
}
cache('configs', NULL);
cookie('admin_token', null);
return json(['code'=>0]);
}
public function testbturl(){
$bt_url = input('post.bt_url');
$bt_key = input('post.bt_key');
if(!$bt_url || !$bt_key)return json(['code'=>-1, 'msg'=>'参数不能为空']);
$btapi = new Btapi($bt_url, $bt_key);
$result = $btapi->get_config();
if($result && isset($result['status']) && $result['status']==1){
$result = $btapi->get_user_info();
if($result && isset($result['username'])){
return json(['code'=>0, 'msg'=>'面板连接测试成功!']);
}else{
return json(['code'=>-1, 'msg'=>'面板连接测试成功,但未安装专用插件']);
}
}else{
return json(['code'=>-1, 'msg'=>isset($result['msg'])?$result['msg']:'面板地址无法连接']);
}
}
public function plugins(){
$typelist = [];
$json_arr = Plugins::get_plugin_list();
if($json_arr){
foreach($json_arr['type'] as $type){
$typelist[$type['id']] = $type['title'];
}
}
View::assign('typelist', $typelist);
return view();
}
public function plugins_data(){
$type = input('post.type/d');
$keyword = input('post.keyword', null, 'trim');
$json_arr = Plugins::get_plugin_list();
if(!$json_arr) return json([]);
$typelist = [];
foreach($json_arr['type'] as $row){
$typelist[$row['id']] = $row['title'];
}
$list = [];
foreach($json_arr['list'] as $plugin){
if($type > 0 && $plugin['type']!=$type) continue;
if(!empty($keyword) && $keyword != $plugin['name'] && stripos($plugin['title'], $keyword)===false) continue;
$versions = [];
foreach($plugin['versions'] as $version){
$ver = $version['m_version'].'.'.$version['version'];
if(isset($version['download'])){
$status = false;
if(file_exists(get_data_dir().'plugins/other/'.$version['download'])){
$status = true;
}
$versions[] = ['status'=>$status, 'type'=>1, 'version'=>$ver, 'download'=>$version['download'], 'md5'=>$version['md5']];
}else{
$status = false;
if(file_exists(get_data_dir().'plugins/package/'.$plugin['name'].'-'.$ver.'.zip')){
$status = true;
}
$versions[] = ['status'=>$status, 'type'=>0, 'version'=>$ver];
}
}
$list[] = [
'id' => $plugin['id'],
'name' => $plugin['name'],
'title' => $plugin['title'],
'type' => $plugin['type'],
'typename' => $typelist[$plugin['type']],
'desc' => str_replace('target="_blank"','target="_blank" rel="noopener noreferrer"',$plugin['ps']),
'price' => $plugin['price'],
'author' => isset($plugin['author']) ? $plugin['author'] : '官方',
'versions' => $versions
];
}
return json($list);
}
public function download_plugin(){
$name = input('post.name', null, 'trim');
$version = input('post.version', null, 'trim');
if(!$name || !$version) return json(['code'=>-1, 'msg'=>'参数不能为空']);
try{
Plugins::download_plugin($name, $version);
Db::name('log')->insert(['uid' => 0, 'action' => '下载插件', 'data' => $name.'-'.$version, 'addtime' => date("Y-m-d H:i:s")]);
return json(['code'=>0,'msg'=>'下载成功']);
}catch(\Exception $e){
return json(['code'=>-1, 'msg'=>$e->getMessage()]);
}
}
public function refresh_plugins(){
try{
Plugins::refresh_plugin_list();
Db::name('log')->insert(['uid' => 0, 'action' => '刷新插件列表', 'data' => '刷新插件列表成功', 'addtime' => date("Y-m-d H:i:s")]);
return json(['code'=>0,'msg'=>'获取最新插件列表成功!']);
}catch(\Exception $e){
return json(['code'=>-1, 'msg'=>$e->getMessage()]);
}
}
public function record(){
return view();
}
public function record_data(){
$ip = input('post.ip', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('record');
if(!empty($ip)){
$select->where('ip', $ip);
}
$total = $select->count();
$rows = $select->order('id','desc')->limit($offset, $limit)->select();
return json(['total'=>$total, 'rows'=>$rows]);
}
public function log(){
return view();
}
public function log_data(){
$action = input('post.action', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$select = Db::name('log');
if(!empty($action)){
$select->where('action', $action);
}
$total = $select->count();
$rows = $select->order('id','desc')->limit($offset, $limit)->select();
return json(['total'=>$total, 'rows'=>$rows]);
}
public function list(){
$type = input('param.type', 'black');
View::assign('type', $type);
View::assign('typename', $type=='white'?'白名单':'黑名单');
return view();
}
public function list_data(){
$type = input('param.type', 'black');
$ip = input('post.ip', null, 'trim');
$offset = input('post.offset/d');
$limit = input('post.limit/d');
$tablename = $type == 'black' ? 'black' : 'white';
$select = Db::name($tablename);
if(!empty($ip)){
$select->where('ip', $ip);
}
$total = $select->count();
$rows = $select->order('id','desc')->limit($offset, $limit)->select();
return json(['total'=>$total, 'rows'=>$rows]);
}
public function list_op(){
$type = input('param.type', 'black');
$tablename = $type == 'black' ? 'black' : 'white';
$act = input('post.act', null);
if($act == 'get'){
$id = input('post.id/d');
if(!$id) return json(['code'=>-1, 'msg'=>'no id']);
$data = Db::name($tablename)->where('id', $id)->find();
return json(['code'=>0, 'data'=>$data]);
}elseif($act == 'add'){
$ip = input('post.ip', null, 'trim');
if(!$ip) return json(['code'=>-1, 'msg'=>'IP不能为空']);
if(Db::name($tablename)->where('ip', $ip)->find()){
return json(['code'=>-1, 'msg'=>'该IP已存在']);
}
Db::name($tablename)->insert([
'ip' => $ip,
'enable' => 1,
'addtime' => date("Y-m-d H:i:s")
]);
return json(['code'=>0, 'msg'=>'succ']);
}elseif($act == 'edit'){
$id = input('post.id/d');
$ip = input('post.ip', null, 'trim');
if(!$id || !$ip) return json(['code'=>-1, 'msg'=>'IP不能为空']);
if(Db::name($tablename)->where('ip', $ip)->where('id', '<>', $id)->find()){
return json(['code'=>-1, 'msg'=>'该IP已存在']);
}
Db::name($tablename)->where('id', $id)->update([
'ip' => $ip
]);
return json(['code'=>0, 'msg'=>'succ']);
}elseif($act == 'enable'){
$id = input('post.id/d');
$enable = input('post.enable/d');
if(!$id) return json(['code'=>-1, 'msg'=>'no id']);
Db::name($tablename)->where('id', $id)->update([
'enable' => $enable
]);
return json(['code'=>0, 'msg'=>'succ']);
}elseif($act == 'del'){
$id = input('post.id/d');
if(!$id) return json(['code'=>-1, 'msg'=>'no id']);
Db::name($tablename)->where('id', $id)->delete();
return json(['code'=>0, 'msg'=>'succ']);
}
return json(['code'=>-1, 'msg'=>'no act']);
}
}

251
app/controller/Api.php

@ -0,0 +1,251 @@
<?php
namespace app\controller;
use think\facade\Db;
use app\BaseController;
use app\lib\Plugins;
class Api extends BaseController
{
//获取插件列表
public function get_plugin_list(){
if(!$this->checklist()) return '';
$record = Db::name('record')->where('ip',$this->clientip)->find();
if($record){
Db::name('record')->where('id',$record['id'])->update(['usetime'=>date("Y-m-d H:i:s")]);
}else{
Db::name('record')->insert(['ip'=>$this->clientip, 'addtime'=>date("Y-m-d H:i:s"), 'usetime'=>date("Y-m-d H:i:s")]);
}
$json_arr = Plugins::get_plugin_list();
if(!$json_arr) return json((object)[]);
return json($json_arr);
}
//下载插件包
public function download_plugin(){
$plugin_name = input('post.name');
$version = input('post.version');
if(!$plugin_name || !$version){
return '参数不能为空';
}
if(!preg_match('/^[a-zA-Z0-9_]+$/', $plugin_name) || !preg_match('/^[0-9.]+$/', $version)){
return '参数不正确';
}
if(!$this->checklist()) '你的服务器被禁止使用此云端';
$filepath = get_data_dir().'plugins/package/'.$plugin_name.'-'.$version.'.zip';
if(file_exists($filepath)){
$filename = $plugin_name.'.zip';
$this->output_file($filepath, $filename);
}else{
return '云端不存在该插件包';
}
}
//下载插件主文件
public function download_plugin_main(){
$plugin_name = input('post.name');
$version = input('post.version');
if(!$plugin_name || !$version){
return '参数不能为空';
}
if(!preg_match('/^[a-zA-Z0-9_]+$/', $plugin_name) || !preg_match('/^[0-9.]+$/', $version)){
return '参数不正确';
}
if(!$this->checklist()) '你的服务器被禁止使用此云端';
$filepath = get_data_dir().'plugins/main/'.$plugin_name.'-'.$version.'.dat';
if(file_exists($filepath)){
$filename = $plugin_name.'_main.py';
$this->output_file($filepath, $filename);
}else{
$filepath = get_data_dir().'plugins/folder/'.$plugin_name.'-'.$version.'/'.$plugin_name.'/'.$plugin_name.'_main.py';
if(file_exists($filepath)){
$filename = $plugin_name.'_main.py';
$this->output_file($filepath, $filename);
}else{
return '云端不存在该插件主文件';
}
}
}
//下载插件其他文件
public function download_plugin_other(){
$fname = input('get.fname');
if(!$fname){
return json(['status'=>false, 'msg'=>'参数不能为空']);
}
if(strpos(dirname($fname),'.')!==false)return json(['status'=>false, 'msg'=>'参数不正确']);
if(!$this->checklist()) return json(['status'=>false, 'msg'=>'你的服务器被禁止使用此云端']);
$filepath = get_data_dir().'plugins/other/'.$fname;
if(file_exists($filepath)){
$filename = basename($fname);
$this->output_file($filepath, $filename);
}else{
return json(['status'=>false, 'msg'=>'云端不存在该插件文件']);
}
}
public function get_update_logs(){
$version = config_get('new_version');
$data = [
[
'title' => 'Linux面板'.$version,
'body' => config_get('update_msg'),
'addtime' => config_get('update_date')
]
];
return jsonp($data);
}
public function get_version(){
$version = config_get('new_version');
return $version;
}
//安装统计
public function setup_count(){
return 'ok';
}
//检测更新
public function check_update(){
$version = config_get('new_version');
$down_url = request()->root(true).'/install/update/LinuxPanel-'.$version.'.zip';
$data = [
'force' => false,
'version' => $version,
'downUrl' => $down_url,
'updateMsg' => config_get('update_msg'),
'uptime' => config_get('update_date'),
'is_beta' => 0,
'adviser' => -1,
'btb' => '',
'beta' => [
'version' => $version,
'downUrl' => $down_url,
'updateMsg' => config_get('update_msg'),
'uptime' => config_get('update_date'),
]
];
return json($data);
}
//获取内测版更新日志
public function get_beta_logs(){
return json(['beta_ps'=>'当前暂无内测版', 'list'=>[]]);
}
//检查用户绑定是否正确
public function check_auth_key(){
return '1';
}
//从云端验证域名是否可访问
public function check_domain(){
$domain = input('post.domain',null,'trim');
$ssl = input('post.ssl/d');
if(!$domain) return json(['status'=>false, 'msg'=>'域名不能为空']);
if(!strpos($domain,'.')) return json(['status'=>false, 'msg'=>'域名格式不正确']);
$domain = str_replace('*.','',$domain);
$ip = gethostbyname($domain);
if(!$ip || $ip == $domain){
return json(['status'=>false, 'msg'=>'无法访问']);
}else{
return json(['status'=>true, 'msg'=>'访问正常']);
}
}
//同步时间
public function get_time(){
return time();
}
//查询是否专业版(废弃)
public function is_pro(){
return json(['endtime'=>true, 'code'=>1]);
}
//获取产品推荐信息
public function get_plugin_remarks(){
return json(['list'=>[], 'pro_list'=>[], 'kfqq'=>'', 'kf'=>'', 'qun'=>'']);
}
//获取指定插件评分
public function get_plugin_socre(){
return json(['total'=>0, 'split'=>[0,0,0,0,0],'page'=>"<div><span class='Pcurrent'>1</span><span class='Pcount'>共计0条数据</span></div>",'data'=>[]]);
}
//提交插件评分
public function plugin_score(){
return json(['status'=>true, 'msg'=>'您的评分已成功提交,感谢您的支持!']);
}
//获取IP地址
public function get_ip_address(){
return $this->clientip;
}
//绑定账号
public function get_auth_token(){
return json(['status'=>false, 'msg'=>'不支持绑定宝塔官网账号', 'data'=>'5b5d']);
}
public function return_success(){
return json(['status'=>true, 'msg'=>1, 'data'=>(object)[]]);
}
public function return_error(){
return json(['status'=>false, 'msg'=>'不支持当前操作']);
}
public function return_empty(){
return '';
}
public function return_empty_array(){
return json([]);
}
public function return_page_data(){
return json(['page'=>"<div><span class='Pcurrent'>1</span><span class='Pnumber'>1/0</span><span class='Pline'>从1-1000条</span><span class='Pcount'>共计0条数据</span></div>", 'data'=>[]]);
}
//检查黑白名单
private function checklist(){
if(config_get('whitelist') == 1){
if(Db::name('white')->where('ip', $this->clientip)->where('enable', 1)->find()){
return true;
}
return false;
}else{
if(Db::name('black')->where('ip', $this->clientip)->where('enable', 1)->find()){
return false;
}
return true;
}
}
//下载大文件
private function output_file($filepath, $filename){
$filesize = filesize($filepath);
$filemd5 = md5_file($filepath);
ob_clean();
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename={$filename}.zip");
header("Content-Length: {$filesize}");
header("File-size: {$filesize}");
header("Content-md5: {$filemd5}");
$read_buffer = 1024 * 100;
$handle = fopen($filepath, 'rb');
$sum_buffer = 0;
while(!feof($handle) && $sum_buffer<$filesize) {
echo fread($handle, min($read_buffer, ($filesize - $sum_buffer) + 1));
$sum_buffer += $read_buffer;
flush();
}
fclose($handle);
exit;
}
}

24
app/controller/Index.php

@ -0,0 +1,24 @@
<?php
namespace app\controller;
use app\BaseController;
use think\facade\View;
class Index extends BaseController
{
public function index()
{
return 'Server is ok';
}
public function download()
{
if(config_get('download_page') == '0' && !request()->islogin){
return redirect('/admin/login');
}
View::assign('siteurl', request()->root(true));
return view();
}
}

17
app/event.php

@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];

203
app/lib/Btapi.php

@ -0,0 +1,203 @@
<?php
namespace app\lib;
use Exception;
class Btapi
{
private $BT_KEY; //接口密钥
private $BT_PANEL; //面板地址
public function __construct($bt_panel, $bt_key){
$this->BT_PANEL = $bt_panel;
$this->BT_KEY = $bt_key;
}
//获取面板配置信息
public function get_config(){
$url = $this->BT_PANEL.'/config?action=get_config';
$p_data = $this->GetKeyData();
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//获取已登录用户信息
public function get_user_info(){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=get_user_info';
$p_data = $this->GetKeyData();
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//从云端获取插件列表
public function get_plugin_list(){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=get_plugin_list';
$p_data = $this->GetKeyData();
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//下载插件包,返回文件路径
public function get_plugin_filename($plugin_name, $version){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=download_plugin';
$p_data = $this->GetKeyData();
$p_data['plugin_name'] = $plugin_name;
$p_data['version'] = $version;
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//下载插件主程序文件,返回文件路径
public function get_plugin_main_filename($plugin_name, $version){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=download_plugin_main';
$p_data = $this->GetKeyData();
$p_data['plugin_name'] = $plugin_name;
$p_data['version'] = $version;
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//解密插件主程序py代码,返回文件路径
public function get_decode_plugin_main($plugin_name, $version){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=decode_plugin_main';
$p_data = $this->GetKeyData();
$p_data['plugin_name'] = $plugin_name;
$p_data['version'] = $version;
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//下载插件其他文件,返回文件路径
public function get_plugin_other_filename($fname){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=download_plugin_other';
$p_data = $this->GetKeyData();
$p_data['fname'] = $fname;
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
//下载文件
public function download($filename, $localpath){
$url = $this->BT_PANEL.'/download';
$p_data = $this->GetKeyData();
$p_data['filename'] = $filename;
$result = $this->curl_download($url.'?'.http_build_query($p_data), $localpath);
return $result;
}
//获取文件base64
public function get_file($filename){
$url = $this->BT_PANEL.'/plugin?action=a&name=kaixin&s=get_file';
$p_data = $this->GetKeyData();
$p_data['filename'] = $filename;
$result = $this->curl($url,$p_data);
$data = json_decode($result,true);
return $data;
}
private function GetKeyData(){
$now_time = time();
$p_data = array(
'request_token' => md5($now_time.''.md5($this->BT_KEY)),
'request_time' => $now_time
);
return $p_data;
}
private function curl($url, $data = null, $timeout = 60)
{
//定义cookie保存位置
$cookie_file=app()->getRuntimePath().md5($this->BT_PANEL).'.cookie';
if(!file_exists($cookie_file)){
touch($cookie_file);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
if($data){
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
private function curl_download($url, $localpath, $timeout = 60)
{
//定义cookie保存位置
$cookie_file=app()->getRuntimePath().md5($this->BT_PANEL).'.cookie';
if(!file_exists($cookie_file)){
touch($cookie_file);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
$fp = fopen($localpath, 'w+');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_exec($ch);
if (curl_errno($ch)) {
$message = curl_error($ch);
curl_close($ch);
fclose($fp);
throw new Exception('下载文件失败:'.$message);
}
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpcode>299){
curl_close($ch);
fclose($fp);
throw new Exception('下载文件失败:HTTPCODE-'.$httpcode);
}
curl_close($ch);
fclose($fp);
return true;
}
}

229
app/lib/Plugins.php

@ -0,0 +1,229 @@
<?php
namespace app\lib;
use Exception;
use ZipArchive;
class Plugins
{
private static function get_btapi(){
$bt_url = config_get('bt_url');
$bt_key = config_get('bt_key');
if(!$bt_url || !$bt_key) throw new Exception('请先配置好宝塔面板接口信息');
$btapi = new Btapi($bt_url, $bt_key);
return $btapi;
}
//刷新插件列表
public static function refresh_plugin_list(){
$btapi = self::get_btapi();
$result = $btapi->get_plugin_list();
if($result && isset($result['list']) && isset($result['type'])){
if(empty($result['list']) || empty($result['type'])){
throw new Exception('获取插件列表失败:插件列表为空');
}
self::save_plugin_list($result);
}else{
throw new Exception('获取插件列表失败:'.($result['msg']?$result['msg']:'面板连接失败'));
}
}
//保存插件列表
private static function save_plugin_list($data){
$data['ip'] = '127.0.0.1';
$data['serverid'] = '';
$data['beta'] = 0;
$data['uid'] = 1;
$data['skey'] = '';
$list = [];
foreach($data['list'] as $plugin){
if(isset($plugin['endtime'])) $plugin['endtime'] = 0;
$list[] = $plugin;
}
$data['list'] = $list;
if($data['pro']>-1) $data['pro'] = 0;
if($data['ltd']>-1) $data['ltd'] = strtotime('+1 year');
$json_file = get_data_dir().'config/plugin_list.json';
if(!file_put_contents($json_file, json_encode($data))){
throw new Exception('保存插件列表失败,文件无写入权限');
}
}
//获取插件列表
public static function get_plugin_list(){
$json_file = get_data_dir().'config/plugin_list.json';
if(file_exists($json_file)){
$data = file_get_contents($json_file);
$json_arr = json_decode($data, true);
if($json_arr){
return $json_arr;
}
}
return false;
}
//获取一个插件信息
public static function get_plugin_info($name){
$json_arr = self::get_plugin_list();
if(!$json_arr) return null;
foreach($json_arr['list'] as $plugin){
if($plugin['name'] == $name){
return $plugin;
}
}
return null;
}
//下载插件(自动判断是否第三方)
public static function download_plugin($plugin_name, $version){
$plugin_info = Plugins::get_plugin_info($plugin_name);
if(!$plugin_info) throw new Exception('未找到该插件信息');
if($plugin_info['type'] == 10 && isset($plugin_info['versions'][0]['download'])){
$fname = $plugin_info['versions'][0]['download'];
$filemd5 = $plugin_info['versions'][0]['md5'];
Plugins::download_plugin_other($fname, $filemd5);
if(isset($plugin_info['min_image']) && strpos($plugin_info['min_image'], 'fname=')){
$fname = substr($plugin_info['min_image'], strpos($plugin_info['min_image'], '?fname=')+7);
Plugins::download_plugin_other($fname);
}
}else{
Plugins::download_plugin_package($plugin_name, $version);
}
}
//下载插件包
public static function download_plugin_package($plugin_name, $version){
$filepath = get_data_dir().'plugins/package/'.$plugin_name.'-'.$version.'.zip';
$btapi = self::get_btapi();
$result = $btapi->get_plugin_filename($plugin_name, $version);
if($result && isset($result['status'])){
if($result['status'] == true){
$filename = $result['filename'];
self::download_file($btapi, $filename, $filepath);
if(file_exists($filepath)){
$zip = new ZipArchive;
if ($zip->open($filepath) === true)
{
$zip->extractTo(get_data_dir().'plugins/folder/'.$plugin_name.'-'.$version);
$zip->close();
$main_filepath = get_data_dir().'plugins/folder/'.$plugin_name.'-'.$version.'/'.$plugin_name.'/'.$plugin_name.'_main.py';
if(file_exists($main_filepath) && filesize($main_filepath)>10){
if(!strpos(file_get_contents($main_filepath), 'import ')){ //加密py文件,需要解密
self::decode_plugin_main($plugin_name, $version, $main_filepath);
$zip->open($filepath, ZipArchive::CREATE);
$zip->addFile($main_filepath, $plugin_name.'/'.$plugin_name.'_main.py');
$zip->close();
}
}
}else{
throw new Exception('插件包解压缩失败');
}
return true;
}else{
throw new Exception('下载插件包失败,本地文件不存在');
}
}else{
throw new Exception('下载插件包失败:'.($result['msg']?$result['msg']:'未知错误'));
}
}else{
throw new Exception('下载插件包失败,接口返回错误');
}
}
//下载插件主程序文件
public static function download_plugin_main($plugin_name, $version){
$filepath = get_data_dir().'plugins/main/'.$plugin_name.'-'.$version.'.dat';
$btapi = self::get_btapi();
$result = $btapi->get_plugin_main_filename($plugin_name, $version);
if($result && isset($result['status'])){
if($result['status'] == true){
$filename = $result['filename'];
self::download_file($btapi, $filename, $filepath);
if(file_exists($filepath)){
return true;
}else{
throw new Exception('下载插件主程序文件失败,本地文件不存在');
}
}else{
throw new Exception('下载插件主程序文件失败:'.($result['msg']?$result['msg']:'未知错误'));
}
}else{
throw new Exception('下载插件主程序文件失败,接口返回错误');
}
}
//解密并下载插件主程序文件
public static function decode_plugin_main($plugin_name, $version, $main_filepath){
$btapi = self::get_btapi();
$result = $btapi->get_decode_plugin_main($plugin_name, $version);
if($result && isset($result['status'])){
if($result['status'] == true){
$filename = $result['filename'];
self::download_file($btapi, $filename, $main_filepath);
return true;
}else{
throw new Exception('解密插件主程序文件失败:'.($result['msg']?$result['msg']:'未知错误'));
}
}else{
throw new Exception('解密插件主程序文件失败,接口返回错误');
}
}
//下载插件其他文件
public static function download_plugin_other($fname, $filemd5 = null){
$filepath = get_data_dir().'plugins/other/'.$fname;
@mkdir(dirname($filepath), 0777, true);
$btapi = self::get_btapi();
$result = $btapi->get_plugin_other_filename($fname);
if($result && isset($result['status'])){
if($result['status'] == true){
$filename = $result['filename'];
self::download_file($btapi, $filename, $filepath);
if(file_exists($filepath)){
if($filemd5 && md5_file($filepath) != $filemd5){
unlink($filepath);
throw new Exception('插件文件MD5校验失败');
}
return true;
}else{
throw new Exception('下载插件文件失败,本地文件不存在');
}
}else{
throw new Exception('下载插件文件失败:'.($result['msg']?$result['msg']:'未知错误'));
}
}else{
throw new Exception('下载插件文件失败,接口返回错误');
}
}
//下载文件
private static function download_file($btapi, $filename, $filepath){
try{
$btapi->download($filename, $filepath);
}catch(Exception $e){
unlink($filepath);
//宝塔bug小文件下载失败,改用base64下载
$result = $btapi->get_file($filename);
if($result && isset($result['status']) && $result['status']==true){
$filedata = base64_decode($result['data']);
if(strlen($filedata) < 4096 && substr($filedata,0,1)=='{' && substr($filedata,-1,1)=='}'){
$arr = json_decode($filedata, true);
if($arr){
throw new Exception('获取文件失败:'.($arr['msg']?$arr['msg']:'未知错误'));
}
}
if(!$filedata){
throw new Exception('获取文件失败:文件内容为空');
}
file_put_contents($filepath, $filedata);
}elseif($result){
throw new Exception('获取文件失败:'.($result['msg']?$result['msg']:'未知错误'));
}else{
throw new Exception('获取文件失败:未知错误');
}
}
}
}

12
app/middleware.php

@ -0,0 +1,12 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class,
\app\middleware\LoadConfig::class,
\app\middleware\AuthAdmin::class
];

24
app/middleware/AuthAdmin.php

@ -0,0 +1,24 @@
<?php
declare (strict_types=1);
namespace app\middleware;
class AuthAdmin
{
public function handle($request, \Closure $next)
{
$islogin = false;
$cookie = cookie('admin_token');
if($cookie){
$token=authcode($cookie, 'DECODE', config_get('syskey'));
list($user, $sid, $expiretime) = explode("\t", $token);
$session=md5(config_get('admin_username').config_get('admin_password'));
if($session==$sid && $expiretime>time()) {
$islogin = true;
}
}
request()->islogin = $islogin;
return $next($request);
}
}

19
app/middleware/CheckAdmin.php

@ -0,0 +1,19 @@
<?php
declare (strict_types=1);
namespace app\middleware;
class CheckAdmin
{
public function handle($request, \Closure $next)
{
if (!request()->islogin) {
if ($request->isAjax() || !$request->isGet()) {
return json(['code'=>-1, 'msg'=>'未登录'])->code(401);
}
return redirect((string)url('/admin/login'));
}
return $next($request);
}
}

29
app/middleware/LoadConfig.php

@ -0,0 +1,29 @@
<?php
declare (strict_types = 1);
namespace app\middleware;
use think\facade\Db;
use think\facade\Config;
class LoadConfig
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
$res = Db::name('config')->cache('configs',0)->column('value','key');
Config::set($res, 'sys');
return $next($request)->header([
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache',
]);
}
}

24
app/middleware/RefererCheck.php

@ -0,0 +1,24 @@
<?php
declare (strict_types=1);
namespace app\middleware;
use think\facade\View;
class RefererCheck
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
if(!checkRefererHost()){
return response('Access Denied', 403);
}
return $next($request);
}
}

9
app/provider.php

@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];

9
app/service.php

@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];

59
app/view/admin/index.html

@ -0,0 +1,59 @@
{extend name="admin/layout" /}
{block name="title"}宝塔第三方云端管理中心{/block}
{block name="main"}
<style>
.query-title {
background-color:#f5fafe;
word-break: keep-all;
}
.query-result{
word-break: break-all;
}
</style>
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-sm-10 col-md-8 center-block" style="float: none;">
<div class="panel panel-primary">
<div class="panel-heading"><h3 class="panel-title">后台管理首页</h3></div>
<div class="list-group">
<div class="list-group-item"><span class="glyphicon glyphicon-stats"></span> <b>宝塔插件统计:</b>共有 {$stat.total} 个,其中免费插件 {$stat.free} 个,专业版插件 {$stat.pro} 个,企业版插件 {$stat.ltd} 个,第三方插件 {$stat.third} 个</div>
<div class="list-group-item"><span class="glyphicon glyphicon-tint"></span> <b>使用记录统计:</b>历史总共数量:{$stat.record_total},正在使用数量:{$stat.record_isuse}</div>
<div class="list-group-item"><span class="glyphicon glyphicon-time"></span> <b>任务运行情况:</b>上次运行时间:{$stat.runtime|raw}&nbsp;&nbsp;<a href="/admin/set/mod/task" class="btn btn-xs btn-info">查看详情</a></div>
<div class="list-group-item"><span class="glyphicon glyphicon-cog"></span> <b>常用功能入口:</b><a href="/admin/plugins" class="btn btn-xs btn-default">插件列表</a>&nbsp;<a href="/admin/record" class="btn btn-xs btn-default">使用记录</a>&nbsp;<a href="/admin/black" class="btn btn-xs btn-default">黑白名单</a>&nbsp;<a href="/download" class="btn btn-xs btn-default" target="_blank">安装脚本</a></div>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">服务器信息</h3>
</div>
<table class="table table-bordered">
<tbody>
<tr>
<td class="query-title">框架版本</td>
<td class="query-result">{$info.framework_version}</td>
</tr>
<tr>
<td class="query-title">PHP版本</td>
<td class="query-result">{$info.php_version}</td>
</tr>
<tr>
<td class="query-title">MySQL版本</td>
<td class="query-result">{$info.mysql_version}</td>
</tr>
<tr>
<td class="query-title">WEB软件</td>
<td class="query-result">{$info.software}</td>
</tr>
<tr>
<td class="query-title">操作系统</td>
<td class="query-result">{$info.os}</td>
</tr>
<tr>
<td class="query-title">服务器时间</td>
<td class="query-result">{$info.date}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{/block}

67
app/view/admin/layout.html

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{block name="title"}标题{/block}</title>
<link href="//cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="//cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="/static/css/bootstrap-table.css" rel="stylesheet" />
<script src="//cdn.staticfile.org/modernizr/2.8.3/modernizr.min.js"></script>
<script src="//cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
<script src="//cdn.staticfile.org/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<!--[if lt IE 9]>
<script src="//cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="//cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">导航按钮</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="./">宝塔第三方云端管理中心</a>
</div><!-- /.navbar-header -->
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="{:checkIfActive('index')}">
<a href="/admin"><i class="fa fa-home"></i> 后台首页</a>
</li>
<li class="{:checkIfActive('plugins')}">
<a href="/admin/plugins"><i class="fa fa-cubes"></i> 插件列表</a>
</li>
<li class="{:checkIfActive('record')}">
<a href="/admin/record"><i class="fa fa-list"></i> 使用记录</a>
</li>
<li class="{:checkIfActive('list')}">
<a href="/admin/list"><i class="fa fa-globe"></i> 黑白名单</a>
</li>
<li class="{:checkIfActive('log')}">
<a href="/admin/log"><i class="fa fa-calendar"></i> 操作日志</a>
</li>
<li class="{:checkIfActive('set')}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-cog"></i> 系统设置<b
class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/admin/set">系统基本设置</a></li>
<li><a href="/admin/set/mod/task">定时任务设置</a></li>
<li><a href="/admin/set/mod/account">管理账号设置</a></li>
</ul>
</li>
<li>
<a href="/admin/logout" onclick="return confirm('确定退出登录吗?')"><i class="fa fa-power-off"></i> 退出登录</a>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container -->
</nav><!-- /.navbar -->
{block name="main"}主内容{/block}
</body>
</html>

209
app/view/admin/list.html

@ -0,0 +1,209 @@
{extend name="admin/layout" /}
{block name="title"}黑白名单{/block}
{block name="main"}
<style>
.alert{margin-bottom: 5px;}
</style>
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-md-10 center-block" style="float: none;">
<ul class="nav nav-tabs">
<li class="{if $type=='black'}active{/if}"><a href="/admin/list">黑名单列表</a></li><li class="{if $type=='white'}active{/if}"><a href="/admin/list/type/white">白名单列表</a></li>
</ul>
{if $type=='black' && config_get('whitelist')=='1'}
<div class="alert alert-warning">提示:当前为白名单模式,黑名单列表里面的记录不会生效。</div>
{/if}
{if $type=='white' && config_get('whitelist')=='0'}
<div class="alert alert-warning">提示:当前未开启白名单模式,白名单列表里面的记录不会生效。</div>
{/if}
{if $type=='black'}
<div class="alert alert-info">添加到黑名单列表中的服务器IP将无法使用此云端</div>
{/if}
{if $type=='white'}
<div class="alert alert-info">只有添加到白名单列表中的服务器IP才可以使用此云端</div>
{/if}
<div id="searchToolbar">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="ip" placeholder="服务器IP">
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit"><i class="fa fa-search"></i>搜索</button>&nbsp;
<a href="javascript:searchClear()" class="btn btn-default"><i class="fa fa-repeat"></i>重置</a>&nbsp;
<a href="javascript:add_item()" class="btn btn-success"><i class="fa fa-plus"></i>添加</a>&nbsp;
</div>
</form>
</div>
<table id="listTable">
</table>
</div>
</div>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
function setEnable(id,enable) {
$.ajax({
type : 'POST',
url : '/admin/list_op/type/{$type}',
data: { act:'enable', id:id, enable:enable},
dataType : 'json',
success : function(data) {
if(data.code == 0){
searchSubmit();
}else{
layer.msg(data.msg, {icon:2, time:1500});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
}
function add_item(){
layer.open({
area: ['360px'],
title: '添加IP{$typename}',
content: '<div class="form-group"><input type="text" class="form-control" name="item_input" placeholder="请输入服务器IP" value=""></div>',
yes: function(){
var ip = $("input[name='item_input']").val();
$.ajax({
type : 'POST',
url : '/admin/list_op/type/{$type}',
data: { act:'add', ip:ip},
dataType : 'json',
success : function(data) {
if(data.code == 0){
layer.msg('添加成功', {icon:1, time:800});
searchSubmit();
}else{
layer.alert(data.msg, {icon: 2});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
},
shadeClose: true
});
}
function edit_item(id){
$.ajax({
type : 'POST',
url : '/admin/list_op/type/{$type}',
data: { act:'get', id:id},
dataType : 'json',
success : function(data) {
if(data.code == 0){
layer.open({
area: ['360px'],
title: '编辑IP{$typename}',
content: '<div class="form-group"><input type="text" class="form-control" name="item_input" placeholder="请输入服务器IP" value="'+data.data.ip+'"></div>',
yes: function(){
var ip = $("input[name='item_input']").val();
$.ajax({
type : 'POST',
url : '/admin/list_op/type/{$type}',
data: { act:'edit', id:id, ip:ip},
dataType : 'json',
success : function(data) {
if(data.code == 0){
layer.msg('修改成功', {icon:1, time:800});
searchSubmit();
}else{
layer.alert(data.msg, {icon: 2});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
},
shadeClose: true
});
}else{
layer.alert(data.msg, {icon: 2});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
}
function del_item(id) {
if(confirm('是否确定删除此记录?')){
$.ajax({
type : 'POST',
url : '/admin/list_op/type/{$type}',
data: { act:'del', id:id},
dataType : 'json',
success : function(data) {
if(data.code == 0){
layer.msg('删除成功!', {icon:1, time:800});
searchSubmit();
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
}
}
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/admin/list_data/type/{$type}',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bottom-border',
columns: [
{
field: 'id',
title: 'ID',
formatter: function(value, row, index) {
return '<b>'+value+'</b>';
}
},
{
field: 'ip',
title: '服务器IP'
},
{
field: 'enable',
title: '是否生效',
formatter: function(value, row, index) {
return value?'<a href="javascript:setEnable('+row.id+',0)"><font color=green><i class="fa fa-check-circle"></i>已生效</font></a>':'<a href="javascript:setEnable('+row.id+',1)"><font color=red><i class="fa fa-times-circle"></i>未生效</font></a>';
}
},
{
field: 'addtime',
title: '添加时间'
},
{
field: '',
title: '操作',
formatter: function(value, row, index) {
return '<a href="javascript:edit_item('+row.id+')" class="btn btn-xs btn-info">编辑</a>&nbsp;<a href="javascript:del_item('+row.id+')" class="btn btn-xs btn-danger">删除</a>';
},
},
],
})
})
</script>
{/block}

74
app/view/admin/log.html

@ -0,0 +1,74 @@
{extend name="admin/layout" /}
{block name="title"}操作日志{/block}
{block name="main"}
<style>
</style>
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-md-10 center-block" style="float: none;">
<div id="searchToolbar">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="action" placeholder="操作类型">
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit"><i class="fa fa-search"></i>搜索</button>&nbsp;
<a href="javascript:searchClear()" class="btn btn-default"><i class="fa fa-repeat"></i>重置</a>&nbsp;
</div>
</form>
</div>
<table id="listTable">
</table>
</div>
</div>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 20;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/admin/log_data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bottom-border',
columns: [
{
field: 'id',
title: 'ID',
formatter: function(value, row, index) {
return '<b>'+value+'</b>';
}
},
{
field: 'uid',
title: '操作人',
formatter: function(value, row, index) {
return value==1?'<font color="green">定时任务</font>':'<font color="blue">管理员</font>';
}
},
{
field: 'action',
title: '操作类型'
},
{
field: 'data',
title: '操作详情',
},
{
field: 'addtime',
title: '操作时间'
},
],
})
})
</script>
{/block}

97
app/view/admin/login.html

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8"/>
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>管理员登录</title>
<link href="//cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//cdn.staticfile.org/modernizr/2.8.3/modernizr.min.js"></script>
<script src="//cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
<!--[if lt IE 9]>
<script src="//cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="//cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-default">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">导航按钮</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="./">宝塔第三方云端管理中心</a>
</div><!-- /.navbar-header -->
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li class="active">
<a href="./login.php"><span class="glyphicon glyphicon-user"></span> 登录</a>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container -->
</nav><!-- /.navbar -->
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-sm-10 col-md-8 col-lg-6 center-block" style="float: none;">
<div class="panel panel-primary">
<div class="panel-heading"><h3 class="panel-title">管理员登录</h3></div>
<div class="panel-body">
<form class="form-horizontal" role="form" onsubmit="return submitlogin()">
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
<input type="text" name="user" value="" class="form-control input-lg" placeholder="用户名" required="required"/>
</div><br/>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
<input type="password" name="pass" class="form-control input-lg" placeholder="密码" required="required"/>
</div><br/>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-adjust"></span></span>
<input type="text" class="form-control input-lg" name="code" placeholder="输入验证码" autocomplete="off" required>
<span class="input-group-addon" style="padding: 0">
<img src="/admin/verifycode" height="45" id="verifycode" onclick="this.src='/admin/verifycode?r='+Math.random();" title="点击更换验证码">
</span>
</div><br/>
<div class="form-group">
<div class="col-xs-12"><input type="submit" value="立即登录" class="btn btn-primary btn-block btn-lg"/></div>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script>
function submitlogin(){
var user = $("input[name='user']").val();
var pass = $("input[name='pass']").val();
var code = $("input[name='code']").val();
if(user=='' || pass==''){layer.alert('用户名或密码不能为空!');return false;}
$.ajax({
type : 'POST',
url : '{:request()->url()}',
data: {username:user, password:pass, code:code},
dataType : 'json',
success : function(data) {
if(data.code == 0){
layer.msg('登录成功,正在跳转', {icon: 1,shade: 0.01,time: 15000});
window.location.href='/admin';
}else{
if(data.msg.indexOf('验证码')==-1){
$("#verifycode").attr('src', '/admin/verifycode?r='+Math.random())
}
layer.alert(data.msg, {icon: 2});
}
},
error:function(data){
layer.msg('服务器错误');
}
});
return false;
}
</script>
</body>
</html>

214
app/view/admin/plugins.html

@ -0,0 +1,214 @@
{extend name="admin/layout" /}
{block name="title"}插件列表{/block}
{block name="main"}
<style>
td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:340px;}
.bt-ico-ask {
border: 1px solid #fb7d00;
border-radius: 8px;
color: #fb7d00;
cursor: help;
display: inline-block;
font-family: arial;
font-size: 11px;
font-style: normal;
height: 16px;
line-height: 16px;
margin-left: 5px;
text-align: center;
width: 16px
}
.bt-ico-ask:hover {
background-color: #fb7d00;
color: #fff
}
</style>
<div class="modal fade" id="help" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">帮助</h4>
</div>
<div class="modal-body">
<p>“版本与状态”一列中,红色的按钮代表本地不存在该版本插件包,需要点击下载;绿色的按钮代表已存在。</p>
<p>插件包本地存储路径是/data/plugins/package/软件标识-版本号.zip,对于部分包含二次验证的插件可以自行修改。</p>
<p>点击【重新获取】按钮会从宝塔官方获取最新插件列表,但是插件包需要手动点击下载。如果需要批量下载插件包,可查看<a href="/admin/set/type/task">定时任务设置</a></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 center-block" style="float: none;">
<div id="searchToolbar">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="keyword" placeholder="应用名称">
</div>
<div class="form-group">
<select name="type" class="form-control"><option value="0">全部插件</option>
{foreach $typelist as $k=>$v}<option value="{$k}">{$v}</option>{/foreach} </select>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit"><i class="fa fa-search"></i>搜索</button>&nbsp;
<a href="javascript:searchClear()" class="btn btn-default"><i class="fa fa-repeat"></i>重置</a>&nbsp;
<a href="javascript:refresh_plugins()" class="btn btn-success"><i class="fa fa-refresh"></i>重新获取</a>&nbsp;
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#help"><i class="fa fa-info-circle"></i>帮助</button>
</div>
</form>
</div>
<table id="listTable">
</table>
</div>
</div>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
function download_version(name, version, status){
if(status == true){
var confirm = layer.confirm('是否确定重新下载'+version+'版本插件包?', {
btn: ['确定','取消']
}, function(){
download_plugin(name, version)
}, function(){
layer.close(confirm)
});
}else{
download_plugin(name, version)
}
}
function download_plugin(name, version){
var ii = layer.msg('正在下载,请稍候...', {icon: 16, shade:0.1, time: 0});
$.ajax({
type : 'POST',
url : '/admin/download_plugin',
data: { name:name, version:version},
dataType : 'json',
success : function(data) {
layer.close(ii)
if(data.code == 0){
layer.alert(data.msg, {icon:1}, function(){layer.closeAll();searchSubmit();});
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii)
layer.msg('服务器错误', {icon:2});
}
});
}
function refresh_plugins(){
var confirm = layer.confirm('是否确定从宝塔官方获取最新插件列表?', {
btn: ['确定','取消']
}, function(){
layer.close(confirm)
var ii = layer.msg('正在获取插件列表,请稍候...', {icon: 16, shade:0.1, time: 0});
$.ajax({
type : 'GET',
url : '/admin/refresh_plugins',
dataType : 'json',
success : function(data) {
layer.close(ii)
if(data.code == 0){
layer.alert(data.msg, {icon:1}, function(){layer.closeAll();searchSubmit();});
}else{
layer.alert(data.msg, {icon:2});
}
},
error:function(data){
layer.close(ii)
layer.msg('服务器错误', {icon:2});
}
});
}, function(){
layer.close(confirm)
});
}
function searchByType(type){
$("input[name=keyword]").val('');
$("select[name=type]").val(type);
searchSubmit()
}
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 20;
$("#listTable").bootstrapTable({
url: '/admin/plugins_data',
pageNumber: 1,
pageSize: 15,
sidePagination: 'client',
classes: 'table table-striped table-hover table-bottom-border',
columns: [
{
field: 'name',
title: '软件标识',
formatter: function(value, row, index) {
return '<b>'+value+'</b>';
}
},
{
field: 'title',
title: '软件名称'
},
{
field: 'type',
title: '软件分类',
formatter: function(value, row, index) {
return '<a href="javascript:searchByType('+value+')" title="查看该分类下的插件">'+row.typename+'</a>';
}
},
{
field: 'desc',
title: '说明',
},
{
field: 'price',
title: '价格',
formatter: function(value, row, index) {
return value > 0 ? '<span style="color:#fc6d26">¥'+value+'</span>' : '免费';
}
},
{
field: 'author',
title: '开发商'
},
{
field: 'versions',
title: '版本与状态',
formatter: function(value, row, index) {
var html = '';
if(row.type == 5){
html += '<a href="javascript:" class="btn btn-xs btn-success" disabled>无需下载</a>';
}else{
$.each(value, function(index,item){
if(item.status)
html += '<a href="javascript:download_version(\''+row.name+'\',\''+item.version+'\','+item.status+')" class="btn btn-xs btn-success">'+item.version+'</a>&nbsp;';
else
html += '<a href="javascript:download_version(\''+row.name+'\',\''+item.version+'\','+item.status+')" class="btn btn-xs btn-danger">'+item.version+'</a>&nbsp;';
})
}
return html
}
},
],
})
})
</script>
{/block}

67
app/view/admin/record.html

@ -0,0 +1,67 @@
{extend name="admin/layout" /}
{block name="title"}使用记录{/block}
{block name="main"}
<style>
</style>
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-md-10 center-block" style="float: none;">
<div id="searchToolbar">
<form onsubmit="return searchSubmit()" method="GET" class="form-inline">
<div class="form-group">
<label>搜索</label>
<input type="text" class="form-control" name="ip" placeholder="服务器IP">
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit"><i class="fa fa-search"></i>搜索</button>&nbsp;
<a href="javascript:searchClear()" class="btn btn-default"><i class="fa fa-repeat"></i>重置</a>&nbsp;
</div>
</form>
</div>
<table id="listTable">
</table>
</div>
</div>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/bootstrap-table.min.js"></script>
<script src="//cdn.staticfile.org/bootstrap-table/1.20.2/extensions/page-jump-to/bootstrap-table-page-jump-to.min.js"></script>
<script src="/static/js/custom.js"></script>
<script>
$(document).ready(function(){
updateToolbar();
const defaultPageSize = 15;
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
$("#listTable").bootstrapTable({
url: '/admin/record_data',
pageNumber: pageNumber,
pageSize: pageSize,
classes: 'table table-striped table-hover table-bottom-border',
columns: [
{
field: 'id',
title: 'ID',
formatter: function(value, row, index) {
return '<b>'+value+'</b>';
}
},
{
field: 'ip',
title: '服务器IP'
},
{
field: 'addtime',
title: '首次安装时间',
},
{
field: 'usetime',
title: '最后使用时间'
},
],
})
})
</script>
{/block}

211
app/view/admin/set.html

@ -0,0 +1,211 @@
{extend name="admin/layout" /}
{block name="title"}系统设置{/block}
{block name="main"}
<div class="container" style="padding-top:70px;">
<div class="col-xs-12 col-sm-10 col-lg-8 center-block" style="float: none;">
{if $mod=='sys'}
<div class="panel panel-primary">
<div class="panel-heading"><h3 class="panel-title">系统基本设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form" role="form">
<div class="form-group">
<label>是否开启白名单模式:</label><br/>
<select class="form-control" name="whitelist" default="{:config_get('whitelist')}"><option value="0">关闭</option><option value="1">开启</option></select>
<font color="green">开启白名单模式后,只有在<a href="/admin/list/type/white" target="_blank">白名单列表</a>中的服务器IP才能使用此云端</font>
</div>
<div class="form-group">
<label>安装脚本展示页面开关:</label>
<select class="form-control" name="download_page" default="{:config_get('download_page')}"><option value="0">关闭</option><option value="1">开启</option></select>
<font color="green">页面地址:<a href="/download" target="_blank">/download</a>,开启后可以公开访问,否则只能管理员访问</font>
</div>
<div class="form-group">
<label>宝塔面板最新版本号:</label>
<input type="text" name="new_version" value="{:config_get('new_version')}" class="form-control"/>
<font color="green">用于一键更新脚本获取最新版本号,以及检测更新接口。并确保已在/public/install/update/放置对应版本更新包</font>
</div>
<div class="form-group">
<label>宝塔面板最新版更新日志:</label>
<textarea class="form-control" name="update_msg" rows="5" placeholder="支持HTML代码">{:config_get('update_msg')}</textarea>
<font color="green">用于检测更新接口返回</font>
</div>
<div class="form-group">
<label>宝塔面板最新版更新日期:</label>
<input type="date" name="update_date" value="{:config_get('update_date')}" class="form-control"/>
<font color="green">用于检测更新接口返回</font>
</div>
<div class="form-group text-center">
<input type="submit" name="submit" value="保存" class="btn btn-success btn-block"/>
</div>
</form>
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading"><h3 class="panel-title">宝塔面板接口设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form" role="form">
<p>以下宝塔面板请使用官方最新脚本安装并绑定账号,用于获取最新插件列表及插件包</p>
<p><a href="/static/file/kaixin.zip">下载专用插件</a>,在面板【软件商店】->【第三方应用】,点击【导入插件】,导入该专用插件,<b>然后重启一次面板</b></p>
<div class="form-group">
<label>宝塔面板URL:</label><br/>
<input type="text" name="bt_url" value="{:config_get('bt_url')}" class="form-control"/>
<font color="green">填写规则如:<u>http://192.168.1.1:8888</u> ,不要带其他后缀</font>
</div>
<div class="form-group">
<label>宝塔面板接口密钥:</label>
<input type="text" name="bt_key" value="{:config_get('bt_key')}" class="form-control"/>
</div>
<div class="form-group text-center">
<button type="button" class="btn btn-info btn-block" id="testbturl">测试连接</button>
<input type="submit" name="submit" value="保存" class="btn btn-success btn-block"/>
</div>
</form>
</div>
</div>
{elseif $mod=='task'}
<div class="panel panel-success">
<div class="panel-heading"><h3 class="panel-title">定时任务说明</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form" role="form">
<div class="alert alert-info">使用以下命令可以从宝塔官方获取最新的插件列表并批量下载插件包(增量更新)。<br/>你也可以将此命令添加到crontab以使此云端的插件保持最新,建议1天执行1次。</div>
<div class="alert alert-warning">上次运行时间:{$runtime|raw}</div>
<div class="list-group-item">php {:app()->getRootPath()}think updateall</div><br/>
</form>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">定时任务设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveSetting(this)" method="post" class="form" role="form">
<div class="form-group">
<label>批量下载插件范围:</label><br/>
<select class="form-control" name="updateall_type" default="{:config_get('updateall_type')}"><option value="0">仅免费插件</option><option value="1">免费插件+专业版插件</option><option value="2">免费插件+专业版插件+企业版插件</option></select><font color="green">(批量下载不包含所有第三方插件,第三方插件需要去手动下载。)</font>
</div>
<div class="form-group text-center">
<input type="submit" name="submit" value="保存" class="btn btn-success btn-block"/>
</div>
</form>
</div>
</div>
{elseif $mod=='account'}
<div class="panel panel-primary">
<div class="panel-heading"><h3 class="panel-title">管理账号设置</h3></div>
<div class="panel-body">
<form onsubmit="return saveAccount(this)" method="post" class="form" role="form">
<div class="form-group">
<label>用户名:</label><br/>
<input type="text" name="username" value="{:config_get('admin_username')}" class="form-control" required/>
</div>
<div class="form-group">
<label>旧密码:</label>
<input type="password" name="oldpwd" value="" class="form-control" placeholder="请输入当前的管理员密码"/>
</div>
<div class="form-group">
<label>新密码:</label>
<input type="password" name="newpwd" value="" class="form-control" placeholder="不修改请留空"/>
</div>
<div class="form-group">
<label>重输密码:</label>
<input type="password" name="newpwd2" value="" class="form-control" placeholder="不修改请留空"/>
</div>
<div class="form-group text-center">
<input type="submit" name="submit" value="保存" class="btn btn-success btn-block"/>
</div>
</form>
</div>
{/if}
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js"></script>
<script>
$(document).ready(function(){
var items = $("select[default]");
for (i = 0; i < items.length; i++) {
$(items[i]).val($(items[i]).attr("default")||0);
}
$("#testbturl").click(function(){
var bt_url = $("input[name=bt_url]").val();
var bt_key = $("input[name=bt_key]").val();
if(bt_url == ''){
layer.alert('宝塔面板URL不能为空');return;
}
if(bt_url.indexOf('http://')==-1 && bt_url.indexOf('https://')==-1){
layer.alert('宝塔面板URL不正确');return;
}
if(bt_key == ''){
layer.alert('宝塔面板接口密钥不能为空');return;
}
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/admin/testbturl',
data : {bt_url:bt_url, bt_key:bt_key},
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.msg(data.msg, {icon: 1, time:1000})
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
})
})
function saveSetting(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/admin/set',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('设置保存成功!', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
function saveAccount(obj){
var ii = layer.load(2, {shade:[0.1,'#fff']});
$.ajax({
type : 'POST',
url : '/admin/setaccount',
data : $(obj).serialize(),
dataType : 'json',
success : function(data) {
layer.close(ii);
if(data.code == 0){
layer.alert('管理账号保存成功!请重新登录。', {
icon: 1,
closeBtn: false
}, function(){
window.location.reload()
});
}else{
layer.alert(data.msg, {icon: 2})
}
},
error:function(data){
layer.close(ii);
layer.msg('服务器错误');
}
});
return false;
}
</script>
{/block}

60
app/view/dispatch_jump.html

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>温馨提示</title>
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<meta name="renderer" content="webkit"/>
<style type="text/css">
*{box-sizing:border-box;margin:0;padding:0;font-family:Lantinghei SC,Open Sans,Arial,Hiragino Sans GB,Microsoft YaHei,"微软雅黑",STHeiti,WenQuanYi Micro Hei,SimSun,sans-serif;-webkit-font-smoothing:antialiased}
body{padding:70px 0;background:#edf1f4;font-weight:400;font-size:1pc;-webkit-text-size-adjust:none;color:#333}
a{outline:0;color:#3498db;text-decoration:none;cursor:pointer}
.system-message{margin:20px 5%;padding:40px 20px;background:#fff;box-shadow:1px 1px 1px hsla(0,0%,39%,.1);text-align:center}
.system-message h1{margin:0;margin-bottom:9pt;color:#444;font-weight:400;font-size:40px}
.system-message .jump,.system-message .image{margin:20px 0;padding:0;padding:10px 0;font-weight:400}
.system-message .jump{font-size:14px}
.system-message .jump a{color:#333}
.system-message p{font-size:9pt;line-height:20px}
.system-message .btn{display:inline-block;margin-right:10px;width:138px;height:2pc;border:1px solid #44a0e8;border-radius:30px;color:#44a0e8;text-align:center;font-size:1pc;line-height:2pc;margin-bottom:5px;}
.success .btn{border-color:#69bf4e;color:#69bf4e}
.error .btn{border-color:#ff8992;color:#ff8992}
.info .btn{border-color:#3498db;color:#3498db}
.copyright p{width:100%;color:#919191;text-align:center;font-size:10px}
.system-message .btn-grey{border-color:#bbb;color:#bbb}
.clearfix:after{clear:both;display:block;visibility:hidden;height:0;content:"."}
@media (max-width:768px){body {padding:20px 0;}}
@media (max-width:480px){.system-message h1{font-size:30px;}}
</style>
</head>
<body>
<div class="system-message {$code}">
<div class="image">
<img src="/static/images/{$code}.svg" alt="" width="150" />
</div>
<h1>{$msg}</h1>
{if $url}
<p class="jump">
页面将在 <span id="wait">{$wait}</span> 秒后自动跳转
</p>
{/if}
<p class="clearfix">
<a href="javascript:history.go(-1);" class="btn btn-grey">返回上一页</a>
{if $url}
<a href="{$url}" class="btn btn-primary">立即跳转</a>
{/if}
</p>
</div>
<script type="text/javascript">
(function () {
var wait = document.getElementById('wait');
var interval = setInterval(function () {
var time = --wait.innerHTML;
if (time <= 0) {
location.href = "{$url}";
clearInterval(interval);
}
}, 1000);
})();
</script>
</body>
</html>

159
app/view/index/download.html

@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="renderer" content="webkit" />
<meta name="force-rendering" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>宝塔面板安装脚本</title>
<link rel="stylesheet" type="text/css" href="/static/css/sanren.css" />
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
<link rel="stylesheet" type="text/css" href="/static/css/download.css" />
</head>
<body>
<div class="down-main">
<div class="d1">
<div class="wrap">
<div class="i1t textcenter">
<h1 class="disflex flex_center flex_lrcenter textcenter">宝塔面板安装脚本<img class="ml10" src="/static/images/i1ico_03.png"></h1>
<div class="text20 mt_25 wap_mt15 textcenter cl8">
<p>2分钟装好面板,一键管理服务器</p>
<p>集成LAMP/LNMP环境安装,网站、FTP、数据库、文件管理、软件安装等功能</p>
</div>
<div class="mt_30 i1ta wap_mt15">
<a href="https://demo.bt.cn" target="_block" rel="noreferrer">查看演示</a>
<a href="javascript:;" id="goInstallLinux">
<img class="middle mr10" src="/static/images/i1aico_03.png">
立即免费安装
</a>
</div>
</div>
</div>
</div>
<div class="d2" id="instal-linux">
<div class="wrap">
<div class="wrap-title linux-title">
<div class="text">使用此云端的宝塔面板(版本:{:config_get('new_version')})</div>
</div>
<div class="desc">
使用 SSH 连接工具,如
<a class="link" href="https://www.putty.org/" target="_blank" rel="noreferrer">PUTTY</a>
连接到您的 Linux 服务器后,
<a class="link" href="https://www.bt.cn/bbs/thread-50002-1-1.html" target="_blank" rel="noreferrer">挂载磁盘</a>
,根据系统执行相应命令开始安装:
</div>
<div class="install-code">
<span class="osname">一键安装脚本</span>
<div class="code-cont">
<div class="command" title="点击复制一键安装脚本">wget -O install.sh {$siteurl}/install/install_6.0.sh && sh install.sh</div>
<span class="ico-copy" title="点击复制一键安装脚本" data-clipboard-text="wget -O install.sh {$siteurl}/install/install_6.0.sh && sh install.sh">复制</span>
</div>
</div>
<div class="install-code">
<span class="osname">一键更新脚本</span>
<div class="code-cont">
<div class="command" title="点击复制一键更新脚本">curl {$siteurl}/install/update6.sh|bash</div>
<span class="ico-copy" title="点击复制一键更新脚本" data-clipboard-text="curl {$siteurl}/install/update6.sh|bash">复制</span>
</div>
</div>
<div class="tips" style="color: orangered; font-weight: 700">
<p>注意:必须为没装过其它环境如Apache/Nginx/php/MySQL的新系统,推荐使用centos 7.X的系统安装宝塔面板</p>
<p style="text-indent: 3em">推荐使用Chrome、火狐、edge浏览器,国产浏览器请使用极速模式访问面板登录地址</p>
<p style="text-indent: 3em">如果使用过官方版或其他第三方云端的版本,使用一键更新脚本即可切换到此云端</p>
</div>
</div>
</div>
<div class="d4" id="instal-cloud">
<div class="wrap">
<div class="wrap-title">
<div class="text">更新日志</div>
</div>
<div class="desc">
<p>宝塔Linux面板更新到{:config_get('new_version')}</p>
</div>
</div>
</div>
<div class="animate-bg"></div>
</div>
<div class="foot">
<div class="wrap">
<div class="fb textcenter">
<div class="fb1 textcenter">
<a href="http://www.bt.cn/new/agreement_open.html" target="_blank" rel="noreferrer">《开源许可协议》</a>
<i></i>
<a href="http://www.bt.cn/new/agreement_user.html" target="_blank" rel="noreferrer">《用户协议》</a>
<i></i>
<a href="http://www.bt.cn/new/agreement_privacy.html" target="_blank" rel="noreferrer">《隐私声明》</a>
</div>
<div class="fb2 mt_15">
<p>
Copyright © 2022 宝塔面板安装脚本
</p>
</div>
</div>
</div>
</div>
<script src="//cdn.staticfile.org/jquery/3.6.0/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="//cdn.staticfile.org/layer/3.5.1/layer.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" src="//cdn.staticfile.org/clipboard.js/1.7.1/clipboard.min.js"></script>
<script type="text/javascript" src="/static/js/dx.js"></script>
<script>
$(function () {
var userId = '';
// 复制安装命令
var clipboard = new Clipboard('.ico-copy', {
text: function (element) {
return $(element).prev().text();
},
});
clipboard
.on('success', function (e) {
layer.msg(e.trigger.title + '成功', { icon: 1 });
})
.on('error', function (e) {
layer.msg('复制失败,请手动选中文本Ctrl+c复制内容', { icon: 2 });
});
$('.install-code .command').click(function () {
$(this).next('.ico-copy').click();
});
$('#goInstallLinux').click(function () {
scrollTop('#instal-linux');
});
$('#goOnlineInstall').click(function () {
scrollTop('#online-instal');
});
$('#goInstallCloud').click(function () {
scrollTop('#instal-cloud');
});
function GetRequest() {
var url = location.search;
//获取url中"?"符后的字串
var theRequest = new Object();
if (url.indexOf('?') != -1) {
var str = url.substr(1);
}
return str;
}
if (GetRequest() == 'bt') {
scrollTop('#instal-linux');
}
// 滚动到指定位置
function scrollTop(el) {
var headHeight = 0;
$('html, body').animate({ scrollTop: $(el).offset().top - headHeight }, { duration: 200, easing: 'swing' });
}
});
</script>
</body>
</html>

41
composer.json

@ -0,0 +1,41 @@
{
"name": "btpanel/cloud",
"description": "BTPanel third cloud site",
"type": "project",
"keywords": [
"btpanel",
"baota",
"aapanel"
],
"homepage": "https://www.bt.cn/",
"license": "Apache-2.0",
"authors": [],
"require": {
"php": ">=7.2.5",
"topthink/framework": "^6.0.0",
"topthink/think-orm": "^2.0",
"topthink/think-view": "^1.0",
"topthink/think-captcha": "^3.0"
},
"require-dev": {
"symfony/var-dumper": "^4.2",
"topthink/think-trace":"^1.0"
},
"autoload": {
"psr-4": {
"app\\": "app"
},
"psr-0": {
"": "extend/"
}
},
"config": {
"preferred-install": "dist"
},
"scripts": {
"post-autoload-dump": [
"@php think service:discover",
"@php think vendor:publish"
]
}
}

32
config/app.php

@ -0,0 +1,32 @@
<?php
// +----------------------------------------------------------------------
// | 应用设置
// +----------------------------------------------------------------------
return [
// 应用地址
'app_host' => env('app.host', ''),
// 应用的命名空间
'app_namespace' => '',
// 是否启用路由
'with_route' => true,
// 默认应用
'default_app' => 'index',
// 默认时区
'default_timezone' => 'Asia/Shanghai',
// 应用映射(自动多应用模式有效)
'app_map' => [],
// 域名绑定(自动多应用模式有效)
'domain_bind' => [],
// 禁止URL访问的应用列表(自动多应用模式有效)
'deny_app_list' => [],
// 异常页面的模板文件
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
// 错误显示信息,非调试模式有效
'error_message' => '页面错误!请稍后再试~',
// 显示错误信息
'show_error_msg' => true,
];

40
config/cache.php

@ -0,0 +1,40 @@
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
'default' => env('cache.driver', 'file'),
// 缓存连接方式配置
'stores' => [
'file' => [
// 驱动方式
'type' => 'File',
// 缓存保存目录
'path' => '',
// 缓存前缀
'prefix' => '',
// 缓存有效期 0表示永久缓存
'expire' => 0,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 序列化机制 例如 ['serialize', 'unserialize']
'serialize' => [],
],
'redis' => [
// 驱动方式
'type' => 'Redis',
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'select' => 0,
// 缓存有效期 0表示永久缓存
'expire' => 3600,
'prefix' => '',
],
// 更多的缓存连接
],
];

39
config/captcha.php

@ -0,0 +1,39 @@
<?php
// +----------------------------------------------------------------------
// | Captcha配置文件
// +----------------------------------------------------------------------
return [
//验证码位数
'length' => 4,
// 验证码字符集合
'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
// 验证码过期时间
'expire' => 1800,
// 是否使用中文验证码
'useZh' => false,
// 是否使用算术验证码
'math' => false,
// 是否使用背景图
'useImgBg' => false,
//验证码字符大小
'fontSize' => 25,
// 是否使用混淆曲线
'useCurve' => true,
//是否添加杂点
'useNoise' => true,
// 验证码字体 不设置则随机
'fontttf' => '',
//背景颜色
'bg' => [243, 251, 254],
// 验证码图片高度
'imageH' => 0,
// 验证码图片宽度
'imageW' => 0,
// 添加额外的验证码设置
// verify => [
// 'length'=>4,
// ...
//],
];

10
config/console.php

@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
// 指令定义
'commands' => [
'updateall' => 'app\command\UpdateAll',
],
];

20
config/cookie.php

@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
// samesite 设置,支持 'strict' 'lax'
'samesite' => '',
];

63
config/database.php

@ -0,0 +1,63 @@
<?php
return [
// 默认使用的数据库连接配置
'default' => env('database.driver', 'mysql'),
// 自定义时间查询规则
'time_query_rule' => [],
// 自动写入时间戳字段
// true为自动识别类型 false关闭
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
'auto_timestamp' => true,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 时间字段配置 配置格式:create_time,update_time
'datetime_field' => '',
// 数据库连接配置信息
'connections' => [
'mysql' => [
// 数据库类型
'type' => env('database.type', 'mysql'),
// 服务器地址
'hostname' => env('database.hostname', '127.0.0.1'),
// 数据库名
'database' => env('database.database', ''),
// 用户名
'username' => env('database.username', 'root'),
// 密码
'password' => env('database.password', ''),
// 端口
'hostport' => env('database.hostport', '3306'),
// 数据库连接参数
'params' => [],
// 数据库编码默认采用utf8
'charset' => env('database.charset', 'utf8mb4'),
// 数据库表前缀
'prefix' => env('database.prefix', ''),
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
// 监听SQL
'trigger_sql' => env('app_debug', true),
// 开启字段缓存
'fields_cache' => false,
],
// 更多的数据库配置信息
],
];

24
config/filesystem.php

@ -0,0 +1,24 @@
<?php
return [
// 默认磁盘
'default' => env('filesystem.driver', 'local'),
// 磁盘列表
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRuntimePath() . 'storage',
],
'public' => [
// 磁盘类型
'type' => 'local',
// 磁盘路径
'root' => app()->getRootPath() . 'public/storage',
// 磁盘路径对应的外部URL路径
'url' => '/storage',
// 可见性
'visibility' => 'public',
],
// 更多的磁盘配置信息
],
];

27
config/lang.php

@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | 多语言设置
// +----------------------------------------------------------------------
return [
// 默认语言
'default_lang' => env('lang.default_lang', 'zh-cn'),
// 允许的语言列表
'allow_lang_list' => [],
// 多语言自动侦测变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 多语言header变量
'header_var' => 'think-lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => false,
];

45
config/log.php

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 日志设置
// +----------------------------------------------------------------------
return [
// 默认日志记录通道
'default' => env('log.channel', 'file'),
// 日志记录级别
'level' => [],
// 日志类型记录的通道 ['error'=>'email',...]
'type_channel' => [],
// 关闭全局日志写入
'close' => false,
// 全局日志处理 支持闭包
'processor' => null,
// 日志通道列表
'channels' => [
'file' => [
// 日志记录方式
'type' => 'File',
// 日志保存目录
'path' => '',
// 单文件日志写入
'single' => false,
// 独立日志级别
'apart_level' => [],
// 最大日志文件数量
'max_files' => 0,
// 使用JSON格式记录
'json' => false,
// 日志处理
'processor' => null,
// 关闭通道日志写入
'close' => false,
// 日志输出格式化
'format' => '[%s][%s] %s',
// 是否实时写入
'realtime_write' => false,
],
// 其它日志通道配置
],
];

8
config/middleware.php

@ -0,0 +1,8 @@
<?php
// 中间件配置
return [
// 别名或分组
'alias' => [],
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
'priority' => [],
];

45
config/route.php

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 路由设置
// +----------------------------------------------------------------------
return [
// pathinfo分隔符
'pathinfo_depr' => '/',
// URL伪静态后缀
'url_html_suffix' => '',
// URL普通方式参数 用于自动生成
'url_common_param' => true,
// 是否开启路由延迟解析
'url_lazy_route' => false,
// 是否强制使用路由
'url_route_must' => true,
// 合并路由规则
'route_rule_merge' => false,
// 路由是否完全匹配
'route_complete_match' => false,
// 访问控制器层名称
'controller_layer' => 'controller',
// 空控制器名
'empty_controller' => 'Error',
// 是否使用控制器后缀
'controller_suffix' => false,
// 默认的路由变量规则
'default_route_pattern' => '[\w\.]+',
// 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
'request_cache_key' => false,
// 请求缓存有效期
'request_cache_expire' => null,
// 全局请求缓存排除规则
'request_cache_except' => [],
// 默认控制器名
'default_controller' => 'Index',
// 默认操作名
'default_action' => 'index',
// 操作方法后缀
'action_suffix' => '',
// 默认JSONP格式返回的处理方法
'default_jsonp_handler' => 'jsonpReturn',
// 默认JSONP处理方法
'var_jsonp_handler' => 'callback',
];

19
config/session.php

@ -0,0 +1,19 @@
<?php
// +----------------------------------------------------------------------
// | 会话设置
// +----------------------------------------------------------------------
return [
// session name
'name' => 'PHPSESSID',
// SESSION_ID的提交变量,解决flash上传跨域
'var_session_id' => '',
// 驱动方式 支持file cache
'type' => 'file',
// 存储连接标识 当type使用cache的时候有效
'store' => null,
// 过期时间
'expire' => 1440,
// 前缀
'prefix' => '',
];

10
config/trace.php

@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | Trace设置 开启调试模式后有效
// +----------------------------------------------------------------------
return [
// 内置Html和Console两种方式 支持扩展
'type' => 'Html',
// 读取的日志通道名
'channel' => '',
];

25
config/view.php

@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Think
'type' => 'Think',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => 'html',
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
];

1
data/config/plugin_list.json
File diff suppressed because it is too large
View File

BIN
data/plugins/other/image/20190401/169f9ef390f68c134c4b8b003ec1a412.png

After

Width: 48  |  Height: 40  |  Size: 2.8 KiB

BIN
data/plugins/other/image/20190416/ac0d8aa620c481be425ea5a008e33480.png

After

Width: 48  |  Height: 40  |  Size: 2.9 KiB

BIN
data/plugins/other/image/20190420/0237badbc85457149fc8772e11c39070.png

After

Width: 40  |  Height: 40  |  Size: 2.5 KiB

BIN
data/plugins/other/image/20190529/b7e5b51c52bc2ead12b338197456916e.png

After

Width: 48  |  Height: 40  |  Size: 1.3 KiB

BIN
data/plugins/other/image/20190615/e7372086b3e573497e612e1003bbe5b3.png

After

Width: 40  |  Height: 40  |  Size: 4.8 KiB

BIN
data/plugins/other/image/20190624/c6bfbb27b99058e14e9e424a4b66104b.png

After

Width: 48  |  Height: 40  |  Size: 21 KiB

BIN
data/plugins/other/image/20190802/cd56d03b758b930a85e127ae24a467ca.png

After

Width: 90  |  Height: 90  |  Size: 3.6 KiB

BIN
data/plugins/other/image/20190808/17e9b99a0a3649deb219968a9172688a.png

BIN
data/plugins/other/image/20190809/3f8f90fde77213080143e63b668d8c77.png

After

Width: 128  |  Height: 128  |  Size: 4.0 KiB

BIN
data/plugins/other/image/20190829/36a4514dda9ebfde9b6fa1c7277c67e8.png

After

Width: 50  |  Height: 50  |  Size: 5.3 KiB

BIN
data/plugins/other/image/20190918/85b94a174957d8c2d078f6e58fef02b8.png

After

Width: 100  |  Height: 100  |  Size: 3.6 KiB

BIN
data/plugins/other/image/20190921/1b28d8321def8455207b6482d78d6534.png

After

Width: 48  |  Height: 48  |  Size: 3.1 KiB

BIN
data/plugins/other/image/20190925/797ff13c9157f656c08a27a1674cbb86.png

After

Width: 720  |  Height: 720  |  Size: 58 KiB

BIN
data/plugins/other/image/20190925/987b50979ba8c2781c62d1ddbe773cef.png

After

Width: 48  |  Height: 48  |  Size: 3.0 KiB

BIN
data/plugins/other/image/20191005/2366e9d746b8e52bfb7f00498dd88d41.png

After

Width: 48  |  Height: 48  |  Size: 598 B

BIN
data/plugins/other/image/20191008/2c48dbb3f2e21fc2eb19d68e0826d162.png

After

Width: 48  |  Height: 48  |  Size: 4.1 KiB

BIN
data/plugins/other/image/20191010/12ae100cb6da09c45ebb64c383f17daa.png

After

Width: 48  |  Height: 48  |  Size: 2.1 KiB

BIN
data/plugins/other/image/20191014/a5a91d5b7503ac6fc40b587c18f884cb.png

After

Width: 48  |  Height: 48  |  Size: 4.1 KiB

BIN
data/plugins/other/image/20191015/3739007955dbad246abd1bc51a3ce9cc.png

After

Width: 40  |  Height: 40  |  Size: 4.8 KiB

BIN
data/plugins/other/image/20191019/57f47dfa77ad9b958fdb0682c44dd2dd.png

After

Width: 40  |  Height: 40  |  Size: 4.8 KiB

BIN
data/plugins/other/image/20191031/3044f0e8898553b27a3f8e3f39b49829.png

After

Width: 40  |  Height: 40  |  Size: 4.8 KiB

BIN
data/plugins/other/image/20191101/ffb9085b6c30168308e289be27f11486.png

After

Width: 49  |  Height: 40  |  Size: 2.1 KiB

BIN
data/plugins/other/image/20191106/1989b486726cdea6a62954fe3b345e3d.png

After

Width: 200  |  Height: 200  |  Size: 2.4 KiB

BIN
data/plugins/other/image/20191107/065808e927f4c37052049b56561fe1cd.png

After

Width: 50  |  Height: 40  |  Size: 2.4 KiB

BIN
data/plugins/other/image/20191108/d74b2c5e165c729f76ca94923835350c.png

After

Width: 48  |  Height: 40  |  Size: 26 KiB

BIN
data/plugins/other/image/20191113/087f6342714ae5d298e8a75b87a9cd1f.png

After

Width: 48  |  Height: 40  |  Size: 22 KiB

BIN
data/plugins/other/image/20191115/a6f31569f41a4a331d0a7d778b3972ab.png

After

Width: 58  |  Height: 40  |  Size: 630 B

BIN
data/plugins/other/image/20191118/331aa690f39edc9f15f64045294e853e.png

After

Width: 24  |  Height: 20  |  Size: 22 KiB

BIN
data/plugins/other/image/20191217/e651e53602b61836185802ba6ea261ab.png

After

Width: 250  |  Height: 250  |  Size: 9.8 KiB

BIN
data/plugins/other/image/20191230/0596328af186e6272f09889cfcca5843.png

After

Width: 48  |  Height: 48  |  Size: 3.0 KiB

BIN
data/plugins/other/image/20200106/00e5f4d74d5e8acb10a5b4b279c0bad0.png

BIN
data/plugins/other/image/20200106/d67109fd986b85ae6291515a7d6b0a61.png

After

Width: 32  |  Height: 32  |  Size: 19 KiB

BIN
data/plugins/other/image/20200108/fcfe197c6ddf9cc3f36629f72c1285d7.png

After

Width: 60  |  Height: 57  |  Size: 6.3 KiB

BIN
data/plugins/other/image/20200120/b388d6ebc2fe5dc1b568663db38b2d49.png

After

Width: 48  |  Height: 39  |  Size: 1.2 KiB

BIN
data/plugins/other/image/20200129/2588534a9f75e23127bd0e882ae1690f.png

After

Width: 112  |  Height: 109  |  Size: 1.3 KiB

BIN
data/plugins/other/image/20200210/50e8a3b77777038f72a3b1467ddca264.png

After

Width: 128  |  Height: 128  |  Size: 3.3 KiB

BIN
data/plugins/other/image/20200218/ba006d057f070951a7b1cbe75e50ec65.png

After

Width: 200  |  Height: 200  |  Size: 3.1 KiB

BIN
data/plugins/other/image/20200315/d92c6611e3678db4bf3f85e0cc9d37cf.png

After

Width: 48  |  Height: 48  |  Size: 2.6 KiB

BIN
data/plugins/other/image/20200315/eea7e9c23a22857b685ac7a0302a542e.png

After

Width: 32  |  Height: 32  |  Size: 212 B

BIN
data/plugins/other/image/20200325/06a8123060338addc6804259a2de83cf.png

After

Width: 200  |  Height: 200  |  Size: 8.2 KiB

BIN
data/plugins/other/image/20200325/4f20682fd226e85ea0e7e8618a4697f6.png

After

Width: 300  |  Height: 300  |  Size: 4.5 KiB

BIN
data/plugins/other/image/20200326/6f437e4f013b783cac17842f2b79478b.png

After

Width: 100  |  Height: 100  |  Size: 917 B

BIN
data/plugins/other/image/20200329/40d01d19b0c2bff6acd5b2c02931d39a.png

After

Width: 32  |  Height: 32  |  Size: 971 B

BIN
data/plugins/other/image/20200330/a558018b235958e380eb27385d296672.png

After

Width: 200  |  Height: 200  |  Size: 4.3 KiB

BIN
data/plugins/other/image/20200401/5ab52b348d46d2fd21fd6af5bd987955.png

After

Width: 32  |  Height: 32  |  Size: 595 B

BIN
data/plugins/other/image/20200407/fea4f518a447cde5cbc5c6df1e1f434d.png

BIN
data/plugins/other/image/20200410/891111b223fb30cb065d6a29410c6d70.png

BIN
data/plugins/other/image/20200427/44047cab050e6d1a4df7ee1c935cd8a2.png

After

Width: 128  |  Height: 128  |  Size: 6.1 KiB

BIN
data/plugins/other/image/20200427/c053de465ca87c22cb8b4518d5a0425c.png

After

Width: 40  |  Height: 40  |  Size: 4.9 KiB

BIN
data/plugins/other/image/20200502/b5f71be748935aedb7d2211ead1da3d6.png

After

Width: 152  |  Height: 153  |  Size: 4.1 KiB

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

Loading…
Cancel
Save