: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
private function add_locations( $locations_to_add ) {
foreach ( $locations_to_add as $location_to_add ) {
$locations = get_post_meta( $location_to_add['form_id'], self::LOCATIONS_META, true );
$locations[] = $location_to_add;
update_post_meta( $location_to_add['form_id'], self::LOCATIONS_META, $locations );
* Update form locations on widget update.
* @param mixed $old_value The old option value.
* @param mixed $value The new option value.
* @param string $option Option name.
public function update_option( $old_value, $value, $option ) {
case self::WPFORMS_WIDGET_OPTION:
$old_locations = $this->search_in_wpforms_widgets( $old_value );
$new_locations = $this->search_in_wpforms_widgets( $value );
case self::TEXT_WIDGET_OPTION:
$old_locations = $this->search_in_text_widgets( $old_value );
$new_locations = $this->search_in_text_widgets( $value );
case self::BLOCK_WIDGET_OPTION:
$old_locations = $this->search_in_block_widgets( $old_value );
$new_locations = $this->search_in_block_widgets( $value );
// phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement
$this->remove_locations( $this->array_udiff( $old_locations, $new_locations ) );
$this->add_locations( $this->array_udiff( $new_locations, $old_locations ) );
* Delete locations and schedule new rescan on change of permalink structure.
* @param string $old_permalink_structure The previous permalink structure.
* @param string $permalink_structure The new permalink structure.
* @noinspection PhpUnusedParameterInspection
public function permalink_structure_changed( $old_permalink_structure, $permalink_structure ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
* Run Forms Locator delete action.
do_action( FormsLocatorScanTask::DELETE_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
* Run Forms Locator scan action.
do_action( FormsLocatorScanTask::RESCAN_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
* Update form locations metas.
* @since 1.8.2.3 Added `$post_before` parameter.
* @param WP_Post|null $post_before The post before the update.
* @param WP_Post $post_after The post after the update.
* @param array $form_ids_before Form IDs before the update.
* @param array $form_ids_after Form IDs after the update.
private function update_form_locations_metas( $post_before, $post_after, $form_ids_before, $form_ids_after ) {
// Determine which locations to remove and which to add.
$form_ids_to_remove = array_diff( $form_ids_before, $form_ids_after );
$form_ids_to_add = array_diff( $form_ids_after, $form_ids_before );
// Loop through each form ID to remove the locations' meta.
foreach ( $form_ids_to_remove as $form_id ) {
$this->get_locations_without_current_post( $form_id, $post_after->ID )
// Determine the titles and slugs.
$old_title = $post_before->post_title ?? '';
$old_slug = $post_before->post_name ?? '';
$new_title = $post_after->post_title;
$new_slug = $post_after->post_name;
// If the title and slug are the same and there are no form IDs to add, bail.
if ( empty( $form_ids_to_add ) && $old_title === $new_title && $old_slug === $new_slug ) {
// Merge the form IDs and remove duplicates.
$form_ids = array_unique( array_merge( $form_ids_to_add, $form_ids_after ) );
$this->save_location_meta( $form_ids, $post_after->ID, $post_after );
* Save the location meta.
* @param array $form_ids Form IDs.
* @param int $post_id Post ID.
* @param WP_Post $post_after Post after the update.
private function save_location_meta( $form_ids, $post_id, $post_after ) {
$url = get_permalink( $post_id );
$url = ( $url === false || is_wp_error( $url ) ) ? '' : $url;
$url = str_replace( $this->home_url, '', $url );
// Loop through each Form ID and save the location meta.
foreach ( $form_ids as $form_id ) {
$locations = $this->get_locations_without_current_post( $form_id, $post_id );
'type' => $post_after->post_type,
'title' => $post_after->post_title,
'status' => $post_after->post_status,
update_post_meta( $form_id, self::LOCATIONS_META, $locations );
* Get post types for search in.
public function get_post_types() {
'publicly_queryable' => true,
$post_types = get_post_types( $args, 'names', 'or' );
unset( $post_types['attachment'] );
$post_types[] = self::WP_TEMPLATE;
$post_types[] = self::WP_TEMPLATE_PART;
* Get post statuses for search in.
public function get_post_statuses() {
return [ 'publish', 'pending', 'draft', 'future', 'private' ];
* Get form ids from the content.
* @param string $content Content.
public function get_form_ids( $content ) {
* Extract id from conventional wpforms shortcode or wpforms block.
* [wpforms id="32" title="true" description="true"]
* <!-- wp:wpforms/form-selector {"clientId":"b5f8e16a-fc28-435d-a43e-7c77719f074c", "formId":"32","displayTitle":true,"displayDesc":true} /-->
* In both, we should find 32.
'#\[\s*wpforms.+id\s*=\s*"(\d+?)".*]|<!-- wp:wpforms/form-selector {.*?"formId":"(\d+?)".*?} /-->#',
array_unique( array_filter( array_merge( ...$matches ) ) )
* Get form locations without a current post.
* @param int $form_id Form id.
* @param int $post_id Post id.
private function get_locations_without_current_post( $form_id, $post_id ) {
$locations = get_post_meta( $form_id, self::LOCATIONS_META, true );
if ( ! is_array( $locations ) ) {
static function ( $location ) use ( $post_id ) {
return $location['id'] !== $post_id;
* Determine whether a post is visible.
* @param array $location Post location.
private function is_post_visible( $location ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$post_id = $location['id'];
if ( ! get_post_type_object( $location['type'] ) ) {
// Post type is not registered.
$post_status_obj = get_post_status_object( $location['status'] );
if ( ! $post_status_obj ) {
// Post status is not registered, assume it's not public.
return current_user_can( $edit_cap, $post_id );
if ( $post_status_obj->public ) {
if ( ! is_user_logged_in() ) {
// User must be logged in to view unpublished posts.
if ( $post_status_obj->protected ) {
// User must have edit permissions on the draft to preview.
return current_user_can( $edit_cap, $post_id );
if ( $post_status_obj->private ) {
return current_user_can( $read_cap, $post_id );
* Build a standalone location.
* @param int $form_id The form ID.
* @param array $form_data Form data.
* @param string $status Form status.
* @return array Location.
public function build_standalone_location( int $form_id, array $form_data, string $status = 'publish' ): array {
if ( empty( $form_id ) || empty( $form_data ) ) {
// Form templates should not have any locations.
if ( get_post_type( $form_id ) === 'wpforms-template' ) {
foreach ( self::STANDALONE_LOCATION_TYPES as $location_type ) {
if ( empty( $form_data['settings'][ "{$location_type}_enable" ] ) ) {
return $this->build_standalone_location_type( $location_type, $form_id, $form_data, $status );
* Build a standalone location.
* @param string $location_type Standalone location type.
* @param int $form_id The form ID.
* @param array $form_data Form data.
* @param string $status Form status.
* @return array Location.
private function build_standalone_location_type( string $location_type, int $form_id, array $form_data, string $status ): array {
$title_key = "{$location_type}_title";
$slug_key = "{$location_type}_page_slug";
$title = $form_data['settings'][ $title_key ] ?? '';
$slug = $form_data['settings'][ $slug_key ] ?? '';
// Return the location array.
'type' => $location_type,
'form_id' => (int) $form_data['id'],
'url' => '/' . $slug . '/',
* Add standalone form locations to post meta.
* Post meta is used to store all forms' locations,
* which is displayed on the WPForms Overview page.
* @param int $form_id Form ID.
* @param array $data Form data.
public function add_standalone_location_to_locations_meta( int $form_id, array $data ) {
// Build standalone location.
$location = $this->build_standalone_location( $form_id, $data );
if ( empty( $location ) ) {
$new_location[] = $location;
$post_meta = get_post_meta( $form_id, self::LOCATIONS_META, true );
// If there is post meta, merge it with the new location.
if ( ! empty( $post_meta ) ) {
// Remove any previously set standalone locations.
$post_meta = $this->remove_standalone_location_from_array( $form_id, $post_meta );
// Merge locations and remove duplicates.
$new_location = array_unique( array_merge( $post_meta, $new_location ), SORT_REGULAR );
update_post_meta( $form_id, self::LOCATIONS_META, $new_location );
* Remove a form page from an array.
* @param int $form_id The form ID.
* @param array $post_meta The post meta.
* @return array $post_meta Filtered post meta.
private function remove_standalone_location_from_array( int $form_id, array $post_meta ): array {
// No form ID or post meta? Bail.
if ( empty( $form_id ) || empty( $post_meta ) ) {
// Loop over all locations.
foreach ( $post_meta as $key => $location ) {
// Verify the location keys exist.
if ( ! isset( $location['form_id'], $location['type'] ) ) {
// If the form ID and location type match.
if ( $location['form_id'] === $form_id && $this->is_standalone( $location['type'] ) ) {
// Unset the form page location.
unset( $post_meta[ $key ] );