PluginProbe ʕ •ᴥ•ʔ
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More / 1.10.1
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More v1.10.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 6 days ago emails 4 months ago fields 6 days ago functions 6 days ago providers 10 months ago templates 4 months ago class-db.php 1 year ago class-fields.php 1 year ago class-form.php 6 days 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
1571 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 try {
781 $_form_id = wp_update_post( $form );
782 } finally {
783 if ( ! empty( $args['skip_revision'] ) ) {
784 add_action( 'post_updated', 'wp_save_post_revision' );
785 }
786 }
787
788 if ( is_wp_error( $_form_id ) ) {
789 return false;
790 }
791
792 /**
793 * Fires after saving the form.
794 *
795 * @since 1.0.0
796 *
797 * @param int $_form_id Form ID.
798 * @param array $form Form.
799 */
800 do_action( 'wpforms_save_form', $_form_id, $form ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
801
802 return $_form_id;
803 }
804
805 /**
806 * Preserve fields meta in 'update' method.
807 *
808 * @since 1.5.8
809 *
810 * @param array $fields Form fields.
811 * @param string|int $form_id Form ID.
812 *
813 * @return array
814 */
815 protected function update__preserve_fields_meta( $fields, $form_id ): array {
816
817 foreach ( $fields as $i => $field_data ) {
818 if ( isset( $field_data['id'] ) ) {
819 $field_meta = $this->get_field_meta( $form_id, $field_data['id'] );
820
821 if ( $field_meta ) {
822 $fields[ $i ]['meta'] = $field_meta;
823 }
824 }
825 }
826
827 return $fields;
828 }
829
830 /**
831 * Sanitize notifications names meta in 'update' method.
832 *
833 * @since 1.5.8
834 *
835 * @param array $notifications Form notifications.
836 *
837 * @return array
838 */
839 protected function update__sanitize_notifications_names( $notifications ): array {
840
841 foreach ( $notifications as &$notification ) {
842 if ( ! empty( $notification['notification_name'] ) ) {
843 $notification['notification_name'] = sanitize_text_field( $notification['notification_name'] );
844 }
845 }
846
847 return $notifications;
848 }
849
850 /**
851 * Duplicate forms.
852 *
853 * @since 1.1.4
854 * @since 1.8.8 Return array of new form IDs instead of true.
855 *
856 * @param array|string $ids Form IDs to duplicate.
857 *
858 * @return bool|array Array of new form IDs or false.
859 */
860 public function duplicate( $ids ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
861
862 // Check for permissions.
863 if ( ! wpforms_current_user_can( 'create_forms' ) ) {
864 return false;
865 }
866
867 $this->remove_form_content_filters();
868
869 if ( ! is_array( $ids ) ) {
870 $ids = [ $ids ];
871 }
872
873 $ids = array_map( 'absint', $ids );
874
875 $duplicate_ids = [];
876
877 foreach ( $ids as $id ) {
878
879 // Get the original entry.
880 $form = get_post( $id );
881
882 if ( ! wpforms_current_user_can( 'view_form_single', $id ) ) {
883 return false;
884 }
885
886 // Confirm form exists.
887 if ( empty( $form ) ) {
888 return false;
889 }
890
891 // Get the form data.
892 $new_form_data = (array) wpforms_decode( $form->post_content );
893
894 if ( $this->is_form_data_slashing_enabled ) {
895 $new_form_data = (array) wp_slash( $new_form_data );
896 }
897
898 // Remove form ID from the title if present.
899 $new_form_data['settings']['form_title'] = str_replace( '(ID #' . absint( $id ) . ')', '', $new_form_data['settings']['form_title'] );
900
901 // Remove '(copy)' from the form template title if present.
902 $new_form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $new_form_data['settings']['form_title'] );
903
904 // Remove trailing spaces.
905 $new_form_data['settings']['form_title'] = rtrim( $new_form_data['settings']['form_title'] );
906
907 // Remove the `-template ` suffix and all after it from the post name.
908 $post_name = preg_replace( '/-template(-\d+)?/', '', $form->post_name );
909
910 // Add some notice messages before form preview area.
911 $new_form_data = $this->add_notices( $new_form_data, (int) $id );
912
913 // Create the duplicate form.
914 $new_form = [
915 'post_content' => wpforms_encode( $new_form_data ),
916 'post_excerpt' => $form->post_excerpt,
917 'post_status' => $form->post_status,
918 'post_title' => $new_form_data['settings']['form_title'],
919 'post_type' => $form->post_type,
920 'post_name' => wpforms_is_form_template( $id ) ? $post_name . '-template' : $post_name,
921 ];
922 $new_form_id = wp_insert_post( $new_form );
923
924 if ( ! $new_form_id || is_wp_error( $new_form_id ) ) {
925 return false;
926 }
927
928 // Set a new form name.
929 $new_form_data['settings']['form_title'] .= $form->post_type === 'wpforms-template' ?
930 ' ' . __( '(copy)', 'wpforms-lite' ) :
931 ' (ID #' . absint( $new_form_id ) . ')';
932
933 // Set a new form ID.
934 $new_form_data['id'] = absint( $new_form_id );
935
936 // Update a new duplicate form.
937 $new_form_id = $this->update( $new_form_id, $new_form_data, [ 'cap' => 'create_forms' ] );
938
939 if ( ! $new_form_id ) {
940 return false;
941 }
942
943 // Add tags to the new form.
944 if ( ! empty( $new_form_data['settings']['form_tags'] ) ) {
945 wp_set_post_terms(
946 $new_form_id,
947 implode( ',', (array) $new_form_data['settings']['form_tags'] ),
948 self::TAGS_TAXONOMY
949 );
950 }
951
952 /**
953 * Fires after the form was duplicated.
954 *
955 * @since 1.8.2.2
956 *
957 * @param int $id Original form ID.
958 * @param int $new_form_id New form ID.
959 * @param array $new_form_data New form data.
960 */
961 do_action( 'wpforms_form_handler_duplicate_form', $id, $new_form_id, $new_form_data );
962
963 $duplicate_ids[] = $new_form_id;
964 }
965
966 return $duplicate_ids;
967 }
968
969 /**
970 * Convert form to a template and vice versa.
971 *
972 * @since 1.8.8
973 *
974 * @param string|int $form_id Form ID.
975 * @param string $convert_to Convert to, `form` or `template`.
976 *
977 * @return false|int New object ID or false on failure.
978 */
979 public function convert( $form_id, string $convert_to ) {
980
981 if ( ! in_array( $convert_to, [ 'form', 'template' ], true ) ) {
982 return false;
983 }
984
985 // Duplicate the form.
986 $ids = $this->duplicate( $form_id );
987
988 if ( empty( $ids ) ) {
989 return false;
990 }
991
992 $new_form_id = current( $ids );
993 $form = get_post( $new_form_id );
994 $form_data = wpforms_decode( $form->post_content );
995
996 if ( $this->is_form_data_slashing_enabled ) {
997 $form_data = wp_slash( $form_data );
998 }
999
1000 /**
1001 * Filters the form data before converting it to a template or vice versa.
1002 *
1003 * @since 1.8.8
1004 *
1005 * @param array $form_data Form data.
1006 * @param string|int $form_id Form ID.
1007 * @param string $convert_to Convert to, `form` or `template`.
1008 */
1009 $form_data = apply_filters( 'wpforms_form_handler_convert_form_data', $form_data, $form_id, $convert_to );
1010
1011 // Set default post type.
1012 $post_type = 'wpforms';
1013
1014 // Remove the numeric suffix from the post name.
1015 // Duplication always adds `-{numeric}` suffix.
1016 $post_name = preg_replace( '/-\d+$/', '', $form->post_name );
1017
1018 // Remove the `-template ` suffix and all after it from the post name.
1019 $post_name = preg_replace( '/-template(-\d+)?/', '', $post_name );
1020
1021 // Remove (copy) from the form title, if present.
1022 $form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $form_data['settings']['form_title'] );
1023
1024 // Remove trailing spaces.
1025 $form_data['settings']['form_title'] = rtrim( $form_data['settings']['form_title'] );
1026
1027 // Remove template description.
1028 unset( $form_data['settings']['template_description'] );
1029
1030 if ( $convert_to === 'template' ) {
1031 $post_type = 'wpforms-template';
1032
1033 // Remove (ID #<Form ID>) from the form title, if present.
1034 $form_data['settings']['form_title'] = preg_replace( '/\(ID #\d+\)/', '', $form_data['settings']['form_title'] );
1035
1036 // Set an empty template description.
1037 $form_data['settings']['template_description'] = '';
1038
1039 // Remove traces of any other template that may have been used to create the original form by setting itself as a template.
1040 $form_data['meta']['template'] = 'wpforms-user-template-' . $new_form_id;
1041
1042 // Add `-template` suffix to the post name.
1043 $post_name .= '-template';
1044 }
1045
1046 wp_update_post(
1047 [
1048 'ID' => $new_form_id,
1049 'post_title' => $form_data['settings']['form_title'],
1050 'post_type' => $post_type,
1051 'post_content' => wpforms_encode( $form_data ),
1052 'post_name' => $post_name,
1053 ]
1054 );
1055
1056 return $new_form_id;
1057 }
1058
1059 /**
1060 * Append notice(s) before form preview, if needed.
1061 *
1062 * @since 1.8.8
1063 *
1064 * @param array $new_form_data New form data.
1065 * @param int $form_id Original form ID.
1066 *
1067 * @return array
1068 */
1069 private function add_notices( array $new_form_data, int $form_id ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
1070
1071 /**
1072 * Add custom notices to be displayed in the preview area of the Form Builder
1073 * after a form or a form template has been duplicated or converted.
1074 *
1075 * @since 1.8.8
1076 *
1077 * @param array $notices Array of notices.
1078 * @param array $new_form_data Form data of the newly duplicated form or form template.
1079 * @param int $form_id Original form ID.
1080 */
1081 $notices = apply_filters( 'wpforms_form_handler_add_notices', [], $new_form_data, $form_id );
1082
1083 if ( empty( $notices ) ) {
1084 return $new_form_data;
1085 }
1086
1087 $current_field_id = ! empty( $new_form_data['fields'] ) ? max( array_keys( $new_form_data['fields'] ) ) : 0;
1088 $code_fields = array_column( $new_form_data['fields'], 'code' );
1089 $next_field_id = $current_field_id;
1090 $warning = [];
1091
1092 foreach ( $notices as $notice ) {
1093 // Skip the duplicate notice if it already exists.
1094 if ( ! empty( $notice['code'] ) && in_array( $notice['code'], $code_fields, true ) ) {
1095 continue;
1096 }
1097
1098 $next_field_id = ++$current_field_id;
1099 $warning[ $next_field_id ] = [
1100 'id' => $next_field_id,
1101 'type' => 'internal-information',
1102 'code' => ! empty( $notice['code'] ) ? esc_attr( $notice['code'] ) : '',
1103 'description' => '',
1104 ];
1105
1106 $warning[ $next_field_id ]['description'] .= ! empty( $notice['title'] ) ? '<strong>' . esc_html( $notice['title'] ) . '</strong>' : '';
1107 $warning[ $next_field_id ]['description'] .= ! empty( $notice['message'] ) ? '<p>' . wp_kses_post( $notice['message'] ) . '</p>' : '';
1108
1109 // Do not add notice with empty body.
1110 if ( empty( $warning[ $next_field_id ]['description'] ) ) {
1111 unset( $warning[ $next_field_id ] );
1112 --$next_field_id; // Reset the next field ID to the previous value.
1113 }
1114 }
1115
1116 if ( ! empty( $warning ) ) {
1117 $new_form_data['fields'] = $warning + $new_form_data['fields'];
1118
1119 // Update the next field ID to be used for future created fields. Otherwise, the IIF field would be overwritten.
1120 $new_form_data['field_id'] = $next_field_id + 1;
1121 }
1122
1123 return $new_form_data;
1124 }
1125
1126 /**
1127 * Add a notice about Zapier zaps disconnected after the form being duplicated or converted to/from the template.
1128 *
1129 * @WPFormsBackCompat Support Zapier v1.5.0 and earlier.
1130 *
1131 * @since 1.8.8
1132 *
1133 * @param array $notices Array of notices.
1134 * @param array $new_form_data Form data.
1135 * @param int $form_id Original form ID.
1136 *
1137 * @return array
1138 * @noinspection HtmlUnknownTarget
1139 * @noinspection PhpUnusedParameterInspection
1140 */
1141 public function _zapier_disconnected_on_duplication( $notices, array $new_form_data, int $form_id ): array { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
1142
1143 // Check if the original form had any Zaps connected.
1144 $is_zapier_connected = get_post_meta( $form_id, 'wpforms_zapier', true );
1145
1146 if ( ! $is_zapier_connected ) {
1147 return $notices;
1148 }
1149
1150 $notices['zapier'] = [
1151 'title' => esc_html__( 'Zaps Have Been Disabled', 'wpforms-lite' ),
1152 'code' => 'disconnected_on_duplication',
1153 'message' => sprintf( /* translators: %s - URL the to list of Zaps. */
1154 __( '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' ),
1155 esc_url( 'https://zapier.com/app/zaps' )
1156 ),
1157 ];
1158
1159 return $notices;
1160 }
1161
1162 /**
1163 * Get the next available field ID and increment by one.
1164 *
1165 * @since 1.0.0
1166 *
1167 * @param string|int $form_id Form ID.
1168 * @param array $args Additional arguments.
1169 *
1170 * @return mixed int or false
1171 */
1172 public function next_field_id( $form_id, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
1173
1174 if ( empty( $form_id ) ) {
1175 return false;
1176 }
1177
1178 $defaults = [
1179 'content_only' => true,
1180 ];
1181
1182 if ( isset( $args['cap'] ) ) {
1183 $defaults['cap'] = $args['cap'];
1184 }
1185
1186 $form = $this->get( $form_id, $defaults );
1187
1188 if ( $this->is_form_data_slashing_enabled ) {
1189 $form = wp_slash( $form );
1190 }
1191
1192 if ( empty( $form ) ) {
1193 return false;
1194 }
1195
1196 $field_id = 0;
1197 $max_field_id = ! empty( $form['fields'] ) ? max( array_keys( $form['fields'] ) ) : 0;
1198
1199 // We pass the `field_id` after duplicating the Layout field that contains a bunch of fields.
1200 // This is needed to avoid multiple AJAX calls after duplicating each field in the Layout.
1201 if ( isset( $args['field_id'] ) ) {
1202 $set_field_id = absint( $args['field_id'] ) - 1;
1203 $field_id = $set_field_id > $max_field_id ? $set_field_id : $max_field_id + 1;
1204 } elseif ( ! empty( $form['field_id'] ) ) {
1205 $field_id = absint( $form['field_id'] );
1206 $field_id = $max_field_id > $field_id ? $max_field_id + 1 : $field_id;
1207 }
1208
1209 $form['field_id'] = $field_id + 1;
1210
1211 // Skip creating a revision for this action.
1212 remove_action( 'post_updated', 'wp_save_post_revision' );
1213
1214 $this->update( $form_id, $form );
1215
1216 // Restore the initial revisions state.
1217 add_action( 'post_updated', 'wp_save_post_revision' );
1218
1219 return $field_id;
1220 }
1221
1222 /**
1223 * Get private meta information for a form.
1224 *
1225 * @since 1.0.0
1226 *
1227 * @param string|int $form_id Form ID.
1228 * @param string $field Field.
1229 * @param array $args Additional arguments.
1230 *
1231 * @return false|array
1232 */
1233 public function get_meta( $form_id, $field = '', $args = [] ) {
1234
1235 if ( empty( $form_id ) ) {
1236 return false;
1237 }
1238
1239 $defaults = [
1240 'content_only' => true,
1241 ];
1242
1243 if ( isset( $args['cap'] ) ) {
1244 $defaults['cap'] = $args['cap'];
1245 }
1246
1247 $data = $this->get( $form_id, $defaults );
1248
1249 if ( ! isset( $data['meta'] ) ) {
1250 return false;
1251 }
1252
1253 if ( empty( $field ) ) {
1254 return $data['meta'];
1255 }
1256
1257 return $data['meta'][ $field ] ?? false;
1258 }
1259
1260 /**
1261 * Update or add form meta information to a form.
1262 *
1263 * @since 1.4.0
1264 *
1265 * @param string|int $form_id Form ID.
1266 * @param string $meta_key Meta key.
1267 * @param mixed $meta_value Meta value.
1268 * @param array $args Additional arguments.
1269 *
1270 * @return false|int|WP_Error
1271 */
1272 public function update_meta( $form_id, $meta_key, $meta_value, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
1273
1274 if ( empty( $form_id ) || empty( $meta_key ) ) {
1275 return false;
1276 }
1277
1278 $this->remove_form_content_filters();
1279
1280 if ( ! isset( $args['cap'] ) ) {
1281 $args['cap'] = 'edit_form_single';
1282 }
1283
1284 $form = $this->get_single( absint( $form_id ), $args );
1285
1286 if ( empty( $form ) ) {
1287 return false;
1288 }
1289
1290 $data = (array) wpforms_decode( $form->post_content );
1291 $meta_key = wpforms_sanitize_key( $meta_key );
1292
1293 $data['meta'][ $meta_key ] = $meta_value;
1294
1295 $form = [
1296 'ID' => $form_id,
1297 'post_content' => wpforms_encode( $data ),
1298 ];
1299
1300 /**
1301 * Allow changing form before updating form meta.
1302 *
1303 * @since 1.4.0
1304 *
1305 * @param array $form Form.
1306 * @param array $data Form data.
1307 */
1308 $form = (array) apply_filters( 'wpforms_update_form_meta_args', $form, $data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1309
1310 if ( ! empty( $args['skip_revision'] ) ) {
1311 remove_action( 'post_updated', 'wp_save_post_revision' );
1312 }
1313
1314 try {
1315 $result = wp_update_post( $form );
1316 } finally {
1317 if ( ! empty( $args['skip_revision'] ) ) {
1318 add_action( 'post_updated', 'wp_save_post_revision' );
1319 }
1320 }
1321
1322 if ( is_wp_error( $result ) ) {
1323 return $result;
1324 }
1325
1326 /**
1327 * Fires when form meta is updated.
1328 *
1329 * @since 1.4.0
1330 *
1331 * @param string|int $form_id Form ID.
1332 * @param array $form Form.
1333 * @param string $meta_key Meta key.
1334 * @param mixed $meta_value Meta value.
1335 */
1336 do_action( 'wpforms_update_form_meta', $form_id, $form, $meta_key, $meta_value ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1337
1338 return $form_id;
1339 }
1340
1341 /**
1342 * Delete form meta information from a form.
1343 *
1344 * @since 1.4.0
1345 *
1346 * @param string|int $form_id Form ID.
1347 * @param string $meta_key Meta key.
1348 * @param array $args Additional arguments.
1349 *
1350 * @return false|int|WP_Error
1351 */
1352 public function delete_meta( $form_id, $meta_key, $args = [] ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
1353
1354 if ( empty( $form_id ) || empty( $meta_key ) ) {
1355 return false;
1356 }
1357
1358 $this->remove_form_content_filters();
1359
1360 if ( ! isset( $args['cap'] ) ) {
1361 $args['cap'] = 'edit_form_single';
1362 }
1363
1364 $form = $this->get_single( absint( $form_id ), $args );
1365
1366 if ( empty( $form ) ) {
1367 return false;
1368 }
1369
1370 $data = (array) wpforms_decode( $form->post_content );
1371 $meta_key = wpforms_sanitize_key( $meta_key );
1372
1373 unset( $data['meta'][ $meta_key ] );
1374
1375 $form = [
1376 'ID' => $form_id,
1377 'post_content' => wpforms_encode( $data ),
1378 ];
1379
1380 /**
1381 * Filters form which meta to be deleted.
1382 *
1383 * @since 1.4.0
1384 *
1385 * @param array $form Form.
1386 * @param array $data Form data.
1387 */
1388 $form = (array) apply_filters( 'wpforms_delete_form_meta_args', $form, $data ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1389 $result = wp_update_post( $form );
1390
1391 if ( is_wp_error( $result ) ) {
1392 return $result;
1393 }
1394
1395 /**
1396 * Fires when form meta is deleted.
1397 *
1398 * @since 1.4.0
1399 *
1400 * @param string|int $form_id Form ID.
1401 * @param array $form Form.
1402 * @param string $meta_key Meta key.
1403 */
1404 do_action( 'wpforms_delete_form_meta', $form_id, $form, $meta_key ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
1405
1406 return $form_id;
1407 }
1408
1409 /**
1410 * Get private meta information for a form field.
1411 *
1412 * @since 1.0.0
1413 *
1414 * @param string|int $form_id Form ID.
1415 * @param string $field_id Field ID.
1416 * @param array $args Additional arguments.
1417 *
1418 * @return array|false
1419 */
1420 public function get_field( $form_id, $field_id = '', $args = [] ) {
1421
1422 if ( empty( $form_id ) ) {
1423 return false;
1424 }
1425
1426 $defaults = [
1427 'content_only' => true,
1428 ];
1429
1430 if ( isset( $args['cap'] ) ) {
1431 $defaults['cap'] = $args['cap'];
1432 }
1433
1434 $data = $this->get( $form_id, $defaults );
1435
1436 return $data['fields'][ $field_id ] ?? false;
1437 }
1438
1439 /**
1440 * Get private meta information for a form field.
1441 *
1442 * @since 1.0.0
1443 *
1444 * @param string|int $form_id Form ID.
1445 * @param string $field_id Field ID.
1446 * @param array $args Additional arguments.
1447 *
1448 * @return array|false
1449 */
1450 public function get_field_meta( $form_id, $field_id = '', $args = [] ) {
1451
1452 $field = $this->get_field( $form_id, $field_id, $args );
1453
1454 if ( ! $field ) {
1455 return false;
1456 }
1457
1458 return $field['meta'] ?? false;
1459 }
1460
1461 /**
1462 * Checks if any forms are present on the site.
1463 *
1464 * @since 1.8.8
1465 *
1466 * @retun bool
1467 */
1468 public function forms_exist(): bool {
1469
1470 return (bool) $this->get(
1471 '',
1472 [
1473 'numberposts' => 1,
1474 'fields' => 'ids',
1475 'no_found_rows' => true,
1476 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
1477 'nopaging' => false,
1478 'update_post_meta_cache' => false,
1479 'update_post_term_cache' => false,
1480 ]
1481 );
1482 }
1483
1484 /**
1485 * Get the forms' count per page.
1486 *
1487 * @since 1.8.8
1488 *
1489 * @return int
1490 */
1491 public function get_count_per_page(): int {
1492
1493 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
1494 /**
1495 * Allow developers to modify the number of forms per page.
1496 *
1497 * @since 1.8.8
1498 *
1499 * @param array $count Forms count per page.
1500 */
1501 return (int) apply_filters( 'wpforms_forms_per_page', 20 );
1502 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
1503 }
1504
1505 /**
1506 * Unslash field keys.
1507 *
1508 * @since 1.9.0
1509 *
1510 * @param array $data Form data.
1511 * @param array $keys Field keys.
1512 *
1513 * @return array
1514 * @noinspection PhpSameParameterValueInspection
1515 */
1516 private function unslash_field_keys( array $data, array $keys ): array {
1517
1518 if ( empty( $data['fields'] ) ) {
1519 return $data;
1520 }
1521
1522 /**
1523 * Filter field keys to be unslashed before saving.
1524 *
1525 * Works used with the filter wpforms_enable_form_data_slashing set to true.
1526 *
1527 * @since 1.9.0
1528 *
1529 * @param array $keys Field keys.
1530 *
1531 * @return array
1532 */
1533 $keys = (array) apply_filters( 'wpforms_form_handler_unslash_field_keys', $keys );
1534
1535 if ( empty( $keys ) ) {
1536 return $data;
1537 }
1538
1539 foreach ( $data['fields'] as $id => $field ) {
1540 foreach ( $keys as $key ) {
1541 if ( isset( $field[ $key ] ) ) {
1542 $data['fields'][ $id ][ $key ] = wp_unslash( $field[ $key ] );
1543 }
1544 }
1545 }
1546
1547 return $data;
1548 }
1549
1550 /**
1551 * Removes content filters that may break forms with HTML and adds a filter to prevent JSON damage.
1552 *
1553 * Specifically, it removes filters that sanitize content in a way that disrupts form functionality:
1554 * - `balanceTags` to prevent unintended tag balancing.
1555 * - `wp_filter_post_kses` to bypass permission-based sanitization (`unfiltered_html` capability).
1556 *
1557 * Additionally, adds a filter to clear `link rel` attributes to avoid unintended JSON issues.
1558 *
1559 * @since 1.9.8
1560 */
1561 private function remove_form_content_filters(): void { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks
1562 // This filter breaks forms if they contain HTML.
1563 remove_filter( 'content_save_pre', 'balanceTags', 50 );
1564 // This filter breaks forms if the current user doesn't have "unfiltered_html" capability.
1565 remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
1566
1567 // Add a filter of the link rel attr to avoid JSON damage.
1568 add_filter( 'wp_targeted_link_rel', '__return_empty_string', 50, 1 );
1569 }
1570 }
1571