is_cart_empty() &&
empty( $wp->query_vars['order-pay'] ) &&
! isset( $wp->query_vars['order-received'] ) &&
! isset( $wp->query_vars['order_pay'] ) &&
! is_customize_preview() &&
apply_filters( 'storeengine/checkout_redirect_empty_cart', true )
) {
wp_safe_redirect( Helper::get_page_permalink( 'cart_page' ) );
exit;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Logout endpoint under My Account page. Logging out requires a valid nonce.
if ( Helper::is_endpoint( 'customer-logout' ) ) {
if ( ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
wp_logout();
if ( ! empty( $_REQUEST['redirect_to'] ) ) {
$redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
} else {
$redirect_to = storeengine_get_logout_redirect_url();
}
wp_safe_redirect( wp_validate_redirect( $redirect_to ) );
exit;
}
/** @noinspection HtmlUnknownTarget */
wp_die(
/* translators: %s: logout url */
sprintf( wp_kses_post( __( 'Are you sure you want to log out? Confirm and log out', 'storeengine' ) ), esc_url( storeengine_logout_url() ) ),
esc_html__( 'Are you sure you want to log out?', 'storeengine' ),
[ 'back_link' => true ]
);
}
// If user landed from another page with redirect-to param.
if ( is_user_logged_in() && ! empty( $_REQUEST['redirect_to'] ) ) {
$redirect_to = wp_validate_redirect( esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) );
if ( $redirect_to ) {
wp_safe_redirect( $redirect_to );
} else {
wp_safe_redirect( remove_query_arg( 'redirect_to' ) );
}
exit;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// Trigger 404 if trying to access an endpoint on wrong page.
if ( Helper::is_endpoint() && ! Helper::is_dashboard() && ! Helper::is_checkout() && apply_filters( 'storeengine/dashboard/endpoint_page_not_found', true ) ) {
$wp_query->set_404();
status_header( 404 );
include get_query_template( '404' );
exit;
}
// Redirect to the product page if we have a single product.
if ( is_search() && is_post_type_archive( Helper::PRODUCT_POST_TYPE ) && apply_filters( 'storeengine/redirect/single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) {
$product = storeengine_get_product( $wp_query->post->ID );
if ( $product && $product->is_visible() ) {
wp_safe_redirect( get_permalink( $product->get_id() ) );
exit;
}
}
}
/**
* SE Login URL
*
* Do not hook this function into login_url ever.
*
* @param string $redirect Path to redirect to on log in.
* @param bool $force_reauth Whether to force reauthorization, even if a cookie is present.
* Default false.
*
* @return string The login URL. Not HTML-encoded.
*/
function storeengine_login_url( string $redirect = '', bool $force_reauth = false ): string {
$auth_redirect_type = Helper::get_settings( 'auth_redirect_type', 'storeengine' );
$login_url = wp_login_url( $redirect, $force_reauth );
if ( 'default' === $auth_redirect_type || is_admin() ) {
return wp_login_url( $redirect, $force_reauth );
}
if ( 'custom' === $auth_redirect_type && Helper::get_settings( 'auth_redirect_url' ) ) {
$login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), Helper::get_settings( 'auth_redirect_url' ) );
} elseif ( 'storeengine' === $auth_redirect_type && Helper::get_settings( 'dashboard_page' ) ) {
$login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), Helper::get_dashboard_url() );
}
if ( $force_reauth ) {
$login_url = add_query_arg( 'reauth', '1', $login_url );
}
return apply_filters( 'storeengine/login_url', $login_url, $redirect, $force_reauth );
}
/**
* Login URL tailored for the checkout "email already registered" flow.
*
* Returns the shopper back to checkout after a successful login and pre-fills
* the email (via the `se_email` query arg the login template reads) so they
* don't have to retype it. Shared by BOTH the inline contact pre-check and the
* Place-Order 409, so the "Log in to continue" call-to-action is identical on
* every surface that can surface it.
*
* @param string $email Email to pre-fill on the login form.
* @param string $redirect Where to send the shopper after login. Defaults to
* the current checkout URL.
*
* @return string
*/
function storeengine_checkout_login_url( string $email = '', string $redirect = '' ): string {
if ( '' === $redirect ) {
$redirect = Helper::get_checkout_url();
}
$url = storeengine_login_url( $redirect );
// add_query_arg() url-encodes the value itself — pass the raw address.
if ( '' !== $email && is_email( $email ) ) {
$url = add_query_arg( 'se_email', $email, $url );
}
return apply_filters( 'storeengine/checkout/contact_login_url', $url, $email, $redirect );
}
/**
* Prevent any user who cannot 'edit_posts' (subscribers, customers etc.) from seeing the admin bar.
*
* @param bool $show_admin_bar If site should display admin bar.
*
* @return bool
*/
function storeengine_disable_admin_bar( bool $show_admin_bar ): bool {
/**
* Controls whether the StoreEngine admin bar should be disabled.
*
* @param bool $enabled
*/
if ( apply_filters( 'storeengine/disable_admin_bar', true ) && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_storeengine' ) ) ) {
$show_admin_bar = false;
}
return $show_admin_bar;
}
/**
* Initialize global product for current request.
* This setup the global before the loop for current page only.
*
* @return void
*/
function storeengine_init_global_product_early() {
if ( empty( $GLOBALS['post'] ) ) {
return;
}
if ( empty( $GLOBALS['post']->post_type ) || $GLOBALS['post']->post_type !== 'storeengine_product' ) {
return;
}
$GLOBALS['product'] = storeengine_get_product( $GLOBALS['post']->ID ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
}
/**
* Initialize global product inside the loop.
*
* @param $post
*
* @return false|SimpleProduct|VariableProduct|void
*/
function storeengine_initialize_product_data( $post ) {
if ( is_int( $post ) ) {
$post = get_post( $post );
}
if ( empty( $post->post_type ) || $post->post_type !== 'storeengine_product' ) {
return;
}
$GLOBALS['the_original_product'] = $GLOBALS['product'] ?? null; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Compat global name mirroring the $product loop global preserved above.
$GLOBALS['product'] = storeengine_get_product( $post->ID ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
return $GLOBALS['product'];
}
function storeengine_archive_title_parts( array $title ): array {
global $wp_query;
if ( Helper::is_shop() ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only archive sort label derived from the public query var; no state change.
if ( ! empty( $_REQUEST['orderby'] ) && $wp_query->get( 'orderby' ) && $wp_query->get( 'order' ) ) {
$sort = strtolower( $wp_query->get( 'orderby' ) . ':' . $wp_query->get( 'order' ) );
switch ( $sort ) {
case 'post_title:asc':
$label = __( 'Alphabetical (A–Z)', 'storeengine' );
break;
case 'post_title:desc':
$label = __( 'Alphabetical (Z–A)', 'storeengine' );
break;
case 'publish_date:desc':
$label = __( 'Newest first', 'storeengine' );
break;
case 'publish_date:asc':
$label = __( 'Oldest first', 'storeengine' );
break;
case 'modified:desc':
$label = __( 'Recently updated', 'storeengine' );
break;
case 'modified:asc':
$label = __( 'Least recently updated', 'storeengine' );
break;
case 'menu_order:asc':
$label = __( 'Custom order (ascending)', 'storeengine' );
break;
case 'menu_order:desc':
$label = __( 'Custom order (descending)', 'storeengine' );
break;
case 'id:asc':
$label = __( 'ID (ascending)', 'storeengine' );
break;
case 'id:desc':
$label = __( 'ID (descending)', 'storeengine' );
break;
default:
$label = str_ends_with( $sort, ':desc' ) ? __( 'Sort (descending)', 'storeengine' ) : __( 'Sort (ascending)', 'storeengine' );
break;
}
if ( $label ) {
$page = $title['page'] ?? null;
$site = $title['site'] ?? null;
unset( $title['page'], $title['site'] );
$title['sort'] = $label;
if ( $page ) {
$title['page'] = $page;
}
if ( $site ) {
$title['site'] = $site;
}
}
}
}
return $title;
}
add_filter( 'document_title_parts', 'storeengine_archive_title_parts' );
/**
* @param int $id
*
* @return AbstractOrder|Order|WP_Error
* @throws StoreEngineException
*/
function storeengine_get_order( int $id ) {
$order = Helper::get_order( $id );
if ( is_wp_error( $order ) ) {
throw StoreEngineException::from_wp_error( $order ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return $order;
}
function storeengine_cart(): Cart {
return StoreEngine::init()->get_cart();
}
if ( ! function_exists( 'storeengine_get_header' ) ) {
function storeengine_get_header( $header_name = 'product' ) {
if ( Helper::is_fse_theme() ) {
?>
>
>
is_type( 'bundled' ) && 'storeengine/templates/single-product/header_right_content' === current_action() ) {
$bundles = $product->get_bundles();
}
if ( empty( $bundles ) || ! is_array( $bundles ) ) {
return;
}
Template::get_template( 'global/bundle-info.php', [ 'bundles' => $bundles ] );
}
}
if ( ! function_exists( 'storeengine_single_product_add_to_cart' ) ) {
function storeengine_single_product_add_to_cart() {
Template::get_template( 'single-product/add-to-cart.php' );
}
}
if ( ! function_exists( 'storeengine_single_product_footer' ) ) {
function storeengine_single_product_footer() {
Template::get_template(
'single-product/footer.php',
);
}
}
if ( ! function_exists( 'storeengine_single_product_description' ) ) {
function storeengine_single_product_description() {
Template::get_template(
'single-product/description.php',
);
}
}
if ( ! function_exists( 'storeengine_add_to_cart_form' ) ) {
function storeengine_add_to_cart_form() {
$cart_page_permalink = Helper::get_page_permalink( 'cart_page' );
$has_view_cart = true;
Template::get_template(
'single-product/add-to-cart.php',
[
'cart_page_permalink' => $cart_page_permalink,
'has_view_cart' => $has_view_cart,
]
);
}
}
function storeengine_placeholder_image_src( $size = 'storeengine_thumbnail' ) {
// @TODO allow admin to change placeholder image.
// @TODO implement size args.
return apply_filters( 'storeengine/placeholder/image_src', STOREENGINE_ASSETS_URI . 'images/thumbnail-placeholder.png', $size );
}
/**
* Get the placeholder image.
*
* @param string $size
* @param string|array $attr
*
* @return string
*
* Mirrors the standard option-seeding on install.
*
* @see add_image_size()
*/
function storeengine_placeholder_image( string $size = 'storeengine_thumbnail', $attr = '' ): string {
// @TODO add thumbnail & single product image size
// @TODO use wp_get_attachment_image
$dimensions = [
'width' => 600,
'height' => 600,
];
$default_attr = [
'class' => 'storeengine-placeholder wp-post-image',
'alt' => __( 'Placeholder', 'storeengine' ),
];
$attr = wp_parse_args( $attr, $default_attr );
$image = storeengine_placeholder_image_src( $size );
$hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] );
$attributes = [];
foreach ( $attr as $name => $value ) {
$attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
}
$image_html = '
'; // phpcs:ignore PluginCheck.CodeAnalysis.ImageFunctions.NonEnqueuedImage
return apply_filters( 'storeengine/placeholder/image', $image_html, $size, $dimensions );
}
function storeengine_has_product_thumbnail( ?int $product_id = null ): bool {
if ( ! $product_id ) {
$product_id = get_the_ID();
}
return (bool) apply_filters( 'storeengine/has_post_thumbnail', has_post_thumbnail( $product_id ), $product_id );
}
function storeengine_has_product_gallery( ?int $product_id = null ): bool {
if ( ! $product_id ) {
$product_id = get_the_ID();
}
// Show the carousel when there's more than one item, or any video (even a
// single video still needs a player rather than the plain-image layout).
$items = storeengine_get_product_gallery_items( $product_id );
$has_video = false;
foreach ( $items as $item ) {
if ( 'video' === $item['type'] ) {
$has_video = true;
break;
}
}
$has_gallery = count( $items ) > 1 || $has_video;
return apply_filters( 'storeengine/has_product_gallery', $has_gallery, $product_id );
}
/**
* Resolve the single-product gallery layout (global setting): carousel |
* stacked | grid. Defaults to carousel.
*/
function storeengine_get_product_gallery_layout(): string {
$layout = \StoreEngine\Utils\Helper::get_settings( 'single_product_gallery_layout', 'carousel' );
$allowed = [ 'carousel', 'stacked', 'grid' ];
if ( ! in_array( $layout, $allowed, true ) ) {
$layout = 'carousel';
}
return apply_filters( 'storeengine/product/gallery_layout', $layout );
}
/**
* Conditionally enqueue the custom (vanilla-JS, no jQuery/Flickity) product
* gallery bundle — only when the product actually has gallery media, so the
* carousel/stacked/grid + lightbox/zoom code loads on demand.
*
* @param int|null $product_id
*/
function storeengine_enqueue_product_gallery_assets( ?int $product_id = null ) {
if ( ! $product_id ) {
$product_id = get_the_ID();
}
if ( ! $product_id || ! storeengine_has_product_gallery( $product_id ) ) {
return;
}
$asset_file = STOREENGINE_ASSETS_DIR_PATH . 'build/product-gallery.' . STOREENGINE_VERSION . '.asset.php';
$asset = file_exists( $asset_file )
? include $asset_file
: [
'dependencies' => [],
'version' => STOREENGINE_VERSION,
];
wp_enqueue_style(
'storeengine-product-gallery',
STOREENGINE_ASSETS_URI . 'build/product-gallery.css',
[],
$asset['version']
);
wp_enqueue_script(
'storeengine-product-gallery',
STOREENGINE_ASSETS_URI . 'build/product-gallery.' . STOREENGINE_VERSION . '.js',
$asset['dependencies'],
$asset['version'],
true
);
wp_localize_script( 'storeengine-product-gallery', 'StoreEngineGalleryL10n', [
'close' => __( 'Close', 'storeengine' ),
'next' => __( 'Next image', 'storeengine' ),
'prev' => __( 'Previous image', 'storeengine' ),
'zoomIn' => __( 'Zoom in', 'storeengine' ),
'zoomOut' => __( 'Zoom out', 'storeengine' ),
'resetZoom' => __( 'Reset zoom', 'storeengine' ),
'playVideo' => __( 'Play video', 'storeengine' ),
/* translators: %1$s current index, %2$s total. */
'counter' => __( '%1$s of %2$s', 'storeengine' ),
] );
}
function storeengine_get_product_image( $size = 'storeengine_thumbnail', $product_id = null, $attr = '', $placeholder = true ) {
$image = '';
$thumbnail_id = 0;
if ( ! $product_id ) {
$product_id = get_the_ID();
}
if ( $product_id ) {
$thumbnail_id = (int) get_post_thumbnail_id( $product_id );
}
if ( ! $thumbnail_id ) {
// Fallback to first item on the gallery.
$ids = get_post_meta( $product_id, '_storeengine_product_gallery_ids', true );
if ( $ids && is_array( $ids ) ) {
$ids = array_unique( array_filter( $ids ) );
$thumbnail_id = reset( $ids );
}
}
if ( $thumbnail_id ) {
$image = wp_get_attachment_image( $thumbnail_id, $size, false, $attr );
}
if ( ! $image && $placeholder ) {
$image = storeengine_placeholder_image( $size, $attr );
}
return apply_filters( 'storeengine/product/image_html', $image, $product_id, $size, $attr, $placeholder );
}
function storeengine_product_image( $size = 'storeengine_thumbnail', $product_id = null, $attr = '', $placeholder = true ) {
// Output from wp image html.
// wp_kses-post remove srcset, sizes etc attributes
echo storeengine_get_product_image( $size, $product_id, $attr, $placeholder ); // phpcs:ignore
}
function storeengine_product_gallery( $product_id = null, $size = 'storeengine_thumbnail', $attr = '' ) {
if ( ! $product_id ) {
$product_id = get_the_ID();
}
if ( $product_id ) {
// @TODO allow admin to choose a featured image too..
// @XXX backend allows admin to add same image multiple time.
$ids = get_post_meta( $product_id, '_storeengine_product_gallery_ids', true );
if ( ! is_array( $ids ) ) {
$ids = [];
}
$ids = array_unique( array_filter( $ids ) );
if ( ! Helper::is_product() && count( $ids ) > 1 ) {
$ids = [ reset( $ids ) ];
$ids = array_filter( $ids );
}
foreach ( $ids as $id ) {
$image = wp_get_attachment_image_src( $id, $size );
if ( ! $image ) {
continue;
}
printf(
'%1$s',
wp_get_attachment_image( $id, $size, false, $attr )
);
}
}
}
/**
* Normalized list of gallery videos for a product.
*
* @param int|null $product_id
*
* @return array
*/
function storeengine_get_product_gallery_videos( ?int $product_id = null ): array {
if ( ! $product_id ) {
$product_id = get_the_ID();
}
$videos = $product_id ? get_post_meta( $product_id, '_storeengine_product_gallery_videos', true ) : [];
if ( ! is_array( $videos ) ) {
$videos = [];
}
$out = [];
foreach ( $videos as $video ) {
$url = is_array( $video ) ? ( $video['url'] ?? '' ) : (string) $video;
$url = trim( (string) $url );
if ( '' === $url ) {
continue;
}
$out[] = [
'url' => $url,
'poster_id' => is_array( $video ) ? absint( $video['poster_id'] ?? 0 ) : 0,
];
}
return apply_filters( 'storeengine/product/gallery_videos', $out, $product_id );
}
/**
* Extract a YouTube video id from common URL shapes (watch, youtu.be, embed,
* shorts). Returns '' when the url is not a recognizable YouTube link.
*/
function storeengine_extract_youtube_id( string $url ): string {
if ( preg_match( '~(?:youtube\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|v/)|youtu\.be/)([A-Za-z0-9_-]{11})~i', $url, $m ) ) {
return $m[1];
}
return '';
}
/**
* Whether a URL points to a directly-playable video file (uploaded / media
* library video, or any .mp4/.webm/... link).
*/
function storeengine_is_video_file_url( string $url ): bool {
$path = wp_parse_url( $url, PHP_URL_PATH );
$ext = $path ? strtolower( pathinfo( $path, PATHINFO_EXTENSION ) ) : '';
return in_array( $ext, [ 'mp4', 'm4v', 'webm', 'ogv', 'ogg', 'mov' ], true );
}
/**
* Sanitise an oEmbed / iframe embed fragment down to a safe, iframe-allowing
* allow-list with wp_kses (mirrors WordPress core's oEmbed sanitisation).
* Strips scripts, event handlers and unsafe protocols while keeping provider
* iframes intact.
*
* Used as defence-in-depth: the JS injects this HTML via innerHTML, so we do
* not want to rely solely on core's `oembed_result` filter still being
* attached (another plugin could remove it) when a lower-privileged editor /
* marketplace vendor — who may lack `unfiltered_html` — supplied the video URL.
*
* @param string $html
* @return string
*/
function storeengine_kses_gallery_embed( string $html ): string {
$allowed = [
'a' => [
'href' => true,
'title' => true,
],
'blockquote' => [ 'class' => true ],
'iframe' => [
'src' => true,
'width' => true,
'height' => true,
'frameborder' => true,
'marginwidth' => true,
'marginheight' => true,
'scrolling' => true,
'title' => true,
'class' => true,
'name' => true,
'allow' => true,
'allowfullscreen' => true,
'loading' => true,
'referrerpolicy' => true,
'sandbox' => true,
'style' => true,
],
];
return wp_kses( $html, $allowed );
}
/**
* Resolve a video URL to sanitised, cached oEmbed HTML.
*
* `wp_oembed_get()` performs an outbound HTTP request (plus provider discovery
* for non-whitelisted hosts) on every call, so the result is cached in a
* transient keyed by URL + width to avoid a fetch on every product-page view.
* The HTML is additionally run through {@see storeengine_kses_gallery_embed()}.
*
* @param string $url
* @param int $width
* @return string Sanitised embed HTML, or '' if the URL isn't embeddable.
*/
function storeengine_get_gallery_oembed_html( string $url, int $width = 1200 ): string {
$cache_key = 'se_gallery_oembed_' . md5( $url . '|' . $width );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return (string) $cached;
}
$embed = wp_oembed_get( $url, [ 'width' => $width ] );
$html = $embed ? storeengine_kses_gallery_embed( (string) $embed ) : '';
// Cache successes for a day; cache misses only briefly so a transient
// provider hiccup doesn't suppress a valid embed for long.
set_transient( $cache_key, $html, $html ? DAY_IN_SECONDS : 5 * MINUTE_IN_SECONDS );
return $html;
}
/**
* Markup for a single gallery video's main (player) cell.
*
* @param array{url:string, poster_id:int} $video
*/
function storeengine_get_gallery_video_html( array $video ): string {
$url = $video['url'];
// Direct video file (uploaded / media library / any .mp4 etc.). Wrap in the
// fixed-aspect container so the carousel cell has a stable height at init
// (a bare