|
|
- <?php
- /*
- * 多站管理台(独立 PHP 版)
- * 功能:登录保护 / 按免费空间分类管理站点(增删改)/ 在线检测 /
- * FTP·FTPS·SFTP 文件管理(浏览、上传、下载、编辑、改名、删除、新建文件夹)/
- * 在线解压 zip / 打包下载
- * 部署:把 manager.php 传到任意支持 PHP 的空间,浏览器打开即可。
- * 数据:同目录 manager-data.php(首行是退出语句,浏览器直接访问也拿不到内容)。
- */
- if (version_compare(PHP_VERSION, '7.0.0', '<')) { die('需要 PHP 7.0 以上'); }
- session_start();
- define('DATA_FILE', __DIR__ . '/manager-data.php');
- define('UNZIP_MAX_ZIP', 25 * 1024 * 1024); // zip 文件最大 25MB
- define('UNZIP_MAX_FILES', 200); // 最多解压 200 个文件
- define('UNZIP_MAX_TOTAL', 50 * 1024 * 1024); // 解压后总大小最大 50MB
- define('EDIT_MAX', 512 * 1024); // 在线编辑最大 512KB
- $PROVIDERS = array(
- 'beget' => array('name' => 'beget.com', 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => '免费版 1 个站点、1 个 FTP 账号'),
- 'freehostia' => array('name' => 'freehostia.com', 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 2097152, 'note' => '免费版单文件上限 2MB,超了传不上去'),
- 'runhosting' => array('name' => 'runhosting.com', 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => '免费版 1GB、FTP 账号 1 个'),
- 'freefr' => array('name' => 'free.fr', 'proto' => 'ftps', 'port' => 21, 'host' => 'ftpperso.free.fr', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => 'FTPS 加密;同一 IP 连接限速,文件操作请勿连续猛点'),
- 'endless' => array('name' => 'theendlessweb.com','proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => 'DirectAdmin 面板'),
- 'xrea' => array('name' => 'xrea.com', 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => 'public_html', 'cycle' => 0, 'max_single' => 0, 'note' => '网站根目录一般是 public_html;免费版页面有广告'),
- 'serv00' => array('name' => 'serv00.com', 'proto' => 'sftp', 'port' => 22, 'host' => '', 'root' => '', 'cycle' => 90, 'max_single' => 0, 'note' => 'SFTP;每 90 天必须登录一次面板或 SSH,否则账号被删'),
- 'other' => array('name' => '其他/自定义', 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => ''),
- );
- /* ---------- 数据层 ---------- */
- function data_load() {
- if (!file_exists(DATA_FILE)) return array('admin_hash' => '', 'sites' => array());
- $raw = file_get_contents(DATA_FILE);
- $pos = strpos($raw, "\n");
- if ($pos === false) return array('admin_hash' => '', 'sites' => array());
- $d = json_decode(substr($raw, $pos + 1), true);
- if (!is_array($d) || !isset($d['sites'])) return array('admin_hash' => '', 'sites' => array());
- return $d;
- }
- function data_save($d) {
- $head = "<?php exit('Access denied'); ?>\n";
- file_put_contents(DATA_FILE, $head . json_encode($d, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
- }
- function &site_ref(&$data, $id) {
- foreach ($data['sites'] as $i => $s) { if ($s['id'] === $id) return $data['sites'][$i]; }
- $null = null; return $null;
- }
- /* ---------- 工具 ---------- */
- function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
- function act() { return isset($_REQUEST['act']) ? $_REQUEST['act'] : ''; }
- function csrf() { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16)); return $_SESSION['csrf']; }
- function csrf_check() {
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (empty($_POST['csrf']) || $_POST['csrf'] !== csrf()) { die('请求校验失败,请返回重试'); }
- }
- }
- function redirect($url) { header('Location: ' . $url); exit; }
- function self_url($params) {
- return basename(__FILE__) . ($params ? '?' . http_build_query($params) : '');
- }
- function flash($msg, $ok = true) { $_SESSION['flash'] = array($msg, $ok); }
- function clean_rel_path($p) {
- $p = str_replace('\\', '/', (string)$p);
- $parts = array();
- foreach (explode('/', $p) as $seg) {
- if ($seg === '' || $seg === '.') continue;
- if ($seg === '..') { array_pop($parts); continue; }
- $parts[] = $seg;
- }
- return implode('/', $parts);
- }
- function fmt_size($n) {
- if ($n < 1024) return $n . ' B';
- if ($n < 1024 * 1024) return round($n / 1024, 1) . ' KB';
- return round($n / 1024 / 1024, 2) . ' MB';
- }
- function provider_of($site, $PROVIDERS) {
- $k = $site['provider'];
- if (isset($PROVIDERS[$k])) return $PROVIDERS[$k];
- return array('name' => $k, 'proto' => 'ftp', 'port' => 21, 'host' => '', 'root' => '', 'cycle' => 0, 'max_single' => 0, 'note' => '');
- }
- /* ---------- 登录 ---------- */
- function is_login() { return !empty($_SESSION['auth']); }
- function require_login($data) {
- if ($data['admin_hash'] === '') { page_setup(); exit; }
- if (!is_login()) { page_login(); exit; }
- }
- function page_head($title) {
- echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
- echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
- echo '<title>' . h($title) . ' · 多站管理台</title><style>';
- echo 'body{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;margin:0;background:#f4f5f7;color:#222}'
- . '.wrap{max-width:1080px;margin:0 auto;padding:16px}'
- . '.bar{background:#1f2937;color:#fff;padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}'
- . '.bar a{color:#cbd5e1;text-decoration:none}.bar b{color:#fff}'
- . '.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:14px;margin:12px 0}'
- . '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}'
- . 'input,select,textarea{padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:14px}'
- . 'input[type=text],input[type=password],input[type=url],textarea{width:100%;box-sizing:border-box}'
- . '.btn{display:inline-block;background:#2563eb;color:#fff;border:none;padding:7px 14px;border-radius:6px;cursor:pointer;text-decoration:none;font-size:14px}'
- . '.btn.gray{background:#6b7280}.btn.red{background:#dc2626}.btn.green{background:#059669}.btn.small{padding:3px 9px;font-size:13px}'
- . '.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}'
- . '.muted{color:#6b7280;font-size:13px}.badge{display:inline-block;padding:1px 8px;border-radius:99px;font-size:12px;background:#e5e7eb}'
- . '.badge.on{background:#d1fae5}.badge.off{background:#fee2e2}.badge.warn{background:#fef3c7;color:#92400e}.badge.danger{background:#fee2e2;color:#991b1b}'
- . '.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}@media(max-width:640px){.grid{grid-template-columns:1fr}}'
- . 'label{font-size:13px;color:#374151;display:block;margin-bottom:3px}'
- . '.loginbox{max-width:380px;margin:60px auto}';
- echo '</style></head><body>';
- }
- function show_flash() {
- if (!empty($_SESSION['flash'])) {
- $f = $_SESSION['flash']; unset($_SESSION['flash']);
- echo '<div class="flash ' . ($f[1] ? 'ok' : 'err') . '">' . h($f[0]) . '</div>';
- }
- }
- function page_setup() {
- page_head('设置密码');
- echo '<div class="wrap"><div class="card loginbox"><h2>🔐 第一次使用:设置管理密码</h2>';
- show_flash();
- echo '<form method="post" action="' . h(self_url(array('act' => 'setup'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
- echo '<p><label>管理密码(至少 6 位)</label><input type="password" name="pw1" required></p>';
- echo '<p><label>再输一遍</label><input type="password" name="pw2" required></p>';
- echo '<p><button class="btn" type="submit">设置并进入</button></p>';
- echo '<p class="muted">这个密码忘了没有找回入口,请务必记牢。密码只存加密哈希。</p>';
- echo '</form></div></div></body></html>';
- }
- function page_login() {
- page_head('登录');
- echo '<div class="wrap"><div class="card loginbox"><h2>🔐 多站管理台 · 登录</h2>';
- show_flash();
- echo '<form method="post" action="' . h(self_url(array('act' => 'login'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
- echo '<p><label>管理密码</label><input type="password" name="pw" required autofocus></p>';
- echo '<p><button class="btn" type="submit">登录</button></p>';
- echo '</form></div></div></body></html>';
- }
- function top_bar() {
- echo '<div class="bar"><b>🗂 多站管理台</b>';
- echo '<a href="' . h(self_url(array())) . '">站点列表</a>';
- echo '<span style="flex:1"></span><span>已登录</span>';
- echo '<a href="' . h(self_url(array('act' => 'passwd'))) . '">修改密码</a>';
- echo '<a href="' . h(self_url(array('act' => 'logout'))) . '">退出</a></div>';
- echo '<div class="wrap">';
- show_flash();
- }
- /* ---------- 在线检测 ---------- */
- function check_site(&$site) {
- $url = trim($site['url']);
- if ($url === '') { $site['last_status'] = '未填网址'; return; }
- if (!preg_match('#^https?://#i', $url)) $url = 'https://' . $url;
- $t0 = microtime(true);
- $ch = curl_init($url);
- curl_setopt_array($ch, array(
- CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => 12, CURLOPT_CONNECTTIMEOUT => 6,
- CURLOPT_USERAGENT => 'SitePanel-Checker/1.0', CURLOPT_SSL_VERIFYPEER => false,
- ));
- curl_exec($ch);
- $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
- $err = curl_error($ch);
- curl_close($ch);
- $site['last_ms'] = (int)round((microtime(true) - $t0) * 1000);
- $site['last_code'] = $code;
- $site['last_check'] = date('Y-m-d H:i:s');
- $site['last_status'] = ($err === '' && $code >= 200 && $code < 400) ? 'online' : 'offline';
- if ($err !== '') $site['last_status'] = 'offline';
- }
- /* ---------- 站点列表(按服务商分类) ---------- */
- function page_sites($data, $PROVIDERS) {
- page_head('站点列表');
- top_bar();
- $q = isset($_GET['q']) ? trim($_GET['q']) : '';
- echo '<form method="get" style="margin:12px 0;display:flex;gap:8px">';
- echo '<input type="text" name="q" placeholder="按站名或域名搜索" value="' . h($q) . '" style="flex:1">';
- echo '<button class="btn" type="submit">搜索</button>';
- echo '<a class="btn green" href="' . h(self_url(array('act' => 'site_edit'))) . '">+ 添加站点</a>';
- echo '<a class="btn gray" href="' . h(self_url(array('act' => 'check_all'))) . '">检测全部</a></form>';
- $total = count($data['sites']);
- $on = 0; foreach ($data['sites'] as $s) { if (isset($s['last_status']) && $s['last_status'] === 'online') $on++; }
- echo '<p class="muted">共 ' . $total . ' 个站点 · 在线 ' . $on . ' 个</p>';
- // 分组:预置服务商顺序在前,其余自定义服务商随后
- $groups = array();
- foreach ($PROVIDERS as $k => $p) $groups[$k] = array();
- foreach ($data['sites'] as $s) {
- if ($q !== '' && stripos($s['name'], $q) === false && stripos($s['url'], $q) === false) continue;
- $k = $s['provider'];
- if (!isset($groups[$k])) $groups[$k] = array();
- $groups[$k][] = $s;
- }
- foreach ($groups as $k => $sites) {
- $preset = isset($PROVIDERS[$k]) ? $PROVIDERS[$k] : null;
- $gname = $preset ? $preset['name'] : $k;
- echo '<div class="card"><h3 style="margin:4px 0">' . h($gname) . ' <span class="badge">' . count($sites) . ' 个站</span></h3>';
- if ($preset && $preset['note'] !== '') echo '<p class="muted" style="margin:2px 0 8px">' . h($preset['note']) . '</p>';
- if ($sites) {
- echo '<table><tr><th>站点</th><th>状态</th><th>登录期限</th><th>操作</th></tr>';
- foreach ($sites as $s) {
- echo '<tr><td><b>' . h($s['name']) . '</b><br><span class="muted">' . h($s['url']) . '</span></td>';
- echo '<td>';
- if (!empty($s['last_status']) && $s['last_status'] === 'online') echo '<span class="badge on">在线 ' . (int)$s['last_code'] . ' · ' . (int)$s['last_ms'] . 'ms</span>';
- elseif (!empty($s['last_status']) && $s['last_status'] === 'offline') echo '<span class="badge off">离线/异常 ' . (int)$s['last_code'] . '</span>';
- else echo '<span class="badge">未检测</span>';
- echo '<br><span class="muted">' . h(isset($s['last_check']) ? $s['last_check'] : '') . '</span></td>';
- echo '<td>';
- $cycle = (int)(isset($s['login_cycle']) ? $s['login_cycle'] : 0);
- if ($cycle > 0) {
- if (!empty($s['last_login_at'])) {
- $left = $cycle - (int)floor((time() - strtotime($s['last_login_at'])) / 86400);
- if ($left < 0) echo '<span class="badge danger">已超期 ' . abs($left) . ' 天</span>';
- elseif ($left <= 14) echo '<span class="badge warn">还剩 ' . $left . ' 天</span>';
- else echo '<span class="badge on">还剩 ' . $left . ' 天</span>';
- echo '<br><a href="' . h(self_url(array('act' => 'mark_login', 'id' => $s['id']))) . '">标记已登录</a>';
- } else echo '<span class="badge warn">未记登录时间</span><br><a href="' . h(self_url(array('act' => 'mark_login', 'id' => $s['id']))) . '">标记已登录</a>';
- } else echo '<span class="muted">—</span>';
- echo '</td>';
- echo '<td style="white-space:nowrap">';
- $u = preg_match('#^https?://#i', $s['url']) ? $s['url'] : 'https://' . $s['url'];
- echo '<a class="btn small" target="_blank" href="' . h($u) . '">打开网站</a> ';
- if (!empty($s['panel_url'])) echo '<a class="btn small gray" target="_blank" href="' . h($s['panel_url']) . '">后台</a> ';
- echo '<a class="btn small green" href="' . h(self_url(array('act' => 'files', 'id' => $s['id']))) . '">文件管理</a> ';
- echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'check', 'id' => $s['id']))) . '">检测</a> ';
- echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'site_edit', 'id' => $s['id']))) . '">编辑</a> ';
- echo '<a class="btn small red" href="' . h(self_url(array('act' => 'site_del', 'id' => $s['id']))) . '" onclick="return confirm(\'确定删除这个站点记录?(只是删管理台里的记录,不动空间上的网站)\')">删除</a>';
- echo '</td></tr>';
- }
- echo '</table>';
- } else {
- echo '<p class="muted">这个分类下还没有站点。</p>';
- }
- echo '<p style="margin:8px 0 0"><a class="btn small" href="' . h(self_url(array('act' => 'site_edit', 'provider' => $k))) . '">+ 在此分类添加站点</a></p>';
- echo '</div>';
- }
- echo '</div></body></html>';
- }
- /* ---------- 站点编辑表单 ---------- */
- function page_site_edit($data, $PROVIDERS) {
- $id = isset($_GET['id']) ? $_GET['id'] : '';
- $s = null;
- if ($id !== '') { $s = site_ref($data, $id); if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); } }
- $pre = isset($_GET['provider']) ? $_GET['provider'] : '';
- if ($s === null) {
- $p = ($pre !== '' && isset($PROVIDERS[$pre])) ? $PROVIDERS[$pre] : $PROVIDERS['other'];
- $s = array('id' => '', 'name' => '', 'url' => '', 'provider' => $pre !== '' ? $pre : 'other',
- 'panel_url' => '', 'proto' => $p['proto'], 'ftp_host' => $p['host'], 'ftp_port' => $p['port'],
- 'ftp_user' => '', 'ftp_pass' => '', 'root' => $p['root'], 'note' => '',
- 'login_cycle' => $p['cycle'], 'last_login_at' => '');
- }
- page_head($id !== '' ? '编辑站点' : '添加站点');
- top_bar();
- echo '<div class="card"><h3>' . ($id !== '' ? '编辑站点' : '添加站点') . '</h3>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'site_save'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
- echo '<input type="hidden" name="id" value="' . h($s['id']) . '">';
- echo '<div class="grid">';
- echo '<div><label>站名 *</label><input type="text" name="name" required value="' . h($s['name']) . '"></div>';
- echo '<div><label>网站地址(域名)*</label><input type="text" name="url" required placeholder="example.com 或 https://example.com" value="' . h($s['url']) . '"></div>';
- echo '<div><label>所属免费空间(分类)</label><select name="provider" style="width:100%">';
- foreach ($PROVIDERS as $k => $p) echo '<option value="' . h($k) . '"' . ($s['provider'] === $k ? ' selected' : '') . '>' . h($p['name']) . '</option>';
- echo '</select></div>';
- echo '<div><label>自定义分类名(选“其他/自定义”时填,可留空)</label><input type="text" name="provider_custom" value="' . (isset($PROVIDERS[$s['provider']]) ? '' : h($s['provider'])) . '"></div>';
- echo '<div><label>空间后台地址</label><input type="text" name="panel_url" placeholder="https://…" value="' . h($s['panel_url']) . '"></div>';
- echo '<div><label>登录保活周期(天,0 = 不提醒;serv00 是 90)</label><input type="text" name="login_cycle" value="' . h($s['login_cycle']) . '"></div>';
- echo '<div><label>连接方式</label><select name="proto" style="width:100%">';
- foreach (array('ftp' => 'FTP', 'ftps' => 'FTPS(加密 FTP)', 'sftp' => 'SFTP(SSH)') as $k => $v)
- echo '<option value="' . $k . '"' . ($s['proto'] === $k ? ' selected' : '') . '>' . $v . '</option>';
- echo '</select></div>';
- echo '<div><label>FTP 主机</label><input type="text" name="ftp_host" placeholder="如 ftpperso.free.fr" value="' . h($s['ftp_host']) . '"></div>';
- echo '<div><label>FTP 端口</label><input type="text" name="ftp_port" value="' . h($s['ftp_port']) . '"></div>';
- echo '<div><label>FTP 用户名</label><input type="text" name="ftp_user" value="' . h($s['ftp_user']) . '"></div>';
- echo '<div><label>FTP 密码' . ($id !== '' ? '(留空 = 不修改)' : '') . '</label><input type="password" name="ftp_pass" value=""></div>';
- echo '<div><label>网站根目录(FTP 登录后相对路径,如 public_html,可留空)</label><input type="text" name="root" value="' . h($s['root']) . '"></div>';
- echo '<div><label>备注</label><input type="text" name="note" value="' . h(isset($s['note']) ? $s['note'] : '') . '"></div>';
- echo '</div>';
- echo '<p><button class="btn" type="submit">保存</button> <a class="btn gray" href="' . h(self_url(array())) . '">返回</a></p>';
- echo '<p class="muted">FTP 密码保存在同目录的数据文件里(该文件浏览器直接访问会被拦截)。管理台密码请设得足够强,并尽量走 HTTPS 打开。</p>';
- echo '</form></div></div></body></html>';
- }
- function page_passwd() {
- page_head('修改密码');
- top_bar();
- echo '<div class="card loginbox" style="margin:12px auto"><h3>修改管理密码</h3>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'passwd_save'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '">';
- echo '<p><label>当前密码</label><input type="password" name="old" required></p>';
- echo '<p><label>新密码(至少 6 位)</label><input type="password" name="pw1" required></p>';
- echo '<p><label>再输一遍新密码</label><input type="password" name="pw2" required></p>';
- echo '<p><button class="btn" type="submit">修改</button></p></form></div></div></body></html>';
- }
- /* ---------- FTP 层(基于 curl,一个实现通吃 FTP/FTPS/SFTP) ---------- */
- function xftp_scheme($site) {
- $p = strtolower($site['proto']);
- if ($p === 'ftps') return 'ftps';
- if ($p === 'sftp') return 'sftp';
- return 'ftp';
- }
- function xftp_base($site) {
- $port = (int)$site['ftp_port']; if ($port <= 0) $port = 21;
- return xftp_scheme($site) . '://' . rawurlencode($site['ftp_user']) . ':' . rawurlencode($site['ftp_pass'])
- . '@' . $site['ftp_host'] . ':' . $port;
- }
- function xftp_enc_path($path) {
- if ($path === '') return '/';
- $segs = array();
- foreach (explode('/', $path) as $seg) { if ($seg !== '') $segs[] = rawurlencode($seg); }
- return '/' . implode('/', $segs);
- }
- // 通用请求。$path 为相对 FTP 根的路径(不带根目录前缀,根目录在调用处拼)。
- // 返回 array(ok, body, err)
- function xftp_req($site, $path, $extra) {
- $url = xftp_base($site) . xftp_enc_path($path);
- $isDir = isset($extra['dir']) && $extra['dir'];
- if ($isDir && substr($url, -1) !== '/') $url .= '/';
- $ch = curl_init($url);
- $opts = array(
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 12,
- CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false,
- );
- if (xftp_scheme($site) === 'ftps') $opts[CURLOPT_USE_SSL] = CURLUSESSL_ALL;
- if (!empty($extra['quote'])) $opts[CURLOPT_QUOTE] = $extra['quote'];
- if (!empty($extra['postquote'])) $opts[CURLOPT_POSTQUOTE] = $extra['postquote'];
- if (!empty($extra['upload_fp'])) {
- $opts[CURLOPT_UPLOAD] = true;
- $opts[CURLOPT_INFILE] = $extra['upload_fp'];
- if (isset($extra['upload_size'])) $opts[CURLOPT_INFILESIZE] = $extra['upload_size'];
- }
- if (!empty($extra['nobody'])) $opts[CURLOPT_NOBODY] = true;
- curl_setopt_array($ch, $opts);
- $body = curl_exec($ch);
- $err = curl_error($ch);
- $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
- curl_close($ch);
- $ok = ($err === '');
- if (!$ok && $body === false) $body = '';
- return array($ok, $body === false ? '' : $body, $err !== '' ? $err : ($ok ? '' : ('FTP 返回 ' . $code)));
- }
- function xftp_join($a, $b) {
- $a = trim($a, '/'); $b = trim($b, '/');
- if ($a === '') return $b;
- if ($b === '') return $a;
- return $a . '/' . $b;
- }
- // 列目录,返回 array(array(name,is_dir,size),...) 已按 目录在前+名称 排序
- function xftp_list($site, $dir) {
- list($ok, $body, $err) = xftp_req($site, $dir, array('dir' => true));
- if (!$ok) return array(false, $err);
- $items = array();
- foreach (preg_split('/\r\n|\r|\n/', trim($body)) as $line) {
- if ($line === '') continue;
- // Unix 格式: drwxr-xr-x 2 user group 4096 Nov 14 10:00 name
- if (preg_match('/^([dl-])[rwx-]{9}\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\w+\s+\d+\s+[\d:]+\s+(.+)$/', $line, $m)) {
- $name = trim($m[3]);
- if ($name === '.' || $name === '..') continue;
- $items[] = array('name' => $name, 'dir' => $m[1] === 'd', 'size' => (int)$m[2]);
- continue;
- }
- // Windows 格式: 11-14-26 10:00AM <DIR> name / 11-14-26 10:00AM 1234 name
- if (preg_match('/^\d{2}-\d{2}-\d{2}\s+\d{2}:\d{2}\s*[AP]M\s+(<DIR>|\d+)\s+(.+)$/i', $line, $m)) {
- $name = trim($m[2]);
- if ($name === '.' || $name === '..') continue;
- $items[] = array('name' => $name, 'dir' => strtoupper($m[1]) === '<DIR>', 'size' => strtoupper($m[1]) === '<DIR>' ? 0 : (int)$m[1]);
- continue;
- }
- // SFTP (OpenSSH ls 风格与 Unix 类似) 已覆盖;再兜底:整行就是名字的情况忽略
- }
- usort($items, function ($a, $b) {
- if ($a['dir'] !== $b['dir']) return $a['dir'] ? -1 : 1;
- return strcasecmp($a['name'], $b['name']);
- });
- return array(true, $items);
- }
- function xftp_download_str($site, $path) {
- return xftp_req($site, $path, array());
- }
- function xftp_upload_fp($site, $path, $fp, $size) {
- list($ok, $b, $err) = xftp_req($site, $path, array('upload_fp' => $fp, 'upload_size' => $size));
- return array($ok, $err);
- }
- function xftp_upload_str($site, $path, $content) {
- $fp = fopen('php://temp', 'r+');
- fwrite($fp, $content);
- rewind($fp);
- $r = xftp_upload_fp($site, $path, $fp, strlen($content));
- fclose($fp);
- return $r;
- }
- function xftp_quote($site, $dirPath, $cmds) {
- // 在 $dirPath 目录上执行 QUOTE 命令(命令在进入该目录后发出)
- list($ok, $b, $err) = xftp_req($site, $dirPath, array('dir' => true, 'nobody' => true, 'quote' => $cmds));
- return array($ok, $err);
- }
- function xftp_mkdir($site, $parent, $name) { return xftp_quote($site, $parent, array('MKD ' . $name)); }
- function xftp_delete_file($site, $parent, $name) { return xftp_quote($site, $parent, array('DELE ' . $name)); }
- function xftp_rmdir($site, $parent, $name) { return xftp_quote($site, $parent, array('RMD ' . $name)); }
- function xftp_rename($site, $parent, $from, $to) { return xftp_quote($site, $parent, array('RNFR ' . $from, 'RNTO ' . $to)); }
- function xftp_delete_recursive($site, $base, $name, $depth) {
- if ($depth > 12) return array(false, '目录层级太深,已停止');
- $full = xftp_join($base, $name);
- list($ok, $items) = xftp_list($site, $full);
- if ($ok) {
- foreach ($items as $it) {
- if ($it['dir']) { $r = xftp_delete_recursive($site, $full, $it['name'], $depth + 1); if (!$r[0]) return $r; }
- else { $r = xftp_delete_file($site, $full, $it['name']); if (!$r[0]) return $r; }
- }
- return xftp_rmdir($site, $base, $name);
- }
- // 不是目录(列不出来)按文件删
- return xftp_delete_file($site, $base, $name);
- }
- // 确保远程目录链存在(逐级 MKD,失败忽略,可能已存在)
- function xftp_ensure_dirs($site, $rootBase, $relDir) {
- if ($relDir === '') return;
- $cur = $rootBase;
- foreach (explode('/', $relDir) as $seg) {
- if ($seg === '') continue;
- xftp_mkdir($site, $cur, $seg);
- $cur = xftp_join($cur, $seg);
- }
- }
- /* ---------- 文件管理页面 ---------- */
- function fm_site_or_die(&$data, $PROVIDERS) {
- $id = isset($_REQUEST['id']) ? $_REQUEST['id'] : '';
- $s = site_ref($data, $id);
- if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); }
- return $s;
- }
- function fm_base($site) { return clean_rel_path(isset($site['root']) ? $site['root'] : ''); }
- function fm_rel() { return clean_rel_path(isset($_REQUEST['path']) ? $_REQUEST['path'] : ''); }
- function fm_url($site, $rel, $extra) {
- $p = array_merge(array('act' => 'files', 'id' => $site['id'], 'path' => $rel), $extra ? $extra : array());
- return self_url($p);
- }
- function page_files($data, $PROVIDERS) {
- $site = fm_site_or_die($data, $PROVIDERS);
- page_head('文件管理');
- top_bar();
- echo '<p><a href="' . h(self_url(array())) . '">← 返回站点列表</a></p>';
- echo '<div class="card"><h3 style="margin:4px 0">📁 ' . h($site['name']) . ' <span class="muted">' . h($site['url']) . '</span></h3>';
- if (empty($site['ftp_host']) || empty($site['ftp_user']) || empty($site['ftp_pass'])) {
- echo '<p>这个站还没填完整的 FTP 信息(主机 / 用户名 / 密码)。</p>';
- echo '<p><a class="btn" href="' . h(self_url(array('act' => 'site_edit', 'id' => $site['id']))) . '">去编辑这个站点,补上 FTP 信息</a></p>';
- echo '</div></div></body></html>';
- return;
- }
- $root = fm_base($site);
- $rel = fm_rel();
- $abs = xftp_join($root, $rel);
- // 面包屑
- echo '<p class="muted">当前位置:<a href="' . h(fm_url($site, '', null)) . '">根目录</a>';
- $acc = '';
- if ($rel !== '') foreach (explode('/', $rel) as $seg) {
- $acc = xftp_join($acc, $seg);
- echo ' / <a href="' . h(fm_url($site, $acc, null)) . '">' . h($seg) . '</a>';
- }
- echo '</p>';
- list($ok, $items) = xftp_list($site, $abs);
- if (!$ok) {
- echo '<div class="flash err">连接或读取目录失败:' . h($items) . '</div>';
- echo '<p class="muted">常见原因:FTP 主机 / 端口 / 用户名密码不对;连接方式选错(比如 serv00 要用 SFTP、free.fr 要用 FTPS);根目录填错。可以回站点编辑里核对后重试。</p>';
- echo '</div></div></body></html>';
- return;
- }
- // 上传 / 新建
- echo '<form method="post" enctype="multipart/form-data" action="' . h(self_url(array('act' => 'upload'))) . '" style="display:inline-block;margin-right:18px">';
- 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) . '">';
- echo '<input type="file" name="file" required> <button class="btn small" type="submit">上传到当前目录</button></form>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'mkdir'))) . '" style="display:inline-block">';
- 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) . '">';
- echo '<input type="text" name="name" placeholder="新文件夹名" required style="width:130px"> <button class="btn small gray" type="submit">新建文件夹</button></form>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'pack'))) . '">';
- 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) . '">';
- echo '<table style="margin-top:12px"><tr><th style="width:28px"></th><th>名称</th><th>大小</th><th>操作</th></tr>';
- foreach ($items as $it) {
- $name = $it['name'];
- echo '<tr><td><input type="checkbox" name="sel[]" value="' . h($name) . '"></td>';
- echo '<td>' . ($it['dir']
- ? '📁 <a href="' . h(fm_url($site, xftp_join($rel, $name), null)) . '"><b>' . h($name) . '</b></a>'
- : '📄 ' . h($name)) . '</td>';
- echo '<td class="muted">' . ($it['dir'] ? '—' : fmt_size($it['size'])) . '</td>';
- echo '<td style="white-space:nowrap">';
- if (!$it['dir']) {
- echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'dl', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">下载</a> ';
- $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
- if (in_array($ext, array('txt', 'php', 'html', 'htm', 'css', 'js', 'json', 'xml', 'md', 'htaccess', 'ini', 'yml', 'yaml', 'sql', 'log')) || $name === '.htaccess')
- echo '<a class="btn small" href="' . h(self_url(array('act' => 'file_edit', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">编辑</a> ';
- 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> ';
- }
- echo '<a class="btn small gray" href="' . h(self_url(array('act' => 'rename_form', 'id' => $site['id'], 'path' => $rel, 'name' => $name))) . '">改名</a> ';
- 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>';
- echo '</td></tr>';
- }
- echo '</table>';
- echo '<p><button class="btn gray" type="submit">把勾选的打包成 zip 下载</button> ';
- echo '<button class="btn gray" type="submit" name="all" value="1">打包当前整个文件夹下载</button></p></form>';
- echo '</div></div></body></html>';
- }
- function page_file_edit($data, $PROVIDERS) {
- $site = fm_site_or_die($data, $PROVIDERS);
- $root = fm_base($site); $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
- $abs = xftp_join(xftp_join($root, $rel), $name);
- list($ok, $body, $err) = xftp_download_str($site, $abs);
- page_head('编辑文件');
- top_bar();
- echo '<p><a href="' . h(fm_url($site, $rel, null)) . '">← 返回文件列表</a></p><div class="card">';
- if (!$ok) { echo '<div class="flash err">读取文件失败:' . h($err) . '</div></div></div></body></html>'; return; }
- if (strlen($body) > EDIT_MAX) { echo '<div class="flash err">文件超过 512KB,不适合在线编辑,请下载后改。</div></div></div></body></html>'; return; }
- if (strpos($body, "\0") !== false) { echo '<div class="flash err">这是二进制文件,不能在线编辑。</div></div></div></body></html>'; return; }
- echo '<h3>✏️ ' . h($name) . '</h3>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'file_save'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
- echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="name" value="' . h($name) . '">';
- echo '<textarea name="content" rows="26" style="width:100%;font-family:monospace;box-sizing:border-box">' . h($body) . '</textarea>';
- echo '<p><button class="btn" type="submit">保存</button></p></form></div></div></body></html>';
- }
- function page_rename_form($data, $PROVIDERS) {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
- page_head('改名'); top_bar();
- echo '<div class="card loginbox" style="margin:12px auto"><h3>重命名:' . h($name) . '</h3>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'rename'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
- echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="from" value="' . h($name) . '">';
- echo '<p><input type="text" name="to" value="' . h($name) . '" required></p>';
- echo '<p><button class="btn" type="submit">改名</button></p></form></div></div></body></html>';
- }
- function page_unzip_form($data, $PROVIDERS) {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
- page_head('解压'); top_bar();
- echo '<div class="card loginbox" style="margin:12px auto"><h3>解压:' . h($name) . '</h3>';
- echo '<form method="post" action="' . h(self_url(array('act' => 'unzip'))) . '">';
- echo '<input type="hidden" name="csrf" value="' . h(csrf()) . '"><input type="hidden" name="id" value="' . h($site['id']) . '">';
- echo '<input type="hidden" name="path" value="' . h($rel) . '"><input type="hidden" name="name" value="' . h($name) . '">';
- echo '<p><label><input type="radio" name="dest" value="here" checked> 解压到当前目录(主题包、程序包一般选这个,注意看包里结构)</label></p>';
- echo '<p><label><input type="radio" name="dest" value="folder"> 解压到同名文件夹里(更稳妥,不会和现有文件混在一起)</label></p>';
- echo '<p class="muted">注意:同名文件会被覆盖。限制:zip ≤ 25MB、文件数 ≤ 200、解压后 ≤ 50MB。</p>';
- echo '<p><button class="btn green" type="submit" onclick="return confirm(\'确定解压?同名文件会被覆盖。\')">开始解压</button></p></form></div></div></body></html>';
- }
- /* ---------- 解压 / 打包 ---------- */
- function do_unzip($site, $PROVIDERS) {
- $root = fm_base($site); $rel = fm_rel();
- $name = basename(isset($_POST['name']) ? $_POST['name'] : '');
- $destChoice = isset($_POST['dest']) ? $_POST['dest'] : 'here';
- $absZip = xftp_join(xftp_join($root, $rel), $name);
- list($ok, $zipBody, $err) = xftp_download_str($site, $absZip);
- if (!$ok) { flash('下载 zip 失败:' . $err, false); redirect(fm_url($site, $rel, null)); }
- if (strlen($zipBody) > UNZIP_MAX_ZIP) { flash('zip 超过 25MB 上限,已停止', false); redirect(fm_url($site, $rel, null)); }
- $tmpZip = tempnam(sys_get_temp_dir(), 'unz');
- file_put_contents($tmpZip, $zipBody);
- $za = new ZipArchive();
- if ($za->open($tmpZip) !== true) { unlink($tmpZip); flash('这个文件不是有效的 zip', false); redirect(fm_url($site, $rel, null)); }
- if ($za->numFiles > UNZIP_MAX_FILES) { $za->close(); unlink($tmpZip); flash('zip 里文件数超过 200 个,已停止', false); redirect(fm_url($site, $rel, null)); }
- $preset = provider_of($site, $PROVIDERS);
- $maxSingle = (int)$preset['max_single'];
- $total = 0; $entries = array();
- for ($i = 0; $i < $za->numFiles; $i++) {
- $st = $za->statIndex($i);
- $en = str_replace('\\', '/', $st['name']);
- if (substr($en, -1) === '/') continue;
- if (strpos($en, '..') !== false || substr($en, 0, 1) === '/') { $za->close(); unlink($tmpZip); flash('zip 里有不安全的路径(' . $en . '),已停止', false); redirect(fm_url($site, $rel, null)); }
- $total += $st['size'];
- if ($maxSingle > 0 && $st['size'] > $maxSingle) { $za->close(); unlink($tmpZip); flash('文件 ' . $en . ' 超过这家空间的单文件上限(' . fmt_size($maxSingle) . '),已停止', false); redirect(fm_url($site, $rel, null)); }
- $entries[] = $en;
- }
- if ($total > UNZIP_MAX_TOTAL) { $za->close(); unlink($tmpZip); flash('解压后总大小超过 50MB,已停止', false); redirect(fm_url($site, $rel, null)); }
- $tmpDir = sys_get_temp_dir() . '/unz' . uniqid();
- mkdir($tmpDir);
- $za->extractTo($tmpDir);
- $za->close(); unlink($tmpZip);
- $destRel = $rel;
- if ($destChoice === 'folder') $destRel = xftp_join($rel, pathinfo($name, PATHINFO_FILENAME));
- $destAbs = xftp_join($root, $destRel);
- $okCount = 0; $fail = array();
- foreach ($entries as $en) {
- $local = $tmpDir . '/' . $en;
- if (!file_exists($local)) continue;
- $dirPart = trim(dirname($en), '.');
- if ($dirPart !== '' && $dirPart !== '.') xftp_ensure_dirs($site, $destAbs, $dirPart);
- $target = xftp_join($destAbs, $en);
- $fp = fopen($local, 'rb');
- list($uok, $uerr) = xftp_upload_fp($site, $target, $fp, filesize($local));
- fclose($fp);
- if ($uok) $okCount++; else $fail[] = $en . '(' . $uerr . ')';
- unlink($local);
- }
- // 清理临时目录
- foreach (array_reverse($entries) as $en) { $d = $tmpDir . '/' . trim(dirname($en), '.'); if (is_dir($d)) @rmdir($d); }
- @rmdir($tmpDir);
- if ($fail) flash('解压完成 ' . $okCount . ' 个文件,失败 ' . count($fail) . ' 个:' . implode(';', array_slice($fail, 0, 3)), false);
- else flash('解压完成,共传上 ' . $okCount . ' 个文件到 ' . ($destChoice === 'folder' ? '同名文件夹' : '当前目录'));
- redirect(fm_url($site, $destRel, null));
- }
- function collect_remote($site, $absDir, $prefix, $tmpDir, &$count, &$total) {
- list($ok, $items) = xftp_list($site, $absDir);
- if (!$ok) return false;
- foreach ($items as $it) {
- $relName = $prefix === '' ? $it['name'] : $prefix . '/' . $it['name'];
- if ($it['dir']) {
- if (!collect_remote($site, xftp_join($absDir, $it['name']), $relName, $tmpDir, $count, $total)) return false;
- } else {
- $count++; $total += $it['size'];
- if ($count > UNZIP_MAX_FILES || $total > UNZIP_MAX_TOTAL) return false;
- list($dok, $body) = xftp_download_str($site, xftp_join($absDir, $it['name']));
- if (!$dok) return false;
- $localDir = $tmpDir . '/' . dirname($relName);
- if (!is_dir($localDir)) mkdir($localDir, 0777, true);
- file_put_contents($tmpDir . '/' . $relName, $body);
- }
- }
- return true;
- }
- function do_pack($site) {
- $root = fm_base($site); $rel = fm_rel(); $abs = xftp_join($root, $rel);
- $sel = array();
- if (!empty($_POST['all'])) {
- list($ok, $items) = xftp_list($site, $abs);
- if ($ok) foreach ($items as $it) $sel[] = $it['name'];
- } elseif (!empty($_POST['sel']) && is_array($_POST['sel'])) {
- foreach ($_POST['sel'] as $n) $sel[] = basename($n);
- }
- if (!$sel) { flash('先勾选要打包的文件,或点“打包当前整个文件夹”', false); redirect(fm_url($site, $rel, null)); }
- $tmpDir = sys_get_temp_dir() . '/pak' . uniqid();
- mkdir($tmpDir);
- $count = 0; $total = 0; $good = true;
- foreach ($sel as $name) {
- list($ok, $items) = xftp_list($site, xftp_join($abs, $name));
- if ($ok) { // 目录
- if (!collect_remote($site, xftp_join($abs, $name), $name, $tmpDir, $count, $total)) { $good = false; break; }
- } else { // 文件
- $count++;
- list($dok, $body) = xftp_download_str($site, xftp_join($abs, $name));
- if (!$dok) { $good = false; break; }
- $total += strlen($body);
- if ($count > UNZIP_MAX_FILES || $total > UNZIP_MAX_TOTAL) { $good = false; break; }
- file_put_contents($tmpDir . '/' . $name, $body);
- }
- }
- if (!$good || $count === 0) { flash('打包失败:可能超过上限(200 个文件 / 50MB)或读取失败', false); redirect(fm_url($site, $rel, null)); }
- $zipPath = $tmpDir . '.zip';
- $za = new ZipArchive();
- $za->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
- $packed = array();
- $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, FilesystemIterator::SKIP_DOTS));
- foreach ($rii as $f) { if ($f->isFile()) { $packed[] = $f->getPathname(); $za->addFile($f->getPathname(), substr($f->getPathname(), strlen($tmpDir) + 1)); } }
- $za->close();
- foreach ($packed as $pf) unlink($pf);
- header('Content-Type: application/zip');
- header('Content-Disposition: attachment; filename="' . preg_replace('/[^A-Za-z0-9_.-]/', '_', $site['name']) . '-backup.zip"');
- header('Content-Length: ' . filesize($zipPath));
- readfile($zipPath);
- unlink($zipPath);
- exit;
- }
- /* ---------- 主流程 ---------- */
- $data = data_load();
- $a = act();
- if ($a === 'setup' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- csrf_check();
- if ($data['admin_hash'] !== '') { flash('已经设置过密码了', false); redirect(self_url(array())); }
- $pw1 = isset($_POST['pw1']) ? $_POST['pw1'] : ''; $pw2 = isset($_POST['pw2']) ? $_POST['pw2'] : '';
- if (strlen($pw1) < 6) { flash('密码至少 6 位', false); redirect(self_url(array())); }
- if ($pw1 !== $pw2) { flash('两次输入的密码不一致', false); redirect(self_url(array())); }
- $data['admin_hash'] = password_hash($pw1, PASSWORD_DEFAULT);
- data_save($data);
- $_SESSION['auth'] = 1;
- flash('密码设置好了,欢迎使用');
- redirect(self_url(array()));
- }
- if ($a === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- csrf_check();
- $pw = isset($_POST['pw']) ? $_POST['pw'] : '';
- if ($data['admin_hash'] !== '' && password_verify($pw, $data['admin_hash'])) {
- session_regenerate_id(true);
- $_SESSION['auth'] = 1;
- redirect(self_url(array()));
- }
- flash('密码不对', false);
- redirect(self_url(array()));
- }
- if ($a === 'logout') { $_SESSION = array(); session_destroy(); redirect(self_url(array())); }
- require_login($data);
- csrf_check();
- if ($a === 'passwd') { page_passwd(); exit; }
- if ($a === 'passwd_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $old = isset($_POST['old']) ? $_POST['old'] : '';
- $pw1 = isset($_POST['pw1']) ? $_POST['pw1'] : ''; $pw2 = isset($_POST['pw2']) ? $_POST['pw2'] : '';
- if (!password_verify($old, $data['admin_hash'])) { flash('当前密码不对', false); redirect(self_url(array('act' => 'passwd'))); }
- if (strlen($pw1) < 6 || $pw1 !== $pw2) { flash('新密码至少 6 位且两次一致', false); redirect(self_url(array('act' => 'passwd'))); }
- $data['admin_hash'] = password_hash($pw1, PASSWORD_DEFAULT);
- data_save($data);
- flash('密码已修改');
- redirect(self_url(array()));
- }
- if ($a === 'site_edit') { page_site_edit($data, $PROVIDERS); exit; }
- if ($a === 'site_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $id = isset($_POST['id']) ? $_POST['id'] : '';
- $prov = isset($_POST['provider']) ? $_POST['provider'] : 'other';
- if ($prov === 'other' && !empty($_POST['provider_custom'])) $prov = trim($_POST['provider_custom']);
- $fields = array(
- 'name' => trim(isset($_POST['name']) ? $_POST['name'] : ''),
- 'url' => trim(isset($_POST['url']) ? $_POST['url'] : ''),
- 'provider' => $prov,
- 'panel_url' => trim(isset($_POST['panel_url']) ? $_POST['panel_url'] : ''),
- 'proto' => isset($_POST['proto']) ? $_POST['proto'] : 'ftp',
- 'ftp_host' => trim(isset($_POST['ftp_host']) ? $_POST['ftp_host'] : ''),
- 'ftp_port' => (int)(isset($_POST['ftp_port']) ? $_POST['ftp_port'] : 21),
- 'ftp_user' => trim(isset($_POST['ftp_user']) ? $_POST['ftp_user'] : ''),
- 'root' => clean_rel_path(isset($_POST['root']) ? $_POST['root'] : ''),
- 'note' => trim(isset($_POST['note']) ? $_POST['note'] : ''),
- 'login_cycle' => (int)(isset($_POST['login_cycle']) ? $_POST['login_cycle'] : 0),
- );
- if ($fields['name'] === '' || $fields['url'] === '') { flash('站名和网址必填', false); redirect(self_url(array('act' => 'site_edit'))); }
- if ($id !== '') {
- $s = &site_ref($data, $id);
- if ($s === null) { flash('站点不存在', false); redirect(self_url(array())); }
- foreach ($fields as $k => $v) $s[$k] = $v;
- if (!empty($_POST['ftp_pass'])) $s['ftp_pass'] = $_POST['ftp_pass'];
- data_save($data);
- flash('站点已保存');
- } else {
- $fields['id'] = bin2hex(random_bytes(6));
- $fields['ftp_pass'] = isset($_POST['ftp_pass']) ? $_POST['ftp_pass'] : '';
- $fields['last_login_at'] = ''; $fields['last_status'] = ''; $fields['last_code'] = 0; $fields['last_ms'] = 0; $fields['last_check'] = '';
- $data['sites'][] = $fields;
- data_save($data);
- flash('站点已添加');
- }
- redirect(self_url(array()));
- }
- if ($a === 'site_del') {
- $id = isset($_GET['id']) ? $_GET['id'] : '';
- $data['sites'] = array_values(array_filter($data['sites'], function ($s) use ($id) { return $s['id'] !== $id; }));
- data_save($data);
- flash('站点记录已删除');
- redirect(self_url(array()));
- }
- if ($a === 'mark_login') {
- $s = &site_ref($data, isset($_GET['id']) ? $_GET['id'] : '');
- if ($s !== null) { $s['last_login_at'] = date('Y-m-d'); data_save($data); flash('已标记今天登录过 ' . $s['name']); }
- redirect(self_url(array()));
- }
- if ($a === 'check') {
- $s = &site_ref($data, isset($_GET['id']) ? $_GET['id'] : '');
- if ($s !== null) { check_site($s); data_save($data); flash('检测完成:' . $s['name'] . ' ' . ($s['last_status'] === 'online' ? '在线' : '离线/异常')); }
- redirect(self_url(array()));
- }
- if ($a === 'check_all') {
- foreach ($data['sites'] as $i => $s) { check_site($data['sites'][$i]); }
- data_save($data);
- flash('全部检测完成');
- redirect(self_url(array()));
- }
- /* 文件管理相关动作 */
- if ($a === 'files') { page_files($data, $PROVIDERS); exit; }
- if ($a === 'file_edit') { page_file_edit($data, $PROVIDERS); exit; }
- if ($a === 'rename_form') { page_rename_form($data, $PROVIDERS); exit; }
- if ($a === 'unzip_form') { page_unzip_form($data, $PROVIDERS); exit; }
- if ($a === 'dl') {
- $site = fm_site_or_die($data, $PROVIDERS);
- $abs = xftp_join(xftp_join(fm_base($site), fm_rel()), basename(isset($_GET['name']) ? $_GET['name'] : ''));
- list($ok, $body, $err) = xftp_download_str($site, $abs);
- if (!$ok) { flash('下载失败:' . $err, false); redirect(fm_url($site, fm_rel(), null)); }
- header('Content-Type: application/octet-stream');
- header('Content-Disposition: attachment; filename="' . basename(isset($_GET['name']) ? $_GET['name'] : 'file') . '"');
- header('Content-Length: ' . strlen($body));
- echo $body; exit;
- }
- if ($a === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel();
- if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { flash('上传失败:没有收到文件', false); redirect(fm_url($site, $rel, null)); }
- $name = basename($_FILES['file']['name']);
- $preset = provider_of($site, $PROVIDERS);
- if ((int)$preset['max_single'] > 0 && $_FILES['file']['size'] > (int)$preset['max_single']) {
- flash('文件超过这家空间的单文件上限(' . fmt_size((int)$preset['max_single']) . '),已拦截', false); redirect(fm_url($site, $rel, null));
- }
- $fp = fopen($_FILES['file']['tmp_name'], 'rb');
- list($ok, $err) = xftp_upload_fp($site, xftp_join(xftp_join(fm_base($site), $rel), $name), $fp, $_FILES['file']['size']);
- fclose($fp);
- flash($ok ? ('已上传 ' . $name) : ('上传失败:' . $err), $ok);
- redirect(fm_url($site, $rel, null));
- }
- if ($a === 'mkdir' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel(); $name = basename(trim(isset($_POST['name']) ? $_POST['name'] : ''));
- list($ok, $err) = xftp_mkdir($site, xftp_join(fm_base($site), $rel), $name);
- flash($ok ? ('已新建文件夹 ' . $name) : ('新建失败:' . $err), $ok);
- redirect(fm_url($site, $rel, null));
- }
- if ($a === 'rename' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel();
- $from = basename(isset($_POST['from']) ? $_POST['from'] : '');
- $to = basename(trim(isset($_POST['to']) ? $_POST['to'] : ''));
- list($ok, $err) = xftp_rename($site, xftp_join(fm_base($site), $rel), $from, $to);
- flash($ok ? '已改名' : ('改名失败:' . $err), $ok);
- redirect(fm_url($site, $rel, null));
- }
- if ($a === 'file_save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel(); $name = basename(isset($_POST['name']) ? $_POST['name'] : '');
- list($ok, $err) = xftp_upload_str($site, xftp_join(xftp_join(fm_base($site), $rel), $name), isset($_POST['content']) ? $_POST['content'] : '');
- flash($ok ? ('已保存 ' . $name) : ('保存失败:' . $err), $ok);
- redirect(fm_url($site, $rel, null));
- }
- if ($a === 'del') {
- // 删除走 GET 但带 csrf 令牌校验
- if (empty($_GET['csrf']) || $_GET['csrf'] !== csrf()) { die('请求校验失败'); }
- $site = fm_site_or_die($data, $PROVIDERS);
- $rel = fm_rel(); $name = basename(isset($_GET['name']) ? $_GET['name'] : '');
- $base = xftp_join(fm_base($site), $rel);
- if (!empty($_GET['dir'])) list($ok, $err) = xftp_delete_recursive($site, $base, $name, 0);
- else list($ok, $err) = xftp_delete_file($site, $base, $name);
- flash($ok ? ('已删除 ' . $name) : ('删除失败:' . $err), $ok);
- redirect(fm_url($site, $rel, null));
- }
- if ($a === 'unzip' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- do_unzip($site, $PROVIDERS);
- exit;
- }
- if ($a === 'pack' && $_SERVER['REQUEST_METHOD'] === 'POST') {
- $site = fm_site_or_die($data, $PROVIDERS);
- do_pack($site);
- exit;
- }
- page_sites($data, $PROVIDERS);
复制代码
|
|