PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 1.9.7.4
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v1.9.7.4
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.4, at includes/class-convertkit-api.php

1,409 lines 37.4 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 * Gets posts from the API.
736 *
737 * @since 1.9.7.4
738 *
739 * @param int $page Page number.
740 * @param int $per_page Number of Posts to return.
741 * @return WP_Error|array
742 */
743 public function get_posts( $page = 1, $per_page = 10 ) {
744
745 $this->log( 'API: get_posts()' );
746
747 // Sanitize some parameters.
748 $page = absint( $page );
749 $per_page = absint( $per_page );
750
751 // Sanity check that parameters aren't outside of the bounds as defined by the API.
752 if ( $page < 1 ) {
753 return new WP_Error( 'convertkit_api_error', __( 'get_posts(): the page parameter must be equal to or greater than 1.', 'convertkit' ) );
754 }
755 if ( $per_page < 1 ) {
756 return new WP_Error( 'convertkit_api_error', __( 'get_posts(): the per_page parameter must be equal to or greater than 1.', 'convertkit' ) );
757 }
758 if ( $per_page > 50 ) {
759 return new WP_Error( 'convertkit_api_error', __( 'get_posts(): the per_page parameter must be equal to or less than 50.', 'convertkit' ) );
760 }
761
762 $posts = array();
763
764 // Send request.
765 $response = $this->get(
766 'posts',
767 array(
768 'api_key' => $this->api_key,
769 'api_secret' => $this->api_secret,
770 'page' => $page,
771 'per_page' => $per_page,
772 )
773 );
774
775 // If an error occured, return WP_Error.
776 if ( is_wp_error( $response ) ) {
777 $this->log( 'API: get_posts(): Error: ' . $response->get_error_message() );
778 return $response;
779 }
780
781 // If no custom fields exist, return WP_Error.
782 if ( ! isset( $response['posts'] ) ) {
783 $this->log( 'API: get_posts(): Error: No broadcasts exist in ConvertKit.' );
784 return new WP_Error( 'convertkit_api_error', __( 'No posts exist in ConvertKit. Visit your ConvertKit account and create your first broadcast.', 'convertkit' ) );
785 }
786 if ( ! count( $response['posts'] ) ) {
787 $this->log( 'API: get_posts(): Error: No broadcasts exist in ConvertKit.' );
788 return new WP_Error( 'convertkit_api_error', __( 'No posts exist in ConvertKit. Visit your ConvertKit account and create your first broadcast.', 'convertkit' ) );
789 }
790
791 return $response['posts'];
792
793 }
794
795 /**
796 * Get HTML from ConvertKit for the given Legacy Form ID.
797 *
798 * This isn't specifically an API function, but for now it's best suited here.
799 *
800 * @param int $id Form ID.
801 * @return WP_Error|string HTML
802 */
803 public function get_form_html( $id ) {
804
805 // Define Legacy Form URL.
806 $url = add_query_arg(
807 array(
808 'k' => $this->api_key,
809 'v' => 2,
810 ),
811 'https://api.convertkit.com/forms/' . $id . '/embed'
812 );
813
814 // Get HTML.
815 $body = $this->get_html( $url );
816
817 return $body;
818
819 }
820
821 /**
822 * Get HTML from ConvertKit for the given Landing Page URL.
823 *
824 * This isn't specifically an API function, but for now it's best suited here.
825 *
826 * @param string $url URL of Landing Page.
827 * @return string HTML
828 */
829 public function get_landing_page_html( $url ) {
830
831 // Get HTML.
832 $body = $this->get_html( $url, false );
833
834 // Inject JS for subscriber forms to work.
835 $scripts = new WP_Scripts();
836 $script = "<script type='text/javascript' src='" . trailingslashit( $scripts->base_url ) . "wp-includes/js/jquery/jquery.js?ver=1.4.0'></script>"; // phpcs:ignore
837 $script .= "<script type='text/javascript' src='" . CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js?ver=' . CONVERTKIT_PLUGIN_VERSION . "'></script>"; // phpcs:ignore
838 $script .= "<script type='text/javascript'>/* <![CDATA[ */var convertkit = {\"ajaxurl\":\"" . admin_url( 'admin-ajax.php' ) . '"};/* ]]> */</script>'; // phpcs:ignore
839
840 $body = str_replace( '</head>', '</head>' . $script, $body );
841
842 return $body;
843
844 }
845
846 /**
847 * Create a Purchase.
848 *
849 * @since 1.9.6.9
850 *
851 * @param array $purchase Purchase Data.
852 * @return WP_Error|array
853 */
854 public function purchase_create( $purchase ) {
855
856 $this->log( 'API: purchase_create(): [ purchase: ' . print_r( $purchase, true ) . ']' ); // phpcs:ignore
857
858 $response = $this->post(
859 'purchases',
860 array(
861 'api_secret' => $this->api_secret,
862 'purchase' => $purchase,
863 )
864 );
865
866 if ( is_wp_error( $response ) ) {
867 $this->log( 'API: purchase_create(): Error: ' . $response->get_error_message() );
868 }
869
870 /**
871 * Runs actions immediately after the purchase data address was successfully created.
872 *
873 * @since 1.9.6.9
874 *
875 * @param array $response API Response
876 * @param array $purchase Purchase Data
877 */
878 do_action( 'convertkit_api_purchase_create_success', $response, $purchase );
879
880 return $response;
881
882 }
883
884 /**
885 * Backward compat. function for updating Forms, Landing Pages and Tags in WordPress options table.
886 *
887 * @since 1.0.0
888 *
889 * @param string $api_key API Key.
890 * @param string $api_secret API Secret.
891 */
892 public function update_resources( $api_key, $api_secret ) { // phpcs:ignore
893
894 // Warn the developer that they shouldn't use this function.
895 _deprecated_function( __FUNCTION__, '1.9.6', 'refresh() in ConvertKit_Resource_Forms, ConvertKit_Resource_Landing_Pages and ConvertKit_Resource_Tags classes.' );
896
897 // Initialize resource classes.
898 $forms = new ConvertKit_Resource_Forms();
899 $landing_pages = new ConvertKit_Resource_Landing_Pages();
900 $tags = new ConvertKit_Resource_Tags();
901
902 // Refresh resources by calling the API and storing the results.
903 $forms->refresh();
904 $landing_pages->refresh();
905 $tags->refresh();
906
907 }
908
909 /**
910 * Backward compat. function for getting a ConvertKit subscriber by their ID.
911 *
912 * @since 1.9.6
913 *
914 * @param int $id Subscriber ID.
915 * @return WP_Error|array
916 */
917 public function get_subscriber( $id ) {
918
919 // Warn the developer that they shouldn't use this function.
920 _deprecated_function( __FUNCTION__, '1.9.6', 'get_subscriber_by_id()' );
921
922 // Pass request to new function.
923 return $this->get_subscriber_by_id( $id );
924
925 }
926
927 /**
928 * Backward compat. function for subscribing a ConvertKit subscriber to the given Tag.
929 *
930 * @since 1.9.6
931 *
932 * @param int $tag Tag ID.
933 * @param array $args Arguments.
934 * @return WP_Error|array
935 */
936 public function add_tag( $tag, $args ) {
937
938 // Warn the developer that they shouldn't use this function.
939 _deprecated_function( __FUNCTION__, '1.9.6', 'tag_subscribe( $tag_id, $email_address )' );
940
941 // Pass request to new function.
942 return $this->tag_subscribe( $tag, $args['email'] );
943
944 }
945
946 /**
947 * Backward compat. function for fetching Legacy Form or Landing Page markup for the given URL.
948 *
949 * @since 1.9.6
950 *
951 * @param string $url URL.
952 * @return WP_Error|string
953 */
954 public function get_resource( $url ) {
955
956 // Warn the developer that they shouldn't use this function.
957 _deprecated_function( __FUNCTION__, '1.9.6', 'get_form_html( $form_id ) or get_landing_page_html( $url )' );
958
959 // Pass request to new function.
960 return $this->get_landing_page_html( $url );
961
962 }
963
964 /**
965 * Backward compat. function for fetching Legacy Form or Landing Page markup for the given URL.
966 *
967 * @since 1.9.6
968 *
969 * @param array $args Arguments (single email key).
970 * @return WP_Error|array
971 */
972 public function form_unsubscribe( $args ) {
973
974 // Warn the developer that they shouldn't use this function.
975 _deprecated_function( __FUNCTION__, '1.9.6', 'unsubscribe( $email_address )' );
976
977 // Pass request to new function.
978 return $this->unsubscribe( $args['email'] );
979
980 }
981
982 /**
983 * Get HTML for the given URL.
984 *
985 * This isn't specifically an API function, but for now it's best suited here.
986 *
987 * @param string $url URL of Form or Landing Page.
988 * @param bool $body_only Return HTML between <body> and </body> tags only.
989 * @return WP_Error|string
990 */
991 private function get_html( $url, $body_only = true ) {
992
993 // Get HTML from URL.
994 $result = wp_remote_get(
995 $url,
996 array(
997 'Accept-Encoding' => 'gzip',
998 'timeout' => $this->get_timeout(),
999 'user-agent' => $this->get_user_agent(),
1000 )
1001 );
1002
1003 // If an error occured, log and return it now.
1004 if ( is_wp_error( $result ) ) {
1005 return $result;
1006 }
1007
1008 // Fetch HTTP response code and body.
1009 $http_response_code = wp_remote_retrieve_response_code( $result );
1010 $body = wp_remote_retrieve_body( $result );
1011
1012 // If the body appears to be JSON containing an error, the request for a Legacy Form
1013 // through api.convertkit.com failed, so return a WP_Error now.
1014 if ( $this->is_json( $body ) ) {
1015 $json = json_decode( $body );
1016 return new WP_Error(
1017 'convertkit_api_error',
1018 sprintf(
1019 /* translators: API Error Message */
1020 __( 'ConvertKit: %s', 'convertkit' ),
1021 $json->error_message
1022 )
1023 );
1024 }
1025
1026 // Get just the scheme and host from the URL.
1027 $url_scheme = wp_parse_url( $url );
1028 $url_scheme_host_only = $url_scheme['scheme'] . '://' . $url_scheme['host'];
1029
1030 // Load the landing page HTML into a DOMDocument.
1031 libxml_use_internal_errors( true );
1032 $html = new DOMDocument();
1033 if ( $body_only ) {
1034 // Prevent DOMDocument from including a doctype on saveHTML().
1035 // We don't use LIBXML_HTML_NOIMPLIED, as it requires a single root element, which Legacy Forms don't have.
1036 $html->loadHTML( mb_convert_encoding( $body, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NODEFDTD );
1037 } else {
1038 $html->loadHTML( mb_convert_encoding( $body, 'HTML-ENTITIES', 'UTF-8' ) );
1039 }
1040
1041 // Convert any relative URLs to absolute URLs in the HTML DOM.
1042 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'a' ), 'href', $url_scheme_host_only );
1043 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'link' ), 'href', $url_scheme_host_only );
1044 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'img' ), 'src', $url_scheme_host_only );
1045 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'script' ), 'src', $url_scheme_host_only );
1046 $this->convert_relative_to_absolute_urls( $html->getElementsByTagName( 'form' ), 'action', $url_scheme_host_only );
1047
1048 // If the entire HTML needs to be returned, return it now.
1049 if ( ! $body_only ) {
1050 return $html->saveHTML();
1051 }
1052
1053 // Remove some HTML tags that DOMDocument adds, returning the output.
1054 // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
1055 // a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
1056 return $this->strip_html_head_body_tags( $html->saveHTML() );
1057
1058 }
1059
1060 /**
1061 * Determines if the given string is JSON.
1062 *
1063 * @since 1.9.6.4
1064 *
1065 * @param string $string Possible JSON String.
1066 * @return bool Is JSON String.
1067 */
1068 private function is_json( $string ) {
1069
1070 json_decode( $string );
1071 return json_last_error() === JSON_ERROR_NONE;
1072
1073 }
1074
1075 /**
1076 * Converts any relative URls to absolute, fully qualified HTTP(s) URLs for the given
1077 * DOM Elements.
1078 *
1079 * @since 1.9.6
1080 *
1081 * @param DOMNodeList<DOMElement> $elements Elements.
1082 * @param string $attribute HTML Attribute.
1083 * @param string $url Absolute URL to prepend to relative URLs.
1084 */
1085 private function convert_relative_to_absolute_urls( $elements, $attribute, $url ) {
1086
1087 // Anchor hrefs.
1088 foreach ( $elements as $element ) {
1089 // Skip if the attribute's value is empty.
1090 if ( empty( $element->getAttribute( $attribute ) ) ) {
1091 continue;
1092 }
1093
1094 // Skip if the attribute's value is a fully qualified URL.
1095 if ( filter_var( $element->getAttribute( $attribute ), FILTER_VALIDATE_URL ) ) {
1096 continue;
1097 }
1098
1099 // Skip if this is a Google Font CSS URL.
1100 if ( strpos( $element->getAttribute( $attribute ), '//fonts.googleapis.com' ) !== false ) {
1101 continue;
1102 }
1103
1104 // If here, the attribute's value is a relative URL, missing the http(s) and domain.
1105 // Prepend the URL to the attribute's value.
1106 $element->setAttribute( $attribute, $url . $element->getAttribute( $attribute ) );
1107 }
1108
1109 }
1110
1111 /**
1112 * Strips <html>, <head> and <body> opening and closing tags from the given markup.
1113 *
1114 * @since 1.9.6.5
1115 *
1116 * @param string $markup HTML Markup.
1117 * @return string HTML Markup
1118 * */
1119 private function strip_html_head_body_tags( $markup ) {
1120
1121 $markup = str_replace( '<html>', '', $markup );
1122 $markup = str_replace( '</html>', '', $markup );
1123 $markup = str_replace( '<head>', '', $markup );
1124 $markup = str_replace( '</head>', '', $markup );
1125 $markup = str_replace( '<body>', '', $markup );
1126 $markup = str_replace( '</body>', '', $markup );
1127
1128 return $markup;
1129
1130 }
1131
1132 /**
1133 * Gets all forms and landing pages from the API.
1134 *
1135 * @since 1.9.6
1136 *
1137 * @return WP_Error|array
1138 */
1139 private function get_forms_landing_pages() {
1140
1141 // Send request.
1142 $response = $this->get(
1143 'forms',
1144 array(
1145 'api_key' => $this->api_key,
1146 )
1147 );
1148
1149 // If an error occured, log and return it now.
1150 if ( is_wp_error( $response ) ) {
1151 return $response;
1152 }
1153
1154 // If no forms exist.
1155 if ( ! isset( $response['forms'] ) ) {
1156 return new WP_Error(
1157 'convertkit_api_error',
1158 __( 'No forms exist in ConvertKit. Visit your ConvertKit account and create your first form.', 'convertkit' )
1159 );
1160 }
1161
1162 // Iterate through forms, determining if each form is a form or landing page.
1163 $forms = array();
1164 $landing_pages = array();
1165 foreach ( $response['forms'] as $form ) {
1166 // Skip archived forms.
1167 if ( isset( $form['archived'] ) && $form['archived'] ) {
1168 continue;
1169 }
1170
1171 switch ( $form['type'] ) {
1172 case 'hosted':
1173 $landing_pages[ $form['id'] ] = $form;
1174 break;
1175
1176 default:
1177 $forms[ $form['id'] ] = $form;
1178 break;
1179 }
1180 }
1181
1182 return array(
1183 'forms' => $forms,
1184 'landing_pages' => $landing_pages,
1185 );
1186
1187 }
1188
1189 /**
1190 * Performs a GET request.
1191 *
1192 * @since 1.9.6
1193 *
1194 * @param string $endpoint API Endpoint.
1195 * @param array $params Params.
1196 * @return WP_Error|array
1197 */
1198 private function get( $endpoint, $params ) {
1199
1200 return $this->request( $endpoint, 'get', $params, true );
1201
1202 }
1203
1204 /**
1205 * Performs a POST request.
1206 *
1207 * @since 1.9.6
1208 *
1209 * @param string $endpoint API Endpoint.
1210 * @param array $params Params.
1211 * @return WP_Error|array
1212 */
1213 private function post( $endpoint, $params ) {
1214
1215 return $this->request( $endpoint, 'post', $params, true );
1216
1217 }
1218
1219 /**
1220 * Main function which handles sending requests to the API using WordPress functions.
1221 *
1222 * @since 1.9.6
1223 *
1224 * @param string $endpoint API Endpoint (required).
1225 * @param string $method HTTP Method (optional).
1226 * @param mixed $params Params (array|boolean|string).
1227 * @param bool $retry_if_rate_limit_hit Retry request if rate limit hit.
1228 * @return WP_Error|array
1229 */
1230 private function request( $endpoint, $method = 'get', $params = array(), $retry_if_rate_limit_hit = true ) {
1231
1232 // Send request.
1233 switch ( $method ) {
1234 case 'get':
1235 $result = wp_remote_get(
1236 $this->add_params_to_url( $this->get_api_url( $endpoint ), $params ),
1237 array(
1238 'Accept-Encoding' => 'gzip',
1239 'timeout' => $this->get_timeout(),
1240 'user-agent' => $this->get_user_agent(),
1241 )
1242 );
1243 break;
1244
1245 case 'post':
1246 $result = wp_remote_post(
1247 $this->get_api_url( $endpoint ),
1248 array(
1249 'Accept-Encoding' => 'gzip',
1250 'headers' => array(
1251 'Content-Type' => 'application/json; charset=utf-8',
1252 ),
1253 'body' => wp_json_encode( $params ),
1254 'timeout' => $this->get_timeout(),
1255 'user-agent' => $this->get_user_agent(),
1256 )
1257 );
1258 break;
1259
1260 default:
1261 $result = new WP_Error(
1262 'convertkit_api_error',
1263 sprintf(
1264 /* translators: HTTP method */
1265 __( 'API request method %s is not supported in ConvertKit_API class.', 'convertkit' ),
1266 $method
1267 )
1268 );
1269 break;
1270 }
1271
1272 // If an error occured, log and return it now.
1273 if ( is_wp_error( $result ) ) {
1274 $this->log( 'API: Error: ' . $result->get_error_message() );
1275 return $result;
1276 }
1277
1278 // Fetch HTTP response code and body.
1279 $http_response_code = wp_remote_retrieve_response_code( $result );
1280 $body = wp_remote_retrieve_body( $result );
1281 $response = json_decode( $body, true );
1282
1283 // If the HTTP response code is 429, we've hit the API's rate limit of 120 requests over 60 seconds.
1284 if ( $http_response_code === 429 ) {
1285 // If retry on rate limit hit is disabled, return a WP_Error.
1286 if ( ! $retry_if_rate_limit_hit ) {
1287 return new WP_Error( 'convertkit_api_error', __( 'Rate limit hit.', 'convertkit' ) );
1288 }
1289
1290 // Retry the request a final time, waiting 2 seconds before.
1291 sleep( 2 );
1292 return $this->request( $endpoint, $method, $params, false );
1293 }
1294
1295 // If an error message or code exists in the response, return a WP_Error.
1296 if ( isset( $response['error'] ) ) {
1297 $this->log( 'API: Error: ' . $response['error'] . ': ' . $response['message'] );
1298 return new WP_Error( 'convertkit_api_error', $response['error'] . ': ' . $response['message'] );
1299 }
1300
1301 return $response;
1302
1303 }
1304
1305 /**
1306 * Returns the maximum amount of time to wait for
1307 * a response to the request before exiting.
1308 *
1309 * @since 1.9.6
1310 *
1311 * @return int Timeout, in seconds.
1312 */
1313 private function get_timeout() {
1314
1315 $timeout = 10;
1316
1317 /**
1318 * Defines the maximum time to allow the API request to run.
1319 *
1320 * @since 2.2.9
1321 *
1322 * @param int $timeout Timeout, in seconds.
1323 */
1324 $timeout = apply_filters( 'convertkit_api_get_timeout', $timeout );
1325
1326 return $timeout;
1327
1328 }
1329
1330 /**
1331 * Gets a customized version of the WordPress default user agent; includes WP Version, PHP version, and ConvertKit plugin version.
1332 *
1333 * @since 1.9.6
1334 *
1335 * @return string User Agent
1336 */
1337 private function get_user_agent() {
1338
1339 global $wp_version;
1340
1341 // Include an unmodified $wp_version.
1342 require ABSPATH . WPINC . '/version.php';
1343
1344 return sprintf(
1345 'WordPress/%1$s;PHP/%2$s;ConvertKit/%3$s;%4$s',
1346 $wp_version,
1347 phpversion(),
1348 CONVERTKIT_PLUGIN_VERSION,
1349 home_url( '/' )
1350 );
1351
1352 }
1353
1354 /**
1355 * Returns the full API URL for the given endpoint.
1356 *
1357 * @since 1.9.6
1358 *
1359 * @param string $endpoint Endpoint.
1360 * @return string API URL
1361 */
1362 private function get_api_url( $endpoint ) {
1363
1364 // For the /posts endpoint, the API base is https://api.convertkit.com/api/v3/$endpoint.
1365 if ( $endpoint === 'posts' ) {
1366 return path_join( $this->api_url_base . 'api/' . $this->api_version, $endpoint );
1367 }
1368
1369 // For all other endpoints, it's https://api.convertkit.com/v3/$endpoint.
1370 return path_join( $this->api_url_base . $this->api_version, $endpoint );
1371
1372 }
1373
1374 /**
1375 * Adds the supplied array of parameters as query arguments to the URL.
1376 *
1377 * @since 1.9.6.9
1378 *
1379 * @param string $url URL.
1380 * @param array $params Parameters for request.
1381 * @return string URL with API Key or API Secret
1382 */
1383 private function add_params_to_url( $url, $params ) {
1384
1385 return add_query_arg( $params, $url );
1386
1387 }
1388
1389 /**
1390 * Adds the given entry to the log file, if debugging is enabled.
1391 *
1392 * @since 1.9.6
1393 *
1394 * @param string $entry Log Entry.
1395 */
1396 private function log( $entry ) {
1397
1398 // Don't log this entry if debugging is disabled.
1399 if ( ! $this->debug ) {
1400 return;
1401 }
1402
1403 // Pass the request to the log class.
1404 $this->log->add( $entry );
1405
1406 }
1407
1408 }
1409