PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
← All changes | app/Hooks/Handlers/ShortCodes/CustomerProfileHandler.php +68 -6 1.4.0 → 1.6.6 View file →
@@ -6,13 +6,19 @@
6 6 use FluentCart\Api\PaymentMethods;
7 7 use FluentCart\Api\Resource\CustomerResource;
8 8 use FluentCart\Api\StoreSettings;
9 9 use FluentCart\App\App;
10 +use FluentCart\App\Services\CustomerIdentity\EmailClaimPortal;
11 +use FluentCart\App\Services\CustomerIdentity\CustomerRecoveryService;
12 +use FluentCart\App\Services\CustomerIdentity\EmailClaimService;
13 +use FluentCart\App\Services\CustomerIdentity\EmailVerificationService;
14 +use FluentCart\App\Helpers\CurrenciesHelper;
10 15 use FluentCart\App\Helpers\Helper;
11 16 use FluentCart\App\Models\Subscription;
12 17 use FluentCart\App\Modules\Templating\AssetLoader;
13 18 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
14 19 use FluentCart\App\Services\TemplateService;
20 +use FluentCart\App\Services\DateTime\DayjsFormatter;
15 21 use FluentCart\App\Services\Translations\TransStrings;
16 22 use FluentCart\App\Vite;
17 23 use FluentCart\Framework\Support\Arr;
18 24 use FluentCart\Framework\Support\Str;
@@ -37,8 +43,18 @@
37 43 public static function register()
38 44 {
39 45 parent::register();
40 46
47 + add_action(CustomerRecoveryService::HOOK, [CustomerRecoveryService::class, 'run']);
48 +
49 + add_action('template_redirect', function () {
50 + $redirect = EmailClaimPortal::handleSubmission();
51 + if ($redirect) {
52 + wp_safe_redirect($redirect);
53 + exit;
54 + }
55 + });
56 +
41 57 // Add wildcard customer profile pages
42 58 // add a custom permalink endpoint
43 59 add_action('init', function () {
44 60 $pageSlug = (new StoreSettings())->getCustomerDashboardPageSlug();
@@ -59,15 +75,26 @@
59 75 public function render(?array $viewData = null)
60 76 {
61 77 if (!is_user_logged_in()) {
62 78 ob_start();
63 - $redirectUrl = (new StoreSettings())->getCustomerProfilePage();
79 + $redirectUrl = $this->resolveLoginRedirectUrl(
80 + (new StoreSettings())->getCustomerProfilePage()
81 + );
82 +
83 + $claimToken = Arr::get($_GET, EmailClaimService::QUERY_TOKEN, '');
84 + if (is_string($claimToken) && $claimToken !== '') {
85 + $redirectUrl = add_query_arg(EmailClaimService::QUERY_TOKEN, sanitize_text_field(wp_unslash($claimToken)), (new StoreSettings())->getCustomerProfilePage());
86 + }
87 +
64 88 if (defined('FLUENT_AUTH_VERSION') && (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled()) {
65 89 ?>
66 90 <div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #CBD5E0; border-radius: 8px;" class="fct_auth_wrap">
67 91 <h4><?php echo esc_html__('Please log in to access your customer portal.', 'fluent-cart'); ?></h4>
68 92 <?php
69 - echo do_shortcode('[fluent_auth redirect_to="' . $redirectUrl . '"]');
93 + // The URL travels inside a double-quoted shortcode attribute, where a
94 + // bracket or quote would truncate the shortcode. Carry them encoded.
95 + $attributeUrl = str_replace(['[', ']', '"'], ['%5B', '%5D', '%22'], $redirectUrl);
96 + echo do_shortcode('[fluent_auth redirect_to="' . $attributeUrl . '"]');
70 97 echo '</div>';
71 98 } else {
72 99 ?>
73 100 <div class="fct_auth_wrap">
@@ -86,8 +113,34 @@
86 113
87 114 $this->renderCustomerAppContainer();
88 115 }
89 116
117 + /**
118 + * Resolve where a logged-out visitor should land once they have logged in.
119 + *
120 + * `redirect_to` is attacker-supplied, so it is only honoured when
121 + * wp_validate_redirect() accepts it. That compares the parsed host against the
122 + * site host. A string-prefix comparison must not be used here: a hostile host
123 + * can be built by suffixing the site host, or by placing the site host in the
124 + * userinfo position ahead of an `@`, and both keep the site URL as a prefix
125 + * while resolving somewhere else entirely.
126 + *
127 + * @param string $fallbackUrl Where to send the visitor when no usable target was supplied.
128 + * @return string
129 + */
130 + public function resolveLoginRedirectUrl($fallbackUrl)
131 + {
132 + if (empty($_GET['redirect_to']) || !is_string($_GET['redirect_to'])) {
133 + return $fallbackUrl;
134 + }
135 +
136 + $intendedRedirectUrl = sanitize_url(wp_unslash($_GET['redirect_to']));
137 +
138 + $validatedUrl = wp_validate_redirect($intendedRedirectUrl, '');
139 +
140 + return $validatedUrl ? $validatedUrl : $fallbackUrl;
141 + }
142 +
90 143 public function renderCustomerAppContainer()
91 144 {
92 145
93 146 // Enqueue global styles
@@ -94,8 +147,15 @@
94 147 Vite::enqueueStyle( 'fluent-cart-customer-profile-global',
95 148 'public/customer-profile/style/customer-profile-global.scss',
96 149 );
97 150
151 + // Gate before custom endpoint callbacks or the dashboard load customer data.
152 + $verificationNotice = EmailClaimPortal::render();
153 + if (EmailVerificationService::isRequired(get_current_user_id())) {
154 + echo $verificationNotice; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in the view
155 + return;
156 + }
157 +
98 158 $customEndpointContent = $this->maybeCustomEndpointContent();
99 159
100 160 if(!$customEndpointContent) {
101 161 (new static())->enqueueStyles();
@@ -102,9 +162,10 @@
102 162 }
103 163
104 164 $colors = self::generateCssColorVariables(Arr::get($this->shortCodeAttributes, 'colors', ''));
105 165 add_action('fluent_cart/customer_menu', array($this, 'renderCustomerMenu'));
106 - add_action('fluent_cart/customer_app', function () use ($customEndpointContent) {
166 + add_action('fluent_cart/customer_app', function () use ($customEndpointContent, $verificationNotice) {
167 + echo $verificationNotice; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in the view
107 168 if($customEndpointContent) {
108 169 echo $customEndpointContent; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
109 170 } else {
110 171 AssetLoader::loadCustomerDashboardAssets();
@@ -350,8 +411,11 @@
350 411 'fluentcart_customer_profile_vars' => [
351 412 'app_slug' => $pageSlug,
352 413 'app_url' => TemplateService::getCustomerProfileUrl(),
353 414 'shop' => $shopLocalizationData,
415 + 'currency_signs' => array_map(function ($sign) {
416 + return html_entity_decode($sign, ENT_QUOTES, 'UTF-8');
417 + }, CurrenciesHelper::getCurrencySigns()),
354 418 'trans' => TransStrings::getCustomerProfileString(),
355 419 'download_url_base' => site_url('fluent-cart/download-file/?fluent_cart_download=true'),
356 420 'placeholder_image' => Vite::getAssetUrl('images/placeholder.svg'),
357 421 'stripe_pub_key' => apply_filters('fluent_cart/payment_methods/stripe_pub_key', ''),
@@ -365,13 +429,11 @@
365 429 'me' => [
366 430 'email' => $currentCustomer ? $currentCustomer->email : '',
367 431 'first_name' => $currentCustomer ? $currentCustomer->first_name : '',
368 432 'last_name' => $currentCustomer ? $currentCustomer->last_name : '',
369 - 'photo' => $currentCustomer ? $currentCustomer->photo : ''
370 -
371 433 ],
372 434 'logout_url' => wp_logout_url(home_url()),
373 - 'datei18' => TransStrings::dateTimeStrings(),
435 + 'datei18' => DayjsFormatter::localizedStrings(),
374 436 'el_strings' => TransStrings::elStrings(),
375 437 'wp_locale' => get_locale(),
376 438 'is_company_name_enabled' => CheckoutFieldsSchema::isCompanyNameEnabled(),
377 439 'is_company_name_required' => CheckoutFieldsSchema::isCompanyNameRequired(),