PluginProbe ʕ •ᴥ•ʔ
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More / 1.10.0.5
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More v1.10.0.5
1.10.1.1 1.10.1 1.10.0.5 trunk 1.1.4 1.1.4.2 1.1.5 1.1.5.1 1.1.6 1.1.6.1 1.1.7 1.1.7.1 1.1.7.2 1.1.8 1.1.8.1 1.1.8.2 1.1.8.3 1.1.8.4 1.10.0.1 1.10.0.2 1.10.0.3 1.10.0.4 1.2.0 1.2.0.1 1.2.1 1.2.2 1.2.2.1 1.2.2.2 1.2.3 1.2.3.1 1.2.3.2 1.2.4 1.2.4.1 1.2.5 1.2.5.1 1.2.6 1.2.7 1.2.8 1.2.8.1 1.2.9 1.3.0 1.3.1 1.3.1.1 1.3.1.2 1.3.2 1.3.3 1.3.5 1.3.6 1.3.6.1 1.3.6.2 1.3.7.2 1.3.7.3 1.3.7.4 1.3.8 1.3.9.1 1.4.0.1 1.4.1.1 1.4.2 1.4.2.1 1.4.2.2 1.4.3 1.4.4 1.4.4.1 1.4.5 1.4.5.1 1.4.5.2 1.4.5.3 1.4.6 1.4.7.1 1.4.7.2 1.4.8.1 1.4.9 1.5.0.1 1.5.0.3 1.5.0.4 1.5.1 1.5.1.1 1.5.1.3 1.5.2.1 1.5.2.2 1.5.2.3 1.5.3 1.5.3.1 1.5.4.1 1.5.4.2 1.5.5 1.5.5.1 1.5.6 1.5.6.2 1.5.7 1.5.8.2 1.5.9.1 1.5.9.4 1.5.9.5 1.6.0.1 1.6.0.2 1.6.1 1.6.2.2 1.6.2.3 1.6.3.1 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.7 1.6.7.1 1.6.7.2 1.6.7.3 1.6.8 1.6.8.1 1.6.9 1.7.0 1.7.1.1 1.7.1.2 1.7.2 1.7.2.1 1.7.3 1.7.4 1.7.4.1 1.7.4.2 1.7.5.1 1.7.5.2 1.7.5.3 1.7.5.5 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.7.9.1 1.8.0.1 1.8.0.2 1.8.1.1 1.8.1.2 1.8.1.3 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.3.1 1.8.4 1.8.4.1 1.8.5.2 1.8.5.3 1.8.5.4 1.8.6.2 1.8.6.3 1.8.6.4 1.8.7.2 1.8.8.2 1.8.8.3 1.8.9.1 1.8.9.2 1.8.9.4 1.8.9.5 1.8.9.6 1.9.0.1 1.9.0.2 1.9.0.3 1.9.0.4 1.9.1.1 1.9.1.2 1.9.1.3 1.9.1.4 1.9.1.5 1.9.1.6 1.9.2.1 1.9.2.2 1.9.2.3 1.9.3.1 1.9.3.2 1.9.4.1 1.9.4.2 1.9.5 1.9.5.1 1.9.5.2 1.9.6 1.9.6.1 1.9.6.2 1.9.7.1 1.9.7.2 1.9.7.3 1.9.8.1 1.9.8.2 1.9.8.4 1.9.8.7 1.9.9.2 1.9.9.3 1.9.9.4
wpforms-lite / includes / class-form.php
wpforms-lite / includes Last commit date
admin 2 weeks ago emails 4 months ago fields 2 weeks ago functions 2 months ago providers 10 months ago templates 4 months ago class-db.php 1 year ago class-fields.php 1 year ago class-form.php 4 months ago class-install.php 1 year ago class-process.php 4 months ago class-providers.php 1 year ago class-templates.php 2 years ago class-widget.php 1 year ago deprecated.php 8 months ago functions-list.php 3 years ago functions.php 11 months ago integrations.php 4 months ago
class-form.php
1567 lines
1 <?php
2
3 // phpcs:disable Generic.Commenting.DocComment.MissingShort
4 /** @noinspection AutoloadingIssuesInspection */
5 /** @noinspection PhpIllegalPsrClassPathInspection */
6 // phpcs:enable Generic.Commenting.DocComment.MissingShort
7
8 /**
9 * All the form goodness and basics.
10 *
11 * Contains a bunch of helper methods as well.
12 *
13 * @since 1.0.0
14 */
15 class WPForms_Form_Handler {
16
17 /**
18 * Tags taxonomy.
19 *
20 * @since 1.7.5
21 */
22 public const TAGS_TAXONOMY = 'wpforms_form_tag';
23
24 /**
25 * Allowed post types.
26 *
27 * @since 1.8.8
28 */
29 public const POST_TYPES = [
30 'wpforms',
31 'wpforms-template',
32 ];
33
34 /**
35 * Is form data slashing enabled.
36 *
37 * @since 1.9.0
38 *
39 * @var bool
40 */
41 private $is_form_data_slashing_enabled;
42
43 /**
44 * Primary class constructor.
45 *
46 * @since 1.0.0
47 */
48 public function __construct() {
49
50 $this->is_form_data_slashing_enabled = wpforms_is_form_data_slashing_enabled();
51
52 $this->hooks();
53 }
54
55 /**
56 * Hooks.
57 *
58 * @since 1.7.5
59 */
60 private function hooks(): void {
61
62 // Register wpforms custom post type and taxonomy.
63 add_action( 'init', [ $this, 'register_taxonomy' ] );
64 add_action( 'init', [ $this, 'register_cpt' ] );
65
66 // Add wpforms to a new-content admin bar menu.
67 add_action( 'admin_bar_menu', [ $this, 'admin_bar' ], 99 );
68 add_action( 'wpforms_create_form', [ $this, 'track_first_form' ], 10, 3 );
69
70 // @WPFormsBackCompat Support Zapier v1.5.0 and earlier.
71 add_filter( 'wpforms_form_handler_add_notices', [ $this, '_zapier_disconnected_on_duplication' ], 10, 3 );
72 }
73
74 /**
75 * Register the custom post type to be used for forms.
76 *
77 * @since 1.0.0
78 */
79 public function register_cpt(): void {
80
81 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
82
83 /**
84 * Filters Custom Post Type arguments.
85 *
86 * @since 1.0.0
87 *
88 * @param array $args Arguments.
89 */
90 $args = apply_filters(
91 'wpforms_post_type_args',
92 [
93 'label' => 'WPForms',
94 'public' => false,
95 'exclude_from_search' => true,
96 'show_ui' => false,
97 'show_in_admin_bar' => false,
98 'rewrite' => false,
99 'query_var' => false,
100 'can_export' => false,
101 'supports' => [ 'title', 'author', 'revisions' ],
102 'capability_type' => 'wpforms_form', // Not using 'capability_type' anywhere. It just has to be custom for security reasons.
103 'map_meta_cap' => false, // Don't let WP to map meta caps to have a granular control over this process via 'map_meta_cap' filter.
104 ]
105 );
106
107 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
108
109 // Register the post type.
110 register_post_type( 'wpforms', $args );
111 }
112
113 /**
114 * Register the new taxonomy for tags.
115 *
116 * @since 1.7.5
117 */
118 public function register_taxonomy(): void {
119
120 /**
121 * Filters Tags taxonomy arguments.
122 *
123 * @since 1.7.5
124 *
125 * @param array $args Arguments.
126 */
127 $args = apply_filters(
128 'wpforms_form_handler_register_taxonomy_args',
129 [
130 'hierarchical' => false,
131 'rewrite' => false,
132 'public' => false,
133 ]
134 );
135
136 register_taxonomy( self::TAGS_TAXONOMY, 'wpforms', $args );
137 }
138
139 /**
140 * Add "WPForms" item to new-content admin bar menu item.
141 *
142 * @since 1.1.7.2
143 *
144 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference.
145 */
146 public function admin_bar( $wp_admin_bar ): void {
147
148 if ( ! is_admin_bar_showing() || ! wpforms_current_user_can( 'create_forms' ) ) {
149 return;
150 }
151
152 $args = [
153 'id' => 'wpforms',
154 'title' => esc_html__( 'WPForms', 'wpforms-lite' ),
155 'href' => admin_url( 'admin.php?page=wpforms-builder' ),
156 'parent' => 'new-content',
157 ];
158
159 $wp_admin_bar->add_node( $args );
160 }
161
162 /**
163 * Preserve the timestamp when the very first form has been created.
164 *
165 * @since 1.6.7.1
166 *
167 * @param int $form_id Newly created form ID.
168 * @param array $form Array past to create a new form in the wp_posts table.
169 * @param array $data Additional form data.
170 *
171 * @noinspection PhpMissingParamTypeInspection
172 * @noinspection PhpUnusedParameterInspection
173 */
174 public function track_first_form( $form_id, $form, $data ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
175
176 // Do we have the value already?
177 $time = get_option( 'wpforms_forms_first_created' );
178
179 // Check whether we have already saved this option - skip.
180 if ( ! empty( $time ) ) {
181 return;
182 }
183
184 // Check whether we have any forms other than the currently created one.
185 $other_form = $this->get(
186 '',
187 [
188 'posts_per_page' => 1,
189 'nopaging' => false,
190 'fields' => 'ids',
191 'post__not_in' => [ $form_id ], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
192 'update_post_meta_cache' => false,
193 'update_post_term_cache' => false,
194 'cap' => false,
195 ]
196 );
197
198 // As we have other forms - we are not certain about the situation, skip.
199 if ( ! empty( $other_form ) ) {
200 return;
201 }
202
203 add_option( 'wpforms_forms_first_created', time(), '', 'no' );
204 }
205
206 /**
207 * Fetch forms.
208 *
209 * @since 1.0.0
210 *
211 * @param mixed $id Form ID.
212 * @param array $args Additional arguments array.
213 *
214 * @return array|false|WP_Post
215 */
216 public function get( $id = '', array $args = [] ) {
217
218 if ( $id === false ) {
219 return false;
220 }
221
222 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
223
224 /**
225 * Allow developers to filter the WPForms_Form_Handler::get() arguments.
226 *
227 * @since 1.0.0
228 *
229 * @param array $args Arguments array.
230 * @param mixed $id Form ID.
231 */
232 $args = (array) apply_filters( 'wpforms_get_form_args', $args, $id );
233
234 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
235
236 // By default, we should return only published forms.
237 $defaults = [
238 'post_status' => 'publish',
239 ];
240
241 $args = wp_parse_args( $args, $defaults );
242
243 $forms = empty( $id ) ? $this->get_multiple( $args ) : $this->get_single( $id, $args );
244
245 return ! empty( $forms ) ? $forms : false;
246 }
247
248 /**
249 * Fetch a single form.
250 *
251 * @since 1.5.8
252 *
253 * @param string|int $id Form ID.
254 * @param array $args Additional arguments array.
255 *
256 * @return array|false|WP_Post
257 */
258 protected function get_single( $id = '', array $args = [] ) {
259
260 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
261
262 /**
263 * Allow developers to filter the get_single() arguments.
264 *
265 * @since 1.5.8
266 *
267 * @param array $args Arguments' array, same as for `get_post()` function.
268 * @param string|int $id Form ID.
269 */
270 $args = apply_filters( 'wpforms_get_single_form_args', $args, $id );
271
272 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
273
274 $access_obj = wpforms()->obj( 'access' );
275
276 if ( ! $access_obj ) {
277 return false;
278 }
279
280 if ( ! isset( $args['cap'] ) && $access_obj->init_allowed() ) {
281 $args['cap'] = 'view_form_single';
282 }
283
284 if ( ! empty( $args['cap'] ) && ! wpforms_current_user_can( $args['cap'], $id ) ) {
285 return false;
286 }
287
288 // If no ID provided, we can't get a single form.
289 if ( empty( $id ) ) {
290 return false;
291 }
292
293 // If ID is provided, we get a single form.
294 $form = get_post( absint( $id ) );
295
296 // Check if the form exists.
297 if ( empty( $form ) || ! $form instanceof WP_Post ) {
298 return false;
299 }
300
301 // Check if the form is of the allowed post type.
302 if ( ! in_array( $form->post_type, self::POST_TYPES, true ) ) {
303 return false;
304 }
305
306 // Decode the form content.
307 if ( ! empty( $args['content_only'] ) ) {
308 $form = wpforms_decode( $form->post_content );
309 }
310
311 return $form;
312 }
313
314 /**
315 * Fetch multiple forms.
316 *
317 * @since 1.5.8
318 * @since 1.7.2 Added support for $args['search']['term'] - search form title or description by term.
319 *
320 * @param array $args Additional arguments array.
321 *
322 * @return array
323 */
324 protected function get_multiple( array $args = [] ): array {
325
326 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
327
328 /**
329 * Allow developers to filter the get_multiple() arguments.
330 *
331 * @since 1.5.8
332 *
333 * @param array $args Arguments' array. Almost the same as for the `get_posts ()` function.
334 * Additional element:
335 * ['search']['term'] - search the form title or description by term.
336 */
337 $args = (array) apply_filters( 'wpforms_get_multiple_forms_args', $args );
338
339 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
340
341 // No ID provided, get multiple forms.
342 $defaults = [
343 'orderby' => 'id',
344 'order' => 'ASC',
345 'no_found_rows' => true,
346 'nopaging' => true,
347 'suppress_filters' => false,
348 ];
349
350 $args = wp_parse_args( $args, $defaults );
351
352 $post_type = $args['post_type'] ?? [];
353
354 // Post type should be one of the allowed post types.
355 $post_type = array_intersect( (array) $post_type, self::POST_TYPES );
356
357 // If no valid (allowed) post types are provided, use the default one.
358 $args['post_type'] = ! empty( $post_type ) ? $post_type : 'wpforms';
359
360 /**
361 * Allow developers to execute some code before get_posts() call inside \WPForms_Form_Handler::get_multiple().
362 *
363 * @since 1.7.2
364 *
365 * @param array $args Arguments of the `get_posts()`.
366 */
367 do_action( 'wpforms_form_handler_get_multiple_before_get_posts', $args );
368
369 $forms = get_posts( $args );
370
371 /**
372 * Allow developers to execute some code right after the get_posts () call inside \WPForms_Form_Handler::get_multiple().
373 *
374 * @since 1.7.2
375 *
376 * @param array $args Arguments of the `get_posts`.
377 * @param array $forms Forms data. Result of getting multiple forms.
378 */
379 do_action( 'wpforms_form_handler_get_multiple_after_get_posts', $args, $forms );
380
381 /**
382 * Allow developers to filter the result of get_multiple().
383 *
384 * @since 1.7.2
385 *
386 * @param array $forms Result of getting multiple forms.
387 */
388 return apply_filters( 'wpforms_form_handler_get_multiple_forms_result', $forms );
389 }
390
391 /**
392 * Update the form status.
393 *
394 * @since 1.7.3
395 *
396 * @param int $form_id Form ID.
397 * @param string $status New status.
398 *
399 * @return bool
400 */
401 public function update_status( $form_id, $status ): bool {
402
403 // Status updates are used only in trash and restore actions,
404 // which are actually part of the deletion operation.
405 // Therefore, we should check the `delete_form_single` and not `edit_form_single` permission.
406 if ( ! wpforms_current_user_can( 'delete_form_single', $form_id ) ) {
407 return false;
408 }
409
410 $form_id = absint( $form_id );
411 $status = empty( $status ) ? 'publish' : sanitize_key( $status );
412
413 /**
414 * Filters the allowed form statuses.
415 *
416 * @since 1.7.3
417 *
418 * @param array $allowed_statuses Array of allowed form statuses. Default: publish, trash.
419 */
420 $allowed = (array) apply_filters( 'wpforms_form_handler_update_status_allowed', [ 'publish', 'trash' ] );
421
422 if ( ! in_array( $status, $allowed, true ) ) {
423 return false;
424 }
425
426 $result = wp_update_post(
427 [
428 'ID' => $form_id,
429 'post_status' => $status,
430 ]
431 );
432
433 /**
434 * Allow developers to execute some code after changing form status.
435 *
436 * @since 1.8.1
437 *
438 * @param string $form_id Form ID.
439 * @param string $status New form status, `publish` or `trash`.
440 */
441 do_action( 'wpforms_form_handler_update_status', $form_id, $status );
442
443 return $result !== 0;
444 }
445
446 /**
447 * Delete all forms in the Trash.
448 *
449 * @since 1.7.3
450 *
451 * @return int|bool Number of deleted forms OR false.
452 */
453 public function empty_trash() {
454
455 $forms = $this->get_multiple(
456 [
457 'post_type' => self::POST_TYPES,
458 'post_status' => 'trash',
459 'fields' => 'ids',
460 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
461 ]
462 );
463
464 if ( empty( $forms ) ) {
465 return false;
466 }
467
468 return $this->delete( $forms ) ? count( $forms ) : false;
469 }
470
471 /**
472 * Delete forms.
473 *
474 * @since 1.0.0
475 *
476 * @param array $ids Form IDs.
477 *
478 * @return bool
479 */
480 public function delete( $ids = [] ): bool {
481
482 if ( ! is_array( $ids ) ) {
483 $ids = [ $ids ];
484 }
485
486 $ids = array_map( 'absint', $ids );
487
488 foreach ( $ids as $id ) {
489
490 // Check for permissions.
491 if ( ! wpforms_current_user_can( 'delete_form_single', $id ) ) {
492 return false;
493 }
494
495 $entry_obj = wpforms()->obj( 'entry' );
496 $entry_meta_obj = wpforms()->obj( 'entry_meta' );
497 $entry_fields_obj = wpforms()->obj( 'entry_fields' );
498
499 if ( $entry_obj && $entry_meta_obj && $entry_fields_obj ) {
500 $entry_obj->delete_by( 'form_id', $id );
501 $entry_meta_obj->delete_by( 'form_id', $id );
502 $entry_fields_obj->delete_by( 'form_id', $id );
503 }
504
505 $form = wp_delete_post( $id, true );
506
507 if ( ! $form ) {
508 return false;
509 }
510 }
511
512 /**
513 * Fires when forms are deleted.
514 *
515 * @since 1.5.1
516 *
517 * @param array $ids Array of form IDs.
518 */
519 do_action( 'wpforms_delete_form', $ids ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
520
521 return true;
522 }
523
524 /**
525 * Add a new form.
526 *
527 * @since 1.0.0
528 *
529 * @param string $title Form title.
530 * @param array $args Additional arguments.
531 * @param array $data Form data.
532 *
533 * @return false|int|WP_Error
534 */
535 public function add( $title = '', $args = [], $data = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
536
537 // Must have a title.
538 if ( empty( $title ) ) {
539 return false;
540 }
541
542 // Check for permissions.
543 if ( ! wpforms_current_user_can( 'create_forms' ) ) {
544 return false;
545 }
546
547 $this->remove_form_content_filters();
548
549 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
550
551 /**
552 * Filters form creation arguments.
553 *
554 * @since 1.0.0
555 *
556 * @param array $args Form creation arguments.
557 * @param array $data Additional data.
558 */
559 $args = apply_filters( 'wpforms_create_form_args', $args, $data );
560
561 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
562
563 $form_content = [
564 'field_id' => '0',
565 'settings' => [
566 'form_title' => sanitize_text_field( $title ),
567 'form_desc' => '',
568 ],
569 ];
570
571 if ( $this->is_form_data_slashing_enabled ) {
572 $form_content = wp_slash( $form_content );
573 }
574
575 $args_form_data = isset( $args['post_content'] ) ? json_decode( wp_unslash( $args['post_content'] ), true ) : null;
576
577 // Prevent $args['post_content'] from overwriting predefined $form_content.
578 // Typically, it happens if the form was created with a form template and a user was not redirected to a form editing screen afterward.
579 // This is only possible if a user has 'wpforms_create_forms' and no 'wpforms_edit_own_forms' capability.
580 if ( is_array( $args_form_data ) ) {
581 $args['post_content'] = wpforms_encode( array_replace_recursive( $form_content, $args_form_data ) );
582 }
583
584 // Merge args and create the form.
585 $form = wp_parse_args(
586 $args,
587 [
588 'post_title' => esc_html( $title ),
589 'post_status' => 'publish',
590 'post_type' => 'wpforms',
591 'post_content' => wpforms_encode( $form_content ),
592 ]
593 );
594
595 $form_id = wp_insert_post( $form );
596
597 // Set form tags.
598 if ( ! empty( $form_id ) && ! empty( $args_form_data['settings']['form_tags'] ) ) {
599 wp_set_post_terms(
600 $form_id,
601 implode( ',', $args_form_data['settings']['form_tags'] ),
602 self::TAGS_TAXONOMY
603 );
604 }
605
606 // If a user has no editing permissions, the form considered to be created out of the WPForms form builder's context.
607 if ( ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) {
608 $data['builder'] = false;
609 }
610
611 // If the form is created outside the context of the WPForms form
612 // builder, then we define some additional default values.
613 if ( ! empty( $form_id ) && isset( $data['builder'] ) && $data['builder'] === false ) {
614 $form_data = json_decode( wp_unslash( $form['post_content'] ), true );
615 $form_data['id'] = $form_id;
616 $form_data['settings']['submit_text'] = esc_html__( 'Submit', 'wpforms-lite' );
617 $form_data['settings']['submit_text_processing'] = esc_html__( 'Sending...', 'wpforms-lite' );
618 $form_data['settings']['notification_enable'] = '1';
619 $form_data['settings']['notifications'] = [
620 '1' => [
621 'email' => '{admin_email}',
622 'subject' => sprintf( /* translators: %s - form name. */
623 esc_html__( 'New Entry: %s', 'wpforms-lite' ),
624 esc_html( $title )
625 ),
626 'sender_name' => get_bloginfo( 'name' ),
627 'sender_address' => '{admin_email}',
628 'message' => '{all_fields}',
629 ],
630 ];
631 $form_data['settings']['confirmations'] = [
632 '1' => [
633 'type' => 'message',
634 'message' => esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' ),
635 'message_scroll' => '1',
636 ],
637 ];
638
639 $this->update( $form_id, $form_data, [ 'cap' => 'create_forms' ] );
640 }
641
642 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
643
644 /**
645 * Fires after the form was created.
646 *
647 * @since 1.0.0
648 *
649 * @param int $form_id Form ID.
650 * @param array $form Form data.
651 * @param array $data Additional data.
652 */
653 do_action( 'wpforms_create_form', $form_id, $form, $data );
654
655 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
656
657 return $form_id;
658 }
659
660 /**
661 * Update form.
662 *
663 * @since 1.0.0
664 *
665 * @param string|int $form_id Form ID.
666 * @param array $data Data retrieved from $_POST and processed.
667 * @param array $args Empty by default. May have custom data not intended to be saved.
668 *
669 * @return int|false
670 */
671 public function update( $form_id = '', array $data = [], array $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
672
673 if ( empty( $data ) ) {
674 return false;
675 }
676
677 if ( empty( $form_id ) && isset( $data['id'] ) ) {
678 $form_id = $data['id'];
679 }
680
681 if ( ! isset( $args['cap'] ) ) {
682 $args['cap'] = 'edit_form_single';
683 }
684
685 if ( ! empty( $args['cap'] ) && ! wpforms_current_user_can( $args['cap'], $form_id ) ) {
686 return false;
687 }
688
689 $this->remove_form_content_filters();
690
691 if ( $this->is_form_data_slashing_enabled ) {
692 // Even though we are not going to unslash some data,
693 // columns-json and calculation_code fields must be unslashed.
694 $data = $this->unslash_field_keys( $data, [ 'columns-json', 'calculation_code' ] );
695 } else {
696 $data = (array) wp_unslash( $data );
697 }
698
699 $title = empty( $data['settings']['form_title'] ) ? get_the_title( $form_id ) : $data['settings']['form_title'];
700 $desc = empty( $data['settings']['form_desc'] ) ? '' : $data['settings']['form_desc'];
701
702 $data['field_id'] = ! empty( $data['field_id'] ) ? wpforms_validate_field_id( $data['field_id'] ) : '0';
703
704 // Preserve the explicit "Do not store spam entries" state.
705 $data['settings']['store_spam_entries'] = $data['settings']['store_spam_entries'] ?? '0';
706
707 // Use the default 'submit' button text if not provided.
708 $data['settings']['submit_text'] = ! empty( $data['settings']['submit_text'] ) ? $data['settings']['submit_text'] : esc_html__( 'Submit', 'wpforms-lite' );
709
710 // Preserve form meta.
711 $meta = $this->get_meta( $form_id );
712
713 /**
714 * Filters the form meta before saving.
715 *
716 * @since 1.9.8
717 *
718 * @param array $meta Form meta.
719 * @param int $form_id Form ID.
720 * @param array $data Form data.
721 */
722 $meta = apply_filters( 'wpforms_form_handler_update_meta', $meta, $form_id, $data );
723
724 if ( $meta ) {
725 $data['meta'] = $meta;
726 }
727
728 // Update category and subcategory only if available.
729 if ( ! empty( $args['category'] ) ) {
730 $data['meta']['category'] = $args['category'];
731 }
732
733 if ( ! empty( $args['subcategory'] ) ) {
734 $data['meta']['subcategory'] = $args['subcategory'];
735 }
736
737 // Preserve fields meta.
738 if ( isset( $data['fields'] ) ) {
739 $data['fields'] = $this->update__preserve_fields_meta( $data['fields'], $form_id );
740 }
741
742 // Sanitize - don't allow tags for users who do not have the appropriate cap.
743 // If we don't do this, forms for these users can get corrupt due to conflicts with wp_kses().
744 if ( ! current_user_can( 'unfiltered_html' ) ) {
745 $data = map_deep( $data, 'wp_strip_all_tags' );
746 }
747
748 // Sanitize notifications names.
749 if ( isset( $data['settings']['notifications'] ) ) {
750 $data['settings']['notifications'] = $this->update__sanitize_notifications_names( $data['settings']['notifications'] );
751 }
752
753 unset( $notification );
754
755 /**
756 * Allow changing post data before saving.
757 *
758 * @since 1.0.0
759 *
760 * @param array $post_data Post data.
761 * @param array $form_data Form data.
762 * @param array $args Empty by default. May have custom data not intended to be saved.
763 */
764 $form = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
765 'wpforms_save_form_args',
766 [
767 'ID' => $form_id,
768 'post_title' => esc_html( $title ),
769 'post_excerpt' => $desc,
770 'post_content' => wpforms_encode( $data ),
771 ],
772 $data,
773 $args
774 );
775
776 if ( ! empty( $args['skip_revision'] ) ) {
777 remove_action( 'post_updated', 'wp_save_post_revision' );
778 }
779
780 $_form_id = wp_update_post( $form );
781
782 if ( ! empty( $args['skip_revision'] ) ) {
783 add_action( 'post_updated', 'wp_save_post_revision' );
784 }
785
786 if ( is_wp_error( $_form_id ) ) {
787 return false;
788 }
789
790 /**
791 * Fires after saving the form.
792 *
793 * @since 1.0.0
794 *
795 * @param int $_form_id Form ID.
796 * @param array $form Form.
797 */
798 do_action( 'wpforms_save_form', $_form_id, $form ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
799
800 return $_form_id;
801 }
802
803 /**
804 * Preserve fields meta in 'update' method.
805 *
806 * @since 1.5.8
807 *
808 * @param array $fields Form fields.
809 * @param string|int $form_id Form ID.
810 *
811 * @return array
812 */
813 protected function update__preserve_fields_meta( $fields, $form_id ): array {
814
815 foreach ( $fields as $i => $field_data ) {
816 if ( isset( $field_data['id'] ) ) {
817 $field_meta = $this->get_field_meta( $form_id, $field_data['id'] );
818
819 if ( $field_meta ) {
820 $fields[ $i ]['meta'] = $field_meta;
821 }
822 }
823 }
824
825 return $fields;
826 }
827
828 /**
829 * Sanitize notifications names meta in 'update' method.
830 *
831 * @since 1.5.8
832 *
833 * @param array $notifications Form notifications.
834 *
835 * @return array
836 */
837 protected function update__sanitize_notifications_names( $notifications ): array {
838
839 foreach ( $notifications as &$notification ) {
840 if ( ! empty( $notification['notification_name'] ) ) {
841 $notification['notification_name'] = sanitize_text_field( $notification['notification_name'] );
842 }
843 }
844
845 return $notifications;
846 }
847
848 /**
849 * Duplicate forms.
850 *
851 * @since 1.1.4
852 * @since 1.8.8 Return array of new form IDs instead of true.
853 *
854 * @param array|string $ids Form IDs to duplicate.
855 *
856 * @return bool|array Array of new form IDs or false.
857 */
858 public function duplicate( $ids ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
859
860 // Check for permissions.
861 if ( ! wpforms_current_user_can( 'create_forms' ) ) {
862 return false;
863 }
864
865 $this->remove_form_content_filters();
866
867 if ( ! is_array( $ids ) ) {
868 $ids = [ $ids ];
869 }
870
871 $ids = array_map( 'absint', $ids );
872
873 $duplicate_ids = [];
874
875 foreach ( $ids as $id ) {
876
877 // Get the original entry.
878 $form = get_post( $id );
879
880 if ( ! wpforms_current_user_can( 'view_form_single', $id ) ) {
881 return false;
882 }
883
884 // Confirm form exists.
885 if ( empty( $form ) ) {
886 return false;
887 }
888
889 // Get the form data.
890 $new_form_data = (array) wpforms_decode( $form->post_content );
891
892 if ( $this->is_form_data_slashing_enabled ) {
893 $new_form_data = (array) wp_slash( $new_form_data );
894 }
895
896 // Remove form ID from the title if present.
897 $new_form_data['settings']['form_title'] = str_replace( '(ID #' . absint( $id ) . ')', '', $new_form_data['settings']['form_title'] );
898
899 // Remove '(copy)' from the form template title if present.
900 $new_form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $new_form_data['settings']['form_title'] );
901
902 // Remove trailing spaces.
903 $new_form_data['settings']['form_title'] = rtrim( $new_form_data['settings']['form_title'] );
904
905 // Remove the `-template ` suffix and all after it from the post name.
906 $post_name = preg_replace( '/-template(-\d+)?/', '', $form->post_name );
907
908 // Add some notice messages before form preview area.
909 $new_form_data = $this->add_notices( $new_form_data, (int) $id );
910
911 // Create the duplicate form.
912 $new_form = [
913 'post_content' => wpforms_encode( $new_form_data ),
914 'post_excerpt' => $form->post_excerpt,
915 'post_status' => $form->post_status,
916 'post_title' => $new_form_data['settings']['form_title'],
917 'post_type' => $form->post_type,
918 'post_name' => wpforms_is_form_template( $id ) ? $post_name . '-template' : $post_name,
919 ];
920 $new_form_id = wp_insert_post( $new_form );
921
922 if ( ! $new_form_id || is_wp_error( $new_form_id ) ) {
923 return false;
924 }
925
926 // Set a new form name.
927 $new_form_data['settings']['form_title'] .= $form->post_type === 'wpforms-template' ?
928 ' ' . __( '(copy)', 'wpforms-lite' ) :
929 ' (ID #' . absint( $new_form_id ) . ')';
930
931 // Set a new form ID.
932 $new_form_data['id'] = absint( $new_form_id );
933
934 // Update a new duplicate form.
935 $new_form_id = $this->update( $new_form_id, $new_form_data, [ 'cap' => 'create_forms' ] );
936
937 if ( ! $new_form_id ) {
938 return false;
939 }
940
941 // Add tags to the new form.
942 if ( ! empty( $new_form_data['settings']['form_tags'] ) ) {
943 wp_set_post_terms(
944 $new_form_id,
945 implode( ',', (array) $new_form_data['settings']['form_tags'] ),
946 self::TAGS_TAXONOMY
947 );
948 }
949
950 /**
951 * Fires after the form was duplicated.
952 *
953 * @since 1.8.2.2
954 *
955 * @param int $id Original form ID.
956 * @param int $new_form_id New form ID.
957 * @param array $new_form_data New form data.
958 */
959 do_action( 'wpforms_form_handler_duplicate_form', $id, $new_form_id, $new_form_data );
960
961 $duplicate_ids[] = $new_form_id;
962 }
963
964 return $duplicate_ids;
965 }
966
967 /**
968 * Convert form to a template and vice versa.
969 *
970 * @since 1.8.8
971 *
972 * @param string|int $form_id Form ID.
973 * @param string $convert_to Convert to, `form` or `template`.
974 *
975 * @return false|int New object ID or false on failure.
976 */
977 public function convert( $form_id, string $convert_to ) {
978
979 if ( ! in_array( $convert_to, [ 'form', 'template' ], true ) ) {
980 return false;
981 }
982
983 // Duplicate the form.
984 $ids = $this->duplicate( $form_id );
985
986 if ( empty( $ids ) ) {
987 return false;
988 }
989
990 $new_form_id = current( $ids );
991 $form = get_post( $new_form_id );
992 $form_data = wpforms_decode( $form->post_content );
993
994 if ( $this->is_form_data_slashing_enabled ) {
995 $form_data = wp_slash( $form_data );
996 }
997
998 /**
999 * Filters the form data before converting it to a template or vice versa.
1000 *
1001 * @since 1.8.8
1002 *
1003 * @param array $form_data Form data.
1004 * @param string|int $form_id Form ID.
1005 * @param string $convert_to Convert to, `form` or `template`.
1006 */
1007 $form_data = apply_filters( 'wpforms_form_handler_convert_form_data', $form_data, $form_id, $convert_to );
1008
1009 // Set default post type.
1010 $post_type = 'wpforms';
1011
1012 // Remove the numeric suffix from the post name.
1013 // Duplication always adds `-{numeric}` suffix.
1014 $post_name = preg_replace( '/-\d+$/', '', $form->post_name );
1015
1016 // Remove the `-template ` suffix and all after it from the post name.
1017 $post_name = preg_replace( '/-template(-\d+)?/', '', $post_name );
1018
1019 // Remove (copy) from the form title, if present.
1020 $form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $form_data['settings']['form_title'] );
1021
1022 // Remove trailing spaces.
1023 $form_data['settings']['form_title'] = rtrim( $form_data['settings']['form_title'] );
1024
1025 // Remove template description.
1026 unset( $form_data['settings']['template_description'] );
1027
1028 if ( $convert_to === 'template' ) {
1029 $post_type = 'wpforms-template';
1030
1031 // Remove (ID #<Form ID>) from the form title, if present.
1032 $form_data['settings']['form_title'] = preg_replace( '/\(ID #\d+\)/', '', $form_data['settings']['form_title'] );
1033
1034 // Set an empty template description.
1035 $form_data['settings']['template_description'] = '';
1036
1037 // Remove traces of any other template that may have been used to create the original form by setting itself as a template.
1038 $form_data['meta']['template'] = 'wpforms-user-template-' . $new_form_id;
1039
1040 // Add `-template` suffix to the post name.
1041 $post_name .= '-template';
1042 }
1043
1044 wp_update_post(
1045 [
1046 'ID' => $new_form_id,
1047 'post_title' => $form_data['settings']['form_title'],
1048 'post_type' => $post_type,
1049 'post_content' => wpforms_encode( $form_data ),
1050 'post_name' => $post_name,
1051 ]
1052 );
1053
1054 return $new_form_id;
1055 }
1056
1057 /**
1058 * Append notice(s) before form preview, if needed.
1059 *
1060 * @since 1.8.8
1061 *
1062 * @param array $new_form_data New form data.
1063 * @param int $form_id Original form ID.
1064 *
1065 * @return array
1066 */
1067 private function add_notices( array $new_form_data, int $form_id ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
1068
1069 /**
1070 * Add custom notices to be displayed in the preview area of the Form Builder
1071 * after a form or a form template has been duplicated or converted.
1072 *
1073 * @since 1.8.8
1074 *
1075 * @param array $notices Array of notices.
1076 * @param array $new_form_data Form data of the newly duplicated form or form template.
1077 * @param int $form_id Original form ID.
1078 */
1079 $notices = apply_filters( 'wpforms_form_handler_add_notices', [], $new_form_data, $form_id );
1080
1081 if ( empty( $notices ) ) {
1082 return $new_form_data;
1083 }
1084
1085 $current_field_id = ! empty( $new_form_data['fields'] ) ? max( array_keys( $new_form_data['fields'] ) ) : 0;
1086 $code_fields = array_column( $new_form_data['fields'], 'code' );
1087 $next_field_id = $current_field_id;
1088 $warning = [];
1089
1090 foreach ( $notices as $notice ) {
1091 // Skip the duplicate notice if it already exists.
1092 if ( ! empty( $notice['code'] ) && in_array( $notice['code'], $code_fields, true ) ) {
1093 continue;
1094 }
1095
1096 $next_field_id = ++$current_field_id;
1097 $warning[ $next_field_id ] = [
1098 'id' => $next_field_id,
1099 'type' => 'internal-information',
1100 'code' => ! empty( $notice['code'] ) ? esc_attr( $notice['code'] ) : '',
1101 'description' => '',
1102 ];
1103
1104 $warning[ $next_field_id ]['description'] .= ! empty( $notice['title'] ) ? '<strong>' . esc_html( $notice['title'] ) . '</strong>' : '';
1105 $warning[ $next_field_id ]['description'] .= ! empty( $notice['message'] ) ? '<p>' . wp_kses_post( $notice['message'] ) . '</p>' : '';
1106
1107 // Do not add notice with empty body.
1108 if ( empty( $warning[ $next_field_id ]['description'] ) ) {
1109 unset( $warning[ $next_field_id ] );
1110 --$next_field_id; // Reset the next field ID to the previous value.
1111 }
1112 }
1113
1114 if ( ! empty( $warning ) ) {
1115 $new_form_data['fields'] = $warning + $new_form_data['fields'];
1116
1117 // Update the next field ID to be used for future created fields. Otherwise, the IIF field would be overwritten.
1118 $new_form_data['field_id'] = $next_field_id + 1;
1119 }
1120
1121 return $new_form_data;
1122 }
1123
1124 /**
1125 * Add a notice about Zapier zaps disconnected after the form being duplicated or converted to/from the template.
1126 *
1127 * @WPFormsBackCompat Support Zapier v1.5.0 and earlier.
1128 *
1129 * @since 1.8.8
1130 *
1131 * @param array $notices Array of notices.
1132 * @param array $new_form_data Form data.
1133 * @param int $form_id Original form ID.
1134 *
1135 * @return array
1136 * @noinspection HtmlUnknownTarget
1137 * @noinspection PhpUnusedParameterInspection
1138 */
1139 public function _zapier_disconnected_on_duplication( $notices, array $new_form_data, int $form_id ): array { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
1140
1141 // Check if the original form had any Zaps connected.
1142 $is_zapier_connected = get_post_meta( $form_id, 'wpforms_zapier', true );
1143
1144 if ( ! $is_zapier_connected ) {
1145 return $notices;
1146 }
1147
1148 $notices['zapier'] = [
1149 'title' => esc_html__( 'Zaps Have Been Disabled', 'wpforms-lite' ),
1150 'code' => 'disconnected_on_duplication',
1151 'message' => sprintf( /* translators: %s - URL the to list of Zaps. */
1152 __( 'Head over to the Zapier settings in the Marketing tab or visit your <a href="%s" target="_blank" rel="noopener noreferrer">Zapier account</a> to restore them.', 'wpforms-lite' ),
1153 esc_url( 'https://zapier.com/app/zaps' )
1154 ),
1155 ];
1156
1157 return $notices;
1158 }
1159
1160 /**
1161 * Get the next available field ID and increment by one.
1162 *
1163 * @since 1.0.0
1164 *
1165 * @param string|int $form_id Form ID.
1166 * @param array $args Additional arguments.
1167 *
1168 * @return mixed int or false
1169 */
1170 public function next_field_id( $form_id, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
1171
1172 if ( empty( $form_id ) ) {
1173 return false;
1174 }
1175
1176 $defaults = [
1177 'content_only' => true,
1178 ];
1179
1180 if ( isset( $args['cap'] ) ) {
1181 $defaults['cap'] = $args['cap'];
1182 }
1183
1184 $form = $this->get( $form_id, $defaults );
1185
1186 if ( $this->is_form_data_slashing_enabled ) {
1187 $form = wp_slash( $form );
1188 }
1189
1190 if ( empty( $form ) ) {
1191 return false;
1192 }
1193
1194 $field_id = 0;
1195 $max_field_id = ! empty( $form['fields'] ) ? max( array_keys( $form['fields'] ) ) : 0;
1196
1197 // We pass the `field_id` after duplicating the Layout field that contains a bunch of fields.
1198 // This is needed to avoid multiple AJAX calls after duplicating each field in the Layout.
1199 if ( isset( $args['field_id'] ) ) {
1200 $set_field_id = absint( $args['field_id'] ) - 1;
1201 $field_id = $set_field_id > $max_field_id ? $set_field_id : $max_field_id + 1;
1202 } elseif ( ! empty( $form['field_id'] ) ) {
1203 $field_id = absint( $form['field_id'] );
1204 $field_id = $max_field_id > $field_id ? $max_field_id + 1 : $field_id;
1205 }
1206
1207 $form['field_id'] = $field_id + 1;
1208
1209 // Skip creating a revision for this action.
1210 remove_action( 'post_updated', 'wp_save_post_revision' );
1211
1212 $this->update( $form_id, $form );
1213
1214 // Restore the initial revisions state.
1215 add_action( 'post_updated', 'wp_save_post_revision' );
1216
1217 return $field_id;
1218 }
1219
1220 /**
1221 * Get private meta information for a form.
1222 *
1223 * @since 1.0.0
1224 *
1225 * @param string|int $form_id Form ID.
1226 * @param string $field Field.
1227 * @param array $args Additional arguments.
1228 *
1229 * @return false|array
1230 */
1231 public function get_meta( $form_id, $field = '', $args = [] ) {
1232
1233 if ( empty( $form_id ) ) {
1234 return false;
1235 }
1236
1237 $defaults = [
1238 'content_only' => true,
1239 ];
1240
1241 if ( isset( $args['cap'] ) ) {
1242 $defaults['cap'] = $args['cap'];
1243 }
1244
1245 $data = $this->get( $form_id, $defaults );
1246
1247 if ( ! isset( $data['meta'] ) ) {
1248 return false;
1249 }
1250
1251 if ( empty( $field ) ) {
1252 return $data['meta'];
1253 }
1254
1255 return $data['meta'][ $field ] ?? false;
1256 }
1257
1258 /**
1259 * Update or add form meta information to a form.
1260 *
1261 * @since 1.4.0
1262 *
1263 * @param string|int $form_id Form ID.
1264 * @param string $meta_key Meta key.
1265 * @param mixed $meta_value Meta value.
1266 * @param array $args Additional arguments.
1267 *
1268 * @return false|int|WP_Error
1269 */
1270 public function update_meta( $form_id, $meta_key, $meta_value, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
1271
1272 if ( empty( $form_id ) || empty( $meta_key ) ) {
1273 return false;
1274 }
1275
1276 $this->remove_form_content_filters();
1277
1278 if ( ! isset( $args['cap'] ) ) {
1279 $args['cap'] = 'edit_form_single';
1280 }
1281
1282 $form = $this->get_single( absint( $form_id ), $args );
1283
1284 if ( empty( $form ) ) {
1285 return false;
1286 }
1287
1288 $data = (array) wpforms_decode( $form->post_content );
1289 $meta_key = wpforms_sanitize_key( $meta_key );
1290
1291 $data['meta'][ $meta_key ] = $meta_value;
1292
1293 $form = [
1294 'ID' => $form_id,
1295 'post_content' => wpforms_encode( $data ),
1296 ];
1297
1298 /**
1299 * Allow changing form before updating form meta.
1300 *
1301 * @since 1.4.0
1302 *
1303 * @param array $form Form.
1304 * @param array $data Form data.
1305 */
1306 $form = (array) apply_filters( 'wpforms_update_form_meta_args', $form, $data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1307
1308 if ( ! empty( $args['skip_revision'] ) ) {
1309 remove_action( 'post_updated', 'wp_save_post_revision' );
1310 }
1311
1312 $result = wp_update_post( $form );
1313
1314 if ( is_wp_error( $result ) ) {
1315 return $result;
1316 }
1317
1318 if ( ! empty( $args['skip_revision'] ) ) {
1319 add_action( 'post_updated', 'wp_save_post_revision' );
1320 }
1321
1322 /**
1323 * Fires when form meta is updated.
1324 *
1325 * @since 1.4.0
1326 *
1327 * @param string|int $form_id Form ID.
1328 * @param array $form Form.
1329 * @param string $meta_key Meta key.
1330 * @param mixed $meta_value Meta value.
1331 */
1332 do_action( 'wpforms_update_form_meta', $form_id, $form, $meta_key, $meta_value ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1333
1334 return $form_id;
1335 }
1336
1337 /**
1338 * Delete form meta information from a form.
1339 *
1340 * @since 1.4.0
1341 *
1342 * @param string|int $form_id Form ID.
1343 * @param string $meta_key Meta key.
1344 * @param array $args Additional arguments.
1345 *
1346 * @return false|int|WP_Error
1347 */
1348 public function delete_meta( $form_id, $meta_key, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
1349
1350 if ( empty( $form_id ) || empty( $meta_key ) ) {
1351 return false;
1352 }
1353
1354 $this->remove_form_content_filters();
1355
1356 if ( ! isset( $args['cap'] ) ) {
1357 $args['cap'] = 'edit_form_single';
1358 }
1359
1360 $form = $this->get_single( absint( $form_id ), $args );
1361
1362 if ( empty( $form ) ) {
1363 return false;
1364 }
1365
1366 $data = (array) wpforms_decode( $form->post_content );
1367 $meta_key = wpforms_sanitize_key( $meta_key );
1368
1369 unset( $data['meta'][ $meta_key ] );
1370
1371 $form = [
1372 'ID' => $form_id,
1373 'post_content' => wpforms_encode( $data ),
1374 ];
1375
1376 /**
1377 * Filters form which meta to be deleted.
1378 *
1379 * @since 1.4.0
1380 *
1381 * @param array $form Form.
1382 * @param array $data Form data.
1383 */
1384 $form = (array) apply_filters( 'wpforms_delete_form_meta_args', $form, $data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1385 $result = wp_update_post( $form );
1386
1387 if ( is_wp_error( $result ) ) {
1388 return $result;
1389 }
1390
1391 /**
1392 * Fires when form meta is deleted.
1393 *
1394 * @since 1.4.0
1395 *
1396 * @param string|int $form_id Form ID.
1397 * @param array $form Form.
1398 * @param string $meta_key Meta key.
1399 */
1400 do_action( 'wpforms_delete_form_meta', $form_id, $form, $meta_key ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1401
1402 return $form_id;
1403 }
1404
1405 /**
1406 * Get private meta information for a form field.
1407 *
1408 * @since 1.0.0
1409 *
1410 * @param string|int $form_id Form ID.
1411 * @param string $field_id Field ID.
1412 * @param array $args Additional arguments.
1413 *
1414 * @return array|false
1415 */
1416 public function get_field( $form_id, $field_id = '', $args = [] ) {
1417
1418 if ( empty( $form_id ) ) {
1419 return false;
1420 }
1421
1422 $defaults = [
1423 'content_only' => true,
1424 ];
1425
1426 if ( isset( $args['cap'] ) ) {
1427 $defaults['cap'] = $args['cap'];
1428 }
1429
1430 $data = $this->get( $form_id, $defaults );
1431
1432 return $data['fields'][ $field_id ] ?? false;
1433 }
1434
1435 /**
1436 * Get private meta information for a form field.
1437 *
1438 * @since 1.0.0
1439 *
1440 * @param string|int $form_id Form ID.
1441 * @param string $field_id Field ID.
1442 * @param array $args Additional arguments.
1443 *
1444 * @return array|false
1445 */
1446 public function get_field_meta( $form_id, $field_id = '', $args = [] ) {
1447
1448 $field = $this->get_field( $form_id, $field_id, $args );
1449
1450 if ( ! $field ) {
1451 return false;
1452 }
1453
1454 return $field['meta'] ?? false;
1455 }
1456
1457 /**
1458 * Checks if any forms are present on the site.
1459 *
1460 * @since 1.8.8
1461 *
1462 * @retun bool
1463 */
1464 public function forms_exist(): bool {
1465
1466 return (bool) $this->get(
1467 '',
1468 [
1469 'numberposts' => 1,
1470 'fields' => 'ids',
1471 'no_found_rows' => true,
1472 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
1473 'nopaging' => false,
1474 'update_post_meta_cache' => false,
1475 'update_post_term_cache' => false,
1476 ]
1477 );
1478 }
1479
1480 /**
1481 * Get the forms' count per page.
1482 *
1483 * @since 1.8.8
1484 *
1485 * @return int
1486 */
1487 public function get_count_per_page(): int {
1488
1489 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
1490 /**
1491 * Allow developers to modify the number of forms per page.
1492 *
1493 * @since 1.8.8
1494 *
1495 * @param array $count Forms count per page.
1496 */
1497 return (int) apply_filters( 'wpforms_forms_per_page', 20 );
1498 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
1499 }
1500
1501 /**
1502 * Unslash field keys.
1503 *
1504 * @since 1.9.0
1505 *
1506 * @param array $data Form data.
1507 * @param array $keys Field keys.
1508 *
1509 * @return array
1510 * @noinspection PhpSameParameterValueInspection
1511 */
1512 private function unslash_field_keys( array $data, array $keys ): array {
1513
1514 if ( empty( $data['fields'] ) ) {
1515 return $data;
1516 }
1517
1518 /**
1519 * Filter field keys to be unslashed before saving.
1520 *
1521 * Works used with the filter wpforms_enable_form_data_slashing set to true.
1522 *
1523 * @since 1.9.0
1524 *
1525 * @param array $keys Field keys.
1526 *
1527 * @return array
1528 */
1529 $keys = (array) apply_filters( 'wpforms_form_handler_unslash_field_keys', $keys );
1530
1531 if ( empty( $keys ) ) {
1532 return $data;
1533 }
1534
1535 foreach ( $data['fields'] as $id => $field ) {
1536 foreach ( $keys as $key ) {
1537 if ( isset( $field[ $key ] ) ) {
1538 $data['fields'][ $id ][ $key ] = wp_unslash( $field[ $key ] );
1539 }
1540 }
1541 }
1542
1543 return $data;
1544 }
1545
1546 /**
1547 * Removes content filters that may break forms with HTML and adds a filter to prevent JSON damage.
1548 *
1549 * Specifically, it removes filters that sanitize content in a way that disrupts form functionality:
1550 * - `balanceTags` to prevent unintended tag balancing.
1551 * - `wp_filter_post_kses` to bypass permission-based sanitization (`unfiltered_html` capability).
1552 *
1553 * Additionally, adds a filter to clear `link rel` attributes to avoid unintended JSON issues.
1554 *
1555 * @since 1.9.8
1556 */
1557 private function remove_form_content_filters(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
1558 // This filter breaks forms if they contain HTML.
1559 remove_filter( 'content_save_pre', 'balanceTags', 50 );
1560 // This filter breaks forms if the current user doesn't have "unfiltered_html" capability.
1561 remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
1562
1563 // Add a filter of the link rel attr to avoid JSON damage.
1564 add_filter( 'wp_targeted_link_rel', '__return_empty_string', 50, 1 );
1565 }
1566 }
1567