| @@ -1,8 +1,14 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace UltimateStoreKit\Builder; |
| 4 | 4 | |
| 5 | +if (! defined('ABSPATH')) { | |
| 6 | + exit; // Exit if accessed directly | |
| 7 | +} | |
| 8 | + | |
| 9 | +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals -- BDTUSK_ / ultimate_store_kit_ / ultimate-store-kit- are this plugin's established public prefixes. | |
| 10 | + | |
| 5 | 11 | if (! defined('WPINC')) { |
| 6 | 12 | die; |
| 7 | 13 | } |
| 8 | 14 | |
| @@ -29,38 +35,16 @@ | ||
| 29 | 35 | add_filter("elementor/document/urls/wp_preview", [$this, 'change_preview_editor_url'], 999, 2); |
| 30 | 36 | |
| 31 | 37 | add_action('elementor/documents/register_controls', [$this, 'register_document_controls']); |
| 32 | 38 | |
| 33 | - // Add demo bypass filter for template preview | |
| 34 | - add_filter('ultimate_store_kit/preview/verified_bypass', function ($verify) { | |
| 35 | - // Check if we're in a demo environment or development site | |
| 36 | - // For demo sites, you might want to check domain names or other indicators | |
| 37 | - $demo_hosts = apply_filters('ultimate_store_kit/demo_hosts', [ | |
| 38 | - 'storekit.pro', | |
| 39 | - 'demo.storekit.pro', | |
| 40 | - 'localhost', | |
| 41 | - '127.0.0.1' | |
| 42 | - ]); | |
| 43 | - | |
| 44 | - $current_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field($_SERVER['HTTP_HOST']) : ''; | |
| 45 | - | |
| 46 | - foreach ($demo_hosts as $host) { | |
| 47 | - if (strpos($current_host, $host) !== false) { | |
| 48 | - return true; | |
| 49 | - } | |
| 50 | - } | |
| 51 | - | |
| 52 | - return $verify; | |
| 53 | - }); | |
| 54 | - | |
| 55 | - // Add filter to enable demo mode for preview URLs | |
| 56 | - add_filter('ultimate_store_kit/preview/use_demo_bypass', function ($use_demo) { | |
| 57 | - // Enable demo bypass in Elementor editor | |
| 58 | - if (isset($_GET['action']) && $_GET['action'] === 'elementor') { | |
| 59 | - return true; | |
| 60 | - } | |
| 61 | - return $use_demo; | |
| 62 | - }); | |
| 39 | + // The template preview used to accept the literal string "verified" in place | |
| 40 | + // of a nonce whenever $_SERVER['HTTP_HOST'] contained one of a list of demo | |
| 41 | + // hostnames. Because Host is attacker-controlled and the check was a substring | |
| 42 | + // match, any unauthenticated visitor could render the Elementor content of an | |
| 43 | + // arbitrary post id -- including drafts and private posts -- on any site whose | |
| 44 | + // Host reached PHP as localhost/127.0.0.1 (common behind a reverse proxy) or | |
| 45 | + // whose domain merely contained one of those strings. Preview is now gated on | |
| 46 | + // the real per-post nonce plus an edit_post capability check; see get_template_id(). | |
| 63 | 47 | } |
| 64 | 48 | |
| 65 | 49 | public function change_preview_editor_url($url, $document) { |
| 66 | 50 | $post_id = $document->get_main_id(); |
| @@ -104,15 +88,11 @@ | ||
| 104 | 88 | |
| 105 | 89 | if (!empty($product_tags) && !is_wp_error($product_tags)) { |
| 106 | 90 | $template_url = get_term_link($product_tags[0]); |
| 107 | 91 | } |
| 108 | - } | |
| 109 | - | |
| 110 | - elseif ($template_slug === 'shop' || $template_slug === 'archive') { | |
| 92 | + } elseif ($template_slug === 'shop' || $template_slug === 'archive') { | |
| 111 | 93 | $template_url = get_permalink(wc_get_page_id('shop')); |
| 112 | - } | |
| 113 | - | |
| 114 | - elseif ($template_slug === 'single' && $post_type === 'product') { | |
| 94 | + } elseif ($template_slug === 'single' && $post_type === 'product') { | |
| 115 | 95 | $page_settings_manager = \Elementor\Core\Settings\Manager::get_settings_managers('page'); |
| 116 | 96 | $page_settings_model = $page_settings_manager->get_model($post_id); |
| 117 | 97 | $sample_product = $page_settings_model->get_settings('usk_builder_sample_post_id'); |
| 118 | 98 | |
| @@ -118,23 +98,19 @@ | ||
| 118 | 98 | |
| 119 | 99 | $template_url = !empty($sample_product) ? |
| 120 | 100 | get_permalink($sample_product) : |
| 121 | 101 | $this->get_default_product_url(); |
| 122 | - } | |
| 123 | - | |
| 124 | - elseif ($template_slug === 'cart') { | |
| 102 | + } elseif ($template_slug === 'cart') { | |
| 125 | 103 | $template_url = wc_get_cart_url(); |
| 126 | - } | |
| 127 | - | |
| 128 | - elseif ($template_slug === 'checkout') { | |
| 104 | + } elseif ($template_slug === 'checkout') { | |
| 129 | 105 | $template_url = wc_get_checkout_url(); |
| 130 | - } | |
| 131 | - | |
| 132 | - elseif ($template_slug === 'myaccount' || strpos($template_slug, 'myaccount-') === 0) { | |
| 106 | + } elseif ( | |
| 107 | + $template_slug === 'myaccount' | |
| 108 | + || $template_slug === 'login' | |
| 109 | + || strpos($template_slug, 'myaccount-') === 0 | |
| 110 | + ) { | |
| 133 | 111 | $template_url = get_permalink(wc_get_page_id('myaccount')); |
| 134 | - } | |
| 135 | - | |
| 136 | - elseif ($template_slug === 'order-received') { | |
| 112 | + } elseif ($template_slug === 'order-received') { | |
| 137 | 113 | $orders = wc_get_orders(['limit' => 1]); |
| 138 | 114 | if (!empty($orders)) { |
| 139 | 115 | $order = $orders[0]; |
| 140 | 116 | $order_id = $order->get_id(); |
| @@ -145,15 +121,12 @@ | ||
| 145 | 121 | if (empty($template_url)) { |
| 146 | 122 | return $url; |
| 147 | 123 | } |
| 148 | 124 | |
| 149 | - $is_demo = apply_filters('ultimate_store_kit/preview/use_demo_bypass', false); | |
| 150 | - $nonce_value = $is_demo ? 'verified' : wp_create_nonce('template_preview_' . $post_id); | |
| 151 | - | |
| 152 | 125 | $param = [ |
| 153 | - 'usk_template_id' => $post_id, | |
| 154 | - 'preview_nonce' => $nonce_value, | |
| 155 | - 'preview' => true | |
| 126 | + 'ultimate_store_kit_template_id' => $post_id, | |
| 127 | + 'ultimate_store_kit_preview_nonce' => wp_create_nonce('ultimate_store_kit_template_preview_' . $post_id), | |
| 128 | + 'preview' => true | |
| 156 | 129 | ]; |
| 157 | 130 | |
| 158 | 131 | // Add parameters two URL |
| 159 | 132 | $url = add_query_arg($param, $template_url); |
| @@ -162,8 +135,9 @@ | ||
| 162 | 135 | } |
| 163 | 136 | |
| 164 | 137 | public function my_custom_fonts() { |
| 165 | 138 | if (is_admin() && Plugin::instance()->editor->is_edit_mode()) { |
| 139 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check on whether a builder template is open; only enqueues a style. | |
| 166 | 140 | if (isset($_REQUEST['usk-template'])) { |
| 167 | 141 | wp_register_style('usk-template-builder-hide-preview-btn-inline', false); // phpcs:ignore |
| 168 | 142 | wp_enqueue_style('usk-template-builder-hide-preview-btn-inline'); |
| 169 | 143 | wp_add_inline_style( |
| @@ -201,9 +175,9 @@ | ||
| 201 | 175 | return; |
| 202 | 176 | } |
| 203 | 177 | $meta = get_post_meta($post->ID); |
| 204 | 178 | |
| 205 | - $templateMeta = optional($meta)[Meta::TEMPLATE_TYPE]; | |
| 179 | + $templateMeta = ultimate_store_kit_optional($meta)[Meta::TEMPLATE_TYPE]; | |
| 206 | 180 | if (! isset($templateMeta[0])) { |
| 207 | 181 | return; |
| 208 | 182 | } |
| 209 | 183 | $postMeta = $templateMeta[0]; |
| @@ -283,29 +257,30 @@ | ||
| 283 | 257 | return $template; |
| 284 | 258 | } |
| 285 | 259 | } |
| 286 | 260 | |
| 287 | - | |
| 261 | + | |
| 288 | 262 | if (is_post_type_archive('product') || is_page(wc_get_page_id('shop')) || is_product_taxonomy()) { |
| 289 | 263 | $template_type = 'shop'; |
| 290 | - | |
| 264 | + | |
| 291 | 265 | if (is_tax('product_cat')) { |
| 292 | 266 | $template_type = 'category'; |
| 293 | 267 | } elseif (is_tax('product_tag')) { |
| 294 | 268 | $template_type = 'tag'; |
| 295 | 269 | } |
| 296 | - | |
| 270 | + | |
| 297 | 271 | if ($custom_template = $this->get_template_id($template_type)) { |
| 298 | 272 | $this->current_template_id = $custom_template; |
| 299 | 273 | return $this->getTemplatePath('woocommerce/archive-product', $template); |
| 300 | 274 | } |
| 301 | 275 | } |
| 302 | - | |
| 303 | 276 | |
| 304 | - if (is_cart()) { | |
| 305 | - if ($custom_template = $this->get_template_id('cart', 'product')) { | |
| 306 | - $this->current_template_id = $custom_template; | |
| 307 | - return $this->getTemplatePath('woocommerce/cart', $template); | |
| 277 | + | |
| 278 | + if ( is_cart() ) { | |
| 279 | + $cart_template = $this->resolve_cart_template( $template ); | |
| 280 | + | |
| 281 | + if ( $cart_template ) { | |
| 282 | + return $cart_template; | |
| 308 | 283 | } |
| 309 | 284 | } |
| 310 | 285 | |
| 311 | 286 | if (is_order_received_page()) { |
| @@ -337,9 +312,28 @@ | ||
| 337 | 312 | * Add wishlist endpoint |
| 338 | 313 | */ |
| 339 | 314 | $query_vars['wishlist'] = 'wishlist'; |
| 340 | 315 | |
| 341 | - if ($endpoint = array_intersect_key($wp->query_vars, $query_vars)) { | |
| 316 | + $endpoint_match = array_intersect_key($wp->query_vars, $query_vars); | |
| 317 | + | |
| 318 | + // Logged-out visitors landing on /my-account/ (no endpoint) see the | |
| 319 | + // single Login & Register template, which is responsible for both | |
| 320 | + // authentication flows. WooCommerce's own form-login.php renders | |
| 321 | + // the login and registration forms on the same URL, so one | |
| 322 | + // template covers both intents. | |
| 323 | + if (! is_user_logged_in() && empty($endpoint_match)) { | |
| 324 | + if ($custom_template = $this->get_template_id('login', 'account')) { | |
| 325 | + $this->current_template_id = $custom_template; | |
| 326 | + | |
| 327 | + if ($newTemplate = $this->getTemplatePath('woocommerce/login')) { | |
| 328 | + return $newTemplate; | |
| 329 | + } | |
| 330 | + | |
| 331 | + return $this->getTemplatePath('woocommerce/my-account', $template); | |
| 332 | + } | |
| 333 | + } | |
| 334 | + | |
| 335 | + if ($endpoint = $endpoint_match) { | |
| 342 | 336 | $endpoint = array_key_first($endpoint); |
| 343 | 337 | |
| 344 | 338 | if ($endpoint && $custom_template = $this->get_template_id($endpoint, 'account')) { |
| 345 | 339 | $this->current_template_id = $custom_template; |
| @@ -384,9 +378,9 @@ | ||
| 384 | 378 | return $this->getTemplatePath('home', $template); |
| 385 | 379 | } |
| 386 | 380 | } |
| 387 | 381 | |
| 388 | - if ($page_Id = intval(get_option('bdt_usk_compare_products_page_id'))) { | |
| 382 | + if ($page_Id = ultimate_store_kit_get_compare_page_option()) { | |
| 389 | 383 | if (is_page($page_Id)) { |
| 390 | 384 | if ($custom_template = $this->get_template_id('compare-products', 'product')) { |
| 391 | 385 | $this->current_template_id = $custom_template; |
| 392 | 386 | return $this->getTemplatePath('home', $template); |
| @@ -428,32 +422,26 @@ | ||
| 428 | 422 | if (null !== $this->current_template_id) { |
| 429 | 423 | return $this->current_template_id; |
| 430 | 424 | } |
| 431 | 425 | |
| 432 | - // Handle template preview from URL parameters | |
| 433 | - if (!empty($_GET['preview']) && !empty($_GET['usk_template_id']) && !empty($_GET['preview_nonce'])) { | |
| 434 | - $usk_template_id = sanitize_text_field(wp_unslash($_GET['usk_template_id'])); | |
| 435 | - $nonce = sanitize_text_field(wp_unslash($_GET['preview_nonce'])); | |
| 426 | + // Handle template preview from URL parameters. | |
| 427 | + if (!empty($_GET['preview']) && !empty($_GET['ultimate_store_kit_template_id']) && !empty($_GET['ultimate_store_kit_preview_nonce'])) { | |
| 428 | + $usk_template_id = absint(wp_unslash($_GET['ultimate_store_kit_template_id'])); | |
| 429 | + $nonce = sanitize_text_field(wp_unslash($_GET['ultimate_store_kit_preview_nonce'])); | |
| 436 | 430 | |
| 437 | - // Special handling for demo bypass | |
| 438 | - $is_demo_bypass = ($nonce === 'verified'); | |
| 439 | - $nonce_verified = $is_demo_bypass ? | |
| 440 | - apply_filters('ultimate_store_kit/preview/verified_bypass', false) : | |
| 441 | - wp_verify_nonce($nonce, 'template_preview_' . $usk_template_id); | |
| 431 | + // The nonce is bound to the specific template and to the user who | |
| 432 | + // generated it, and the capability check makes sure a leaked preview | |
| 433 | + // URL cannot be replayed by someone who may not edit the template. | |
| 434 | + $nonce_verified = $usk_template_id | |
| 435 | + && wp_verify_nonce($nonce, 'ultimate_store_kit_template_preview_' . $usk_template_id) | |
| 436 | + && current_user_can('edit_post', $usk_template_id); | |
| 442 | 437 | |
| 443 | - if ($nonce_verified) { | |
| 444 | - // For demo bypass mode, we don't need to check template type | |
| 445 | - if ($is_demo_bypass) { | |
| 446 | - $this->current_template_id = (int)$usk_template_id; | |
| 447 | - return $this->current_template_id; | |
| 448 | - } | |
| 449 | - | |
| 450 | - // For normal preview, check template type | |
| 438 | + if ($nonce_verified && get_post_type($usk_template_id) === Meta::POST_TYPE) { | |
| 451 | 439 | $template_type = get_post_meta($usk_template_id, Meta::TEMPLATE_TYPE, true); |
| 452 | 440 | if (!empty($template_type)) { |
| 453 | 441 | $template_data = explode(Builder_Template_Helper::separator(), $template_type); |
| 454 | 442 | if (count($template_data) >= 2 && $template_data[1] === $slug && ($postType === false || $template_data[0] === $postType)) { |
| 455 | - $this->current_template_id = (int)$usk_template_id; | |
| 443 | + $this->current_template_id = $usk_template_id; | |
| 456 | 444 | return $this->current_template_id; |
| 457 | 445 | } |
| 458 | 446 | } |
| 459 | 447 | } |
| @@ -489,8 +477,25 @@ | ||
| 489 | 477 | return $default; |
| 490 | 478 | } |
| 491 | 479 | |
| 492 | 480 | /** |
| 481 | + * Resolve the plugin cart template for WooCommerce cart requests. | |
| 482 | + * | |
| 483 | + * @param string $template Default WordPress template path. | |
| 484 | + * @return string|false | |
| 485 | + */ | |
| 486 | + protected function resolve_cart_template( $template ) { | |
| 487 | + if ( ! Cart_Render::is_available() ) { | |
| 488 | + return false; | |
| 489 | + } | |
| 490 | + | |
| 491 | + $custom_template = $this->get_template_id( 'cart', 'product' ); | |
| 492 | + $this->current_template_id = $custom_template ? absint( $custom_template ) : null; | |
| 493 | + | |
| 494 | + return $this->getTemplatePath( 'woocommerce/cart', $template ); | |
| 495 | + } | |
| 496 | + | |
| 497 | + /** | |
| 493 | 498 | * Get default product URL with proper error checking |
| 494 | 499 | * |
| 495 | 500 | * @return string |
| 496 | 501 | */ |
| @@ -495,9 +500,9 @@ | ||
| 495 | 500 | * @return string |
| 496 | 501 | */ |
| 497 | 502 | protected function get_default_product_url() { |
| 498 | 503 | $products = wc_get_products(['status' => 'publish', 'limit' => 1]); |
| 499 | - | |
| 504 | + | |
| 500 | 505 | if (!empty($products) && isset($products[0]) && is_object($products[0])) { |
| 501 | 506 | $product = $products[0]; |
| 502 | 507 | if (method_exists($product, 'get_id')) { |
| 503 | 508 | return get_permalink($product->get_id()); |
| @@ -502,9 +507,9 @@ | ||
| 502 | 507 | if (method_exists($product, 'get_id')) { |
| 503 | 508 | return get_permalink($product->get_id()); |
| 504 | 509 | } |
| 505 | 510 | } |
| 506 | - | |
| 511 | + | |
| 507 | 512 | // Fallback to shop page if no products found |
| 508 | 513 | return get_permalink(wc_get_page_id('shop')); |
| 509 | 514 | } |
| 510 | 515 | } |