PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.12.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.12.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
← All changes | inc/database/base.php +12 -362 trunk1.12.0 View file →
@@ -228,194 +228,8 @@
228 228 return $this->table_name;
229 229 }
230 230
231 231 /**
232 - * Whether this table currently exists in the database.
233 - *
234 - * Deliberately `SHOW TABLES LIKE` rather than the existing get_columns():
235 - * `SHOW COLUMNS FROM <missing table>` is a MySQL error, so it pollutes
236 - * $wpdb->last_error, prints under WP_DEBUG_DISPLAY, and cannot tell "the table
237 - * is gone" apart from "SHOW is denied". This returns a clean empty set instead.
238 - *
239 - * esc_like() matters because $wpdb->prefix contains `_`, which is a LIKE
240 - * wildcard — without it `wp_srfm_entries` would also match `wpXsrfm_entries`.
241 - * The comparison is against the real, unescaped name so the match stays exact.
242 - *
243 - * Fails safe: any DB-level error reports the table as present. A false "your
244 - * database needs updating" on a transient connection blip is worse than a
245 - * missed one, because the notice it drives asks the user to alter their schema.
246 - *
247 - * @since 2.12.6
248 - * @return bool True when the table exists, or when existence cannot be determined.
249 - */
250 - public function table_exists() {
251 - $wpdb = $this->wpdb;
252 - $table = $this->get_tablename();
253 -
254 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema lookup; the caller owns caching, and a cached answer here would defeat the check.
255 - $found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
256 -
257 - if ( ! empty( $wpdb->last_error ) ) {
258 - return true;
259 - }
260 -
261 - return $found === $table;
262 - }
263 -
264 - /**
265 - * A table holding this table's data under a different prefix, if there is one.
266 - *
267 - * Changing `$table_prefix` — a manual edit, a restored dump from a site with a
268 - * different prefix, or a security plugin that renames tables and misses the ones
269 - * it does not know about — leaves our data behind under the old name while the
270 - * plugin looks for the new one. Creating a fresh empty table there would strand
271 - * every stored entry, so look for the old one first and adopt it instead.
272 - *
273 - * Refuses to guess. Returns '' unless exactly one credible candidate exists, and
274 - * only when that candidate carries every column this table's schema declares —
275 - * an unrelated table that merely ends in the same words is never touched.
276 - *
277 - * On multisite, other blogs' tables are legitimate and belong to those blogs.
278 - * Anything matching the `{base_prefix}{digits}_` pattern, or the base prefix
279 - * itself, is excluded so a subsite can never adopt another subsite's data.
280 - *
281 - * @since 2.12.6
282 - * @return string Full table name to adopt, or '' when there is nothing safe to adopt.
283 - */
284 - public function find_adoptable_table() {
285 - $wpdb = $this->wpdb;
286 - $correct = $this->get_tablename();
287 - $needle = 'srfm_' . $this->table_suffix;
288 -
289 - // Wildcard on the left only: the name must *end* at the suffix, so a
290 - // deliberate copy such as `wp_srfm_entries_backup` is never a candidate.
291 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema lookup; a cached answer would defeat the check.
292 - $found = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', '%' . $wpdb->esc_like( $needle ) ) );
293 -
294 - if ( ! empty( $wpdb->last_error ) || ! is_array( $found ) ) {
295 - return '';
296 - }
297 -
298 - $base = $wpdb->base_prefix;
299 - $blog_table = '/^' . preg_quote( $base, '/' ) . '\d+_' . preg_quote( $needle, '/' ) . '$/';
300 - $candidates = [];
301 -
302 - foreach ( $found as $table ) {
303 - $table = (string) $table;
304 -
305 - // The table we are looking for, another blog's table, or the network's
306 - // main-site table — none of these are ours to rename.
307 - if ( $table === $correct || $base . $needle === $table || preg_match( $blog_table, $table ) ) {
308 - continue;
309 - }
310 -
311 - $candidates[] = $table;
312 - }
313 -
314 - // More than one and we cannot tell which holds the real data. Refuse rather
315 - // than pick, and let the caller fall back to creating an empty table.
316 - if ( 1 !== count( $candidates ) ) {
317 - return '';
318 - }
319 -
320 - return $this->has_expected_columns( $candidates[0] ) ? $candidates[0] : '';
321 - }
322 -
323 - /**
324 - * Rename a differently-prefixed table into this table's expected name.
325 - *
326 - * RENAME rather than create-and-copy: it is atomic, needs no second copy of the
327 - * data, and cannot half-succeed and leave rows in two places.
328 - *
329 - * @param string $from Full name of the table to adopt.
330 - * @since 2.12.6
331 - * @return bool True when the table is in place afterwards.
332 - */
333 - public function adopt_table( $from ) {
334 - $wpdb = $this->wpdb;
335 - $to = $this->get_tablename();
336 -
337 - if ( empty( $from ) || $from === $to ) {
338 - return false;
339 - }
340 -
341 - // Never rename over an existing table; the one already in place wins.
342 - if ( $this->table_exists() ) {
343 - return true;
344 - }
345 -
346 - $query = $wpdb->prepare( 'RENAME TABLE %1s TO %2s', str_replace( '`', '', $from ), str_replace( '`', '', $to ) ); // phpcs:ignore -- Same complex-placeholder pattern as create(): identifiers must not be quoted, and both names come from SHOW TABLES / $wpdb->prefix.
347 -
348 - if ( ! $query ) {
349 - // prepare() returned nothing usable; do not fall through to a raw query.
350 - return false;
351 - }
352 -
353 - $wpdb->query( $query ); // phpcs:ignore -- We are already using prepare above, and one-off DDL has nothing to cache.
354 -
355 - if ( ! empty( $wpdb->last_error ) ) {
356 - /** This action is documented in inc/database/base.php */
357 - do_action( 'srfm_db_upgrade_query_failed', $wpdb->last_error, 'RENAME TABLE', $to );
358 - }
359 -
360 - return $this->table_exists();
361 - }
362 -
363 - /**
364 - * Stamp this site's owner signature onto a table's MySQL comment.
365 - *
366 - * Best-effort: a host that refuses ALTER simply leaves the table unstamped,
367 - * which later reads as "ownership unproven" — the safe direction.
368 - *
369 - * @param string $table Full table name; defaults to this table's own name.
370 - * @since 2.12.6
371 - * @return void
372 - */
373 - public function stamp_owner_signature( $table = '' ) {
374 - $wpdb = $this->wpdb;
375 - $table = '' === $table ? $this->get_tablename() : $table;
376 -
377 - $query = $wpdb->prepare( 'ALTER TABLE %1s COMMENT = %s', str_replace( '`', '', $table ), $this->get_owner_signature() ); // phpcs:ignore -- Identifier must not be quoted; the comment value is a bound, quoted string.
378 -
379 - if ( ! $query ) {
380 - return;
381 - }
382 -
383 - $wpdb->query( $query ); // phpcs:ignore -- Prepared above; one-off DDL with nothing to cache.
384 - }
385 -
386 - /**
387 - * Whether a table carries this site's owner signature.
388 - *
389 - * Gates adoption: on shared hosting a different install's identically-named,
390 - * same-schema table can be the only candidate, and renaming it in would destroy
391 - * that site's data. Deny by default — anything but an exact signature match
392 - * (including a read error, an empty comment, or a legacy table stamped before
393 - * this plugin wrote signatures) returns false.
394 - *
395 - * @param string $table Full table name to inspect.
396 - * @since 2.12.6
397 - * @return bool
398 - */
399 - public function table_belongs_to_site( $table ) {
400 - $wpdb = $this->wpdb;
401 - $bare = str_replace( '`', '', (string) $table );
402 -
403 - if ( '' === $bare ) {
404 - return false;
405 - }
406 -
407 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema lookup; a cached answer would defeat the check.
408 - $comment = $wpdb->get_var( $wpdb->prepare( 'SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $bare ) );
409 -
410 - if ( ! empty( $wpdb->last_error ) || ! is_string( $comment ) || '' === $comment ) {
411 - return false;
412 - }
413 -
414 - return hash_equals( $this->get_owner_signature(), $comment );
415 - }
416 -
417 - /**
418 232 * Conditionally returns current database charset or collate.
419 233 *
420 234 * @since 0.0.10
421 235 * @return string
@@ -472,31 +286,10 @@
472 286
473 287 if ( false === $result ) {
474 288 // Stop DB alteration if we have any error.
475 289 $this->db_upgradable = false;
476 -
477 - /**
478 - * Fires when a table could not be created.
479 - *
480 - * Column changes have announced their failures since 2.11.0 but table
481 - * creation never did — so the one failure that leaves a site with no
482 - * table at all, a host denying CREATE TABLE, was the only silent one.
483 - * Same signature as the ALTER case so one listener can handle both.
484 - *
485 - * @param string $last_error The database error.
486 - * @param string $query The query that failed.
487 - * @param string $table_name The table it was for.
488 - * @since 2.12.6
489 - */
490 - do_action( 'srfm_db_upgrade_query_failed', $wpdb->last_error, $query, $this->get_tablename() );
491 290 }
492 291
493 - if ( false !== $result ) {
494 - // Stamp our own table so a future adoption can prove it belongs to this
495 - // site before renaming it in. See stamp_owner_signature().
496 - $this->stamp_owner_signature();
497 - }
498 -
499 292 return $result;
500 293 }
501 294
502 295 /**
@@ -627,22 +420,10 @@
627 420 // Execute the query.
628 421 $result = $wpdb->query( $query ); // phpcs:ignore -- It is okay. We are already using prepare above and we need to do DB query directly here.
629 422
630 423 if ( false === $result ) {
631 - // Stop DB alteration if we have any error. A failed ALTER leaves the table
632 - // version un-bumped, so it retries on every request — expose the underlying
633 - // error so persistent failures are diagnosable (hook for logging/monitoring).
424 + // Stop DB alteration if we have any error.
634 425 $this->db_upgradable = false;
635 -
636 - /**
637 - * Fires when a SureForms DB schema-upgrade query fails.
638 - *
639 - * @since 2.11.0
640 - * @param string $last_error The DB error message ( $wpdb->last_error ).
641 - * @param string $query The ALTER query that failed.
642 - * @param string $table The table being altered.
643 - */
644 - do_action( 'srfm_db_upgrade_query_failed', $this->wpdb->last_error, $query, $this->get_tablename() );
645 426 }
646 427
647 428 return $result;
648 429 }
@@ -729,12 +510,8 @@
729 510 $format = $prepared_data['format'];
730 511 }
731 512
732 513 $result = $this->wpdb->insert( $this->get_tablename(), $prepared_data['data'], $format );
733 -
734 - // Reset cache so subsequent queries in the same request include the new row.
735 - $this->cache_reset();
736 -
737 514 return $result ? $this->wpdb->insert_id : false;
738 515 }
739 516
740 517 /**
@@ -847,57 +624,8 @@
847 624 return Helper::get_array_value( $this->cache_set( $query, $results ) );
848 625 }
849 626
850 627 /**
851 - * Retrieves a list of records based on the provided arguments.
852 - *
853 - * This method fetches results from the database, allowing for various
854 - * customization options such as filtering, pagination, and sorting.
855 - *
856 - * @param array<string,mixed> $args {
857 - * Optional. An array of arguments to customize the query.
858 - *
859 - * @type array $where An associative array of conditions to filter the results.
860 - * @type int $limit The maximum number of results to return. Default is 10.
861 - * @type int $offset The number of records to skip before starting to collect results. Default is 0.
862 - * @type string $orderby The column by which to order the results. Default is 'created_at'.
863 - * @type string $order The direction of the order (ASC or DESC). Default is 'DESC'.
864 - * }
865 - * @param bool $set_limit Whether to set the limit on the query. Default is true.
866 - *
867 - * @since 1.13.0
868 - * @return array<mixed> The results of the query, typically an array of objects or associative arrays.
869 - */
870 - public function get_records_by_args( $args = [], $set_limit = true ) {
871 - $_args = wp_parse_args(
872 - $args,
873 - [
874 - 'where' => [],
875 - 'columns' => '*',
876 - 'limit' => 10,
877 - 'offset' => 0,
878 - 'orderby' => 'created_at',
879 - 'order' => 'DESC',
880 - ]
881 - );
882 - $allowed_orderby = $this->get_allowed_orderby_columns();
883 - $orderby = in_array( $_args['orderby'], $allowed_orderby, true ) ? $_args['orderby'] : 'created_at';
884 - $order = 'ASC' === strtoupper( Helper::get_string_value( $_args['order'] ) ) ? 'ASC' : 'DESC';
885 - $extra_queries = [
886 - sprintf( 'ORDER BY `%1$s` %2$s', $orderby, $order ),
887 - ];
888 -
889 - if ( $set_limit ) {
890 - $extra_queries[] = sprintf( 'LIMIT %1$d, %2$d', absint( $_args['offset'] ), absint( $_args['limit'] ) );
891 - }
892 - return $this->get_results(
893 - $_args['where'],
894 - $_args['columns'],
895 - $extra_queries
896 - );
897 - }
898 -
899 - /**
900 628 * Get the total number of rows in the table.
901 629 *
902 630 * @param array<mixed> $where_clauses Optional. An associative array of WHERE clauses for the SQL query.
903 631 * @since 0.0.13
@@ -930,70 +658,8 @@
930 658 return Helper::get_integer_value( $this->cache_set( $query, $results ) );
931 659 }
932 660
933 661 /**
934 - * The signature this plugin stamps on tables it owns on this site.
935 - *
936 - * A random per-site token, generated once and stored in options. Embedded in
937 - * the table's MySQL comment at creation time; the comment survives RENAME, so a
938 - * table that moved under a different prefix still carries it, while an unrelated
939 - * install sharing the same database carries a different one.
940 - *
941 - * @since 2.12.6
942 - * @return string
943 - */
944 - protected function get_owner_signature() {
945 - $token = get_option( 'srfm_db_owner_token' );
946 -
947 - if ( ! is_string( $token ) || '' === $token ) {
948 - $token = wp_generate_password( 20, false );
949 - update_option( 'srfm_db_owner_token', $token, false );
950 - }
951 -
952 - return 'srfm-owner:' . $token;
953 - }
954 -
955 - /**
956 - * Whether a table carries every column this table's schema declares.
957 - *
958 - * Guards adoption: a same-named table from an unrelated source should never be
959 - * renamed into place just because its name matches.
960 - *
961 - * @param string $table Full table name to inspect.
962 - * @since 2.12.6
963 - * @return bool
964 - */
965 - protected function has_expected_columns( $table ) {
966 - $wpdb = $this->wpdb;
967 -
968 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema lookup; a cached answer would defeat the check.
969 - $columns = $wpdb->get_col( $wpdb->prepare( 'SHOW COLUMNS FROM %1s', str_replace( '`', '', $table ) ) ); // phpcs:ignore -- Same complex-placeholder pattern as create(): an identifier must not be quoted, and the name comes from SHOW TABLES on this connection.
970 -
971 - if ( ! empty( $wpdb->last_error ) || ! is_array( $columns ) ) {
972 - return false;
973 - }
974 -
975 - foreach ( array_keys( $this->get_schema() ) as $column ) {
976 - if ( ! in_array( $column, $columns, true ) ) {
977 - return false;
978 - }
979 - }
980 -
981 - return true;
982 - }
983 -
984 - /**
985 - * Get the allowed column names for ORDER BY clauses.
986 - * Child classes may override this method to restrict orderable columns further.
987 - *
988 - * @since 2.6.0
989 - * @return array<string>
990 - */
991 - protected function get_allowed_orderby_columns() {
992 - return array_merge( array_keys( $this->get_schema() ), [ 'updated_at' ] );
993 - }
994 -
995 - /**
996 662 * Retrieve a cached value by its key.
997 663 *
998 664 * @param string $key The cache key.
999 665 * @since 0.0.10
@@ -1064,9 +730,9 @@
1064 730 $wpdb = $this->wpdb;
1065 731
1066 732 // If there are WHERE clauses, prepare and append them to the query.
1067 733 if ( is_array( $where_clauses ) ) {
1068 - $groups = [];
734 + $where = '';
1069 735 $values = [];
1070 736 $schema = $this->get_schema();
1071 737
1072 738 foreach ( $where_clauses as $key => $value ) {
@@ -1071,12 +737,10 @@
1071 737
1072 738 foreach ( $where_clauses as $key => $value ) {
1073 739
1074 740 $relation = ! empty( $value['RELATION'] ) ? trim( $value['RELATION'] ) : 'AND';
1075 - $relation = in_array( strtoupper( $relation ), [ 'AND', 'OR' ], true ) ? strtoupper( $relation ) : 'AND';
1076 741
1077 742 if ( is_int( $key ) ) {
1078 - $clause_parts = [];
1079 743 foreach ( $value as $_key => $_value ) {
1080 744 if ( is_int( $_key ) ) {
1081 745 // Check if the operator is allowed.
1082 746 if ( ! in_array( $_value['compare'], $this->allowed_where_operators, true ) ) {
@@ -1082,42 +746,28 @@
1082 746 if ( ! in_array( $_value['compare'], $this->allowed_where_operators, true ) ) {
1083 747 continue;
1084 748 }
1085 749
1086 - // Skip if key is not in schema.
1087 - if ( ! isset( $schema[ $_value['key'] ] ) ) {
1088 - continue;
1089 - }
1090 -
1091 750 switch ( $_value['compare'] ) {
1092 751 case 'LIKE':
1093 - // Single quotes to match WP core. Under a MySQL session with
1094 - // ANSI_QUOTES set (not in WP's incompatible_modes list, which
1095 - // only names the compound ANSI mode) a double-quoted pattern
1096 - // parses as an identifier and the query hard-fails, taking out
1097 - // both the listing and its COUNT(*).
1098 - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . " '%%" . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . "%%'";
1099 - $values[] = $_value['value'];
752 + $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' "%%' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . '%%" ' . $relation;
753 + $values[] = $_value['value'];
1100 754 break;
1101 755
1102 756 case 'IN':
1103 757 // Based on the number of values and datatype, it will create WHERE clause for $wpdb::prepare method. Eg: for ID with three values column: ID IN (%d, %d, %d).
1104 - $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) );
1105 - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ')';
1106 - $values = array_merge( $values, $_value['value'] );
758 + $datatype = $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) );
759 + $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ') ' . $relation;
760 + $values = array_merge( $values, $_value['value'] );
1107 761 break;
1108 762
1109 763 default:
1110 - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) );
1111 - $values[] = $_value['value'];
764 + $where .= ' ' . $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . ' ' . $relation;
765 + $values[] = $_value['value'];
1112 766 break;
1113 767 }
1114 768 }
1115 769 }
1116 -
1117 - if ( ! empty( $clause_parts ) ) {
1118 - $groups[] = '(' . implode( ' ' . $relation . ' ', $clause_parts ) . ')';
1119 - }
1120 770 continue;
1121 771 }
1122 772
1123 773 if ( ! isset( $schema[ $key ] ) ) {
@@ -1124,17 +774,17 @@
1124 774 // Skip strictly if current key is not in our schema.
1125 775 continue;
1126 776 }
1127 777
1128 - $groups[] = '(' . $key . ' = ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $key ]['type'] ) ) . ')';
778 + $where .= ' ' . $key . ' = ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $key ]['type'] ) ) . ' ' . $relation;
1129 779 $values[] = $value;
1130 780 }
1131 781
1132 - if ( empty( $groups ) ) {
782 + if ( ! $where ) {
1133 783 return '';
1134 784 }
1135 785
1136 - $where = ' WHERE ' . implode( ' AND ', $groups );
786 + $where = ' WHERE ' . trim( trim( $where, $relation ) );
1137 787
1138 788 // Prepare the query with placeholders.
1139 789 // @phpstan-ignore-next-line -- We are already assigning non-literal string above using "get_format_by_datatype" methods.
1140 790 return $wpdb->prepare( $where, ...$values ); // phpcs:ignore -- We are returning prepared sql query here. We are already using necessary placeholders in $where variable.