diff --git a/app/Http/Controllers/Import/BankController.php b/app/Http/Controllers/Import/BankController.php index 2e16476e8e..d54fa5726d 100644 --- a/app/Http/Controllers/Import/BankController.php +++ b/app/Http/Controllers/Import/BankController.php @@ -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])); - } } } diff --git a/app/Services/Bunq/Object/MonetaryAccountBank.php b/app/Services/Bunq/Object/MonetaryAccountBank.php new file mode 100644 index 0000000000..31f7224be2 --- /dev/null +++ b/app/Services/Bunq/Object/MonetaryAccountBank.php @@ -0,0 +1,23 @@ +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']); diff --git a/app/Services/Bunq/Request/BunqRequest.php b/app/Services/Bunq/Request/BunqRequest.php index 6712f178d6..629378377f 100644 --- a/app/Services/Bunq/Request/BunqRequest.php +++ b/app/Services/Bunq/Request/BunqRequest.php @@ -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)); } /** diff --git a/app/Services/Bunq/Request/DeleteDeviceSessionRequest.php b/app/Services/Bunq/Request/DeleteDeviceSessionRequest.php new file mode 100644 index 0000000000..66c5d1eecc --- /dev/null +++ b/app/Services/Bunq/Request/DeleteDeviceSessionRequest.php @@ -0,0 +1,49 @@ +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; + } +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/DeviceSessionRequest.php b/app/Services/Bunq/Request/DeviceSessionRequest.php new file mode 100644 index 0000000000..66bc0dc768 --- /dev/null +++ b/app/Services/Bunq/Request/DeviceSessionRequest.php @@ -0,0 +1,149 @@ + $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; + } + + +} \ No newline at end of file diff --git a/app/Support/Import/Information/BunqInformation.php b/app/Support/Import/Information/BunqInformation.php new file mode 100644 index 0000000000..971336df75 --- /dev/null +++ b/app/Support/Import/Information/BunqInformation.php @@ -0,0 +1,105 @@ +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; + } +} \ No newline at end of file diff --git a/app/Support/Import/Information/InformationInterface.php b/app/Support/Import/Information/InformationInterface.php new file mode 100644 index 0000000000..a3a8fb6dca --- /dev/null +++ b/app/Support/Import/Information/InformationInterface.php @@ -0,0 +1,39 @@ + [ 'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites', ], + 'import_info' => [ + 'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation', + ], 'bunq' => [ 'server' => 'https://sandbox.public.api.bunq.com', ],