-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathPost_Revision_Command.php
More file actions
436 lines (384 loc) · 11.8 KB
/
Post_Revision_Command.php
File metadata and controls
436 lines (384 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
<?php
use WP_CLI\Utils;
/**
* Manages post revisions.
*
* ## EXAMPLES
*
* # Restore a post revision
* $ wp post revision restore 123
* Success: Restored revision 123.
*
* # Show diff between two revisions
* $ wp post revision diff 123 456
*
* @package wp-cli
*/
class Post_Revision_Command {
/**
* Valid post fields that can be compared.
*
* @var array<string>
*/
private $valid_fields = [
'post_title',
'post_content',
'post_excerpt',
'post_name',
'post_status',
'post_type',
'post_author',
'post_date',
'post_date_gmt',
'post_modified',
'post_modified_gmt',
'post_parent',
'menu_order',
'comment_status',
'ping_status',
];
/**
* Restores a post revision.
*
* ## OPTIONS
*
* <revision_id>
* : The revision ID to restore.
*
* ## EXAMPLES
*
* # Restore a post revision
* $ wp post revision restore 123
* Success: Restored revision 123.
*
* @subcommand restore
*
* @param array{0: string} $args Positional arguments.
*/
public function restore( $args ) {
$revision_id = (int) $args[0];
// Get the revision post
$revision = wp_get_post_revision( $revision_id );
/**
* Work around https://core.trac.wordpress.org/ticket/64643.
* @var int $revision_id
*/
if ( ! $revision ) {
WP_CLI::error( "Invalid revision ID {$revision_id}." );
}
// Restore the revision
$restored_post_id = wp_restore_post_revision( $revision_id );
// wp_restore_post_revision() returns post ID on success, false on failure, or null if revision is same as current
if ( false === $restored_post_id ) {
WP_CLI::error( "Failed to restore revision {$revision_id}." );
} elseif ( null === $restored_post_id ) {
WP_CLI::warning( "Revision {$revision_id} is the same as the current post. No action taken." );
} else {
WP_CLI::success( "Restored revision {$revision_id}." );
}
}
/**
* Gets a post or revision object by ID.
*
* @param int $id The post or revision ID.
* @param string $name The name to use in error messages ('from' or 'to').
* @return \WP_Post The post or revision object.
*/
private function get_post_or_revision( $id, $name ) {
$post = wp_get_post_revision( $id );
/**
* Work around https://core.trac.wordpress.org/ticket/64643.
* @var int $id
*/
if ( ! $post instanceof \WP_Post ) {
// Try as a regular post
$post = get_post( $id );
if ( ! $post instanceof \WP_Post ) {
WP_CLI::error( "Invalid '{$name}' ID {$id}." );
}
}
return $post;
}
/**
* Shows the difference between two revisions.
*
* ## OPTIONS
*
* <from>
* : The 'from' revision ID or post ID.
*
* [<to>]
* : The 'to' revision ID or post ID. If not provided, compares with the current post.
*
* [--field=<field>]
* : Compare specific field(s). Default: post_content
*
* ## EXAMPLES
*
* # Show diff between two revisions
* $ wp post revision diff 123 456
*
* # Show diff between a revision and the current post
* $ wp post revision diff 123
*
* @subcommand diff
*
* @param array{0: string, 1?: string} $args Positional arguments.
* @param array{field?: string} $assoc_args Associative arguments.
*/
public function diff( $args, $assoc_args ) {
$from_id = (int) $args[0];
$to_id = isset( $args[1] ) ? (int) $args[1] : null;
$field = Utils\get_flag_value( $assoc_args, 'field', 'post_content' );
// Get the 'from' revision or post
$from_revision = $this->get_post_or_revision( $from_id, 'from' );
// Get the 'to' revision or post
$to_revision = null;
if ( $to_id ) {
$to_revision = $this->get_post_or_revision( $to_id, 'to' );
} elseif ( 'revision' === $from_revision->post_type ) {
// If no 'to' ID provided, use the parent post of the revision
$to_revision = get_post( $from_revision->post_parent );
if ( ! $to_revision instanceof \WP_Post ) {
WP_CLI::error( "Could not find parent post for revision {$from_id}." );
}
} else {
WP_CLI::error( "Please provide a 'to' revision ID when comparing posts." );
}
// Validate field
if ( ! in_array( $field, $this->valid_fields, true ) ) {
WP_CLI::error( "Invalid field '{$field}'. Valid fields: " . implode( ', ', $this->valid_fields ) );
}
// Get the field values - use isset to check if field exists on the object
if ( ! isset( $from_revision->{$field} ) ) {
WP_CLI::error( "Field '{$field}' not found on post/revision {$from_id}." );
}
// $to_revision is guaranteed to be non-null at this point due to earlier validation
if ( ! isset( $to_revision->{$field} ) ) {
$to_error_id = $to_id ?? $to_revision->ID;
WP_CLI::error( "Field '{$field}' not found on revision/post {$to_error_id}." );
}
$left_string = $from_revision->{$field};
$right_string = $to_revision->{$field};
// Split content into lines for diff
$left_lines = explode( "\n", $left_string );
$right_lines = explode( "\n", $right_string );
if ( ! class_exists( 'Text_Diff', false ) ) {
// @phpstan-ignore constant.notFound
require ABSPATH . WPINC . '/wp-diff.php';
}
// Create Text_Diff object
$text_diff = new \Text_Diff( 'auto', [ $left_lines, $right_lines ] );
// Check if there are any changes
if ( 0 === $text_diff->countAddedLines() && 0 === $text_diff->countDeletedLines() ) {
WP_CLI::success( 'No difference found.' );
return;
}
// Display header
WP_CLI::line(
WP_CLI::colorize(
sprintf(
'%%y--- %s (%s) - ID %d%%n',
$from_revision->post_title,
$from_revision->post_modified,
$from_revision->ID
)
)
);
WP_CLI::line(
WP_CLI::colorize(
sprintf(
'%%y+++ %s (%s) - ID %d%%n',
$to_revision->post_title,
$to_revision->post_modified,
$to_revision->ID
)
)
);
WP_CLI::line( '' );
// Render the diff using CLI-friendly format
$this->render_cli_diff( $text_diff );
}
/**
* Renders a diff in CLI-friendly format with colors.
*
* @param \Text_Diff $diff The diff object to render.
*/
private function render_cli_diff( $diff ) {
$edits = $diff->getDiff();
foreach ( $edits as $edit ) {
if ( $edit instanceof \Text_Diff_Op_copy ) {
// Unchanged lines - show in default color
foreach ( $edit->orig as $line ) {
WP_CLI::line( ' ' . $line );
}
} elseif ( $edit instanceof \Text_Diff_Op_add ) {
// Added lines - show in green
foreach ( $edit->final as $line ) {
WP_CLI::line( WP_CLI::colorize( '%g+ ' . $line . '%n' ) );
}
} elseif ( $edit instanceof \Text_Diff_Op_delete ) {
// Deleted lines - show in red
foreach ( $edit->orig as $line ) {
WP_CLI::line( WP_CLI::colorize( '%r- ' . $line . '%n' ) );
}
} elseif ( $edit instanceof \Text_Diff_Op_change ) {
// Changed lines - show deletions in red, additions in green
foreach ( $edit->orig as $line ) {
WP_CLI::line( WP_CLI::colorize( '%r- ' . $line . '%n' ) );
}
foreach ( $edit->final as $line ) {
WP_CLI::line( WP_CLI::colorize( '%g+ ' . $line . '%n' ) );
}
}
}
}
/**
* Deletes old post revisions.
*
* ## OPTIONS
*
* [<post-id>...]
* : One or more post IDs to prune revisions for. If not provided, prunes revisions for all posts.
*
* [--latest=<limit>]
* : Keep only the latest N revisions per post. Older revisions will be deleted.
*
* [--earliest=<limit>]
* : Keep only the earliest N revisions per post. Newer revisions will be deleted.
*
* [--yes]
* : Skip confirmation prompt.
*
* ## EXAMPLES
*
* # Delete all but the latest 5 revisions for post 123
* $ wp post revision prune 123 --latest=5
* Success: Deleted 3 revisions for post 123.
*
* # Delete all but the latest 5 revisions for all posts
* $ wp post revision prune --latest=5
* Success: Deleted 150 revisions across 30 posts.
*
* # Delete all but the earliest 2 revisions for posts 123 and 456
* $ wp post revision prune 123 456 --earliest=2
* Success: Deleted 5 revisions for post 123.
* Success: Deleted 3 revisions for post 456.
*
* @subcommand prune
*/
public function prune( $args, $assoc_args ) {
$latest = Utils\get_flag_value( $assoc_args, 'latest', null );
$earliest = Utils\get_flag_value( $assoc_args, 'earliest', null );
// Validate flags
if ( null === $latest && null === $earliest ) {
WP_CLI::error( 'Please specify either --latest or --earliest flag.' );
}
if ( null !== $latest && null !== $earliest ) {
WP_CLI::error( 'Cannot specify both --latest and --earliest flags.' );
}
$limit = $latest ?? $earliest;
$keep_latest = null !== $latest;
if ( ! is_numeric( $limit ) || (int) $limit < 1 ) {
WP_CLI::error( 'Limit must be a positive integer.' );
}
$limit = (int) $limit;
// Get posts to process
if ( ! empty( $args ) ) {
$post_ids = array_map( 'intval', $args );
} else {
// Get all posts that have revisions
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$post_ids = $wpdb->get_col(
"SELECT DISTINCT post_parent FROM {$wpdb->posts} WHERE post_type = 'revision' AND post_parent > 0"
);
$post_ids = array_map( 'intval', $post_ids );
}
if ( empty( $post_ids ) ) {
WP_CLI::warning( 'No posts found with revisions.' );
return;
}
// Confirm deletion if processing multiple posts without --yes flag
if ( count( $post_ids ) > 1 && ! Utils\get_flag_value( $assoc_args, 'yes', false ) ) {
WP_CLI::confirm(
sprintf(
'Are you sure you want to prune revisions for %d posts?',
count( $post_ids )
),
$assoc_args
);
}
$total_deleted = 0;
$posts_processed = 0;
foreach ( $post_ids as $post_id ) {
$deleted = $this->prune_post_revisions( $post_id, $limit, $keep_latest );
if ( false === $deleted ) {
WP_CLI::warning( "Post {$post_id} does not exist or has no revisions." );
continue;
}
if ( $deleted > 0 ) {
++$posts_processed;
$total_deleted += $deleted;
WP_CLI::success( "Deleted {$deleted} revision" . ( $deleted > 1 ? 's' : '' ) . " for post {$post_id}." );
} elseif ( count( $post_ids ) === 1 ) {
WP_CLI::success( "No revisions to delete for post {$post_id}." );
}
}
if ( count( $post_ids ) > 1 ) {
if ( $total_deleted > 0 ) {
WP_CLI::success(
sprintf(
'Deleted %d revision%s across %d post%s.',
$total_deleted,
$total_deleted > 1 ? 's' : '',
$posts_processed,
$posts_processed > 1 ? 's' : ''
)
);
} else {
WP_CLI::success( 'No revisions to delete.' );
}
}
}
/**
* Prunes revisions for a single post.
*
* @param int $post_id The post ID.
* @param int $limit Number of revisions to keep.
* @param bool $keep_latest Whether to keep the latest revisions (true) or earliest (false).
* @return int|false Number of revisions deleted, or false if post not found.
*/
private function prune_post_revisions( $post_id, $limit, $keep_latest ) {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Get all revisions for this post
$revisions = wp_get_post_revisions( $post_id, [ 'order' => 'ASC' ] );
if ( empty( $revisions ) ) {
return false;
}
$revision_count = count( $revisions );
// If we have fewer or equal revisions than the limit, nothing to delete
if ( $revision_count <= $limit ) {
return 0;
}
// Determine which revisions to delete
$revisions_array = array_values( $revisions );
if ( $keep_latest ) {
// Keep the latest N, delete the rest (from beginning)
$to_delete = array_slice( $revisions_array, 0, $revision_count - $limit );
} else {
// Keep the earliest N, delete the rest (from end)
$to_delete = array_slice( $revisions_array, $limit );
}
$deleted = 0;
foreach ( $to_delete as $revision ) {
if ( $revision instanceof \WP_Post && wp_delete_post_revision( $revision->ID ) ) {
++$deleted;
}
}
return $deleted;
}
}