HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/vfconf/wp-content/plugins/so-widgets-bundle/widgets/post-carousel/post-carousel.php
<?php
/*
Widget Name: Post Carousel
Description: Display blog posts or custom post types in a responsive, customizable carousel layout.
Author: SiteOrigin
Author URI: https://siteorigin.com
Documentation: https://siteorigin.com/widgets-bundle/post-carousel-widget/
Keywords: blog, query
*/

/**
 * Add the carousel image sizes.
 */
function sow_carousel_register_image_sizes() {
	add_image_size( 'sow-carousel-default', 272, 182, true );
}
add_action( 'init', 'sow_carousel_register_image_sizes' );

/**
 * Handles post limits for carousel widgets.
 *
 * Enforces limits from filters, query settings, and pagination constraints.
 * Calculates correct post slice based on current page and sets 'sow-total_posts'
 * for pagination UI.
 *
 * @param WP_Query $posts The query result object containing posts.
 * @param int $paged Current page number (0-indexed).
 *
 * @return WP_Query Modified query object with correct post limits applied.
 */
function sow_carousel_handle_post_limit( $posts, $paged = 0 ) {
	$post_limit = apply_filters( 'siteorigin_widgets_post_carousel_post_limit', false );

	// Account for the Post Query Max Posts setting.
	if ( ! empty( $posts->query['posts_limit'] ) ) {
		$post_limit = min(
			$posts->found_posts,
			$posts->query['posts_limit']
		);
	}

	// If there's no limit, or the limit is higher than the total number of
	// posts, return the posts without modification.
	if ( ! is_numeric( $post_limit ) || $posts->found_posts < $post_limit ) {
		set_query_var( 'sow-total_posts', $posts->found_posts );
		return $posts;
	}

	$posts_per_page = $posts->query['posts_per_page'];
	$current = $posts_per_page * ( $paged - 1 );

	if ( $current < 0 ) {
		$current = $posts_per_page;
	}

	set_query_var( 'sow-total_posts', $post_limit - 1 );

	if ( $current >= $post_limit ) {
		// Check if we've exceeded the expected pagination.
		if ( $paged !== 0 && $current > $post_limit ) {
			$posts->posts = array();
			$posts->post_count = 0;
		} else {
			// We haven't. Limit the number of posts to the remaining amount.
			$remaining = max( 1, $post_limit - ( $current - $posts_per_page ) );
			$posts->post_count = min( $posts_per_page, $remaining );

			// Make sure we're not trying to slice with zero length.
			if ( $posts->post_count > 0 ) {
				$posts->posts = array_slice(
					$posts->posts,
					0,
					$posts->post_count
				);
			}
		}
	}

	return $posts;
}

function sow_carousel_get_next_posts_page() {
	if (
		empty( $_REQUEST['_widgets_nonce'] ) ||
		! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ||
		empty( $_GET['instance_hash'] )
	) {
		die();
	}

	$instance_hash = $_GET['instance_hash'];
	global $wp_widget_factory;
	/** @var SiteOrigin_Widget $widget */
	$widget = ! empty( $wp_widget_factory->widgets['SiteOrigin_Widget_PostCarousel_Widget'] ) ?
	$wp_widget_factory->widgets['SiteOrigin_Widget_PostCarousel_Widget'] : null;
	if ( empty( $widget ) ) {
		die();
	}

	// Try to get the widget instance.
	$instance = $widget->get_stored_instance( $instance_hash );
	if ( empty( $instance ) ) {
		// Couldn't detect instance. Try to get it from the filter.
		$widget_instance = apply_filters( 'siteorigin_widgets_post_carousel_ajax_widget_instance', $instance_hash );

		if ( empty( $widget_instance ) || ! is_array( $widget_instance ) ) {
			die();
		}

		$instance = $widget_instance['instance'];
		$widget = $widget_instance['widget'];
	}

	// Let's set up the template variables, and try to load posts.
	$instance['paged'] = (int) $_GET['paged'];
	$template_vars = $widget->get_template_variables( $instance, array() );
	// If there aren't any settings, we can't output a template.
	if ( empty( $template_vars['settings'] ) ) {
		die();
	}

	$settings = $template_vars['settings'];
	$settings['posts'] = sow_carousel_handle_post_limit(
		$template_vars['settings']['posts'],
		$instance['paged']
	);

	// Don't output anything if there are no posts to return.
	if ( empty( $settings['posts'] ) ) {
		exit();
	}

	ob_start();
	include apply_filters( 'siteorigin_post_carousel_ajax_item_template', 'tpl/item.php', $instance );
	wp_send_json(
		array(
			'html' => ob_get_clean(),
		)
	);
}
add_action( 'wp_ajax_sow_carousel_load', 'sow_carousel_get_next_posts_page' );
add_action( 'wp_ajax_nopriv_sow_carousel_load', 'sow_carousel_get_next_posts_page' );

