diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..120e0e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +Icon? +Thumbs.db +ehthumbs.db +*.bak +*.swm +*.swn +*.swo +*.swp +*~ +/vendor +/.idea +cache.properties +build +!build/phpcs.xml +!build/phpmd.xml +!build/phpunit.xml +/composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f3b6214 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php + +sudo: required +php: + - 5.4 + +script: phpunit --configuration build/phpunit.xml + +notifications: + email: + recipients: + - travisci@aweber.com diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1ea705 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2010-2013, AWeber Communications, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of AWeber Communications, Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL AWEBER COMMUNICATIONS, INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README b/README deleted file mode 100644 index d6ab588..0000000 --- a/README +++ /dev/null @@ -1,7 +0,0 @@ -AWeber API PHP Library -====================== - -PHP library for easily integrating with the AWeber API. - -For complete documentation: -https://labs.aweber.com/docs/php-library-walkthrough diff --git a/README.md b/README.md new file mode 100644 index 0000000..c09fcd6 --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# This client library has been deprecated. +### This library will no longer be supported and does not support OAuth2. +### Please visit our [PHP Examples](https://github.com/aweber/public-api-examples/tree/master/php) as a reference. +---------------- + +AWeber API PHP Library [![Build Status](https://secure.travis-ci.org/aweber/AWeber-API-PHP-Library.png?branch=master)](http://travis-ci.org/aweber/AWeber-API-PHP-Library) +====================== + +PHP library for easily integrating with the AWeber API. + + +Basic Usage: +------------ +Refer to demo.php to see a working example of how to authenticate an app and query the API. + +For more complete documentation please refer to: +https://labs.aweber.com/docs/php-library-walkthrough + + +Handling Errors: +---------------- +Sometimes errors happen and your application should handle them appropriately. +Whenever an API error occurs an AWeberAPIException will be raised with a detailed +error message and documentation link to explain what is wrong. + +You should wrap any calls to the API in a try/except block. + +Common Errors: + * Bad request (400 error) + * Your application is not authorized (401 error) + * Your application has been rate limited (403 error) + * Resource not found (404 error) + * API Temporarily unavailable (503 error) + +Refer to https://labs.aweber.com/docs/troubleshooting for the complete list + +Example Below: + +```php +getAccount($accessKey, $accessSecret); + +try { + $resource = $account->loadFromUrl('/accounts/idontexist'); +} catch (AWeberAPIException $exc) { + print "
  • $exc->type on $exc->url, refer to $exc->message for more info ...
    "; +} +?> +``` + + +Accessing personally identifiable subscriber data +------------------------------------------------- +In order to view or update the email, name, misc_notes, and ip_address fields of a subscriber, your app must +specifically request access to subscriber data. Refer to our documentation at +https://labs.aweber.com/docs/permissions for more information on how to be able to access personally identifiable +subscriber information. + + +Changelog: +---------- +2017-09-02: v1.1.18 + * Fixing issues on the formatting of nested objects on 'application/json' requests + * Fixing issue with extra data being sent in header. + * Fixing issue to allow the creation of custom fields to be sent as form encoded. + +2017-08-21: v1.1.17 + * Fixing UTF-8 issues on creates + +2016-12-16: v1.1.16 + * Composer autoload with classmap + +2016-11-09: v1.1.15 + * Create AWeberEntry for Broadcast Entry endpoint + +2015-02-17: v1.1.13 + * Remove double encoding in requests to support utf-8 + +2014-02-05: v1.1.12 + * Add composer file. + +2013-04-25: v1.1.11 + * Fixed a bug in the Collection Find Subscriber method where fetching the next page in the collection had not + included the previous search parameters. + + * We've changed how we store collection data internally in AWeberCollection objects to + reduce the amount of memory required for large collections. + + To lower memory usage, the AWeberCollection only stores a single page of entries + as you iterate thru the collection. + + - foreach and sequential array indexing operations now require less memory. + + - Random access of array elements by indexes will fetch pages of the collection + from the API on demand if the collection data is not already in memory. + +2013-02-07: v1.1.10 + * Updated APIUnreachableException to provide more diagnostic data. + +2013-01-03: v1.1.9 + * Updated client library to support 1.0.17 of the API. (Broadcast Statistics) + +2012-12-13: v1.1.8 + * Fixed a bug that resulted in Exceptions being raised when using collections when the collection size is zero. + +2012-12-10: v1.1.7 + * Added a parameter to the Move Subscriber method for last followup message number sent. + * to support version 1.0.16 of the API. See https://labs.aweber.com/docs/changelog + +2012-09-19: v1.1.6 + * Fixed a bug that prevented resource attributes from being saved when the initial value of the resource attribute was null. + * used array_key_exists instead of isset for evaluation of associative arrays. Requires PHP >= 4.0.7 + +2012-07-05: v1.1.5 + * Fixed a bug were a utf8_encode notice was raised when updating subscriber custom field values. + +2012-05-08: v1.1.4 + Some API Developers have reported AWeberOAuthDataMissing exceptions when using the demo.php script. + This error message is not helpful as the typical cause for this exception is an invalid consumer key or secret. + + The client library has been refactored to always raise an AWeberAPIException when a 40x/50x http status code + response is returned. This exception will clearly indicate the cause of the error for easier troubleshooting. + * Refactored makeRequest to always raise an AWeberAPIException when a 40x or 50x status is returned. + * Refactored makeRequest to indicate transient networking or firewall connectivity issues. + * Refactored mock adaptor makeRequest for testing to behave the same way as the real makeRequest does. + +2012-04-18: v1.1.3 + + * Removed usage of deprecated split function. + +2011-12-23: v1.1.2 + + * Fixed a bug in the AWeberCollection class to properly set the URL of entries found in collections. + +2011-10-10: v1.1.1 + + * Raise an E_USER_WARNING instead of a fatal error if multiple instances of the client library are installed. + +2011-08-29: v1.1.0 + + * Modified client library to raise an AWeberAPIException on any API errors (HTTP status >= 400) + * Refactored tests for better code coverage + * Refactored move and create methods to return the resource or raise an AWeberAPIException on error. + * Added getActivity method to a subscriber entry. + + + +Running Tests: +-------------- +Testing the PHP api library requires installation of a few utilities. + +### Requirements ### +[Apache Ant](http://ant.apache.org/) is used to run the build targets in the build.xml file. Get the latest version. + +Setup `/etc/php.ini` configuration file. Make sure `include_path` contains the correct directories.(`/usr/lib/php` on MacOS) Set `date.timezone` to your local timezone. + +### Execute Tests ### +Once the above requirements are installed, make sure to run `composer install`, this will ensure all the test dependencies are installed. + +Run the tests from the base directory using the command: `ant`. + +Individual test can be run by specifying ant targets: `ant phpunit`, `ant phpcs`. diff --git a/aweber_api/aweber.php b/aweber_api/aweber.php new file mode 100644 index 0000000..7fcca54 --- /dev/null +++ b/aweber_api/aweber.php @@ -0,0 +1,292 @@ +baseUri; + } + + public function removeBaseUri($url) { + return str_replace($this->getBaseUri(), '', $url); + } + + public function getAccessTokenUrl() { + return $this->accessTokenUrl; + } + + public function getAuthorizeUrl() { + return $this->authorizeUrl; + } + + public function getRequestTokenUrl() { + return $this->requestTokenUrl; + } + + public function getAuthTokenFromUrl() { return ''; } + public function getUserData() { return ''; } + +} + +/** + * AWeberAPIBase + * + * Base object that all AWeberAPI objects inherit from. Allows specific pieces + * of functionality to be shared across any object in the API, such as the + * ability to introspect the collections map. + * + * @package + * @version $id$ + */ +class AWeberAPIBase { + + /** + * Maintains data about what children collections a given object type + * contains. + */ + static protected $_collectionMap = array( + 'account' => array('lists', 'integrations'), + 'broadcast_campaign' => array('links', 'messages', 'stats'), + 'followup_campaign' => array('links', 'messages', 'stats'), + 'link' => array('clicks'), + 'list' => array('campaigns', 'custom_fields', 'subscribers', + 'web_forms', 'web_form_split_tests'), + 'web_form' => array(), + 'web_form_split_test' => array('components'), + ); + + /** + * loadFromUrl + * + * Creates an object, either collection or entry, based on the given + * URL. + * + * @param mixed $url URL for this request + * @access public + * @return AWeberEntry or AWeberCollection + */ + public function loadFromUrl($url) { + $data = $this->adapter->request('GET', $url); + return $this->readResponse($data, $url); + } + + protected function _cleanUrl($url) { + return str_replace($this->adapter->app->getBaseUri(), '', $url); + } + + /** + * readResponse + * + * Interprets a response, and creates the appropriate object from it. + * @param mixed $response Data returned from a request to the AWeberAPI + * @param mixed $url URL that this data was requested from + * @access protected + * @return mixed + */ + protected function readResponse($response, $url) { + $this->adapter->parseAsError($response); + if (!empty($response['id']) || !empty($response['broadcast_id'])) { + return new AWeberEntry($response, $url, $this->adapter); + } else if (array_key_exists('entries', $response)) { + return new AWeberCollection($response, $url, $this->adapter); + } + return false; + } +} + +/** + * AWeberAPI + * + * Creates a connection to the AWeberAPI for a given consumer application. + * This is generally the starting point for this library. Instances can be + * created directly with consumerKey and consumerSecret. + * @uses AWeberAPIBase + * @package + * @version $id$ + */ +class AWeberAPI extends AWeberAPIBase { + + /** + * @var String Consumer Key + */ + public $consumerKey = false; + + /** + * @var String Consumer Secret + */ + public $consumerSecret = false; + + /** + * @var Object - Populated in setAdapter() + */ + public $adapter = false; + + /** + * Uses the app's authorization code to fetch an access token + * + * @param String Authorization code from authorize app page + */ + public static function getDataFromAweberID($string) { + list($consumerKey, $consumerSecret, $requestToken, $tokenSecret, $verifier) = AWeberAPI::_parseAweberID($string); + + if (!$verifier) { + return null; + } + $aweber = new AweberAPI($consumerKey, $consumerSecret); + $aweber->adapter->user->requestToken = $requestToken; + $aweber->adapter->user->tokenSecret = $tokenSecret; + $aweber->adapter->user->verifier = $verifier; + list($accessToken, $accessSecret) = $aweber->getAccessToken(); + return array($consumerKey, $consumerSecret, $accessToken, $accessSecret); + } + + protected static function _parseAWeberID($string) { + $values = explode('|', $string); + if (count($values) < 5) { + return null; + } + return array_slice($values, 0, 5); + } + + /** + * Sets the consumer key and secret for the API object. The + * key and secret are listed in the My Apps page in the labs.aweber.com + * Control Panel OR, in the case of distributed apps, will be returned + * from the getDataFromAweberID() function + * + * @param String Consumer Key + * @param String Consumer Secret + * @return null + */ + public function __construct($key, $secret) { + // Load key / secret + $this->consumerKey = $key; + $this->consumerSecret = $secret; + + $this->setAdapter(); + } + + /** + * Returns the authorize URL by appending the request + * token to the end of the Authorize URI, if it exists + * + * @return string The Authorization URL + */ + public function getAuthorizeUrl() { + $requestToken = $this->user->requestToken; + return (empty($requestToken)) ? + $this->adapter->app->getAuthorizeUrl() + : + $this->adapter->app->getAuthorizeUrl() . "?oauth_token={$this->user->requestToken}"; + } + + /** + * Sets the adapter for use with the API + */ + public function setAdapter($adapter=null) { + if (empty($adapter)) { + $serviceProvider = new AWeberServiceProvider(); + $adapter = new OAuthApplication($serviceProvider); + $adapter->consumerKey = $this->consumerKey; + $adapter->consumerSecret = $this->consumerSecret; + } + $this->adapter = $adapter; + } + + /** + * Fetches account data for the associated account + * + * @param String Access Token (Only optional/cached if you called getAccessToken() earlier + * on the same page) + * @param String Access Token Secret (Only optional/cached if you called getAccessToken() earlier + * on the same page) + * @return Object AWeberCollection Object with the requested + * account data + */ + public function getAccount($token=false, $secret=false) { + if ($token && $secret) { + $user = new OAuthUser(); + $user->accessToken = $token; + $user->tokenSecret = $secret; + $this->adapter->user = $user; + } + + $body = $this->adapter->request('GET', '/accounts'); + $accounts = $this->readResponse($body, '/accounts'); + return $accounts[0]; + } + + /** + * PHP Automagic + */ + public function __get($item) { + if ($item == 'user') return $this->adapter->user; + trigger_error("Could not find \"{$item}\""); + } + + /** + * Request a request token from AWeber and associate the + * provided $callbackUrl with the new token + * @param String The URL where users should be redirected + * once they authorize your app + * @return Array Contains the request token as the first item + * and the request token secret as the second item of the array + */ + public function getRequestToken($callbackUrl) { + $requestToken = $this->adapter->getRequestToken($callbackUrl); + return array($requestToken, $this->user->tokenSecret); + } + + /** + * Request an access token using the request tokens stored in the + * current user object. You would want to first set the request tokens + * on the user before calling this function via: + * + * $aweber->user->tokenSecret = $_COOKIE['requestTokenSecret']; + * $aweber->user->requestToken = $_GET['oauth_token']; + * $aweber->user->verifier = $_GET['oauth_verifier']; + * + * @return Array Contains the access token as the first item + * and the access token secret as the second item of the array + */ + public function getAccessToken() { + return $this->adapter->getAccessToken(); + } +} + +?> diff --git a/aweber_api/aweber_api.php b/aweber_api/aweber_api.php index ae7c99c..201de64 100644 --- a/aweber_api/aweber_api.php +++ b/aweber_api/aweber_api.php @@ -1,292 +1,8 @@ baseUri; - } - - public function getAccessTokenUrl() { - return $this->accessTokenUrl; - } - - public function getAuthorizeUrl() { - return $this->authorizeUrl; - } - - public function getRequestTokenUrl() { - return $this->requestTokenUrl; - } - - public function getAuthTokenFromUrl() { return ''; } - public function getUserData() { return ''; } - -} - -/** - * AWeberAPIBase - * - * Base object that all AWeberAPI objects inherit from. Allows specific pieces - * of functionality to be shared across any object in the API, such as the - * ability to introspect the collections map. - * - * @package - * @version $id$ - */ -class AWeberAPIBase { - - /** - * Maintains data about what children collections a given object type - * contains. - */ - static protected $_collectionMap = array( - 'account' => array('lists', 'integrations'), - 'broadcast_campaign' => array('links', 'messages'), - 'followup_campaign' => array('links', 'messages'), - 'link' => array('clicks'), - 'list' => array('campaigns', 'subscribers', - 'web_forms', 'web_form_split_tests'), - 'web_form' => array(), - 'web_form_split_test' => array('components'), - ); - - /** - * loadFromUrl - * - * Creates an object, either collection or entry, based on the given - * URL. - * - * @param mixed $url URL for this request - * @access public - * @return AWeberEntry or AWeberCollection - */ - public function loadFromUrl($url) { - try { - $data = $this->adapter->request('GET', $url); - return $this->readResponse($data, $url); - } catch (AWeberException $e) { - return null; - } - } - - protected function _cleanUrl($url) { - return str_replace($this->adapter->app->getBaseUri(), '', $url); - } - - /** - * readResponse - * - * Interprets a response, and creates the appropriate object from it. - * @param mixed $response Data returned from a request to the AWeberAPI - * @param mixed $url URL that this data was requested from - * @access protected - * @return mixed - */ - protected function readResponse($response, $url) { - $this->adapter->parseAsError($response); - if (!empty($response['id'])) { - return new AWeberEntry($response, $url, $this->adapter); - } else if (isset($response['entries'])) { - return new AWeberCollection($response, $url, $this->adapter); - } - return false; - } +if (class_exists('AWeberAPI')) { + trigger_error("Duplicate: Another AWeberAPI client library is already in scope.", E_USER_WARNING); } - -/** - * AWeberAPI - * - * Creates a connection to the AWeberAPI for a given consumer application. - * This is generally the starting point for this library. Instances can be - * created directly with consumerKey and consumerSecret. - * @uses AWeberAPIBase - * @package - * @version $id$ - */ -class AWeberAPI extends AWeberAPIBase { - - /** - * @var String Consumer Key - */ - public $consumerKey = false; - - /** - * @var String Consumer Secret - */ - public $consumerSecret = false; - - /** - * @var Object - Populated in setAdapter() - */ - public $adapter = false; - - /** - * Uses the app's authorization code to fetch an access token - * - * @param String Authorization code from authorize app page - */ - public static function getDataFromAweberID($string) { - list($consumerKey, $consumerSecret, $requestToken, $tokenSecret, $verifier) = AWeberAPI::_parseAweberID($string); - - if (!$verifier) { - return null; - } - $aweber = new AweberAPI($consumerKey, $consumerSecret); - $aweber->adapter->user->requestToken = $requestToken; - $aweber->adapter->user->tokenSecret = $tokenSecret; - $aweber->adapter->user->verifier = $verifier; - list($accessToken, $accessSecret) = $aweber->getAccessToken(); - return array($consumerKey, $consumerSecret, $accessToken, $accessSecret); - } - - protected static function _parseAWeberID($string) { - $values = split('\|', $string); - if (count($values) < 5) { - return null; - } - return array_slice($values, 0, 5); - } - - /** - * Sets the consumer key and secret for the API object. The - * key and secret are listed in the My Apps page in the labs.aweber.com - * Control Panel OR, in the case of distributed apps, will be returned - * from the getDataFromAweberID() function - * - * @param String Consumer Key - * @param String Consumer Secret - * @return null - */ - public function __construct($key, $secret) { - // Load key / secret - $this->consumerKey = $key; - $this->consumerSecret = $secret; - - $this->setAdapter(); - } - - /** - * Returns the authorize URL by appending the request - * token to the end of the Authorize URI, if it exists - * - * @return string The Authorization URL - */ - public function getAuthorizeUrl() { - $requestToken = $this->user->requestToken; - return (empty($requestToken)) ? - $this->adapter->app->getAuthorizeUrl() - : - $this->adapter->app->getAuthorizeUrl() . "?oauth_token={$this->user->requestToken}"; - } - - /** - * Sets the adapter for use with the API - */ - public function setAdapter($adapter=null) { - if (empty($adapter)) { - $serviceProvider = new AWeberServiceProvider(); - $adapter = new OAuthApplication($serviceProvider); - $adapter->consumerKey = $this->consumerKey; - $adapter->consumerSecret = $this->consumerSecret; - } - $this->adapter = $adapter; - } - - /** - * Fetches account data for the associated account - * - * @param String Access Token (Only optional/cached if you called getAccessToken() earlier - * on the same page) - * @param String Access Token Secret (Only optional/cached if you called getAccessToken() earlier - * on the same page) - * @return Object AWeberCollection Object with the requested - * account data - */ - public function getAccount($token=false, $secret=false) { - if ($token && $secret) { - $user = new OAuthUser(); - $user->accessToken = $token; - $user->tokenSecret = $secret; - $this->adapter->user = $user; - } - - $body = $this->adapter->request('GET', '/accounts'); - $accounts = $this->readResponse($body, '/accounts'); - return $accounts[0]; - } - - /** - * PHP Automagic - */ - public function __get($item) { - if ($item == 'user') return $this->adapter->user; - trigger_error("Could not find \"{$item}\""); - } - - /** - * Request a request token from AWeber and associate the - * provided $callbackUrl with the new token - * @param String The URL where users should be redirected - * once they authorize your app - * @return Array Contains the request token as the first item - * and the request token secret as the second item of the array - */ - public function getRequestToken($callbackUrl) { - $requestToken = $this->adapter->getRequestToken($callbackUrl); - return array($requestToken, $this->user->tokenSecret); - } - - /** - * Request an access token using the request tokens stored in the - * current user object. You would want to first set the request tokens - * on the user before calling this function via: - * - * $aweber->user->tokenSecret = $_COOKIE['requestTokenSecret']; - * $aweber->user->requestToken = $_GET['oauth_token']; - * $aweber->user->verifier = $_GET['oauth_verifier']; - * - * @return Array Contains the access token as the first item - * and the access token secret as the second item of the array - */ - public function getAccessToken() { - return $this->adapter->getAccessToken(); - } +else { + require_once('aweber.php'); } - -?> diff --git a/aweber_api/aweber_collection.php b/aweber_api/aweber_collection.php index 3029d5a..0ce6fed 100644 --- a/aweber_api/aweber_collection.php +++ b/aweber_api/aweber_collection.php @@ -2,7 +2,43 @@ class AWeberCollection extends AWeberResponse implements ArrayAccess, Iterator, Countable { protected $pageSize = 100; - protected $_entries = array(); + protected $pageStart = 0; + + protected function _updatePageSize() { + + # grab the url, or prev and next url and pull ws.size from it + $url = $this->url; + if (array_key_exists('next_collection_link', $this->data)) { + $url = $this->data['next_collection_link']; + + } elseif (array_key_exists('prev_collection_link', $this->data)) { + $url = $this->data['prev_collection_link']; + } + + # scan querystring for ws_size + $url_parts = parse_url($url); + + # we have a query string + if (array_key_exists('query', $url_parts)) { + parse_str($url_parts['query'], $params); + + # we have a ws_size + if (array_key_exists('ws_size', $params)) { + + # set pageSize + $this->pageSize = $params['ws_size']; + return; + } + } + + # we dont have one, just count the # of entries + $this->pageSize = count($this->data['entries']); + } + + public function __construct($response, $url, $adapter) { + parent::__construct($response, $url, $adapter); + $this->_updatePageSize(); + } /** * @var array Holds list of keys that are not publicly accessible @@ -22,41 +58,35 @@ class AWeberCollection extends AWeberResponse implements ArrayAccess, Iterator, * @return AWeberEntry */ public function getById($id) { - try { - $data = $this->adapter->request('GET', "{$this->url}/{$id}"); - return $this->_makeEntry($data, $id, "{$this->url}/{$id}"); - } catch (AWeberException $e) { - return null; - } + $data = $this->adapter->request('GET', "{$this->url}/{$id}"); + $url = "{$this->url}/{$id}"; + return new AWeberEntry($data, $url, $this->adapter); } - /** - * _getPageParams + /** getParentEntry * - * Returns an array of GET params used to set the page of a collection - * request - * @param int $start Which entry offset should this page start on? - * @param int $size How many entries should be included in this page? - * @access protected - * @return void + * Gets an entry's parent entry + * Returns NULL if no parent entry */ - protected function _getPageParams($start=0, $size=20) { - if ($start > 0) { - $params = array( - 'ws.start' => $start, - 'ws.size' => $size, - ); - ksort($params); - } else { - $params = array(); + public function getParentEntry(){ + $url_parts = explode('/', $this->url); + $size = count($url_parts); + + # Remove collection id and slash from end of url + $url = substr($this->url, 0, -strlen($url_parts[$size-1])-1); + + try { + $data = $this->adapter->request('GET', $url); + return new AWeberEntry($data, $url, $this->adapter); + } catch (Exception $e) { + return NULL; } - return $params; } /** * _type * - * Interpret what type of resources are held in this collection by + * Interpret what type of resources are held in this collection by * analyzing the URL * * @access protected @@ -69,133 +99,144 @@ protected function _type() { } /** - * _calculatePageSize + * create * - * Calculates the page size of this collection based on the data in the - * next and prev links. + * Invoke the API method to CREATE a new entry resource. * - * @access protected - * @return integer - */ - protected function _calculatePageSize() { - if (isset($this->data['next_collection_link'])) { - $url = $this->data['next_collection_link']; - $urlParts = parse_url($url); - if (empty($urlParts['query'])) return $this->pageSize; - $query = array(); - parse_str($urlParts['query'], $query); - if (empty($query['ws_size'])) return $this->pageSize; - $this->pageSize = $query['ws_size']; - } - return $this->pageSize; - } - - /** - * _loadPageForOffset - * - * Makes a request for an additional page of entries, based on the given - * offset. Calculates the start / size of the page needed to get that - * offset, requests for it, and then merges the data into it internal - * collection of entry data. + * Note: Not all entry resources are eligible to be created, please + * refer to the AWeber API Reference Documentation at + * https://labs.aweber.com/docs/reference/1.0 for more + * details on which entry resources may be created and what + * attributes are required for creating resources. * - * @param mixed $offset The offset requested, 0 to total_size-1 - * @access protected - * @return void + * @access public + * @param params mixed associtative array of key/value pairs. + * @return AWeberEntry(Resource) The new resource created */ - protected function _loadPageForOffset($offset, $attempt=1) { - $this->_calculatePageSize(); - $start = round($offset / $this->pageSize) * $this->pageSize; - $params = $this->_getPageParams($start, $this->pageSize); + public function create($kv_pairs) { + # Create Resource + $params = array_merge(array('ws.op' => 'create'), $kv_pairs); + $headers = $this->_type() == 'custom_fields' ? array() : array('Content-Type: application/json'); + $data = $this->adapter->request('POST', $this->url, $params, array('return' => 'headers'), $headers); - // Loading page - try { - $data = $this->adapter->request('GET', $this->url, $params); - $this->adapter->debug = false; - } - catch (Exception $e) { - if ($attempt < 3) $this->_loadPageForOffset($offset, ++$attempt); - return; - } - $rekeyed = array(); - foreach ($data['entries'] as $key => $entry) { - $rekeyed[$key+$data['start']] = $entry; - } - $this->data['entries'] = array_merge($this->data['entries'], $rekeyed); + # Return new Resource + $url = $data['Location']; + $resource_data = $this->adapter->request('GET', $url); + return new AWeberEntry($resource_data, $url, $this->adapter); } /** - * _getEntry + * find * - * Makes sure that entry offset's page is loaded, then returns it. Returns - * null if the entry can't be loaded, even after requesting the needed - * page. + * Invoke the API 'find' operation on a collection to return a subset + * of that collection. Not all collections support the 'find' operation. + * refer to https://labs.aweber.com/docs/reference/1.0 for more information. * - * @param mixed $offset Offset being requested. - * @access protected - * @return void + * @param mixed $search_data Associative array of key/value pairs used as search filters + * * refer to https://labs.aweber.com/docs/reference/1.0 for a + * complete list of valid search filters. + * * filtering on attributes that require additional permissions to + * display requires an app authorized with those additional permissions. + * @access public + * @return AWeberCollection */ - protected function _getEntry($offset) { - if (empty($this->data['entries'][$offset])) { - $this->_loadPageForOffset($offset); - } - return (empty($this->data['entries'][$offset]))? null : - $this->data['entries'][$offset]; - } + public function find($search_data) { + # invoke find operation + $params = array_merge($search_data, array('ws.op' => 'find')); + $data = $this->adapter->request('GET', $this->url, $params); - /** - * _makeEntry - * - * Creates an entry object from the given entry data. Optionally can take - * the id and URL of the entry, though that data can be infered from the - * context in which _makeEntry is being called. - * - * @param mixed $data Array of data returned from an API request for - * entry, or as part of the entries array in this collection. - * @param mixed $id ID of the entry. (Optional) - * @param mixed $url URL used to retrieve this entry (Optional) - * @access protected - * @return void - */ - protected function _makeEntry($data, $id = false, $url= false) { - if (!$id) { - $id = $data['id']; - } - if (!$url) { - $url = "{$this->url}/{$id}"; - } - return new AWeberEntry($data, $url, $this->adapter); - } + # get total size + $ts_params = array_merge($params, array('ws.show' => 'total_size')); + $total_size = $this->adapter->request('GET', $this->url, $ts_params, array('return' => 'integer')); + $data['total_size'] = $total_size; + # return collection + return $this->readResponse($data, $this->url); + } - /** - * ArrayAccess interface methods + /* + * ArrayAccess Functions * * Allows this object to be accessed via bracket notation (ie $obj[$x]) * http://php.net/manual/en/class.arrayaccess.php */ - public function offsetSet($offset, $value) { } - public function offsetUnset($offset) {} + + public function offsetSet($offset, $value) {} + public function offsetUnset($offset) {} public function offsetExists($offset) { + if ($offset >=0 && $offset < $this->total_size) { return true; } return false; } + protected function _fetchCollectionData($offset) { + + # we dont have a next page, we're done + if (!array_key_exists('next_collection_link', $this->data)) { + return null; + } + + # snag query string args from collection + $parsed = parse_url($this->data['next_collection_link']); + + # parse the query string to get params + $pairs = explode('&', $parsed['query']); + foreach ($pairs as $pair) { + list($key, $val) = explode('=', $pair); + $params[$key] = $val; + } + + # calculate new args + $limit = $params['ws.size']; + $pagination_offset = intval($offset / $limit) * $limit; + $params['ws.start'] = $pagination_offset; + + # fetch data, exclude query string + $url_parts = explode('?', $this->url); + $data = $this->adapter->request('GET', $url_parts[0], $params); + $this->pageStart = $params['ws.start']; + $this->pageSize = $params['ws.size']; + + $collection_data = array('entries', 'next_collection_link', 'prev_collection_link', 'ws.start'); + + foreach ($collection_data as $item) { + if (!array_key_exists($item, $this->data)) { + continue; + } + if (!array_key_exists($item, $data)) { + continue; + } + $this->data[$item] = $data[$item]; + } + } + public function offsetGet($offset) { - if (!$this->offsetExists($offset)) return null; - if (!empty($this->_entries[$offset])) return $this->_entries[$offset]; - $this->_entries[$offset] = $this->_makeEntry($this->_getEntry($offset)); - return $this->_entries[$offset]; + if (!$this->offsetExists($offset)) { + return null; + } + + $limit = $this->pageSize; + $pagination_offset = intval($offset / $limit) * $limit; + + # load collection page if needed + if ($pagination_offset !== $this->pageStart) { + $this->_fetchCollectionData($offset); + } + + $entry = $this->data['entries'][$offset - $pagination_offset]; + + # we have an entry, cast it to an AWeberEntry and return it + $entry_url = $this->adapter->app->removeBaseUri($entry['self_link']); + return new AWeberEntry($entry, $entry_url, $this->adapter); } - /** - * Iterator interface methods - * - * Provides iterator functionality. - * http://php.net/manual/en/class.iterator.php + /* + * Iterator */ protected $_iterationKey = 0; + public function current() { return $this->offsetGet($this->_iterationKey); } @@ -216,14 +257,13 @@ public function valid() { return $this->offsetExists($this->key()); } - /** + /* * Countable interface methods - * * Allows PHP's count() and sizeOf() functions to act on this object * http://www.php.net/manual/en/class.countable.php */ + public function count() { return $this->total_size; } - } diff --git a/aweber_api/aweber_entry.php b/aweber_api/aweber_entry.php index f4c769a..1b01984 100644 --- a/aweber_api/aweber_entry.php +++ b/aweber_api/aweber_entry.php @@ -6,7 +6,6 @@ class AWeberEntry extends AWeberResponse { * @var array Holds list of data keys that are not publicly accessible */ protected $_privateData = array( - 'self_link', 'resource_type_link', 'http_etag', ); @@ -47,18 +46,22 @@ public function attrs() { } /** - * _type + * _type * - * Used to pull the name of this resource from its resource_type_link + * Used to pull the name of this resource from its resource_type_link * @access protected * @return String */ protected function _type() { if (empty($this->type)) { - $typeLink = $this->data['resource_type_link']; - if (empty($typeLink)) return null; - list($url, $type) = explode('#', $typeLink); - $this->type = $type; + if (!empty($this->data['resource_type_link'])) { + list($url, $type) = explode('#', $this->data['resource_type_link']); + $this->type = $type; + } elseif (!empty($this->data['broadcast_id'])) { + $this->type = 'broadcast'; + } else { + return null; + } } return $this->type; } @@ -73,11 +76,43 @@ protected function _type() { * if the delete request failed. */ public function delete() { - $status = $this->adapter->request('DELETE', $this->url, array(), array('return' => 'status')); - if (substr($status, 0, 2) == '20') return true; - return false; + $this->adapter->request('DELETE', $this->url, array(), array('return' => 'status')); + return true; } + /** + * move + * + * Invoke the API method to MOVE an entry resource to a different List. + * + * Note: Not all entry resources are eligible to be moved, please + * refer to the AWeber API Reference Documentation at + * https://labs.aweber.com/docs/reference/1.0 for more + * details on which entry resources may be moved and if there + * are any requirements for moving that resource. + * + * @access public + * @param AWeberEntry(List) List to move Resource (this) too. + * @return mixed AWeberEntry(Resource) Resource created on List ($list) + * or False if resource was not created. + */ + public function move($list, $last_followup_message_number_sent=NULL) { + # Move Resource + $params = array( + 'ws.op' => 'move', + 'list_link' => $list->self_link + ); + if (isset($last_followup_message_number_sent)) { + $params['last_followup_message_number_sent'] = $last_followup_message_number_sent; + } + + $data = $this->adapter->request('POST', $this->url, $params, array('return' => 'headers')); + + # Return new Resource + $url = $data['Location']; + $resource_data = $this->adapter->request('GET', $url); + return new AWeberEntry($resource_data, $url, $this->adapter); + } /** * save @@ -88,10 +123,7 @@ public function delete() { */ public function save() { if (!empty($this->_localDiff)) { - $data = $this->adapter->request('PATCH', $this->url, $this->_localDiff, array('return' => 'status')); - if (substr($data, 0, 2) !== '20') { - return false; - } + $data = $this->adapter->request('PATCH', $this->url, $this->_localDiff, array('return' => 'status'), array('Content-Type: application/json')); } $this->_localDiff = array(); return true; @@ -101,10 +133,10 @@ public function save() { /** * __get * - * Used to look up items in data, and special properties like type and + * Used to look up items in data, and special properties like type and * child collections dynamically. * - * @param String $value Attribute being accessed + * @param String $value Attribute being accessed * @access public * @throws AWeberResourceNotImplemented * @return mixed @@ -138,7 +170,7 @@ public function __get($value) { * @access public */ public function __set($key, $value) { - if (isset($this->data[$key])) { + if (array_key_exists($key, $this->data)) { $this->_localDiff[$key] = $value; return $this->data[$key] = $value; } else { @@ -146,6 +178,72 @@ public function __set($key, $value) { } } + /** + * findSubscribers + * + * Looks through all lists for subscribers + * that match the given filter + * @access public + * @return AWeberCollection + */ + public function findSubscribers($search_data) { + $this->_methodFor(array('account')); + $params = array_merge($search_data, array('ws.op' => 'findSubscribers')); + $data = $this->adapter->request('GET', $this->url, $params); + + $ts_params = array_merge($params, array('ws.show' => 'total_size')); + $total_size = $this->adapter->request('GET', $this->url, $ts_params, array('return' => 'integer')); + + # return collection + $data['total_size'] = $total_size; + $url = $this->url . '?'. http_build_query($params); + return new AWeberCollection($data, $url, $this->adapter); + } + + /** + * getActivity + * + * Returns analytics activity for a given subscriber + * @access public + * @return AWeberCollection + */ + public function getActivity() { + $this->_methodFor(array('subscriber')); + $params = array('ws.op' => 'getActivity'); + $data = $this->adapter->request('GET', $this->url, $params); + + $ts_params = array_merge($params, array('ws.show' => 'total_size')); + $total_size = $this->adapter->request('GET', $this->url, $ts_params, array('return' => 'integer')); + + # return collection + $data['total_size'] = $total_size; + $url = $this->url . '?'. http_build_query($params); + return new AWeberCollection($data, $url, $this->adapter); + } + + /** getParentEntry + * + * Gets an entry's parent entry + * Returns NULL if no parent entry + */ + public function getParentEntry(){ + $url_parts = explode('/', $this->url); + $size = count($url_parts); + + #Remove entry id and slash from end of url + $url = substr($this->url, 0, -strlen($url_parts[$size-1])-1); + + #Remove collection name and slash from end of url + $url = substr($url, 0, -strlen($url_parts[$size-2])-1); + + try { + $data = $this->adapter->request('GET', $url); + return new AWeberEntry($data, $url, $this->adapter); + } catch (Exception $e) { + return NULL; + } + } + /** * getWebForms * @@ -178,11 +276,11 @@ public function getWebFormSplitTests() { /** * _parseNamedOperation * - * Turns a dumb array of json into an array of Entries. This is NOT + * Turns a dumb array of json into an array of Entries. This is NOT * a collection, but simply an array of entries, as returned from a * named operation. * - * @param array $data + * @param array $data * @access protected * @return array */ @@ -190,7 +288,7 @@ protected function _parseNamedOperation($data) { $results = array(); foreach($data as $entryData) { $results[] = new AWeberEntry($entryData, str_replace($this->adapter->app->getBaseUri(), '', - $entryData['self_link']), $this->adapter); + $entryData['self_link']), $this->adapter); } return $results; } @@ -210,7 +308,7 @@ protected function _methodFor($entryTypes) { } /** - * _getCollection + * _getCollection * * Returns the AWeberCollection object representing the given * collection name, relative to this entry. @@ -222,12 +320,7 @@ protected function _methodFor($entryTypes) { protected function _getCollection($value) { if (empty($this->_collections[$value])) { $url = "{$this->url}/{$value}"; - try { - $data = $this->adapter->request('GET', $url); - } - catch (Exception $e) { - $data = array('entries' => array(), 'total_size' => 0, 'start' => 0); - } + $data = $this->adapter->request('GET', $url); $this->_collections[$value] = new AWeberCollection($data, $url, $this->adapter); } return $this->_collections[$value]; @@ -252,5 +345,3 @@ protected function _isChildCollection($value) { } } - -?> diff --git a/aweber_api/aweber_response.php b/aweber_api/aweber_response.php index a688e3f..5d021a2 100644 --- a/aweber_api/aweber_response.php +++ b/aweber_api/aweber_response.php @@ -62,7 +62,7 @@ public function __get($value) { if (in_array($value, $this->_privateData)) { return null; } - if (isset($this->data[$value])) { + if (array_key_exists($value, $this->data)) { return $this->data[$value]; } if ($value == 'type') return $this->_type(); diff --git a/aweber_api/curl_object.php b/aweber_api/curl_object.php new file mode 100644 index 0000000..39783ed --- /dev/null +++ b/aweber_api/curl_object.php @@ -0,0 +1,103 @@ + diff --git a/aweber_api/exceptions.php b/aweber_api/exceptions.php index 954bc45..5e24d89 100644 --- a/aweber_api/exceptions.php +++ b/aweber_api/exceptions.php @@ -2,6 +2,33 @@ class AWeberException extends Exception { } +/** + * Thrown when the API returns an error. (HTTP status >= 400) + * + * + * @uses AWeberException + * @package + * @version $id$ + */ +class AWeberAPIException extends AWeberException { + + public $type; + public $status; + public $message; + public $documentation_url; + public $url; + + public function __construct($error, $url) { + // record specific details of the API exception for processing + $this->url = $url; + $this->type = $error['type']; + $this->status = array_key_exists('status', $error) ? $error['status'] : ''; + $this->message = $error['message']; + $this->documentation_url = $error['documentation_url']; + + parent::__construct($this->message); + } +} /** * Thrown when attempting to use a resource that is not implemented. diff --git a/aweber_api/oauth_adapter.php b/aweber_api/oauth_adapter.php index a6b698a..0b9d454 100644 --- a/aweber_api/oauth_adapter.php +++ b/aweber_api/oauth_adapter.php @@ -2,7 +2,7 @@ interface AWeberOAuthAdapter { - public function request($method, $uri, $data = array()); + public function request($method, $uri, $data = array(), $options = array(), $headers = array()); public function getRequestToken($callbackUrl=false); } diff --git a/aweber_api/oauth_application.php b/aweber_api/oauth_application.php index b2c74f2..35891b6 100644 --- a/aweber_api/oauth_application.php +++ b/aweber_api/oauth_application.php @@ -1,4 +1,5 @@ app = $parentApp; } $this->user = new OAuthUser(); + $this->curl = new CurlObject(); } /** @@ -100,20 +108,32 @@ public function __construct($parentApp = false) { * @param mixed $uri * @param array $data * @param array $options + * @param array $headers * @access public * @return void + * @throws AWeberResponseError */ - public function request($method, $uri, $data = array(), $options = array()) { + public function request($method, $uri, $data = array(), $options = array(), $headers = array()) { + $uri = $this->app->removeBaseUri($uri); $url = $this->app->getBaseUri() . $uri; - $response = $this->makeRequest($method, $url, $data); - if (!$response) { - throw new AWeberResponseError($uri); - } - if (!empty($options['return']) && $options['return'] == 'status') { - return $response->headers['Status-Code']; + + + $response = $this->makeRequest($method, $url, $data, $headers); + if (!empty($options['return'])) { + if ($options['return'] == 'status') { + return $response->headers['Status-Code']; + } + if ($options['return'] == 'headers') { + return $response->headers; + } + if ($options['return'] == 'integer') { + return intval($response->body); + } } + $data = json_decode($response->body, true); - if (empty($options['allow_empty']) && empty($data)) { + + if (empty($options['allow_empty']) && !isset($data)) { throw new AWeberResponseError($uri); } return $data; @@ -144,6 +164,7 @@ public function getRequestToken($callbackUrl=false) { * * @access public * @return void + * @throws AWeberOAuthDataMissing */ public function getAccessToken() { $resp = $this->makeRequest('POST', $this->app->getAccessTokenUrl(), @@ -161,7 +182,6 @@ public function getAccessToken() { return array($data['oauth_token'], $data['oauth_token_secret']); } - /** * parseAsError * @@ -186,10 +206,11 @@ public function parseAsError($response) { * Enforce that all the fields in requiredFields are present and not * empty in data. If a required field is empty, throw an exception. * - * @param mixed $data Array of data - * @param mixed $requiredFields Array of required field names. - * @access protected + * @param mixed $data Array of data + * @param mixed $requiredFields Array of required field names. * @return void + * @throws AWeberOAuthDataMissing + * @access protected */ protected function requiredFromResponse($data, $requiredFields) { foreach ($requiredFields as $field) { @@ -203,25 +224,26 @@ protected function requiredFromResponse($data, $requiredFields) { * get * * Make a get request. Used to exchange user tokens with serice provider. - * @param mixed $url URL to make a get request from. - * @param array $data Data for the request. + * @param mixed $url URL to make a get request from. + * @param String $url_params URL parameter string + * @param mixed $headers Headers for the request * @access protected * @return void */ - protected function get($url, $data) { - $url = $this->_addParametersToUrl($url, $data); - $handle = curl_init($url); - $resp = $this->_sendRequest($handle); + protected function get($url, $url_params, $headers = array()) { + $url = $this->_addParametersToUrl($url, $url_params); + $handle = $this->curl->init($url); + $resp = $this->_sendRequest($handle, $headers); return $resp; } /** * _addParametersToUrl * - * Adds the parameters in associative array $data to the + * Adds the parameters in associative array $data to the * given URL - * @param String $url URL - * @param array $data Parameters to be added as a query string to + * @param String $url URL + * @param String $data Parameters to be added as a query string to * the URL provided * @access protected * @return void @@ -229,9 +251,9 @@ protected function get($url, $data) { protected function _addParametersToUrl($url, $data) { if (!empty($data)) { if (strpos($url, '?') === false) { - $url .= '?'.$this->buildData($data); + $url .= '?' . $data; } else { - $url .= '&'.$this->buildData($data); + $url .= '&' . $data; } } return $url; @@ -289,7 +311,7 @@ public function createSignature($sigBase, $sigKey) { * @return void Encoded data */ protected function encode($data) { - return rawurlencode(utf8_encode($data)); + return rawurlencode($data); } /** @@ -320,7 +342,7 @@ public function getOAuthRequestData() { return array( 'oauth_token' => $token, 'oauth_consumer_key' => $this->consumerKey, - 'oauth_version' => $this->version, + 'oauth_version' => $this->oAuthVersion, 'oauth_timestamp' => $ts, 'oauth_signature_method' => $this->signatureMethod, 'oauth_nonce' => $nonce); @@ -353,15 +375,17 @@ public function createSignatureBase($method, $url, $data) { $method = $this->encode(strtoupper($method)); $query = parse_url($url, PHP_URL_QUERY); if ($query) { - $url = array_shift(split('\?', $url, 2)); - $items = split('&', $query); + $parts = explode('?', $url, 2); + $url = array_shift($parts); + $items = explode('&', $query); foreach ($items as $item) { - list($key, $value) = split('=', $item); - $data[$key] = $value; + list($key, $value) = explode('=', $item); + $data[rawurldecode($key)] = rawurldecode($value); } } $url = $this->encode($url); $data = $this->encode($this->collapseDataForSignature($data)); + return $method.'&'.$url.'&'.$data; } @@ -408,47 +432,83 @@ public function signRequest($method, $url, $data) { * makeRequest * * Public facing function to make a request + * * @param mixed $method - * @param mixed $url - * @param mixed $data + * @param mixed $url - Reserved characters in query params MUST be escaped + * @param mixed $data - Reserved characters in values MUST NOT be escaped + * @param mixed $headers - Reserved characters in values MUST NOT be escaped * @access public * @return void + * + * @throws AWeberAPIException */ - public function makeRequest($method, $url, $data=array()) { - $oauth = $this->prepareRequest($method, $url, $data); + public function makeRequest($method, $url, $data=array(), $headers=array()) { + if ($this->debug) echo "\n** {$method}: $url\n"; - if (strtoupper($method) == 'POST') { - $resp = $this->post($url, $oauth); - } else if (strtoupper($method) == 'DELETE') { - $resp = $this->delete($url, $oauth); - } else if (strtoupper($method) == 'PATCH') { - $oauth = $this->prepareRequest($method, $url, array()); - $resp = $this->patch($url, $oauth, $data); - } else { - $resp = $this->get($url, $oauth, $data); + + list($urlParams, $requestBody) = $this->formatRequestData($method, $url, $data, $headers); + + switch (strtoupper($method)) { + case 'POST': + $resp = $this->post($url, $urlParams, $requestBody, $headers); + break; + + case 'GET': + $resp = $this->get($url, $urlParams, $headers); + break; + + case 'DELETE': + $resp = $this->delete($url, $urlParams, $headers); + break; + + case 'PATCH': + $headers = $this->_ensureContentType($headers, 'application/json'); + $resp = $this->patch($url, $urlParams, $requestBody, $headers); + break; + } + + // enable debug output + if ($this->debug) { + echo "
    ";
    +            print_r($oauth);
    +            echo " --> Status: {$resp->headers['Status-Code']}\n";
    +            echo " --> Body: {$resp->body}";
    +            echo "
    "; + } + + if (!$resp) { + $msg = 'Unable to connect to the AWeber API. (' . $this->error . ')'; + $error = array('message' => $msg, 'type' => 'APIUnreachableError', + 'documentation_url' => 'https://labs.aweber.com/docs/troubleshooting'); + throw new AWeberAPIException($error, $url); + } + + if($resp->headers['Status-Code'] >= 400) { + $data = json_decode($resp->body, true); + throw new AWeberAPIException($data['error'], $url); } - if ($this->debug) print_r($oauth); - if ($this->debug) echo " --> Status: {$resp->headers['Status-Code']}\n"; - if ($this->debug) echo " --> Body: {$resp->body}"; + return $resp; } /** - * put + * patch * - * Prepare an OAuth put method. + * Prepare an OAuth patch method. * - * @param mixed $url URL where we are making the request to - * @param mixed $data Data that is used to make the request - * @access private + * @param mixed $url URL where we are making the request to + * @param mixed $url_params URL parameter string + * @param mixed $post_field Data that is used to make the request + * @param mixed $headers Headers for the request + * @access protected * @return void */ - protected function patch($url, $oauth, $data) { - $url = $this->_addParametersToUrl($url, $oauth); - $handle = curl_init($url); - curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'PATCH'); - curl_setopt($handle, CURLOPT_POSTFIELDS, json_encode($data)); - $resp = $this->_sendRequest($handle); + protected function patch($url, $url_params, $post_field, $headers = array()) { + $url = $this->_addParametersToUrl($url, $url_params); + $handle = $this->curl->init($url); + $this->curl->setopt($handle, CURLOPT_CUSTOMREQUEST, 'PATCH'); + $this->curl->setopt($handle, CURLOPT_POSTFIELDS, $post_field); + $resp = $this->_sendRequest($handle, $headers); return $resp; } @@ -457,17 +517,19 @@ protected function patch($url, $oauth, $data) { * * Prepare an OAuth post method. * - * @param mixed $url URL where we are making the request to - * @param mixed $data Data that is used to make the request - * @access private + * @param mixed $url URL where we are making the request to + * @param mixed $url_params URL parameter string + * @param mixed $post_field Data that is used to make the request + * @param mixed $headers Headers for the request + * @access protected * @return void */ - protected function post($url, $oauth) { - $handle = curl_init($url); - $postData = $this->buildData($oauth); - curl_setopt($handle, CURLOPT_POST, true); - curl_setopt($handle, CURLOPT_POSTFIELDS, $postData); - $resp = $this->_sendRequest($handle); + protected function post($url, $url_params, $post_field, $headers = array()) { + $url = $this->_addParametersToUrl($url, $url_params); + $handle = $this->curl->init($url); + $this->curl->setopt($handle, CURLOPT_POST, true); + $this->curl->setopt($handle, CURLOPT_POSTFIELDS, $post_field); + $resp = $this->_sendRequest($handle, $headers); return $resp; } @@ -476,15 +538,16 @@ protected function post($url, $oauth) { * * Makes a DELETE request * @param mixed $url URL where we are making the request to - * @param mixed $data Data that is used in the request + * @param mixed $url_params URL parameter string + * @param mixed $headers Headers for the request * @access protected * @return void */ - protected function delete($url, $data) { - $url = $this->_addParametersToUrl($url, $data); - $handle = curl_init($url); - curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'DELETE'); - $resp = $this->_sendRequest($handle); + protected function delete($url, $url_params, $headers = array()) { + $url = $this->_addParametersToUrl($url, $url_params); + $handle = $this->curl->init($url); + $this->curl->setopt($handle, CURLOPT_CUSTOMREQUEST, 'DELETE'); + $resp = $this->_sendRequest($handle, $headers); return $resp; } @@ -509,24 +572,26 @@ public function buildData($data) { * _sendRequest * * Actually makes a request. - * @param mixed $handle + * @param mixed $handle Curl handle + * @param array $headers Additional headers needed for request * @access private * @return void */ - private function _sendRequest($handle) { - curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); - curl_setopt($handle, CURLOPT_HEADER, true); - curl_setopt($handle, CURLOPT_HTTPHEADER, array('Expect:', 'Content-Type: application/json')); - curl_setopt($handle, CURLOPT_USERAGENT, $this->userAgent); - curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, FALSE); - curl_setopt($handle, CURLOPT_VERBOSE, FALSE); - curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($handle, CURLOPT_TIMEOUT, 90); - $resp = curl_exec($handle); + private function _sendRequest($handle, $headers = array()) { + $this->curl->setopt($handle, CURLOPT_RETURNTRANSFER, true); + $this->curl->setopt($handle, CURLOPT_HEADER, true); + $this->curl->setopt($handle, CURLOPT_HTTPHEADER, $headers); + $this->curl->setopt($handle, CURLOPT_USERAGENT, $this->userAgent()); + $this->curl->setopt($handle, CURLOPT_SSL_VERIFYPEER, FALSE); + $this->curl->setopt($handle, CURLOPT_VERBOSE, FALSE); + $this->curl->setopt($handle, CURLOPT_CONNECTTIMEOUT, 10); + $this->curl->setopt($handle, CURLOPT_TIMEOUT, 90); + $resp = $this->curl->execute($handle); if ($resp) { return new CurlResponse($resp); } - $this->error = curl_errno($handle).' - '.curl_error($handle); + $this->error = $this->curl->errno($handle) . ' - ' . + $this->curl->error($handle); return false; } @@ -571,6 +636,74 @@ public function parseResponse($resp) { return $data; } + /** + * userAgent + * + * Generates the user agent for the cURL command + * + * @return string + */ + protected function userAgent() { + return $this->userAgentTitle . $this->clientVersion . ' PHP/' . PHP_VERSION . ' ' . php_uname('m') . '-' . strtolower(php_uname('s')) . '-'. php_uname('r'); + } + + /** + * @param $method + * @param $headers + * @return bool + * + * Return True if headers array does not contain 'Content-Type: application/json' and is a POST, GET, or DELETE request + */ + protected function needsUrlFormatting($method, $headers) { + return !in_array("Content-Type: application/json", $headers) && in_array($method, array('POST', 'GET', 'DELETE')); + } + + /** + * @param $method + * @param $url + * @param $data + * @param $headers + * @return array + */ + protected function formatRequestData($method, $url, $data, $headers) + { + # WARNING: If not being sent as json, non-primitive items in data must be json serialized in GET and POST. + if ($this->needsUrlFormatting($method, $headers)) { + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = json_encode($value); + } + } + $urlParams = $this->buildData($this->prepareRequest($method, $url, $data)); + $requestBody = $this->buildData($data); + } else { + $urlParams = $this->buildData($this->prepareRequest($method, $url, array())); + $requestBody = json_encode($data); + } + return array($urlParams, $requestBody); + } + + /** + * Checks the $headers array for content-type and adds the header if it doesn't exist and replaces it if isn't + * what is passed. + * + * @param $headers + * @param $expectedContentType + * @return array + */ + private function _ensureContentType($headers, $expectedContentType) { + + foreach ($headers as $key => $value) { + if ( stripos($value, 'content-type:') !== false ) { + unset($headers[$key]); + } + + } + + $headers[] = 'Content-Type: ' . $expectedContentType; + return $headers; + } + } /** diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..54705e7 --- /dev/null +++ b/build.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/phpcs.xml b/build/phpcs.xml new file mode 100644 index 0000000..77ff2c8 --- /dev/null +++ b/build/phpcs.xml @@ -0,0 +1,5 @@ + + + A custom coding standard. + + diff --git a/build/phpmd.xml b/build/phpmd.xml new file mode 100644 index 0000000..2ad0db5 --- /dev/null +++ b/build/phpmd.xml @@ -0,0 +1,25 @@ + + + + My custom rule set that checks my code... + + + + + + + + + + + + + + + diff --git a/build/phpunit.xml b/build/phpunit.xml new file mode 100644 index 0000000..3a05bd0 --- /dev/null +++ b/build/phpunit.xml @@ -0,0 +1,14 @@ + + + + + + + + + ../tests/ + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..99bec3f --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "aweber/aweber", + "description": "The official AWeber API client library.", + "homepage": "https://www.aweber.com", + "license": "GPL-2.0", + "keywords": [ + "email", + "api", + "client" + ], + "support": { + "email": "api@aweber.com", + "source": "https://github.com/aweber/AWeber-API-PHP-Library", + "issues": "https://github.com/aweber/AWeber-API-PHP-Library" + }, + "autoload": { + "classmap": [ + "aweber_api/" + ] + }, + "require-dev": { + "phpdocumentor/phpdocumentor": "^2.9", + "pdepend/pdepend": "^2.2", + "phpmd/phpmd": "^2.4", + "bamboohr/phpcs": "^0.1.4", + "sebastian/phpcpd": "^2.0", + "phploc/phploc": "^3.0", + "squizlabs/php_codesniffer": "^2.7", + "php-di/phpdoc-reader": "^2.0", + "mayflower/php-codebrowser": "^1.1", + "phpunit/phpunit": "^5.6" + } +} diff --git a/aweber_api/demo.php b/demo.php similarity index 88% rename from aweber_api/demo.php rename to demo.php index 1516c18..9841c7e 100644 --- a/aweber_api/demo.php +++ b/demo.php @@ -1,9 +1,10 @@ adapter->debug = true; + +# set this to true to view the actual api request and response +$aweber->adapter->debug = false; + $account = $aweber->getAccount($_COOKIE['accessToken'], $_COOKIE['accessTokenSecret']); -$account->loadFromUrl('/accounts/326084?ws.op=getWebForms'); + ?> @@ -52,7 +56,7 @@ ?> subject; ?> - sent_date)); ?> + sent_at)); ?>