| @@ -116,8 +116,9 @@ | ||
| 116 | 116 | 'MXN', |
| 117 | 117 | 'SGD', |
| 118 | 118 | 'HKD', |
| 119 | 119 | 'NOK', |
| 120 | + 'PLN', | |
| 120 | 121 | 'TRY', |
| 121 | 122 | 'RUB', |
| 122 | 123 | 'INR', |
| 123 | 124 | 'BRL', |
| @@ -1093,43 +1094,12 @@ | ||
| 1093 | 1094 | |
| 1094 | 1095 | // Handle additional where conditions if provided. |
| 1095 | 1096 | if ( ! empty( $_args['where'] ) ) { |
| 1096 | 1097 | foreach ( $_args['where'] as $where_group ) { |
| 1097 | - if ( is_array( $where_group ) ) { | |
| 1098 | - foreach ( $where_group as $condition ) { | |
| 1099 | - if ( isset( $condition['key'], $condition['compare'], $condition['value'] ) ) { | |
| 1100 | - // Validate column name against whitelist. | |
| 1101 | - if ( ! in_array( $condition['key'], self::ALLOWED_COLUMNS, true ) ) { | |
| 1102 | - continue; | |
| 1103 | - } | |
| 1104 | - | |
| 1105 | - // Validate and normalize the comparison operator. | |
| 1106 | - $operator = strtoupper( trim( $condition['compare'] ) ); | |
| 1107 | - | |
| 1108 | - // Skip this condition if operator is not in whitelist. | |
| 1109 | - if ( ! in_array( $operator, self::ALLOWED_OPERATORS, true ) ) { | |
| 1110 | - continue; | |
| 1111 | - } | |
| 1112 | - | |
| 1113 | - $column = $condition['key']; | |
| 1114 | - | |
| 1115 | - if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) && is_array( $condition['value'] ) ) { | |
| 1116 | - $ids = array_map( 'absint', $condition['value'] ); | |
| 1117 | - if ( empty( $ids ) ) { | |
| 1118 | - $ids = [ 0 ]; | |
| 1119 | - } | |
| 1120 | - $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); | |
| 1121 | - $where_clause .= " AND {$column} {$operator} ({$placeholders})"; | |
| 1122 | - foreach ( $ids as $id ) { | |
| 1123 | - $params[] = $id; | |
| 1124 | - } | |
| 1125 | - } else { | |
| 1126 | - $where_clause .= " AND {$column} {$operator} %s"; | |
| 1127 | - $params[] = $condition['value']; | |
| 1128 | - } | |
| 1129 | - } | |
| 1130 | - } | |
| 1098 | + if ( ! is_array( $where_group ) ) { | |
| 1099 | + continue; | |
| 1131 | 1100 | } |
| 1101 | + $where_clause .= self::build_clause_for_group( $where_group, $params ); | |
| 1132 | 1102 | } |
| 1133 | 1103 | } |
| 1134 | 1104 | |
| 1135 | 1105 | // Order by. |
| @@ -1155,9 +1125,9 @@ | ||
| 1155 | 1125 | // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string is built dynamically above based on conditions. |
| 1156 | 1126 | $query = $wpdb->prepare( $query, $params ); |
| 1157 | 1127 | } |
| 1158 | 1128 | |
| 1159 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Custom table query with dynamic preparation, caching not applicable for dynamic queries. | |
| 1129 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table query with dynamic preparation; table name internal, not user input; caching not applicable for dynamic queries. | |
| 1160 | 1130 | $results = $wpdb->get_results( $query, ARRAY_A ); |
| 1161 | 1131 | |
| 1162 | 1132 | return is_array( $results ) ? $results : []; |
| 1163 | 1133 | } |
| @@ -1197,45 +1167,12 @@ | ||
| 1197 | 1167 | |
| 1198 | 1168 | // Handle additional where conditions if provided. |
| 1199 | 1169 | if ( ! empty( $where_conditions ) ) { |
| 1200 | 1170 | foreach ( $where_conditions as $where_group ) { |
| 1201 | - if ( is_array( $where_group ) ) { | |
| 1202 | - foreach ( $where_group as $condition ) { | |
| 1203 | - if ( isset( $condition['key'], $condition['compare'], $condition['value'] ) ) { | |
| 1204 | - // Validate column name against whitelist. | |
| 1205 | - if ( ! in_array( $condition['key'], self::ALLOWED_COLUMNS, true ) ) { | |
| 1206 | - continue; | |
| 1207 | - } | |
| 1208 | - | |
| 1209 | - // Validate and normalize the comparison operator. | |
| 1210 | - $operator = strtoupper( trim( $condition['compare'] ) ); | |
| 1211 | - | |
| 1212 | - // Skip this condition if operator is not in whitelist. | |
| 1213 | - if ( ! in_array( $operator, self::ALLOWED_OPERATORS, true ) ) { | |
| 1214 | - continue; | |
| 1215 | - } | |
| 1216 | - | |
| 1217 | - $column = $condition['key']; | |
| 1218 | - | |
| 1219 | - // Special handling for IN/NOT IN with arrays. | |
| 1220 | - if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) && is_array( $condition['value'] ) ) { | |
| 1221 | - $ids = array_map( 'absint', $condition['value'] ); | |
| 1222 | - // Prevent empty IN (). | |
| 1223 | - if ( empty( $ids ) ) { | |
| 1224 | - $ids = [ 0 ]; | |
| 1225 | - } | |
| 1226 | - $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); | |
| 1227 | - $where_clause .= " AND {$column} {$operator} ({$placeholders})"; | |
| 1228 | - foreach ( $ids as $id ) { | |
| 1229 | - $params[] = $id; | |
| 1230 | - } | |
| 1231 | - } else { | |
| 1232 | - $where_clause .= " AND {$column} {$operator} %s"; | |
| 1233 | - $params[] = $condition['value']; | |
| 1234 | - } | |
| 1235 | - } | |
| 1236 | - } | |
| 1171 | + if ( ! is_array( $where_group ) ) { | |
| 1172 | + continue; | |
| 1237 | 1173 | } |
| 1174 | + $where_clause .= self::build_clause_for_group( $where_group, $params ); | |
| 1238 | 1175 | } |
| 1239 | 1176 | } |
| 1240 | 1177 | |
| 1241 | 1178 | // Build and execute query. |
| @@ -1245,9 +1182,9 @@ | ||
| 1245 | 1182 | // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string is built dynamically above based on conditions. |
| 1246 | 1183 | $query = $wpdb->prepare( $query, $params ); |
| 1247 | 1184 | } |
| 1248 | 1185 | |
| 1249 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Custom table query with dynamic preparation, caching not applicable for count operations. | |
| 1186 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table query with dynamic preparation; table name internal, not user input; caching not applicable for count operations. | |
| 1250 | 1187 | $result = $wpdb->get_var( $query ); |
| 1251 | 1188 | |
| 1252 | 1189 | return absint( $result ); |
| 1253 | 1190 | } |
| @@ -1290,6 +1227,118 @@ | ||
| 1290 | 1227 | return false; |
| 1291 | 1228 | } |
| 1292 | 1229 | |
| 1293 | 1230 | return 'payment' === ( $payment['type'] ?? '' ) && ! empty( $payment['subscription_id'] ); |
| 1231 | + } | |
| 1232 | + | |
| 1233 | + /** | |
| 1234 | + * Build the SQL fragment for a single where group, handling both flat | |
| 1235 | + * AND-only groups and explicit RELATION (OR/AND) groups. | |
| 1236 | + * | |
| 1237 | + * Flat group example: | |
| 1238 | + * [ [ 'key' => 'status', 'compare' => '=', 'value' => 'paid' ] ] | |
| 1239 | + * → " AND status = %s" | |
| 1240 | + * | |
| 1241 | + * RELATION group example: | |
| 1242 | + * [ | |
| 1243 | + * 'RELATION' => 'OR', | |
| 1244 | + * [ 'key' => 'status', 'compare' => '=', 'value' => 'canceled' ], | |
| 1245 | + * [ 'key' => 'subscription_status', 'compare' => '=', 'value' => 'canceled' ], | |
| 1246 | + * ] | |
| 1247 | + * → " AND (status = %s OR subscription_status = %s)" | |
| 1248 | + * | |
| 1249 | + * Conditions with disallowed columns or operators are silently skipped. | |
| 1250 | + * Always prefixes the returned fragment with " AND " so callers can append | |
| 1251 | + * directly to a `WHERE 1=1` clause. | |
| 1252 | + * | |
| 1253 | + * @param array<int|string,mixed> $where_group Group of conditions, optionally with 'RELATION'. | |
| 1254 | + * @param array<mixed> $params Reference to running params array; appended in place. | |
| 1255 | + * @since 2.9.0 | |
| 1256 | + * @return string SQL fragment to append, or empty string if nothing valid. | |
| 1257 | + */ | |
| 1258 | + private static function build_clause_for_group( array $where_group, array &$params ) { | |
| 1259 | + if ( empty( $where_group ) ) { | |
| 1260 | + return ''; | |
| 1261 | + } | |
| 1262 | + | |
| 1263 | + $is_relation_group = ! empty( $where_group['RELATION'] ) && is_string( $where_group['RELATION'] ); | |
| 1264 | + $relation = $is_relation_group && 'OR' === strtoupper( $where_group['RELATION'] ) | |
| 1265 | + ? 'OR' | |
| 1266 | + : 'AND'; | |
| 1267 | + | |
| 1268 | + $sub_clauses = []; | |
| 1269 | + | |
| 1270 | + foreach ( $where_group as $key => $condition ) { | |
| 1271 | + if ( 'RELATION' === $key || ! is_array( $condition ) ) { | |
| 1272 | + continue; | |
| 1273 | + } | |
| 1274 | + if ( ! isset( $condition['key'], $condition['compare'], $condition['value'] ) ) { | |
| 1275 | + continue; | |
| 1276 | + } | |
| 1277 | + if ( ! in_array( $condition['key'], self::ALLOWED_COLUMNS, true ) ) { | |
| 1278 | + continue; | |
| 1279 | + } | |
| 1280 | + | |
| 1281 | + $operator = strtoupper( trim( (string) $condition['compare'] ) ); | |
| 1282 | + if ( ! in_array( $operator, self::ALLOWED_OPERATORS, true ) ) { | |
| 1283 | + continue; | |
| 1284 | + } | |
| 1285 | + | |
| 1286 | + $column = $condition['key']; | |
| 1287 | + | |
| 1288 | + if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) ) { | |
| 1289 | + if ( ! is_array( $condition['value'] ) ) { | |
| 1290 | + // A scalar used to fall through to the else branch and emit | |
| 1291 | + // `col IN %s`, which is a syntax error that fails the whole | |
| 1292 | + // query. Base::prepare_where_clauses() reports this and drops | |
| 1293 | + // the condition; do the same rather than leave the two builders | |
| 1294 | + // disagreeing on malformed input. | |
| 1295 | + _doing_it_wrong( | |
| 1296 | + __METHOD__, | |
| 1297 | + esc_html( "{$operator} requires an array value, received " . gettype( $condition['value'] ) . '.' ), | |
| 1298 | + '2.12.7' | |
| 1299 | + ); | |
| 1300 | + continue; | |
| 1301 | + } | |
| 1302 | + | |
| 1303 | + $ids = array_map( 'absint', $condition['value'] ); | |
| 1304 | + if ( [] === $ids ) { | |
| 1305 | + // Same empty-list handling as Base::prepare_where_clauses(), so | |
| 1306 | + // the two builders cannot disagree. An empty IN matches nothing. | |
| 1307 | + // An empty NOT IN excludes nothing, and is dropped rather than | |
| 1308 | + // written as a literal, because a literal true would make an | |
| 1309 | + // enclosing OR group match every row. | |
| 1310 | + // | |
| 1311 | + // This builder serves the manage_options-gated admin payments | |
| 1312 | + // listing only, through get_all_main_payments() and | |
| 1313 | + // get_total_main_payments_by_status(). The payment-history | |
| 1314 | + // shortcode's customer filter is compiled by | |
| 1315 | + // Base::prepare_where_clauses() via Payments::get_all() -- that | |
| 1316 | + // group is where an OR fail-open would actually leak, and it is | |
| 1317 | + // covered by the change in base.php. | |
| 1318 | + if ( 'IN' === $operator ) { | |
| 1319 | + $sub_clauses[] = '1 = 0'; | |
| 1320 | + } | |
| 1321 | + continue; | |
| 1322 | + } | |
| 1323 | + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); | |
| 1324 | + $sub_clauses[] = "{$column} {$operator} ({$placeholders})"; | |
| 1325 | + foreach ( $ids as $id ) { | |
| 1326 | + $params[] = $id; | |
| 1327 | + } | |
| 1328 | + } else { | |
| 1329 | + $sub_clauses[] = "{$column} {$operator} %s"; | |
| 1330 | + $params[] = $condition['value']; | |
| 1331 | + } | |
| 1332 | + } | |
| 1333 | + | |
| 1334 | + if ( empty( $sub_clauses ) ) { | |
| 1335 | + return ''; | |
| 1336 | + } | |
| 1337 | + | |
| 1338 | + if ( $is_relation_group ) { | |
| 1339 | + return ' AND (' . implode( " {$relation} ", $sub_clauses ) . ')'; | |
| 1340 | + } | |
| 1341 | + | |
| 1342 | + return ' AND ' . implode( ' AND ', $sub_clauses ); | |
| 1294 | 1343 | } |
| 1295 | 1344 | } |