PluginProbe ʕ •ᴥ•ʔ
Admin Columns / 7.1.4
Admin Columns v7.1.4
7.1.4 7.0.19 2.3.5 2.4 2.4.1 2.4.10 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.6.1 2.5.6.2 2.5.6.3 2.5.6.4 3.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.7 3.1 3.1.1 3.1.10 3.1.2 3.1.3 3.1.5 3.2.3 3.2.7 3.3.1 3.4.1 3.4.6 3.4.8 4.0.1 4.0.3 4.1.6 4.2.2 4.2.5 4.3 4.3.2 4.4.1 4.4.4 4.4.5 4.5.5 4.6.1 4.7.18 4.7.19 4.7.20 4.7.7 7.0.13 7.0.14 7.0.16 trunk 1.0 1.1 1.1.3 1.2 1.2.1 1.3 1.3.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.5.1 1.4.6 1.4.6.1 1.4.6.2 1.4.6.3 1.4.6.4 1.4.7 1.4.8 1.4.9 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.2 2.2.1 2.2.1.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.5.1 2.2.6 2.2.6.1 2.2.6.2 2.2.6.3 2.2.6.4 2.2.7 2.2.8 2.2.8.1 2.2.9 2.3.1 2.3.2 2.3.3
codepress-admin-columns / classes / Plugin / Update / V4000.php
codepress-admin-columns / classes / Plugin / Update Last commit date
V4000.php 2 days ago V7000.php 2 days ago V7004.php 2 days ago
V4000.php
758 lines
1 <?php
2
3 declare(strict_types=1);
4
5 namespace AC\Plugin\Update;
6
7 use AC\Plugin\Update;
8 use AC\Plugin\Version;
9 use AC\Storage\Repository\ListScreenOrder;
10 use AC\Type\TableId;
11 use DateTime;
12
13 class V4000 extends Update
14 {
15 public const DATABASE_TABLE = 'admin_columns';
16
17 public const LAYOUT_PREFIX = 'cpac_layouts';
18 public const COLUMNS_PREFIX = 'cpac_options_';
19
20 public const PROGRESS_KEY = 'ac_update_progress';
21 public const REPLACEMENT_IDS_KEY = 'ac_update_replacement_ids';
22
23 private int $next_step;
24
25 public function __construct()
26 {
27 parent::__construct(new Version('4.0.0'));
28
29 // because `get_option` could be cached we only fetch the next step from the DB on initialisation.
30 $this->next_step = $this->get_next_step();
31 }
32
33 public function apply_update(): void
34 {
35 // just in case we need a bit of extra time to execute our upgrade script
36 $max_exec = (int)ini_get('max_execution_time');
37 if ($max_exec > 0 && $max_exec < 120) {
38 @set_time_limit(120);
39 }
40
41 global $wpdb;
42
43 // Apply update in chunks to minimize the impact of a timeout.
44 switch ($this->next_step) {
45 case 1:
46 $this->create_database();
47
48 $this
49 ->update_next_step(2)
50 ->apply_update();
51 break;
52 case 2:
53 // 1. migrate segments to site specific user preference. Previously this was stored globally.
54 $this->migrate_segments_preferences();
55
56 // go to next step
57 $this
58 ->update_next_step(3)
59 ->apply_update();
60
61 break;
62 case 3:
63
64 // 2. migrate column settings to new DB table
65 $replaced_list_ids = $this->migrate_list_screen_settings();
66
67 // $replaced_list_ids contains a list of empty id's that are replaced with unique id's
68 $this->update_replacement_ids($replaced_list_ids);
69
70 // go to next step
71 $this
72 ->update_next_step(4)
73 ->apply_update();
74
75 break;
76 case 4:
77
78 // 3. User Preference "Segments": ac_preferences_search_segments
79 $this->update_user_preferences_segments($this->get_replacement_ids());
80
81 // go to next step
82 $this
83 ->update_next_step(5)
84 ->apply_update();
85
86 break;
87 case 5:
88 $replaced_list_ids = $this->get_replacement_ids();
89
90 // 4. User Preference "Horizontal Scrolling": ac_preferences_show_overflow_table
91 $this->update_user_preference_by_key(
92 $wpdb->get_blog_prefix() . 'ac_preferences_show_overflow_table',
93 $replaced_list_ids
94 );
95
96 // 5. User Preference "Sort": ac_preferences_sorted_by
97 $this->update_user_preference_by_key(
98 $wpdb->get_blog_prefix() . 'ac_preferences_sorted_by',
99 $replaced_list_ids
100 );
101
102 // go to next step
103 $this
104 ->update_next_step(6)
105 ->apply_update();
106
107 break;
108 case 6:
109 $this->migrate_invalid_network_settings();
110
111 // go to next step
112 $this
113 ->update_next_step(7)
114 ->apply_update();
115
116 break;
117 case 7:
118 $replaced_list_ids = $this->get_replacement_ids();
119
120 // 6. User Preference "Table selection": wp_ac_preferences_layout_table
121 $this->migrate_user_preferences_table_selection($replaced_list_ids);
122
123 // 7. Migrate layout order from `usermeta` to the `option table`
124 $this->migrate_list_screen_order($replaced_list_ids);
125
126 // clear steps and replacement id's
127 $this->flush_temp_data();
128
129 break;
130 }
131 }
132
133 private function update_replacement_ids(array $ids): void
134 {
135 update_option(self::REPLACEMENT_IDS_KEY, $ids, false);
136 }
137
138 private function get_replacement_ids(): array
139 {
140 return (array)get_option(self::REPLACEMENT_IDS_KEY, []);
141 }
142
143 private function get_next_step(): int
144 {
145 return (int)get_option(self::PROGRESS_KEY, 1);
146 }
147
148 private function update_next_step(int $step): self
149 {
150 $this->next_step = $step;
151
152 update_option(self::PROGRESS_KEY, $this->next_step, false);
153
154 return $this;
155 }
156
157 private function flush_temp_data()
158 {
159 delete_option(self::PROGRESS_KEY);
160 delete_option(self::REPLACEMENT_IDS_KEY);
161 }
162
163 // Segments were stored globally, ignoring individual sites on a multisite network. Segments are now stored per site.
164 private function migrate_segments_preferences()
165 {
166 global $wpdb;
167
168 $prefix = 'ac_preferences_search_segments_';
169
170 $sql = "
171 SELECT *
172 FROM $wpdb->usermeta
173 WHERE meta_key LIKE '$prefix%'
174 ORDER BY `umeta_id` DESC
175 ";
176
177 $results = $wpdb->get_results($sql);
178
179 foreach ($results as $row) {
180 $data = maybe_unserialize($row->meta_value);
181
182 if ($data) {
183 update_user_option($row->user_id, $row->meta_key, $data);
184 }
185 }
186 }
187
188 private function update_user_preferences_segments(array $list_ids)
189 {
190 global $wpdb;
191
192 $prefix = $wpdb->get_blog_prefix() . 'ac_preferences_search_segments_';
193
194 foreach ($list_ids as $list_key => $ids) {
195 foreach ($ids as $deprecated_id => $list_id) {
196 $old_meta_key = $prefix . ($deprecated_id ?: $list_key);
197
198 // Segments were stored globally, ignoring individual sites on a multisite network. Segments are now stored per site.
199 $new_meta_key = $prefix . $list_id;
200
201 $sql = $wpdb->prepare(
202 "SELECT user_id, meta_value FROM $wpdb->usermeta WHERE meta_key = %s",
203 $old_meta_key
204 );
205
206 $results = $wpdb->get_results($sql);
207
208 foreach ($results as $row) {
209 $wpdb->insert(
210 $wpdb->usermeta,
211 [
212 'user_id' => $row->user_id,
213 'meta_key' => $new_meta_key,
214 'meta_value' => $row->meta_value,
215 ],
216 [
217 '%d',
218 '%s',
219 '%s',
220 ]
221 );
222 }
223 }
224 }
225 }
226
227 private function map_to_storage_keys(array $list_ids): array
228 {
229 $map = [];
230
231 foreach ($list_ids as $list_key => $ids) {
232 foreach ($ids as $deprecated_id => $list_id) {
233 $old_meta_key = $list_key . $deprecated_id;
234 $new_meta_key = $list_key . $list_id;
235
236 $map[$old_meta_key] = $new_meta_key;
237 }
238 }
239
240 return $map;
241 }
242
243 private function update_user_preference_by_key(string $meta_key, array $list_ids): void
244 {
245 global $wpdb;
246
247 $results = $wpdb->get_results($wpdb->prepare("SELECT * FROM $wpdb->usermeta WHERE meta_key = %s", $meta_key));
248
249 $storage_keys = $this->map_to_storage_keys($list_ids);
250
251 foreach ($results as $row) {
252 $data = maybe_unserialize($row->meta_value);
253
254 if (empty($data)) {
255 continue;
256 }
257
258 $orginal_data = $data;
259
260 foreach ($storage_keys as $old_key => $new_key) {
261 // Replace old key with new key
262 if (isset($data[$old_key])) {
263 $data[$new_key] = $data[$old_key];
264
265 unset($data[$old_key]);
266 }
267 }
268
269 // no update needed
270 if ($data === $orginal_data) {
271 continue;
272 }
273
274 $wpdb->query(
275 $wpdb->prepare(
276 "UPDATE $wpdb->usermeta SET meta_value = %s WHERE umeta_id = %d",
277 serialize($data),
278 $row->umeta_id
279 )
280 );
281 }
282 }
283
284 private function migrate_user_preferences_table_selection(array $replaced_list_ids)
285 {
286 global $wpdb;
287
288 $meta_key = $wpdb->get_blog_prefix() . 'ac_preferences_layout_table';
289 $results = $wpdb->get_results($wpdb->prepare("SELECT * FROM $wpdb->usermeta WHERE meta_key = %s", $meta_key));
290
291 foreach ($results as $row) {
292 $data = maybe_unserialize($row->meta_value);
293
294 if (empty($data) || ! is_array($data)) {
295 continue;
296 }
297
298 $orginal_data = $data;
299
300 foreach ($replaced_list_ids as $list_key => $ids) {
301 if (! array_key_exists($list_key, $data)) {
302 continue;
303 }
304
305 $old_id = $data[$list_key];
306
307 if (array_key_exists($old_id, $ids)) {
308 // use replaced ID
309 $data[$list_key] = $ids[$old_id];
310 }
311 }
312
313 // no update needed
314 if ($data === $orginal_data) {
315 continue;
316 }
317
318 $wpdb->query(
319 $wpdb->prepare(
320 "UPDATE $wpdb->usermeta SET meta_value = %s WHERE umeta_id = %d",
321 serialize($data),
322 $row->umeta_id
323 )
324 );
325 }
326 }
327
328 /**
329 * Migrate the order of the list screens from the `usermeta` to the `options` table
330 */
331 private function migrate_list_screen_order(array $replaced_list_ids)
332 {
333 global $wpdb;
334
335 $meta_key = $wpdb->get_blog_prefix() . 'ac_preferences_layout_order';
336
337 $sql = $wpdb->prepare(
338 "SELECT meta_value FROM {$wpdb->prefix}usermeta WHERE meta_key = %s AND meta_value != '' LIMIT 1",
339 $meta_key
340 );
341
342 $result = $wpdb->get_var($sql);
343
344 if (! $result) {
345 return;
346 }
347
348 $list_screens = maybe_unserialize($result);
349
350 if (! $list_screens || ! is_array($list_screens)) {
351 return;
352 }
353
354 $order = new ListScreenOrder();
355
356 foreach ($list_screens as $list_key => $ids) {
357 if ($ids && is_array($ids)) {
358 // try and replace deprecated id's
359 foreach ($ids as $k => $id) {
360 $ids[$k] = $this->maybe_replace_id($replaced_list_ids, $list_key, $id);
361 }
362
363 $order->set(new TableId($list_key), $ids);
364 }
365 }
366 }
367
368 /**
369 * Since Network list screens have their own preference, we have to remove any network list screens from the preferences
370 */
371 private function migrate_invalid_network_settings(): void
372 {
373 global $wpdb;
374
375 $results = $wpdb->get_results(
376 $wpdb->prepare(
377 "
378 SELECT *
379 FROM $wpdb->usermeta
380 WHERE meta_key = %s
381 ",
382 $wpdb->get_blog_prefix() . 'ac_preferences_settings'
383 )
384 );
385
386 foreach ($results as $row) {
387 $data = maybe_unserialize($row->meta_value);
388
389 if (empty($data) || ! is_array($data)) {
390 continue;
391 }
392
393 if (isset($data['list_screen']) && in_array($data['list_screen'], ['wp-ms_sites', 'wp-ms_users'], true)) {
394 $wpdb->query($wpdb->prepare("DELETE FROM $wpdb->usermeta WHERE umeta_id = %d", $row->umeta_id));
395 }
396 }
397 }
398
399 private function maybe_replace_id(array $replaced_list_ids, string $list_key, $id)
400 {
401 if (! array_key_exists($list_key, $replaced_list_ids)) {
402 return $id;
403 }
404
405 $_ids = $replaced_list_ids[$list_key];
406
407 if (! $_ids || ! is_array($_ids)) {
408 return $id;
409 }
410
411 foreach ($_ids as $old_id => $new_id) {
412 if ($old_id === $id) {
413 return $new_id;
414 }
415 }
416
417 return $id;
418 }
419
420 /**
421 * @return array<array<string, mixed>>
422 */
423 private function get_layouts_data(): array
424 {
425 global $wpdb;
426
427 $prefix = self::LAYOUT_PREFIX;
428
429 $sql = "
430 SELECT option_name, option_value
431 FROM $wpdb->options
432 WHERE option_name LIKE '$prefix%'
433 ORDER BY `option_id` DESC
434 ";
435
436 $results = $wpdb->get_results($sql);
437
438 $layouts = [];
439
440 foreach ($results as $row) {
441 $data = maybe_unserialize($row->option_value);
442
443 if (is_object($data)) {
444 $list_id = $data->id ?? null;
445
446 $list_key = $this->remove_prefix(self::LAYOUT_PREFIX, $row->option_name);
447 $list_key = $this->remove_suffix((string)$list_id, $list_key);
448
449 if (! $list_key) {
450 continue;
451 }
452
453 $layout_data = [
454 'id' => $list_id,
455 'key' => $list_key,
456 'name' => '',
457 'users' => [],
458 'roles' => [],
459 ];
460
461 if (! empty($data->name)) {
462 $layout_data['name'] = $data->name;
463 }
464 if (! empty($data->users) && is_array($data->users)) {
465 $layout_data['users'] = array_map('intval', $data->users);
466 }
467 if (! empty($data->roles) && is_array($data->roles)) {
468 $layout_data['roles'] = array_map('strval', $data->roles);
469 }
470
471 $layouts[] = $layout_data;
472 }
473 }
474
475 return $layouts;
476 }
477
478 private function get_columns_data(): array
479 {
480 global $wpdb;
481
482 $prefix = self::COLUMNS_PREFIX;
483
484 $sql = "
485 SELECT option_name, option_value
486 FROM {$wpdb->options}
487 WHERE option_name LIKE '$prefix%'
488 ORDER BY `option_id` DESC
489 ";
490
491 $results = $wpdb->get_results($sql);
492
493 $columns = [];
494
495 foreach ($results as $row) {
496 if ($this->has_suffix('__default', $row->option_name)) {
497 continue;
498 }
499
500 $storage_key = $this->remove_prefix(self::COLUMNS_PREFIX, $row->option_name);
501
502 $column_data = maybe_unserialize($row->option_value);
503
504 $columns[$storage_key] = $column_data && is_array($column_data) ? $column_data : [];
505 }
506
507 return $columns;
508 }
509
510 /**
511 * @return array List of replaced id's
512 */
513 private function migrate_list_screen_settings(): array
514 {
515 $migrate = [];
516
517 /**
518 * $replaced_list_ids [ $list_key => [ $deprecated_list_id => $new_list_id ] ]
519 */
520 $replaced_list_ids = [];
521
522 // 1. Fetch data
523 $layouts_data = $this->get_layouts_data();
524 $columns_data = $this->get_columns_data();
525
526 // 2. Process Pro settings
527 foreach ($layouts_data as $layout_data) {
528 $list_key = $layout_data['key'];
529
530 $storage_key = $list_key . $layout_data['id'];
531
532 $columns = [];
533
534 // Check for column settings
535 if (isset($columns_data[$storage_key])) {
536 $columns = $columns_data[$storage_key];
537
538 // Remove used column settings from stack
539 unset($columns_data[$storage_key]);
540 }
541
542 $settings = [];
543 if ($layout_data['users']) {
544 $settings['users'] = $layout_data['users'];
545 }
546 if ($layout_data['roles']) {
547 $settings['roles'] = $layout_data['roles'];
548 }
549
550 $list_id = $layout_data['id'];
551
552 if (! $list_id) {
553 $list_id = uniqid();
554
555 // add to list of id's that have been replaced
556 $replaced_list_ids[$list_key][''] = $list_id;
557 }
558
559 $list_data = [
560 'id' => $list_id,
561 'key' => $list_key,
562 'title' => $layout_data['name'],
563 'columns' => $columns,
564 'settings' => $settings,
565 ];
566
567 $migrate[] = $list_data;
568 }
569
570 // 3. Process Free column settings
571 foreach ($columns_data as $list_key => $columns) {
572 if (empty($columns)) {
573 continue;
574 }
575
576 // Skip columns that contain a list ID
577 if ($this->contains_list_id($list_key)) {
578 continue;
579 }
580
581 $list_id = uniqid();
582
583 $list_data = [
584 'id' => $list_id,
585 'key' => $list_key,
586 'title' => __('Original', 'codepress-admin-columns'),
587 'settings' => [],
588 'columns' => $columns,
589 ];
590
591 $migrate[] = $list_data;
592
593 // add to list of id's that have been replaced
594 $replaced_list_ids[$list_key][''] = $list_id;
595 }
596
597 // 4. Make sure all ID's are unique.
598 // A duplicate `id` is possible when a user manually exported their list screen settings and
599 // changed the list screen `key` (e.g. from post to page), without changing the `id`, and imported these settings.
600 $migrate = $this->unique_ids($migrate);
601
602 // 5. Insert into DB
603 foreach ($migrate as $list_data) {
604 $this->insert($list_data);
605 }
606
607 return $replaced_list_ids;
608 }
609
610 private function unique_ids(array $migrate): array
611 {
612 $ids = [];
613
614 foreach ($migrate as $k => $list_data) {
615 if (in_array($list_data['id'], $ids, true)) {
616 // Truly eliminates a duplicate $unique_id, because `uniqid()` is based on the current time in microseconds
617 usleep(1000);
618
619 $migrate[$k]['id'] = uniqid();
620 }
621
622 $ids[] = $list_data['id'];
623 }
624
625 return $migrate;
626 }
627
628 private function create_database(): void
629 {
630 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
631
632 global $wpdb;
633
634 $collate = $wpdb->get_charset_collate();
635
636 $table_name = $wpdb->prefix . 'admin_columns';
637
638 $sql = "
639 CREATE TABLE $table_name (
640 id bigint(20) unsigned NOT NULL auto_increment,
641 list_id varchar(20) NOT NULL default '',
642 list_key varchar(100) NOT NULL default '',
643 title varchar(255) NOT NULL default '',
644 columns mediumtext,
645 settings mediumtext,
646 date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
647 date_modified datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
648 PRIMARY KEY (id),
649 UNIQUE KEY `list_id` (`list_id`)
650 ) $collate;
651 ";
652
653 dbDelta($sql);
654 }
655
656 private function insert(array $data)
657 {
658 global $wpdb;
659
660 $date = new DateTime();
661
662 $wpdb->insert(
663 $wpdb->prefix . self::DATABASE_TABLE,
664 [
665 'title' => $data['title'],
666 'list_id' => $data['id'],
667 'list_key' => $data['key'],
668 'columns' => serialize($data['columns']),
669 'settings' => serialize($data['settings']),
670 'date_modified' => $date->format('Y-m-d H:i:s'),
671 'date_created' => $date->format('Y-m-d H:i:s'),
672 ],
673 [
674 '%s',
675 '%s',
676 '%s',
677 '%s',
678 '%s',
679 '%s',
680 '%s',
681 ]
682 );
683 }
684
685 private function contains_list_id(string $storage_key): bool
686 {
687 $prefixes = [
688 'wp-users',
689 'wp-media',
690 'wp-comments',
691 'wp-ms_sites',
692 'wp-ms_users',
693 ];
694
695 foreach ($prefixes as $prefix) {
696 if ($this->has_prefix($prefix, $storage_key)) {
697 $layout_id = $this->remove_prefix($prefix, $storage_key);
698 $layout_id = substr($layout_id, -13);
699
700 return $this->is_layout_id((string)$layout_id);
701 }
702 }
703
704 // Is it a taxonomy?
705 if ($this->has_prefix('wp-taxonomy_', $storage_key)) {
706 $taxonomy = $this->remove_prefix('wp-taxonomy_', $storage_key);
707
708 if (taxonomy_exists($taxonomy)) {
709 return false;
710 }
711
712 $layout_id = substr($taxonomy, -13);
713
714 return $this->is_layout_id((string)$layout_id);
715 }
716
717 // is it a post?
718 if (post_type_exists($storage_key)) {
719 return false;
720 }
721
722 $layout_id = substr($storage_key, -13);
723
724 return $this->is_layout_id((string)$layout_id);
725 }
726
727 private function is_layout_id(string $id): bool
728 {
729 return $id && (strlen($id) === 13) && $this->is_hex($id);
730 }
731
732 private function is_hex(string $string): bool
733 {
734 return '' == trim(substr($string, -13), '0..9A..Fa..f');
735 }
736
737 private function remove_prefix(string $prefix, string $string): string
738 {
739 return (string)preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $string);
740 }
741
742 private function remove_suffix(string $suffix, string $string): string
743 {
744 return (string)preg_replace('/' . preg_quote($suffix, '/') . '$/', '', $string);
745 }
746
747 private function has_prefix(string $prefix, string $string): bool
748 {
749 return substr($string, 0, strlen($prefix)) === $prefix;
750 }
751
752 private function has_suffix(string $suffix, string $string): bool
753 {
754 return substr($string, strlen($suffix) * -1) === $suffix;
755 }
756
757 }
758