diff --git a/README.md b/README.md index c70742b..de47fe0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,242 @@ -This is a sample library for connecting to the trackvia api. This is a sample implementation and there is no guarantee that it is a complete implementation. +Trackvia API +============ -The specific use case for this implematation is working directly with JSON input and output. +PHP Library +----------- -It is recommended that you use Mavan to import the depencies required to use this code base. +This library requires that you are at least using PHP 5.3 + +**Code examples are available in example.php** + +The Trackvia API uses OAuth2 for authentication. This PHP library minimizes the work you need to do for OAuth2. Just provide the proper credentials and token data when needed and the library will handle the authentication for you. + +### Authentication flow (if you're interested): + +- Request an access token on behalf of the user. Access tokens are unique per user. You will need the user's credentials to request the first access token. +- You will get back an 'access\_token', 'expires\_at' timestamp, and a 'refresh\_token'. Save this information. +- Now you can make an API request using the access token. +- When the access token expires (in 24 hours), you can request a new one using the refresh token. +- Once the refresh token expires (in 7 days), you go back to the first step of using user credentials to get an access token. + +The code +======== + +### First, create a new Api object using your client credentials. + +``` +$tv = new Api(array( + 'client_id' => 'your_client_id', + 'client_secret' => 'your_client_secret' + + // You can optionally pass in the user credentials here too + // 'username' => 'sample_username', + // 'password' => 'sample_password' +)); +``` + +Your client ID and secret are specific to your account and are accessible from within your Trackvia account settings. You must always set the client_id and secret so that the library can automatically request a new token for you. + +### Add a listener function + +It is up to you to save the access token when it is obtained automatically. The Api class will trigger an event when a new Access Token has been acquired. You can attach a closure function to this event that handles saving the token data to your database. + +``` +$tv->on('new_token', function ($tokenData, $extraParams) { + $accessToken = $tokenData['access_token']; + $refreshToken = $tokenData['refresh_token']; + + // timestamp when the access token expires + $expiresAt = $tokenData['expires_at']; + + //============ + // code to save token data to your database + //============ + +}, $extraParams); +``` + +The important pieces of data to save are 'access\_token', 'refresh\_token', and 'expires\_at'. + +`$extraParams` is an optional array of values that can be attached with the listener function. This array will then be passed into the function when it is called. + +### No access token yet? + +If you don't have an access token yet, then you need to set the user's credentials (username, password) before making API requests. + +``` +$tv->setUserCreds('sample_username', 'sample_password'); +``` + +### Already have an access token? + +If you are loading a saved token, set it before making any requests + +``` +$tv->setTokenData(array( + 'access_token' => $savedToken['access_token'], + 'refresh_token' => $savedToken['refresh_token'], + 'expires_at' => $savedToken['expires_at'] +)); +``` + +### Now make an API request + +When a request is successful (200 OK), data will be returned in array format. + +Any errors returned from the API server will be thrown as PHP exceptions. + +``` +//********************* +// Get a list of dashboards +//********************* +$dashboards = $tv->getDashboards(); + +//********************* +// Get a single dashboard by dashboard_id +//********************* +$dashboards = $tv->getDashboard(2); + + +//********************* +// Get a list of all apps accessible by the user +//********************* +$apps = $tv->getApps(); + + +//********************* +// Get app data for a specific app_id +//********************* +$app = $tv->getApp(22087); + + +//********************* +// Get data for a table +//********************* +$table = $tv->getTable(62995); + + +//********************* +// Get a list of forms for a table +//********************* +$forms = $tv->getForms(62995); + +//********************* +// Get data for a specific form +//********************* +$form = $tv->getForm(3); + + +//********************* +// Get data for a view +//********************* +$view = $tv->getView(289592); + + +//************** +// Get a record +//************** +$record = $tv->getRecord(77113576); + + +//****************** +// Add a new record +//****************** +$tableId = 62994; +$record = array( + 'Times Visited' => 1, + 'City' => 'Denver', + 'State' => 'CO', + 'Menu Link' => 'http://www.google.com', + 'Restaurant' => 'Fake Restaurant 1', + 'Address' => '1555 Blake Street', + 'Hours' => '11am - 11pm', + 'Phone Number' => '720.524.4345', + 'Cuisine' => 'Burgers', + 'Notes' => 'Order by phone. No delivery.', + 'Zip' => '80202' +); +$tv->addRecord($tableId, $record); + + +//********************** +// Add multiple records +// (batch inserts) +//********************** +$tableId = 62994; +$records = array(); +// 1st record +$records[] = array( + 'Times Visited' => 1, + 'City' => 'Denver', + 'State' => 'CO', + 'Restaurant' => 'Fake Restaurant 2', + 'Address' => '1555 Blake Street', + 'Phone Number' => '720.524.4345', + 'Cuisine' => 'Burgers', + 'Notes' => 'Order by phone. No delivery.', + 'Zip' => '80202' +); +// 2nd record +$records[] = array( + 'Times Visited' => 3, + 'City' => 'Denver', + 'State' => 'CO', + 'Restaurant' => 'Fake Restaurant 3', + 'Address' => '1555 Blake Street', + 'Phone Number' => '720.524.4345', + 'Cuisine' => 'Burgers', + 'Notes' => 'Order by phone. No delivery.', + 'Zip' => '80202' +); +$tv->addRecords($tableId, $records); + + +//************************ +// Update a single record +//************************ +$recordId = 105973377; +$record = array( + 'Times Visited' => 5, + 'Restaurant' => 'Fake Restaurant Updated!!' +); +$tv->updateRecord($recordId, $record); + + +//************************ +// Update multiple records +// (batch updates) +//************************ +$tableId = 62994; +$records = array(); +// 1st record +$records[] = array( + 'id' => 105976918, // record id + 'fields' => array( + 'Times Visited' => 12, + 'Restaurant' => 'Fake Restaurant Updated2!!' + ) +); +// 2nd record +$records[] = array( + 'id' => 105976919, // record id + 'fields' => array( + 'Times Visited' => 13, + 'Restaurant' => 'Fake Restaurant Updated3!!' + ) +); +$tv->updateRecords($tableId, $records); + + +//************************ +// Delete a single record +//************************ +$tv->deleteRecord(105973364); + + +//************************ +// Delete a multiple records +// (batch delete) +//************************ +$tableId = 62994; +$tv->deleteRecords($tableId, array(105976918, 105976919)); +``` diff --git a/Tests/ApiTest.php b/Tests/ApiTest.php new file mode 100644 index 0000000..abb68f2 --- /dev/null +++ b/Tests/ApiTest.php @@ -0,0 +1,22 @@ + '13_2s6wg16cwtk48kcgggo8kcgow44w0k8k4800ssw4oss0coc0g8', + 'client_secret' => '4htbckzh1qm8wo4s88gw44g8gs80g00so0sg0kw8kkoccco8gg' + ); + $this->api = new Trackvia\Api($params); + } + + public function testAuthenticate() + { + $this->assertNotEmpty($this->api); + } +} \ No newline at end of file diff --git a/Tests/AuthenticationTest.php b/Tests/AuthenticationTest.php new file mode 100644 index 0000000..c3dcc64 --- /dev/null +++ b/Tests/AuthenticationTest.php @@ -0,0 +1,135 @@ + '13_2s6wg16cwtk48kcgggo8kcgow44w0k8k4800ssw4oss0coc0g8', + 'client_secret' => '4htbckzh1qm8wo4s88gw44g8gs80g00so0sg0kw8kkoccco8gg' + ); + $this->request = new Trackvia\Request(); + $this->auth = new Trackvia\Authentication($this->request, $params); + + $this->setMockTokenData(); + } + + protected function setMockTokenData($expiresAt = null) + { + $this->auth->setTokenData(array( + 'access_token' => 'zH_4mT6c71qn8fD5_zIKabURN7jBj0oKvz19OUvVb5I', + 'refresh_token' => 'v5G2ZFOIx3YM0bpggN1MP_WwhVleo4zvpY2A8kEzq30', + 'expires_at' => ($expiresAt != null ? $expiresAt : (time() + 10000)) + )); + } + + public function testSetUserCreds() + { + $this->assertFalse($this->auth->hasUserCreds()); + + $this->auth->setUserCreds('testuser', 'testpassword'); + + $this->assertTrue($this->auth->hasUserCreds()); + } + + public function testClearAccessToken() + { + $this->assertTrue($this->auth->hasAccessToken()); + + $this->auth->clearAccessToken(); + + $this->assertFalse($this->auth->hasAccessToken()); + } + + public function testClearAllTokens() + { + $auth = $this->auth; + $this->assertNotEmpty($auth->getTokenData()); + + $auth->clearAllTokens(); + + $this->assertEmpty($auth->getTokenData()); + } + + public function testSetTokenData() + { + $auth = $this->auth; + $auth->clearAllTokens(); + + // verify there is no token data first + $this->assertEmpty($auth->getTokenData()); + $this->assertEmpty($auth->getAccessToken()); + $this->assertFalse($auth->hasAccessToken()); + $this->assertFalse($auth->hasRefreshToken()); + $this->assertEmpty($auth->getRefreshToken()); + + $this->setMockTokenData(); + + $this->assertNotEmpty($auth->getTokenData()); + $this->assertNotEmpty($auth->getAccessToken()); + $this->assertTrue($auth->hasAccessToken()); + $this->assertTrue($auth->hasRefreshToken()); + $this->assertNotEmpty($auth->getRefreshToken()); + } + + public function testIsAccessTokenExpired() + { + // expiresAt should already be set ahead from now + $this->assertFalse($this->auth->isAccessTokenExpired()); + $this->setMockTokenData(time() - 10000); + $this->assertTrue($this->auth->isAccessTokenExpired()); + } + + public function testAuthenticateWithExistingToken() + { + $this->assertTrue($this->auth->authenticate()); + } + + public function testBadCredentialsException() + { + $auth = $this->auth; + $this->auth->clearAllTokens(); + + $this->auth->setUserCreds('api.tester', 'fake_password'); + + try { + $auth->authenticate(); + } catch(\Exception $e) { + $this->assertEquals($e->getMessage(), 'invalid_grant'); + } + } + + public function testAuthenticateWithUserCreds() + { + $auth = $this->auth; + $this->auth->clearAllTokens(); + + // auth should fail with no token or user creds + $this->assertFalse($auth->authenticate()); + $this->assertEmpty($auth->getTokenData()); + + $this->auth->setUserCreds('api.tester', 'co3823se'); + $response = $this->auth->authenticate(); + + // make sure we have some token data now + $this->assertNotEmpty($auth->getTokenData()); + + return $response; + } + + /** + * @depends testAuthenticateWithUserCreds + */ + public function testAuthenticateWithRefreshToken($tokenData) + { + $this->assertNotEmpty($tokenData['refresh_token']); + $this->auth->setTokenData($tokenData); + $this->auth->clearAccessToken(); + + $response = $this->auth->authenticate(); + $this->assertTrue($this->auth->hasAccessToken()); + } +} \ No newline at end of file diff --git a/Trackvia/Api.php b/Trackvia/Api.php new file mode 100755 index 0000000..2cb9886 --- /dev/null +++ b/Trackvia/Api.php @@ -0,0 +1,437 @@ +request = new Request(); + $this->auth = new Authentication($this->request, $params); + + // add an event listener for a new token on the authentication object + $this->auth->on('new_access_token', array($this, 'onNewAccessToken')); + } + + /** + * Authenticate the user with OAuth2 + * @return array Access token data + */ + public function authenticate() + { + return $this->auth->authenticate(); + } + + /** + * Method to handle the new_token even trigger by the authentication class + * Bubble up the event with token data for the client. + */ + public function onNewAccessToken($data) + { + $this->trigger('new_token', $data); + } + + /** + * Set the token data for authentication + * @param array $tokenData + */ + public function setTokenData($tokenData) + { + $this->auth->setTokenData($tokenData); + } + + public function setUserCredentials($username, $password) + { + $this->auth->setUserCreds($username, $password); + } + + public function getAuthentication() + { + return $this->auth; + } + + public function getRequest() + { + return $this->request; + } + + /** + * Check if the response failed and if the token is expired. + * + * Any errors returned from the API server will be thrown as an Exception. + * + * @param array $response + * @return boolean + */ + private function checkResponse() + { + $response = $this->request->getResponse(); + if (is_array($response) && isset($response['error_description'])) { + switch ($response['error_description']) { + case self::EXPIRED_ACCESS_TOKEN: + $this->isTokenExpired = true; + // return here so we don't throw this error + // so we can use the refresh token + return false; + } + + // throw an \Exception with the returned error message + throw new \Exception('API Error :: ' . $response['error_description']); + } + + return true; + } + + /** + * Make an api request. + * + * @param string $url + * @param string $httpMethod The http method to use with this request + * @param string $data Optional data to send with request + * @param string $contentType + * @return array The json parsed response from the server + */ + private function api($url, $httpMethod = 'GET', $data = null, $contentType = null) + { + // trigger an event + $this->trigger('api_request_init', array('url' => $url)); + + $this->authenticate(); + + $accessToken = $this->auth->getAccessToken(); + if (!$accessToken) { + // should have a token at this point + // if not, something went wrong + throw new \Exception('Cannot make an api request without an access token'); + } + + // save this request in case we need to use the refresh token + $lastRequest = array( + 'url' => $url, + 'method' => $httpMethod, + 'data' => $data + ); + + // add the access token onto the url + $url = $url . '?access_token='.$accessToken; + + $this->trigger('api_request_send', array('url' => $url, 'http_method' => $httpMethod, 'data' => $data)); + + $this->request + ->setMethod($httpMethod) + ->setData($data); + + if ($contentType) { + $this->request->setContentType("application/$contentType"); + } + // now send the request + $this->request->send($url); + + $this->trigger('api_request_complete', array('url' => $url, 'response' => $this->request->getResponse())); + + // check the response for any errors + $vaild = $this->checkResponse(); + + if (!$vaild && $this->isTokenExpired) { + // blow out the current token so a new one gets requested + $this->auth->clearAccessToken(); + + // redo the last api request + $this->api( + $lastRequest['url'], + $lastRequest['method'], + $lastRequest['data'] + ); + } + + return $this->request->getResponse(); + } + + /** + * Get a list of all dashboards, or a single dashboard. + * + * Optional id parameter lets you specify one dashboard if you want. + * Leave is empty to get all dashboards back. + * + * @param int $id + * @return array Array of dashboard data returned from the api + */ + public function getDashboards($id = null) + { + // build the url + $url = self::BASE_URL . self::DASHBOARDS_URL . ($id ? '/'.$id : ''); + + return $this->api($url, 'GET'); + } + + /** + * Get data for a single dashboard. + * + * @param int $id + * @return array + */ + public function getDashboard($id) + { + return $this->getDashboards($id); + } + + /** + * Get a list of all apps, or a single app. + * + * Optional app_id parameter lets you specify one app if you want. + * Leave is empty to get all apps back. + * + * @param int $appId + * @return array Array of app data returned from the api + */ + public function getApps($appId = null) + { + // build the url + $url = self::BASE_URL . self::APPS_URL . ($appId ? '/'.$appId : ''); + + return $this->api($url, 'GET'); + } + + /** + * Get data for a single app by app_id. + * This will provide you with all the tables available for this app. + * + * @param int $appId + * @return array Array of app data returned fromt the api + */ + public function getApp($appId) + { + return $this->getApps($appId); + } + + /** + * Get table data back for a table_id. + * This will provide you all the views available for this table. + * + * @param int $tableId + * @return array Array of table data returned from the api + */ + public function getTable($tableId) + { + // build the url + $url = self::BASE_URL . self::TABLES_URL .'/'. $tableId; + + return $this->api($url, 'GET'); + } + + public function getTableForeignKeyValues($tableId, $fkId) + { + $url = self::BASE_URL . self::TABLES_URL .'/'. $tableId . '/foreign_keys/' . $fkId; + return $this->api($url, 'GET'); + } + + /** + * Get view data back for a view_id. + * This will provide you with all the records under this view. + * + * @param int $viewId + * @return array Array of view data returned from the api + */ + public function getView($viewId) + { + // build the url + $url = self::BASE_URL . self::VIEWS_URL .'/'. $viewId; + + return $this->api($url, 'GET'); + } + + /** + * Get Record data back for a record_id. + * This will provide you with all the column data for a record. + * + * @param int $id + * @return array Array of Record data returned from the api + */ + public function getRecord($id) + { + // build the url + $url = self::BASE_URL . self::RECORDS_URL .'/'. $id; + + return $this->api($url, 'GET'); + } + + /** + * Add a new record to a table + * @param int $id + * @param array $record + * @return array + */ + public function addRecord($id, $record) + { + $url = self::BASE_URL . self::RECORDS_URL; + $data = array( + 'table_id' => $id, + 'records' => array($record) + ); + return $this->api($url, 'POST', json_encode($data), 'json'); + } + + /** + * Add more than one record at once to a table. Batch inserts. + * + * @param int $tableId + * @param array $records + * @return array + */ + public function addRecords($tableId, $records) + { + $url = self::BASE_URL . self::RECORDS_URL; + $data = array( + 'table_id' => $tableId, + 'records' => $records + ); + return $this->api($url, 'POST', json_encode($data), 'json'); + } + + /** + * Update a single record. + * + * @param int $id + * @param array $data + * @return array + */ + public function updateRecord($id, $data) + { + $url = self::BASE_URL . self::RECORDS_URL .'/'. $id; + return $this->api($url, 'PUT', json_encode($data), 'json'); + } + + /** + * Update multiple records. + * + * @param int $tableId + * @param array $records + * @return array + */ + public function updateRecords($tableId, $records) + { + $url = self::BASE_URL . self::RECORDS_URL; + $data = array( + 'table_id' => $tableId, + 'records' => $records + ); + return $this->api($url, 'PUT', json_encode($data), 'json'); + } + + /** + * Delete a record by id. + * + * @param int $id + */ + public function deleteRecord($id) + { + $url = self::BASE_URL . self::RECORDS_URL .'/'. $id; + return $this->api($url, 'DELETE'); + } + + /** + * Delete multiple records at once. Batch delete. + * + * @param int $tableId + * @param array $records + */ + public function deleteRecords($tableId, $records) + { + $url = self::BASE_URL . self::RECORDS_URL; + $data = array( + 'table_id' => $tableId, + 'records' => $records + ); + return $this->api($url, 'DELETE', json_encode($data), 'json'); + } + + /** + * Get the forms for a table. + * @param int $tableId The id of the table + * @return array + */ + public function getForms($tableId) + { + $url = self::BASE_URL . self::TABLES_URL .'/'. $tableId .'/'. self::FORMS_URL; + + return $this->api($url, 'GET'); + } + + /** + * Get data for a specific form + * @param int $formId The id of the form + * @return array + */ + public function getForm($formId) + { + $url = self::BASE_URL . self::FORMS_URL .'/'. $formId; + + return $this->api($url, 'GET'); + } + + /** + * Search for records on a table. + * + * @param int $tableId + * @param string $term + * @param int $viewId [optional] + * @param string $method (POST|GET) + * @return array + */ + public function search($tableId, $term, $viewId = null, $method = 'GET') + { + if (is_null($viewId)) { + $url = self::BASE_URL . self::SEARCH_URL .'/'. $tableId .'/'. urlencode($term); + } else { + $url = self::BASE_URL . self::SEARCH_URL .'/'. $tableId .'/'.$viewId.'/'. urlencode($term); + } + return $this->api($url, $method); + } + + +} diff --git a/Trackvia/Authentication.php b/Trackvia/Authentication.php new file mode 100755 index 0000000..e5a822b --- /dev/null +++ b/Trackvia/Authentication.php @@ -0,0 +1,391 @@ +request = $request; + + if (!is_array($params)) { + throw new \Exception('You must pass in your client_id and client_secrect'); + } + if (!isset($params['client_id'])) { + throw new \Exception('No client_id provided. This is required.'); + } + if (!isset($params['client_secret'])) { + throw new \Exception('No client_secrect provided. This is required.'); + } + + $this->clientId = $params['client_id']; + $this->clientSecret = $params['client_secret']; + + if (isset($params['username'])) { + // user credentials flow is being used + $this->setUserCreds($params['username'], $params['password']); + } + } + + /** + * Set the user credentials to use for authentication + * @param string $username + * @param string $password + */ + public function setUserCreds($username, $password) + { + $this->userCreds = array( + 'username' => $username, + 'password' => $password + ); + } + + /** + * Whether or not user creds are provided + * @return boolean + */ + public function hasUserCreds() + { + return ( + !empty($this->userCreds) && + isset($this->userCreds['username']) && + isset($this->userCreds['password']) + ); + } + + /** + * Set the token data to use for authentication. + * @param array $params + */ + public function setTokenData($params) + { + $this->tokenData = $params; + } + + /** + * Get the currently set token data + * @return array + */ + public function getTokenData() + { + return $this->tokenData; + } + + /** + * Check if there is an access token set. + * @return boolean + */ + public function hasAccessToken() + { + return !empty($this->tokenData) && isset($this->tokenData['access_token']) && $this->tokenData['access_token'] != ''; + } + + /** + * Get the current access token. + * @return string + */ + public function getAccessToken() + { + return isset($this->tokenData['access_token']) ? $this->tokenData['access_token'] : null; + } + + /** + * Check if there is a refresh token set. + * @return boolean + */ + public function hasRefreshToken() + { + if( !empty($this->tokenData) && isset($this->tokenData['refresh_token']) && $this->tokenData['refresh_token']) { + $retval = true; + $token = $this->tokenData['refresh_token']; + } else { + $retval = false; + $token = null; + } + + $this->trigger('has_refresh_token', array('refresh_token' => $token)); + + return $retval; + } + + /** + * Get the current refresh token. + * @return string + */ + public function getRefreshToken() + { + return isset($this->tokenData['refresh_token']) ? $this->tokenData['refresh_token'] : null; + } + + public function getExpiresAt() + { + return $this->tokenData['expires_at']; + } + + /** + * Clear the current access token by setting it to null. + * Clearing the access token before calling the "authenticate" method + * will force it to get a new access token. + */ + public function clearAccessToken() + { + if ($this->hasAccessToken()) { + $this->tokenData['access_token'] = null; + } + } + + /** + * Clear access and refresh tokens so that authentication will request a new token + */ + public function clearAllTokens() + { + $this->tokenData = array(); + } + + /** + * Check if the current token expired. + * We check the expired_at time that should be set by the client. + * @return boolean + */ + public function isAccessTokenExpired() + { + if (!isset($this->tokenData['expires_at']) || $this->tokenData['expires_at'] <= time()) { + return true; + } + return false; + } + + /** + * Check if there is an access token and if it is expired based on the expired_at property. + * Not a valid token if either condition fails. + * + * @return boolean + */ + public function isAccessTokenValid() + { + $retval = $this->hasAccessToken() && !$this->isAccessTokenExpired(); + $this->trigger('is_token_valid', array('is_valid' => $retval)); + return $retval; + } + + /** + * Check if the response failed and if the token is expired. + * + * Any errors returned from the API server will be thrown as an Exception. + * + * @param array $response + * @return boolean + */ + private function checkResponse() + { + $response = $this->request->getResponse(); + $httpCode = $this->request->getResponseCode(); + + if (!$response) { + throw new \Exception('Requesting Access Token failed'); + + return false; + } + + if ($httpCode == 400 && isset($response['error'])) { + // throw an Exception with the returned error message + $msg = isset($response['error_description']) ? $response['error_description'] : $response['error']; + throw new \Exception($msg); + + return false; + } + + return true; + } + + /** + * Get an access token from the Trackvia OAuth2 server. + * + * @param string $username The user's username credential + * @param string $password The user's password credential + * @return array|boolean Array of token data returned from the auth server or false on error + */ + public function requestTokenWithUserCreds($username, $password) + { + $this->trigger('request_token_with_user_creds', array( + 'username' => $username, + 'password' => $password + )); + + $url = $this->getTokenUrl(); + + $this->request + ->setMethod('post') + ->setData(array( + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'password', + 'username' => $username, + 'password' => $password + )) + ->send($url); + + $valid = $this->checkResponse(); + + if ($valid) { + $this->tokenData = $this->request->getResponse(); + $this->tokenData['expires_at'] = $this->tokenData['expires_in'] + time(); + + $this->trigger('new_access_token', $this->tokenData); + + return $this->tokenData; + } + + return false; + } + + /** + * Get a new access token with a refresh token. + * + * @param string $refreshToken + * @return array|boolean Array of token data returned from the auth server or false on error + */ + public function requestTokenWithRefreshToken($refreshToken) + { + $this->trigger('request_token_with_refresh_token', array( + 'refresh_token' => $refreshToken + )); + + // use the refresh token to get a new access token + $url = $this->getTokenUrl(); + + $this->request + ->setMethod('post') + ->setData(array( + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken + )) + ->send($url); + + $valid = $this->checkResponse(); + + if ($valid) { + $this->tokenData = $this->request->getResponse(); + $this->tokenData['expires_at'] = $this->tokenData['expires_in'] + time(); + + $this->trigger('new_access_token', $this->tokenData); + + return $this->tokenData; + } + + return false; + } + + /** + * Authenticate the user based on what parameters have been set so far. + * If there is no token, request one with user creds if they exist. + * + * @return array Access token data + */ + public function authenticate() + { + $response = false; + + if ($this->isAccessTokenValid()) { + $response = true; + } else { + if (!$this->hasRefreshToken()) { + // no tokens available, so we need to request new ones + $this->trigger('no_authentication_tokens'); + + // check for user credentials flow first + if ($this->hasUserCreds()) { + $this->trigger('authenticate_with_user_creds'); + $response = $this->requestTokenWithUserCreds( + $this->userCreds['username'], + $this->userCreds['password'] + ); + } else { + $this->trigger('no_authentication'); + } + + //TODO add support for redirecting user to auth trackvia endpoint + } + else { + // use the refresh token to get a new access token + try { + $response = $this->requestTokenWithRefreshToken($this->getRefreshToken()); + } + catch (\Exception $e) { + switch ($e->getMessage()) { + case Api::EXPIRED_REFRESH_TOKEN: + $this->trigger('refresh_token_expired'); + // Refresh token is expired so fallback to another method + $this->clearAllTokens(); + $this->authenticate(); + break; + } + + // just throw the original Exception for any other errors + throw $e; + } + } + } + + return $response; + } + + public function getAuthUrl() + { + return Api::BASE_URL . self::AUTH_URL; + } + + public function getTokenUrl() + { + return Api::BASE_URL . self::TOKEN_URL; + } +} \ No newline at end of file diff --git a/Trackvia/EventDispatcher.php b/Trackvia/EventDispatcher.php new file mode 100644 index 0000000..b9471de --- /dev/null +++ b/Trackvia/EventDispatcher.php @@ -0,0 +1,54 @@ + + */ +abstract class EventDispatcher +{ + /** + * Array to store callbacks for different event names + * @var array + */ + protected $events = array(); + + /** + * Attach an event listener. + * @param string $event + * @param string|array $callback A callback function name that can be passed into call_user_func() + */ + public function on($event, $callback, $extraParams = array()) + { + if (!is_callable($callback)) { + throw new \Exception("Callback argument is not callable. Check you have the right function name."); + } + + if (!isset($this->events[$event])) { + // initialize an array for this event name + $this->events[$event] = array(); + } + + $this->events[$event][] = array($callback, $extraParams); + } + + /** + * Trigger a binded event. + * This will call all binded callback functions for a given event. + * @param string $event The event name + * @param array Array of optional data to trigger with the event + */ + public function trigger($event, $data = array()) + { + if (isset($this->events[$event])) { + // loop through each callback for this event + foreach ($this->events[$event] as $callbackData) { + call_user_func($callbackData[0], $data, $callbackData[1]); + } + } + } +} \ No newline at end of file diff --git a/Trackvia/Log.php b/Trackvia/Log.php new file mode 100755 index 0000000..6c03d2d --- /dev/null +++ b/Trackvia/Log.php @@ -0,0 +1,132 @@ +subject = $subject; + + // pass in the events we want to listen to + $this->initListeners(array( + // Api class + // 'authenticate', + 'new_access_token', + 'api_request_init', + 'api_request_send', + 'api_request_complete', + + //Authentication class + 'is_token_valid', + 'has_refresh_token', + 'request_token_with_user_creds', + 'request_token_with_refresh_token', + 'refresh_token_expired', + 'no_authentication_tokens', + 'authenticate_with_user_creds', + 'no_authentication' + )); + } + + private function logInfo($text) + { + echo "INFO :: " . get_class($this->subject) . " :: $text\n"; + } + + /** + * Setup the event listeners so we can log certain events. + * Events name are auto mapped to method name on_$eventname + * @param array $events + */ + private function initListeners($events) + { + foreach ($events as $event) { + $this->subject->on($event, array($this, 'on_' . $event)); + } + } + +// Api event listeners + public function on_authenticate($data) + { + $this->logInfo("Authenticating with Trackvia server"); + } + + public function on_new_access_token($data) + { + $this->logInfo('New access token granted'); + print_r($data); + } + + public function on_api_request_init($data) + { + $url = $data['url']; + $this->logInfo("Init api request to url \"$url\""); + } + + public function on_api_request_send($data) + { + $url = $data['url']; + $method = $data['http_method']; + $this->logInfo("Sending $method request to url \"$url\""); + + if (isset($data['data'])) { + $this->logInfo("Sending request body - " . $data['data']); + } + } + + public function on_api_request_complete($data) + { + $url = $data['url']; + $this->logInfo("Request Complete"); + echo "API Response Data:\n"; + print_r($data['response']); + } + + +// Authentication event listeners + public function on_is_token_valid($data) + { + $this->logInfo("Is current access token valid? " . ($data['is_valid'] ? 'Yes' : 'No') ); + } + + public function on_has_refresh_token($data) + { + $this->logInfo("Is there a refresh token? " . ($data['refresh_token'] ? 'Yes' : 'No') ); + } + + public function on_request_token_with_user_creds($data) + { + $username = $data['username']; + $password = $data['password']; + $this->logInfo("Requesting new access token with user creds - user: $username, pass: $password"); + } + + public function on_request_token_with_refresh_token($data) + { + $refresh_token = $data['refresh_token']; + $this->logInfo("Requesting new access token with refresh token - token: $refresh_token"); + } + + public function on_refresh_token_expired() + { + $this->logInfo("Refresh token is expired"); + } + + public function on_no_authentication_tokens() + { + $this->logInfo("No authentication tokens available"); + } + + public function on_authenticate_with_user_creds() + { + $this->logInfo("Authenticating with user credentials"); + } + + public function on_no_authentication() + { + $this->logInfo("No way to authenticate"); + } +} \ No newline at end of file diff --git a/Trackvia/Request.php b/Trackvia/Request.php new file mode 100755 index 0000000..d1f9a38 --- /dev/null +++ b/Trackvia/Request.php @@ -0,0 +1,117 @@ +curl = curl_init(); + } + + public function addHeader($name, $value) + { + $this->headers[$name] = $value; + return $this; + } + + public function setContentType($contentType) + { + return $this->addHeader('Content-Type', $contentType); + } + + public function setMethod($method) + { + $method = strtoupper($method); + if ( !in_array($method, array('POST', 'GET', 'PUT', 'DELETE')) ) { + throw new \Exception('HTTP request method "' . $method . '" not supported'); + } + $this->method = $method; + return $this; + } + + public function setData($data) + { + $this->postData = $data; + return $this; + } + + public function send($url) + { + $data = $this->postData; + + // tack data onto the query string for GET requests + if ($this->method == 'GET' && is_array($data) && !empty($data)) { + + $queryString = http_build_query($data); + if (strpos($url, '?') === false) { + $url .= '?'.$queryString; + } else { + // query string is already part of the url + $url .= '&'.$queryString; + } + $data = array(); //empty the data array + } + + $ch = $this->curl; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method); + if (!empty($data)) { + // set any post data + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + + // set any headers + if (!empty($this->headers)) { + $headers = array(); + foreach ($this->headers as $name => $value) { + $headers[] = "$name: $value"; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $this->response = curl_exec($ch); + $this->response = json_decode($this->response, true); + + return $this->response; + } + + /** + * Get the data returned from the last response + * @return mixed + */ + public function getResponse() + { + return $this->response; + } + + /** + * Get the response http code from the last request + * @return int + */ + public function getResponseCode() + { + return (int) curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + } + +} \ No newline at end of file diff --git a/advanced_example.php b/advanced_example.php new file mode 100644 index 0000000..b9a9d75 --- /dev/null +++ b/advanced_example.php @@ -0,0 +1,90 @@ + CLIENT_ID, + 'client_secret' => CLIENT_SECRET, +)); + +// load the saved token for this user +function load_saved_token_data() +{ + //=========== + // code to load the token from your database + //=========== + + // return array( + // 'access_token' => 'user_access_token', + // 'refresh_token' => 'user_refresh_token', + // 'expires_at' => 'expires_timestamp' + // ); +} +$savedToken = load_saved_token_data(); + +// If there is saved token data to use, set it now +if (!empty($savedToken) && isset($savedToken['access_token'])) { + + $tv->setTokenData(array( + 'access_token' => $savedToken['access_token'], + 'refresh_token' => $savedToken['refresh_token'], + 'expires_at' => $savedToken['expires_at'] + )); + +} else { + // No token data. + // So you need to get the user credentials for authentication. + $tv->setUserCredentials(USERNAME, PASSWORD); +} + +// extra parameters to pass in for the "new_token" event callback +$extraParams = array('extra_params' => 'this can be whatever you want'); + +// attach a listener function for when a new token is generated so you can save it to a database +$tv->on('new_token', function ($tokenData, $extraParams) { + $accessToken = $tokenData['access_token']; + $refreshToken = $tokenData['refresh_token']; + + // timestamp when the access token expires + $expiresAt = $tokenData['expires_at']; + + //============ + // code to save token data to your database + //============ + +}, $extraParams); + +/* + +//********************* +// Setup the logger for debugging (optional) +//********************* +$authLog = new Log($tv->getAuthentication()); +$log = new Log($tv); + +*/ + +/** + * Get a list of your apps + */ +$apps = $tv->getApps(); +var_dump($apps); \ No newline at end of file diff --git a/example.php b/example.php new file mode 100755 index 0000000..81f6e81 --- /dev/null +++ b/example.php @@ -0,0 +1,81 @@ + CLIENT_ID, + 'client_secret' => CLIENT_SECRET, + 'username' => USERNAME, + 'password' => PASSWORD +)); + +?> + + +
+ ++ Your example code requires your client_id, client_secret, username and password. You can get these + at the Trackvia account settings page. + Your username and password are the username and password you use to login to TrackVia. +
++ Once you have your credentials, update them in the top of the example.php file. +
+ ++ Below is a simple example to show you how to get a list of all your TrackVia apps. +
+ getApps() will + * get a list of all the apps that you have in your account along with the app id. + */ + $apps = $tv->getApps(); + ?> +