PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 1.9.12 All 161 releases
woocommerce-pos / includes / Templates / Payment.php

Payment.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.17, at includes/Templates/Payment.php

521 lines 19.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Payment template.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see http://wcpos.com
8 * @package WCPOS\WooCommercePOS
9 */
10
11 namespace WCPOS\WooCommercePOS\Templates;
12
13 use Exception;
14 use WCPOS\WooCommercePOS\Services\Settings;
15
16 /**
17 * Payment class.
18 */
19 class Payment {
20 /**
21 * The order ID.
22 *
23 * @var int
24 */
25 private $order_id;
26
27 /**
28 * The order.
29 *
30 * @var \WC_Order
31 */
32 private $order;
33
34 /**
35 * The coupon nonce.
36 *
37 * @var string
38 */
39 private $coupon_nonce;
40
41 /**
42 * The troubleshooting form nonce.
43 *
44 * @var string
45 */
46 private $troubleshooting_form_nonce;
47
48 /**
49 * Disable wp_head setting.
50 *
51 * @var bool
52 */
53 private $disable_wp_head;
54
55 /**
56 * Disable wp_footer setting.
57 *
58 * @var bool
59 */
60 private $disable_wp_footer;
61
62 /**
63 * Constructor.
64 *
65 * @param int $order_id The order ID.
66 */
67 public function __construct( int $order_id ) {
68 $this->order_id = $order_id;
69 $this->check_troubleshooting_form_submission();
70 $settings_service = Settings::instance();
71 $this->disable_wp_head = (bool) $settings_service->get_settings( 'checkout', 'disable_wp_head' );
72 $this->disable_wp_footer = (bool) $settings_service->get_settings( 'checkout', 'disable_wp_footer' );
73
74 // this is a checkout page.
75 add_filter( 'woocommerce_is_checkout', '__return_true' );
76 // remove the terms and conditions checkbox.
77 add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
78
79 // remove junk from head.
80 add_filter( 'show_admin_bar', '__return_false' );
81 remove_action( 'wp_head', 'rsd_link' );
82 remove_action( 'wp_head', 'wp_generator' );
83 remove_action( 'wp_head', 'feed_links', 2 );
84 remove_action( 'wp_head', 'index_rel_link' );
85 remove_action( 'wp_head', 'wlwmanifest_link' );
86 remove_action( 'wp_head', 'feed_links_extra', 3 );
87 remove_action( 'wp_head', 'start_post_rel_link', 10, 0 );
88 remove_action( 'wp_head', 'parent_post_rel_link', 10, 0 );
89 remove_action( 'wp_head', 'adjacent_posts_rel_link', 10, 0 );
90 remove_action( 'wp_head', 'wp_shortlink_wp_head', 10, 0 );
91 remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 );
92
93 add_action( 'wp_enqueue_scripts', array( $this, 'remove_scripts_and_styles' ), 100 );
94 }
95
96 /**
97 * Each theme will apply its own styles to the checkout page.
98 * I want to keep it simple, so we remove all styles and scripts associated with the active theme.
99 * NOTE: This is not perfect, we don't know the theme handle, so we just take a guess from the source URL.
100 *
101 * @return void
102 */
103 /**
104 * Remove enqueued scripts and styles.
105 *
106 * This function dequeues all scripts and styles that are not specified in the WCPOS settings,
107 * unless they are specifically included by the 'woocommerce_pos_payment_template_dequeue_script_handles'
108 * and 'woocommerce_pos_payment_template_dequeue_style_handles' filters.
109 *
110 * @since 1.3.0
111 */
112 public function remove_scripts_and_styles(): void {
113 global $wp_styles, $wp_scripts;
114
115 /**
116 * List of script handles to exclude from the payment template.
117 *
118 * @since 1.3.0
119 */
120 $script_exclude_list = apply_filters(
121 'woocommerce_pos_payment_template_dequeue_script_handles',
122 Settings::instance()->dequeue_script_handles()
123 );
124
125 /**
126 * List of style handles to exclude from the payment template.
127 *
128 * @since 1.3.0
129 */
130 $style_exclude_list = apply_filters(
131 'woocommerce_pos_payment_template_dequeue_style_handles',
132 Settings::instance()->dequeue_style_handles()
133 );
134
135 // Loop through all enqueued styles and dequeue those that are in the exclusion list.
136 if ( \is_array( $style_exclude_list ) ) {
137 foreach ( $wp_styles->queue as $handle ) {
138 if ( \in_array( $handle, $style_exclude_list, true ) ) {
139 wp_dequeue_style( $handle );
140 }
141 }
142 }
143
144 // Loop through all enqueued scripts and dequeue those that are in the exclusion list.
145 if ( \is_array( $script_exclude_list ) ) {
146 foreach ( $wp_scripts->queue as $handle ) {
147 if ( \in_array( $handle, $script_exclude_list, true ) ) {
148 wp_dequeue_script( $handle );
149 }
150 }
151 }
152 }
153
154
155 /**
156 * Render the payment template.
157 *
158 * @return void
159 */
160 public function get_template(): void {
161 if ( ! \defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
162 \define( 'WOOCOMMERCE_CHECKOUT', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WooCommerce constant.
163 }
164
165 do_action( 'woocommerce_pos_before_pay' );
166
167 try {
168 // initialize order and nonces before the user is switched to customer.
169 $this->initialize_order_and_nonces();
170
171 // Verify order key to prevent unauthenticated access.
172 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Order key is the auth mechanism here, matching WooCommerce core behavior.
173 $provided_key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : '';
174 if ( ! $provided_key || $provided_key !== $this->order->get_order_key() ) {
175 wp_die(
176 esc_html__( 'Sorry, this order cannot be paid for. The order key is missing or invalid.', 'woocommerce-pos' ),
177 /* translators: Short WCPOS UI label; keep concise. */
178 esc_html__( 'Error', 'woocommerce-pos' ),
179 array( 'response' => 403 )
180 );
181 }
182
183 /*
184 * The wp_set_current_user() function changes the global user object but it does not authenticate the user
185 * for the current session. This means that it will not affect nonce creation or validation because WordPress
186 * nonces are tied to the user's session.
187 *
188 * @TODO - is this the best way to do this?
189 */
190 wp_set_current_user( $this->order->get_customer_id() );
191 add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ), 10, 2 );
192
193 // Logged in customer trying to pay for someone else's order.
194 if ( ! current_user_can( 'pay_for_order', $this->order_id ) ) {
195 wp_die( esc_html__( 'This order cannot be paid for. Please contact us if you need assistance.', 'woocommerce-pos' ) );
196 }
197
198 // We need to reload the gateways here to use the current customer details.
199 WC()->payment_gateways()->init();
200 $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
201
202 $order_button_text = apply_filters( 'woocommerce_pay_order_button_text', /* translators: Short WCPOS UI label; keep concise. */ __( 'Pay for order', 'woocommerce-pos' ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core hook.
203
204 include woocommerce_pos_locate_template( 'payment.php' );
205 } catch ( Exception $e ) {
206 wc_print_notice( $e->getMessage(), 'error' );
207 }
208 }
209
210 /**
211 * Render the troubleshooting form HTML.
212 *
213 * @return string
214 */
215 public function get_troubleshooting_form_html(): string {
216 global $wp_styles, $wp_scripts;
217 $style_handles = $wp_styles->queue;
218 $script_handles = $wp_scripts->queue;
219
220 $style_exclude_list = apply_filters(
221 'woocommerce_pos_payment_template_dequeue_style_handles',
222 Settings::instance()->dequeue_style_handles()
223 );
224
225 $script_exclude_list = apply_filters(
226 'woocommerce_pos_payment_template_dequeue_script_handles',
227 Settings::instance()->dequeue_script_handles()
228 );
229
230 $merged_style_handles = array_unique( array_merge( $style_handles, $style_exclude_list ) );
231 $merged_script_handles = array_unique( array_merge( $script_handles, $script_exclude_list ) );
232
233 ob_start();
234 ?>
235 <div class="woocommerce-pos-troubleshooting">
236 <div style="text-align:right">
237 <button type="button" class="open-troubleshooting-modal"><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Checkout Settings', 'woocommerce-pos' ); ?></button>
238 </div>
239 <div id="troubleshooting-modal" class="troubleshooting-modal" style="display: none;">
240 <div class="troubleshooting-modal-content">
241 <span class="close-troubleshooting-modal">&times;</span>
242 <form id="troubleshooting-form" method="POST">
243 <p class="woocommerce-info"><?php esc_html_e( 'Scripts and styles may interfere with the custom payment template, use this form to dequeue any problematic scripts.', 'woocommerce-pos' ); ?></p>
244 <div style="margin-bottom: 20px;">
245 <h3><?php esc_html_e( 'Disable All Styles and Scripts', 'woocommerce-pos' ); ?></h3>
246 <label for="disable_wp_head">
247 <input type="checkbox" id="disable_wp_head" name="disable_wp_head" value="1" <?php checked( $this->disable_wp_head ); ?>>
248 <?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Disable wp_head', 'woocommerce-pos' ); ?>
249 </label>
250 <br>
251 <label for="disable_wp_footer">
252 <input type="checkbox" id="disable_wp_footer" name="disable_wp_footer" value="1" <?php checked( $this->disable_wp_footer ); ?>>
253 <?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Disable wp_footer', 'woocommerce-pos' ); ?>
254 </label>
255 </div>
256 <div style="display: flex; justify-content: space-between;margin-bottom:20px;">
257 <div style="flex: 1;">
258 <h3><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Disable Selected Styles', 'woocommerce-pos' ); ?></h3>
259 <?php
260 foreach ( $merged_style_handles as $handle ) {
261 $checked = ! \in_array( $handle, $style_exclude_list, true ) ? 'checked' : '';
262 ?>
263 <input type="checkbox" id="<?php echo esc_attr( $handle ); ?>" name="styles[]" value="<?php echo esc_attr( $handle ); ?>" <?php echo esc_attr( $checked ); ?>>
264 <label for="<?php echo esc_attr( $handle ); ?>"><?php echo esc_html( $handle ); ?></label>
265 <input type="hidden" name="all_styles[]" value="<?php echo esc_attr( $handle ); ?>"><br>
266 <?php } ?>
267 </div>
268 <div style="flex: 1;">
269 <h3><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Disable Selected Scripts', 'woocommerce-pos' ); ?></h3>
270 <?php
271 foreach ( $merged_script_handles as $handle ) {
272 $checked = ! \in_array( $handle, $script_exclude_list, true ) ? 'checked' : '';
273 ?>
274 <input type="checkbox" id="<?php echo esc_attr( $handle ); ?>" name="scripts[]" value="<?php echo esc_attr( $handle ); ?>" <?php echo esc_attr( $checked ); ?>>
275 <label for="<?php echo esc_html( $handle ); ?>"><?php echo esc_html( $handle ); ?></label>
276 <input type="hidden" name="all_scripts[]" value="<?php echo esc_attr( $handle ); ?>"><br>
277 <?php } ?>
278 </div>
279 </div>
280 <input type="hidden" name="troubleshooting_form_nonce" value="<?php echo esc_attr( $this->troubleshooting_form_nonce ); ?>" />
281 <button type="submit"><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Submit', 'woocommerce-pos' ); ?></button>
282 </form>
283 </div>
284 </div>
285 </div>
286
287 <script>
288 document.querySelector('.open-troubleshooting-modal').addEventListener('click', () => {
289 document.getElementById('troubleshooting-modal').style.display = 'block';
290 });
291 document.querySelector('.close-troubleshooting-modal').addEventListener('click', () => {
292 document.getElementById('troubleshooting-modal').style.display = 'none';
293 });
294 window.addEventListener('click', event => {
295 if (event.target === document.getElementById('troubleshooting-modal')) {
296 document.getElementById('troubleshooting-modal').style.display = 'none';
297 }
298 });
299 </script>
300 <?php
301 return ob_get_clean();
302 }
303
304 /**
305 * Render the cashier details HTML.
306 *
307 * @return string
308 */
309 public function get_cashier_details_html(): string {
310 $cashier = $this->order->get_meta( '_pos_user', true );
311 $cashier = get_user_by( 'id', $cashier );
312
313 ob_start();
314 ?>
315 <div class="cashier">
316 <span><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Cashier', 'woocommerce-pos' ); ?>: </span>
317 <span class="cashier-name"><?php echo esc_html( $cashier->display_name ); ?></span>
318 </div>
319 <?php
320 return ob_get_clean();
321 }
322
323 /**
324 * Render the address fields HTML.
325 *
326 * @return string
327 */
328 public function get_paying_customer_details_html(): string {
329 $customer = wp_get_current_user();
330 ob_start();
331 ?>
332 <div class="current-user">
333 <span><?php /* translators: Short WCPOS UI label; keep concise. */ esc_html_e( 'Paying as customer', 'woocommerce-pos' ); ?>: </span>
334 <span class="user-name"><?php echo 0 === $customer->ID ? /* translators: Short WCPOS UI label; keep concise. */ esc_html__( 'Guest', 'woocommerce-pos' ) : esc_html( $customer->display_name ); ?></span>
335 </div>
336 <div class="address-fields" style="display: none;">
337 <section class="woocommerce-customer-details">
338
339 <section class="woocommerce-columns woocommerce-columns--2 woocommerce-columns--addresses col2-set addresses">
340 <div class="woocommerce-column woocommerce-column--1 woocommerce-column--billing-address col-1">
341 <h2 class="woocommerce-column__title"><?php esc_html_e( 'Billing address', 'woocommerce' ); ?></h2>
342 <address>
343 <?php echo wp_kses_post( $this->order->get_formatted_billing_address( esc_html__( 'N/A', 'woocommerce' ) ) ); ?>
344 <?php if ( $this->order->get_billing_phone() ) { ?>
345 <p class="woocommerce-customer-details--phone"><?php echo esc_html( $this->order->get_billing_phone() ); ?></p>
346 <?php } ?>
347 <?php if ( $this->order->get_billing_email() ) { ?>
348 <p class="woocommerce-customer-details--email"><?php echo esc_html( $this->order->get_billing_email() ); ?></p>
349 <?php } ?>
350 </address>
351 </div><!-- /.col-1 -->
352
353 <div class="woocommerce-column woocommerce-column--2 woocommerce-column--shipping-address col-2">
354 <h2 class="woocommerce-column__title"><?php esc_html_e( 'Shipping address', 'woocommerce' ); ?></h2>
355 <address>
356 <?php echo wp_kses_post( $this->order->get_formatted_shipping_address( esc_html__( 'N/A', 'woocommerce' ) ) ); ?>
357 <?php if ( $this->order->get_shipping_phone() ) { ?>
358 <p class="woocommerce-customer-details--phone"><?php echo esc_html( $this->order->get_shipping_phone() ); ?></p>
359 <?php } ?>
360 </address>
361 </div><!-- /.col-2 -->
362
363 </section><!-- /.col2-set -->
364
365 <?php do_action( 'woocommerce_order_details_after_customer_details', $this->order ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core hook. ?>
366
367 </section>
368 </div>
369 <script>
370 document.querySelector('.current-user .user-name').addEventListener('click', () => {
371 const addressFields = document.querySelector('.address-fields');
372 addressFields.style.display = addressFields.style.display === 'none' ? 'block' : 'none';
373 });
374 </script>
375 <?php
376 return ob_get_clean();
377 }
378
379 /**
380 * Render the coupon form HTML.
381 *
382 * @return string
383 */
384 public function get_coupon_form_html(): string {
385 ob_start();
386 ?>
387 <div class="coupons">
388 <form method="post" action="">
389 <input type="hidden" name="pos_coupon_nonce" value="<?php echo esc_attr( $this->coupon_nonce ); ?>" />
390 <input type="text" name="pos_coupon_code" class="input-text" placeholder="<?php esc_attr_e( 'Coupon code', 'woocommerce' ); ?>" id="pos_coupon_code" value="" />
391 <button type="submit" class="button" name="pos_apply_coupon" value="<?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?>">
392 <?php esc_html_e( 'Apply coupon', 'woocommerce' ); ?>
393 </button>
394
395 <?php
396 $coupons = $this->order->get_items( 'coupon' );
397 if ( $coupons ) {
398 echo '<h3>' . esc_html__( 'Applied coupons', 'woocommerce' ) . '</h3>';
399 echo '<ul>';
400 foreach ( $coupons as $coupon ) {
401 echo '<li>' . esc_html( $coupon->get_code() ) . ' <button type="submit" class="button" name="pos_remove_coupon" value="' . esc_attr( $coupon->get_code() ) . '">' . esc_html__( 'Remove', 'woocommerce' ) . '</button></li>';
402 }
403 echo '</ul>';
404 }
405 ?>
406 </form>
407 </div>
408 <?php
409 return ob_get_clean();
410 }
411
412 /**
413 * Fix: when checking out as Guest on the desktop application, WordPress gets a $uid from the
414 * session, eg: 't_8b04f8283e7edc5aeee2867c89dd06'. This causes the nonce check to fail.
415 *
416 * @param mixed $uid The user ID.
417 * @param mixed $action The nonce action.
418 */
419 public function nonce_user_logged_out( $uid, $action ) {
420 if ( 'woocommerce-pay' === $action ) {
421 return 0;
422 }
423
424 return $uid;
425 }
426
427 /**
428 * Initialize the order and nonce properties.
429 */
430 private function initialize_order_and_nonces(): void {
431 $this->order = wc_get_order( $this->order_id );
432
433 if ( ! $this->order || $this->order->get_id() !== $this->order_id ) {
434 wp_die( esc_html__( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce-pos' ) );
435 }
436
437 if ( $this->order->is_paid() ) {
438 wp_die( esc_html__( 'Sorry, this order has already been paid for.', 'woocommerce-pos' ) );
439 }
440
441 $this->coupon_nonce = wp_create_nonce( 'pos_coupon_action' );
442 $this->troubleshooting_form_nonce = wp_create_nonce( 'troubleshooting_form_nonce' );
443 }
444
445 /**
446 * Save the settings from the troubleshooting form.
447 *
448 * @return void
449 */
450 private function check_troubleshooting_form_submission(): void {
451 // Check if our form has been submitted.
452 if ( isset( $_POST['troubleshooting_form_nonce'] ) ) {
453 // Only allow users with manage_woocommerce capability to modify checkout settings.
454 if ( ! current_user_can( 'manage_woocommerce' ) ) {
455 wp_die(
456 esc_html__( 'You do not have permission to modify checkout settings.', 'woocommerce-pos' ),
457 /* translators: Short WCPOS UI label; keep concise. */
458 esc_html__( 'Error', 'woocommerce-pos' ),
459 array( 'response' => 403 )
460 );
461 }
462
463 // Verify the nonce.
464 if ( ! wp_verify_nonce( $_POST['troubleshooting_form_nonce'], 'troubleshooting_form_nonce' ) ) {
465 // Nonce doesn't verify, we should stop execution here.
466 die( 'Nonce value cannot be verified.' );
467 }
468
469 // This will hold your sanitized data.
470 $sanitized_data = array();
471
472 // Sanitize all_styles array.
473 if ( isset( $_POST['all_styles'] ) && \is_array( $_POST['all_styles'] ) ) {
474 $sanitized_data['all_styles'] = array_map( 'sanitize_text_field', wp_unslash( $_POST['all_styles'] ) );
475 }
476
477 // Sanitize styles array.
478 if ( isset( $_POST['styles'] ) && \is_array( $_POST['styles'] ) ) {
479 $sanitized_data['styles'] = array_map( 'sanitize_text_field', wp_unslash( $_POST['styles'] ) );
480 } else {
481 $sanitized_data['styles'] = array(); // consider all styles unchecked if 'styles' is not submitted.
482 }
483
484 // Sanitize all_scripts array.
485 if ( isset( $_POST['all_scripts'] ) && \is_array( $_POST['all_scripts'] ) ) {
486 $sanitized_data['all_scripts'] = array_map( 'sanitize_text_field', wp_unslash( $_POST['all_scripts'] ) );
487 }
488
489 // Sanitize scripts array.
490 if ( isset( $_POST['scripts'] ) && \is_array( $_POST['scripts'] ) ) {
491 $sanitized_data['scripts'] = array_map( 'sanitize_text_field', wp_unslash( $_POST['scripts'] ) );
492 } else {
493 $sanitized_data['scripts'] = array(); // consider all scripts unchecked if 'scripts' is not submitted.
494 }
495
496 // Calculate unchecked styles and scripts.
497 $unchecked_styles = isset( $sanitized_data['all_styles'] ) ? array_diff( $sanitized_data['all_styles'], $sanitized_data['styles'] ) : array();
498 $unchecked_scripts = isset( $sanitized_data['all_scripts'] ) ? array_diff( $sanitized_data['all_scripts'], $sanitized_data['scripts'] ) : array();
499
500 // Sanitize disable_wp_head and disable_wp_footer options.
501 $disable_wp_head = isset( $_POST['disable_wp_head'] ) ? (bool) $_POST['disable_wp_head'] : false;
502 $disable_wp_footer = isset( $_POST['disable_wp_footer'] ) ? (bool) $_POST['disable_wp_footer'] : false;
503
504 // @TODO - the save settings function should allow saving by key
505 $checkout_settings = woocommerce_pos_get_settings( 'checkout' );
506 $new_settings = array_merge(
507 $checkout_settings,
508 array(
509 'disable_wp_head' => $disable_wp_head,
510 'disable_wp_footer' => $disable_wp_footer,
511 'dequeue_style_handles' => $unchecked_styles,
512 'dequeue_script_handles' => $unchecked_scripts,
513 )
514 );
515
516 $settings_service = Settings::instance();
517 $settings_service->save_settings( 'checkout', $new_settings );
518 }
519 }
520 }
521