首次推送
This commit is contained in:
779776787
2025-08-12 14:19:34 +08:00
parent cff0114a8d
commit efca70ecb2
494 changed files with 340297 additions and 2 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* --------------------------------------------------------------------
* @description 项目的基础控制器,负责处理所有控制器的基础逻辑。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
defined('IN_APP') or die('Direct access is not allowed.');
class BaseController extends Controller {
public function __construct(){
parent::__construct();
$this->checkAuth();
}
final protected function checkAuth(){
if (C('NEED_LOGIN') !== true) return;
$controllerName = C('CONTROLLER_NAME');
$ACTION_NAME = C('ACTION_NAME');
$islogined = !empty($_SESSION['user_authenticated']);
if (!$islogined) {
if($controllerName !== 'Login'){
return $this->redirect('index.php/Login');
}
}
}
}
?>
+120
View File
@@ -0,0 +1,120 @@
<?php
/**
* --------------------------------------------------------------------
* @description 项目的编辑器控制器,负责显示和处理规则编辑器。
* 该控制器处理规则文件的加载、编辑和保存功能。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
defined('IN_APP') or die('Direct access is not allowed.');
/**
* 负责显示和处理规则编辑器的控制器
*/
class EditController extends BaseController {
/**
* 默认方法,根据文件类型显示不同的编辑器
* 访问URL: index.php/Edit?file=...
*/
public function indexAction(){
$file_content = '""';
$file_path_for_js = '""';
$full_path = '';
$requested_file = '';
if (isset($_GET['file'])) {
$base_dir = realpath(ROOT_PATH . '/box');
$requested_file = str_replace(['../', '..\\'], '', $_GET['file']);
$full_path = realpath($base_dir . '/' . $requested_file);
if ($full_path && strpos($full_path, $base_dir) === 0 && file_exists($full_path)) {
$content = file_get_contents($full_path);
$data = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE && is_object($data) && isset($data->spider) && is_string($data->spider)) {
$spiderParts = explode(';md5;', $data->spider);
$jarRelativePath = $spiderParts[0];
$jsonDir = dirname($full_path);
$jarAbsolutePath = realpath($jsonDir . '/' . $jarRelativePath);
if ($jarAbsolutePath && is_readable($jarAbsolutePath)) {
$md5 = md5_file($jarAbsolutePath);
$data->spider = $jarRelativePath . ';md5;' . $md5;
$content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}
$file_content = json_encode($content);
$file_path_for_js = json_encode($requested_file);
} else {
$error_message = '错误:文件未找到或路径无效。路径: ' . htmlspecialchars($requested_file);
$file_content = json_encode($error_message);
}
} else {
$file_content = json_encode('错误:未指定要编辑的文件。');
}
$this->assign('file_content_for_js', $file_content);
$this->assign('file_path_for_js', $file_path_for_js);
$this->assign('file_path', $full_path ? formatPathForVSCode($full_path) : '');
$file_extension = strtolower(pathinfo($requested_file, PATHINFO_EXTENSION));
$api = $_GET['api'] ?? '';
if ($api === 'csp_XYQHiker'){
return $this->display('Edit/XYQHiker');
}
if (in_array($file_extension, ['json', 'js', 'py', 'php']) || $api === 'editor') {
$this->assign('file_extension', $file_extension);
return $this->display('Edit/editor');
}
$this->display('Edit/index');
}
/**
* 处理文件保存请求
* 访问URL: index.php/Edit/save
*/
public function saveAction() {
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => '无效的请求方法']);
return;
}
$filePath = $_POST['filePath'] ?? null;
$fileContent = $_POST['fileContent'] ?? null;
if (!$filePath || $fileContent === null) {
echo json_encode(['success' => false, 'message' => '文件路径或内容不能为空']);
return;
}
$baseDir = realpath(ROOT_PATH . '/box');
$sanitizedPath = str_replace(['../', '..\\'], '', $filePath);
$targetPath = $baseDir . DIRECTORY_SEPARATOR . $sanitizedPath;
if (strpos(realpath(dirname($targetPath)), $baseDir) !== 0) {
echo json_encode(['success' => false, 'message' => '错误:禁止访问此路径']);
return;
}
try {
$result = file_put_contents($targetPath, $fileContent);
if ($result === false) {
throw new Exception('无法写入文件,请检查服务器上 /box 目录及其子文件的写入权限。');
}
echo json_encode(['success' => true, 'message' => '文件 ' . htmlspecialchars(basename($targetPath)) . ' 保存成功!']);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '保存失败:' . $e->getMessage()]);
}
}
}
?>
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* --------------------------------------------------------------------
* @description 项目的首页控制器,负责处理首页的显示逻辑。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
defined('IN_APP') or die('Direct access is not allowed.');
class IndexController extends BaseController {
public function indexAction(){
$this->display('Index/index');
}
}
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* --------------------------------------------------------------------
* @description 项目的登录控制器,负责处理用户登录和登出逻辑。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
defined('IN_APP') or die('Direct access is not allowed.');
class LoginController extends BaseController {
public function indexAction(){
$this->display('Login/index');
}
public function doLoginAction(){
$password = $_POST['password'] ?? '';
$correct_password = C('PASSWORD');
if (!empty($password) && $password === $correct_password) {
$_SESSION['user_authenticated'] = true;
$this->redirect('/');
} else {
$this->assign('error', '密码错误,请重试。');
$this->display('Login/index');
}
}
public function logoutAction(){
session_destroy();
$this->redirect('/');
}
}
?>
+197
View File
@@ -0,0 +1,197 @@
<?php
/**
* --------------------------------------------------------------------
* @description 项目的核心控制器,负责处理远程URL的代理加载、文件列表、文件检查和配置保存等功能。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
defined('IN_APP') or die('Direct access is not allowed.');
class ProxyController extends BaseController {
private $baseSaveDir = './box/';
private $cacheDir = './cache/';
private $cacheTtl = 3600;
/**
* 构造函数,确保目录存在
*/
public function __construct(){
parent::__construct();
if (!is_dir($this->baseSaveDir)) mkdir($this->baseSaveDir, 0755, true);
if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
}
/**
* 代理远程URL加载 (默认Action)
* 访问URL: index.php/Proxy/load?target_url=...
* (已优化本地文件加载逻辑,可自动计算spider的MD5)
*/
public function loadAction() {
if (!isset($_GET['target_url'])) {
$this->ajaxReturn(['error' => '缺少目标URL (target_url) 参数']);
}
$targetUrl = $_GET['target_url'];
$serverHost = $_SERVER['HTTP_HOST'];
if (strpos($targetUrl, $serverHost) !== false && strpos($targetUrl, '/box/') !== false) {
$urlParts = parse_url($targetUrl);
$localPath = realpath(ROOT_PATH . $urlParts['path']);
if ($localPath && file_exists($localPath) && strpos($localPath, realpath(ROOT_PATH)) === 0) {
header('Content-Type: application/json; charset=utf-8');
$content = file_get_contents($localPath);
$data = json_decode($content);
if (json_last_error() === JSON_ERROR_NONE && is_object($data) && isset($data->spider) && is_string($data->spider)) {
$spiderParts = explode(';md5;', $data->spider);
$jarRelativePath = $spiderParts[0];
$jsonDir = dirname($localPath);
$jarAbsolutePath = realpath($jsonDir . '/' . $jarRelativePath);
if ($jarAbsolutePath && is_readable($jarAbsolutePath)) {
$md5 = md5_file($jarAbsolutePath);
$data->spider = $jarRelativePath . ';md5;' . $md5;
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} else {
$data->spider = $jarRelativePath;
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
} else {
echo $content;
}
exit;
} else {
$this->ajaxReturn(['error' => '本地文件未找到或路径无效']);
}
}
if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
$cacheHash = md5($targetUrl);
$cacheFilePath = $this->cacheDir . $cacheHash . '.cache';
if (file_exists($cacheFilePath) && (time() - filemtime($cacheFilePath) < $this->cacheTtl)) {
header('Content-Type: application/json; charset=utf-8');
echo file_get_contents($cacheFilePath);
exit;
}
$result = httpCurl(['url' => $targetUrl]);
if (isset($result['error'])) {
$this->ajaxReturn(['error' => 'cURL 请求失败: ' . $result['error']]);
} else {
$httpCode = $result['info']['http_code'];
$contentType = $result['info']['content_type'];
if ($httpCode >= 200 && $httpCode < 400) {
file_put_contents($cacheFilePath, $result['body']);
if ($contentType) header('Content-Type: ' . $contentType);
else header('Content-Type: application/json; charset=utf-8');
echo $result['body'];
exit;
} else {
http_response_code($httpCode);
echo "无法从目标服务器获取内容。服务器返回错误: HTTP " . $httpCode;
exit;
}
}
}
/**
* 列出文件
* 访问URL: index.php/Proxy/listFiles
*/
public function listFilesAction() {
if (!is_dir($this->baseSaveDir)) {
$this->ajaxReturn([]);
}
function scan_directory_recursive($dir, $baseDir) {
$result = [];
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$path = $dir . '/' . $item;
$relativePath = str_replace($baseDir, '', $path);
if (is_dir($path)) {
$result[] = ['name' => $item, 'type' => 'dir', 'path' => ltrim($relativePath, '/'), 'children' => scan_directory_recursive($path, $baseDir)];
} else {
$result[] = ['name' => $item, 'type' => 'file', 'path' => ltrim($relativePath, '/')];
}
}
return $result;
}
$fileTree = scan_directory_recursive(rtrim($this->baseSaveDir, '/'), rtrim($this->baseSaveDir, '/') . '/');
$this->ajaxReturn($fileTree);
}
/**
* 检查文件是否存在
* 访问URL: index.php/Proxy/checkFileExists?path=...
*/
public function checkFileExistsAction(){
$filePath = isset($_GET['path']) ? sanitize_path($_GET['path']) : '';
$fullPath = $this->baseSaveDir . $filePath;
$this->ajaxReturn(['exists' => file_exists($fullPath), 'path' => $filePath]);
}
/**
* 保存配置文件
* 访问URL: index.php/Proxy/saveConfig (POST请求)
*/
public function saveConfigAction() {
if (!isset($_POST['dir'], $_POST['filename'], $_POST['content'])) {
$this->ajaxReturn(['success' => false, 'message' => '缺少保存配置的参数']);
}
$targetDir = $this->baseSaveDir . sanitize_path($_POST['dir']) . '/';
$filename = sanitize_path($_POST['filename']);
if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
if (file_put_contents($targetDir . $filename, $_POST['content']) !== false) {
$this->ajaxReturn(['success' => true, 'message' => '配置文件保存成功']);
} else {
$this->ajaxReturn(['success' => false, 'message' => '配置文件写入失败,请检查目录权限']);
}
}
/**
* 下载资源文件
* 访问URL: index.php/Proxy/downloadAsset (POST请求)
*/
public function downloadAssetAction() {
if (!isset($_POST['source_url'], $_POST['target_dir'], $_POST['relative_path'])) {
$this->ajaxReturn(['success' => false, 'message' => '缺少下载资源的参数']);
}
$sourceUrl = $_POST['source_url'];
$targetDir = sanitize_path($_POST['target_dir']);
$relativePath = sanitize_path($_POST['relative_path']);
$localFullPath = $this->baseSaveDir . $targetDir . '/' . $relativePath;
$localDir = dirname($localFullPath);
if (!is_dir($localDir)) {
if (!mkdir($localDir, 0755, true)) {
$this->ajaxReturn(['success' => false, 'message' => '创建目录失败: ' . $localDir]);
}
}
$result = httpCurl(['url' => $sourceUrl]);
if (isset($result['error'])) {
$this->ajaxReturn(['success' => false, 'message' => 'cURL下载失败: ' . $result['error']]);
} else {
if (file_put_contents($localFullPath, $result['body']) !== false) {
$this->ajaxReturn(['success' => true, 'message' => '资源下载成功: ' . $localFullPath]);
} else {
$this->ajaxReturn(['success' => false, 'message' => '文件写入本地失败']);
}
}
}
}
+1
View File
@@ -0,0 +1 @@
模板开发中....
+1
View File
@@ -0,0 +1 @@
模板开发中....
+198
View File
@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TVbox规则编辑器 - <?php echo isset($_GET['file']) ? htmlspecialchars($_GET['file']) : ''; ?></title>
<link rel="stylesheet" href="/assets/css/ui.css?t=<?php echo time();?>">
<link rel="stylesheet" href="/assets/css/main.css?t=<?php echo time();?>">
</head>
<body>
<div class="container">
<div class="header-container">
<div class="file-info">
<div class="file-path">
<span class="file-icon">📄</span>
<span class="file-name"><?php echo isset($_GET['file']) ? htmlspecialchars(basename($_GET['file'])) : '未选择文件'; ?></span>
</div>
<?php if(isset($_GET['file'])): ?>
<div class="full-path"><?php echo htmlspecialchars($_GET['file']); ?></div>
<?php endif; ?>
</div>
<div class="header-actions">
<div class="btn-group">
<!-- <button id="advancedModeBtn" class="btn warning-btn">普通模式</button> -->
<button id="saveBtn" class="btn primary-btn">保存修改</button>
<!-- <button id="helpBtn" class="btn secondary-btn">语法帮助</button> -->
<button id="editBtn" class="btn secondary-btn">在线编辑</button>
<button id="variableBtn" class="btn secondary-btn">变量设置</button>
<button id="autoTestBtn" class="btn primary-btn">一键测试</button>
</div>
</div>
</div>
<form id="ruleForm">
<div class="tabs">
<div class="tab-btn active" onclick="openTab(event, 'basic')">基础信息</div>
<div class="tab-btn" onclick="openTab(event, 'home')">首页规则</div>
<div class="tab-btn" onclick="openTab(event, 'category')">分类规则</div>
<div class="tab-btn" onclick="openTab(event, 'detail')">详情规则</div>
<div class="tab-btn" onclick="openTab(event, 'play')">播放规则</div>
<div class="tab-btn" onclick="openTab(event, 'search')">搜索规则</div>
</div>
<div id="basic" class="tab-content active">基础内容</div>
<div id="home" class="tab-content">首页内容</div>
<div id="category" class="tab-content">
<div class="tabs">
<div class="tab-btn active" onclick="openTab(event, 'category-rules-basic')">分类规则</div>
<div class="tab-btn" onclick="openTab(event, 'category-filter-menu')">筛选菜单</div>
</div>
<div id="category-rules-basic" class="tab-content active">分类规则内容</div>
<div id="category-filter-menu" class="tab-content">筛选菜单内容</div>
</div>
<div id="detail" class="tab-content">详情内容</div>
<div id="play" class="tab-content">播放内容</div>
<div id="search" class="tab-content">搜索内容</div>
</form>
<div class="toast-container"></div>
<script id="test-modal-template" type="text/x-handlebars-template">
<div class="form-group">
<label for="testUrl">测试URL</label>
<div class="input-with-buttons">
<input type="text" id="testUrl" placeholder="输入要测试的网页URL">
<button type="button" id="toggleSourceBtn" class="btn secondary-btn">源码</button>
</div>
</div>
<textarea id="sourceHtmlInput" placeholder="当这里不为空时,将优先使用此处源码进行测试" style="display:none; width:100%; min-height:80px; margin-top: -10px; margin-bottom: 10px;"></textarea>
<div class="form-group">
<label for="testSelectorInput">CSS选择器</label>
<div class="input-with-buttons">
<input type="text" id="testSelectorInput">
<button id="applySelectorBtn" class="btn primary-btn">应用</button>
</div>
</div>
<div class="test-result-container" style="display:none;">
<div class="controls-container" style="justify-content:space-between; margin-bottom:10px;">
<h4 style="margin:0; font-size: 1em;">测试结果:</h4>
<button id="toggleResultModeBtn" class="btn secondary-btn">切换模式</button>
</div>
<div id="testResultContent" style="background:#f8f9fa; border:1px solid #e9ecef; padding:10px; border-radius:6px; min-height: 100px; max-height: 200px; overflow-y:auto;"></div>
</div>
</script>
<script id="variable-modal-template" type="text/x-handlebars-template">
<div id="variableInputs">
{{! The content will be dynamically generated by JS }}
</div>
</script>
<script id="help-modal-template" type="text/x-handlebars-template">
<div class="help-content">
<p>以下是TVbox规则中常用的CSS选择器及语法规则的简要说明:</p>
<h4>1. 选择器分隔符 <code>&&</code></h4>
<ul>
<li><strong>作用:</strong> 用于将多个选择器连接起来,表示在前一个选择器找到的元素内部继续查找。</li>
<li><strong>示例:</strong> <code>.parent&&.child</code> - 先找到所有 class 为 <code>parent</code> 的元素,再在其内部寻找 class 为 <code>child</code> 的子元素。</li>
</ul>
<h4>2. 文本内容筛选 <code>:contains()</code></h4>
<ul>
<li><strong>作用:</strong> 在CSS选择器后使用,用于筛选出包含特定文本内容的元素。</li>
<li><strong>示例:</strong> <code>.sDes:contains(主演:)</code> - 找到所有 class 为 <code>sDes</code> 且内部文本包含“主演:”的元素。</li>
</ul>
<h4>3. 元素索引 <code>,</code></h4>
<ul>
<li><strong>作用:</strong> 在选择器后使用逗号加数字,用于精确选取特定索引位置的元素。索引从0开始。</li>
<li><strong>示例:</strong> <code>.item,1</code> - 选取所有 class 为 <code>item</code> 的元素中的**第二个**元素。</li>
<li><strong>倒数索引:</strong> <code>.item,-1</code> - 选取所有 class 为 <code>item</code> 的元素中的**倒数第一个**元素。</li>
</ul>
<h4>4. 内容提取 <code>Text</code> / <code>Html</code> / 属性名</h4>
<ul>
<li><strong>作用:</strong> 在选择器末尾使用,指定要提取的元素内容类型。</li>
<li><strong><code>&&Text</code>:</strong> 提取元素的纯文本内容,会忽略HTML标签。</li>
<li><strong><code>&&Html</code>:</strong> 提取元素的内部HTML代码,包含子标签。</li>
<li><strong><code>&&属性名</code>:</strong> 提取元素的某个属性值,例如:<code>&&href</code>(链接地址),<code>&&data-src</code>(懒加载图片地址)。</li>
</ul>
<h4>5. 文本删除 <code>!</code></h4>
<ul>
<li><strong>作用:</strong> 在规则末尾使用,用于删除提取到的文本中指定的内容。</li>
<li><strong>示例:</strong> <code>&&Text!主演:</code> - 提取文本后,删除其中的“主演:”这几个字。</li>
</ul>
<h4>6. 高级属性选择器</h4>
<ul>
<li><strong><code>[属性^="值"]</code>:</strong> 选取属性值以某个字符串**开头**的元素。
<div class="example-code">img[src^="https://"] - 选取所有 src 属性值以“https://”开头的图片。</div>
</li>
<li><strong><code>[属性$="值"]</code>:</strong> 选取属性值以某个字符串**结尾**的元素。
<div class="example-code">a[href$=".m3u8"] - 选取所有 href 属性值以“.m3u8”结尾的链接。</div>
</li>
<li><strong><code>[属性*="值"]</code>:</strong> 选取属性值**包含**某个字符串的元素。
<div class="example-code">a[href*="m3u8"] - 选取所有 href 属性值包含“m3u8”的链接。</div>
</li>
</ul>
<h4>7. 排除特定内容的元素 <code>:not(:matches())</code></h4>
<ul>
<li><strong>作用:</strong> 这是一个复合选择器,用于选取所有符合某选择器的元素,但排除其中包含特定文本或满足另一条件的元素。
<div class="example-code">.module-item:not(:matches(主角|主演))</div>
</li>
<li><strong>解释:</strong> 上述示例会选取所有 class 为 <code>module-item</code> 的元素,但会**排除**那些其文本内容为“主角”或“主演”的元素。
<ul>
<li><code>:not()</code> 是否定伪类,用于排除符合括号内选择器的元素。</li>
<li><code>:matches()</code> 是匹配伪类,用于同时匹配多个选择器。</li>
<li><code>|</code>(竖线)在规则中代表“或”,可以在 <code>:matches()</code> 中连接多个要排除的文本。</li>
</ul>
</li>
</ul>
<h4>8. 结构伪类 <code>:first-child</code> / <code>:last-child</code></h4>
<ul>
<li><strong>作用:</strong> 用于选取作为其父元素的第一个或最后一个子元素的特定元素。</li>
<li><strong><code>:first-child</code>:</strong> 选取第一个子元素。
<div class="example-code">.detail-info>ul>li:first-child&&Text - 选取 class 为 detail-info 的元素下 ul 的第一个 li 元素的文本内容。</div>
</li>
<li><strong><code>:last-child</code>:</strong> 选取最后一个子元素。
<div class="example-code">.detail-info>ul>li:last-child&&Text - 选取 class 为 detail-info 的元素下 ul 的最后一个 li 元素的文本内容。</div>
</li>
</ul>
</div>
</script>
<script id="form-field-template" type="text/x-handlebars-template">
<div class="form-group {{#if isAdvanced}}advanced-field{{/if}}">
<label for="{{id}}">{{key}}</label>
<div class="input-with-buttons">
{{#if (eq type "textarea")}}
<textarea id="{{id}}" name="{{id}}"></textarea>
{{else}}
<input type="{{#if type}}{{type}}{{else}}text{{/if}}" id="{{id}}" name="{{id}}">
{{/if}}
</div>
</div>
</script>
<script>
const fileContentFromServer = <?php echo $file_content_for_js; ?>;
const filePathFromServer = <?php echo $file_path_for_js; ?>;
</script>
<script src="/assets/js/handlebars.min.js"></script>
<script src="/assets/js/utils.js?t=<?php echo time();?>"></script>
<script src="/assets/js/el.js"></script>
<script src="/assets/js/script.js?t=<?php echo time();?>"></script>
<button id="scrollToTopBtn" title="返回顶部">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 19V5M5 12l7-7 7 7"/>
</svg>
</button>
</body>
</html>
+376
View File
@@ -0,0 +1,376 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在编辑 - <?php echo isset($_GET['file']) ? htmlspecialchars(basename($_GET['file'])) : 'N/A'; ?></title>
<link rel="stylesheet" href="/assets/css/ui.css?t=<?php echo time();?>">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden; /* 防止出现滚动条 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f8f9fa;
}
.editor-page-wrapper {
display: flex;
flex-direction: column;
height: 100vh; /* 占满整个视口高度 */
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 15px;
background-color: #fff;
border-bottom: 1px solid #dee2e6;
flex-shrink: 0; /* 防止头部被压缩 */
}
.file-info .file-name {
font-weight: 600;
color: #343a40;
}
.file-info .full-path {
font-size: 12px;
color: #6c757d;
margin-top: 2px;
}
#editor-container {
flex-grow: 1; /* 编辑器占满所有剩余空间 */
position: relative;
}
.checkbox-group { margin: 0; } /* 修正弹窗内边距 */
</style>
</head>
<body>
<div class="editor-page-wrapper">
<header class="editor-header">
<div class="file-info">
<div class="file-path">
<span class="file-icon">📄</span>
<span class="file-name"><?php echo isset($_GET['file']) ? htmlspecialchars(basename($_GET['file'])) : '未选择文件'; ?></span>
</div>
<?php if(isset($_GET['file'])): ?>
<div class="full-path"><?php echo htmlspecialchars($_GET['file']); ?></div>
<?php endif; ?>
</div>
<div class="header-actions">
<div class="btn-group">
<button id="vscodeBtn" class="btn secondary-btn" title="在VSCode中打开">VSCode</button>
<button id="fullscreenBtn" class="btn secondary-btn">全屏</button>
<button id="settingsBtn" class="btn secondary-btn">设置</button>
<button id="saveBtn" class="btn primary-btn">保存</button>
</div>
</div>
</header>
<main id="editor-container"></main>
</div>
<script id="settings-modal-template" type="text/x-handlebars-template">
<div class="form-group">
<label for="theme-switcher">编辑器主题</label>
<select id="theme-switcher" class="form-control"></select>
</div>
<div class="form-group">
<label for="font-size-switcher">字体大小</label>
<select id="font-size-switcher" class="form-control">
<option value="12">12px</option>
<option value="14" selected>14px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
<option value="24">24px</option>
</select>
</div>
<div class="form-group">
<label for="soft-wrap-toggle">自动换行</label>
<select id="soft-wrap-toggle" class="form-control">
<option value="true">开启</option>
<option value="false">关闭</option>
</select>
</div>
<div class="form-group">
<label for="tab-size-switcher">Tab 宽度</label>
<select id="tab-size-switcher" class="form-control">
<option value="2">2空格</option>
<option value="4">4空格</option>
</select>
</div>
<div class="form-group">
<label for="keybinding-switcher">键盘快捷键方案</label>
<select id="keybinding-switcher" class="form-control">
<option value="">默认 (Ace)</option>
<option value="ace/keyboard/vscode">VSCode</option>
<option value="ace/keyboard/vim">Vim</option>
<option value="ace/keyboard/emacs">Emacs</option>
<option value="ace/keyboard/sublime">Sublime</option>
</select>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="line-number-toggle" style="width: auto;">
<label for="line-number-toggle">隐藏行号</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="highlight-line-toggle" style="width: auto;">
<label for="highlight-line-toggle">高亮当前行</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="show-indent-guides-toggle" style="width: auto;">
<label for="show-indent-guides-toggle">显示缩进向导</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="live-autocomplete-toggle" style="width: auto;">
<label for="live-autocomplete-toggle">实时自动补全</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="enable-snippets-toggle" style="width: auto;">
<label for="enable-snippets-toggle">启用代码片段</label>
</div>
</script>
<div class="toast-container"></div>
<script src="/assets/js/ace/ace.js"></script>
<script src="/assets/js/ace/ext-modelist.js"></script>
<script src="/assets/js/ace/ext-language_tools.js"></script>
<script src="/assets/js/handlebars.min.js"></script>
<script src="/assets/js/utils.js?t=<?php echo time();?>"></script>
<script>
const fileContentFromServer = <?php echo $file_content_for_js ?? '""'; ?>;
const filePathFromServer = <?php echo $file_path_for_js ?? '""'; ?>;
const fileFullPath = '<?php echo $file_path ?? '""'; ?>';
const aceThemes = {
"亮色主题 (Light)": [
{ name: "Chrome", path: "ace/theme/chrome" },
{ name: "GitHub", path: "ace/theme/github" },
{ name: "Solarized Light", path: "ace/theme/solarized_light" },
{ name: "Xcode", path: "ace/theme/xcode" },
],
"暗色主题 (Dark)": [
{ name: "Monokai", path: "ace/theme/monokai" },
{ name: "Dracula", path: "ace/theme/dracula" },
{ name: "Nord Dark", path: "ace/theme/nord_dark" },
{ name: "Solarized Dark", path: "ace/theme/solarized_dark" },
{ name: "Twilight", path: "ace/theme/twilight" },
]
};
document.addEventListener('DOMContentLoaded', () => {
let settingsModal = null;
const editor = ace.edit("editor-container");
/**
* ---------------------------------------------------------------------
* 编辑器初始化
* ---------------------------------------------------------------------
*/
editor.setShowPrintMargin(false);
const modelist = ace.require("ace/ext/modelist");
const mode = modelist.getModeForPath(filePathFromServer).mode;
editor.session.setMode(mode);
if (fileContentFromServer && !fileContentFromServer.startsWith('错误:')) {
editor.setValue(fileContentFromServer, -1);
} else if (fileContentFromServer) {
editor.setValue(`// ${fileContentFromServer}`);
}
const savedSettings = {
theme: localStorage.getItem('ace_editor_theme') || 'ace/theme/monokai',
fontSize: parseInt(localStorage.getItem('ace_editor_fontSize') || '14', 10),
showGutter: localStorage.getItem('ace_editor_showGutter') !== 'false',
softWrap: localStorage.getItem('ace_editor_softWrap') === 'true',
tabSize: parseInt(localStorage.getItem('ace_editor_tabSize') || '4', 10),
highlightLine: localStorage.getItem('ace_editor_highlightLine') !== 'false',
keybinding: localStorage.getItem('ace_editor_keybinding') || '',
liveAutocomplete: localStorage.getItem('ace_editor_liveAutocomplete') === 'true',
enableSnippets: localStorage.getItem('ace_editor_enableSnippets') !== 'false',
showIndentGuides: localStorage.getItem('ace_editor_showIndentGuides') !== 'false'
};
editor.setTheme(savedSettings.theme);
editor.setFontSize(savedSettings.fontSize);
editor.renderer.setShowGutter(savedSettings.showGutter);
editor.session.setUseWrapMode(savedSettings.softWrap);
editor.session.setTabSize(savedSettings.tabSize);
editor.setHighlightActiveLine(savedSettings.highlightLine);
editor.setKeyboardHandler(savedSettings.keybinding || null);
editor.setDisplayIndentGuides(savedSettings.showIndentGuides);
editor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: savedSettings.liveAutocomplete,
enableSnippets: savedSettings.enableSnippets
});
/**
* ---------------------------------------------------------------------
* 页面主要按钮事件绑定
* ---------------------------------------------------------------------
*/
document.getElementById('saveBtn').addEventListener('click', () => {
if (!filePathFromServer) {
showToast('文件路径未知,无法保存。', 'error');
return;
}
const newContent = editor.getValue();
const formData = new FormData();
formData.append('filePath', filePathFromServer);
formData.append('fileContent', newContent);
showToast('正在保存...', 'info');
fetch('/index.php/Edit/save', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(result => {
if (result.success) {
showToast(result.message, 'success');
} else {
throw new Error(result.message);
}
})
.catch(err => {
showToast(`保存失败: ${err.message}`, 'error');
});
});
document.getElementById('vscodeBtn')?.addEventListener('click', () => {
openInVSCode(fileFullPath);
});
document.getElementById('settingsBtn').addEventListener('click', () => {
if (!settingsModal) {
const template = Handlebars.compile(document.getElementById('settings-modal-template').innerHTML);
settingsModal = new Modal({
id: 'editor-settings-modal',
title: '编辑器设置',
content: template(),
footer: `
<button type="button" class="btn secondary-btn" data-close-modal>取消</button>
<button type="button" class="btn primary-btn" id="apply-settings">应用</button>
`
});
const modalBody = settingsModal.getBodyElement();
const themeSelect = modalBody.querySelector('#theme-switcher');
themeSelect.innerHTML = '';
for (const groupName in aceThemes) {
const optgroup = document.createElement('optgroup');
optgroup.label = groupName;
aceThemes[groupName].forEach(theme => {
const option = new Option(theme.name, theme.path);
optgroup.appendChild(option);
});
themeSelect.appendChild(optgroup);
}
settingsModal.getFooterElement().querySelector('#apply-settings').addEventListener('click', () => {
const newSettings = {
theme: modalBody.querySelector('#theme-switcher').value,
fontSize: parseInt(modalBody.querySelector('#font-size-switcher').value, 10),
showGutter: !modalBody.querySelector('#line-number-toggle').checked,
softWrap: modalBody.querySelector('#soft-wrap-toggle').value === 'true',
tabSize: parseInt(modalBody.querySelector('#tab-size-switcher').value, 10),
highlightLine: modalBody.querySelector('#highlight-line-toggle').checked,
keybinding: modalBody.querySelector('#keybinding-switcher').value,
liveAutocomplete: modalBody.querySelector('#live-autocomplete-toggle').checked,
enableSnippets: modalBody.querySelector('#enable-snippets-toggle').checked,
showIndentGuides: modalBody.querySelector('#show-indent-guides-toggle').checked
};
editor.setTheme(newSettings.theme);
editor.setFontSize(newSettings.fontSize);
editor.renderer.setShowGutter(newSettings.showGutter);
editor.session.setUseWrapMode(newSettings.softWrap);
editor.session.setTabSize(newSettings.tabSize);
editor.setHighlightActiveLine(newSettings.highlightLine);
editor.setKeyboardHandler(newSettings.keybinding || null);
editor.setDisplayIndentGuides(newSettings.showIndentGuides);
editor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: newSettings.liveAutocomplete,
enableSnippets: newSettings.enableSnippets
});
for (const key in newSettings) {
localStorage.setItem(`ace_editor_${key}`, newSettings[key]);
}
showToast('设置已应用', 'success');
settingsModal.close();
});
settingsModal.getFooterElement().querySelector('[data-close-modal]').addEventListener('click', () => settingsModal.close());
}
const modalBody = settingsModal.getBodyElement();
modalBody.querySelector('#theme-switcher').value = localStorage.getItem('ace_editor_theme') || 'ace/theme/monokai';
modalBody.querySelector('#font-size-switcher').value = localStorage.getItem('ace_editor_fontSize') || '14';
modalBody.querySelector('#line-number-toggle').checked = localStorage.getItem('ace_editor_showGutter') === 'false';
modalBody.querySelector('#soft-wrap-toggle').value = localStorage.getItem('ace_editor_softWrap') || 'false';
modalBody.querySelector('#tab-size-switcher').value = localStorage.getItem('ace_editor_tabSize') || '4';
modalBody.querySelector('#highlight-line-toggle').checked = localStorage.getItem('ace_editor_highlightLine') !== 'false';
modalBody.querySelector('#keybinding-switcher').value = localStorage.getItem('ace_editor_keybinding') || '';
modalBody.querySelector('#live-autocomplete-toggle').checked = localStorage.getItem('ace_editor_liveAutocomplete') === 'true';
modalBody.querySelector('#enable-snippets-toggle').checked = localStorage.getItem('ace_editor_enableSnippets') !== 'false';
modalBody.querySelector('#show-indent-guides-toggle').checked = localStorage.getItem('ace_editor_showIndentGuides') !== 'false';
settingsModal.open();
});
const fullscreenButton = document.getElementById('fullscreenBtn');
if (fullscreenButton) {
function toggleFullScreen() {
const doc = document;
const docEl = doc.documentElement;
const requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullscreen || docEl.msRequestFullscreen;
const cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
if (requestFullScreen) {
requestFullScreen.call(docEl);
}
} else {
if (cancelFullScreen) {
cancelFullScreen.call(doc);
}
}
}
fullscreenButton.addEventListener('click', toggleFullScreen);
document.addEventListener('fullscreenchange', () => {
const isFullScreen = !!document.fullscreenElement;
fullscreenButton.textContent = isFullScreen ? '退出全屏' : '全屏';
});
}
function openInVSCode(fullPath) {
if (!fullPath) {
alert('无效的文件路径');
return;
}
const vscodeUri = `vscode://file${fullPath}`;
window.location.href = vscodeUri;
setTimeout(() => {
if (!document.hidden) {
alert(`无法自动打开VSCode,请确认:\n\n1. 您已在本地安装了VSCode。\n2. 文件路径对您的本地环境是可访问的。\n\n路径: ${fullPath}`);
}
}, 2000);
}
});
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
编辑器不存在!
+294
View File
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TVbox 规则加载器</title>
<link rel="stylesheet" href="assets/css/ui.css?t=<?php echo time();?>">
<link rel="stylesheet" href="assets/css/main.css?t=<?php echo time();?>">
</head>
<body>
<div class="container">
<header class="main-header d-flex align-items-center flex-wrap">
<div class="file-path me-auto">
<span class="file-icon">📖</span>
<span id="file-name-display" class="file-name">选择文件或输入链接</span>
</div>
<div class="layout-selector">
<label for="column-select">列表布局:</label>
<select id="column-select" name="column-select">
<option value="1">每行1个</option>
<option value="2" selected="">每行2个</option>
<option value="3">每行3个</option>
<option value="4">每行4个</option>
</select>
</div>
<div class="global-actions">
<div class="btn-group gbtn-sm">
<button id="saveBtn" class="btn primary-btn">保存修改</button>
<button id="historyBtn" class="btn secondary-btn">文件历史</button>
<button id="online-edit-btn" class="btn secondary-btn">在线编辑</button>
<button id="downloadRulesBtn" class="btn secondary-btn">下载</button>
</div>
</div>
<div class="input-with-buttons w-100 mt-2">
<input type="text" id="jsonUrlInput" placeholder="请输入TVbox规则集合的JSON链接">
<div class="btn-group">
<button id="readUrlBtn" class="btn primary-btn">加载</button>
<button id="viewSourceBtn" class="btn secondary-btn">查看源码</button>
<button id="selectFileBtn" class="btn secondary-btn">选择文件</button>
</div>
</div>
</header>
<main class="main-content">
<div class="tabs">
<div class="tab-btn active" onclick="openTab(event, 'basic')" data-tab="basic">基础信息</div>
<div class="tab-btn" onclick="openTab(event, 'lives')" data-tab="lives">直播规则</div>
<div class="tab-btn" onclick="openTab(event, 'sites')" data-tab="sites">爬虫规则</div>
<div class="tab-btn" onclick="openTab(event, 'parses')" data-tab="parses">解析接口</div>
<div class="tab-btn" onclick="openTab(event, 'filters')" data-tab="filters">广告过滤</div>
</div>
<div id="basic" class="tab-content active" style="display: block;"></div>
<div id="lives" class="tab-content" style="display: none;"></div>
<div id="sites" class="tab-content" style="display: none;"></div>
<div id="parses" class="tab-content" style="display: none;"></div>
<div id="filters" class="tab-content" style="display: none;"></div>
</main>
<div id="loading" style="display: none; text-align: center; padding: 20px; font-size: 16px;">正在读取内容...</div>
</div>
<div id="templates" style="display: none;">
<script id="add-site-modal-template" type="text/x-handlebars-template">
<div id="create-spider-form-modal" class="details-panel create-panel active" style="max-height:none; opacity:1; padding:0; background:none;">
<div class="form-group"><label for="new-site-name-modal">规则名称</label><input id="new-site-name-modal" type="text" placeholder="例如:酷云影视"></div>
<div class="form-group"><label for="new-site-key-modal">唯一标识</label><input id="new-site-key-modal" type="text" placeholder="例如:ky_m"></div>
<div class="form-group" style="grid-column: 1 / -1;"><label for="new-site-ext-modal">规则链接</label><input id="new-site-ext-modal" type="text" placeholder="http://.../rule.json"></div>
<div class="form-group"><label for="new-site-api-modal">爬虫接口</label><input id="new-site-api-modal" type="text" value="csp_XYQHiker"></div>
<div class="form-group"><label for="new-site-type-modal">类型</label><select id="new-site-type-modal"><option value="1">1 (csp)</option><option value="0">0 (vod)</option><option value="2">2</option><option value="3" selected>3</option></select></div>
<div class="form-group"><label for="new-site-jar-modal">Jar文件</label><input id="new-site-jar-modal" type="text" placeholder="例如:./libs/Panda.jar"></div>
<div class="form-group checkbox-group">
<input type="checkbox" id="new-site-searchable-modal" style="width: auto;" checked>
<label>可搜索</label>
<input type="checkbox" id="new-site-filterable-modal" style="width: auto;" checked>
<label>可筛选</label>
<input type="checkbox" id="new-site-quick-modal" style="width: auto;" checked>
<label>快速搜索</label>
</div>
</div>
</script>
<script id="add-parse-modal-template" type="text/x-handlebars-template">
<div id="create-parse-form-modal" class="details-panel create-panel active" style="max-height:none; opacity:1; padding:0; background:none;">
<div class="details-form-grid">
<div class="form-group"><label for="new-parse-name-modal">接口名称</label><input id="new-parse-name-modal" type="text" placeholder="例如:XX解析"></div>
<div class="form-group"><label for="new-parse-type-modal">类型</label><input id="new-parse-type-modal" type="text" placeholder="0, 1, 2, 3"></div>
<div class="form-group" style="grid-column: 1 / -1;"><label for="new-parse-url-modal">接口地址(URL)</label><input id="new-parse-url-modal" type="text" placeholder="http://..."></div>
<div class="form-group" style="grid-column: 1 / -1;"><label for="new-parse-ext-modal">扩展参数(ext)</label><textarea id="new-parse-ext-modal" rows="3" placeholder='例如:{"header":{"user-agent":"PC_UA"}}'></textarea></div>
</div>
</div>
</script>
<script id="add-filter-modal-template" type="text/x-handlebars-template">
<div id="create-filter-form-modal" class="details-panel create-panel active" style="max-height:none; opacity:1; padding:0; background:none;">
<div class="details-form-grid">
<div class="form-group"><label for="new-filter-name-modal">规则名称</label><input id="new-filter-name-modal" type="text" placeholder="例如:非凡过滤"></div>
<div class="form-group"><label for="new-filter-host-modal">主机名</label><input id="new-filter-host-modal" type="text" placeholder="例如:vip.ffzy"></div>
<div class="form-group" style="grid-column: 1 / -1;"><label for="new-filter-hosts-modal">主机列表</label><textarea id="new-filter-hosts-modal" rows="3" placeholder='例如:["vip.ffzy"]'></textarea></div>
<div class="form-group" style="grid-column: 1 / -1;"><label for="new-filter-rules-modal">规则列表</label><textarea id="new-filter-rules-modal" rows="3" placeholder='例如:["playwm/?video_id="]'></textarea></div>
</div>
</div>
</script>
<script id="basic-tab-template" type="text/x-handlebars-template">
<div class="form-group">
<label for="spider-url">爬虫Jar (spider) <span id="status-spider" class="download-status"></span></label>
<input type="text" id="spider-url" name="spider-url" value="{{spiderPath}}">
</div>
<div class="form-group">
<label for="wallpaper-url">壁纸 (wallpaper)</label>
<input type="text" id="wallpaper-url" name="wallpaper-url" value="{{wallpaper}}">
</div>
<div class="form-group">
<label for="ijk-url">播放器 (ijk)</label>
<textarea id="ijk-url" name="ijk-url" rows="5">{{ijk}}</textarea>
</div>
<div class="form-group">
<label for="warning-text">警告文本 (warningText)</label>
<textarea id="warning-text" name="warning-text" rows="3">{{warningText}}</textarea>
</div>
</script>
<script id="simple-item-template" type="text/x-handlebars-template">
<div id="{{itemType}}-item-{{index}}" class="rule-item-container" data-index="{{index}}" data-item-type="{{itemType}}">
<button type="button" class="delete-item-btn">&times;</button>
<div class="form-group">
<label for="{{itemType}}-{{index}}">{{name}}</label>
<div class="input-with-buttons">
<input type="text" id="{{itemType}}-{{index}}" value="{{url}}" readonly>
<div class="action-btn-group">
<button type="button" class="btn btn-sm secondary-btn action-btn" data-action="test-url" data-url="{{url}}">测试</button>
</div>
</div>
</div>
</div>
</script>
<script id="site-item-template" type="text/x-handlebars-template">
<div id="site-item-{{index}}" class="rule-item-container" data-api="{{api}}" data-index="{{index}}" data-item-type="sites">
<button type="button" class="delete-item-btn">&times;</button>
<div class="form-group">
<label for="site-{{index}}">
{{name}}
{{#if hasAssets}}
<span id="status-site-item-{{index}}" class="download-status {{combinedStatus}}"></span>
{{/if}}
</label>
<div class="input-with-buttons">
<input type="text" id="site-{{index}}" value="{{displayValue}}" readonly>
<div class="action-btn-group">
<button type="button" class="btn btn-sm secondary-btn action-btn" data-action="edit-file">编辑</button>
</div>
</div>
</div>
</div>
</script>
<script id="filter-item-template" type="text/x-handlebars-template">
<div id="rules-item-{{index}}" class="rule-item-container" data-index="{{index}}" data-item-type="rules">
<button type="button" class="delete-item-btn">&times;</button>
<div class="form-group">
<label for="rules-{{index}}">{{displayName}}</label>
<textarea id="rules-{{index}}" readonly rows="3">{{displayValue}}</textarea>
</div>
</div>
</script>
<script id="tab-content-template" type="text/x-handlebars-template">
<div class="controls-container d-flex justify-between align-items-center">
<div class="left-controls">
{{#if showCreateButton}}
<div class="btn-group">
<button type="button" class="btn primary-btn create-new-btn" data-item-type="{{itemType}}">+ 新增</button>
<button type="button" class="btn danger-btn delete-all-btn" data-item-type="{{itemType}}">清空</button>
</div>
{{else}}
<button type="button" class="btn danger-btn delete-all-btn" data-item-type="{{itemType}}">清空</button>
{{/if}}
</div>
<div class="right-controls">
{{#if (eq itemType "sites")}}
<div class="btn-group">
<button type="button" class="btn secondary-btn site-filter-btn" data-filter-type="equals" data-filter-value="csp_XYQHiker">只看XYQH</button>
<button type="button" class="btn secondary-btn site-filter-btn" data-filter-type="equals" data-filter-value="csp_XBPQ">只看XBPQ</button>
<button type="button" class="btn secondary-btn site-filter-btn" data-filter-type="endsWith" data-filter-value=".js">只看Js</button>
</div>
{{/if}}
</div>
</div>
<div class="rule-list-grid"></div>
</script>
<script id="tab-content-template1" type="text/x-handlebars-template">
<div class="controls-container">
<div class="left-controls">
<div class="btn-group">
<button type="button" class="btn danger-btn delete-all-btn" data-item-type="{{itemType}}">删除全部</button>
</div>
</div>
<div class="right-controls">
{{{rightControls}}}
</div>
</div>
<div class="rule-list-grid"></div>
</script>
<script id="details-modal-body-template" type="text/x-handlebars-template">
<div class="details-form-grid">
{{#each fields}}
<div class="details-item" {{#if this.fullWidth}}style="grid-column: 1 / -1;"{{/if}}>
<label class="details-label" for="{{this.id}}">{{this.label}}</label>
{{#if this.isBoolean}}
<div class="input-with-buttons">
<input class="details-input" type="text" id="{{this.id}}" value="{{this.value}}">
<button type="button" class="btn btn-sm success-btn bool-setter" data-target-id="{{this.id}}" data-value="{{this.trueValue}}">{{this.trueText}}</button>
<button type="button" class="btn btn-sm danger-btn bool-setter" data-target-id="{{this.id}}" data-value="{{this.falseValue}}">{{this.falseText}}</button>
</div>
{{else if this.isTextarea}}
<textarea class="details-input" id="{{this.id}}" rows="3">{{this.value}}</textarea>
{{else}}
<input class="details-input" type="text" id="{{this.id}}" value="{{this.value}}">
{{/if}}
</div>
{{/each}}
</div>
</script>
<script id="file-browser-body-template" type="text/x-handlebars-template">
{{#if files.length}}
<ul class="file-list">
{{#each files}}
{{#if (eq type "dir")}}
<li class="dir collapsed">
<div class="file-list-item is-dir">
<span class="icon toggle-icon">+</span>
<span class="icon">📁</span> {{name}}
</div>
{{{buildList children}}}
</li>
{{else}}
<li>
{{#if (endsWith name ".json")}}
<div class="file-list-item is-file">
<label>
<input type="radio" name="server-file-radio" value="{{path}}">
<span class="icon">📄</span> {{name}}
</label>
</div>
{{else}}
<div class="file-list-item is-file" style="padding-left: 30px;">
<span class="icon">▫️</span> {{name}}
</div>
{{/if}}
</li>
{{/if}}
{{/each}}
</ul>
{{else}}
<p>服务器上的 "box" 目录为空或不存在。</p>
{{/if}}
</script>
<script id="download-modal-template" type="text/x-handlebars-template">
<div class="form-group">
<label for="download-dir-input">存放目录名 (在服务器box/目录下创建)</label>
<input type="text" id="download-dir-input" placeholder="例如: my_config">
</div>
<div class="form-group">
<label for="download-filename-input">配置文件名</label>
<input type="text" id="download-filename-input" value="config.json">
</div>
</script>
</div>
<input type="file" id="localFileInput" accept=".json" style="display: none;">
<div class="toast-container"></div>
<script src="assets/js/handlebars.min.js"></script>
<script src="assets/js/utils.js?t=<?php echo time();?>"></script>
<script src="assets/js/main.js?t=<?php echo time();?>"></script>
<button id="scrollToTopBtn" title="返回顶部">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 19V5M5 12l7-7 7 7"/>
</svg>
</button>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - TVbox 规则加载器</title>
<style>
* {
box-sizing: border-box;
}
html {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f8f9fa;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 40px;
margin: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 25px rgba(0, 0, 0, .05);
text-align: center;
}
h1 {
margin-top: 0;
margin-bottom: 25px;
font-weight: 600;
font-size: 24px;
color: #343a40;
}
form {
display: flex;
flex-direction: column;
gap: 20px;
}
input[type="password"] {
padding: 12px 15px;
font-size: 16px;
border-radius: 6px;
border: 1px solid #ced4da;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
input[type="password"]:focus {
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
}
button {
padding: 12px 20px;
font-size: 16px;
font-weight: 500;
border-radius: 6px;
border: none;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color .2s ease-in-out, transform .1s ease;
}
button:hover {
background-color: #0069d9;
}
button:active {
transform: scale(.98);
}
.error-message {
color: #dc3545;
margin-top: 15px;
margin-bottom: 0;
text-align: left;
font-size: 14px;
}
</style>
</head>
<body>
<div class="login-container">
<h1>登录</h1>
<form method="POST" action="/<?php echo WWW_URL('Login', 'doLogin'); ?>">
<input type="password" name="password" placeholder="请输入密码" required autofocus>
<button type="submit"> </button>
</form>
<?php if (!empty($error)): ?>
<p class="error-message"><?php echo htmlspecialchars($error); ?></p>
<?php endif; ?>
</div>
</body>
</html>
+189
View File
@@ -0,0 +1,189 @@
<?php
if (!defined('IN_APP')) {
die('Direct access is not allowed.');
}
/**
* 清理并规范化路径,防止目录遍历攻击
* @param string $path
* @return string
*/
function sanitize_path($path) {
// 移除协议、域名和 ../ & .\
$path = preg_replace('/^[\w]+:\/\/[^\/]+/', '', $path);
$path = str_replace(['../', '..\\'], '', $path);
return trim($path, '/\\');
}
/**
* 根据配置生成前端可用的URL
*
* @param string $controller 控制器名称 (例如: 'Proxy')
* @param string $action 方法名称 (例如: 'load')
* @param array $params URL查询参数数组 (例如: ['key' => 'value'])
* @return string 构建好的URL
*/
function WWW_URL($controller, $action, $params = []) {
$path_mod = C('PATH_MOD');
$rewrite = C('REWRITE');
$baseUrl = '';
$queryParams = $params;
if (strcasecmp($path_mod, 'PATH_INFO') === 0) {
$baseUrl = $rewrite ? '' : 'index.php';
$baseUrl .= '/' . $controller . '/' . $action;
} else { // NORMAL 模式
$baseUrl = 'index.php';
$queryParams['c'] = $controller;
$queryParams['a'] = $action;
}
$queryString = http_build_query($queryParams);
if (!empty($queryString)) {
return $baseUrl . '?' . $queryString;
}
return $baseUrl;
}
// 格式化路径用于VSCode
function formatPathForVSCode($path) {
// 统一使用正斜杠
$path = str_replace('\\', '/', $path);
// 处理Windows盘符 (如 C:/path → /C:/path)
if (preg_match('/^[A-Za-z]:/', $path)) {
$path = '/' . $path;
}
// 编码特殊字符但保留斜杠
$parts = explode('/', $path);
$encodedParts = array_map('rawurlencode', $parts);
$encodedPath = implode('/', $encodedParts);
return $encodedPath;
}
/**
* 检测当前是否运行在本地开发环境
*
* @return bool 如果是本地环境返回true,否则返回false
*/
function isLocalEnvironment() {
// 1. 检查服务器IP地址
$serverIP = $_SERVER['SERVER_ADDR'] ?? '';
$remoteIP = $_SERVER['REMOTE_ADDR'] ?? '';
$localIPs = ['127.0.0.1', '::1'];
if (in_array($serverIP, $localIPs)) {
return true;
}
// 2. 检查主机名
$host = $_SERVER['HTTP_HOST'] ?? '';
if (strpos($host, 'localhost') !== false ||
strpos($host, '.local') !== false ||
strpos($host, '127.0.0.1') !== false) {
return true;
}
// 3. 检查开发环境变量(如Laravel的APP_ENV
if (getenv('APP_ENV') === 'local' || getenv('APP_ENV') === 'development') {
return true;
}
// 4. 检查常见的开发域名模式
if (preg_match('/\.(test|dev|local)$/', $host)) {
return true;
}
// 5. 检查X-Forwarded-For头(适用于某些本地代理设置)
$forwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($forwardedFor && in_array(trim(explode(',', $forwardedFor)[0], $localIPs))) {
return true;
}
// 6. 检查是否通过VPN或内网访问
$ipLong = ip2long($remoteIP);
$privateRanges = [
['10.0.0.0', '10.255.255.255'],
['172.16.0.0', '172.31.255.255'],
['192.168.0.0', '192.168.255.255']
];
foreach ($privateRanges as $range) {
$start = ip2long($range[0]);
$end = ip2long($range[1]);
if ($ipLong >= $start && $ipLong <= $end) {
return true;
}
}
return false;
}
/**
* 发起一个 cURL HTTP 请求
*
* @param array $options cURL请求的配置数组
* @return array 成功时返回 ['body' => string, 'info' => array], 失败时返回 ['error' => string]
* 如果 RETURNHEADER 为 true, 成功时返回 ['header' => string, 'body' => string, 'info' => array]
*/
function httpCurl($options) {
if (empty($options['url'])) {
return ['error' => 'cURL Error: URL is required.'];
}
$url = $options['url'];
$postData = $options['data'] ?? null;
$headers = $options['header'] ?? [];
$timeout = $options['TIMEOUT'] ?? 15;
$returnHeader = $options['RETURNHEADER'] ?? false;
$returnTransfer = $options['RETURNTRANSFER'] ?? true;
$followLocation = $options['FOLLOWLOCATION'] ?? true;
if (!array_filter($headers, function($h) { return stripos($h, 'User-Agent:') === 0; })) {
$headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36';
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, $returnTransfer);
curl_setopt($ch, CURLOPT_HEADER, $returnHeader);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $followLocation);
curl_setopt($ch, CURLOPT_MAXREDIRS, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
if (!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
if (!empty($postData)) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
$response = curl_exec($ch);
$err = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($err) {
return ['error' => $err];
}
$result = ['body' => $response, 'info' => $info];
if ($returnHeader) {
$headerSize = $info['header_size'];
$result['header'] = substr($response, 0, $headerSize);
$result['body'] = substr($response, $headerSize);
}
return $result;
}
+26
View File
@@ -0,0 +1,26 @@
<?php
// 应用配置文件
return array(
/**
* URL路由模式
* - 'NORMAL': 普通模式 index.php?c=Controller&a=Action
* - 'PATH_INFO': 路径模式 index.php/Controller/Action
*/
'PATH_MOD' => 'PATH_INFO',
/**
* 是否开启URL重写(伪静态)
* 开启后会隐藏URL中的 index.php,需要Web服务器配置重写规则。
* - true: /Controller/Action
* - false: /index.php/Controller/Action
*/
'REWRITE' => false,
/**
* 其他应用配置
*/
'PASSWORD' => 'tvbox',
'NEED_LOGIN' => true,
'USE_SESSION' => true,
'PC_UA' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
);