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
fluent-cart / app / Http / Routes / WebRoutes.php

WebRoutes.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Http/Routes/WebRoutes.php

415 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Http\Routes;
4
5 use FluentCart\Api\Resource\FrontendResource\CartResource;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\App;
8
9 use FluentCart\App\Helpers\CartHelper;
10 use FluentCart\App\Helpers\Helper;
11 use FluentCart\App\Hooks\Handlers\ShortCodes\Checkout\CheckoutPageHandler;
12 use FluentCart\App\Hooks\Handlers\ShortCodes\ReceiptHandler;
13 use FluentCart\App\Http\Controllers\WebController\FileDownloader;
14 use FluentCart\App\Models\Cart;
15 use FluentCart\App\Models\ProductVariation;
16 use FluentCart\App\Modules\PaymentMethods\PayPalGateway\API\PayPalPartnerRenderer;
17 use FluentCart\App\Modules\Templating\AssetLoader;
18 use FluentCart\App\Services\FrontendView;
19 use FluentCart\App\Services\PrintService;
20 use FluentCart\App\Services\Renderer\CartRenderer;
21 use FluentCart\App\Services\TemplateService;
22 use FluentCart\App\Services\URL;
23 use FluentCart\App\Vite;
24 use FluentCart\Framework\Support\Arr;
25 use FluentCart\App\Services\Renderer\CheckoutRenderer;
26 use FluentCart\App\Services\Renderer\ModalCheckoutRenderer;
27 use FluentCart\App\Services\Renderer\OrderReviewRenderer;
28
29 class WebRoutes
30 {
31 public static function register()
32 {
33
34 // Late on init, deliberately. These routes do not merely register — they
35 // TAKE OVER the request, render a full page and die(). At the default
36 // priority this callback is queued at plugin-include time, so it runs
37 // before anything that registers on init from `fluentcart_loaded` (every
38 // add-on, including FluentCart Pro). A route that renders and dies before
39 // those listeners exist silently drops whatever they would have rendered
40 // — which is why the saved-payment-method picker and the save-my-card
41 // consent box appeared on the checkout page but never in modal checkout.
42 add_action('init', function () {
43 self::registerRoutes();
44 }, 99);
45 }
46
47 public static function renderModalCheckout() {
48 add_action('wp_footer', function () {
49 if (!AssetLoader::isFrontendAssetsMarked() && !AssetLoader::isFluentCartContext()) {
50 return;
51 }
52
53 AssetLoader::loadModalCheckoutAssets();
54
55 if (
56 isset($_SERVER['HTTP_SEC_FETCH_DEST']) &&
57 $_SERVER['HTTP_SEC_FETCH_DEST'] === 'iframe'
58 ) {
59 return;
60 }
61
62 $cart = CartHelper::getCart();
63
64 if (!$cart) {
65 $cart = new Cart();
66 }
67
68 (new ModalCheckoutRenderer($cart))->render();
69 });
70 }
71
72 public static function registerRoutes()
73 {
74 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
75 $page = sanitize_text_field(wp_unslash($_REQUEST['fluent-cart'] ?? ''));
76
77 if (empty($page)) {
78 return;
79 }
80
81 $page = sanitize_text_field($page);
82
83 if (!$page) {
84 return;
85 }
86
87 if ($page === 'instant_checkout') {
88 $requestData = App::request()->all();
89 $variationId = sanitize_text_field(App::request()->get('item_id'));
90 $quantity = App::request()->get('quantity', 1);
91 // Raw request flag; actual validation and normalization
92 // happens in CartResource.
93 $isCustom = (bool)Arr::get($requestData, 'is_custom', false);
94
95 if(!$isCustom) {
96 if (!is_numeric($variationId)) {
97 return;
98 }
99 }
100
101 if (is_numeric($quantity)) {
102 $quantity = intval($quantity);
103 $quantity = max($quantity, 1);
104 } else {
105 $quantity = 1;
106 }
107
108 if($isCustom) {
109 $variation = apply_filters('fluent_cart/cart/validate_custom_item', null, [
110 'item_id' => $variationId,
111 'quantity' => $quantity,
112 'is_custom' => $isCustom,
113 'action' => 'instant_checkout',
114 ]);
115
116 if (!is_object($variation)) {
117 $variation = (object) $variation;
118 }
119
120 } else {
121 $variation = ProductVariation::query()->find($variationId);
122 }
123
124 if (empty($variation)) {
125 return;
126 }
127
128 $soldIndividually = $isCustom
129 ? !empty($variation->sold_individually)
130 : (bool) $variation->soldIndividually();
131
132 if ($soldIndividually) {
133 $quantity = 1;
134 }
135
136 $cart = CartResource::generateCartForInstantCheckout($variationId, $quantity, [
137 'is_custom' => $isCustom,
138 'variation' => $variation
139 ]);
140
141 if (is_wp_error($cart)) {
142 (new CheckoutPageHandler())->enqueueStyles();
143 ob_start();
144 (new CartRenderer([]))->renderEmpty(
145 $cart->get_error_message()
146 );
147 $view = ob_get_clean();
148 FrontendView::make(__('Product Not Found', 'fluent-cart'), $view);
149 die();
150 }
151
152 $coupons = App::request()->get('coupons', '');
153 if ($coupons) {
154 $coupons = explode(',', $coupons);
155 $coupons = array_map('sanitize_text_field', $coupons);
156 $couponResult = $cart->applyCoupon($coupons);
157
158
159 $couponErrors = [];
160 if (is_wp_error($couponResult)) {
161 $couponErrors[] = esc_html($couponResult->get_error_message());
162 } elseif (is_array($couponResult)) {
163 $perCouponResults = Arr::get($couponResult, 'coupon_results', []);
164 foreach ($coupons as $code) {
165 foreach ($perCouponResults as $resultCode => $result) {
166 if (strcasecmp((string) $resultCode, (string) $code) === 0) {
167 $errorMessage = Arr::get($result, 'error', '');
168 if ($errorMessage !== '') {
169 $couponErrors[] = esc_html($errorMessage);
170 }
171 break;
172 }
173 }
174 }
175 }
176
177 if ($couponErrors) {
178 $checkoutData = is_array($cart->checkout_data) ? $cart->checkout_data : [];
179 $checkoutData['__checkout_error_notices'] = $couponErrors;
180 $cart->checkout_data = $checkoutData;
181 $cart->save();
182 }
183 }
184
185 $target_path = (new StoreSettings())->getCheckoutPage();
186
187 $redirectTo = App::request()->get('redirect_to');
188 if (!empty($redirectTo)) {
189 $url = sanitize_url(wp_unslash($redirectTo));
190 $allowedHosts = [parse_url(home_url(), PHP_URL_HOST)];
191 $allowedHosts = apply_filters('fluent_cart/instant_checkout/allowed_redirect_hosts', $allowedHosts, [
192 'allowed_hosts' => $allowedHosts
193 ]);
194 if (!empty($url) && filter_var($url, FILTER_VALIDATE_URL) && in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true)) {
195 $target_path = $url;
196 }
197 }
198
199
200 // Step 1: Get current query string and parse to array
201
202 $queryArray = [];
203 $queryString = Arr::get( App::request()->server(), 'QUERY_STRING');
204 if ( isset($queryString) ) {
205 parse_str($queryString, $queryArray);
206 }
207
208 unset($queryArray['fluent-cart']);
209 unset($queryArray['item_id']);
210 unset($queryArray['quantity']);
211 unset($queryArray['coupons']);
212 unset($queryArray['redirect_to']);
213
214 if (isset($queryArray['is_custom'])) {
215 unset($queryArray['is_custom']);
216 }
217
218 $queryArray['fct_cart_hash'] = $cart->cart_hash;
219
220 $redirect_url = URL::appendQueryParams($target_path, $queryArray);
221
222 wp_redirect($redirect_url);
223 exit;
224 }
225
226 $served = self::handleMainRoutes($page);
227
228 // Handle faker routes if enabled and not already served
229 if (!$served && App::config()->get('using_faker') === true) {
230 $served = FakerRoutes::handle($page);
231 }
232
233 if (has_action('fluent_cart_action_' . $page)) {
234 $requestData = App::request()->all();
235 do_action('fluent_cart_action_' . $page, $requestData);
236 die();
237 }
238
239 if ($served) {
240 die();
241 }
242
243 return '';
244 }
245
246 private static function handleMainRoutes($page): bool
247 {
248 $request = App::request();
249 switch ($page) {
250 case 'fluent_cart_payment_authenticate':
251 (new PayPalPartnerRenderer($request->mode))->render(
252 $request->all()
253 );
254 return true;
255 case 'download-by-id':
256 case 'download-file':
257 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in view template
258 echo (new FileDownloader())->index(App::request());
259 return true;
260
261 case 'receipt':
262 $receiptHandler = new ReceiptHandler();
263
264 $view = $receiptHandler->renderRedirectPage([
265 'type' => 'receipt'
266 ]);
267
268 FrontendView::make(
269 __('Order Receipt', 'fluent-cart'),
270 $view,
271 [
272 'styles' => [
273 'public/checkout/style/confirmation.scss'
274 ],
275 'scripts' => [
276 [
277 'source' => 'public/lib/printThis-2.0.0.min.js',
278 'isStatic' => true
279 ],
280 'public/print/Print.js'
281 ],
282 'enqueue_prefix' => 'fluent-cart-checkout-order-receipt',
283 'wp_head' => false,
284 'wp_footer' => false,
285 ]);
286 return true;
287
288 case 'order-review':
289 ob_start();
290 $orderFound = (new OrderReviewRenderer(
291 sanitize_text_field($request->get('order_hash', ''))
292 ))->render();
293 $orderReview = ob_get_clean();
294
295 // A bad or stale link is a page that does not exist, and
296 // says so to crawlers as well as people.
297 if (!$orderFound) {
298 status_header(404);
299 }
300
301 // wp_head/wp_footer left on, unlike the receipt: the review
302 // form's script and stylesheet are enqueued during render()
303 // and there is nowhere else for them to print. It also lets
304 // the theme dress a page the customer reaches from an email.
305 // wp_head/wp_footer forced on. This route renders the whole
306 // document, so nothing else prints them — and the view's
307 // is_page() guard would otherwise skip both on a site whose
308 // front page is static, silently dropping every asset the
309 // review form (and Pro's photo fields) enqueued.
310 FrontendView::make(
311 __('Review Your Order', 'fluent-cart'),
312 $orderReview,
313 [
314 'wp_head' => true,
315 'wp_footer' => true,
316 ]
317 );
318 return true;
319
320 case 'print-invoice':
321 return self::handlePrintRoute('invoice');
322
323 case 'print-packing-slip':
324 return self::handlePrintRoute('packingSlip');
325
326 case 'print-delivery-slip':
327 return self::handlePrintRoute('deliverySlip');
328
329 case 'print-shipping-slip':
330 return self::handlePrintRoute('shippingSlip');
331
332 case 'print-dispatch-slip':
333 return self::handlePrintRoute('dispatchSlip');
334
335 case 'modal_checkout':
336 return self::handleModalCheckout();
337
338 default:
339 return false;
340 }
341 }
342
343 private static function handleModalCheckout(): bool
344 {
345 $variationId = App::request()->get('item_id');
346 $quantity = App::request()->get('quantity', 1);
347
348 if (!is_numeric($variationId)) {
349 return false;
350 }
351
352 if (is_numeric($quantity)) {
353 $quantity = intval($quantity);
354 $quantity = max($quantity, 1);
355 } else {
356 $quantity = 1;
357 }
358
359 $variation = ProductVariation::query()->find($variationId);
360
361 if (empty($variation)) {
362 return false;
363 }
364
365 $soldIndividually = $variation->soldIndividually();
366
367 if ($soldIndividually) {
368 $quantity = 1;
369 }
370
371 $cart = CartResource::generateCartForInstantCheckout($variationId, $quantity);
372
373 if ($cart) {
374 $modalCheckoutRenderer = new ModalCheckoutRenderer($cart);
375 ob_start();
376 $modalCheckoutRenderer->renderForm();
377 $checkoutContent = ob_get_clean();
378
379 AssetLoader::loadCheckoutAssets($cart);
380
381 Vite::enqueueStyle(
382 'fluentcart-modal-checkout-iframe-css',
383 'public/checkout/style/checkout-iframe.scss'
384 );
385
386 // Vite::enqueueScript(
387 // 'fluentcart-modal-checkout-form-js',
388 // 'public/checkout/ModalCheckoutForm.js',
389 // []
390 // );
391
392 FrontendView::make(__('Checkout', 'fluent-cart'), $checkoutContent);
393 die();
394 }
395
396 return false;
397
398
399 }
400
401 private static function handlePrintRoute($method): bool
402 {
403 if (!is_user_logged_in() || !current_user_can('manage_options')) {
404 return false;
405 }
406
407 $order = App::request()->get('order');
408 if (!empty($order)) {
409 PrintService::$method($order);
410 return true;
411 }
412 return false;
413 }
414 }
415