if ( ! class_exists( 'SiteOrigin_Widget_Base_Carousel' ) ) {
	include_once plugin_dir_path( SOW_BUNDLE_BASE_FILE ) . '/base/inc/widgets/base-carousel.class.php';
}

class SiteOrigin_Widget_PostCarousel_Widget extends SiteOrigin_Widget_Base_Carousel {
	public function __construct() {
		parent::__construct(
			'sow-post-carousel',
			__( 'SiteOrigin Post Carousel', 'so-widgets-bundle' ),
			array(
				'description' => __( 'Display blog posts or custom post types in a responsive, customizable carousel layout.', 'so-widgets-bundle' ),
				'instance_storage' => true,
				'help' => 'https://siteorigin.com/widgets-bundle/post-carousel-widget/',
			),
			array(),
			false,
			plugin_dir_path( __FILE__ )
		);
	}

	public function initialize() {
		// Let the carousel base class do its initialization.
		parent::initialize();

		$this->register_frontend_scripts(
			array(
				array(
					'sow-post-carousel',
					plugin_dir_url( __FILE__ ) . 'js/script' . SOW_BUNDLE_JS_SUFFIX . '.js',
					array( 'jquery', 'slick' ),
					SOW_BUNDLE_VERSION,
					true,
				),
			)
		);

		// Is this a widget preview?
		if (
			! empty( $_POST ) &&
			! empty( $_POST['action'] ) &&
			$_POST['action'] == 'so_widgets_preview'
		) {
			$this->register_theme_assets();
		} else {
			add_action( 'wp_enqueue_scripts', array( $this, 'register_theme_assets' ) );
			add_action( 'enqueue_block_assets', array( $this, 'register_theme_assets' ) );
		}
	}

	public function override_carousel_settings() {
		return apply_filters(
			'siteorigin_widgets_post_carousel_settings_form',
			array(
				'breakpoints' => apply_filters(
					'siteorigin_widgets_post_carousel_breakpoints',
					array(
						'tablet_landscape' => 1366,
						'tablet_portrait'  => 1025,
						'mobile'           => 480,
					)
				),
				'slides_to_scroll' => array(
					'desktop' => 1,
					'tablet_landscape' => 2,
					'tablet_portrait' => 2,
					'mobile' => 1,
				),
				'navigation' => array(
					'desktop' => true,
					'tablet_landscape' => true,
					'tablet_portrait' => true,
					'mobile' => false,
				),
				'slides_to_show' => array(),
				'navigation_dots_label' => '',
			)
		);
	}

	public function register_theme_assets() {
		wp_register_style( 'sow-post-carousel-base', plugin_dir_url( __FILE__ ) . 'css/base.css' );
		do_action( 'siteorigin_widgets_post_carousel_theme_assets' );

		if ( $this->is_block_editor_page() ) {
			wp_enqueue_style( 'sow-post-carousel-base' );
		}
	}

	public function get_style_name( $instance ) {
		$theme = self::get_theme( $instance );

		// Try to load carousel theme stylesheet.
		wp_enqueue_style( 'sow-post-carousel-' . $theme );

		add_action( 'wp_head', array( $this, 'prevent_potential_shifts' ), 10, 0 );

		return $theme;
	}

