DatPilot: điểm sức khoẻ web (dùng được cả khi chưa kết nối), sao lưu thủ công tải về máy, * dán mã kết nối, xem trạng thái, ngắt kết nối. Mọi nút đều qua admin-post có nonce + quyền manage_options. */ defined( 'ABSPATH' ) || exit; class DATPCO_Admin { const PAGE = 'options-general.php?page=datpilot'; public static function boot() { add_action( 'admin_menu', function () { add_options_page( 'DatPilot', 'DatPilot', 'manage_options', 'datpilot', array( __CLASS__, 'page' ) ); } ); foreach ( array( 'save', 'disconnect', 'deep_scan', 'backup_download', 'backup_delete' ) as $a ) { add_action( 'admin_post_datpco_' . $a, array( __CLASS__, $a ) ); } add_action( 'wp_ajax_datpco_job', array( __CLASS__, 'ajax_job' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); add_filter( 'plugin_action_links_' . plugin_basename( DATPCO_FILE ), function ( $l ) { array_unshift( $l, 'Sức khoẻ web' ); return $l; } ); } /** JS của trang Cài đặt → DatPilot: chạy sao lưu/quét theo lát, hỏi lại trước khi xoá. Chỉ nạp ở trang này. */ public static function assets( $hook ) { if ( 'settings_page_datpilot' !== $hook ) { return; } wp_enqueue_script( 'datpco-admin', plugins_url( 'assets/admin.js', DATPCO_FILE ), array(), DATPCO_VERSION, true ); wp_localize_script( 'datpco-admin', 'datpcoAdmin', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'datpco_job' ), 'doneBackup' => admin_url( self::PAGE . '&datpco_msg=backup_ok' ), 'doneScan' => admin_url( self::PAGE . '&datpco_msg=scan_ok' ), ) ); } private static function cap() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Không có quyền.', 403 ); } } private static function back( $msg ) { wp_safe_redirect( add_query_arg( 'datpco_msg', rawurlencode( $msg ), admin_url( self::PAGE ) ) ); exit; } public static function save() { self::cap(); check_admin_referer( 'datpco_save' ); $key = isset( $_POST['datpco_key'] ) ? sanitize_text_field( wp_unslash( $_POST['datpco_key'] ) ) : ''; $p = DATPCO_Auth::parse_key( $key ); $msg = 'ok'; if ( is_wp_error( $p ) ) { $msg = $p->get_error_message(); } else { $portal = ( defined( 'DATPCO_PORTAL' ) && DATPCO_PORTAL ) ? DATPCO_PORTAL : 'https://app.datpilot.com'; update_option( DATPCO_Auth::OPTION, array( 'site_id' => $p['site_id'], 'secret' => $p['secret'], 'portal' => $portal, 'connected' => time() ), false ); $h = DATPCO_Auth::hello(); if ( is_wp_error( $h ) ) { delete_option( DATPCO_Auth::OPTION ); $msg = $h->get_error_message(); } } self::back( $msg ); } public static function disconnect() { self::cap(); check_admin_referer( 'datpco_disconnect' ); delete_option( DATPCO_Auth::OPTION ); self::back( 'disconnected' ); } public static function deep_scan() { self::cap(); check_admin_referer( 'datpco_deep_scan' ); $r = DATPCO_Health::deep_scan(); self::back( is_wp_error( $r ) ? $r->get_error_message() : 'scanned' ); } /** Sao lưu thủ công chạy theo lát: trình duyệt gọi start rồi step liên tục tới khi xong. */ public static function ajax_job() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Không có quyền.' ), 403 ); } check_ajax_referer( 'datpco_job' ); $op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : ''; $kind = isset( $_POST['kind'] ) && 'scan' === sanitize_key( wp_unslash( $_POST['kind'] ) ) ? 'scan' : 'backup'; if ( 'start' === $op ) { $r = DATPCO_Jobs::start( $kind, 'scan' === $kind ? array() : array( 'label' => 'Sao lưu thủ công trong wp-admin' ) ); } elseif ( 'step' === $op ) { $r = DATPCO_Jobs::step( isset( $_POST['id'] ) ? sanitize_key( wp_unslash( $_POST['id'] ) ) : '' ); } else { $r = new WP_Error( 'datpco_op', 'Thao tác không hợp lệ.' ); } if ( is_wp_error( $r ) ) { wp_send_json_error( array( 'message' => $r->get_error_message(), 'code' => $r->get_error_code() ) ); } wp_send_json_success( $r ); } public static function backup_delete() { self::cap(); check_admin_referer( 'datpco_backup_delete' ); DATPCO_Backup::delete( isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '' ); self::back( 'backup_deleted' ); } public static function backup_download() { self::cap(); check_admin_referer( 'datpco_backup_download' ); $id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : ''; $part = isset( $_POST['part'] ) && 'db' === sanitize_key( wp_unslash( $_POST['part'] ) ) ? 'db' : 'files'; $n = isset( $_POST['n'] ) ? max( 1, absint( $_POST['n'] ) ) : 1; if ( ! DATPCO_Backup::meta( $id ) || ( 'files' === $part && ! DATPCO_Backup::path( $id, 'files', $n ) ) ) { wp_die( 'Không thấy file sao lưu.', 404 ); } DATPCO_Backup::prepare(); while ( ob_get_level() ) { ob_end_clean(); } $name = sanitize_file_name( wp_parse_url( home_url(), PHP_URL_HOST ) . '-' . $id . ( 'db' === $part ? '.sql.gz' : sprintf( '-ma-nguon-%02d.zip', $n ) ) ); nocache_headers(); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . $name . '"' ); DATPCO_Backup::stream( $id, $part, $n ); // CSDL: mọi đoạn nối lại, giải mã khi tải exit; } private static function notice( $msg ) { $known = array( 'ok' => array( 'success', 'Đã kết nối với DatPilot.' ), 'disconnected' => array( 'info', 'Đã ngắt kết nối.' ), 'scanned' => array( 'success', 'Đã quét sâu file lõi WordPress.' ), 'backup_ok' => array( 'success', 'Đã tạo bản sao lưu. Nên tải về máy để giữ một bản ngoài hosting.' ), 'backup_deleted' => array( 'info', 'Đã xoá bản sao lưu.' ), 'scan_ok' => array( 'success', 'Đã quét xong, kết quả ở mục Quét mã độc và spam SEO.' ), ); list( $type, $text ) = isset( $known[ $msg ] ) ? $known[ $msg ] : array( 'error', $msg ); echo '

