<?php
// phpcs:disable Generic.Commenting.DocComment.MissingShort
/** @noinspection PhpUnnecessaryCurlyVarSyntaxInspection */
// phpcs:enable Generic.Commenting.DocComment.MissingShort
namespace WPForms\Forms;
use WP_Post;
use WPForms\Tasks\Actions\FormsLocatorScanTask;
/**
* Class Locator.
*
* @since 1.7.4
*/
class Locator {
/**
* Column name on Forms Overview admin page.
*
* @since 1.7.4
*/
const COLUMN_NAME = 'locations';
/**
* Locations meta key.
*
* @since 1.7.4
*/
const LOCATIONS_META = 'wpforms_form_locations';
/**
* WPForms widget name.
*
* @since 1.7.4
*/
const WPFORMS_WIDGET_NAME = 'wpforms-widget';
/**
* WPForms widget prefix.
*
* @since 1.7.4
*/
const WPFORMS_WIDGET_PREFIX = self::WPFORMS_WIDGET_NAME . '-';
/**
* WPForms widgets option name.
*
* @since 1.7.4
*/
const WPFORMS_WIDGET_OPTION = 'widget_' . self::WPFORMS_WIDGET_NAME;
/**
* Text widget name.
*
* @since 1.7.4
*/
const TEXT_WIDGET_NAME = 'text';
/**
* Text widget prefix.
*
* @since 1.7.4
*/
const TEXT_WIDGET_PREFIX = self::TEXT_WIDGET_NAME . '-';
/**
* Text widgets option name.
*
* @since 1.7.4
*/
const TEXT_WIDGET_OPTION = 'widget_' . self::TEXT_WIDGET_NAME;
/**
* Block widget name.
*
* @since 1.7.4
*/
const BLOCK_WIDGET_NAME = 'block';
/**
* Block widget prefix.
*
* @since 1.7.4
*/
const BLOCK_WIDGET_PREFIX = self::BLOCK_WIDGET_NAME . '-';
/**
* Block widgets' option name.
*
* @since 1.7.4
*/
const BLOCK_WIDGET_OPTION = 'widget_' . self::BLOCK_WIDGET_NAME;
/**
* Location type for widget.
* For a page/post, the location type is the post type.
*
* @since 1.7.4
*/
const WIDGET = 'widget';
/**
* WP template post type.
*
* @since 1.7.4
*/
const WP_TEMPLATE = 'wp_template';
/**
* WP template post type.
*
* @since 1.7.4.1
*/
const WP_TEMPLATE_PART = 'wp_template_part';
/**
* Standalone location types.
*
* @since 1.8.7
*/
const STANDALONE_LOCATION_TYPES = [ 'form_pages', 'conversational_forms' ];
/**
* Default title for WPForms widget.
* For WPForms widget, we extract title from the widget. If it is empty, we use the default one.
*
* @since 1.7.4
*
* @var string
*/
private $wpforms_widget_title = '';
/**
* Default title for text widget.
* For text widget, we extract title from the widget. If it is empty, we use the default one.
*
* @since 1.7.4
*
* @var string
*/
private $text_widget_title = '';
/**
* Fixed title for block widget.
*
* @since 1.7.4
*
* @var string
*/
private $block_widget_title = '';
/**
* Home url.
*
* @since 1.7.4
*
* @var string
*/
private $home_url;
/**
* Scan status.
*
* @since 1.7.4
*
* @var string
*/
private $scan_status;
/**
* Init class.
*
* @since 1.7.4
*/
public function init() {
$this->home_url = home_url();
$this->scan_status = (string) get_option( FormsLocatorScanTask::SCAN_STATUS );
$this->wpforms_widget_title = __( 'WPForms Widget', 'wpforms-lite' );
$this->text_widget_title = __( 'Text Widget', 'wpforms-lite' );
$this->block_widget_title = __( 'Block Widget', 'wpforms-lite' );
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.7.4
*/
private function hooks() {
// View hooks.
add_filter( 'wpforms_admin_forms_table_facades_columns_data', [ $this, 'add_column_data' ] );
add_filter( 'wpforms_overview_table_column_value', [ $this, 'column_value' ], 10, 3 );
add_filter( 'wpforms_overview_row_actions', [ $this, 'row_actions_all' ], 10, 2 );
add_action( 'wpforms_overview_enqueue', [ $this, 'localize_overview_script' ] );
// Monitoring hooks.
add_action( 'save_post', [ $this, 'save_post' ], 10, 3 );
add_action( 'post_updated', [ $this, 'post_updated' ], 10, 3 );
add_action( 'wp_trash_post', [ $this, 'trash_post' ] );
add_action( 'untrash_post', [ $this, 'untrash_post' ] );
add_action( 'delete_post', [ $this, 'trash_post' ] );
add_action( 'permalink_structure_changed', [ $this, 'permalink_structure_changed' ], 10, 2 );
$wpforms_widget_option = self::WPFORMS_WIDGET_OPTION;
$text_widget_option = self::TEXT_WIDGET_OPTION;
$block_widget_option = self::BLOCK_WIDGET_OPTION;
add_action( "update_option_{$wpforms_widget_option}" , [ $this, 'update_option' ], 10, 3 );
add_action( "update_option_{$text_widget_option}" , [ $this, 'update_option' ], 10, 3 );
add_action( "update_option_{$block_widget_option}", [ $this, 'update_option' ], 10, 3 );
}
/**
* Add locations' column to the view.
*
* @since 1.7.4
* @deprecated 1.8.6
*
* @param array $columns Columns.
*
* @return array
*/
public function add_column( $columns ) {
// Deprecate this method since the Locations column data should be added via the `wpforms_admin_forms_table_facades_columns_data` filter.
_deprecated_function( __METHOD__, '1.8.6 of the WPForms plugin', __CLASS__ . '::add_column_data()' );
$columns[ self::COLUMN_NAME ] =
sprintf(
'<span class="wpforms-locations-column-title">%1$s</span>' .
'<span class="wpforms-locations-column-icon" title="%2$s"></span>',
esc_html__( 'Locations', 'wpforms-lite' ),
esc_html__( 'Form locations', 'wpforms-lite' )
);
return $columns;
}
/**
* Add locations' column to the table columns data.
*
* @since 1.8.6
*
* @param array|mixed $columns Columns data.
*
* @return array
*/
public function add_column_data( $columns ): array {
$columns = (array) $columns;
$columns[ self::COLUMN_NAME ] = [
'label' => esc_html__( 'Locations', 'wpforms-lite' ),
'label_html' => sprintf(
'<span class="wpforms-locations-column-title">%1$s</span>' .
'<span class="wpforms-locations-column-icon" title="%2$s"></span>',
esc_html__( 'Locations', 'wpforms-lite' ),
esc_html__( 'Form locations', 'wpforms-lite' )
),
];
return $columns;
}
/**
* Display column value.
*
* @since 1.7.4
*
* @param mixed $value Column value.
* @param WP_Post $form Form.
* @param string $column_name Column name.
*
* @return mixed
*/
public function column_value( $value, $form, $column_name ) {
if ( $column_name !== self::COLUMN_NAME ) {
return $value;
}
$form_locations = get_post_meta( $form->ID, self::LOCATIONS_META, true );
if ( $form_locations === '' ) {
$empty_values = [
'' => '—',
FormsLocatorScanTask::SCAN_STATUS_IN_PROGRESS => '...',
FormsLocatorScanTask::SCAN_STATUS_COMPLETED => '0',
];
return $empty_values[ $this->scan_status ];
}
$values = $this->get_location_rows( $form_locations );
if ( ! $values ) {
return '0';
}
$column_value = sprintf(
'<span class="wpforms-locations-count"><a href="#" title="%s">%d</a></span>',
esc_attr__( 'View form locations', 'wpforms-lite' ),
count( $values )
);
$column_value .= '<p class="locations-list">' . implode( '', $values ) . '</p>';
return $column_value;
}
/**
* Row actions for view "All".
*
* @since 1.7.4
*
* @param array $row_actions Row actions.
* @param WP_Post $form Form object.
*
* @return array
*/
public function row_actions_all( $row_actions, $form ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$form_locations = get_post_meta( $form->ID, self::LOCATIONS_META, true );
if ( ! $form_locations ) {
return $row_actions;
}
$locations = [
'locations' => sprintf(
'<a href="#" title="%s">%s</a>',
esc_attr__( 'View form locations', 'wpforms-lite' ),
esc_html__( 'Locations', 'wpforms-lite' )
),
];
// Insert Locations action before the first available position in the positions' list or at the end of $row_actions.
$positions = [
'preview_',
'duplicate',
'trash',
];
$keys = array_keys( $row_actions );
foreach ( $positions as $position ) {
$pos = array_search( $position, $keys, true );
if ( $pos !== false ) {
break;
}
}
$pos = $pos === false ? count( $row_actions ) : $pos;
return array_slice( $row_actions, 0, $pos ) + $locations + array_slice( $row_actions, $pos );
}
/**
* Localize the overview script to pass translation strings.
*
* @since 1.7.4
*/
public function localize_overview_script() {
wp_localize_script(
'wpforms-admin-forms-overview',
'wpforms_forms_locator',
[
'paneTitle' => __( 'Form Locations', 'wpforms-lite' ),
'close' => __( 'Close', 'wpforms-lite' ),
]
);
}
/**
* Get id of the sidebar where the widget is positioned.
*
* @since 1.7.4
*
* @param string $widget_id Widget id.
*
* @return string
*/
private function get_widget_sidebar_id( $widget_id ) {
$sidebars_widgets = wp_get_sidebars_widgets();
foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widgets ) {
foreach ( $sidebar_widgets as $sidebar_widget ) {
if ( $widget_id === $sidebar_widget ) {
return (string) $sidebar_id;
}
}
}
return '';
}
/**
* Get the name of the sidebar where the widget is positioned.
*
* @since 1.7.4
*
* @param string $widget_id Widget id.
*
* @return string
*/
private function get_widget_sidebar_name( $widget_id ) {
$sidebar_id = $this->get_widget_sidebar_id( $widget_id );
if ( ! $sidebar_id ) {
return '';
}
$sidebar = $this->get_sidebar( $sidebar_id );
return isset( $sidebar['name'] ) ? (string) $sidebar['name'] : '';
}
/**
* Retrieves the registered sidebar with the given ID.
*
* @since 1.7.4
*
* @global array $wp_registered_sidebars The registered sidebars.
*
* @param string $id The sidebar ID.
*
* @return array|null The discovered sidebar, or null if it is not registered.
*/
private function get_sidebar( $id ) {
if ( function_exists( 'wp_get_sidebar' ) ) {
return wp_get_sidebar( $id );
}
global $wp_registered_sidebars;
if ( ! $wp_registered_sidebars ) {
return null;
}
foreach ( $wp_registered_sidebars as $sidebar ) {
if ( $sidebar['id'] === $id ) {
return $sidebar;
}
}
if ( $id === 'wp_inactive_widgets' ) {
return [
'id' => 'wp_inactive_widgets',
'name' => __( 'Inactive widgets', 'wpforms-lite' ),
];
}
return null;
}
/**
* Get post location title.
*
* @since 1.7.4
*
* @param array $form_location Form location.
*
* @return string
*/
private function get_post_location_title( $form_location ) {
$title = $form_location['title'];
if ( $this->is_wp_template( $form_location['type'] ) ) {
return __( 'Site editor template', 'wpforms-lite' ) . ': ' . $title;
}
return $title;
}
/**
* Whether locations' type is WP Template.
*
* @since 1.7.4.1
*
* @param string $location_type Location type.
*
* @return bool
*/
private function is_wp_template( $location_type ) {
return in_array( $location_type, [ self::WP_TEMPLATE, self::WP_TEMPLATE_PART ], true );
}
/**
* Whether a location type is standalone.
*
* @since 1.8.7
*
* @param string $location_type Location type.
*
* @return bool
*/
private function is_standalone( string $location_type ): bool {
return in_array( $location_type, self::STANDALONE_LOCATION_TYPES, true );
}
/**
* Get location title.
*
* @since 1.7.4
*
* @param array $form_location Form location.
*
* @return string
*/
private function get_location_title( $form_location ) {
if ( $form_location['type'] !== self::WIDGET ) {
return $this->get_post_location_title( $form_location );
}
$sidebar_name = $this->get_widget_sidebar_name( $form_location['id'] );
if ( ! $sidebar_name ) {
// The widget is not found.
return '';
}
$title = $form_location['title'];
if ( ! $title ) {
if ( strpos( $form_location['id'], self::WPFORMS_WIDGET_PREFIX ) === 0 ) {
$title = $this->wpforms_widget_title;
}
if ( strpos( $form_location['id'], 'text-' ) === 0 ) {
$title = $this->text_widget_title;
}
}
return $sidebar_name . ': ' . $title;
}
/**
* Get location url.
*
* @since 1.7.4
*
* @param array $form_location Form location.
*
* @return string
*/
private function get_location_url( $form_location ) {
// Get widget or wp_template url.
if ( $form_location['type'] === self::WIDGET || $this->is_wp_template( $form_location['type'] ) ) {
return '';
}
// Get standalone url.
if ( $this->is_standalone( $form_location['type'] ) ) {
return $form_location['url'];
}
// Get post url.
if ( ! $this->is_post_visible( $form_location ) ) {
return '';
}
return $form_location['url'];
}
/**
* Get location edit url.
*
* @since 1.7.4
*
* @param array $form_location Form location.
*
* @return string
*/
private function get_location_edit_url( array $form_location ): string {
// Get widget url.
if ( $form_location['type'] === self::WIDGET ) {
return current_user_can( 'edit_theme_options' ) ? admin_url( 'widgets.php' ) : '';
}
// Get standalone url.
if ( $this->is_standalone( $form_location['type'] ) ) {
return add_query_arg(
[
'page' => 'wpforms-builder',
'view' => 'settings',
'form_id' => $form_location['form_id'],
],
admin_url( 'admin.php' )
);
}
// Get post url.
if ( ! $this->is_post_visible( $form_location ) ) {
return '';
}
if ( $this->is_wp_template( $form_location['type'] ) ) {
return add_query_arg(
[
'postType' => $form_location['type'],
'postId' => get_stylesheet() . '//' . str_replace( '/', '', $form_location['url'] ),
],
admin_url( 'site-editor.php' )
);
}
return (string) get_edit_post_link( $form_location['id'], '' );
}
/**
* Get location information to output as a row in the location pane.
*
* @since 1.7.4
*
* @param array $form_location Form location.
*
* @return string
* @noinspection PhpTernaryExpressionCanBeReducedToShortVersionInspection
* @noinspection ElvisOperatorCanBeUsedInspection
*/
private function get_location_row( $form_location ) {
$title = $this->get_location_title( $form_location );
$title = $title ? $title : __( '(no title)', 'wpforms-lite' );
$location_url = $this->get_location_url( $form_location );
$location_link = '';
if ( $location_url ) {
$location_full_url = $this->home_url . $location_url;
// phpcs:ignore Generic.Commenting.DocComment.MissingShort
/** @noinspection HtmlUnknownTarget */
$location_link = sprintf(
' <a href="%1$s" target="_blank" class="wpforms-locations-link">%2$s <i class="fa fa-external-link" aria-hidden="true"></i></a>',
esc_url( $location_full_url ),
esc_url( $location_url )
);
}
$location_edit_url = $this->get_location_edit_url( $form_location );
$location_edit_url = $location_edit_url ? $location_edit_url : '#';
// phpcs:ignore Generic.Commenting.DocComment.MissingShort
/** @noinspection HtmlUnknownTarget */
$location_edit_link = sprintf(
'<a href="%1$s">%2$s</a>',
esc_url( $location_edit_url ),
esc_html( $title )
);
// Escaped above.
return sprintf(
'<span class="wpforms-locations-list-item">%s</span>',
$location_edit_link . $location_link
);
}
/**
* Get location information to output as rows in the location pane.
*
* @since 1.7.4
*
* @param array $form_locations Form locations.
*
* @return array
*/
private function get_location_rows( $form_locations ) {
$rows = [];
foreach ( $form_locations as $form_location ) {
$rows[] = $this->get_location_row( $form_location );
}
$rows = array_unique( array_filter( $rows ) );
uasort(
$rows,
static function ( $a, $b ) {
$pattern = '/href=".+widgets.php">(.+?)</i';
$widget_title_a = preg_match( $pattern, $a, $ma ) ? $ma[1] : '';
$widget_title_b = preg_match( $pattern, $b, $mb ) ? $mb[1] : '';
return strcmp( $widget_title_a, $widget_title_b );
}
);
return $rows;
}
/**
* Update form location on save_post action.
*
* @since 1.7.4
*
* @param int $post_ID Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
*
* @noinspection PhpUnusedParameterInspection
*/
public function save_post( $post_ID, $post, $update ) {
if (
$update ||
! in_array( $post->post_type, $this->get_post_types(), true ) ||
! in_array( $post->post_status, $this->get_post_statuses(), true )
) {
return;
}
$form_ids = $this->get_form_ids( $post->post_content );
$this->update_form_locations_metas( null, $post, [], $form_ids );
}
/**
* Update form location on post_updated action.
*
* @since 1.7.4
*
* @param int $post_id Post id.
* @param WP_Post $post_after Post after the update.
* @param WP_Post $post_before Post before the update.
*
* @noinspection PhpUnusedParameterInspection
*/
public function post_updated( $post_id, $post_after, $post_before ) {
if (
! in_array( $post_after->post_type, $this->get_post_types(), true ) ||
! in_array( $post_after->post_status, $this->get_post_statuses(), true )
) {
return;
}
$form_ids_before = $this->get_form_ids( $post_before->post_content );
$form_ids_after = $this->get_form_ids( $post_after->post_content );
$this->update_form_locations_metas( $post_before, $post_after, $form_ids_before, $form_ids_after );
}
/**
* Update form locations on trash_post action.
*
* @since 1.7.4
*
* @param int $post_id Post id.
*/
public function trash_post( $post_id ) {
$post = get_post( $post_id );
$form_ids_before = $this->get_form_ids( $post->post_content );
$form_ids_after = [];
$this->update_form_locations_metas( null, $post, $form_ids_before, $form_ids_after );
}
/**
* Update form locations on untrash_post action.
*
* @since 1.7.4
*
* @param int $post_id Post id.
*/
public function untrash_post( $post_id ) {
$post = get_post( $post_id );
$form_ids_before = [];
$form_ids_after = $this->get_form_ids( $post->post_content );
$this->update_form_locations_metas( null, $post, $form_ids_before, $form_ids_after );
}
/**
* Prepare widgets for further search.
*
* @since 1.7.4
*
* @param array|null $widgets Widgets.
* @param string $type Widget type.
*
* @return array
*/
private function prepare_widgets( $widgets, $type ) {
$params = [
'wpforms' => [
'option' => self::WPFORMS_WIDGET_OPTION,
'content' => 'form_id',
],
'text' => [
'option' => self::TEXT_WIDGET_OPTION,
'content' => 'text',
],
'block' => [
'option' => self::BLOCK_WIDGET_OPTION,
'content' => 'content',
],
];
if ( ! array_key_exists( $type, $params ) ) {
return [];
}
$option = $params[ $type ]['option'];
$content = $params[ $type ]['content'];
$widgets = $widgets ?? (array) get_option( $option, [] );
return array_filter(
$widgets,
static function ( $widget ) use ( $content ) {
return isset( $widget[ $content ] );
}
);
}
/**
* Search forms in WPForms widgets.
*
* @since 1.7.4
*
* @param array $widgets Widgets.
*
* @return array
*/
private function search_in_wpforms_widgets( $widgets = null ) {
$widgets = $this->prepare_widgets( $widgets, 'wpforms' );
$locations = [];
foreach ( $widgets as $id => $widget ) {
$locations[] = [
'type' => self::WIDGET,
'title' => $widget['title'],
'form_id' => $widget['form_id'],
'id' => self::WPFORMS_WIDGET_PREFIX . $id,
];
}
return $locations;
}
/**
* Search forms in text widgets.
*
* @since 1.7.4
*
* @param array $widgets Widgets.
*
* @return array
*/
private function search_in_text_widgets( $widgets = null ) {
$widgets = $this->prepare_widgets( $widgets, 'text' );
$locations = [];
foreach ( $widgets as $id => $widget ) {
$form_ids = $this->get_form_ids( $widget['text'] );
foreach ( $form_ids as $form_id ) {
$locations[] = [
'type' => self::WIDGET,
'title' => $widget['title'],
'form_id' => $form_id,
'id' => self::TEXT_WIDGET_PREFIX . $id,
];
}
}
return $locations;
}
/**
* Search forms in block widgets.
*
* @since 1.7.4
*
* @param array $widgets Widgets.
*
* @return array
*/
private function search_in_block_widgets( $widgets = null ) {
$widgets = $this->prepare_widgets( $widgets, 'block' );
$locations = [];
foreach ( $widgets as $id => $widget ) {
$form_ids = $this->get_form_ids( $widget['content'] );
foreach ( $form_ids as $form_id ) {
$locations[] = [
'type' => self::WIDGET,
'title' => $this->block_widget_title,
'form_id' => $form_id,
'id' => self::BLOCK_WIDGET_PREFIX . $id,
];
}
}
return $locations;
}
/**
* Search forms in widgets.
*
* @since 1.7.4
*
* @return array
*/
public function search_in_widgets() {
return array_merge(
$this->search_in_wpforms_widgets(),
$this->search_in_text_widgets(),
$this->search_in_block_widgets()
);
}
/**
* Get the difference of two arrays containing locations.
*
* @since 1.7.4
*
* @param array $locations1 Locations to subtract from.
* @param array $locations2 Locations to subtract.
*
* @return array
*/
private function array_udiff( $locations1, $locations2 ) {
return array_udiff(
$locations1,
$locations2,
static function ( $a, $b ) {
return ( $a === $b ) ? 0 : - 1;
}
);
}
/**
* Remove locations from metas.
*
* @since 1.7.4
*
* @param array $locations_to_remove Locations to remove.
*
* @return void
*/
private function remove_locations( $locations_to_remove ) {
foreach ( $locations_to_remove as $location_to_remove ) {
$locations = get_post_meta( $location_to_remove['form_id'], self::LOCATIONS_META, true );
if ( ! $locations ) {
continue;
}
foreach ( $locations as $key => $location ) {
if ( $location['id'] === $location_to_remove['id'] ) {
unset( $locations[ $key ] );
}
}
update_post_meta( $location_to_remove['form_id'], self::LOCATIONS_META, $locations );
}
}
/**
* Add locations to metas.
*
* @since 1.7.4
*
* @param array $locations_to_add Locations to add.
*
* @return void
*/
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 );
if ( ! $locations ) {
$locations = [];
}
$locations[] = $location_to_add;
update_post_meta( $location_to_add['form_id'], self::LOCATIONS_META, $locations );
}
}
/**
* Update form locations on widget update.
*
* @since 1.7.4
*
* @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 ) {
switch ( $option ) {
case self::WPFORMS_WIDGET_OPTION:
$old_locations = $this->search_in_wpforms_widgets( $old_value );
$new_locations = $this->search_in_wpforms_widgets( $value );
break;
case self::TEXT_WIDGET_OPTION:
$old_locations = $this->search_in_text_widgets( $old_value );
$new_locations = $this->search_in_text_widgets( $value );
break;
case self::BLOCK_WIDGET_OPTION:
$old_locations = $this->search_in_block_widgets( $old_value );
$new_locations = $this->search_in_block_widgets( $value );
break;
default:
// phpcs:ignore WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement
return;
}
$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.
*
* @since 1.7.4
*
* @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.
*
* @since 1.7.4
*/
do_action( FormsLocatorScanTask::DELETE_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
/**
* Run Forms Locator scan action.
*
* @since 1.7.4
*/
do_action( FormsLocatorScanTask::RESCAN_ACTION ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Update form locations metas.
*
* @since 1.7.4
* @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 ) {
update_post_meta(
$form_id,
self::LOCATIONS_META,
$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 ) {
return;
}
// 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.
*
* @since 1.8.2.3
*
* @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 ) {
// Build the URL.
$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 );
$locations[] = [
'type' => $post_after->post_type,
'title' => $post_after->post_title,
'form_id' => $form_id,
'id' => $post_id,
'status' => $post_after->post_status,
'url' => $url,
];
update_post_meta( $form_id, self::LOCATIONS_META, $locations );
}
}
/**
* Get post types for search in.
*
* @since 1.7.4
*
* @return string[]
*/
public function get_post_types() {
$args = [
'public' => true,
'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;
return $post_types;
}
/**
* Get post statuses for search in.
*
* @since 1.7.4
*
* @return string[]
*/
public function get_post_statuses() {
return [ 'publish', 'pending', 'draft', 'future', 'private' ];
}
/**
* Get form ids from the content.
*
* @since 1.7.4
*
* @param string $content Content.
*
* @return int[]
*/
public function get_form_ids( $content ) {
$form_ids = [];
if (
preg_match_all(
/**
* Extract id from conventional wpforms shortcode or wpforms block.
* Examples:
* [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+?)".*?} /-->#',
$content,
$matches
)
) {
array_shift( $matches );
$form_ids = array_map(
'intval',
array_unique( array_filter( array_merge( ...$matches ) ) )
);
}
return $form_ids;
}
/**
* Get form locations without a current post.
*
* @since 1.7.4
*
* @param int $form_id Form id.
* @param int $post_id Post id.
*
* @return array
*/
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 ) ) {
$locations = [];
}
return array_filter(
$locations,
static function ( $location ) use ( $post_id ) {
return $location['id'] !== $post_id;
}
);
}
/**
* Determine whether a post is visible.
*
* @since 1.7.4
*
* @param array $location Post location.
*
* @return bool
*/
private function is_post_visible( $location ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$edit_cap = 'edit_post';
$read_cap = 'read_post';
$post_id = $location['id'];
if ( ! get_post_type_object( $location['type'] ) ) {
// Post type is not registered.
return false;
}
$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 ) {
return true;
}
if ( ! is_user_logged_in() ) {
// User must be logged in to view unpublished posts.
return false;
}
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 );
}
return false;
}
/**
* Build a standalone location.
*
* @since 1.8.7
*
* @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 ) ) {
return [];
}
// Form templates should not have any locations.
if ( get_post_type( $form_id ) === 'wpforms-template' ) {
return [];
}
foreach ( self::STANDALONE_LOCATION_TYPES as $location_type ) {
if ( empty( $form_data['settings'][ "{$location_type}_enable" ] ) ) {
continue;
}
return $this->build_standalone_location_type( $location_type, $form_id, $form_data, $status );
}
return [];
}
/**
* Build a standalone location.
*
* @since 1.8.8
*
* @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.
return [
'type' => $location_type,
'title' => $title,
'form_id' => (int) $form_data['id'],
'id' => $form_id,
'status' => $status,
'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.
*
* @since 1.8.7
*
* @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 );
// No location? Bail.
if ( empty( $location ) ) {
return;
}
// Setup data.
$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.
update_post_meta( $form_id, self::LOCATIONS_META, $new_location );
}
/**
* Remove a form page from an array.
*
* @since 1.8.7
*
* @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 ) ) {
return [];
}
// Loop over all locations.
foreach ( $post_meta as $key => $location ) {
// Verify the location keys exist.
if ( ! isset( $location['form_id'], $location['type'] ) ) {
continue;
}
// 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 ] );
}
}
return $post_meta;
}
}