PluginProbe
ReviewX – Multi-Criteria Reviews for WooCommerce with Google Reviews & Schema / trunk
ReviewX – Multi-Criteria Reviews for WooCommerce with Google Reviews & Schema vtrunk
2.5.1 2.5.0 2.4.1 2.4.0 2.3.11 2.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 All 143 releases
reviewx / app / Utilities / Helper.php

Helper.php in ReviewX – Multi-Criteria Reviews for WooCommerce with Google Reviews & Schema trunk, at app/Utilities/Helper.php

386 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ReviewX\Utilities;
4
5 \defined("ABSPATH") || exit;
6 use ReviewX\Firebase\JWT\JWT;
7 use ReviewX\Utilities\Auth\Client;
8 use Throwable;
9 use ReviewX\WPDrill\Plugin;
10 use ReviewX\WPDrill\Response;
11 class Helper
12 {
13 /**
14 * Optional file handle the JSONL buffer flushes into during a bulk data sync, so the
15 * whole dataset is never held in memory at once (the old behaviour OOM-ed large stores
16 * and was a major cause of sync failures). Null = plain in-memory buffering (default).
17 *
18 * @var resource|null
19 */
20 private static $jsonlSink = null;
21 /** Set when any fwrite to the sink fails (disk full / short write) so the caller can abort. */
22 private static bool $jsonlSinkError = \false;
23 /** Flush the buffer to the sink once it grows past this many bytes (~1MB). */
24 private const JSONL_FLUSH_BYTES = 1048576;
25 /**
26 * @param resource $handle
27 */
28 public static function setJsonlSink($handle) : void
29 {
30 self::$jsonlSink = \is_resource($handle) ? $handle : null;
31 self::$jsonlSinkError = \false;
32 }
33 /**
34 * Flush whatever is left in $buffer to the sink and detach it.
35 */
36 public static function flushJsonlSink(string &$buffer) : void
37 {
38 if (self::$jsonlSink !== null && $buffer !== '') {
39 if (\fwrite(self::$jsonlSink, $buffer) === \false) {
40 self::$jsonlSinkError = \true;
41 }
42 $buffer = '';
43 }
44 self::$jsonlSink = null;
45 }
46 /**
47 * True if any write to the sink failed during the last sync — the generated file is
48 * incomplete and must NOT be handed to SaaS.
49 */
50 public static function jsonlSinkHadError() : bool
51 {
52 return self::$jsonlSinkError;
53 }
54 public static function plugin() : Plugin
55 {
56 return Plugin::getInstance();
57 }
58 public static function rest($data = []) : Response
59 {
60 return new Response($data);
61 }
62 public static function pluginPath(string $path = "") : string
63 {
64 return REVIEWX_DIR_PATH . \ltrim($path, "/");
65 }
66 public static function resourcePath(string $path = "") : string
67 {
68 return self::pluginPath("resources/" . \ltrim($path, "/"));
69 }
70 public static function storagePath(string $path = "") : string
71 {
72 return self::pluginPath("storage/" . \ltrim($path, "/"));
73 }
74 public static function pluginFile() : string
75 {
76 return REVIEWX_FILE;
77 }
78 public static function getAuthToken() : string
79 {
80 if (!Client::has()) {
81 return "";
82 }
83 $payload = ["iss" => isset($_SERVER["HTTP_HOST"]) ? \sanitize_text_field(\wp_unslash($_SERVER["HTTP_HOST"])) : '', "iat" => \time(), "exp" => \time() + 300, "nbf" => \time(), "jti" => \uniqid("", \true)];
84 $additionalPayload = ["uid" => Client::getUid()];
85 // Encode the payload and return the JWT token
86 return JWT::encode(\array_merge($payload, $additionalPayload), Client::getSecret(), "HS256");
87 }
88 public static function getWpDomainNameOnly() : string
89 {
90 return \trim(\wp_parse_url(\home_url(), \PHP_URL_HOST), '/');
91 }
92 public static function getApiResponse($response)
93 {
94 try {
95 if (!\is_object($response)) {
96 return self::rest([])->fails('Invalid response from SaaS');
97 }
98 $parsed = $response->autoParse();
99 $message = $parsed['message'] ?? '';
100 $statusCode = $response->getStatusCode();
101 if ($statusCode >= Response::HTTP_OK && $statusCode < 300) {
102 return self::rest($response->getApiData())->success($message, $statusCode);
103 }
104 return self::rest($response->getApiData())->fails((string) $message, $statusCode);
105 } catch (Throwable $th) {
106 return self::rest([])->fails($th->getMessage());
107 }
108 }
109 public static function rvxApi($data = [])
110 {
111 return new Response($data);
112 }
113 public static function saasResponse($response) : Response
114 {
115 $content = $response->autoParse();
116 $statusCode = $response->getStatusCode();
117 $data = $content['data'] ?? [];
118 $message = $content['message'] ?? ($statusCode >= 400 ? 'SaaS API Error' : 'Success');
119 if ($statusCode >= 200 && $statusCode < 300) {
120 return self::rest($data)->success($message, $statusCode);
121 } else {
122 return self::rest($data)->fails((string) $message, $statusCode);
123 }
124 }
125 public static function loggedIn()
126 {
127 return \is_user_logged_in() ? 1 : 0;
128 }
129 public static function getWpCurrentUser()
130 {
131 $user = \wp_get_current_user();
132 return $user->ID > 0 ? $user : null;
133 }
134 public static function arrayGet($data, $accessor, $default = null)
135 {
136 $accessorArray = \is_array($accessor) ? $accessor : \explode(".", $accessor);
137 $value = $data[\array_shift($accessorArray)] ?? $default;
138 foreach ($accessorArray as $key) {
139 if (!isset($value[$key])) {
140 return $default;
141 }
142 $value = $value[$key];
143 }
144 return $value;
145 }
146 public static function verifiedCustomer($customer_id) : bool
147 {
148 $orders = \wc_get_orders(["customer" => $customer_id, "status" => ["completed", "processing", "on-hold", "pending-payment"], "limit" => 1]);
149 if (!empty($orders)) {
150 return \true;
151 }
152 return \false;
153 }
154 public static function debugLog($message = "")
155 {
156 $logMessage = "Output is: " . self::stringifyLogMessage($message);
157 return $logMessage;
158 }
159 public static function arrayValue($array, $key, $default = null)
160 {
161 if (!\is_array($array)) {
162 return $default;
163 }
164 if (\is_null($key)) {
165 return $array;
166 }
167 if (\array_key_exists($key, $array)) {
168 return $array[$key];
169 }
170 foreach (\explode(".", $key) as $segment) {
171 if (\array_key_exists($segment, $array)) {
172 $array = $array[$segment];
173 } else {
174 return $default;
175 }
176 }
177 return $array;
178 }
179 public static function retrieveReviewId($order_id, $prod_id, $user_id)
180 {
181 if (isset($order_id, $prod_id, $user_id)) {
182 $comments = \get_comments([
183 'post_id' => $prod_id,
184 'user_id' => $user_id,
185 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required to match review by order identifier
186 'meta_key' => 'rvx_order',
187 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Required to match review by order identifier
188 'meta_value' => $order_id,
189 'fields' => 'ids',
190 'number' => 1,
191 'no_found_rows' => \true,
192 ]);
193 if (!empty($comments)) {
194 return $comments[0];
195 }
196 }
197 return 0;
198 }
199 public static function getIpAddress()
200 {
201 $http_client_ip = isset($_SERVER["HTTP_CLIENT_IP"]) ? \sanitize_text_field(\wp_unslash($_SERVER["HTTP_CLIENT_IP"])) : null;
202 $remote_addr = isset($_SERVER["REMOTE_ADDR"]) ? \sanitize_text_field(\wp_unslash($_SERVER["REMOTE_ADDR"])) : null;
203 $ip_address = $http_client_ip ?? $remote_addr;
204 return \explode(":", $ip_address)[0] ?? null;
205 }
206 public static function loadTemplate($template_name, $data = [])
207 {
208 \extract($data);
209 $template_path = REVIEWX_DIR_PATH . "widget/components/" . $template_name . ".php";
210 if (\file_exists($template_path)) {
211 include $template_path;
212 } else {
213 \printf(
214 /* translators: %s: Template name */
215 \esc_html__('Template file not found: %s', 'reviewx'),
216 \esc_html($template_name)
217 );
218 }
219 }
220 public static function prepareLangArray() : array
221 {
222 $json_file_path = REVIEWX_DIR_PATH . "/translation.json";
223 $json_content = \file_get_contents($json_file_path);
224 $translations = \json_decode($json_content, \true);
225 $result = [];
226 foreach ($translations as $key => $text) {
227 $result[$key] = $text;
228 }
229 return $result;
230 }
231 public static function rvxGetOrderStatus($orderStatus) : ?string
232 {
233 return \str_replace('wc-', '', $orderStatus);
234 }
235 /**
236 * Whether a prefixed WordPress/WooCommerce table exists on this install.
237 *
238 * WooCommerce analytics lookup tables (wc_customer_lookup, wc_order_stats) are absent on
239 * older WooCommerce versions and on installs where analytics was never built, so querying
240 * them blind throws and takes the whole data sync down with it. Cached per request.
241 *
242 * @param string $table table name WITHOUT the WordPress prefix
243 * @return bool
244 */
245 public static function tableExists(string $table) : bool
246 {
247 static $cache = [];
248 if (isset($cache[$table])) {
249 return $cache[$table];
250 }
251 global $wpdb;
252 $fullTable = $wpdb->prefix . $table;
253 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- schema check, cached in-request
254 $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $fullTable));
255 return $cache[$table] = $found === $fullTable;
256 }
257 /**
258 * The single source of truth for a customer's cross-system identity.
259 *
260 * Registered buyer -> "{site_uid}-{user_id}", guest -> "{site_uid}-md5(lowercase email)".
261 * Returns NULL when neither is known (e.g. a blocks/Store-API draft order created before
262 * the billing email is filled in). Returning null matters: the previous inline version
263 * fell back to "{site_uid}-0", which collapsed every emailless guest into one shared
264 * customer record on the SaaS side.
265 *
266 * @param int $customer_id WP user id (0 for guests)
267 * @param string $email
268 * @return string|null
269 */
270 public static function customerWpUniqueId($customer_id, $email) : ?string
271 {
272 $customer_id = (int) $customer_id;
273 if ($customer_id > 0) {
274 return Client::getUid() . '-' . $customer_id;
275 }
276 $email = \is_string($email) ? \trim($email) : '';
277 return $email !== '' ? Client::getUid() . '-' . \md5(\strtolower($email)) : null;
278 }
279 public static function appendToJsonl(&$buffer, $data, $jsonOptions = \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE)
280 {
281 $json = \wp_json_encode($data, $jsonOptions);
282 if ($json === \false) {
283 return \false;
284 }
285 // One JSON object per line, each line newline-terminated. The SaaS parser trims and
286 // skips empty lines, so a trailing newline at EOF is harmless — and this keeps the
287 // flush-to-file logic below trivial (no cross-chunk separator bookkeeping).
288 $buffer .= $json . "\n";
289 // During a bulk sync, spill to the file handle once the buffer gets large so peak
290 // memory stays bounded regardless of store size.
291 if (self::$jsonlSink !== null && \strlen($buffer) >= self::JSONL_FLUSH_BYTES) {
292 // Record a failed mid-stream write (disk full) — jsonlSinkHadError() gates
293 // publishing the file, and this spill path previously ignored the result.
294 if (\fwrite(self::$jsonlSink, $buffer) === \false) {
295 self::$jsonlSinkError = \true;
296 }
297 $buffer = '';
298 }
299 return \true;
300 }
301 public static function rvxLog($message, $context = 'debug')
302 {
303 $normalizedMessage = self::stringifyLogMessage($message);
304 $formattedMessage = \sprintf("[ReviewX] [%s] [%s]: %s", \wp_date('Y-m-d H:i:s'), \strtoupper($context), \trim($normalizedMessage));
305 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
306 \error_log($formattedMessage);
307 }
308 public static function domainSupport()
309 {
310 // Get the full site URL including the subdomain and subdirectory (if present)
311 $site_url = get_site_url();
312 // Parse the URL to extract the components
313 $parsed_url = \wp_parse_url($site_url);
314 // Rebuild the base URL (scheme + host + path)
315 $full_domain = $parsed_url['scheme'] . '://' . $parsed_url['host'];
316 // Add the path (subdirectory) if it exists
317 if (isset($parsed_url['path'])) {
318 $full_domain .= \rtrim($parsed_url['path'], '/');
319 }
320 return $full_domain;
321 }
322 public static function getRestAPIurl() : string
323 {
324 // If init hasn't fired yet → don't fallback
325 if (!did_action('init')) {
326 // Return fallback REST API URL
327 return site_url('/?rest_route=/reviewx');
328 }
329 // init has fired, return proper REST API URL
330 return get_rest_url(null, 'reviewx');
331 }
332 public static function orderStatus($newStatus) : string
333 {
334 $statusMap = ['processing' => 'processing', 'pending' => 'pending', 'on-hold' => 'on_hold', 'completed' => 'completed', 'cancelled' => 'cancelled', 'refunded' => 'refunded', 'failed' => 'failed', 'checkout-draft' => 'draft', 'auto-draft' => 'auto_draft'];
335 if (!$statusMap[$newStatus]) {
336 return 'any';
337 }
338 return $statusMap[$newStatus];
339 }
340 public static function orderItemStatus($newStatus) : string
341 {
342 $statusMap = ['processing' => 'PROCESSING', 'pending' => 'PENDING_PAYMENT', 'on-hold' => 'ON_HOLD', 'completed' => 'COMPLETED', 'cancelled' => 'CANCELLED', 'refunded' => 'REFUNDED', 'failed' => 'FAILED', 'checkout-draft' => 'DRAFT', 'auto-draft' => 'AUTO-DRAFT'];
343 if (!$statusMap[$newStatus]) {
344 return 'any';
345 }
346 return $statusMap[$newStatus];
347 }
348 public static function validateReturnDate($dateString)
349 {
350 if (\is_numeric($dateString) && $dateString > 0) {
351 $timestamp = (int) $dateString;
352 } else {
353 $timestamp = \strtotime($dateString);
354 }
355 if ($timestamp === \false || $timestamp < 0) {
356 return null;
357 }
358 return \wp_date('Y-m-d H:i:s', $timestamp);
359 }
360 public static function formatToTwoDecimalPlaces($number)
361 {
362 if (empty($number)) {
363 $number = 0;
364 }
365 return \number_format((float) $number, 2);
366 }
367 public static function getWpClientInfo() : array
368 {
369 $current_user = \wp_get_current_user();
370 $first_name = $current_user->first_name ?: $current_user->user_login;
371 $last_name = $current_user->last_name ?: '';
372 return ['domain' => \ReviewX\Utilities\Helper::getWpDomainNameOnly(), 'url' => \home_url(), 'site_locale' => \get_locale(), 'first_name' => \sanitize_text_field($first_name), 'last_name' => \sanitize_text_field($last_name)];
373 }
374 private static function stringifyLogMessage($message) : string
375 {
376 if (\is_string($message)) {
377 return $message;
378 }
379 if (\is_scalar($message) || null === $message) {
380 return (string) $message;
381 }
382 $encodedMessage = \wp_json_encode($message, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
383 return \false !== $encodedMessage ? $encodedMessage : '[unserializable log payload]';
384 }
385 }
386