PluginProbe ʕ •ᴥ•ʔ
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.6.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.6.1
4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.11.0 1.11.1 1.11.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.0 1.9.1 1.9.2 1.9.3 1.9.4 1.9.5 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.13.0 2.14.0 2.14.1 2.15.0 2.15.1 2.16.0 2.16.1 2.16.2 2.16.3 2.17.0 2.17.1 2.17.2 2.18.0 2.19.0 2.19.2 2.19.3 2.19.4 2.2.0 2.2.1 2.20.0 2.20.1 2.20.2 2.20.3 2.20.4 2.20.5 2.20.6 2.21.0 2.22.0 2.22.1 2.23.0 2.24.0 2.25.0 2.25.1 2.25.2 2.26.0 2.27.0 2.27.1 2.28.0 2.29.0 2.29.1 2.29.2 2.29.3 2.29.4 2.3.0 2.3.1 2.30.0 2.31.0 2.31.1 2.31.2 2.31.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.40.0 2.40.1 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.9.0 3.0.0 3.0.0-RC1 3.0.0-RC2 3.0.0-beta1 3.0.0-beta2 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.10.0 3.10.1 3.11.0 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.13.4 3.14.0 3.15.0 3.15.1 3.15.2 3.15.3 3.15.4 3.15.5 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.16.6 3.16.7 3.16.8 3.17.0 3.17.1 3.17.2 3.17.3 3.17.4 3.17.5 3.17.6 3.18.0 3.19.0 3.19.1 3.19.2 3.2.0 3.2.1 3.2.2 3.20.0 3.20.1 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 4.0.0 4.0.1 4.0.2 4.0.3 trunk 4.1.0 0.2.19.1 4.1.1 1.0.0 4.2.0 1.0.1 1.0.2
surecart / app / src / Database / CartLineItemDetailsMigrationService.php
surecart / app / src / Database Last commit date
Tables 1 year ago CartLineItemDetailsMigrationService.php 5 days ago GeneralMigration.php 1 year ago MigrationsServiceProvider.php 5 days ago ProductPageMigrationService.php 1 year ago RewriteRulesMigrationService.php 1 year ago Table.php 4 years ago ThemeMigrationService.php 4 months ago UpdateMigrationServiceProvider.php 1 month ago UserMetaMigrationsService.php 4 years ago VersionMigration.php 1 year ago WebhookMigrationsService.php 1 year ago
CartLineItemDetailsMigrationService.php
214 lines
1 <?php
2
3 namespace SureCart\Database;
4
5 /**
6 * Migrates saved cart templates: wraps the standalone `cart-line-item-variant`
7 * block inside the new `cart-line-item-details` container so bundle line-items
8 * get the collapsible "+N more" region on upgrade.
9 *
10 * The `cart-line-item-note` block is intentionally left standalone — in
11 * released carts it already sits right after the variant, so once the variant
12 * is wrapped the note naturally ends up after the details container.
13 */
14 class CartLineItemDetailsMigrationService extends VersionMigration {
15 /**
16 * The key for the migration.
17 *
18 * @var string
19 */
20 protected $migration_key = 'surecart_cart_line_item_details_migration_version';
21
22 /**
23 * Whether any post update failed during the run. When true we skip marking
24 * the migration complete so it retries on the next admin_init.
25 *
26 * @var bool
27 */
28 protected $failed = false;
29
30 /**
31 * Default variant attributes — matches the default cart template.
32 *
33 * @var string
34 */
35 const DEFAULT_CHILD_ATTRS = '{"style":{"typography":{"fontSize":"14px","lineHeight":"1.4"}}}';
36
37 /**
38 * Matches the details container form: opener (with or without attrs),
39 * inner blocks, closer. Its presence means the cart is already migrated.
40 *
41 * @var string
42 */
43 const DETAILS_WRAPPER_PATTERN = '/<!--\s*wp:surecart\/cart-line-item-details\b.*?-->.*?<!--\s*\/wp:surecart\/cart-line-item-details\s*-->/s';
44
45 /**
46 * Matches the standalone self-closing variant block the container now owns.
47 *
48 * @var string
49 */
50 const VARIANT_BLOCK_PATTERN = '/[ \t]*<!--\s*wp:surecart\/cart-line-item-variant\b.*?\/-->\n?/s';
51
52 /**
53 * Run the migration.
54 *
55 * @return void
56 */
57 public function run(): void {
58 foreach ( $this->getCartPosts() as $cart_post ) {
59 $content = $cart_post->post_content ?? '';
60 if ( empty( $content ) ) {
61 continue;
62 }
63
64 $migrated = $this->consolidateDetails( $content );
65 if ( $migrated === $content ) {
66 continue;
67 }
68
69 $result = wp_update_post(
70 array(
71 'ID' => $cart_post->ID,
72 'post_content' => $migrated,
73 ),
74 true
75 );
76
77 // Leave the migration unmarked (see complete()) so a failed update
78 // retries next admin_init instead of being stranded on the old markup.
79 if ( is_wp_error( $result ) || empty( $result ) ) {
80 $this->failed = true;
81 }
82 }
83 }
84
85 /**
86 * Only mark the migration complete when every cart updated cleanly.
87 *
88 * @return void
89 */
90 public function complete() {
91 if ( $this->failed ) {
92 return;
93 }
94 parent::complete();
95 }
96
97 /**
98 * Collect the cart posts to migrate.
99 *
100 * Covers both storage locations:
101 * - wp_template_part with post_name 'cart' (newer installs).
102 * - sc_cart post type (legacy installs whose cart hasn't been moved yet).
103 *
104 * @return array
105 */
106 protected function getCartPosts(): array {
107 $cart_part_query = new \WP_Query(
108 array(
109 'post_type' => 'wp_template_part',
110 'post_status' => array( 'auto-draft', 'draft', 'publish' ),
111 'posts_per_page' => -1,
112 'name' => 'cart',
113 'lazy_load_term_meta' => false,
114 )
115 );
116
117 $sc_cart_query = new \WP_Query(
118 array(
119 'post_type' => 'sc_cart',
120 'post_status' => array( 'auto-draft', 'draft', 'publish' ),
121 'posts_per_page' => -1,
122 'lazy_load_term_meta' => false,
123 )
124 );
125
126 return array_merge( $cart_part_query->posts ?? array(), $sc_cart_query->posts ?? array() );
127 }
128
129 /**
130 * Wrap the standalone variant block in a details container, preserving the
131 * merchant's variant styling. The note is left untouched.
132 *
133 * Deterministic, so re-running on migrated content is a no-op.
134 *
135 * @param string $content Post content.
136 * @return string Migrated content (unchanged if nothing matched).
137 */
138 protected function consolidateDetails( string $content ): string {
139 // Idempotent: a details container already present means the cart is in
140 // the target shape (container owns the variant; note stays standalone).
141 if ( preg_match( self::DETAILS_WRAPPER_PATTERN, $content ) ) {
142 return $content;
143 }
144
145 // Preserve the existing variant attributes (merchant styling).
146 $variant_attrs = $this->blockAttrs( $content, 'cart-line-item-variant' ) ?? self::DEFAULT_CHILD_ATTRS;
147
148 $details = "<!-- wp:surecart/cart-line-item-details -->\n"
149 . '<!-- wp:surecart/cart-line-item-variant ' . $variant_attrs . " /-->\n"
150 . '<!-- /wp:surecart/cart-line-item-details -->';
151
152 // Wrap the first standalone variant in place. The note (a separate
153 // standalone block) is deliberately not matched, so it stays exactly
154 // where it is — after the variant, hence after the new container.
155 if ( preg_match( self::VARIANT_BLOCK_PATTERN, $content ) ) {
156 $migrated = preg_replace( self::VARIANT_BLOCK_PATTERN, $details . "\n", $content, 1 );
157 return is_string( $migrated ) ? $migrated : $content;
158 }
159
160 // No variant block at all: inject the container before the first
161 // sensible anchor so bundle items still show for older carts. Prefer
162 // the note so the container lands before it (details, then note).
163 $anchor = $this->firstExistingAnchor(
164 $content,
165 array(
166 'surecart/cart-line-item-note',
167 'surecart/cart-line-item-status',
168 'surecart/cart-line-item-quantity',
169 'surecart/cart-line-item-remove',
170 )
171 );
172
173 if ( ! $anchor ) {
174 return $content;
175 }
176
177 return str_replace(
178 '<!-- wp:' . $anchor,
179 $details . "\n" . '<!-- wp:' . $anchor,
180 $content
181 );
182 }
183
184 /**
185 * Get the attribute JSON of the first occurrence of a self-closing block.
186 *
187 * @param string $content Post content to scan.
188 * @param string $block Block name without the `surecart/` prefix.
189 * @return string|null Attribute JSON, or null when absent/attribute-less.
190 */
191 protected function blockAttrs( string $content, string $block ): ?string {
192 if ( preg_match( '/<!--\s*wp:surecart\/' . preg_quote( $block, '/' ) . '\s+(\{.*?\})\s*\/-->/s', $content, $matches ) ) {
193 return $matches[1];
194 }
195 return null;
196 }
197
198 /**
199 * Pick the first anchor block from a preferred list that exists in the content.
200 *
201 * @param string $content Post content to scan.
202 * @param array $candidates Block names, in order of preference.
203 * @return string|null Block name to anchor against, or null if none match.
204 */
205 protected function firstExistingAnchor( string $content, array $candidates ): ?string {
206 foreach ( $candidates as $block_name ) {
207 if ( has_block( $block_name, $content ) ) {
208 return $block_name;
209 }
210 }
211 return null;
212 }
213 }
214