找回密码
 注册
搜索
免费空间 免费域名 免费AI 老牌主机商首月仅1美分!27美元/年!Namecheap优惠码Spaceship优惠码
查看: 23|回复: 4

[程序代码] 一个网站管理你的所有免费空间

[复制链接]
发表于 昨天 22:48 | 显示全部楼层 |阅读模式
本帖最后由 zhulisui 于 2026-11-14 22:57 编辑

大家肯定有好多免费空间 管理十分不方便 都保存在TXT  EXCEL 里  现在一个网站  登入进去 想管理哪个就管理哪个 十分方便




下载地址:点此下载


免费空间分类 大家可以随意修改   就一个单php文件 开源



多站管理台(独立 PHP 版)使用说明
================================


一个单文件 PHP 程序,传到任意支持 PHP 的免费空间就能跑,
不用数据库,不依赖 Muse,随时可以搬走。
功能:登录保护、按免费空间分类增删站点、在线检测、
FTP/FTPS/SFTP 文件管理(浏览/上传/下载/在线编辑/改名/删除/新建文件夹)、
在线解压 zip、打包下载备份。


1. 把压缩包里的 manager.php 上传到某个空间的网站目录
(比如你 serv00 或某个站的根目录下,也可以单独建个文件夹放它)。
2. 浏览器打开 https://你的域名/manager.php
3. 第一次打开会让你设置管理密码(至少 6 位)。
★ 这个密码忘了没有任何找回入口,请记牢,不要发给任何人。
4. 登录后,先到各个分类下面把你的站点添加进去,
每个站的 FTP 主机/用户名/密码在“编辑站点”里填,只填一次。


PHP 7.0 以上,需要 curl 扩展(绝大多数空间都有)和 ZipArchive 扩展(解压/打包用)。
没有 ZipArchive 时,除了在线解压/打包,其余功能不受影响(会报错提示)。


同目录下自动生成 manager-data.php,里面是站点清单和 FTP 密码等数据。
这个文件的第一行是退出语句,用浏览器直接打开它只会看到空白/报错,拿不到内容。
但还是要做到:管理台密码设强一点、尽量用 HTTPS 打开管理台、
不要把 manager-data.php 改名成.txt /.json 之类后缀。


- freehostia:单文件最大 2MB,程序里已经按这个上限拦截上传和解压,
大 zip 请拆小或换个空间当中转。
- free.fr:FTP 主机固定 ftpperso.free.fr,连接方式选 FTPS;
它家限制同一 IP 的连接频率,文件操作别连续猛点,出错等半分钟再试。
- serv00:连接方式选 SFTP、端口 22;每 90 天要登录一次,
在站点里把“登录保活周期”填 90,管理台会倒计时提醒,登录后点“标记已登录”。
SFTP 下浏览/上传/下载一般没问题;改名/删除/新建文件夹用的是 FTP 命令语法,
万一某项在 serv00 上失败,把报错原话发给 Baxter 再适配。
- xrea:网站根目录一般填 public_html。
- beget:免费版只有 1 个 FTP 账号,照面板里给的 FTP 信息填即可。


zip ≤ 25MB、文件数 ≤ 200、解压后总大小 ≤ 50MB,超了会停下报错。
解压是把 zip 先下载到管理台所在空间、解开、再逐个文件传到目标站,
所以大包会慢一些,属正常。


