wp-studio 1.7.9 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/{_events-ByiJRMQ1.mjs → _events-BcapW3eh.mjs} +5 -5
- package/dist/cli/{certificate-manager-Bp1E0km4.mjs → certificate-manager-SVYcCL_i.mjs} +6 -1
- package/dist/cli/{delete-D1lAYFtg.mjs → delete-D1924O3o.mjs} +8 -4
- package/dist/cli/{helpers-Bh-WikQr.mjs → helpers-oQuItT8n.mjs} +4 -153
- package/dist/cli/{index-CyYXE85k.mjs → index-4lan3TI_.mjs} +24 -4
- package/dist/cli/{index-DBCpWm8k.mjs → index-BjzOJKPi.mjs} +525 -256
- package/dist/cli/{index-BiCiEz-r.mjs → index-DRQnCQvM.mjs} +510 -365
- package/dist/cli/{list-COLca0rr.mjs → list-DOFyyV1f.mjs} +4 -3
- package/dist/cli/{login-OvZJPTV5.mjs → login-BtPZeZ4G.mjs} +3 -3
- package/dist/cli/{logout-BaG-QFSN.mjs → logout-Cr631QzG.mjs} +3 -3
- package/dist/cli/main.mjs +2 -2
- package/dist/cli/paths-CqXGLB7R.mjs +195 -0
- package/dist/cli/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/cli/plugin/skills/need-for-speed/SKILL.md +55 -0
- package/dist/cli/plugin/skills/site-spec/SKILL.md +35 -0
- package/dist/cli/plugin/skills/taxonomist/SKILL.md +270 -0
- package/dist/cli/plugin/skills/taxonomist/scripts/apply-changes.php +223 -0
- package/dist/cli/plugin/skills/taxonomist/scripts/backup.php +112 -0
- package/dist/cli/plugin/skills/taxonomist/scripts/export-posts.php +119 -0
- package/dist/cli/plugin/skills/taxonomist/scripts/restore.php +233 -0
- package/dist/cli/process-manager-daemon.mjs +11 -4
- package/dist/cli/{process-manager-ipc-heiF195f.mjs → process-manager-ipc-BisO0qtU.mjs} +1 -1
- package/dist/cli/proxy-daemon.mjs +1 -1
- package/dist/cli/prune-pm-logs-COryxqeo.mjs +41 -0
- package/dist/cli/resume-BwDwdJtq.mjs +113 -0
- package/dist/cli/{rewrite-wp-cli-post-content-DH3hRTU5.mjs → rewrite-wp-cli-post-content-2zlfFnKT.mjs} +1 -1
- package/dist/cli/{set-DY9OcXFg.mjs → set-D5eeqHbp.mjs} +3 -3
- package/dist/cli/{set-BX9MWFxi.mjs → set-DYnzUz_G.mjs} +4 -4
- package/dist/cli/{status-CgY39wpU.mjs → status-DNvMZBqD.mjs} +2 -2
- package/dist/cli/{well-known-paths-CG_o9mSO.mjs → well-known-paths-BYA1Bw5o.mjs} +1 -1
- package/dist/cli/wordpress-server-child.mjs +2 -2
- package/dist/cli/{wp-DeUSBbLc.mjs → wp-DD2-QiiP.mjs} +2 -2
- package/dist/cli/wp-files/latest/available-site-translations.json +1 -1
- package/dist/cli/wp-files/skills/STUDIO.md +1 -1
- package/dist/cli/wp-files/skills/studio-cli/SKILL.md +1 -1
- package/package.json +9 -10
- package/patches/@mariozechner+pi-tui+0.54.0.patch +12 -0
- package/scripts/postinstall-npm.mjs +1 -0
- package/dist/cli/paths-BPK_RySX.mjs +0 -31
- package/dist/cli/resume-DshNzC7q.mjs +0 -62
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Apply category changes from an AI-generated suggestions file.
|
|
4
|
+
*
|
|
5
|
+
* Reads a JSON file of per-post category suggestions and applies them.
|
|
6
|
+
* Categories are resolved by slug to prevent drift between the
|
|
7
|
+
* export/analysis and apply phases.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* TAXONOMIST_SUGGESTIONS=/path/to/suggestions.json studio wp eval-file apply-changes.php
|
|
11
|
+
*
|
|
12
|
+
* Environment variables:
|
|
13
|
+
* TAXONOMIST_SUGGESTIONS Path to the suggestions JSON file. Required.
|
|
14
|
+
* Format: [{"post_id": 123, "cats": ["wordpress", "ai"]}, ...]
|
|
15
|
+
* Values in "cats" are category slugs.
|
|
16
|
+
* TAXONOMIST_LOG Path for the change log TSV.
|
|
17
|
+
* Default: /tmp/taxonomist-changes.tsv
|
|
18
|
+
* TAXONOMIST_MODE "preview" (default) shows what would change.
|
|
19
|
+
* "apply" executes the changes.
|
|
20
|
+
* TAXONOMIST_REMOVE_CATS Comma-separated category slugs to strip from
|
|
21
|
+
* posts that receive new suggestions.
|
|
22
|
+
*
|
|
23
|
+
* @package Taxonomist
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
$suggestions_file = getenv( 'TAXONOMIST_SUGGESTIONS' );
|
|
27
|
+
$log_file = getenv( 'TAXONOMIST_LOG' ) ? getenv( 'TAXONOMIST_LOG' ) : '/tmp/taxonomist-changes.tsv';
|
|
28
|
+
$apply_mode = getenv( 'TAXONOMIST_MODE' ) ? getenv( 'TAXONOMIST_MODE' ) : 'preview';
|
|
29
|
+
$remove_cats_str = getenv( 'TAXONOMIST_REMOVE_CATS' ) ? getenv( 'TAXONOMIST_REMOVE_CATS' ) : '';
|
|
30
|
+
|
|
31
|
+
if ( ! $suggestions_file || ! file_exists( $suggestions_file ) ) {
|
|
32
|
+
WP_CLI::error( 'Set TAXONOMIST_SUGGESTIONS to the suggestions JSON path' );
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$suggestions_json = file_get_contents( $suggestions_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
|
36
|
+
if ( false === $suggestions_json ) {
|
|
37
|
+
WP_CLI::error( 'Failed to read suggestions file' );
|
|
38
|
+
}
|
|
39
|
+
$suggestions = json_decode( $suggestions_json, true );
|
|
40
|
+
if ( null === $suggestions && JSON_ERROR_NONE !== json_last_error() ) {
|
|
41
|
+
WP_CLI::error( 'Failed to parse suggestions file' );
|
|
42
|
+
}
|
|
43
|
+
if ( ! is_array( $suggestions ) ) {
|
|
44
|
+
WP_CLI::error( 'Suggestions file must decode to a JSON array' );
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$all_cats = get_terms(
|
|
48
|
+
array(
|
|
49
|
+
'taxonomy' => 'category',
|
|
50
|
+
'hide_empty' => false,
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
$slug_to_id = array();
|
|
54
|
+
$slug_to_name = array();
|
|
55
|
+
foreach ( $all_cats as $t ) {
|
|
56
|
+
$slug_to_id[ $t->slug ] = $t->term_id;
|
|
57
|
+
$slug_to_name[ $t->slug ] = $t->name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$remove_slugs = array_filter( array_map( 'trim', explode( ',', $remove_cats_str ) ) );
|
|
61
|
+
$remove_ids = array();
|
|
62
|
+
foreach ( $remove_slugs as $slug ) {
|
|
63
|
+
if ( isset( $slug_to_id[ $slug ] ) ) {
|
|
64
|
+
$remove_ids[] = $slug_to_id[ $slug ];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$default_cat_id = (int) get_option( 'default_category' );
|
|
69
|
+
if ( in_array( $default_cat_id, $remove_ids, true ) ) {
|
|
70
|
+
$default_term = get_term( $default_cat_id, 'category' );
|
|
71
|
+
$default_name = $default_term ? $default_term->name : "ID $default_cat_id";
|
|
72
|
+
WP_CLI::error(
|
|
73
|
+
"TAXONOMIST_REMOVE_CATS includes '$default_name' which is the site's default category. " .
|
|
74
|
+
'Change the default category setting first (wp option update default_category NEW_ID), then retry.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$unresolved = array();
|
|
79
|
+
foreach ( $suggestions as $suggestion ) {
|
|
80
|
+
$suggested_refs = isset( $suggestion['cats'] ) ? $suggestion['cats'] : array();
|
|
81
|
+
foreach ( $suggested_refs as $ref ) {
|
|
82
|
+
if ( ! isset( $slug_to_id[ $ref ] ) ) {
|
|
83
|
+
$unresolved[ $ref ] = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if ( ! empty( $unresolved ) ) {
|
|
88
|
+
$list = implode( ', ', array_keys( $unresolved ) );
|
|
89
|
+
WP_CLI::error(
|
|
90
|
+
"Taxonomy drift detected: these categories from the suggestions do not exist in the live site: $list. " .
|
|
91
|
+
'Re-export and re-analyze, or create the missing categories first.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
|
96
|
+
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
|
97
|
+
$log = fopen( $log_file, 'w' );
|
|
98
|
+
if ( false === $log ) {
|
|
99
|
+
WP_CLI::error( 'Failed to open log file for writing: ' . $log_file );
|
|
100
|
+
}
|
|
101
|
+
$header_written = fputcsv( $log, array( 'timestamp', 'action', 'post_id', 'post_title', 'old_categories', 'new_categories', 'cats_added', 'cats_removed' ), "\t" );
|
|
102
|
+
if ( false === $header_written ) {
|
|
103
|
+
fclose( $log );
|
|
104
|
+
WP_CLI::error( 'Failed to write log header to ' . $log_file );
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$changes = 0;
|
|
108
|
+
$skipped = 0;
|
|
109
|
+
$error_count = 0;
|
|
110
|
+
|
|
111
|
+
foreach ( $suggestions as $suggestion ) {
|
|
112
|
+
$current_post_id = isset( $suggestion['post_id'] ) ? $suggestion['post_id'] : $suggestion['id'];
|
|
113
|
+
$suggested_refs = isset( $suggestion['cats'] ) ? $suggestion['cats'] : array();
|
|
114
|
+
|
|
115
|
+
if ( empty( $suggested_refs ) ) {
|
|
116
|
+
++$skipped;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$current_post = get_post( $current_post_id );
|
|
121
|
+
if ( ! $current_post ) {
|
|
122
|
+
++$error_count;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$current_ids = wp_get_post_categories( $current_post_id );
|
|
127
|
+
$current_names = array();
|
|
128
|
+
foreach ( $current_ids as $cid ) {
|
|
129
|
+
$t = get_term( $cid, 'category' );
|
|
130
|
+
if ( $t && ! is_wp_error( $t ) ) {
|
|
131
|
+
$current_names[ $cid ] = $t->name;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$kept_ids = array();
|
|
136
|
+
foreach ( $current_ids as $cid ) {
|
|
137
|
+
if ( ! in_array( $cid, $remove_ids, true ) ) {
|
|
138
|
+
$kept_ids[] = $cid;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
$suggested_ids = array();
|
|
143
|
+
foreach ( $suggested_refs as $ref ) {
|
|
144
|
+
if ( isset( $slug_to_id[ $ref ] ) ) {
|
|
145
|
+
$suggested_ids[] = $slug_to_id[ $ref ];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
$new_ids = array_values( array_unique( array_merge( array_values( $kept_ids ), $suggested_ids ) ) );
|
|
150
|
+
|
|
151
|
+
$sorted_current = $current_ids;
|
|
152
|
+
$sorted_new = $new_ids;
|
|
153
|
+
sort( $sorted_current );
|
|
154
|
+
sort( $sorted_new );
|
|
155
|
+
if ( $sorted_current === $sorted_new ) {
|
|
156
|
+
++$skipped;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$added_ids = array_diff( $new_ids, $current_ids );
|
|
161
|
+
$removed_ids = array_diff( $current_ids, $new_ids );
|
|
162
|
+
|
|
163
|
+
$added_names = array();
|
|
164
|
+
foreach ( $added_ids as $aid ) {
|
|
165
|
+
$t = get_term( $aid, 'category' );
|
|
166
|
+
if ( $t ) {
|
|
167
|
+
$added_names[] = $t->name;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$removed_names = array();
|
|
172
|
+
foreach ( $removed_ids as $rid ) {
|
|
173
|
+
$removed_names[] = isset( $current_names[ $rid ] ) ? $current_names[ $rid ] : '?';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
$new_names = array();
|
|
177
|
+
foreach ( $new_ids as $nid ) {
|
|
178
|
+
$t = get_term( $nid, 'category' );
|
|
179
|
+
if ( $t ) {
|
|
180
|
+
$new_names[] = $t->name;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$ts = gmdate( 'Y-m-d H:i:s' );
|
|
185
|
+
$log_row = array(
|
|
186
|
+
$ts,
|
|
187
|
+
'SET_CATS',
|
|
188
|
+
$current_post_id,
|
|
189
|
+
$current_post->post_title,
|
|
190
|
+
implode( '|', array_values( $current_names ) ),
|
|
191
|
+
implode( '|', $new_names ),
|
|
192
|
+
implode( '|', $added_names ),
|
|
193
|
+
implode( '|', $removed_names ),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if ( 'apply' === $apply_mode ) {
|
|
197
|
+
$set_result = wp_set_post_categories( $current_post_id, $new_ids );
|
|
198
|
+
if ( is_wp_error( $set_result ) || false === $set_result ) {
|
|
199
|
+
++$error_count;
|
|
200
|
+
WP_CLI::warning( 'Failed to set categories for post ID ' . $current_post_id );
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
$log_result = fputcsv( $log, $log_row, "\t" );
|
|
206
|
+
if ( false === $log_result ) {
|
|
207
|
+
fclose( $log );
|
|
208
|
+
WP_CLI::error( 'Failed to write change log row for post ID ' . $current_post_id );
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
++$changes;
|
|
212
|
+
if ( 0 === $changes % 200 ) {
|
|
213
|
+
WP_CLI::log( "Processed $changes changes..." );
|
|
214
|
+
wp_cache_flush();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fclose( $log );
|
|
219
|
+
// phpcs:enable
|
|
220
|
+
|
|
221
|
+
$verb = ( 'apply' === $apply_mode ) ? 'Applied' : 'Would apply';
|
|
222
|
+
WP_CLI::success( "$verb $changes changes. Skipped: $skipped. Errors: $error_count." );
|
|
223
|
+
WP_CLI::log( 'Log: ' . $log_file );
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Create a complete backup of the current taxonomy state.
|
|
4
|
+
*
|
|
5
|
+
* Captures every category term with its metadata and the exact category
|
|
6
|
+
* assignments for every published post. Together these allow a full restore
|
|
7
|
+
* to the pre-change state.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* studio wp eval-file backup.php
|
|
11
|
+
*
|
|
12
|
+
* Environment variables:
|
|
13
|
+
* TAXONOMIST_OUTPUT Path for the backup JSON file.
|
|
14
|
+
* Default: /tmp/taxonomist-backup.json
|
|
15
|
+
*
|
|
16
|
+
* @package Taxonomist
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
$output_file = getenv( 'TAXONOMIST_OUTPUT' ) ? getenv( 'TAXONOMIST_OUTPUT' ) : '/tmp/taxonomist-backup.json';
|
|
20
|
+
|
|
21
|
+
$terms = get_terms(
|
|
22
|
+
array(
|
|
23
|
+
'taxonomy' => 'category',
|
|
24
|
+
'hide_empty' => false,
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
$term_data = array();
|
|
28
|
+
foreach ( $terms as $t ) {
|
|
29
|
+
$term_data[] = array(
|
|
30
|
+
'term_id' => $t->term_id,
|
|
31
|
+
'name' => $t->name,
|
|
32
|
+
'slug' => $t->slug,
|
|
33
|
+
'description' => $t->description,
|
|
34
|
+
'count' => $t->count,
|
|
35
|
+
'parent' => $t->parent,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$post_cats = array();
|
|
40
|
+
$batch_size = 200;
|
|
41
|
+
$last_id = 0;
|
|
42
|
+
|
|
43
|
+
while ( true ) {
|
|
44
|
+
$query_args = array(
|
|
45
|
+
'posts_per_page' => $batch_size,
|
|
46
|
+
'post_status' => 'publish',
|
|
47
|
+
'post_type' => 'post',
|
|
48
|
+
'orderby' => 'ID',
|
|
49
|
+
'order' => 'ASC',
|
|
50
|
+
'suppress_filters' => false,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if ( $last_id > 0 ) {
|
|
54
|
+
add_filter(
|
|
55
|
+
'posts_where',
|
|
56
|
+
$keyset_filter = function ( $where ) use ( $last_id ) {
|
|
57
|
+
global $wpdb;
|
|
58
|
+
return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $last_id );
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$all_posts = get_posts( $query_args );
|
|
64
|
+
|
|
65
|
+
if ( isset( $keyset_filter ) ) {
|
|
66
|
+
remove_filter( 'posts_where', $keyset_filter );
|
|
67
|
+
unset( $keyset_filter );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ( empty( $all_posts ) ) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
foreach ( $all_posts as $p ) {
|
|
75
|
+
$cat_ids = wp_get_post_categories( $p->ID );
|
|
76
|
+
$cat_slugs = wp_get_post_categories( $p->ID, array( 'fields' => 'slugs' ) );
|
|
77
|
+
|
|
78
|
+
$post_cats[] = array(
|
|
79
|
+
'post_id' => $p->ID,
|
|
80
|
+
'post_title' => $p->post_title,
|
|
81
|
+
'category_ids' => $cat_ids,
|
|
82
|
+
'category_slugs' => array_values( $cat_slugs ),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$last_id = $all_posts[ count( $all_posts ) - 1 ]->ID;
|
|
87
|
+
wp_cache_flush();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$default_cat_id = (int) get_option( 'default_category' );
|
|
91
|
+
$default_cat_term = get_term( $default_cat_id, 'category' );
|
|
92
|
+
$default_cat_slug = $default_cat_term ? $default_cat_term->slug : '';
|
|
93
|
+
|
|
94
|
+
$backup = array(
|
|
95
|
+
'timestamp' => gmdate( 'Y-m-d H:i:s' ),
|
|
96
|
+
'site_url' => get_site_url(),
|
|
97
|
+
'total_posts' => count( $post_cats ),
|
|
98
|
+
'total_categories' => count( $term_data ),
|
|
99
|
+
'default_category_slug' => $default_cat_slug,
|
|
100
|
+
'categories' => $term_data,
|
|
101
|
+
'post_categories' => $post_cats,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
$backup_json = wp_json_encode( $backup, JSON_PRETTY_PRINT );
|
|
105
|
+
if ( false === $backup_json ) {
|
|
106
|
+
WP_CLI::error( 'Failed to JSON-encode backup payload' );
|
|
107
|
+
}
|
|
108
|
+
$bytes_written = file_put_contents( $output_file, $backup_json ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
|
109
|
+
if ( false === $bytes_written ) {
|
|
110
|
+
WP_CLI::error( 'Failed to write backup file: ' . $output_file );
|
|
111
|
+
}
|
|
112
|
+
WP_CLI::success( 'Backup saved to ' . $output_file . ' (' . count( $post_cats ) . ' posts, ' . count( $term_data ) . ' categories)' );
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Export all published posts with full content and categories to JSON.
|
|
4
|
+
*
|
|
5
|
+
* Streams posts one-by-one to avoid memory issues on large sites.
|
|
6
|
+
* Output is a JSON array where each element contains the post ID, title,
|
|
7
|
+
* publication date, full text content (HTML stripped), assigned category
|
|
8
|
+
* names, and permalink URL.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* studio wp eval-file export-posts.php
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* TAXONOMIST_OUTPUT Path for the output JSON file.
|
|
15
|
+
* Default: /tmp/taxonomist-export.json
|
|
16
|
+
*
|
|
17
|
+
* @package Taxonomist
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
$output_file = getenv( 'TAXONOMIST_OUTPUT' ) ? getenv( 'TAXONOMIST_OUTPUT' ) : '/tmp/taxonomist-export.json';
|
|
21
|
+
|
|
22
|
+
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
|
23
|
+
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
|
|
24
|
+
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
|
25
|
+
$fp = fopen( $output_file, 'w' );
|
|
26
|
+
if ( false === $fp ) {
|
|
27
|
+
WP_CLI::error( 'Failed to open export file for writing: ' . $output_file );
|
|
28
|
+
}
|
|
29
|
+
$write_or_error = static function ( $file_pointer, $contents, $context ) {
|
|
30
|
+
if ( false === fwrite( $file_pointer, $contents ) ) {
|
|
31
|
+
WP_CLI::error( 'Failed to write ' . $context );
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
$write_or_error( $fp, "[\n", 'opening JSON array to ' . $output_file );
|
|
35
|
+
|
|
36
|
+
$batch_size = 100;
|
|
37
|
+
$last_id = 0;
|
|
38
|
+
$total_exported = 0;
|
|
39
|
+
$first = true;
|
|
40
|
+
|
|
41
|
+
while ( true ) {
|
|
42
|
+
$query_args = array(
|
|
43
|
+
'posts_per_page' => $batch_size,
|
|
44
|
+
'post_status' => 'publish',
|
|
45
|
+
'post_type' => 'post',
|
|
46
|
+
'orderby' => 'ID',
|
|
47
|
+
'order' => 'ASC',
|
|
48
|
+
'suppress_filters' => false,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if ( $last_id > 0 ) {
|
|
52
|
+
add_filter(
|
|
53
|
+
'posts_where',
|
|
54
|
+
$keyset_filter = function ( $where ) use ( $last_id ) {
|
|
55
|
+
global $wpdb;
|
|
56
|
+
return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $last_id );
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$all_posts = get_posts( $query_args );
|
|
62
|
+
|
|
63
|
+
if ( isset( $keyset_filter ) ) {
|
|
64
|
+
remove_filter( 'posts_where', $keyset_filter );
|
|
65
|
+
unset( $keyset_filter );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ( empty( $all_posts ) ) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
foreach ( $all_posts as $p ) {
|
|
73
|
+
if ( ! $first ) {
|
|
74
|
+
$write_or_error( $fp, ",\n", 'JSON separator to ' . $output_file );
|
|
75
|
+
}
|
|
76
|
+
$first = false;
|
|
77
|
+
|
|
78
|
+
$cat_names = wp_get_post_categories( $p->ID, array( 'fields' => 'names' ) );
|
|
79
|
+
$cat_slugs = wp_get_post_categories( $p->ID, array( 'fields' => 'slugs' ) );
|
|
80
|
+
|
|
81
|
+
$content = wp_strip_all_tags( $p->post_content );
|
|
82
|
+
$content = preg_replace( '/\s+/', ' ', $content );
|
|
83
|
+
$content = trim( $content );
|
|
84
|
+
|
|
85
|
+
$row = wp_json_encode(
|
|
86
|
+
array(
|
|
87
|
+
'post_id' => $p->ID,
|
|
88
|
+
'title' => html_entity_decode( $p->post_title, ENT_QUOTES, 'UTF-8' ),
|
|
89
|
+
'date' => $p->post_date,
|
|
90
|
+
'content' => $content,
|
|
91
|
+
'categories' => array_values( $cat_names ),
|
|
92
|
+
'category_slugs' => array_values( $cat_slugs ),
|
|
93
|
+
'url' => get_permalink( $p->ID ),
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if ( false === $row ) {
|
|
98
|
+
fclose( $fp );
|
|
99
|
+
WP_CLI::error( 'Failed to JSON-encode post ID ' . $p->ID );
|
|
100
|
+
}
|
|
101
|
+
$write_or_error( $fp, $row, 'post row for post ID ' . $p->ID );
|
|
102
|
+
++$total_exported;
|
|
103
|
+
|
|
104
|
+
if ( 0 === $total_exported % 500 ) {
|
|
105
|
+
WP_CLI::log( "Exported $total_exported posts..." );
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$last_id = $all_posts[ count( $all_posts ) - 1 ]->ID;
|
|
110
|
+
wp_cache_flush();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
$write_or_error( $fp, "\n]", 'closing JSON array to ' . $output_file );
|
|
114
|
+
if ( false === fclose( $fp ) ) {
|
|
115
|
+
WP_CLI::error( 'Failed to close export file: ' . $output_file );
|
|
116
|
+
}
|
|
117
|
+
// phpcs:enable
|
|
118
|
+
|
|
119
|
+
WP_CLI::success( "Exported $total_exported posts to $output_file" );
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Restore taxonomy state from a backup file.
|
|
4
|
+
*
|
|
5
|
+
* Performs a full authoritative restore: the category taxonomy after this
|
|
6
|
+
* script runs will exactly match the backup.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* TAXONOMIST_BACKUP=/path/to/backup.json studio wp eval-file restore.php
|
|
10
|
+
*
|
|
11
|
+
* Environment variables:
|
|
12
|
+
* TAXONOMIST_BACKUP Path to the backup JSON file created by backup.php.
|
|
13
|
+
* Required.
|
|
14
|
+
*
|
|
15
|
+
* @package Taxonomist
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
$backup_file = getenv( 'TAXONOMIST_BACKUP' );
|
|
19
|
+
if ( ! $backup_file || ! file_exists( $backup_file ) ) {
|
|
20
|
+
WP_CLI::error( 'Set TAXONOMIST_BACKUP env var to the backup file path' );
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
$backup = json_decode( file_get_contents( $backup_file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
|
24
|
+
if ( ! $backup ) {
|
|
25
|
+
WP_CLI::error( 'Failed to parse backup file' );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
WP_CLI::log( 'Restoring from backup: ' . $backup['timestamp'] );
|
|
29
|
+
WP_CLI::log( 'Posts: ' . $backup['total_posts'] . ', Categories: ' . $backup['total_categories'] );
|
|
30
|
+
|
|
31
|
+
$backup_slugs = array();
|
|
32
|
+
$old_id_to_slug = array();
|
|
33
|
+
foreach ( $backup['categories'] as $category ) {
|
|
34
|
+
$backup_slugs[ $category['slug'] ] = $category;
|
|
35
|
+
$old_id_to_slug[ $category['term_id'] ] = $category['slug'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Step 1: Recreate missing categories and update existing ones.
|
|
39
|
+
$existing_terms = get_terms(
|
|
40
|
+
array(
|
|
41
|
+
'taxonomy' => 'category',
|
|
42
|
+
'hide_empty' => false,
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
$existing_by_slug = array();
|
|
46
|
+
foreach ( $existing_terms as $t ) {
|
|
47
|
+
$existing_by_slug[ $t->slug ] = $t;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$slug_to_id = array();
|
|
51
|
+
$created = 0;
|
|
52
|
+
$updated = 0;
|
|
53
|
+
|
|
54
|
+
foreach ( $backup['categories'] as $category ) {
|
|
55
|
+
$slug = $category['slug'];
|
|
56
|
+
|
|
57
|
+
if ( isset( $existing_by_slug[ $slug ] ) ) {
|
|
58
|
+
$existing = $existing_by_slug[ $slug ];
|
|
59
|
+
$needs_update = (
|
|
60
|
+
$existing->name !== $category['name'] ||
|
|
61
|
+
$existing->description !== $category['description']
|
|
62
|
+
);
|
|
63
|
+
if ( $needs_update ) {
|
|
64
|
+
wp_update_term(
|
|
65
|
+
$existing->term_id,
|
|
66
|
+
'category',
|
|
67
|
+
array(
|
|
68
|
+
'name' => $category['name'],
|
|
69
|
+
'description' => $category['description'],
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
++$updated;
|
|
73
|
+
WP_CLI::log( 'Updated category: ' . $category['name'] );
|
|
74
|
+
}
|
|
75
|
+
$slug_to_id[ $slug ] = $existing->term_id;
|
|
76
|
+
} else {
|
|
77
|
+
$temp_name = $category['name'];
|
|
78
|
+
$result = wp_insert_term(
|
|
79
|
+
$temp_name,
|
|
80
|
+
'category',
|
|
81
|
+
array(
|
|
82
|
+
'slug' => $category['slug'],
|
|
83
|
+
'description' => $category['description'],
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
if ( is_wp_error( $result ) && 'term_exists' === $result->get_error_code() ) {
|
|
87
|
+
$temp_name = $category['name'] . '-taxonomist-' . uniqid();
|
|
88
|
+
$result = wp_insert_term(
|
|
89
|
+
$temp_name,
|
|
90
|
+
'category',
|
|
91
|
+
array(
|
|
92
|
+
'slug' => $category['slug'] . '-taxonomist-' . uniqid(),
|
|
93
|
+
'description' => $category['description'],
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if ( ! is_wp_error( $result ) ) {
|
|
98
|
+
$slug_to_id[ $slug ] = $result['term_id'];
|
|
99
|
+
++$created;
|
|
100
|
+
WP_CLI::log( 'Recreated category: ' . $category['name'] );
|
|
101
|
+
} else {
|
|
102
|
+
WP_CLI::warning( 'Failed to recreate ' . $category['name'] . ': ' . $result->get_error_message() );
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 2: Restore parent-child hierarchy.
|
|
108
|
+
$hierarchy_fixed = 0;
|
|
109
|
+
foreach ( $backup['categories'] as $category ) {
|
|
110
|
+
$slug = $category['slug'];
|
|
111
|
+
$old_parent = $category['parent'];
|
|
112
|
+
|
|
113
|
+
if ( ! isset( $slug_to_id[ $slug ] ) ) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$current_id = $slug_to_id[ $slug ];
|
|
118
|
+
$target_parent_id = 0;
|
|
119
|
+
|
|
120
|
+
if ( $old_parent > 0 && isset( $old_id_to_slug[ $old_parent ] ) ) {
|
|
121
|
+
$parent_slug = $old_id_to_slug[ $old_parent ];
|
|
122
|
+
if ( isset( $slug_to_id[ $parent_slug ] ) ) {
|
|
123
|
+
$target_parent_id = $slug_to_id[ $parent_slug ];
|
|
124
|
+
} else {
|
|
125
|
+
WP_CLI::warning( "Parent slug '$parent_slug' not found for category '$slug'" );
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$current_term = get_term( $current_id, 'category' );
|
|
130
|
+
if ( ! $current_term ) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
$needs_fix = (
|
|
135
|
+
(int) $current_term->parent !== $target_parent_id ||
|
|
136
|
+
$current_term->name !== $category['name'] ||
|
|
137
|
+
$current_term->slug !== $category['slug']
|
|
138
|
+
);
|
|
139
|
+
if ( $needs_fix ) {
|
|
140
|
+
wp_update_term(
|
|
141
|
+
$current_id,
|
|
142
|
+
'category',
|
|
143
|
+
array(
|
|
144
|
+
'parent' => $target_parent_id,
|
|
145
|
+
'name' => $category['name'],
|
|
146
|
+
'slug' => $category['slug'],
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
++$hierarchy_fixed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ( $hierarchy_fixed > 0 ) {
|
|
154
|
+
WP_CLI::log( "Fixed $hierarchy_fixed parent-child relationships." );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 3: Restore every post's categories.
|
|
158
|
+
$restored = 0;
|
|
159
|
+
$error_count = 0;
|
|
160
|
+
foreach ( $backup['post_categories'] as $pc ) {
|
|
161
|
+
$current_post_id = $pc['post_id'];
|
|
162
|
+
$target_ids = array();
|
|
163
|
+
|
|
164
|
+
foreach ( $pc['category_slugs'] as $slug ) {
|
|
165
|
+
if ( isset( $slug_to_id[ $slug ] ) ) {
|
|
166
|
+
$target_ids[] = $slug_to_id[ $slug ];
|
|
167
|
+
} else {
|
|
168
|
+
WP_CLI::warning( "Category slug '$slug' not found for post ID $current_post_id" );
|
|
169
|
+
++$error_count;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
$current_post = get_post( $current_post_id );
|
|
174
|
+
if ( ! $current_post ) {
|
|
175
|
+
WP_CLI::warning( 'Post ID ' . $current_post_id . ' no longer exists' );
|
|
176
|
+
++$error_count;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$set_result = wp_set_post_categories( $current_post_id, $target_ids );
|
|
181
|
+
if ( is_wp_error( $set_result ) || false === $set_result ) {
|
|
182
|
+
WP_CLI::warning( 'Failed to restore categories for post ID ' . $current_post_id );
|
|
183
|
+
++$error_count;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
++$restored;
|
|
187
|
+
|
|
188
|
+
if ( 0 === $restored % 500 && $restored > 0 ) {
|
|
189
|
+
WP_CLI::log( "Restored $restored posts..." );
|
|
190
|
+
wp_cache_flush();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Step 4: Delete categories created after the backup.
|
|
195
|
+
$post_restore_terms = get_terms(
|
|
196
|
+
array(
|
|
197
|
+
'taxonomy' => 'category',
|
|
198
|
+
'hide_empty' => false,
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
$deleted = 0;
|
|
202
|
+
foreach ( $post_restore_terms as $t ) {
|
|
203
|
+
if ( ! isset( $backup_slugs[ $t->slug ] ) ) {
|
|
204
|
+
$default_cat = (int) get_option( 'default_category' );
|
|
205
|
+
if ( $t->term_id === $default_cat ) {
|
|
206
|
+
$first_backup_slug = array_key_first( $backup_slugs );
|
|
207
|
+
if ( $first_backup_slug && isset( $slug_to_id[ $first_backup_slug ] ) ) {
|
|
208
|
+
update_option( 'default_category', $slug_to_id[ $first_backup_slug ] );
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
wp_delete_term( $t->term_id, 'category' );
|
|
212
|
+
WP_CLI::log( 'Deleted post-backup category: ' . $t->name );
|
|
213
|
+
++$deleted;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Step 5: Restore the default_category setting.
|
|
218
|
+
if ( isset( $backup['default_category_slug'] ) ) {
|
|
219
|
+
$default_slug = $backup['default_category_slug'];
|
|
220
|
+
if ( isset( $slug_to_id[ $default_slug ] ) ) {
|
|
221
|
+
update_option( 'default_category', $slug_to_id[ $default_slug ] );
|
|
222
|
+
WP_CLI::log( 'Restored default category: ' . $default_slug );
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Step 6: Recount term usage.
|
|
227
|
+
WP_CLI::runcommand( 'term recount category' );
|
|
228
|
+
|
|
229
|
+
WP_CLI::success(
|
|
230
|
+
"Restored $restored posts. " .
|
|
231
|
+
"Created $created categories, updated $updated, deleted $deleted. " .
|
|
232
|
+
"Errors: $error_count."
|
|
233
|
+
);
|