PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 All 454 releases
elementor / modules / global-classes / database / migrations / reconcile-downgraded-posts.php

reconcile-downgraded-posts.php in Elementor Website Builder – more than just a page builder 4.3.0, at modules/global-classes/database/migrations/reconcile-downgraded-posts.php

236 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\GlobalClasses\Database\Migrations;
4
5 use Elementor\Core\Database\Base_Migration;
6 use Elementor\Core\Upgrade\Manager;
7 use Elementor\Core\Kits\Concerns\Has_Kit_Dependency;
8 use Elementor\Modules\GlobalClasses\Global_Class_Post;
9 use Elementor\Modules\GlobalClasses\Global_Class_Post_Type;
10 use Elementor\Modules\GlobalClasses\Global_Classes_Labels;
11 use Elementor\Modules\GlobalClasses\Global_Classes_Order;
12 use Elementor\Modules\GlobalClasses\Utils\Global_Class_Data_Normalizer;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Reconciles global class posts for users who already ran the posts migration.
20 *
21 * Upgrade states:
22 * - A: still on aggregate structure, handled by `Migrate_To_Posts` (which now sets an initial edit timestamp).
23 * - B: upgraded, downgraded to the aggregated structure, then edited classes.
24 * - C: upgraded and kept editing classes in posts.
25 *
26 * B and C already have DB version 2, so they skip `Migrate_To_Posts`.
27 * This migration aligns aggregate kit meta with class posts, for group B
28 */
29 class Reconcile_Downgraded_Posts extends Base_Migration {
30 use Has_Kit_Dependency;
31
32 private const QUERY_LIMIT = 10; // Just a safety measurement to make sure we have a big-enough sample size of new edits' versions
33
34 private const POST_STORAGE_INTRO_VERSION = '4.1.0';
35
36 private $should_overwrite = null;
37
38 public function up() {
39 Global_Class_Post_Type::ensure_registered();
40
41 $kit = $this->get_kit();
42
43 if ( ! $kit ) {
44 return;
45 }
46
47 $aggregate = Migrate_To_Posts::get_aggregate_global_classes( $kit );
48
49 if ( empty( $aggregate ) || empty( $aggregate['items'] ) ) {
50 return;
51 }
52
53 $items = Global_Class_Data_Normalizer::normalize_styles( $aggregate['items'] );
54 $order = $aggregate['order'] ?? array_keys( $items );
55
56 $touched_any = $this->reconcile_posts( $items );
57
58 if ( ! $touched_any ) {
59 return;
60 }
61
62 $this->sync_order_and_labels( $kit, $items, $order );
63
64 Migrate_To_Posts::run_document_tracking( $kit );
65 }
66
67 private function reconcile_posts( array $items ): bool {
68 $post_map = $this->build_post_map( $items );
69
70 if ( null === $post_map ) {
71 return false;
72 }
73
74 if ( ! $this->should_overwrite_existing_posts() ) {
75 return false;
76 }
77
78 return $this->apply_reconciliation( $items, $post_map );
79 }
80
81 /**
82 * @return array<string, Global_Class_Post|null>|null Null when a Group A post is detected (edit timestamp found).
83 */
84 private function build_post_map( array $items ): ?array {
85 $post_map = [];
86
87 foreach ( $items as $class_id => $item ) {
88 $post = Global_Class_Post::find_by_class_id( $class_id, false );
89
90 if ( $post && $post->has_edit_timestamp() ) {
91 $this->set_should_overwrite( false );
92
93 return null;
94 }
95
96 $post_map[ $class_id ] = $post;
97 }
98
99 return $post_map;
100 }
101
102 private function apply_reconciliation( array $items, array $post_map ): bool {
103 $touched_any = false;
104
105 foreach ( $items as $class_id => $item ) {
106 $post = $post_map[ $class_id ];
107 $normalized_item = Global_Class_Data_Normalizer::normalize_style( $class_id, $item );
108
109 if ( ! $post ) {
110 $created = Global_Class_Post::create(
111 $normalized_item['id'],
112 $normalized_item['label'],
113 $normalized_item
114 );
115
116 if ( $created ) {
117 $touched_any = true;
118 }
119
120 continue;
121 }
122
123 $normalized_data = Global_Class_Data_Normalizer::normalize_style_fields( $normalized_item );
124 $post->update_data( $normalized_data );
125 $post->update_label( $normalized_item['label'] );
126 $touched_any = true;
127 }
128
129 return $touched_any;
130 }
131
132 private function set_should_overwrite( bool $should_overwrite ): void {
133 $this->should_overwrite = $should_overwrite;
134 }
135
136 private function should_overwrite_existing_posts(): bool {
137 if ( null !== $this->should_overwrite ) {
138 return $this->should_overwrite;
139 }
140
141 $history = Manager::get_installs_history();
142 $intro_ts = $history[ self::POST_STORAGE_INTRO_VERSION ] ?? null;
143
144 if ( ! $intro_ts ) {
145 $this->set_should_overwrite( true );
146
147 return true;
148 }
149
150 global $wpdb;
151
152 $cutoff = gmdate( 'Y-m-d H:i:s', (int) $intro_ts );
153 $cpt = Global_Class_Post_Type::CPT;
154
155 $versions = $wpdb->get_col(
156 $wpdb->prepare(
157 "SELECT pm.meta_value
158 FROM {$wpdb->postmeta} pm
159 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
160 AND p.post_type <> %s
161 WHERE pm.meta_key = '_elementor_version'
162 AND pm.meta_value <> %s
163 AND p.post_modified_gmt > %s
164 ORDER BY pm.meta_value ASC
165 LIMIT %d",
166 $cpt,
167 self::POST_STORAGE_INTRO_VERSION,
168 $cutoff,
169 self::QUERY_LIMIT
170 )
171 );
172
173 foreach ( $versions as $version ) {
174 if ( ! is_string( $version ) || '' === $version ) {
175 continue;
176 }
177
178 if ( version_compare( $version, self::POST_STORAGE_INTRO_VERSION, '<' ) ) {
179 $this->should_overwrite = true;
180
181 return true;
182 }
183 }
184
185 $this->set_should_overwrite( false );
186
187 return false;
188 }
189
190 private function sync_order_and_labels( $kit, array $items, array $order ): void {
191 $aggregated_ids = array_keys( $items );
192 $frontend_order = Global_Classes_Order::make( $kit )->set_preview( false );
193 $merged_order = array_values( array_unique( array_merge( $order, array_diff( $frontend_order->get_order(), $aggregated_ids ) ) ) );
194
195 $frontend_order->set_order( $merged_order );
196
197 $frontend_labels = Global_Classes_Labels::make( $kit )->set_preview( false );
198 $label_map = $frontend_labels->get_labels();
199 $label_to_id = array_flip( $label_map );
200
201 foreach ( $merged_order as $id ) {
202 $class_post = Global_Class_Post::find_by_class_id( $id, false );
203
204 if ( $class_post && $class_post->was_edited() ) {
205 $candidate = $class_post->get_label();
206 } elseif ( isset( $items[ $id ] ) ) {
207 $candidate = $items[ $id ]['label'];
208 } else {
209 continue;
210 }
211
212 // Original style using the current style's label
213 $owner_id = $label_to_id[ $candidate ] ?? null;
214
215 if ( $owner_id === $id ) {
216 continue;
217 }
218
219 if ( null !== $owner_id ) {
220 // Duplicate label - generate a unique one
221 $candidate = Global_Classes_Labels::generate_unique_label( $candidate, array_values( $label_map ) );
222 }
223
224 if ( $class_post && $class_post->get_label() !== $candidate ) {
225 $class_post->update_label( $candidate );
226 }
227
228 unset( $label_to_id[ $label_map[ $id ] ?? '' ] );
229 $label_map[ $id ] = $candidate;
230 $label_to_id[ $candidate ] = $id;
231 }
232
233 $frontend_labels->set_labels( $label_map );
234 }
235 }
236