1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace GeminiLabs\SiteReviews; |
4
|
|
|
|
5
|
|
|
use GeminiLabs\SiteReviews\Database\Query; |
6
|
|
|
use GeminiLabs\SiteReviews\Database\Search\SearchAssignedPosts; |
7
|
|
|
use GeminiLabs\SiteReviews\Database\Search\SearchAssignedUsers; |
8
|
|
|
use GeminiLabs\SiteReviews\Database\Search\SearchPosts; |
9
|
|
|
use GeminiLabs\SiteReviews\Database\Search\SearchUsers; |
10
|
|
|
use GeminiLabs\SiteReviews\Database\Tables; |
11
|
|
|
use GeminiLabs\SiteReviews\Helpers\Arr; |
12
|
|
|
use GeminiLabs\SiteReviews\Helpers\Cast; |
13
|
|
|
use GeminiLabs\SiteReviews\Helpers\Str; |
14
|
|
|
use GeminiLabs\SiteReviews\Modules\Sanitizer; |
15
|
|
|
|
16
|
|
|
class Database |
17
|
|
|
{ |
18
|
|
|
use Deprecated; |
19
|
|
|
|
20
|
|
|
protected \wpdb $db; |
21
|
|
|
|
22
|
48 |
|
public function __construct() |
23
|
|
|
{ |
24
|
48 |
|
global $wpdb; |
25
|
48 |
|
$this->db = $wpdb; |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Use this before bulk insert (see: $this->finishTransaction()). |
30
|
|
|
*/ |
31
|
44 |
|
public function beginTransaction(string $table): void |
32
|
|
|
{ |
33
|
44 |
|
if (glsr(Tables::class)->isSqlite()) { |
34
|
|
|
$this->dbQuery("BEGIN TRANSACTION;"); |
35
|
44 |
|
} elseif (glsr(Tables::class)->isInnodb($table)) { |
36
|
|
|
$this->dbQuery("START TRANSACTION;"); |
37
|
44 |
|
} elseif (glsr(Tables::class)->isMyisam($table)) { |
38
|
|
|
$this->dbQuery("SET autocommit = 0;"); |
39
|
|
|
} |
40
|
|
|
} |
41
|
|
|
|
42
|
44 |
|
public function dbGetCol(string $sql): array |
43
|
|
|
{ |
44
|
44 |
|
return $this->logErrors($this->db->get_col($sql)); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Query result in format specified by $output or null on failure. |
49
|
|
|
* |
50
|
|
|
* @return array|object|null |
51
|
|
|
*/ |
52
|
44 |
|
public function dbGetResults(string $sql, string $output = 'OBJECT') |
53
|
|
|
{ |
54
|
44 |
|
$output = Str::restrictTo(['ARRAY_A', 'ARRAY_N', 'OBJECT', 'OBJECT_K'], $output, OBJECT); |
55
|
44 |
|
return $this->logErrors($this->db->get_results($sql, $output)); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Query result in format specified by $output or null on failure. |
60
|
|
|
* |
61
|
|
|
* @return array|object|void|null |
62
|
|
|
*/ |
63
|
24 |
|
public function dbGetRow(string $sql, string $output) |
64
|
|
|
{ |
65
|
24 |
|
$output = Str::restrictTo(['ARRAY_A', 'ARRAY_N', 'OBJECT'], $output, OBJECT); |
66
|
24 |
|
return $this->logErrors($this->db->get_row($sql, $output)); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Query result as string, or null on failure. |
71
|
|
|
*/ |
72
|
22 |
|
public function dbGetVar(string $sql): ?string |
73
|
|
|
{ |
74
|
22 |
|
return $this->logErrors($this->db->get_var($sql)); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* True for CREATE, ALTER, TRUNCATE and DROP queries. |
79
|
|
|
* Number of rows affected/selected for all other queries. |
80
|
|
|
* False on error. |
81
|
|
|
* |
82
|
|
|
* @return int|bool |
83
|
|
|
*/ |
84
|
44 |
|
public function dbQuery(string $sql) |
85
|
|
|
{ |
86
|
44 |
|
return $this->logErrors($this->db->query($sql)); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* True for CREATE, ALTER, TRUNCATE and DROP queries. |
91
|
|
|
* Number of rows affected/selected for all other queries. |
92
|
|
|
* False on error. |
93
|
|
|
* |
94
|
|
|
* @return int|bool |
95
|
|
|
*/ |
96
|
44 |
|
public function dbSafeQuery(string $sql) |
97
|
|
|
{ |
98
|
44 |
|
if (glsr(Tables::class)->isSqlite()) { |
99
|
|
|
return $this->dbQuery($sql); |
100
|
|
|
} |
101
|
44 |
|
$this->db->query('SET foreign_key_checks = 0'); |
102
|
44 |
|
$result = $this->logErrors($this->db->query($sql)); |
103
|
44 |
|
$this->db->query('SET foreign_key_checks = 1'); |
104
|
44 |
|
return $result; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Number of rows deleted. False on error. |
109
|
|
|
* |
110
|
|
|
* @return int|false |
111
|
|
|
*/ |
112
|
3 |
|
public function delete(string $table, array $where) |
113
|
|
|
{ |
114
|
3 |
|
$result = $this->db->delete(glsr(Tables::class)->table($table), $where); |
115
|
3 |
|
glsr(Query::class)->sql($this->db->last_query); // for logging use only |
116
|
3 |
|
return $this->logErrors($result); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Number of rows deleted. False on error. |
121
|
|
|
* |
122
|
|
|
* @param string|string[] $keys |
123
|
|
|
* |
124
|
|
|
* @return int|false |
125
|
|
|
*/ |
126
|
44 |
|
public function deleteMeta($keys, string $table = 'postmeta') |
127
|
|
|
{ |
128
|
44 |
|
$table = glsr(Tables::class)->table($table); |
129
|
44 |
|
$metaKeys = glsr(Query::class)->escValuesForInsert(Arr::convertFromString($keys)); |
130
|
44 |
|
$sql = glsr(Query::class)->sql(" |
131
|
44 |
|
DELETE FROM {$table} WHERE meta_key IN {$metaKeys} |
132
|
44 |
|
"); |
133
|
44 |
|
return $this->dbQuery($sql); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Use this after bulk insert (see: $this->beginTransaction()). |
138
|
|
|
*/ |
139
|
44 |
|
public function finishTransaction(string $table): void |
140
|
|
|
{ |
141
|
44 |
|
if (glsr(Tables::class)->isSqlite()) { |
142
|
|
|
$this->dbQuery("COMMIT;"); |
143
|
44 |
|
} elseif (glsr(Tables::class)->isInnodb($table)) { |
144
|
|
|
$this->dbQuery("COMMIT;"); |
145
|
44 |
|
} elseif (glsr(Tables::class)->isMyisam($table)) { |
146
|
|
|
$this->dbQuery("SET autocommit = 1;"); |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Number of rows inserted. False on error. |
152
|
|
|
* |
153
|
|
|
* @return int|false |
154
|
|
|
*/ |
155
|
24 |
|
public function insert(string $table, array $data) |
156
|
|
|
{ |
157
|
24 |
|
$this->db->insert_id = 0; |
158
|
24 |
|
$table = glsr(Tables::class)->table($table); |
159
|
24 |
|
$fields = glsr(Query::class)->escFieldsForInsert(array_keys($data)); |
160
|
24 |
|
$values = glsr(Query::class)->escValuesForInsert($data); |
161
|
24 |
|
if (glsr(Tables::class)->isSqlite()) { |
162
|
|
|
$sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}"); |
163
|
|
|
} else { |
164
|
24 |
|
$sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}"); |
165
|
|
|
} |
166
|
24 |
|
$result = $this->dbQuery($sql); |
167
|
24 |
|
return $result; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Number of rows inserted. False on error. |
172
|
|
|
* |
173
|
|
|
* @return int|false |
174
|
|
|
*/ |
175
|
|
|
public function insertBulk(string $table, array $values, array $fields) |
176
|
|
|
{ |
177
|
|
|
$this->db->insert_id = 0; |
178
|
|
|
$data = []; |
179
|
|
|
foreach ($values as $value) { |
180
|
|
|
$value = array_intersect_key($value, array_flip($fields)); // only keep field values |
181
|
|
|
if (count($value) === count($fields)) { |
182
|
|
|
$value = array_merge(array_flip($fields), $value); // make sure the order is correct |
183
|
|
|
$data[] = glsr(Query::class)->escValuesForInsert($value); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
$table = glsr(Tables::class)->table($table); |
187
|
|
|
$fields = glsr(Query::class)->escFieldsForInsert($fields); |
188
|
|
|
$values = implode(',', $data); |
189
|
|
|
if (glsr(Tables::class)->isSqlite()) { |
190
|
|
|
$sql = glsr(Query::class)->sql("INSERT OR IGNORE INTO {$table} {$fields} VALUES {$values}"); |
191
|
|
|
} else { |
192
|
|
|
$sql = glsr(Query::class)->sql("INSERT IGNORE INTO {$table} {$fields} VALUES {$values}"); |
193
|
|
|
} |
194
|
|
|
return $this->dbQuery($sql); |
195
|
|
|
} |
196
|
|
|
|
197
|
8 |
|
public function isMigrationNeeded(): bool |
198
|
|
|
{ |
199
|
8 |
|
$postTypes = wp_count_posts(glsr()->post_type); |
200
|
8 |
|
$postCount = Arr::get($postTypes, 'publish'); |
201
|
8 |
|
if (empty($postCount)) { |
202
|
8 |
|
return false; |
203
|
|
|
} |
204
|
|
|
$sql = glsr(Query::class)->sql("SELECT COUNT(*) FROM table|ratings WHERE is_approved = 1"); |
205
|
|
|
return empty($this->dbGetVar($sql)); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* @param mixed $result |
210
|
|
|
* |
211
|
|
|
* @return mixed |
212
|
|
|
*/ |
213
|
47 |
|
public function logErrors($result = null) |
214
|
|
|
{ |
215
|
47 |
|
if ($this->db->last_error) { |
216
|
|
|
glsr_log()->error($this->db->last_error); |
217
|
|
|
glsr_trace(); |
218
|
|
|
} |
219
|
47 |
|
return $result; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* An array of values if $single is false. |
224
|
|
|
* The value of the meta field if $single is true. |
225
|
|
|
* False for an invalid $post_id (non-numeric, zero, or negative value). |
226
|
|
|
* An empty string if a valid but non-existing post ID is passed. |
227
|
|
|
* |
228
|
|
|
* @return mixed |
229
|
|
|
*/ |
230
|
|
|
public function meta(int $postId, string $key, bool $single = true) |
231
|
|
|
{ |
232
|
|
|
$key = Str::prefix($key, '_'); |
233
|
|
|
$postId = Cast::toInt($postId); |
234
|
|
|
return get_post_meta($postId, $key, $single); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* The new meta field ID if a field with the given key didn't exist and was therefore added. |
239
|
|
|
* True on successful update. False on failure or if the value passed to the function |
240
|
|
|
* is the same as the one that is already in the database. |
241
|
|
|
* |
242
|
|
|
* @param mixed $value |
243
|
|
|
* |
244
|
|
|
* @return int|bool |
245
|
|
|
*/ |
246
|
|
|
public function metaSet(int $postId, string $key, $value) |
247
|
|
|
{ |
248
|
|
|
$key = Str::prefix($key, '_'); |
249
|
|
|
$postId = Cast::toInt($postId); |
250
|
|
|
return update_metadata('post', $postId, $key, $value); // update_metadata works with revisions |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
public function posts(array $args = []): array |
254
|
|
|
{ |
255
|
|
|
$postTypes = get_post_types([ |
256
|
|
|
'_builtin' => false, |
257
|
|
|
'exclude_from_search' => true, |
258
|
|
|
'public' => true, |
259
|
|
|
'show_in_rest' => true, |
260
|
|
|
'show_ui' => true, |
261
|
|
|
]); |
262
|
|
|
$postTypes[] = 'post'; |
263
|
|
|
$postTypes[] = 'page'; |
264
|
|
|
$args = wp_parse_args($args, [ |
265
|
|
|
'ignore_sticky_posts' => true, |
266
|
|
|
'numberposts' => 50, |
267
|
|
|
'order' => 'ASC', |
268
|
|
|
'orderby' => 'title', |
269
|
|
|
'post_type' => array_values($postTypes), |
270
|
|
|
]); |
271
|
|
|
$posts = get_posts($args); |
272
|
|
|
$results = []; |
273
|
8 |
|
foreach ($posts as $post) { |
274
|
|
|
$title = sanitize_text_field($post->post_title); |
275
|
8 |
|
$results[$post->ID] = trim("{$title} ({$post->ID})"); |
276
|
8 |
|
} |
277
|
8 |
|
return $results; |
278
|
8 |
|
} |
279
|
8 |
|
|
280
|
8 |
|
public function searchAssignedPosts(string $searchTerm): SearchAssignedPosts |
281
|
8 |
|
{ |
282
|
8 |
|
return glsr(SearchAssignedPosts::class)->search($searchTerm); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
public function searchAssignedUsers(string $searchTerm): SearchAssignedUsers |
286
|
|
|
{ |
287
|
8 |
|
return glsr(SearchAssignedUsers::class)->search($searchTerm); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
public function searchPosts(string $searchTerm): SearchPosts |
291
|
|
|
{ |
292
|
|
|
return glsr(SearchPosts::class)->search($searchTerm); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
public function searchUsers(string $searchTerm): SearchUsers |
296
|
|
|
{ |
297
|
|
|
return glsr(SearchUsers::class)->search($searchTerm); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
public function terms(array $args = []): array |
301
|
|
|
{ |
302
|
|
|
$args = wp_parse_args($args, [ |
303
|
|
|
'count' => false, |
304
|
|
|
'fields' => 'id=>name', |
305
|
|
|
'hide_empty' => false, |
306
|
|
|
'taxonomy' => glsr()->taxonomy, |
307
|
|
|
]); |
308
|
|
|
$terms = get_terms($args); |
309
|
|
|
if (is_wp_error($terms)) { |
310
|
|
|
glsr_log()->error($terms->get_error_message()); |
311
|
|
|
glsr_trace(); |
312
|
|
|
return []; |
313
|
|
|
} |
314
|
|
|
return $terms; |
315
|
|
|
} |
316
|
|
|
|
317
|
46 |
|
/** |
318
|
|
|
* Number of rows updated. False on error. |
319
|
46 |
|
* |
320
|
46 |
|
* @return int|false |
321
|
|
|
*/ |
322
|
|
|
public function update(string $table, array $data, array $where) |
323
|
|
|
{ |
324
|
46 |
|
$result = $this->db->update(glsr(Tables::class)->table($table), $data, $where); |
325
|
|
|
glsr(Query::class)->sql($this->db->last_query); // for logging use only |
326
|
|
|
return $this->logErrors($result); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
public function users(array $args = []): array |
330
|
|
|
{ |
331
|
|
|
$args = wp_parse_args($args, [ |
332
|
|
|
'fields' => ['ID', 'display_name', 'user_nicename'], |
333
|
|
|
'number' => 50, // only get the first 50 users! |
334
|
|
|
'orderby' => 'display_name', |
335
|
|
|
]); |
336
|
|
|
$results = []; |
337
|
|
|
$users = get_users($args); |
338
|
|
|
foreach ($users as $user) { |
339
|
|
|
$name = glsr(Sanitizer::class)->sanitizeUserName( |
340
|
|
|
$user->display_name, |
341
|
|
|
$user->user_nicename |
342
|
|
|
); |
343
|
|
|
$results[$user->ID] = $name; |
344
|
|
|
} |
345
|
|
|
return $results; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
public function version(): string |
349
|
|
|
{ |
350
|
|
|
$dbVersion = Cast::toString(get_option(glsr()->prefix.'db_version')); |
351
|
|
|
if (version_compare($dbVersion, Application::DB_VERSION, '>')) { // version should never be higher than plugin database version |
352
|
|
|
update_option(glsr()->prefix.'db_version', '1.0'); // setting it to a low version will trigger the plugin migration |
353
|
|
|
$dbVersion = '1.0'; |
354
|
|
|
} |
355
|
|
|
return $dbVersion; |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|