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 +72 -87 3.6.7 → 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 }
@@ -204,16 +219,43 @@
204 219 'query' => $query
205 220 ] )
206 221 ];
207 222
208 - $retryCount = 0;
209 - $maxRetries = defined('TEMPLATELY_HTTP_RETRY') ? TEMPLATELY_HTTP_RETRY : 3;
210 - $args = wp_parse_args( $args, $_default_args );
211 - do {
212 - $response = wp_remote_post( $this->url(), $args );
213 - $retryCount++;
214 - } while ( is_wp_error( $response ) && $retryCount < $maxRetries );
223 + $args = wp_parse_args( $args, $_default_args );
215 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 +
216 258 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
217 259 Helper::log( 'Retry Count: ' . $retryCount );
218 260 // Helper::log( 'RAW RESPONSE: ' );
219 261 // Helper::log( $response );
@@ -230,96 +272,38 @@
230 272 * @param array $args
231 273 * @return mixed
232 274 */
233 275 private function maybeErrors( &$response, $args = [] ) {
234 - if ( $response instanceof WP_Error ) {
235 - return $response; // Return WP_Error, if it is an error.
236 - }
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 + ] );
237 284
238 - // Check for verification header before processing response body
239 - Helper::check_verification_header( $response );
240 - Helper::check_site_disconnection( $response );
285 + if ( $normalized->is_error() ) {
286 + $error = $normalized->error();
241 287
242 - $response_code = wp_remote_retrieve_response_code( $response );
243 - $response_message = wp_remote_retrieve_response_message( $response );
244 -
245 - /**
246 - * Retrieve Data from Response Body.
247 - */
248 - $response = json_decode( wp_remote_retrieve_body( $response ), true );
249 - /**
250 - * If the graphql hit with any error.
251 - */
252 - if ( ! empty( $response['errors'] ) ) {
253 288 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
254 - Helper::log( 'ERROR: ' );
255 - Helper::log( $response['errors'] );
256 - Helper::log( 'END ERROR' );
289 + Helper::log( 'ERROR: ' . $error->code() . ' — ' . $error->message() );
257 290 }
258 - if ( is_array( $response['errors'] ) ) {
259 - $wp_error = new WP_Error;
260 - array_walk( $response['errors'], function ( $error ) use ( &$wp_error ) {
261 - if ( isset( $error['message'] ) ) {
262 - if ( $error['message'] === 'validation' ) {
263 - array_walk( $error['extensions'], function ( $_error, $_error_key ) use ( &$wp_error ) {
264 - if ( $_error_key == 'validation' ) {
265 - array_walk( $_error, function ( $v_error, $key ) use ( &$wp_error ) {
266 - $wp_error->add( "{$key}_error", $v_error[0] );
267 - } );
268 - }
269 - } );
270 - } else {
271 - $error_data = [];
272 - if(!empty($error["extensions"]["statusText"])) {
273 - $error_data["statusText"] = $error["extensions"]["statusText"];
274 - }
275 - if ( isset( $error['debugMessage'] ) ) {
276 - $wp_error->add( 'templately_graphql_error', $error['debugMessage'] );
277 - } else {
278 - $wp_error->add( 'templately_graphql_error', $error['message'], $error_data );
279 - }
280 - }
281 - }
282 - } );
283 291
284 - if( $wp_error->get_error_code() === 'templately_graphql_error' ) {
285 - if( $wp_error->get_error_message() == 'Unauthorized' ) {
286 - $global_user = Login::get_instance()->delete();
287 -
288 - return [
289 - 'redirect' => true,
290 - 'url' => 'sign-in',
291 - 'user' => $global_user,
292 - ];
293 - }
294 - }
295 -
296 - 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();
297 299 }
298 - } elseif ( ! empty( $response['status'] ) && $response['status'] === 'error' ) {
299 300
300 - if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
301 - Helper::log( 'ERROR: ' );
302 - Helper::log( $response );
303 - Helper::log( 'END ERROR' );
304 - }
305 -
306 - $error_data = [];
307 - if ( ! empty( $response['statusText'] ) ) {
308 - $error_data['statusText'] = $response['statusText'];
309 - }
310 -
311 - $error_message = ! empty( $response['message'] ) ? $response['message'] : __( 'Unknown error occurred', 'templately' );
312 -
313 - return new WP_Error( 'templately_api_error', $error_message, $error_data );
301 + return $error;
314 302 }
315 303
316 - $_response = isset( $response['data'][$this->endpoint] ) ? $response['data'][$this->endpoint] : [];
317 - // {"data":{"connectWithApiKey":{"status":"error","message":"Invalid API key.","user":null}}}
318 - if ( ! empty( $response['status'] ) && $response['status'] === 'error' ) {
304 + $_response = $normalized->payload();
319 305
320 - }
321 -
322 306 if ( defined( 'TEMPLATELY_DEBUG_LOG' ) && TEMPLATELY_DEBUG_LOG ) {
323 307 Helper::log( 'RESPONSE: ' );
324 308 Helper::log( $_response );
325 309 Helper::log( 'END RESPONSE' );
@@ -326,5 +310,6 @@
326 310 }
327 311
328 312 return $_response;
329 313 }
314 +
330 315 }