PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.7
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.7
2.12.7 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 All 97 releases
sureforms / inc / duplicate-form.php

duplicate-form.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.7, at inc/duplicate-form.php

287 lines 8.7 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 // The view counter belongs to the form that earned it. Cloning it
127 // would give the copy an impression count with no submissions behind
128 // it — a permanent 0% conversion rate — and double-count those views
129 // in the site-wide analytics total.
130 if ( \SRFM\Inc\Form_Views::META_KEY === $meta_key ) {
131 continue;
132 }
133
134 // Handle unserialized metas (these are already arrays/objects).
135 // Note: get_post_meta() returns unslashed data, but add_post_meta()
136 // internally runs wp_unslash() on the value. Without re-slashing,
137 // backslashes are stripped — corrupting JSON-string metas (e.g.
138 // _srfm_save_resume, _srfm_conditional_confirmation) whose escaped
139 // quotes (\") then fail json_decode() in their sanitize callbacks,
140 // causing those callbacks to wipe the value to an empty string.
141 // wp_slash() pre-escapes so wp_unslash() restores the original.
142 if ( in_array( $meta_key, $unserialized_metas, true ) ) {
143 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
144 // Ensure the value is a string before unserializing.
145 $first_value = $meta_values[0];
146 if ( is_string( $first_value ) ) {
147 $meta_value = maybe_unserialize( $first_value );
148 add_post_meta( $new_form_id, $meta_key, wp_slash( $meta_value ) );
149 }
150 }
151 } else {
152 // Handle serialized metas (get first value).
153 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
154 add_post_meta( $new_form_id, $meta_key, wp_slash( $meta_values[0] ) );
155 }
156 }
157 }
158 }
159
160 // Allow other plugins to hook after duplication.
161 do_action( 'srfm_after_form_duplicated', $new_form_id, $form_id );
162
163 // Get edit URL for the new form.
164 $edit_url = admin_url( 'admin.php?page=sureforms_form_editor&post=' . $new_form_id );
165
166 // Return success response.
167 return [
168 'success' => true,
169 'original_form_id' => $form_id,
170 'new_form_id' => $new_form_id,
171 'new_form_title' => $new_title,
172 'edit_url' => $edit_url,
173 ];
174 }
175
176 /**
177 * Handle duplicate form REST API request
178 *
179 * Infrastructure through the permission_callback.
180 *
181 * @param \WP_REST_Request $request Full details about the request.
182 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
183 * @since 2.3.0
184 */
185 public function handle_duplicate_form_rest( $request ) {
186 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
187
188 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
189 return new \WP_Error(
190 'invalid_nonce',
191 __( 'Nonce verification failed.', 'sureforms' ),
192 [ 'status' => 403 ]
193 );
194 }
195
196 $form_id = absint( $request->get_param( 'form_id' ) );
197 $title_suffix = sanitize_text_field( $request->get_param( 'title_suffix' ) );
198
199 // Duplicate the form.
200 $result = $this->duplicate_form( $form_id, $title_suffix );
201
202 if ( is_wp_error( $result ) ) {
203 return $result;
204 }
205
206 return new \WP_REST_Response( $result, 200 );
207 }
208
209 /**
210 * Generate unique title by appending suffix
211 *
212 * If a form with the same title already exists, append a number.
213 *
214 * @param string $base_title Original form title.
215 * @param string $suffix Suffix to append. Default ' (Copy)'.
216 * @return string Unique title.
217 * @since 2.3.0
218 */
219 private function generate_unique_title( $base_title, $suffix = ' (Copy)' ) {
220 $new_title = $base_title . $suffix;
221 $counter = 2;
222
223 // Check if a form with this title exists.
224 while ( $this->title_exists( $new_title ) ) {
225 $new_title = $base_title . $suffix . ' ' . $counter;
226 ++$counter;
227 }
228
229 return $new_title;
230 }
231
232 /**
233 * Check if a form title already exists
234 *
235 * @param string $title Title to check.
236 * @return bool True if title exists, false otherwise.
237 * @since 2.3.0
238 */
239 private function title_exists( $title ) {
240 global $wpdb;
241
242 $query = $wpdb->prepare(
243 "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'sureforms_form' AND post_status != 'trash' LIMIT 1",
244 $title
245 );
246
247 $existing = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
248
249 return ! empty( $existing );
250 }
251
252 /**
253 * Update formId in Gutenberg blocks
254 *
255 * Replaces the old form ID with new form ID in the block markup.
256 * This function performs a direct string replacement on the post content
257 * to update formId references in Gutenberg block attributes.
258 *
259 * @param string $content Post content with blocks.
260 * @param int $old_id Original form ID.
261 * @param int $new_id New form ID.
262 * @return string Updated content with new form ID references.
263 * @since 2.3.0
264 */
265 private function update_block_form_ids( $content, $old_id, $new_id ) {
266 // Direct string replacement - no escaping needed for this operation.
267 return str_replace(
268 '"formId":' . intval( $old_id ),
269 '"formId":' . intval( $new_id ),
270 $content
271 );
272 }
273
274 /**
275 * Get list of unserialized post meta keys
276 *
277 * These meta keys are already arrays/objects and don't need double unserializing.
278 *
279 * @return array<string> Array of meta keys.
280 * @since 2.3.0
281 */
282 private function get_unserialized_post_metas() {
283 $export_instance = Export::get_instance();
284 return $export_instance->get_unserialized_post_metas();
285 }
286 }
287