PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.3
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.3
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / updates / update-1.10.0.php

update-1.10.0.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.3, at includes/updates/update-1.10.0.php

272 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Update to 1.10.0
4 *
5 * Promote legacy multisite per-blog user uuids to the network-wide key.
6 *
7 * Before user identity consolidated in Sync\Pos_Uuid (#1450), the cashier
8 * endpoint minted `_woocommerce_pos_uuid_{blog_id}` user meta on multisite
9 * while every other reader used the plain `_woocommerce_pos_uuid` key —
10 * forking one user into two POS identities. #1450 adds lazy read-time
11 * adoption of the current blog's legacy value; this migration performs the
12 * promotion deterministically for ALL users in one pass and deletes the
13 * legacy rows. The lazy adoption stays as a safety net for rows this run
14 * cannot see. MERGE ORDER MATTERS: this migration must not ship without
15 * #1450 — the pre-#1450 cashier endpoint re-mints a per-blog uuid the
16 * moment its row disappears, re-opening the fork this migration closes.
17 *
18 * Promotion policy (deliberately close to, but not identical to,
19 * Pos_Uuid::adopt_legacy_multisite_user_uuid):
20 * - Any existing VALID plain uuid wins — it is what the /customers endpoint
21 * has already served to clients. All plain rows are checked, matching
22 * Pos_Uuid::read_valid_uuid_from_meta; invalid-only values count as absent.
23 * - Otherwise blog ids are tried in ASCENDING order and the first valid
24 * legacy value not owned by another live user's plain key is promoted.
25 * A collision (cloned/imported row) falls through to the NEXT blog's
26 * value — preserving some legacy identity beats minting fresh. The lazy
27 * path instead adopts the CURRENT blog's value; whichever writes first
28 * wins and the other becomes a no-op, so the two policies cannot flap.
29 * - Legacy rows are deleted only when the user ends the pass with a valid
30 * plain uuid or had no promotable value at all. A failed meta write
31 * leaves the user's legacy rows in place for the lazy path, because the
32 * ladder is one-shot and will not retry. NOTE: deletion trades rollback
33 * convenience for a clean meta table — a later downgrade to 1.9.x
34 * re-mints per-blog cashier uuids instead of finding these rows.
35 *
36 * usermeta is a single network-global table and the legacy key encodes the
37 * blog id, so one run sweeps the whole network — no switch_to_blog() needed.
38 * Re-runs (the update ladder fires per blog on multisite) find no legacy rows
39 * and are no-ops.
40 *
41 * @author Paul Kilmurray <paul@kilbot.com>
42 *
43 * @see http://wcpos.com
44 * @package WCPOS\WooCommercePOS
45 */
46
47 namespace WCPOS\WooCommercePOS;
48
49 use WCPOS\WooCommercePOS\Sync\Pos_Uuid;
50
51 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
52 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
53 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Queries are prepared before execution.
54 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Only table names and an array_fill-built %s placeholder list are interpolated.
55 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Update script with file-scoped variables.
56
57 global $wpdb;
58
59 // The trailing separator is part of the escaped literal, so the plain
60 // `_woocommerce_pos_uuid` key itself cannot match. The users JOIN skips
61 // orphaned rows of deleted users — nothing reads them and promoting them
62 // would recreate garbage. Non-numeric suffixes are filtered out below.
63 $wcpos_legacy_rows = $wpdb->get_results(
64 $wpdb->prepare(
65 "SELECT m.user_id, m.meta_key, m.meta_value
66 FROM {$wpdb->usermeta} m
67 INNER JOIN {$wpdb->users} u ON u.ID = m.user_id
68 WHERE m.meta_key LIKE %s
69 ORDER BY m.user_id ASC, m.umeta_id ASC",
70 $wpdb->esc_like( Pos_Uuid::META_KEY . '_' ) . '%'
71 )
72 );
73
74 if ( '' !== $wpdb->last_error ) {
75 if ( \function_exists( 'wc_get_logger' ) ) {
76 wc_get_logger()->error(
77 'WCPOS 1.10.0 migration: legacy uuid discovery query failed, no rows were migrated: ' . $wpdb->last_error,
78 array( 'source' => 'woocommerce-pos' )
79 );
80 }
81
82 return;
83 }
84
85 $wcpos_legacy_key_pattern = '/^' . preg_quote( Pos_Uuid::META_KEY, '/' ) . '_(\d+)$/';
86
87 // Group as [user_id][blog_id] => value. Duplicate rows for one key keep the
88 // FIRST (lowest umeta_id) value unless a later row is the only valid uuid.
89 $wcpos_legacy_by_user = array();
90 foreach ( (array) $wcpos_legacy_rows as $wcpos_row ) {
91 if ( ! preg_match( $wcpos_legacy_key_pattern, (string) $wcpos_row->meta_key, $wcpos_matches ) ) {
92 continue;
93 }
94 $wcpos_user_id = (int) $wcpos_row->user_id;
95 $wcpos_blog_id = (int) $wcpos_matches[1];
96 $wcpos_value = (string) $wcpos_row->meta_value;
97 if (
98 ! isset( $wcpos_legacy_by_user[ $wcpos_user_id ][ $wcpos_blog_id ] )
99 || ( ! Pos_Uuid::is_uuid( $wcpos_legacy_by_user[ $wcpos_user_id ][ $wcpos_blog_id ] ) && Pos_Uuid::is_uuid( $wcpos_value ) )
100 ) {
101 $wcpos_legacy_by_user[ $wcpos_user_id ][ $wcpos_blog_id ] = $wcpos_value;
102 }
103 }
104
105 // Prefetch which candidate values are already owned as a LIVE user's plain
106 // key (one chunked query instead of one query per candidate — meta_value is
107 // unindexed, so per-candidate lookups would range-scan every customer row).
108 // Deleted users' stale rows are excluded, mirroring uuid_owned_by_other_user.
109 $wcpos_candidate_values = array();
110 foreach ( $wcpos_legacy_by_user as $wcpos_blog_values ) {
111 foreach ( $wcpos_blog_values as $wcpos_value ) {
112 if ( Pos_Uuid::is_uuid( $wcpos_value ) ) {
113 $wcpos_candidate_values[ $wcpos_value ] = true;
114 }
115 }
116 }
117
118 $wcpos_owners_by_value = array();
119 foreach ( array_chunk( array_keys( $wcpos_candidate_values ), 500 ) as $wcpos_chunk ) {
120 $wcpos_placeholders = implode( ',', array_fill( 0, \count( $wcpos_chunk ), '%s' ) );
121 $wcpos_owner_rows = $wpdb->get_results(
122 $wpdb->prepare(
123 "SELECT m.meta_value, m.user_id
124 FROM {$wpdb->usermeta} m
125 INNER JOIN {$wpdb->users} u ON u.ID = m.user_id
126 WHERE m.meta_key = %s AND m.meta_value IN ( {$wcpos_placeholders} )",
127 array_merge( array( Pos_Uuid::META_KEY ), $wcpos_chunk )
128 )
129 );
130 if ( '' !== $wpdb->last_error ) {
131 if ( \function_exists( 'wc_get_logger' ) ) {
132 wc_get_logger()->error(
133 'WCPOS 1.10.0 migration: uuid ownership prefetch query failed, no rows were migrated: ' . $wpdb->last_error,
134 array( 'source' => 'woocommerce-pos' )
135 );
136 }
137
138 return;
139 }
140 foreach ( (array) $wcpos_owner_rows as $wcpos_owner_row ) {
141 $wcpos_owners_by_value[ strtolower( (string) $wcpos_owner_row->meta_value ) ][] = (int) $wcpos_owner_row->user_id;
142 }
143 }
144
145 $wcpos_promoted = 0;
146 $wcpos_collisions = 0;
147 $wcpos_failed = 0;
148 $wcpos_cleaned = 0;
149
150 foreach ( $wcpos_legacy_by_user as $wcpos_user_id => $wcpos_blog_values ) {
151 // All plain rows, matching Pos_Uuid::read_valid_uuid_from_meta — a valid
152 // uuid in ANY row wins even when an invalid duplicate sits in front of it.
153 $wcpos_plain_values = get_user_meta( $wcpos_user_id, Pos_Uuid::META_KEY );
154 $wcpos_has_valid_plain = false;
155 foreach ( (array) $wcpos_plain_values as $wcpos_plain_value ) {
156 if ( Pos_Uuid::is_uuid( $wcpos_plain_value ) ) {
157 $wcpos_has_valid_plain = true;
158
159 break;
160 }
161 }
162
163 $wcpos_write_failed = false;
164
165 if ( ! $wcpos_has_valid_plain ) {
166 ksort( $wcpos_blog_values ); // Ascending blog id, numerically.
167
168 foreach ( $wcpos_blog_values as $wcpos_legacy_value ) {
169 if ( ! Pos_Uuid::is_uuid( $wcpos_legacy_value ) ) {
170 continue;
171 }
172
173 // Owned by a DIFFERENT live user (cloned/imported row): fall through
174 // to the next blog's value rather than serve a duplicate RxDB key.
175 // Users iterate in ascending id order and promotions land in this
176 // map as they happen, so shared values converge deterministically.
177 $wcpos_collides = false;
178 foreach ( $wcpos_owners_by_value[ strtolower( $wcpos_legacy_value ) ] ?? array() as $wcpos_owner_id ) {
179 if ( $wcpos_owner_id !== $wcpos_user_id ) {
180 $wcpos_collides = true;
181
182 break;
183 }
184 }
185 if ( $wcpos_collides ) {
186 ++$wcpos_collisions;
187
188 continue;
189 }
190
191 $wcpos_owned_value = $wcpos_legacy_value;
192 if ( array() === (array) $wcpos_plain_values ) {
193 // Unique add: if #1450's lazy adoption raced us and already wrote
194 // a plain uuid, the add fails and THAT value stands.
195 $wcpos_written = false !== add_user_meta( $wcpos_user_id, Pos_Uuid::META_KEY, $wcpos_legacy_value, true );
196 } else {
197 // Invalid-only plain rows: overwrite, as the lazy path would.
198 $wcpos_written = false !== update_user_meta( $wcpos_user_id, Pos_Uuid::META_KEY, $wcpos_legacy_value );
199 }
200
201 if ( ! $wcpos_written ) {
202 // Re-read before declaring failure — a concurrent writer winning
203 // the unique add is success, just not OUR value.
204 wp_cache_delete( $wcpos_user_id, 'user_meta' );
205 foreach ( (array) get_user_meta( $wcpos_user_id, Pos_Uuid::META_KEY ) as $wcpos_recheck_value ) {
206 if ( Pos_Uuid::is_uuid( $wcpos_recheck_value ) ) {
207 $wcpos_written = true;
208 $wcpos_owned_value = $wcpos_recheck_value;
209
210 break;
211 }
212 }
213 }
214
215 if ( $wcpos_written ) {
216 ++$wcpos_promoted;
217 $wcpos_owners_by_value[ strtolower( $wcpos_owned_value ) ][] = $wcpos_user_id;
218 } else {
219 // Genuine write failure (meta filter veto, DB error). The ladder
220 // is one-shot, so keep this user's legacy rows for the lazy path.
221 $wcpos_write_failed = true;
222 }
223
224 break;
225 }
226 }
227
228 if ( $wcpos_write_failed ) {
229 ++$wcpos_failed;
230
231 continue;
232 }
233
234 foreach ( array_keys( $wcpos_blog_values ) as $wcpos_blog_id ) {
235 delete_user_meta( $wcpos_user_id, Pos_Uuid::META_KEY . '_' . $wcpos_blog_id );
236 }
237 ++$wcpos_cleaned;
238 }
239
240 if ( array() !== $wcpos_legacy_by_user && \function_exists( 'wc_get_logger' ) ) {
241 $wcpos_logger = wc_get_logger();
242 $wcpos_message = \sprintf(
243 'WCPOS 1.10.0 migration: promoted %d legacy per-blog user uuid(s) to the network key, %d collision fallback(s), cleaned legacy rows for %d user(s), %d write failure(s).',
244 $wcpos_promoted,
245 $wcpos_collisions,
246 $wcpos_cleaned,
247 $wcpos_failed
248 );
249 if ( $wcpos_failed > 0 ) {
250 $wcpos_logger->error( $wcpos_message . ' Failed users keep their legacy rows for lazy adoption.', array( 'source' => 'woocommerce-pos' ) );
251 } else {
252 $wcpos_logger->info( $wcpos_message, array( 'source' => 'woocommerce-pos' ) );
253 }
254 }
255
256
257 /*
258 * Seed the activation latches for stores that already exist.
259 *
260 * `pos_app_opened` (#793 Phase 4) reports the site's first open once, guarded
261 * by an option that only new installs are supposed to be missing. Every store
262 * upgrading to 1.10.0 is missing it by definition, so without this their next
263 * POS open would be reported as first-ever, inventing an activation spike out
264 * of the existing user base.
265 *
266 * Seeded unconditionally: this site predates the latch, so its true first open
267 * is unknowable, and an unknown first is better than a wrong one.
268 */
269 add_option( Services\Lifecycle_Events::FIRST_OPEN_OPTION, Services\Lifecycle_Events::LATCH_VALUE, '', true );
270
271 // phpcs:enable
272