PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.3
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.3
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.3, at includes/Templates/Frontend.php

386 lines 13.4 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 $default_bundle_ref = implode( '.', \array_slice( explode( '.', VERSION ), 0, 2 ) );
159
160 /**
161 * The web-bundle ref served from jsDelivr (or a full base URL).
162 *
163 * Override via the WCPOS_WEB_BUNDLE_REF constant / env var or this filter to
164 * point a site at another lane for testing the in-development build locally
165 * or on staging: a branch (e.g. `next`), a tag, a commit, or a full base URL
166 * (anything containing `://`, e.g. a local dev server or an EAS preview).
167 *
168 * @hook woocommerce_pos_web_bundle_ref
169 */
170 $bundle_ref = (string) apply_filters( 'woocommerce_pos_web_bundle_ref', $explicit_bundle_ref ?? $default_bundle_ref );
171 $bundle_ref = trim( $bundle_ref );
172 if ( '' === $bundle_ref ) {
173 $bundle_ref = $default_bundle_ref;
174 }
175 $bundle_overridden = $bundle_ref !== $default_bundle_ref;
176
177 // No trailing slash: Metro's runtime concatenates `cdnBaseUrl` with leading-slash paths
178 // (`/_expo/...`, `/assets/...`); a trailing slash here would produce `//`, which jsDelivr
179 // 301-redirects with a year-long cache, breaking lazy chunk loads in the browser.
180 if ( false !== strpos( (string) $bundle_ref, '://' ) ) {
181 // Full base URL (local dev server, EAS preview, etc.).
182 $cdn_base_url = rtrim( $bundle_ref, '/' );
183 } elseif ( $development && ! $bundle_overridden ) {
184 // Development default: the local web build server.
185 $cdn_base_url = 'http://localhost:4567/build';
186 } else {
187 // jsDelivr web-bundle lane (e.g. `1.9`, `1.10`, `next`, a tag or commit).
188 $cdn_base_url = 'https://cdn.jsdelivr.net/gh/wcpos/web-bundle@' . rawurlencode( $bundle_ref ) . '/build';
189 }
190 $wcpos_base_path = rtrim( wp_parse_url( woocommerce_pos_url(), PHP_URL_PATH ), '/' );
191 $stores = array_map(
192 function ( $store ) {
193 return $store->get_data();
194 },
195 wcpos_get_stores()
196 );
197
198 $site_uuid = wcpos_get_site_uuid();
199 $opfs_worker_hash = hash_file( 'sha256', PLUGIN_PATH . 'assets/js/opfs.worker.js' );
200 if ( false === $opfs_worker_hash ) {
201 $opfs_worker_hash = VERSION;
202 }
203
204 // Pos_Uuid is the sole authority for `_woocommerce_pos_uuid`: the value here
205 // must match what /cashier and /customers serve, or the client forks identities.
206 $user_uuid = Pos_Uuid::ensure_user_uuid( $user );
207
208 $vars = array(
209 'version' => VERSION,
210 'manifest' => $cdn_base_url . '/metadata.json?v=' . $opfs_worker_hash,
211 'homepage' => woocommerce_pos_url(),
212 'logout_url' => $this->pos_logout_url(),
213 'site' => array(
214 'uuid' => $site_uuid,
215 'url' => get_option( 'siteurl' ),
216 'name' => get_option( 'blogname' ),
217 'description' => get_option( 'blogdescription' ),
218 'home' => home_url(),
219 'gmt_offset' => get_option( 'gmt_offset' ),
220 'timezone_string' => get_option( 'timezone_string' ),
221 'wp_version' => get_bloginfo( 'version' ),
222 'wc_version' => WC()->version,
223 'wcpos_version' => VERSION,
224 'wp_api_url' => get_rest_url(),
225 'wc_api_url' => trailingslashit( get_rest_url( null, 'wc/v3' ) ),
226 'wcpos_api_url' => trailingslashit( get_rest_url( null, 'wcpos/v2' ) ),
227 'wcpos_login_url' => Template_Router::get_auth_url(),
228 'locale' => get_locale(),
229 ),
230 'wp_credentials' => $this->wp_credentials,
231 'stores' => $stores,
232 );
233
234 /**
235 * Filters the javascript variables passed to the POS.
236 *
237 * @param array $vars
238 *
239 * @returns array $vars
240 *
241 * @since 1.0.0
242 *
243 * @hook woocommerce_pos_inline_vars
244 */
245 $vars = apply_filters( 'woocommerce_pos_inline_vars', $vars );
246 $initial_props = wp_json_encode( $vars );
247 $cdn_base_url = wp_json_encode( $cdn_base_url );
248
249 /**
250 * Add path to worker scripts.
251 */
252 $idb_worker = PLUGIN_URL . 'assets/js/indexeddb.worker.js';
253 $opfs_worker = add_query_arg(
254 'ver',
255 $opfs_worker_hash,
256 PLUGIN_URL . 'assets/js/opfs.worker.js'
257 );
258
259 // getScript helper and initialProps.
260 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline JavaScript for POS frontend
261 echo "<script>
262 function getScript(source, callback, onError) {
263 var script = document.createElement('script');
264 script.async = true;
265 script.onload = script.onreadystatechange = function(_, isAbort) {
266 if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
267 script.onload = script.onreadystatechange = null;
268 script = undefined;
269 if (!isAbort && callback) setTimeout(callback, 0);
270 }
271 };
272 script.onerror = function() {
273 script.onload = script.onreadystatechange = null;
274 if (onError) onError(new Error('Failed to load script: ' + source));
275 };
276 script.src = source;
277 document.head.appendChild(script);
278 }
279
280 function loadCSS(source, callback) {
281 var link = document.createElement('link');
282 link.rel = 'stylesheet';
283 link.href = source;
284 link.onload = function() {
285 if (callback) callback();
286 };
287 link.onerror = function() {
288 console.error('Failed to load CSS file:', source);
289 };
290 document.head.appendChild(link);
291 }
292
293 var idbWorker = '{$idb_worker}';
294 var opfsWorker = '{$opfs_worker}';
295 var initialProps = {$initial_props};
296 var cdnBaseUrl = {$cdn_base_url};
297 var baseUrl = '{$wcpos_base_path}';
298 </script>" . "\n";
299
300 echo "<script>
301 // no-cache: revalidate the manifest with the CDN (ETag/304) on every boot.
302 // jsDelivr serves it with max-age=604800 and the ?v= buster only changes on
303 // plugin deploys, so a default fetch pins users to a stale bundle for up to
304 // 7 days after a web-bundle publish.
305 var request = new Request(initialProps.manifest, { cache: 'no-cache' });
306
307 window.fetch(request)
308 .then(function(response) { return response.json(); })
309 .then(function(data) {
310 // v1 metadata uses 'bundles' array (metro runtime, common, entry)
311 // v0 fallback uses single 'bundle' string
312 var webMeta = (data && data.fileMetadata && data.fileMetadata.web) || {};
313 var bundles = Array.isArray(webMeta.bundles)
314 ? webMeta.bundles.filter(Boolean)
315 : (webMeta.bundle ? [webMeta.bundle] : []);
316
317 if (!bundles.length) {
318 throw new Error('No JavaScript bundles declared in metadata.json');
319 }
320
321 function loadBundles(index) {
322 if (index >= bundles.length) return;
323 var source = cdnBaseUrl + '/' + bundles[index];
324 getScript(source, function() {
325 loadBundles(index + 1);
326 }, function(error) {
327 console.error(error.message);
328 });
329 }
330
331 if (data.fileMetadata.web.css) {
332 loadCSS(cdnBaseUrl + '/' + data.fileMetadata.web.css, function() {
333 loadBundles(0);
334 });
335 } else {
336 loadBundles(0);
337 }
338 })
339 .catch(function(error) {
340 console.error('Error fetching manifest:', error);
341 });
342 </script>" . "\n";
343 }
344
345 /**
346 * Get the POS logout URL.
347 *
348 * @return string
349 */
350 private function pos_logout_url() {
351 /**
352 * Get the login URL, allow other plugins to customise the URL. eg: WPS Hide Login.
353 */
354 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook
355 $login_url = apply_filters( 'login_url', site_url( '/wp-login.php' ), 'logout', false );
356
357 $redirect_to = urlencode( woocommerce_pos_url() );
358 $reauth = 1;
359 $wcpos = 1;
360 $logout_nonce = wp_create_nonce( 'log-out' );
361
362 return "{$login_url}?action=logout&_wpnonce={$logout_nonce}&redirect_to={$redirect_to}&reauth={$reauth}&wcpos={$wcpos}";
363 }
364
365
366
367
368 /**
369 * Disable caching conflicts.
370 */
371 private function no_cache(): void {
372 // disable W3 Total Cache minify.
373 if ( ! \defined( 'DONOTMINIFY' ) ) {
374 \define( 'DONOTMINIFY', 'true' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Third-party constant
375 }
376
377 // disable WP Super Cache.
378 if ( ! \defined( 'DONOTCACHEPAGE' ) ) {
379 \define( 'DONOTCACHEPAGE', 'true' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Third-party constant
380 }
381
382 // disable Lite Speed Cache.
383 do_action( 'litespeed_control_set_nocache', 'nocache WoCommerce POS web application' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Third-party hook
384 }
385 }
386