refresh_token(); if ( null === $refresh || '' === $refresh || $connection->home_url_changed() ) { return; } wp_remote_post( 'https://oauth2.googleapis.com/revoke', array( 'timeout' => 5, 'body' => array( 'token' => $refresh ), ) ); } catch ( \Throwable $e ) { unset( $e ); // Best effort only. } }; /** * Deletes every option whose name starts with the given prefix, by name. * The names come from a prepared lookup and each one is removed with * delete_option(), which keeps WordPress's option cache correct on its * own. A direct DELETE would leave stale options in the cache, and the * only fix is to flush the whole object cache, which empties it for the * entire site (on some hosts, for every site). * * The prefix is checked again in PHP, which is always case-sensitive. * Most database collations (and SQLite's LIKE) compare ASCII * case-insensitively, so the lookup also returns names that differ from * the prefix only in case (Tagsmith_Foo for tagsmith_), which are not * ours to delete. * * @param string $prefix Literal start of the option names. */ $delete_options_starting_with = static function ( string $prefix ): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- only looks the names up; delete_option() below deletes and invalidates the cache. $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $prefix ) . '%' ) ); foreach ( $names as $name ) { if ( ! str_starts_with( $name, $prefix ) ) { continue; } delete_option( $name ); } }; /** * Deletes every Tagsmith option and transient for the current site * (or, inside a switch_to_blog(), the current blog). */ $delete_tagsmith_data = static function () use ( $revoke_token, $delete_options_starting_with ): void { $revoke_token(); // The Clarity snapshot table (created only once a Data Export token // was saved; dropping a missing table is a no-op). ( new \Tagsmith\Clarity\SnapshotStore() )->drop(); // Today's Clarity calls per project, a network option on multisite // (on a single site the wildcard below catches it too). delete_site_option( \Tagsmith\Clarity\ClaritySync::CALLS_OPTION ); // Every option this plugin has ever added starts with tagsmith_, // so a single prefix lookup catches all of them without needing a // name list to stay in sync. $delete_options_starting_with( 'tagsmith_' ); $delete_options_starting_with( '_transient_tagsmith_' ); $delete_options_starting_with( '_transient_timeout_tagsmith_' ); // With a persistent object cache, transients live in the cache and // not in the options table, so the lookups above find none of them. // The ones with a fixed name are removed by name: the access token // (it holds a credential), and the quota, back-off, lock and error // flags, which would otherwise outlast the plugin by up to an hour. // The rest have a hash or a one-time value in their names (cached // reports, the sign-in hand-over, a failing-page-views flag), so // they cannot be listed; they expire by themselves. $fixed_transients = array( \Tagsmith\Auth\Connection::ACCESS_TRANSIENT, \Tagsmith\Reports\QuotaGuard::QUOTA_LOW, \Tagsmith\Rest\ReportsController::FORCE_LOCK, \Tagsmith\Rest\SystemController::CACHE_CLEAR_LOCK, \Tagsmith\Admin\PostsColumn::ERROR_TRANSIENT, ); // One back-off per API. foreach ( \Tagsmith\Reports\QuotaGuard::APIS as $api ) { $fixed_transients[] = \Tagsmith\Reports\QuotaGuard::backoff_key( $api ); } foreach ( $fixed_transients as $transient ) { delete_transient( $transient ); } }; /** * Deletes the current site's data, but only when it has opted in via * settings.delete_on_uninstall (default true). */ $maybe_delete_current_site = static function () use ( $delete_tagsmith_data ): void { $settings = new \Tagsmith\Settings(); if ( ! (bool) $settings->get( 'delete_on_uninstall' ) ) { return; } $delete_tagsmith_data(); }; /** * Per-site cleanup: pending posts-column warm-up and Clarity sync events * always go (their handlers are being deleted with the plugin); stored * data only when the site opted in. */ $uninstall_current_site = static function () use ( $maybe_delete_current_site ): void { wp_unschedule_hook( 'tagsmith_warm_posts_column' ); wp_unschedule_hook( \Tagsmith\Clarity\ClaritySync::CRON_HOOK ); $maybe_delete_current_site(); }; if ( is_multisite() ) { $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0, ) ); foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); $uninstall_current_site(); restore_current_blog(); } } else { $uninstall_current_site(); } } )();