PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.0
ووسلام – همگام سازی ووکامرس و باسلام v1.10.0
1.10.19 1.10.20 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 All 53 releases
sync-basalam / includes / Migrations / MigratorService.php

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

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