'text/html' ) ); \wp_enqueue_script( 'wp-theme-plugin-editor' ); \wp_enqueue_style( 'wp-codemirror' ); \wp_enqueue_style( 'advan-file-editor-style', \ADVAN_PLUGIN_ROOT_URL . 'css' . \DIRECTORY_SEPARATOR . 'wfe.css', array(), ADVAN_VERSION ); \wp_enqueue_script( 'advan-file-editor-script', \ADVAN_PLUGIN_ROOT_URL . 'js' . \DIRECTORY_SEPARATOR . 'admin' . \DIRECTORY_SEPARATOR . 'wfe.js', array( 'jquery', 'wp-codemirror', 'wp-i18n' ), ADVAN_VERSION, true ); \wp_localize_script( 'advan-file-editor-script', 'AFE_Ajax', array( 'ajax_url' => \admin_url( 'admin-ajax.php' ), 'nonce' => \wp_create_nonce( 'advan_file_editor_nonce' ), 'base' => self::BASE_DIR, ) ); } /** * Renders the main page * * @return void * * @since 4.0.0 */ public static function render_page() { // Capability guard: only allow administrators (or users with equivalent capability). if ( ! \current_user_can( 'manage_options' ) ) { \wp_die( \esc_html__( 'You do not have permission to manage files.', '0-day-analytics' ) ); } ?>

'File uploaded successfully.', 'path' => $target_path, 'name' => $filename, ) ); } /** * AJAX: Creates a new file or folder * * @return void * * @since 4.0.0 */ public static function ajax_create() { // Standardize nonce and capability checks. WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $dir = \sanitize_text_field( $_POST['dir'] ?? self::BASE_DIR ); $name = \sanitize_file_name( $_POST['name'] ?? '' ); $type = $_POST['type'] ?? 'file'; $parent = self::safe_path( $dir ); if ( ! $parent || ! is_dir( $parent ) ) { \wp_send_json_error( 'Invalid directory.' ); } $path = $parent . DIRECTORY_SEPARATOR . $name; if ( file_exists( $path ) ) { \wp_send_json_error( 'Already exists.' ); } if ( 'folder' === $type ) { mkdir( $path, 0755 ); } else { file_put_contents( $path, '' ); } \wp_send_json_success( 'Created.' ); } /** * AJAX: Lists directory contents * * @return void * * @since 4.0.0 */ public static function ajax_list_dir() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $dir = \sanitize_text_field( $_POST['dir'] ?? \ABSPATH ); $real = self::safe_path( $dir ); if ( ! $real || ! is_dir( $real ) ) { \wp_send_json_error( 'Invalid directory' ); } $items = array(); foreach ( scandir( $real ) as $file ) { if ( '.' === $file || '..' === $file ) { continue; } $real_path = $real . \DIRECTORY_SEPARATOR . $file; $type = is_dir( $real_path ) ? 'dir' : 'file'; $display = $file; if ( 'file' === $type ) { $size = @filesize( $real_path ); if ( false !== $size ) { $display .= ' (' . \size_format( (int) $size ) . ')'; } } $symlink_path = $dir . \DIRECTORY_SEPARATOR . $file; $items[] = array( 'name' => $display, 'path' => $symlink_path, 'type' => $type, ); } \wp_send_json_success( $items ); } /** * AJAX: Gets file content * * @return void * * @since 4.0.0 */ public static function ajax_get_file() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real || ! is_readable( $real ) ) { \wp_send_json_error( 'Cannot read file.' ); } \wp_send_json_success( array( 'content' => file_get_contents( $real ) ) ); } /** * AJAX: Saves file content * * @return void * * @since 4.0.0 */ public static function ajax_save_file() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $content = $_POST['content'] ?? ''; $real = self::safe_path( $file ); if ( ! $real || ! is_writable( $real ) ) { \wp_send_json_error( 'Cannot write file.' ); } self::create_backup( $real ); file_put_contents( $real, \wp_unslash( $content ), LOCK_EX ); \wp_send_json_success( 'File saved and backup created.' ); } /** * AJAX: Generates a diff preview * * @return void * * @since 4.0.0 */ public static function ajax_diff() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $new = $_POST['new_content'] ?? ''; $real = self::safe_path( $file ); if ( ! $real || ! is_file( $real ) ) { \wp_send_json_error( 'Invalid file.' ); } $diff = \wp_text_diff( file_get_contents( $real ), \wp_unslash( $new ), array( 'title' => 'Changes' ) ); \wp_send_json_success( array( 'diff' => $diff ) ); } /** * AJAX: Deletes a file (moves to trash) * * @return void * * @since 4.0.0 */ public static function ajax_delete() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $target = \sanitize_text_field( $_POST['path'] ?? '' ); $real = self::safe_path( $target ); if ( ! $real || ! file_exists( $real ) ) { \wp_send_json_error( 'Not found.' ); } // Check permissions if ( ! is_writable( dirname( $real ) ) ) { \wp_send_json_error( 'Permission denied: parent directory not writable.' ); } // Ensure trash directory exists and is writable. $trash_dir = self::get_trash_dir(); if ( ! is_dir( $trash_dir ) ) { @\wp_mkdir_p( $trash_dir ); self::harden_directory( $trash_dir ); } if ( ! is_writable( $trash_dir ) ) { \wp_send_json_error( 'Trash directory is not writable.' ); } // Generate unique trash path. $base_name = basename( $real ); $trash_path = $trash_dir . \DIRECTORY_SEPARATOR . $base_name . '.' . time(); $counter = 0; while ( file_exists( $trash_path ) ) { ++$counter; $trash_path = $trash_dir . \DIRECTORY_SEPARATOR . $base_name . '.' . time() . '_' . $counter; } // Try to move to trash. if ( @rename( $real, $trash_path ) ) { self::$last_deleted = array( 'src' => $real, 'trash' => $trash_path, ); self::save_meta(); $msg = is_dir( $trash_path ) ? 'Directory moved to trash.' : 'Moved to trash.'; \wp_send_json_success( $msg ); } else { // If rename fails, try alternative approach for directories. if ( is_dir( $real ) ) { // Try to copy recursively and then delete. if ( self::recursive_copy( $real, $trash_path ) ) { self::delete_dir( $real ); if ( ! file_exists( $real ) ) { self::$last_deleted = array( 'src' => $real, 'trash' => $trash_path, ); self::save_meta(); \wp_send_json_success( 'Directory moved to trash.' ); } else { // Cleanup trash if original still exists self::delete_dir( $trash_path ); \wp_send_json_error( 'Unable to delete directory after copying.' ); } } else { \wp_send_json_error( 'Unable to copy directory to trash.' ); } } else { \wp_send_json_error( 'Unable to move to trash. Check file permissions.' ); } } $src = self::$last_deleted['src']; $trash = self::$last_deleted['trash']; \wp_mkdir_p( dirname( $src ) ); rename( $trash, $src ); self::$last_deleted = null; self::save_meta(); \wp_send_json_success( array( 'msg' => 'Restored ' . basename( $src ) ) ); } /** * AJAX: Empties the trash * * @return void * * @since 4.0.0 */ public static function ajax_empty_trash() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); self::delete_dir( self::get_trash_dir() ); \wp_mkdir_p( self::get_trash_dir() ); self::$last_deleted = null; self::save_meta(); \wp_send_json_success( 'Trash emptied.' ); } /** * AJAX: Lists backups for a file * * @return void * * @since 4.0.0 */ public static function ajax_list_backups() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real ) { \wp_send_json_error( 'Invalid path.' ); } $rel = ltrim( str_replace( self::BASE_DIR, '', $real ), \DIRECTORY_SEPARATOR ); $dir = self::get_backup_dir() . \DIRECTORY_SEPARATOR . dirname( $rel ); $pattern = $dir . \DIRECTORY_SEPARATOR . basename( $real ) . '.*.bak'; $backups = glob( $pattern ); rsort( $backups ); $list = array_map( 'basename', $backups ); \wp_send_json_success( $list ); } /** * AJAX: Restores a backup * * @return void * * @since 4.0.0 */ public static function ajax_restore_backup() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $backup = \sanitize_text_field( $_POST['backup'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real || ! is_writable( $real ) ) { \wp_send_json_error( 'Invalid file.' ); } $rel = ltrim( str_replace( self::BASE_DIR, '', $real ), \DIRECTORY_SEPARATOR ); $dir = self::get_backup_dir() . \DIRECTORY_SEPARATOR . dirname( $rel ); $backup_path = $dir . \DIRECTORY_SEPARATOR . basename( $backup ); if ( ! file_exists( $backup_path ) ) { \wp_send_json_error( 'Backup not found.' ); } self::create_backup( $real ); copy( $backup_path, $real ); \wp_send_json_success( 'Backup restored.' ); } /** * AJAX: Restores a backup * * @return void * * @since 4.1.0 */ public static function ajax_delete_backup() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_GET['file'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real ) { \wp_die( 'Invalid file.' ); } $rel = ltrim( str_replace( self::BASE_DIR, '', $real ), \DIRECTORY_SEPARATOR ); $backup = \sanitize_text_field( $_POST['backup'] ?? '' ); $dir = self::get_backup_dir() . \DIRECTORY_SEPARATOR . dirname( $rel ); $backup_path = $dir . \DIRECTORY_SEPARATOR . basename( $backup ); if ( ! file_exists( $backup_path ) ) { \wp_send_json_error( 'Backup not found.' ); } \unlink( $backup_path ); \wp_send_json_success( 'Backup deleted.' ); } /** * AJAX: Downloads a backup * * @return void * * @since 4.0.0 */ public static function ajax_download_backup() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_GET['file'] ?? '' ); $backup = \sanitize_text_field( $_GET['backup'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real ) { \wp_die( 'Invalid file.' ); } $rel = ltrim( str_replace( self::BASE_DIR, '', $real ), \DIRECTORY_SEPARATOR ); $path = self::get_backup_dir() . \DIRECTORY_SEPARATOR . dirname( $rel ) . \DIRECTORY_SEPARATOR . basename( $backup ); if ( ! file_exists( $path ) ) { \wp_die( 'Backup not found.' ); } header( 'Content-Type: application/octet-stream' ); header( 'X-Content-Type-Options: nosniff' ); header( 'Content-Disposition: attachment; filename="' . basename( $backup ) . '"' ); header( 'Content-Length: ' . filesize( $path ) ); header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); header( 'Pragma: no-cache' ); readfile( $path ); exit; } /** * AJAX: Downloads a backup * * @return void * * @since 4.0.0 */ public static function ajax_compare_backup() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_POST['file'] ?? '' ); $backup = \sanitize_text_field( $_POST['backup'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real || ! is_file( $real ) ) { \wp_send_json_error( 'Invalid file.' ); } $rel = ltrim( str_replace( self::BASE_DIR, '', $real ), \DIRECTORY_SEPARATOR ); $backup_path = self::get_backup_dir() . \DIRECTORY_SEPARATOR . dirname( $rel ) . \DIRECTORY_SEPARATOR . basename( $backup ); if ( ! file_exists( $backup_path ) ) { \wp_send_json_error( 'Backup not found.' ); } $old = file_get_contents( $backup_path ); $new = file_get_contents( $real ); $diff = \wp_text_diff( $old, $new, array( 'title' => 'Backup vs Current' ) ); \wp_send_json_success( array( 'diff' => $diff ) ); } /** * AJAX: Downloads a regular file (not backup) * * @return void * * @since 4.1.1 */ public static function ajax_download_file() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $file = \sanitize_text_field( $_GET['file'] ?? '' ); $real = self::safe_path( $file ); if ( ! $real || ! is_file( $real ) || ! is_readable( $real ) ) { \wp_die( 'Invalid file.' ); } $filename = basename( $real ); header( 'Content-Type: application/octet-stream' ); header( 'X-Content-Type-Options: nosniff' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Content-Length: ' . filesize( $real ) ); header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); header( 'Pragma: no-cache' ); readfile( $real ); exit; } /** * AJAX: Renames a file or directory * * @return void * * @since 4.1.1 */ public static function ajax_rename() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $src = \sanitize_text_field( $_POST['file'] ?? '' ); $new_name = \sanitize_file_name( $_POST['new_name'] ?? '' ); if ( empty( $src ) || empty( $new_name ) ) { \wp_send_json_error( 'Missing parameters.' ); } $real = self::safe_path( $src ); if ( ! $real || ! file_exists( $real ) ) { \wp_send_json_error( 'Source not found.' ); } $target = dirname( $real ) . \DIRECTORY_SEPARATOR . $new_name; if ( file_exists( $target ) ) { \wp_send_json_error( 'Target exists.' ); } if ( ! @rename( $real, $target ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged \wp_send_json_error( 'Unable to rename.' ); } \wp_send_json_success( array( 'new_path' => $target, 'new_name' => basename( $target ), ) ); } /** * AJAX: Duplicates a file or directory * * @return void * * @since 4.1.1 */ public static function ajax_duplicate() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $src = \sanitize_text_field( $_POST['file'] ?? '' ); if ( empty( $src ) ) { \wp_send_json_error( 'Missing parameters.' ); } $real = self::safe_path( $src ); if ( ! $real || ! file_exists( $real ) ) { \wp_send_json_error( 'Source not found.' ); } $is_dir = is_dir( $real ); $dirname = dirname( $real ); $basename = basename( $real ); // Determine target name. $ext = ''; $name_only = $basename; if ( ! $is_dir && false !== strpos( $basename, '.' ) ) { $parts = explode( '.', $basename ); $ext = array_pop( $parts ); $name_only = implode( '.', $parts ); } $base_candidate = $name_only . '-copy'; $counter = 1; $target = ''; while ( true ) { $suffix = ( $counter > 1 ) ? '-' . $counter : ''; $target_name = $base_candidate . $suffix . ( $is_dir ? '' : ( ( '' !== $ext ) ? '.' . $ext : '' ) ); $target = $dirname . \DIRECTORY_SEPARATOR . $target_name; if ( ! file_exists( $target ) ) { break; } ++$counter; } // Perform copy. if ( $is_dir ) { self::recursive_copy( $real, $target ); } elseif ( ! @copy( $real, $target ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged \wp_send_json_error( 'Unable to duplicate file.' ); } \wp_send_json_success( array( 'new_path' => $target, 'new_name' => basename( $target ), ) ); } /** * Recursively copies a directory. * * @param string $src Source dir. * @param string $dst Destination dir. * * @return bool True on success, false on failure. * * @since 4.1.1 */ private static function recursive_copy( $src, $dst ) { if ( ! is_dir( $src ) ) { return false; } if ( ! @wp_mkdir_p( $dst ) ) { return false; } $dir_handle = opendir( $src ); if ( false === $dir_handle ) { return false; } $success = true; while ( false !== ( $item = readdir( $dir_handle ) ) ) { if ( '.' === $item || '..' === $item ) { continue; } $from = $src . \DIRECTORY_SEPARATOR . $item; $to = $dst . \DIRECTORY_SEPARATOR . $item; if ( is_dir( $from ) ) { if ( ! self::recursive_copy( $from, $to ) ) { $success = false; } } elseif ( ! @copy( $from, $to ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $success = false; } } closedir( $dir_handle ); return $success; } /** * Determines whether to use ZipArchive or PclZip * * @return string 'ziparchive' or 'pclzip' * * @since 4.7.2 */ private static function get_zip_method() { if ( class_exists( '\ZipArchive', false ) ) { return 'ziparchive'; } return 'pclzip'; } /** * Recursively lists all files in a directory * * @param string $dir Directory path. * @param string $base_path Base path for relative paths. * * @return array List of file paths relative to base. * * @since 4.7.1 */ private static function list_files_recursive( $dir, $base_path ) { $files = array(); $dir_handle = opendir( $dir ); if ( false === $dir_handle ) { return $files; } while ( false !== ( $item = readdir( $dir_handle ) ) ) { if ( '.' === $item || '..' === $item ) { continue; } $path = $dir . \DIRECTORY_SEPARATOR . $item; if ( is_dir( $path ) ) { $files = array_merge( $files, self::list_files_recursive( $path, $base_path ) ); } else { $files[] = str_replace( $base_path . \DIRECTORY_SEPARATOR, '', $path ); } } closedir( $dir_handle ); return $files; } /** * AJAX: Starts archive creation (initialization phase) * * @return void * * @since 4.7.1 */ public static function ajax_create_archive_start() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $source = \sanitize_text_field( $_POST['path'] ?? '' ); if ( empty( $source ) ) { \wp_send_json_error( 'Missing parameters.' ); } $real = self::safe_path( $source ); if ( ! $real || ! file_exists( $real ) ) { \wp_send_json_error( 'Source not found.' ); } $is_dir = is_dir( $real ); // Determine archive location. if ( $is_dir ) { $archive_dir = dirname( $real ); } else { $archive_dir = dirname( $real ); } // Generate archive filename. $basename = basename( $real ); $archive_base = $basename . '-' . gmdate( 'Ymd-His' ); $archive_path = $archive_dir . \DIRECTORY_SEPARATOR . $archive_base . '.zip'; // List all files to archive. $files = array(); if ( $is_dir ) { $files = self::list_files_recursive( $real, $real ); } else { $files = array( basename( $real ) ); } if ( empty( $files ) ) { \wp_send_json_error( 'No files to archive.' ); } \wp_send_json_success( array( 'source' => $real, 'archive_path' => $archive_path, 'files' => $files, 'total_files' => count( $files ), 'is_dir' => $is_dir, ) ); } /** * AJAX: Cancels archive creation and deletes partial file * * @return void * * @since 4.7.1 */ public static function ajax_create_archive_cancel() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); if ( empty( $archive_path ) ) { \wp_send_json_error( 'Missing archive path.' ); } // Validate archive path is within safe directories. $archive_real = self::safe_path( dirname( $archive_path ) ); if ( ! $archive_real ) { \wp_send_json_error( 'Invalid archive location.' ); } // Delete the partial archive if it exists. if ( file_exists( $archive_path ) ) { @unlink( $archive_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } \wp_send_json_success( 'Archive creation cancelled.' ); } /** * AJAX: Processes a batch of files for archiving * * @return void * * @since 4.7.1 */ public static function ajax_create_archive_batch() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $source = \sanitize_text_field( $_POST['source'] ?? '' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); $files_json = $_POST['files'] ?? ''; $processed = (int) ( $_POST['processed'] ?? 0 ); $batch_size = (int) ( $_POST['batch_size'] ?? 50 ); $is_dir = 'true' === ( $_POST['is_dir'] ) ? true : false; if ( empty( $source ) || empty( $archive_path ) || empty( $files_json ) ) { \wp_send_json_error( 'Missing parameters.' ); } $files = json_decode( \stripslashes( $files_json ), true ); if ( ! is_array( $files ) ) { \wp_send_json_error( 'Invalid files list.' ); } $real = self::safe_path( $source ); if ( ! $real || ! file_exists( $real ) ) { \wp_send_json_error( 'Source not found.' ); } // Validate archive path is within safe directories. $archive_real = self::safe_path( dirname( $archive_path ) ); if ( ! $archive_real ) { \wp_send_json_error( 'Invalid archive location.' ); } $zip_method = self::get_zip_method(); if ( 'ziparchive' === $zip_method ) { // Use ZipArchive. $zip = new \ZipArchive(); if ( 0 === $processed ) { $result = $zip->open( $archive_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ); } else { $result = $zip->open( $archive_path ); } if ( true !== $result ) { \wp_send_json_error( 'Failed to open/create archive.' ); } // Process batch of files. $batch_files = array_slice( $files, $processed, $batch_size ); foreach ( $batch_files as $file ) { if ( $is_dir ) { $file_path = $real . \DIRECTORY_SEPARATOR . $file; } else { $file_path = $real; } if ( file_exists( $file_path ) && is_file( $file_path ) ) { $zip->addFile( $file_path, $file ); } } $zip->close(); } else { // Use PclZip. require_once \ABSPATH . 'wp-admin/includes/class-pclzip.php'; $zip = new \PclZip( $archive_path ); // Process batch of files. $batch_files = array_slice( $files, $processed, $batch_size ); $files_to_add = array(); foreach ( $batch_files as $file ) { if ( $is_dir ) { $file_path = $real . \DIRECTORY_SEPARATOR . $file; } else { $file_path = $real; } if ( file_exists( $file_path ) && is_file( $file_path ) ) { $files_to_add[] = array( \PCLZIP_ATT_FILE_NAME => $file_path, \PCLZIP_ATT_FILE_NEW_SHORT_NAME => $file, ); } } if ( ! empty( $files_to_add ) ) { if ( 0 === $processed ) { // First batch - create new archive. $result = $zip->create( $files_to_add, \PCLZIP_OPT_REMOVE_ALL_PATH ); } else { // Subsequent batches - add to existing archive. $result = $zip->add( $files_to_add, \PCLZIP_OPT_REMOVE_ALL_PATH ); } if ( 0 === $result ) { \wp_send_json_error( 'PclZip error: ' . $zip->errorInfo( true ) ); } } } $new_processed = $processed + count( $batch_files ); $done = $new_processed >= count( $files ); \wp_send_json_success( array( 'processed' => $new_processed, 'total' => count( $files ), 'done' => $done, ) ); } /** * AJAX: Finalizes archive creation * * @return void * * @since 4.7.1 */ public static function ajax_create_archive_finish() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); if ( empty( $archive_path ) ) { \wp_send_json_error( 'Missing archive path.' ); } // Validate archive exists. if ( ! file_exists( $archive_path ) ) { \wp_send_json_error( 'Archive not found.' ); } $size = filesize( $archive_path ); \wp_send_json_success( array( 'archive' => basename( $archive_path ), 'size' => \size_format( $size ), ) ); } /** * AJAX: Starts archive extraction (initialization phase) * * @return void * * @since 4.7.2 */ public static function ajax_extract_archive_start() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); $target_dir = \sanitize_text_field( $_POST['target_dir'] ?? '' ); if ( empty( $archive_path ) || empty( $target_dir ) ) { \wp_send_json_error( 'Missing parameters.' ); } $real_archive = self::safe_path( $archive_path ); if ( ! $real_archive || ! file_exists( $real_archive ) || ! is_file( $real_archive ) ) { \wp_send_json_error( 'Archive not found or invalid.' ); } $real_target = self::safe_path( $target_dir ); if ( ! $real_target ) { \wp_send_json_error( 'Invalid target directory.' ); } $zip_method = self::get_zip_method(); // Create extraction directory based on archive name. $archive_basename = pathinfo( $real_archive, PATHINFO_FILENAME ); $extract_dir = $real_target . \DIRECTORY_SEPARATOR . $archive_basename; // Check if directory already exists. if ( file_exists( $extract_dir ) ) { \wp_send_json_error( 'Extraction directory already exists: ' . basename( $extract_dir ) ); } $files = array(); if ( 'ziparchive' === $zip_method ) { // Use ZipArchive. $zip = new \ZipArchive(); $res = $zip->open( $real_archive ); if ( true !== $res ) { \wp_send_json_error( 'Failed to open archive. Error code: ' . $res ); } // Get list of files in archive. $total_files = $zip->numFiles; for ( $i = 0; $i < $total_files; $i++ ) { $stat = $zip->statIndex( $i ); if ( false !== $stat ) { $files[] = array( 'name' => $stat['name'], 'index' => $i, 'size' => $stat['size'], 'method' => 'ziparchive', ); } } $zip->close(); } else { // Use PclZip. require_once \ABSPATH . 'wp-admin/includes/class-pclzip.php'; $zip = new \PclZip( $real_archive ); $archive_list = $zip->listContent(); if ( ! is_array( $archive_list ) ) { \wp_send_json_error( 'Failed to read archive: ' . $zip->errorInfo( true ) ); } // Get list of files in archive. foreach ( $archive_list as $i => $file_info ) { $files[] = array( 'name' => $file_info['filename'], 'index' => $i, 'size' => $file_info['size'], 'method' => 'pclzip', ); } } if ( empty( $files ) ) { \wp_send_json_error( 'Archive is empty or cannot be read.' ); } // Create extraction directory. if ( ! @\wp_mkdir_p( $extract_dir ) ) { \wp_send_json_error( 'Failed to create extraction directory.' ); } \wp_send_json_success( array( 'archive_path' => $real_archive, 'extract_dir' => $extract_dir, 'files' => $files, 'total_files' => count( $files ), ) ); } /** * AJAX: Processes a batch of files for extraction * * @return void * * @since 4.7.2 */ public static function ajax_extract_archive_batch() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); $extract_dir = \sanitize_text_field( $_POST['extract_dir'] ?? '' ); $files_json = $_POST['files'] ?? ''; $processed = (int) ( $_POST['processed'] ?? 0 ); $batch_size = (int) ( $_POST['batch_size'] ?? 50 ); if ( empty( $archive_path ) || empty( $extract_dir ) || empty( $files_json ) ) { \wp_send_json_error( 'Missing parameters.' ); } $files = json_decode( \stripslashes( $files_json ), true ); if ( ! is_array( $files ) ) { \wp_send_json_error( 'Invalid files list.' ); } $real_archive = self::safe_path( $archive_path ); if ( ! $real_archive || ! file_exists( $real_archive ) ) { \wp_send_json_error( 'Archive not found.' ); } if ( ! file_exists( $extract_dir ) ) { \wp_send_json_error( 'Extraction directory not found.' ); } // Process batch of files. $batch_files = array_slice( $files, $processed, $batch_size ); $errors = array(); $extracted = 0; // Determine which method was used from the first file. $zip_method = ! empty( $batch_files ) && isset( $batch_files[0]['method'] ) ? $batch_files[0]['method'] : 'ziparchive'; if ( 'ziparchive' === $zip_method ) { // Use ZipArchive. $zip = new \ZipArchive(); $res = $zip->open( $real_archive ); if ( true !== $res ) { \wp_send_json_error( 'Failed to open archive for extraction.' ); } foreach ( $batch_files as $file_info ) { $file_name = $file_info['name']; $index = $file_info['index']; // Skip directories (they'll be created as needed). if ( substr( $file_name, -1 ) === '/' ) { continue; } $target_path = $extract_dir . \DIRECTORY_SEPARATOR . $file_name; // Ensure target directory exists. $target_dir = dirname( $target_path ); if ( ! file_exists( $target_dir ) ) { @\wp_mkdir_p( $target_dir ); } // Extract file. $content = $zip->getFromIndex( $index ); if ( false === $content ) { $errors[] = 'Failed to extract: ' . basename( $file_name ); continue; } if ( false === @file_put_contents( $target_path, $content ) ) { $errors[] = 'Failed to write: ' . basename( $file_name ); continue; } ++$extracted; } $zip->close(); } else { // Use PclZip. require_once \ABSPATH . 'wp-admin/includes/class-pclzip.php'; $zip = new \PclZip( $real_archive ); $archive_list = $zip->listContent(); if ( ! is_array( $archive_list ) ) { \wp_send_json_error( 'Failed to read archive: ' . $zip->errorInfo( true ) ); } foreach ( $batch_files as $file_info ) { $file_name = $file_info['name']; // Skip directories. if ( substr( $file_name, -1 ) === '/' ) { continue; } $target_path = $extract_dir . \DIRECTORY_SEPARATOR . $file_name; // Ensure target directory exists. $target_dir = dirname( $target_path ); if ( ! file_exists( $target_dir ) ) { @\wp_mkdir_p( $target_dir ); } // Extract single file. $result = $zip->extract( \PCLZIP_OPT_BY_NAME, $file_name, \PCLZIP_OPT_PATH, $extract_dir, \PCLZIP_OPT_REMOVE_ALL_PATH ); if ( 0 === $result ) { $errors[] = 'Failed to extract: ' . basename( $file_name ); continue; } // Move file to correct location if needed (PclZip extracts to flat structure with REMOVE_ALL_PATH). if ( strpos( $file_name, '/' ) !== false ) { $flat_file = $extract_dir . \DIRECTORY_SEPARATOR . basename( $file_name ); if ( file_exists( $flat_file ) && $flat_file !== $target_path ) { if ( ! @rename( $flat_file, $target_path ) ) { $errors[] = 'Failed to move: ' . basename( $file_name ); continue; } } } ++$extracted; } } $new_processed = $processed + count( $batch_files ); $done = $new_processed >= count( $files ); \wp_send_json_success( array( 'processed' => $new_processed, 'total' => count( $files ), 'done' => $done, 'extracted' => $extracted, 'errors' => $errors, ) ); } /** * AJAX: Cancels archive extraction and deletes partial files * * @return void * * @since 4.7.2 */ public static function ajax_extract_archive_cancel() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $extract_dir = \sanitize_text_field( $_POST['extract_dir'] ?? '' ); if ( empty( $extract_dir ) ) { \wp_send_json_error( 'Missing extraction directory.' ); } // Delete the partial extraction directory if it exists. if ( file_exists( $extract_dir ) && is_dir( $extract_dir ) ) { self::delete_dir( $extract_dir ); } \wp_send_json_success( 'Extraction cancelled and partial files removed.' ); } /** * AJAX: Finalizes archive extraction * * @return void * * @since 4.7.2 */ public static function ajax_extract_archive_finish() { WP_Helper::verify_admin_nonce( 'advan_file_editor_nonce', '_ajax_nonce' ); $extract_dir = \sanitize_text_field( $_POST['extract_dir'] ?? '' ); $archive_path = \sanitize_text_field( $_POST['archive_path'] ?? '' ); $delete_after = isset( $_POST['delete_archive'] ) && $_POST['delete_archive']; if ( empty( $extract_dir ) ) { \wp_send_json_error( 'Missing extraction directory.' ); } // Validate extraction directory exists. if ( ! file_exists( $extract_dir ) || ! is_dir( $extract_dir ) ) { \wp_send_json_error( 'Extraction directory not found.' ); } // Optionally delete the archive after extraction. // if ( $delete_after && ! empty( $archive_path ) && file_exists( $archive_path ) ) { // @unlink( $archive_path ); // } // Count extracted items. $items = self::count_dir_items( $extract_dir ); \wp_send_json_success( array( 'extract_dir' => basename( $extract_dir ), 'items' => $items, ) ); } /** * Counts items in a directory recursively * * @param string $dir Directory path. * * @return int Number of items. * * @since 4.7.2 */ private static function count_dir_items( $dir ) { $count = 0; if ( ! is_dir( $dir ) ) { return $count; } $dir_handle = opendir( $dir ); if ( false === $dir_handle ) { return $count; } while ( false !== ( $item = readdir( $dir_handle ) ) ) { if ( '.' === $item || '..' === $item ) { continue; } $path = $dir . \DIRECTORY_SEPARATOR . $item; if ( is_dir( $path ) ) { $count += self::count_dir_items( $path ); } else { ++$count; } } closedir( $dir_handle ); return $count; } } }