diff --git a/.gitignore b/.gitignore index db78b71..17b20b7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ composer.lock vendor phpunit.xml .phpunit-watcher.yml +.buildpath +.settings +.project +test.php diff --git a/composer.json b/composer.json index 334efb9..777ce3b 100644 --- a/composer.json +++ b/composer.json @@ -1,46 +1,50 @@ { - "name": "kapersoft/sharefile-api", - "description": "A minimal implementation of ShareFile Api", - "keywords": [ - "kapersoft", - "sharefile-api", - "sharefile", - "api", - "php" - ], - "homepage": "https://github.com/kapersoft/sharefile-api", - "license": "MIT", - "authors": [ - { - "name": "Jan Willem Kaper", - "email": "kapersoft@gmail.com", - "homepage": "https://kapersoft.com", - "role": "Developer" - } - ], - "require": { - "php": "^7.0", - "guzzlehttp/guzzle": "^6.2" - }, - "require-dev": { - "larapack/dd": "1.*", - "mikey179/vfsStream": "^1.6", - "phpunit/phpunit": "6.4.x-dev" - }, - "autoload": { - "psr-4": { - "Kapersoft\\Sharefile\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Kapersoft\\Sharefile\\Test\\": "tests" - } - }, - "scripts": { - "test": "vendor/bin/phpunit" - }, - "config": { - "sort-packages": true - } + "name" : "kapersoft/sharefile-api", + "description" : "A minimal implementation of ShareFile Api", + "keywords" : [ + "kapersoft", + "sharefile-api", + "sharefile", + "api", + "php" + ], + "homepage" : "https://github.com/kapersoft/sharefile-api", + "license" : "MIT", + "authors" : [{ + "name" : "Jan Willem Kaper", + "email" : "kapersoft@gmail.com", + "homepage" : "https://kapersoft.com", + "role" : "Developer" + } + ], + "require" : { + "php" : "^7.0", + "slacker775/oauth2-sharefile" : "^1.0", + "guzzlehttp/guzzle" : "^6.2", + "slacker775/oauth2-tokenstorage" : "^1.0" + }, + "require-dev" : { + "larapack/dd" : "1.*", + "mikey179/vfsstream" : "^1.6", + "phpunit/phpunit" : "^6.4" + }, + "autoload" : { + "psr-4" : { + "Kapersoft\\ShareFile\\" : "src/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Kapersoft\\Sharefile\\Test\\" : "tests/" + } + }, + "scripts" : { + "test" : "vendor/bin/phpunit" + }, + "config" : { + "sort-packages" : true + }, + "suggest" : { + "league/flysystem" : "Utilize Flysystem for OAuth Token Storage" + } } diff --git a/src/Client.php b/src/Client.php index 1ce9188..376ce1d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1,14 +1,19 @@ authenticate($hostname, $client_id, $client_secret, $username, $password, $handler); + const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; - if (! isset($response['access_token']) || ! isset($response['subdomain'])) { - throw new Exception("Incorrect response from Authentication: 'access_token' or 'subdomain' is missing."); - } - - $this->token = $response; - $this->client = new GuzzleClient( - [ - 'handler' => $handler, - 'headers' => [ - 'Authorization' => "Bearer {$this->token['access_token']}", - ], - ] - ); - } + // 8 megabytes /** - * ShareFile authentication using username/password. + * Client constructor. * - * @param string $hostname ShareFile hostname - * @param string $client_id OAuth2 client_id - * @param string $client_secret OAuth2 client_secret - * @param string $username ShareFile username - * @param string $password ShareFile password - * @param MockHandler|HandlerStack $handler Guzzle Handler + * @param string $hostname + * ShareFile hostname + * @param string $client_id + * OAuth2 client_id + * @param string $client_secret + * OAuth2 client_secret + * @param string $username + * ShareFile username + * @param string $password + * ShareFile password + * @param MockHandler|HandlerStack $handler + * Guzzle Handler * * @throws Exception - * - * @return array */ - protected function authenticate(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null):array + public function __construct(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null, TokenStorageInterface $tokenRepository = null) { - $uri = "https://{$hostname}/oauth/token"; - - $parameters = [ - 'grant_type' => 'password', - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'username' => $username, - 'password' => $password, + $this->tokenRepository = $tokenRepository; + + $client = new HttpClient([ + 'handler' => $handler + ]); + + $this->authProvider = new AuthProvider([ + 'clientId' => $client_id, + 'clientSecret' => $client_secret + ], [ + 'httpClient' => $client + ]); + + $this->options = [ + 'username' => $username, + 'password' => $password, + 'baseUrl' => $hostname ]; - - try { - $client = new GuzzleClient(['handler' => $handler]); - $response = $client->post( - $uri, - ['form_params' => $parameters] - ); - } catch (ClientException $exception) { - throw $exception; - } - - if ($response->getStatusCode() == '200') { - return json_decode($response->getBody(), true); - } else { - throw new Exception('Authentication error', $response->getStatusCode()); - } } /** @@ -134,11 +124,16 @@ protected function authenticate(string $hostname, string $client_id, string $cli * * @return array */ - public function getUser(string $userId = ''):array + public function getUser(string $userId = '') : array { return $this->get("Users({$userId})"); } + public function updateUser(string $userId, array $data) : array + { + return $this->patch("Users({$userId})", $data); + } + /** * Create a folder. * @@ -149,7 +144,7 @@ public function getUser(string $userId = ''):array * * @return array */ - public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false):array + public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false) : array { $parameters = $this->buildHttpQuery( [ @@ -174,7 +169,7 @@ public function createFolder(string $parentId, string $name, string $description * * @return array */ - public function getItemById(string $itemId, bool $getChildren = false):array + public function getItemById(string $itemId, bool $getChildren = false) : array { $parameters = $getChildren === true ? '$expand=Children' : ''; @@ -189,7 +184,7 @@ public function getItemById(string $itemId, bool $getChildren = false):array * * @return array */ - public function getItemByPath(string $path, string $itemId = ''):array + public function getItemByPath(string $path, string $itemId = '') : array { if (empty($itemId)) { return $this->get("Items/ByPath?Path={$path}"); @@ -199,13 +194,13 @@ public function getItemByPath(string $path, string $itemId = ''):array } /** - * Get breadcrumps of an item. + * Get breadcrumbs of an item. * * @param string $itemId Item Id * * @return array */ - public function getItemBreadcrumps(string $itemId):array + public function getItemBreadcrumbs(string $itemId) : array { return $this->get("Items({$itemId})/Breadcrumbs"); } @@ -219,7 +214,7 @@ public function getItemBreadcrumps(string $itemId):array * * @return array */ - public function copyItem(string $targetId, string $itemId, bool $overwrite = false):array + public function copyItem(string $targetId, string $itemId, bool $overwrite = false) : array { $parameters = $this->buildHttpQuery( [ @@ -241,7 +236,7 @@ public function copyItem(string $targetId, string $itemId, bool $overwrite = fal * * @return array */ - public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true):array + public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true) : array { $parameters = $this->buildHttpQuery( [ @@ -262,7 +257,7 @@ public function updateItem(string $itemId, array $data, bool $forceSync = true, * * @return string */ - public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false):string + public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false) : string { $parameters = $this->buildHttpQuery( [ @@ -368,9 +363,10 @@ public function uploadFileStandard(string $filename, string $folderId, bool $unz { $chunkUri = $this->getChunkUri('standard', $filename, $folderId, $unzip, $overwrite, $notify); - $response = $this->client->request( + $request = $this->authProvider->getAuthenticatedRequest( 'POST', $chunkUri['ChunkUri'], + $this->accessToken, [ 'multipart' => [ [ @@ -380,6 +376,7 @@ public function uploadFileStandard(string $filename, string $folderId, bool $unz ], ] ); + $response = $this->authProvider->getResponse($request); return (string) $response->getBody(); } @@ -436,7 +433,7 @@ public function uploadFileStreamed($stream, string $folderId, string $filename = 'index' => $index, 'byteOffset' => $index * $chunkSize, 'hash' => md5($data), - 'filehash' => Psr7\hash(Psr7\stream_for($stream), 'md5'), + 'filehash' => \GuzzleHttp\Psr7\hash(\GuzzleHttp\Psr7\stream_for($stream), 'md5'), 'finish' => true, ] ); @@ -513,6 +510,41 @@ public function getItemAccessControls(string $itemId, string $userId = ''):array } } + public function getAccessToken(): AccessToken + { + $tokenId = sprintf('sf-%s', $this->options['username']); + + if ($this->accessToken === null) { + if ($this->tokenRepository !== null) { + try { + $this->accessToken = $this->tokenRepository->loadToken($tokenId); + } catch(TokenNotFoundException $e) {} + } + + if ($this->accessToken === null) { + $this->accessToken = $this->authProvider->getAccessToken('password', [ + 'username' => $this->options['username'], + 'password' => $this->options['password'], + 'baseUrl' => $this->options['baseUrl'] + ]); + + if ($this->tokenRepository !== null) { + $this->tokenRepository->storeToken($this->accessToken, $tokenId); + } + } + } + + if ($this->accessToken->hasExpired() === true) { + $this->accessToken = $this->authProvider->getAccessToken('refresh_token', [ + 'refresh_token' => $this->accessToken->getRefreshToken() + ]); + if ($this->tokenRepository !== null) { + $this->tokenRepository->storeAccessToken($tokenId, $this->accessToken); + } + } + return $this->accessToken; + } + /** * Build API uri. * @@ -522,7 +554,7 @@ public function getItemAccessControls(string $itemId, string $userId = ''):array */ protected function buildUri(string $endpoint): string { - return "https://{$this->token['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; + return "https://{$this->accessToken->getValues()['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; } /** @@ -538,11 +570,14 @@ protected function buildUri(string $endpoint): string */ protected function request(string $method, string $endpoint, $json = null) { + $accessToken = $this->getAccessToken(); + $uri = $this->buildUri($endpoint); $options = $json != null ? ['json' => $json] : []; try { - $response = $this->client->request($method, $uri, $options); + $request = $this->authProvider->getAuthenticatedRequest($method, $uri, $accessToken, $options); + $response = $this->authProvider->getResponse($request); } catch (ClientException $exception) { throw $this->determineException($exception); } @@ -612,9 +647,10 @@ protected function delete(string $endpoint) */ protected function uploadChunk($uri, $data) { - $response = $this->client->request( + $request = $this->authProvider->getAuthenticatedRequest( 'POST', $uri, + $this->accessToken, [ 'headers' => [ 'Content-Length' => strlen($data), @@ -623,6 +659,7 @@ protected function uploadChunk($uri, $data) 'body' => $data, ] ); + $response = $this->authProvider->getResponse($request); return (string) $response->getBody(); } diff --git a/src/Exceptions/BadRequest.php b/src/Exceptions/BadRequest.php index 2bf67d8..7147b92 100644 --- a/src/Exceptions/BadRequest.php +++ b/src/Exceptions/BadRequest.php @@ -1,6 +1,6 @@ 'my_access_code', 'subdomain' => 'subdomain']))] + [new Response(200, [], json_encode(['access_token' => 'my_access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60]))] ); $client = new Client( @@ -72,89 +72,7 @@ public function it_can_be_instantiated() // @codingStandardsIgnoreLine ); $this->assertInstanceOf(Client::class, $client); - $this->assertEquals('my_access_code', $client->token['access_token']); - } - - /** - * Test for it_can_throw_an_exception. - * - * @test - * - * @return void - */ - public function it_can_throw_an_exception() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [new Response(400)] - ); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Authentication error'); - $this->expectExceptionCode(400); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); - } - - /** - * Test for it_can_handle_an_incorrect_authentication_response. - * - * @test - * - * @return void - */ - public function it_can_handle_an_incorrect_authentication_response() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [new Response(200, [], json_encode([]))] - ); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Incorrect response from Authentication: 'access_token' or 'subdomain' is missing."); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); - } - - /** - * Test for it_can_throw_an_client_exception. - * - * @test - * - * @return void - */ - public function it_can_throw_an_client_exception() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [ - new ClientException('Could not resolve host: hostname', new Request('POST', 'hostname'), new response(404)), - ] - ); - - $this->expectException(ClientException::class); - $this->expectExceptionCode(404); - $this->expectExceptionMessage('Could not resolve host: hostname'); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); + $this->assertEquals('my_access_code', $client->getAccessToken()->getToken()); } /** @@ -247,7 +165,7 @@ public function it_can_get_an_item_with_children() // @codingStandardsIgnoreLine $expectedResponse = ['odata.type' => 'odata.metadata', 'odata.count' => '2', 'value' => []]; $mockClient = $this->getMockClient($expectedResponse); - $response = $mockClient->getItemBreadcrumps(Client::FOLDER_HOME); + $response = $mockClient->getItemBreadcrumbs(Client::FOLDER_HOME); $this->assertSame('GET', (string) $this->getLastRequest()->getMethod()); $this->assertSame('https://subdomain.sf-api.com/sf/v3/Items(home)/Breadcrumbs', (string) $this->getLastRequest()->getUri()); @@ -274,13 +192,13 @@ public function it_can_get_item_by_path() // @codingStandardsIgnoreLine } /** - * Test for it_can_get_item_breadcrumps. + * Test for it_can_get_item_breadcrumbs. * * @test * * @return void */ - public function it_can_get_item_breadcrumps() // @codingStandardsIgnoreLine + public function it_can_get_item_breadcrumbs() // @codingStandardsIgnoreLine { $expectedResponse = ['odata.type' => 'ShareFile.Api.Models.Folder', 'Id' => 'top', 'Children' => '']; $mockClient = $this->getMockClient($expectedResponse); @@ -442,7 +360,7 @@ public function it_can_upload_an_item_using_single_http_post() // @codingStandar // Create response $expectedResponse = 'OK'; $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60])), new Response(200, [], json_encode(['ChunkUri' => 'https://storage-eu-202.sharefile.com/upload.aspx?uploadid=my_upload_id'])), new Response(200, [], $expectedResponse), ]; @@ -484,7 +402,7 @@ public function it_can_upload_an_item_using_multiple_http_posts() // @codingStan // Create response $expectedResponse = 'fo66e8f5-3aa3-405b-8129-f9a749dd4e99'; $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60])), new Response(200, [], json_encode(['ChunkUri' => 'https://storage-eu-202.sharefile.com/upload.aspx?uploadid=my_upload_id'])), new Response(200, [], 'true'), new Response(200, [], $expectedResponse), @@ -648,7 +566,14 @@ private function getMockClient($responseBody, array $responseHeaders = []): Clie } $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode([ + 'access_token' => 'access_code', + 'subdomain' => 'subdomain', + 'expires' => time() + 60, + 'refresh_token' => 'refresh_code', + 'token_type' => 'bearer', + 'appcp' => 'sharefile.com' + ])), new Response(200, $responseHeaders, $responseBody), ];