PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.3
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.3
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 +126 -8 3.6.23.7.3 View file →
@@ -56,24 +56,49 @@
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 + *
60 65 * @return string
61 66 */
62 67 public static function get_ip() {
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;
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;
70 72 }
71 73
72 - return sanitize_text_field($ip);
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';
73 88 }
74 89
75 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 + /**
76 101 * Get views for front-end display
77 102 *
78 103 * @param string $name it will be file name only from the view's folder.
79 104 * @param array $data
@@ -89,8 +114,29 @@
89 114 }
90 115 }
91 116
92 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 + /**
93 139 * Get API URL for Templately endpoints
94 140 *
95 141 * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
96 142 * @return string Complete API URL
@@ -126,8 +172,13 @@
126 172 'Authorization' => 'Bearer ' . $api_key,
127 173 'x-templately-ip' => self::get_ip(),
128 174 'x-templately-url' => home_url('/'),
129 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',
130 181 ];
131 182
132 183 // Add Content-Type for POST requests
133 184 if (strtoupper($method) === 'POST') {
@@ -717,7 +768,74 @@
717 768 $r[$key] = $value;
718 769 }
719 770 }
720 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 );
721 839 }
722 840
723 841 }