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