PluginProbe ʕ •ᴥ•ʔ
WP-Sweep / 2.0.1
WP-Sweep v2.0.1
2.0.1 2.0.0 1.2.0 trunk 1.0.10 1.0.11 1.0.12 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9
wp-sweep / readme.txt
wp-sweep Last commit date
includes 6 days ago js 6 days ago LICENSE 2 weeks ago index.php 2 weeks ago readme.txt 6 days ago uninstall.php 6 days ago wp-sweep.php 6 days ago
readme.txt
243 lines
1 # WP-Sweep
2 Contributors: GamerZ
3 Donate link: https://lesterchan.net/site/donation/
4 Tags: sweep, cleanup, optimize, database, revisions
5 Requires at least: 6.8
6 Tested up to: 7.1
7 Stable tag: 2.0.1
8 Requires PHP: 8.2
9 License: GPLv2 or later
10 License URI: https://www.gnu.org/licenses/gpl-2.0.html
11
12 WP-Sweep allows you to clean up unused, orphaned and duplicated data in your WordPress. It also optimizes your database tables.
13
14 ## Description
15 WP-Sweep finds the rows WordPress leaves behind and removes them. Revisions of posts you edited years ago, meta belonging to a post that was deleted, terms attached to nothing, term relationships pointing at posts that no longer exist, expired transients: none of it is visible anywhere in wp-admin, and all of it is in your database.
16
17 It uses the proper WordPress delete functions wherever they can reach the row, rather than running raw delete queries, so the hooks other plugins rely on still fire. Only the rows the API refuses to touch — orphaned meta whose object ID is `0` — are deleted directly.
18
19 ### Features
20 * Revisions
21 * Auto drafts
22 * Deleted posts
23 * Unapproved comments
24 * Spammed comments
25 * Deleted comments
26 * Orphaned post meta
27 * Orphaned comment meta
28 * Orphaned user meta
29 * Orphaned term meta
30 * Orphaned term relationships
31 * Unused terms
32 * Duplicated post meta
33 * Duplicated comment meta
34 * Duplicated user meta
35 * Duplicated term meta
36 * Transient options
37 * oEmbed caches in post meta
38 * Optimizes database tables
39
40 ### Delete functions used
41 * `wp_delete_post_revision()`
42 * `wp_delete_post()`
43 * `wp_delete_comment()`
44 * `delete_post_meta()`
45 * `delete_comment_meta()`
46 * `delete_user_meta()`
47 * `delete_term_meta()`
48 * `wp_remove_object_terms()`
49 * `wp_delete_term()`
50 * `delete_transient()`
51 * `delete_site_transient()`
52
53 ### Known incompatibilities
54 These plugins keep data in places WP-Sweep reads as orphaned. Protect their meta keys with the filters under Usage before sweeping.
55
56 * [Custom Fonts](https://wordpress.org/plugins/custom-fonts/)
57 * [Elementor Popup Builder](https://elementor.com/features/popup-builder/)
58 * [MailPress](https://wordpress.org/plugins/mailpress/)
59 * [Meta Slider](https://wordpress.org/support/plugin/ml-slider/)
60 * [Polylang](https://wordpress.org/plugins/polylang/)
61 * [Slider Revolution](https://revolution.themepunch.com/)
62 * [Viba Portfolio](https://codecanyon.net/item/viba-portfolio-wordpress-plugin/9561599)
63 * [WPML](https://wpml.org/)
64
65 ### Donations
66 I spent most of my free time creating, updating, maintaining and supporting these plugins, if you really love my plugins and could spare me a couple of bucks, I will really appreciate it. If not feel free to use it without any obligations.
67
68 ## Installation
69
70 1. Install and activate the plugin. The screen appears at `WP-Admin -> Tools -> WP-Sweep`.
71 1. **Back your database up before you sweep anything.** Every sweep is irreversible, and there is no undo.
72
73 ## Usage
74 Every sweep is irreversible, so back your database up first.
75
76 The screen lists every sweep with a short description of what it removes, how many items that is, and what proportion of the table they are. Tick the ones you want and use the **Sweep** bulk action, or use each row's **Sweep** and **Details** buttons one at a time. Nothing on the screen needs JavaScript: the buttons are ordinary links and the bulk action is an ordinary form.
77
78 There is no settings screen. The one thing worth tuning — how many items **Details** lists, 500 by default — is the `wp_sweep_limit_details` filter. The cap exists because the whole sample is held in memory and written into the page.
79
80 ### WP-CLI
81 ~~~
82 wp sweep --all
83 wp sweep revisions
84 wp sweep revisions auto_drafts deleted_posts
85 ~~~
86
87 ### REST API
88 All three routes need the `activate_plugins` capability.
89
90 ~~~
91 GET /wp-json/sweep/v1/count/<name>
92 GET /wp-json/sweep/v1/details/<name>
93 DELETE /wp-json/sweep/v1/sweep/<name>
94 ~~~
95
96 ### Item names
97 `revisions`, `auto_drafts`, `deleted_posts`, `unapproved_comments`, `spam_comments`, `deleted_comments`, `transient_options`, `orphan_postmeta`, `orphan_commentmeta`, `orphan_usermeta`, `orphan_termmeta`, `orphan_term_relationships`, `unused_terms`, `duplicated_postmeta`, `duplicated_commentmeta`, `duplicated_usermeta`, `duplicated_termmeta`, `optimize_database`, `oembed_postmeta`.
98
99 ### Filters
100 * `wp_sweep_postmeta_whitelist` (array) — post meta keys that must never be deleted, by the orphaned or the duplicated post meta sweep. `*` matches any run of characters. Default: empty.
101 * `wp_sweep_commentmeta_whitelist` (array) — the same, for comment meta.
102 * `wp_sweep_usermeta_whitelist` (array) — the same, for user meta.
103 * `wp_sweep_termmeta_whitelist` (array) — the same, for term meta.
104 * `wp_sweep_excluded_taxonomies` (array) — taxonomies left out of the orphaned term relationships sweep. Default: `array( 'link_category' )`.
105 * `wp_sweep_excluded_termids` (array) — term IDs left out of the unused terms sweep. Default: each taxonomy's default term, plus any term that is the parent of another.
106 * `wp_sweep_limit_details` (int) — how many items a Details list shows. Default: 500.
107 * `wp_sweep_defer_counts` (bool) — whether the Sweep screen renders first and fetches its counts afterwards, one at a time. Return `false` to compute every count with the page, as versions before 2.0.1 always did. Default: `true`.
108 * `wp_sweep_capability` (string `$capability`, string `$context`) — the capability required. `$context` is one of `sweep`, `ajax` or `rest`.
109 * `wp_sweep_total_count` (int `$count`, string `$name`) — the total number of rows a sweep's percentage is measured against.
110 * `wp_sweep_count` (int `$count`, string `$name`) — how many items a sweep would remove.
111 * `wp_sweep_details` (array `$details`, string `$name`) — the sample list shown by Details.
112 * `wp_sweep_sweep` (string `$message`, string `$name`) — the message reported after a sweep has run.
113
114 ### Actions
115 `wp_sweep_admin_post_sweep`, `wp_sweep_admin_comment_sweep`, `wp_sweep_admin_user_sweep`, `wp_sweep_admin_term_sweep`, `wp_sweep_admin_option_sweep` and `wp_sweep_admin_database_sweep` all fire below the sweep table, in that order.
116
117 Protect specific post meta keys from being swept:
118
119 ~~~
120 add_filter( 'wp_sweep_postmeta_whitelist', function ( $meta_keys ) {
121 $meta_keys[] = '_my_plugin_setting';
122 $meta_keys[] = '_acme_*';
123 return $meta_keys;
124 } );
125 ~~~
126
127 Exclude an additional taxonomy from the orphaned term relationships sweep:
128
129 ~~~
130 add_filter( 'wp_sweep_excluded_taxonomies', function ( $taxonomies ) {
131 $taxonomies[] = 'product_type';
132 return $taxonomies;
133 } );
134 ~~~
135
136 ## Frequently Asked Questions
137
138 ### The Tools -> Sweep screen has a new address
139
140 It is still under **Tools**, but the address changed from `tools.php?page=wp-sweep/admin.php` to `tools.php?page=wp-sweep`. Update any bookmark.
141
142 The old address was the legacy "plugin file as menu slug" form, which put the plugin's installation directory name into the page URL. That is also why the screen used to break for anyone who installed WP-Sweep under a different directory name — renamed by hand, or unzipped as `wp-sweep-2.0.0`. Neither happens now.
143
144 ### Where did Sweep All go?
145
146 Every row has a checkbox and the table has a **Sweep** bulk action, so ticking the header checkbox and applying it does what Sweep All did — and lets you leave out the ones you do not want.
147
148 ### My snippet calling WPSweep::get_instance() stopped working
149
150 The classes are renamed in 2.0.0:
151
152 * `WPSweep` is now `WP_Sweep`
153 * `WPSweep_Api` is now `WP_Sweep_API`
154 * `WPSweep_Command` is now `WP_Sweep_Command`
155
156 So `WPSweep::get_instance()->sweep( 'revisions' )` becomes `WP_Sweep::get_instance()->sweep( 'revisions' )`. There is no compatibility alias, so the old name raises a fatal error rather than failing quietly.
157
158 Every filter and every `wp_sweep_admin_*_sweep` action keeps the name it had.
159
160 ### Which filters can I use to protect data from being swept?
161
162 Four per-type filters take a list of meta keys that must never be deleted, and each supports `*` as a wildcard:
163
164 ~~~
165 add_filter( 'wp_sweep_postmeta_whitelist', function ( $keys ) {
166 $keys[] = '_my_plugin_setting';
167 $keys[] = '_acme_*';
168 return $keys;
169 } );
170 ~~~
171
172 The same applies to `wp_sweep_commentmeta_whitelist`, `wp_sweep_usermeta_whitelist` and `wp_sweep_termmeta_whitelist`. Terms are protected with `wp_sweep_excluded_termids`, and taxonomies are kept out of the orphaned term relationships sweep with `wp_sweep_excluded_taxonomies`.
173
174 In 2.0.0 these lists also protect the **duplicated** meta sweeps, which the documentation always said they did and the code never did.
175
176 ### A key I protected was swept anyway, or far too much was kept
177
178 Before 2.0.0 the exclusion was a SQL `LIKE` clause, and `LIKE` treats an underscore as a single-character wildcard. A pattern of `_my_key` therefore also matched `Xmy!key`, and a good deal else. Matching happens in PHP now, so an underscore is an underscore and only `*` is a wildcard. Check your list if you were relying on the old behaviour.
179
180 ### Does WP-Sweep leave anything behind when I delete it?
181
182 Nothing. WP-Sweep stores no option rows, creates no database tables, registers no capabilities and schedules no events. `uninstall.php` runs anyway and sweeps up after itself across a whole network rather than the first hundred sites.
183
184 ## Screenshots
185
186 1. Tools -> WP-Sweep, listing every sweep with what it removes and how much of it there is
187 2. A Details list, showing a sample of what one sweep would remove
188 3. The same screen after a bulk sweep, reporting what went
189
190 ## Changelog
191 ### 2.0.1
192 * NEW: A Sweep link on the plugin's row of the Plugins screen, opening Tools -> Sweep.
193 * FIXED: The Sweep screen timed out on large databases. 2.0.0 computed every count before printing a byte — and asked for each table's total row count once per row instead of once, so `SELECT COUNT(*)` ran against the postmeta table four times per load. The screen now renders at once and fetches the counts afterwards, one request at a time, so no single request outlives PHP's time limit. Without JavaScript, a link computes them with the page the way 2.0.0 always did, and the new `wp_sweep_defer_counts` filter restores that behaviour outright.
194 * FIXED: Counting the duplicated meta sweeps fetched every duplicate row's ids into PHP through `GROUP_CONCAT`, just to add them up. On a postmeta table with millions of duplicates that alone could exhaust the request. The count now reads per-key totals only; the ids are read where they are needed, by the sweep that deletes them.
195
196 ### 2.0.0
197 * FIXED: Unused Terms read "unused" off the count column, and core's own counter counts *published* posts — so a term used only by drafts, pending posts, private posts or posts in the trash showed a count of zero and was deleted, taking its relationships with it. Those posts came back untagged with nothing to say why. A term now has to be attached to nothing at all
198 * FIXED: Sweeping the terms of a taxonomy nothing registers any more finished with `DELETE FROM wp_terms WHERE term_id NOT IN ( SELECT term_id FROM wp_term_taxonomy )`, which is every stray row in the table rather than the ones the sweep had just orphaned. Rows the screen had neither counted nor listed were deleted along with them. It names the terms it orphaned now
199 * FIXED: Orphaned Term Relationships decided a row was an orphan by looking for its `object_id` in `wp_posts` — which only asks the right question when the taxonomy belongs to posts. For a taxonomy registered against users or comments, `object_id` is a user or comment ID, so every relationship whose ID happened to outnumber the posts was deleted while the user was still there. `link_category` was excluded by hand for exactly this reason; the exclusion is now derived from what each taxonomy is registered against
200 * FIXED: The oEmbed sweep matched `%_oembed_%`, and in a LIKE pattern an underscore matches any single character — so it meant "any meta key containing oembed with a character either side", and it was a *contains* match rather than a prefix one. Keys belonging to other plugins were hard-deleted along with the caches. WordPress writes these as `_oembed_{hash}`, so the pattern is now anchored at the start with its underscores escaped
201 * FIXED: The oEmbed sweep was the one meta sweep that never consulted the protected-keys list, so a key a site had explicitly added to `wp_sweep_postmeta_whitelist` was deleted anyway — the list was even read for it and then ignored
202 * FIXED: Optimising the database ran `SHOW TABLES` with no prefix, which is every table in the *schema*. On a database shared between installs — an ordinary hosting arrangement — the details view listed every co-tenant's tables, and the sweep issued `OPTIMIZE TABLE` against installs this administrator does not administer. It is scoped to this install's prefix now, with `wp_sweep_optimize_tables` for a site that wants more
203 * BREAKING: Requires WordPress 6.8 and PHP 8.2.
204 * BREAKING: The screen stays under Tools but its address changed, from `tools.php?page=wp-sweep/admin.php` to `tools.php?page=wp-sweep`. The old form put the installation directory name into the URL.
205 * BREAKING: The classes are renamed `WPSweep` -> `WP_Sweep`, `WPSweep_Api` -> `WP_Sweep_API` and `WPSweep_Command` -> `WP_Sweep_Command`. Every filter and action keeps its name.
206 * BREAKING: `WP_Sweep::$limit_details` is gone. Read it with `limit_details()` and change it with the `wp_sweep_limit_details` filter.
207 * BREAKING: The six `wp_sweep_admin_*_sweep` actions fire below the single sweep table, in the order they always had, rather than below six separate ones.
208 * BREAKING: Sweep All is gone. Tick the header checkbox and apply the `Sweep` bulk action instead, which does the same thing and lets you leave rows out.
209 * NEW: Bulk sweeping. Tick the sweeps you want and run them in one go instead of one click at a time.
210 * NEW: The sweeps are grouped on screen. The unfiltered view puts each under a heading with an icon -- Post, Comment, User, Term, Option, Database -- while staying one table, so a single bulk sweep still runs everything you tick. Sorting a column drops the headings rather than leaving them describing rows they no longer group.
211 * NEW: Group filters and sortable columns, from a real `WP_List_Table`.
212 * NEW: The whole screen works with JavaScript turned off. The row actions are ordinary nonced links and the bulk action is an ordinary form post.
213 * NEW: The meta key whitelists protect the duplicated meta sweeps as well as the orphaned ones, which the readme always claimed and the code never did.
214 * NEW: `wp_sweep_capability` and `wp_sweep_limit_details` filters. WP-Sweep has no settings screen: `wp_sweep_limit_details` is how the Details cap is changed.
215 * NEW: Every sweep carries a description of what it removes, shown under its name, and counts worth acting on are emphasised.
216 * NEW: An `uninstall.php` that cleans up across a whole network rather than the first hundred sites. WP-Sweep itself stores nothing: no option rows, no tables, no capabilities and no scheduled events.
217 * NEW: Restructured into `includes/`, following the Plugin Handbook.
218 * NEW: PHPUnit and vitest suites, and GitHub Actions CI across six WordPress and PHP combinations, single site and multisite.
219 * CHANGED: Meta key exclusions are matched in PHP rather than with SQL `LIKE`, so an underscore in a protected key is an underscore rather than a wildcard.
220 * CHANGED: Every database call goes through one method, and every query is prepared.
221 * FIXED: A stored XSS on the admin screen. The Details list was built by string concatenation and injected as HTML, so a comment author name containing markup ran as script in the administrator's browser.
222 * FIXED: The plugin no longer builds paths from its own directory name, so the admin script loads when it is installed under a directory other than `wp-sweep`.
223 * FIXED: Filtering `wp_sweep_excluded_termids` to an empty array produced invalid SQL, leaving the Unused Terms count blank.
224 * FIXED: A term was excluded from the Unused Terms sweep whenever its ID matched a `default_<taxonomy>` option, even if that option pointed at a term that no longer exists. WordPress ships `default_link_category` set to 2, so on most sites whatever term held ID 2 quietly refused to sweep.
225 * FIXED: Sweeping without JavaScript deleted data and displayed nothing; the result is now shown.
226 * FIXED: The multisite uninstall loop stopped at 100 sites, hydrated whole site objects to read one column, and left the switch stack unwound by one.
227 * FIXED: Request parameters are sanitized and validated against the plugin's own list of sweeps.
228 * FIXED: The two "Sweep Complete" messages `wp sweep` prints were the only strings in the plugin that were never passed through the translation functions
229
230 ## Upgrade Notice
231
232 ### 2.0.0
233
234 Requires WordPress 6.8 and PHP 8.2.
235
236 **The screen's address changed**, though it is still at **Tools -> WP-Sweep**: `tools.php?page=wp-sweep/admin.php` is now `tools.php?page=wp-sweep`. The old form embedded the plugin's folder name, which broke the screen for anyone who installed WP-Sweep under a different one.
237
238 **Sweep All is gone.** Every row has a checkbox; tick the one in the header and apply the **Sweep** bulk action for the same effect, with the option of leaving rows out.
239
240 **Code holding the singleton needs editing.** `WPSweep::get_instance()` is now `WP_Sweep::get_instance()`, and the `$sweep->limit_details` property is now the `$sweep->limit_details()` method. The old spellings fail rather than falling back. Filter and action names are unchanged, as are the `wp sweep` command and the `/wp-json/sweep/v1/` routes.
241
242 **Two sweeps remove less than they did, deliberately.** Meta keys protected by the whitelist filters are honoured by the duplicated meta sweeps as well as the orphaned ones, which is what the documentation always said. Matching also moved out of SQL, so an underscore in a protected key is matched literally rather than as a wildcard: `_my_key` protects that key and nothing else. Review your list if you relied on the old behaviour.
243