	public function get_widget_form() {
		$design_settings = $this->design_settings_form_fields(
			array(
				'navigation' => array(
					'type' => 'section',
					'label' => __( 'Navigation', 'so-widgets-bundle' ),
					'hide' => true,
					'fields' => array(
						'navigation_color' => array(
							'type' => 'color',
							'label' => __( 'Arrow Color', 'so-widgets-bundle' ),
							'default' => '#fff',
						),
						'navigation_color_hover' => array(
							'type' => 'color',
							'label' => __( 'Arrow Hover Color', 'so-widgets-bundle' ),
						),
						'navigation_background' => array(
							'type' => 'color',
							'label' => __( 'Background', 'so-widgets-bundle' ),
							'default' => '#333',
						),
						'navigation_hover_background' => array(
							'type' => 'color',
							'label' => __( 'Hover Background', 'so-widgets-bundle' ),
							'default' => '#444',
						),
					),
				),
			)
		);

		// Override defaults.
		$design_settings['fields']['item_title']['label'] = __( 'Post Title', 'so-widgets-bundle' );
		$design_settings['fields']['item_title']['fields']['tag']['default'] = 'h3';

		// Reposition thumbnail settings.
		$design_settings['fields'] = array_merge(
			array(
				'thumbnail' => array(
					'type' => 'section',
					'label' => __( 'Post Thumbnail', 'so-widgets-bundle' ),
					'hide' => true,
					'fields' => array(
						'thumbnail_overlay_hover_color' => array(
							'type' => 'color',
							'label' => __( 'Thumbnail Overlay Hover Color', 'so-widgets-bundle' ),
							'default' => '#3279bb',
						),
						'thumbnail_overlay_hover_opacity' => array(
							'type' => 'slider',
							'label' => __( 'Thumbnail Overlay Hover Opacity', 'so-widgets-bundle' ),
							'default' => '0.5',
							'min' => 0,
							'max' => 1,
							'step' => 0.1,
						),
					),
				),
			),
			$design_settings['fields']
		);

		$carousel_settings = $this->carousel_settings_form_fields();
		$carousel_settings['fields']['loop']['description'] = __( 'Automatically return to the first post after the last post.', 'so-widgets-bundle' );

		$carousel_settings['fields']['autoplay'] = array(
			'type' => 'presets',
			'label' => __( 'Autoplay', 'so-widgets-bundle' ),
			'default_preset' => 'off',
			'state_emitter' => array(
				'callback' => 'select',
				'args' => array( 'autoplay' ),
			),
			'options' => array(
				'off' => array(
					'label' => __( 'Off', 'so-widgets-bundle' ),
					'values' => array(
						'carousel_settings' => array(
							'animation_speed' => 400,
							'timeout' => 8000,
						),
					),
				),
				'on' => array(
					'label' => __( 'On', 'so-widgets-bundle' ),
					'values' => array(
						'carousel_settings' => array(
							'animation_speed' => 400,
							'timeout' => 8000,
						),
					),
				),
				'continuous' => array(
					'label' => __( 'Continuous', 'so-widgets-bundle' ),
					'values' => array(
						'carousel_settings' => array(
							'animation' => 'linear',
							'animation_speed' => 8000,
							'timeout' => 400,
							'autoplay_pause_hover' => true,
						),
					),
				),
			),
		);

		// Continuous autoplay doesn't have navigation, so we need to hide the setting.
		$carousel_settings['fields']['arrows']['state_handler'] = array(
			'autoplay[continuous]' => array( 'hide' ),
			'_else[autoplay]' => array( 'show' ),
		);

		// Hide autoplay specific settings if autoplay is off.
		$if_off_hide = array(
			'autoplay[off]' => array( 'hide' ),
			'_else[autoplay]' => array( 'show' ),
		);
		$carousel_settings['fields']['autoplay_pause_hover']['state_handler'] = $if_off_hide;
		$carousel_settings['fields']['timeout']['state_handler'] = $if_off_hide;

		return array(
			'title' => array(
				'type' => 'text',
				'label' => __( 'Title', 'so-widgets-bundle' ),
			),

			'default_thumbnail' => array(
				'type'     => 'media',
				'library'  => 'image',
				'label'    => __( 'Default Thumbnail', 'so-widgets-bundle' ),
				'choose'   => __( 'Choose Thumbnail', 'so-widgets-bundle' ),
				'update'   => __( 'Set Thumbnail', 'so-widgets-bundle' ),
				'fallback' => true,
			),

			'image_size' => array(
				'type' => 'image-size',
				'label' => __( 'Featured Image Size', 'so-widgets-bundle' ),
				'default' => 'sow-carousel-default',
			),

			'link_target' => array(
				'type' => 'select',
				'label' => __( 'Link Target', 'so-widgets-bundle' ),
				'description' => __( 'Choose where to open each carousel item.', 'so-widgets-bundle' ),
				'options' => array(
					'same'    => __( 'Same window ', 'so-widgets-bundle' ),
					'new'    => __( 'New window ', 'so-widgets-bundle' ),
				),
			),

			'carousel_settings' => $carousel_settings,

			'posts' => array(
				'type' => 'posts',
				'label' => __( 'Posts Query', 'so-widgets-bundle' ),
				'hide' => true,
				'posts_limit' => true,
				'fields' => array(
					'posts_per_page' => array(
						'label' => __( 'Posts Per Load', 'so-widgets-bundle' ),
						'description' => __( 'Set the number of posts preloaded in the background when clicking next. The default is 10.', 'so-widgets-bundle' ),
					),
				),
			),
			'design' => $design_settings,
			'responsive' => $this->responsive_form_fields(),
		);
	}

