PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
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 1.9.13 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / uninstall.php

uninstall.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at uninstall.php

564 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fired when the plugin is uninstalled (deleted via the WordPress admin).
4 *
5 * WCPOS distinguishes three kinds of data:
6 *
7 * - Derived/operational state — sync tables (rebuildable from WooCommerce
8 * data on reinstall), cron events, transients, the print-job queue, JWT
9 * secrets/tokens, sync watermarks and version latches, plugin-owned upload
10 * directories and log files. Always removed.
11 *
12 * - User-authored configuration — POS settings, receipt templates (and their
13 * revisions, taxonomies, and options), the receipt sequence counter, the
14 * cashier role. Preserved by default (a delete/reinstall cycle must not
15 * destroy a merchant's receipt designs or numbering continuity). Removed
16 * only when a full wipe is requested via the WCPOS_REMOVE_ALL_DATA
17 * constant (wp-config.php) or the wcpos_remove_all_data option.
18 *
19 * - Data owned by others — WCPOS Pro's options (woocommerce_pos_pro_*,
20 * wcpos_stores_migrated) are NEVER touched, even on a full wipe: they
21 * belong to a different plugin. POS metadata on orders/products
22 * (_woocommerce_pos_* post/order meta) is part of WooCommerce's order
23 * history and is also never touched.
24 *
25 * This file runs standalone — the plugin is NOT loaded — so table, option,
26 * hook, and post-type names are hardcoded. Test_Uninstall guards them
27 * against drift from the class constants.
28 *
29 * There is deliberately no is-this-really-an-uninstall guard at the top:
30 * the file only defines functions, and the sweep at the bottom is gated on
31 * WP_UNINSTALL_PLUGIN. Loading the definitions (tests, direct access) has
32 * no side effects.
33 *
34 * @author Paul Kilmurray <paul@kilbot.com.au>
35 *
36 * @see https://wcpos.com
37 * @package WooCommercePOS
38 */
39
40 /**
41 * Plugin table names (without the site prefix) dropped on uninstall.
42 *
43 * Includes the two pre-1.10 legacy tables in case the site skipped the
44 * upgrade that removes them.
45 *
46 * @return string[] Table name suffixes.
47 */
48 function woocommerce_pos_uninstall_table_suffixes(): array {
49 return array(
50 'wcpos_sync_journal',
51 'wcpos_sync_stored_digest',
52 'wcpos_sync_mutations',
53 // Legacy (pre-unified-journal) tables.
54 'wcpos_sync_change_log',
55 'wcpos_sync_order_index',
56 );
57 }
58
59 /**
60 * Cron hooks whose scheduled events are cleared on uninstall.
61 *
62 * @return string[] Hook names.
63 */
64 function woocommerce_pos_uninstall_cron_hooks(): array {
65 return array(
66 'wcpos_sync_journal_purge',
67 'wcpos_print_job_purge',
68 'wcpos_integrity_digest_rebuild',
69 'wcpos_cloud_print_submit',
70 'wcpos_relay_reregister',
71 'wcpos_analytics_group_refresh',
72 // Legacy (pre-unified-journal) purge hook.
73 'wcpos_change_log_purge',
74 );
75 }
76
77 /**
78 * Whether the user opted into removing user-authored configuration too.
79 *
80 * The constant accepts booleans and boolean-ish strings; a pasted
81 * define( 'WCPOS_REMOVE_ALL_DATA', 'no' ) must NOT trigger the wipe, so
82 * both paths validate rather than truthiness-cast.
83 */
84 function woocommerce_pos_uninstall_remove_all_data(): bool {
85 if ( \defined( 'WCPOS_REMOVE_ALL_DATA' ) ) {
86 return (bool) filter_var( WCPOS_REMOVE_ALL_DATA, FILTER_VALIDATE_BOOLEAN );
87 }
88
89 return (bool) filter_var( get_option( 'wcpos_remove_all_data', 'no' ), FILTER_VALIDATE_BOOLEAN );
90 }
91
92 /**
93 * Whether WCPOS Pro is present on this install (active or merely on disk).
94 *
95 * Pro shares the free plugin's log source and translation namespace, so log
96 * and translation cleanup is skipped while Pro may still be using them. The
97 * detection result passes through a filter so tests can pin either state
98 * deterministically, regardless of what exists on the checkout's disk —
99 * during a real uninstall no plugin code is loaded, so the filter is a
100 * pass-through.
101 */
102 function woocommerce_pos_uninstall_pro_installed(): bool {
103 $pro_plugin = 'woocommerce-pos-pro/woocommerce-pos-pro.php';
104 $installed = file_exists( trailingslashit( WP_PLUGIN_DIR ) . $pro_plugin )
105 || in_array( $pro_plugin, (array) get_option( 'active_plugins', array() ), true );
106 if ( ! $installed && \function_exists( 'is_multisite' ) && is_multisite() ) {
107 $network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
108 $installed = isset( $network_plugins[ $pro_plugin ] );
109 }
110
111 return (bool) apply_filters( 'woocommerce_pos_uninstall_pro_installed', $installed );
112 }
113
114 /**
115 * Delete POS session/token/preference user meta.
116 *
117 * Network-wide by nature: wp_usermeta is shared across a multisite network
118 * and these keys are not blog-prefixed, so this runs ONCE, not per site.
119 * Prefix sweep rather than a key list — every key the plugin writes uses
120 * one of these prefixes, so new keys are covered automatically.
121 */
122 function woocommerce_pos_uninstall_user_meta(): void {
123 global $wpdb;
124
125 $patterns = array(
126 $wpdb->esc_like( '_woocommerce_pos_' ) . '%',
127 $wpdb->esc_like( '_wcpos_' ) . '%',
128 $wpdb->esc_like( 'wcpos_' ) . '%',
129 );
130 $where = implode( ' OR ', array_fill( 0, \count( $patterns ), 'meta_key LIKE %s' ) );
131
132 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is placeholders only, prepared here; prefix-scoped uninstall sweep.
133 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->usermeta} WHERE {$where}", $patterns ) );
134 }
135
136 /**
137 * Recursively delete a plugin-owned directory.
138 *
139 * Refuses anything whose basename does not start with the plugin prefix, so
140 * a corrupted path can never delete outside plugin-owned directories.
141 *
142 * @param string $dir Absolute directory path.
143 */
144 function woocommerce_pos_uninstall_rmdir( string $dir ): void {
145 if ( is_link( $dir ) || ! is_dir( $dir ) || 0 !== strpos( basename( $dir ), 'wcpos-' ) ) {
146 return;
147 }
148
149 $items = new RecursiveIteratorIterator(
150 new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ),
151 RecursiveIteratorIterator::CHILD_FIRST
152 );
153 foreach ( $items as $item ) {
154 if ( $item->isDir() && ! $item->isLink() ) {
155 // phpcs:ignore WordPress.WP.AlternativeFunctions -- WP_Filesystem is not initialised during uninstall.
156 rmdir( $item->getPathname() );
157 } else {
158 // phpcs:ignore WordPress.WP.AlternativeFunctions
159 unlink( $item->getPathname() );
160 }
161 }
162 // phpcs:ignore WordPress.WP.AlternativeFunctions
163 rmdir( $dir );
164 }
165
166 /**
167 * Delete every post of a type, plus its revisions, meta, and term relationships.
168 *
169 * Bulk SQL instead of wp_delete_post() per row — the print-job queue can be
170 * large. Children (revisions and their meta, term relationships) go first
171 * because the joins need the parent rows.
172 *
173 * @param string $post_type Post type to delete.
174 */
175 function woocommerce_pos_uninstall_post_type( string $post_type ): void {
176 global $wpdb;
177
178 // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Bulk delete at uninstall.
179 $wpdb->query(
180 $wpdb->prepare(
181 "DELETE pm FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} r ON r.ID = pm.post_id INNER JOIN {$wpdb->posts} p ON p.ID = r.post_parent WHERE r.post_type = 'revision' AND p.post_type = %s",
182 $post_type
183 )
184 );
185 $wpdb->query(
186 $wpdb->prepare(
187 "DELETE r FROM {$wpdb->posts} r INNER JOIN {$wpdb->posts} p ON p.ID = r.post_parent WHERE r.post_type = 'revision' AND p.post_type = %s",
188 $post_type
189 )
190 );
191 $wpdb->query(
192 $wpdb->prepare(
193 "DELETE tr FROM {$wpdb->term_relationships} tr INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id WHERE p.post_type = %s",
194 $post_type
195 )
196 );
197 $wpdb->query(
198 $wpdb->prepare(
199 "DELETE pm FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE p.post_type = %s",
200 $post_type
201 )
202 );
203 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->posts} WHERE post_type = %s", $post_type ) );
204 // phpcs:enable WordPress.DB.DirectDatabaseQuery
205 }
206
207 /**
208 * Delete a plugin taxonomy's terms, term meta, and term-taxonomy rows.
209 *
210 * @param string $taxonomy Taxonomy slug.
211 */
212 function woocommerce_pos_uninstall_taxonomy( string $taxonomy ): void {
213 global $wpdb;
214
215 // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Bulk delete at uninstall.
216 $wpdb->query(
217 $wpdb->prepare(
218 "DELETE tm FROM {$wpdb->termmeta} tm INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = tm.term_id WHERE tt.taxonomy = %s",
219 $taxonomy
220 )
221 );
222 $wpdb->query(
223 $wpdb->prepare(
224 "DELETE t FROM {$wpdb->terms} t INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id WHERE tt.taxonomy = %s",
225 $taxonomy
226 )
227 );
228 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s", $taxonomy ) );
229 // phpcs:enable WordPress.DB.DirectDatabaseQuery
230 }
231
232 /**
233 * Map a raw count onto its reporting band.
234 *
235 * Mirrors Analytics_Profile::COUNT_BANDS, which cannot be used here because no
236 * plugin code is loaded during uninstall. Test_Uninstall pins the two together.
237 *
238 * @param int $count The raw count.
239 *
240 * @return string The band label.
241 */
242 function woocommerce_pos_uninstall_count_band( int $count ): string {
243 foreach ( array(
244 '0' => 0,
245 '1-10' => 10,
246 '11-100' => 100,
247 '101-1000' => 1000,
248 ) as $label => $upper_bound ) {
249 if ( $count <= $upper_bound ) {
250 return $label;
251 }
252 }
253
254 return '1000+';
255 }
256
257 /**
258 * Report the uninstall to product analytics, if the user opted into tracking.
259 *
260 * The plugin is not loaded during uninstall, so this cannot use the Analytics
261 * service and hardcodes the option names and endpoint instead — the same
262 * doctrine the rest of this file follows. Test_Uninstall pins these against
263 * the class constants they mirror.
264 *
265 * Deliberately narrow: consent is read from the stored settings and anything
266 * other than an explicit "allowed" sends nothing. The payload carries no
267 * store data beyond what the deactivation event already reports.
268 *
269 * Must run BEFORE the option sweep, which deletes the consent setting and the
270 * site UUID this depends on.
271 */
272 function woocommerce_pos_uninstall_report(): void {
273 $settings = get_option( 'woocommerce_pos_settings_general', array() );
274 $consent = \is_array( $settings ) && isset( $settings['tracking_consent'] )
275 ? $settings['tracking_consent']
276 : null;
277
278 // Sites that answered before the setting moved to `general` still hold it in
279 // the legacy `tools` option. General_Section::migrate() resolves that in
280 // memory and deliberately never writes it back, so reading `general` alone
281 // would treat an explicitly opted-in legacy site as undecided and silently
282 // drop its event. Mirror the same fallback, same precedence.
283 if ( null === $consent ) {
284 $legacy_tools = get_option( 'woocommerce_pos_settings_tools', array() );
285 $consent = \is_array( $legacy_tools ) && isset( $legacy_tools['tracking_consent'] )
286 ? $legacy_tools['tracking_consent']
287 : 'undecided';
288 }
289
290 if ( 'allowed' !== $consent ) {
291 return;
292 }
293
294 $site_uuid = get_option( 'woocommerce_pos_uuid', '' );
295 if ( ! \is_string( $site_uuid ) || '' === $site_uuid ) {
296 return;
297 }
298
299 // Prefer the acting user's UUID so the event joins the rest of their
300 // history; fall back to the site so an uninstall run by WP-CLI (no current
301 // user) is still counted.
302 $distinct_id = '';
303 if ( \function_exists( 'get_current_user_id' ) ) {
304 $user_id = get_current_user_id();
305 if ( $user_id ) {
306 $distinct_id = (string) get_user_meta( $user_id, '_woocommerce_pos_uuid', true );
307 }
308 }
309 if ( '' === $distinct_id ) {
310 $distinct_id = 'site_' . $site_uuid;
311 }
312
313 // Read the release being deleted from its plugin header. The persisted db
314 // version can be stale when updated files are deleted before version_check().
315 $plugin_data = get_file_data( __DIR__ . '/woocommerce-pos.php', array( 'version' => 'Version' ), 'plugin' );
316 $plugin_version = $plugin_data['version'] ?? '';
317
318 $installed_at = (int) get_option( 'woocommerce_pos_installed_at', 0 );
319 $properties = array(
320 '$groups' => array( 'site' => $site_uuid ),
321 'plugin_version' => \is_string( $plugin_version ) ? $plugin_version : '',
322 'locale' => get_locale(),
323 );
324
325 if ( $installed_at > 0 ) {
326 $properties['days_since_install'] = max( 0, (int) floor( ( time() - $installed_at ) / DAY_IN_SECONDS ) );
327 }
328
329 // Banded, matching Analytics_Profile — an exact order count never leaves.
330 // The group refresh persists the band precisely so this does not depend on a
331 // warm cache; the hourly landing-profile transient is only a fallback for
332 // sites that have not refreshed since this shipped. Neither is present on a
333 // site that never consented, which never reaches this line anyway.
334 $band = get_option( 'woocommerce_pos_analytics_order_band', '' );
335 if ( \is_string( $band ) && '' !== $band ) {
336 $properties['order_count_band'] = $band;
337 } else {
338 $profile = get_transient( 'wcpos_landing_profile' );
339 if ( \is_array( $profile ) && isset( $profile['order_count'] ) ) {
340 $properties['order_count_band'] = woocommerce_pos_uninstall_count_band( (int) $profile['order_count'] );
341 }
342 }
343
344 // Mirror Analytics::get_token() / get_host(): constant first, then the
345 // filter. The plugin is not loaded, but a mu-plugin or wp-config define can
346 // still point a self-hosted deployment at its own project, and sending its
347 // uninstall events to the default project instead would be wrong twice over.
348 $token = \defined( 'WCPOS_POSTHOG_TOKEN' ) ? (string) WCPOS_POSTHOG_TOKEN : 'phc_BhTJzZ7fXMqcD4MiaUJQsQqPkEpu94yoSAthXFBWemvd';
349 $token = (string) apply_filters( 'woocommerce_pos_posthog_token', $token );
350
351 $body = wp_json_encode(
352 array(
353 'api_key' => $token,
354 'event' => 'wcpos_uninstalled',
355 'distinct_id' => $distinct_id,
356 'properties' => $properties,
357 'timestamp' => gmdate( 'c' ),
358 )
359 );
360
361 if ( false === $body ) {
362 return;
363 }
364
365 $host = \defined( 'WCPOS_POSTHOG_HOST' ) ? (string) WCPOS_POSTHOG_HOST : 'https://ph.wcpos.com';
366 $host = (string) apply_filters( 'woocommerce_pos_posthog_host', $host );
367
368 wp_remote_post(
369 untrailingslashit( $host ) . '/capture/',
370 array(
371 'blocking' => false,
372 'timeout' => 2.0,
373 'headers' => array( 'Content-Type' => 'application/json' ),
374 'body' => $body,
375 )
376 );
377 }
378
379 /**
380 * Remove WCPOS data for the current site.
381 *
382 * User meta and the object-cache flush are handled once at the network
383 * level (see the bottom of this file), not here.
384 *
385 * @param bool|null $remove_all Also remove user-authored configuration
386 * (settings, templates, receipt sequence, role).
387 * Defaults to woocommerce_pos_uninstall_remove_all_data().
388 */
389 function woocommerce_pos_uninstall_site( ?bool $remove_all = null ): void {
390 global $wpdb;
391
392 // Read the opt-in BEFORE the option sweep below deletes it.
393 if ( null === $remove_all ) {
394 $remove_all = woocommerce_pos_uninstall_remove_all_data();
395 }
396
397 // 1. Clear scheduled events (all events per hook, regardless of args).
398 foreach ( woocommerce_pos_uninstall_cron_hooks() as $hook ) {
399 wp_unschedule_hook( $hook );
400 }
401
402 // 2. Drop plugin tables. All are derived from WooCommerce data and are
403 // rebuilt on reinstall.
404 foreach ( woocommerce_pos_uninstall_table_suffixes() as $suffix ) {
405 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}{$suffix}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- Known plugin table names; uninstall context.
406 }
407
408 // 3. Delete plugin post types: the print-job queue always; receipt
409 // templates (including revisions and taxonomies) only on a full wipe.
410 woocommerce_pos_uninstall_post_type( 'wcpos_print_job' );
411 if ( $remove_all ) {
412 woocommerce_pos_uninstall_post_type( 'wcpos_template' );
413 woocommerce_pos_uninstall_taxonomy( 'wcpos_template_type' );
414 woocommerce_pos_uninstall_taxonomy( 'wcpos_template_category' );
415 }
416
417 // 4. Roles and capabilities. Kept by default: removing the cashier role
418 // would strand every user assigned to it, so that only happens on a full
419 // wipe. (Deactivation already strips the two access caps.)
420 if ( $remove_all && \function_exists( 'wp_roles' ) ) {
421 foreach ( wp_roles()->role_objects as $role ) {
422 foreach ( array_keys( (array) $role->capabilities ) as $cap ) {
423 if ( false !== strpos( $cap, 'woocommerce_pos' ) || false !== strpos( $cap, 'wcpos_store' ) ) {
424 $role->remove_cap( $cap );
425 }
426 }
427 }
428 remove_role( 'cashier' );
429 }
430
431 // Pro bundles the free core, so artifacts shared with it (translations,
432 // their cache transients, WooCommerce logs) stay while Pro is installed.
433 $pro_installed = woocommerce_pos_uninstall_pro_installed();
434
435 // 5. Delete plugin options and transients. Both plugin prefixes are
436 // swept; WCPOS Pro's data is ALWAYS excluded (it belongs to a different
437 // plugin), and user-authored configuration is excluded unless $remove_all.
438 $patterns = array(
439 $wpdb->esc_like( 'wcpos_' ) . '%',
440 $wpdb->esc_like( 'woocommerce_pos_' ) . '%',
441 $wpdb->esc_like( '_transient_wcpos_' ) . '%',
442 $wpdb->esc_like( '_transient_timeout_wcpos_' ) . '%',
443 $wpdb->esc_like( '_transient_woocommerce_pos_' ) . '%',
444 $wpdb->esc_like( '_transient_timeout_woocommerce_pos_' ) . '%',
445 );
446
447 $preserved = array(
448 // WCPOS Pro's data — never ours to delete.
449 $wpdb->esc_like( 'woocommerce_pos_pro_' ) . '%',
450 $wpdb->esc_like( 'wcpos_pro_' ) . '%',
451 $wpdb->esc_like( 'wcpos_stores_migrated' ),
452 $wpdb->esc_like( '_transient_woocommerce_pos_pro_' ) . '%',
453 $wpdb->esc_like( '_transient_timeout_woocommerce_pos_pro_' ) . '%',
454 $wpdb->esc_like( '_transient_wcpos_i18n_woocommerce-pos-pro_' ) . '%',
455 $wpdb->esc_like( '_transient_timeout_wcpos_i18n_woocommerce-pos-pro_' ) . '%',
456 // WooCommerce core POS settings — owned by WooCommerce 10.5+.
457 $wpdb->esc_like( 'woocommerce_pos_store_name' ),
458 $wpdb->esc_like( 'woocommerce_pos_store_phone' ),
459 $wpdb->esc_like( 'woocommerce_pos_store_email' ),
460 $wpdb->esc_like( 'woocommerce_pos_refund_returns_policy' ),
461 );
462 if ( $pro_installed ) {
463 // Version cache for the shared `woocommerce-pos` translations kept below.
464 $preserved[] = $wpdb->esc_like( '_transient_wcpos_i18n_woocommerce-pos_' ) . '%';
465 $preserved[] = $wpdb->esc_like( '_transient_timeout_wcpos_i18n_woocommerce-pos_' ) . '%';
466 }
467 if ( ! $remove_all ) {
468 $preserved = array_merge(
469 $preserved,
470 array(
471 $wpdb->esc_like( 'woocommerce_pos_settings_' ) . '%',
472 $wpdb->esc_like( 'wcpos_active_template_' ) . '%',
473 $wpdb->esc_like( 'wcpos_template_order_' ) . '%',
474 $wpdb->esc_like( 'wcpos_disabled_virtual_templates_' ) . '%',
475 $wpdb->esc_like( 'wcpos_receipt_sequence_counter' ),
476 )
477 );
478 }
479
480 $where = '(' . implode( ' OR ', array_fill( 0, \count( $patterns ), 'option_name LIKE %s' ) ) . ')';
481 $where .= str_repeat( ' AND option_name NOT LIKE %s', \count( $preserved ) );
482
483 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is placeholders only, prepared here; prefix-scoped uninstall sweep.
484 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE {$where}", array_merge( $patterns, $preserved ) ) );
485
486 // 6. Plugin-owned files: downloaded translations, template render cache,
487 // dompdf scratch, and this plugin's unshared WooCommerce logs.
488 $log_table = $wpdb->prefix . 'woocommerce_log';
489 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WooCommerce's known per-site log table; uninstall context.
490 $log_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $log_table ) ) );
491 if ( ! $pro_installed && $log_table === $log_table_exists ) {
492 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Removing exact-source operational logs at uninstall.
493 $wpdb->delete( $log_table, array( 'source' => 'woocommerce-pos' ), array( '%s' ) );
494 }
495
496 // The custom i18n service downloads only .l10n.php files; WordPress owns
497 // any core-managed .mo and hashed .json artifacts in this directory.
498 // The `woocommerce-pos` text domain is shared with Pro's bundled core, so
499 // its translations stay in place while Pro remains installed.
500 if ( ! $pro_installed ) {
501 $language_files = glob( trailingslashit( WP_LANG_DIR ) . 'plugins/woocommerce-pos-*.l10n.php' );
502 foreach ( is_array( $language_files ) ? $language_files : array() as $language_file ) {
503 if ( 1 !== preg_match( '/^woocommerce-pos-[a-z]{2,3}(?:_[A-Za-z0-9]+)*(?:@[A-Za-z0-9]+)?\.l10n\.php$/', basename( $language_file ) ) ) {
504 continue;
505 }
506 // phpcs:ignore WordPress.WP.AlternativeFunctions -- WP_Filesystem is not initialised during uninstall.
507 unlink( $language_file );
508 }
509 }
510
511 $uploads = wp_upload_dir( null, false );
512 if ( empty( $uploads['error'] ) && ! empty( $uploads['basedir'] ) ) {
513 // The fallback language directory also holds Pro's own downloads.
514 if ( ! $pro_installed ) {
515 woocommerce_pos_uninstall_rmdir( trailingslashit( $uploads['basedir'] ) . 'wcpos-languages' );
516 }
517 woocommerce_pos_uninstall_rmdir( trailingslashit( $uploads['basedir'] ) . 'wcpos-templates' );
518
519 if ( ! $pro_installed ) {
520 $log_files = glob( trailingslashit( $uploads['basedir'] ) . 'wc-logs/woocommerce-pos-*.log' );
521 foreach ( \is_array( $log_files ) ? $log_files : array() as $log_file ) {
522 if ( 1 !== preg_match( '/^woocommerce-pos-\d{4}-\d{2}-\d{2}-/', basename( $log_file ) ) ) {
523 continue;
524 }
525 // phpcs:ignore WordPress.WP.AlternativeFunctions -- WP_Filesystem is not initialised during uninstall.
526 unlink( $log_file );
527 }
528 }
529 }
530 woocommerce_pos_uninstall_rmdir( rtrim( get_temp_dir(), '/\\' ) . '/wcpos-dompdf' );
531 }
532
533 // Run the sweep only when WordPress is actually uninstalling the plugin.
534 if ( \defined( 'WP_UNINSTALL_PLUGIN' ) ) {
535 // Report churn ONCE, before anything is deleted: the report needs the
536 // consent setting, the site UUID and the user meta that the sweep removes,
537 // and firing it per site would put a network request in front of every blog
538 // of a large multisite uninstall.
539 woocommerce_pos_uninstall_report();
540
541 if ( \function_exists( 'is_multisite' ) && is_multisite() ) {
542 // number => 0 removes WP_Site_Query's default 100-site cap.
543 $woocommerce_pos_sites = get_sites(
544 array(
545 'fields' => 'ids',
546 'number' => 0,
547 )
548 );
549
550 foreach ( $woocommerce_pos_sites as $woocommerce_pos_site_id ) {
551 switch_to_blog( (int) $woocommerce_pos_site_id );
552 woocommerce_pos_uninstall_site();
553 restore_current_blog();
554 }
555 } else {
556 woocommerce_pos_uninstall_site();
557 }
558
559 // Network-wide surfaces, once: user meta is a shared table, and the
560 // object-cache flush is global.
561 woocommerce_pos_uninstall_user_meta();
562 wp_cache_flush();
563 }
564