PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.9
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.9
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 / Templates / Frontend.php

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

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