Files
medicalalert-web-reloaded/wp/wp-content/plugins/facetwp/includes/class-renderer.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

663 lines
22 KiB
PHP

<?php
class FacetWP_Renderer
{
/* (array) Data for the current facets */
public $facets;
/* (array) Data for the current template */
public $template;
/* (array) WP_Query arguments */
public $query_args;
/* (array) Data used to build the pager */
public $pager_args;
/* (string) MySQL WHERE clause passed to each facet */
public $where_clause = '';
/* (array) AJAX parameters passed in */
public $ajax_params;
/* (array) HTTP parameters from the original page (URI, GET) */
public $http_params;
/* (boolean) Is search active? */
public $is_search = false;
/* (boolean) Are we preloading? */
public $is_preload = false;
/* (array) Cache preloaded facet values */
public $preloaded_values;
/* (array) The final WP_Query object */
public $query;
function __construct() {
$this->facet_types = FWP()->helper->facet_types;
}
/**
* Generate the facet output
* @param array $params An array of arrays (see the FacetWP->refresh() method)
* @return array
*/
function render( $params ) {
$output = [
'facets' => [],
'template' => '',
'settings' => [],
];
// Hook params
$params = apply_filters( 'facetwp_render_params', $params );
// First ajax refresh?
$first_load = (bool) $params['first_load'];
$is_bfcache = (bool) $params['is_bfcache'];
// Initial pageload?
$this->is_preload = isset( $params['is_preload'] );
// Set the AJAX and HTTP params
$this->ajax_params = $params;
$this->http_params = $params['http_params'];
// Validate facets
$this->facets = [];
foreach ( $params['facets'] as $f ) {
$name = $f['facet_name'];
$facet = FWP()->helper->get_facet_by_name( $name );
if ( $facet ) {
// Default to "OR" mode
$facet['operator'] = $facet['operator'] ?? 'or';
// Support the "facetwp_preload_url_vars" hook
if ( $first_load && empty( $f['selected_values'] ) && ! empty( $this->http_params['url_vars'][ $name ] ) ) {
$f['selected_values'] = $this->http_params['url_vars'][ $name ];
}
// Support commas within search / autocomplete facets
if ( 'search' == $facet['type'] || 'autocomplete' == $facet['type'] ) {
$f['selected_values'] = implode( ',', (array) $f['selected_values'] );
}
$facet['selected_values'] = FWP()->helper->sanitize( $f['selected_values'] );
$this->facets[ $name ] = $facet;
}
}
// Get the template from $helper->settings
if ( 'wp' == $params['template'] ) {
$this->template = [ 'name' => 'wp' ];
$query_args = FWP()->request->query_vars ?? [];
}
else {
$this->template = FWP()->helper->get_template_by_name( $params['template'] );
$query_args = $this->get_query_args();
}
// Detect search string
if ( ! empty( $query_args['s'] ) ) {
$this->is_search = true;
}
// Run the query once (prevent duplicate queries when preloading)
if ( empty( $this->query_args ) ) {
// Support "post__in"
if ( empty( $query_args['post__in'] ) ) {
$query_args['post__in'] = [];
}
// Get the template "query" field
$query_args = apply_filters( 'facetwp_query_args', $query_args, $this );
// Pagination
$query_args['paged'] = empty( $params['paged'] ) ? 1 : (int) $params['paged'];
// Preserve SQL_CALC_FOUND_ROWS
unset( $query_args['no_found_rows'] );
// Narrow posts based on facet selections
$post_ids = $this->get_filtered_post_ids( $query_args );
// Update the SQL query
if ( ! empty( $post_ids ) ) {
if ( FWP()->is_filtered ) {
$query_args['post__in'] = $post_ids;
}
$this->where_clause = ' AND post_id IN (' . implode( ',', $post_ids ) . ')';
}
// Set the default limit
if ( empty( $query_args['posts_per_page'] ) ) {
$query_args['posts_per_page'] = (int) get_option( 'posts_per_page' );
}
// Adhere to the "per page" box
$per_page = isset( $params['extras']['per_page'] ) ? $params['extras']['per_page'] : '';
if ( ! empty( $per_page ) && 'default' != $per_page ) {
$query_args['posts_per_page'] = (int) $per_page;
}
$this->query_args = apply_filters( 'facetwp_filtered_query_args', $query_args, $this );
// Run the WP_Query
$this->query = new WP_Query( $this->query_args );
}
// Debug
if ( 'on' == FWP()->helper->get_setting( 'debug_mode', 'off' ) ) {
$output['settings']['debug'] = $this->get_debug_info();
}
// Generate the template HTML
// For performance gains, skip the template on pageload
if ( 'wp' != $this->template['name'] ) {
if ( ! $first_load || $is_bfcache || apply_filters( 'facetwp_template_force_load', false ) ) {
$output['template'] = $this->get_template_html();
}
}
// Don't render these facets
$frozen_facets = $params['frozen_facets'];
// Calculate pager args
$pager_args = [
'page' => (int) $this->query_args['paged'],
'per_page' => (int) $this->query_args['posts_per_page'],
'total_rows' => (int) $this->query->found_posts,
'total_pages' => 1,
];
if ( 0 < $pager_args['per_page'] ) {
$pager_args['total_pages'] = ceil( $pager_args['total_rows'] / $pager_args['per_page'] );
}
$pager_args = apply_filters( 'facetwp_pager_args', $pager_args, $this );
$this->pager_args = $pager_args;
// Stick the pager args into the JSON response
$output['settings']['pager'] = $pager_args;
// Display the pagination HTML
if ( isset( $params['extras']['pager'] ) ) {
$output['pager'] = $this->paginate( $pager_args );
}
// Display the "per page" HTML
if ( isset( $params['extras']['per_page'] ) ) {
$output['per_page'] = $this->get_per_page_box();
}
// Display the counts HTML
if ( isset( $params['extras']['counts'] ) ) {
$output['counts'] = $this->get_result_count( $pager_args );
}
// Not paging
if ( 0 == $params['soft_refresh'] ) {
$output['settings']['num_choices'] = [];
}
// Get facet data
foreach ( $this->facets as $facet_name => $the_facet ) {
$facet_type = $the_facet['type'];
$ui_type = empty( $the_facet['ui_type'] ) ? $facet_type : $the_facet['ui_type'];
// Invalid facet type
if ( ! isset( $this->facet_types[ $facet_type ] ) ) {
continue;
}
// Skip facets when paging
if ( 0 < $params['soft_refresh'] && 'pager' != $facet_type ) {
continue;
}
// Get facet labels
if ( 0 == $params['soft_refresh'] ) {
$output['settings']['labels'][ $facet_name ] = facetwp_i18n( $the_facet['label'] );
}
// Load all facets on back / forward button press (first_load = true)
if ( ! $first_load ) {
// Skip frozen facets
if ( isset( $frozen_facets[ $facet_name ] ) ) {
continue;
}
}
$args = [
'facet' => $the_facet,
'where_clause' => $this->where_clause,
'selected_values' => $the_facet['selected_values'],
];
// Load facet values if needed
if ( method_exists( $this->facet_types[ $facet_type ], 'load_values' ) ) {
// Grab preloaded values if available
if ( isset( $this->preloaded_values[ $facet_name ] ) ) {
$args['values'] = $this->preloaded_values[ $facet_name ];
}
else {
$args['values'] = $this->facet_types[ $facet_type ]->load_values( $args );
if ( $this->is_preload ) {
$this->preloaded_values[ $facet_name ] = $args['values'];
}
}
}
// Filter the render args
$args = apply_filters( 'facetwp_facet_render_args', $args );
// Return the number of available choices
if ( isset( $args['values'] ) ) {
$num_choices = 0;
$is_ghost = FWP()->helper->facet_is( $the_facet, 'ghosts', 'yes' );
foreach ( $args['values'] as $choice ) {
if ( isset( $choice['counter'] ) && ( 0 < $choice['counter'] || $is_ghost ) ) {
$num_choices++;
}
}
$output['settings']['num_choices'][ $facet_name ] = $num_choices;
}
// Generate the facet HTML
$html = $this->facet_types[ $ui_type ]->render( $args );
$output['facets'][ $facet_name ] = apply_filters( 'facetwp_facet_html', $html, $args );
// Return any JS settings
if ( method_exists( $this->facet_types[ $ui_type ], 'settings_js' ) ) {
$output['settings'][ $facet_name ] = $this->facet_types[ $ui_type ]->settings_js( $args );
}
// Grab num_choices for slider facets
if ( 'slider' == $the_facet['type'] ) {
$min = $output['settings'][ $facet_name ]['range']['min'];
$max = $output['settings'][ $facet_name ]['range']['max'];
$output['settings']['num_choices'][ $facet_name ] = ( $min == $max ) ? 0 : 1;
}
}
return apply_filters( 'facetwp_render_output', $output, $params );
}
/**
* Get WP_Query arguments by executing the template "query" field
* @return null
*/
function get_query_args() {
$defaults = [];
// Allow templates to piggyback archives
if ( apply_filters( 'facetwp_template_use_archive', false ) ) {
$main_query = $GLOBALS['wp_the_query'];
// Initial pageload
if ( $main_query->is_archive || $main_query->is_search ) {
if ( $main_query->is_category ) {
$defaults['cat'] = $main_query->get( 'cat' );
}
elseif ( $main_query->is_tag ) {
$defaults['tag_id'] = $main_query->get( 'tag_id' );
}
elseif ( $main_query->is_tax ) {
$defaults['taxonomy'] = $main_query->get( 'taxonomy' );
$defaults['term'] = $main_query->get( 'term' );
}
elseif ( $main_query->is_search ) {
$defaults['s'] = $main_query->get( 's' );
}
$this->archive_args = $defaults;
}
// Subsequent ajax requests
elseif ( ! empty( $this->http_params['archive_args'] ) ) {
foreach ( $this->http_params['archive_args'] as $key => $val ) {
if ( in_array( $key, [ 'cat', 'tag_id', 'taxonomy', 'term', 's' ] ) ) {
$defaults[ $key ] = $val;
}
}
}
}
// Use the query builder
if ( isset( $this->template['modes'] ) && 'visual' == $this->template['modes']['query'] ) {
$query_args = FWP()->builder->parse_query_obj( $this->template['query_obj'] );
}
else {
// remove UTF-8 non-breaking spaces
$query_args = preg_replace( "/\xC2\xA0/", ' ', $this->template['query'] );
$query_args = (array) eval( '?>' . $query_args );
}
// Merge the two arrays
return array_merge( $defaults, $query_args );
}
/**
* Get ALL post IDs for the matching query
* @return array An array of post IDs
*/
function get_filtered_post_ids( $query_args = [] ) {
if ( empty( $query_args ) ) {
$query_args = $this->query_args;
}
// Only get relevant post IDs
$args = array_merge( $query_args, [
'paged' => 1,
'posts_per_page' => -1,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'cache_results' => false,
'no_found_rows' => true,
'nopaging' => true, // prevent "offset" issues
'facetwp' => false,
'fields' => 'ids',
] );
$query = new WP_Query( $args );
// Allow hooks to modify the default post IDs
$post_ids = apply_filters( 'facetwp_pre_filtered_post_ids', $query->posts, $this );
// Store the unfiltered post IDs
FWP()->unfiltered_post_ids = $post_ids;
foreach ( $this->facets as $facet_name => $the_facet ) {
$facet_type = $the_facet['type'];
// Stop looping
if ( empty( $post_ids ) ) {
break;
}
$matches = [];
$selected_values = $the_facet['selected_values'];
if ( empty( $selected_values ) ) {
continue;
}
// Handle each facet
if ( isset( $this->facet_types[ $facet_type ] ) ) {
$hook_params = [
'facet' => $the_facet,
'selected_values' => $selected_values,
];
// Hook to support custom filter_posts() handler
$matches = apply_filters( 'facetwp_facet_filter_posts', false, $hook_params );
if ( false === $matches ) {
$matches = $this->facet_types[ $facet_type ]->filter_posts( $hook_params );
}
}
// Skip this facet
if ( 'continue' == $matches ) {
continue;
}
// Force array
$matches = (array) $matches;
// Store post IDs per facet (needed for "OR" mode)
FWP()->or_values[ $facet_name ] = $matches;
if ( 'search' == $facet_type ) {
$this->is_search = true;
}
// For search facets, loop through $matches to set order
// For other facets, loop through $post_ids to preserve the existing order
$needles = ( 'search' == $facet_type ) ? $matches : $post_ids;
$haystack = ( 'search' == $facet_type ) ? $post_ids : $matches;
$haystack = array_flip( $haystack );
$intersected_ids = [];
foreach ( $needles as $post_id ) {
if ( isset( $haystack[ $post_id ] ) ) {
$intersected_ids[] = $post_id;
}
}
$post_ids = $intersected_ids;
}
$post_ids = apply_filters( 'facetwp_filtered_post_ids', array_values( $post_ids ), $this );
// Store the filtered post IDs
FWP()->filtered_post_ids = $post_ids;
// Set a flag for whether filtering is applied
FWP()->is_filtered = ( FWP()->filtered_post_ids !== FWP()->unfiltered_post_ids );
// Return a zero array if no matches
return empty( $post_ids ) ? [ 0 ] : $post_ids;
}
/**
* Run the template display code
* @return string (HTML)
*/
function get_template_html() {
global $post, $wp_query;
$output = apply_filters( 'facetwp_template_html', false, $this );
if ( false === $output ) {
ob_start();
// Preserve globals
$temp_post = is_object( $post ) ? clone $post : $post;
$temp_wp_query = is_object( $wp_query ) ? clone $wp_query : $wp_query;
$query = $this->query;
$wp_query = $query; // Make $query->blah() optional
if ( isset( $this->template['modes'] ) && 'visual' == $this->template['modes']['display'] ) {
echo FWP()->builder->render_layout( $this->template['layout'] );
}
else {
// Remove UTF-8 non-breaking spaces
$display_code = $this->template['template'];
$display_code = preg_replace( "/\xC2\xA0/", ' ', $display_code );
eval( '?>' . $display_code );
}
// Reset globals
$post = $temp_post;
$wp_query = $temp_wp_query;
// Store buffered output
$output = ob_get_clean();
}
$output = preg_replace( "/\xC2\xA0/", ' ', $output );
return $output;
}
/**
* Result count (1-10 of 234)
* @param array $params An array with "page", "per_page", and "total_rows"
* @return string
*/
function get_result_count( $params = [] ) {
$text_of = __( 'of', 'fwp-front' );
$page = (int) $params['page'];
$per_page = (int) $params['per_page'];
$total_rows = (int) $params['total_rows'];
if ( $per_page < $total_rows ) {
$lower = ( 1 + ( ( $page - 1 ) * $per_page ) );
$upper = ( $page * $per_page );
$upper = ( $total_rows < $upper ) ? $total_rows : $upper;
$output = "$lower-$upper $text_of $total_rows";
}
else {
$lower = ( 0 < $total_rows ) ? 1 : 0;
$upper = $total_rows;
$output = $total_rows;
}
return apply_filters( 'facetwp_result_count', $output, [
'lower' => $lower,
'upper' => $upper,
'total' => $total_rows,
] );
}
/**
* Pagination
* @param array $params An array with "page", "per_page", and "total_rows"
* @return string
*/
function paginate( $params = [] ) {
$pager_class = FWP()->helper->facet_types['pager'];
$pager_class->pager_args = $params;
$output = $pager_class->render_numbers([
'inner_size' => 2,
'dots_label' => '…',
'prev_label' => '&lt;&lt;',
'next_label' => '&gt;&gt;',
]);
return apply_filters( 'facetwp_pager_html', $output, $params );
}
/**
* "Per Page" dropdown box
* @return string
*/
function get_per_page_box() {
$pager_class = FWP()->helper->facet_types['pager'];
$pager_class->pager_args = $this->pager_args;
$options = apply_filters( 'facetwp_per_page_options', [ 10, 25, 50, 100 ] );
$output = $pager_class->render_per_page([
'default_label' => __( 'Per page', 'fwp-front' ),
'per_page_options' => implode( ',', $options )
]);
return apply_filters( 'facetwp_per_page_html', $output, [
'options' => $options
] );
}
/**
* Get debug info for the browser console
* @since 3.5.7
*/
function get_debug_info() {
$last_indexed = get_option( 'facetwp_last_indexed' );
$last_indexed = $last_indexed ? human_time_diff( $last_indexed ) : 'never';
$debug = [
'query_args' => $this->query_args,
'sql' => $this->query->request,
'facets' => $this->facets,
'template' => $this->template,
'settings' => FWP()->helper->settings['settings'],
'last_indexed' => $last_indexed,
'row_counts' => FWP()->helper->get_row_counts(),
'hooks_used' => $this->get_hooks_used()
];
// Reduce debug payload
if ( ! empty( $this->query_args['post__in'] ) ) {
$debug['query_args']['post__in_count'] = count( $this->query_args['post__in'] );
$debug['query_args']['post__in'] = array_slice( $this->query_args['post__in'], 0, 10 );
$debug['sql'] = preg_replace_callback( '/posts.ID IN \((.*?)\)/s', function( $matches ) {
$count = substr_count( $matches[1], ',' ) + 1;
return ( $count <= 10 ) ? $matches[0] : "posts.ID IN (<$count IDs>)";
}, $debug['sql'] );
}
return $debug;
}
/**
* Display the location of relevant hooks (for Debug Mode)
* @since 3.5.7
*/
function get_hooks_used() {
$relevant_hooks = [];
foreach ( $GLOBALS['wp_filter'] as $tag => $hook_data ) {
if ( 0 === strpos( $tag, 'facetwp' ) || 'pre_get_posts' == $tag ) {
foreach ( $hook_data->callbacks as $callbacks ) {
foreach ( $callbacks as $cb ) {
if ( is_string( $cb['function'] ) && false !== strpos( $cb['function'], '::' ) ) {
$cb['function'] = explode( '::', $cb['function'] );
}
if ( is_array( $cb['function'] ) ) {
$class = is_object( $cb['function'][0] ) ? get_class( $cb['function'][0] ) : $cb['function'][0];
$ref = new ReflectionMethod( $class, $cb['function'][1] );
}
elseif ( is_object( $cb['function'] ) ) {
if ( is_a( $cb['function'], 'Closure' ) ) {
$ref = new ReflectionFunction( $cb['function'] );
}
else {
$class = get_class( $cb['function'] );
$ref = new ReflectionMethod( $class, '__invoke' );
}
}
else {
$ref = new ReflectionFunction( $cb['function'] );
}
$filename = str_replace( ABSPATH, '', $ref->getFileName() );
// ignore built-in hooks
if ( false === strpos( $filename, 'plugins/facetwp' ) ) {
if ( false !== strpos( $filename, 'wp-content' ) ) {
$relevant_hooks[ $tag ][] = $filename . ':' . $ref->getStartLine();
}
}
}
}
}
}
return $relevant_hooks;
}
}