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 / Templates / Frontend.php

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

399 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Frontend 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 WCPOS\WooCommercePOS\Services\Auth;
14 use WCPOS\WooCommercePOS\Services\Cashier;
15 use WCPOS\WooCommercePOS\Services\Lifecycle_Events;
16 use WCPOS\WooCommercePOS\Services\Settings;
17 use WCPOS\WooCommercePOS\Sync\Pos_Uuid;
18 use WCPOS\WooCommercePOS\Template_Router;
19 use const WCPOS\WooCommercePOS\PLUGIN_PATH;
20 use const WCPOS\WooCommercePOS\PLUGIN_URL;
21 use const WCPOS\WooCommercePOS\SHORT_NAME;
22 use const WCPOS\WooCommercePOS\VERSION;
23
24 /**
25 * Frontend class.
26 */
27 class Frontend {
28 /**
29 * Stores user credentials data for use in footer().
30 *
31 * @var array
32 */
33 /** Stores user credentials data for use in footer.
34 *
35 * @var array
36 */
37 private $wp_credentials = array();
38
39 /**
40 * Render the frontend template.
41 *
42 * @return void
43 */
44 public function get_template(): void {
45 // force ssl.
46 if ( ! is_ssl() && Settings::instance()->force_ssl_enabled() ) {
47 wp_safe_redirect( woocommerce_pos_url() );
48 exit;
49 }
50
51 // check auth.
52 if ( ! is_user_logged_in() ) {
53 add_filter( 'login_url', array( $this, 'login_url' ) );
54 auth_redirect();
55 }
56
57 // check privileges.
58 $user = wp_get_current_user();
59 if ( ! Cashier::instance()->can_open_pos( $user ) ) {
60 wp_die( esc_html( Cashier::instance()->missing_pos_capabilities_message( $user ) ), '', array( 'response' => 403 ) );
61 }
62
63 // disable cache plugins.
64 $this->no_cache();
65
66 // last chance before frontend template is rendered.
67 do_action( 'woocommerce_pos_frontend_template_redirect' );
68
69 /*
70 * Deprecated action.
71 *
72 * @TODO remove in 1.5.0
73 */
74 if ( has_action( 'woocommerce_pos_template_redirect' ) ) {
75 do_action_deprecated( 'woocommerce_pos_template_redirect', array(), 'Version_1.4.0', 'woocommerce_pos_frontend_template_redirect' );
76 }
77
78 // add head & footer actions.
79 add_action( 'woocommerce_pos_head', array( $this, 'head' ) );
80 add_action( 'woocommerce_pos_footer', array( $this, 'footer' ) );
81
82 // Generate user credentials BEFORE including template to ensure cookies can be set.
83 // The set_web_session_cookie() call in Auth::get_user_data() requires headers not yet sent.
84 $user = wp_get_current_user();
85 $auth_service = Auth::instance();
86 $this->wp_credentials = $auth_service->get_user_data( $user, true );
87
88 // The activation funnel's step the admin side cannot see: the POS itself
89 // being opened. Recorded here rather than by tracking the menu link, so
90 // a bookmark, a direct URL or a till that never touches wp-admin all
91 // count — and so it counts opens, not clicks that may never arrive.
92 //
93 // Everything above has already established that this is a logged-in user
94 // with `access_woocommerce_pos`, past the SSL redirect.
95 ( new Lifecycle_Events() )->report_app_opened();
96
97 include woocommerce_pos_locate_template( 'pos.php' );
98 exit;
99 }
100
101 /**
102 * Add variable to login url to signify POS login.
103 *
104 * @param string $login_url The login URL.
105 *
106 * @return mixed
107 */
108 public function login_url( $login_url ) {
109 return add_query_arg( SHORT_NAME, '1', $login_url );
110 }
111
112 /**
113 * Output the head scripts.
114 */
115 public function head(): void {
116 }
117
118 /**
119 * Output the footer scripts.
120 */
121 public function footer(): void {
122 /**
123 * Filters whether the POS is in development mode.
124 *
125 * When true, loads the web bundle from localhost instead of CDN.
126 * Useful for local development of the web application.
127 *
128 * @since 1.8.0
129 *
130 * @param bool $development Whether development mode is enabled.
131 * Defaults to checking WCPOS_DEVELOPMENT constant,
132 * then $_ENV['DEVELOPMENT'].
133 *
134 * @hook woocommerce_pos_development_mode
135 */
136 $development = apply_filters(
137 'woocommerce_pos_development_mode',
138 ( \defined( 'WCPOS_DEVELOPMENT' ) && WCPOS_DEVELOPMENT ) || ( isset( $_ENV['DEVELOPMENT'] ) && wp_validate_boolean( sanitize_text_field( wp_unslash( $_ENV['DEVELOPMENT'] ) ) ) )
139 );
140
141 $user = wp_get_current_user();
142
143 // Explicit web-bundle override (constant or env). Null when unset.
144 $explicit_bundle_ref = null;
145 $env_bundle_ref = getenv( 'WCPOS_WEB_BUNDLE_REF' );
146 if ( \defined( 'WCPOS_WEB_BUNDLE_REF' ) && WCPOS_WEB_BUNDLE_REF ) {
147 $explicit_bundle_ref = WCPOS_WEB_BUNDLE_REF;
148 } elseif ( ! empty( $_ENV['WCPOS_WEB_BUNDLE_REF'] ) ) {
149 $explicit_bundle_ref = sanitize_text_field( wp_unslash( $_ENV['WCPOS_WEB_BUNDLE_REF'] ) );
150 } elseif ( false !== $env_bundle_ref && '' !== $env_bundle_ref ) {
151 $explicit_bundle_ref = sanitize_text_field( wp_unslash( $env_bundle_ref ) );
152 } elseif ( ! empty( $_SERVER['WCPOS_WEB_BUNDLE_REF'] ) ) {
153 $explicit_bundle_ref = sanitize_text_field( wp_unslash( $_SERVER['WCPOS_WEB_BUNDLE_REF'] ) );
154 }
155
156 // Default to the plugin's own major.minor so the stable lane tracks the
157 // version automatically: a 1.9.x plugin loads `@1.9`, a 1.10.x plugin loads
158 // `@1.10`, etc. — no edit needed as versions roll.
159
160 /*
161 * One jsDelivr ref per lane, named after the lane (owner ruling, 2026-09-04):
162 * released lane → `@<major.minor>` (this default; the tag is cut at release)
163 * next lane → `@next` — the `next` BRANCH of wcpos/web-bundle IS the dev
164 * lane's tag. There is no versioned/prerelease tag for `next`.
165 * dev-next sets WCPOS_WEB_BUNDLE_REF=next to load it.
166 */
167 // When `next` becomes `main`, the released ref simply becomes the new
168 // major.minor (e.g. `@1.11`) via this default.
169 $default_bundle_ref = implode( '.', \array_slice( explode( '.', VERSION ), 0, 2 ) );
170
171 /**
172 * The web-bundle ref served from jsDelivr (or a full base URL).
173 *
174 * Override via the WCPOS_WEB_BUNDLE_REF constant / env var or this filter to
175 * point a site at another lane for testing the in-development build locally
176 * or on staging: the `next` lane's ref is the branch `next`
177 * (https://cdn.jsdelivr.net/gh/wcpos/web-bundle@next); a tag, a commit, or a
178 * full base URL (anything containing `://`, e.g. a local dev server or an EAS
179 * preview) also work.
180 *
181 * @hook woocommerce_pos_web_bundle_ref
182 */
183 $bundle_ref = (string) apply_filters( 'woocommerce_pos_web_bundle_ref', $explicit_bundle_ref ?? $default_bundle_ref );
184 $bundle_ref = trim( $bundle_ref );
185 if ( '' === $bundle_ref ) {
186 $bundle_ref = $default_bundle_ref;
187 }
188 $bundle_overridden = $bundle_ref !== $default_bundle_ref;
189
190 // No trailing slash: Metro's runtime concatenates `cdnBaseUrl` with leading-slash paths
191 // (`/_expo/...`, `/assets/...`); a trailing slash here would produce `//`, which jsDelivr
192 // 301-redirects with a year-long cache, breaking lazy chunk loads in the browser.
193 if ( false !== strpos( (string) $bundle_ref, '://' ) ) {
194 // Full base URL (local dev server, EAS preview, etc.).
195 $cdn_base_url = rtrim( $bundle_ref, '/' );
196 } elseif ( $development && ! $bundle_overridden ) {
197 // Development default: the local web build server.
198 $cdn_base_url = 'http://localhost:4567/build';
199 } else {
200 // jsDelivr web-bundle lane (e.g. `1.9`, `1.10`, `next`, a tag or commit).
201 $cdn_base_url = 'https://cdn.jsdelivr.net/gh/wcpos/web-bundle@' . rawurlencode( $bundle_ref ) . '/build';
202 }
203 $wcpos_base_path = rtrim( wp_parse_url( woocommerce_pos_url(), PHP_URL_PATH ), '/' );
204 $stores = array_map(
205 function ( $store ) {
206 return $store->get_data();
207 },
208 wcpos_get_stores()
209 );
210
211 $site_uuid = wcpos_get_site_uuid();
212 $opfs_worker_hash = hash_file( 'sha256', PLUGIN_PATH . 'assets/js/opfs.worker.js' );
213 if ( false === $opfs_worker_hash ) {
214 $opfs_worker_hash = VERSION;
215 }
216
217 // Pos_Uuid is the sole authority for `_woocommerce_pos_uuid`: the value here
218 // must match what /cashier and /customers serve, or the client forks identities.
219 $user_uuid = Pos_Uuid::ensure_user_uuid( $user );
220
221 $vars = array(
222 'version' => VERSION,
223 'manifest' => $cdn_base_url . '/metadata.json?v=' . $opfs_worker_hash,
224 'homepage' => woocommerce_pos_url(),
225 'logout_url' => $this->pos_logout_url(),
226 'site' => array(
227 'uuid' => $site_uuid,
228 'url' => get_option( 'siteurl' ),
229 'name' => get_option( 'blogname' ),
230 'description' => get_option( 'blogdescription' ),
231 'home' => home_url(),
232 'gmt_offset' => get_option( 'gmt_offset' ),
233 'timezone_string' => get_option( 'timezone_string' ),
234 'wp_version' => get_bloginfo( 'version' ),
235 'wc_version' => WC()->version,
236 'wcpos_version' => VERSION,
237 'wp_api_url' => get_rest_url(),
238 'wc_api_url' => trailingslashit( get_rest_url( null, 'wc/v3' ) ),
239 'wcpos_api_url' => trailingslashit( get_rest_url( null, 'wcpos/v2' ) ),
240 'wcpos_login_url' => Template_Router::get_auth_url(),
241 'locale' => get_locale(),
242 ),
243 'wp_credentials' => $this->wp_credentials,
244 'stores' => $stores,
245 );
246
247 /**
248 * Filters the javascript variables passed to the POS.
249 *
250 * @param array $vars
251 *
252 * @returns array $vars
253 *
254 * @since 1.0.0
255 *
256 * @hook woocommerce_pos_inline_vars
257 */
258 $vars = apply_filters( 'woocommerce_pos_inline_vars', $vars );
259 $initial_props = wp_json_encode( $vars );
260 $cdn_base_url = wp_json_encode( $cdn_base_url );
261
262 /**
263 * Add path to worker scripts.
264 */
265 $idb_worker = PLUGIN_URL . 'assets/js/indexeddb.worker.js';
266 $opfs_worker = add_query_arg(
267 'ver',
268 $opfs_worker_hash,
269 PLUGIN_URL . 'assets/js/opfs.worker.js'
270 );
271
272 // getScript helper and initialProps.
273 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline JavaScript for POS frontend
274 echo "<script>
275 function getScript(source, callback, onError) {
276 var script = document.createElement('script');
277 script.async = true;
278 script.onload = script.onreadystatechange = function(_, isAbort) {
279 if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
280 script.onload = script.onreadystatechange = null;
281 script = undefined;
282 if (!isAbort && callback) setTimeout(callback, 0);
283 }
284 };
285 script.onerror = function() {
286 script.onload = script.onreadystatechange = null;
287 if (onError) onError(new Error('Failed to load script: ' + source));
288 };
289 script.src = source;
290 document.head.appendChild(script);
291 }
292
293 function loadCSS(source, callback) {
294 var link = document.createElement('link');
295 link.rel = 'stylesheet';
296 link.href = source;
297 link.onload = function() {
298 if (callback) callback();
299 };
300 link.onerror = function() {
301 console.error('Failed to load CSS file:', source);
302 };
303 document.head.appendChild(link);
304 }
305
306 var idbWorker = '{$idb_worker}';
307 var opfsWorker = '{$opfs_worker}';
308 var initialProps = {$initial_props};
309 var cdnBaseUrl = {$cdn_base_url};
310 var baseUrl = '{$wcpos_base_path}';
311 </script>" . "\n";
312
313 echo "<script>
314 // no-cache: revalidate the manifest with the CDN (ETag/304) on every boot.
315 // jsDelivr serves it with max-age=604800 and the ?v= buster only changes on
316 // plugin deploys, so a default fetch pins users to a stale bundle for up to
317 // 7 days after a web-bundle publish.
318 var request = new Request(initialProps.manifest, { cache: 'no-cache' });
319
320 window.fetch(request)
321 .then(function(response) { return response.json(); })
322 .then(function(data) {
323 // v1 metadata uses 'bundles' array (metro runtime, common, entry)
324 // v0 fallback uses single 'bundle' string
325 var webMeta = (data && data.fileMetadata && data.fileMetadata.web) || {};
326 var bundles = Array.isArray(webMeta.bundles)
327 ? webMeta.bundles.filter(Boolean)
328 : (webMeta.bundle ? [webMeta.bundle] : []);
329
330 if (!bundles.length) {
331 throw new Error('No JavaScript bundles declared in metadata.json');
332 }
333
334 function loadBundles(index) {
335 if (index >= bundles.length) return;
336 var source = cdnBaseUrl + '/' + bundles[index];
337 getScript(source, function() {
338 loadBundles(index + 1);
339 }, function(error) {
340 console.error(error.message);
341 });
342 }
343
344 if (data.fileMetadata.web.css) {
345 loadCSS(cdnBaseUrl + '/' + data.fileMetadata.web.css, function() {
346 loadBundles(0);
347 });
348 } else {
349 loadBundles(0);
350 }
351 })
352 .catch(function(error) {
353 console.error('Error fetching manifest:', error);
354 });
355 </script>" . "\n";
356 }
357
358 /**
359 * Get the POS logout URL.
360 *
361 * @return string
362 */
363 private function pos_logout_url() {
364 /**
365 * Get the login URL, allow other plugins to customise the URL. eg: WPS Hide Login.
366 */
367 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook
368 $login_url = apply_filters( 'login_url', site_url( '/wp-login.php' ), 'logout', false );
369
370 $redirect_to = urlencode( woocommerce_pos_url() );
371 $reauth = 1;
372 $wcpos = 1;
373 $logout_nonce = wp_create_nonce( 'log-out' );
374
375 return "{$login_url}?action=logout&_wpnonce={$logout_nonce}&redirect_to={$redirect_to}&reauth={$reauth}&wcpos={$wcpos}";
376 }
377
378
379
380
381 /**
382 * Disable caching conflicts.
383 */
384 private function no_cache(): void {
385 // disable W3 Total Cache minify.
386 if ( ! \defined( 'DONOTMINIFY' ) ) {
387 \define( 'DONOTMINIFY', 'true' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Third-party constant
388 }
389
390 // disable WP Super Cache.
391 if ( ! \defined( 'DONOTCACHEPAGE' ) ) {
392 \define( 'DONOTCACHEPAGE', 'true' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Third-party constant
393 }
394
395 // disable Lite Speed Cache.
396 do_action( 'litespeed_control_set_nocache', 'nocache WoCommerce POS web application' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party hook
397 }
398 }
399