open( $path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) { @unlink( $sql_tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions return self::record_failure( $type, __( 'Could not create the backup archive.', 'argus-wordpress-defence' ) ); } $zip->addFile( $sql_tmp, 'database.sql' ); self::add_directory_to_zip( $zip, WP_CONTENT_DIR, 'wp-content', array( wp_normalize_path( self::backup_dir() ), wp_normalize_path( WP_CONTENT_DIR . '/uploads/argus-wpd-data/html-cache' ), ) ); $zip->close(); @unlink( $sql_tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.WP.AlternativeFunctions if ( ! file_exists( $path ) ) { return self::record_failure( $type, __( 'The backup archive could not be written to disk.', 'argus-wordpress-defence' ) ); } global $wpdb; $wpdb->insert( Argus_DB::table( 'backups' ), array( 'filename' => $filename, 'type' => $type, 'size_bytes' => filesize( $path ), 'status' => self::STATUS_COMPLETE, 'created_at' => current_time( 'mysql', true ), ) ); update_option( self::LAST_SUCCESS_OPTION, current_time( 'mysql', true ), false ); Argus_Events::record( 'backup_created', 'info', sprintf( 'Backup created (%s).', $type ), array( 'filename' => $filename, 'type' => $type ) ); self::enforce_retention(); return array( 'success' => true, 'message' => __( 'Backup created successfully.', 'argus-wordpress-defence' ) ); } protected static function record_failure( $type, $message ) { global $wpdb; $wpdb->insert( Argus_DB::table( 'backups' ), array( 'filename' => 'failed-' . gmdate( 'Y-m-d-His' ) . '-' . bin2hex( random_bytes( 4 ) ), 'type' => $type, 'size_bytes' => 0, 'status' => self::STATUS_FAILED, 'error_message' => $message, 'created_at' => current_time( 'mysql', true ), ) ); Argus_Events::record( 'backup_failed', 'medium', $message, array( 'type' => $type ) ); return array( 'success' => false, 'message' => $message ); } protected static function dump_database( $dest_path ) { global $wpdb; $handle = fopen( $dest_path, 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( ! $handle ) { return false; } fwrite( $handle, "-- ARGUS Defence database backup\n-- Generated: " . current_time( 'mysql', true ) . " UTC\n\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions fwrite( $handle, "SET FOREIGN_KEY_CHECKS=0;\n\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions $tables = $wpdb->get_col( 'SHOW TABLES' ); // phpcs:ignore foreach ( $tables as $table ) { $create = $wpdb->get_row( "SHOW CREATE TABLE `{$table}`", ARRAY_N ); // phpcs:ignore if ( ! $create ) { continue; } fwrite( $handle, "DROP TABLE IF EXISTS `{$table}`;\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions fwrite( $handle, $create[1] . ";\n\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore $offset = 0; while ( $offset < $total ) { $rows = $wpdb->get_results( "SELECT * FROM `{$table}` LIMIT " . self::DB_DUMP_BATCH_SIZE . " OFFSET {$offset}", ARRAY_A ); // phpcs:ignore foreach ( $rows as $row ) { $columns = array_map( function ( $c ) { return '`' . $c . '`'; }, array_keys( $row ) ); $values = array_map( function ( $v ) use ( $wpdb ) { return null === $v ? 'NULL' : "'" . esc_sql( $v ) . "'"; }, array_values( $row ) ); fwrite( $handle, "INSERT INTO `{$table}` (" . implode( ',', $columns ) . ') VALUES (' . implode( ',', $values ) . ");\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions } $offset += self::DB_DUMP_BATCH_SIZE; } fwrite( $handle, "\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions } fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions return true; } protected static function add_directory_to_zip( ZipArchive $zip, $source_dir, $zip_root, array $exclude_prefixes ) { $source_dir = wp_normalize_path( $source_dir ); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $source_dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); foreach ( $iterator as $item ) { $item_path = wp_normalize_path( $item->getPathname() ); foreach ( $exclude_prefixes as $prefix ) { if ( 0 === strpos( $item_path, $prefix ) ) { continue 2; } } $relative = $zip_root . '/' . ltrim( str_replace( $source_dir, '', $item_path ), '/' ); if ( $item->isDir() ) { $zip->addEmptyDir( $relative ); } else { $zip->addFile( $item_path, $relative ); } } } public static function paginated( $page = 1, $per_page = self::PER_PAGE ) { global $wpdb; $table = Argus_DB::table( 'backups' ); $page = max( 1, (int) $page ); $offset = ( $page - 1 ) * $per_page; $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d OFFSET %d", $per_page, $offset ) // phpcs:ignore ); return array( 'rows' => $rows, 'total' => $total, 'total_pages' => max( 1, (int) ceil( $total / $per_page ) ), 'page' => $page, ); } public static function get( $id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . Argus_DB::table( 'backups' ) . ' WHERE id = %d', $id ) // phpcs:ignore ); } public static function delete( $id ) { $row = self::get( $id ); if ( ! $row ) { return array( 'success' => false, 'message' => __( 'Backup not found.', 'argus-wordpress-defence' ) ); } $path = trailingslashit( self::backup_dir() ) . $row->filename; if ( file_exists( $path ) ) { wp_delete_file( $path ); } global $wpdb; $wpdb->delete( Argus_DB::table( 'backups' ), array( 'id' => $id ), array( '%d' ) ); Argus_Events::record( 'backup_deleted', 'info', 'Backup deleted.', array( 'filename' => $row->filename ) ); return array( 'success' => true, 'message' => __( 'Backup deleted.', 'argus-wordpress-defence' ) ); } protected static function enforce_retention() { $keep = max( 1, (int) Argus_Settings::get( 'backup_retention_count', 5 ) ); global $wpdb; $table = Argus_DB::table( 'backups' ); $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$table} WHERE status = %s ORDER BY created_at DESC", self::STATUS_COMPLETE ) // phpcs:ignore ); foreach ( array_slice( $ids, $keep ) as $id ) { self::delete( (int) $id ); } } public static function status() { $last_success = get_option( self::LAST_SUCCESS_OPTION, '' ); $interval_secs = max( 1, (int) Argus_Settings::get( 'backup_interval_hours', 24 ) ) * HOUR_IN_SECONDS; global $wpdb; $available = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM ' . Argus_DB::table( 'backups' ) . ' WHERE status = %s', self::STATUS_COMPLETE ) // phpcs:ignore ); return array( 'last_backup' => $last_success, 'next_backup' => ( 'manual' !== Argus_Settings::get( 'backup_schedule_mode', 'automatic' ) && $last_success ) ? gmdate( 'Y-m-d H:i:s', strtotime( $last_success . ' UTC' ) + $interval_secs ) : '', 'available' => $available, ); } public static function maybe_run_scheduled() { if ( 'manual' === Argus_Settings::get( 'backup_schedule_mode', 'automatic' ) ) { return; } $last_success = get_option( self::LAST_SUCCESS_OPTION, '' ); $interval_secs = max( 1, (int) Argus_Settings::get( 'backup_interval_hours', 24 ) ) * HOUR_IN_SECONDS; if ( $last_success && ( strtotime( $last_success . ' UTC' ) + $interval_secs ) > time() ) { return; } self::create( self::TYPE_SCHEDULED ); } }