PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.10.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.10.1
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 / duplicate-form.php
duplicate-form.php
279 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sureforms form duplication.
4 *
5 * @package sureforms.
6 * @since 2.3.0
7 */
8
9 namespace SRFM\Inc;
10
11 use SRFM\Inc\Traits\Get_Instance;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * Duplicate Form Class.
19 *
20 * @since 2.3.0
21 */
22 class Duplicate_Form {
23 use Get_Instance;
24
25 /**
26 * Duplicate a form with all its metadata
27 *
28 * @param int $form_id Form ID to duplicate.
29 * @param string $title_suffix Suffix to append to title. Default ' (Copy)'.
30 * @return array<string, mixed>|\WP_Error Result with new form ID or error.
31 * @since 2.3.0
32 */
33 public function duplicate_form( $form_id, $title_suffix = ' (Copy)' ) {
34 // Validate form ID.
35 $form_id = intval( $form_id );
36 if ( $form_id <= 0 ) {
37 return new \WP_Error(
38 'invalid_form_id',
39 __( 'Invalid form ID provided.', 'sureforms' ),
40 [ 'status' => 400 ]
41 );
42 }
43
44 // Get source form.
45 $source_form = get_post( $form_id );
46
47 if ( ! $source_form ) {
48 return new \WP_Error(
49 'form_not_found',
50 __( 'Source form not found.', 'sureforms' ),
51 [ 'status' => 404 ]
52 );
53 }
54
55 // Verify it's a sureforms_form post type.
56 if ( SRFM_FORMS_POST_TYPE !== $source_form->post_type ) {
57 return new \WP_Error(
58 'invalid_post_type',
59 __( 'The specified post is not a SureForms form.', 'sureforms' ),
60 [ 'status' => 400 ]
61 );
62 }
63
64 // Get all post meta.
65 $post_meta = get_post_meta( $form_id );
66
67 // Create new form title with suffix.
68 $new_title = $this->generate_unique_title( $source_form->post_title, $title_suffix );
69
70 // Prepare new post data.
71 // Note: wp_insert_post() internally calls wp_unslash() which removes backslashes.
72 // This corrupts unicode escapes like \u003c (used for < in JSON block attributes).
73 // We must use wp_slash() to pre-escape the content so wp_unslash() results in correct content.
74 $new_post_args = [
75 'post_title' => $new_title,
76 'post_content' => wp_slash( $source_form->post_content ),
77 'post_status' => 'draft', // Always create as draft for safety.
78 'post_type' => SRFM_FORMS_POST_TYPE,
79 'post_author' => get_current_user_id(), // Use current user as author.
80 ];
81
82 // Create the new post.
83 $new_form_id_or_error = wp_insert_post( $new_post_args );
84
85 // Check for WP_Error or invalid post ID.
86 if ( ! is_int( $new_form_id_or_error ) || $new_form_id_or_error <= 0 ) {
87 return new \WP_Error(
88 'duplication_failed',
89 __( 'Failed to create duplicate form.', 'sureforms' ),
90 [ 'status' => 500 ]
91 );
92 }
93
94 // At this point, we're certain $new_form_id_or_error is a valid post ID (int).
95 $new_form_id = Helper::get_integer_value( $new_form_id_or_error );
96
97 // Update formId in Gutenberg blocks.
98 $updated_content = $this->update_block_form_ids( $source_form->post_content, $form_id, $new_form_id );
99
100 // Update the post content with new formId.
101 // Use wp_slash() for the same reason as above - to preserve unicode escapes.
102 wp_update_post(
103 [
104 'ID' => $new_form_id,
105 'post_content' => wp_slash( $updated_content ),
106 ]
107 );
108
109 // Get list of unserialized meta keys.
110 $unserialized_metas = $this->get_unserialized_post_metas();
111
112 // Copy all post meta.
113 // Ensure $post_meta is an array before iterating.
114 if ( is_array( $post_meta ) ) {
115 foreach ( $post_meta as $meta_key => $meta_values ) {
116 // Ensure meta_key is a string.
117 if ( ! is_string( $meta_key ) ) {
118 continue;
119 }
120
121 // Skip WordPress internal meta keys.
122 if ( '_edit_lock' === $meta_key || '_edit_last' === $meta_key ) {
123 continue;
124 }
125
126 // Handle unserialized metas (these are already arrays/objects).
127 // Note: get_post_meta() returns unslashed data, but add_post_meta()
128 // internally runs wp_unslash() on the value. Without re-slashing,
129 // backslashes are stripped — corrupting JSON-string metas (e.g.
130 // _srfm_save_resume, _srfm_conditional_confirmation) whose escaped
131 // quotes (\") then fail json_decode() in their sanitize callbacks,
132 // causing those callbacks to wipe the value to an empty string.
133 // wp_slash() pre-escapes so wp_unslash() restores the original.
134 if ( in_array( $meta_key, $unserialized_metas, true ) ) {
135 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
136 // Ensure the value is a string before unserializing.
137 $first_value = $meta_values[0];
138 if ( is_string( $first_value ) ) {
139 $meta_value = maybe_unserialize( $first_value );
140 add_post_meta( $new_form_id, $meta_key, wp_slash( $meta_value ) );
141 }
142 }
143 } else {
144 // Handle serialized metas (get first value).
145 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
146 add_post_meta( $new_form_id, $meta_key, wp_slash( $meta_values[0] ) );
147 }
148 }
149 }
150 }
151
152 // Allow other plugins to hook after duplication.
153 do_action( 'srfm_after_form_duplicated', $new_form_id, $form_id );
154
155 // Get edit URL for the new form.
156 $edit_url = admin_url( 'admin.php?page=sureforms_form_editor&post=' . $new_form_id );
157
158 // Return success response.
159 return [
160 'success' => true,
161 'original_form_id' => $form_id,
162 'new_form_id' => $new_form_id,
163 'new_form_title' => $new_title,
164 'edit_url' => $edit_url,
165 ];
166 }
167
168 /**
169 * Handle duplicate form REST API request
170 *
171 * Infrastructure through the permission_callback.
172 *
173 * @param \WP_REST_Request $request Full details about the request.
174 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
175 * @since 2.3.0
176 */
177 public function handle_duplicate_form_rest( $request ) {
178 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
179
180 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
181 return new \WP_Error(
182 'invalid_nonce',
183 __( 'Nonce verification failed.', 'sureforms' ),
184 [ 'status' => 403 ]
185 );
186 }
187
188 $form_id = absint( $request->get_param( 'form_id' ) );
189 $title_suffix = sanitize_text_field( $request->get_param( 'title_suffix' ) );
190
191 // Duplicate the form.
192 $result = $this->duplicate_form( $form_id, $title_suffix );
193
194 if ( is_wp_error( $result ) ) {
195 return $result;
196 }
197
198 return new \WP_REST_Response( $result, 200 );
199 }
200
201 /**
202 * Generate unique title by appending suffix
203 *
204 * If a form with the same title already exists, append a number.
205 *
206 * @param string $base_title Original form title.
207 * @param string $suffix Suffix to append. Default ' (Copy)'.
208 * @return string Unique title.
209 * @since 2.3.0
210 */
211 private function generate_unique_title( $base_title, $suffix = ' (Copy)' ) {
212 $new_title = $base_title . $suffix;
213 $counter = 2;
214
215 // Check if a form with this title exists.
216 while ( $this->title_exists( $new_title ) ) {
217 $new_title = $base_title . $suffix . ' ' . $counter;
218 ++$counter;
219 }
220
221 return $new_title;
222 }
223
224 /**
225 * Check if a form title already exists
226 *
227 * @param string $title Title to check.
228 * @return bool True if title exists, false otherwise.
229 * @since 2.3.0
230 */
231 private function title_exists( $title ) {
232 global $wpdb;
233
234 $query = $wpdb->prepare(
235 "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'sureforms_form' AND post_status != 'trash' LIMIT 1",
236 $title
237 );
238
239 $existing = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
240
241 return ! empty( $existing );
242 }
243
244 /**
245 * Update formId in Gutenberg blocks
246 *
247 * Replaces the old form ID with new form ID in the block markup.
248 * This function performs a direct string replacement on the post content
249 * to update formId references in Gutenberg block attributes.
250 *
251 * @param string $content Post content with blocks.
252 * @param int $old_id Original form ID.
253 * @param int $new_id New form ID.
254 * @return string Updated content with new form ID references.
255 * @since 2.3.0
256 */
257 private function update_block_form_ids( $content, $old_id, $new_id ) {
258 // Direct string replacement - no escaping needed for this operation.
259 return str_replace(
260 '"formId":' . intval( $old_id ),
261 '"formId":' . intval( $new_id ),
262 $content
263 );
264 }
265
266 /**
267 * Get list of unserialized post meta keys
268 *
269 * These meta keys are already arrays/objects and don't need double unserializing.
270 *
271 * @return array<string> Array of meta keys.
272 * @since 2.3.0
273 */
274 private function get_unserialized_post_metas() {
275 $export_instance = Export::get_instance();
276 return $export_instance->get_unserialized_post_metas();
277 }
278 }
279