PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.2
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / migrator / bootstrap.php
bootstrap.php
362 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Migrator Bootstrap — wires REST routes, importer factory, and admin assets.
4 *
5 * Registers three REST endpoints under `/sureforms/v1/migrator/` by filtering
6 * into `srfm_rest_api_endpoints`:
7 *
8 * GET /migrator/sources — list installable sources
9 * GET /migrator/sources/(?P<key>[a-z0-9]+)/forms — list forms in one source
10 * POST /migrator/sources/(?P<key>[a-z0-9]+)/import — import selected forms
11 *
12 * Each route uses `Helper::get_items_permissions_check` for capability gating,
13 * matching the existing pattern in `inc/rest-api.php`.
14 *
15 * @package sureforms
16 * @since 2.11.0
17 */
18
19 namespace SRFM\Inc\Migrator;
20
21 use SRFM\Inc\Helper;
22 use SRFM\Inc\Migrator\Importers\Cf7_Importer;
23 use SRFM\Inc\Migrator\Importers\Gravity_Importer;
24 use SRFM\Inc\Migrator\Importers\Ninja_Importer;
25 use SRFM\Inc\Migrator\Importers\Wpforms_Importer;
26 use SRFM\Inc\Traits\Get_Instance;
27 use WP_Error;
28 use WP_REST_Request;
29 use WP_REST_Response;
30
31 if ( ! defined( 'ABSPATH' ) ) {
32 exit;
33 }
34
35 /**
36 * Bootstrap
37 *
38 * @since 2.11.0
39 */
40 class Bootstrap {
41 use Get_Instance;
42
43 /**
44 * Allowlist of source keys → importer classes.
45 *
46 * @var array<string,string>
47 */
48 private $importer_classes = [
49 'cf7' => Cf7_Importer::class,
50 'wpforms' => Wpforms_Importer::class,
51 'gravity' => Gravity_Importer::class,
52 'ninja' => Ninja_Importer::class,
53 ];
54
55 /**
56 * Constructor — hook into the REST endpoint filter.
57 *
58 * @since 2.11.0
59 */
60 public function __construct() {
61 add_filter( 'srfm_rest_api_endpoints', [ $this, 'register_routes' ] );
62 }
63
64 /**
65 * Append migrator routes to the SureForms REST endpoint registry.
66 *
67 * @since 2.11.0
68 *
69 * @param array<string,array<string,mixed>> $endpoints Existing endpoint registry.
70 * @return array<string,array<string,mixed>>
71 */
72 public function register_routes( $endpoints ) {
73 if ( ! is_array( $endpoints ) ) {
74 $endpoints = [];
75 }
76
77 $endpoints['migrator/sources'] = [
78 'methods' => 'GET',
79 'callback' => [ $this, 'rest_list_sources' ],
80 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
81 ];
82
83 $endpoints['migrator/sources/(?P<key>[a-z0-9]+)/forms'] = [
84 'methods' => 'GET',
85 'callback' => [ $this, 'rest_list_forms' ],
86 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
87 'args' => [
88 'key' => [
89 'sanitize_callback' => 'sanitize_key',
90 ],
91 ],
92 ];
93
94 $endpoints['migrator/sources/(?P<key>[a-z0-9]+)/import'] = [
95 'methods' => 'POST',
96 'callback' => [ $this, 'rest_import_forms' ],
97 'permission_callback' => [ Helper::class, 'get_items_permissions_check' ],
98 'args' => [
99 'key' => [
100 'sanitize_callback' => 'sanitize_key',
101 ],
102 'form_ids' => [
103 'sanitize_callback' => [ $this, 'sanitize_form_ids' ],
104 'default' => [],
105 ],
106 'dry_run' => [
107 'sanitize_callback' => 'rest_sanitize_boolean',
108 'default' => false,
109 ],
110 'behavior' => [
111 'sanitize_callback' => [ $this, 'sanitize_behavior' ],
112 'default' => [],
113 ],
114 'post_status' => [
115 // Migrator imports default to draft so the user reviews the
116 // migrated markup before publishing; pass 'publish' to override.
117 'sanitize_callback' => static function ( $value ) {
118 return in_array( $value, [ 'draft', 'publish' ], true ) ? $value : 'draft';
119 },
120 'default' => 'draft',
121 ],
122 'skip_existing' => [
123 'sanitize_callback' => 'rest_sanitize_boolean',
124 'default' => false,
125 ],
126 ],
127 ];
128
129 return $endpoints;
130 }
131
132 /**
133 * GET /migrator/sources — list importable plugins.
134 *
135 * @since 2.11.0
136 *
137 * @param WP_REST_Request $request REST request.
138 * @return WP_REST_Response|WP_Error
139 */
140 public function rest_list_sources( $request ) {
141 $nonce_error = $this->verify_nonce( $request );
142 if ( $nonce_error instanceof WP_Error ) {
143 return $nonce_error;
144 }
145 $out = [];
146 foreach ( array_keys( $this->importer_classes ) as $key ) {
147 $importer = $this->get_importer( $key );
148 if ( null === $importer ) {
149 continue;
150 }
151 $installed = $importer->exist();
152 // Single per-form scan: list_forms() already resolves each form's
153 // imported_srfm_id (via the memoized imported-map), so derive both
154 // the total and imported counts from its rows in one pass — this
155 // endpoint runs on every onboarding boot + Forms-listing page load
156 // (review #2).
157 $forms = $installed ? $importer->list_forms() : [];
158 $forms_count = count( $forms );
159 $imported_count = count(
160 array_filter(
161 $forms,
162 static function ( $row ) {
163 return ! empty( $row['imported_srfm_id'] );
164 }
165 )
166 );
167 // `pending` is what the onboarding picker actually offers.
168 $pending_count = max( 0, $forms_count - $imported_count );
169 $out[] = [
170 'key' => $importer->get_key(),
171 'title' => $importer->get_title(),
172 'installed' => $installed,
173 'form_count' => $forms_count,
174 'imported_count' => $imported_count,
175 'pending_count' => $pending_count,
176 ];
177 }
178 return new WP_REST_Response( [ 'sources' => $out ], 200 );
179 }
180
181 /**
182 * GET /migrator/sources/{key}/forms — list forms inside one source.
183 *
184 * @since 2.11.0
185 *
186 * @param WP_REST_Request $request REST request.
187 * @return WP_REST_Response|WP_Error
188 */
189 public function rest_list_forms( $request ) {
190 $nonce_error = $this->verify_nonce( $request );
191 if ( $nonce_error instanceof WP_Error ) {
192 return $nonce_error;
193 }
194 $key = (string) $request->get_param( 'key' );
195 $importer = $this->get_importer( $key );
196 if ( null === $importer ) {
197 return new WP_REST_Response(
198 [ 'message' => __( 'Unknown migration source.', 'sureforms' ) ],
199 404
200 );
201 }
202 if ( ! $importer->exist() ) {
203 return new WP_REST_Response(
204 [ 'message' => __( 'Source plugin is not active.', 'sureforms' ) ],
205 400
206 );
207 }
208 return new WP_REST_Response(
209 [ 'forms' => $importer->list_forms() ],
210 200
211 );
212 }
213
214 /**
215 * POST /migrator/sources/{key}/import — import (or dry-run) selected forms.
216 *
217 * @since 2.11.0
218 *
219 * @param WP_REST_Request $request REST request.
220 * @return WP_REST_Response|WP_Error
221 */
222 public function rest_import_forms( $request ) {
223 $nonce_error = $this->verify_nonce( $request );
224 if ( $nonce_error instanceof WP_Error ) {
225 return $nonce_error;
226 }
227 $key = (string) $request->get_param( 'key' );
228 $importer = $this->get_importer( $key );
229 if ( null === $importer ) {
230 return new WP_REST_Response(
231 [ 'message' => __( 'Unknown migration source.', 'sureforms' ) ],
232 404
233 );
234 }
235 if ( ! $importer->exist() ) {
236 return new WP_REST_Response(
237 [ 'message' => __( 'Source plugin is not active.', 'sureforms' ) ],
238 400
239 );
240 }
241 $form_ids = $request->get_param( 'form_ids' );
242 if ( ! is_array( $form_ids ) ) {
243 $form_ids = [];
244 }
245 $dry_run = (bool) $request->get_param( 'dry_run' );
246 $behavior = $request->get_param( 'behavior' );
247 if ( ! is_array( $behavior ) ) {
248 $behavior = [];
249 }
250 $post_status = (string) $request->get_param( 'post_status' );
251 // `skip_existing` is the onboarding-step's safe-default — when no per-form
252 // behavior is provided, any source form already mapped to a SureForms
253 // post is skipped instead of overwritten. Per-form entries in $behavior
254 // still take precedence (explicit beats implicit).
255 $skip_existing = (bool) $request->get_param( 'skip_existing' );
256 $result = $importer->import_forms( $form_ids, $dry_run, $behavior, $post_status, $skip_existing );
257 return new WP_REST_Response( $result, 200 );
258 }
259
260 /**
261 * Sanitize the re-import behavior map — keys are source-form ids, values
262 * are one of `update`, `skip`, `create`. Unknown actions and non-scalar
263 * keys are dropped silently so the migrator falls back to its default
264 * `update` behavior.
265 *
266 * @since 2.11.0
267 *
268 * @param mixed $value Raw value from the REST request.
269 * @return array<string,string>
270 */
271 public function sanitize_behavior( $value ) {
272 if ( ! is_array( $value ) ) {
273 return [];
274 }
275 $allowed = [ 'update', 'skip', 'create' ];
276 $out = [];
277 foreach ( $value as $source_id => $action ) {
278 $action = is_string( $action ) ? strtolower( $action ) : '';
279 if ( ! in_array( $action, $allowed, true ) ) {
280 continue;
281 }
282 $key = is_int( $source_id ) || is_numeric( $source_id ) ? (string) (int) $source_id : sanitize_text_field( (string) $source_id );
283 if ( '' === $key ) {
284 continue;
285 }
286 $out[ $key ] = $action;
287 }
288 return $out;
289 }
290
291 /**
292 * Sanitize a list of form ids — accepts ints or alphanumeric strings (source
293 * plugins use both).
294 *
295 * @since 2.11.0
296 *
297 * @param mixed $value Raw value.
298 * @return array<int,string>
299 */
300 public function sanitize_form_ids( $value ) {
301 if ( ! is_array( $value ) ) {
302 return [];
303 }
304 $out = [];
305 foreach ( $value as $v ) {
306 if ( is_int( $v ) || is_numeric( $v ) ) {
307 $out[] = (string) (int) $v;
308 continue;
309 }
310 if ( is_string( $v ) ) {
311 $out[] = sanitize_text_field( $v );
312 }
313 }
314 return $out;
315 }
316
317 /**
318 * Resolve a source key into an importer instance.
319 *
320 * @since 2.11.0
321 *
322 * @param string $key Source key.
323 * @return Base_Migrator|null
324 */
325 private function get_importer( $key ) {
326 $key = sanitize_key( (string) $key );
327 if ( ! isset( $this->importer_classes[ $key ] ) ) {
328 return null;
329 }
330 $class = $this->importer_classes[ $key ];
331 if ( ! class_exists( $class ) ) {
332 return null;
333 }
334 $instance = new $class();
335 return $instance instanceof Base_Migrator ? $instance : null;
336 }
337
338 /**
339 * Verify the WordPress REST cookie nonce.
340 *
341 * Returns a WP_Error the REST callback can short-circuit on. The shape matches
342 * REST conventions (rest_cookie_invalid_nonce, 403) so api.js receives a
343 * properly structured error response instead of an AJAX-shaped envelope.
344 *
345 * @since 2.11.0
346 *
347 * @param WP_REST_Request $request REST request.
348 * @return WP_Error|null
349 */
350 private function verify_nonce( $request ) {
351 $nonce = (string) $request->get_header( 'X-WP-Nonce' );
352 if ( wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
353 return null;
354 }
355 return new WP_Error(
356 'rest_cookie_invalid_nonce',
357 __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
358 [ 'status' => 403 ]
359 );
360 }
361 }
362