PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.1
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
defender-security / framework / db / class-mapper.php

class-mapper.php in Defender Security – Malware Scanner, Login Security & Firewall 5.5.1, at framework/db/class-mapper.php

554 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Data mapper for CRUD.
4 *
5 * @package Calotes\DB
6 */
7
8 namespace Calotes\DB;
9
10 use ReflectionClass;
11 use Calotes\Base\Model;
12 use Calotes\Base\Component;
13
14 /**
15 * Responsible for performing CRUD operations.
16 */
17 class Mapper extends Component {
18
19 /**
20 * Contain the current model class name.
21 *
22 * @var string
23 */
24 private $repository;
25
26 /**
27 * The columns to select in the query.
28 *
29 * @var string
30 */
31 private $select = '';
32
33 /**
34 * Where statements for the query.
35 *
36 * @var array
37 */
38 private $where = array();
39
40 /**
41 * The grouping parameter for the query.
42 *
43 * @var string
44 */
45 private $group = '';
46
47 /**
48 * The ordering parameter for the query.
49 *
50 * @var string
51 */
52 private $order = '';
53
54 /**
55 * The limit for the query results.
56 *
57 * @var string
58 */
59 private $limit = '';
60
61 /**
62 * Cache for storing retrieved records.
63 *
64 * @var array
65 */
66 private $known = array();
67
68 /**
69 * Store the last executed query.
70 *
71 * @var string
72 */
73 public $saved_queries = '';
74
75 /**
76 * Set the repository class name.
77 *
78 * @param mixed $class_name The class name to set for the repository.
79 *
80 * @return $this
81 */
82 public function get_repository( $class_name ) {
83 $this->repository = $class_name;
84
85 return $this;
86 }
87
88 /**
89 * Set the columns to select in the SQL query.
90 *
91 * @param mixed $select The columns to select.
92 *
93 * @return $this
94 */
95 public function select( $select ) {
96 $this->select = $select;
97
98 return $this;
99 }
100
101 /**
102 * Set the WHERE clause for the query based on the provided arguments.
103 *
104 * @param mixed ...$args The conditions to apply in the WHERE clause.
105 *
106 * @return $this
107 */
108 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
114
115 return $this;
116 }
117
118 [$key, $operator, $value] = $args;
119 if ( ! $this->valid_operator( $operator ) ) {
120 // Prevent this operator.
121 return $this;
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 );
135 $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 }
147
148 return $this;
149 }
150
151 /**
152 * Guess the type of value for correcting placeholder.
153 *
154 * @param mixed $value The value to guess.
155 *
156 * @return string
157 */
158 private function guess_var_type( $value ): string {
159 if ( filter_var( $value, FILTER_VALIDATE_INT ) ) {
160 return '%d';
161 }
162
163 if ( filter_var( $value, FILTER_VALIDATE_FLOAT ) ) {
164 return '%f';
165 }
166
167 return '%s';
168 }
169
170 /**
171 * Find a record by its ID.
172 *
173 * @param int $id The ID of the record.
174 *
175 * @return $this
176 */
177 public function find_by_id( $id ) {
178 global $wpdb;
179 $this->where[] = $wpdb->prepare( 'id = %d', $id );
180
181 return $this;
182 }
183
184 /**
185 * Set the group by clause for the SQL query based on the provided argument.
186 *
187 * @param string $group_by The column to group by.
188 *
189 * @return $this
190 */
191 public function group_by( $group_by ) {
192 global $wpdb;
193 $this->group = str_replace(
194 "'",
195 '',
196 $wpdb->prepare( 'GROUP BY %s', $group_by )
197 );
198
199 return $this;
200 }
201
202 /**
203 * Set the order for the SQL query based on the provided arguments.
204 *
205 * @param mixed $order_by The column to order by.
206 * @param string $order The order direction, defaults to 'asc'.
207 *
208 * @return $this
209 */
210 public function order_by( $order_by, $order = 'asc' ) {
211 global $wpdb;
212 if ( ! in_array( $order, array( 'asc', 'desc' ), true ) ) {
213 // Fall it back.
214 $order = 'asc';
215 }
216 $this->order = str_replace(
217 "'",
218 '',
219 $wpdb->prepare( 'ORDER BY %s %s', $order_by, $order )
220 );
221
222 return $this;
223 }
224
225 /**
226 * Set the limit for the SQL query based on the provided value.
227 *
228 * @param int $limit The limit value.
229 * @param int|null $offset The offset value.
230 *
231 * @return $this
232 */
233 public function limit( $limit, $offset = null ) {
234 global $wpdb;
235
236 if ( null === $offset ) {
237 $this->limit = $wpdb->prepare( 'LIMIT %d', $limit );
238 } else {
239 $this->limit = $wpdb->prepare( 'LIMIT %d OFFSET %d', $limit, $offset );
240 }
241
242 return $this;
243 }
244
245 /**
246 * Find the first.
247 *
248 * @return null|Model
249 */
250 public function first() {
251 $this->limit = 'LIMIT 0,1';
252 $sql = $this->query_build(); // SQL is prepared here. We will ignore prepare rules.
253 $this->saved_queries = $sql;
254 global $wpdb;
255 $data = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
256 if ( is_null( $data ) ) {
257 return null;
258 }
259 // Check if we have any json string in property.
260 foreach ( $data as &$datum ) {
261 if ( is_string( $datum ) ) {
262 $tmp = json_decode( $datum, true );
263 if ( is_array( $tmp ) ) {
264 $datum = $tmp;
265 }
266 }
267 }
268 $class_name = $this->repository;
269 $model = new $class_name();
270 $model->import( $data );
271
272 return $model;
273 }
274
275 /**
276 * Retrieves the models based on the data obtained from get_results().
277 *
278 * @return array
279 */
280 public function get() {
281 $data = $this->get_results();
282 $models = array();
283 foreach ( $data as $row ) {
284 foreach ( $row as &$property ) {
285 if ( is_string( $property ) ) {
286 $tmp = json_decode( $property, true );
287 if ( is_array( $tmp ) ) {
288 $property = $tmp;
289 }
290 }
291 }
292 $class_name = $this->repository;
293 $model = new $class_name();
294 $model->import( $row );
295 $models[] = $model;
296 }
297
298 return $models;
299 }
300
301 /**
302 * Get records in array form.
303 *
304 * @return array
305 * @since 2.7.0
306 */
307 public function get_results() {
308 $sql = $this->query_build(); // SQL is prepared here.
309 $this->saved_queries = $sql;
310
311 global $wpdb;
312 $data = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
313 if ( is_null( $data ) ) {
314 $data = array();
315 }
316
317 return $data;
318 }
319
320 /**
321 * Get the count of records based on the provided query.
322 *
323 * @return string|null The count of records.
324 */
325 public function count() {
326 global $wpdb;
327 $sql = $this->query_build( 'COUNT(*)' ); // SQL is prepared here.
328
329 $result = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
330
331 return $result;
332 }
333
334 /**
335 * Handle the insert/update of current model.
336 *
337 * @param Model $model The model to save.
338 *
339 * @return int|bool The ID of current record OR false.
340 * @throws \ReflectionException If class is not defined.
341 */
342 public function save( Model &$model ) {
343 global $wpdb;
344 $data = $model->export();
345 $data_type = array();
346 $exported_type = $model->export_type();
347 unset( $data['table'] );
348 unset( $data['safe'] );
349 foreach ( $data as $key => &$val ) {
350 if ( is_array( $val ) ) {
351 $val = wp_json_encode( $val );
352 } elseif ( is_bool( $val ) ) {
353 $val = $val ? 1 : 0;
354 }
355
356 $data_type[] = $exported_type[ $key ] ?? '%s';
357 }
358 $table = self::table( $model );
359 if ( $model->id ) {
360 $ret = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
361 $table,
362 $data,
363 array( 'id' => $model->id ),
364 $data_type,
365 array( '%d' )
366 );
367 } else {
368 $ret = $wpdb->insert( $table, $data, $data_type ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
369 // Bind this for later use.
370 $model->id = $wpdb->insert_id;
371 }
372
373 if ( false === $ret ) {
374 return false;
375 }
376
377 return $wpdb->insert_id;
378 }
379
380 /**
381 * Delete a record from the database table based on the provided conditions.
382 *
383 * @param mixed $where The conditions to apply when deleting the record.
384 *
385 * @return int|false The number of rows affected or false on failure.
386 * @throws \ReflectionException If class is not defined.
387 */
388 public function delete( $where ) {
389 $table = self::table();
390 global $wpdb;
391
392 return $wpdb->delete( $table, $where ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
393 }
394
395 /**
396 * Delete all records from the database table based on the provided conditions.
397 *
398 * @return int|false The number of rows affected or false on failure.
399 * @throws \ReflectionException If class is not defined.
400 */
401 public function delete_all() {
402 $table = self::table();
403 global $wpdb;
404
405 $where = implode( ' AND ', $this->where );
406 $sql = "DELETE FROM $table WHERE $where";
407 $this->clear();
408
409 return $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
410 }
411
412 /**
413 * Delete records from the database table based on the provided conditions and limit.
414 *
415 * @return int|false The number of rows affected or false on failure.
416 * @throws \ReflectionException If class is not defined.
417 */
418 public function delete_by_limit() {
419 $table = self::table();
420 global $wpdb;
421
422 $where = implode( ' AND ', $this->where );
423 $limit = $this->limit;
424 $order = $this->order;
425 $sql = "DELETE FROM $table WHERE $where $order $limit";
426 $this->clear();
427
428 return $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
429 }
430
431 /**
432 * Handle the truncation of a database table.
433 *
434 * @return int|false The number of rows affected or false on failure.
435 * @throws \ReflectionException If class is not defined.
436 */
437 public function truncate() {
438 $table = self::table();
439 global $wpdb;
440
441 $query = "TRUNCATE TABLE $table"; // SQL is prepared here. so we can ignore WordPress.DB.PreparedSQL.NotPrepared.
442
443 return $wpdb->query( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
444 }
445
446 /**
447 * It is used to retrieve the table name associated with a given model.
448 *
449 * @param mixed $model (optional) The model object or class name. If not provided, it uses the "repository"
450 * property of the class.
451 *
452 * @return string|false The table name with the WordPress database prefix, or false if the table property doesn't
453 * exist or an exception occurs.
454 * @throws \ReflectionException If the model class doesn't exist.
455 */
456 private function table( $model = null ) {
457 if ( is_null( $model ) ) {
458 $class_name = $this->repository;
459 $model = $this->get_model();
460 } else {
461 $class_name = $model;
462 }
463 $refection = new ReflectionClass( $class_name );
464 if ( $refection->hasProperty( 'table' ) ) {
465 $property = $refection->getProperty( 'table' );
466 $property->setAccessible( true );
467
468 $table = $property->getValue( $model );
469 global $wpdb;
470
471 // Have to set the prefix.
472 return $wpdb->base_prefix . $table;
473 }
474 // This when class doesn't exist.
475 return false;
476 }
477
478 /**
479 * Reset all the queries prepare after an action.
480 */
481 private function clear() {
482 $this->select = '';
483 $this->where = array();
484 $this->group = '';
485 $this->order = '';
486 $this->limit = '';
487 }
488
489 /**
490 * Join the stuff on the table to make a full query statement.
491 * SQL params e.g. WHERE, ORDER or LIMIT were escaped on separate methods.
492 *
493 * @param string $select Columns to select.
494 *
495 * @return string
496 * @throws \ReflectionException If class is not defined.
497 */
498 private function query_build( $select = '*' ) {
499 $table = $this->table();
500 $where = implode( ' AND ', $this->where );
501
502 $select = ! empty( $this->select ) ? $this->select : $select;
503 $group_by = $this->group;
504 $order_by = $this->order;
505 $limit = $this->limit;
506 $sql = "SELECT $select FROM $table WHERE $where $group_by $order_by $limit";
507 $this->clear();
508
509 return $sql;
510 }
511
512 /**
513 * Checks if the given operator is valid.
514 *
515 * @param string $operator The operator to check.
516 *
517 * @return bool True if the operator is valid, false otherwise.
518 */
519 private function valid_operator( $operator ) {
520 $operator = strtolower( $operator );
521 $allowed = array(
522 'in',
523 'not in',
524 '>',
525 '<',
526 '=',
527 '<=',
528 '>=',
529 'like',
530 'between',
531 'regexp',
532 'not regexp',
533 );
534
535 return in_array( $operator, $allowed, true );
536 }
537
538 /**
539 * Cache the model instance for clone & reference use.
540 *
541 * @return mixed
542 */
543 private function get_model() {
544 if ( isset( $this->known[ $this->repository ] ) ) {
545 return $this->known[ $this->repository ];
546 }
547 $class = $this->repository;
548 $model = new $class();
549 $this->known[ $this->repository ] = $model;
550
551 return $model;
552 }
553 }
554