* All the form goodness and basics.
* Contains a bunch of helper methods as well.
class WPForms_Form_Handler {
const TAGS_TAXONOMY = 'wpforms_form_tag';
* Is form data slashing enabled.
private $is_form_data_slashing_enabled;
* Primary class constructor.
public function __construct() {
$this->is_form_data_slashing_enabled = wpforms_is_form_data_slashing_enabled();
private function hooks() {
// Register wpforms custom post type and taxonomy.
add_action( 'init', [ $this, 'register_taxonomy' ] );
add_action( 'init', [ $this, 'register_cpt' ] );
// Add wpforms to new-content admin bar menu.
add_action( 'admin_bar_menu', [ $this, 'admin_bar' ], 99 );
add_action( 'wpforms_create_form', [ $this, 'track_first_form' ], 10, 3 );
// @WPFormsBackCompat Support Zapier v1.5.0 and earlier.
add_filter( 'wpforms_form_handler_add_notices', [ $this, '_zapier_disconnected_on_duplication' ], 10, 3 );
* Register the custom post type to be used for forms.
public function register_cpt() {
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Filters Custom Post Type arguments.
* @param array $args Arguments.
'wpforms_post_type_args',
'exclude_from_search' => true,
'show_in_admin_bar' => false,
'supports' => [ 'title', 'author', 'revisions' ],
'capability_type' => 'wpforms_form', // Not using 'capability_type' anywhere. It just has to be custom for security reasons.
'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.
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
// Register the post type.
register_post_type( 'wpforms', $args );
* Register the new taxonomy for tags.
public function register_taxonomy() {
* Filters Tags taxonomy arguments.
* @param array $args Arguments.
'wpforms_form_handler_register_taxonomy_args',
register_taxonomy( self::TAGS_TAXONOMY, 'wpforms', $args );
* Add "WPForms" item to new-content admin bar menu item.
* @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference.
public function admin_bar( $wp_admin_bar ) {
if ( ! is_admin_bar_showing() || ! wpforms_current_user_can( 'create_forms' ) ) {
'title' => esc_html__( 'WPForms', 'wpforms-lite' ),
'href' => admin_url( 'admin.php?page=wpforms-builder' ),
'parent' => 'new-content',
$wp_admin_bar->add_node( $args );
* Preserve the timestamp when the very first form has been created.
* @param int $form_id Newly created form ID.
* @param array $form Array past to create a new form in wp_posts table.
* @param array $data Additional form data.
public function track_first_form( $form_id, $form, $data ) {
// Do we have the value already?
$time = get_option( 'wpforms_forms_first_created' );
// Check whether we have already saved this option - skip.
if ( ! empty( $time ) ) {
// Check whether we have any forms other than the currently created one.
$other_form = $this->get(
'post__not_in' => [ $form_id ], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
// As we have other forms - we are not certain about the situation, skip.
if ( ! empty( $other_form ) ) {
add_option( 'wpforms_forms_first_created', time(), '', 'no' );
* @param mixed $id Form ID.
* @param array $args Additional arguments array.
* @return array|bool|null|WP_Post
public function get( $id = '', array $args = [] ) {
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Allow developers to filter the WPForms_Form_Handler::get() arguments.
* @param array $args Arguments array.
* @param mixed $id Form ID.
$args = (array) apply_filters( 'wpforms_get_form_args', $args, $id );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
// By default, we should return only published forms.
'post_status' => 'publish',
$args = (array) wp_parse_args( $args, $defaults );
$forms = empty( $id ) ? $this->get_multiple( $args ) : $this->get_single( $id, $args );
return ! empty( $forms ) ? $forms : false;
* @param string|int $id Form ID.
* @param array $args Additional arguments array.
* @return array|bool|null|WP_Post
protected function get_single( $id = '', array $args = [] ) {
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Allow developers to filter the get_single() arguments.
* @param array $args Arguments array, same as for `get_post()` function.
* @param string|int $id Form ID.
$args = apply_filters( 'wpforms_get_single_form_args', $args, $id );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
if ( ! isset( $args['cap'] ) && wpforms()->obj( 'access' )->init_allowed() ) {
$args['cap'] = 'view_form_single';
if ( ! empty( $args['cap'] ) && ! wpforms_current_user_can( $args['cap'], $id ) ) {
// If no ID provided, we can't get a single form.
// If ID is provided, we get a single form.
$form = get_post( absint( $id ) );
// Check if the form exists.
if ( empty( $form ) || ! $form instanceof WP_Post ) {
// Check if the form is of the allowed post type.
if ( ! in_array( $form->post_type, self::POST_TYPES, true ) ) {
// Decode the form content.
if ( ! empty( $args['content_only'] ) ) {
$form = wpforms_decode( $form->post_content );
* @since 1.7.2 Added support for $args['search']['term'] - search form title or description by term.
* @param array $args Additional arguments array.
protected function get_multiple( array $args = [] ): array {
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Allow developers to filter the get_multiple() arguments.
* @param array $args Arguments array. Almost the same as for `get_posts()` function.
* ['search']['term'] - search the form title or description by term.
$args = (array) apply_filters( 'wpforms_get_multiple_forms_args', $args );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
// No ID provided, get multiple forms.
'suppress_filters' => false,
$args = wp_parse_args( $args, $defaults );
$post_type = $args['post_type'] ?? [];
// Post type should be one of the allowed post types.
$post_type = array_intersect( (array) $post_type, self::POST_TYPES );
// If no valid (allowed) post types are provided, use the default one.
$args['post_type'] = ! empty( $post_type ) ? $post_type : 'wpforms';
* Allow developers to execute some code before get_posts() call inside \WPForms_Form_Handler::get_multiple().
* @param array $args Arguments of the `get_posts()`.
do_action( 'wpforms_form_handler_get_multiple_before_get_posts', $args );
$forms = get_posts( $args );
* Allow developers to execute some code right after get_posts() call inside \WPForms_Form_Handler::get_multiple().
* @param array $args Arguments of the `get_posts`.
* @param array $forms Forms data. Result of getting multiple forms.
do_action( 'wpforms_form_handler_get_multiple_after_get_posts', $args, $forms );
* Allow developers to filter the result of get_multiple().
* @param array $forms Result of getting multiple forms.
return apply_filters( 'wpforms_form_handler_get_multiple_forms_result', $forms );
* Update the form status.
* @param int $form_id Form ID.
* @param string $status New status.
public function update_status( $form_id, $status ) {
// Status updates are used only in trash and restore actions,
// which are actually part of the deletion operation.
// Therefore, we should check the `delete_form_single` and not `edit_form_single` permission.
if ( ! wpforms_current_user_can( 'delete_form_single', $form_id ) ) {
$form_id = absint( $form_id );
$status = empty( $status ) ? 'publish' : sanitize_key( $status );
* Filters the allowed form statuses.
* @param array $allowed_statuses Array of allowed form statuses. Default: publish, trash.
$allowed = (array) apply_filters( 'wpforms_form_handler_update_status_allowed', [ 'publish', 'trash' ] );
if ( ! in_array( $status, $allowed, true ) ) {
$result = wp_update_post(
'post_status' => $status,
* Allow developers to execute some code after changing form status.
* @param string $form_id Form ID.
* @param string $status New form status, `publish` or `trash`.
do_action( 'wpforms_form_handler_update_status', $form_id, $status );
* Delete all forms in the Trash.
* @return int|bool Number of deleted forms OR false.
public function empty_trash() {
$forms = $this->get_multiple(
'post_type' => self::POST_TYPES,
'post_status' => 'trash',
'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
return $this->delete( $forms ) ? count( $forms ) : false;
* @param array $ids Form IDs.
public function delete( $ids = [] ) {
if ( ! is_array( $ids ) ) {
$ids = array_map( 'absint', $ids );
foreach ( $ids as $id ) {
// Check for permissions.
if ( ! wpforms_current_user_can( 'delete_form_single', $id ) ) {
if ( class_exists( 'WPForms_Entry_Handler', false ) ) {
wpforms()->obj( 'entry' )->delete_by( 'form_id', $id );
wpforms()->obj( 'entry_meta' )->delete_by( 'form_id', $id );
wpforms()->obj( 'entry_fields' )->delete_by( 'form_id', $id );
$form = wp_delete_post( $id, true );
do_action( 'wpforms_delete_form', $ids );