	public function modify_instance( $instance ) {
		// Migrate old Design settings to new settings structure.
		if (
			is_array( $instance ) &&
			isset( $instance['design'] ) &&
			isset( $instance['design']['thumbnail_overlay_hover_color'] )
		) {
			$instance['design']['thumbnail'] = array(
				'thumbnail_overlay_hover_color' => ! empty( $instance['design']['thumbnail_overlay_hover_color'] ) ? $instance['design']['thumbnail_overlay_hover_color'] : '#3279bb',
				'thumbnail_overlay_hover_opacity' => ! empty( $instance['design']['thumbnail_overlay_hover_opacity'] ) ? $instance['design']['thumbnail_overlay_hover_opacity'] : 0.5,
			);
			$instance['design']['navigation'] = array(
				'navigation_color' => ! empty( $instance['design']['navigation_color'] ) ? $instance['design']['navigation_color'] : '#fff',
				'navigation_color_hover' => ! empty( $instance['design']['navigation_color_hover'] ) ? $instance['design']['navigation_color_hover'] : '',
				'navigation_background' => ! empty( $instance['design']['navigation_background'] ) ? $instance['design']['navigation_background'] : '#333',
				'navigation_hover_background' => ! empty( $instance['design']['navigation_hover_background'] ) ? $instance['design']['navigation_hover_background'] : '#444',
			);
		}

		// Migrate settings to the Settings section.
		if ( isset( $instance['loop_posts'] ) ) {
			if ( empty( $instance['carousel_settings'] ) ) {
				$instance['carousel_settings'] = array();
			}

			$instance['carousel_settings']['loop'] = $instance['loop_posts'];
			unset( $instance['loop_posts'] );
		}

		// Migrate autoplay settings.
		if (
			isset( $instance['carousel_settings']['autoplay'] ) &&
			is_bool( $instance['carousel_settings']['autoplay'] )
		) {
			if ( empty( $instance['carousel_settings']['autoplay'] ) ) {
				$instance['carousel_settings']['autoplay'] = 'off';
			} elseif ( ! empty( $instance['carousel_settings']['autoplay_continuous_scroll'] ) ) {
				$instance['carousel_settings']['autoplay'] = 'continuous';

				// Adjust timeout/autoplay if they're using defaults.
				if ( $instance['carousel_settings']['timeout'] == 8000 ) {
					$instance['carousel_settings']['timeout'] = 400;
				}

				if ( $instance['carousel_settings']['autoplay'] == 400 ) {
					$instance['carousel_settings']['autoplay'] = 4000;
				}
			} else {
				$instance['carousel_settings']['autoplay'] = 'on';
			}
		}

		return $instance;
	}

