PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.4.9
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.4.9
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 +8 -270 3.7.53.4.9 View file →
@@ -56,49 +56,24 @@
56 56
57 57 /**
58 58 * Collect IP from request.
59 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 60 * @return string
66 61 */
67 62 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;
63 + $ip = '127.0.0.1'; // Local IP
64 + if (! empty($_SERVER['HTTP_CLIENT_IP'])) {
65 + $ip = $_SERVER['HTTP_CLIENT_IP'];
66 + } elseif (! empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
67 + $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
68 + } else {
69 + $ip = ! empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : $ip;
72 70 }
73 71
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';
72 + return sanitize_text_field($ip);
88 73 }
89 74
90 75 /**
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 76 * Get views for front-end display
102 77 *
103 78 * @param string $name it will be file name only from the view's folder.
104 79 * @param array $data
@@ -114,29 +89,8 @@
114 89 }
115 90 }
116 91
117 92 /**
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 93 * Get API URL for Templately endpoints
140 94 *
141 95 * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
142 96 * @return string Complete API URL
@@ -172,13 +126,8 @@
172 126 'Authorization' => 'Bearer ' . $api_key,
173 127 'x-templately-ip' => self::get_ip(),
174 128 'x-templately-url' => home_url('/'),
175 129 '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 130 ];
182 131
183 132 // Add Content-Type for POST requests
184 133 if (strtoupper($method) === 'POST') {
@@ -184,15 +133,8 @@
184 133 if (strtoupper($method) === 'POST') {
185 134 $headers['Content-Type'] = 'application/json';
186 135 }
187 136
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 137 // Merge additional headers
196 138 $headers = array_merge($headers, $extra_headers);
197 139
198 140 $args = [
@@ -217,12 +159,8 @@
217 159
218 160 // Check for verification header in the response
219 161 self::check_verification_header($response);
220 162
221 -
222 - // Check for site disconnection in response body
223 - self::check_site_disconnection($response);
224 -
225 163 return $response;
226 164 }
227 165
228 166 /**
@@ -280,46 +218,8 @@
280 218 return $sanitized_value;
281 219 }
282 220
283 221 /**
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 222 * Check for X-Templately-Verified header and update user verification status
323 223 *
324 224 * @param array|WP_Error $response The HTTP response array from wp_remote_get/wp_remote_post
325 225 * @return void
@@ -371,103 +271,8 @@
371 271 return false;
372 272 }
373 273
374 274 /**
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 275 * API Error Formatter
471 276 *
472 277 * @param int $error_code
473 278 * @param mixed $error_message
@@ -768,74 +573,7 @@
768 573 $r[$key] = $value;
769 574 }
770 575 }
771 576 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 577 }
840 578
841 579 }