array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'email' => $email, 'site_url' => home_url(), 'site_secret' => LeanPixel_Verify::new_secret(), ) ), 'timeout' => 45, ), '' ); LeanPixel_Verify::forget_secret(); return $result; } /** * Confirm the email address of a new free key with the emailed code. * * @param string $code The 6-digit code. * @return array|WP_Error { account } */ public static function confirm( $code ) { $result = self::request( 'POST', '/confirm', array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'code' => $code ) ), 'timeout' => 20, ) ); if ( ! is_wp_error( $result ) && isset( $result['account'] ) ) { self::remember_account( $result['account'] ); } return $result; } /** * Ask for a new confirmation code. * * @return array|WP_Error */ public static function resend() { return self::request( 'POST', '/resend', array( 'timeout' => 30 ) ); } /** * Fetch plan and usage for a key. * * @param string|null $key Key to check, or null for the stored key. * @return array|WP_Error */ public static function account( $key = null ) { $result = self::request( 'GET', '/account', array( 'timeout' => 20 ), $key ); if ( ! is_wp_error( $result ) && null === $key ) { self::remember_account( $result['account'] ); } return $result; } /** * Send one image file for optimization. * * @param string $file Absolute path. * @param string $mime Mime type of the file. * @param string $image Id shared by every file of one image, so the image is counted once. * @return array|WP_Error Decoded response; file data is still base64. */ public static function optimize( $file, $mime, $image = '' ) { $settings = LeanPixel_Settings::all(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file. $body = file_get_contents( $file ); if ( false === $body ) { return new WP_Error( 'unreadable', __( 'The file could not be read.', 'lean-pixel' ) ); } $query = array( 'image' => $image, 'level' => $settings['level'], 'webp' => $settings['webp'] ? 1 : 0, 'avif' => $settings['avif'] ? 1 : 0, 'keep_metadata' => $settings['keep_metadata'] ? 1 : 0, ); $result = self::request( 'POST', '/optimize?' . http_build_query( $query ), array( 'headers' => array( 'Content-Type' => $mime, 'Accept' => self::BUNDLE_TYPE . ', application/json', ), 'body' => $body, 'timeout' => 120, // The reply holds at most three files, none larger than the upload. 'limit_response_size' => 3 * strlen( $body ) + MB_IN_BYTES, ) ); unset( $body ); if ( ! is_wp_error( $result ) && isset( $result['account'] ) ) { self::remember_account( $result['account'] ); } return $result; } /** Last known plan and usage, without a network call. */ public static function cached_account() { $account = get_option( LeanPixel_Settings::ACCOUNT, array() ); return is_array( $account ) ? $account : array(); } public static function remember_account( $account ) { if ( ! is_array( $account ) ) { return; } // Keep only the fields the plugin shows, each forced to its expected type. $clean = array(); foreach ( array( 'email', 'plan', 'plan_name', 'period', 'resets_at' ) as $field ) { if ( isset( $account[ $field ] ) && is_scalar( $account[ $field ] ) ) { $clean[ $field ] = substr( sanitize_text_field( (string) $account[ $field ] ), 0, 190 ); } } // Whether the plan includes AVIF. Services older than this field included it for everyone. $clean['avif'] = ( ! isset( $account['avif'] ) || ! empty( $account['avif'] ) ) ? 1 : 0; foreach ( array( 'limit', 'used', 'max_file_bytes', 'max_sites' ) as $field ) { if ( isset( $account[ $field ] ) && is_numeric( $account[ $field ] ) ) { $clean[ $field ] = absint( $account[ $field ] ); } } update_option( LeanPixel_Settings::ACCOUNT, $clean, false ); } /** * Read a bundle reply: 4 bytes holding the length of a JSON header, the * header, then the files it lists, in the order optimized, webp, avif. * * @param string $body Raw response body. * @return array|null The header with each file's bytes added, or null if malformed. */ private static function parse_bundle( $body ) { if ( strlen( $body ) < 4 ) { return null; } $length = unpack( 'N', substr( $body, 0, 4 ) ); $length = (int) $length[1]; if ( $length < 2 || $length > strlen( $body ) - 4 || $length > MB_IN_BYTES ) { return null; } $data = json_decode( substr( $body, 4, $length ), true ); if ( ! is_array( $data ) ) { return null; } $offset = 4 + $length; foreach ( array( 'optimized', 'webp', 'avif' ) as $name ) { if ( empty( $data[ $name ] ) || ! is_array( $data[ $name ] ) ) { $data[ $name ] = null; continue; } $size = isset( $data[ $name ]['size'] ) ? (int) $data[ $name ]['size'] : 0; $bytes = $size > 0 ? substr( $body, $offset, $size ) : ''; if ( $size < 1 || strlen( $bytes ) !== $size ) { return null; } $data[ $name ]['bytes'] = $bytes; $offset += $size; } return $data; } /** * @param string $method HTTP method. * @param string $path Path below the API base URL. * @param array $args wp_remote_request arguments. * @param string|null $key Key to use; null for the stored key, '' for none. * @return array|WP_Error */ private static function request( $method, $path, array $args, $key = null ) { if ( null === $key ) { $key = (string) LeanPixel_Settings::get( 'api_key' ); if ( '' === $key ) { return new WP_Error( 'not_connected', __( 'LeanPixel is not connected yet.', 'lean-pixel' ) ); } } $args['method'] = $method; $args['headers'] = array_merge( array( 'Accept' => 'application/json' ), isset( $args['headers'] ) ? $args['headers'] : array(), array( 'X-Site-Url' => home_url() ) ); if ( '' !== $key ) { $args['headers']['X-Api-Key'] = $key; } $args['user-agent'] = 'LeanPixel/' . LEANPIXEL_VERSION . '; ' . home_url(); $response = wp_remote_request( untrailingslashit( LEANPIXEL_API_URL ) . $path, $args ); if ( is_wp_error( $response ) ) { return new WP_Error( 'connection_failed', /* translators: %s: error detail */ sprintf( __( 'Could not reach the optimization service: %s', 'lean-pixel' ), $response->get_error_message() ) ); } $status = (int) wp_remote_retrieve_response_code( $response ); $type = (string) wp_remote_retrieve_header( $response, 'content-type' ); $data = 0 === stripos( $type, self::BUNDLE_TYPE ) ? self::parse_bundle( wp_remote_retrieve_body( $response ) ) : json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) ) { return new WP_Error( 'bad_response', /* translators: %d: HTTP status code */ sprintf( __( 'The optimization service returned an unexpected response (HTTP %d).', 'lean-pixel' ), $status ) ); } if ( empty( $data['ok'] ) ) { $code = isset( $data['error']['code'] ) ? sanitize_key( $data['error']['code'] ) : 'api_error'; $message = isset( $data['error']['message'] ) ? sanitize_text_field( $data['error']['message'] ) : __( 'The optimization service reported an error.', 'lean-pixel' ); return new WP_Error( $code, $message ); } return $data; } }