<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[7881] sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities: WordCamp Utilities: Add a generic, extendable API client class</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/7881">7881</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/7881","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>coreymckrill</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2018-11-22 04:13:15 +0000 (Thu, 22 Nov 2018)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Utilities: Add a generic, extendable API client class

This class provides methods for making redundant remote requests similar
to those in our Meetup API client. Our existing API client classes may be
able to extend it and use these methods with low refactor effort.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclasscurrencyxrtclientphp">sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-currency-xrt-client.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassmeetupclientphp">sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-meetup-client.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassqboclientphp">sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-qbo-client.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassstripeclientphp">sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-stripe-client.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassapiclientphp">sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-api-client.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassapiclientphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-api-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-api-client.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-api-client.php   2018-11-22 04:13:15 UTC (rev 7881)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,218 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+namespace WordCamp\Utilities;
+defined( 'WPINC' ) || die();
+
+use WP_Error;
+
+/**
+ * Class API_Client
+ *
+ * A generic, extendable class for making requests to an API.
+ *
+ * @package WordCamp\Utilities
+ */
+class API_Client {
+       /**
+        * @var WP_Error|null Container for errors.
+        */
+       public $error = null;
+
+       /**
+        * @var string|array A function to call to determine whether to throttle requests to the API.
+        */
+       protected $throttle_callback = '';
+
+       /*
+        * @var array A list of integer response codes that should break the "tenacious" remote request loop.
+        */
+       protected $breaking_response_codes = [];
+
+       /**
+        * API_Client constructor.
+        *
+        * @param array $settings {
+        *     Optional. Settings for the client.
+        *
+        *     @type callable $throttle_callback       A function to call to determine whether to throttle requests to
+        *                                             the API.
+        *     @type array    $breaking_response_codes A list of integer response codes that should break the "tenacious"
+        *                                             remote request loop.
+        * }
+        */
+       public function __construct( array $settings = [] ) {
+               $this->error = new WP_Error();
+
+               $defaults = [
+                       'throttle_callback'       => '',
+                       'breaking_response_codes' => [ 400, 401, 404, 429 ],
+               ];
+
+               $settings = wp_parse_args( $settings, $defaults );
+
+               $this->throttle_callback       = $settings['throttle_callback'];
+               $this->breaking_response_codes = $settings['breaking_response_codes'];
+       }
+
+       /**
+        * Wrapper for `wp_remote_get` to retry requests that fail temporarily for various reasons.
+        *
+        * One common example of a reason a request would fail, but later succeed, is when the first request times out.
+        *
+        * Based on `wcorg_redundant_remote_get`.
+        *
+        * @param string $url
+        * @param array  $args
+        *
+        * @return array|WP_Error
+        */
+       protected function tenacious_remote_request( $url, array $args = [] ) {
+               $attempt_count  = 0;
+               $max_attempts   = 3;
+               $breaking_codes = $this->breaking_response_codes;
+
+               // The default of 5 seconds often results in frequent timeouts.
+               if ( empty( $args['timeout'] ) ) {
+                       $args['timeout'] = 15;
+               }
+
+               while ( $attempt_count < $max_attempts ) {
+                       $response      = wp_remote_request( $url, $args );
+                       $response_code = wp_remote_retrieve_response_code( $response );
+
+                       // This is called before breaking in case a new request is made immediately.
+                       $this->maybe_throttle( $response );
+
+                       if ( in_array( $response_code, $breaking_codes, true ) ) {
+                               break;
+                       }
+
+                       /*
+                        * Sometimes an API inexplicably returns a success code with an empty body, but will return a valid
+                        * response if the exact request is retried.
+                        */
+                       if ( 200 === $response_code && ! empty( wp_remote_retrieve_body( $response ) ) ) {
+                               break;
+                       }
+
+                       $attempt_count++;
+
+                       /**
+                        * Action: Fires when tenacious_remote_request fails a request attempt.
+                        *
+                        * Note that the request parameter includes the request URL which may contain sensitive information such as
+                        * an API key. This should be redacted before outputting anywhere public.
+                        *
+                        * @param array $response
+                        * @param array $request
+                        * @param int   $attempt_count
+                        * @param int   $max_attempts
+                        */
+                       do_action( 'api_client_tenacious_remote_request_attempt', $response, compact( 'url', 'args' ), $attempt_count, $max_attempts );
+
+                       if ( $attempt_count < $max_attempts ) {
+                               $retry_after = wp_remote_retrieve_header( $response, 'retry-after' ) ?: 5;
+                               $wait        = min( $retry_after * $attempt_count, 30 );
+
+                               $this->cli_message( "\nRequest failed $attempt_count times. Pausing for $wait seconds before retrying." );
+
+                               sleep( $wait );
+                       }
+               }
+
+               if ( $attempt_count === $max_attempts && ( 200 !== $response_code || is_wp_error( $response ) ) ) {
+                       $this->cli_message( "\nRequest failed $attempt_count times. Giving up." );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Wrapper method for a request using the GET method.
+        *
+        * @param $url
+        * @param array $args
+        *
+        * @return array|WP_Error
+        */
+       public function tenacious_remote_get( $url, array $args = [] ) {
+               $args['method'] = 'GET';
+
+               return $this->tenacious_remote_request( $url, $args );
+       }
+
+       /**
+        * Wrapper method for a request using the POST method.
+        *
+        * @param $url
+        * @param array $args
+        *
+        * @return array|WP_Error
+        */
+       public function tenacious_remote_post( $url, array $args = [] ) {
+               $args['method'] = 'POST';
+
+               return $this->tenacious_remote_request( $url, $args );
+       }
+
+       /**
+        * Check the rate limit status in an API response and delay further execution if necessary.
+        *
+        * @param array $response
+        */
+       private function maybe_throttle( $response ) {
+               if ( ! is_callable( $this->throttle_callback ) ) {
+                       if ( ! empty( $this->throttle_callback ) ) {
+                               $this->error->add(
+                                       'invalid_throttle_callback',
+                                       'The specified throttle callback is not callable.'
+                               );
+                       }
+
+                       return;
+               }
+
+               call_user_func( $this->throttle_callback, $response );
+       }
+
+       /**
+        * Extract error information from an API response and add it to our error handler.
+        *
+        * This is just a stub. Extending classes should define their own method that handles the error codes and
+        * messages specific to the API they are dealing with.
+        *
+        * @param array|WP_Error $response
+        *
+        * @return bool True if the error was handled.
+        */
+       public function handle_error_response( $response ) {
+               if ( is_wp_error( $response ) ) {
+                       $codes = $response->get_error_codes();
+
+                       foreach ( $codes as $code ) {
+                               $messages = $response->get_error_messages( $code );
+
+                               foreach ( $messages as $message ) {
+                                       $this->error->add( $code, $message );
+                               }
+                       }
+
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Outputs a message when the command is run from the PHP command line.
+        *
+        * @param string $message
+        *
+        * @return void
+        */
+       protected function cli_message( $message ) {
+               if ( 'cli' === php_sapi_name() ) {
+                       echo $message;
+               }
+       }
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclasscurrencyxrtclientphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-currency-xrt-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-currency-xrt-client.php        2018-11-21 18:44:57 UTC (rev 7880)
+++ sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-currency-xrt-client.php  2018-11-22 04:13:15 UTC (rev 7881)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9,6 +9,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Get historical exchange rates for the major currencies. Designed to be able to use different API sources for
</span><span class="cx" style="display: block; padding: 0 10px">  * the exchange rate data. Initially built using the Fixer API (fixer.io). Now includes Open Exchange Rates.
</span><span class="cx" style="display: block; padding: 0 10px">  * "XRT" = "exchange rate".
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ *
+ * TODO Refactor this to use the API_Client base class.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class Currency_XRT_Client {
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassmeetupclientphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-meetup-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-meetup-client.php      2018-11-21 18:44:57 UTC (rev 7880)
+++ sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-meetup-client.php        2018-11-22 04:13:15 UTC (rev 7881)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -7,6 +7,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Class Meetup_Client
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ *
+ * TODO Refactor this to use the API_Client base class.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class Meetup_Client {
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassqboclientphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-qbo-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-qbo-client.php 2018-11-21 18:44:57 UTC (rev 7880)
+++ sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-qbo-client.php   2018-11-22 04:13:15 UTC (rev 7881)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -9,6 +9,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * This is a general purpose client, whereas the one in `plugins/wordcamp-qbo-client` is specific to the WordCamp
</span><span class="cx" style="display: block; padding: 0 10px">  * Payments plugin. Eventually, we should probably merge the two into a single general purpose client, rather
</span><span class="cx" style="display: block; padding: 0 10px">  * than having multiple.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ *
+ * TODO Refactor this to use the API_Client base class.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class QBO_Client {
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentmupluginsutilitiesclassstripeclientphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-stripe-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-stripe-client.php      2018-11-21 18:44:57 UTC (rev 7880)
+++ sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/utilities/class-stripe-client.php        2018-11-22 04:13:15 UTC (rev 7881)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -12,6 +12,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * This is favorable over the official `stripe-php` library, because we don't have to worry about keeping it
</span><span class="cx" style="display: block; padding: 0 10px">  * updated, it only has the functionality we're actually using, we won't end up with unit tests, etc inside
</span><span class="cx" style="display: block; padding: 0 10px">  * a publicly accessible folder, etc.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ *
+ * TODO Refactor this to use the API_Client base class.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class Stripe_Client {
</span><span class="cx" style="display: block; padding: 0 10px">        const API_URL = 'https://api.stripe.com';
</span></span></pre>
</div>
</div>

</body>
</html>