PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
fluent-cart / app / Models / BatchQuery / Batch.php

Batch.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at app/Models/BatchQuery/Batch.php

290 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Models\BatchQuery;
4
5 use FluentCart\Framework\Database\Orm\Model;
6
7 class Batch implements BatchInterface
8 {
9
10 protected $db;
11
12 public function __construct()
13 {
14 global $wpdb;
15 $this->db = $wpdb;
16 }
17
18 public function update(Model $table, array $values, ?string $index = null, bool $raw = false)
19 {
20 $final = [];
21 $ids = [];
22
23 if (!count($values)) {
24 return false;
25 }
26
27 if (!isset($index) || empty($index)) {
28 $index = $table->getKeyName();
29 }
30
31 // The keys of each row become raw SQL column identifiers below. They can
32 // originate from user input (e.g. the products/bulk-update payload), so an
33 // unvalidated key carrying a backtick breaks out of the identifier context
34 // and injects SQL. Reject anything that is not a plain column identifier.
35 $this->assertIdentifier($index);
36
37 $driver = $table->getConnection()->getName();
38 foreach ($values as $key => $val) {
39 $ids[] = $this->db->prepare('%s', $val[$index]);
40
41 if ($table->usesTimestamps()) {
42 $updatedAtColumn = $table->getUpdatedAtColumn();
43
44 if (!isset($val[$updatedAtColumn])) {
45 $val[$updatedAtColumn] = gmdate($table->getDateFormat());
46 }
47 }
48
49 foreach (array_keys($val) as $field) {
50 if ($field !== $index) {
51 $this->assertIdentifier($field);
52 $indexValue = $this->db->prepare('%s', $val[$index]);
53 // If increment / decrement
54 if (gettype($val[$field]) == 'array') {
55
56 $isMathOperator = true;
57 // If array has two values
58 if (!array_key_exists(0, $val[$field]) || !array_key_exists(1, $val[$field])) {
59 $isMathOperator = false;
60
61 }
62
63 if($isMathOperator){
64 // Check first value
65 if (gettype($val[$field][0]) != 'string' || !in_array($val[$field][0], ['+', '-', '*', '/', '%'])) {
66 throw new \TypeError('First value in Increment/Decrement array needs to be a string and a math operator (+, -, *, /, %)');
67 }
68 // Check second value
69 if (!is_numeric($val[$field][1])) {
70 throw new \TypeError('Second value in Increment/Decrement array needs to be numeric');
71 }
72 // Increment / decrement
73 if (Common::disableBacktick($driver)) {
74 $value = $field . $val[$field][0] . $val[$field][1];
75 } else {
76 $value = '`' . $field . '`' . $val[$field][0] . $val[$field][1];
77 }
78 }
79 else{
80 // Array values are serialized to JSON and dropped into the
81 // SQL as a string literal. json_encode() does NOT escape
82 // single quotes, so an array value such as other_info
83 // carrying a quote breaks out of the literal and injects
84 // SQL. prepare('%s', ...) adds the quotes and escapes the
85 // payload (quotes and backslashes) safely.
86 $value = $this->db->prepare('%s', json_encode($val[$field]));
87 }
88
89 } else {
90 // Scalar value. prepare('%s', ...) both quotes and fully
91 // escapes it. Common::mysqlEscape() must NOT be used here:
92 // it treats a JSON-valid scalar string (e.g. the literal
93 // "O'Reilly") specially and returns it decoded but unescaped,
94 // so the apostrophe would break out of the string literal.
95 // $raw is an internal-only path (no caller passes it).
96 if (is_null($val[$field])) {
97 $value = 'NULL';
98 } elseif ($raw) {
99 $value = Common::mysqlEscape($val[$field]);
100 } else {
101 $value = $this->prepareScalar($table, $val[$field]);
102 }
103 }
104
105 if (Common::disableBacktick($driver))
106 $final[$field][] = 'WHEN ' . $index . ' = ' . $indexValue . ' THEN ' . $value . ' ';
107 else
108 $final[$field][] = 'WHEN `' . $index . '` = ' . $indexValue . ' THEN ' . $value . ' ';
109 }
110 }
111 }
112
113 if (Common::disableBacktick($driver)) {
114
115 $cases = '';
116 foreach ($final as $k => $v) {
117 $cases .= '"' . $k . '" = (CASE ' . implode("\n", $v) . "\n"
118 . 'ELSE "' . $k . '" END), ';
119 }
120
121 $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN(" . implode(",", $ids) . ");";
122
123 } else {
124
125 $cases = '';
126 foreach ($final as $k => $v) {
127 $cases .= '`' . $k . '` = (CASE ' . implode("\n", $v) . "\n"
128 . 'ELSE `' . $k . '` END), ';
129 }
130
131 $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . implode(",", $ids) . ");";
132
133 }
134
135 return $this->db->query($query);
136
137 }
138
139 /**
140 * Update multiple rows
141 * @param Model $table
142 * @param array $values
143 * @param string $index
144 * @param string|null $index2
145 * @param bool $raw
146 * @return bool|int
147 *
148 * @desc
149 * Example
150 * $table = 'users';
151 * $value = [
152 * [
153 * 'id' => 1,
154 * 'status' => 'active',
155 * 'nickname' => 'Mohammad'
156 * ] ,
157 * [
158 * 'id' => 5,
159 * 'status' => 'deactive',
160 * 'nickname' => 'Ghanbari'
161 * ] ,
162 * ];
163 * $index = 'id';
164 * $index2 = 'user_id';
165 *
166 */
167 public function updateWithTwoIndex(Model $table, array $values, ?string $index = null, ?string $index2 = null, bool $raw = false)
168 {
169 $final = [];
170 $ids = [];
171 $driver = $table->getConnection()->getName();
172
173 if (!count($values)) {
174 return false;
175 }
176
177 if (!isset($index) || empty($index)) {
178 $index = $table->getKeyName();
179 }
180
181 // Identifiers are interpolated as raw SQL below — reject anything that is
182 // not a plain column name so an attacker-supplied key cannot inject SQL.
183 $this->assertIdentifier($index);
184 $this->assertIdentifier($index2);
185
186 foreach ($values as $key => $val) {
187 $id1 = $this->db->prepare('%s', $val[$index]);
188 $id2 = $this->db->prepare('%s', $val[$index2]);
189 $ids[] = $id1;
190 $ids2[] = $id2;
191 foreach (array_keys($val) as $field) {
192 if ($field !== $index || $field !== $index2) {
193 $this->assertIdentifier($field);
194 // prepare('%s', ...) quotes and escapes; Common::mysqlEscape()
195 // is unsafe for JSON-valid scalar strings. $raw is internal-only.
196 if (is_null($val[$field])) {
197 $value = 'NULL';
198 } elseif ($raw) {
199 $value = Common::mysqlEscape($val[$field]);
200 } else {
201 $value = $this->prepareScalar($table, $val[$field]);
202 }
203
204 if (Common::disableBacktick($driver)) {
205 $final[$field][] = 'WHEN (' . $index . ' = ' . $id1 . ' AND ' . $index2 . ' = ' . $id2 . ') THEN ' . $value . ' ';
206 } else {
207 $final[$field][] = 'WHEN (`' . $index . '` = ' . $id1 . ' AND `' . $index2 . '` = ' . $id2 . ') THEN ' . $value . ' ';
208 }
209 }
210 }
211 }
212
213
214 if (Common::disableBacktick($driver)) {
215 $cases = '';
216 foreach ($final as $k => $v) {
217 $cases .= '"' . $k . '" = (CASE ' . implode("\n", $v) . "\n"
218 . 'ELSE "' . $k . '" END), ';
219 }
220
221 $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN(" . implode(",", $ids) . ") AND \"$index2\" IN(" . implode(",", $ids2) . ");";
222 } else {
223 $cases = '';
224 foreach ($final as $k => $v) {
225 $cases .= '`' . $k . '` = (CASE ' . implode("\n", $v) . "\n"
226 . 'ELSE `' . $k . '` END), ';
227 }
228 $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . implode(",", $ids) . ")" . " AND `$index2` IN(" . implode(",", $ids2) . ");";
229 }
230 return $this->db->query($query);
231 }
232
233 /**
234 * Get the full table name.
235 *
236 * @param Model $model
237 * @return string
238 */
239 private function getFullTableName(Model $model): string
240 {
241 return $this->db->prefix . $model->getTable();
242 }
243
244 /**
245 * Guard a value that is about to be used as a raw SQL column/index identifier.
246 *
247 * Identifiers cannot be bound as parameters, so any key that is interpolated
248 * between backticks must be a plain column name. A value carrying a backtick
249 * (or anything outside [A-Za-z0-9_]) would break out of the identifier and
250 * inject SQL, so we fail closed rather than escape — a legitimate column name
251 * always matches.
252 *
253 * @param string $identifier
254 * @return void
255 * @throws \InvalidArgumentException
256 */
257 private function assertIdentifier($identifier): void
258 {
259 if (!is_string($identifier) || !preg_match('/^[A-Za-z0-9_]+$/', $identifier)) {
260 throw new \InvalidArgumentException('Invalid column identifier in batch update.');
261 }
262 }
263
264 /**
265 * Normalize a non-null scalar value, then bind it via prepare().
266 *
267 * prepare('%s', ...) only accepts scalars: a DateTime or a stringable object
268 * would become an empty string. Callers legitimately pass DateTime objects
269 * (e.g. updated_at, refunded_at) and boolean flags, which the previous
270 * string-concatenation path rendered via __toString()/(int) cast. Reproduce
271 * that normalization before binding so stored values are unchanged.
272 *
273 * @param Model $table
274 * @param mixed $input non-null value
275 * @return string prepared, quoted SQL literal
276 */
277 private function prepareScalar(Model $table, $input): string
278 {
279 if ($input instanceof \DateTimeInterface) {
280 $input = $input->format($table->getDateFormat());
281 } elseif (is_bool($input)) {
282 $input = (int) $input;
283 } elseif (is_object($input) && method_exists($input, '__toString')) {
284 $input = (string) $input;
285 }
286
287 return $this->db->prepare('%s', $input);
288 }
289 }
290