PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.4
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.4
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
templately / includes / Utils / Helper.php

Helper.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.7.4, at includes/Utils/Helper.php

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