PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.2
V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 2.10.2 All 137 releases
bit-form / includes / Core / Database / Model.php

Model.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.2.2, at includes/Core/Database/Model.php

648 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Provides Base Model Class
5 */
6
7 namespace BitCode\BitForm\Core\Database;
8
9 /**
10 * Undocumented class
11 */
12
13 use BitCode\BitForm\Core\Util\Log;
14 use WP_Error;
15
16 class Model
17 {
18 protected static $table;
19 protected static $primary_key;
20 protected $app_db;
21 protected $table_name;
22 protected $db_response;
23
24 /**
25 * Undocumented function
26 */
27 public function __construct()
28 {
29 global $wpdb;
30 $this->app_db = $wpdb;
31 $this->table_name = $wpdb->prefix . static::$table;
32 }
33
34 /**
35 * Insert a row
36 *
37 * @return mixed insert id on success, WP_Error on failure
38 */
39 public function insert($data = [])
40 {
41 if (is_null($data)) {
42 return new WP_Error('empty_data', 'Form data is empty');
43 }
44 $result = $this->app_db->insert(
45 $this->table_name,
46 $data
47 );
48 return $this->getResult($result);
49 }
50
51 /**
52 * Undocumented function
53 *
54 * @param string|string[] $item
55 * @param array $condition
56 *
57 * @return mixed
58 */
59 public function get($item = '*', $condition = [], $limit = null, $offset = null, $order_by = null, $order_follow = null)
60 {
61 static $tableExistsCache = [];
62 if (!isset($tableExistsCache[$this->table_name])) {
63 $tableExistsCache[$this->table_name] = !is_null(
64 $this->app_db->get_var(
65 $this->app_db->prepare('SHOW TABLES LIKE %s', $this->table_name)
66 )
67 );
68 }
69 if (!$tableExistsCache[$this->table_name]) {
70 return [];
71 }
72 if (\is_array($item)) {
73 $column_to_select = implode(',', $item);
74 } else {
75 $column_to_select = $item;
76 }
77 $checkCondition = $this->checkCondition($condition);
78 if (is_wp_error($checkCondition)) {
79 return $checkCondition;
80 }
81 $order = null;
82 if (!\is_null($order_by)) {
83 $order_follow = \is_null($order_follow) ? 'ASC' : $order_follow;
84 $direction = \is_string($order_follow) ? strtoupper(trim($order_follow)) : '';
85 if ($this->isSafeConditionIdentifier($order_by) && \in_array($direction, ['ASC', 'DESC'], true)) {
86 $order .= ' ORDER BY ' . $this->quoteIdentifier($order_by) . ' ' . $direction;
87 } else {
88 Log::debug_log([
89 'message' => 'Model::get() ignored an unsafe ORDER BY',
90 'table' => $this->table_name,
91 'order_by' => $order_by,
92 'order_follow' => $order_follow,
93 ]);
94 }
95 }
96 $paginate = null;
97 if (!\is_null($limit)) {
98 $limit = \intval($limit);
99 $paginate .= " LIMIT $limit ";
100 }
101 if (!\is_null($offset)) {
102 $offset = \intval($offset);
103 $paginate .= " OFFSET $offset ";
104 }
105 if (empty($condition)) {
106 $sql = "SELECT $column_to_select FROM `$this->table_name` $order $paginate";
107 $all_values = null;
108 } else {
109 $formatted_conditions = $this->getFormatedCondition($condition);
110 if ($formatted_conditions) {
111 $condition_to_check = $formatted_conditions['conditions'];
112 $all_values = $formatted_conditions['values'];
113 } else {
114 $condition_to_check = null;
115 $all_values = null;
116 }
117 $sql = "SELECT $column_to_select FROM `$this->table_name`"
118 . $condition_to_check . $order . $paginate;
119 }
120 return $this->execute($sql, $all_values)->getResult();
121 }
122
123 /**
124 * Undocumented function
125 *
126 * @param string $item
127 * @param array $condition
128 *
129 * @return void
130 */
131 public function count($condition = null)
132 {
133 $checkCondition = $this->checkCondition($condition);
134 if (is_wp_error($checkCondition)) {
135 return $checkCondition;
136 }
137 if (empty($condition)) {
138 $result = $this->app_db->query(
139 "SELECT COUNT(*) FROM `$this->table_name`"
140 );
141 } else {
142 $formatted_conditions = $this->getFormatedCondition($condition);
143 if ($formatted_conditions) {
144 $condition_to_check = $formatted_conditions['conditions'];
145 $all_values = $formatted_conditions['values'];
146 } else {
147 $condition_to_check = null;
148 $all_values = null;
149 }
150 $result = $this->app_db->query(
151 $this->app_db->prepare(
152 "SELECT COUNT(*) as count FROM `{$this->table_name}`"
153 . $condition_to_check,
154 $all_values
155 )
156 );
157 }
158 if (!$result) {
159 if ($this->app_db->last_error) {
160 return new WP_Error('db_error', $this->app_db->last_error);
161 }
162 return new WP_Error('db_error', 'Result is empty');
163 } else {
164 return $this->app_db->last_result;
165 }
166 }
167
168 /**
169 * Undocumented function
170 *
171 * @param array $data_to_update
172 * @param array $condition
173 *
174 * @return mixed affected-row count on success, WP_Error on failure or when no row matched
175 */
176 public function update(array $data, array $condition)
177 {
178 if (
179 !\is_null($data)
180 && \is_array($data)
181 && array_keys($data) !== range(0, count($data) - 1)
182 ) {
183 $data_to_update = $data;
184 } else {
185 return new WP_Error(
186 'update_error',
187 'Nothing to update'
188 );
189 }
190 $update_condition = (!\is_null($condition) &&
191 array_keys($condition) !== range(0, count($condition) - 1)) ? $condition : null;
192 $result = $this->app_db->update(
193 $this->table_name,
194 $data_to_update,
195 $update_condition
196 );
197 return $this->getResult($result);
198 }
199
200 /**
201 * Undocumented function
202 *
203 * @param array $data_to_update
204 * @param array $condition
205 *
206 * @return void
207 */
208 public function bulkUpdate(array $data = null, array $condition = null)
209 {
210 if (
211 !\is_null($data)
212 && \is_array($data)
213 && array_keys($data) !== range(0, count($data) - 1)
214 ) {
215 $data_to_update = $data;
216 } else {
217 return new WP_Error(
218 'update_error',
219 'Nothing to update'
220 );
221 }
222
223 $update_fields = '';
224 $all_values = [];
225 $index_checker = 0;
226 $data_count = count($data_to_update) - 1;
227 foreach ($data_to_update as $field_name => $field_value) {
228 $update_fields .= $field_name . ' = ' . $this->getFieldFormat($field_value);
229 if ($index_checker < $data_count) {
230 $update_fields .= ',';
231 }
232 $index_checker = $index_checker + 1;
233 $all_values[] = $field_value;
234 }
235 $update_condition = (!\is_null($condition) &&
236 array_keys($condition) !== range(0, count($condition) - 1)) ? $condition : null;
237 $formatted_conditions = $this->getFormatedCondition($update_condition);
238 if ($formatted_conditions) {
239 $condition_to_check = $formatted_conditions['conditions'];
240 $all_values = array_merge($all_values, $formatted_conditions['values']);
241 } else {
242 $condition_to_check = null;
243 }
244 $result = $this->app_db->query(
245 $this->app_db->prepare(
246 "UPDATE `{$this->table_name}` SET $update_fields $condition_to_check",
247 $all_values
248 )
249 );
250 return $this->getResult($result);
251 }
252
253 /**
254 * Duplicate's row
255 *
256 * @param array $data_to_update
257 * @param array $condition
258 *
259 * @return void
260 */
261 public function duplicate(array $columns, array $duplicate, array $condition)
262 {
263 if (!(!\is_null($columns)
264 && \is_array($columns)
265 && array_keys($columns) === range(0, count($columns) - 1)
266 && !\is_null($duplicate)
267 && \is_array($duplicate)
268 && array_keys($duplicate) === range(0, count($duplicate) - 1))) {
269 return new WP_Error(
270 'duplicate_error',
271 'Nothing to duplicate'
272 );
273 }
274
275 $dupCol = '';
276 $insCol = \implode(',', $columns);
277 $all_values = [];
278 $data_count = count($duplicate) - 1;
279 foreach ($duplicate as $dupKey => $dupColName) {
280 if (in_array($dupColName, $columns)) {
281 $dupCol .= $dupColName;
282 } else {
283 $dupCol .= $this->getFieldFormat($dupColName);
284 $all_values[] = $dupColName;
285 }
286 if ($dupKey < $data_count) {
287 $dupCol .= ',';
288 }
289 }
290 $condition_to_check = null;
291 $update_condition = (!\is_null($condition) &&
292 array_keys($condition) !== range(0, count($condition) - 1)) ? $condition : null;
293 $formatted_conditions = $this->getFormatedCondition($update_condition);
294 if ($formatted_conditions) {
295 $condition_to_check = $formatted_conditions['conditions'];
296 $all_values = array_merge($all_values, $formatted_conditions['values']);
297 }
298 $query = "INSERT INTO `{$this->table_name}` ($insCol)
299 SELECT $dupCol FROM `{$this->table_name}` $condition_to_check";
300 $this->execute($query, $all_values);
301 return $this->getResult();
302 }
303
304 public function trash(array $condition = null)
305 {
306 if (
307 !\is_null($condition)
308 && \is_array($condition)
309 && array_keys($condition) !== range(0, count($condition) - 1)
310 ) {
311 $delete_condition = $condition;
312 } else {
313 return new WP_Error(
314 'deletion_error',
315 'At least 1 condition needed'
316 );
317 }
318 $update_condition = (!\is_null($condition) &&
319 array_keys($condition) !== range(0, count($condition) - 1)) ? $condition : null;
320 $result = $this->app_db->update(
321 $this->table_name,
322 $data_to_update,
323 $update_condition
324 );
325 return $this->getResult($result);
326 }
327
328 public function delete(array $condition = null)
329 {
330 if (
331 !\is_null($condition)
332 && \is_array($condition)
333 && array_keys($condition) !== range(0, count($condition) - 1)
334 ) {
335 $delete_condition = $condition;
336 } else {
337 return new WP_Error(
338 'deletion_error',
339 'At least 1 condition needed'
340 );
341 }
342 $result = $this->app_db->delete(
343 $this->table_name,
344 $delete_condition
345 );
346 return $this->getResult($result);
347 }
348
349 public function bulkDelete(array $condition = null)
350 {
351 if (
352 !\is_null($condition)
353 && \is_array($condition)
354 && array_keys($condition) !== range(0, count($condition) - 1)
355 ) {
356 $delete_condition = $condition;
357 } else {
358 return new WP_Error(
359 'deletion_error',
360 'At least 1 condition needed'
361 );
362 }
363 // $formatted_conditions = $this->getFormatedCondition($delete_condition, $check_operator);
364 $formatted_conditions = $this->getFormatedCondition($delete_condition);
365 if ($formatted_conditions) {
366 $condition_to_check = $formatted_conditions['conditions'];
367 $all_values = $formatted_conditions['values'];
368 } else {
369 $condition_to_check = null;
370 return new WP_Error(
371 'deletion_error',
372 'At least 1 condition needed'
373 );
374 }
375 $result = $this->app_db->query(
376 $this->app_db->prepare(
377 "DELETE FROM `{$this->table_name}` $condition_to_check",
378 $all_values
379 )
380 );
381 return $this->getResult($result);
382 }
383
384 protected function getFieldFormat($value)
385 {
386 return ('integer' === gettype($value)) ?
387 '%d' : (('double' === gettype($value)) ? '%f' : '%s');
388 }
389
390 /**
391 *
392 * @param mixed $identifier
393 *
394 * @return bool
395 */
396 protected function isSafeConditionIdentifier($identifier)
397 {
398 if (!\is_string($identifier)) {
399 return false;
400 }
401 $identifier = trim($identifier);
402 if ('' === $identifier) {
403 return false;
404 }
405
406 // A condition column may be table-qualified and backtick-quoted — the multi-table JOIN DELETE
407 // in FormEntryModel::bulkDelete() *must* pass `wp_bitforms_form_entries`.`id`, because a bare
408 // `id` is ambiguous across the two joined tables. Validate each segment on its own.
409 $parts = explode('.', $identifier);
410 if (count($parts) > 2) {
411 return false;
412 }
413 foreach ($parts as $part) {
414 $part = trim($part);
415 if (\strlen($part) > 1 && '`' === $part[0] && '`' === substr($part, -1)) {
416 $part = substr($part, 1, -1);
417 }
418 if (1 !== preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $part)) {
419 return false;
420 }
421 }
422
423 return true;
424 }
425
426 /**
427 * Backtick-quote a validated identifier, leaving an already-quoted or table-qualified one alone.
428 * Only ever call this on a value isSafeConditionIdentifier() has approved.
429 *
430 * @param string $identifier
431 *
432 * @return string
433 */
434 protected function quoteIdentifier($identifier)
435 {
436 $identifier = trim($identifier);
437 if (false !== strpos($identifier, '`') || false !== strpos($identifier, '.')) {
438 return $identifier;
439 }
440
441 return '`' . $identifier . '`';
442 }
443
444 /**
445 * @param mixed $operator
446 *
447 * @return bool
448 */
449 protected function isSafeConditionOperator($operator)
450 {
451 static $allowed = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS', 'IS NOT'];
452
453 return \is_string($operator) && \in_array(strtoupper(trim($operator)), $allowed, true);
454 }
455
456 /**
457 * A WHERE that matches nothing. Keeps a placeholder so the caller's
458 * $wpdb->prepare($sql, $values) still has something to bind.
459 *
460 * @return array
461 */
462 private function impossibleCondition()
463 {
464 return [
465 'conditions' => ' WHERE 1=%d ',
466 'values' => [0],
467 ];
468 }
469
470 protected function getFormatedCondition($condition, $check_operator = null, $join_operator = ' AND ')
471 {
472 if (\is_null($condition)) {
473 return false;
474 }
475 $no_condition = count($condition);
476 $index_checker = 0;
477 $condition_to_check = ' WHERE ';
478 $all_values = [];
479 foreach ($condition as $key => $value) {
480 if (!$this->isSafeConditionIdentifier($key)) {
481 return $this->impossibleCondition();
482 }
483 $value_type = '';
484 if (is_array($value)) {
485 // Check for raw SQL values first
486 if (isset($value['raw'])) {
487 if (!\is_string($value['raw'])) {
488 return $this->impossibleCondition();
489 }
490 $set_check_operator = isset($value['operator']) ? $value['operator'] : '=';
491 $value_type = $value['raw']; // Use raw SQL directly
492 // Don't add to $all_values since it's raw SQL
493 } elseif (isset($value['operator'])) {
494 // logic for operator arrays
495 $set_check_operator = $value['operator'];
496 $value_type .= $this->getFieldFormat($value['value']);
497 $all_values[] = $value['value'];
498 } else {
499 // logic for IN conditions
500 $set_check_operator = \is_null($check_operator) ? 'in' : $check_operator;
501 $value_type .= ' ( ';
502 $value_index_checker = 0;
503 $value_count = count($value) - 1;
504 foreach ($value as $condKey => $condValue) {
505 $value_type .= $this->getFieldFormat($condValue);
506 $all_values[] = $condValue;
507 if ($value_index_checker < $value_count) {
508 $value_type .= ', ';
509 }
510 $value_index_checker = $value_index_checker + 1;
511 }
512 $value_type .= ' )';
513 }
514 } else {
515 // logic for simple values
516 $set_check_operator = \is_null($check_operator) ? '=' : $check_operator;
517 $value_type .= $this->getFieldFormat($value);
518 $all_values[] = $value;
519 }
520 if (!$this->isSafeConditionOperator($set_check_operator)) {
521 return $this->impossibleCondition();
522 }
523 $condition_to_check = $condition_to_check . $key . " $set_check_operator " . $value_type;
524 if ($index_checker < $no_condition - 1) {
525 $condition_to_check = $condition_to_check . " $join_operator ";
526 }
527 $index_checker = $index_checker + 1;
528 }
529 return [
530 'conditions' => $condition_to_check,
531 'values' => $all_values
532 ];
533 }
534
535 /**
536 * @param array $values values about to be bound by $wpdb->prepare()
537 *
538 * @return string|null the offending PHP type, or null when every value is bindable
539 */
540 private function findUnbindableValue(array $values)
541 {
542 foreach ($values as $value) {
543 if (!is_scalar($value) && !is_null($value)) {
544 return \gettype($value);
545 }
546 }
547
548 return null;
549 }
550
551 protected function checkCondition(array $condition)
552 {
553 if (!is_null($condition) && array_keys($condition) === range(0, count($condition) - 1)) {
554 return new WP_Error(
555 'get_condition',
556 'Require ASSOC_ARRAY but found N_ARRAY'
557 );
558 }
559 return true;
560 }
561
562 protected function execute($sql, $values = null)
563 {
564 // Clear the previous call's outcome before running a new query, so a failure can never be
565 // read back by whatever this instance is used for next.
566 $this->db_response = null;
567 if (is_null($values)) {
568 $preparedQuery = $sql;
569 } else {
570 $invalid = $this->findUnbindableValue((array) $values);
571 if (null !== $invalid) {
572 Log::debug_log([
573 'message' => 'Model::execute() received an unbindable condition value',
574 'table' => $this->table_name,
575 'type' => $invalid,
576 'sql' => $sql,
577 ]);
578 $this->db_response = new WP_Error('invalid_query_value', 'Query value must be scalar, ' . $invalid . ' given');
579
580 return $this;
581 }
582 $preparedQuery = $this->app_db->prepare($sql, $values);
583 }
584 // echo " Q S " . $preparedQuery . " Q EE";
585 if (empty($preparedQuery)) {
586 $this->db_response = new WP_Error('null_query', 'prepared query is empty');
587 } else {
588 $this->db_response = false !== stripos($preparedQuery, 'DELETE') ? $this->app_db->query($preparedQuery)
589 : $this->app_db->get_results($preparedQuery, OBJECT_K);
590 }
591 // print_r($this->app_db->last_query);
592 return $this;
593 }
594
595 protected function getResult($db_response = null)
596 {
597 // The caller's own result wins. $db_response is an instance property that only execute()
598 // writes, and models are reused (AdminFormHandler keeps a static FormModel for the whole
599 // request), so letting the property override an explicitly passed result made insert() and
600 // update() report the outcome of some earlier, unrelated query on the same object.
601 // Without this fallback, execute()->getResult() (which passes no argument) never sees the
602 // query it just ran and every read returns 'result_empty'.
603 if (null === $db_response) {
604 $db_response = $this->db_response;
605 }
606
607 if (is_wp_error($db_response)) {
608 return $db_response;
609 }
610 if (!empty($this->app_db->last_error)) {
611 return new WP_Error('db_error', $this->app_db->last_error);
612 }
613 if (!$db_response) {
614 if ($this->app_db->num_rows > 0) {
615 $response = $this->app_db->num_rows;
616 }
617 if (is_wp_error($db_response)) {
618 $response = $db_response;
619 }
620 $response = new WP_Error('result_empty', 'Result is empty');
621 } elseif (is_array($this->app_db->last_result) && !empty($this->app_db->last_result)) {
622 $response = $this->app_db->last_result;
623 } elseif ($this->app_db->insert_id) {
624 $response = $this->app_db->insert_id;
625 } else {
626 $response = $db_response;
627 }
628 $this->app_db->flush();
629 return $response;
630 }
631
632 /**
633 * Get last inserted id
634 *
635 * @return int
636 */
637 public function lastId()
638 {
639 $sql = "SELECT id FROM `{$this->table_name}`
640 ORDER BY id DESC LIMIT 1";
641 $result = $this->execute($sql)->getResult();
642 if (is_wp_error($result)) {
643 return 0;
644 }
645 return $result[0]->id;
646 }
647 }
648