Skip to content

Commit bf6a7ac

Browse files
Address WordPress PHPUnit test fails: charset detection, length validation, capabilities
1 parent 7be6b9c commit bf6a7ac

File tree

1 file changed

+235
-20
lines changed

1 file changed

+235
-20
lines changed

wp-includes/sqlite/class-wp-sqlite-db.php

Lines changed: 235 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,154 @@ public function set_charset( $dbh, $charset = null, $collate = null ) {
7878
}
7979

8080
/**
81-
* Method to get the character set for the database.
82-
* Hardcoded to utf8mb4 for now.
81+
* Retrieves the character set for the given column.
8382
*
84-
* @param string $table The table name.
85-
* @param string $column The column name.
83+
* @since 2.3.0
8684
*
87-
* @return string The character set.
85+
* @param string $table Table name.
86+
* @param string $column Column name.
87+
* @return string|false Column character set as a string. False if the column has
88+
* no character set (e.g., numeric or binary columns).
8889
*/
8990
public function get_col_charset( $table, $column ) {
90-
// Hardcoded for now.
91-
return 'utf8mb4';
91+
$table_key = strtolower( $table );
92+
$column_key = strtolower( $column );
93+
94+
/**
95+
* Filters the column charset value before the DB is checked.
96+
*
97+
* @since 2.3.0
98+
*
99+
* @param string|null|false $charset The character set to use. Default null.
100+
* @param string $table The name of the table being checked.
101+
* @param string $column The name of the column being checked.
102+
*/
103+
$charset = apply_filters( 'pre_get_col_charset', null, $table, $column );
104+
if ( null !== $charset ) {
105+
return $charset;
106+
}
107+
108+
// Use SHOW FULL COLUMNS to get column metadata with collation info.
109+
// This works because the driver translates this to query the information schema.
110+
$results = $this->get_results( "SHOW FULL COLUMNS FROM `{$table_key}`" );
111+
112+
if ( ! $results ) {
113+
return false;
114+
}
115+
116+
// Build column metadata cache.
117+
$columns = array();
118+
foreach ( $results as $col ) {
119+
$columns[ strtolower( $col->Field ) ] = $col;
120+
}
121+
$this->col_meta[ $table_key ] = $columns;
122+
123+
// Check if column exists.
124+
if ( ! isset( $columns[ $column_key ] ) ) {
125+
return false;
126+
}
127+
128+
// Return false for non-string columns (no collation).
129+
if ( empty( $columns[ $column_key ]->Collation ) ) {
130+
return false;
131+
}
132+
133+
// Extract charset from collation (e.g., 'utf8mb4_general_ci' -> 'utf8mb4').
134+
list( $charset ) = explode( '_', $columns[ $column_key ]->Collation );
135+
return $charset;
136+
}
137+
138+
/**
139+
* Retrieves the maximum string length allowed in a given column.
140+
*
141+
* @since 2.3.0
142+
*
143+
* @param string $table Table name.
144+
* @param string $column Column name.
145+
* @return array|false {
146+
* Array of column length information, false if the column has no length
147+
* (for example, numeric column).
148+
*
149+
* @type string $type One of 'byte' or 'char'.
150+
* @type int $length The column length.
151+
* }
152+
*/
153+
public function get_col_length( $table, $column ) {
154+
$table_key = strtolower( $table );
155+
$column_key = strtolower( $column );
156+
157+
// Check cached column metadata first.
158+
if ( isset( $this->col_meta[ $table_key ][ $column_key ] ) ) {
159+
$type = $this->col_meta[ $table_key ][ $column_key ]->Type;
160+
} else {
161+
// Query column info if not cached.
162+
$results = $this->get_results( "SHOW FULL COLUMNS FROM `{$table_key}`" );
163+
if ( ! $results ) {
164+
return false;
165+
}
166+
167+
$columns = array();
168+
foreach ( $results as $col ) {
169+
$columns[ strtolower( $col->Field ) ] = $col;
170+
}
171+
$this->col_meta[ $table_key ] = $columns;
172+
173+
if ( ! isset( $columns[ $column_key ] ) ) {
174+
return false;
175+
}
176+
$type = $columns[ $column_key ]->Type;
177+
}
178+
179+
// Parse the type to get length info.
180+
$typeinfo = explode( '(', $type );
181+
$basetype = strtolower( $typeinfo[0] );
182+
183+
if ( ! empty( $typeinfo[1] ) ) {
184+
$length = (int) trim( $typeinfo[1], ')' );
185+
} else {
186+
$length = false;
187+
}
188+
189+
switch ( $basetype ) {
190+
case 'char':
191+
case 'varchar':
192+
return array(
193+
'type' => 'char',
194+
'length' => $length,
195+
);
196+
case 'binary':
197+
case 'varbinary':
198+
return array(
199+
'type' => 'byte',
200+
'length' => $length,
201+
);
202+
case 'tinyblob':
203+
case 'tinytext':
204+
return array(
205+
'type' => 'byte',
206+
'length' => 255,
207+
);
208+
case 'blob':
209+
case 'text':
210+
return array(
211+
'type' => 'byte',
212+
'length' => 65535,
213+
);
214+
case 'mediumblob':
215+
case 'mediumtext':
216+
return array(
217+
'type' => 'byte',
218+
'length' => 16777215,
219+
);
220+
case 'longblob':
221+
case 'longtext':
222+
return array(
223+
'type' => 'byte',
224+
'length' => 4294967295,
225+
);
226+
default:
227+
return false;
228+
}
92229
}
93230

