1212defined ('WPINC ' ) || exit ();
1313
1414/**
15- * Provides compatibility with WCML for currency handling.
15+ * Provides compatibility with WCML for currency handling and crawler currency simulation .
1616 */
1717class WCML {
1818
19+ const OPT_PICK = 'litespeed_wcml_crawler_currencies ' ;
20+
1921 /**
20- * Holds the current WCML currency.
22+ * Currently resolved client currency.
2123 *
2224 * @var string
2325 */
@@ -37,6 +39,16 @@ public static function detect() {
3739
3840 add_filter ('wcml_client_currency ' , __CLASS__ . '::apply_client_currency ' );
3941 add_action ('wcml_set_client_currency ' , __CLASS__ . '::set_client_currency ' );
42+ add_filter ('litespeed_vary ' , __CLASS__ . '::apply_vary ' );
43+ add_filter ('litespeed_crawler_cookies ' , __CLASS__ . '::inject_vary_row ' );
44+
45+ self ::_detect_crawler_currency ();
46+
47+ // `litespeed_update_confs` piggybacks on LSCWP's nonce + cap verification.
48+ if (is_admin ()) {
49+ add_action ('litespeed_crawler_cookies_after ' , __CLASS__ . '::render_picker ' );
50+ add_action ('litespeed_update_confs ' , __CLASS__ . '::_handle_save ' );
51+ }
4052 }
4153
4254 /**
@@ -53,7 +65,7 @@ public static function set_client_currency( $currency ) {
5365 }
5466
5567 /**
56- * Applies the client currency and adjusts vary accordingly .
68+ * Applies the client currency.
5769 *
5870 * @since 3.0
5971 * @access public
@@ -62,22 +74,228 @@ public static function set_client_currency( $currency ) {
6274 */
6375 public static function apply_client_currency ( $ currency ) {
6476 self ::$ _currency = $ currency ;
65- add_filter ('litespeed_vary ' , __CLASS__ . '::apply_vary ' );
66-
6777 return $ currency ;
6878 }
6979
7080 /**
71- * Appends WCML currency to vary list.
81+ * Appends WCML currency to the vary list.
7282 *
7383 * @since 3.0
7484 * @access public
7585 * @param array $vary_list The existing vary list.
7686 * @return array The updated vary list including WCML currency.
7787 */
7888 public static function apply_vary ( $ vary_list ) {
89+ if (empty (self ::$ _currency )) {
90+ global $ woocommerce_wpml ;
91+ if (is_object ($ woocommerce_wpml )
92+ && isset ($ woocommerce_wpml ->multi_currency )
93+ && method_exists ($ woocommerce_wpml ->multi_currency , 'get_client_currency ' )
94+ ) {
95+ self ::$ _currency = $ woocommerce_wpml ->multi_currency ->get_client_currency ();
96+ }
97+ if (empty (self ::$ _currency )) {
98+ self ::$ _currency = get_option ('woocommerce_currency ' , 'USD ' );
99+ }
100+ }
101+
79102 $ vary_list ['wcml_currency ' ] = self ::$ _currency ;
80-
81103 return $ vary_list ;
82104 }
105+
106+ /**
107+ * Inject the WCML vary row into the crawler cookie list.
108+ *
109+ * @since 7.9
110+ * @access public
111+ * @param mixed $cookies Crawler cookie list.
112+ * @return array
113+ */
114+ public static function inject_vary_row ( $ cookies ) {
115+ if (!is_array ($ cookies )) {
116+ $ cookies = [];
117+ }
118+
119+ $ currencies = self ::_currencies ();
120+ $ selected = count ($ currencies ) >= 2 ? self ::_selected ($ currencies ) : [];
121+ if (empty ($ selected )) {
122+ return $ cookies ;
123+ }
124+
125+ $ vals = [];
126+ foreach ($ selected as $ code ) {
127+ $ h = self ::_vary_hash ($ code );
128+ if (!empty ($ h )) {
129+ $ vals [] = $ h ;
130+ }
131+ }
132+ if (empty ($ vals )) {
133+ return $ cookies ;
134+ }
135+
136+ $ vary_name = \LiteSpeed \Vary::cls ()->get_vary_name ();
137+
138+ // Strip only when injecting — preserves manually configured rows on unpicked sites.
139+ $ cookies = array_values (array_filter (
140+ $ cookies ,
141+ function ( $ c ) use ( $ vary_name ) {
142+ return is_array ($ c ) && isset ($ c ['name ' ]) && $ vary_name !== $ c ['name ' ];
143+ }
144+ ));
145+ $ cookies [] = [ 'name ' => $ vary_name , 'vals ' => $ vals ];
146+ return $ cookies ;
147+ }
148+
149+ /**
150+ * Render the WCML multi-currency crawler picker.
151+ *
152+ * @since 7.9
153+ * @access public
154+ * @return void
155+ */
156+ public static function render_picker () {
157+ $ currencies = self ::_currencies ();
158+ if (count ($ currencies ) < 2 ) {
159+ return ;
160+ }
161+
162+ $ selected = self ::_selected ($ currencies );
163+ ?>
164+ <div style="margin-top:10px;">
165+ <input type="hidden" name="litespeed_wcml_crawler_present" value="1" />
166+ <h4><?php echo esc_html__ ('WCML Multi-currency Crawl ' , 'litespeed-cache ' ); ?> </h4>
167+ <p class="litespeed-desc">
168+ <?php echo esc_html__ ('Pick currencies to crawl. The matching _lscache_vary cookie is appended to the crawler cookie list automatically; crawler hits are reverse-mapped back to the selected currency at request time. ' , 'litespeed-cache ' ); ?>
169+ <br />
170+ <?php echo esc_html__ ('Note: applies to guest crawls only. Role-simulated crawls use the role vary cookie, which overrides the per-currency value. ' , 'litespeed-cache ' ); ?>
171+ </p>
172+ <?php foreach ($ currencies as $ code ) : ?>
173+ <label style="display:inline-block; margin: 0 14px 6px 0;">
174+ <input type="checkbox" name="litespeed_wcml_crawler_currencies[]"
175+ value="<?php echo esc_attr ($ code ); ?> "
176+ <?php checked (in_array ($ code , $ selected , true )); ?> />
177+ <code><?php echo esc_html ($ code ); ?> </code>
178+ </label>
179+ <?php endforeach ; ?>
180+ </div>
181+ <?php
182+ }
183+
184+ /**
185+ * Persist user pick. Vary row itself is computed on the fly (see inject_vary_row()).
186+ *
187+ * @since 7.9
188+ * @access public
189+ * @return void
190+ */
191+ public static function _handle_save () {
192+ // phpcs:disable WordPress.Security.NonceVerification.Missing -- LSCWP verified nonce before firing this action.
193+ if (!isset ($ _POST ['litespeed_wcml_crawler_present ' ])) {
194+ return ;
195+ }
196+
197+ $ posted = [];
198+ if (isset ($ _POST ['litespeed_wcml_crawler_currencies ' ]) && is_array ($ _POST ['litespeed_wcml_crawler_currencies ' ])) {
199+ $ posted = array_map ('sanitize_text_field ' , wp_unslash ($ _POST ['litespeed_wcml_crawler_currencies ' ]));
200+ }
201+ // phpcs:enable
202+
203+ $ selected = array_values (array_intersect (self ::_currencies (), $ posted ));
204+ update_option (self ::OPT_PICK , $ selected , false );
205+ }
206+
207+ /**
208+ * Reverse-lookup currency from the vary cookie. Hash map computed on the fly.
209+ *
210+ * @since 7.9
211+ * @access private
212+ * @return void
213+ */
214+ private static function _detect_crawler_currency () {
215+ // Crawler-only: real visitors resolve currency via WCML state, not via cookie reverse-lookup.
216+ if (
217+ empty ($ _SERVER ['HTTP_USER_AGENT ' ])
218+ || 0 !== strpos (wp_unslash ((string ) $ _SERVER ['HTTP_USER_AGENT ' ]), \LiteSpeed \Crawler::FAST_USER_AGENT ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
219+ ) {
220+ return ;
221+ }
222+
223+ $ vary_name = \LiteSpeed \Vary::cls ()->get_vary_name ();
224+ if (empty ($ _COOKIE [ $ vary_name ])) {
225+ return ;
226+ }
227+
228+ $ currencies = self ::_currencies ();
229+ $ selected = count ($ currencies ) >= 2 ? self ::_selected ($ currencies ) : [];
230+ if (empty ($ selected )) {
231+ return ;
232+ }
233+
234+ $ val = sanitize_text_field (wp_unslash ($ _COOKIE [ $ vary_name ]));
235+ foreach ($ selected as $ code ) {
236+ if (self ::_vary_hash ($ code ) === $ val ) {
237+ add_filter ('wcml_client_currency ' , function () use ( $ code ) {
238+ return $ code ;
239+ }, 0 );
240+ self ::apply_client_currency ($ code );
241+ return ;
242+ }
243+ }
244+ }
245+
246+ /**
247+ * User-selected currencies, intersected with what is currently available.
248+ *
249+ * @since 7.9
250+ * @access private
251+ * @param string[] $available Available WCML currencies.
252+ * @return string[]
253+ */
254+ private static function _selected ( $ available ) {
255+ $ stored = get_option (self ::OPT_PICK , []);
256+ if (!is_array ($ stored )) {
257+ return [];
258+ }
259+ return array_values (array_intersect ($ available , $ stored ));
260+ }
261+
262+ /**
263+ * All active WCML currency codes.
264+ *
265+ * @since 7.9
266+ * @access private
267+ * @return string[]
268+ */
269+ private static function _currencies () {
270+ global $ woocommerce_wpml ;
271+ if (is_object ($ woocommerce_wpml )
272+ && isset ($ woocommerce_wpml ->multi_currency )
273+ && method_exists ($ woocommerce_wpml ->multi_currency , 'get_currency_codes ' )
274+ ) {
275+ return $ woocommerce_wpml ->multi_currency ->get_currency_codes ();
276+ }
277+
278+ $ settings = get_option ('_wcml_settings ' , []);
279+ if (!empty ($ settings ['currency_options ' ]) && is_array ($ settings ['currency_options ' ])) {
280+ return array_keys ($ settings ['currency_options ' ]);
281+ }
282+
283+ return [];
284+ }
285+
286+ /**
287+ * Compute the vary cookie value for a guest under the given currency.
288+ *
289+ * @since 7.9
290+ * @access private
291+ * @param string $currency Currency code.
292+ * @return string|false Vary value, or false if Vary cannot finalize (LITESPEED_GUEST etc.).
293+ */
294+ private static function _vary_hash ( $ currency ) {
295+ $ prev = self ::$ _currency ;
296+ self ::$ _currency = $ currency ;
297+ $ hash = \LiteSpeed \Vary::cls ()->finalize_default_vary (-1 );
298+ self ::$ _currency = $ prev ;
299+ return $ hash ;
300+ }
83301}
0 commit comments