PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 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 All 112 releases
← All changes | includes/Utils/Http.php +136 -80 3.0.9 → trunk View file →
@@ -1,8 +1,11 @@
1 1 <?php
2 2 namespace Templately\Utils;
3 3
4 -use Templately\API\Login;
4 +use Templately\Modules\Auth\REST\Login;
5 +use Templately\Utils\Response\ErrorCode;
6 +use Templately\Utils\Response\ResponseNormalizer;
7 +use Templately\Utils\Response\RetryPolicy;
5 8 use WP_Error;
6 9
7 10 class Http extends Base {
8 11 /**
@@ -31,9 +34,9 @@
31 34 /**
32 35 * Setting the development mode.
33 36 */
34 37 public function __construct() {
35 - $this->dev_mode = defined( 'TEMPLATELY_DEV' ) && TEMPLATELY_DEV;
38 + $this->dev_mode = Helper::is_dev_api();
36 39 }
37 40
38 41 /**
39 42 * Determining the endpoint URL based on the mode.
@@ -40,27 +43,80 @@
40 43 *
41 44 * @return string
42 45 */
43 46 public function url() {
44 - if ( $this->dev_mode ) {
47 + if ( Helper::is_dev_api() ) {
45 48 $this->url = 'https://app.templately.dev/api/plugin';
46 49 }
50 +
51 + /**
52 + * Filter the API endpoint URL
53 + *
54 + * @since 3.5.0
55 + * @param string $url The endpoint URL
56 + */
57 + $this->url = apply_filters('templately_dev_api_endpoint_url', $this->url);
58 +
47 59 return $this->url;
48 60 }
49 61
50 62 /**
51 - * Preparing query arguments.
52 - * Unknown.
53 - *
54 - * @return string
55 - */
56 - public static function prepare( $query, ...$args ) {
57 - return sprintf( $query, ...$args );
63 + * Generate Google OAuth authentication URL
64 + *
65 + * @param string $redirect_to Optional redirect path after authentication
66 + * @return string The Google auth URL with query parameters
67 + */
68 +public function google_auth_url($redirect_to = '', $current_url = '') {
69 + $base_url = $this->url();
70 + // Replace /api/plugin with /api/auth/plugin/google
71 + $auth_url = str_replace('/api/plugin', '/api/auth/plugin/google', $base_url);
72 +
73 + // Get the referer to return to the exact same page we initiated login from securely
74 + if ( ! empty( $current_url ) ) {
75 + $referer = esc_url_raw( $current_url );
76 + } else {
77 + $referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
58 78 }
59 79
60 - /**
61 - * Preparing query arguments
62 - *
80 + $return_url = wp_validate_redirect( $referer, '' );
81 +
82 + if ( empty( $return_url ) ) {
83 + $return_url = admin_url( 'admin.php?page=templately' );
84 + }
85 +
86 + // Unique random state — doubles as cache busting and as the CSRF token the
87 + // callback validates. Only minted into a transient for a logged-in user:
88 + // this endpoint is public, and an anonymous caller could otherwise flood
89 + // wp_options with tokens that can never authorize anything.
90 + $state = wp_generate_password( 32, false );
91 + $state_owner = get_current_user_id();
92 +
93 + if ( $state_owner > 0 ) {
94 + Database::set_transient( 'google_state_' . $state, $state_owner, 15 * MINUTE_IN_SECONDS );
95 + }
96 +
97 + $return_params = [
98 + 'templately_google_login' => '1',
99 + 'templately_state' => $state,
100 + ];
101 +
102 + // Add redirect-to parameter if provided
103 + if (!empty($redirect_to)) {
104 + $return_params['redirect-to'] = $redirect_to;
105 + }
106 +
107 + $site_url_with_params = add_query_arg($return_params, $return_url);
108 +
109 + $query_params = [
110 + 'site_url' => urlencode($site_url_with_params),
111 + 'site_ip' => Helper::get_ip(),
112 + 'state' => $state,
113 + ];
114 +
115 + return add_query_arg($query_params, $auth_url);
116 +}
117 +
118 +/**
63 119 * @param array $args
64 120 * @return string
65 121 */
66 122 protected function prepareArgs( $args ) {
@@ -72,9 +128,9 @@
72 128 case is_string( $value ) && ( $value === 'true' || $value === 'false' ):
73 129 $prepareArgs .= "$key:" . $value . ",";
74 130 break;
75 131 default:
76 - $prepareArgs .= "$key:" . '"' . $value . '"' . ",";
132 + $prepareArgs .= "$key:" . '"' . Helper::esc_json_string( $value ) . '"' . ",";
77 133 break;
78 134 }
79 135 }
80 136
@@ -138,11 +194,13 @@
138 194 $query = $this->query;
139 195 }
140 196
141 197 $headers = [
142 - 'Content-Type' => 'application/json',
143 - 'x-templately-ip' => Helper::get_ip(),
144 - 'x-templately-url' => home_url( '/' )
198 + 'Content-Type' => 'application/json',
199 + 'Accept' => 'application/json',
200 + 'x-templately-ip' => Helper::get_ip(),
201 + 'x-templately-url' => home_url( '/' ),
202 + 'x-templately-version' => TEMPLATELY_VERSION,
145 203 ];
146 204
147 205 if ( ! empty( $args['headers'] ) ) {
148 206 $headers = wp_parse_args( $args['headers'], $headers );
@@ -154,9 +212,9 @@
154 212 Helper::log( 'QUERY: ' . $query );
155 213 }
156 214
157 215 $_default_args = [
158 - 'timeout' => $this->dev_mode ? 40 : 30,
216 + 'timeout' => $this->dev_mode ? 120 : 30,
159 217 'headers' => $headers,
160 218 'body' => wp_json_encode( [
161 219 'query' => $query
162 220 ] )
@@ -161,21 +219,48 @@
161 219 'query' => $query
162 220 ] )
163 221 ];
164 222
165 - $retryCount = 0;
166 - $maxRetries = defined('TEMPLATELY_HTTP_RETRY') ? TEMPLATELY_HTTP_RETRY : 3;
167 - $args = wp_parse_args( $args, $_default_args );
168 - do {
169 - $response = wp_remote_post( $this->url(), $args );
170 - $retryCount++;
171 - } while ( is_wp_error( $response ) && $retryCount < $maxRetries );
223 + $args = wp_parse_args( $args, $_default_args );
172 224
225 + // 043 / PRD PHP-1 — the retry decision moved to RetryPolicy.
226 + //
227 + // This loop retried on WP_Error only, and with NO DELAY: three requests
228 + // within milliseconds at a server that had just failed to answer one. It
229 + // also treated every HTTP status as final, so a 502 from a restarting
230 + // gateway was never retried at all. RetryPolicy adds the transient
231 + // statuses and a jittered backoff.
232 + $attempt = 0;
233 + $maxRetries = defined( 'TEMPLATELY_HTTP_RETRY' ) ? (int) TEMPLATELY_HTTP_RETRY : RetryPolicy::MAX_ATTEMPTS;
234 +
235 + // Entry-point marker (engagement telemetry). A URL QUERY PARAM so the cloud's
236 + // access logs capture it with zero cloud-side code — never a GraphQL argument
237 + // (unknown arguments fail GraphQL validation; a query param on the endpoint
238 + // URL is ignored by the resolver). Appended here, NOT in url(): url() also
239 + // feeds google_auth_url(), which must stay clean.
240 + $request_url = $this->url();
241 + if ( '' !== Helper::get_request_source() ) {
242 + $request_url = add_query_arg( 'tl_source', Helper::get_request_source(), $request_url );
243 + }
244 +
245 + while ( true ) {
246 + $response = wp_remote_post( $request_url, $args );
247 +
248 + if ( $attempt + 1 >= $maxRetries || ! RetryPolicy::should_retry( $response, $attempt ) ) {
249 + break;
250 + }
251 +
252 + RetryPolicy::wait( $attempt );
253 + $attempt++;
254 + }
255 +
256 + $retryCount = $attempt + 1;
257 +
173 258 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
174 259 Helper::log( 'Retry Count: ' . $retryCount );
175 - Helper::log( 'RAW RESPONSE: ' );
176 - Helper::log( $response );
177 - Helper::log( 'END RAW RESPONSE' );
260 + // Helper::log( 'RAW RESPONSE: ' );
261 + // Helper::log( $response );
262 + // Helper::log( 'END RAW RESPONSE' );
178 263 }
179 264
180 265 return $this->maybeErrors( $response, $args );
181 266 }
@@ -180,9 +265,9 @@
180 265 return $this->maybeErrors( $response, $args );
181 266 }
182 267
183 268 /**
184 - * Formating the self::post() response
269 + * Formatting the self::post() response
185 270 *
186 271 * @param mixed $response
187 272 * @param array $args
188 273 * @return mixed
@@ -187,67 +272,37 @@
187 272 * @param array $args
188 273 * @return mixed
189 274 */
190 275 private function maybeErrors( &$response, $args = [] ) {
191 - if ( $response instanceof WP_Error ) {
192 - return $response; // Return WP_Error, if it is an error.
193 - }
276 + // 043 FR-003 — every shape the cloud can return is classified in ONE
277 + // place now. The hand-rolled cascade this replaced grew a branch per
278 + // discovered shape and still disagreed with the equivalent cascade in
279 + // `Helper::make_api_request()`; see the 28 captured fixtures in
280 + // `specs/043-core-api-response-contract/fixtures/`.
281 + $normalized = ResponseNormalizer::normalize( $response, [
282 + 'endpoint' => $this->endpoint,
283 + ] );
194 284
195 - $response_code = wp_remote_retrieve_response_code( $response );
196 - $response_message = wp_remote_retrieve_response_message( $response );
285 + if ( $normalized->is_error() ) {
286 + $error = $normalized->error();
197 287
198 - /**
199 - * Retrive Data from Response Body.
200 - */
201 - $response = json_decode( wp_remote_retrieve_body( $response ), true );
202 - /**
203 - * If the graphql hit with any error.
204 - */
205 - if ( ! empty( $response['errors'] ) ) {
206 288 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
207 - Helper::log( 'ERROR: ' );
208 - Helper::log( $response['errors'] );
209 - Helper::log( 'END ERROR' );
289 + Helper::log( 'ERROR: ' . $error->code() . ' — ' . $error->message() );
210 290 }
211 - if ( is_array( $response['errors'] ) ) {
212 - $wp_error = new WP_Error;
213 - array_walk( $response['errors'], function ( $error ) use ( &$wp_error ) {
214 - if ( isset( $error['message'] ) ) {
215 - if ( $error['message'] === 'validation' ) {
216 - array_walk( $error['extensions'], function ( $_error, $_error_key ) use ( &$wp_error ) {
217 - if ( $_error_key == 'validation' ) {
218 - array_walk( $_error, function ( $v_error, $key ) use ( &$wp_error ) {
219 - $wp_error->add( "{$key}_error", $v_error[0] );
220 - } );
221 - }
222 - } );
223 - } else {
224 - if ( isset( $error['debugMessage'] ) ) {
225 - $wp_error->add( 'templately_graphql_error', $error['debugMessage'] );
226 - } else {
227 - $wp_error->add( 'templately_graphql_error', $error['message'] );
228 - }
229 - }
230 - }
231 - } );
232 291
233 - if( $wp_error->get_error_code() === 'templately_graphql_error' ) {
234 - if( $wp_error->get_error_message() == 'Unauthorized' ) {
235 - $global_user = Login::get_instance()->delete();
292 + // An expired session still tears down the stored login — but it now
293 + // ALSO returns a real error. It used to return a plain array
294 + // (`['redirect' => true, …]`), which every `is_wp_error()` caller
295 + // read as SUCCESS and happily passed on as a payload. That is the
296 + // INV-2 class of bug this contract exists to remove.
297 + if ( ErrorCode::AUTH_EXPIRED === $error->code() || ErrorCode::INVALID_API_KEY === $error->code() ) {
298 + Login::get_instance()->delete();
299 + }
236 300
237 - return [
238 - 'redirect' => true,
239 - 'url' => 'sign-in',
240 - 'user' => $global_user,
241 - ];
242 - }
243 - }
244 -
245 - return $wp_error;
246 - }
301 + return $error;
247 302 }
248 303
249 - $_response = isset( $response['data'][$this->endpoint] ) ? $response['data'][$this->endpoint] : [];
304 + $_response = $normalized->payload();
250 305
251 306 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
252 307 Helper::log( 'RESPONSE: ' );
253 308 Helper::log( $_response );
@@ -255,5 +310,6 @@
255 310 }
256 311
257 312 return $_response;
258 313 }
314 +
259 315 }