settings = $settings; $this->signers = $signers; $this->logger = $logger; } /** * POST a payload to an S2S endpoint. * * @param string $endpoint Endpoint suffix, e.g. events. * @param array $payload JSON-serialisable body. * @return Response */ public function send( string $endpoint, array $payload ): Response { $body = wp_json_encode( $payload ); if ( ! is_string( $body ) ) { // Almost always a string somewhere in the payload that is not valid UTF-8. Retrying will // produce the same result, so this is reported as a permanent failure rather than a timeout. return Response::failed( __( 'The event payload could not be encoded as JSON.', 'adpix-analytics' ) ); } return $this->request( 'POST', $endpoint, array(), $body ); } /** * GET a resource from an S2S endpoint. * * @param string $endpoint Endpoint suffix, e.g. attribution. * @param array $query Query parameters. * @return Response */ public function fetch( string $endpoint, array $query = array() ): Response { return $this->request( 'GET', $endpoint, $query, '' ); } /** * Make one authenticated request. * * @param string $method HTTP method. * @param string $endpoint Endpoint suffix. * @param array $query Query parameters. * @param string $body Request body. * @return Response */ private function request( string $method, string $endpoint, array $query, string $body ): Response { $settings = $this->settings->load(); if ( ! $settings->is_connected() ) { return Response::failed( __( 'AdPix is not connected: set the property ID and server key.', 'adpix-analytics' ) ); } $url = $settings->collector_url() . self::BASE_PATH . ltrim( $endpoint, '/' ); if ( array() !== $query ) { $url = add_query_arg( array_map( 'rawurlencode', array_map( 'strval', $query ) ), $url ); } if ( ! wp_http_validate_url( $url ) ) { return Response::failed( __( 'The configured AdPix collector URL is not valid.', 'adpix-analytics' ) ); } $headers = array( 'Accept' => 'application/json', 'X-Sov-Site' => $settings->str( 'property_id' ), 'Authorization' => 'Bearer ' . $settings->str( 'server_key' ), ); if ( '' !== $body ) { $headers['Content-Type'] = 'application/json; charset=utf-8'; } // Signing happens last, over the exact bytes about to be sent. Anything that edited the body // afterwards would invalidate the signature. $headers = $this->signers->for_settings( $settings )->sign( $headers, $body ); $response = wp_remote_request( $url, array( 'method' => $method, 'headers' => $headers, 'body' => '' !== $body ? $body : null, 'timeout' => (int) apply_filters( 'adpix_analytics_http_timeout', self::TIMEOUT, $endpoint ), 'redirection' => 0, 'httpversion' => '1.1', 'user-agent' => $this->user_agent(), 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { $this->logger->error( 'Transport failure', array( 'endpoint' => $endpoint, 'code' => $response->get_error_code(), 'message' => $response->get_error_message(), ) ); return Response::failed( $response->get_error_message() ); } $status = (int) wp_remote_retrieve_response_code( $response ); $decoded = json_decode( (string) wp_remote_retrieve_body( $response ), true ); $result = new Response( $status, is_array( $decoded ) ? $decoded : array() ); if ( $result->is_ok() ) { $this->logger->debug( 'Request succeeded', array( 'endpoint' => $endpoint, 'status' => $status ) ); return $result; } // A missing snapshot is an ordinary answer to "has this order been attributed yet", not a fault, // and logging it would fill the site owner's error list every time they opened an old order. if ( 404 !== $status ) { // The payload is deliberately absent from the log: it carries the customer's email address // and possibly their phone number, and a debug log is a file anyone with FTP can read. $this->logger->error( 'The collector rejected a call', array( 'endpoint' => $endpoint, 'status' => $status, 'message' => $result->error(), 'request_id' => $result->request_id(), ) ); } return $result; } /** * Identify the caller, so a property owner can tell this integration apart from their own scripts. * * @return string */ private function user_agent(): string { return 'AdPixAnalyticsWP/' . ADPIX_ANALYTICS_VERSION . '; ' . home_url( '/' ); } }