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 +73 -87 3.6.5 → 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 /**
@@ -79,10 +82,22 @@
79 82 if ( empty( $return_url ) ) {
80 83 $return_url = admin_url( 'admin.php?page=templately' );
81 84 }
82 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 +
83 97 $return_params = [
84 98 'templately_google_login' => '1',
99 + 'templately_state' => $state,
85 100 ];
86 101
87 102 // Add redirect-to parameter if provided
88 103 if (!empty($redirect_to)) {
@@ -93,9 +108,9 @@
93 108
94 109 $query_params = [
95 110 'site_url' => urlencode($site_url_with_params),
96 111 'site_ip' => Helper::get_ip(),
97 - 'state' => wp_generate_password(32, false) // Add unique random state for cache busting
112 + 'state' => $state,
98 113 ];
99 114
100 115 return add_query_arg($query_params, $auth_url);
101 116 }
@@ -180,8 +195,9 @@
180 195 }
181 196
182 197 $headers = [
183 198 'Content-Type' => 'application/json',
199 + 'Accept' => 'application/json',
184 200 'x-templately-ip' => Helper::get_ip(),
185 201 'x-templately-url' => home_url( '/' ),
186 202 'x-templately-version' => TEMPLATELY_VERSION,
187 203 ];
@@ -203,16 +219,43 @@
203 219 'query' => $query
204 220 ] )
205 221 ];
206 222
207 - $retryCount = 0;
208 - $maxRetries = defined('TEMPLATELY_HTTP_RETRY') ? TEMPLATELY_HTTP_RETRY : 3;
209 - $args = wp_parse_args( $args, $_default_args );
210 - do {
211 - $response = wp_remote_post( $this->url(), $args );
212 - $retryCount++;
213 - } while ( is_wp_error( $response ) && $retryCount < $maxRetries );
223 + $args = wp_parse_args( $args, $_default_args );
214 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 +
215 258 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
216 259 Helper::log( 'Retry Count: ' . $retryCount );
217 260 // Helper::log( 'RAW RESPONSE: ' );
218 261 // Helper::log( $response );
@@ -229,96 +272,38 @@
229 272 * @param array $args
230 273 * @return mixed
231 274 */
232 275 private function maybeErrors( &$response, $args = [] ) {
233 - if ( $response instanceof WP_Error ) {
234 - return $response; // Return WP_Error, if it is an error.
235 - }
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 + ] );
236 284
237 - // Check for verification header before processing response body
238 - Helper::check_verification_header( $response );
239 - Helper::check_site_disconnection( $response );
285 + if ( $normalized->is_error() ) {
286 + $error = $normalized->error();
240 287
241 - $response_code = wp_remote_retrieve_response_code( $response );
242 - $response_message = wp_remote_retrieve_response_message( $response );
243 -
244 - /**
245 - * Retrieve Data from Response Body.
246 - */
247 - $response = json_decode( wp_remote_retrieve_body( $response ), true );
248 - /**
249 - * If the graphql hit with any error.
250 - */
251 - if ( ! empty( $response['errors'] ) ) {
252 288 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
253 - Helper::log( 'ERROR: ' );
254 - Helper::log( $response['errors'] );
255 - Helper::log( 'END ERROR' );
289 + Helper::log( 'ERROR: ' . $error->code() . ' — ' . $error->message() );
256 290 }
257 - if ( is_array( $response['errors'] ) ) {
258 - $wp_error = new WP_Error;
259 - array_walk( $response['errors'], function ( $error ) use ( &$wp_error ) {
260 - if ( isset( $error['message'] ) ) {
261 - if ( $error['message'] === 'validation' ) {
262 - array_walk( $error['extensions'], function ( $_error, $_error_key ) use ( &$wp_error ) {
263 - if ( $_error_key == 'validation' ) {
264 - array_walk( $_error, function ( $v_error, $key ) use ( &$wp_error ) {
265 - $wp_error->add( "{$key}_error", $v_error[0] );
266 - } );
267 - }
268 - } );
269 - } else {
270 - $error_data = [];
271 - if(!empty($error["extensions"]["statusText"])) {
272 - $error_data["statusText"] = $error["extensions"]["statusText"];
273 - }
274 - if ( isset( $error['debugMessage'] ) ) {
275 - $wp_error->add( 'templately_graphql_error', $error['debugMessage'] );
276 - } else {
277 - $wp_error->add( 'templately_graphql_error', $error['message'], $error_data );
278 - }
279 - }
280 - }
281 - } );
282 291
283 - if( $wp_error->get_error_code() === 'templately_graphql_error' ) {
284 - if( $wp_error->get_error_message() == 'Unauthorized' ) {
285 - $global_user = Login::get_instance()->delete();
286 -
287 - return [
288 - 'redirect' => true,
289 - 'url' => 'sign-in',
290 - 'user' => $global_user,
291 - ];
292 - }
293 - }
294 -
295 - return $wp_error;
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();
296 299 }
297 - } elseif ( ! empty( $response['status'] ) && $response['status'] === 'error' ) {
298 300
299 - if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
300 - Helper::log( 'ERROR: ' );
301 - Helper::log( $response );
302 - Helper::log( 'END ERROR' );
303 - }
304 -
305 - $error_data = [];
306 - if ( ! empty( $response['statusText'] ) ) {
307 - $error_data['statusText'] = $response['statusText'];
308 - }
309 -
310 - $error_message = ! empty( $response['message'] ) ? $response['message'] : __( 'Unknown error occurred', 'templately' );
311 -
312 - return new WP_Error( 'templately_api_error', $error_message, $error_data );
301 + return $error;
313 302 }
314 303
315 - $_response = isset( $response['data'][$this->endpoint] ) ? $response['data'][$this->endpoint] : [];
316 - // {"data":{"connectWithApiKey":{"status":"error","message":"Invalid API key.","user":null}}}
317 - if ( ! empty( $response['status'] ) && $response['status'] === 'error' ) {
304 + $_response = $normalized->payload();
318 305
319 - }
320 -
321 306 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
322 307 Helper::log( 'RESPONSE: ' );
323 308 Helper::log( $_response );
324 309 Helper::log( 'END RESPONSE' );
@@ -325,5 +310,6 @@
325 310 }
326 311
327 312 return $_response;
328 313 }
314 +
329 315 }