PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 1.9.7.2
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v1.9.7.2
3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 2.3.2 All 195 releases
convertkit / includes / class-convertkit-api.php

class-convertkit-api.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 1.9.7.2, at includes/class-convertkit-api.php

1,342 lines 35.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ConvertKit API class.
4 *
5 * @package ConvertKit
6 * @author ConvertKit
7 */
8
9 /**
10 * ConvertKit API class
11 *
12 * @package ConvertKit
13 * @author ConvertKit
14 */
15 class ConvertKit_API {
16
17 /**
18 * ConvertKit API Key
19 *
20 * @var mixed bool | string
21 */
22 protected $api_key = false;
23
24 /**
25 * ConvertKit API Secret
26 *
27 * @var mixed bool | string
28 */
29 protected $api_secret = false;
30
31 /**
32 * Save debug data to log
33 *
34 * @var bool
35 */
36 protected $debug = false;
37
38 /**
39 * Version of ConvertKit API
40 *
41 * @var string
42 */
43 protected $api_version = 'v3';
44
45 /**
46 * ConvertKit API URL
47 *
48 * @var string
49 */
50 protected $api_url_base = 'https://api.convertkit.com/';
51
52 /**
53 * Holds the log class for writing to the log file
54 *
55 * @var ConvertKit_Log
56 */
57 private $log;
58
59 /**
60 * Sets up the API with the required credentials.
61 *
62 * @since 1.9.6
63 *
64 * @param mixed $api_key ConvertKit API Key.
65 * @param mixed $api_secret ConvertKit API Secret.
66 * @param bool $debug Save data to log.
67 */
68 public function __construct( $api_key = false, $api_secret = false, $debug = false ) {
69
70 // Set API credentials and debugging.
71 $this->api_key = $api_key;
72 $this->api_secret = $api_secret;
73 $this->debug = $debug;
74 $this->log = new ConvertKit_Log();
75
76 }
77
78 /**
79 * Gets account information from the API.
80 *
81 * @since 1.9.6
82 *
83 * @return WP_Error|array
84 */
85 public function account() {
86
87 $this->log( 'API: account()' );
88
89 return $this->get(
90 'account',
91 array(
92 'api_secret' => $this->api_secret,
93 )
94 );
95
96 }
97
98 /**
99 * Gets all subscription forms from the API.
100 *
101 * @since 1.9.6
102 *
103 * @return WP_Error|array
104 */
105 public function get_subscription_forms() {
106
107 $this->log( 'API: get_subscription_forms()' );
108
109 // Send request.
110 return $this->get(
111 'subscription_forms',
112 array(
113 'api_key' => $this->api_key,
114 )
115 );
116
117 }
118
119 /**
120 * Gets all forms from the API.
121 *
122 * @since 1.9.6
123 *
124 * @return WP_Error|array
125 */
126 public function get_forms() {
127
128 $this->log( 'API: get_forms()' );
129
130 // Get all forms and landing pages from the API.
131 $forms = $this->get_forms_landing_pages();
132
133 // If an error occured, log and return it now.
134 if ( is_wp_error( $forms ) ) {
135 $this->log( 'API: get_forms(): Error: ' . $forms->get_error_message() );
136 return $forms;
137 }
138
139 return $forms['forms'];
140
141 }
142
143 /**
144 * Subscribes an email address to a form.
145 *
146 * @since 1.9.6
147 *
148 * @param int $form_id Form ID.
149 * @param string $email Email Address.
150 * @param string $first_name First Name.
151 * @param mixed $fields Custom Fields (false|array).
152 * @return WP_Error|array
153 */
154 public function form_subscribe( $form_id, $email, $first_name = '', $fields = false ) {
155
156 // Backward compat. if $email is an array comprising of email and name keys.
157 if ( is_array( $email ) ) { // @phpstan-ignore-line.
158 _deprecated_function( __FUNCTION__, '1.9.6', 'form_subscribe( $form_id, $email, $first_name )' );
159 $first_name = $email['name'];
160 $email = $email['email'];
161 }
162
163 $this->log( 'API: form_subscribe(): [ form_id: ' . $form_id . ', email: ' . $email . ', first_name: ' . $first_name . ' ]' );
164
165 // Sanitize some parameters.
166 $form_id = absint( $form_id );
167 $email = trim( $email );
168 $first_name = trim( $first_name );
169
170 // Return error if no Form ID or email address is specified.
171 if ( empty( $form_id ) ) {
172 return new WP_Error( 'convertkit_api_error', __( 'form_subscribe(): the form_id parameter is empty.', 'convertkit' ) );
173 }
174 if ( empty( $email ) ) {
175 return new WP_Error( 'convertkit_api_error', __( 'form_subscribe(): the email parameter is empty.', 'convertkit' ) );
176 }
177
178 // Build request parameters.
179 $params = array(
180 'api_key' => $this->api_key,
181 'email' => $email,
182 'first_name' => $first_name,
183 );
184 if ( $fields ) {
185 $params['fields'] = $fields;
186 }
187
188 // Send request.
189 $response = $this->post( 'forms/' . $form_id . '/subscribe', $params );
190
191 // If an error occured, log and return it now.
192 if ( is_wp_error( $response ) ) {
193 $this->log( 'API: form_subscribe(): Error: ' . $response->get_error_message() );
194 return $response;
195 }
196
197 /**
198 * Runs actions immediately after the email address was successfully subscribed to the form.
199 *
200 * @since 1.9.6
201 *
202 * @param array $response API Response
203 * @param int $form_id Form ID
204 * @param string $email Email Address
205 * @param string $first_name First Name
206 * @param mixed $fields Custom Fields (false|array)
207 */
208 do_action( 'convertkit_api_form_subscribe_success', $response, $form_id, $email, $first_name, $fields );
209
210 return $response;
211
212 }
213
214 /**
215 * Gets all landing pages from the API.
216 *
217 * @since 1.9.6
218 *
219 * @return WP_Error|array
220 */
221 public function get_landing_pages() {
222
223 $this->log( 'API: get_landing_pages()' );
224
225 // Get all forms and landing pages from the API.
226 $forms = $this->get_forms_landing_pages();
227
228 // If an error occured, log and return it now.
229 if ( is_wp_error( $forms ) ) {
230 $this->log( 'API: get_landing_pages(): Error: ' . $forms->get_error_message() );
231 return $forms;
232 }
233
234 return $forms['landing_pages'];
235
236 }
237
238 /**
239 * Fetches all sequences from the API.
240 *
241 * @since 1.9.6
242 *
243 * @return WP_Error|array
244 */
245 public function get_sequences() {
246
247 $this->log( 'API: get_sequences()' );
248
249 $sequences = array();
250
251 // Send request.
252 $response = $this->get(
253 'sequences',
254 array(
255 'api_key' => $this->api_key,
256 )
257 );
258
259 // If an error occured, log and return it now.
260 if ( is_wp_error( $response ) ) {
261 $this->log( 'API: get_sequences(): Error: ' . $response->get_error_message() );
262 return $response;
263 }
264
265 // If no sequences exist, return WP_Error.
266 if ( ! isset( $response['courses'] ) ) {
267 $this->log( 'API: get_sequences(): Error: No sequences exist in ConvertKit.' );
268 return new WP_Error( 'convertkit_api_error', __( 'No sequences exist in ConvertKit. Visit your ConvertKit account and create your first sequence.', 'convertkit' ) );
269 }
270 if ( ! count( $response['courses'] ) ) {
271 $this->log( 'API: get_sequences(): Error: No sequences exist in ConvertKit.' );
272 return new WP_Error( 'convertkit_api_error', __( 'No sequences exist in ConvertKit. Visit your ConvertKit account and create your first sequence.', 'convertkit' ) );
273 }
274
275 foreach ( $response['courses'] as $sequence ) {
276 $sequences[] = $sequence;
277 }
278
279 return $sequences;
280
281 }
282
283 /**
284 * Subscribes an email address to a sequence.
285 *
286 * @since 1.9.6
287 *
288 * @param string $sequence_id Sequence ID.
289 * @param string $email Email Address.
290 * @param string $first_name First Name.
291 * @param mixed $fields Custom Fields (false|array).
292 * @return WP_Error|array
293 */
294 public function sequence_subscribe( $sequence_id, $email, $first_name = '', $fields = false ) {
295
296 $this->log( 'API: sequence_subscribe(): [ sequence_id: ' . $sequence_id . ', email: ' . $email . ']' );
297
298 // Sanitize some parameters.
299 $sequence_id = trim( $sequence_id );
300 $email = trim( $email );
301 $first_name = trim( $first_name );
302
303 // Return error if no Sequence ID or email address is specified.
304 if ( empty( $sequence_id ) ) {
305 return new WP_Error( 'convertkit_api_error', __( 'sequence_subscribe(): the sequence_id parameter is empty.', 'convertkit' ) );
306 }
307 if ( empty( $email ) ) {
308 return new WP_Error( 'convertkit_api_error', __( 'sequence_subscribe(): the email parameter is empty.', 'convertkit' ) );
309 }
310
311 // Build request parameters.
312 $params = array(
313 'api_key' => $this->api_key,
314 'email' => $email,
315 'first_name' => $first_name,
316 );
317 if ( $fields ) {
318 $params['fields'] = $fields;
319 }
320
321 // Send request.
322 $response = $this->post( 'sequences/' . $sequence_id . '/subscribe', $params );
323
324 // If an error occured, log and return it now.
325 if ( is_wp_error( $response ) ) {
326 $this->log( 'API: sequence_subscribe(): Error: ' . $response->get_error_message() );
327 return $response;
328 }
329
330 /**
331 * Runs actions immediately after the email address was successfully subscribed to the sequence.
332 *
333 * @since 1.9.6
334 *
335 * @param array $response API Response
336 * @param string $sequence_id Sequence ID
337 * @param string $email Email Address
338 * @param mixed $fields Custom Fields (false|array)
339 */
340 do_action( 'convertkit_api_sequence_subscribe_success', $response, $sequence_id, $email, $fields );
341
342 return $response;
343
344 }
345
346 /**
347 * Fetches all tags from the API.
348 *
349 * @since 1.9.6
350 *
351 * @return WP_Error|array
352 */
353 public function get_tags() {
354
355 $this->log( 'API: get_tags()' );
356
357 $tags = array();
358
359 // Send request.
360 $response = $this->get(
361 'tags',
362 array(
363 'api_key' => $this->api_key,
364 )
365 );
366
367 // If an error occured, log and return it now.
368 if ( is_wp_error( $response ) ) {
369 $this->log( 'API: get_tags(): Error: ' . $response->get_error_message() );
370 return $response;
371 }
372
373 // If no tags exist, return WP_Error.
374 if ( ! isset( $response['tags'] ) ) {
375 $this->log( 'API: get_tags(): Error: No tags exist in ConvertKit.' );
376 return new WP_Error( 'convertkit_api_error', __( 'No tags exist in ConvertKit. Visit your ConvertKit account and create your first tag.', 'convertkit' ) );
377 }
378 if ( ! count( $response['tags'] ) ) {
379 $this->log( 'API: get_tags(): Error: No tags exist in ConvertKit.' );
380 return new WP_Error( 'convertkit_api_error', __( 'No tags exist in ConvertKit. Visit your ConvertKit account and create your first tag.', 'convertkit' ) );
381 }
382
383 foreach ( $response['tags'] as $tag ) {
384 $tags[] = $tag;
385 }
386
387 return $tags;
388
389 }
390
391 /**
392 * Subscribes an email address to a tag.
393 *
394 * @since 1.9.6
395 *
396 * @param int $tag_id Tag ID.
397 * @param string $email Email Address.
398 * @param string $first_name First Name.
399 * @param mixed $fields Custom Fields (false|array).
400 * @return WP_Error|array
401 */
402 public function tag_subscribe( $tag_id, $email, $first_name = '', $fields = false ) {
403
404 $this->log( 'API: tag_subscribe(): [ tag_id: ' . $tag_id . ', email: ' . $email . ']' );
405
406 // Sanitize some parameters.
407 $tag_id = absint( $tag_id );
408 $email = trim( $email );
409 $first_name = trim( $first_name );
410
411 // Return error if no Tag ID or email address is specified.
412 if ( empty( $tag_id ) ) {
413 return new WP_Error( 'convertkit_api_error', __( 'tag_subscribe(): the tag_id parameter is empty.', 'convertkit' ) );
414 }
415 if ( empty( $email ) ) {
416 return new WP_Error( 'convertkit_api_error', __( 'tag_subscribe(): the email parameter is empty.', 'convertkit' ) );
417 }
418
419 // Build request parameters.
420 $params = array(
421 'api_key' => $this->api_key,
422 'email' => $email,
423 'first_name' => $first_name,
424 );
425 if ( $fields ) {
426 $params['fields'] = $fields;
427 }
428
429 // Send request.
430 $response = $this->post( 'tags/' . $tag_id . '/subscribe', $params );
431
432 // If an error occured, log and return it now.
433 if ( is_wp_error( $response ) ) {
434 $this->log( 'API: tag_subscribe(): Error: ' . $response->get_error_message() );
435 return $response;
436 }
437
438 /**
439 * Runs actions immediately after the email address was successfully subscribed to the tag.
440 *
441 * @since 1.9.6
442 *
443 * @param array $response API Response
444 * @param int $tag_id Tag ID
445 * @param string $email Email Address
446 * @param mixed $fields Custom Fields (false|array).
447 */
448 do_action( 'convertkit_api_tag_subscribe_success', $response, $tag_id, $email, $fields );
449
450 return $response;
451
452 }
453
454 /**
455 * Gets a subscriber by their email address.
456 *
457 * @since 1.9.6
458 *
459 * @param string $email Email Address.
460 * @return WP_Error|array
461 */
462 public function get_subscriber_by_email( $email ) {
463
464 $this->log( 'API: get_subscriber_by_email(): [ email: ' . $email . ']' );
465
466 // Sanitize some parameters.
467 $email = trim( $email );
468
469 // Return error if email address is specified.
470 if ( empty( $email ) ) {
471 return new WP_Error( 'convertkit_api_error', __( 'get_subscriber_by_email(): the email parameter is empty.', 'convertkit' ) );
472 }
473
474 // Send request.
475 $response = $this->get(
476 'subscribers',
477 array(
478 'api_secret' => $this->api_secret,
479 'email_address' => $email,
480 )
481 );
482
483 // If an error occured, log and return it now.
484 if ( is_wp_error( $response ) ) {
485 $this->log( 'API: get_subscriber_by_email(): Error: ' . $response->get_error_message() );
486 return $response;
487 }
488
489 // If no subscribers exist, return WP_Error.
490 if ( ! absint( $response['total_subscribers'] ) ) {
491 $error = new WP_Error(
492 'convertkit_api_error',
493 sprintf(
494 /* translators: Email Address */
495 __( 'No subscriber(s) exist in ConvertKit matching the email address %s.', 'convertkit' ),
496 $email
497 )
498 );
499
500 $this->log( 'API: get_subscriber_by_email(): Error: ' . $error->get_error_message() );
501
502 return $error;
503 }
504
505 return $response['subscribers'][0];
506
507 }
508
509 /**
510 * Gets a subscriber by their ConvertKit subscriber ID.
511 *
512 * @since 1.9.6
513 *
514 * @param int $subscriber_id Subscriber ID.
515 * @return WP_Error|array
516 */
517 public function get_subscriber_by_id( $subscriber_id ) {
518
519 $this->log( 'API: get_subscriber_by_id(): [ subscriber_id: ' . $subscriber_id . ']' );
520
521 // Sanitize some parameters.
522 $subscriber_id = absint( $subscriber_id );
523
524 // Return error if no Subscriber ID is specified.
525 if ( empty( $subscriber_id ) ) {
526 return new WP_Error( 'convertkit_api_error', __( 'get_subscriber_by_id(): the subscriber_id parameter is empty.', 'convertkit' ) );
527 }
528
529 // Send request.
530 $response = $this->get(
531 'subscribers/' . $subscriber_id,
532 array(
533 'api_secret' => $this->api_secret,
534 )
535 );
536
537 // If an error occured, log and return it now.
538 if ( is_wp_error( $response ) ) {
539 $this->log( 'API: get_subscriber_by_id(): Error: ' . $response->get_error_message() );
540 return $response;
541 }
542
543 // If no subscriber exists, return WP_Error.
544 if ( ! isset( $response['subscriber'] ) ) {
545 $error = new WP_Error(
546 'convertkit_api_error',
547 sprintf(
548 /* translators: Subscriber ID */
549 __( 'No subscriber exist in ConvertKit matching the subscriber ID %s.', 'convertkit' ),
550 $subscriber_id
551 )
552 );
553
554 $this->log( 'API: get_subscriber_by_id(): Error: ' . $error->get_error_message() );
555
556 return $error;
557 }
558
559 return $response['subscriber'];
560
561 }
562
563 /**
564 * Gets a list of tags for the given ConvertKit subscriber ID.
565 *
566 * @since 1.9.6
567 *
568 * @param int $subscriber_id Subscriber ID.
569 * @return WP_Error|array
570 */
571 public function get_subscriber_tags( $subscriber_id ) {
572
573 $this->log( 'API: get_subscriber_tags(): [ subscriber_id: ' . $subscriber_id . ']' );
574
575 // Sanitize some parameters.
576 $subscriber_id = absint( $subscriber_id );
577
578 // Return error if no Subscriber ID is specified.
579 if ( empty( $subscriber_id ) ) {
580 return new WP_Error( 'convertkit_api_error', __( 'get_subscriber_tags(): the subscriber_id parameter is empty.', 'convertkit' ) );
581 }
582
583 // Send request.
584 $response = $this->get(
585 'subscribers/' . $subscriber_id . '/tags',
586 array(
587 'api_key' => $this->api_key,
588 )
589 );
590
591 // If an error occured, log and return it now.
592 if ( is_wp_error( $response ) ) {
593 $this->log( 'API: get_subscriber_tags(): Error: ' . $response->get_error_message() );
594 return $response;
595 }
596
597 // If no tags exists, return WP_Error.
598 if ( ! isset( $response['tags'] ) ) {
599 $error = new WP_Error(
600 'convertkit_api_error',
601 sprintf(
602 /* translators: Subscriber ID */
603 __( 'No tags exist in ConvertKit for the subscriber ID %s.', 'convertkit' ),
604 $subscriber_id
605 )
606 );
607
608 $this->log( 'API: get_subscriber_tags(): Error: ' . $error->get_error_message() );
609
610 return $error;
611 }
612
613 return $response['tags'];
614
615 }
616
617 /**
618 * Returns the subscriber's ID by their email address.
619 *
620 * @since 1.9.6
621 *
622 * @param string $email_address Email Address.
623 * @return WP_Error|int
624 */
625 public function get_subscriber_id( $email_address ) {
626
627 // Get subscriber.
628 $subscriber = $this->get_subscriber_by_email( $email_address );
629
630 // If an error occured, log and return it now.
631 if ( is_wp_error( $subscriber ) ) {
632 return $subscriber;
633 }
634
635 // Return ID.
636 return $subscriber['id'];
637
638 }
639
640 /**
641 * Unsubscribes an email address.
642 *
643 * @since 1.9.6
644 *
645 * @param string $email Email Address.
646 * @return WP_Error|array
647 */
648 public function unsubscribe( $email ) {
649
650 $this->log( 'API: unsubscribe(): [ email: ' . $email . ']' );
651
652 // Sanitize some parameters.
653 $email = trim( $email );
654
655 // Return error if no email address is specified.
656 if ( empty( $email ) ) {
657 return new WP_Error( 'convertkit_api_error', __( 'unsubscribe(): the email parameter is empty.', 'convertkit' ) );
658 }
659
660 // Send request.
661 $response = $this->post(
662 'unsubscribe',
663 array(
664 'api_secret' => $this->api_secret,
665 'email' => $email,
666 )
667 );
668
669 // If an error occured, log and return it now.
670 if ( is_wp_error( $response ) ) {
671 $this->log( 'API: unsubscribe(): Error: ' . $response->get_error_message() );
672 return $response;
673 }
674
675 /**
676 * Runs actions immediately after the email address was successfully unsubscribed.
677 *
678 * @since 1.9.6
679 *
680 * @param array $response API Response
681 * @param string $email Email Address
682 */
683 do_action( 'convertkit_api_form_unsubscribe_success', $response, $email );
684
685 return $response;
686
687 }
688
689 /**
690 * Gets all custom fields from the API.
691 *
692 * @since 1.9.6.9
693 *
694 * @return WP_Error|array
695 */
696 public function get_custom_fields() {
697
698 $this->log( 'API: get_custom_fields()' );
699
700 $custom_fields = array();
701
702 // Send request.
703 $response = $this->get(
704 'custom_fields',
705 array(
706 'api_key' => $this->api_key,
707 )
708 );
709
710 // If an error occured, return WP_Error.
711 if ( is_wp_error( $response ) ) {
712 $this->log( 'API: get_custom_fields(): Error: ' . $response->get_error_message() );
713 return $response;
714 }
715
716 // If no custom fields exist, return WP_Error.
717 if ( ! isset( $response['custom_fields'] ) ) {
718 $this->log( 'API: get_custom_fields(): Error: No custom fields exist in ConvertKit.' );
719 return new WP_Error( 'convertkit_api_error', __( 'No custom fields exist in ConvertKit. Visit your ConvertKit account and create your first custom field.', 'convertkit' ) );
720 }
721 if ( ! count( $response['custom_fields'] ) ) {
722 $this->log( 'API: get_custom_fields(): Error: No custom fields exist in ConvertKit.' );
723 return new WP_Error( 'convertkit_api_error', __( 'No custom fields exist in ConvertKit. Visit your ConvertKit account and create your first custom field.', 'convertkit' ) );
724 }
725
726 foreach ( $response['custom_fields'] as $custom_field ) {
727 $custom_fields[] = $custom_field;
728 }
729
730 return $custom_fields;
731
732 }
733
734 /**
735 * Get HTML from ConvertKit for the given Legacy Form ID.
736 *
737 * This isn't specifically an API function, but for now it's best suited here.
738 *
739 * @param int $id Form ID.
740 * @return WP_Error|string HTML
741 */
742 public function get_form_html( $id ) {
743
744 // Define Legacy Form URL.
745 $url = add_query_arg(
746 array(
747 'k' => $this->api_key,
748 'v' => 2,
749 ),
750 'https://api.convertkit.com/forms/' . $id . '/embed'
751 );
752
753 // Get HTML.
754 $body = $this->get_html( $url );
755
756 return $body;
757
758 }
759
760 /**
761 * Get HTML from ConvertKit for the given Landing Page URL.
762 *
763 * This isn't specifically an API function, but for now it's best suited here.
764 *
765 * @param string $url URL of Landing Page.
766 * @return string HTML
767 */
768 public function get_landing_page_html( $url ) {
769
770 // Get HTML.
771 $body = $this->get_html( $url, false );
772
773 // Inject JS for subscriber forms to work.
774 $scripts = new WP_Scripts();
775 $script = "<script type='text/javascript' src='" . trailingslashit( $scripts->base_url ) . "wp-includes/js/jquery/jquery.js?ver=1.4.0'></script>"; // phpcs:ignore
776 $script .= "<script type='text/javascript' src='" . CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js?ver=' . CONVERTKIT_PLUGIN_VERSION . "'></script>"; // phpcs:ignore
777 $script .= "<script type='text/javascript'>/* <![CDATA[ */var convertkit = {\"ajaxurl\":\"" . admin_url( 'admin-ajax.php' ) . '"};/* ]]> */</script>'; // phpcs:ignore
778
779 $body = str_replace( '</head>', '</head>' . $script, $body );
780
781 return $body;
782
783 }
784
785 /**
786 * Create a Purchase.
787 *
788 * @since 1.9.6.9
789 *
790 * @param array $purchase Purchase Data.
791 * @return WP_Error|array
792 */
793 public function purchase_create( $purchase ) {
794
795 $this->log( 'API: purchase_create(): [ purchase: ' . print_r( $purchase, true ) . ']' ); // phpcs:ignore
796
797 $response = $this->post(
798 'purchases',
799 array(
800 'api_secret' => $this->api_secret,
801 'purchase' => $purchase,
802 )
803 );
804
805 if ( is_wp_error( $response ) ) {
806 $this->log( 'API: purchase_create(): Error: ' . $response->get_error_message() );
807 }
808
809 /**
810 * Runs actions immediately after the purchase data address was successfully created.
811 *
812 * @since 1.9.6.9
813 *
814 * @param array $response API Response
815 * @param array $purchase Purchase Data
816 */
817 do_action( 'convertkit_api_purchase_create_success', $response, $purchase );
818
819 return $response;
820
821 }
822
823 /**
824 * Backward compat. function for updating Forms, Landing Pages and Tags in WordPress options table.
825 *
826 * @since 1.0.0
827 *
828 * @param string $api_key API Key.
829 * @param string $api_secret API Secret.
830 */
831 public function update_resources( $api_key, $api_secret ) { // phpcs:ignore
832
833 // Warn the developer that they shouldn't use this function.
834 _deprecated_function( __FUNCTION__, '1.9.6', 'refresh() in ConvertKit_Resource_Forms, ConvertKit_Resource_Landing_Pages and ConvertKit_Resource_Tags classes.' );
835
836 // Initialize resource classes.
837 $forms = new ConvertKit_Resource_Forms();
838 $landing_pages = new ConvertKit_Resource_Landing_Pages();
839 $tags = new ConvertKit_Resource_Tags();
840
841 // Refresh resources by calling the API and storing the results.
842 $forms->refresh();
843 $landing_pages->refresh();
844 $tags->refresh();
845
846 }
847
848 /**
849 * Backward compat. function for getting a ConvertKit subscriber by their ID.
850 *
851 * @since 1.9.6
852 *
853 * @param int $id Subscriber ID.
854 * @return WP_Error|array
855 */
856 public function get_subscriber( $id ) {
857
858 // Warn the developer that they shouldn't use this function.
859 _deprecated_function( __FUNCTION__, '1.9.6', 'get_subscriber_by_id()' );
860
861 // Pass request to new function.
862 return $this->get_subscriber_by_id( $id );
863
864 }
865
866 /**
867 * Backward compat. function for subscribing a ConvertKit subscriber to the given Tag.
868 *
869 * @since 1.9.6
870 *
871 * @param int $tag Tag ID.
872 * @param array $args Arguments.
873 * @return WP_Error|array
874 */
875 public function add_tag( $tag, $args ) {
876
877 // Warn the developer that they shouldn't use this function.
878 _deprecated_function( __FUNCTION__, '1.9.6', 'tag_subscribe( $tag_id, $email_address )' );
879
880 // Pass request to new function.
881 return $this->tag_subscribe( $tag, $args['email'] );
882
883 }
884
885 /**
886 * Backward compat. function for fetching Legacy Form or Landing Page markup for the given URL.
887 *
888 * @since 1.9.6
889 *
890 * @param string $url URL.
891 * @return WP_Error|string
892 */
893 public function get_resource( $url ) {
894
895 // Warn the developer that they shouldn't use this function.
896 _deprecated_function( __FUNCTION__, '1.9.6', 'get_form_html( $form_id ) or get_landing_page_html( $url )' );
897
898 // Pass request to new function.
899 return $this->get_landing_page_html( $url );
900
901 }
902
903 /**
904 * Backward compat. function for fetching Legacy Form or Landing Page markup for the given URL.
905 *
906 * @since 1.9.6
907 *
908 * @param array $args Arguments (single email key).
909 * @return WP_Error|array
910 */
911 public function form_unsubscribe( $args ) {
912
913 // Warn the developer that they shouldn't use this function.
914 _deprecated_function( __FUNCTION__, '1.9.6', 'unsubscribe( $email_address )' );
915
916 // Pass request to new function.
917 return $this->unsubscribe( $args['email'] );
918
919 }
920
921 /**
922 * Get HTML for the given URL.
923 *
924 * This isn't specifically an API function, but for now it's best suited here.
925 *
926 * @param string $url URL of Form or Landing Page.
927 * @param bool $body_only Return HTML between <body> and </body> tags only.
928 * @return WP_Error|string
929 */
930 private function get_html( $url, $body_only = true ) {
931
932 // Get HTML from URL.
933 $result = wp_remote_get(
934 $url,
935 array(
936 'Accept-Encoding' => 'gzip',
937 'timeout' => $this->get_timeout(),
938 'user-agent' => $this->get_user_agent(),
939 )
940 );
941
942 // If an error occured, log and return it now.
943 if ( is_wp_error( $result ) ) {
944 return $result;
945 }
946
947 // Fetch HTTP response code and body.
948 $http_response_code = wp_remote_retrieve_response_code( $result );
949 $body = wp_remote_retrieve_body( $result );
950
951 // If the body appears to be JSON containing an error, the request for a Legacy Form
952 // through api.convertkit.com failed, so return a WP_Error now.
953 if ( $this->is_json( $body ) ) {
954 $json = json_decode( $body );
955 return new WP_Error(
956 'convertkit_api_error',
957 sprintf(
958 /* translators: API Error Message */
959 __( 'ConvertKit: %s', 'convertkit' ),
960 $json->error_message
961 )
962 );
963 }
964
965 // Get just the scheme and host from the URL.
966 $url_scheme = wp_parse_url( $url );
967 $url_scheme_host_only = $url_scheme['scheme'] . '://' . $url_scheme['host'];
968
969 // Load the landing page HTML into a DOMDocument.
970 libxml_use_internal_errors( true );
971 $html = new DOMDocument();
972 if ( $body_only ) {
973 // Prevent DOMDocument from including a doctype on saveHTML().
974 // We don't use LIBXML_HTML_NOIMPLIED, as it requires a single root element, which Legacy Forms don't have.
975 $html->loadHTML( mb_convert_encoding( $body, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NODEFDTD );
976 } else {
977 $html->loadHTML( mb_convert_encoding( $body, 'HTML-ENTITIES', 'UTF-8' ) );
978 }
979
980 // Convert any relative URLs to absolute URLs in the HTML DOM.
981 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'a' ), 'href', $url_scheme_host_only );
982 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'link' ), 'href', $url_scheme_host_only );
983 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'img' ), 'src', $url_scheme_host_only );
984 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'script' ), 'src', $url_scheme_host_only );
985 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'form' ), 'action', $url_scheme_host_only );
986
987 // If the entire HTML needs to be returned, return it now.
988 if ( ! $body_only ) {
989 return $html->saveHTML();
990 }
991
992 // Remove some HTML tags that DOMDocument adds, returning the output.
993 // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
994 // a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
995 return $this->strip_html_head_body_tags( $html->saveHTML() );
996
997 }
998
999 /**
1000 * Determines if the given string is JSON.
1001 *
1002 * @since 1.9.6.4
1003 *
1004 * @param string $string Possible JSON String.
1005 * @return bool Is JSON String.
1006 */
1007 private function is_json( $string ) {
1008
1009 json_decode( $string );
1010 return json_last_error() === JSON_ERROR_NONE;
1011
1012 }
1013
1014 /**
1015 * Converts any relative URls to absolute, fully qualified HTTP(s) URLs for the given
1016 * DOM Elements.
1017 *
1018 * @since 1.9.6
1019 *
1020 * @param DOMNodeList<DOMElement> $elements Elements.
1021 * @param string $attribute HTML Attribute.
1022 * @param string $url Absolute URL to prepend to relative URLs.
1023 */
1024 private function convert_relative_to_absolute_urls( $elements, $attribute, $url ) {
1025
1026 // Anchor hrefs.
1027 foreach ( $elements as $element ) {
1028 // Skip if the attribute's value is empty.
1029 if ( empty( $element->getAttribute( $attribute ) ) ) {
1030 continue;
1031 }
1032
1033 // Skip if the attribute's value is a fully qualified URL.
1034 if ( filter_var( $element->getAttribute( $attribute ), FILTER_VALIDATE_URL ) ) {
1035 continue;
1036 }
1037
1038 // Skip if this is a Google Font CSS URL.
1039 if ( strpos( $element->getAttribute( $attribute ), '//fonts.googleapis.com' ) !== false ) {
1040 continue;
1041 }
1042
1043 // If here, the attribute's value is a relative URL, missing the http(s) and domain.
1044 // Prepend the URL to the attribute's value.
1045 $element->setAttribute( $attribute, $url . $element->getAttribute( $attribute ) );
1046 }
1047
1048 }
1049
1050 /**
1051 * Strips <html>, <head> and <body> opening and closing tags from the given markup.
1052 *
1053 * @since 1.9.6.5
1054 *
1055 * @param string $markup HTML Markup.
1056 * @return string HTML Markup
1057 * */
1058 private function strip_html_head_body_tags( $markup ) {
1059
1060 $markup = str_replace( '<html>', '', $markup );
1061 $markup = str_replace( '</html>', '', $markup );
1062 $markup = str_replace( '<head>', '', $markup );
1063 $markup = str_replace( '</head>', '', $markup );
1064 $markup = str_replace( '<body>', '', $markup );
1065 $markup = str_replace( '</body>', '', $markup );
1066
1067 return $markup;
1068
1069 }
1070
1071 /**
1072 * Gets all forms and landing pages from the API.
1073 *
1074 * @since 1.9.6
1075 *
1076 * @return WP_Error|array
1077 */
1078 private function get_forms_landing_pages() {
1079
1080 // Send request.
1081 $response = $this->get(
1082 'forms',
1083 array(
1084 'api_key' => $this->api_key,
1085 )
1086 );
1087
1088 // If an error occured, log and return it now.
1089 if ( is_wp_error( $response ) ) {
1090 return $response;
1091 }
1092
1093 // If no forms exist.
1094 if ( ! isset( $response['forms'] ) ) {
1095 return new WP_Error(
1096 'convertkit_api_error',
1097 __( 'No forms exist in ConvertKit. Visit your ConvertKit account and create your first form.', 'convertkit' )
1098 );
1099 }
1100
1101 // Iterate through forms, determining if each form is a form or landing page.
1102 $forms = array();
1103 $landing_pages = array();
1104 foreach ( $response['forms'] as $form ) {
1105 // Skip archived forms.
1106 if ( isset( $form['archived'] ) && $form['archived'] ) {
1107 continue;
1108 }
1109
1110 switch ( $form['type'] ) {
1111 case 'hosted':
1112 $landing_pages[ $form['id'] ] = $form;
1113 break;
1114
1115 default:
1116 $forms[ $form['id'] ] = $form;
1117 break;
1118 }
1119 }
1120
1121 return array(
1122 'forms' => $forms,
1123 'landing_pages' => $landing_pages,
1124 );
1125
1126 }
1127
1128 /**
1129 * Performs a GET request.
1130 *
1131 * @since 1.9.6
1132 *
1133 * @param string $endpoint API Endpoint.
1134 * @param array $params Params.
1135 * @return WP_Error|array
1136 */
1137 private function get( $endpoint, $params ) {
1138
1139 return $this->request( $endpoint, 'get', $params, true );
1140
1141 }
1142
1143 /**
1144 * Performs a POST request.
1145 *
1146 * @since 1.9.6
1147 *
1148 * @param string $endpoint API Endpoint.
1149 * @param array $params Params.
1150 * @return WP_Error|array
1151 */
1152 private function post( $endpoint, $params ) {
1153
1154 return $this->request( $endpoint, 'post', $params, true );
1155
1156 }
1157
1158 /**
1159 * Main function which handles sending requests to the API using WordPress functions.
1160 *
1161 * @since 1.9.6
1162 *
1163 * @param string $endpoint API Endpoint (required).
1164 * @param string $method HTTP Method (optional).
1165 * @param mixed $params Params (array|boolean|string).
1166 * @param bool $retry_if_rate_limit_hit Retry request if rate limit hit.
1167 * @return WP_Error|array
1168 */
1169 private function request( $endpoint, $method = 'get', $params = array(), $retry_if_rate_limit_hit = true ) {
1170
1171 // Send request.
1172 switch ( $method ) {
1173 case 'get':
1174 $result = wp_remote_get(
1175 $this->add_params_to_url( $this->get_api_url( $endpoint ), $params ),
1176 array(
1177 'Accept-Encoding' => 'gzip',
1178 'timeout' => $this->get_timeout(),
1179 'user-agent' => $this->get_user_agent(),
1180 )
1181 );
1182 break;
1183
1184 case 'post':
1185 $result = wp_remote_post(
1186 $this->get_api_url( $endpoint ),
1187 array(
1188 'Accept-Encoding' => 'gzip',
1189 'headers' => array(
1190 'Content-Type' => 'application/json; charset=utf-8',
1191 ),
1192 'body' => wp_json_encode( $params ),
1193 'timeout' => $this->get_timeout(),
1194 'user-agent' => $this->get_user_agent(),
1195 )
1196 );
1197 break;
1198
1199 default:
1200 $result = new WP_Error(
1201 'convertkit_api_error',
1202 sprintf(
1203 /* translators: HTTP method */
1204 __( 'API request method %s is not supported in ConvertKit_API class.', 'convertkit' ),
1205 $method
1206 )
1207 );
1208 break;
1209 }
1210
1211 // If an error occured, log and return it now.
1212 if ( is_wp_error( $result ) ) {
1213 $this->log( 'API: Error: ' . $result->get_error_message() );
1214 return $result;
1215 }
1216
1217 // Fetch HTTP response code and body.
1218 $http_response_code = wp_remote_retrieve_response_code( $result );
1219 $body = wp_remote_retrieve_body( $result );
1220 $response = json_decode( $body, true );
1221
1222 // If the HTTP response code is 429, we've hit the API's rate limit of 120 requests over 60 seconds.
1223 if ( $http_response_code === 429 ) {
1224 // If retry on rate limit hit is disabled, return a WP_Error.
1225 if ( ! $retry_if_rate_limit_hit ) {
1226 return new WP_Error( 'convertkit_api_error', __( 'Rate limit hit.', 'convertkit' ) );
1227 }
1228
1229 // Retry the request a final time, waiting 2 seconds before.
1230 sleep( 2 );
1231 return $this->request( $endpoint, $method, $params, false );
1232 }
1233
1234 // If an error message or code exists in the response, return a WP_Error.
1235 if ( isset( $response['error'] ) ) {
1236 $this->log( 'API: Error: ' . $response['error'] . ': ' . $response['message'] );
1237 return new WP_Error( 'convertkit_api_error', $response['error'] . ': ' . $response['message'] );
1238 }
1239
1240 return $response;
1241
1242 }
1243
1244 /**
1245 * Returns the maximum amount of time to wait for
1246 * a response to the request before exiting.
1247 *
1248 * @since 1.9.6
1249 *
1250 * @return int Timeout, in seconds.
1251 */
1252 private function get_timeout() {
1253
1254 $timeout = 10;
1255
1256 /**
1257 * Defines the maximum time to allow the API request to run.
1258 *
1259 * @since 2.2.9
1260 *
1261 * @param int $timeout Timeout, in seconds.
1262 */
1263 $timeout = apply_filters( 'convertkit_api_get_timeout', $timeout );
1264
1265 return $timeout;
1266
1267 }
1268
1269 /**
1270 * Gets a customized version of the WordPress default user agent; includes WP Version, PHP version, and ConvertKit plugin version.
1271 *
1272 * @since 1.9.6
1273 *
1274 * @return string User Agent
1275 */
1276 private function get_user_agent() {
1277
1278 global $wp_version;
1279
1280 // Include an unmodified $wp_version.
1281 require ABSPATH . WPINC . '/version.php';
1282
1283 return sprintf(
1284 'WordPress/%1$s;PHP/%2$s;ConvertKit/%3$s;%4$s',
1285 $wp_version,
1286 phpversion(),
1287 CONVERTKIT_PLUGIN_VERSION,
1288 home_url( '/' )
1289 );
1290
1291 }
1292
1293 /**
1294 * Returns the full API URL for the given endpoint.
1295 *
1296 * @since 1.9.6
1297 *
1298 * @param string $endpoint Endpoint.
1299 * @return string API URL
1300 */
1301 private function get_api_url( $endpoint ) {
1302
1303 return path_join( $this->api_url_base . $this->api_version, $endpoint );
1304
1305 }
1306
1307 /**
1308 * Adds the supplied array of parameters as query arguments to the URL.
1309 *
1310 * @since 1.9.6.9
1311 *
1312 * @param string $url URL.
1313 * @param array $params Parameters for request.
1314 * @return string URL with API Key or API Secret
1315 */
1316 private function add_params_to_url( $url, $params ) {
1317
1318 return add_query_arg( $params, $url );
1319
1320 }
1321
1322 /**
1323 * Adds the given entry to the log file, if debugging is enabled.
1324 *
1325 * @since 1.9.6
1326 *
1327 * @param string $entry Log Entry.
1328 */
1329 private function log( $entry ) {
1330
1331 // Don't log this entry if debugging is disabled.
1332 if ( ! $this->debug ) {
1333 return;
1334 }
1335
1336 // Pass the request to the log class.
1337 $this->log->add( $entry );
1338
1339 }
1340
1341 }
1342