| @@ -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,25 +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' | |
| 106 | + } elseif ( | |
| 107 | + $template_slug === 'myaccount' | |
| 133 | 108 | || $template_slug === 'login' |
| 134 | - || strpos($template_slug, 'myaccount-') === 0) { | |
| 109 | + || strpos($template_slug, 'myaccount-') === 0 | |
| 110 | + ) { | |
| 135 | 111 | $template_url = get_permalink(wc_get_page_id('myaccount')); |
| 136 | - } | |
| 137 | - | |
| 138 | - elseif ($template_slug === 'order-received') { | |
| 112 | + } elseif ($template_slug === 'order-received') { | |
| 139 | 113 | $orders = wc_get_orders(['limit' => 1]); |
| 140 | 114 | if (!empty($orders)) { |
| 141 | 115 | $order = $orders[0]; |
| 142 | 116 | $order_id = $order->get_id(); |
| @@ -147,15 +121,12 @@ | ||
| 147 | 121 | if (empty($template_url)) { |
| 148 | 122 | return $url; |
| 149 | 123 | } |
| 150 | 124 | |
| 151 | - $is_demo = apply_filters('ultimate_store_kit/preview/use_demo_bypass', false); | |
| 152 | - $nonce_value = $is_demo ? 'verified' : wp_create_nonce('template_preview_' . $post_id); | |
| 153 | - | |
| 154 | 125 | $param = [ |
| 155 | - 'usk_template_id' => $post_id, | |
| 156 | - 'preview_nonce' => $nonce_value, | |
| 157 | - '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 | |
| 158 | 129 | ]; |
| 159 | 130 | |
| 160 | 131 | // Add parameters two URL |
| 161 | 132 | $url = add_query_arg($param, $template_url); |
| @@ -164,8 +135,9 @@ | ||
| 164 | 135 | } |
| 165 | 136 | |
| 166 | 137 | public function my_custom_fonts() { |
| 167 | 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. | |
| 168 | 140 | if (isset($_REQUEST['usk-template'])) { |
| 169 | 141 | wp_register_style('usk-template-builder-hide-preview-btn-inline', false); // phpcs:ignore |
| 170 | 142 | wp_enqueue_style('usk-template-builder-hide-preview-btn-inline'); |
| 171 | 143 | wp_add_inline_style( |
| @@ -203,9 +175,9 @@ | ||
| 203 | 175 | return; |
| 204 | 176 | } |
| 205 | 177 | $meta = get_post_meta($post->ID); |
| 206 | 178 | |
| 207 | - $templateMeta = optional($meta)[Meta::TEMPLATE_TYPE]; | |
| 179 | + $templateMeta = ultimate_store_kit_optional($meta)[Meta::TEMPLATE_TYPE]; | |
| 208 | 180 | if (! isset($templateMeta[0])) { |
| 209 | 181 | return; |
| 210 | 182 | } |
| 211 | 183 | $postMeta = $templateMeta[0]; |
| @@ -285,29 +257,30 @@ | ||
| 285 | 257 | return $template; |
| 286 | 258 | } |
| 287 | 259 | } |
| 288 | 260 | |
| 289 | - | |
| 261 | + | |
| 290 | 262 | if (is_post_type_archive('product') || is_page(wc_get_page_id('shop')) || is_product_taxonomy()) { |
| 291 | 263 | $template_type = 'shop'; |
| 292 | - | |
| 264 | + | |
| 293 | 265 | if (is_tax('product_cat')) { |
| 294 | 266 | $template_type = 'category'; |
| 295 | 267 | } elseif (is_tax('product_tag')) { |
| 296 | 268 | $template_type = 'tag'; |
| 297 | 269 | } |
| 298 | - | |
| 270 | + | |
| 299 | 271 | if ($custom_template = $this->get_template_id($template_type)) { |
| 300 | 272 | $this->current_template_id = $custom_template; |
| 301 | 273 | return $this->getTemplatePath('woocommerce/archive-product', $template); |
| 302 | 274 | } |
| 303 | 275 | } |
| 304 | - | |
| 305 | 276 | |
| 306 | - if (is_cart()) { | |
| 307 | - if ($custom_template = $this->get_template_id('cart', 'product')) { | |
| 308 | - $this->current_template_id = $custom_template; | |
| 309 | - 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; | |
| 310 | 283 | } |
| 311 | 284 | } |
| 312 | 285 | |
| 313 | 286 | if (is_order_received_page()) { |
| @@ -346,13 +319,13 @@ | ||
| 346 | 319 | // single Login & Register template, which is responsible for both |
| 347 | 320 | // authentication flows. WooCommerce's own form-login.php renders |
| 348 | 321 | // the login and registration forms on the same URL, so one |
| 349 | 322 | // template covers both intents. |
| 350 | - if ( ! is_user_logged_in() && empty($endpoint_match) ) { | |
| 351 | - if ( $custom_template = $this->get_template_id('login', 'account') ) { | |
| 323 | + if (! is_user_logged_in() && empty($endpoint_match)) { | |
| 324 | + if ($custom_template = $this->get_template_id('login', 'account')) { | |
| 352 | 325 | $this->current_template_id = $custom_template; |
| 353 | 326 | |
| 354 | - if ( $newTemplate = $this->getTemplatePath('woocommerce/login') ) { | |
| 327 | + if ($newTemplate = $this->getTemplatePath('woocommerce/login')) { | |
| 355 | 328 | return $newTemplate; |
| 356 | 329 | } |
| 357 | 330 | |
| 358 | 331 | return $this->getTemplatePath('woocommerce/my-account', $template); |
| @@ -405,9 +378,9 @@ | ||
| 405 | 378 | return $this->getTemplatePath('home', $template); |
| 406 | 379 | } |
| 407 | 380 | } |
| 408 | 381 | |
| 409 | - if ($page_Id = intval(get_option('bdt_usk_compare_products_page_id'))) { | |
| 382 | + if ($page_Id = ultimate_store_kit_get_compare_page_option()) { | |
| 410 | 383 | if (is_page($page_Id)) { |
| 411 | 384 | if ($custom_template = $this->get_template_id('compare-products', 'product')) { |
| 412 | 385 | $this->current_template_id = $custom_template; |
| 413 | 386 | return $this->getTemplatePath('home', $template); |
| @@ -449,32 +422,26 @@ | ||
| 449 | 422 | if (null !== $this->current_template_id) { |
| 450 | 423 | return $this->current_template_id; |
| 451 | 424 | } |
| 452 | 425 | |
| 453 | - // Handle template preview from URL parameters | |
| 454 | - if (!empty($_GET['preview']) && !empty($_GET['usk_template_id']) && !empty($_GET['preview_nonce'])) { | |
| 455 | - $usk_template_id = sanitize_text_field(wp_unslash($_GET['usk_template_id'])); | |
| 456 | - $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'])); | |
| 457 | 430 | |
| 458 | - // Special handling for demo bypass | |
| 459 | - $is_demo_bypass = ($nonce === 'verified'); | |
| 460 | - $nonce_verified = $is_demo_bypass ? | |
| 461 | - apply_filters('ultimate_store_kit/preview/verified_bypass', false) : | |
| 462 | - 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); | |
| 463 | 437 | |
| 464 | - if ($nonce_verified) { | |
| 465 | - // For demo bypass mode, we don't need to check template type | |
| 466 | - if ($is_demo_bypass) { | |
| 467 | - $this->current_template_id = (int)$usk_template_id; | |
| 468 | - return $this->current_template_id; | |
| 469 | - } | |
| 470 | - | |
| 471 | - // For normal preview, check template type | |
| 438 | + if ($nonce_verified && get_post_type($usk_template_id) === Meta::POST_TYPE) { | |
| 472 | 439 | $template_type = get_post_meta($usk_template_id, Meta::TEMPLATE_TYPE, true); |
| 473 | 440 | if (!empty($template_type)) { |
| 474 | 441 | $template_data = explode(Builder_Template_Helper::separator(), $template_type); |
| 475 | 442 | if (count($template_data) >= 2 && $template_data[1] === $slug && ($postType === false || $template_data[0] === $postType)) { |
| 476 | - $this->current_template_id = (int)$usk_template_id; | |
| 443 | + $this->current_template_id = $usk_template_id; | |
| 477 | 444 | return $this->current_template_id; |
| 478 | 445 | } |
| 479 | 446 | } |
| 480 | 447 | } |
| @@ -510,8 +477,25 @@ | ||
| 510 | 477 | return $default; |
| 511 | 478 | } |
| 512 | 479 | |
| 513 | 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 | + /** | |
| 514 | 498 | * Get default product URL with proper error checking |
| 515 | 499 | * |
| 516 | 500 | * @return string |
| 517 | 501 | */ |
| @@ -516,9 +500,9 @@ | ||
| 516 | 500 | * @return string |
| 517 | 501 | */ |
| 518 | 502 | protected function get_default_product_url() { |
| 519 | 503 | $products = wc_get_products(['status' => 'publish', 'limit' => 1]); |
| 520 | - | |
| 504 | + | |
| 521 | 505 | if (!empty($products) && isset($products[0]) && is_object($products[0])) { |
| 522 | 506 | $product = $products[0]; |
| 523 | 507 | if (method_exists($product, 'get_id')) { |
| 524 | 508 | return get_permalink($product->get_id()); |
| @@ -523,9 +507,9 @@ | ||
| 523 | 507 | if (method_exists($product, 'get_id')) { |
| 524 | 508 | return get_permalink($product->get_id()); |
| 525 | 509 | } |
| 526 | 510 | } |
| 527 | - | |
| 511 | + | |
| 528 | 512 | // Fallback to shop page if no products found |
| 529 | 513 | return get_permalink(wc_get_page_id('shop')); |
| 530 | 514 | } |
| 531 | 515 | } |