mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
Addresses the first SonarCloud analysis on PR #1 (128 new-code findings). Genuine issues are fixed in code; convention/style rules (camelCase naming php:S100/S101/S116, too-many-returns php:S1142) and the standalone test harnesses are handled via the SonarCloud profile + sonar-project.properties because snake_case + WP_Error guards are the WordPress conventions here. Security: - connector: transfer ids are now whitelisted ([A-Za-z0-9_-]{8,64}) and rejected before any path use; the residual S2083 path-build findings (the resume-keyed store must use the id) carry NOSONAR + rationale after the HMAC gate + strict validation. Complexity (php:S3776) - extracted helpers so no function sits far over the raised threshold: - connector: search_replace -> data_tables/text_columns/rewrite_rows; restore_archive -> restore_db; import -> run_dump_statement. - migrate-abilities: execute_migrate -> push_archive_packets/packet_error/ push_finalize/poll_job. - db-importer: run_statement helper (per-statement execution shared with the trailing no-';' fragment). - restore-engine: restore() -> restore_database(); append_log() dropped its unused $filename parameter. - char-state-machine extractors (extract_statement x2) carry NOSONAR + rationale. Other: - packager: .emcp extension -> const EXT; add_files split into file_iterator/ dir_is_skipped/zip_relative_name. - db-exporter: removed unused $col_ph. - migrate module: single-arg nonces (WP default _wpnonce, matching the view), page-URL helper, dropped unused $back params. - page-migrate view: single-arg wp_nonce_field(). - restore-engine + serialized-search-replace: tabs -> spaces. - sonar-project.properties: exclude tests/** + vendor/** (test harnesses mirror WP core function names by design). Tests: tests/test-migrate-packager.php 8 suites still green; php -l clean. Co-Authored-By: Claude <[email protected]>
199 lines
5.9 KiB
PHP
199 lines
5.9 KiB
PHP
<?php
|
|
/**
|
|
* Database Importer for .emcp SQL dumps.
|
|
*
|
|
* Streams the dump line-by-line and splits statements with a quote-aware
|
|
* scanner, so a semicolon inside a quoted value (or a CREATE TABLE column
|
|
* comment) can never truncate a statement. Session transaction-control
|
|
* directives (SET AUTOCOMMIT / START TRANSACTION / COMMIT / BEGIN / ROLLBACK)
|
|
* are deliberately skipped — running them on the shared $wpdb connection would
|
|
* otherwise leave autocommit off and silently roll every later write back at
|
|
* request end (a live-caught bug in the original Backup/Restore port).
|
|
*
|
|
* @package EMCP_Tools
|
|
* @since 3.12.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
|
|
|
class EMCP_Tools_DB_Importer {
|
|
|
|
/** Directives that must never run on the live connection. */
|
|
const SKIP_PREFIXES = array(
|
|
'SET AUTOCOMMIT',
|
|
'START TRANSACTION',
|
|
'BEGIN',
|
|
'COMMIT',
|
|
'ROLLBACK',
|
|
);
|
|
|
|
/**
|
|
* Import a .sql dump into the current database.
|
|
*
|
|
* @param string $filepath Path to the dump.
|
|
* @return array|false { statements, executed, skipped, errors, error_details } or false when the file is missing.
|
|
*/
|
|
public static function import_from_file( string $filepath ) {
|
|
if ( ! is_file( $filepath ) ) {
|
|
return false;
|
|
}
|
|
global $wpdb;
|
|
$stats = array(
|
|
'statements' => 0,
|
|
'executed' => 0,
|
|
'skipped' => 0,
|
|
'errors' => 0,
|
|
'error_details' => array(),
|
|
);
|
|
|
|
$handle = fopen( $filepath, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
if ( false === $handle ) {
|
|
return false;
|
|
}
|
|
$buffer = '';
|
|
while ( false !== ( $line = fgets( $handle ) ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
$buffer .= $line;
|
|
while ( null !== ( $stmt = self::extract_statement( $buffer ) ) ) {
|
|
$stats['statements']++;
|
|
$trimmed = self::strip_leading_comments( $stmt );
|
|
if ( self::is_skip_directive( $trimmed ) ) {
|
|
$stats['skipped']++;
|
|
continue;
|
|
}
|
|
self::run_statement( $trimmed, $stats );
|
|
}
|
|
}
|
|
// Trailing statement with no final ';' — execute it if anything remains.
|
|
$trimmed = self::strip_leading_comments( $buffer );
|
|
if ( '' !== $trimmed && ! self::is_skip_directive( $trimmed ) ) {
|
|
$stats['statements']++;
|
|
self::run_statement( $trimmed, $stats );
|
|
}
|
|
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Run one dump statement through the live connection, updating stats.
|
|
*
|
|
* @param string $sql Trimmed statement (directives already filtered).
|
|
* @param array $stats In/out counters.
|
|
*/
|
|
private static function run_statement( string $sql, array &$stats ): void {
|
|
global $wpdb;
|
|
$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB -- validated dump statement; admin-authorized restore.
|
|
if ( false === $result ) {
|
|
$stats['errors']++;
|
|
if ( count( $stats['error_details'] ) < 20 ) {
|
|
$stats['error_details'][] = array(
|
|
'error' => $wpdb->last_error,
|
|
'stmt' => self::truncate( $sql, 200 ),
|
|
);
|
|
}
|
|
} else {
|
|
$stats['executed']++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop full-line `--` comments that lead a statement so a directive after a
|
|
* dump banner (which begins with `--`) is still recognised as such.
|
|
*
|
|
* @param string $stmt Statement (may include leading comment lines).
|
|
* @return string Comment-free statement.
|
|
*/
|
|
private static function strip_leading_comments( string $stmt ): string {
|
|
$out = preg_replace( '/^[ \t]*--[^\r\n]*(?:\r?\n|$)/m', '', $stmt );
|
|
return null === $out ? $stmt : trim( $out );
|
|
}
|
|
|
|
/**
|
|
* Pull one complete statement (ending in a top-level ';') off the buffer.
|
|
*
|
|
* @param string $buffer In/out accumulator (reference).
|
|
* @return string|null The statement including its trailing ';', or null when no complete statement exists yet.
|
|
*/
|
|
private static function extract_statement( string &$buffer ) { // NOSONAR -- a per-character SQL quote/comment state machine has no meaningful decomposition below its branch table.
|
|
$len = strlen( $buffer );
|
|
$i = 0;
|
|
$sq = false; // Single-quoted string.
|
|
$dq = false; // Double-quoted string.
|
|
$bt = false; // Backtick-quoted identifier.
|
|
$lc = false; // Line comment (-- to EOL).
|
|
$bc = false; // Block comment (/* ... */).
|
|
while ( $i < $len ) {
|
|
$c = $buffer[ $i ];
|
|
if ( $lc ) {
|
|
if ( "\n" === $c ) {
|
|
$lc = false;
|
|
}
|
|
} elseif ( $bc ) {
|
|
if ( '*' === $c && isset( $buffer[ $i + 1 ] ) && '/' === $buffer[ $i + 1 ] ) {
|
|
$bc = false;
|
|
$i++;
|
|
}
|
|
} elseif ( $sq || $dq || $bt ) {
|
|
if ( '\\' === $c ) {
|
|
$i++; // Skip escaped character.
|
|
} elseif ( $sq && "'" === $c ) {
|
|
$sq = false;
|
|
} elseif ( $dq && '"' === $c ) {
|
|
$dq = false;
|
|
} elseif ( $bt && '`' === $c ) {
|
|
$bt = false;
|
|
}
|
|
} else {
|
|
if ( "'" === $c ) {
|
|
$sq = true;
|
|
} elseif ( '"' === $c ) {
|
|
$dq = true;
|
|
} elseif ( '`' === $c ) {
|
|
$bt = true;
|
|
} elseif ( '-' === $c && isset( $buffer[ $i + 1 ] ) && '-' === $buffer[ $i + 1 ] ) {
|
|
$lc = true;
|
|
$i++;
|
|
} elseif ( '/' === $c && isset( $buffer[ $i + 1 ] ) && '*' === $buffer[ $i + 1 ] ) {
|
|
$bc = true;
|
|
$i++;
|
|
} elseif ( ';' === $c ) {
|
|
$stmt = substr( $buffer, 0, $i + 1 );
|
|
$buffer = substr( $buffer, $i + 1 );
|
|
return $stmt;
|
|
}
|
|
}
|
|
$i++;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Whether a trimmed statement is a session transaction-control directive.
|
|
*
|
|
* @param string $stmt Trimmed statement.
|
|
* @return bool
|
|
*/
|
|
private static function is_skip_directive( string $stmt ): bool {
|
|
$upper = strtoupper( $stmt );
|
|
foreach ( self::SKIP_PREFIXES as $prefix ) {
|
|
if ( 0 === strpos( $upper, $prefix ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Truncate a statement for an error report.
|
|
*
|
|
* @param string $stmt Statement.
|
|
* @param int $max Max chars.
|
|
* @return string
|
|
*/
|
|
private static function truncate( string $stmt, int $max = 200 ): string {
|
|
if ( strlen( $stmt ) <= $max ) {
|
|
return $stmt;
|
|
}
|
|
return substr( $stmt, 0, $max ) . '…';
|
|
}
|
|
}
|