PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 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 All 50 releases
← All changes | app/Hooks/Handlers/ShortCodes/CustomerProfileHandler.php +95 -7 1.5.2 → 1.7.0 View file →
@@ -6,13 +6,20 @@
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;
18 +use FluentCart\App\Services\ProductReviewService;
13 19 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
14 20 use FluentCart\App\Services\TemplateService;
21 +use FluentCart\App\Services\DateTime\DayjsFormatter;
15 22 use FluentCart\App\Services\Translations\TransStrings;
16 23 use FluentCart\App\Vite;
17 24 use FluentCart\Framework\Support\Arr;
18 25 use FluentCart\Framework\Support\Str;
@@ -37,8 +44,18 @@
37 44 public static function register()
38 45 {
39 46 parent::register();
40 47
48 + add_action(CustomerRecoveryService::HOOK, [CustomerRecoveryService::class, 'run']);
49 +
50 + add_action('template_redirect', function () {
51 + $redirect = EmailClaimPortal::handleSubmission();
52 + if ($redirect) {
53 + wp_safe_redirect($redirect);
54 + exit;
55 + }
56 + });
57 +
41 58 // Add wildcard customer profile pages
42 59 // add a custom permalink endpoint
43 60 add_action('init', function () {
44 61 $pageSlug = (new StoreSettings())->getCustomerDashboardPageSlug();
@@ -59,23 +76,40 @@
59 76 public function render(?array $viewData = null)
60 77 {
61 78 if (!is_user_logged_in()) {
62 79 ob_start();
63 - $redirectUrl = (new StoreSettings())->getCustomerProfilePage();
80 + $redirectUrl = $this->resolveLoginRedirectUrl(
81 + (new StoreSettings())->getCustomerProfilePage()
82 + );
83 +
84 + $claimToken = Arr::get($_GET, EmailClaimService::QUERY_TOKEN, '');
85 + if (is_string($claimToken) && $claimToken !== '') {
86 + $redirectUrl = add_query_arg(EmailClaimService::QUERY_TOKEN, sanitize_text_field(wp_unslash($claimToken)), (new StoreSettings())->getCustomerProfilePage());
87 + }
88 +
64 89 if (defined('FLUENT_AUTH_VERSION') && (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled()) {
65 90 ?>
66 91 <div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #CBD5E0; border-radius: 8px;" class="fct_auth_wrap">
67 92 <h4><?php echo esc_html__('Please log in to access your customer portal.', 'fluent-cart'); ?></h4>
68 93 <?php
69 - echo do_shortcode('[fluent_auth redirect_to="' . $redirectUrl . '"]');
94 + // The URL travels inside a double-quoted shortcode attribute, where a
95 + // bracket or quote would truncate the shortcode. Carry them encoded.
96 + $attributeUrl = str_replace(['[', ']', '"'], ['%5B', '%5D', '%22'], $redirectUrl);
97 + echo do_shortcode('[fluent_auth redirect_to="' . $attributeUrl . '"]');
70 98 echo '</div>';
71 99 } else {
100 + // The Login link wears the portal's button pair (see
101 + // customer-profile-global.scss), not the theme's `.button`.
102 + Vite::enqueueStyle(
103 + 'fluent-cart-customer-profile-global',
104 + 'public/customer-profile/style/customer-profile-global.scss'
105 + );
72 106 ?>
73 107 <div class="fct_auth_wrap">
74 108 <div class="fct_auth_message">
75 109 <h2><?php echo esc_html__('Login', 'fluent-cart'); ?></h2>
76 110 <p><?php echo esc_html__('Please log in to access your customer portal.', 'fluent-cart'); ?></p>
77 - <a href="<?php echo esc_url(wp_login_url($redirectUrl ?? '')); ?>" class="button">
111 + <a href="<?php echo esc_url(wp_login_url($redirectUrl ?? '')); ?>" class="button fct-customer-login-btn">
78 112 <?php echo esc_html__('Login', 'fluent-cart'); ?>
79 113 </a>
80 114 </div>
81 115 </div>
@@ -86,8 +120,34 @@
86 120
87 121 $this->renderCustomerAppContainer();
88 122 }
89 123
124 + /**
125 + * Resolve where a logged-out visitor should land once they have logged in.
126 + *
127 + * `redirect_to` is attacker-supplied, so it is only honoured when
128 + * wp_validate_redirect() accepts it. That compares the parsed host against the
129 + * site host. A string-prefix comparison must not be used here: a hostile host
130 + * can be built by suffixing the site host, or by placing the site host in the
131 + * userinfo position ahead of an `@`, and both keep the site URL as a prefix
132 + * while resolving somewhere else entirely.
133 + *
134 + * @param string $fallbackUrl Where to send the visitor when no usable target was supplied.
135 + * @return string
136 + */
137 + public function resolveLoginRedirectUrl($fallbackUrl)
138 + {
139 + if (empty($_GET['redirect_to']) || !is_string($_GET['redirect_to'])) {
140 + return $fallbackUrl;
141 + }
142 +
143 + $intendedRedirectUrl = sanitize_url(wp_unslash($_GET['redirect_to']));
144 +
145 + $validatedUrl = wp_validate_redirect($intendedRedirectUrl, '');
146 +
147 + return $validatedUrl ? $validatedUrl : $fallbackUrl;
148 + }
149 +
90 150 public function renderCustomerAppContainer()
91 151 {
92 152
93 153 // Enqueue global styles
@@ -94,8 +154,19 @@
94 154 Vite::enqueueStyle( 'fluent-cart-customer-profile-global',
95 155 'public/customer-profile/style/customer-profile-global.scss',
96 156 );
97 157
158 + // Gate before custom endpoint callbacks or the dashboard load customer data.
159 + $verificationNotice = EmailClaimPortal::render();
160 + if (EmailVerificationService::isRequired(get_current_user_id())) {
161 + echo $verificationNotice; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in the view
162 + return;
163 + }
164 +
165 + if (!EmailVerificationService::isEnabled()) {
166 + CustomerResource::getCurrentCustomer(true);
167 + }
168 +
98 169 $customEndpointContent = $this->maybeCustomEndpointContent();
99 170
100 171 if(!$customEndpointContent) {
101 172 (new static())->enqueueStyles();
@@ -102,9 +173,10 @@
102 173 }
103 174
104 175 $colors = self::generateCssColorVariables(Arr::get($this->shortCodeAttributes, 'colors', ''));
105 176 add_action('fluent_cart/customer_menu', array($this, 'renderCustomerMenu'));
106 - add_action('fluent_cart/customer_app', function () use ($customEndpointContent) {
177 + add_action('fluent_cart/customer_app', function () use ($customEndpointContent, $verificationNotice) {
178 + echo $verificationNotice; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in the view
107 179 if($customEndpointContent) {
108 180 echo $customEndpointContent; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
109 181 } else {
110 182 AssetLoader::loadCustomerDashboardAssets();
@@ -237,8 +309,16 @@
237 309 'icon_svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
238 310 <path d="M10.75 8.5H14.5L10 13L5.5 8.5H9.25V3.25H10.75V8.5ZM4 15.25H16V10H17.5V16C17.5 16.1989 17.421 16.3897 17.2803 16.5303C17.1397 16.671 16.9489 16.75 16.75 16.75H3.25C3.05109 16.75 2.86032 16.671 2.71967 16.5303C2.57902 16.3897 2.5 16.1989 2.5 16V10H4V15.25Z" fill="currentColor"/>
239 311 </svg>'
240 312 ],
313 + 'reviews' => [
314 + 'label' => __('My Reviews', 'fluent-cart'),
315 + 'css_class' => 'fct_route',
316 + 'link' => $baseUrl . 'reviews',
317 + 'icon_svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
318 + <path d="M10 1.875L12.4635 6.86713L17.9727 7.66771L13.9863 11.5533L14.9271 17.0398L10 14.4488L5.07295 17.0398L6.01368 11.5533L2.02734 7.66771L7.53647 6.86713L10 1.875ZM10 5.26443L8.53252 8.23787L5.25123 8.71472L7.62536 11.0289L7.06498 14.2963L10 12.7534L12.935 14.2963L12.3746 11.0289L14.7488 8.71472L11.4675 8.23787L10 5.26443Z" fill="currentColor"/>
319 + </svg>'
320 + ],
241 321 'profile' => [
242 322 'label' => __('Profile', 'fluent-cart'),
243 323 'css_class' => 'fct_route',
244 324 'link' => $baseUrl . 'profile',
@@ -253,9 +333,13 @@
253 333 if (!ModuleSettings::isActive('license') || !App::isProActive()) {
254 334 unset($menuItems['licenses']);
255 335 }
256 336
337 + if (ProductReviewService::getReviewSettings()['reviews_enabled'] !== 'yes') {
338 + unset($menuItems['reviews']);
339 + }
257 340
341 +
258 342 if($currentCustomer) {
259 343 $hasSubscriptions = Subscription::query()->where('customer_id', $currentCustomer->id)->exists();
260 344 if(!$hasSubscriptions) {
261 345 unset($menuItems['subscriptions']);
@@ -279,9 +363,9 @@
279 363 $user = wp_get_current_user();
280 364 $profileData = [
281 365 'email' => $user->user_email,
282 366 'full_name' => $user->display_name,
283 - 'photo' => get_avatar_url($user->ID)
367 + 'photo' => Helper::getUserAvatarUrl($user->ID, $user->user_email)
284 368 ];
285 369 }
286 370
287 371 add_filter('fct_allowed_svg_tags', function ($tags) {
@@ -350,11 +434,15 @@
350 434 'fluentcart_customer_profile_vars' => [
351 435 'app_slug' => $pageSlug,
352 436 'app_url' => TemplateService::getCustomerProfileUrl(),
353 437 'shop' => $shopLocalizationData,
438 + 'currency_signs' => array_map(function ($sign) {
439 + return html_entity_decode($sign, ENT_QUOTES, 'UTF-8');
440 + }, CurrenciesHelper::getCurrencySigns()),
354 441 'trans' => TransStrings::getCustomerProfileString(),
355 442 'download_url_base' => site_url('fluent-cart/download-file/?fluent_cart_download=true'),
356 - 'placeholder_image' => Vite::getAssetUrl('images/placeholder.svg'),
443 + 'placeholder_image' => Helper::getProductPlaceholderUrl(),
444 + 'reviews_enabled' => ProductReviewService::getReviewSettings()['reviews_enabled'] === 'yes',
357 445 'stripe_pub_key' => apply_filters('fluent_cart/payment_methods/stripe_pub_key', ''),
358 446 'paypal_client_id' => apply_filters('fluent_cart/payment_methods/paypal_client_id', '', []),
359 447 'assets_path' => Vite::getAssetUrl(),
360 448 'rest' => Helper::getRestInfo(),
@@ -367,9 +455,9 @@
367 455 'first_name' => $currentCustomer ? $currentCustomer->first_name : '',
368 456 'last_name' => $currentCustomer ? $currentCustomer->last_name : '',
369 457 ],
370 458 'logout_url' => wp_logout_url(home_url()),
371 - 'datei18' => TransStrings::dateTimeStrings(),
459 + 'datei18' => DayjsFormatter::localizedStrings(),
372 460 'el_strings' => TransStrings::elStrings(),
373 461 'wp_locale' => get_locale(),
374 462 'is_company_name_enabled' => CheckoutFieldsSchema::isCompanyNameEnabled(),
375 463 'is_company_name_required' => CheckoutFieldsSchema::isCompanyNameRequired(),