'POST', 'callback' => [ $this, 'refresh' ], 'permission_callback' => [ $this, 'allow_public_access' ], ] ); } /** * Always true: the caller is the backend worker, not a logged-in user. * Authentication happens inside the handler via the callback signature. */ public function allow_public_access() { return true; } /** * POST /mintall/v1/internal/mc4p/refresh * * Body: { product_ids: ["", ...] } * * Response: { ok: true, requested: , synced: , unprocessed: [...] } * where `synced` counts products whose stored certificate is now current * (written or already fresh) and `unprocessed` lists ids the request did * not get to, either because the time budget ran out or because the * certificate API stopped answering. Products unknown to Mintall are * neither synced nor unprocessed. Everything not synced stays due on the * backend, which re-pushes it on its next sweep. * * 503 when the initial product lookup fails: nothing was attempted, and the * backend treats 5xx as transient and retries the row with backoff. */ public function refresh( $request ) { $deadline = self::request_start() + self::TIME_BUDGET_SECONDS; if ( ! Callback_Signature::verify( $request, self::REFRESH_PATH ) ) { return $this->invalid_signature(); } $params = $request->get_json_params(); $raw = isset( $params['product_ids'] ) && is_array( $params['product_ids'] ) ? $params['product_ids'] : []; $ids = array_values( array_unique( array_filter( array_map( 'absint', $raw ) ) ) ); if ( empty( $ids ) ) { return $this->error_response( 'Missing product_ids.', 400 ); } if ( count( $ids ) > self::MAX_PRODUCTS ) { return $this->error_response( 'Too many product_ids.', 400 ); } // Read the key at request time; never store or log it. $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $this->error_response( 'Not connected to Mintall.', 409 ); } // One lookup feeds every per-product sync (mirrors bulk_enable_visibility). // If it fails the certificate API is unreachable, so every per-product // fallback would fail the same way: bail now and let the backend retry. $ext_ids = array_map( 'strval', $ids ); $budget = self::seconds_left( $deadline ); $rows = $budget >= 1 ? Mintall_Products::lookup( $api_key, $ext_ids, $budget ) : new \WP_Error( 'mintall_budget_exhausted', 'No time budget left for lookup.' ); if ( is_wp_error( $rows ) ) { return new \WP_REST_Response( [ 'error' => 'upstream_unavailable' ], 503 ); } $row_map = []; foreach ( (array) $rows as $row ) { $ext = (string) ( $row['external_product_id'] ?? '' ); if ( '' !== $ext ) { $row_map[ $ext ] = $row; } } $synced = 0; $unprocessed = []; foreach ( $ext_ids as $index => $ext_id ) { if ( ! isset( $row_map[ $ext_id ] ) ) { // The lookup succeeded and didn't return this id: unknown to // Mintall. Skip rather than let the sync re-look it up. continue; } if ( self::seconds_left( $deadline ) < 1 ) { $unprocessed = array_slice( $ext_ids, $index ); break; } $result = Mintall_MC4P::sync_product_certificate( $api_key, (int) $ext_id, $row_map[ $ext_id ], $deadline ); if ( null === $result ) { // Transient failure after retries: the API is degraded, so // stop instead of spending the remaining ids on it too. $unprocessed = array_slice( $ext_ids, $index ); break; } if ( $result ) { ++$synced; } } return new \WP_REST_Response( [ 'ok' => true, 'requested' => count( $ext_ids ), 'synced' => $synced, 'unprocessed' => array_values( array_intersect( $unprocessed, array_keys( $row_map ) ) ), ] ); } /** * When PHP started handling this request, as a microtime float. */ private static function request_start() { $start = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? (float) $_SERVER['REQUEST_TIME_FLOAT'] : 0.0; return $start > 0 ? $start : microtime( true ); } /** * Whole seconds left before the deadline, never below zero. */ private static function seconds_left( $deadline ) { return max( 0, (int) floor( $deadline - microtime( true ) ) ); } /** * 403 for a request whose signature did not verify. The backend treats * 4xx as a permanent failure for this attempt and retries with backoff. */ private function invalid_signature() { return new \WP_REST_Response( [ 'error' => 'invalid_signature' ], 403 ); } }