- 管理台本身建议单独放在一个不对外宣传的目录/域名下。
- FTP 密码只存在你放管理台的那个空间里,选一个相对靠谱的空间放它
(比如 serv00),别放在快要到期的站上。
- 定期用“打包当前整个文件夹”给重要站点下载一份备份到本地。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| 发表于 昨天 22:58 | 显示全部楼层
  1. <?php
  2. /*
  3. * 多站管理台(独立 PHP 版)
  4. * 功能:登录保护 / 按免费空间分类管理站点(增删改)/ 在线检测 /
  5. *       FTP·FTPS·SFTP 文件管理(浏览、上传、下载、编辑、改名、删除、新建文件夹)/
  6. *       在线解压 zip / 打包下载
  7. * 部署:把 manager.php 传到任意支持 PHP 的空间,浏览器打开即可。
  8. * 数据:同目录 manager-data.php(首行是退出语句,浏览器直接访问也拿不到内容)。
  9. */
  10. if (version_compare(PHP_VERSION, '7.0.0', '<')) { die('需要 PHP 7.0 以上'); }
  11. session_start();

  12. define('DATA_FILE', __DIR__ . '/manager-data.php');
  13. define('UNZIP_MAX_ZIP', 25 * 1024 * 1024);   // zip 文件最大 25MB
  14. define('UNZIP_MAX_FILES', 200);              // 最多解压 200 个文件
  15. define('UNZIP_MAX_TOTAL', 50 * 1024 * 1024); // 解压后总大小最大 50MB
  16. define('EDIT_MAX', 512 * 1024);              // 在线编辑最大 512KB

  17. $PROVIDERS = array(
  18.     'beget'      => array('name' => 'beget.com',       'proto' => 'ftp',  'port' => 21, 'host' => '',                  'root' => '',            'cycle' => 0,  'max_single' => 0,       'note' => '免费版 1 个站点、1 个 FTP 账号'),
  19.     'freehostia' => array('name' => 'freehostia.com',  'proto' => 'ftp',  'port' => 21, 'host' => '',                  'root' => '',            'cycle' => 0,  'max_single' => 2097152, 'note' => '免费版单文件上限 2MB,超了传不上去'),
  20.     'runhosting' => array('name' => 'runhosting.com',  'proto' => 'ftp',  'port' => 21, 'host' => '',                  'root' => '',            'cycle' => 0,  'max_single' => 0,       'note' => '免费版 1GB、FTP 账号 1 个'),
  21.     'freefr'     => array('name' => 'free.fr',         'proto' => 'ftps', 'port' => 21, 'host' => 'ftpperso.free.fr',  'root' => '',            'cycle' => 0,  'max_single' => 0,       'note' => 'FTPS 加密;同一 IP 连接限速,文件操作请勿连续猛点'),
  22.     'endless'    => array('name' => 'theendlessweb.com','proto' => 'ftp', 'port' => 21, 'host' => '',                  'root' => '',            'cycle' => 0,  'max_single' => 0,       'note' => 'DirectAdmin 面板'),
  23.     'xrea'       => array('name' => 'xrea.com',        'proto' => 'ftp',  'port' => 21, 'host' => '',                  'root' => 'public_html', 'cycle' => 0,  'max_single' => 0,       'note' => '网站根目录一般是 public_html;免费版页面有广告'),
  24.     'serv00'     => array('name' => 'serv00.com',      'proto' => 'sftp', 'port' => 22, 'host' => '',                  'root' => '',            'cycle' => 90, 'max_single' => 0,       'note' => 'SFTP;每 90 天必须登录一次面板或 SSH,否则账号被删'),
  25.     'other'      => array('name' => '其他/自定义',      'proto' => 'ftp',  'port' => 21, 'host' => '',                  'root' => '',            'cycle' => 0,  'max_single' => 0,       'note' => ''),
  26. );

  27. /* ---------- 数据层 ---------- */
  28. function data_load() {
  29.     if (!file_exists(DATA_FILE)) return array('admin_hash' => '', 'sites' => array());
  30.     $raw = file_get_contents(DATA_FILE);
  31.     $pos = strpos($raw, "\n");
  32.     if ($pos === false) return array('admin_hash' => '', 'sites' => array());
  33.     $d = json_decode(substr($raw, $pos + 1), true);
  34.     if (!is_array($d) || !isset($d['sites'])) return array('admin_hash' => '', 'sites' => array());
  35.     return $d;
  36. }
  37. function data_save($d) {
  38.     $head = "<?php exit('Access denied'); ?>\n";
  39.     file_put_contents(DATA_FILE, $head . json_encode($d, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
  40. }
  41. function &site_ref(&$data, $id) {
  42.     foreach ($data['sites'] as $i => $s) { if ($s['id'] === $id) return $data['sites'][$i]; }
  43.     $null = null; return $null;
  44. }

  45. /* ---------- 工具 ---------- */
  46. function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
  47. function act() { return isset($_REQUEST['act']) ? $_REQUEST['act'] : ''; }
  48. function csrf() { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16)); return $_SESSION['csrf']; }
  49. function csrf_check() {
  50.     if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  51.         if (empty($_POST['csrf']) || $_POST['csrf'] !== csrf()) { die('请求校验失败,请返回重试'); }
  52.     }
  53. }
  54. function redirect($url) { header('Location: ' . $url); exit; }
  55. function self_url($params) {
  56.     return basename(__FILE__) . ($params ? '?' . http_build_query($params) : '');
  57. }
  58. function flash($msg, $ok = true) { $_SESSION['flash'] = array($msg, $ok); }
  59. function clean_rel_path($p) {
  60.     $p = str_replace('\\', '/', (string)$p);
  61.     $parts = array();
  62.     foreach (explode('/', $p) as $seg) {
  63.         if ($seg === '' || $seg === '.') continue;
  64.         if ($seg === '..') { array_pop($parts); continue; }
  65.         $parts[] = $seg;
  66.     }
  67.     return implode('/', $parts);
  68. }
  69. function fmt_size($n) {
  70.     if ($n < 1024) return $n . ' B';
  71.     if ($n < 1024 * 1024) return round($n / 1024, 1) . ' KB';
  72.     return round($n / 1024 / 1024, 2) . ' MB';
  73. }
  74. function provider_of($site, $PROVIDERS) {
  75.     $k = $site['provider'];
  76.     if (isset($PROVIDERS[$k])) return $PROVIDERS[$k];
  77.     return array('name' => $k, 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => '');
  78. }

  79. /* ---------- 登录 ---------- */
  80. function is_login() { return !empty($_SESSION['auth']); }
  81. function require_login($data) {
  82.     if ($data['admin_hash'] === '') { page_setup(); exit; }
  83.     if (!is_login()) { page_login(); exit; }
  84. }
  85. function page_head($title) {
  86.     echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
  87.     echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
  88.     echo '<title>' . h($title) . ' · 多站管理台</title><style>';
  89.     echo 'body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;margin:0;background:#f4f5f7;color:#222}'
  90.         . '.wrap{max-width:1080px;margin:0 auto;padding:16px}'
  91.         . '.bar{background:#1f2937;color:#fff;padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}'
  92.         . '.bar a{color:#cbd5e1;text-decoration:none}.bar b{color:#fff}'
  93.         . '.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:14px;margin:12px 0}'
  94.         . 'table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #eee;padding:6px 8px;text-align:left;font-size:14px;vertical-align:top}'
  95.         . 'input,select,textarea{padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:14px}'
  96.         . 'input[type=text],input[type=password],input[type=url],textarea{width:100%;box-sizing:border-box}'
  97.         . '.btn{display:inline-block;background:#2563eb;color:#fff;border:none;padding:7px 14px;border-radius:6px;cursor:pointer;text-decoration:none;font-size:14px}'
  98.         . '.btn.gray{background:#6b7280}.btn.red{background:#dc2626}.btn.green{background:#059669}.btn.small{padding:3px 9px;font-size:13px}'
  99.         . '.flash{padding:10px 14px;border-radius:8px;margin:12px 0}.flash.ok{background:#ecfdf5;border:1px solid #10b981}.flash.err{background:#fef2f2;border:1px solid #ef4444}'
  100.         . '.muted{color:#6b7280;font-size:13px}.badge{display:inline-block;padding:1px 8px;border-radius:99px;font-size:12px;background:#e5e7eb}'
  101.         . '.badge.on{background:#d1fae5}.badge.off{background:#fee2e2}.badge.warn{background:#fef3c7;color:#92400e}.badge.danger{background:#fee2e2;color:#991b1b}'
  102.         . '.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}@media(max-width:640px){.grid{grid-template-columns:1fr}}'
  103.         . 'label{font-size:13px;color:#374151;display:block;margin-bottom:3px}'
  104.         . '.loginbox{max-width:380px;margin:60px auto}';
  105.     echo '</style></head><body>';
  106. }
  107. function show_flash() {
  108.     if (!empty($_SESSION['flash'])) {
  109.         $f = $_SESSION['flash']; unset($_SESSION['flash']);
  110.         echo '<div class="flash ' . ($f[1] ? 'ok' : 'err') . '">' . h($f[0]) . '</div>';
  111.     }
  112. }
  113. function page_setup() {
  114.     page_head('设置密码');
  115.     echo '<div class="wrap"><div class="card loginbox"><h2>🔐 第一次使用:设置管理密码</h2>';
  116.     show_flash();
  117.     echo '<form method="post" action="' . h(self_url(array('act' => 'setup'))) . '">';
  118.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
  119.     echo '<p><label>管理密码(至少 6 位)</label><input type="password" name="pw1" required></p>';
  120.     echo '<p><label>再输一遍</label><input type="password" name="pw2" required></p>';
  121.     echo '<p><button class="btn" type="submit">设置并进入</button></p>';
  122.     echo '<p class="muted">这个密码忘了没有找回入口,请务必记牢。密码只存加密哈希。</p>';
  123.     echo '</form></div></div></body></html>';
  124. }
  125. function page_login() {
  126.     page_head('登录');
  127.     echo '<div class="wrap"><div class="card loginbox"><h2>🔐 多站管理台 · 登录</h2>';
  128.     show_flash();
  129.     echo '<form method="post" action="' . h(self_url(array('act' => 'login'))) . '">';
  130.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
  131.     echo '<p><label>管理密码</label><input type="password" name="pw" required autofocus></p>';
  132.     echo '<p><button class="btn" type="submit">登录</button></p>';
  133.     echo '</form></div></div></body></html>';
  134. }
  135. function top_bar() {
  136.     echo '<div class="bar"><b>🗂 多站管理台</b>';
  137.     echo '<a href="' . h(self_url(array())) . '">站点列表</a>';
  138.     echo '<span style="flex:1"></span><span>已登录</span>';
  139.     echo '<a href="' . h(self_url(array('act' => 'passwd'))) . '">修改密码</a>';
  140.     echo '<a href="' . h(self_url(array('act' => 'logout'))) . '">退出</a></div>';
  141.     echo '<div class="wrap">';
  142.     show_flash();
  143. }

  144. /* ---------- 在线检测 ---------- */
  145. function check_site(&$site) {
  146.     $url = trim($site['url']);
  147.     if ($url === '') { $site['last_status'] = '未填网址'; return; }
  148.     if (!preg_match('#^https?://#i', $url)) $url = 'https://' . $url;
  149.     $t0 = microtime(true);
  150.     $ch = curl_init($url);
  151.     curl_setopt_array($ch, array(
  152.         CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true,
  153.         CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => 12, CURLOPT_CONNECTTIMEOUT => 6,
  154.         CURLOPT_USERAGENT => 'SitePanel-Checker/1.0', CURLOPT_SSL_VERIFYPEER => false,
  155.     ));
  156.     curl_exec($ch);
  157.     $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  158.     $err = curl_error($ch);
  159.     curl_close($ch);
  160.     $site['last_ms'] = (int)round((microtime(true) - $t0) * 1000);
  161.     $site['last_code'] = $code;
  162.     $site['last_check'] = date('Y-m-d H:i:s');
  163.     $site['last_status'] = ($err === '' && $code >= 200 && $code < 400) ? 'online' : 'offline';
  164.     if ($err !== '') $site['last_status'] = 'offline';
  165. }

  166. /* ---------- 站点列表(按服务商分类) ---------- */
  167. function page_sites($data, $PROVIDERS) {
  168.     page_head('站点列表');
  169.     top_bar();
  170.     $q = isset($_GET['q']) ? trim($_GET['q']) : '';
  171.     echo '<form method="get" style="margin:12px 0;display:flex;gap:8px">';
  172.     echo '<input type="text" name="q" placeholder="按站名或域名搜索" value="' . h($q) . '" style="flex:1">';
  173.     echo '<button class="btn" type="submit">搜索</button>';
  174.     echo '<a class="btn green" href="' . h(self_url(array('act' => 'site_edit'))) . '">+ 添加站点</a>';
  175.     echo '<a class="btn gray" href="' . h(self_url(array('act' => 'check_all'))) . '">检测全部</a></form>';

  176.     $total = count($data['sites']);
  177.     $on = 0; foreach ($data['sites'] as $s) { if (isset($s['last_status']) && $s['last_status'] === 'online') $on++; }
  178.     echo '<p class="muted">共 ' . $total . ' 个站点 · 在线 ' . $on . ' 个</p>';

  179.     // 分组:预置服务商顺序在前,其余自定义服务商随后
  180.     $groups = array();
  181.     foreach ($PROVIDERS as $k => $p) $groups[$k] = array();
  182.     foreach ($data['sites'] as $s) {
  183.         if ($q !== '' && stripos($s['name'], $q) === false && stripos($s['url'], $q) === false) continue;
  184.         $k = $s['provider'];
  185.         if (!isset($groups[$k])) $groups[$k] = array();
  186.         $groups[$k][] = $s;
  187.     }
  188.     foreach ($groups as $k => $sites) {
  189.         $preset = isset($PROVIDERS[$k]) ? $PROVIDERS[$k] : null;
  190.         $gname = $preset ? $preset['name'] : $k;
  191.         echo '<div class="card"><h3 style="margin:4px 0">' . h($gname) . ' <span class="badge">' . count($sites) . ' 个站</span></h3>';
  192.         if ($preset && $preset['note'] !== '') echo '<p class="muted" style="margin:2px 0 8px">' . h($preset['note']) . '</p>';
  193.         if ($sites) {
  194.             echo '<table><tr><th>站点</th><th>状态</th><th>登录期限</th><th>操作</th></tr>';
  195.             foreach ($sites as $s) {
  196.                 echo '<tr><td><b>' . h($s['name']) . '</b><br><span class="muted">' . h($s['url']) . '</span></td>';
  197.                 echo '<td>';
  198.                 if (!empty($s['last_status']) && $s['last_status'] === 'online') echo '<span class="badge on">在线 ' . (int)$s['last_code'] . ' · ' . (int)$s['last_ms'] . 'ms</span>';
  199.                 elseif (!empty($s['last_status']) && $s['last_status'] === 'offline') echo '<span class="badge off">离线/异常 ' . (int)$s['last_code'] . '</span>';
  200.                 else echo '<span class="badge">未检测</span>';
  201.                 echo '<br><span class="muted">' . h(isset($s['last_check']) ? $s['last_check'] : '') . '</span></td>';
  202.                 echo '<td>';
  203.                 $cycle = (int)(isset($s['login_cycle']) ? $s['login_cycle'] : 0);
  204.                 if ($cycle > 0) {
  205.                     if (!empty($s['last_login_at'])) {
  206.                         $left = $cycle - (int)floor((time() - strtotime($s['last_login_at'])) / 86400);
  207.                         if ($left < 0) echo '<span class="badge danger">已超期 ' . abs($left) . ' 天</span>';
  208.                         elseif ($left <= 14) echo '<span class="badge warn">还剩 ' . $left . ' 天</span>';
  209.                         else echo '<span class="badge on">还剩 ' . $left . ' 天</span>';
  210.                         echo '<br><a href="' . h(self_url(array('act' => 'mark_login', 'id' => $s['id']))) . '">标记已登录</a>';
  211.                     } else echo '<span class="badge warn">未记登录时间</span><br><a href="' . h(self_url(array('act' => 'mark_login', 'id' => $s['id']))) . '">标记已登录</a>';
  212.                 } else echo '<span class="muted">—</span>';
  213.                 echo '</td>';
  214.                 echo '<td style="white-space:nowrap">';
  215.                 $u = preg_match('#^https?://#i', $s['url']) ? $s['url'] : 'https://' . $s['url'];
  216.                 echo '<a class="btn small" target="_blank" href="' . h($u) . '">打开网站</a> ';
  217.                 if (!empty($s['panel_url'])) echo '<a class="btn small gray" target="_blank" href="' . h($s['panel_url']) . '">后台</a> ';
  218.                 echo '<a class="btn small green" href="' . h(self_url(array('act' => 'files', 'id' => $s['id']))) . '">文件管理</a> ';
  219.                 echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'check', 'id' => $s['id']))) . '">检测</a> ';
  220.                 echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'site_edit', 'id' => $s['id']))) . '">编辑</a> ';
  221.                 echo '<a class="btn small red" href="' . h(self_url(array('act' => 'site_del', 'id' => $s['id']))) . '" onclick="return confirm(\'确定删除这个站点记录?(只是删管理台里的记录,不动空间上的网站)\')">删除</a>';
  222.                 echo '</td></tr>';
  223.             }
  224.             echo '</table>';
  225.         } else {
  226.             echo '<p class="muted">这个分类下还没有站点。</p>';
  227.         }
  228.         echo '<p style="margin:8px 0 0"><a class="btn small" href="' . h(self_url(array('act' => 'site_edit', 'provider' => $k))) . '">+ 在此分类添加站点</a></p>';
  229.         echo '</div>';
  230.     }
  231.     echo '</div></body></html>';
  232. }

  233. /* ---------- 站点编辑表单 ---------- */
  234. function page_site_edit($data, $PROVIDERS) {
  235.     $id = isset($_GET['id']) ? $_GET['id'] : '';
  236.     $s = null;
  237.     if ($id !== '') { $s = site_ref($data, $id); if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); } }
  238.     $pre = isset($_GET['provider']) ? $_GET['provider'] : '';
  239.     if ($s === null) {
  240.         $p = ($pre !== '' && isset($PROVIDERS[$pre])) ? $PROVIDERS[$pre] : $PROVIDERS['other'];
  241.         $s = array('id' => '', 'name' => '', 'url' => '', 'provider' => $pre !== '' ? $pre : 'other',
  242.             'panel_url' => '', 'proto' => $p['proto'], 'ftp_host' => $p['host'], 'ftp_port' => $p['port'],
  243.             'ftp_user' => '', 'ftp_pass' => '', 'root' => $p['root'], 'note' => '',
  244.             'login_cycle' => $p['cycle'], 'last_login_at' => '');
  245.     }
  246.     page_head($id !== '' ? '编辑站点' : '添加站点');
  247.     top_bar();
  248.     echo '<div class="card"><h3>' . ($id !== '' ? '编辑站点' : '添加站点') . '</h3>';
  249.     echo '<form method="post" action="' . h(self_url(array('act' => 'site_save'))) . '">';
  250.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
  251.     echo '<input type="hidden" name="id" value="' . h($s['id']) . '">';
  252.     echo '<div class="grid">';
  253.     echo '<div><label>站名 *</label><input type="text" name="name" required value="' . h($s['name']) . '"></div>';
  254.     echo '<div><label>网站地址(域名)*</label><input type="text" name="url" required placeholder="example.com 或 https://example.com" value="' . h($s['url']) . '"></div>';
  255.     echo '<div><label>所属免费空间(分类)</label><select name="provider" style="width:100%">';
  256.     foreach ($PROVIDERS as $k => $p) echo '<option value="' . h($k) . '"' . ($s['provider'] === $k ? ' selected' : '') . '>' . h($p['name']) . '</option>';
  257.     echo '</select></div>';
  258.     echo '<div><label>自定义分类名(选“其他/自定义”时填,可留空)</label><input type="text" name="provider_custom" value="' . (isset($PROVIDERS[$s['provider']]) ? '' : h($s['provider'])) . '"></div>';
  259.     echo '<div><label>空间后台地址</label><input type="text" name="panel_url" placeholder="https://…" value="' . h($s['panel_url']) . '"></div>';
  260.     echo '<div><label>登录保活周期(天,0 = 不提醒;serv00 是 90)</label><input type="text" name="login_cycle" value="' . h($s['login_cycle']) . '"></div>';
  261.     echo '<div><label>连接方式</label><select name="proto" style="width:100%">';
  262.     foreach (array('ftp' => 'FTP', 'ftps' => 'FTPS(加密 FTP)', 'sftp' => 'SFTP(SSH)') as $k => $v)
  263.         echo '<option value="' . $k . '"' . ($s['proto'] === $k ? ' selected' : '') . '>' . $v . '</option>';
  264.     echo '</select></div>';
  265.     echo '<div><label>FTP 主机</label><input type="text" name="ftp_host" placeholder="如 ftpperso.free.fr" value="' . h($s['ftp_host']) . '"></div>';
  266.     echo '<div><label>FTP 端口</label><input type="text" name="ftp_port" value="' . h($s['ftp_port']) . '"></div>';
  267.     echo '<div><label>FTP 用户名</label><input type="text" name="ftp_user" value="' . h($s['ftp_user']) . '"></div>';
  268.     echo '<div><label>FTP 密码' . ($id !== '' ? '(留空 = 不修改)' : '') . '</label><input type="password" name="ftp_pass" value=""></div>';
  269.     echo '<div><label>网站根目录(FTP 登录后相对路径,如 public_html,可留空)</label><input type="text" name="root" value="' . h($s['root']) . '"></div>';
  270.     echo '<div><label>备注</label><input type="text" name="note" value="' . h(isset($s['note']) ? $s['note'] : '') . '"></div>';
  271.     echo '</div>';
  272.     echo '<p><button class="btn" type="submit">保存</button> <a class="btn gray" href="' . h(self_url(array())) . '">返回</a></p>';
  273.     echo '<p class="muted">FTP 密码保存在同目录的数据文件里(该文件浏览器直接访问会被拦截)。管理台密码请设得足够强,并尽量走 HTTPS 打开。</p>';
  274.     echo '</form></div></div></body></html>';
  275. }

  276. function page_passwd() {
  277.     page_head('修改密码');
  278.     top_bar();
  279.     echo '<div class="card loginbox" style="margin:12px auto"><h3>修改管理密码</h3>';
  280.     echo '<form method="post" action="' . h(self_url(array('act' => 'passwd_save'))) . '">';
  281.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
  282.     echo '<p><label>当前密码</label><input type="password" name="old" required></p>';
  283.     echo '<p><label>新密码(至少 6 位)</label><input type="password" name="pw1" required></p>';
  284.     echo '<p><label>再输一遍新密码</label><input type="password" name="pw2" required></p>';
  285.     echo '<p><button class="btn" type="submit">修改</button></p></form></div></div></body></html>';
  286. }

  287. /* ---------- FTP 层(基于 curl,一个实现通吃 FTP/FTPS/SFTP) ---------- */
  288. function xftp_scheme($site) {
  289.     $p = strtolower($site['proto']);
  290.     if ($p === 'ftps') return 'ftps';
  291.     if ($p === 'sftp') return 'sftp';
  292.     return 'ftp';
  293. }
  294. function xftp_base($site) {
  295.     $port = (int)$site['ftp_port']; if ($port <= 0) $port = 21;
  296.     return xftp_scheme($site) . '://' . rawurlencode($site['ftp_user']) . ':' . rawurlencode($site['ftp_pass'])
  297.         . '@' . $site['ftp_host'] . ':' . $port;
  298. }
  299. function xftp_enc_path($path) {
  300.     if ($path === '') return '/';
  301.     $segs = array();
  302.     foreach (explode('/', $path) as $seg) { if ($seg !== '') $segs[] = rawurlencode($seg); }
  303.     return '/' . implode('/', $segs);
  304. }
  305. // 通用请求。$path 为相对 FTP 根的路径(不带根目录前缀,根目录在调用处拼)。
  306. // 返回 array(ok, body, err)
  307. function xftp_req($site, $path, $extra) {
  308.     $url = xftp_base($site) . xftp_enc_path($path);
  309.     $isDir = isset($extra['dir']) && $extra['dir'];
  310.     if ($isDir && substr($url, -1) !== '/') $url .= '/';
  311.     $ch = curl_init($url);
  312.     $opts = array(
  313.         CURLOPT_RETURNTRANSFER => true,
  314.         CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 12,
  315.         CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false,
  316.     );
  317.     if (xftp_scheme($site) === 'ftps') $opts[CURLOPT_USE_SSL] = CURLUSESSL_ALL;
  318.     if (!empty($extra['quote'])) $opts[CURLOPT_QUOTE] = $extra['quote'];
  319.     if (!empty($extra['postquote'])) $opts[CURLOPT_POSTQUOTE] = $extra['postquote'];
  320.     if (!empty($extra['upload_fp'])) {
  321.         $opts[CURLOPT_UPLOAD] = true;
  322.         $opts[CURLOPT_INFILE] = $extra['upload_fp'];
  323.         if (isset($extra['upload_size'])) $opts[CURLOPT_INFILESIZE] = $extra['upload_size'];
  324.     }
  325.     if (!empty($extra['nobody'])) $opts[CURLOPT_NOBODY] = true;
  326.     curl_setopt_array($ch, $opts);
  327.     $body = curl_exec($ch);
  328.     $err = curl_error($ch);
  329.     $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  330.     curl_close($ch);
  331.     $ok = ($err === '');
  332.     if (!$ok && $body === false) $body = '';
  333.     return array($ok, $body === false ? '' : $body, $err !== '' ? $err : ($ok ? '' : ('FTP 返回 ' . $code)));
  334. }
  335. function xftp_join($a, $b) {
  336.     $a = trim($a, '/'); $b = trim($b, '/');
  337.     if ($a === '') return $b;
  338.     if ($b === '') return $a;
  339.     return $a . '/' . $b;
  340. }
  341. // 列目录,返回 array(array(name,is_dir,size),...) 已按 目录在前+名称 排序
  342. function xftp_list($site, $dir) {
  343.     list($ok, $body, $err) = xftp_req($site, $dir, array('dir' => true));
  344.     if (!$ok) return array(false, $err);
  345.     $items = array();
  346.     foreach (preg_split('/\r\n|\r|\n/', trim($body)) as $line) {
  347.         if ($line === '') continue;
  348.         // Unix 格式: drwxr-xr-x 2 user group 4096 Nov 14 10:00 name
  349.         if (preg_match('/^([dl-])[rwx-]{9}\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\w+\s+\d+\s+[\d:]+\s+(.+)$/', $line, $m)) {
  350.             $name = trim($m[3]);
  351.             if ($name === '.' || $name === '..') continue;
  352.             $items[] = array('name' => $name, 'dir' => $m[1] === 'd', 'size' => (int)$m[2]);
  353.             continue;
  354.         }
  355.         // Windows 格式: 11-14-26 10:00AM <DIR> name / 11-14-26 10:00AM 1234 name
  356.         if (preg_match('/^\d{2}-\d{2}-\d{2}\s+\d{2}:\d{2}\s*[AP]M\s+(<DIR>|\d+)\s+(.+)$/i', $line, $m)) {
  357.             $name = trim($m[2]);
  358.             if ($name === '.' || $name === '..') continue;
  359.             $items[] = array('name' => $name, 'dir' => strtoupper($m[1]) === '<DIR>', 'size' => strtoupper($m[1]) === '<DIR>' ? 0 : (int)$m[1]);
  360.             continue;
  361.         }
  362.         // SFTP (OpenSSH ls 风格与 Unix 类似) 已覆盖;再兜底:整行就是名字的情况忽略
  363.     }
  364.     usort($items, function ($a, $b) {
  365.         if ($a['dir'] !== $b['dir']) return $a['dir'] ? -1 : 1;
  366.         return strcasecmp($a['name'], $b['name']);
  367.     });
  368.     return array(true, $items);
  369. }
  370. function xftp_download_str($site, $path) {
  371.     return xftp_req($site, $path, array());
  372. }
  373. function xftp_upload_fp($site, $path, $fp, $size) {
  374.     list($ok, $b, $err) = xftp_req($site, $path, array('upload_fp' => $fp, 'upload_size' => $size));
  375.     return array($ok, $err);
  376. }
  377. function xftp_upload_str($site, $path, $content) {
  378.     $fp = fopen('php://temp', 'r+');
  379.     fwrite($fp, $content);
  380.     rewind($fp);
  381.     $r = xftp_upload_fp($site, $path, $fp, strlen($content));
  382.     fclose($fp);
  383.     return $r;
  384. }
  385. function xftp_quote($site, $dirPath, $cmds) {
  386.     // 在 $dirPath 目录上执行 QUOTE 命令(命令在进入该目录后发出)
  387.     list($ok, $b, $err) = xftp_req($site, $dirPath, array('dir' => true, 'nobody' => true, 'quote' => $cmds));
  388.     return array($ok, $err);
  389. }
  390. function xftp_mkdir($site, $parent, $name) { return xftp_quote($site, $parent, array('MKD ' . $name)); }
  391. function xftp_delete_file($site, $parent, $name) { return xftp_quote($site, $parent, array('DELE ' . $name)); }
  392. function xftp_rmdir($site, $parent, $name) { return xftp_quote($site, $parent, array('RMD ' . $name)); }
  393. function xftp_rename($site, $parent, $from, $to) { return xftp_quote($site, $parent, array('RNFR ' . $from, 'RNTO ' . $to)); }
  394. function xftp_delete_recursive($site, $base, $name, $depth) {
  395.     if ($depth > 12) return array(false, '目录层级太深,已停止');
  396.     $full = xftp_join($base, $name);
  397.     list($ok, $items) = xftp_list($site, $full);
  398.     if ($ok) {
  399.         foreach ($items as $it) {
  400.             if ($it['dir']) { $r = xftp_delete_recursive($site, $full, $it['name'], $depth + 1); if (!$r[0]) return $r; }
  401.             else { $r = xftp_delete_file($site, $full, $it['name']); if (!$r[0]) return $r; }
  402.         }
  403.         return xftp_rmdir($site, $base, $name);
  404.     }
  405.     // 不是目录(列不出来)按文件删
  406.     return xftp_delete_file($site, $base, $name);
  407. }
  408. // 确保远程目录链存在(逐级 MKD,失败忽略,可能已存在)
  409. function xftp_ensure_dirs($site, $rootBase, $relDir) {
  410.     if ($relDir === '') return;
  411.     $cur = $rootBase;
  412.     foreach (explode('/', $relDir) as $seg) {
  413.         if ($seg === '') continue;
  414.         xftp_mkdir($site, $cur, $seg);
  415.         $cur = xftp_join($cur, $seg);
  416.     }
  417. }

  418. /* ---------- 文件管理页面 ---------- */
  419. function fm_site_or_die(&$data, $PROVIDERS) {
  420.     $id = isset($_REQUEST['id']) ? $_REQUEST['id'] : '';
  421.     $s = site_ref($data, $id);
  422.     if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); }
  423.     return $s;
  424. }
  425. function fm_base($site) { return clean_rel_path(isset($site['root']) ? $site['root'] : ''); }
  426. function fm_rel() { return clean_rel_path(isset($_REQUEST['path']) ? $_REQUEST['path'] : ''); }
  427. function fm_url($site, $rel, $extra) {
  428.     $p = array_merge(array('act' => 'files', 'id' => $site['id'], 'path' => $rel), $extra ? $extra : array());
  429.     return self_url($p);
  430. }
  431. function page_files($data, $PROVIDERS) {
  432.     $site = fm_site_or_die($data, $PROVIDERS);
  433.     page_head('文件管理');
  434.     top_bar();
  435.     echo '<p><a href="' . h(self_url(array())) . '">← 返回站点列表</a></p>';
  436.     echo '<div class="card"><h3 style="margin:4px 0">📁 ' . h($site['name']) . ' <span class="muted">' . h($site['url']) . '</span></h3>';
  437.     if (empty($site['ftp_host']) || empty($site['ftp_user']) || empty($site['ftp_pass'])) {
  438.         echo '<p>这个站还没填完整的 FTP 信息(主机 / 用户名 / 密码)。</p>';
  439.         echo '<p><a class="btn" href="' . h(self_url(array('act' => 'site_edit', 'id' => $site['id']))) . '">去编辑这个站点,补上 FTP 信息</a></p>';
  440.         echo '</div></div></body></html>';
  441.         return;
  442.     }
  443.     $root = fm_base($site);
  444.     $rel = fm_rel();
  445.     $abs = xftp_join($root, $rel);
  446.     // 面包屑
  447.     echo '<p class="muted">当前位置:<a href="' . h(fm_url($site, '', null)) . '">根目录</a>';
  448.     $acc = '';
  449.     if ($rel !== '') foreach (explode('/', $rel) as $seg) {
  450.         $acc = xftp_join($acc, $seg);
  451.         echo ' / <a href="' . h(fm_url($site, $acc, null)) . '">' . h($seg) . '</a>';
  452.     }
  453.     echo '</p>';
  454.     list($ok, $items) = xftp_list($site, $abs);
  455.     if (!$ok) {
  456.         echo '<div class="flash err">连接或读取目录失败:' . h($items) . '</div>';
  457.         echo '<p class="muted">常见原因:FTP 主机 / 端口 / 用户名密码不对;连接方式选错(比如 serv00 要用 SFTP、free.fr 要用 FTPS);根目录填错。可以回站点编辑里核对后重试。</p>';
  458.         echo '</div></div></body></html>';
  459.         return;
  460.     }
  461.     // 上传 / 新建
  462.     echo '<form method="post" enctype="multipart/form-data" action="' . h(self_url(array('act' => 'upload'))) . '" style="display:inline-block;margin-right:18px">';
  463.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '"><input type="hidden" name="path" value="' . h($rel) . '">';
  464.     echo '<input type="file" name="file" required> <button class="btn small" type="submit">上传到当前目录</button></form>';
  465.     echo '<form method="post" action="' . h(self_url(array('act' => 'mkdir'))) . '" style="display:inline-block">';
  466.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '"><input type="hidden" name="path" value="' . h($rel) . '">';
  467.     echo '<input type="text" name="name" placeholder="新文件夹名" required style="width:130px"> <button class="btn small gray" type="submit">新建文件夹</button></form>';

  468.     echo '<form method="post" action="' . h(self_url(array('act' => 'pack'))) . '">';
  469.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '"><input type="hidden" name="path" value="' . h($rel) . '">';
  470.     echo '<table style="margin-top:12px"><tr><th style="width:28px"></th><th>名称</th><th>大小</th><th>操作</th></tr>';
  471.     foreach ($items as $it) {
  472.         $name = $it['name'];
  473.         echo '<tr><td><input type="checkbox" name="sel[]" value="' . h($name) . '"></td>';
  474.         echo '<td>' . ($it['dir']
  475.             ? '📁 <a href="' . h(fm_url($site, xftp_join($rel, $name), null)) . '"><b>' . h($name) . '</b></a>'
  476.             : '📄 ' . h($name)) . '</td>';
  477.         echo '<td class="muted">' . ($it['dir'] ? '—' : fmt_size($it['size'])) . '</td>';
  478.         echo '<td style="white-space:nowrap">';
  479.         if (!$it['dir']) {
  480.             echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'dl', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">下载</a> ';
  481.             $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
  482.             if (in_array($ext, array('txt', 'php', 'html', 'htm', 'css', 'js', 'json', 'xml', 'md', 'htaccess', 'ini', 'yml', 'yaml', 'sql', 'log')) || $name === '.htaccess')
  483.                 echo '<a class="btn small" href="' . h(self_url(array('act' => 'file_edit', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">编辑</a> ';
  484.             if ($ext === 'zip') echo '<a class="btn small green" href="' . h(self_url(array('act' => 'unzip_form', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">解压</a> ';
  485.         }
  486.         echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'rename_form', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">改名</a> ';
  487.         echo '<a class="btn small red" href="' . h(self_url(array('act' => 'del', 'id' => $site['id'], 'path' => $rel, 'name' => $name, 'dir' => $it['dir'] ? 1 : 0, 'csrf' => csrf()))) . '" onclick="return confirm(\'确定删除 ' . h($name) . '?' . ($it['dir'] ? '文件夹会连里面的内容一起删掉!' : '') . '\')">删除</a>';
  488.         echo '</td></tr>';
  489.     }
  490.     echo '</table>';
  491.     echo '<p><button class="btn gray" type="submit">把勾选的打包成 zip 下载</button> ';
  492.     echo '<button class="btn gray" type="submit" name="all" value="1">打包当前整个文件夹下载</button></p></form>';
  493.     echo '</div></div></body></html>';
  494. }

  495. function page_file_edit($data, $PROVIDERS) {
  496.     $site = fm_site_or_die($data, $PROVIDERS);
  497.     $root = fm_base($site); $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
  498.     $abs = xftp_join(xftp_join($root, $rel), $name);
  499.     list($ok, $body, $err) = xftp_download_str($site, $abs);
  500.     page_head('编辑文件');
  501.     top_bar();
  502.     echo '<p><a href="' . h(fm_url($site, $rel, null)) . '">← 返回文件列表</a></p><div class="card">';
  503.     if (!$ok) { echo '<div class="flash err">读取文件失败:' . h($err) . '</div></div></div></body></html>'; return; }
  504.     if (strlen($body) > EDIT_MAX) { echo '<div class="flash err">文件超过 512KB,不适合在线编辑,请下载后改。</div></div></div></body></html>'; return; }
  505.     if (strpos($body, "\0") !== false) { echo '<div class="flash err">这是二进制文件,不能在线编辑。</div></div></div></body></html>'; return; }
  506.     echo '<h3>✏️ ' . h($name) . '</h3>';
  507.     echo '<form method="post" action="' . h(self_url(array('act' => 'file_save'))) . '">';
  508.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
  509.     echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="name" value="' . h($name) . '">';
  510.     echo '<textarea name="content" rows="26" style="width:100%;font-family:monospace;box-sizing:border-box">' . h($body) . '</textarea>';
  511.     echo '<p><button class="btn" type="submit">保存</button></p></form></div></div></body></html>';
  512. }

  513. function page_rename_form($data, $PROVIDERS) {
  514.     $site = fm_site_or_die($data, $PROVIDERS);
  515.     $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
  516.     page_head('改名'); top_bar();
  517.     echo '<div class="card loginbox" style="margin:12px auto"><h3>重命名:' . h($name) . '</h3>';
  518.     echo '<form method="post" action="' . h(self_url(array('act' => 'rename'))) . '">';
  519.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
  520.     echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="from" value="' . h($name) . '">';
  521.     echo '<p><input type="text" name="to" value="' . h($name) . '" required></p>';
  522.     echo '<p><button class="btn" type="submit">改名</button></p></form></div></div></body></html>';
  523. }

  524. function page_unzip_form($data, $PROVIDERS) {
  525.     $site = fm_site_or_die($data, $PROVIDERS);
  526.     $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
  527.     page_head('解压'); top_bar();
  528.     echo '<div class="card loginbox" style="margin:12px auto"><h3>解压:' . h($name) . '</h3>';
  529.     echo '<form method="post" action="' . h(self_url(array('act' => 'unzip'))) . '">';
  530.     echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
  531.     echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="name" value="' . h($name) . '">';
  532.     echo '<p><label><input type="radio" name="dest" value="here" checked> 解压到当前目录(主题包、程序包一般选这个,注意看包里结构)</label></p>';
  533.     echo '<p><label><input type="radio" name="dest" value="folder"> 解压到同名文件夹里(更稳妥,不会和现有文件混在一起)</label></p>';
  534.     echo '<p class="muted">注意:同名文件会被覆盖。限制:zip ≤ 25MB、文件数 ≤ 200、解压后 ≤ 50MB。</p>';
  535.     echo '<p><button class="btn green" type="submit" onclick="return confirm(\'确定解压?同名文件会被覆盖。\')">开始解压</button></p></form></div></div></body></html>';
  536. }

  537. /* ---------- 解压 / 打包 ---------- */
  538. function do_unzip($site, $PROVIDERS) {
  539.     $root = fm_base($site); $rel = fm_rel();
  540.     $name = basename(isset($_POST['name']) ? $_POST['name'] : '');
  541.     $destChoice = isset($_POST['dest']) ? $_POST['dest'] : 'here';
  542.     $absZip = xftp_join(xftp_join($root, $rel), $name);
  543.     list($ok, $zipBody, $err) = xftp_download_str($site, $absZip);
  544.     if (!$ok) { flash('下载 zip 失败:' . $err, false); redirect(fm_url($site, $rel, null)); }
  545.     if (strlen($zipBody) > UNZIP_MAX_ZIP) { flash('zip 超过 25MB 上限,已停止', false); redirect(fm_url($site, $rel, null)); }
  546.     $tmpZip = tempnam(sys_get_temp_dir(), 'unz');
  547.     file_put_contents($tmpZip, $zipBody);
  548.     $za = new ZipArchive();
  549.     if ($za->open($tmpZip) !== true) { unlink($tmpZip); flash('这个文件不是有效的 zip', false); redirect(fm_url($site, $rel, null)); }
  550.     if ($za->numFiles > UNZIP_MAX_FILES) { $za->close(); unlink($tmpZip); flash('zip 里文件数超过 200 个,已停止', false); redirect(fm_url($site, $rel, null)); }
  551.     $preset = provider_of($site, $PROVIDERS);
  552.     $maxSingle = (int)$preset['max_single'];
  553.     $total = 0; $entries = array();
  554.     for ($i = 0; $i < $za->numFiles; $i++) {
  555.         $st = $za->statIndex($i);
  556.         $en = str_replace('\\', '/', $st['name']);
  557.         if (substr($en, -1) === '/') continue;
  558.         if (strpos($en, '..') !== false || substr($en, 0, 1) === '/') { $za->close(); unlink($tmpZip); flash('zip 里有不安全的路径(' . $en . '),已停止', false); redirect(fm_url($site, $rel, null)); }
  559.         $total += $st['size'];
  560.         if ($maxSingle > 0 && $st['size'] > $maxSingle) { $za->close(); unlink($tmpZip); flash('文件 ' . $en . ' 超过这家空间的单文件上限(' . fmt_size($maxSingle) . '),已停止', false); redirect(fm_url($site, $rel, null)); }
  561.         $entries[] = $en;
  562.     }
  563.     if ($total > UNZIP_MAX_TOTAL) { $za->close(); unlink($tmpZip); flash('解压后总大小超过 50MB,已停止', false); redirect(fm_url($site, $rel, null)); }
  564.     $tmpDir = sys_get_temp_dir() . '/unz' . uniqid();
  565.     mkdir($tmpDir);
  566.     $za->extractTo($tmpDir);
  567.     $za->close(); unlink($tmpZip);
  568.     $destRel = $rel;
  569.     if ($destChoice === 'folder') $destRel = xftp_join($rel, pathinfo($name, PATHINFO_FILENAME));
  570.     $destAbs = xftp_join($root, $destRel);
  571.     $okCount = 0; $fail = array();
  572.     foreach ($entries as $en) {
  573.         $local = $tmpDir . '/' . $en;
  574.         if (!file_exists($local)) continue;
  575.         $dirPart = trim(dirname($en), '.');
  576.         if ($dirPart !== '' && $dirPart !== '.') xftp_ensure_dirs($site, $destAbs, $dirPart);
  577.         $target = xftp_join($destAbs, $en);
  578.         $fp = fopen($local, 'rb');
  579.         list($uok, $uerr) = xftp_upload_fp($site, $target, $fp, filesize($local));
  580.         fclose($fp);
  581.         if ($uok) $okCount++; else $fail[] = $en . '(' . $uerr . ')';
  582.         unlink($local);
  583.     }
  584.     // 清理临时目录
  585.     foreach (array_reverse($entries) as $en) { $d = $tmpDir . '/' . trim(dirname($en), '.'); if (is_dir($d)) @rmdir($d); }
  586.     @rmdir($tmpDir);
  587.     if ($fail) flash('解压完成 ' . $okCount . ' 个文件,失败 ' . count($fail) . ' 个:' . implode(';', array_slice($fail, 0, 3)), false);
  588.     else flash('解压完成,共传上 ' . $okCount . ' 个文件到 ' . ($destChoice === 'folder' ? '同名文件夹' : '当前目录'));
  589.     redirect(fm_url($site, $destRel, null));
  590. }

  591. function collect_remote($site, $absDir, $prefix, $tmpDir, &$count, &$total) {
  592.     list($ok, $items) = xftp_list($site, $absDir);
  593.     if (!$ok) return false;
  594.     foreach ($items as $it) {
  595.         $relName = $prefix === '' ? $it['name'] : $prefix . '/' . $it['name'];
  596.         if ($it['dir']) {
  597.             if (!collect_remote($site, xftp_join($absDir, $it['name']), $relName, $tmpDir, $count, $total)) return false;
  598.         } else {
  599.             $count++; $total += $it['size'];
  600.             if ($count > UNZIP_MAX_FILES || $total > UNZIP_MAX_TOTAL) return false;
  601.             list($dok, $body) = xftp_download_str($site, xftp_join($absDir, $it['name']));
  602.             if (!$dok) return false;
  603.             $localDir = $tmpDir . '/' . dirname($relName);
  604.             if (!is_dir($localDir)) mkdir($localDir, 0777, true);
  605.             file_put_contents($tmpDir . '/' . $relName, $body);
  606.         }
  607.     }
  608.     return true;
  609. }
  610. function do_pack($site) {
  611.     $root = fm_base($site); $rel = fm_rel(); $abs = xftp_join($root, $rel);
  612.     $sel = array();
  613.     if (!empty($_POST['all'])) {
  614.         list($ok, $items) = xftp_list($site, $abs);
  615.         if ($ok) foreach ($items as $it) $sel[] = $it['name'];
  616.     } elseif (!empty($_POST['sel']) && is_array($_POST['sel'])) {
  617.         foreach ($_POST['sel'] as $n) $sel[] = basename($n);
  618.     }
  619.     if (!$sel) { flash('先勾选要打包的文件,或点“打包当前整个文件夹”', false); redirect(fm_url($site, $rel, null)); }
  620.     $tmpDir = sys_get_temp_dir() . '/pak' . uniqid();
  621.     mkdir($tmpDir);
  622.     $count = 0; $total = 0; $good = true;
  623.     foreach ($sel as $name) {
  624.         list($ok, $items) = xftp_list($site, xftp_join($abs, $name));
  625.         if ($ok) { // 目录
  626.             if (!collect_remote($site, xftp_join($abs, $name), $name, $tmpDir, $count, $total)) { $good = false; break; }
  627.         } else {     // 文件
  628.             $count++;
  629.             list($dok, $body) = xftp_download_str($site, xftp_join($abs, $name));
  630.             if (!$dok) { $good = false; break; }
  631.             $total += strlen($body);
  632.             if ($count > UNZIP_MAX_FILES || $total > UNZIP_MAX_TOTAL) { $good = false; break; }
  633.             file_put_contents($tmpDir . '/' . $name, $body);
  634.         }
  635.     }
  636.     if (!$good || $count === 0) { flash('打包失败:可能超过上限(200 个文件 / 50MB)或读取失败', false); redirect(fm_url($site, $rel, null)); }
  637.     $zipPath = $tmpDir . '.zip';
  638.     $za = new ZipArchive();
  639.     $za->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
  640.     $packed = array();
  641.     $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, FilesystemIterator::SKIP_DOTS));
  642.     foreach ($rii as $f) { if ($f->isFile()) { $packed[] = $f->getPathname(); $za->addFile($f->getPathname(), substr($f->getPathname(), strlen($tmpDir) + 1)); } }
  643.     $za->close();
  644.     foreach ($packed as $pf) unlink($pf);
  645.     header('Content-Type: application/zip');
  646.     header('Content-Disposition: attachment; filename="' . preg_replace('/[^A-Za-z0-9_.-]/', '_', $site['name']) . '-backup.zip"');
  647.     header('Content-Length: ' . filesize($zipPath));
  648.     readfile($zipPath);
  649.     unlink($zipPath);
  650.     exit;
  651. }

  652. /* ---------- 主流程 ---------- */
  653. $data = data_load();
  654. $a = act();

  655. if ($a === 'setup' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  656.     csrf_check();
  657.     if ($data['admin_hash'] !== '') { flash('已经设置过密码了', false); redirect(self_url(array())); }
  658.     $pw1 = isset($_POST['pw1']) ? $_POST['pw1'] : ''; $pw2 = isset($_POST['pw2']) ? $_POST['pw2'] : '';
  659.     if (strlen($pw1) < 6) { flash('密码至少 6 位', false); redirect(self_url(array())); }
  660.     if ($pw1 !== $pw2) { flash('两次输入的密码不一致', false); redirect(self_url(array())); }
  661.     $data['admin_hash'] = password_hash($pw1, PASSWORD_DEFAULT);
  662.     data_save($data);
  663.     $_SESSION['auth'] = 1;
  664.     flash('密码设置好了,欢迎使用');
  665.     redirect(self_url(array()));
  666. }
  667. if ($a === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  668.     csrf_check();
  669.     $pw = isset($_POST['pw']) ? $_POST['pw'] : '';
  670.     if ($data['admin_hash'] !== '' && password_verify($pw, $data['admin_hash'])) {
  671.         session_regenerate_id(true);
  672.         $_SESSION['auth'] = 1;
  673.         redirect(self_url(array()));
  674.     }
  675.     flash('密码不对', false);
  676.     redirect(self_url(array()));
  677. }
  678. if ($a === 'logout') { $_SESSION = array(); session_destroy(); redirect(self_url(array())); }

  679. require_login($data);
  680. csrf_check();

  681. if ($a === 'passwd') { page_passwd(); exit; }
  682. if ($a === 'passwd_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  683.     $old = isset($_POST['old']) ? $_POST['old'] : '';
  684.     $pw1 = isset($_POST['pw1']) ? $_POST['pw1'] : ''; $pw2 = isset($_POST['pw2']) ? $_POST['pw2'] : '';
  685.     if (!password_verify($old, $data['admin_hash'])) { flash('当前密码不对', false); redirect(self_url(array('act' => 'passwd'))); }
  686.     if (strlen($pw1) < 6 || $pw1 !== $pw2) { flash('新密码至少 6 位且两次一致', false); redirect(self_url(array('act' => 'passwd'))); }
  687.     $data['admin_hash'] = password_hash($pw1, PASSWORD_DEFAULT);
  688.     data_save($data);
  689.     flash('密码已修改');
  690.     redirect(self_url(array()));
  691. }
  692. if ($a === 'site_edit') { page_site_edit($data, $PROVIDERS); exit; }
  693. if ($a === 'site_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  694.     $id = isset($_POST['id']) ? $_POST['id'] : '';
  695.     $prov = isset($_POST['provider']) ? $_POST['provider'] : 'other';
  696.     if ($prov === 'other' && !empty($_POST['provider_custom'])) $prov = trim($_POST['provider_custom']);
  697.     $fields = array(
  698.         'name' => trim(isset($_POST['name']) ? $_POST['name'] : ''),
  699.         'url' => trim(isset($_POST['url']) ? $_POST['url'] : ''),
  700.         'provider' => $prov,
  701.         'panel_url' => trim(isset($_POST['panel_url']) ? $_POST['panel_url'] : ''),
  702.         'proto' => isset($_POST['proto']) ? $_POST['proto'] : 'ftp',
  703.         'ftp_host' => trim(isset($_POST['ftp_host']) ? $_POST['ftp_host'] : ''),
  704.         'ftp_port' => (int)(isset($_POST['ftp_port']) ? $_POST['ftp_port'] : 21),
  705.         'ftp_user' => trim(isset($_POST['ftp_user']) ? $_POST['ftp_user'] : ''),
  706.         'root' => clean_rel_path(isset($_POST['root']) ? $_POST['root'] : ''),
  707.         'note' => trim(isset($_POST['note']) ? $_POST['note'] : ''),
  708.         'login_cycle' => (int)(isset($_POST['login_cycle']) ? $_POST['login_cycle'] : 0),
  709.     );
  710.     if ($fields['name'] === '' || $fields['url'] === '') { flash('站名和网址必填', false); redirect(self_url(array('act' => 'site_edit'))); }
  711.     if ($id !== '') {
  712.         $s = &site_ref($data, $id);
  713.         if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); }
  714.         foreach ($fields as $k => $v) $s[$k] = $v;
  715.         if (!empty($_POST['ftp_pass'])) $s['ftp_pass'] = $_POST['ftp_pass'];
  716.         data_save($data);
  717.         flash('站点已保存');
  718.     } else {
  719.         $fields['id'] = bin2hex(random_bytes(6));
  720.         $fields['ftp_pass'] = isset($_POST['ftp_pass']) ? $_POST['ftp_pass'] : '';
  721.         $fields['last_login_at'] = ''; $fields['last_status'] = ''; $fields['last_code'] = 0; $fields['last_ms'] = 0; $fields['last_check'] = '';
  722.         $data['sites'][] = $fields;
  723.         data_save($data);
  724.         flash('站点已添加');
  725.     }
  726.     redirect(self_url(array()));
  727. }
  728. if ($a === 'site_del') {
  729.     $id = isset($_GET['id']) ? $_GET['id'] : '';
  730.     $data['sites'] = array_values(array_filter($data['sites'], function ($s) use ($id) { return $s['id'] !== $id; }));
  731.     data_save($data);
  732.     flash('站点记录已删除');
  733.     redirect(self_url(array()));
  734. }
  735. if ($a === 'mark_login') {
  736.     $s = &site_ref($data, isset($_GET['id']) ? $_GET['id'] : '');
  737.     if ($s !== null) { $s['last_login_at'] = date('Y-m-d'); data_save($data); flash('已标记今天登录过 ' . $s['name']); }
  738.     redirect(self_url(array()));
  739. }
  740. if ($a === 'check') {
  741.     $s = &site_ref($data, isset($_GET['id']) ? $_GET['id'] : '');
  742.     if ($s !== null) { check_site($s); data_save($data); flash('检测完成:' . $s['name'] . ' ' . ($s['last_status'] === 'online' ? '在线' : '离线/异常')); }
  743.     redirect(self_url(array()));
  744. }
  745. if ($a === 'check_all') {
  746.     foreach ($data['sites'] as $i => $s) { check_site($data['sites'][$i]); }
  747.     data_save($data);
  748.     flash('全部检测完成');
  749.     redirect(self_url(array()));
  750. }

  751. /* 文件管理相关动作 */
  752. if ($a === 'files') { page_files($data, $PROVIDERS); exit; }
  753. if ($a === 'file_edit') { page_file_edit($data, $PROVIDERS); exit; }
  754. if ($a === 'rename_form') { page_rename_form($data, $PROVIDERS); exit; }
  755. if ($a === 'unzip_form') { page_unzip_form($data, $PROVIDERS); exit; }
  756. if ($a === 'dl') {
  757.     $site = fm_site_or_die($data, $PROVIDERS);
  758.     $abs = xftp_join(xftp_join(fm_base($site), fm_rel()), basename(isset($_GET['name']) ? $_GET['name'] : ''));
  759.     list($ok, $body, $err) = xftp_download_str($site, $abs);
  760.     if (!$ok) { flash('下载失败:' . $err, false); redirect(fm_url($site, fm_rel(), null)); }
  761.     header('Content-Type: application/octet-stream');
  762.     header('Content-Disposition: attachment; filename="' . basename(isset($_GET['name']) ? $_GET['name'] : 'file') . '"');
  763.     header('Content-Length: ' . strlen($body));
  764.     echo $body; exit;
  765. }
  766. if ($a === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  767.     $site = fm_site_or_die($data, $PROVIDERS);
  768.     $rel = fm_rel();
  769.     if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { flash('上传失败:没有收到文件', false); redirect(fm_url($site, $rel, null)); }
  770.     $name = basename($_FILES['file']['name']);
  771.     $preset = provider_of($site, $PROVIDERS);
  772.     if ((int)$preset['max_single'] > 0 && $_FILES['file']['size'] > (int)$preset['max_single']) {
  773.         flash('文件超过这家空间的单文件上限(' . fmt_size((int)$preset['max_single']) . '),已拦截', false); redirect(fm_url($site, $rel, null));
  774.     }
  775.     $fp = fopen($_FILES['file']['tmp_name'], 'rb');
  776.     list($ok, $err) = xftp_upload_fp($site, xftp_join(xftp_join(fm_base($site), $rel), $name), $fp, $_FILES['file']['size']);
  777.     fclose($fp);
  778.     flash($ok ? ('已上传 ' . $name) : ('上传失败:' . $err), $ok);
  779.     redirect(fm_url($site, $rel, null));
  780. }
  781. if ($a === 'mkdir' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  782.     $site = fm_site_or_die($data, $PROVIDERS);
  783.     $rel = fm_rel(); $name = basename(trim(isset($_POST['name']) ? $_POST['name'] : ''));
  784.     list($ok, $err) = xftp_mkdir($site, xftp_join(fm_base($site), $rel), $name);
  785.     flash($ok ? ('已新建文件夹 ' . $name) : ('新建失败:' . $err), $ok);
  786.     redirect(fm_url($site, $rel, null));
  787. }
  788. if ($a === 'rename' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  789.     $site = fm_site_or_die($data, $PROVIDERS);
  790.     $rel = fm_rel();
  791.     $from = basename(isset($_POST['from']) ? $_POST['from'] : '');
  792.     $to = basename(trim(isset($_POST['to']) ? $_POST['to'] : ''));
  793.     list($ok, $err) = xftp_rename($site, xftp_join(fm_base($site), $rel), $from, $to);
  794.     flash($ok ? '已改名' : ('改名失败:' . $err), $ok);
  795.     redirect(fm_url($site, $rel, null));
  796. }
  797. if ($a === 'file_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  798.     $site = fm_site_or_die($data, $PROVIDERS);
  799.     $rel = fm_rel(); $name = basename(isset($_POST['name']) ? $_POST['name'] : '');
  800.     list($ok, $err) = xftp_upload_str($site, xftp_join(xftp_join(fm_base($site), $rel), $name), isset($_POST['content']) ? $_POST['content'] : '');
  801.     flash($ok ? ('已保存 ' . $name) : ('保存失败:' . $err), $ok);
  802.     redirect(fm_url($site, $rel, null));
  803. }
  804. if ($a === 'del') {
  805.     // 删除走 GET 但带 csrf 令牌校验
  806.     if (empty($_GET['csrf']) || $_GET['csrf'] !== csrf()) { die('请求校验失败'); }
  807.     $site = fm_site_or_die($data, $PROVIDERS);
  808.     $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
  809.     $base = xftp_join(fm_base($site), $rel);
  810.     if (!empty($_GET['dir'])) list($ok, $err) = xftp_delete_recursive($site, $base, $name, 0);
  811.     else list($ok, $err) = xftp_delete_file($site, $base, $name);
  812.     flash($ok ? ('已删除 ' . $name) : ('删除失败:' . $err), $ok);
  813.     redirect(fm_url($site, $rel, null));
  814. }
  815. if ($a === 'unzip' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  816.     $site = fm_site_or_die($data, $PROVIDERS);
  817.     do_unzip($site, $PROVIDERS);
  818.     exit;
  819. }
  820. if ($a === 'pack' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  821.     $site = fm_site_or_die($data, $PROVIDERS);
  822.     do_pack($site);
  823.     exit;
  824. }

  825. page_sites($data, $PROVIDERS);
复制代码


发表于 昨天 22:58 | 显示全部楼层
这个不错
 楼主| 发表于 昨天 23:01 | 显示全部楼层
检测那里得修改一下 默认检测https 莫斯 free.fr 填入 **.free.fr 就检测异常了 其实网站是正常
发表于 7 小时前 | 显示全部楼层
zhulisui 发表于 2026-11-14 23:01
检测那里得修改一下 默认检测https 莫斯 free.fr 填入 **.free.fr 就检测异常了 其实网站是正常 ...

下载文件包跟下面的源代码啥关系?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|小黑屋|免费吧论坛

GMT+8, 2026-11-15 08:08 , Processed in 0.076834 second(s), 3 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表