PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.18
ووسلام – همگام سازی ووکامرس و باسلام v1.10.18
1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 1.8.7 1.8.4 All 51 releases
sync-basalam / includes / Migrations / MigratorService.php

MigratorService.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.18, at includes/Migrations/MigratorService.php

482 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Migrations;
4
5 use SyncBasalam\Admin\Settings\SettingsConfig;
6 use SyncBasalam\Services\Products\ProductConnection;
7 use SyncBasalam\Services\WebhookService;
8 use SyncBasalam\Utilities\ProductMetaKey;
9
10 defined('ABSPATH') || exit;
11 class MigratorService
12 {
13 private $wpdb;
14
15 public function __construct()
16 {
17 global $wpdb;
18 $this->wpdb = $wpdb;
19 }
20
21 public function migratePayments()
22 {
23 $source = $this->wpdb->prefix . 'bsl_payments';
24 $target = $this->wpdb->prefix . 'sync_basalam_payments';
25
26 if ($this->tableExists($source) && $this->tableExists($target)) {
27 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
28 $this->wpdb->query("INSERT INTO $target (payment_id, invoice_id, user_id, city_id, province_id, order_id)
29 SELECT payment_id, invoice_id, user_id, city_id, province_id, order_id FROM $source");
30 }
31 }
32
33 public function migrateOptions()
34 {
35 $source = $this->wpdb->prefix . 'bsl_map_options';
36 $target = $this->wpdb->prefix . 'sync_basalam_map_options';
37
38 if ($this->tableExists($source) && $this->tableExists($target)) {
39 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
40 $this->wpdb->query("INSERT INTO $target (woo_name, sync_basalam_name)
41 SELECT woo_name, bslm_name FROM $source");
42 }
43 }
44
45 public function migrateUploadedPhotos()
46 {
47 $source = $this->wpdb->prefix . 'bsl_uploaded_photo';
48 $target = $this->wpdb->prefix . 'sync_basalam_uploaded_media';
49
50 if ($this->tableExists($source) && $this->tableExists($target)) {
51 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
52 $this->wpdb->query(
53 "INSERT IGNORE INTO {$target} (type, source_identity, media_id, media_url, created_at)
54 SELECT 'photo', CONCAT('attachment:', woo_photo_id), bslm_photo_id, bslm_photo_url, NOW()
55 FROM {$source}
56 WHERE woo_photo_id IS NOT NULL"
57 );
58 }
59 }
60
61 public function migratePostMeta()
62 {
63 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
64 $this->wpdb->query(
65 "UPDATE {$this->wpdb->postmeta}
66 SET meta_key = REPLACE(meta_key, 'bslm_', 'sync_basalam_')
67 WHERE meta_key LIKE '%bslm_%'"
68 );
69
70 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
71 $this->wpdb->query(
72 "UPDATE {$this->wpdb->postmeta}
73 SET meta_key = REPLACE(meta_key, 'bsl_', 'sync_basalam_')
74 WHERE meta_key LIKE '%bsl_%'"
75 );
76 }
77
78 public function migrateOptionsRows()
79 {
80 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
81 $this->wpdb->query(
82 "UPDATE {$this->wpdb->options}
83 SET option_name = REPLACE(option_name, 'bslm_', 'sync_basalam_')
84 WHERE option_name LIKE '%bslm_%'"
85 );
86
87 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix / core $wpdb properties, not user input.
88 $this->wpdb->query(
89 "UPDATE {$this->wpdb->options}
90 SET option_name = REPLACE(option_name, 'bsl_', 'sync_basalam_')
91 WHERE option_name LIKE '%bsl_%'"
92 );
93 }
94
95 public function migrateActions()
96 {
97 $source = $this->wpdb->prefix . 'actionscheduler_actions';
98
99 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input; values are prepared.
100 $this->wpdb->query(
101 $this->wpdb->prepare(
102 "UPDATE $source
103 SET hook = REPLACE(hook, 'bslm_', 'sync_basalam_')
104 WHERE hook LIKE %s AND status = %s",
105 '%bslm_%',
106 'pending'
107 )
108 );
109
110 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input; values are prepared.
111 $this->wpdb->query(
112 $this->wpdb->prepare(
113 "UPDATE $source
114 SET hook = REPLACE(hook, 'bsl_', 'sync_basalam_')
115 WHERE hook LIKE %s AND status = %s",
116 '%bsl_%',
117 'pending'
118 )
119 );
120 }
121
122 public function migrateSettings()
123 {
124 $settingsRowName = 'sync_basalam_settings';
125
126 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from core $wpdb property, not user input; value is prepared.
127 $settingsSerialized = $this->wpdb->get_var(
128 $this->wpdb->prepare(
129 "SELECT option_value FROM {$this->wpdb->options} WHERE option_name = %s",
130 $settingsRowName
131 )
132 );
133
134 if (!$settingsSerialized) return;
135
136 $settings = maybe_unserialize($settingsSerialized);
137
138 if (!is_array($settings)) return;
139
140 $keyMap = [
141 'basalam_client_id' => 'client_id',
142 'basalam_webhook_id' => 'webhook_id',
143 'basalam_token' => 'token',
144 'basalam_refresh_token' => 'refresh_token',
145 'basalam_vendor_id' => 'vendor_id',
146 'basalam_is_vendor' => 'is_vendor',
147 'basalam_increase' => 'price_change_value',
148 'basalam_round' => 'round_price',
149 'basalam_webhook_token' => 'webhook_header_token',
150 'basalam_auto_confirm_order' => 'auto_confirm_order',
151 'basalam_product_wholesale' => 'all_products_wholesale',
152 ];
153
154 $newSettings = [];
155 foreach ($settings as $key => $value) {
156 $newSettings[$keyMap[$key] ?? $key] = $value;
157 }
158
159 update_option($settingsRowName, $newSettings);
160 }
161
162 public function renameOldTables()
163 {
164 $tables = [
165 $this->wpdb->prefix . 'bsl_payments',
166 $this->wpdb->prefix . 'bsl_map_options',
167 $this->wpdb->prefix . 'bsl_uploaded_photo',
168 ];
169
170 foreach ($tables as $table) {
171 if ($this->tableExists($table)) {
172 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
173 $this->wpdb->query("RENAME TABLE $table TO {$table}_old");
174 }
175 }
176 }
177
178 private function tableExists($table)
179 {
180
181 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Legacy-data migration; no object cache for these one-time operational queries.
182 return $this->wpdb->get_var($this->wpdb->prepare("SHOW TABLES LIKE %s", $table)) === $table;
183 }
184
185 private function columnExists($table, $column)
186 {
187
188 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input; value is prepared.
189 $result = $this->wpdb->get_results($this->wpdb->prepare(
190 "SHOW COLUMNS FROM `$table` LIKE %s",
191 $column
192 ));
193
194 return !empty($result);
195 }
196
197 public function addCreatedAtColumnToUploadedPhoto()
198 {
199
200 $table = $this->wpdb->prefix . 'sync_basalam_uploaded_photo';
201
202 if ($this->tableExists($table) && !$this->columnExists($table, 'created_at')) {
203 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
204 $this->wpdb->query("ALTER TABLE $table ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP");
205 }
206 }
207
208 public function addNewWebhookEvents()
209 {
210 $webhookService = new WebhookService();
211 $webhookService->setupWebhook();
212 }
213
214 public function addFinancialDataColumns()
215 {
216 $table = $this->wpdb->prefix . 'sync_basalam_payments';
217
218 if (!$this->tableExists($table)) {
219 return;
220 }
221
222 $columns = [
223 'fee_amount' => "ALTER TABLE $table ADD COLUMN fee_amount DECIMAL(10,2) DEFAULT 0",
224 'balance_amount' => "ALTER TABLE $table ADD COLUMN balance_amount DECIMAL(10,2) DEFAULT 0",
225 'purchase_count' => "ALTER TABLE $table ADD COLUMN purchase_count INT DEFAULT 0"
226 ];
227
228 foreach ($columns as $columnName => $sql) {
229 if (!$this->columnExists($table, $columnName)) {
230 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; ALTER statement built from static SQL with a $wpdb->prefix table identifier, not user input.
231 $this->wpdb->query($sql);
232 }
233 }
234 }
235
236 public function addUniqueInvoiceIdIndex()
237 {
238 $table = $this->wpdb->prefix . 'sync_basalam_payments';
239
240 if (!$this->tableExists($table)) {
241 return;
242 }
243
244 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Legacy-data migration; no object cache for these one-time operational queries; values are prepared.
245 $existingIndex = $this->wpdb->get_var(
246 $this->wpdb->prepare(
247 "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS
248 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = %s LIMIT 1",
249 $table,
250 'unique_invoice_id'
251 )
252 );
253
254 if ($existingIndex) {
255 return;
256 }
257
258 $this->removeDuplicateInvoicePayments($table);
259
260 $this->wpdb->hide_errors();
261 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
262 $this->wpdb->query("ALTER TABLE {$table} ADD UNIQUE KEY unique_invoice_id (invoice_id)");
263 $this->wpdb->show_errors();
264 }
265
266 private function removeDuplicateInvoicePayments($table)
267 {
268 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input.
269 $duplicates = $this->wpdb->get_results(
270 "SELECT invoice_id, MIN(id) AS keep_id
271 FROM {$table}
272 WHERE invoice_id IS NOT NULL AND invoice_id > 0
273 GROUP BY invoice_id
274 HAVING COUNT(*) > 1"
275 );
276
277 if (empty($duplicates)) {
278 return;
279 }
280
281 foreach ($duplicates as $duplicate) {
282 $invoiceId = (int) $duplicate->invoice_id;
283 $keepId = (int) $duplicate->keep_id;
284
285 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input; values are prepared.
286 $rowsToDelete = $this->wpdb->get_results(
287 $this->wpdb->prepare(
288 "SELECT id, order_id FROM {$table} WHERE invoice_id = %d AND id <> %d",
289 $invoiceId,
290 $keepId
291 )
292 );
293
294 foreach ($rowsToDelete as $row) {
295 $orderId = (int) ($row->order_id ?? 0);
296 if ($orderId > 0 && function_exists('wc_get_order')) {
297 $order = wc_get_order($orderId);
298 if ($order instanceof \WC_Order) {
299 try {
300 $order->delete(true);
301 } catch (\Throwable $e) {
302 \SyncBasalam\Logger\Logger::error(
303 "حذف سفارش تکراری ووکا�
304 رس با شناسه {$orderId} نا�
305 وفق بود: " . $e->getMessage()
306 );
307 }
308 }
309 }
310
311 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Legacy-data migration; no object cache for these one-time operational queries.
312 $this->wpdb->delete($table, ['id' => (int) $row->id], ['%d']);
313 }
314
315 \SyncBasalam\Logger\Logger::debug(
316 "سفارشات تکراری برای invoice_id {$invoiceId} پاک‌سازی شدند، شناسه نگه‌داشته‌شده: {$keepId}"
317 );
318 }
319 }
320
321 public function addRetryAfterColumnToJobManager()
322 {
323 $tableName = $this->wpdb->prefix . 'sync_basalam_job_manager';
324
325 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from $wpdb->prefix, not user input; value is prepared.
326 $columnExists = $this->wpdb->get_var(
327 $this->wpdb->prepare("SHOW COLUMNS FROM `{$tableName}` LIKE %s", 'retry_after')
328 );
329
330 if (!$columnExists) {
331 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
332 $this->wpdb->query("ALTER TABLE {$tableName} ADD COLUMN retry_after BIGINT(20) NULL DEFAULT NULL");
333 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
334 $this->wpdb->query("ALTER TABLE {$tableName} ADD INDEX idx_retry_after (retry_after)");
335 }
336 }
337
338 public function migrateProductMetaKeysToVendorId()
339 {
340 $settings = get_option('sync_basalam_settings', []);
341 if (!is_array($settings)) return;
342
343 $vendorId = $settings[SettingsConfig::VENDOR_ID] ?? null;
344 $vendorId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $vendorId);
345
346 if ($vendorId === '') return;
347
348 $postmetaTable = $this->wpdb->postmeta;
349
350 $metaKeyMap = [
351 ProductMetaKey::PRODUCT_ID => ProductMetaKey::basalamProductId($vendorId),
352 ProductMetaKey::PRODUCT_SYNC_STATUS => ProductMetaKey::basalamProductSyncStatus($vendorId),
353 ProductMetaKey::PRODUCT_STATUS => ProductMetaKey::basalamProductStatus($vendorId),
354 ];
355
356 foreach ($metaKeyMap as $oldMetaKey => $newMetaKey) {
357 if ($oldMetaKey === $newMetaKey) continue;
358
359 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from core $wpdb property, not user input; values are prepared.
360 $this->wpdb->query(
361 $this->wpdb->prepare(
362 "INSERT INTO {$postmetaTable} (post_id, meta_key, meta_value)
363 SELECT old_meta.post_id, %s, old_meta.meta_value
364 FROM {$postmetaTable} old_meta
365 LEFT JOIN {$postmetaTable} new_meta
366 ON new_meta.post_id = old_meta.post_id
367 AND new_meta.meta_key = %s
368 WHERE old_meta.meta_key = %s
369 AND new_meta.meta_id IS NULL",
370 $newMetaKey,
371 $newMetaKey,
372 $oldMetaKey
373 )
374 );
375
376 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifier from core $wpdb property, not user input; value is prepared.
377 $this->wpdb->query(
378 $this->wpdb->prepare(
379 "DELETE FROM {$postmetaTable} WHERE meta_key = %s",
380 $oldMetaKey
381 )
382 );
383 }
384 }
385
386 public function clearAuthTokens()
387 {
388 $settings = get_option('sync_basalam_settings', []);
389 if (!is_array($settings)) return;
390
391 $settings[SettingsConfig::TOKEN] = null;
392 $settings[SettingsConfig::REFRESH_TOKEN] = null;
393
394 update_option('sync_basalam_settings', $settings);
395 }
396
397 /**
398 * Variant price/stock used to follow the product price/stock checkboxes of the custom sync mode.
399 * Copy the old values so variable products keep syncing after the new checkboxes are introduced.
400 */
401 public function backfillVariantSyncFields()
402 {
403 $settings = get_option('sync_basalam_settings', []);
404 if (!is_array($settings)) return;
405
406 // The new keys may already have been filled with their "0" default by a settings read.
407 if (empty($settings[SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_PRICE])) {
408 $settings[SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_PRICE] = $settings[SettingsConfig::SYNC_PRODUCT_FIELD_PRICE] ?? 0;
409 }
410
411 if (empty($settings[SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_STOCK])) {
412 $settings[SettingsConfig::SYNC_PRODUCT_FIELD_VARIANT_STOCK] = $settings[SettingsConfig::SYNC_PRODUCT_FIELD_STOCK] ?? 0;
413 }
414
415 update_option('sync_basalam_settings', $settings);
416 }
417
418 public function createUploadedMediaTable()
419 {
420 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
421
422 $tableName = $this->wpdb->prefix . 'sync_basalam_uploaded_media';
423 $charsetCollate = $this->wpdb->get_charset_collate();
424
425 $sql = "CREATE TABLE $tableName (
426 id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
427 type VARCHAR(16) NOT NULL,
428 source_identity VARCHAR(191) NOT NULL,
429 media_id BIGINT UNSIGNED NULL,
430 media_url VARCHAR(2083) NULL,
431 created_at DATETIME NOT NULL,
432 PRIMARY KEY (id),
433 UNIQUE KEY unique_type_source (type, source_identity),
434 KEY idx_type_created (type, created_at),
435 KEY idx_created_at (created_at)
436 ) $charsetCollate;";
437
438 dbDelta($sql);
439
440 $this->migrateLegacyUploadedPhotos($tableName);
441 }
442
443 /**
444 * اصلاح �
445 حصولاتی که به دلیل کپی شدن، به یک �
446 حصول باسلا�
447 اتصال �
448 شترک دارند.
449 * قدی�
450 ی‌ترین �
451 حصول (�
452 حصول اصلی) �
453 تصل �
454 ی‌�
455 اند و اتصال نسخه‌های کپی حذف �
456 ی‌شود.
457 */
458 public function repairDuplicateProductConnections(): void
459 {
460 ProductConnection::repairDuplicateConnections();
461 }
462
463 private function migrateLegacyUploadedPhotos(string $targetTable): void
464 {
465 $legacyTable = $this->wpdb->prefix . 'sync_basalam_uploaded_photo';
466
467 if (!$this->tableExists($legacyTable)) return;
468
469 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Legacy-data migration; table identifiers from $wpdb->prefix, not user input.
470 $this->wpdb->query(
471 "INSERT IGNORE INTO {$targetTable} (type, source_identity, media_id, media_url, created_at)
472 SELECT 'photo', CONCAT('attachment:', woo_photo_id), sync_basalam_photo_id, sync_basalam_photo_url, COALESCE(created_at, NOW())
473 FROM {$legacyTable}
474 WHERE woo_photo_id IS NOT NULL"
475 );
476
477 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- One-time schema migration; table identifier from $wpdb->prefix, not user input.
478 $this->wpdb->query("DROP TABLE IF EXISTS {$legacyTable}");
479 }
480
481 }
482