PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | framework/db/class-mapper.php +218 -79 5.6.1 → 6.3.0 View file →
@@ -6,9 +6,8 @@
6 6 */
7 7
8 8 namespace Calotes\DB;
9 9
10 -use ReflectionClass;
11 10 use Calotes\Base\Model;
12 11 use Calotes\Base\Component;
13 12
14 13 /**
@@ -74,9 +73,9 @@
74 73
75 74 /**
76 75 * Set the repository class name.
77 76 *
78 - * @param mixed $class_name The class name to set for the repository.
77 + * @param mixed $class_name The class name to set for the repository.
79 78 *
80 79 * @return $this
81 80 */
82 81 public function get_repository( $class_name ) {
@@ -87,9 +86,9 @@
87 86
88 87 /**
89 88 * Set the columns to select in the SQL query.
90 89 *
91 - * @param mixed $select The columns to select.
90 + * @param mixed $select The columns to select.
92 91 *
93 92 * @return $this
94 93 */
95 94 public function select( $select ) {
@@ -100,50 +99,33 @@
100 99
101 100 /**
102 101 * Set the WHERE clause for the query based on the provided arguments.
103 102 *
104 - * @param mixed ...$args The conditions to apply in the WHERE clause.
103 + * Supports multiple call signatures:
104 + * - where($column, $value) - equals comparison
105 + * - where($column, $operator, $value) - custom operator comparison
105 106 *
107 + * @param mixed ...$args The conditions to apply in the WHERE clause.
108 + *
106 109 * @return $this
107 110 */
108 111 public function where( ...$args ) {
109 - global $wpdb;
110 - if ( 2 === count( $args ) ) {
111 - list($key, $value) = $args;
112 - // It is safe because guess_var_type() returns correct placeholder and value goes through $wpdb->prepare().
113 - $this->where[] = $wpdb->prepare( "`$key` = " . $this->guess_var_type( $value ), $value ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
112 + $result = $this->prepare_where_args( $args );
114 113
114 + if ( null === $result ) {
115 115 return $this;
116 116 }
117 117
118 - [$key, $operator, $value] = $args;
118 + [ $column, $operator, $value ] = $result;
119 +
119 120 if ( ! $this->valid_operator( $operator ) ) {
120 - // Prevent this operator.
121 121 return $this;
122 122 }
123 - if ( in_array( strtolower( $operator ), array( 'in', 'not in' ), true ) ) {
124 - $tmp = $key . " {$operator} (" . implode(
125 - ', ',
126 - array_fill( 0, count( $value ), $this->guess_var_type( $value ) )
127 - ) . ')';
128 - $sql = call_user_func_array(
129 - array(
130 - $wpdb,
131 - 'prepare',
132 - ),
133 - array_merge( array( $tmp ), $value )
134 - );
123 +
124 + $sql = $this->compile_where( $column, $operator, $value );
125 +
126 + if ( $sql ) {
135 127 $this->where[] = $sql;
136 - } elseif ( 'between' === strtolower( $operator ) ) {
137 - // It is safe because guess_var_type() returns correct placeholder and value goes through $wpdb->prepare().
138 - $this->where[] = $wpdb->prepare(
139 - "{$key} {$operator} {$this->guess_var_type($value[0])} AND {$this->guess_var_type($value[1])}", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
140 - $value[0],
141 - $value[1]
142 - );
143 - } else {
144 - // It is safe because guess_var_type() returns correct placeholder and value goes through $wpdb->prepare().
145 - $this->where[] = $wpdb->prepare( "`$key` $operator {$this->guess_var_type($value)}", $value ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
146 128 }
147 129
148 130 return $this;
149 131 }
@@ -148,20 +130,139 @@
148 130 return $this;
149 131 }
150 132
151 133 /**
134 + * Prepare where arguments - handles both 2-arg and 3-arg signatures.
135 + *
136 + * @param array $args The arguments passed to where().
137 + *
138 + * @return array|null [$column, $operator, $value] or null if invalid argument count.
139 + */
140 + private function prepare_where_args( array $args ): ?array {
141 + $count = count( $args );
142 +
143 + if ( 2 === $count ) {
144 + return array( $args[0], '=', $args[1] );
145 + }
146 +
147 + if ( 3 === $count ) {
148 + return array( $args[0], $args[1], $args[2] );
149 + }
150 +
151 + return null;
152 + }
153 +
154 + /**
155 + * Compile a where clause into SQL.
156 + *
157 + * @param string $column The column name.
158 + * @param string $operator The operator.
159 + * @param mixed $value The value to compare against.
160 + *
161 + * @return string|null The compiled SQL or null if invalid.
162 + */
163 + private function compile_where( string $column, string $operator, $value ): ?string {
164 + if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $column ) ) {
165 + return null;
166 + }
167 +
168 + global $wpdb;
169 +
170 + $op_lower = strtolower( $operator );
171 +
172 + // Handle IN / NOT IN operators.
173 + if ( in_array( $op_lower, array( 'in', 'not in' ), true ) ) {
174 + return $this->compile_where_in( $column, $operator, $value );
175 + }
176 +
177 + // Handle BETWEEN operator.
178 + if ( 'between' === $op_lower ) {
179 + return $this->compile_where_between( $column, $operator, $value );
180 + }
181 +
182 + // Handle basic comparison operators.
183 + $placeholder = $this->guess_var_type( $value );
184 +
185 + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
186 + return $wpdb->prepare( "%i $operator $placeholder", $column, $value );
187 + }
188 +
189 + /**
190 + * Compile a WHERE IN / NOT IN clause.
191 + *
192 + * @param string $column The column name.
193 + * @param string $operator The operator (IN or NOT IN).
194 + * @param mixed $values The values (should be array, invalid inputs skipped).
195 + *
196 + * @return string|null The compiled SQL or null if empty values.
197 + */
198 + private function compile_where_in( string $column, string $operator, $values ): ?string {
199 + if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $column ) ) {
200 + return null;
201 + }
202 +
203 + if ( ! is_array( $values ) || 0 === count( $values ) ) {
204 + return null;
205 + }
206 +
207 + global $wpdb;
208 +
209 + $placeholders = array();
210 + foreach ( $values as $val ) {
211 + $placeholders[] = $this->guess_var_type( $val );
212 + }
213 +
214 + $sql_template = '%i ' . $operator . ' (' . implode( ', ', $placeholders ) . ')';
215 +
216 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
217 + return $wpdb->prepare( $sql_template, $column, ...$values );
218 + }
219 +
220 + /**
221 + * Compile a WHERE BETWEEN clause.
222 + *
223 + * @param string $column The column name.
224 + * @param string $operator The operator (BETWEEN).
225 + * @param mixed $values The values (should be array with min/max, invalid inputs skipped).
226 + *
227 + * @return string|null The compiled SQL or null if invalid values.
228 + */
229 + private function compile_where_between( string $column, string $operator, $values ): ?string {
230 + if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $column ) ) {
231 + return null;
232 + }
233 +
234 + if ( ! is_array( $values ) || count( $values ) < 2 ) {
235 + return null;
236 + }
237 +
238 + global $wpdb;
239 +
240 + $placeholder_min = $this->guess_var_type( $values[0] );
241 + $placeholder_max = $this->guess_var_type( $values[1] );
242 +
243 + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
244 + return $wpdb->prepare(
245 + "%i $operator $placeholder_min AND $placeholder_max", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
246 + $column,
247 + $values[0],
248 + $values[1]
249 + );
250 + }
251 +
252 + /**
152 253 * Guess the type of value for correcting placeholder.
153 254 *
154 - * @param mixed $value The value to guess.
255 + * @param mixed $value The value to guess.
155 256 *
156 257 * @return string
157 258 */
158 259 private function guess_var_type( $value ): string {
159 - if ( filter_var( $value, FILTER_VALIDATE_INT ) ) {
260 + if ( false !== filter_var( $value, FILTER_VALIDATE_INT ) ) {
160 261 return '%d';
161 262 }
162 263
163 - if ( filter_var( $value, FILTER_VALIDATE_FLOAT ) ) {
264 + if ( false !== filter_var( $value, FILTER_VALIDATE_FLOAT ) ) {
164 265 return '%f';
165 266 }
166 267
167 268 return '%s';
@@ -169,9 +270,9 @@
169 270
170 271 /**
171 272 * Find a record by its ID.
172 273 *
173 - * @param int $id The ID of the record.
274 + * @param int $id The ID of the record.
174 275 *
175 276 * @return $this
176 277 */
177 278 public function find_by_id( $id ) {
@@ -183,20 +284,33 @@
183 284
184 285 /**
185 286 * Set the group by clause for the SQL query based on the provided argument.
186 287 *
187 - * @param string $group_by The column to group by.
288 + * @param string $group_by The column to group by.
188 289 *
189 290 * @return $this
190 291 */
191 292 public function group_by( $group_by ) {
192 293 global $wpdb;
193 - $this->group = str_replace(
194 - "'",
195 - '',
196 - $wpdb->prepare( 'GROUP BY %s', $group_by )
197 - );
198 294
295 + if ( ! is_string( $group_by ) || '' === trim( $group_by ) ) {
296 + return $this;
297 + }
298 +
299 + $parts = array_map( 'trim', explode( ',', $group_by ) );
300 + $safe_parts = array();
301 + foreach ( $parts as $part ) {
302 + if ( preg_match( '/^[a-zA-Z0-9_]+$/', $part ) ) {
303 + $safe_parts[] = $wpdb->prepare( '%i', $part );
304 + }
305 + }
306 +
307 + if ( empty( $safe_parts ) ) {
308 + return $this;
309 + }
310 +
311 + $this->group = 'GROUP BY ' . implode( ', ', $safe_parts );
312 +
199 313 return $this;
200 314 }
201 315
202 316 /**
@@ -201,25 +315,36 @@
201 315
202 316 /**
203 317 * Set the order for the SQL query based on the provided arguments.
204 318 *
205 - * @param mixed $order_by The column to order by.
206 - * @param string $order The order direction, defaults to 'asc'.
319 + * @param mixed $order_by The column to order by.
320 + * @param string $order The order direction, defaults to 'asc'.
207 321 *
208 322 * @return $this
209 323 */
210 324 public function order_by( $order_by, $order = 'asc' ) {
211 325 global $wpdb;
212 - if ( ! in_array( $order, array( 'asc', 'desc' ), true ) ) {
213 - // Fall it back.
214 - $order = 'asc';
326 +
327 + $order = 'desc' === strtolower( (string) $order ) ? 'DESC' : 'ASC';
328 +
329 + if ( ! is_string( $order_by ) || '' === trim( $order_by ) ) {
330 + return $this;
215 331 }
216 - $this->order = str_replace(
217 - "'",
218 - '',
219 - $wpdb->prepare( 'ORDER BY %s %s', $order_by, $order )
220 - );
221 332
333 + $parts = array_map( 'trim', explode( ',', $order_by ) );
334 + $safe_parts = array();
335 + foreach ( $parts as $part ) {
336 + if ( preg_match( '/^[a-zA-Z0-9_]+$/', $part ) ) {
337 + $safe_parts[] = $wpdb->prepare( '%i', $part );
338 + }
339 + }
340 +
341 + if ( empty( $safe_parts ) ) {
342 + return $this;
343 + }
344 +
345 + $this->order = 'ORDER BY ' . implode( ', ', $safe_parts ) . ' ' . $order;
346 +
222 347 return $this;
223 348 }
224 349
225 350 /**
@@ -224,10 +349,10 @@
224 349
225 350 /**
226 351 * Set the limit for the SQL query based on the provided value.
227 352 *
228 - * @param int $limit The limit value.
229 - * @param int|null $offset The offset value.
353 + * @param int $limit The limit value.
354 + * @param int|null $offset The offset value.
230 355 *
231 356 * @return $this
232 357 */
233 358 public function limit( $limit, $offset = null ) {
@@ -301,9 +426,9 @@
301 426 /**
302 427 * Get records in array form.
303 428 *
304 429 * @return array
305 - * @since 2.7.0
430 + * @since 2.7.0
306 431 */
307 432 public function get_results() {
308 433 $sql = $this->query_build(); // SQL is prepared here.
309 434 $this->saved_queries = $sql;
@@ -333,9 +458,9 @@
333 458
334 459 /**
335 460 * Handle the insert/update of current model.
336 461 *
337 - * @param Model $model The model to save.
462 + * @param Model $model The model to save.
338 463 *
339 464 * @return int|bool The ID of current record OR false.
340 465 * @throws \ReflectionException If class is not defined.
341 466 */
@@ -379,9 +504,9 @@
379 504
380 505 /**
381 506 * Delete a record from the database table based on the provided conditions.
382 507 *
383 - * @param mixed $where The conditions to apply when deleting the record.
508 + * @param mixed $where The conditions to apply when deleting the record.
384 509 *
385 510 * @return int|false The number of rows affected or false on failure.
386 511 * @throws \ReflectionException If class is not defined.
387 512 */
@@ -437,8 +562,20 @@
437 562 public function truncate() {
438 563 $table = self::table();
439 564 global $wpdb;
440 565
566 + // Uninstall/reset flows can run in mixed Free/Pro states where some tables were never created.
567 + $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
568 + $wpdb->prepare(
569 + 'SHOW TABLES LIKE %s',
570 + $wpdb->esc_like( $table )
571 + )
572 + );
573 +
574 + if ( empty( $exists ) ) {
575 + return false;
576 + }
577 +
441 578 $query = "TRUNCATE TABLE $table"; // SQL is prepared here. so we can ignore WordPress.DB.PreparedSQL.NotPrepared.
442 579
443 580 return $wpdb->query( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
444 581 }
@@ -445,10 +582,10 @@
445 582
446 583 /**
447 584 * It is used to retrieve the table name associated with a given model.
448 585 *
449 - * @param mixed $model (optional) The model object or class name. If not provided, it uses the "repository"
450 - * property of the class.
586 + * @param mixed $model (optional) The model object or class name. If not provided, it uses the "repository"
587 + * property of the class.
451 588 *
452 589 * @return string|false The table name with the WordPress database prefix, or false if the table property doesn't
453 590 * exist or an exception occurs.
454 591 * @throws \ReflectionException If the model class doesn't exist.
@@ -454,23 +591,18 @@
454 591 * @throws \ReflectionException If the model class doesn't exist.
455 592 */
456 593 private function table( $model = null ) {
457 594 if ( is_null( $model ) ) {
458 - $class_name = $this->repository;
459 - $model = $this->get_model();
460 - } else {
461 - $class_name = $model;
595 + $model = $this->get_model();
462 596 }
463 - $refection = new ReflectionClass( $class_name );
464 - if ( $refection->hasProperty( 'table' ) ) {
465 - $property = $refection->getProperty( 'table' );
466 - $property->setAccessible( true );
597 + if ( is_object( $model ) && method_exists( $model, 'get_table' ) ) {
598 + $table = $model->get_table();
599 + if ( null !== $table ) {
600 + global $wpdb;
467 601
468 - $table = $property->getValue( $model );
469 - global $wpdb;
470 -
471 - // Have to set the prefix.
472 - return $wpdb->base_prefix . $table;
602 + // Have to set the prefix.
603 + return $wpdb->base_prefix . $table;
604 + }
473 605 }
474 606 // This when class doesn't exist.
475 607 return false;
476 608 }
@@ -489,18 +621,25 @@
489 621 /**
490 622 * Join the stuff on the table to make a full query statement.
491 623 * SQL params e.g. WHERE, ORDER or LIMIT were escaped on separate methods.
492 624 *
493 - * @param string $select Columns to select.
625 + * @param string $select Columns to select.
494 626 *
495 627 * @return string
496 628 * @throws \ReflectionException If class is not defined.
497 629 */
498 - private function query_build( $select = '*' ) {
499 - $table = $this->table();
500 - $where = implode( ' AND ', $this->where );
630 + private function query_build( string $select = '*' ) {
631 + $table = $this->table();
632 + $where_parts = array_filter(
633 + $this->where,
634 + static function ( $clause ) {
635 + return null !== $clause && '' !== trim( (string) $clause );
636 + }
637 + );
638 + $where = implode( ' AND ', $where_parts );
639 + $where = '' === $where ? '1=1' : $where;
501 640
502 - $select = ! empty( $this->select ) ? $this->select : $select;
641 + $select = '' !== $this->select ? $this->select : $select;
503 642 $group_by = $this->group;
504 643 $order_by = $this->order;
505 644 $limit = $this->limit;
506 645 $sql = "SELECT $select FROM $table WHERE $where $group_by $order_by $limit";
@@ -511,9 +650,9 @@
511 650
512 651 /**
513 652 * Checks if the given operator is valid.
514 653 *
515 - * @param string $operator The operator to check.
654 + * @param string $operator The operator to check.
516 655 *
517 656 * @return bool True if the operator is valid, false otherwise.
518 657 */
519 658 private function valid_operator( $operator ) {