	public function get_less_variables( $instance ) {
		if ( empty( $instance ) ) {
			return array();
		}

		$size = siteorigin_widgets_get_image_size( $instance['image_size'] );

		$thumb_width = '';
		$thumb_height = '';
		$thumb_hover_width = '';
		$thumb_hover_height = '';

		if ( ! ( empty( $size['width'] ) || empty( $size['height'] ) ) ) {
			$thumb_width = $size['width'] - $size['width'] * 0.1;
			$thumb_height = $size['height'] - $size['height'] * 0.1;
			$thumb_hover_width = $size['width'];
			$thumb_hover_height = $size['height'];
		}

		$less_vars = array(
			'thumbnail_width' => $thumb_width . 'px',
			'thumbnail_height' => $thumb_height . 'px',
			'thumbnail_hover_width' => $thumb_hover_width . 'px',
			'thumbnail_hover_height' => $thumb_hover_height . 'px',
			'thumbnail_overlay_hover_color' => ! empty( $instance['design']['thumbnail']['thumbnail_overlay_hover_color'] ) ? $instance['design']['thumbnail']['thumbnail_overlay_hover_color'] : '',
			'thumbnail_overlay_hover_opacity' => ! empty( $instance['design']['thumbnail']['thumbnail_overlay_hover_opacity'] ) ? $instance['design']['thumbnail']['thumbnail_overlay_hover_opacity'] : 0.5,
			'navigation_color' => ! empty( $instance['design']['navigation']['navigation_color'] ) ? $instance['design']['navigation']['navigation_color'] : '',
			'navigation_color_hover' => ! empty( $instance['design']['navigation']['navigation_color_hover'] ) ? $instance['design']['navigation']['navigation_color_hover'] : '',
			'navigation_background' => ! empty( $instance['design']['navigation']['navigation_background'] ) ? $instance['design']['navigation']['navigation_background'] : '',
			'navigation_hover_background' => ! empty( $instance['design']['navigation']['navigation_hover_background'] ) ? $instance['design']['navigation']['navigation_hover_background'] : '',
			'item_title_tag' => ! empty( $instance['design']['item_title']['tag'] ) ? $instance['design']['item_title']['tag'] : '',
			'item_title_font_size' => ! empty( $instance['design']['item_title']['size'] ) ? $instance['design']['item_title']['size'] : '',
			'item_title_color' => ! empty( $instance['design']['item_title']['color'] ) ? $instance['design']['item_title']['color'] : '',
		);
		$less_vars = $this->responsive_less_variables( $less_vars, $instance );

		if (
			! empty( $instance['design']['item_title'] ) &&
			! empty( $instance['design']['item_title']['font'] )
		) {
			$item_title_font = siteorigin_widget_get_font( $instance['design']['item_title']['font'] );
			$less_vars['title_font'] = $item_title_font['family'];

			if ( ! empty( $item_title_font['weight'] ) ) {
				$less_vars['title_font_style'] = $item_title_font['style'];
				$less_vars['title_font_weight'] = $item_title_font['weight_raw'];
			}
		}

		return $less_vars;
	}

	public static function get_theme( $instance ) {
		return empty( $instance['design']['theme'] ) || ! class_exists( 'SiteOrigin_Premium_Plugin_Carousel' ) ? 'base' : $instance['design']['theme'];
	}