94231
/**
@@ -145,10 +282,64 @@ public function set_sql_mode( $modes = array() ) {
145282
*
146283
* @return bool True to indicate the connection was successfully closed.
147284
*/
285+
/**
286+
* Close the database connection.
287+
*
288+
* @since 2.3.0
289+
*
290+
* @return bool True if connection was closed successfully.
291+
*/
148292
public function close() {
293+
if ( ! $this->dbh ) {
294+
return false;
295+
}
296+
297+
$this->dbh = null;
298+
$this->ready = false;
299+
$this->has_connected = false;
300+
149301
return true;
150302
}
151303

304+
/**
305+
* Determines the best charset and collation for the database connection.
306+
*
307+
* This overrides wpdb::determine_charset() to handle SQLite's lack of mysqli.
308+
* WordPress expects utf8 to be upgraded to utf8mb4 when supported.
309+
*
310+
* @since 2.3.0
311+
*
312+
* @param string $charset The character set to check.
313+
* @param string $collate The collation to check.
314+
* @return array {
315+
* Array containing the determined charset and collation.
316+
*
317+
* @type string $charset The determined character set.
318+
* @type string $collate The determined collation.
319+
* }
320+
*/
321+
public function determine_charset( $charset, $collate ) {
322+
if ( 'utf8' === $charset ) {
323+
$charset = 'utf8mb4';
324+
}
325+
326+
if ( 'utf8mb4' === $charset ) {
327+
// _general_ is outdated, so we can upgrade it to _unicode_, instead.
328+
if ( ! $collate || 'utf8_general_ci' === $collate ) {
329+
$collate = 'utf8mb4_unicode_ci';
330+
} else {
331+
$collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
332+
}
333+
}
334+
335+
// _unicode_520_ is a better collation, we should use that when it's available.
336+
if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) {
337+
$collate = 'utf8mb4_unicode_520_ci';
338+
}
339+
340+
return compact( 'charset', 'collate' );
341+
}
342+
152343
/**
153344
* Method to select the database connection.
154345
*
@@ -453,16 +644,27 @@ public function query( $query ) {
453644
// Save the query count before running another query.
454645
$last_query_count = count( $this->queries ?? array() );
455646

456-
/*
457-
* @TODO: WPDB uses "$this->check_current_query" to check table/column
458-
* charset and strip all invalid characters from the query.
459-
* This is an involved process that we can bypass for SQLite,
460-
* if we simply strip all invalid UTF-8 characters from the query.
461-
*
462-
* To do so, mb_convert_encoding can be used with an optional
463-
* fallback to a htmlspecialchars method. E.g.:
464-
* https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
465-
*/
647+
// Check for invalid text in the query, similar to parent wpdb behavior.
648+
if ( $this->check_current_query && ! $this->check_ascii( $query ) ) {
649+
$stripped_query = $this->strip_invalid_text_from_query( $query );
650+
/*
651+
* strip_invalid_text_from_query() can perform queries, so we need
652+
* to flush again, just to make sure everything is clear.
653+
*/
654+
$this->flush();
655+
if ( $stripped_query !== $query ) {
656+
$this->insert_id = 0;
657+
$this->last_query = $query;
658+
659+
wp_load_translations_early();
660+
661+
$this->last_error = __( 'WordPress database error: Could not perform query because it contains invalid data.' );
662+
663+
return false;
664+
}
665+
}
666+
$this->check_current_query = true;
667+
466668
$this->_do_query( $query );
467669

468670
if ( $this->last_error ) {
@@ -612,18 +814,31 @@ protected function load_col_info() {
612814
* Method to return what the database can do.
613815
*
614816
* This overrides wpdb::has_cap() to avoid using MySQL functions.
615-
* SQLite supports subqueries, but not support collation, group_concat and set_charset.
817+
* SQLite via this driver supports all common MySQL capabilities.
616818
*
617819
* @see wpdb::has_cap()
618820
*
619821
* @param string $db_cap The feature to check for. Accepts 'collation',
620822
* 'group_concat', 'subqueries', 'set_charset',
621-
* 'utf8mb4', or 'utf8mb4_520'.
823+
* 'utf8mb4', 'utf8mb4_520', or 'identifier_placeholders'.
622824
*
623825
* @return bool Whether the database feature is supported, false otherwise.
624826
*/
625827
public function has_cap( $db_cap ) {
626-
return 'subqueries' === strtolower( $db_cap );
828+
$db_cap = strtolower( $db_cap );
829+
830+
switch ( $db_cap ) {
831+
case 'collation':
832+
case 'group_concat':
833+
case 'subqueries':
834+
case 'set_charset':
835+
case 'utf8mb4':
836+
case 'utf8mb4_520':
837+
case 'identifier_placeholders':
838+
return true;
839+
}
840+
841+
return false;
627842
}
628843

629844
/**

0 commit comments

Comments
 (0)