push
首次推送
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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('/');
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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' => '文件写入本地失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
模板开发中....
|
||||
@@ -0,0 +1 @@
|
||||
模板开发中....
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
编辑器不存在!
|
||||
@@ -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">×</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">×</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">×</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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
Reference in New Issue
Block a user