	public function get_template_variables( $instance, $args ) {
		$theme = self::get_theme( $instance );

		if (
			! empty( $instance['default_thumbnail'] ) ||
			! empty( $instance['default_thumbnail_fallback'] )
		) {
			$default_thumbnail = siteorigin_widgets_get_attachment_image_src( $instance['default_thumbnail'], $instance['image_size'], $instance['default_thumbnail_fallback'] );
		}

		$query = siteorigin_widget_post_selector_process_query(
			wp_parse_args(
				$instance['posts'],
				array(
					'paged' => empty( $instance['paged'] ) ? 1 : $instance['paged'],
				)
			)
		);
		$posts = new WP_Query( $query );

		$carousel_settings = $this->carousel_settings_template_variables( $instance['carousel_settings'], false );
		$carousel_settings['autoplay_continuous_scroll'] = $instance['carousel_settings']['autoplay'] === 'continuous';
		// The base theme doesn't support dot navigation so let's remove it.
		if ( $theme == 'base' ) {
			unset( $carousel_settings['dots'] );
		}

		$carousel_settings['loop'] = ! empty( $instance['carousel_settings']['loop'] );
		$carousel_settings['item_overflow'] = true;
		$carousel_settings = apply_filters( 'siteorigin_widgets_post_carousel_settings_frontend', $carousel_settings, $instance );

		$size = siteorigin_widgets_get_image_size( $instance['image_size'] );

		$navigation = 'title';

		// If continuous scroll is enabled, hide the navigation.
		if (
			! empty( $instance['carousel_settings']['autoplay'] ) &&
			$instance['carousel_settings']['autoplay'] === 'continuous'
		) {
			$navigation = 'hidden';
		}

		return array(
			'settings' => array(
				'args' => $args,
				'title' => $instance['title'],
				'theme' => $theme,
				'posts' => sow_carousel_handle_post_limit( $posts ),
				'default_thumbnail' => ! empty( $default_thumbnail ) ? $default_thumbnail[0] : false,
				'image_size' => $instance['image_size'],
				'link_target' => ! empty( $instance['link_target'] ) ? $instance['link_target'] : 'same',
				'item_template' => apply_filters( 'siteorigin_post_carousel_item_template', plugin_dir_path( __FILE__ ) . 'tpl/item.php' ),
				'navigation' => $navigation,
				'navigation_arrows' => isset( $instance['carousel_settings']['arrows'] ) ? ! empty( $instance['carousel_settings']['arrows'] ) : true,
				'navigation_dots' => isset( $instance['carousel_settings']['dots'] ) ? ! empty( $instance['carousel_settings']['dots'] ) : false,
				'height' => ! empty( $size['height'] ) ? 'min-height: ' . $size['height'] . 'px' : '',
				'item_title_tag' => siteorigin_widget_valid_tag(
					! empty( $instance['design']['item_title']['tag'] ) ? $instance['design']['item_title']['tag'] : 'h3',
					'h3'
				),
				'attributes' => array(
					'widget' => 'post',
					'fetching' => 'false',
					'page' => 1,
					'ajax-url' => sow_esc_url( wp_nonce_url( admin_url( 'admin-ajax.php' ), 'widgets_action', '_widgets_nonce' ) ),

					// Base carousel specific settings.
					'item_count' => get_query_var( 'sow-total_posts' ),
					'carousel_settings' => json_encode( $carousel_settings ),
					'responsive' => $this->responsive_template_variables( $instance['responsive'] ),
					'variable_width' => 'true',
				),
			),
		);
	}

	public function get_template_name( $instance ) {
		return 'base';
	}

	public function get_form_teaser() {
		if ( class_exists( 'SiteOrigin_Premium' ) ) {
			return false;
		}

		return sprintf(
			__( 'Get access to additional carousel themes with %sSiteOrigin Premium%s', 'so-widgets-bundle' ),
			'<a href="https://siteorigin.com/downloads/premium/?featured_addon=plugin/carousel" target="_blank" rel="noopener noreferrer">',
			'</a>'
		);
	}

	/**
	 * Prevent potential layout shifts during carousel initialization
	 *
	 * Adds CSS to ensure carousels have a minimum height while loading
	 * and only become visible after Slick initialization is complete.
	 * Uses static variable to ensure styles are only added once per page load.
	 */
	public function prevent_potential_shifts(): void {
		// Use static variable to ensure this only runs once
		static $carousel_styles_added = false;

		if ( $carousel_styles_added ) {
			return;
		}
		?><style type="text/css">
			.sow-carousel-wrapper:has(.slick-initialized) {
				visibility: visible !important;
				opacity: 1 !important;
			}

			.sow-post-carousel-wrapper:not(:has(.slick-initialized)) .sow-carousel-items {
				visibility: hidden;
			}
		</style>
		<?php
		$carousel_styles_added = true;
	}
}

siteorigin_widget_register( 'sow-post-carousel', __FILE__, 'SiteOrigin_Widget_PostCarousel_Widget' );