PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.7
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.7
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
image-optimization / classes / client / client.php

client.php in Image Optimizer – Compress Images and Convert to WebP or AVIF 1.7.7, at classes/client/client.php

347 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimization\Classes\Client;
4
5 use ImageOptimization\Classes\Exceptions\Client_Exception;
6 use ImageOptimization\Classes\File_Utils;
7 use ImageOptimization\Classes\Image\Image;
8 use ImageOptimization\Classes\Logger;
9 use ImageOptimization\Modules\Connect\Module as Connect_Module;
10 use ImageOptimization\Modules\Optimization\Classes\Optimize_Image;
11 use ImageOptimization\Modules\Stats\Classes\Optimization_Stats;
12 use Throwable;
13 use WP_Error;
14
15 use ImageOptimization\Plugin;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Class Client
23 */
24 class Client {
25 const BASE_URL = 'https://my.elementor.com/api/v2/image-optimizer/';
26 const BASE_URL_FEEDBACK = 'https://feedback-api.prod.apps.elementor.red/apps/api/v1/';
27 const SITE_INFO = 'site/info';
28 const SITE_INFO_TRANSIENT = 'image_optimizer_site_info';
29 const SITE_INFO_TRANSIENT_ERROR = 'image_optimizer_site_info_error';
30 const ERROR_CACHE_DURATION = MINUTE_IN_SECONDS * 5;
31
32 private bool $refreshed = false;
33
34 public static ?Client $instance = null;
35
36 /**
37 * get_instance
38 * @return Client|null
39 */
40 public static function get_instance(): ?Client {
41 if ( ! self::$instance ) {
42 self::$instance = new self();
43 }
44
45 return self::$instance;
46 }
47
48 public static function get_site_info( $endpoint = null ): array {
49 $data = [
50 // Which API version is used.
51 'app_version' => IMAGE_OPTIMIZATION_VERSION,
52 // Which language to return.
53 'site_lang' => get_bloginfo( 'language' ),
54 // site to connect
55 'site_url' => trailingslashit( home_url() ),
56 // current user
57 'local_id' => get_current_user_id(),
58
59 ];
60
61 if ( Optimize_Image::IMAGE_OPTIMIZE_ENDPOINT === $endpoint ) {
62 // Media library stats
63 $data['media_data'] = base64_encode( wp_json_encode( self::get_request_stats() ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
64 }
65
66 return $data;
67 }
68
69 /**
70 * Get site info
71 * @return mixed|WP_Error|null
72 */
73 public static function get_subscription_info() {
74 $cached_info = get_transient( self::SITE_INFO_TRANSIENT );
75
76 if ( $cached_info ) {
77 return $cached_info;
78 }
79
80 $cached_error = get_transient( self::SITE_INFO_TRANSIENT_ERROR );
81
82 if ( $cached_error ) {
83 Logger::debug( 'Status check skipped due to recent error: ' . $cached_error );
84 return null;
85 }
86
87 try {
88 $info = self::get_instance()->make_request( 'POST', self::SITE_INFO );
89 } catch ( Throwable $t ) {
90 $error_message = $t->getMessage();
91
92 Logger::error( 'Cannot get site info response from service: ' . $error_message );
93
94 set_transient( self::SITE_INFO_TRANSIENT_ERROR, $error_message, self::ERROR_CACHE_DURATION );
95
96 return null;
97 }
98
99 set_transient( self::SITE_INFO_TRANSIENT, $info, DAY_IN_SECONDS );
100
101 return $info;
102 }
103
104 private static function get_request_stats(): array {
105 $optimization_stats = Optimization_Stats::get_image_stats();
106 $image_sizes = [];
107
108 foreach ( wp_get_registered_image_subsizes() as $image_size_key => $image_size_data ) {
109 $image_sizes[] = [
110 'label' => $image_size_key,
111 'size' => "{$image_size_data['width']}x{$image_size_data['height']}",
112 ];
113 }
114
115 return [
116 'not_optimized_images' => $optimization_stats['total_image_count'] - $optimization_stats['optimized_image_count'],
117 'optimized_images' => $optimization_stats['optimized_image_count'],
118 'images_sizes' => $image_sizes,
119 ];
120 }
121
122 public function make_request( $method, $endpoint, $body = [], array $headers = [], $file = false, $file_name = '' ) {
123 $headers = array_replace_recursive([
124 'x-elementor-image-optimizer' => IMAGE_OPTIMIZATION_VERSION,
125 ], $headers);
126
127 $headers = array_replace_recursive(
128 $headers,
129 $this->is_connected() ? $this->generate_authentication_headers( $endpoint ) : []
130 );
131
132 $body = array_replace_recursive( $body, $this->get_site_info( $endpoint ) );
133
134 try {
135 if ( $file ) {
136 $boundary = wp_generate_password( 24, false );
137 $body = $this->get_upload_request_body( $body, $file, $boundary, $file_name );
138 // add content type header
139 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
140 }
141 } catch ( Client_Exception $ce ) {
142 return new WP_Error( 500, $ce->getMessage() );
143 }
144
145 $response = $this->request(
146 $method,
147 $endpoint,
148 [
149 'timeout' => 50,
150 'headers' => $headers,
151 'body' => $body,
152 ]
153 );
154
155 return ( new Client_Response( $response ) )->handle();
156 }
157
158 public static function get_feedback_base_url() {
159 return apply_filters( 'image_optimizer_feedback_base_url', self::BASE_URL_FEEDBACK );
160 }
161
162 private static function get_remote_url( $endpoint ): string {
163 $base_url = apply_filters( 'image_optimizer_client_get_base_url', self::BASE_URL );
164
165 if ( strpos( $endpoint, 'feedback/' ) !== false ) {
166 return self::get_feedback_base_url() . $endpoint;
167 }
168
169 return $base_url . $endpoint;
170 }
171
172 protected function is_connected(): bool {
173 return Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->is_connected();
174 }
175
176 protected function generate_authentication_headers( $endpoint ): array {
177 $connect_instance = Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance;
178
179 if ( ! $connect_instance->get_is_connect_on_fly() ) {
180 $headers = [
181 'data' => base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
182 wp_json_encode( [ 'app' => 'library' ] )
183 ),
184 'access_token' => $connect_instance->get_access_token() ?? '',
185 'client_id' => $connect_instance->get_client_id() ?? '',
186 ];
187
188 if ( $connect_instance->is_activated() ) {
189 $headers['key'] = $connect_instance->get_activation_state() ?? '';
190 }
191 } else {
192 $headers = $this->add_bearer_token([
193 'x-elementor-apps-connect' => true,
194 ]);
195 }
196
197 $headers['endpoint'] = $endpoint;
198 $headers['x-elementor-apps'] = 'image-optimizer';
199
200 return $headers;
201 }
202
203 protected function request( $method, $endpoint, $args = [] ) {
204 $args['method'] = $method;
205
206 $response = wp_remote_request(
207 self::get_remote_url( $endpoint ),
208 $args
209 );
210
211 if ( is_wp_error( $response ) ) {
212 $message = $response->get_error_message();
213
214 return new WP_Error(
215 $response->get_error_code(),
216 is_array( $message ) ? join( ', ', $message ) : $message
217 );
218 }
219
220 $body = wp_remote_retrieve_body( $response );
221 $response_code = (int) wp_remote_retrieve_response_code( $response );
222
223 if ( ! $response_code ) {
224 return new WP_Error( 500, 'No Response' );
225 }
226
227 // Server sent a success message without content.
228 if ( 'null' === $body ) {
229 $body = true;
230 }
231
232 $body = json_decode( $body );
233
234 if ( false === $body ) {
235 return new WP_Error( 422, 'Wrong Server Response' );
236 }
237
238 if ( Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->get_is_connect_on_fly() ) {
239 // If the token is invalid, refresh it and try again once only.
240 if ( ! $this->refreshed && ! empty( $body->message ) && ( false !== strpos( $body->message, 'Invalid Token' ) ) ) {
241 Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->refresh_token();
242 $this->refreshed = true;
243 $args['headers'] = $this->add_bearer_token( $args['headers'] );
244 return $this->request( $method, $endpoint, $args );
245 }
246 }
247
248 // If there is mismatch then trigger the mismatch flow explicitly.
249 if ( ! $this->refreshed && ! empty( $body->message ) && ( false !== strpos( $body->message, 'site url mismatch' ) ) ) {
250 Connect_Module::get_connect()->data()->set_home_url( 'https://wrongurl' );
251 return new WP_Error( 401, 'site url mismatch' );
252 }
253
254 if ( ! in_array( $response_code, [ 200, 201 ], true ) ) {
255 // In case $as_array = true.
256 $message = $body->message ?? wp_remote_retrieve_response_message( $response );
257 $message = is_array( $message ) ? join( ', ', $message ) : $message;
258 $code = isset( $body->code ) ? (int) $body->code : $response_code;
259
260 return new WP_Error( $code, $message );
261 }
262
263 return $body;
264 }
265
266 public function add_bearer_token( $headers ) {
267 if ( $this->is_connected() ) {
268 $headers['Authorization'] = 'Bearer ' . Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->get_access_token();
269 }
270 return $headers;
271 }
272
273 /**
274 * get_upload_request_body
275 *
276 * @param array $body
277 * @param $file
278 * @param string $boundary
279 * @param string $file_name
280 *
281 * @return string
282 * @throws Client_Exception
283 */
284 private function get_upload_request_body( array $body, $file, string $boundary, string $file_name = '' ): string {
285 $payload = '';
286
287 // add all body fields as standard POST fields:
288 foreach ( $body as $name => $value ) {
289 $payload .= '--' . $boundary;
290 $payload .= "\r\n";
291 $payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . '"' . "\r\n\r\n";
292 $payload .= $value;
293 $payload .= "\r\n";
294 }
295
296 if ( is_array( $file ) ) {
297 foreach ( $file as $key => $file_data ) {
298 $payload .= $this->get_file_payload( $file_data['name'], $file_data['type'], $file_data['path'], $boundary );
299 }
300 } else {
301 $image_mime = image_type_to_mime_type( exif_imagetype( $file ) );
302
303 if (
304 ! in_array( $image_mime, Image::get_supported_mime_types(), true ) &&
305 ( 'application/octet-stream' === $image_mime && 'avif' !== File_Utils::get_extension( $file ) )
306 ) {
307 throw new Client_Exception( esc_html( "Unsupported mime type `$image_mime`" ) );
308 }
309
310 if ( empty( $file_name ) ) {
311 $file_name = basename( $file );
312 }
313
314 $payload .= $this->get_file_payload( $file_name, $image_mime, $file, $boundary );
315 }
316
317 $payload .= '--' . $boundary . '--';
318
319 return $payload;
320 }
321
322 /**
323 * get_file_payload
324 * @param string $filename
325 * @param string $file_type
326 * @param string $file_path
327 * @param string $boundary
328 * @return string
329 */
330 private function get_file_payload( string $filename, string $file_type, string $file_path, string $boundary ): string {
331 $name = $filename ?? basename( $file_path );
332 $mime_type = 'image' === $file_type ? image_type_to_mime_type( exif_imagetype( $file_path ) ) : $file_type;
333 $payload = '';
334
335 // Upload the file
336 $payload .= '--' . $boundary;
337 $payload .= "\r\n";
338 $payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) . '"; filename="' . esc_attr( $name ) . '"' . "\r\n";
339 $payload .= 'Content-Type: ' . $mime_type . "\r\n";
340 $payload .= "\r\n";
341 $payload .= file_get_contents( $file_path );
342 $payload .= "\r\n";
343
344 return $payload;
345 }
346 }
347