' . esc_html( $text ) . '

'; } /** In một nút gửi form admin-post có nonce (in thẳng, mọi giá trị đã escape tại chỗ). */ private static function button( $action, $label, $fields = array(), $class = 'button', $confirm = '' ) { echo '
'; wp_nonce_field( 'datpco_' . $action ); echo ''; foreach ( $fields as $k => $v ) { echo ''; } echo '
'; } public static function page() { $c = DATPCO_Auth::connection(); $msg = isset( $_GET['datpco_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['datpco_msg'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- chỉ chọn câu thông báo để hiện, không đổi dữ liệu $h = DATPCO_Health::report(); $col = array( 'A' => '#15803d', 'B' => '#4d7c0f', 'C' => '#b45309', 'D' => '#c2410c', 'F' => '#b91c1c' ); $sev = array( 'high' => 'Nặng', 'med' => 'Vừa', 'low' => 'Nhẹ' ); echo '

DatPilot: sức khoẻ web

'; if ( $msg ) { self::notice( $msg ); } // --- điểm sức khoẻ $bad = array_values( array_filter( $h['checks'], function ( $i ) { return ! $i['ok']; } ) ); usort( $bad, function ( $a, $b ) { return DATPCO_Health::WEIGHT[ $b['sev'] ] - DATPCO_Health::WEIGHT[ $a['sev'] ]; } ); echo '
'; echo '
' . (int) $h['score'] . '' . esc_html( $h['grade'] ) . '
'; echo '

Điểm sức khoẻ ' . (int) $h['score'] . '/100

' . count( $h['checks'] ) . ' mục kiểm tra, ' . count( $bad ) . ' mục cần xử lý. Phần kiểm tra chỉ đọc, không thay đổi gì trên web.

'; echo '

' . ( $h['deep_at'] ? 'Quét sâu file lõi lần cuối: ' . esc_html( wp_date( 'd/m/Y H:i', $h['deep_at'] ) ) . '. ' : '' ) . '

'; echo '
'; self::button( 'deep_scan', $h['deep_at'] ? 'Quét sâu lại file lõi' : 'Quét sâu file lõi (so với wordpress.org)' ); echo '
'; if ( $bad ) { echo ''; foreach ( $bad as $i ) { echo ''; } echo '
MứcVấn đềCách sửa
' . esc_html( $sev[ $i['sev'] ] ) . '' . esc_html( $i['title'] ) . '
' . esc_html( $i['detail'] ) . '
' . esc_html( $i['fix'] ) . '
Nhờ DatPilot làm
'; } else { echo '

Không có vấn đề nào trong các mục đã kiểm.

'; } $deep = get_option( DATPCO_Health::LAST ); if ( is_array( $deep ) && ! empty( $deep['files'] ) ) { echo '
File lõi khác bản gốc (tối đa 20)' . esc_html( implode( "\n", $deep['files'] ) ) . '
'; } echo '
'; $job = DATPCO_Jobs::load(); $run = ( $job && empty( $job['done'] ) && time() - (int) $job['updated'] < DATPCO_Jobs::STALE ) ? $job : null; // --- lỗi PHP nghiêm trọng $errs = DATPCO_Errors::read( time() - 7 * DAY_IN_SECONDS, 20 ); echo '

Lỗi PHP 7 ngày qua

'; $mode = array( 'mu' => 'Đang bắt mọi lỗi nghiêm trọng, kể cả lỗi khi plugin khác đang nạp.', 'plugin' => 'Đang bắt lỗi nghiêm trọng xảy ra sau khi plugin này nạp.', '' => 'Chưa bắt được lỗi: thư mục sao lưu chưa có (bấm Tạo bản sao lưu một lần).' ); echo '

' . esc_html( $mode[ $errs['mode'] ] ?? $mode[''] ) . ' Không cần bật WP_DEBUG; chỉ ghi loại lỗi, file, dòng, trang bị lỗi; đã xoá email, số điện thoại, dữ liệu trong câu lỗi. Lỗi lưu trên hosting của bạn, chỉ gửi đi khi web đã kết nối DatPilot.

'; if ( $errs['errors'] ) { echo ''; foreach ( $errs['errors'] as $x ) { echo ''; } echo '
Lần cuốiLỗiNghi doSố lần
' . esc_html( wp_date( 'd/m H:i', (int) $x['last'] ) ) . '
' . esc_html( $x['uri'] ) . '
' . esc_html( $x['file'] . ':' . $x['line'] ) . '
' . esc_html( $x['msg'] ) . '
' . esc_html( $x['slug'] ? $x['kind'] . ' ' . $x['slug'] . ( $x['version'] ? ' ' . $x['version'] : '' ) : ( 'core' === $x['kind'] ? 'lõi WordPress' : '' ) ) . '' . (int) $x['n'] . '
'; echo '

Nhờ DatPilot sửa lỗi

'; } else { echo '

Không có lỗi PHP nghiêm trọng nào.

'; } echo '
'; // --- quét mã độc, cửa hậu, spam SEO $scan = get_option( DATPCO_Scan::LAST ); echo '

Quét mã độc và spam SEO

'; echo '

So file lõi WordPress và plugin tải từ wordpress.org với bản gốc, dò cửa hậu thường gặp trong theme, plugin trả phí, thư mục ảnh tải lên; tìm bài bị chèn spam cờ bạc, nhà cái, link ẩn. Chỉ đọc, không xoá hay sửa gì. Web lớn mất vài phút, giữ trang này mở tới khi xong.

'; echo '

'; if ( is_array( $scan ) ) { $sp = is_array( $scan['spam'] ) ? $scan['spam'] : array(); echo '

Lần quét gần nhất ' . esc_html( wp_date( 'd/m/Y H:i', (int) $scan['at'] ) ) . ': ' . (int) $scan['files'] . ' file, ' . (int) $scan['plugins']['verified'] . ' plugin đối chiếu được với wordpress.org, ' . (int) $scan['plugins']['unverified'] . ' plugin chỉ dò dấu hiệu. ' . '' . (int) $scan['high'] . ' dấu hiệu nặng, ' . (int) $scan['med'] . ' dấu hiệu cần xem.

'; if ( $scan['hits'] ) { echo ''; foreach ( $scan['hits'] as $x ) { echo ''; } echo '
MứcỞ đâuVì sao đáng ngờ
' . esc_html( $sev[ $x['sev'] ] ) . '' . esc_html( $x['path'] ) . '' . esc_html( $x['why'] ) . '
'; if ( $scan['more'] ) { echo '

Còn ' . (int) $scan['more'] . ' dấu hiệu nữa không hiện hết.

'; } } if ( ! empty( $sp['samples'] ) ) { echo '
Bài/trang nghi bị chèn spam
'; } if ( $scan['high'] ) { echo '

Nhờ DatPilot dọn mã độc

'; } } echo '
'; // --- sao lưu thủ công echo '

Sao lưu thủ công

'; echo '

Sao lưu cơ sở dữ liệu và thư mục wp-content (trừ ảnh tải lên, cache). Lưu trên hosting của bạn, giữ tối đa ' . (int) DATPCO_Backup::KEEP . ' bản; nên tải về máy một bản. Chạy từng phần nhỏ nên không bị hosting cắt giữa chừng; giữ trang này mở tới khi xong.

'; echo '

' . ( $run ? 'Đang có việc ' . esc_html( $run['type'] ) . ' chạy dở.' : '' ) . '

'; $list = DATPCO_Backup::all(); if ( $list ) { echo ''; foreach ( $list as $b ) { $size = size_format( (int) $b['db_bytes'] + (int) ( $b['zip_bytes'] ?? 0 ) ); echo ''; } echo '
Bản sao lưuDung lượng
' . esc_html( wp_date( 'd/m/Y H:i', (int) $b['created'] ) ) . '
' . esc_html( $b['label'] ?? '' ) . '
' . esc_html( $size ) . ''; self::button( 'backup_download', 'Tải CSDL', array( 'id' => $b['id'], 'part' => 'db' ) ); $np = count( isset( $b['parts'] ) ? $b['parts'] : array() ); for ( $k = 1; $k <= $np; $k++ ) { self::button( 'backup_download', $np > 1 ? "Mã nguồn $k/$np" : 'Tải mã nguồn', array( 'id' => $b['id'], 'part' => 'files', 'n' => $k ) ); } self::button( 'backup_delete', 'Xoá', array( 'id' => $b['id'] ), 'button-link-delete button', 'Xoá bản sao lưu này?' ); echo '
'; } echo '
'; // --- kết nối echo '

Kết nối DatPilot

'; if ( $c ) { echo '

Trạng thái: đã kết nối (mã web ' . esc_html( $c['site_id'] ) . ') từ ' . esc_html( wp_date( 'd/m/Y H:i', (int) ( isset( $c['connected'] ) ? $c['connected'] : time() ) ) ) . '.

'; echo '

DatPilot theo dõi web sập và báo cho bạn, gửi báo cáo sức khoẻ hằng tháng. DatPilot chỉ gửi được các lệnh đọc thông tin, điểm sức khoẻ, quét chỉ đọc, đọc lỗi PHP, xoá cache, tạo/xoá bản sao lưu (bản sao lưu nằm trên hosting của bạn, không gửi về DatPilot); mỗi lệnh có chữ ký riêng. Plugin này không cho DatPilot cài, bật, tắt, cập nhật plugin hay đổi cài đặt của web. Ngắt kết nối hoặc tắt plugin bất kỳ lúc nào là DatPilot không truy cập được nữa.

'; self::button( 'disconnect', 'Ngắt kết nối' ); } else { echo '

Kết nối miễn phí để DatPilot báo khi web sập (Telegram/email) và gửi báo cáo sức khoẻ hằng tháng. Dán mã kết nối lấy ở datpilot.com → Web của tôi. Chưa dán mã thì plugin không gửi gì ra ngoài.

'; echo '
'; wp_nonce_field( 'datpco_save' ); echo ''; echo ' '; echo '
'; } echo '
'; } }