PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.1 2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 All 130 releases
wdesignkit / includes / admin / import / class-wdkit-import-cleanup.php

class-wdkit-import-cleanup.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/admin/import/class-wdkit-import-cleanup.php

226 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post-import cleanup — the destructive corner of the importer.
4 *
5 * Everything here deletes content permanently, so the whole class is off unless a caller
6 * explicitly opts in. `Wdkit_Import_Context::normalize()` defaults
7 * `allow_destructive_cleanup` to false and there is no code path that flips it implicitly.
8 *
9 * ── What the browser does, and why it is safe there ────────────────────────
10 *
11 * 1. `wdkit_remove_header_footer` — despite the name, this force-deletes whatever post ids it
12 * is handed, any post type, skipping the trash. It performs no ownership check at all.
13 * The wizard is safe because of its caller, not the handler: it only ever passes
14 * `current_section.current`, which is populated exclusively with `inserted_id` values from
15 * posts that same import run just created, and is cleared immediately afterwards. It is
16 * used to clear away posts created during a failed-template retry pass before the sections
17 * are re-imported.
18 *
19 * 2. `wkit_remove_dummy_post` — deletes WordPress's default "Hello world!" post, matched on
20 * post_type `post` + that exact title + the `uncategorized` category, one result, force
21 * delete. The browser does this unconditionally during an import.
22 *
23 * ── What this class does differently, and why ─────────────────────────────
24 *
25 * For (1) it will only ever delete ids that are recorded in the import session as having been
26 * created by this run. That is a stronger guarantee than the browser's in-memory array,
27 * because it survives a crash — but it is also the only thing that makes remote-triggered
28 * cleanup defensible, so it is enforced here rather than trusted from a caller.
29 *
30 * For (2) the honest position is that ownership cannot be proven: "Hello world!" is created by
31 * WordPress, not by this importer, and a site could in principle have a real post with that
32 * title. So it stays behind the same opt-in and reports `ownership_not_proven` when the opt-in
33 * is absent, rather than quietly deleting.
34 *
35 * @package Wdesignkit
36 * @since 2.6.5
37 */
38
39 if ( ! defined( 'ABSPATH' ) ) {
40 exit;
41 }
42
43 if ( ! class_exists( 'Wdkit_Import_Cleanup' ) ) {
44
45 /**
46 * Import cleanup.
47 */
48 class Wdkit_Import_Cleanup {
49
50 /**
51 * Remove WordPress's default sample post.
52 *
53 * @param array $context Validated import context.
54 * @return array{status:string,reason?:string}
55 */
56 public static function remove_sample_post( $context ) {
57 if ( empty( $context['allow_destructive_cleanup'] ) ) {
58 /* Deliberately the exact shape the brief asks for: the default remote runner
59 * reports why it did nothing rather than doing something irreversible. */
60 return array(
61 'status' => 'skipped',
62 'reason' => 'ownership_not_proven',
63 );
64 }
65
66 if ( ! current_user_can( 'delete_posts' ) ) {
67 return array(
68 'status' => 'skipped',
69 'reason' => 'missing_delete_posts_capability',
70 );
71 }
72
73 if ( ! class_exists( 'Wdkit_Import_temp_Ajax' ) ) {
74 return array(
75 'status' => 'skipped',
76 'reason' => 'service_unavailable',
77 );
78 }
79
80 $instance = Wdkit_Import_temp_Ajax::get_instance();
81
82 if ( ! method_exists( $instance, 'wdkit_remove_dummy_post_data' ) ) {
83 return array(
84 'status' => 'skipped',
85 'reason' => 'service_unavailable',
86 );
87 }
88
89 $response = $instance->wdkit_remove_dummy_post_data();
90
91 /* "No matching post found" is a success for our purposes — it means the site has
92 * already been cleaned, which is what makes this idempotent. */
93 return array(
94 'status' => ! empty( $response['success'] ) ? 'removed' : 'not_found',
95 );
96 }
97
98 /**
99 * Delete posts this import created and then abandoned.
100 *
101 * Mirrors the wizard's retry-cleanup pass. `$candidate_ids` is intersected with the ids
102 * the session recorded, so an id that this run did not create cannot be deleted even if
103 * a caller asks for it.
104 *
105 * @param array $candidate_ids Post ids proposed for deletion.
106 * @param array $context Validated import context.
107 * @param string $session_id Session id.
108 * @return array{status:string,deleted:int[],refused:int[],reason?:string}
109 */
110 public static function remove_orphaned_posts( $candidate_ids, $context, $session_id ) {
111 $result = array(
112 'status' => 'skipped',
113 'deleted' => array(),
114 'refused' => array(),
115 );
116
117 if ( empty( $context['allow_destructive_cleanup'] ) ) {
118 $result['reason'] = 'destructive_cleanup_disabled';
119
120 return $result;
121 }
122
123 $candidate_ids = array_values(
124 array_unique(
125 array_filter(
126 array_map( 'intval', (array) $candidate_ids ),
127 static function ( $id ) {
128 return $id > 0;
129 }
130 )
131 )
132 );
133
134 if ( empty( $candidate_ids ) ) {
135 $result['reason'] = 'nothing_proposed';
136
137 return $result;
138 }
139
140 $owned = self::session_created_ids( $session_id );
141
142 if ( empty( $owned ) ) {
143 $result['reason'] = 'ownership_not_proven';
144
145 return $result;
146 }
147
148 $deletable = array();
149
150 foreach ( $candidate_ids as $id ) {
151 if ( isset( $owned[ $id ] ) ) {
152 $deletable[] = $id;
153 } else {
154 /* Refused, not silently dropped — a caller proposing an id this run did not
155 * create is a bug worth seeing in the session record. */
156 $result['refused'][] = $id;
157 }
158 }
159
160 if ( empty( $deletable ) ) {
161 $result['reason'] = 'no_owned_ids_in_proposal';
162
163 return $result;
164 }
165
166 if ( ! class_exists( 'Wdkit_Import_temp_Ajax' ) ) {
167 $result['reason'] = 'service_unavailable';
168
169 return $result;
170 }
171
172 $instance = Wdkit_Import_temp_Ajax::get_instance();
173
174 if ( ! method_exists( $instance, 'wdkit_delete_posts_data' ) ) {
175 $result['reason'] = 'service_unavailable';
176
177 return $result;
178 }
179
180 $instance->wdkit_delete_posts_data( $deletable );
181
182 $result['status'] = 'deleted';
183 $result['deleted'] = $deletable;
184
185 return $result;
186 }
187
188 /**
189 * Post ids the session records as created by this run.
190 *
191 * Reads page, post and product steps, because all three write a WordPress post row.
192 *
193 * @param string $session_id Session id.
194 * @return array<int,bool> Keyed by id for O(1) membership tests.
195 */
196 public static function session_created_ids( $session_id ) {
197 $owned = array();
198
199 $session = Wdkit_Import_Session::get( $session_id );
200
201 if ( null === $session || empty( $session['results'] ) ) {
202 return $owned;
203 }
204
205 foreach ( $session['results'] as $step => $record ) {
206 if ( ! is_array( $record ) ) {
207 continue;
208 }
209
210 $step = (string) $step;
211
212 if ( ( 0 === strpos( $step, 'page_' ) || 0 === strpos( $step, 'post_' ) ) && ! empty( $record['post_id'] ) ) {
213 $owned[ (int) $record['post_id'] ] = true;
214 continue;
215 }
216
217 if ( 0 === strpos( $step, 'product_' ) && ! empty( $record['product_id'] ) ) {
218 $owned[ (int) $record['product_id'] ] = true;
219 }
220 }
221
222 return $owned;
223 }
224 }
225 }
226