Expand bunq related code.

This commit is contained in:
James Cole 2017-08-19 09:22:44 +02:00
parent ca0f09c8f7
commit 4694e31e35
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
10 changed files with 533 additions and 24 deletions

View File

@ -13,6 +13,7 @@ namespace FireflyIII\Http\Controllers\Import;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Support\Import\Information\InformationInterface;
use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface;
use Illuminate\Http\Request;
use Log;
@ -21,15 +22,36 @@ use Session;
class BankController extends Controller
{
/**
*
* This method must ask the user all parameters necessary to start importing data. This may not be enough
* to finish the import itself (ie. mapping) but it should be enough to begin: accounts to import from,
* accounts to import into, data ranges, etc.
*/
public function form()
public function form(string $bank)
{
$class = config(sprintf('firefly.import_pre.%s', $bank));
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser(auth()->user());
if ($object->hasPrerequisites()) {
return redirect(route('import.banq.prerequisites', [$bank]));
}
$class = config(sprintf('firefly.import_info.%s', $bank));
/** @var InformationInterface $object */
$object = app($class);
$object->setUser(auth()->user());
$remoteAccounts = $object->getAccounts();
}
/**
* This method processes the prerequisites the user has entered in the previous step.
*
* Whatever storePrerequisites does, it should make sure that the system is ready to continue immediately. So
* no extra calls or stuff, except maybe to open a session
*
* @see PrerequisitesInterface::storePrerequisites
*
* @param Request $request
* @param string $bank
*
@ -60,6 +82,9 @@ class BankController extends Controller
}
/**
* This method shows you, if necessary, a form that allows you to enter any required values, such as API keys,
* login passwords or other values.
*
* @param string $bank
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
@ -77,10 +102,7 @@ class BankController extends Controller
return view($view, $parameters);
}
if (!$object->hasPrerequisites()) {
return redirect(route('import.bank.form', [$bank]));
}
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* MonetaryAccountBank.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class MonetaryAccountBank
*
* @package FireflyIII\Services\Bunq\Object
*/
class MonetaryAccountBank extends BunqObject
{
}

View File

@ -0,0 +1,34 @@
<?php
/**
* UserCompany.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
/**
* Class UserCompany
*
* @package FireflyIII\Services\Bunq\Object
*/
class UserCompany extends BunqObject
{
/**
* UserCompany constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
}
}

View File

@ -90,6 +90,9 @@ class UserPerson extends BunqObject
*/
public function __construct(array $data)
{
if (count($data) === 0) {
return;
}
$this->id = intval($data['id']);
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);

View File

@ -99,23 +99,24 @@ abstract class BunqRequest
* @param string $data
*
* @return string
* @throws FireflyException
*/
protected function generateSignature(string $method, string $uri, array $headers, string $data): string
{
if (strlen($this->privateKey) === 0) {
throw new Exception('No private key present.');
throw new FireflyException('No private key present.');
}
if (strtolower($method) === 'get') {
if (strtolower($method) === 'get' || strtolower($method) === 'delete') {
$data = '';
}
$uri = str_replace(['https://api.bunq.com', 'https://sandbox.public.api.bunq.com'], '', $uri);
$toSign = strtoupper($method) . ' ' . $uri . "\n";
$toSign = sprintf("%s %s\n", strtoupper($method), $uri);
$headersToSign = ['Cache-Control', 'User-Agent'];
ksort($headers);
foreach ($headers as $name => $value) {
if (in_array($name, $headersToSign) || substr($name, 0, 7) === 'X-Bunq-') {
$toSign .= $name . ': ' . $value . "\n";
$toSign .= sprintf("%s: %s\n", $name, $value);
}
}
$toSign .= "\n" . $data;
@ -184,6 +185,48 @@ abstract class BunqRequest
return [];
}
/**
* @param string $uri
* @param array $headers
*
* @return array
* @throws Exception
*/
protected function sendSignedBunqDelete(string $uri, array $headers): array
{
if (strlen($this->server) === 0) {
throw new FireflyException('No bunq server defined');
}
$fullUri = $this->server . $uri;
$signature = $this->generateSignature('delete', $uri, $headers, '');
$headers['X-Bunq-Client-Signature'] = $signature;
try {
$response = Requests::delete($fullUri, $headers);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
Log::debug(sprintf('Response to DELETE %s is %s', $fullUri, $body));
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
return $array;
}
/**
* @param string $uri
* @param array $data
@ -208,17 +251,20 @@ abstract class BunqRequest
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
$array['ResponseHeaders'] = $responseHeaders;
return $array;
}
@ -243,17 +289,49 @@ abstract class BunqRequest
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
$array['ResponseHeaders'] = $responseHeaders;
return $array;
}
/**
* @param string $uri
* @param array $headers
*
* @return array
*/
protected function sendUnsignedBunqDelete(string $uri, array $headers): array
{
$fullUri = $this->server . $uri;
try {
$response = Requests::delete($fullUri, $headers);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
return $array;
}
@ -274,13 +352,17 @@ abstract class BunqRequest
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$responseHeaders = $response->headers->getAll();
$array = json_decode($body, true);
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
$array['ResponseHeaders'] = $responseHeaders;
return $array;
}
@ -313,7 +395,7 @@ abstract class BunqRequest
$message[] = $error['error_description'];
}
}
throw new FireflyException(join(', ', $message));
throw new FireflyException('Bunq ERROR ' . $response['ResponseStatusCode'] . ': ' . join(', ', $message));
}
/**

View File

@ -0,0 +1,49 @@
<?php
/**
* DeleteDeviceSessionRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Token\InstallationToken;
use FireflyIII\Services\Bunq\Token\SessionToken;
use Log;
/**
* Class DeleteDeviceSessionRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class DeleteDeviceSessionRequest extends BunqRequest
{
/** @var SessionToken */
private $sessionToken;
/**
*
*/
public function call(): void
{
Log::debug('Going to send bunq delete session request.');
$uri = sprintf('/v1/session/%d', $this->sessionToken->getId());
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->sessionToken->getToken();
$this->sendSignedBunqDelete($uri, $headers);
return;
}
/**
* @param SessionToken $sessionToken
*/
public function setSessionToken(SessionToken $sessionToken)
{
$this->sessionToken = $sessionToken;
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* DeviceSessionRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Id\DeviceSessionId;
use FireflyIII\Services\Bunq\Object\UserCompany;
use FireflyIII\Services\Bunq\Object\UserPerson;
use FireflyIII\Services\Bunq\Token\InstallationToken;
use FireflyIII\Services\Bunq\Token\SessionToken;
use Log;
/**
* Class DeviceSessionRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class DeviceSessionRequest extends BunqRequest
{
/** @var DeviceSessionId */
private $deviceSessionId;
/** @var InstallationToken */
private $installationToken;
/** @var SessionToken */
private $sessionToken;
/** @var UserCompany */
private $userCompany;
/** @var UserPerson */
private $userPerson;
/**
*
*/
public function call(): void
{
$uri = '/v1/session-server';
$data = ['secret' => $this->secret];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->installationToken->getToken();
$response = $this->sendSignedBunqPost($uri, $data, $headers);
$this->deviceSessionId = $this->extractDeviceSessionId($response);
$this->sessionToken = $this->extractSessionToken($response);
$this->userPerson = $this->extractUserPerson($response);
$this->userCompany = $this->extractUserCompany($response);
Log::debug(sprintf('Session ID: %s', serialize($this->deviceSessionId)));
Log::debug(sprintf('Session token: %s', serialize($this->sessionToken)));
Log::debug(sprintf('Session user person: %s', serialize($this->userPerson)));
Log::debug(sprintf('Session user company: %s', serialize($this->userCompany)));
return;
}
/**
* @return DeviceSessionId
*/
public function getDeviceSessionId(): DeviceSessionId
{
return $this->deviceSessionId;
}
/**
* @return SessionToken
*/
public function getSessionToken(): SessionToken
{
return $this->sessionToken;
}
/**
* @return UserPerson
*/
public function getUserPerson(): UserPerson
{
return $this->userPerson;
}
/**
* @param InstallationToken $installationToken
*/
public function setInstallationToken(InstallationToken $installationToken)
{
$this->installationToken = $installationToken;
}
/**
* @param array $response
*
* @return DeviceSessionId
*/
private function extractDeviceSessionId(array $response): DeviceSessionId
{
$data = $this->getKeyFromResponse('Id', $response);
$deviceSessionId = new DeviceSessionId;
$deviceSessionId->setId(intval($data['id']));
return $deviceSessionId;
}
private function extractSessionToken(array $response): SessionToken
{
$data = $this->getKeyFromResponse('Token', $response);
$sessionToken = new SessionToken($data);
return $sessionToken;
}
/**
* @param $response
*
* @return UserCompany
*/
private function extractUserCompany($response): UserCompany
{
$data = $this->getKeyFromResponse('UserCompany', $response);
$userCompany = new UserCompany($data);
return $userCompany;
}
/**
* @param $response
*
* @return UserPerson
*/
private function extractUserPerson($response): UserPerson
{
$data = $this->getKeyFromResponse('UserPerson', $response);
$userPerson = new UserPerson($data);
return $userPerson;
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* BunqInformation.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Information;
use FireflyIII\Services\Bunq\Request\DeleteDeviceSessionRequest;
use FireflyIII\Services\Bunq\Request\DeviceSessionRequest;
use FireflyIII\Services\Bunq\Token\SessionToken;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Log;
use Preferences;
/**
* Class BunqInformation
*
* @package FireflyIII\Support\Import\Information
*/
class BunqInformation implements InformationInterface
{
/** @var User */
private $user;
/**
* Returns a collection of accounts. Preferrably, these follow a uniform Firefly III format so they can be managed over banks.
*
* @return Collection
*/
public function getAccounts(): Collection
{
Log::debug('Now in getAccounts()');
$sessionToken = $this->startSession();
// get list of Bunq accounts:
$this->closeSession($sessionToken);
return new Collection;
}
/**
* Set the user for this Prerequisites-routine. Class is expected to implement and save this.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @param SessionToken $sessionToken
*/
private function closeSession(SessionToken $sessionToken): void
{
Log::debug('Going to close session');
$apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
$serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
$server = config('firefly.bunq.server');
$privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
$request = new DeleteDeviceSessionRequest();
$request->setSecret($apiKey);
$request->setServer($server);
$request->setPrivateKey($privateKey);
$request->setServerPublicKey($serverPublicKey);
$request->setSessionToken($sessionToken);
$request->call();
return;
}
/**
* @return SessionToken
*/
private function startSession(): SessionToken
{
Log::debug('Now in startSession.');
$apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data;
$serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data;
$server = config('firefly.bunq.server');
$privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data;
$installationToken = Preferences::getForUser($this->user, 'bunq_installation_token')->data;
$request = new DeviceSessionRequest();
$request->setSecret($apiKey);
$request->setServerPublicKey($serverPublicKey);
$request->setServer($server);
$request->setPrivateKey($privateKey);
$request->setInstallationToken($installationToken);
$request->call();
$sessionToken = $request->getSessionToken();
Log::debug(sprintf('Now have got session token: %s', serialize($sessionToken)));
return $sessionToken;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* InformationInterface.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Information;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Interface InformationInterface
*
* @package FireflyIII\Support\Import\Information
*/
interface InformationInterface
{
/**
* Set the user for this Prerequisites-routine. Class is expected to implement and save this.
*
* @param User $user
*/
public function setUser(User $user): void;
/**
* Returns a collection of accounts. Preferrably, these follow a uniform Firefly III format so they can be managed over banks.
*
* @return Collection
*/
public function getAccounts(): Collection;
}

View File

@ -42,6 +42,9 @@ return [
'import_pre' => [
'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites',
],
'import_info' => [
'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation',
],
'bunq' => [
'server' => 'https://sandbox.public.api.bunq.com',
],