Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.81% covered (warning)
73.81%
31 / 42
60.00% covered (danger)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Uninstaller
73.17% covered (warning)
73.17%
30 / 41
60.00% covered (danger)
60.00%
3 / 5
16.26
0.00% covered (danger)
0.00%
0 / 1
 run
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
7.10
 cleanup_site
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 cleanup_plugin_transients
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 cleanup_plugin_options
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 cleanup_custom_fields
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Uninstall cleanup: deletes every BeyondWords option, transient and post-meta row.
4 *
5 * @package BeyondWords\Core
6 * @since   3.7.0
7 * @since   7.0.0 Refactored to BeyondWords namespace with snake_case methods.
8 */
9
10declare( strict_types = 1 );
11
12namespace BeyondWords\Core;
13
14defined( 'ABSPATH' ) || exit;
15
16/**
17 * Database cleanup helpers used during plugin uninstall.
18 *
19 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
20 */
21class Uninstaller {
22
23    /**
24     * Run the full uninstall cleanup for every site on the install.
25     *
26     * `uninstall.php` runs once, in the main site's context, yet every value is
27     * stored per-site — so on multisite each site must be visited in turn.
28     *
29     * @since 7.0.0
30     *
31     * @return void
32     */
33    public static function run(): void {
34        if ( ! is_multisite() ) {
35            self::cleanup_site();
36            return;
37        }
38
39        // `number => 0` lifts the default 100-site cap.
40        $site_ids = get_sites(
41            [
42                'fields' => 'ids',
43                'number' => 0,
44            ]
45        );
46
47        foreach ( $site_ids as $site_id ) {
48            switch_to_blog( (int) $site_id );
49            self::cleanup_site();
50            restore_current_blog();
51        }
52    }
53
54    /**
55     * Delete every BeyondWords value for the current site.
56     *
57     * @since 7.0.0
58     *
59     * @return void
60     */
61    private static function cleanup_site(): void {
62        self::cleanup_plugin_transients();
63        self::cleanup_plugin_options();
64        self::cleanup_custom_fields();
65    }
66
67    /**
68     * Delete every BeyondWords transient.
69     *
70     * @return int Rows deleted.
71     */
72    public static function cleanup_plugin_transients(): int {
73        global $wpdb;
74
75        // Transients are stored as `_transient_<key>` + `_transient_timeout_<key>`
76        // option pairs; sweep both prefixes or the timeout rows are orphaned.
77        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
78        $count = $wpdb->query(
79            "DELETE FROM $wpdb->options
80            WHERE `option_name` LIKE '_transient_beyondwords_%'
81            OR `option_name` LIKE '_transient_timeout_beyondwords_%'"
82        );
83
84        return (int) $count;
85    }
86
87    /**
88     * Delete every BeyondWords plugin option (current + deprecated).
89     *
90     * @return int Options deleted.
91     */
92    public static function cleanup_plugin_options(): int {
93        $options = Utils::get_options( 'all' );
94        $total   = 0;
95
96        foreach ( $options as $option ) {
97            // Options are stored per-site via `update_option()`, so `delete_option()`
98            // is the correct call on both single-site and multisite.
99            if ( delete_option( $option ) ) {
100                ++$total;
101            }
102
103            // Defensive: a legacy install may have stored a matching network option.
104            if ( is_multisite() ) {
105                delete_site_option( $option );
106            }
107        }
108
109        return $total;
110    }
111
112    /**
113     * Delete every BeyondWords post-meta value.
114     *
115     * Deletes by meta_id in per-key batches — a single `DELETE … WHERE meta_key
116     * IN (…)` can lock the postmeta table for unacceptably long.
117     *
118     * @return int Meta rows deleted.
119     */
120    public static function cleanup_custom_fields(): int {
121        global $wpdb;
122
123        $fields = Utils::get_post_meta_keys( 'all' );
124        $total  = 0;
125
126        foreach ( $fields as $field ) {
127            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
128            $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT `meta_id` FROM {$wpdb->postmeta} WHERE `meta_key` = %s;", $field ) );
129
130            if ( empty( $meta_ids ) ) {
131                continue;
132            }
133
134            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
135            $count = $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE `meta_id` IN ( " . implode( ',', array_map( 'intval', $meta_ids ) ) . ' );' );
136
137            if ( $count ) {
138                $total += (int) $count;
139            }
140        }
141
142        return $total;
143    }
144}