From bfdea91bc744c994091c89d7b5a47d9b28b49982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 24 Sep 2025 00:17:45 +0100 Subject: [PATCH 1/5] Support auth for phpmyadmin --- .../handler-sqlite-translation.php | 2 +- php-implementation/mysql-server.php | 177 +++++++++++++++++- php-implementation/run-pdo.php | 2 +- php-implementation/run-sqlite-translation.php | 4 +- .../sqlite-database-integration | 2 +- 5 files changed, 174 insertions(+), 13 deletions(-) diff --git a/php-implementation/handler-sqlite-translation.php b/php-implementation/handler-sqlite-translation.php index 717491b..c6b1b4a 100644 --- a/php-implementation/handler-sqlite-translation.php +++ b/php-implementation/handler-sqlite-translation.php @@ -22,7 +22,7 @@ class SQLiteTranslationHandler implements MySQLQueryHandler { public function __construct($sqlite_database_path) { define('FQDB', $sqlite_database_path); define('FQDBDIR', dirname(FQDB) . '/'); - $this->wpdb = new WP_SQLite_DB(); + $this->wpdb = new WP_SQLite_DB($sqlite_database_path); } public function handleQuery(string $query): MySQLServerQueryResult { diff --git a/php-implementation/mysql-server.php b/php-implementation/mysql-server.php index f616735..56027f0 100644 --- a/php-implementation/mysql-server.php +++ b/php-implementation/mysql-server.php @@ -398,17 +398,150 @@ public function receiveBytes(string $data): ?string { * @return string Response packet to send back */ private function processAuthentication(string $payload): string { - // For simplicity, we're auto-accepting all auth attempts - // In a real implementation, you would parse the handshake response - // and verify username/password - + $offset = 0; + $payloadLength = strlen($payload); + + $capabilityFlags = $this->readUnsignedIntLittleEndian($payload, $offset, 4); + $offset += 4; + + $clientMaxPacketSize = $this->readUnsignedIntLittleEndian($payload, $offset, 4); + $offset += 4; + + $clientCharacterSet = 0; + if ($offset < $payloadLength) { + $clientCharacterSet = ord($payload[$offset]); + } + $offset += 1; + + // Skip reserved bytes (always zero) + $offset = min($payloadLength, $offset + 23); + + $username = $this->readNullTerminatedString($payload, $offset); + + $authResponse = ''; + if ($capabilityFlags & MySQLProtocol::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { + $authResponseLength = $this->readLengthEncodedInt($payload, $offset); + $authResponse = substr($payload, $offset, $authResponseLength); + $offset = min($payloadLength, $offset + $authResponseLength); + } elseif ($capabilityFlags & MySQLProtocol::CLIENT_SECURE_CONNECTION) { + $authResponseLength = 0; + if ($offset < $payloadLength) { + $authResponseLength = ord($payload[$offset]); + } + $offset += 1; + $authResponse = substr($payload, $offset, $authResponseLength); + $offset = min($payloadLength, $offset + $authResponseLength); + } else { + $authResponse = $this->readNullTerminatedString($payload, $offset); + } + + $database = ''; + if ($capabilityFlags & MySQLProtocol::CLIENT_CONNECT_WITH_DB) { + $database = $this->readNullTerminatedString($payload, $offset); + } + + $authPluginName = ''; + if ($capabilityFlags & MySQLProtocol::CLIENT_PLUGIN_AUTH) { + $authPluginName = $this->readNullTerminatedString($payload, $offset); + } + + if ($capabilityFlags & MySQLProtocol::CLIENT_CONNECT_ATTRS) { + $attrsLength = $this->readLengthEncodedInt($payload, $offset); + $offset = min($payloadLength, $offset + $attrsLength); + } + $this->authenticated = true; - $this->sequence_id = 2; // sequence continues: handshake was seq 0, auth response seq 1 - + $this->sequence_id = 2; + + $responsePackets = ''; + + if ($authPluginName === MySQLProtocol::AUTH_PLUGIN_NAME) { + $fastAuthPayload = chr(MySQLProtocol::AUTH_MORE_DATA) . chr(MySQLProtocol::CACHING_SHA2_FAST_AUTH); + $responsePackets .= MySQLProtocol::encodeInt24(strlen($fastAuthPayload)); + $responsePackets .= MySQLProtocol::encodeInt8($this->sequence_id++); + $responsePackets .= $fastAuthPayload; + } + $okPacket = MySQLProtocol::buildOkPacket(); - return MySQLProtocol::encodeInt24(strlen($okPacket)) . - MySQLProtocol::encodeInt8($this->sequence_id++) . - $okPacket; + $responsePackets .= MySQLProtocol::encodeInt24(strlen($okPacket)); + $responsePackets .= MySQLProtocol::encodeInt8($this->sequence_id++); + $responsePackets .= $okPacket; + + return $responsePackets; + } + + private function readUnsignedIntLittleEndian(string $payload, int $offset, int $length): int { + $slice = substr($payload, $offset, $length); + if ($slice === '' || $length <= 0) { + return 0; + } + + switch ($length) { + case 1: + return ord($slice[0]); + case 2: + $padded = str_pad($slice, 2, "\x00", STR_PAD_RIGHT); + $unpacked = unpack('v', $padded); + return $unpacked[1] ?? 0; + case 3: + case 4: + default: + $padded = str_pad($slice, 4, "\x00", STR_PAD_RIGHT); + $unpacked = unpack('V', $padded); + return $unpacked[1] ?? 0; + } + } + + private function readNullTerminatedString(string $payload, int &$offset): string { + $nullPosition = strpos($payload, "\0", $offset); + if ($nullPosition === false) { + $result = substr($payload, $offset); + $offset = strlen($payload); + return $result; + } + + $result = substr($payload, $offset, $nullPosition - $offset); + $offset = $nullPosition + 1; + return $result; + } + + private function readLengthEncodedInt(string $payload, int &$offset): int { + if ($offset >= strlen($payload)) { + return 0; + } + + $first = ord($payload[$offset]); + $offset += 1; + + if ($first < 0xfb) { + return $first; + } + + if ($first === 0xfb) { + return 0; + } + + if ($first === 0xfc) { + $value = $this->readUnsignedIntLittleEndian($payload, $offset, 2); + $offset += 2; + return $value; + } + + if ($first === 0xfd) { + $value = $this->readUnsignedIntLittleEndian($payload, $offset, 3); + $offset += 3; + return $value; + } + + // 0xfe indicates an 8-byte integer + $value = 0; + $slice = substr($payload, $offset, 8); + if ($slice !== '') { + $slice = str_pad($slice, 8, "\x00"); + $value = unpack('P', $slice)[1]; + } + $offset += 8; + return (int) $value; } /** @@ -576,7 +709,9 @@ public function start() { $this->clientServers[$clientId] = new MySQLGateway($this->query_handler); // Send initial handshake + echo "Pre handshake\n"; $handshake = $this->clientServers[$clientId]->getInitialHandshake(); + echo "Post handshake\n"; socket_write($client, $handshake); } // Remove server socket from read array @@ -584,8 +719,24 @@ public function start() { } // Handle client activity + echo "Waiting for client activity\n"; foreach ($read as $client) { + echo "calling socket_read\n"; $data = @socket_read($client, 4096); + echo "socket_read returned\n"; + $display = ''; + for ($i = 0; $i < strlen($data); $i++) { + $byte = ord($data[$i]); + if ($byte >= 32 && $byte <= 126) { + // Printable ASCII character + $display .= $data[$i]; + } else { + // Non-printable, show as hex + $display .= sprintf('%02x ', $byte); + } + } + echo rtrim($display) . "\n"; + if ($data === false || $data === '') { // Client disconnected echo "Client disconnected.\n"; @@ -600,13 +751,18 @@ public function start() { try { // Process the data $clientId = spl_object_id($client); + echo "Receiving bytes\n"; $response = $this->clientServers[$clientId]->receiveBytes($data); if ($response) { + echo "Writing response\n"; + echo $response; socket_write($client, $response); } + echo "Response written\n"; // Process any buffered data while ($this->clientServers[$clientId]->hasBufferedData()) { + echo "Processing buffered data\n"; try { $response = $this->clientServers[$clientId]->receiveBytes(''); if ($response) { @@ -616,10 +772,13 @@ public function start() { break; } } + echo "After the while loop\n"; } catch (IncompleteInputException $e) { + echo "Incomplete input exception\n"; continue; } } + echo "restarting the while() loop!\n"; } } } diff --git a/php-implementation/run-pdo.php b/php-implementation/run-pdo.php index 28a7816..642882f 100644 --- a/php-implementation/run-pdo.php +++ b/php-implementation/run-pdo.php @@ -10,6 +10,6 @@ $db_path = __DIR__ . '/database/test.db'; $server = new MySQLSocketServer( new PDOHandler(new PDO("sqlite:$db_path")), - ['port' => 3306] + ['port' => 3316] ); $server->start(); diff --git a/php-implementation/run-sqlite-translation.php b/php-implementation/run-sqlite-translation.php index 00a1a8e..f07bb49 100644 --- a/php-implementation/run-sqlite-translation.php +++ b/php-implementation/run-sqlite-translation.php @@ -9,9 +9,11 @@ require_once __DIR__ . '/mysql-server.php'; require_once __DIR__ . '/handler-sqlite-translation.php'; +define('WP_SQLITE_AST_DRIVER', true); + $server = new MySQLSocketServer( new SQLiteTranslationHandler(__DIR__ . '/database/test.db'), - ['port' => 3306] + ['port' => 3316] ); $server->start(); diff --git a/php-implementation/sqlite-database-integration b/php-implementation/sqlite-database-integration index 271163b..0700853 160000 --- a/php-implementation/sqlite-database-integration +++ b/php-implementation/sqlite-database-integration @@ -1 +1 @@ -Subproject commit 271163bf750f72caa93da36ff61482f4110d8e9f +Subproject commit 0700853c484871d7ab266ac01bcd8e31eab61557 From 059ec211a6d228d286b59565d4c2d0b149646e2f Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 25 Sep 2025 22:44:46 +0100 Subject: [PATCH 2/5] Making Adminer work WIP --- client.php | 14 ++- .../handler-sqlite-translation.php | 95 ++++++++------- php-implementation/mysql-server.php | 113 +++++++++++++----- .../sqlite-database-integration | 2 +- 4 files changed, 146 insertions(+), 78 deletions(-) diff --git a/client.php b/client.php index af82bda..c137580 100644 --- a/client.php +++ b/client.php @@ -1,7 +1,7 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e) { die("Connection failed: " . $e->getMessage()); @@ -14,14 +14,16 @@ decimal_column DECIMAL(10,2) NOT NULL DEFAULT 0, float_column FLOAT(10,2) NOT NULL DEFAULT 0, enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a', - date_column DATE NOT NULL DEFAULT CURRENT_DATE, - PRIMARY KEY (ID), + date_column DATE NOT NULL, + PRIMARY KEY (ID) ) - "); -$result = $pdo->exec("INSERT INTO wptests_users (decimal_column, float_column, enum_column, date_column) VALUES (123.45, 678.90, 'b', '2024-02-14')"); +"); + +$result = $pdo->exec("INSERT INTO wptests_users (decimal_column, float_column, enum_column, date_column) VALUES (123.45, 678.90, 'a', '2024-02-14')"); +$result = $pdo->exec("INSERT INTO wptests_users (decimal_column, float_column, enum_column, date_column) VALUES (987, 321, 'b', '2024-02-14')"); $stmt = $pdo->prepare("SELECT * FROM wptests_users WHERE ID > :id"); -$stmt->execute(['id' => 0]); +$stmt->execute(['id' => 2]); $row = $stmt->fetch(PDO::FETCH_ASSOC); var_dump($row); diff --git a/php-implementation/handler-sqlite-translation.php b/php-implementation/handler-sqlite-translation.php index c6b1b4a..6c703bc 100644 --- a/php-implementation/handler-sqlite-translation.php +++ b/php-implementation/handler-sqlite-translation.php @@ -9,6 +9,12 @@ function apply_filters($tag, $value) { return $value; } +// A dummy polyfill – function is called by the wpdb class. +function wp_debug_backtrace_summary( $ignore_class = null, $skip_frames = 0, $pretty = true ) { + return 'unknown'; +} + +require_once __DIR__ . '/sqlite-database-integration/version.php'; require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-lexer.php'; require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php'; require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php'; @@ -22,7 +28,7 @@ class SQLiteTranslationHandler implements MySQLQueryHandler { public function __construct($sqlite_database_path) { define('FQDB', $sqlite_database_path); define('FQDBDIR', dirname(FQDB) . '/'); - $this->wpdb = new WP_SQLite_DB($sqlite_database_path); + $this->wpdb = new WP_SQLite_DB('wordpress'); } public function handleQuery(string $query): MySQLServerQueryResult { @@ -36,52 +42,59 @@ public function handleQuery(string $query): MySQLServerQueryResult { ); } $rows = $this->wpdb->get_results($query, ARRAY_A); - $columns = $this->computeColumnInfo($rows); + if ($this->wpdb->last_error) { + return new ErrorQueryResult($this->wpdb->last_error); + } + $columns = $this->computeColumnInfo(); return new SelectQueryResult($columns, $rows); } - public function computeColumnInfo($rows) { - if (empty($rows)) { - return []; - } - + public function computeColumnInfo() { $columns = []; - $firstRow = $rows[0]; - - foreach ($firstRow as $key => $value) { - $columnType = 8; // Default to LONGLONG - $columnLength = 1; - $decimals = 0; - - // Analyze all rows to find the maximum length and most specific type - foreach ($rows as $row) { - $currentValue = $row[$key]; - - if (is_string($currentValue)) { - $columnType = 253; // VARCHAR - $columnLength = max($columnLength, strlen($currentValue)); - } elseif (is_numeric($currentValue)) { - if (is_int($currentValue) || $currentValue == (int)$currentValue) { - if ($columnType != 253) { // Don't override VARCHAR - $columnType = 3; // LONG - $columnLength = 11; - } - } else { - if ($columnType != 253) { // Don't override VARCHAR - $columnType = 246; // DECIMAL - $columnLength = 10; - $decimals = 2; - } - } - } + + $column_meta = $this->wpdb->get_dbh()->get_last_column_meta(); + + $types = [ + 'DECIMAL' => MySQLProtocol::FIELD_TYPE_DECIMAL, + 'TINY' => MySQLProtocol::FIELD_TYPE_TINY, + 'SHORT' => MySQLProtocol::FIELD_TYPE_SHORT, + 'LONG' => MySQLProtocol::FIELD_TYPE_LONG, + 'FLOAT' => MySQLProtocol::FIELD_TYPE_FLOAT, + 'DOUBLE' => MySQLProtocol::FIELD_TYPE_DOUBLE, + 'NULL' => MySQLProtocol::FIELD_TYPE_NULL, + 'TIMESTAMP' => MySQLProtocol::FIELD_TYPE_TIMESTAMP, + 'LONGLONG' => MySQLProtocol::FIELD_TYPE_LONGLONG, + 'INT24' => MySQLProtocol::FIELD_TYPE_INT24, + 'DATE' => MySQLProtocol::FIELD_TYPE_DATE, + 'TIME' => MySQLProtocol::FIELD_TYPE_TIME, + 'DATETIME' => MySQLProtocol::FIELD_TYPE_DATETIME, + 'YEAR' => MySQLProtocol::FIELD_TYPE_YEAR, + 'NEWDATE' => MySQLProtocol::FIELD_TYPE_NEWDATE, + 'VARCHAR' => MySQLProtocol::FIELD_TYPE_VARCHAR, + 'BIT' => MySQLProtocol::FIELD_TYPE_BIT, + 'NEWDECIMAL' => MySQLProtocol::FIELD_TYPE_NEWDECIMAL, + 'ENUM' => MySQLProtocol::FIELD_TYPE_ENUM, + 'SET' => MySQLProtocol::FIELD_TYPE_SET, + 'TINY_BLOB' => MySQLProtocol::FIELD_TYPE_TINY_BLOB, + 'MEDIUM_BLOB' => MySQLProtocol::FIELD_TYPE_MEDIUM_BLOB, + 'LONG_BLOB' => MySQLProtocol::FIELD_TYPE_LONG_BLOB, + 'BLOB' => MySQLProtocol::FIELD_TYPE_BLOB, + 'VAR_STRING' => MySQLProtocol::FIELD_TYPE_VAR_STRING, + 'STRING' => MySQLProtocol::FIELD_TYPE_STRING, + 'GEOMETRY' => MySQLProtocol::FIELD_TYPE_GEOMETRY, + ]; + + foreach ($column_meta as $column) { + $type = $types[$column['native_type']]; + if ( null === $type ) { + throw new Exception('Unknown column type: ' . $column['native_type']); } - $columns[] = [ - 'name' => $key, - 'length' => $columnLength ?? 1, - 'type' => $columnType, - 'flags' => 129, - 'decimals' => $decimals + 'name' => $column['name'], + 'length' => $column['len'], + 'type' => $type, + 'flags' => 129, + 'decimals' => $column['precision'] ]; } return $columns; diff --git a/php-implementation/mysql-server.php b/php-implementation/mysql-server.php index 56027f0..8e94e19 100644 --- a/php-implementation/mysql-server.php +++ b/php-implementation/mysql-server.php @@ -76,38 +76,42 @@ class MySQLProtocol { /** * MySQL command types - * + * * @see https://dev.mysql.com/doc/dev/mysql-server/8.4.3/page_protocol_command_phase.html */ - /** Tells the server that the client wants it to close the connection. */ - const COM_QUIT = 0x01; - /** Tells the server to execute a query. */ - const COM_QUERY = 0x03; - /** Check if the server is alive. */ - const COM_PING = 0x0E; - /** Tells the server to send the binlog dump. */ - const COM_BINLOG_DUMP = 0x12; - /** Tells the server to register a slave. */ - const COM_REGISTER_SLAVE = 0x15; - /** - * The server returns a COM_STMT_PREPARE Response which contains a statement-id which is ised to identify the prepared statement. - */ - const COM_STMT_PREPARE = 0x16; - /** - * Asks the server to execute a prepared statement as identified by statement_id. - */ - const COM_STMT_EXECUTE = 0x17; - const COM_STMT_CLOSE = 0x19; - /** - * COM_STMT_RESET resets the data of a prepared statement which was accumulated with - * COM_STMT_SEND_LONG_DATA commands and closes the cursor if it was opened with - * COM_STMT_EXECUTE. - * - * The server will send a OK_Packet if the statement could be reset, a ERR_Packet if not. - * - * @see https://dev.mysql.com/doc/dev/mysql-server/8.4.3/page_protocol_com_stmt_reset.html - */ - const COM_STMT_RESET = 0x1A; + const COM_SLEEP = 0x00; /** Tells the server to sleep for the given number of seconds. */ + const COM_QUIT = 0x01; /** Tells the server that the client wants it to close the connection. */ + const COM_INIT_DB = 0x02; /** Change the default schema of the connection. */ + const COM_QUERY = 0x03; /** Tells the server to execute a query. */ + const COM_FIELD_LIST = 0x04; /** Deprecated. Returns the list of fields for the given table. */ + const COM_CREATE_DB = 0x05; /** Currently refused by the server. */ + const COM_DROP_DB = 0x06; /** Currently refused by the server. */ + const COM_UNUSED_2 = 0x07; /** Unused. Used to be COM_REFRESH. */ + const COM_UNUSED_1 = 0x08; /** Unused. Used to be COM_SHUTDOWN. */ + const COM_STATISTICS = 0x09; /** Get a human readable string of some internal status vars. */ + const COM_UNUSED_4 = 0x0A; /** Unused. Used to be COM_PROCESS_INFO. */ + const COM_CONNECT = 0x0B; /** Currently refused by the server. */ + const COM_UNUSED_5 = 0x0C; /** Unused. Used to be COM_PROCESS_KILL. */ + const COM_DEBUG = 0x0D; /** Dump debug info to server's stdout. */ + const COM_PING = 0x0E; /** Check if the server is alive. */ + const COM_TIME = 0x0F; /** Currently refused by the server. */ + const COM_DELAYED_INSERT = 0x10; /** Functionality removed. */ + const COM_CHANGE_USER = 0x11; /** Change the user of the connection. */ + const COM_BINLOG_DUMP = 0x12; /** Tells the server to send the binlog dump. */ + const COM_TABLE_DUMP = 0x13; /** Tells the server to send the table dump. */ + const COM_CONNECT_OUT = 0x14; /** Currently refused by the server. */ + const COM_REGISTER_SLAVE = 0x15; /** Tells the server to register a slave. */ + const COM_STMT_PREPARE = 0x16; /** Tells the server to prepare a statement. */ + const COM_STMT_EXECUTE = 0x17; /** Tells the server to execute a prepared statement. */ + const COM_STMT_SEND_LONG_DATA = 0x18; /** Tells the server to send long data for a prepared statement. */ + const COM_STMT_CLOSE = 0x19; /** Tells the server to close a prepared statement. */ + const COM_STMT_RESET = 0x1A; /** Tells the server to reset a prepared statement. */ + const COM_SET_OPTION = 0x1B; /** Tells the server to set an option. */ + const COM_STMT_FETCH = 0x1C; /** Tells the server to fetch a result from a prepared statement. */ + const COM_DAEMON = 0x1D; /** Currently refused by the server. */ + const COM_BINLOG_DUMP_GTID = 0x1E; /** Tells the server to send the binlog dump in GTID mode. */ + const COM_RESET_CONNECTION = 0x1F; /** Tells the server to reset the connection. */ + const COM_CLONE = 0x20; /** Tells the server to clone a server. */ // Special packet markers const OK_PACKET = 0x00; @@ -120,12 +124,57 @@ class MySQLProtocol { const CACHING_SHA2_FULL_AUTH = 4; const AUTH_PLUGIN_NAME = 'caching_sha2_password'; + // Field types + const FIELD_TYPE_DECIMAL = 0x00; + const FIELD_TYPE_TINY = 0x01; + const FIELD_TYPE_SHORT = 0x02; + const FIELD_TYPE_LONG = 0x03; + const FIELD_TYPE_FLOAT = 0x04; + const FIELD_TYPE_DOUBLE = 0x05; + const FIELD_TYPE_NULL = 0x06; + const FIELD_TYPE_TIMESTAMP = 0x07; + const FIELD_TYPE_LONGLONG = 0x08; + const FIELD_TYPE_INT24 = 0x09; + const FIELD_TYPE_DATE = 0x0a; + const FIELD_TYPE_TIME = 0x0b; + const FIELD_TYPE_DATETIME = 0x0c; + const FIELD_TYPE_YEAR = 0x0d; + const FIELD_TYPE_NEWDATE = 0x0e; + const FIELD_TYPE_VARCHAR = 0x0f; + const FIELD_TYPE_BIT = 0x10; + const FIELD_TYPE_NEWDECIMAL = 0xf6; + const FIELD_TYPE_ENUM = 0xf7; + const FIELD_TYPE_SET = 0xf8; + const FIELD_TYPE_TINY_BLOB = 0xf9; + const FIELD_TYPE_MEDIUM_BLOB = 0xfa; + const FIELD_TYPE_LONG_BLOB = 0xfb; + const FIELD_TYPE_BLOB = 0xfc; + const FIELD_TYPE_VAR_STRING = 0xfd; + const FIELD_TYPE_STRING = 0xfe; + const FIELD_TYPE_GEOMETRY = 0xff; + + // Field flags + const NOT_NULL_FLAG = 0x1; + const PRI_KEY_FLAG = 0x2; + const UNIQUE_KEY_FLAG = 0x4; + const MULTIPLE_KEY_FLAG = 0x8; + const BLOB_FLAG = 0x10; + const UNSIGNED_FLAG = 0x20; + const ZEROFILL_FLAG = 0x40; + const BINARY_FLAG = 0x80; + const ENUM_FLAG = 0x100; + const AUTO_INCREMENT_FLAG = 0x200; + const TIMESTAMP_FLAG = 0x400; + const SET_FLAG = 0x800; + // Character set and collation constants (using utf8mb4 general collation) const CHARSET_UTF8MB4 = 0xff; // Collation ID 255 (utf8mb4_0900_ai_ci) // Max packet length constant const MAX_PACKET_LENGTH = 0x00ffffff; + private $current_db = ''; + // Helper: Packets assembly and parsing public static function encodeInt8(int $val): string { return chr($val & 0xff); @@ -382,6 +431,10 @@ public function receiveBytes(string $data): ?string { if ($command === MySQLProtocol::COM_QUERY) { $query = substr($payload, 1); return $this->processQuery($query); + } elseif ($command === MySQLProtocol::COM_INIT_DB) { + return $this->processQuery('USE ' . substr($payload, 1)); + } elseif ($command === MySQLProtocol::COM_QUIT) { + return ''; } else { // Unsupported command $errPacket = MySQLProtocol::buildErrPacket(0x04D2, "HY000", "Unsupported command"); diff --git a/php-implementation/sqlite-database-integration b/php-implementation/sqlite-database-integration index 0700853..04ce62f 160000 --- a/php-implementation/sqlite-database-integration +++ b/php-implementation/sqlite-database-integration @@ -1 +1 @@ -Subproject commit 0700853c484871d7ab266ac01bcd8e31eab61557 +Subproject commit 04ce62f91aaa78127b70451ecbe740152c4210e3 From d0f8a46bfa5d4465e1bf87f50d401d71ff7b6d2d Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Sat, 27 Sep 2025 18:31:47 +0200 Subject: [PATCH 3/5] Remove dependency on WPDB, use the SQLite driver directly --- .../handler-sqlite-translation.php | 75 ++++++++++--------- .../sqlite-database-integration | 2 +- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/php-implementation/handler-sqlite-translation.php b/php-implementation/handler-sqlite-translation.php index 6c703bc..4ca8b90 100644 --- a/php-implementation/handler-sqlite-translation.php +++ b/php-implementation/handler-sqlite-translation.php @@ -2,57 +2,62 @@ define('WP_DEBUG', false); -require_once __DIR__ . '/wpdb-polyfill.php'; - -// A polyfill – function is called by the wpdb class. -function apply_filters($tag, $value) { - return $value; -} - -// A dummy polyfill – function is called by the wpdb class. -function wp_debug_backtrace_summary( $ignore_class = null, $skip_frames = 0, $pretty = true ) { - return 'unknown'; -} - require_once __DIR__ . '/sqlite-database-integration/version.php'; -require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-lexer.php'; -require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php'; -require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php'; -require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-token.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/parser/class-wp-parser-grammar.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/parser/class-wp-parser.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/parser/class-wp-parser-node.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/parser/class-wp-parser-token.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/mysql/class-wp-mysql-token.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/mysql/class-wp-mysql-lexer.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/mysql/class-wp-mysql-parser.php'; require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; -require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php'; +require_once __DIR__ . '/sqlite-database-integration/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php'; + class SQLiteTranslationHandler implements MySQLQueryHandler { - private $wpdb; + /** @var WP_SQLite_Driver */ + private $sqlite_driver; public function __construct($sqlite_database_path) { define('FQDB', $sqlite_database_path); define('FQDBDIR', dirname(FQDB) . '/'); - $this->wpdb = new WP_SQLite_DB('wordpress'); + + $this->sqlite_driver = new WP_SQLite_Driver( + new WP_SQLite_Connection( array( 'path' => $sqlite_database_path ) ), + 'wordpress' + ); } public function handleQuery(string $query): MySQLServerQueryResult { - // An extremely naive check. We should be using the MySQL parser to - // determine this: - if(!str_starts_with(strtolower($query), 'select')) { - $this->wpdb->query($query); - return new OkayPacketResult( - $this->wpdb->rows_affected ?? 0, - $this->wpdb->insert_id ?? 0 - ); - } - $rows = $this->wpdb->get_results($query, ARRAY_A); - if ($this->wpdb->last_error) { - return new ErrorQueryResult($this->wpdb->last_error); + try { + // An extremely naive check. We should be using the MySQL parser to + // determine this: + if(!str_starts_with(strtolower($query), 'select')) { + $this->sqlite_driver->query($query); + return new OkayPacketResult( + $this->sqlite_driver->get_last_return_value() ?? 0, + $this->sqlite_driver->get_insert_id() ?? 0 + ); + } + + $rows = $this->sqlite_driver->query($query, PDO::FETCH_ASSOC); + $columns = $this->computeColumnInfo(); + return new SelectQueryResult($columns, $rows); + } catch (Throwable $e) { + return new ErrorQueryResult($e->getMessage()); } - $columns = $this->computeColumnInfo(); - return new SelectQueryResult($columns, $rows); } public function computeColumnInfo() { $columns = []; - $column_meta = $this->wpdb->get_dbh()->get_last_column_meta(); + $column_meta = $this->sqlite_driver->get_last_column_meta(); $types = [ 'DECIMAL' => MySQLProtocol::FIELD_TYPE_DECIMAL, @@ -85,7 +90,7 @@ public function computeColumnInfo() { ]; foreach ($column_meta as $column) { - $type = $types[$column['native_type']]; + $type = $types[$column['native_type']] ?? null; if ( null === $type ) { throw new Exception('Unknown column type: ' . $column['native_type']); } diff --git a/php-implementation/sqlite-database-integration b/php-implementation/sqlite-database-integration index 04ce62f..b33f923 160000 --- a/php-implementation/sqlite-database-integration +++ b/php-implementation/sqlite-database-integration @@ -1 +1 @@ -Subproject commit 04ce62f91aaa78127b70451ecbe740152c4210e3 +Subproject commit b33f923d0ea80e01507cb509c5af58ede3679637 From b1f970b9f235ed969431e3c24da5bd77517b75d7 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 16 Oct 2025 12:55:34 +0200 Subject: [PATCH 4/5] Fix "mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after end of packet" --- php-implementation/mysql-server.php | 6 +++++- php-implementation/run-sqlite-translation.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/php-implementation/mysql-server.php b/php-implementation/mysql-server.php index 8e94e19..11e56c4 100644 --- a/php-implementation/mysql-server.php +++ b/php-implementation/mysql-server.php @@ -323,7 +323,11 @@ public static function buildResultSetPackets(SelectQueryResult $result): string // 4. Row data packets (each row is a series of length-encoded values) foreach ($result->rows as $row) { $rowPayload = ""; - foreach ($row as $val) { + // Iterate through columns in the defined order to match column definitions + foreach ($result->columns as $col) { + $columnName = $col['name']; + $val = $row->{$columnName} ?? null; + if ($val === null) { // NULL is represented by 0xfb (NULL_VALUE) $rowPayload .= "\xfb"; diff --git a/php-implementation/run-sqlite-translation.php b/php-implementation/run-sqlite-translation.php index f07bb49..b8f3a11 100644 --- a/php-implementation/run-sqlite-translation.php +++ b/php-implementation/run-sqlite-translation.php @@ -13,7 +13,7 @@ $server = new MySQLSocketServer( new SQLiteTranslationHandler(__DIR__ . '/database/test.db'), - ['port' => 3316] + ['port' => 3306] ); $server->start(); From fcb890906bcdba862fab647b0b915a1c369e3d10 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Mon, 20 Oct 2025 15:11:10 +0200 Subject: [PATCH 5/5] Return rows and columns when column info available --- .../handler-sqlite-translation.php | 20 ++++++++----------- .../sqlite-database-integration | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/php-implementation/handler-sqlite-translation.php b/php-implementation/handler-sqlite-translation.php index 4ca8b90..d0f2576 100644 --- a/php-implementation/handler-sqlite-translation.php +++ b/php-implementation/handler-sqlite-translation.php @@ -36,19 +36,15 @@ public function __construct($sqlite_database_path) { public function handleQuery(string $query): MySQLServerQueryResult { try { - // An extremely naive check. We should be using the MySQL parser to - // determine this: - if(!str_starts_with(strtolower($query), 'select')) { - $this->sqlite_driver->query($query); - return new OkayPacketResult( - $this->sqlite_driver->get_last_return_value() ?? 0, - $this->sqlite_driver->get_insert_id() ?? 0 - ); + $rows = $this->sqlite_driver->query($query); + if ( $this->sqlite_driver->get_last_column_count() > 0 ) { + $columns = $this->computeColumnInfo(); + return new SelectQueryResult($columns, $rows); } - - $rows = $this->sqlite_driver->query($query, PDO::FETCH_ASSOC); - $columns = $this->computeColumnInfo(); - return new SelectQueryResult($columns, $rows); + return new OkayPacketResult( + $this->sqlite_driver->get_last_return_value() ?? 0, + $this->sqlite_driver->get_insert_id() ?? 0 + ); } catch (Throwable $e) { return new ErrorQueryResult($e->getMessage()); } diff --git a/php-implementation/sqlite-database-integration b/php-implementation/sqlite-database-integration index b33f923..e066e8b 160000 --- a/php-implementation/sqlite-database-integration +++ b/php-implementation/sqlite-database-integration @@ -1 +1 @@ -Subproject commit b33f923d0ea80e01507cb509c5af58ede3679637 +Subproject commit e066e8b4d865eee3f9bee5ce2bfac33c6f1e6897