PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.8.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.8.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 / duplicate-form.php
duplicate-form.php
272 lines 7.9 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 if ( in_array( $meta_key, $unserialized_metas, true ) ) {
128 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
129 // Ensure the value is a string before unserializing.
130 $first_value = $meta_values[0];
131 if ( is_string( $first_value ) ) {
132 $meta_value = maybe_unserialize( $first_value );
133 add_post_meta( $new_form_id, $meta_key, $meta_value );
134 }
135 }
136 } else {
137 // Handle serialized metas (get first value).
138 if ( is_array( $meta_values ) && isset( $meta_values[0] ) ) {
139 add_post_meta( $new_form_id, $meta_key, $meta_values[0] );
140 }
141 }
142 }
143 }
144
145 // Allow other plugins to hook after duplication.
146 do_action( 'srfm_after_form_duplicated', $new_form_id, $form_id );
147
148 // Get edit URL for the new form.
149 $edit_url = admin_url( 'admin.php?page=sureforms_form_editor&post=' . $new_form_id );
150
151 // Return success response.
152 return [
153 'success' => true,
154 'original_form_id' => $form_id,
155 'new_form_id' => $new_form_id,
156 'new_form_title' => $new_title,
157 'edit_url' => $edit_url,
158 ];
159 }
160
161 /**
162 * Handle duplicate form REST API request
163 *
164 * Infrastructure through the permission_callback.
165 *
166 * @param \WP_REST_Request $request Full details about the request.
167 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
168 * @since 2.3.0
169 */
170 public function handle_duplicate_form_rest( $request ) {
171 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
172
173 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
174 return new \WP_Error(
175 'invalid_nonce',
176 __( 'Nonce verification failed.', 'sureforms' ),
177 [ 'status' => 403 ]
178 );
179 }
180
181 $form_id = absint( $request->get_param( 'form_id' ) );
182 $title_suffix = sanitize_text_field( $request->get_param( 'title_suffix' ) );
183
184 // Duplicate the form.
185 $result = $this->duplicate_form( $form_id, $title_suffix );
186
187 if ( is_wp_error( $result ) ) {
188 return $result;
189 }
190
191 return new \WP_REST_Response( $result, 200 );
192 }
193
194 /**
195 * Generate unique title by appending suffix
196 *
197 * If a form with the same title already exists, append a number.
198 *
199 * @param string $base_title Original form title.
200 * @param string $suffix Suffix to append. Default ' (Copy)'.
201 * @return string Unique title.
202 * @since 2.3.0
203 */
204 private function generate_unique_title( $base_title, $suffix = ' (Copy)' ) {
205 $new_title = $base_title . $suffix;
206 $counter = 2;
207
208 // Check if a form with this title exists.
209 while ( $this->title_exists( $new_title ) ) {
210 $new_title = $base_title . $suffix . ' ' . $counter;
211 ++$counter;
212 }
213
214 return $new_title;
215 }
216
217 /**
218 * Check if a form title already exists
219 *
220 * @param string $title Title to check.
221 * @return bool True if title exists, false otherwise.
222 * @since 2.3.0
223 */
224 private function title_exists( $title ) {
225 global $wpdb;
226
227 $query = $wpdb->prepare(
228 "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'sureforms_form' AND post_status != 'trash' LIMIT 1",
229 $title
230 );
231
232 $existing = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
233
234 return ! empty( $existing );
235 }
236
237 /**
238 * Update formId in Gutenberg blocks
239 *
240 * Replaces the old form ID with new form ID in the block markup.
241 * This function performs a direct string replacement on the post content
242 * to update formId references in Gutenberg block attributes.
243 *
244 * @param string $content Post content with blocks.
245 * @param int $old_id Original form ID.
246 * @param int $new_id New form ID.
247 * @return string Updated content with new form ID references.
248 * @since 2.3.0
249 */
250 private function update_block_form_ids( $content, $old_id, $new_id ) {
251 // Direct string replacement - no escaping needed for this operation.
252 return str_replace(
253 '"formId":' . intval( $old_id ),
254 '"formId":' . intval( $new_id ),
255 $content
256 );
257 }
258
259 /**
260 * Get list of unserialized post meta keys
261 *
262 * These meta keys are already arrays/objects and don't need double unserializing.
263 *
264 * @return array<string> Array of meta keys.
265 * @since 2.3.0
266 */
267 private function get_unserialized_post_metas() {
268 $export_instance = Export::get_instance();
269 return $export_instance->get_unserialized_post_metas();
270 }
271 }
272