PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.16
1.10.19 1.10.18 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 All 163 releases
woocommerce-pos / includes / Init.php

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

376 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Load required classes.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see http://wcpos.com
8 * @package WCPOS\WooCommercePOS
9 */
10
11 namespace WCPOS\WooCommercePOS;
12
13 use WCPOS\WooCommercePOS\Admin\Consent;
14 use WCPOS\WooCommercePOS\Admin\Menu;
15 use WCPOS\WooCommercePOS\Services\Auth as AuthService;
16 use WCPOS\WooCommercePOS\Services\Extensions;
17 use WCPOS\WooCommercePOS\Services\Receipt_Snapshot_Store;
18 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
19 use WP_HTTP_Response;
20 use WP_REST_Request;
21 use WP_REST_Server;
22 use const DOING_AJAX;
23
24 /**
25 * Init class.
26 */
27 class Init {
28 /**
29 * Constructor.
30 */
31 public function __construct() {
32 // global helper functions.
33 require_once PLUGIN_PATH . 'includes/wcpos-functions.php';
34 require_once PLUGIN_PATH . 'includes/wcpos-store-functions.php';
35
36 // Tracking consent pop-up + callout. Registered here (during
37 // plugins_loaded) so its lifecycle hooks (activated_plugin,
38 // upgrader_process_complete) are in place before those actions
39 // fire on a plugin activation or update request.
40 new Consent();
41
42 // Init hooks.
43 add_action( 'init', array( $this, 'init' ) );
44 add_action( 'rest_api_init', array( $this, 'init_rest_api' ), 20 );
45 add_filter( 'query_vars', array( $this, 'query_vars' ) );
46
47 // Headers for API discoverability.
48 add_filter( 'rest_pre_serve_request', array( $this, 'rest_pre_serve_request' ), 5, 4 );
49 add_action( 'send_headers', array( $this, 'send_headers' ), 99, 1 );
50 add_action( 'send_headers', array( $this, 'remove_x_frame_options' ), 9999, 1 );
51
52 /*
53 * Add JWT authentication filter.
54 *
55 * Hook order: plugins_loaded -> init (determine_current_user) -> rest_api_init
56 *
57 * This filter runs at priority 20 (after WordPress core's cookie auth at priority 10).
58 * It must be registered here (during plugins_loaded) because determine_current_user
59 * fires during 'init', which is BEFORE rest_api_init where our API class loads.
60 */
61 add_filter( 'determine_current_user', array( $this, 'determine_current_user_early' ), 20 );
62 }
63
64 /**
65 * Early authentication check for JWT tokens.
66 *
67 * This runs BEFORE rest_api_init, so we can authenticate users before WP REST API
68 * permission callbacks run. This is especially important for authorization via
69 * query parameter (?authorization=Bearer...) which some servers require.
70 *
71 * Note: We don't check for X-WCPOS header here because:
72 * 1. The header check uses getallheaders() which may not work in all environments
73 * 2. JWT authentication should work regardless - the token itself is proof of WCPOS usage
74 * 3. Invalid tokens (non-WCPOS) will fail validation anyway
75 *
76 * @param false|int $user_id User ID if one has been determined, false otherwise.
77 *
78 * @return false|int User ID if authenticated, original value otherwise.
79 */
80 public function determine_current_user_early( $user_id ) {
81 // Skip if user already authenticated.
82 if ( ! empty( $user_id ) ) {
83 return $user_id;
84 }
85
86 // Check for authorization token (header or param).
87 $auth_header = $this->get_auth_header_early();
88 if ( ! \is_string( $auth_header ) || empty( $auth_header ) ) {
89 return $user_id;
90 }
91
92 // Extract Bearer token.
93 list( $token ) = sscanf( $auth_header, 'Bearer %s' );
94 if ( ! $token ) {
95 return $user_id;
96 }
97
98 // Validate token - this will fail for non-WCPOS tokens.
99 $auth_service = AuthService::instance();
100 $decoded_token = $auth_service->validate_token( $token );
101
102 if ( is_wp_error( $decoded_token ) ) {
103 return $user_id;
104 }
105
106 // Return the authenticated user ID.
107 return absint( $decoded_token->data->user->id );
108 }
109
110 /**
111 * Load the required resources.
112 */
113 public function init(): void {
114 $this->init_common();
115 $this->init_frontend();
116 $this->init_admin();
117 $this->init_integrations();
118 }
119
120 /**
121 * Loads the POS API and duck punches the WC REST API.
122 */
123 public function init_rest_api(): void {
124 $is_wcpos_request = woocommerce_pos_request();
125
126 if ( $is_wcpos_request ) {
127 new API();
128 } else {
129 // Queue the registration at a later priority of the SAME
130 // rest_api_init pass this method runs on (priority 20), so
131 // register_rest_route() executes during the action as WP requires.
132 // When this method is called outside the action (tests), the
133 // add_action is simply inert.
134 add_action( 'rest_api_init', array( $this, 'register_public_relay_routes' ), 30 );
135 $this->log_unmarked_wcpos_rest_request();
136 new WC_API();
137 }
138 }
139
140 /**
141 * Register the relay's public consent-callback route for unmarked requests.
142 *
143 * The WCPOS Cloud Print relay proves site consent by fetching
144 * print-jobs/relay-verification WITHOUT the WCPOS request marker, so this
145 * single public route must exist even when the full WCPOS API is not
146 * loaded. Everything else stays behind the marker.
147 */
148 public function register_public_relay_routes(): void {
149 register_rest_route(
150 SHORT_NAME . '/v1',
151 '/print-jobs/relay-verification',
152 array(
153 'methods' => 'GET',
154 'callback' => array( new API\Print_Jobs_Controller(), 'relay_verification' ),
155 'permission_callback' => '__return_true',
156 )
157 );
158 }
159
160 /**
161 * Log requests for a WCPOS namespace that omitted the required request marker.
162 *
163 * This runs before WCPOS routes are registered, so it captures the otherwise
164 * silent rest_no_route response. Warnings are limited by API version to avoid
165 * allowing repeated unauthenticated requests to flood WooCommerce logs.
166 */
167 private function log_unmarked_wcpos_rest_request(): void {
168 global $wp;
169
170 $route = isset( $wp->query_vars['rest_route'] )
171 ? '/' . ltrim( sanitize_text_field( wp_unslash( (string) $wp->query_vars['rest_route'] ) ), '/' )
172 : '';
173
174 if ( 1 !== preg_match( '#^/wcpos/v([12])(?:/|$)#', $route, $matches ) ) {
175 return;
176 }
177
178 // The relay's consent callback is expected unmarked traffic (see
179 // register_public_relay_routes()), not a misconfigured client.
180 if ( '/wcpos/v1/print-jobs/relay-verification' === $route ) {
181 return;
182 }
183
184 $transient = 'wcpos_missing_request_marker_v' . $matches[1];
185 if ( false !== get_transient( $transient ) ) {
186 return;
187 }
188
189 set_transient( $transient, 1, 5 * MINUTE_IN_SECONDS );
190 Logger::warning( $route . ': missing WCPOS request marker.' );
191 }
192
193 /**
194 * Adds 'wcpos' to the query variables allowed before processing.
195 *
196 * Allows (publicly allowed) query vars to be added, removed, or changed prior
197 * to executing the query. Needed to allow custom rewrite rules using your own arguments
198 * to work, or any other custom query variables you want to be publicly available.
199 *
200 * @param string[] $query_vars The array of allowed query variable names.
201 *
202 * @return string[] The array of allowed query variable names.
203 */
204 public function query_vars( array $query_vars ): array {
205 $query_vars[] = SHORT_NAME;
206
207 return $query_vars;
208 }
209
210 /**
211 * Allow pre-flight requests from WCPOS Desktop and Mobile Apps
212 * Note: pre-flight requests cannot have headers, so I can't filter by pos request
213 * See: https://fetch.spec.whatwg.org/#cors-preflight-fetch.
214 *
215 * @param bool $served Whether the request has already been served.
216 * Default false.
217 * @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`.
218 * @param WP_REST_Request $request Request used to generate the response.
219 * @param WP_REST_Server $server Server instance.
220 *
221 * @return bool $served
222 */
223 public function rest_pre_serve_request( $served, WP_HTTP_Response $result, WP_REST_Request $request, WP_REST_Server $server ) {
224 if ( 'OPTIONS' == $request->get_method() ) {
225 $allow_headers = array(
226 'Authorization', // For user-agent authentication with a server.
227 'X-WP-Nonce', // WordPress-specific header, used for CSRF protection.
228 'Content-Disposition', // Informs how to process the response data.
229 'Content-MD5', // For verifying data integrity.
230 'Content-Type', // Specifies the media type of the resource.
231 'X-HTTP-Method-Override', // Used to override the HTTP method.
232 'X-WCPOS', // Used to identify WCPOS requests.
233 );
234
235 $server->send_header( 'Access-Control-Allow-Origin', '*' );
236 $server->send_header( 'Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE' );
237 $server->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );
238 }
239
240 return $served;
241 }
242
243 /**
244 * Allow HEAD checks for WP API Link URL and server uptime
245 * Fires once the requested HTTP headers for caching, content type, etc. have been sent.
246 *
247 * FIXME: Why is Link header not exposed sometimes on my development machine?
248 *
249 * @return void
250 */
251 public function send_headers(): void {
252 // some server convert HEAD to GET method, so use this query param instead.
253 if ( isset( $_GET['_method'] ) && 'head' === strtolower( sanitize_text_field( wp_unslash( $_GET['_method'] ) ) ) ) {
254 header( 'Access-Control-Allow-Origin: *' );
255 header( 'Access-Control-Expose-Headers: Link' );
256 }
257 }
258
259 /**
260 * Some security plugins will set X-Frame-Options: SAMEORIGIN/DENY, which will prevent the POS desktop
261 * application from opening pages like the login in an iframe.
262 *
263 * For pages we need, we will remove the X-Frame-Options header.
264 *
265 * @param mixed $wp The WP object.
266 *
267 * @return void
268 */
269 public function remove_x_frame_options( $wp ): void {
270 if ( woocommerce_pos_request() || isset( $wp->query_vars['wcpos-login'] ) ) {
271 if ( ! headers_sent() && \function_exists( 'header_remove' ) ) {
272 header_remove( 'X-Frame-Options' );
273 }
274 }
275 }
276
277 /**
278 * Get authorization header/param value.
279 *
280 * Checks multiple sources for the authorization token:
281 * 1. HTTP_AUTHORIZATION server variable (standard)
282 * 2. REDIRECT_HTTP_AUTHORIZATION (Apache CGI workaround)
283 * 3. authorization query parameter (for servers that strip auth headers)
284 *
285 * @return false|string The authorization value or false if not found.
286 */
287 private function get_auth_header_early() {
288 // Check HTTP_AUTHORIZATION (not empty - htaccess SetEnvIf can set empty value).
289 if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
290 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
291 }
292
293 // Check REDIRECT_HTTP_AUTHORIZATION (Apache CGI).
294 if ( ! empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
295 return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
296 }
297
298 // Check authorization query param.
299 if ( ! empty( $_GET['authorization'] ) ) {
300 return sanitize_text_field( wp_unslash( $_GET['authorization'] ) );
301 }
302
303 return false;
304 }
305
306 /**
307 * Common initializations.
308 */
309 private function init_common(): void {
310 // init the Services.
311 SettingsService::instance();
312 AuthService::instance();
313 Extensions::instance();
314 Receipt_Snapshot_Store::instance();
315
316 // init other functionality needed by both frontend and admin.
317 new i18n();
318 new Gateways();
319 new Products();
320 new Orders();
321 new Emails();
322 new Templates();
323 new Services\Print_Job_Service();
324 new Services\Cloud_Print_Trigger_Service();
325 new Services\Cloud_Print_Submit_Service();
326 new Services\Cloud_Print_Relay_Service();
327 }
328
329 /**
330 * Frontend specific initializations.
331 */
332 private function init_frontend(): void {
333 if ( ! is_admin() ) {
334 new Template_Router();
335 new Form_Handler();
336 new Storefront_Receipts();
337 }
338 }
339
340 /**
341 * Admin specific initializations.
342 */
343 private function init_admin(): void {
344 if ( is_admin() ) {
345 // Register AJAX handler before the branch so it's available during AJAX requests.
346 add_action( 'wp_ajax_wcpos_track_upgrade_click_ajax', array( Menu::class, 'handle_upgrade_click_ajax' ) );
347 add_action( 'admin_post_wcpos_track_upgrade_click', array( Menu::class, 'handle_upgrade_click_redirect' ) );
348
349 if ( \defined( 'DOING_AJAX' ) && DOING_AJAX ) {
350 new AJAX();
351 } else {
352 new Admin();
353 }
354 }
355 }
356
357 /**
358 * Integrations.
359 */
360 private function init_integrations(): void {
361 // WooCommerce Bookings - http://www.woothemes.com/products/woocommerce-bookings/
362 // if ( class_exists( 'WC-Bookings' ) ) {
363 // new Integrations\Bookings();
364 // }.
365
366 // Yoast SEO - https://wordpress.org/plugins/wordpress-seo/.
367 if ( class_exists( 'WPSEO_Options' ) ) {
368 new Integrations\WPSEO();
369 }
370
371 // wePOS alters the WooCommerce REST API, breaking the expected schema
372 // It's very bad form on their part, but we need to work around it.
373 new Integrations\WePOS();
374 }
375 }
376