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

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