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