plugin_name = $plugin_name;
$this->version = $version;
add_action( 'wp_ajax_3dweb_ps_action_get_endpoint', array( self::class, 'dweb_ps_handle_get_endpoint_request' ) );
add_action( 'wp_ajax_nopriv_3dweb_ps_action_get_endpoint', array( self::class, 'dweb_ps_handle_get_endpoint_request' ) );
add_action( 'wp_ajax_3dweb_ps_public_download_design', array( $this, 'dweb_ps_handle_public_design_download' ) );
add_action( 'wp_ajax_nopriv_3dweb_ps_public_download_design', array( $this, 'dweb_ps_handle_public_design_download' ) );
}
/**
* Generates a temporary DOM identifier.
*
* @return string
*/
protected function generate_id() {
$permitted_chars = implode( '', range( 'a', 'z' ) );
return 'cnf--' . substr( str_shuffle( $permitted_chars ), 0, 10 );
}
/**
* Register the JavaScript for the public-facing side of the site.
*
* @since 1.5.0
*/
protected function load_scripts() {
wp_enqueue_script(
$this->plugin_name . '_public_core',
plugin_dir_url( __DIR__ ) . 'js/3dweb-ps-public.js',
array( 'jquery' ),
$this->version,
true
);
wp_enqueue_script(
$this->plugin_name . '_public_flexslider',
plugin_dir_url( __DIR__ ) . 'js/sliders/3dweb-ps-flexslider.js',
array( 'jquery' ),
$this->version,
true
);
$this->dweb_ps_load_extra_scripts();
}
/**
* Enqueues theme-specific styles.
*
* @return void
*/
public function dweb_ps_enqueue_styles() {
$this->dweb_ps_load_extra_styles();
}
/**
* Enqueues public scripts and localized session data.
*
* @return void
*/
public function dweb_ps_enqueue_scripts() {
global $post;
$this->load_scripts();
if ( isset( $post ) && is_object( $post ) && isset( $post->ID ) ) {
$post_id = (int) $post->ID;
} else {
$post_id = (int) get_queried_object_id();
}
$product_sku = '';
if ( $post_id > 0 ) {
$product_sku = sanitize_meta(
DWeb_PS_WOO_METABOX::FIELD_PRODUCT_SKU,
get_post_meta( $post_id, DWeb_PS_WOO_METABOX::FIELD_PRODUCT_SKU, true ),
'post'
);
}
if ( empty( $product_sku ) ) {
return;
}
$assets = array();
$team_reference = $this->get_team_reference();
if ( $team_reference ) {
$response = ( new DWeb_PS_API() )->dweb_ps_get_session_assets( $team_reference );
if ( $response && ! isset( $response['error'] ) && ! isset( $response['errors'] ) && isset( $response['data'] ) ) {
$assets = $response['data'];
}
}
// Hook for the endpoint request.
wp_localize_script(
$this->plugin_name . '_woo_js',
'dwebPsConfig',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'security' => wp_create_nonce( '3dweb_ps-nonce' ),
'action' => '3dweb_ps_action_get_endpoint',
'product_id' => $post_id,
'product_sku' => $product_sku,
'assets' => $assets,
'team_reference' => $this->get_team_reference(),
'team_reference_key' => self::TEAM_SESSION_REFERENCE,
'showDesignLink' => (bool) get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_SHOW_SESSION_DESIGN_LINK, true ),
'useThreeSixtyView' => false,
'debug' => get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_DEBUG, false ),
'threeSixtyConfig' => array(),
'translations' => array(
'startConfiguration' => get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_START_BUTTON_TEXT, __( 'Start configuration', '3dweb-print-studio' ) ),
'loading' => __( 'Loading ...', '3dweb-print-studio' ),
'sessionClosed' => get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_SESSION_CLOSED_TEXT, 'Design: {reference}' ),
'galleryLoading' => get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_GALLERY_LOADING_TEXT, __( 'Preparing your preview...', '3dweb-print-studio' ) ),
'sessionLoading' => get_option( DWeb_PS_ADMIN_OPTIONS::CONFIGURATOR_SESSION_LOADING_TEXT, __( 'Opening configurator...', '3dweb-print-studio' ) ),
),
)
);
}
/**
* Handles the public AJAX endpoint requests.
*
* @return void
*/
public static function dweb_ps_handle_get_endpoint_request() {
check_ajax_referer( '3dweb_ps-nonce', 'security' );
$method = isset( $_POST['method'] ) ? sanitize_text_field( wp_unslash( $_POST['method'] ) ) : '';
switch ( $method ) {
case 'get_session':
$product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
$product_sku = isset( $_POST['product_sku'] )
? sanitize_text_field( wp_unslash( $_POST['product_sku'] ) )
: '';
$callback_url = isset( $_POST['post_url'] ) ? esc_url_raw( wp_unslash( $_POST['post_url'] ) ) : '';
if ( $product_id <= 0 ) {
wp_send_json_error( array( 'message' => 'Invalid product ID.' ), 400 );
}
if ( get_post_type( $product_id ) !== 'product' ) {
wp_send_json_error( array( 'message' => 'Invalid product.' ), 400 );
}
if ( ! wc_get_product( $product_id ) ) {
wp_send_json_error( array( 'message' => 'Product not found.' ), 400 );
}
if ( '' === $product_sku ) {
wp_send_json_error( array( 'message' => 'Missing product SKU.' ), 400 );
}
if ( '' === $callback_url ) {
wp_send_json_error( array( 'message' => 'Missing callback URL.' ), 400 );
}
// TODO: Build the callback URL server-side instead of accepting it from client input.
$validator = new self( '3dweb_ps', DWEB_PS_VERSION );
if ( ! $validator->is_allowed_callback_url( $callback_url ) ) {
wp_send_json_error( array( 'message' => 'Invalid callback URL.' ), 400 );
}
$stored_product_sku = get_post_meta( $product_id, DWeb_PS_WOO_METABOX::FIELD_PRODUCT_SKU, true );
$stored_product_sku = is_string( $stored_product_sku ) ? sanitize_text_field( $stored_product_sku ) : '';
if ( '' === $stored_product_sku || ! hash_equals( $stored_product_sku, $product_sku ) ) {
wp_send_json_error( array( 'message' => 'Product SKU does not match the selected product.' ), 400 );
}
$callback_url = add_query_arg(
self::TEAM_SESSION_NONCE_KEY,
wp_create_nonce( self::TEAM_SESSION_NONCE_ACTION ),
$callback_url
);
$response = ( new DWeb_PS_API() )->dweb_ps_create_new_session( $product_sku, $callback_url );
break;
case 'get_assets':
$self = new self( '3dweb_ps', '1.0.0' );
$self->load_session_urls();
$response = $self->main_images;
break;
default:
$response = false;
break;
}
if ( false === $response || isset( $response['error'] ) || isset( $response['errors'] ) ) {
$error_message = 'Something went wrong';
if ( is_array( $response ) ) {
if ( isset( $response['message'] ) ) {
$error_message = $response['message'];
} elseif ( isset( $response['error'] ) ) {
$error_message = $response['error'];
} elseif ( isset( $response['errors'] ) ) {
// Handle multiple errors if returned by the API.
if ( is_array( $response['errors'] ) ) {
$error_message = implode(
', ',
array_map(
function ( $err ) {
return is_array( $err ) ? implode( ': ', $err ) : $err;
},
$response['errors']
)
);
} else {
$error_message = $response['errors'];
}
}
}
wp_send_json_error( array( 'message' => $error_message ) );
}
wp_send_json_success( $response );
}
/**
* Gets the team reference from the current request.
*
* @return string|null
*/
private function get_team_reference() {
$raw_team_reference = filter_input( INPUT_GET, self::TEAM_SESSION_REFERENCE, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! is_string( $raw_team_reference ) || '' === $raw_team_reference ) {
return null;
}
$nonce = $this->get_team_reference_nonce_from_request( INPUT_GET );
if ( ! $this->is_valid_team_reference_nonce( $nonce ) ) {
return null;
}
return $this->sanitize_team_reference( wp_unslash( $raw_team_reference ) );
}
/**
* Outputs the hidden configurator reference field.
*
* @return void
*/
public function dweb_ps_handle_add_custom_hidden_field() {
$team_reference = $this->get_team_reference();
if ( $team_reference ) {
printf(
'',
esc_attr( self::TEAM_SESSION_REFERENCE ),
esc_attr( $team_reference )
);
printf(
'',
esc_attr( self::TEAM_SESSION_NONCE_KEY ),
esc_attr( wp_create_nonce( self::TEAM_SESSION_NONCE_ACTION ) )
);
}
}
/**
* Stores the configurator reference on cart items.
*
* @param array $cart_item_data Cart item data.
* @param int $product_id Product ID.
* @return array
*/
public function dweb_ps_handle_add_to_cart_item( $cart_item_data, $product_id ) {
if ( $product_id ) {
$nonce = $this->get_team_reference_nonce_from_request( INPUT_POST );
if ( ! $this->is_valid_team_reference_nonce( $nonce ) ) {
return $cart_item_data;
}
$raw_team_reference = filter_input( INPUT_POST, self::TEAM_SESSION_REFERENCE, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$team_reference = is_string( $raw_team_reference )
? $this->sanitize_team_reference( wp_unslash( $raw_team_reference ) )
: null;
if ( null !== $team_reference ) {
$cart_item_data[ self::TEAM_SESSION_REFERENCE ] = $team_reference;
}
}
return $cart_item_data;
}
/**
* Redirects back to the source page after add to cart.
*
* @param string $url Fallback redirect URL.
* @return string
*/
public function dweb_ps_handle_add_to_cart_redirect( $url ) {
$redirect_url = wp_get_referer();
if ( ! $redirect_url ) {
$redirect_url = $url;
}
if ( ! $redirect_url ) {
$redirect_url = home_url( '/' );
}
$team_reference = null;
$team_nonce = null;
$redirect_query = wp_parse_url( $redirect_url, PHP_URL_QUERY );
if ( is_string( $redirect_query ) ) {
parse_str( $redirect_query, $query_args );
if ( isset( $query_args[ self::TEAM_SESSION_REFERENCE ] ) ) {
$team_reference = $this->sanitize_team_reference( $query_args[ self::TEAM_SESSION_REFERENCE ] );
}
if ( isset( $query_args[ self::TEAM_SESSION_NONCE_KEY ] ) ) {
$team_nonce = sanitize_text_field( (string) $query_args[ self::TEAM_SESSION_NONCE_KEY ] );
}
}
if ( null === $team_reference || ! $this->is_valid_team_reference_nonce( $team_nonce ) ) {
return $redirect_url;
}
$redirect_url = remove_query_arg( array( 'add-to-cart', 'added-to-cart', 'quantity' ), $redirect_url );
return add_query_arg(
array(
self::TEAM_SESSION_REFERENCE => $team_reference,
self::TEAM_SESSION_NONCE_KEY => $team_nonce,
),
$redirect_url
);
}
/**
* Gets the team reference nonce from request input.
*
* @param int $input_type Request input type.
* @return string
*/
private function get_team_reference_nonce_from_request( $input_type ) {
$raw_nonce = filter_input( $input_type, self::TEAM_SESSION_NONCE_KEY, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! is_string( $raw_nonce ) || '' === $raw_nonce ) {
return '';
}
return sanitize_text_field( wp_unslash( $raw_nonce ) );
}
/**
* Validates the team reference nonce value.
*
* @param string $nonce Nonce.
* @return bool
*/
private function is_valid_team_reference_nonce( $nonce ) {
if ( ! is_string( $nonce ) || '' === $nonce ) {
return false;
}
return (bool) wp_verify_nonce( $nonce, self::TEAM_SESSION_NONCE_ACTION );
}
/**
* Adds the configurator reference to displayed cart item data.
*
* @param array $item_data Cart item display data.
* @param array $cart_item Cart item values.
* @return array
*/
public function dweb_ps_handle_get_item_data( $item_data, $cart_item ) {
$team_reference = isset( $cart_item[ self::TEAM_SESSION_REFERENCE ] )
? $this->sanitize_team_reference( $cart_item[ self::TEAM_SESSION_REFERENCE ] )
: null;
if ( null !== $team_reference ) {
$item_data[] = array(
'key' => 'reference',
'value' => wc_clean( $team_reference ),
);
}
return $item_data;
}
/**
* Persists configurator metadata on order items.
*
* @param WC_Order_Item_Product $item Order item.
* @param string $cart_item_key Cart item key.
* @param array $values Cart item values.
* @param WC_Order $order Order object.
* @return WC_Order_Item_Product
*/
public function dweb_ps_handle_create_order_line_item( $item, $cart_item_key, $values, $order ) {
unset( $order, $cart_item_key );
if ( isset( $values[ self::TEAM_SESSION_REFERENCE ] ) ) {
$team_reference = $this->sanitize_team_reference( $values[ self::TEAM_SESSION_REFERENCE ] );
if ( null === $team_reference ) {
return $item;
}
$item->add_meta_data( self::TEAM_SESSION_REFERENCE, $team_reference, true );
$design_url = $this->get_session_design_url( $team_reference );
if ( $design_url ) {
$item->add_meta_data( 'design', $design_url, true );
}
}
return $item;
}
/**
* Replaces the cart image with the generated session image.
*
* @param string $image Original image HTML.
* @param array $cart_item Cart item values.
* @param string $cart_item_key Cart item key.
* @return string
*/
public function dweb_ps_handle_change_cart_image( $image, $cart_item, $cart_item_key ) {
unset( $cart_item_key );
$custom_image_url = $this->get_cart_item_custom_image_url( $cart_item );
if ( ! $custom_image_url ) {
return $image;
}
$alt_text = '';
if ( isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ) {
$alt_text = $cart_item['data']->get_name();
}
return sprintf(
'
',
esc_url( $custom_image_url ),
esc_attr( $alt_text )
);
}
/**
* Replaces Store API cart images with the generated session image.
*
* @param array $product_images Product image objects.
* @param array $cart_item Cart item values.
* @param string $cart_item_key Cart item key.
* @return array
*/
public function dweb_ps_handle_store_api_cart_item_images( $product_images, $cart_item, $cart_item_key ) {
unset( $cart_item_key );
$custom_image_url = $this->get_cart_item_custom_image_url( $cart_item );
if ( ! $custom_image_url ) {
return $product_images;
}
$base_image = null;
if ( is_array( $product_images ) && ! empty( $product_images ) && is_object( $product_images[0] ) ) {
$base_image = $product_images[0];
}
$image = (object) array(
'id' => isset( $base_image->id ) ? (int) $base_image->id : (int) ( $cart_item['product_id'] ?? 0 ),
'src' => $custom_image_url,
'thumbnail' => $custom_image_url,
'srcset' => isset( $base_image->srcset ) ? (string) $base_image->srcset : '',
'sizes' => isset( $base_image->sizes ) ? (string) $base_image->sizes : '',
'name' => isset( $base_image->name ) ? (string) $base_image->name : '',
'alt' => isset( $base_image->alt ) ? (string) $base_image->alt : '',
);
return array( $image );
}
/**
* Replaces admin order thumbnails with the generated session image.
*
* @param string $thumbnail Original thumbnail HTML.
* @param int $item_id Order item ID.
* @param WC_Order_Item_Product $item Order item.
* @return string
*/
public function dweb_ps_handle_admin_order_item_thumbnail( $thumbnail, $item_id, $item ) {
if ( ! $item || ! is_a( $item, 'WC_Order_Item_Product' ) ) {
return $thumbnail;
}
$team_reference = $this->sanitize_team_reference( $item->get_meta( self::TEAM_SESSION_REFERENCE, true ) );
if ( null === $team_reference ) {
return $thumbnail;
}
$custom_image_url = $this->get_session_main_image_url( $team_reference );
if ( ! $custom_image_url ) {
return $thumbnail;
}
$alt_text = $item->get_name();
return sprintf(
'
',
esc_url( $custom_image_url ),
esc_attr( $alt_text )
);
}
/**
* Formats displayed order item meta values.
*
* @param string $display_value Display value.
* @param object $meta Meta object.
* @param mixed $item Order item.
* @return string
*/
public function dweb_ps_handle_order_item_display_meta_value( $display_value, $meta, $item ) {
if ( ! is_object( $meta ) || ! isset( $meta->key ) || 'design' !== $meta->key ) {
return $display_value;
}
if ( ! $item || ! is_a( $item, 'WC_Order_Item_Product' ) ) {
return $display_value;
}
if ( ! is_string( $display_value ) || ! filter_var( $display_value, FILTER_VALIDATE_URL ) ) {
return $display_value;
}
return $this->build_design_actions_html( $display_value, is_admin(), $item );
}
/**
* Formats order item meta labels and injects the design link metadata.
*
* @param array $formatted_meta Formatted meta array.
* @param mixed $item Order item.
* @return array
*/
public function dweb_ps_handle_order_item_formatted_meta_data( $formatted_meta, $item ) {
if ( ! $item || ! is_a( $item, 'WC_Order_Item_Product' ) ) {
return $formatted_meta;
}
$team_reference = $this->sanitize_team_reference( $item->get_meta( self::TEAM_SESSION_REFERENCE, true ) );
if ( null === $team_reference ) {
return $formatted_meta;
}
$team_reference_label = apply_filters(
'dweb_ps_team_reference_label',
__( 'Reference', '3dweb-print-studio' ),
$item
);
foreach ( $formatted_meta as $meta_key => $meta ) {
if ( isset( $meta->key ) && self::TEAM_SESSION_REFERENCE === $meta->key ) {
$formatted_meta[ $meta_key ]->display_key = $team_reference_label;
}
}
foreach ( $formatted_meta as $meta ) {
if ( isset( $meta->key ) && 'design' === $meta->key ) {
return $formatted_meta;
}
}
$design_url = $item->get_meta( 'design', true );
if ( ! $design_url ) {
$design_url = $this->get_session_design_url( $team_reference );
}
if ( ! $design_url ) {
return $formatted_meta;
}
$display_value = $this->build_design_actions_html( $design_url, is_admin(), $item );
$formatted_meta['cnf_design'] = (object) array(
'key' => 'design',
'value' => $design_url,
'display_key' => 'design',
'display_value' => $display_value,
);
return $formatted_meta;
}
/**
* Streams a design download for administrators.
*
* @return void
*/
public function dweb_ps_handle_admin_design_download() {
// phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce registers this capability.
if ( ! current_user_can( 'edit_shop_orders' ) ) {
wp_die( 'Forbidden', 403 );
}
$raw_nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$nonce = is_string( $raw_nonce ) ? sanitize_text_field( wp_unslash( $raw_nonce ) ) : '';
if ( ! wp_verify_nonce( $nonce, '3dweb_ps_download_design' ) ) {
wp_die( 'Invalid nonce', 403 );
}
$raw_design_url = filter_input( INPUT_GET, 'url', FILTER_SANITIZE_URL );
$design_url = is_string( $raw_design_url ) ? esc_url_raw( wp_unslash( $raw_design_url ) ) : '';
if (
! $design_url ||
! wp_http_validate_url( $design_url ) ||
! $this->is_allowed_design_download_url( $design_url )
) {
wp_die( 'Invalid design URL', 400 );
}
$this->stream_design_download( $design_url );
}
/**
* Streams a design download for public order links.
*
* @return void
*/
public function dweb_ps_handle_public_design_download() {
$raw_order_id = filter_input( INPUT_GET, 'order_id', FILTER_SANITIZE_NUMBER_INT );
$order_id = is_string( $raw_order_id ) ? absint( wp_unslash( $raw_order_id ) ) : 0;
$raw_order_key = filter_input( INPUT_GET, 'order_key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$order_key = is_string( $raw_order_key ) ? sanitize_text_field( wp_unslash( $raw_order_key ) ) : '';
$raw_nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$nonce = is_string( $raw_nonce ) ? sanitize_text_field( wp_unslash( $raw_nonce ) ) : '';
$raw_design_url = filter_input( INPUT_GET, 'url', FILTER_SANITIZE_URL );
$design_url = is_string( $raw_design_url ) ? esc_url_raw( wp_unslash( $raw_design_url ) ) : '';
if ( ! $order_id || ! $order_key || ! $nonce || ! wp_verify_nonce( $nonce, '3dweb_ps_public_download_design_' . $order_id ) ) {
wp_die( 'Invalid request', 403 );
}
$order = wc_get_order( $order_id );
if ( ! $order || ! hash_equals( (string) $order->get_order_key(), (string) $order_key ) ) {
wp_die( 'Invalid order', 403 );
}
if (
! $design_url ||
! wp_http_validate_url( $design_url ) ||
! $this->is_allowed_design_download_url( $design_url )
) {
wp_die( 'Invalid design URL', 400 );
}
$this->stream_design_download( $design_url );
}
/**
* Validates whether a design download URL is allowed.
*
* @param string $url Design URL.
* @return bool
*/
private function is_allowed_design_download_url( $url ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( ! $host ) {
return false;
}
$host = strtolower( $host );
$allowed_hosts = array();
$configurator_host = ( new DWeb_PS_API() )->dweb_ps_get_configurator_host();
$configurator_parsed_host = wp_parse_url( $configurator_host, PHP_URL_HOST );
if ( $configurator_parsed_host ) {
$allowed_hosts[] = strtolower( $configurator_parsed_host );
}
$allowed_suffixes = array(
'.3dweb.io',
);
if ( in_array( $host, $allowed_hosts, true ) ) {
return true;
}
foreach ( $allowed_suffixes as $suffix ) {
if ( 0 === substr_compare( $host, $suffix, -strlen( $suffix ) ) ) {
return true;
}
}
return false;
}
/**
* Validates whether a callback URL points to the current site.
*
* @param string $url Callback URL.
* @return bool
*/
private function is_allowed_callback_url( $url ) {
if ( ! wp_http_validate_url( $url ) ) {
return false;
}
$callback_host = wp_parse_url( $url, PHP_URL_HOST );
$site_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
if ( ! $callback_host || ! $site_host ) {
return false;
}
return strtolower( $callback_host ) === strtolower( $site_host );
}
/**
* Gets the generated image URL for a cart item.
*
* @param array $cart_item Cart item values.
* @return string|null
*/
private function get_cart_item_custom_image_url( $cart_item ) {
if ( ! is_array( $cart_item ) || empty( $cart_item[ self::TEAM_SESSION_REFERENCE ] ) ) {
return null;
}
$team_reference = $this->sanitize_team_reference( $cart_item[ self::TEAM_SESSION_REFERENCE ] );
if ( null === $team_reference ) {
return null;
}
return $this->get_session_main_image_url( $team_reference );
}
/**
* Gets cached session assets for a team reference.
*
* @param string $team_reference Team reference.
* @return array|null
*/
private function get_session_assets_by_reference( $team_reference ) {
$team_reference = $this->sanitize_team_reference( $team_reference );
if ( null === $team_reference ) {
return null;
}
if ( isset( $this->session_assets_cache[ $team_reference ] ) ) {
return $this->session_assets_cache[ $team_reference ];
}
$response = ( new DWeb_PS_API() )->dweb_ps_get_session_assets( $team_reference );
if ( ! $response || isset( $response['error'] ) || isset( $response['errors'] ) || ! isset( $response['data'] ) || ! is_array( $response['data'] ) ) {
$this->session_assets_cache[ $team_reference ] = null;
return null;
}
$this->session_assets_cache[ $team_reference ] = $response['data'];
return $this->session_assets_cache[ $team_reference ];
}
/**
* Sanitizes a team reference value.
*
* @param mixed $value Raw value.
* @return string|null
*/
private function sanitize_team_reference( $value ) {
if ( ! is_scalar( $value ) ) {
return null;
}
$team_reference = sanitize_text_field( (string) $value );
// Support both observed 3DWeb session reference lengths.
if ( '' === $team_reference || ! preg_match( '/^[a-f0-9]{13,14}$/', $team_reference ) ) {
return null;
}
return $team_reference;
}
/**
* Gets the main generated image URL for a session.
*
* @param string $team_reference Team reference.
* @return string|null
*/
private function get_session_main_image_url( $team_reference ) {
$assets = $this->get_session_assets_by_reference( $team_reference );
if ( ! $assets || ! isset( $assets['main_0'] ) ) {
return null;
}
$main_image = $assets['main_0'];
if ( is_array( $main_image ) && ! empty( $main_image['url'] ) ) {
return esc_url_raw( $main_image['url'] );
}
if ( is_string( $main_image ) && filter_var( $main_image, FILTER_VALIDATE_URL ) ) {
return esc_url_raw( $main_image );
}
return null;
}
/**
* Gets the generated design URL for a session.
*
* @param string $team_reference Team reference.
* @return string|null
*/
private function get_session_design_url( $team_reference ) {
$assets = $this->get_session_assets_by_reference( $team_reference );
if ( ! $assets || ! isset( $assets['design'] ) || ! is_array( $assets['design'] ) || empty( $assets['design'] ) ) {
return null;
}
$first_design = reset( $assets['design'] );
if ( is_string( $first_design ) && filter_var( $first_design, FILTER_VALIDATE_URL ) ) {
return esc_url_raw( $first_design );
}
if ( is_array( $first_design ) && ! empty( $first_design['url'] ) && filter_var( $first_design['url'], FILTER_VALIDATE_URL ) ) {
return esc_url_raw( $first_design['url'] );
}
return null;
}
/**
* Builds the HTML for design open/download actions.
*
* @param string $url Design URL.
* @param bool $is_admin Whether the current context is admin.
* @param mixed|null $item Optional order item.
* @return string
*/
private function build_design_actions_html( $url, $is_admin = false, $item = null ) {
$safe_url = esc_url( $url );
if ( $is_admin ) {
$download_url = add_query_arg(
array(
'action' => '3dweb_ps_download_design',
'url' => $url,
'_wpnonce' => wp_create_nonce( '3dweb_ps_download_design' ),
),
admin_url( 'admin-ajax.php' )
);
return sprintf(
'Open design / Download',
$safe_url,
esc_url( $download_url )
);
}
$download_url = $safe_url;
if ( $item && is_a( $item, 'WC_Order_Item_Product' ) ) {
$order_id = (int) $item->get_order_id();
$order = $order_id ? wc_get_order( $order_id ) : null;
if ( $order ) {
$download_url = add_query_arg(
array(
'action' => '3dweb_ps_public_download_design',
'url' => $url,
'order_id' => $order_id,
'order_key' => $order->get_order_key(),
'_wpnonce' => wp_create_nonce( '3dweb_ps_public_download_design_' . $order_id ),
),
admin_url( 'admin-ajax.php' )
);
}
}
return sprintf(
'Open design / Download',
$safe_url,
esc_url( $download_url )
);
}
/**
* Streams the design file to the browser.
*
* @param string $design_url Design URL.
* @return void
*/
private function stream_design_download( $design_url ) {
$response = wp_safe_remote_get(
$design_url,
array(
'timeout' => 30,
'redirection' => 5,
)
);
if ( is_wp_error( $response ) ) {
wp_die( 'Download failed', 502 );
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( $status_code < 200 || $status_code >= 300 ) {
wp_die( 'Design unavailable', 502 );
}
$body = wp_remote_retrieve_body( $response );
if ( '' === $body ) {
wp_die( 'Empty design file', 502 );
}
$path = wp_parse_url( $design_url, PHP_URL_PATH );
$filename = $path ? wp_basename( $path ) : 'design.png';
if ( ! $filename || false === strpos( $filename, '.' ) ) {
$filename = 'design.png';
}
$filename = sanitize_file_name( $filename );
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
if ( ! $content_type ) {
$content_type = 'application/octet-stream';
}
nocache_headers();
header( 'Content-Description: File Transfer' );
header( 'Content-Type: ' . $content_type );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $body ) );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary file stream output.
echo $body;
exit;
}
/**
* Loads session image URLs for the current team reference.
*
* @return void
*/
private function load_session_urls() {
$team_reference = $this->get_team_reference();
if ( $team_reference && ! $this->session_images_loaded ) {
$response = ( new DWeb_PS_API() )->dweb_ps_get_session_assets( $team_reference );
if (
! $response ||
isset( $response['error'] ) ||
isset( $response['errors'] ) ||
! isset( $response['data'] ) ||
! is_array( $response['data'] )
) {
return;
}
$this->session_images_loaded = true;
$this->main_images = array();
foreach ( array( 'main_0', 'main_90', 'main_180', 'main_270' ) as $key ) {
if ( isset( $response['data'][ $key ] ) ) {
$this->main_images[] = $response['data'][ $key ];
}
}
}
}
/**
* Replaces main gallery image params with generated session images.
*
* @param array $params Image params.
* @param int $attachment_id Attachment ID.
* @return array
*/
public function dweb_ps_handle_image_params( $params, $attachment_id ) {
$product = wc_get_product();
if ( ! $product ) {
return $params;
}
if ( ! is_array( $params ) ) {
return $params;
}
$this->load_session_urls();
if ( empty( $this->main_images ) ) {
return $params;
}
$gallery = $product->get_gallery_image_ids();
$attachment_product_id = (int) get_post_meta( $attachment_id, '_product_id', true );
$current_product_id = (int) get_the_ID();
// Ensure the product ID matches the current attachment product.
if ( $product->get_id() !== $attachment_product_id || $product->get_id() !== $current_product_id ) {
return $params;
}
// Add the main product ID to the gallery.
array_unshift( $gallery, $product->get_image_id() );
// Find the matching attachment in the gallery.
$index = array_search( $attachment_id, $gallery, true );
if ( false === $index ) {
return $params;
}
$old_url = $params['data-src'];
// Use a temporary placeholder image.
$polled_url = null;
if ( false !== $index && isset( $this->main_images[ $index ] ) ) {
$polled_url = $this->main_images[ $index ];
}
$url = plugin_dir_url( __DIR__ ) . 'img/wait-placeholder.svg';
// Replace the main source with the placeholder image.
$params['src'] = $this->add_height_to_image_url( $url, 302 );
// Replace related responsive attributes.
$params['srcset'] = $this->add_height_to_image_url( $url, 600, 600 );
// Change the thumbnail source.
$params['data-src'] = $this->add_height_to_image_url( $url, 210 );
$params['data-large_image'] = $url;
$params['data-cnf3dweb_url'] = $polled_url;
$params['data-cnf3dweb_url_old'] = $old_url;
return $params;
}
/**
* Replaces thumbnail image params with generated session images.
*
* @param array $image Image data.
* @param int $attachment_id Attachment ID.
* @param mixed $size Requested size.
* @param bool $icon Whether the image is an icon.
* @return array
*/
public function dweb_ps_handle_image_params_thumb( $image, $attachment_id, $size, $icon ) {
unset( $size, $icon );
if ( ! $this->get_team_reference() ) {
return $image;
}
$product = wc_get_product();
if ( ! $product ) {
return $image;
}
$attachment_product_id = (int) get_post_meta( $attachment_id, '_product_id', true );
// Ensure the product ID matches the current attachment product.
if ( $product->get_id() !== $attachment_product_id ) {
return $image;
}
$gallery = $product->get_gallery_image_ids();
// Add the main product ID to the gallery.
array_unshift( $gallery, $product->get_image_id() );
// Find the matching attachment in the gallery.
$index = array_search( $attachment_id, $gallery, true );
if ( false === $index ) {
return $image;
}
$image[0] = plugin_dir_url( __DIR__ ) . 'img/wait-placeholder.svg';
return $image;
}
/**
* Adds width and height query args to an image URL.
*
* @param string $url Image URL.
* @param int $height Image height.
* @param int|null $width Image width.
* @return string
*/
private function add_height_to_image_url( $url, $height, $width = null ) {
if ( $width ) {
return add_query_arg(
array(
'h' => $height,
'w' => $width,
'mode' => 'fill',
),
$url
);
}
return add_query_arg(
array(
'h' => $height,
),
$url
);
}
}