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 +103 -10 1.3.21 → 1.7.0 View file →
@@ -6,12 +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;
19 +use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
13 20 use FluentCart\App\Services\TemplateService;
21 +use FluentCart\App\Services\DateTime\DayjsFormatter;
14 22 use FluentCart\App\Services\Translations\TransStrings;
15 23 use FluentCart\App\Vite;
16 24 use FluentCart\Framework\Support\Arr;
17 25 use FluentCart\Framework\Support\Str;
@@ -36,8 +44,18 @@
36 44 public static function register()
37 45 {
38 46 parent::register();
39 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 +
40 58 // Add wildcard customer profile pages
41 59 // add a custom permalink endpoint
42 60 add_action('init', function () {
43 61 $pageSlug = (new StoreSettings())->getCustomerDashboardPageSlug();
@@ -58,23 +76,40 @@
58 76 public function render(?array $viewData = null)
59 77 {
60 78 if (!is_user_logged_in()) {
61 79 ob_start();
62 - $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 +
63 89 if (defined('FLUENT_AUTH_VERSION') && (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled()) {
64 90 ?>
65 91 <div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #CBD5E0; border-radius: 8px;" class="fct_auth_wrap">
66 92 <h4><?php echo esc_html__('Please log in to access your customer portal.', 'fluent-cart'); ?></h4>
67 93 <?php
68 - 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 . '"]');
69 98 echo '</div>';
70 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 + );
71 106 ?>
72 107 <div class="fct_auth_wrap">
73 108 <div class="fct_auth_message">
74 109 <h2><?php echo esc_html__('Login', 'fluent-cart'); ?></h2>
75 110 <p><?php echo esc_html__('Please log in to access your customer portal.', 'fluent-cart'); ?></p>
76 - <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">
77 112 <?php echo esc_html__('Login', 'fluent-cart'); ?>
78 113 </a>
79 114 </div>
80 115 </div>
@@ -85,8 +120,34 @@
85 120
86 121 $this->renderCustomerAppContainer();
87 122 }
88 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 +
89 150 public function renderCustomerAppContainer()
90 151 {
91 152
92 153 // Enqueue global styles
@@ -93,8 +154,19 @@
93 154 Vite::enqueueStyle( 'fluent-cart-customer-profile-global',
94 155 'public/customer-profile/style/customer-profile-global.scss',
95 156 );
96 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 +
97 169 $customEndpointContent = $this->maybeCustomEndpointContent();
98 170
99 171 if(!$customEndpointContent) {
100 172 (new static())->enqueueStyles();
@@ -101,9 +173,10 @@
101 173 }
102 174
103 175 $colors = self::generateCssColorVariables(Arr::get($this->shortCodeAttributes, 'colors', ''));
104 176 add_action('fluent_cart/customer_menu', array($this, 'renderCustomerMenu'));
105 - 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
106 179 if($customEndpointContent) {
107 180 echo $customEndpointContent; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
108 181 } else {
109 182 AssetLoader::loadCustomerDashboardAssets();
@@ -236,8 +309,16 @@
236 309 'icon_svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
237 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"/>
238 311 </svg>'
239 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 + ],
240 321 'profile' => [
241 322 'label' => __('Profile', 'fluent-cart'),
242 323 'css_class' => 'fct_route',
243 324 'link' => $baseUrl . 'profile',
@@ -252,9 +333,13 @@
252 333 if (!ModuleSettings::isActive('license') || !App::isProActive()) {
253 334 unset($menuItems['licenses']);
254 335 }
255 336
337 + if (ProductReviewService::getReviewSettings()['reviews_enabled'] !== 'yes') {
338 + unset($menuItems['reviews']);
339 + }
256 340
341 +
257 342 if($currentCustomer) {
258 343 $hasSubscriptions = Subscription::query()->where('customer_id', $currentCustomer->id)->exists();
259 344 if(!$hasSubscriptions) {
260 345 unset($menuItems['subscriptions']);
@@ -278,9 +363,9 @@
278 363 $user = wp_get_current_user();
279 364 $profileData = [
280 365 'email' => $user->user_email,
281 366 'full_name' => $user->display_name,
282 - 'photo' => get_avatar_url($user->ID)
367 + 'photo' => Helper::getUserAvatarUrl($user->ID, $user->user_email)
283 368 ];
284 369 }
285 370
286 371 add_filter('fct_allowed_svg_tags', function ($tags) {
@@ -349,11 +434,15 @@
349 434 'fluentcart_customer_profile_vars' => [
350 435 'app_slug' => $pageSlug,
351 436 'app_url' => TemplateService::getCustomerProfileUrl(),
352 437 'shop' => $shopLocalizationData,
438 + 'currency_signs' => array_map(function ($sign) {
439 + return html_entity_decode($sign, ENT_QUOTES, 'UTF-8');
440 + }, CurrenciesHelper::getCurrencySigns()),
353 441 'trans' => TransStrings::getCustomerProfileString(),
354 442 'download_url_base' => site_url('fluent-cart/download-file/?fluent_cart_download=true'),
355 - 'placeholder_image' => Vite::getAssetUrl('images/placeholder.svg'),
443 + 'placeholder_image' => Helper::getProductPlaceholderUrl(),
444 + 'reviews_enabled' => ProductReviewService::getReviewSettings()['reviews_enabled'] === 'yes',
356 445 'stripe_pub_key' => apply_filters('fluent_cart/payment_methods/stripe_pub_key', ''),
357 446 'paypal_client_id' => apply_filters('fluent_cart/payment_methods/paypal_client_id', '', []),
358 447 'assets_path' => Vite::getAssetUrl(),
359 448 'rest' => Helper::getRestInfo(),
@@ -364,15 +453,19 @@
364 453 'me' => [
365 454 'email' => $currentCustomer ? $currentCustomer->email : '',
366 455 'first_name' => $currentCustomer ? $currentCustomer->first_name : '',
367 456 'last_name' => $currentCustomer ? $currentCustomer->last_name : '',
368 - 'photo' => $currentCustomer ? $currentCustomer->photo : ''
369 -
370 457 ],
371 458 'logout_url' => wp_logout_url(home_url()),
372 - 'datei18' => TransStrings::dateTimeStrings(),
459 + 'datei18' => DayjsFormatter::localizedStrings(),
373 460 'el_strings' => TransStrings::elStrings(),
374 - 'wp_locale' => get_locale()
461 + 'wp_locale' => get_locale(),
462 + 'is_company_name_enabled' => CheckoutFieldsSchema::isCompanyNameEnabled(),
463 + 'is_company_name_required' => CheckoutFieldsSchema::isCompanyNameRequired(),
464 + 'is_vat_number_enabled' => CheckoutFieldsSchema::isVatNumberEnabled(),
465 + 'is_vat_number_required' => CheckoutFieldsSchema::isVatNumberRequired(),
466 + 'is_legal_registration_id_enabled' => CheckoutFieldsSchema::isLegalRegistrationIdEnabled(),
467 + 'is_legal_registration_id_required' => CheckoutFieldsSchema::isLegalRegistrationIdRequired()
375 468 ],
376 469 'fluentCartRestVars' => [
377 470 'rest' => Helper::getRestInfo(),
378 471 ],