PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
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 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / Template_Router.php

Template_Router.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/Template_Router.php

291 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template Router Class.
4 *
5 * Handles routing for POS frontend templates.
6 *
7 * @author Paul Kilmurray <paul@kilbot.com>
8 *
9 * @see http://wcpos.com
10 * @package WCPOS\WooCommercePOS
11 */
12
13 namespace WCPOS\WooCommercePOS;
14
15 use WC_Abstract_Order;
16 use WCPOS\WooCommercePOS\Services\Settings;
17
18 /**
19 * Template_Router class.
20 */
21 class Template_Router {
22 /**
23 * Auth path slug for POS authorization endpoint.
24 */
25 public const AUTH_PATH = 'wcpos-auth';
26
27 /**
28 * Checkout path slug for POS checkout endpoints.
29 *
30 * @note 'wcpos-checkout' slug is used instead of 'checkout' to avoid conflicts
31 * with WC checkout, eg: x-frame-options: SAMEORIGIN
32 */
33 public const CHECKOUT_PATH = 'wcpos-checkout';
34
35 /**
36 * POS frontend slug.
37 *
38 * @var string
39 */
40 private $pos_regex;
41
42 /**
43 * POS login slug.
44 *
45 * @var string
46 */
47 private $pos_login_regex;
48
49 /**
50 * POS auth slug.
51 *
52 * @var string
53 */
54 private $pos_auth_regex;
55
56 /**
57 * POS checkout slug regex, see CHECKOUT_PATH.
58 *
59 * @var string
60 */
61 private $pos_checkout_regex;
62
63
64 /**
65 * Constructor.
66 */
67 public function __construct() {
68 $this->pos_regex = '^' . Admin\Permalink::get_slug() . '(/(.*))?/?$';
69 $this->pos_login_regex = '^wcpos-login/?';
70 $this->pos_auth_regex = '^' . self::AUTH_PATH . '/?';
71 $this->pos_checkout_regex = '^' . self::CHECKOUT_PATH . '/([a-z-]+)/([0-9]+)[/]?$';
72
73 $this->add_rewrite_rules();
74
75 add_filter( 'option_rewrite_rules', array( $this, 'rewrite_rules' ), 1 );
76 add_action( 'wp', array( $this, 'maybe_set_checkout_context' ), 1 );
77 add_action( 'template_redirect', array( $this, 'template_redirect' ), 1 );
78
79 // Priority 999 to ensure this filter runs after any other plugins that may hijack the order received url.
80 add_filter( 'woocommerce_get_checkout_order_received_url', array( $this, 'order_received_url' ), 999, 2 );
81 }
82
83 /**
84 * Get the full URL for the POS authorization endpoint.
85 *
86 * Respects the force_ssl setting, like wcpos_url(), so the login URL works
87 * when the site home URL is http but the POS is served over https. The
88 * trailing slash follows the site's permalink structure, via
89 * user_trailingslashit().
90 *
91 * @return string
92 */
93 public static function get_auth_url(): string {
94 return home_url( user_trailingslashit( self::AUTH_PATH ), Settings::instance()->url_scheme() );
95 }
96
97 /**
98 * Set checkout context early so payment gateways can detect the page.
99 *
100 * Many payment plugins (e.g. PayPal) check is_checkout() and is_checkout_pay_page()
101 * during the 'wp' action to decide whether to enqueue their assets. Our POS checkout
102 * uses custom rewrite rules, so WooCommerce doesn't recognize it as a checkout page
103 * by default. Adding the woocommerce_is_checkout filter here (before plugins run
104 * their checks) ensures is_checkout_pay_page() returns true for our payment template.
105 *
106 * @return void
107 */
108 public function maybe_set_checkout_context(): void {
109 /**
110 * WordPress environment instance.
111 *
112 * @var \WP $wp
113 */
114 global $wp;
115
116 if ( isset( $wp->query_vars['order-pay'] ) && $wp->matched_rule === $this->pos_checkout_regex ) {
117 add_filter( 'woocommerce_is_checkout', '__return_true' );
118 }
119 }
120
121 /**
122 * Make sure cache contains POS rewrite rules.
123 *
124 * @param array|bool $rules Rewrite rules.
125 *
126 * @return array|bool
127 */
128 public function rewrite_rules( $rules ) {
129 return isset( $rules[ $this->pos_regex ], $rules[ $this->pos_login_regex ], $rules[ $this->pos_auth_regex ], $rules[ $this->pos_checkout_regex ] ) ? $rules : false;
130 }
131
132 /**
133 * Output the matched template.
134 */
135 public function template_redirect(): void {
136 global $wp;
137
138 $rewrite_rules_to_templates = array(
139 $this->pos_regex => __NAMESPACE__ . '\\Templates\\Frontend',
140 $this->pos_login_regex => __NAMESPACE__ . '\\Templates\\Login',
141 $this->pos_auth_regex => __NAMESPACE__ . '\\Templates\\Auth',
142 $this->pos_checkout_regex => array(
143 'order-pay' => __NAMESPACE__ . '\\Templates\\Payment',
144 'order-received' => __NAMESPACE__ . '\\Templates\\Received',
145 'wcpos-receipt' => __NAMESPACE__ . '\\Templates\\Receipt',
146 ),
147 );
148
149 foreach ( $rewrite_rules_to_templates as $rule => $classname ) {
150 if ( $wp->matched_rule === $rule ) {
151 $this->clean_response_headers();
152
153 if ( \is_array( $classname ) ) {
154 $this->load_checkout_template( $classname );
155 } else {
156 $this->load_template( $classname );
157 }
158 exit;
159 }
160 }
161 }
162
163
164 /**
165 * Just like the checkout/payment.php template, we hijack the order received url so we can display a stripped down
166 * version of the receipt.
167 *
168 * @param string $order_received_url The order received URL.
169 * @param WC_Abstract_Order $order The order object.
170 *
171 * @return string
172 */
173 public function order_received_url( string $order_received_url, WC_Abstract_Order $order ): string {
174 global $wp;
175
176 // check is pos.
177 if ( ! woocommerce_pos_request() || ! isset( $wp->query_vars['order-pay'] ) ) {
178 return $order_received_url;
179 }
180
181 $redirect = add_query_arg(
182 array(
183 'key' => $order->get_order_key(),
184 ),
185 wcpos_checkout_url( 'order-received/' . $order->get_id() )
186 );
187
188 return $redirect;
189 }
190
191 /**
192 * Add rewrite rules for POS endpoints.
193 *
194 * @NOTE: 'order-pay' and 'order-received' rewrite tags are added by WC
195 *
196 * @return void
197 */
198 private function add_rewrite_rules(): void {
199 add_rewrite_tag( '%wcpos%', '([^&]+)' );
200 add_rewrite_tag( '%wcpos-receipt%', '([^&]+)' );
201 add_rewrite_tag( '%wcpos-login%', '([^&]+)' );
202 add_rewrite_tag( '%wcpos-auth%', '([^&]+)' );
203 add_rewrite_rule( $this->pos_regex, 'index.php?wcpos=1', 'top' );
204 add_rewrite_rule( $this->pos_login_regex, 'index.php?wcpos-login=1', 'top' );
205 add_rewrite_rule( $this->pos_auth_regex, 'index.php?wcpos-auth=1', 'top' );
206 add_rewrite_rule( $this->pos_checkout_regex, 'index.php?$matches[1]=$matches[2]&wcpos=1', 'top' );
207 }
208
209 /**
210 * Loads order templates, additionally checks query var is a valid order id.
211 *
212 * @param array $classnames Template class names keyed by query var.
213 *
214 * @return void
215 */
216 private function load_checkout_template( array $classnames ): void {
217 global $wp;
218
219 foreach ( $classnames as $query_var => $classname ) {
220 if ( isset( $wp->query_vars[ $query_var ] ) ) {
221 $order_id = absint( $wp->query_vars[ $query_var ] );
222
223 if ( class_exists( $classname ) && $order_id ) {
224 $template = new $classname( $order_id );
225 $template->get_template();
226
227 return;
228 }
229 }
230 }
231
232 wp_die( /* translators: Error message displayed when a template cannot be found. */ esc_html__( 'Template not found.', 'woocommerce-pos' ) );
233 }
234
235 /**
236 * Loads all other templates.
237 *
238 * @param string $classname The template class name.
239 *
240 * @return void
241 */
242 private function load_template( string $classname ): void {
243 if ( class_exists( $classname ) ) {
244 $template = new $classname();
245 $template->get_template();
246
247 return;
248 }
249
250 wp_die( /* translators: Error message displayed when a template cannot be found. */ esc_html__( 'Template not found.', 'woocommerce-pos' ) );
251 }
252
253 /**
254 * Remove Content-Security-Policy headers set by security plugins.
255 *
256 * Security plugins like LiteSpeed Cache and Wordfence can set restrictive CSP headers
257 * that block the POS from loading JavaScript and CSS bundles from cdn.jsdelivr.net.
258 * Since POS pages use custom templates (not the regular WordPress frontend), we strip
259 * these headers to prevent interference.
260 *
261 * @return void
262 */
263 private function clean_response_headers(): void {
264 if ( headers_sent() || ! \function_exists( 'header_remove' ) ) {
265 return;
266 }
267
268 header_remove( 'Content-Security-Policy' );
269 header_remove( 'Content-Security-Policy-Report-Only' );
270
271 /**
272 * Filters the Content-Security-Policy header value for POS pages.
273 *
274 * By default no CSP header is set, which avoids breaking payment gateway scripts
275 * that load from unpredictable domains. Return a non-empty policy string to set
276 * a custom CSP header.
277 *
278 * @since 1.9.0
279 *
280 * @param string $policy The CSP policy string. Default empty (no CSP set).
281 *
282 * @hook woocommerce_pos_content_security_policy
283 */
284 $policy = apply_filters( 'woocommerce_pos_content_security_policy', '' );
285
286 if ( ! empty( $policy ) ) {
287 header( 'Content-Security-Policy: ' . $policy );
288 }
289 }
290 }
291