| @@ -88,9 +88,9 @@ | ||
| 88 | 88 | * |
| 89 | 89 | * @var array<string> |
| 90 | 90 | * @since 1.8.0 |
| 91 | 91 | */ |
| 92 | - private $allowed_where_operators = [ 'LIKE', 'IN', '=', '!=', '>', '<', '>=', '<=' ]; | |
| 92 | + private $allowed_where_operators = [ 'LIKE', 'IN', 'NOT IN', '=', '!=', '>', '<', '>=', '<=' ]; | |
| 93 | 93 | |
| 94 | 94 | /** |
| 95 | 95 | * Init class. |
| 96 | 96 | * |
| @@ -789,9 +789,14 @@ | ||
| 789 | 789 | * @since 0.0.13 |
| 790 | 790 | * @return int|false The number of rows deleted, or false on error. |
| 791 | 791 | */ |
| 792 | 792 | public function use_delete( $where, $where_format = null ) { |
| 793 | - return $this->wpdb->delete( $this->get_tablename(), $where, $where_format ); | |
| 793 | + $result = $this->wpdb->delete( $this->get_tablename(), $where, $where_format ); | |
| 794 | + | |
| 795 | + // Reset cache so subsequent queries in the same request exclude the deleted row. | |
| 796 | + $this->cache_reset(); | |
| 797 | + | |
| 798 | + return $result; | |
| 794 | 799 | } |
| 795 | 800 | |
| 796 | 801 | /** |
| 797 | 802 | * Retrieve results from the database based on the given WHERE clauses and selected columns. |
| @@ -828,10 +833,12 @@ | ||
| 828 | 833 | // Add a semicolon at the end of the query. |
| 829 | 834 | $query = rtrim( trim( $query ), ';' ) . ';'; |
| 830 | 835 | |
| 831 | 836 | $cached_results = $this->cache_get( $query ); |
| 832 | - if ( $cached_results ) { | |
| 833 | - // Return the cached data if exists. | |
| 837 | + if ( null !== $cached_results ) { | |
| 838 | + // Return the cached data if exists. Tested against null rather than | |
| 839 | + // truthiness: an empty result set is a real answer, and re-running the | |
| 840 | + // query for it means every no-match lookup runs once per caller. | |
| 834 | 841 | return Helper::get_array_value( $cached_results ); |
| 835 | 842 | } |
| 836 | 843 | |
| 837 | 844 | // phpcs:ignore |
| @@ -917,10 +924,12 @@ | ||
| 917 | 924 | // Add a semicolon at the end of the query. |
| 918 | 925 | $query = rtrim( trim( $query ), ';' ) . ';'; |
| 919 | 926 | |
| 920 | 927 | $cached_results = $this->cache_get( $query ); |
| 921 | - if ( $cached_results ) { | |
| 922 | - // Return the cached data if exists. | |
| 928 | + if ( null !== $cached_results ) { | |
| 929 | + // Return the cached data if exists. Tested against null rather than | |
| 930 | + // truthiness: a count of zero is a real answer, and the editor exclusion | |
| 931 | + // makes zero the common case rather than the exception. | |
| 923 | 932 | return Helper::get_integer_value( $cached_results ); |
| 924 | 933 | } |
| 925 | 934 | |
| 926 | 935 | // phpcs:ignore |
| @@ -1051,8 +1060,9 @@ | ||
| 1051 | 1060 | * @type string $RELATION Optional. The logical relation ('AND' or 'OR'). |
| 1052 | 1061 | * } |
| 1053 | 1062 | * } |
| 1054 | 1063 | * |
| 1064 | + * @since 2.12.7 -- Added support for "NOT IN" compare. | |
| 1055 | 1065 | * @since 1.1.1 -- Added support for "IN" compare. |
| 1056 | 1066 | * @since 0.0.13 |
| 1057 | 1067 | * @return string The prepared SQL WHERE clause with placeholders, or an empty string if no clauses were provided. |
| 1058 | 1068 | */ |
| @@ -1077,10 +1087,19 @@ | ||
| 1077 | 1087 | if ( is_int( $key ) ) { |
| 1078 | 1088 | $clause_parts = []; |
| 1079 | 1089 | foreach ( $value as $_key => $_value ) { |
| 1080 | 1090 | if ( is_int( $_key ) ) { |
| 1091 | + // Normalised before the allowlist test. Payments' | |
| 1092 | + // builder upper-cases and trims, this one compared | |
| 1093 | + // strictly -- so a caller writing 'not in' was honoured | |
| 1094 | + // by one and silently dropped by the other. A dropped | |
| 1095 | + // condition used to be harmless; now that NOT IN is the | |
| 1096 | + // exclusion primitive, dropping it disables the | |
| 1097 | + // exclusion without a word. | |
| 1098 | + $compare = strtoupper( trim( Helper::get_string_value( $_value['compare'] ) ) ); | |
| 1099 | + | |
| 1081 | 1100 | // Check if the operator is allowed. |
| 1082 | - if ( ! in_array( $_value['compare'], $this->allowed_where_operators, true ) ) { | |
| 1101 | + if ( ! in_array( $compare, $this->allowed_where_operators, true ) ) { | |
| 1083 | 1102 | continue; |
| 1084 | 1103 | } |
| 1085 | 1104 | |
| 1086 | 1105 | // Skip if key is not in schema. |
| @@ -1087,9 +1106,9 @@ | ||
| 1087 | 1106 | if ( ! isset( $schema[ $_value['key'] ] ) ) { |
| 1088 | 1107 | continue; |
| 1089 | 1108 | } |
| 1090 | 1109 | |
| 1091 | - switch ( $_value['compare'] ) { | |
| 1110 | + switch ( $compare ) { | |
| 1092 | 1111 | case 'LIKE': |
| 1093 | 1112 | // Single quotes to match WP core. Under a MySQL session with |
| 1094 | 1113 | // ANSI_QUOTES set (not in WP's incompatible_modes list, which |
| 1095 | 1114 | // only names the compound ANSI mode) a double-quoted pattern |
| @@ -1094,21 +1113,51 @@ | ||
| 1094 | 1113 | // ANSI_QUOTES set (not in WP's incompatible_modes list, which |
| 1095 | 1114 | // only names the compound ANSI mode) a double-quoted pattern |
| 1096 | 1115 | // parses as an identifier and the query hard-fails, taking out |
| 1097 | 1116 | // 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'] ) ) . "%%'"; | |
| 1117 | + $clause_parts[] = $_value['key'] . ' ' . $compare . " '%%" . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ) . "%%'"; | |
| 1099 | 1118 | $values[] = $_value['value']; |
| 1100 | 1119 | break; |
| 1101 | 1120 | |
| 1102 | 1121 | case 'IN': |
| 1122 | + case 'NOT IN': | |
| 1123 | + // A scalar is a caller bug, not an empty set, and it must | |
| 1124 | + // surface. 'NOT IN' with value 5 -- a plausible typo for | |
| 1125 | + // [ 5 ] -- would otherwise drop the condition and exclude | |
| 1126 | + // nobody, with no error and a green test suite, while the | |
| 1127 | + // same typo on 'IN' fails closed. On a primitive whose only | |
| 1128 | + // job is scoping data, that asymmetry is a hazard. | |
| 1129 | + if ( ! is_array( $_value['value'] ) ) { | |
| 1130 | + _doing_it_wrong( | |
| 1131 | + __METHOD__, | |
| 1132 | + esc_html( "{$compare} requires an array value, received " . gettype( $_value['value'] ) . '.' ), | |
| 1133 | + '2.12.7' | |
| 1134 | + ); | |
| 1135 | + break; | |
| 1136 | + } | |
| 1137 | + | |
| 1138 | + // An empty list cannot be interpolated: "col IN ()" is a syntax | |
| 1139 | + // error that fails the whole query, listing and COUNT alike. | |
| 1140 | + // An empty IN matches nothing, so '1 = 0' says that in any | |
| 1141 | + // relation. An empty NOT IN excludes nothing, but a literal | |
| 1142 | + // would be '1 = 1', and that makes an enclosing OR group | |
| 1143 | + // unconditionally true. Dropping the condition means the same | |
| 1144 | + // thing under AND and stays fail-closed under OR. | |
| 1145 | + if ( [] === $_value['value'] ) { | |
| 1146 | + if ( 'IN' === $compare ) { | |
| 1147 | + $clause_parts[] = '1 = 0'; | |
| 1148 | + } | |
| 1149 | + break; | |
| 1150 | + } | |
| 1151 | + | |
| 1103 | 1152 | // 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 | 1153 | $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 ) ) . ')'; | |
| 1154 | + $clause_parts[] = $_value['key'] . ' ' . $compare . ' (' . implode( ', ', array_fill( 0, count( $_value['value'] ), $datatype ) ) . ')'; | |
| 1106 | 1155 | $values = array_merge( $values, $_value['value'] ); |
| 1107 | 1156 | break; |
| 1108 | 1157 | |
| 1109 | 1158 | default: |
| 1110 | - $clause_parts[] = $_value['key'] . ' ' . $_value['compare'] . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); | |
| 1159 | + $clause_parts[] = $_value['key'] . ' ' . $compare . ' ' . $this->get_format_by_datatype( Helper::get_string_value( $schema[ $_value['key'] ]['type'] ) ); | |
| 1111 | 1160 | $values[] = $_value['value']; |
| 1112 | 1161 | break; |
| 1113 | 1162 | } |
| 1114 | 1163 | } |
| @@ -1133,8 +1182,16 @@ | ||
| 1133 | 1182 | return ''; |
| 1134 | 1183 | } |
| 1135 | 1184 | |
| 1136 | 1185 | $where = ' WHERE ' . implode( ' AND ', $groups ); |
| 1186 | + | |
| 1187 | + if ( [] === $values ) { | |
| 1188 | + // Every branch that builds a placeholder also pushes a value, so an | |
| 1189 | + // empty list here means the only conditions were constant ones. There | |
| 1190 | + // is nothing for prepare() to fill, and calling it with no placeholder | |
| 1191 | + // trips _doing_it_wrong. | |
| 1192 | + return $where; | |
| 1193 | + } | |
| 1137 | 1194 | |
| 1138 | 1195 | // Prepare the query with placeholders. |
| 1139 | 1196 | // @phpstan-ignore-next-line -- We are already assigning non-literal string above using "get_format_by_datatype" methods. |
| 1140 | 1197 | return $wpdb->prepare( $where, ...$values ); // phpcs:ignore -- We are returning prepared sql query here. We are already using necessary placeholders in $where variable. |