: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Filters form creation arguments.
* @param array $args Form creation arguments.
* @param array $data Additional data.
$args = apply_filters( 'wpforms_create_form_args', $args, $data );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
'form_title' => sanitize_text_field( $title ),
$args_form_data = isset( $args['post_content'] ) ? json_decode( wp_unslash( $args['post_content'] ), true ) : null;
// Prevent $args['post_content'] from overwriting predefined $form_content.
// Typically, it happens if the form was created with a form template and a user was not redirected to a form editing screen afterwards.
// This is only possible if a user has 'wpforms_create_forms' and no 'wpforms_edit_own_forms' capability.
if ( is_array( $args_form_data ) ) {
$args['post_content'] = wpforms_encode( array_replace_recursive( $form_content, $args_form_data ) );
// Merge args and create the form.
'post_title' => esc_html( $title ),
'post_status' => 'publish',
'post_type' => 'wpforms',
'post_content' => wpforms_encode( $form_content ),
$form_id = wp_insert_post( $form );
if ( ! empty( $form_id ) && ! empty( $args_form_data['settings']['form_tags'] ) ) {
implode( ',', $args_form_data['settings']['form_tags'] ),
// If user has no editing permissions the form considered to be created out of the WPForms form builder's context.
if ( ! wpforms_current_user_can( 'edit_form_single', $form_id ) ) {
$data['builder'] = false;
// If the form is created outside the context of the WPForms form
// builder, then we define some additional default values.
if ( ! empty( $form_id ) && isset( $data['builder'] ) && $data['builder'] === false ) {
$form_data = json_decode( wp_unslash( $form['post_content'] ), true );
$form_data['id'] = $form_id;
$form_data['settings']['submit_text'] = esc_html__( 'Submit', 'wpforms-lite' );
$form_data['settings']['submit_text_processing'] = esc_html__( 'Sending...', 'wpforms-lite' );
$form_data['settings']['notification_enable'] = '1';
$form_data['settings']['notifications'] = [
'email' => '{admin_email}',
'subject' => sprintf( /* translators: %s - form name. */
esc_html__( 'New Entry: %s', 'wpforms-lite' ),
'sender_name' => get_bloginfo( 'name' ),
'sender_address' => '{admin_email}',
'replyto' => '{field_id="1"}',
'message' => '{all_fields}',
$form_data['settings']['confirmations'] = [
'message' => esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' ),
$this->update( $form_id, $form_data, [ 'cap' => 'create_forms' ] );
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
* Fires after the form was created.
* @param int $form_id Form ID.
* @param array $form Form data.
* @param array $data Additional data.
do_action( 'wpforms_create_form', $form_id, $form, $data );
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
* @param string|int $form_id Form ID.
* @param array $data Data retrieved from $_POST and processed.
* @param array $args Empty by default, may have custom data not intended to be saved.
* @internal param string $title
public function update( $form_id = '', $data = [], $args = [] ) {
if ( empty( $form_id ) && isset( $data['id'] ) ) {
if ( ! isset( $args['cap'] ) ) {
$args['cap'] = 'edit_form_single';
if ( ! empty( $args['cap'] ) && ! wpforms_current_user_can( $args['cap'], $form_id ) ) {
// This filter breaks forms if they contain HTML.
remove_filter( 'content_save_pre', 'balanceTags', 50 );
// Add filter of the link rel attr to avoid JSON damage.
add_filter( 'wp_targeted_link_rel', '__return_empty_string', 50, 1 );
$data = wp_unslash( $data );
$title = empty( $data['settings']['form_title'] ) ? get_the_title( $form_id ) : $data['settings']['form_title'];
$desc = empty( $data['settings']['form_desc'] ) ? '' : $data['settings']['form_desc'];
$data['field_id'] = ! empty( $data['field_id'] ) ? wpforms_validate_field_id( $data['field_id'] ) : '0';
// Preserve explicit "Do not store spam entries" state.
$data['settings']['store_spam_entries'] = $data['settings']['store_spam_entries'] ?? '0';
$meta = $this->get_meta( $form_id );
// Update category and subcategory only if available.
if ( ! empty( $args['category'] ) ) {
$data['meta']['category'] = $args['category'];
if ( ! empty( $args['subcategory'] ) ) {
$data['meta']['subcategory'] = $args['subcategory'];
if ( isset( $data['fields'] ) ) {
$data['fields'] = $this->update__preserve_fields_meta( $data['fields'], $form_id );
// Sanitize - don't allow tags for users who do not have appropriate cap.
// If we don't do this, forms for these users can get corrupt due to
// conflicts with wp_kses().
if ( ! current_user_can( 'unfiltered_html' ) ) {
$data = map_deep( $data, 'wp_strip_all_tags' );
// Sanitize notifications names.
if ( isset( $data['settings']['notifications'] ) ) {
$data['settings']['notifications'] = $this->update__sanitize_notifications_names( $data['settings']['notifications'] );
if ( wpforms_is_form_template( $form_id ) ) {
$form_title = $data['settings']['form_title'];
// We need setup slugs for the form template because these fields are hidden in the form builder.
$data['settings']['form_pages_page_slug'] = sanitize_title( $form_title );
$data['settings']['conversational_forms_page_slug'] = sanitize_title( $form_title );
'wpforms_save_form_args',
'post_title' => esc_html( $title ),
'post_content' => wpforms_encode( $data ),
$_form_id = wp_update_post( $form );
do_action( 'wpforms_save_form', $_form_id, $form );
* Preserve fields meta in 'update' method.
* @param array $fields Form fields.
* @param string|int $form_id Form ID.
protected function update__preserve_fields_meta( $fields, $form_id ) {
foreach ( $fields as $i => $field_data ) {
if ( isset( $field_data['id'] ) ) {
$field_meta = $this->get_field_meta( $form_id, $field_data['id'] );
$fields[ $i ]['meta'] = $field_meta;
* Sanitize notifications names meta in 'update' method.
* @param array $notifications Form notifications.
protected function update__sanitize_notifications_names( $notifications ) {
foreach ( $notifications as $id => &$notification ) {
if ( ! empty( $notification['notification_name'] ) ) {
$notification['notification_name'] = sanitize_text_field( $notification['notification_name'] );
* @since 1.8.8 Return array of new form IDs instead of true.
* @param array|string $ids Form IDs to duplicate.
* @return bool|array Array of new form IDs or false.
public function duplicate( $ids ) { // phpcs:disable WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh
// Check for permissions.
if ( ! wpforms_current_user_can( 'create_forms' ) ) {
// Add filter of the link rel attr to avoid JSON damage.
add_filter( 'wp_targeted_link_rel', '__return_empty_string', 50, 1 );
// This filter breaks forms if they contain HTML.
remove_filter( 'content_save_pre', 'balanceTags', 50 );
if ( ! is_array( $ids ) ) {
$ids = array_map( 'absint', $ids );
foreach ( $ids as $id ) {
if ( ! wpforms_current_user_can( 'view_form_single', $id ) ) {
$new_form_data = wpforms_decode( $form->post_content );
// Remove form ID from title if present.
$new_form_data['settings']['form_title'] = str_replace( '(ID #' . absint( $id ) . ')', '', $new_form_data['settings']['form_title'] );
// Remove '(copy)' from the form template title if present.
$new_form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $new_form_data['settings']['form_title'] );
// Remove trailing spaces.
$new_form_data['settings']['form_title'] = rtrim( $new_form_data['settings']['form_title'] );
// Remove `-template` suffix and all after it from the post name.
$post_name = preg_replace( '/-template(-\d+)?/', '', $form->post_name );
// Add some notice messages before form preview area.
$new_form_data = $this->add_notices( $new_form_data, (int) $id );
// Create the duplicate form.
'post_content' => wpforms_encode( $new_form_data ),
'post_excerpt' => $form->post_excerpt,
'post_status' => $form->post_status,
'post_title' => $new_form_data['settings']['form_title'],
'post_type' => $form->post_type,
'post_name' => wpforms_is_form_template( $id ) ? $post_name . '-template' : $post_name,
$new_form_id = wp_insert_post( $new_form );
if ( ! $new_form_id || is_wp_error( $new_form_id ) ) {
$new_form_data['settings']['form_title'] .= $form->post_type === 'wpforms-template' ?
' ' . __( '(copy)', 'wpforms-lite' ) :
' (ID #' . absint( $new_form_id ) . ')';
$new_form_data['id'] = absint( $new_form_id );
// Update new duplicate form.
$new_form_id = $this->update( $new_form_id, $new_form_data, [ 'cap' => 'create_forms' ] );
if ( ! $new_form_id || is_wp_error( $new_form_id ) ) {
// Add tags to the new form.
if ( ! empty( $new_form_data['settings']['form_tags'] ) ) {
implode( ',', (array) $new_form_data['settings']['form_tags'] ),
* Fires after the form was duplicated.
* @param int $id Original form ID.
* @param int $new_form_id New form ID.
* @param array $new_form_data New form data.
do_action( 'wpforms_form_handler_duplicate_form', $id, $new_form_id, $new_form_data );
$duplicate_ids[] = $new_form_id;
* Convert form to a template and vice versa.
* @param string|int $form_id Form ID.
* @param string $convert_to Convert to, `form` or `template`.
* @return false|int New object ID or false on failure.
public function convert( $form_id, string $convert_to ) {
if ( ! in_array( $convert_to, [ 'form', 'template' ], true ) ) {
$ids = $this->duplicate( $form_id );
$new_form_id = current( $ids );
$form = get_post( $new_form_id );
$form_data = wpforms_decode( $form->post_content );
* Filters the form data before converting it to a template or vice versa.
* @param array $form_data Form data.
* @param string|int $form_id Form ID.
* @param string $convert_to Convert to, `form` or `template`.
$form_data = apply_filters( 'wpforms_form_handler_convert_form_data', $form_data, $form_id, $convert_to );
// Set default post type.
// Remove numeric suffix from the post name.
// Duplication always adds `-{numeric}` suffix.
$post_name = preg_replace( '/-\d+$/', '', $form->post_name );
// Remove `-template` suffix and all after it from the post name.
$post_name = preg_replace( '/-template(-\d+)?/', '', $post_name );
// Remove (copy) from the form title, if present.
$form_data['settings']['form_title'] = str_replace( __( '(copy)', 'wpforms-lite' ), '', $form_data['settings']['form_title'] );
// Remove trailing spaces.
$form_data['settings']['form_title'] = rtrim( $form_data['settings']['form_title'] );
// Remove template description.
unset( $form_data['settings']['template_description'] );
if ( $convert_to === 'template' ) {
$post_type = 'wpforms-template';
// Remove (ID #<Form ID>) from the form title, if present.
$form_data['settings']['form_title'] = preg_replace( '/\(ID #\d+\)/', '', $form_data['settings']['form_title'] );
// Set empty template description.
$form_data['settings']['template_description'] = '';
// Remove traces of any other template that may have been used to create the original form by setting itself as a template.
$form_data['meta']['template'] = 'wpforms-user-template-' . $new_form_id;
// Add `-template` suffix to the post name.
$post_name .= '-template';
'post_title' => $form_data['settings']['form_title'],
'post_type' => $post_type,
'post_content' => wpforms_encode( $form_data ),
'post_name' => $post_name,
* Append notice(s) before form preview, if needed.
* @param array $new_form_data New form data.
* @param int $form_id Original form ID.
private function add_notices( array $new_form_data, int $form_id ): array {
* Add custom notices to be displayed in the preview area of the Form Builder
* after a form or a form template has been duplicated or converted.
* @param array $notices Array of notices.
* @param array $new_form_data Form data of the newly duplicated form or form template.
* @param int $form_id Original form ID.
$notices = apply_filters( 'wpforms_form_handler_add_notices', [], $new_form_data, $form_id );
if ( empty( $notices ) ) {
$current_field_id = ! empty( $new_form_data['fields'] ) ? max( array_keys( $new_form_data['fields'] ) ) : 0;
$code_fields = array_column( $new_form_data['fields'], 'code' );
$next_field_id = $current_field_id;