PluginProbe ʕ •ᴥ•ʔ
Hostinger Reach – AI-Powered Email Marketing for WordPress / 1.8.0
Hostinger Reach – AI-Powered Email Marketing for WordPress v1.8.0
1.8.0 1.7.2 1.7.1 1.7.0 1.6.0 1.5.9 1.5.8 1.5.7 1.5.6 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 trunk 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6
hostinger-reach / src / Api / Handlers / ReachApiHandler.php
hostinger-reach / src / Api / Handlers Last commit date
ApiHandler.php 3 days ago FormsApiHandler.php 6 months ago HostingApiHandler.php 3 months ago IntegrationsApiHandler.php 1 month ago ReachApiHandler.php 3 days ago
ReachApiHandler.php
582 lines
1 <?php
2
3 namespace Hostinger\Reach\Api\Handlers;
4
5 use Hostinger\Reach\Api\ApiKeyManager;
6 use Hostinger\Reach\Api\ResourceIdManager;
7 use Hostinger\Reach\Functions;
8 use Hostinger\Reach\Integrations\Reach\ReachFormIntegration;
9 use WP_Error;
10 use WP_REST_Request;
11 use WP_REST_Response;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 die;
15 }
16
17 class ReachApiHandler extends ApiHandler {
18 public const CONNECTION_SUCCESS_TRANSIENT = 'hostinger_reach_connection_success';
19 private const CONNECTION_SUCCESS_TRANSIENT_EXPIRATION = MINUTE_IN_SECONDS;
20
21 protected string $hostinger_auth_url;
22 protected string $reach_domain;
23 public ApiKeyManager $api_key_manager;
24 public ResourceIdManager $resource_id_manager;
25
26 public function __construct( Functions $functions, ApiKeyManager $api_key_manager, ResourceIdManager $resource_id_manager ) {
27 parent::__construct( $functions );
28 $this->api_key_manager = $api_key_manager;
29 $this->resource_id_manager = $resource_id_manager;
30 $this->set_api_base_name();
31 $this->init_hooks();
32 }
33
34
35 public function get_not_connected_error_message(): string {
36 return __( 'Your site is not connected to Reach. Connect to Reach and try again.', 'hostinger-reach' );
37 }
38
39 public function init_hooks(): void {
40
41 add_filter(
42 'allowed_http_origins',
43 function ( $origins ) {
44 $origins[] = HOSTINGER_REACH_REST_URI;
45
46 return $origins;
47 }
48 );
49
50 add_filter(
51 'rest_exposed_cors_headers',
52 function ( array $exposed_headers ): array {
53 $exposed_headers[] = 'X-WP-Total';
54 $exposed_headers[] = 'X-WP-TotalPages';
55 $exposed_headers[] = 'Link';
56
57 return array_values( array_unique( $exposed_headers ) );
58 }
59 );
60
61 add_filter(
62 'rest_allowed_cors_headers',
63 function ( array $allowed_headers, WP_REST_Request $request ): array {
64 $allowed_headers[] = 'Authorization';
65 $allowed_headers[] = 'Content-Type';
66 $allowed_headers[] = 'X-WP-Nonce';
67
68 return array_values( array_unique( $allowed_headers ) );
69 },
70 10,
71 2
72 );
73
74 /**
75 * Submits a contact to Reach
76 *
77 * @param array $data The data to be sent
78 * email: string - - Contact email
79 * name: string (optional) - Contact name
80 * surname: string (optional) - Contact surname
81 * group: string (optional) - The group to which the contact belongs WordPress by default
82 * metadata: array (optional) - Additional metadata to be sent with the contact
83 * - plugin: string - The name of the plugin sending the contact
84 *
85 * example of usage:
86 *
87 * do_action( 'hostinger_reach_submit', array(
88 * 'email' => 'john.doe@example.com',
89 * 'name' => 'John',
90 * 'surname' => 'Doe',
91 * 'group' => 'your plugin name',
92 * 'metadata' => array(
93 * 'plugin' => 'your plugin name',
94 * )
95 * ))
96 *
97 * @fires hostinger_reach_contact_submitted when the contact is submitted successfully
98 * @fires hostinger_reach_contact_failed when the contact submission fails
99 * @since 1.1.0
100 *
101 */
102 if ( ! has_action( 'hostinger_reach_submit' ) ) {
103 add_action( 'hostinger_reach_submit', array( $this, 'post_contact' ) );
104 }
105 }
106
107 public function get_default_headers(): array {
108 return array(
109 'Authorization' => 'Bearer ' . $this->api_key_manager->get_token(),
110 );
111 }
112
113 public function is_connected(): bool {
114 return ! empty( $this->api_key_manager->get_token() );
115 }
116
117 public function get_reach_domain(): string {
118 return $this->reach_domain;
119 }
120
121 public function get_resource_id(): string {
122 if ( ! $this->is_connected() ) {
123 return ResourceIdManager::NON_EXISTENT_RESOURCE_ID;
124 }
125
126 $resource_id = $this->resource_id_manager->get_resource_id();
127
128 if ( ! empty( $resource_id ) ) {
129 return $resource_id;
130 }
131
132 return $this->generate_resource_id();
133 }
134
135 public function generate_resource_id(): string {
136 $overview_data = $this->get_overview_handler()->get_data();
137 if ( isset( $overview_data['data']['resourceId'] ) ) {
138 $this->resource_id_manager->store_resource_id( $overview_data['data']['resourceId'] );
139 } else {
140 $this->resource_id_manager->store_resource_id( ResourceIdManager::NON_EXISTENT_RESOURCE_ID );
141 }
142
143 return $this->resource_id_manager->get_resource_id();
144 }
145
146 public function post_contact_handler( WP_REST_Request $request ): WP_REST_Response {
147 if ( ! $this->is_authorized( $request ) ) {
148 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
149 }
150
151 $email = $request->get_param( 'email' );
152 $name = $request->get_param( 'name' );
153 $surname = $request->get_param( 'surname' );
154 $form_id = $request->get_param( 'id' );
155 $metadata = $request->get_param( 'metadata' );
156 $tags = $request->get_param( 'tags' );
157 $group = apply_filters( 'hostinger_reach_get_group', $request->get_param( 'group' ), $form_id );
158
159 return $this->post_contact(
160 array(
161 'form_id' => $form_id,
162 'group' => $group,
163 'tags' => $tags,
164 'email' => $email,
165 'name' => $name,
166 'surname' => $surname,
167 'metadata' => $metadata,
168 )
169 );
170 }
171
172 public function is_authorized( WP_REST_Request $request ): bool {
173 $nonce = $request->get_header( 'X-WP-Nonce' );
174
175 return wp_verify_nonce( $nonce, 'wp_rest' ) && $this->get_connection_status_handler();
176 }
177
178 public function post_generate_auth_url(): WP_REST_Response {
179 $this->api_key_manager->generate_csrf();
180
181 $query_params = array(
182 'fromPlugin' => true,
183 'type' => 'wordpress',
184 'userType' => $this->get_functions()->is_hostinger_user() ? 'internal' : 'external',
185 'token' => urlencode( $this->api_key_manager->get_csrf() ),
186 'domain' => $this->get_functions()->get_host_info(),
187 );
188
189 $reach_url = add_query_arg( $query_params, $this->reach_domain . 'settings/connect-site' );
190 $auth_url = add_query_arg(
191 array(
192 'redirectUrl' => urlencode( $reach_url ),
193 ),
194 $this->hostinger_auth_url
195 );
196
197 return new WP_REST_Response(
198 array(
199 'auth_url' => $auth_url,
200 'success' => true,
201 ),
202 200
203 );
204 }
205
206 public function get_connection_status_handler(): bool {
207 if ( ! $this->is_connected() ) {
208 return false;
209 }
210
211 $status = $this->get( 'connection/status' );
212 if ( is_wp_error( $status ) || wp_remote_retrieve_response_code( $status ) >= 400 ) {
213 return false;
214 }
215
216 return true;
217 }
218
219 public function post_token_handler( WP_REST_Request $request ): WP_REST_Response {
220 $csrf_field = $request->get_param( 'csrf_field' );
221 $token = $request->get_param( 'token' );
222 if ( ! $this->api_key_manager->validate_csrf( $csrf_field ) ) {
223 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
224 }
225
226 $this->api_key_manager->store_token( $token );
227 $this->api_key_manager->clear_csrf();
228
229 if ( ! $this->get_connection_status_handler() ) {
230 $this->api_key_manager->clear_token();
231
232 return new WP_REST_Response( array( 'success' => false ) );
233 }
234
235 $this->set_connection_success_transient();
236
237 return new WP_REST_Response( array( 'success' => true ) );
238 }
239
240 public function post_connect_handler(): WP_REST_Response {
241 if ( ! $this->get_connection_status_handler() ) {
242 return new WP_REST_Response( array( 'success' => false ), 400 );
243 }
244
245 $domain = apply_filters( 'hostinger_reach_domain', parse_url( get_option( 'siteurl' ), PHP_URL_HOST ) );
246
247 $this->delete(
248 'websites/connect',
249 array(
250 'domain' => $domain,
251 )
252 );
253
254 $response = $this->post(
255 'websites/connect',
256 array(
257 'domain' => $domain,
258 'type' => 'wordpress',
259 )
260 );
261
262 if ( is_wp_error( $response ) ) {
263 $this->api_key_manager->clear_token();
264
265 return $this->handle_wp_error( $response );
266 }
267
268 if ( ! isset( $response['response']['code'] ) || $response['response']['code'] >= 300 ) {
269 $this->api_key_manager->clear_token();
270
271 return new WP_REST_Response(
272 array(
273 'success' => false,
274 'data' => $response['response']['message'] ?? __( 'Error connecting your site', 'hostinger-reach' ),
275 ),
276 $response['response']['code'] ?? 400,
277 );
278 }
279
280 $this->set_connection_success_transient();
281
282 return new WP_REST_Response( array( 'success' => true ) );
283 }
284
285 public function get_connection_success_handler(): WP_REST_Response {
286 return new WP_REST_Response( array( 'success' => (bool) get_transient( self::CONNECTION_SUCCESS_TRANSIENT ) ) );
287 }
288
289 public function delete_connection_success_handler(): WP_REST_Response {
290 delete_transient( self::CONNECTION_SUCCESS_TRANSIENT );
291
292 return new WP_REST_Response( array( 'success' => true ) );
293 }
294
295 public function get_overview_handler(): WP_REST_Response {
296 if ( ! $this->get_connection_status_handler() ) {
297 $this->api_key_manager->clear_token();
298 $this->resource_id_manager->clear_resource_id();
299
300 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
301 }
302
303 $response = $this->get( 'overview' );
304
305 if ( is_wp_error( $response ) ) {
306 return $this->handle_wp_error( $response );
307 }
308
309 return $this->handle_response( $response );
310 }
311
312 public function post_contact( array $data ): WP_REST_Response {
313 if ( ! $this->get_connection_status_handler() ) {
314 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
315 }
316
317 $contact = $this->parse_contact( $data );
318 $group_name = $data['group'] ? $data['group'] : HOSTINGER_REACH_DEFAULT_CONTACT_LIST;
319 $tag_ids = $this->get_tag_ids( $data['tags'] ?? '', $group_name );
320
321 if ( empty( $tag_ids ) ) {
322 $group_tag = $this->create_tag_from_group( $group_name );
323 if ( ! empty( $group_tag ) ) {
324 $tag_ids[] = $group_tag;
325 }
326 }
327
328 $args = array(
329 'tagUuids' => $tag_ids,
330 'groupName' => $group_name,
331 'contacts' => array( $contact ),
332 );
333
334 $response = $this->post(
335 'contacts',
336 $args
337 );
338
339 if ( is_wp_error( $response ) ) {
340 do_action( 'hostinger_reach_contact_failed', $data );
341
342 return $this->handle_wp_error( $response );
343 }
344
345 do_action( 'hostinger_reach_contact_submitted', $data );
346
347 return $this->handle_response( $response );
348 }
349
350 public function post_import_contacts( array $contacts_data ): WP_REST_Response {
351 if ( ! $this->get_connection_status_handler() ) {
352 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
353 }
354
355 $group = $contacts_data[0]['metadata']['group'] ?? HOSTINGER_REACH_DEFAULT_CONTACT_LIST;
356
357 $contacts = array();
358 foreach ( $contacts_data as $contact_data ) {
359 $contacts[] = $this->parse_contact( $contact_data );
360 }
361
362 $args = array(
363 'groupName' => $group,
364 'contacts' => $contacts,
365 );
366
367 $response = $this->post(
368 'contacts',
369 $args
370 );
371
372 if ( is_wp_error( $response ) ) {
373 do_action( 'hostinger_reach_imports_contact_failed' );
374
375 return $this->handle_wp_error( $response );
376 }
377
378 do_action( 'hostinger_reach_contacts_imported', count( $contacts ), $group );
379
380 return $this->handle_response( $response );
381 }
382
383 public function post_webhook_event( array $webhook_payload ): WP_REST_Response {
384 if ( ! $this->get_connection_status_handler() ) {
385 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
386 }
387
388 if ( ! isset( $webhook_payload['name'] ) ) {
389 return $this->handle_wp_error( new WP_Error( 'Bad request', 'Missing parameter [name] in the WebHook data' ) );
390 }
391
392 if ( ! isset( $webhook_payload['contact']['email'] ) ) {
393 return $this->handle_wp_error( new WP_Error( 'Bad request', 'Missing parameter [contact.email] in the WebHook data' ) );
394 }
395
396 $webhook_payload['timestamp'] = gmdate( 'Y-m-d\TH:i:s\Z' );
397
398 $response = $this->post(
399 'webhooks',
400 $webhook_payload
401 );
402
403 if ( is_wp_error( $response ) ) {
404 return $this->handle_wp_error( $response );
405 }
406
407 return $this->handle_response( $response );
408 }
409
410 public function get_tags_handler(): WP_REST_Response {
411 if ( ! $this->get_connection_status_handler() ) {
412
413 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
414 }
415
416 $response = $this->get( 'tags' );
417
418 if ( is_wp_error( $response ) ) {
419 return $this->handle_wp_error( $response );
420 }
421
422 return $this->handle_response( $response );
423 }
424
425 public function post_tags_handler( WP_REST_Request $request ): WP_REST_Response {
426 $nonce = $request->get_header( 'X-WP-Nonce' );
427 if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) || ! $this->get_connection_status_handler() ) {
428 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
429 }
430
431 $names = $request->get_param( 'names' );
432 if ( empty( $names ) ) {
433 return $this->handle_wp_error( new WP_Error( 'names_empty', 'Names parameter cannot be empty.' ) );
434 }
435
436 $response = $this->post(
437 'tags',
438 array(
439 'names' => $names,
440 )
441 );
442
443 if ( is_wp_error( $response ) ) {
444 return $this->handle_wp_error( $response );
445 }
446
447 return $this->handle_response( $response );
448 }
449
450 private function parse_contact( array $data ): array {
451 $contact = array(
452 'email' => sanitize_email( $data['email'] ),
453 );
454
455 if ( ! empty( $data['name'] ) ) {
456 $contact['name'] = $this->ensure_utf8( (string) $data['name'] );
457 }
458
459 if ( ! empty( $data['surname'] ) ) {
460 $contact['surname'] = $this->ensure_utf8( (string) $data['surname'] );
461 }
462
463 $metadata = $data['metadata'] ?? array();
464 if ( ! is_array( $metadata ) ) {
465 $metadata = array();
466 }
467
468 // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Internal metadata key.
469 $metadata['platform'] = 'wordpress';
470
471 if ( ! isset( $metadata['plugin'] ) ) {
472 $metadata['plugin'] = ReachFormIntegration::INTEGRATION_NAME;
473 }
474
475 $contact['metadata'] = $metadata;
476
477 return $contact;
478 }
479
480 public function get_forms_handler(): WP_REST_Response {
481 if ( ! $this->get_connection_status_handler() ) {
482 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
483 }
484
485 $response = $this->get( 'forms' );
486
487 if ( is_wp_error( $response ) ) {
488 return $this->handle_wp_error( $response );
489 }
490
491 return $this->handle_response( $response );
492 }
493
494 public function get_form_preview_handler( WP_REST_Request $request ): WP_REST_Response {
495 if ( ! $this->get_connection_status_handler() ) {
496
497 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
498 }
499 $form_id = $request->get_param( 'id' );
500 $response = $this->get( 'forms/' . $form_id . '/preview' );
501
502 if ( is_wp_error( $response ) ) {
503 return $this->handle_wp_error( $response );
504 }
505
506 if ( empty( wp_remote_retrieve_body( $response ) ) ) {
507 return new WP_REST_Response( array( 'error' => 'preview_not_ready' ), 404 );
508 }
509
510 return $this->handle_image_response( $response );
511 }
512
513 private function ensure_utf8( string $value ): string {
514 if ( $value === '' || ! function_exists( 'mb_check_encoding' ) || mb_check_encoding( $value, 'UTF-8' ) ) {
515 return $value;
516 }
517
518 $detected = mb_detect_encoding( $value, array( 'UTF-8', 'Windows-1252', 'ISO-8859-1' ), true );
519 $converted = mb_convert_encoding( $value, 'UTF-8', $detected !== false ? $detected : 'ISO-8859-1' );
520
521 return is_string( $converted ) ? $converted : $value;
522 }
523
524 private function set_connection_success_transient(): void {
525 set_transient( self::CONNECTION_SUCCESS_TRANSIENT, true, self::CONNECTION_SUCCESS_TRANSIENT_EXPIRATION );
526 }
527
528 private function set_api_base_name(): void {
529 if ( $this->get_functions()->is_staging() ) {
530 $this->hostinger_auth_url = 'https://auth.hostinger.dev/login';
531 $this->reach_domain = 'https://reach.hostinger.dev/';
532 } else {
533 $this->hostinger_auth_url = 'https://auth.hostinger.com/login';
534 $this->reach_domain = 'https://reach.hostinger.com/';
535 }
536
537 $this->api_base_name = $this->reach_domain . 'api/public/v1/';
538 }
539
540 private function get_tag_ids( string $tag_names, string $group_name ): array {
541 if ( empty( $tag_names ) && empty( $group_name ) ) {
542 return array();
543 }
544
545 $tag_names = explode( ',', $tag_names );
546 $tag_ids = array();
547 $handler = $this->get_tags_handler();
548 $tags_data = $handler->get_data();
549 $tags = $tags_data['data'] ?? array();
550
551 foreach ( $tags as $tag ) {
552 if ( in_array( $tag['value'], $tag_names, true ) || $tag['value'] === $group_name ) {
553 $tag_ids[] = $tag['uuid'];
554 }
555 }
556
557 return $tag_ids;
558 }
559
560 private function create_tag_from_group( string $group ): string {
561 if ( empty( $group ) ) {
562 return '';
563 }
564
565 $response = $this->post(
566 'tags',
567 array(
568 'names' => array( $group ),
569 )
570 );
571
572 if ( is_wp_error( $response ) ) {
573 return '';
574 }
575
576 $body = wp_remote_retrieve_body( $response );
577 $body = json_decode( $body, true );
578
579 return $body['data'][0]['uuid'] ?? '';
580 }
581 }
582