Files
medicalalert-web-reloaded/wp/wp-content/plugins/facetwp/includes/class-indexer.php
Tony Volpe 779393381f Merged in feature/plugins-update (pull request #9)
wp plugin updates from pantheon

* wp plugin updates from pantheon
2023-12-15 18:08:21 +00:00

692 lines
21 KiB
PHP

<?php
class FacetWP_Indexer
{
/* (boolean) wp_insert_post running? */
public $is_saving = false;
/* (boolean) Whether to index a single post */
public $index_all = false;
/* (int) Number of posts to index before updating progress */
public $chunk_size = 10;
/* (string) Whether a temporary table is active */
public $table;
/* (array) Facet properties for the value being indexed */
public $facet;
/* (array) Value modifiers set via the admin UI */
public $modifiers;
function __construct() {
$this->set_table( 'auto' );
$this->run_cron();
if ( apply_filters( 'facetwp_indexer_is_enabled', true ) ) {
$this->run_hooks();
}
}
/**
* Event listeners
* @since 2.8.4
*/
function run_hooks() {
add_action( 'save_post', [ $this, 'save_post' ] );
add_action( 'delete_post', [ $this, 'delete_post' ] );
add_action( 'edited_term', [ $this, 'edit_term' ], 10, 3 );
add_action( 'delete_term', [ $this, 'delete_term' ], 10, 3 );
add_action( 'set_object_terms', [ $this, 'set_object_terms' ] );
add_action( 'facetwp_indexer_cron', [ $this, 'get_progress' ] );
add_filter( 'wp_insert_post_parent', [ $this, 'is_wp_insert_post' ] );
}
/**
* Cron task
* @since 2.8.5
*/
function run_cron() {
if ( ! wp_next_scheduled( 'facetwp_indexer_cron' ) ) {
wp_schedule_single_event( time() + 300, 'facetwp_indexer_cron' );
}
}
/**
* Update the index when posts get saved
* @since 0.1.0
*/
function save_post( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( false !== wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'auto-draft' == get_post_status( $post_id ) ) {
return;
}
$this->index( $post_id );
$this->is_saving = false;
}
/**
* Update the index when posts get deleted
* @since 0.6.0
*/
function delete_post( $post_id ) {
global $wpdb;
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
/**
* Update the index when terms get saved
* @since 0.6.0
*/
function edit_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
$term = get_term( $term_id, $taxonomy );
$slug = FWP()->helper->safe_value( $term->slug );
$matches = FWP()->helper->get_facets_by( 'source', "tax/$taxonomy" );
if ( ! empty( $matches ) ) {
$facet_names = wp_list_pluck( $matches, 'name' );
$facet_names = implode( "','", esc_sql( $facet_names ) );
$wpdb->query( $wpdb->prepare( "
UPDATE {$this->table}
SET facet_value = %s, facet_display_value = %s
WHERE facet_name IN ('$facet_names') AND term_id = %d",
$slug, $term->name, $term_id
) );
}
}
/**
* Update the index when terms get deleted
* @since 0.6.0
*/
function delete_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
$matches = FWP()->helper->get_facets_by( 'source', "tax/$taxonomy" );
if ( ! empty( $matches ) ) {
$facet_names = wp_list_pluck( $matches, 'name' );
$facet_names = implode( "','", esc_sql( $facet_names ) );
$wpdb->query( "
DELETE FROM {$this->table}
WHERE facet_name IN ('$facet_names') AND term_id = $term_id"
);
}
}
/**
* We're hijacking wp_insert_post_parent
* Prevent our set_object_terms() hook from firing within wp_insert_post
* @since 2.2.2
*/
function is_wp_insert_post( $post_parent ) {
$this->is_saving = true;
return $post_parent;
}
/**
* Support for manual taxonomy associations
* @since 0.8.0
*/
function set_object_terms( $object_id ) {
if ( ! $this->is_saving ) {
$this->index( $object_id );
}
}
/**
* Rebuild the facet index
* @param mixed $post_id The post ID (set to FALSE to re-index everything)
*/
function index( $post_id = false ) {
global $wpdb;
$this->index_all = ( false === $post_id );
// Index everything
if ( $this->index_all ) {
// Store the pre-index settings (see FacetWP_Diff)
update_option( 'facetwp_settings_last_index', get_option( 'facetwp_settings' ), 'no' );
// PHP sessions are blocking, so close if active
if ( PHP_SESSION_ACTIVE === session_status() ) {
session_write_close();
}
// Bypass the PHP timeout
ini_set( 'max_execution_time', 0 );
// Prevent multiple indexing processes
$touch = (int) $this->get_transient( 'touch' );
if ( 0 < $touch ) {
// Run only if the indexer is inactive or stalled
if ( ( time() - $touch ) < 60 ) {
exit;
}
}
else {
// Create temp table
$this->manage_temp_table( 'create' );
}
}
// Index a single post
elseif ( is_int( $post_id ) ) {
// Clear table values
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
// Exit
else {
return;
}
// Resume indexing?
$offset = (int) ( $_POST['offset'] ?? 0 );
$attempt = (int) ( $_POST['retries'] ?? 0 );
if ( 0 < $offset ) {
$post_ids = json_decode( get_option( 'facetwp_indexing' ), true );
}
else {
$post_ids = $this->get_post_ids_to_index( $post_id );
// Store post IDs
if ( $this->index_all ) {
update_option( 'facetwp_indexing', json_encode( $post_ids ) );
}
}
// Count total posts
$num_total = count( $post_ids );
// Get all facet sources
$facets = FWP()->helper->get_facets();
// Populate an array of facet value modifiers
$this->load_value_modifiers( $facets );
foreach ( $post_ids as $counter => $post_id ) {
// Advance until we reach the offset
if ( $counter < $offset ) {
continue;
}
// Update the progress bar
if ( $this->index_all ) {
if ( 0 == ( $counter % $this->chunk_size ) ) {
$num_retries = (int) $this->get_transient( 'retries' );
// Exit if newer retries exist
if ( $attempt < $num_retries ) {
exit;
}
// Exit if the indexer was cancelled
wp_cache_delete( 'facetwp_indexing_cancelled', 'options' );
if ( 'yes' === get_option( 'facetwp_indexing_cancelled', 'no' ) ) {
update_option( 'facetwp_transients', '' );
update_option( 'facetwp_indexing', '' );
$this->manage_temp_table( 'delete' );
exit;
}
$transients = [
'num_indexed' => $counter,
'num_total' => $num_total,
'retries' => $attempt,
'touch' => time(),
];
update_option( 'facetwp_transients', json_encode( $transients ) );
}
}
// If the indexer stalled, start from the last valid chunk
if ( 0 < $offset && ( $counter - $offset < $this->chunk_size ) ) {
$wpdb->query( "DELETE FROM {$this->table} WHERE post_id = $post_id" );
}
$this->index_post( $post_id, $facets );
}
// Indexing complete
if ( $this->index_all ) {
update_option( 'facetwp_last_indexed', time(), 'no' );
update_option( 'facetwp_transients', '', 'no' );
update_option( 'facetwp_indexing', '', 'no' );
$this->manage_temp_table( 'replace' );
$this->manage_temp_table( 'delete' );
}
do_action( 'facetwp_indexer_complete' );
}
/**
* Get an array of post IDs to index
* @since 3.6.8
*/
function get_post_ids_to_index( $post_id = false ) {
$args = [
'post_type' => 'any',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'orderby' => 'ID',
'cache_results' => false,
'no_found_rows' => true,
];
if ( is_int( $post_id ) ) {
$args['p'] = $post_id;
$args['posts_per_page'] = 1;
}
$args = apply_filters( 'facetwp_indexer_query_args', $args );
$query = new WP_Query( $args );
return (array) $query->posts;
}
/**
* Index an individual post
* @since 3.6.8
*/
function index_post( $post_id, $facets ) {
// Force WPML to change the language
do_action( 'facetwp_indexer_post', [ 'post_id' => $post_id ] );
// Loop through all facets
foreach ( $facets as $facet ) {
// Do not index search facets
if ( 'search' == $facet['type'] ) {
continue;
}
$this->facet = $facet;
$source = $facet['source'] ?? '';
// Set default index_row() params
$defaults = [
'post_id' => $post_id,
'facet_name' => $facet['name'],
'facet_source' => $source,
'facet_value' => '',
'facet_display_value' => '',
'term_id' => 0,
'parent_id' => 0,
'depth' => 0,
'variation_id' => 0,
];
$defaults = apply_filters( 'facetwp_indexer_post_facet_defaults', $defaults, [
'facet' => $facet
] );
// Set flag for custom handling
$this->is_overridden = true;
// Bypass default indexing
$bypass = apply_filters( 'facetwp_indexer_post_facet', false, [
'defaults' => $defaults,
'facet' => $facet
] );
if ( $bypass ) {
continue;
}
$this->is_overridden = false;
// Get rows to insert
$rows = $this->get_row_data( $defaults );
foreach ( $rows as $row ) {
$this->index_row( $row );
}
}
}
/**
* Get data for a table row
* @since 2.1.1
*/
function get_row_data( $defaults ) {
$output = [];
$facet = $this->facet;
$post_id = $defaults['post_id'];
$source = $defaults['facet_source'];
if ( 'tax/' == substr( $source, 0, 4 ) ) {
$used_terms = [];
$taxonomy = substr( $source, 4 );
$term_objects = wp_get_object_terms( $post_id, $taxonomy );
if ( is_wp_error( $term_objects ) ) {
return $output;
}
// Store the term depths
$hierarchy = FWP()->helper->get_term_depths( $taxonomy );
// Only index child terms
$children = false;
if ( ! empty( $facet['parent_term'] ) ) {
$children = get_term_children( $facet['parent_term'], $taxonomy );
}
foreach ( $term_objects as $term ) {
// If "parent_term" is set, only index children
if ( false !== $children && ! in_array( $term->term_id, $children ) ) {
continue;
}
// Prevent duplicate terms
if ( isset( $used_terms[ $term->term_id ] ) ) {
continue;
}
$used_terms[ $term->term_id ] = true;
// Handle hierarchical taxonomies
$term_info = $hierarchy[ $term->term_id ];
$depth = $term_info['depth'];
// Adjust depth if parent_term is set
if ( ! empty( $facet['parent_term'] ) ) {
if ( isset( $hierarchy[ $facet['parent_term'] ] ) ) {
$anchor = (int) $hierarchy[ $facet['parent_term'] ]['depth'] + 1;
$depth = ( $depth - $anchor );
}
}
$params = $defaults;
$params['facet_value'] = $term->slug;
$params['facet_display_value'] = $term->name;
$params['term_id'] = $term->term_id;
$params['parent_id'] = $term_info['parent_id'];
$params['depth'] = $depth;
$output[] = $params;
// Automatically index implicit parents
if ( 'hierarchy' == $facet['type'] || FWP()->helper->facet_is( $facet, 'hierarchical', 'yes' ) ) {
while ( $depth > 0 ) {
$term_id = $term_info['parent_id'];
$term_info = $hierarchy[ $term_id ];
$depth = $depth - 1;
if ( ! isset( $used_terms[ $term_id ] ) ) {
$used_terms[ $term_id ] = true;
$params = $defaults;
$params['facet_value'] = $term_info['slug'];
$params['facet_display_value'] = $term_info['name'];
$params['term_id'] = $term_id;
$params['parent_id'] = $term_info['parent_id'];
$params['depth'] = $depth;
$output[] = $params;
}
}
}
}
}
elseif ( 'cf/' == substr( $source, 0, 3 ) ) {
$source_noprefix = substr( $source, 3 );
$values = get_post_meta( $post_id, $source_noprefix, false );
foreach ( $values as $value ) {
$params = $defaults;
$params['facet_value'] = $value;
$params['facet_display_value'] = $value;
$output[] = $params;
}
}
elseif ( 'post' == substr( $source, 0, 4 ) ) {
$post = get_post( $post_id );
$value = $post->{$source};
$display_value = $value;
if ( 'post_author' == $source ) {
$user = get_user_by( 'id', $value );
$display_value = ( $user instanceof WP_User ) ? $user->display_name : $value;
}
elseif ( 'post_type' == $source ) {
$post_type = get_post_type_object( $value );
if ( isset( $post_type->labels->name ) ) {
$display_value = $post_type->labels->name;
}
}
$params = $defaults;
$params['facet_value'] = $value;
$params['facet_display_value'] = $display_value;
$output[] = $params;
}
return apply_filters( 'facetwp_indexer_row_data', $output, [
'defaults' => $defaults,
'facet' => $this->facet
] );
}
/**
* Index a facet value
* @since 0.6.0
*/
function index_row( $params ) {
// Allow for custom indexing
$params = apply_filters( 'facetwp_index_row', $params, $this );
// Allow hooks to bypass the row insertion
if ( is_array( $params ) ) {
$this->insert( $params );
}
}
/**
* Save a facet value to DB
* This can be trigged by "facetwp_index_row" to handle multiple values
* @since 1.2.5
*/
function insert( $params ) {
global $wpdb;
$value = $params['facet_value'];
$display_value = $params['facet_display_value'];
// Only accept scalar values
if ( '' === $value || ! is_scalar( $value ) ) {
return;
}
// Apply UI-based modifiers
if ( isset( $this->modifiers[ $params['facet_name'] ] ) ) {
$mod = $this->modifiers[ $params['facet_name' ] ];
$is_match = in_array( $display_value, $mod['values'] );
if ( ( 'exclude' == $mod['type'] && $is_match ) || ( 'include' == $mod['type'] && ! $is_match ) ) {
return;
}
}
$wpdb->query( $wpdb->prepare( "INSERT INTO {$this->table}
(post_id, facet_name, facet_value, facet_display_value, term_id, parent_id, depth, variation_id) VALUES (%d, %s, %s, %s, %d, %d, %d, %d)",
$params['post_id'],
$params['facet_name'],
FWP()->helper->safe_value( $value ),
$display_value,
$params['term_id'],
$params['parent_id'],
$params['depth'],
$params['variation_id']
) );
}
/**
* Get the indexing completion percentage
* @return mixed The decimal percentage, or -1
* @since 0.1.0
*/
function get_progress() {
$return = -1;
$num_indexed = (int) $this->get_transient( 'num_indexed' );
$num_total = (int) $this->get_transient( 'num_total' );
$retries = (int) $this->get_transient( 'retries' );
$touch = (int) $this->get_transient( 'touch' );
if ( 0 < $num_total ) {
// Resume a stalled indexer
if ( 60 < ( time() - $touch ) ) {
$post_data = [
'blocking' => false,
'timeout' => 0.02,
'body' => [
'action' => 'facetwp_resume_index',
'offset' => $num_indexed,
'retries' => $retries + 1,
'touch' => $touch
]
];
wp_remote_post( admin_url( 'admin-ajax.php' ), $post_data );
}
// Calculate the percent completion
if ( $num_indexed != $num_total ) {
$return = round( 100 * ( $num_indexed / $num_total ), 2 );
}
}
return $return;
}
/**
* Get indexer transient variables
* @since 1.7.8
*/
function get_transient( $name = false ) {
$transients = get_option( 'facetwp_transients' );
if ( ! empty( $transients ) ) {
$transients = json_decode( $transients, true );
if ( $name ) {
return $transients[ $name ] ?? false;
}
return $transients;
}
return false;
}
/**
* Set either the index or temp table
* @param string $table 'auto', 'index', or 'temp'
* @since 4.1.4
*/
function set_table( $table = 'auto' ) {
global $wpdb;
if ( 'auto' == $table ) {
$table = ( '' == get_option( 'facetwp_indexing', '' ) ) ? 'index' : 'temp';
}
$this->table = $wpdb->prefix . 'facetwp_' . $table;
}
/**
* Index table management
* @since 3.5
*/
function manage_temp_table( $action = 'create' ) {
global $wpdb;
$table = $wpdb->prefix . 'facetwp_index';
$temp_table = $wpdb->prefix . 'facetwp_temp';
if ( 'create' == $action ) {
$wpdb->query( "CREATE TABLE $temp_table LIKE $table" );
$this->set_table( 'temp' );
}
elseif ( 'replace' == $action ) {
$wpdb->query( "TRUNCATE TABLE $table" );
$wpdb->query( "INSERT INTO $table SELECT * FROM $temp_table" );
}
elseif ( 'delete' == $action ) {
$wpdb->query( "DROP TABLE IF EXISTS $temp_table" );
$this->set_table( 'index' );
}
}
/**
* Populate an array of facet value modifiers (defined in the admin UI)
* @since 3.5.6
*/
function load_value_modifiers( $facets ) {
$output = [];
foreach ( $facets as $facet ) {
$name = $facet['name'];
$type = empty( $facet['modifier_type'] ) ? 'off' : $facet['modifier_type'];
if ( 'include' == $type || 'exclude' == $type ) {
$temp = preg_split( '/\r\n|\r|\n/', trim( $facet['modifier_values'] ) );
$values = [];
// Compare using both original and encoded values
foreach ( $temp as $val ) {
$val = trim( $val );
$val_encoded = htmlentities( $val );
$val_decoded = html_entity_decode( $val );
$values[ $val ] = true;
$values[ $val_encoded ] = true;
$values[ $val_decoded ] = true;
}
$output[ $name ] = [ 'type' => $type, 'values' => array_keys( $values ) ];
}
}
$this->modifiers = $output;
}
}