PluginProbe ʕ •ᴥ•ʔ
Hostinger Reach – AI-Powered Email Marketing for WordPress / 1.5.6
Hostinger Reach – AI-Powered Email Marketing for WordPress v1.5.6
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 7 months ago FormsApiHandler.php 6 months ago HostingApiHandler.php 3 months ago IntegrationsApiHandler.php 3 months ago ReachApiHandler.php 2 months ago
ReachApiHandler.php
488 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 return new WP_REST_Response( array( 'success' => false ) );
225 }
226
227 return new WP_REST_Response( array( 'success' => true ) );
228 }
229
230 public function get_overview_handler(): WP_REST_Response {
231 if ( ! $this->get_connection_status_handler() ) {
232 $this->api_key_manager->clear_token();
233 $this->resource_id_manager->clear_resource_id();
234
235 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
236 }
237
238 $response = $this->get( 'overview' );
239
240 if ( is_wp_error( $response ) ) {
241 return $this->handle_wp_error( $response );
242 }
243
244 return $this->handle_response( $response );
245 }
246
247 public function post_contact( array $data ): WP_REST_Response {
248 if ( ! $this->get_connection_status_handler() ) {
249 $this->api_key_manager->clear_token();
250
251 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
252 }
253
254 $contact = $this->parse_contact( $data );
255 $group_name = $data['group'] ? $data['group'] : HOSTINGER_REACH_DEFAULT_CONTACT_LIST;
256 $tag_ids = $this->get_tag_ids( $data['tags'] ?? '', $group_name );
257
258 if ( empty( $tag_ids ) ) {
259 $group_tag = $this->create_tag_from_group( $group_name );
260 if ( ! empty( $group_tag ) ) {
261 $tag_ids[] = $group_tag;
262 }
263 }
264
265 $args = array(
266 'tagUuids' => $tag_ids,
267 'groupName' => $group_name,
268 'contacts' => array( $contact ),
269 );
270
271 $response = $this->post(
272 'contacts',
273 $args
274 );
275
276 if ( is_wp_error( $response ) ) {
277 do_action( 'hostinger_reach_contact_failed', $data );
278
279 return $this->handle_wp_error( $response );
280 }
281
282 do_action( 'hostinger_reach_contact_submitted', $data );
283
284 return $this->handle_response( $response );
285 }
286
287 public function post_import_contacts( array $contacts_data ): WP_REST_Response {
288 if ( ! $this->get_connection_status_handler() ) {
289 $this->api_key_manager->clear_token();
290
291 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
292 }
293
294 $group = $contacts_data[0]['metadata']['group'] ?? HOSTINGER_REACH_DEFAULT_CONTACT_LIST;
295
296 $contacts = array();
297 foreach ( $contacts_data as $contact_data ) {
298 $contacts[] = $this->parse_contact( $contact_data );
299 }
300
301 $args = array(
302 'groupName' => $group,
303 'contacts' => $contacts,
304 );
305
306 $response = $this->post(
307 'contacts',
308 $args
309 );
310
311 if ( is_wp_error( $response ) ) {
312 do_action( 'hostinger_reach_imports_contact_failed' );
313
314 return $this->handle_wp_error( $response );
315 }
316
317 do_action( 'hostinger_reach_contacts_imported', count( $contacts ), $group );
318
319 return $this->handle_response( $response );
320 }
321
322 public function post_webhook_event( array $webhook_payload ): WP_REST_Response {
323 if ( ! $this->get_connection_status_handler() ) {
324 $this->api_key_manager->clear_token();
325
326 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action' ) );
327 }
328
329 if ( ! isset( $webhook_payload['name'] ) ) {
330 return $this->handle_wp_error( new WP_Error( 'Bad request', 'Missing parameter [name] in the WebHook data' ) );
331 }
332
333 if ( ! isset( $webhook_payload['contact']['email'] ) ) {
334 return $this->handle_wp_error( new WP_Error( 'Bad request', 'Missing parameter [contact.email] in the WebHook data' ) );
335 }
336
337 $webhook_payload['timestamp'] = gmdate( 'Y-m-d\TH:i:s\Z' );
338
339 $response = $this->post(
340 'webhooks',
341 $webhook_payload
342 );
343
344 if ( is_wp_error( $response ) ) {
345 return $this->handle_wp_error( $response );
346 }
347
348 return $this->handle_response( $response );
349 }
350
351 public function get_tags_handler(): WP_REST_Response {
352 if ( ! $this->get_connection_status_handler() ) {
353 $this->api_key_manager->clear_token();
354
355 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
356 }
357
358 $response = $this->get( 'tags' );
359
360 if ( is_wp_error( $response ) ) {
361 return $this->handle_wp_error( $response );
362 }
363
364 return $this->handle_response( $response );
365 }
366
367 public function post_tags_handler( WP_REST_Request $request ): WP_REST_Response {
368 $nonce = $request->get_header( 'X-WP-Nonce' );
369 if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) || ! $this->get_connection_status_handler() ) {
370 $this->api_key_manager->clear_token();
371
372 return $this->handle_wp_error( new WP_Error( $this->get_not_connected_error_message(), 'You cannot perform this action', array( 'status' => 403 ) ) );
373 }
374
375 $names = $request->get_param( 'names' );
376 if ( empty( $names ) ) {
377 return $this->handle_wp_error( new WP_Error( 'names_empty', 'Names parameter cannot be empty.' ) );
378 }
379
380 $response = $this->post(
381 'tags',
382 array(
383 'names' => $names,
384 )
385 );
386
387 if ( is_wp_error( $response ) ) {
388 return $this->handle_wp_error( $response );
389 }
390
391 return $this->handle_response( $response );
392 }
393
394 private function parse_contact( array $data ): array {
395 $contact = array(
396 'email' => sanitize_email( $data['email'] ),
397 );
398
399 if ( ! empty( $data['name'] ) ) {
400 $contact['name'] = $this->ensure_utf8( (string) $data['name'] );
401 }
402
403 if ( ! empty( $data['surname'] ) ) {
404 $contact['surname'] = $this->ensure_utf8( (string) $data['surname'] );
405 }
406
407 $metadata = $data['metadata'] ?? array();
408 if ( ! is_array( $metadata ) ) {
409 $metadata = array();
410 }
411
412 // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Internal metadata key.
413 $metadata['platform'] = 'wordpress';
414
415 if ( ! isset( $metadata['plugin'] ) ) {
416 $metadata['plugin'] = ReachFormIntegration::INTEGRATION_NAME;
417 }
418
419 $contact['metadata'] = $metadata;
420
421 return $contact;
422 }
423
424 private function ensure_utf8( string $value ): string {
425 if ( $value === '' || ! function_exists( 'mb_check_encoding' ) || mb_check_encoding( $value, 'UTF-8' ) ) {
426 return $value;
427 }
428
429 $detected = mb_detect_encoding( $value, array( 'UTF-8', 'Windows-1252', 'ISO-8859-1' ), true );
430 $converted = mb_convert_encoding( $value, 'UTF-8', $detected !== false ? $detected : 'ISO-8859-1' );
431
432 return is_string( $converted ) ? $converted : $value;
433 }
434
435 private function set_api_base_name(): void {
436 if ( $this->get_functions()->is_staging() ) {
437 $this->hostinger_auth_url = 'https://auth.hostinger.dev/login';
438 $this->reach_domain = 'https://reach.hostinger.dev/';
439 } else {
440 $this->hostinger_auth_url = 'https://auth.hostinger.com/login';
441 $this->reach_domain = 'https://reach.hostinger.com/';
442 }
443
444 $this->api_base_name = $this->reach_domain . 'api/public/v1/';
445 }
446
447 private function get_tag_ids( string $tag_names, string $group_name ): array {
448 if ( empty( $tag_names ) && empty( $group_name ) ) {
449 return array();
450 }
451
452 $tag_names = explode( ',', $tag_names );
453 $tag_ids = array();
454 $handler = $this->get_tags_handler();
455 $tags_data = $handler->get_data();
456 $tags = $tags_data['data'] ?? array();
457
458 foreach ( $tags as $tag ) {
459 if ( in_array( $tag['value'], $tag_names, true ) || $tag['value'] === $group_name ) {
460 $tag_ids[] = $tag['uuid'];
461 }
462 }
463
464 return $tag_ids;
465 }
466
467 private function create_tag_from_group( string $group ): string {
468 if ( empty( $group ) ) {
469 return '';
470 }
471
472 $response = $this->post(
473 'tags',
474 array(
475 'names' => array( $group ),
476 )
477 );
478
479 if ( is_wp_error( $response ) ) {
480 return '';
481 }
482
483 $body = wp_remote_retrieve_body( $response );
484 $body = json_decode( $body, true );
485 return $body['data'][0]['uuid'] ?? '';
486 }
487 }
488