'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); self::site(); restore_current_blog(); } } /** * Clean up the current site. * * @return void */ public static function site(): void { self::revoke(); self::unschedule(); self::delete_rules(); self::drop_tables(); self::delete_options(); } /** * Tell smsportal.app to revoke the key. Failures are ignored: the data goes either way. * * @return void */ private static function revoke(): void { $connection = get_option( 'smsportal_connection' ); $api_key = is_array( $connection ) && isset( $connection['api_key'] ) && is_string( $connection['api_key'] ) ? $connection['api_key'] : ''; if ( '' === $api_key ) { return; } // A copy of the site (staging, a clone) holds the original's key: revoking it would cut the original off. $home = is_array( $connection ) && isset( $connection['home'] ) && is_string( $connection['home'] ) ? $connection['home'] : ''; if ( '' !== $home && untrailingslashit( $home ) !== untrailingslashit( (string) get_option( 'home' ) ) ) { return; } $server = untrailingslashit( defined( 'SMSPORTAL_URL' ) ? (string) SMSPORTAL_URL : 'https://smsportal.app' ); wp_remote_request( $server . '/api/v1/integrations/connection', array( 'method' => 'DELETE', 'timeout' => self::REVOKE_TIMEOUT, 'redirection' => 0, 'headers' => array( 'x-api-key' => $api_key ), ) ); } /** * Remove WP-Cron events and Action Scheduler actions of the plugin. * * @return void */ private static function unschedule(): void { foreach ( self::HOOKS as $hook ) { wp_unschedule_hook( $hook ); } if ( function_exists( 'as_unschedule_all_actions' ) && self::has_action_scheduler_tables() ) { as_unschedule_all_actions( '', array(), self::GROUP ); } } /** * Whether this site has Action Scheduler's tables (on a network, not every site does). * * @return bool */ private static function has_action_scheduler_tables(): bool { global $wpdb; $table = $wpdb->prefix . 'actionscheduler_actions'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one check during uninstall. return $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); } /** * Delete the rules with their meta, in any status. * * @return void */ private static function delete_rules(): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- every status, including trash and auto-draft. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", self::RULE_POST_TYPE ) ); foreach ( is_array( $ids ) ? $ids : array() as $id ) { wp_delete_post( (int) $id, true ); } } /** * Drop the plugin's tables. * * @return void */ private static function drop_tables(): void { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- dropping the plugin's own tables. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}smsportal_messages" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}smsportal_replies" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}smsportal_connection_log" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}smsportal_blocklist" ); // phpcs:enable } /** * Delete the options and every smsportal_* option and transient (connection flow state, notices, * caches, webhook de-duplication). * * @return void */ private static function delete_options(): void { global $wpdb; foreach ( self::OPTIONS as $option ) { delete_option( $option ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- names by prefix; the caches are cleared below. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( 'smsportal_' ) . '%', $wpdb->esc_like( '_transient_smsportal_' ) . '%', $wpdb->esc_like( '_transient_timeout_smsportal_' ) . '%' ) ); wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); } }