PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / redirections / class-metasync-redirection-database.php

class-metasync-redirection-database.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.25, at redirections/class-metasync-redirection-database.php

402 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The database operations for the redirections.
5 *
6 * @since 1.0.0
7 * @package Metasync
8 * @subpackage Metasync/redirections
9 * @author Engineering Team <[email protected]>
10 */
11 if (!class_exists('Metasync_Redirection_Database')) {
12 class Metasync_Redirection_Database
13 {
14 public static $table_name = "metasync_redirections";
15 private static $structure_verified = false;
16
17 private function get_table_name()
18 {
19 global $wpdb;
20 return $wpdb->prefix . self::$table_name;
21 }
22
23 /**
24 * Ensure table structure is up to date
25 */
26 private function ensure_table_structure()
27 {
28 // Run schema inspection at most once per request — table schema only changes on activation/upgrade, handled by class-db-migrations.php.
29 if (self::$structure_verified) { return; }
30 self::$structure_verified = true;
31
32 global $wpdb;
33 $table_name = $this->get_table_name();
34
35 // Check if table exists
36 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
37 // Table doesn't exist, run full migration
38 require_once dirname(__FILE__, 2) . '/database/class-db-migrations.php';
39 MetaSync_DBMigration::activation();
40 return;
41 }
42
43 // Check if required columns exist
44 $columns = $wpdb->get_col("DESCRIBE {$table_name}");
45
46 $required_columns = [
47 'pattern_type' => "ALTER TABLE {$table_name} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact' AFTER status",
48 'regex_pattern' => "ALTER TABLE {$table_name} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type",
49 'description' => "ALTER TABLE {$table_name} ADD COLUMN description TEXT NULL AFTER regex_pattern",
50 'created_at' => "ALTER TABLE {$table_name} ADD COLUMN created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER description",
51 'updated_at' => "ALTER TABLE {$table_name} ADD COLUMN updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER created_at",
52 'last_accessed_at' => "ALTER TABLE {$table_name} ADD COLUMN last_accessed_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER updated_at"
53 ];
54
55 foreach ($required_columns as $column => $sql) {
56 if (!in_array($column, $columns)) {
57 $wpdb->query($sql);
58 }
59 }
60
61 // Widen pattern_type for installs whose column predates wildcard
62 // matching — column-exists checks never re-ALTER, so the enum would
63 // otherwise keep silently rejecting wildcard rows.
64 if (in_array('pattern_type', $columns)) {
65 $column_info = $wpdb->get_row($wpdb->prepare("SHOW COLUMNS FROM {$table_name} LIKE %s", 'pattern_type'));
66 if ($column_info && strpos((string) $column_info->Type, 'wildcard') === false) {
67 $wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact'");
68 }
69 }
70
71 // Check and add indexes
72 $indexes = $wpdb->get_results("SHOW INDEX FROM {$table_name}");
73 $index_names = array_column($indexes, 'Key_name');
74
75 $required_indexes = [
76 'pattern_type' => "ALTER TABLE {$table_name} ADD KEY pattern_type (pattern_type)",
77 'created_at' => "ALTER TABLE {$table_name} ADD KEY created_at (created_at)",
78 'status_created' => "ALTER TABLE {$table_name} ADD KEY status_created (status, created_at)"
79 ];
80
81 foreach ($required_indexes as $index => $sql) {
82 if (!in_array($index, $index_names)) {
83 $wpdb->query($sql);
84 }
85 }
86
87 // Set default pattern_type for existing records
88 $wpdb->query("UPDATE {$table_name} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
89 }
90
91 /**
92 * Manually trigger table structure update
93 * Can be called from admin or via AJAX if needed
94 */
95 public function force_table_update()
96 {
97 self::$structure_verified = false;
98 $this->ensure_table_structure();
99 return true;
100 }
101
102 public function getAllRecords()
103 {
104 global $wpdb;
105 $tableName = $this->get_table_name();
106 return $wpdb->get_results(" SELECT * FROM `$tableName` ");
107 }
108
109 public function getAllActiveRecords()
110 {
111 global $wpdb;
112 $tableName = $this->get_table_name();
113
114 // Check cache first
115 $cache_key = 'metasync_active_redirections';
116 $cached_redirections = wp_cache_get($cache_key, 'metasync');
117
118 if ($cached_redirections !== false) {
119 return $cached_redirections;
120 }
121
122 // id tiebreaker: created_at has one-second resolution, so rows created in
123 // the same second (bulk import, migrations) would come back in arbitrary
124 // order — and the engine's first-match-wins scan would then be
125 // nondeterministic between requests.
126 $redirections = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$tableName` WHERE status = %s ORDER BY created_at DESC, id DESC", 'active'));
127
128 // Cache for 1 hour
129 wp_cache_set($cache_key, $redirections, 'metasync', HOUR_IN_SECONDS);
130
131 return $redirections;
132 }
133
134 /**
135 * Find a single redirection by ID
136 * @param int $id The redirection ID
137 * @return object|null The redirection record or null if not found
138 */
139 public function find($id)
140 {
141 global $wpdb;
142 $tableName = $this->get_table_name();
143 return $wpdb->get_row($wpdb->prepare("SELECT * FROM `$tableName` WHERE id = %d", intval($id)));
144 }
145
146 /**
147 * Add a record.
148 * @param array $args Values to insert.
149 */
150 public function add($args)
151 {
152 global $wpdb;
153
154 // Ensure table structure is up to date
155 $this->ensure_table_structure();
156
157 $args = wp_parse_args(
158 $args,
159 [
160 'sources_from' => [],
161 'url_redirect_to' => site_url(),
162 'http_code' => 301,
163 'hits_count' => 0,
164 'status' => 'active',
165 'pattern_type' => 'exact',
166 'regex_pattern' => null,
167 'description' => '',
168 'created_at' => current_time('mysql'),
169 'updated_at' => current_time('mysql'),
170 ]
171 );
172
173 // Serialize sources_from if array (rest of codebase uses serialized format)
174 if ( is_array( $args['sources_from'] ) ) {
175 $sources = $args['sources_from'];
176 $args['sources_from'] = serialize( array_combine( array_values( $sources ), array_fill( 0, count( $sources ), 'exact' ) ) ?: [] );
177 }
178
179 $result = $wpdb->insert( $this->get_table_name(), $args );
180
181 // Clear cache after adding
182 $this->clear_cache();
183
184 return $result !== false ? (int) $wpdb->insert_id : false;
185 }
186
187 /**
188 * Update a record.
189 * @param array $args Values to update.
190 * @param string $id
191 */
192 public function update($args, $id)
193 {
194 global $wpdb;
195
196 // Ensure table structure is up to date
197 $this->ensure_table_structure();
198
199 $tableName = $this->get_table_name();
200 $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM `$tableName` WHERE `id` = %s ", $id));
201 if (!$row) return false;
202
203 $args['updated_at'] = current_time('mysql');
204 $result = $wpdb->update($tableName, $args, ['id' => $id]);
205
206 // Clear cache after updating
207 $this->clear_cache();
208
209 // Row count on success (0 = no-op write), false on query failure —
210 // callers checking `=== false` could never fire against void.
211 return $result;
212 }
213
214 /**
215 * Get total number of rows in the DB table).
216 */
217 public function get_count()
218 {
219 global $wpdb;
220 $tableName = $this->get_table_name();
221 return (int) $wpdb->get_var("SELECT COUNT(*) FROM `$tableName`");
222 }
223
224 /**
225 * Delete a redirection record.
226 */
227 public function delete($items)
228 {
229 global $wpdb;
230 $tableName = $this->get_table_name();
231 if (!is_array($items) || empty($items)) return;
232 $ids = implode(',', array_fill(0, count($items), '%d'));
233 $wpdb->query($wpdb->prepare(
234 "
235 DELETE FROM `$tableName`
236 WHERE `id` IN ($ids) ",
237 $items
238 ));
239
240 // Clear cache after deleting
241 $this->clear_cache();
242 }
243
244 /**
245 * activate a redirection record.
246 */
247 public function update_status($items, $status)
248 {
249 global $wpdb;
250 $tableName = $this->get_table_name();
251 if (!is_array($items) || empty($items)) return;
252 $ids = implode(', ', array_fill(0, count($items), '%d'));
253 $set_status = $wpdb->prepare(
254 "
255 UPDATE `$tableName`
256 SET `status` = %s, `updated_at` = %s",
257 $status,
258 current_time('mysql')
259 );
260 $where = $wpdb->prepare(
261 "
262 WHERE `id` IN ( $ids )",
263 $items
264 );
265 $query = "{$set_status}{$where}";
266 $wpdb->query($query);
267
268 // Clear cache after updating status
269 $this->clear_cache();
270 }
271
272 /**
273 * Update if URL is matched and hit.
274 * @param object $row Record to update.
275 */
276 public function update_counter($row)
277 {
278 global $wpdb;
279 // Atomic increment. The $row object comes from getAllActiveRecords(),
280 // which can be an hour-stale cache hit, and concurrent requests that
281 // both read the same hits_count would lose every hit but one. Bump in
282 // SQL so the stored counter is correct regardless of what $row saw.
283 $wpdb->query(
284 $wpdb->prepare(
285 "UPDATE `" . $this->get_table_name() . "` SET `hits_count` = `hits_count` + 1, `last_accessed_at` = %s WHERE `id` = %d",
286 current_time('mysql'),
287 absint($row->id)
288 )
289 );
290 }
291
292 /**
293 * Clear redirection cache
294 */
295 public function clear_cache()
296 {
297 wp_cache_delete('metasync_active_redirections', 'metasync');
298 }
299
300 /**
301 * Search redirections with filters
302 * @param array $filters Search filters
303 */
304 public function search_redirections($filters = [])
305 {
306 global $wpdb;
307 $tableName = $this->get_table_name();
308
309 $where_conditions = ['1=1'];
310 $where_values = [];
311
312 if (!empty($filters['search'])) {
313 $where_conditions[] = "(sources_from LIKE %s OR url_redirect_to LIKE %s OR description LIKE %s)";
314 $search_term = '%' . $wpdb->esc_like($filters['search']) . '%';
315 $where_values[] = $search_term;
316 $where_values[] = $search_term;
317 $where_values[] = $search_term;
318 }
319
320 if (!empty($filters['status'])) {
321 $where_conditions[] = "status = %s";
322 $where_values[] = $filters['status'];
323 }
324
325 if (!empty($filters['pattern_type'])) {
326 $where_conditions[] = "pattern_type = %s";
327 $where_values[] = $filters['pattern_type'];
328 }
329
330 if (!empty($filters['http_code'])) {
331 $where_conditions[] = "http_code = %d";
332 $where_values[] = intval($filters['http_code']);
333 }
334
335 $where_clause = implode(' AND ', $where_conditions);
336 $order_by = !empty($filters['order_by']) ? sanitize_sql_orderby($filters['order_by']) : 'created_at';
337 $order = !empty($filters['order']) && strtoupper($filters['order']) === 'ASC' ? 'ASC' : 'DESC';
338
339 // Add pagination support
340 $limit_clause = '';
341 if (isset($filters['per_page']) && isset($filters['offset'])) {
342 $limit_clause = " LIMIT %d OFFSET %d";
343 $where_values[] = intval($filters['per_page']);
344 $where_values[] = intval($filters['offset']);
345 }
346
347 $query = "SELECT * FROM `$tableName` WHERE $where_clause ORDER BY $order_by $order" . $limit_clause;
348
349 if (!empty($where_values)) {
350 return $wpdb->get_results($wpdb->prepare($query, $where_values));
351 } else {
352 return $wpdb->get_results($query);
353 }
354 }
355
356 /**
357 * Count total redirections with filters (for pagination)
358 * @param array $filters Search filters
359 */
360 public function count_redirections($filters = [])
361 {
362 global $wpdb;
363 $tableName = $this->get_table_name();
364
365 $where_conditions = ['1=1'];
366 $where_values = [];
367
368 if (!empty($filters['search'])) {
369 $where_conditions[] = "(sources_from LIKE %s OR url_redirect_to LIKE %s OR description LIKE %s)";
370 $search_term = '%' . $wpdb->esc_like($filters['search']) . '%';
371 $where_values[] = $search_term;
372 $where_values[] = $search_term;
373 $where_values[] = $search_term;
374 }
375
376 if (!empty($filters['status'])) {
377 $where_conditions[] = "status = %s";
378 $where_values[] = $filters['status'];
379 }
380
381 if (!empty($filters['pattern_type'])) {
382 $where_conditions[] = "pattern_type = %s";
383 $where_values[] = $filters['pattern_type'];
384 }
385
386 if (!empty($filters['http_code'])) {
387 $where_conditions[] = "http_code = %d";
388 $where_values[] = intval($filters['http_code']);
389 }
390
391 $where_clause = implode(' AND ', $where_conditions);
392 $query = "SELECT COUNT(*) FROM `$tableName` WHERE $where_clause";
393
394 if (!empty($where_values)) {
395 return $wpdb->get_var($wpdb->prepare($query, $where_values));
396 } else {
397 return $wpdb->get_var($query);
398 }
399 }
400 }
401 }
402