PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.2
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
← All changes | includes/Utils/Helper.php +699 -78 3.0.43.7.2 View file →
@@ -1,11 +1,15 @@
1 1 <?php
2 +
2 3 namespace Templately\Utils;
3 4
5 +use Elementor\Plugin;
6 +use Templately\Core\Importer\Utils\Utils;
4 7 use WP_Error;
5 8 use WP_REST_Response;
6 9 use function get_plugins;
7 10 use function is_plugin_active;
11 +
8 12 /**
9 13 * Utility Helper for Templately
10 14 *
11 15 * This class contains some helper functions for easy access.
@@ -12,66 +16,230 @@
12 16 *
13 17 * @since 1.0.0
14 18 */
15 19 class Helper extends Base {
16 - /**
17 - * Get installed WordPress Plugin List
18 - * @return array
19 - */
20 - public static function get_plugins(){
21 - if( ! function_exists( 'get_plugins' ) ) {
22 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
23 - }
24 - return get_plugins();
25 - }
20 + /**
21 + * Check if development API should be used
22 + *
23 + * @return bool True if development API should be used
24 + */
25 + public static function is_dev_api(){
26 + // Only check TEMPLATELY_DEV_API constant - no fallback mechanisms
27 + return defined( 'TEMPLATELY_DEV_API' ) && constant( 'TEMPLATELY_DEV_API' );
28 + }
26 29
27 - /**
28 - * Get installed WordPress Plugin List
29 - * @return boolean
30 - */
31 - public static function is_plugin_active( $plugin ){
32 - if( ! function_exists( 'is_plugin_active' ) ) {
33 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
34 - }
35 - return is_plugin_active( $plugin );
36 - }
30 + /**
31 + * Get installed WordPress Plugin List
32 + * @return array
33 + */
34 + public static function get_plugins() {
35 + if (! function_exists('get_plugins')) {
36 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
37 + }
38 + return get_plugins();
39 + }
40 + public static function is_plugins_installed($plugin_file) {
41 + $_plugins = self::get_plugins();
42 + $is_installed = isset($_plugins[$plugin_file]);
43 + return $is_installed;
44 + }
37 45
38 - /**
46 + /**
47 + * Get installed WordPress Plugin List
48 + * @return boolean
49 + */
50 + public static function is_plugin_active($plugin) {
51 + if (! function_exists('is_plugin_active')) {
52 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
53 + }
54 + return is_plugin_active($plugin);
55 + }
56 +
57 + /**
39 58 * Collect IP from request.
40 59 *
60 + * Prefers REMOTE_ADDR since it cannot be spoofed by the client. When it is
61 + * a private/reserved address (reverse proxy, Docker bridge gateway like
62 + * 192.168.65.1, local dev), the forwarded headers are scanned for the first
63 + * public IP. If nothing public is found, the request is local: 127.0.0.1.
64 + *
41 65 * @return string
42 66 */
43 67 public static function get_ip() {
44 - $ip = '127.0.0.1'; // Local IP
45 - if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
46 - $ip = $_SERVER['HTTP_CLIENT_IP'];
47 - } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
48 - $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
68 + $remote_addr = ! empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
69 +
70 + if (self::is_public_ip($remote_addr)) {
71 + return $remote_addr;
72 + }
73 +
74 + foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP'] as $header) {
75 + if (empty($_SERVER[$header])) {
76 + continue;
77 + }
78 + $candidates = explode(',', sanitize_text_field($_SERVER[$header]));
79 + foreach ($candidates as $candidate) {
80 + $candidate = trim($candidate);
81 + if (self::is_public_ip($candidate)) {
82 + return $candidate;
83 + }
84 + }
85 + }
86 +
87 + return '127.0.0.1';
88 + }
89 +
90 + /**
91 + * Check whether a string is a valid public (non-private, non-reserved) IP.
92 + *
93 + * @param string $ip
94 + * @return bool
95 + */
96 + private static function is_public_ip($ip): bool {
97 + return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
98 + }
99 +
100 + /**
101 + * Get views for front-end display
102 + *
103 + * @param string $name it will be file name only from the view's folder.
104 + * @param array $data
105 + * @return void
106 + */
107 + public static function views($name, $data = []) {
108 + extract($data);
109 + $helper = self::class;
110 + $file = TEMPLATELY_PATH . 'views/' . $name . '.php';
111 +
112 + if (is_readable($file)) {
113 + include_once $file;
114 + }
115 + }
116 +
117 + /**
118 + * Get API URL for Templately endpoints
119 + *
120 + * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
121 + * @return string Complete API URL
122 + */
123 + public static function get_api_url($endpoint): string {
124 + $base_url = self::is_dev_api() ? 'https://app.templately.dev' : 'https://app.templately.com';
125 +
126 + /**
127 + * Filter the base URL for development API
128 + *
129 + * @since 3.5.0
130 + * @param string $base_url The default base URL
131 + */
132 + $base_url = apply_filters('templately_dev_api_base_url', $base_url);
133 +
134 + return "{$base_url}/api/{$endpoint}";
135 + }
136 +
137 + /**
138 + * Make a unified API request to Templately API
139 + *
140 + * @param string $method HTTP method (GET or POST)
141 + * @param string $api_url Complete API URL
142 + * @param array $body Request body data (for POST requests)
143 + * @param array $extra_headers Additional headers beyond the standard ones
144 + * @param int $timeout Request timeout in seconds (default: 30)
145 + * @return array|WP_Error Response array or WP_Error on failure
146 + */
147 + private static function make_api_request($method, $api_url, $body = [], $extra_headers = [], $timeout = 30) {
148 + $api_key = Options::get_instance()->get('api_key');
149 +
150 + $headers = [
151 + 'Authorization' => 'Bearer ' . $api_key,
152 + 'x-templately-ip' => self::get_ip(),
153 + 'x-templately-url' => home_url('/'),
154 + 'x-templately-version' => defined( 'TEMPLATELY_VERSION' ) ? constant( 'TEMPLATELY_VERSION' ) : '1.0.0',
155 + // Force JSON responses so the cloud returns JSON errors instead of an HTML
156 + // error page (which json_decode() cannot parse). Binary/XML downloads
157 + // (zip pack, attachment WXR) use their own wp_remote_* calls and bypass
158 + // this helper, so they are unaffected. Callers can override via $extra_headers.
159 + 'Accept' => 'application/json',
160 + ];
161 +
162 + // Add Content-Type for POST requests
163 + if (strtoupper($method) === 'POST') {
164 + $headers['Content-Type'] = 'application/json';
165 + }
166 +
167 + // Resolve requested platform: $_REQUEST wins (frontend-supplied), then caller's extra_headers, then default.
168 + if ( isset( $_REQUEST['requested_platform'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
169 + $extra_headers['x-templately-requested-platform'] = sanitize_text_field( wp_unslash( $_REQUEST['requested_platform'] ) );
170 + } elseif ( ! isset( $extra_headers['x-templately-requested-platform'] ) ) {
171 + $extra_headers['x-templately-requested-platform'] = 'templately';
172 + }
173 +
174 + // Merge additional headers
175 + $headers = array_merge($headers, $extra_headers);
176 +
177 + $args = [
178 + 'timeout' => $timeout,
179 + 'headers' => $headers,
180 + ];
181 +
182 + // Apply filter to allow network admin or other functionality to modify request args
183 + $args = apply_filters( 'templately_api_request_params', $args, $method, $api_url );
184 +
185 + // Add body for POST requests
186 + if (strtoupper($method) === 'POST') {
187 + $args['body'] = is_array($body) ? json_encode($body) : $body;
188 + }
189 +
190 + // Make the appropriate request
191 + if (strtoupper($method) === 'POST') {
192 + $response = wp_remote_post($api_url, $args);
49 193 } else {
50 - $ip = ! empty( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : $ip;
194 + $response = wp_remote_get($api_url, $args);
51 195 }
52 196
53 - return sanitize_text_field( $ip );
197 + // Check for verification header in the response
198 + self::check_verification_header($response);
199 +
200 +
201 + // Check for site disconnection in response body
202 + self::check_site_disconnection($response);
203 +
204 + return $response;
54 205 }
55 206
56 - /**
57 - * Get views for front-end display
58 - *
59 - * @param string $name it will be file name only from the view's folder.
60 - * @param array $data
61 - * @return void
62 - */
63 - public static function views( $name, $data = [] ){
64 - extract( $data );
65 - $helper = self::class;
66 - $file = TEMPLATELY_PATH . 'views/' . $name . '.php';
207 + /**
208 + * Make a GET request to Templately API
209 + *
210 + * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
211 + * @param array $query_params Query parameters as key-value pairs
212 + * @param array $extra_headers Additional headers beyond the standard ones
213 + * @param int $timeout Request timeout in seconds (default: 30)
214 + * @return array|WP_Error Response array or WP_Error on failure
215 + */
216 + public static function make_api_get_request($endpoint, $query_params = [], $extra_headers = [], $timeout = 30) {
217 + $api_url = self::get_api_url($endpoint);
67 218
68 - if( is_readable( $file ) ) {
69 - include_once $file;
70 - }
71 - }
219 + // Add query parameters if provided
220 + if (!empty($query_params)) {
221 + $api_url = add_query_arg($query_params, $api_url);
222 + }
72 223
224 + return self::make_api_request('GET', $api_url, [], $extra_headers, $timeout);
225 + }
226 +
73 227 /**
228 + * Make a POST request to Templately API
229 + *
230 + * @param string $endpoint API endpoint path (e.g., 'v2/feedback/store')
231 + * @param array $body Request body data
232 + * @param array $extra_headers Additional headers beyond the standard ones
233 + * @param int $timeout Request timeout in seconds (default: 30)
234 + * @return array|WP_Error Response array or WP_Error on failure
235 + */
236 + public static function make_api_post_request($endpoint, $body = [], $extra_headers = [], $timeout = 30) {
237 + $api_url = self::get_api_url($endpoint);
238 + return self::make_api_request('POST', $api_url, $body, $extra_headers, $timeout);
239 + }
240 +
241 + /**
74 242 * Sanitize Helper
75 243 *
76 244 * @param mixed $value
77 245 * @param string $type
@@ -77,15 +245,15 @@
77 245 * @param string $type
78 246 *
79 247 * @return bool|string
80 248 */
81 - public static function sanitize( $value, $type = 'text' ){
82 - switch ( $type ) {
249 + public static function sanitize($value, $type = 'text') {
250 + switch ($type) {
83 251 case 'boolean':
84 - $sanitized_value = rest_sanitize_boolean( $value );
252 + $sanitized_value = rest_sanitize_boolean($value);
85 253 break;
86 254 default:
87 - $sanitized_value = sanitize_text_field( $value );
255 + $sanitized_value = sanitize_text_field($value);
88 256 break;
89 257 }
90 258
91 259 return $sanitized_value;
@@ -91,8 +259,194 @@
91 259 return $sanitized_value;
92 260 }
93 261
94 262 /**
263 + * Escape a string for safe embedding inside a GraphQL or JSON string literal.
264 + *
265 + * GraphQL string escaping rules are identical to JSON string escaping (per the
266 + * GraphQL spec), so wp_json_encode() is the authoritative escaper. We strip the
267 + * outer quotes it adds and return only the escaped inner content, ready to be
268 + * wrapped in your own quote pair.
269 + *
270 + * Handles pre-encoded JSON: when the caller has already run json_encode() +
271 + * wp_slash() on a value (e.g. categories, dependencies in Items.php), the
272 + * quotes are already escaped as \" and the string is ready to embed. Calling
273 + * wp_json_encode() again would double-escape those backslashes. We detect this
274 + * case by checking whether wp_unslash() produces valid JSON, and if so, return
275 + * the value directly without further encoding.
276 + *
277 + * @param string $value Raw string or wp_slash(json_encode()) output.
278 + * @return string Escaped string, safe to place between double quotes in GraphQL/JSON.
279 + */
280 + public static function esc_json_string( $value ) {
281 + $value = (string) $value;
282 +
283 + // If wp_slash() was applied to a JSON string upstream, the quotes are
284 + // already escaped (e.g. {\"key\":\"val\"}). Detect this by unslashing and
285 + // checking for valid JSON — if it matches, the value is already suitable
286 + // for embedding in a string literal; return it as-is to avoid doubling backslashes.
287 + $unslashed = wp_unslash( $value );
288 + if ( $unslashed !== $value ) {
289 + $decoded = json_decode( $unslashed, true );
290 + if ( json_last_error() === JSON_ERROR_NONE && null !== $decoded ) {
291 + return $value;
292 + }
293 + }
294 +
295 + $encoded = wp_json_encode( $value );
296 + // wp_json_encode wraps the value in "...", strip those outer quotes.
297 + return substr( $encoded, 1, -1 );
298 + }
299 +
300 + /**
301 + * Check for X-Templately-Verified header and update user verification status
302 + *
303 + * @param array|WP_Error $response The HTTP response array from wp_remote_get/wp_remote_post
304 + * @return void
305 + */
306 + public static function check_verification_header($response) {
307 + // Only process if response is not a WP_Error and contains headers
308 + if (is_wp_error($response)) {
309 + return;
310 + }
311 +
312 + // Retrieve the X-Templately-Verified header
313 + $verification_header = wp_remote_retrieve_header($response, 'X-Templately-Verified');
314 +
315 + // Check if header exists and has a truthy value
316 + if (!empty($verification_header) && filter_var($verification_header, FILTER_VALIDATE_BOOLEAN)) {
317 + try {
318 + // Get current user data
319 + $options = Options::get_instance();
320 + $user = $options->get('user');
321 +
322 + // Only update if user data exists and is not already verified
323 + if (!empty($user) && is_array($user) && empty($user['is_verified'])) {
324 + // Set verification flag
325 + $user['is_verified'] = true;
326 +
327 + // Save updated user data
328 + $options->set('user', $user);
329 +
330 + }
331 +
332 + if (!empty($user['is_verified'])){
333 + if(!headers_sent()){
334 + header( 'X-Templately-Verified: true' );
335 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
336 + self::log('User verification status already updated via X-Templately-Verified header');
337 + }
338 + }
339 +
340 + return true;
341 + }
342 + } catch (\Exception $e) {
343 + // Log error if debug logging is enabled
344 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
345 + self::log('Error updating user verification status: ' . $e->getMessage());
346 + }
347 + }
348 + }
349 +
350 + return false;
351 + }
352 +
353 + /**
354 + * Check for site disconnection status in API response body
355 + *
356 + * Detects SiteNotConnected errors and updates user disconnection status.
357 + * Sends X-Templately-Disconnected header for frontend detection.
358 + *
359 + *
360 + * @param array|WP_Error|mixed $response The response object or body array
361 + * @return bool True if site is disconnected, false otherwise
362 + */
363 + public static function check_site_disconnection($response) {
364 + if (is_wp_error($response)) {
365 + return false;
366 + }
367 +
368 + $response_body = $response;
369 +
370 + // If it's a raw WP response array with body, decode it
371 + if (is_array($response) && isset($response['body']) && is_string($response['body'])) {
372 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
373 + }
374 +
375 + // Check if response body indicates site disconnection
376 + if (!is_array($response_body)) {
377 + return false;
378 + }
379 +
380 + $status = $response_body['status'] ?? null;
381 + $status_text = $response_body['statusText'] ?? null;
382 +
383 + // Check for SiteNotConnected error
384 + if ($status === 'error' && $status_text === 'SiteNotConnected') {
385 + try {
386 + // Get current user data
387 + $options = Options::get_instance();
388 + $user = $options->get('user');
389 +
390 + // Only update if user data exists
391 + if (!empty($user) && is_array($user)) {
392 + // Set disconnection flag
393 + $user['is_disconnected'] = true;
394 +
395 + // Save updated user data
396 + $options->set('user', $user);
397 +
398 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
399 + self::log('Site disconnection detected: SiteNotConnected status');
400 + }
401 + }
402 +
403 + // Send header for frontend detection
404 + if (!headers_sent()) {
405 + header('X-Templately-Disconnected: true');
406 + }
407 +
408 + return true;
409 + } catch (\Exception $e) {
410 + // Log error if debug logging is enabled
411 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
412 + self::log('Error updating site disconnection status: ' . $e->getMessage());
413 + }
414 + }
415 + }
416 +
417 + return false;
418 + }
419 +
420 + /**
421 + * Clear site disconnection status
422 + *
423 + * Called after successful site migration to reset the disconnection flag.
424 + *
425 + * @return void
426 + */
427 + public static function clear_site_disconnection() {
428 + try {
429 + $options = Options::get_instance();
430 + $user = $options->get('user');
431 +
432 + if (!empty($user) && is_array($user)) {
433 + $user['site_url'] = base64_encode( home_url('/') );
434 + $user['is_disconnected'] = false;
435 + $options->set('user', $user);
436 +
437 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
438 + self::log('Site disconnection status cleared and URL updated.');
439 + }
440 + }
441 + } catch (\Exception $e) {
442 + if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
443 + self::log('Error clearing site disconnection status: ' . $e->getMessage());
444 + }
445 + }
446 + }
447 +
448 + /**
95 449 * API Error Formatter
96 450 *
97 451 * @param int $error_code
98 452 * @param mixed $error_message
@@ -100,15 +454,18 @@
100 454 * @param integer $status
101 455 * @param array $additional_data
102 456 * @return WP_Error
103 457 */
104 - public static function error( $error_code, $error_message, $endpoint = '', $status = 500, $additional_data = [] ) {
458 + public static function error($error_code, $error_message, $endpoint = '', $status = 500, $additional_data = []) {
105 459 $additional_data['status'] = $status;
106 - if ( ! empty( $endpoint ) ) {
460 + if (! empty($endpoint)) {
107 461 $additional_data['endpoint'] = $endpoint;
108 462 }
463 + // Add browser padding to avoid browsers not serving small JSON responses
464 + $padding_length = 512;
465 + $additional_data['browser_padding'] = str_repeat(' ', $padding_length);
109 466
110 - return new WP_Error( $error_code, $error_message, $additional_data );
467 + return new WP_Error($error_code, $error_message, $additional_data);
111 468 }
112 469
113 470 /**
114 471 * API Response Formatter
@@ -115,10 +472,10 @@
115 472 *
116 473 * @param mixed $data
117 474 * @return WP_REST_Response
118 475 */
119 - public static function success( $data ) {
120 - return new WP_REST_Response( $data, 200 );
476 + public static function success($data) {
477 + return new WP_REST_Response($data, 200);
121 478 }
122 479
123 480 /**
124 481 * Normalize Favourites Data
@@ -128,42 +485,42 @@
128 485 * @param boolean $undo
129 486 *
130 487 * @return array
131 488 */
132 - public function normalizeFavourites( $favourites, $_favourites = [], $undo = false ){
133 - if( $undo ) {
134 - $_favourites[ $favourites['type'] ] = array_values(array_filter( $_favourites[ $favourites['type'] ], function( $item ) use( $favourites ) {
489 + public function normalizeFavourites($favourites, $_favourites = [], $undo = false) {
490 + if ($undo) {
491 + $_favourites[$favourites['type']] = array_values(array_filter($_favourites[$favourites['type']], function ($item) use ($favourites) {
135 492 return $item != $favourites['id'];
136 - } ));
493 + }));
137 494 return $_favourites;
138 495 }
139 496
140 - array_map( function( $item ) use ( &$_favourites) {
141 - if( ! is_null( $item ) ) {
497 + array_map(function ($item) use (&$_favourites) {
498 + if (! is_null($item)) {
142 499 $item = (array) $item;
143 - if ( isset( $_favourites[ $item['type'] ] ) ){
144 - $_favourites[ $item['type'] ][] = $item['id'];
500 + if (isset($_favourites[$item['type']])) {
501 + $_favourites[$item['type']][] = $item['id'];
145 502 } else {
146 - $_favourites[ $item['type'] ] = [ $item['id'] ];
503 + $_favourites[$item['type']] = [$item['id']];
147 504 }
148 505 }
149 506 return $_favourites;
150 - }, $favourites );
507 + }, $favourites);
151 508
152 509 return $_favourites;
153 510 }
154 511
155 - public function normalizeReviews( $favourites, $_favourites = [], $undo = false ){
156 - array_map( function( $item ) use ( &$_favourites) {
157 - if( ! is_null( $item ) ) {
512 + public function normalizeReviews($favourites, $_favourites = [], $undo = false) {
513 + array_map(function ($item) use (&$_favourites) {
514 + if (! is_null($item)) {
158 515 $item = (array) $item;
159 - if ( !isset( $_favourites[ $item['type'] ] ) ){
160 - $_favourites[ $item['type'] ] = [];
516 + if (!isset($_favourites[$item['type']])) {
517 + $_favourites[$item['type']] = [];
161 518 }
162 - $_favourites[ $item['type'] ][$item['type_id']] = $item['rating'];
519 + $_favourites[$item['type']][$item['type_id']] = $item['rating'];
163 520 }
164 521 return $_favourites;
165 - }, $favourites );
522 + }, $favourites);
166 523
167 524 return $_favourites;
168 525 }
169 526
@@ -172,10 +529,10 @@
172 529 *
173 530 * @param object $triggered_by
174 531 * @return void
175 532 */
176 - public static function trigger_error( $triggered_by, $method = 'get_instance' ){
177 - $class = get_class( $triggered_by );
533 + public static function trigger_error($triggered_by, $method = 'get_instance') {
534 + $class = get_class($triggered_by);
178 535 $trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
179 536 $file = $trace[0]['file'];
180 537 $line = $trace[0]['line'];
181 538 trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);
@@ -183,17 +540,281 @@
183 540
184 541 /**
185 542 * Printing Error Logs in debug.log file.
186 543 *
187 - * @param mixed $log
544 + * @param mixed $log The data to log
545 + * @param string $context Optional context for categorizing log entries
546 + * @param string $level Optional log level (debug, info, warning, error)
188 547 * @return void
189 548 */
190 - public static function log( $log ){
191 - if ( defined('WP_DEBUG_LOG') && WP_DEBUG_LOG === true ) {
192 - if ( is_array( $log ) || is_object( $log ) ) {
193 - error_log( print_r( $log, true ) );
549 + public static function log($log, $context = '', $level = 'info') {
550 + // Allow complete override of logging behavior
551 + $override_result = apply_filters('templately_log_override', null, $log, $context, $level);
552 + if ($override_result !== null) {
553 + return;
554 + }
555 +
556 + // Only log if WP_DEBUG_LOG is enabled
557 + if (!defined('WP_DEBUG_LOG') || !WP_DEBUG_LOG) {
558 + return;
559 + }
560 +
561 + // Format the log message
562 + $formatted_message = self::format_log_message($log, $context, $level);
563 +
564 + // Write to error log
565 + error_log($formatted_message);
566 + }
567 +
568 + /**
569 + * Format log message with context and level
570 + *
571 + * @param mixed $log The data to log
572 + * @param string $context Context for categorizing log entries
573 + * @param string $level Log level
574 + * @return string Formatted log message
575 + */
576 + private static function format_log_message($log, $context = '', $level = 'info') {
577 + // Convert arrays and objects to readable format
578 + if (is_array($log) || is_object($log)) {
579 + $log_content = print_r($log, true);
580 + } else {
581 + $log_content = (string) ($log ?: '');
582 + }
583 +
584 + // Build the formatted message
585 + $timestamp = current_time('Y-m-d H:i:s');
586 + $level_upper = strtoupper($level);
587 +
588 + if (!empty($context)) {
589 + return "[{$timestamp}] [{$level_upper}] [{$context}] {$log_content}";
590 + } else {
591 + return "[{$timestamp}] [{$level_upper}] {$log_content}";
592 + }
593 + }
594 +
595 + public static function should_flush() {
596 + if (isset($_REQUEST['is_lightspeed']) && $_REQUEST['is_lightspeed'] === 'true') {
597 + return false;
598 + }
599 + return (!defined('TEMPLATELY_IGNORE_FLUSH_ALL') || !TEMPLATELY_IGNORE_FLUSH_ALL) && strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') === false;
600 + }
601 +
602 + public static function get_block_by_name($blocks, $search) {
603 + $queue = $blocks;
604 +
605 + while (!empty($queue)) {
606 + $current_block = array_shift($queue);
607 +
608 + if ($search === $current_block['blockName']) {
609 + return $current_block;
610 + }
611 +
612 + if (isset($current_block['innerBlocks'])) {
613 + // Add nested blocks to the end of the queue for processing
614 + $queue = array_merge($queue, $current_block['innerBlocks']);
615 + }
616 + }
617 +
618 + return false;
619 + }
620 +
621 + /**
622 + * Only checks if user can install/activate plugins
623 + *
624 + * @param [type] $cap
625 + * @param [type] ...$args
626 + * @return void
627 + */
628 + public static function current_user_can($cap, ...$args) {
629 + $user = wp_get_current_user();
630 +
631 + // Multisite super admin has all caps by definition, Unless specifically denied.
632 + if (is_multisite() && is_super_admin($user->ID)) {
633 + return true;
634 + }
635 +
636 + $caps = map_meta_cap($cap, $user->ID, ...$args);
637 +
638 + switch ($cap) {
639 + case 'install_plugins':
640 + case 'upload_plugins':
641 + $caps = ['install_plugins'];
642 + break;
643 + case 'install_themes':
644 + case 'upload_themes':
645 + $caps = ['install_themes'];
646 + break;
647 + case 'activate_plugins':
648 + case 'deactivate_plugins':
649 + case 'activate_plugin':
650 + case 'deactivate_plugin':
651 + $caps = ['activate_plugins'];
652 + break;
653 + default:
654 + break;
655 + }
656 +
657 + // Maintain BC for the argument passed to the "user_has_cap" filter.
658 + $args = array_merge(array($cap, $user->ID), $args);
659 +
660 + /**
661 + * See WP_User::has_cap() for description.
662 + */
663 + $capabilities = apply_filters('user_has_cap', $user->allcaps, $caps, $args, $user);
664 +
665 + // Everyone is allowed to exist.
666 + $capabilities['exist'] = true;
667 +
668 + // Nobody is allowed to do things they are not allowed to do.
669 + unset($capabilities['do_not_allow']);
670 +
671 + // Must have ALL requested caps.
672 + foreach ((array) $caps as $cap) {
673 + if (empty($capabilities[$cap])) {
674 + return false;
675 + }
676 + }
677 +
678 + return true;
679 + }
680 +
681 + /**
682 + * Calculates the elapsed time and checks if it is close to the maximum execution time.
683 + * Returns true if the script should exit to avoid exceeding the limit.
684 + *
685 + * @return bool True if the script should exit, false otherwise.
686 + */
687 + public static function fsi_should_exit() {
688 + if (defined('TEMPLATELY_START_TIME') && ini_get('max_execution_time')) {
689 + $max_time = ini_get('max_execution_time');
690 + $elapsed = microtime(true) - TEMPLATELY_START_TIME;
691 + $delay = max(5, $max_time * 20 / 100);
692 +
693 + // Check if elapsed time is close to max execution time
694 + if ($max_time - $elapsed <= $delay) {
695 + return ['max_time' => $max_time, 'elapsed' => $elapsed, 'delay' => $delay];
696 + }
697 + }
698 + return false;
699 + }
700 +
701 + /**
702 + * Enable Elementor Container
703 + * This function will enable the Elementor Container feature.
704 + * Without this feature, some of the templates may not work properly.
705 + *
706 + * @return boolean
707 + */
708 + public static function enable_elementor_container() {
709 + if (class_exists('Elementor\Plugin')) {
710 + $control_name = Plugin::instance()->experiments->get_feature_option_key('container');
711 + if (get_option($control_name) !== 'active') {
712 + update_option($control_name, 'active');
713 + return true;
714 + }
715 + }
716 + return false;
717 + }
718 +
719 + /**
720 + * Undocumented function
721 + *
722 + * @param [type] $args
723 + * @param [type] $defaults
724 + * @return array
725 + */
726 + public static function recursive_wp_parse_args($args, $defaults) {
727 + $args = (array) $args;
728 + $defaults = (array) $defaults;
729 + $r = $defaults;
730 + foreach ($args as $key => $value) {
731 + if (is_array($value) && isset($r[$key])) {
732 + // also handle numeric array. if both $value and $r[ $key ] are numeric array. wp_is_numeric_array()
733 + if (wp_is_numeric_array($value) && wp_is_numeric_array($r[$key])) {
734 + foreach ($value as $k => $v) {
735 + if (!in_array($v, $r[$key])) {
736 + if (!isset($r[$key][$k])) {
737 + $r[$key][$k] = $v;
738 + } else {
739 + $r[$key][] = $v;
740 + }
741 + }
742 + }
743 + } else {
744 + $r[$key] = self::recursive_wp_parse_args($value, $r[$key]);
745 + }
194 746 } else {
195 - error_log( $log );
747 + $r[$key] = $value;
196 748 }
197 749 }
750 + return $r;
198 751 }
199 -}
752 +
753 + /**
754 + * Creates the plugin's working directory under wp-uploads and blocks direct
755 + * web access to it.
756 + *
757 + * Everything the importer needs on disk lands here: the extracted pack (its
758 + * WXR, its template JSON, its attachments), the AI-generated page JSON, and
759 + * the FSI logs. wp-uploads is web-served, so these paths are not private just
760 + * because their session id is a uuid — the guards are what makes them
761 + * unreadable, not the name.
762 + *
763 + * .htaccess covers Apache and is inherited by everything below this point;
764 + * web.config covers IIS; index.php stops a directory listing on any server.
765 + * nginx honours none of them, so an nginx site still needs a location rule —
766 + * this raises the floor, it does not replace server configuration.
767 + *
768 + * @param string $dir Absolute path to create and protect.
769 + *
770 + * @return bool Whether the directory exists and is usable.
771 + */
772 + public static function protect_directory( $dir ) {
773 + if ( empty( $dir ) ) {
774 + return false;
775 + }
776 +
777 + if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
778 + return false;
779 + }
780 +
781 + $guards = [
782 + 'index.php' => "<?php\n// Silence is golden.\n",
783 + '.htaccess' => "# Templately working files — not for direct access.\n<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n",
784 + 'web.config' => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\t<system.webServer>\n\t\t<authorization>\n\t\t\t<deny users=\"*\" />\n\t\t</authorization>\n\t</system.webServer>\n</configuration>\n",
785 + ];
786 +
787 + foreach ( $guards as $file => $contents ) {
788 + $path = trailingslashit( $dir ) . $file;
789 + // Never overwrite: a site owner may have relaxed these deliberately.
790 + if ( ! file_exists( $path ) ) {
791 + @file_put_contents( $path, $contents ); // phpcs:ignore
792 + }
793 + }
794 +
795 + return true;
796 + }
797 +
798 + /**
799 + * Absolute path to the plugin's protected working directory in wp-uploads.
800 + *
801 + * @param string $sub Optional subdirectory ('tmp', 'log', 'preview', ...).
802 + *
803 + * @return string Trailing-slashed path, or '' when uploads is unusable.
804 + */
805 + public static function upload_dir( $sub = '' ) {
806 + $upload_dir = wp_upload_dir();
807 +
808 + if ( ! empty( $upload_dir['error'] ) || empty( $upload_dir['basedir'] ) ) {
809 + return '';
810 + }
811 +
812 + $base = trailingslashit( $upload_dir['basedir'] ) . 'templately' . DIRECTORY_SEPARATOR;
813 +
814 + // The guards go on the root so every subdirectory inherits them.
815 + self::protect_directory( $base );
816 +
817 + return '' === $sub ? $base : trailingslashit( $base . $sub );
818 + }
819 +
820 +}