From 0774258516e42533996c297cbb436fd89a70ce85 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 9 Dec 2017 12:08:24 +0100 Subject: [PATCH 1/5] Exceptions when class does not exist. --- app/Http/Controllers/Import/BankController.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Import/BankController.php b/app/Http/Controllers/Import/BankController.php index 9358481970..fe4e6790ce 100644 --- a/app/Http/Controllers/Import/BankController.php +++ b/app/Http/Controllers/Import/BankController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Import; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Support\Import\Information\InformationInterface; use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface; @@ -43,6 +44,9 @@ class BankController extends Controller public function form(string $bank) { $class = config(sprintf('firefly.import_pre.%s', $bank)); + if(!class_exists($class)) { + throw new FireflyException(sprintf('Cannot find class %s', $class)); + } /** @var PrerequisitesInterface $object */ $object = app($class); $object->setUser(auth()->user()); @@ -72,6 +76,9 @@ class BankController extends Controller public function postForm(Request $request, string $bank) { $class = config(sprintf('firefly.import_pre.%s', $bank)); + if(!class_exists($class)) { + throw new FireflyException(sprintf('Cannot find class %s', $class)); + } /** @var PrerequisitesInterface $object */ $object = app($class); $object->setUser(auth()->user()); @@ -110,6 +117,9 @@ class BankController extends Controller { Log::debug(sprintf('Now in postPrerequisites for %s', $bank)); $class = config(sprintf('firefly.import_pre.%s', $bank)); + if(!class_exists($class)) { + throw new FireflyException(sprintf('Cannot find class %s', $class)); + } /** @var PrerequisitesInterface $object */ $object = app($class); $object->setUser(auth()->user()); @@ -142,13 +152,17 @@ class BankController extends Controller public function prerequisites(string $bank) { $class = config(sprintf('firefly.import_pre.%s', $bank)); + if(!class_exists($class)) { + throw new FireflyException(sprintf('Cannot find class %s', $class)); + } /** @var PrerequisitesInterface $object */ $object = app($class); $object->setUser(auth()->user()); if ($object->hasPrerequisites()) { $view = $object->getView(); - $parameters = $object->getViewParameters(); + $parameters = ['title' => strval(trans('firefly.import_index_title')),'mainTitleIcon' => 'fa-archive']; + $parameters = $object->getViewParameters() + $parameters; return view($view, $parameters); } From aa9500f5ad5354bae5cb81d252b359965e363cf6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 9 Dec 2017 12:23:28 +0100 Subject: [PATCH 2/5] Initial code to get providers from Spectre. --- app/Jobs/GetSpectreProviders.php | 77 ++++ app/Models/SpectreProvider.php | 32 ++ app/Services/Bunq/Request/BunqRequest.php | 3 + .../Spectre/Request/ListProvidersRequest.php | 80 ++++ .../Spectre/Request/SpectreRequest.php | 378 ++++++++++++++++++ .../Prerequisites/SpectrePrerequisites.php | 229 +++++++++++ config/firefly.php | 44 +- .../2017_12_09_111046_changes_for_spectre.php | 47 +++ public/images/logos/bunq.png | Bin 1605 -> 3292 bytes public/images/logos/csv.png | Bin 0 -> 3515 bytes public/images/logos/plaid.png | Bin 0 -> 8326 bytes public/images/logos/spectre.png | Bin 0 -> 3446 bytes resources/lang/en_US/bank.php | 10 +- resources/lang/en_US/form.php | 4 + resources/views/import/index.twig | 29 +- .../views/import/spectre/prerequisites.twig | 58 +++ 16 files changed, 964 insertions(+), 27 deletions(-) create mode 100644 app/Jobs/GetSpectreProviders.php create mode 100644 app/Models/SpectreProvider.php create mode 100644 app/Services/Spectre/Request/ListProvidersRequest.php create mode 100644 app/Services/Spectre/Request/SpectreRequest.php create mode 100644 app/Support/Import/Prerequisites/SpectrePrerequisites.php create mode 100644 database/migrations/2017_12_09_111046_changes_for_spectre.php create mode 100644 public/images/logos/csv.png create mode 100644 public/images/logos/plaid.png create mode 100644 public/images/logos/spectre.png create mode 100644 resources/views/import/spectre/prerequisites.twig diff --git a/app/Jobs/GetSpectreProviders.php b/app/Jobs/GetSpectreProviders.php new file mode 100644 index 0000000000..0184d6ce4f --- /dev/null +++ b/app/Jobs/GetSpectreProviders.php @@ -0,0 +1,77 @@ +user = $user; + Log::debug('Constructed job GetSpectreProviders'); + } + + /** + * Execute the job. + */ + public function handle() + { + /** @var Configuration $configValue */ + $configValue = app('fireflyconfig')->get('spectre_provider_download', 0); + $now = time(); + if ($now - intval($configValue->data) < 86400) { + Log::debug(sprintf('Difference is %d, so will NOT execute job.', ($now - intval($configValue->data)))); + + return; + } + Log::debug(sprintf('Difference is %d, so will execute job.', ($now - intval($configValue->data)))); + + // get user + + // fire away! + $request = new ListProvidersRequest($this->user); + $request->call(); + + // store all providers: + $providers = $request->getProviders(); + foreach ($providers as $provider) { + // find provider? + $dbProvider = SpectreProvider::where('spectre_id', $provider['id'])->first(); + if (is_null($dbProvider)) { + $dbProvider = new SpectreProvider; + } + // update fields: + $dbProvider->spectre_id = $provider['id']; + $dbProvider->code = $provider['code']; + $dbProvider->mode = $provider['mode']; + $dbProvider->status = $provider['status']; + $dbProvider->interactive = $provider['interactive'] === 1; + $dbProvider->automatic_fetch = $provider['automatic_fetch'] === 1; + $dbProvider->country_code = $provider['country_code']; + $dbProvider->data = $provider; + $dbProvider->save(); + Log::debug(sprintf('Stored provider #%d under ID #%d', $provider['id'], $dbProvider->id)); + } + + app('fireflyconfig')->set('spectre_provider_download', time()); + + return; + } +} diff --git a/app/Models/SpectreProvider.php b/app/Models/SpectreProvider.php new file mode 100644 index 0000000000..4cdf9e9d55 --- /dev/null +++ b/app/Models/SpectreProvider.php @@ -0,0 +1,32 @@ + 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'interactive' => 'boolean', + 'automatic_fetch' => 'boolean', + 'data' => 'array', + ]; + + protected $fillable = ['spectre_id', 'code', 'mode', 'name', 'status', 'interactive', 'automatic_fetch', 'country_code', 'data']; + +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/BunqRequest.php b/app/Services/Bunq/Request/BunqRequest.php index dcc4a23729..ca73d0443c 100644 --- a/app/Services/Bunq/Request/BunqRequest.php +++ b/app/Services/Bunq/Request/BunqRequest.php @@ -160,6 +160,9 @@ abstract class BunqRequest return $result; } + /** + * @return array + */ protected function getDefaultHeaders(): array { $userAgent = sprintf('FireflyIII v%s', config('firefly.version')); diff --git a/app/Services/Spectre/Request/ListProvidersRequest.php b/app/Services/Spectre/Request/ListProvidersRequest.php new file mode 100644 index 0000000000..a4abd1ad2c --- /dev/null +++ b/app/Services/Spectre/Request/ListProvidersRequest.php @@ -0,0 +1,80 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use Log; + +/** + * Class ListUserRequest. + */ +class ListProvidersRequest extends SpectreRequest +{ + protected $providers = []; + + /** + * + */ + public function call(): void + { + $hasNextPage = true; + $nextId = 0; + while ($hasNextPage) { + Log::debug(sprintf('Now calling for next_id %d', $nextId)); + $parameters = ['include_fake_providers' => 'true', 'include_provider_fields' => 'true', 'from_id' => $nextId]; + $uri = '/api/v3/providers?' . http_build_query($parameters); + $response = $this->sendSignedSpectreGet($uri, []); + + // count entries: + Log::debug(sprintf('Found %d entries in data-array', count($response['data']))); + + // extract next ID + $hasNextPage = false; + if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) { + $hasNextPage = true; + $nextId = $response['meta']['next_id']; + Log::debug(sprintf('Next ID is now %d.', $nextId)); + } else { + Log::debug('No next page.'); + } + + // store providers: + foreach ($response['data'] as $providerArray) { + $providerId = $providerArray['id']; + $this->providers[$providerId] = $providerArray; + Log::debug(sprintf('Stored provider #%d', $providerId)); + } + } + + return; + } + + /** + * @return array + */ + public function getProviders(): array + { + return $this->providers; + } + + +} diff --git a/app/Services/Spectre/Request/SpectreRequest.php b/app/Services/Spectre/Request/SpectreRequest.php new file mode 100644 index 0000000000..a7ce46f4d5 --- /dev/null +++ b/app/Services/Spectre/Request/SpectreRequest.php @@ -0,0 +1,378 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\User; +use Log; +use Requests; +use Requests_Exception; + +//use FireflyIII\Services\Bunq\Object\ServerPublicKey; + +/** + * Class BunqRequest. + */ +abstract class SpectreRequest +{ + /** @var string */ + protected $clientId = ''; + protected $expiresAt = 0; + /** @var ServerPublicKey */ + protected $serverPublicKey; + /** @var string */ + protected $serviceSecret = ''; + /** @var string */ + private $privateKey = ''; + /** @var string */ + private $server = ''; + /** @var User */ + private $user; + + /** + * SpectreRequest constructor. + */ + public function __construct(User $user) + { + $this->user = $user; + $this->server = config('firefly.spectre.server'); + $this->expiresAt = time() + 180; + $privateKey = app('preferences')->get('spectre_private_key', null); + $this->privateKey = $privateKey->data; + + // set client ID + $clientId = app('preferences')->get('spectre_client_id', null); + $this->clientId = $clientId->data; + + // set service secret + $serviceSecret = app('preferences')->get('spectre_service_secret', null); + $this->serviceSecret = $serviceSecret->data; + } + + /** + * + */ + abstract public function call(): void; + + /** + * @return string + */ + public function getClientId(): string + { + return $this->clientId; + } + + /** + * @param string $clientId + */ + public function setClientId(string $clientId): void + { + $this->clientId = $clientId; + } + + /** + * @return string + */ + public function getServer(): string + { + return $this->server; + } + + /** + * @return ServerPublicKey + */ + public function getServerPublicKey(): ServerPublicKey + { + return $this->serverPublicKey; + } + + /** + * @param ServerPublicKey $serverPublicKey + */ + public function setServerPublicKey(ServerPublicKey $serverPublicKey) + { + $this->serverPublicKey = $serverPublicKey; + } + + /** + * @return string + */ + public function getServiceSecret(): string + { + return $this->serviceSecret; + } + + /** + * @param string $serviceSecret + */ + public function setServiceSecret(string $serviceSecret): void + { + $this->serviceSecret = $serviceSecret; + } + + /** + * @param string $privateKey + */ + public function setPrivateKey(string $privateKey) + { + $this->privateKey = $privateKey; + } + + /** + * @param string $secret + */ + public function setSecret(string $secret) + { + $this->secret = $secret; + } + + /** + * @param string $method + * @param string $uri + * @param string $data + * + * @return string + * + * @throws FireflyException + */ + protected function generateSignature(string $method, string $uri, string $data): string + { + if (0 === strlen($this->privateKey)) { + throw new FireflyException('No private key present.'); + } + if ('get' === strtolower($method) || 'delete' === strtolower($method)) { + $data = ''; + } + // base64(sha1_signature(private_key, "Expires-at|request_method|original_url|post_body|md5_of_uploaded_file|"))) + // Prepare the signature + $toSign = $this->expiresAt . '|' . strtoupper($method) . '|' . $uri . '|' . $data . ''; // no file so no content there. + Log::debug(sprintf('String to sign: %s', $toSign)); + $signature = ''; + + // Sign the data + openssl_sign($toSign, $signature, $this->privateKey, OPENSSL_ALGO_SHA1); + $signature = base64_encode($signature); + + return $signature; + } + + /** + * @return array + */ + protected function getDefaultHeaders(): array + { + $userAgent = sprintf('FireflyIII v%s', config('firefly.version')); + + return [ + 'Client-Id' => $this->getClientId(), + 'Service-Secret' => $this->getServiceSecret(), + 'Accept' => 'application/json', + 'Content-type' => 'application/json', + 'Cache-Control' => 'no-cache', + 'User-Agent' => $userAgent, + 'Expires-at' => $this->expiresAt, + ]; + } + + /** + * @param string $uri + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedBunqDelete(string $uri, array $headers): array + { + if (0 === strlen($this->server)) { + 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 = intval($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 + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('post', $uri, $headers, $body); + $headers['X-Bunq-Client-Signature'] = $signature; + try { + $response = Requests::post($fullUri, $headers, $body); + } 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 = intval($response->status_code); + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + 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 + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedSpectreGet(string $uri, array $data): array + { + if (0 === strlen($this->server)) { + throw new FireflyException('No Spectre server defined'); + } + + $headers = $this->getDefaultHeaders(); + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('get', $fullUri, $body); + $headers['Signature'] = $signature; + + Log::debug('Final headers for spectre signed get request:', $headers); + try { + $response = Requests::get($fullUri, $headers); + } catch (Requests_Exception $e) { + throw new FireflyException(sprintf('Request Exception: %s', $e->getMessage())); + } + $statusCode = intval($response->status_code); + + if ($statusCode !== 200) { + throw new FireflyException(sprintf('Status code %d: %s', $statusCode, $response->body)); + } + + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + 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; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + */ + protected function sendUnsignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + try { + $response = Requests::post($fullUri, $headers, $body); + } 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; + } +} diff --git a/app/Support/Import/Prerequisites/SpectrePrerequisites.php b/app/Support/Import/Prerequisites/SpectrePrerequisites.php new file mode 100644 index 0000000000..a55c0f87b6 --- /dev/null +++ b/app/Support/Import/Prerequisites/SpectrePrerequisites.php @@ -0,0 +1,229 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Prerequisites; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Jobs\GetSpectreProviders; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Preference; +use FireflyIII\User; +use Illuminate\Http\Request; +use Illuminate\Support\MessageBag; +use Log; +use Preferences; +use Requests; +use Requests_Exception; + +/** + * This class contains all the routines necessary to connect to Bunq. + */ +class SpectrePrerequisites implements PrerequisitesInterface +{ + /** @var User */ + private $user; + + /** + * Returns view name that allows user to fill in prerequisites. Currently asks for the API key. + * + * @return string + */ + public function getView(): string + { + return 'import.spectre.prerequisites'; + } + + /** + * Returns any values required for the prerequisites-view. + * + * @return array + */ + public function getViewParameters(): array + { + $publicKey = $this->getPublicKey(); + $subTitle = strval(trans('bank.spectre_title')); + $subTitleIcon = 'fa-archive'; + + return compact('publicKey', 'subTitle', 'subTitleIcon'); + } + + /** + * Returns if this import method has any special prerequisites such as config + * variables or other things. The only thing we verify is the presence of the API key. Everything else + * tumbles into place: no installation token? Will be requested. No device server? Will be created. Etc. + * + * @return bool + */ + public function hasPrerequisites(): bool + { + $values = [ + Preferences::getForUser($this->user, 'spectre_client_id', false), + Preferences::getForUser($this->user, 'spectre_app_secret', false), + Preferences::getForUser($this->user, 'spectre_service_secret', false), + ]; + /** @var Preference $value */ + foreach ($values as $value) { + if (false === $value->data || null === $value->data) { + Log::info(sprintf('Config var "%s" is missing.', $value->name)); + + return true; + } + } + Log::debug('All prerequisites are here!'); + + // at this point, check if all providers are present. Providers are shared amongst + // users in a multi-user environment. + GetSpectreProviders::dispatch($this->user); + + return false; + } + + /** + * 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; + + return; + } + + /** + * This method responds to the user's submission of an API key. It tries to register this instance as a new Firefly III device. + * If this fails, the error is returned in a message bag and the user is notified (this is fairly friendly). + * + * @param Request $request + * + * @return MessageBag + */ + public function storePrerequisites(Request $request): MessageBag + { + Log::debug('Storing Spectre API keys..'); + Preferences::setForUser($this->user, 'spectre_client_id', $request->get('client_id')); + Preferences::setForUser($this->user, 'spectre_app_secret', $request->get('app_secret')); + Preferences::setForUser($this->user, 'spectre_service_secret', $request->get('service_secret')); + Log::debug('Done!'); + + return new MessageBag; + } + + /** + * This method creates a new public/private keypair for the user. This isn't really secure, since the key is generated on the fly with + * no regards for HSM's, smart cards or other things. It would require some low level programming to get this right. But the private key + * is stored encrypted in the database so it's something. + */ + private function createKeyPair(): void + { + Log::debug('Generate new Spectre key pair for user.'); + $keyConfig = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + // Create the private and public key + $res = openssl_pkey_new($keyConfig); + + // Extract the private key from $res to $privKey + $privKey = ''; + openssl_pkey_export($res, $privKey); + + // Extract the public key from $res to $pubKey + $pubKey = openssl_pkey_get_details($res); + + Preferences::setForUser($this->user, 'spectre_private_key', $privKey); + Preferences::setForUser($this->user, 'spectre_public_key', $pubKey['key']); + Log::debug('Created key pair'); + + return; + } + + /** + * Get the private key from the users preferences. + * + * @return string + */ + private function getPrivateKey(): string + { + Log::debug('get private key'); + $preference = Preferences::getForUser($this->user, 'spectre_private_key', null); + if (null === $preference) { + Log::debug('private key is null'); + // create key pair + $this->createKeyPair(); + } + $preference = Preferences::getForUser($this->user, 'spectre_private_key', null); + Log::debug('Return private key for user'); + + return $preference->data; + } + + /** + * Get a public key from the users preferences. + * + * @return string + */ + private function getPublicKey(): string + { + Log::debug('get public key'); + $preference = Preferences::getForUser($this->user, 'spectre_public_key', null); + if (null === $preference) { + Log::debug('public key is null'); + // create key pair + $this->createKeyPair(); + } + $preference = Preferences::getForUser($this->user, 'spectre_public_key', null); + Log::debug('Return public key for user'); + + return $preference->data; + } + + /** + * Request users server remote IP. Let's assume this value will not change any time soon. + * + * @return string + * + * @throws FireflyException + */ + private function getRemoteIp(): string + { + $preference = Preferences::getForUser($this->user, 'external_ip', null); + if (null === $preference) { + try { + $response = Requests::get('https://api.ipify.org'); + } catch (Requests_Exception $e) { + throw new FireflyException(sprintf('Could not retrieve external IP: %s', $e->getMessage())); + } + if (200 !== $response->status_code) { + throw new FireflyException(sprintf('Could not retrieve external IP: %d %s', $response->status_code, $response->body)); + } + $serverIp = $response->body; + Preferences::setForUser($this->user, 'external_ip', $serverIp); + + return $serverIp; + } + + return $preference->data; + } + +} diff --git a/config/firefly.php b/config/firefly.php index 744d82cfc3..b4219692d4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -28,39 +28,49 @@ declare(strict_types=1); */ return [ - 'configuration' => [ + 'configuration' => [ 'single_user_mode' => true, 'is_demo_site' => false, ], - 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'version' => '4.6.11.1', - 'maxUploadSize' => 15242880, - 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf','text/plain'], - 'list_length' => 10, - 'export_formats' => [ + 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), + 'version' => '4.6.11.1', + 'maxUploadSize' => 15242880, + 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf', 'text/plain'], + 'list_length' => 10, + 'export_formats' => [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], - 'import_formats' => [ + 'import_formats' => [ 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', ], - 'import_configurators' => [ + 'import_configurators' => [ 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', ], - 'import_processors' => [ + 'import_processors' => [ 'csv' => 'FireflyIII\Import\FileProcessor\CsvProcessor', ], - 'import_pre' => [ - 'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites', + 'import_pre' => [ + 'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites', + 'spectre' => 'FireflyIII\Support\Import\Prerequisites\SpectrePrerequisites', + 'plaid' => 'FireflyIII\Support\Import\Prerequisites\PlairPrerequisites', ], - 'import_info' => [ - 'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation', + 'import_info' => [ + 'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation', + 'spectre' => 'FireflyIII\Support\Import\Information\SpectreInformation', + 'plaid' => 'FireflyIII\Support\Import\Information\PlaidInformation', ], - 'import_transactions' => [ - 'bunq' => 'FireflyIII\Support\Import\Transactions\BunqTransactions', + 'import_transactions' => [ + 'bunq' => 'FireflyIII\Support\Import\Transactions\BunqTransactions', + 'spectre' => 'FireflyIII\Support\Import\Transactions\SpectreTransactions', + 'plaid' => 'FireflyIII\Support\Import\Transactions\PlaidTransactions', ], - 'bunq' => [ + 'bunq' => [ 'server' => 'https://sandbox.public.api.bunq.com', ], + 'spectre' => [ + 'server' => 'https://www.saltedge.com', + ], + 'default_export_format' => 'csv', 'default_import_format' => 'csv', 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], diff --git a/database/migrations/2017_12_09_111046_changes_for_spectre.php b/database/migrations/2017_12_09_111046_changes_for_spectre.php new file mode 100644 index 0000000000..ff25ce1aef --- /dev/null +++ b/database/migrations/2017_12_09_111046_changes_for_spectre.php @@ -0,0 +1,47 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + //'spectre_id', 'code', 'mode', 'name', 'status', 'interactive', 'automatic_fetch', 'country_code', 'data' + $table->integer('spectre_id', false, true); + $table->string('code', 100); + $table->string('mode', 20); + $table->string('status', 20); + $table->boolean('interactive')->default(0); + $table->boolean('automatic_fetch')->default(0); + $table->string('country_code', 3); + $table->text('data'); + } + ); + } + } +} diff --git a/public/images/logos/bunq.png b/public/images/logos/bunq.png index c1223bc4fc2ad3cbc6fda0603369137f43bc60fd..fad92342b79678a336fa1396060bc0d3f44d4cad 100644 GIT binary patch delta 2453 zcmZ`*dpOgJ8#kBCq7-r$lKZhuF2g9AyWfb(k`80bT$mlJp>pUThy2`0 zC}B$dLavQMWNx|Jil6hxd7ksnc|XtlzMtpwd7t-r|9rbeCuLQBb3xmRLF6EOe0*Z| zb|}}wj{GaZM-Hp$k1CG?1G&tm}L+&FW#MBiK2N^NV z1+IG;!H`e_J>a^jO8}h^d<7M7xQH4MLb8A;{~buYpDFdGZy>>s_7C;YMN;tBP2C8g z(0|Ct!+ZXoW>$bsA2)^To5B#v`mn!<35+=kGC66Cgwk)2O)pXiI9dn=6A}{4Y!xs9 z(yqow@$rG(>`|5&qR{eH1{pIfdp?~vYO5q}#|M8~3GyVK$S<>#wMvLPCMs8IADpTE z<)P2BM?$BoI>xo#hAi{%qBwjI_a~3y<6X_%{FM>P1fSP-;A+8$1VMp5b-Z!w&dT%o z2QlFm%>>tkvop+uKdna1m9`&)$jUwBv%x%^bno45Z5z@g~yZ8j@#eR5@t?Ddne- zb#{+#Y4bW~xhR?4Q(F4HuG(Y|MZ$6-nA!;~*5g!1jbMupA@$Y9K|vd*yM`F)zDM$R zEIgYOmb#tetV0S}L5LW-yC$8W1k=wKQDEUebEUZ78i76b-SF5lQ0&pkD~A4Y8obIL zr-9UmEA)=*7xd&JQTVSTKEQLo$6rJShWLG3TNehyeZQ+!!^kfM-7F)?t1fe&T-_VOo)ev)B5gBQ4_s%G{o!e_Rh^zBN<+wCaF zPl?QgKJGVC(jp;7F3IC5=vT8TUAOX34rxU0CY$0XZzfo!2+;0EplgW17|-Io zX8Q*lQ0iQOrmxTM=Yn0eYbDc-pbCNc$$~;^M^1?jPt3@Wj~9s6SyD9Ze-i5$vy-Rs zw+oRU?(I>H-8hJO%@l_A`2)&Sf5*k*b~S{s&^i92oji|$6|nMR*HBf|N57f&Yu@;J zujNmm&abh*1qod#2RNxcUr(hspP}w+9{+j!Xk63Oi>F&`M(%4`hIrGB;fnJWokawh zyZ184itp!c3zH++sIp@P<=+;=Ad@~P8jzlWHeS-EeLAA?C{mhQ?OSDbtb)Y&tz7g;i0z!}{j)b@wyAjd$zb!m3= z&a{UA&gxue$2Cunoz<|A(VCfd|00pP%b44r_XnQH`1XP$ECw_SFUM)Lk29{V3Loq#pr|?Ybr4g7W zX-@A-b-{<+n)6@$o}o|rI8~`cbr=IunAX{bP1{P2kaQ0`1*fKd)^U<$cMX#QHxQJ# z)zsntX!_L-@y#ul?Efk##Od%C(DDqA*;|aIX5c7a%>c(!g{ghx#q>5Pn6WFKMFNpA zm)7V)_42h`Xd55e+I!fbS@vED**so=F9xQnzmy!8zhHJ(Ie{(-ll+9Q@nCj%s%th7 zmxGqL*X(@cJ|+1il&r7+5##iFKY@;Y@nrvgQ|D-gj|&~#KCu&e(=+(Z{+pJAIV;#^ zv<}7!AZFum55=cb#G7tI2J0%wRuUGdj>QE{q|*=Z+nN<>=(I@J!Msray2#P9_>s|% z!l#!UH{@w3w|dB2vy5zClp3hvqvedv_KN`p>@OKmTzkXi_cP0PI0^VjYr#%J!dyHYqtb3{^po4lmz?e{u=pe ziu+icmjwxFV(f%SjwuMw2W6G2;T*eILwf9uAMe;Lm?l;K|J=!?d!oY%6@Ru|ZjQua z3yKXZ1nM-JAvo2O@a8vM2ilD;uLGyI6GVO_w&d zd{=DYyGX&o_6XNDhR7PP4O4l$2rMoa4l6;DoMaiI?pFG~os$MFZILm*x*-Ks@Cbpb zf@4@TNpKssk`q@k)YU@u0O+cw`u{zuu|5X=`LECNPa1XzIohL4wKnJ;r;>C3h8LlwC$iS|daD8T@L5`5=O!&Y)yRy{xz}A<`ajO>ousAJdh%8!CqO4J!%*Mb z@O!Ck4;pdgZq`DQc{*oNybvyczF$2yqAc;#ODU~yrDrj%#Fp^3Lpm0g;)+2dY-dPkwDP(2fBUR~(f|Me delta 766 zcmVDb0b7^mGlQ#r5fBrxCb^rhZI7vi7R7efYl)Fz9Q51)FQHUlgXaSvpC^XVh zLQh2}-Bws&<)1-AYhh#Ptd-8nMiRm!p}|H7quFdC35npE_+GznGKXAl7GpWT{(6Jr?uf52H(R^<)KK{y55 zL*ef?6^HOG8nV*y#x+I(`Zrd&fF`5USI~wylLht@h*ciJeh051*BTWaFi2<_qk!<| z*oTlx-^M5)`swE>9Dw$DgB@PLA}ql+ShoV|4t$0^e=@D}Z{Mk^{&DrcL)n5yU_e)L z0YdtAoUx2)Y;&+Ke>gW*sZ8beY{5@>0B*wx>_V=WROs+NT!#bUIcF&H1HsA-u3b;7$IK> wCuTnS^T@n`+G?K6oIrVtYn!Esx!kqZUq|$!f?nC_Pyhe`07*qoM6N<$f;yU2RR910 diff --git a/public/images/logos/csv.png b/public/images/logos/csv.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717761541d39bac7ecc7f280498896a2ddfa6b GIT binary patch literal 3515 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 literal 0 HcmV?d00001 diff --git a/public/images/logos/plaid.png b/public/images/logos/plaid.png new file mode 100644 index 0000000000000000000000000000000000000000..23187ce8aa1f2923bd3517e1416ecfb6af29df96 GIT binary patch literal 8326 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 J{331~_&+?Cnk@hT literal 0 HcmV?d00001 diff --git a/public/images/logos/spectre.png b/public/images/logos/spectre.png new file mode 100644 index 0000000000000000000000000000000000000000..6c2189e96c34f3a4263953d0801a93fc4792eccf GIT binary patch literal 3446 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 'Prerequisites for an import from bunq', - 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', + 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + + // Spectre: + 'spectre_title' => 'Import using Spectre', + 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', + 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', + 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', ]; diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 6d7a98c2fe..9e3af6052c 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -186,6 +186,10 @@ return [ 'csv_delimiter' => 'CSV field delimiter', 'csv_import_account' => 'Default import account', 'csv_config' => 'CSV import configuration', + 'client_id' => 'Client ID', + 'service_secret' => 'Service secret', + 'app_secret' => 'App secret', + 'public_key' => 'Public key', 'due_date' => 'Due date', diff --git a/resources/views/import/index.twig b/resources/views/import/index.twig index 8e2876dc52..bb941e7c1c 100644 --- a/resources/views/import/index.twig +++ b/resources/views/import/index.twig @@ -20,21 +20,34 @@
-
-

+

+
{# bunq import #} - {# - + bunq
Import from bunq
- #} -

+
+
+ {# import from Spectre #} + + Spectre
+ Import using Spectre +
+
+
+ {# import from Plaid #} + + Plaid
+ Import using Plaid +
+
diff --git a/resources/views/import/spectre/prerequisites.twig b/resources/views/import/spectre/prerequisites.twig new file mode 100644 index 0000000000..1b225e6a12 --- /dev/null +++ b/resources/views/import/spectre/prerequisites.twig @@ -0,0 +1,58 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('bank.spectre_prerequisites_title') }}

+
+
+
+
+

+ {{ trans('bank.spectre_prerequisites_text')|raw }} +

+
+
+ +
+
+ {{ ExpandedForm.text('client_id') }} + {{ ExpandedForm.text('service_secret') }} + {{ ExpandedForm.text('app_secret') }} +
+
+
+
+

{{ trans('bank.spectre_enter_pub_key')|raw }}

+
+ + +
+ +
+
+
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %} From e488d7d84c1eeec153dff47aac09ad7dd9727e2d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 9 Dec 2017 19:13:00 +0100 Subject: [PATCH 3/5] More code for Spectre import. --- .../Controllers/Import/BankController.php | 81 +---- .../Configurator/SpectreConfigurator.php | 178 ++++++++++ .../Configuration/Spectre/SelectBank.php | 76 ++++ .../Configuration/Spectre/SelectCountry.php | 329 ++++++++++++++++++ .../Import/Information/SpectreInformation.php | 207 +++++++++++ .../Prerequisites/SpectrePrerequisites.php | 57 +-- config/firefly.php | 4 +- resources/lang/en_US/bank.php | 14 +- resources/lang/en_US/form.php | 1 + .../views/import/spectre/select-bank.twig | 42 +++ .../views/import/spectre/select-country.twig | 42 +++ routes/web.php | 5 +- 12 files changed, 910 insertions(+), 126 deletions(-) create mode 100644 app/Import/Configurator/SpectreConfigurator.php create mode 100644 app/Support/Import/Configuration/Spectre/SelectBank.php create mode 100644 app/Support/Import/Configuration/Spectre/SelectCountry.php create mode 100644 app/Support/Import/Information/SpectreInformation.php create mode 100644 resources/views/import/spectre/select-bank.twig create mode 100644 resources/views/import/spectre/select-country.twig diff --git a/app/Http/Controllers/Import/BankController.php b/app/Http/Controllers/Import/BankController.php index fe4e6790ce..0bf671d4dc 100644 --- a/app/Http/Controllers/Import/BankController.php +++ b/app/Http/Controllers/Import/BankController.php @@ -24,7 +24,7 @@ namespace FireflyIII\Http\Controllers\Import; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Support\Import\Information\InformationInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface; use Illuminate\Http\Request; use Log; @@ -32,72 +32,27 @@ 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. - * - * @param string $bank - * - * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View - */ - public function form(string $bank) - { - $class = config(sprintf('firefly.import_pre.%s', $bank)); - if(!class_exists($class)) { - throw new FireflyException(sprintf('Cannot find class %s', $class)); - } - /** @var PrerequisitesInterface $object */ - $object = app($class); - $object->setUser(auth()->user()); - - if ($object->hasPrerequisites()) { - return redirect(route('import.bank.prerequisites', [$bank])); - } - $class = config(sprintf('firefly.import_info.%s', $bank)); - /** @var InformationInterface $object */ - $object = app($class); - $object->setUser(auth()->user()); - $remoteAccounts = $object->getAccounts(); - - return view('import.bank.form', compact('remoteAccounts', 'bank')); - } /** - * With the information given in the submitted form Firefly III will call upon the bank's classes to return transaction - * information as requested. The user will be able to map unknown data and continue. Or maybe, it's put into some kind of - * fake CSV file and forwarded to the import routine. + * Once there are no prerequisites, this method will create an importjob object and + * redirect the user to a view where this object can be used by a bank specific + * class to process. * - * @param Request $request - * @param string $bank + * @param ImportJobRepositoryInterface $repository + * @param string $bank * * @return \Illuminate\Http\RedirectResponse|null + * @throws FireflyException */ - public function postForm(Request $request, string $bank) + public function createJob(ImportJobRepositoryInterface $repository, string $bank) { $class = config(sprintf('firefly.import_pre.%s', $bank)); - if(!class_exists($class)) { + if (!class_exists($class)) { throw new FireflyException(sprintf('Cannot find class %s', $class)); } - /** @var PrerequisitesInterface $object */ - $object = app($class); - $object->setUser(auth()->user()); + $importJob = $repository->create($bank); - if ($object->hasPrerequisites()) { - return redirect(route('import.bank.prerequisites', [$bank])); - } - $remoteAccounts = $request->get('do_import'); - if (!is_array($remoteAccounts) || 0 === count($remoteAccounts)) { - Session::flash('error', 'Must select accounts'); - - return redirect(route('import.bank.form', [$bank])); - } - $remoteAccounts = array_keys($remoteAccounts); - $class = config(sprintf('firefly.import_pre.%s', $bank)); - // get import file - unset($remoteAccounts, $class); - - // get import config + return redirect(route('import.bank.configure', [$bank, $importJob->key])); } /** @@ -112,12 +67,13 @@ class BankController extends Controller * @param string $bank * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws FireflyException */ public function postPrerequisites(Request $request, string $bank) { Log::debug(sprintf('Now in postPrerequisites for %s', $bank)); $class = config(sprintf('firefly.import_pre.%s', $bank)); - if(!class_exists($class)) { + if (!class_exists($class)) { throw new FireflyException(sprintf('Cannot find class %s', $class)); } /** @var PrerequisitesInterface $object */ @@ -126,7 +82,7 @@ class BankController extends Controller if (!$object->hasPrerequisites()) { Log::debug(sprintf('No more prerequisites for %s, move to form.', $bank)); - return redirect(route('import.bank.form', [$bank])); + return redirect(route('import.bank.create-job', [$bank])); } Log::debug('Going to store entered preprerequisites.'); // store post data @@ -138,7 +94,7 @@ class BankController extends Controller return redirect(route('import.bank.prerequisites', [$bank])); } - return redirect(route('import.bank.form', [$bank])); + return redirect(route('import.bank.create-job', [$bank])); } /** @@ -148,11 +104,12 @@ class BankController extends Controller * @param string $bank * * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View + * @throws FireflyException */ public function prerequisites(string $bank) { $class = config(sprintf('firefly.import_pre.%s', $bank)); - if(!class_exists($class)) { + if (!class_exists($class)) { throw new FireflyException(sprintf('Cannot find class %s', $class)); } /** @var PrerequisitesInterface $object */ @@ -161,12 +118,12 @@ class BankController extends Controller if ($object->hasPrerequisites()) { $view = $object->getView(); - $parameters = ['title' => strval(trans('firefly.import_index_title')),'mainTitleIcon' => 'fa-archive']; + $parameters = ['title' => strval(trans('firefly.import_index_title')), 'mainTitleIcon' => 'fa-archive']; $parameters = $object->getViewParameters() + $parameters; return view($view, $parameters); } - return redirect(route('import.bank.form', [$bank])); + return redirect(route('import.bank.create-job', [$bank])); } } diff --git a/app/Import/Configurator/SpectreConfigurator.php b/app/Import/Configurator/SpectreConfigurator.php new file mode 100644 index 0000000000..9a8d295101 --- /dev/null +++ b/app/Import/Configurator/SpectreConfigurator.php @@ -0,0 +1,178 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Import\Configurator; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Support\Import\Configuration\ConfigurationInterface; +use FireflyIII\Support\Import\Configuration\Spectre\SelectBank; +use FireflyIII\Support\Import\Configuration\Spectre\SelectCountry; +use Log; + +/** + * Class SpectreConfigurator. + */ +class SpectreConfigurator implements ConfiguratorInterface +{ + /** @var ImportJob */ + private $job; + + /** @var string */ + private $warning = ''; + + /** + * ConfiguratorInterface constructor. + */ + public function __construct() + { + } + + /** + * Store any data from the $data array into the job. + * + * @param array $data + * + * @return bool + * + * @throws FireflyException + */ + public function configureJob(array $data): bool + { + $class = $this->getConfigurationClass(); + $job = $this->job; + /** @var ConfigurationInterface $object */ + $object = new $class($this->job); + $object->setJob($job); + $result = $object->storeConfiguration($data); + $this->warning = $object->getWarningMessage(); + + return $result; + } + + /** + * Return the data required for the next step in the job configuration. + * + * @return array + * + * @throws FireflyException + */ + public function getNextData(): array + { + $class = $this->getConfigurationClass(); + $job = $this->job; + /** @var ConfigurationInterface $object */ + $object = app($class); + $object->setJob($job); + + return $object->getData(); + } + + /** + * @return string + * + * @throws FireflyException + */ + public function getNextView(): string + { + if (!$this->job->configuration['selected-country']) { + return 'import.spectre.select-country'; + } + if (!$this->job->configuration['selected-bank']) { + return 'import.spectre.select-bank'; + } + + throw new FireflyException('No view for state'); + } + + /** + * Return possible warning to user. + * + * @return string + */ + public function getWarningMessage(): string + { + return $this->warning; + } + + /** + * @return bool + */ + public function isJobConfigured(): bool + { + $config = $this->job->configuration; + $config['selected-country'] = $config['selected-country'] ?? false; + $config['selected-bank'] = $config['selected-bank'] ?? false; + $this->job->configuration = $config; + $this->job->save(); + + if ($config['selected-country'] && $config['selected-bank'] && false) { + return true; + } + + return false; + } + + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + if (null === $this->job->configuration || 0 === count($this->job->configuration)) { + Log::debug(sprintf('Gave import job %s initial configuration.', $this->job->key)); + $this->job->configuration = [ + 'selected-country' => false, + ]; + $this->job->save(); + } + } + + /** + * @return string + * + * @throws FireflyException + */ + private function getConfigurationClass(): string + { + $class = false; + switch (true) { + case !$this->job->configuration['selected-country']: + $class = SelectCountry::class; + break; + case !$this->job->configuration['selected-bank']: + $class = SelectBank::class; + break; + default: + break; + } + + if (false === $class || 0 === strlen($class)) { + throw new FireflyException('Cannot handle current job state in getConfigurationClass().'); + } + if (!class_exists($class)) { + throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class)); + } + + return $class; + } +} diff --git a/app/Support/Import/Configuration/Spectre/SelectBank.php b/app/Support/Import/Configuration/Spectre/SelectBank.php new file mode 100644 index 0000000000..1a9622f710 --- /dev/null +++ b/app/Support/Import/Configuration/Spectre/SelectBank.php @@ -0,0 +1,76 @@ +job->configuration; + $selection = SpectreProvider::where('country_code', $config['country'])->where('status', 'active')->get(); + $providers = []; + /** @var SpectreProvider $provider */ + foreach ($selection as $provider) { + $providerId = $provider->spectre_id; + $name = $provider->data['name']; + $providers[$providerId] = $name; + } + + return compact('providers'); + } + + /** + * Return possible warning to user. + * + * @return string + */ + public function getWarningMessage(): string + { + return ''; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + $config = $this->job->configuration; + $config['bank'] = intval($data['bank_code']) ?? 0; // default to fake country. + $config['selected-bank'] = true; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } +} \ No newline at end of file diff --git a/app/Support/Import/Configuration/Spectre/SelectCountry.php b/app/Support/Import/Configuration/Spectre/SelectCountry.php new file mode 100644 index 0000000000..de49d9545d --- /dev/null +++ b/app/Support/Import/Configuration/Spectre/SelectCountry.php @@ -0,0 +1,329 @@ + 'Afghanistan', + 'AX' => 'Aland Islands', + 'AL' => 'Albania', + 'DZ' => 'Algeria', + 'AS' => 'American Samoa', + 'AD' => 'Andorra', + 'AO' => 'Angola', + 'AI' => 'Anguilla', + 'AQ' => 'Antarctica', + 'AG' => 'Antigua and Barbuda', + 'AR' => 'Argentina', + 'AM' => 'Armenia', + 'AW' => 'Aruba', + 'AU' => 'Australia', + 'AT' => 'Austria', + 'AZ' => 'Azerbaijan', + 'BS' => 'Bahamas', + 'BH' => 'Bahrain', + 'BD' => 'Bangladesh', + 'BB' => 'Barbados', + 'BY' => 'Belarus', + 'BE' => 'Belgium', + 'BZ' => 'Belize', + 'BJ' => 'Benin', + 'BM' => 'Bermuda', + 'BT' => 'Bhutan', + 'BO' => 'Bolivia', + 'BQ' => 'Bonaire, Saint Eustatius and Saba', + 'BA' => 'Bosnia and Herzegovina', + 'BW' => 'Botswana', + 'BV' => 'Bouvet Island', + 'BR' => 'Brazil', + 'IO' => 'British Indian Ocean Territory', + 'VG' => 'British Virgin Islands', + 'BN' => 'Brunei', + 'BG' => 'Bulgaria', + 'BF' => 'Burkina Faso', + 'BI' => 'Burundi', + 'KH' => 'Cambodia', + 'CM' => 'Cameroon', + 'CA' => 'Canada', + 'CV' => 'Cape Verde', + 'KY' => 'Cayman Islands', + 'CF' => 'Central African Republic', + 'TD' => 'Chad', + 'CL' => 'Chile', + 'CN' => 'China', + 'CX' => 'Christmas Island', + 'CC' => 'Cocos Islands', + 'CO' => 'Colombia', + 'KM' => 'Comoros', + 'CK' => 'Cook Islands', + 'CR' => 'Costa Rica', + 'HR' => 'Croatia', + 'CU' => 'Cuba', + 'CW' => 'Curacao', + 'CY' => 'Cyprus', + 'CZ' => 'Czech Republic', + 'CD' => 'Democratic Republic of the Congo', + 'DK' => 'Denmark', + 'DJ' => 'Djibouti', + 'DM' => 'Dominica', + 'DO' => 'Dominican Republic', + 'TL' => 'East Timor', + 'EC' => 'Ecuador', + 'EG' => 'Egypt', + 'SV' => 'El Salvador', + 'GQ' => 'Equatorial Guinea', + 'ER' => 'Eritrea', + 'EE' => 'Estonia', + 'ET' => 'Ethiopia', + 'FK' => 'Falkland Islands', + 'FO' => 'Faroe Islands', + 'FJ' => 'Fiji', + 'FI' => 'Finland', + 'FR' => 'France', + 'GF' => 'French Guiana', + 'PF' => 'French Polynesia', + 'TF' => 'French Southern Territories', + 'GA' => 'Gabon', + 'GM' => 'Gambia', + 'GE' => 'Georgia', + 'DE' => 'Germany', + 'GH' => 'Ghana', + 'GI' => 'Gibraltar', + 'GR' => 'Greece', + 'GL' => 'Greenland', + 'GD' => 'Grenada', + 'GP' => 'Guadeloupe', + 'GU' => 'Guam', + 'GT' => 'Guatemala', + 'GG' => 'Guernsey', + 'GN' => 'Guinea', + 'GW' => 'Guinea-Bissau', + 'GY' => 'Guyana', + 'HT' => 'Haiti', + 'HM' => 'Heard Island and McDonald Islands', + 'HN' => 'Honduras', + 'HK' => 'Hong Kong', + 'HU' => 'Hungary', + 'IS' => 'Iceland', + 'IN' => 'India', + 'ID' => 'Indonesia', + 'IR' => 'Iran', + 'IQ' => 'Iraq', + 'IE' => 'Ireland', + 'IM' => 'Isle of Man', + 'IL' => 'Israel', + 'IT' => 'Italy', + 'CI' => 'Ivory Coast', + 'JM' => 'Jamaica', + 'JP' => 'Japan', + 'JE' => 'Jersey', + 'JO' => 'Jordan', + 'KZ' => 'Kazakhstan', + 'KE' => 'Kenya', + 'KI' => 'Kiribati', + 'XK' => 'Kosovo', + 'KW' => 'Kuwait', + 'KG' => 'Kyrgyzstan', + 'LA' => 'Laos', + 'LV' => 'Latvia', + 'LB' => 'Lebanon', + 'LS' => 'Lesotho', + 'LR' => 'Liberia', + 'LY' => 'Libya', + 'LI' => 'Liechtenstein', + 'LT' => 'Lithuania', + 'LU' => 'Luxembourg', + 'MO' => 'Macao', + 'MK' => 'Macedonia', + 'MG' => 'Madagascar', + 'MW' => 'Malawi', + 'MY' => 'Malaysia', + 'MV' => 'Maldives', + 'ML' => 'Mali', + 'MT' => 'Malta', + 'MH' => 'Marshall Islands', + 'MQ' => 'Martinique', + 'MR' => 'Mauritania', + 'MU' => 'Mauritius', + 'YT' => 'Mayotte', + 'MX' => 'Mexico', + 'FM' => 'Micronesia', + 'MD' => 'Moldova', + 'MC' => 'Monaco', + 'MN' => 'Mongolia', + 'ME' => 'Montenegro', + 'MS' => 'Montserrat', + 'MA' => 'Morocco', + 'MZ' => 'Mozambique', + 'MM' => 'Myanmar', + 'NA' => 'Namibia', + 'NR' => 'Nauru', + 'NP' => 'Nepal', + 'NL' => 'Netherlands', + 'NC' => 'New Caledonia', + 'NZ' => 'New Zealand', + 'NI' => 'Nicaragua', + 'NE' => 'Niger', + 'NG' => 'Nigeria', + 'NU' => 'Niue', + 'NF' => 'Norfolk Island', + 'KP' => 'North Korea', + 'MP' => 'Northern Mariana Islands', + 'NO' => 'Norway', + 'OM' => 'Oman', + 'PK' => 'Pakistan', + 'PW' => 'Palau', + 'PS' => 'Palestinian Territory', + 'PA' => 'Panama', + 'PG' => 'Papua New Guinea', + 'PY' => 'Paraguay', + 'PE' => 'Peru', + 'PH' => 'Philippines', + 'PN' => 'Pitcairn', + 'PL' => 'Poland', + 'PT' => 'Portugal', + 'PR' => 'Puerto Rico', + 'QA' => 'Qatar', + 'CG' => 'Republic of the Congo', + 'RE' => 'Reunion', + 'RO' => 'Romania', + 'RU' => 'Russia', + 'RW' => 'Rwanda', + 'BL' => 'Saint Barthelemy', + 'SH' => 'Saint Helena', + 'KN' => 'Saint Kitts and Nevis', + 'LC' => 'Saint Lucia', + 'MF' => 'Saint Martin', + 'PM' => 'Saint Pierre and Miquelon', + 'VC' => 'Saint Vincent and the Grenadines', + 'WS' => 'Samoa', + 'SM' => 'San Marino', + 'ST' => 'Sao Tome and Principe', + 'SA' => 'Saudi Arabia', + 'SN' => 'Senegal', + 'RS' => 'Serbia', + 'SC' => 'Seychelles', + 'SL' => 'Sierra Leone', + 'SG' => 'Singapore', + 'SX' => 'Sint Maarten', + 'SK' => 'Slovakia', + 'SI' => 'Slovenia', + 'SB' => 'Solomon Islands', + 'SO' => 'Somalia', + 'ZA' => 'South Africa', + 'GS' => 'South Georgia and the South Sandwich Islands', + 'KR' => 'South Korea', + 'SS' => 'South Sudan', + 'ES' => 'Spain', + 'LK' => 'Sri Lanka', + 'SD' => 'Sudan', + 'SR' => 'Suriname', + 'SJ' => 'Svalbard and Jan Mayen', + 'SZ' => 'Swaziland', + 'SE' => 'Sweden', + 'CH' => 'Switzerland', + 'SY' => 'Syria', + 'TW' => 'Taiwan', + 'TJ' => 'Tajikistan', + 'TZ' => 'Tanzania', + 'TH' => 'Thailand', + 'TG' => 'Togo', + 'TK' => 'Tokelau', + 'TO' => 'Tonga', + 'TT' => 'Trinidad and Tobago', + 'TN' => 'Tunisia', + 'TR' => 'Turkey', + 'TM' => 'Turkmenistan', + 'TC' => 'Turks and Caicos Islands', + 'TV' => 'Tuvalu', + 'VI' => 'U.S. Virgin Islands', + 'UG' => 'Uganda', + 'UA' => 'Ukraine', + 'AE' => 'United Arab Emirates', + 'GB' => 'United Kingdom', + 'US' => 'United States', + 'UM' => 'United States Minor Outlying Islands', + 'UY' => 'Uruguay', + 'UZ' => 'Uzbekistan', + 'VU' => 'Vanuatu', + 'VA' => 'Vatican', + 'VE' => 'Venezuela', + 'VN' => 'Vietnam', + 'WF' => 'Wallis and Futuna', + 'EH' => 'Western Sahara', + 'YE' => 'Yemen', + 'ZM' => 'Zambia', + 'ZW' => 'Zimbabwe', + 'XF' => 'Fake Country (for testing)', + 'XO' => 'Other financial applications', + ]; + /** @var ImportJob */ + private $job; + + /** + * Get the data necessary to show the configuration screen. + * + * @return array + */ + public function getData(): array + { + $providers = SpectreProvider::get(); + $countries = []; + /** @var SpectreProvider $provider */ + foreach ($providers as $provider) { + $countries[$provider->country_code] = $this->allCountries[$provider->country_code] ?? $provider->country_code; + } + asort($countries); + + return compact('countries'); + } + + /** + * Return possible warning to user. + * + * @return string + */ + public function getWarningMessage(): string + { + return ''; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + $config = $this->job->configuration; + $config['country'] = $data['country_code'] ?? 'XF'; // default to fake country. + $config['selected-country'] = true; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } +} \ No newline at end of file diff --git a/app/Support/Import/Information/SpectreInformation.php b/app/Support/Import/Information/SpectreInformation.php new file mode 100644 index 0000000000..04a3037c3b --- /dev/null +++ b/app/Support/Import/Information/SpectreInformation.php @@ -0,0 +1,207 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Information; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Services\Bunq\Object\Alias; +use FireflyIII\Services\Bunq\Object\MonetaryAccountBank; +use FireflyIII\Services\Bunq\Request\DeleteDeviceSessionRequest; +use FireflyIII\Services\Bunq\Request\DeviceSessionRequest; +use FireflyIII\Services\Bunq\Request\ListMonetaryAccountRequest; +use FireflyIII\Services\Bunq\Request\ListUserRequest; +use FireflyIII\Services\Bunq\Token\SessionToken; +use FireflyIII\Support\CacheProperties; +use FireflyIII\User; +use Illuminate\Support\Collection; +use Log; +use Preferences; + +/** + * Class SpectreInformation + */ +class SpectreInformation 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. + * + * The format for these bank accounts is basically this: + * + * id: bank specific id + * name: bank appointed name + * number: account number (usually IBAN) + * currency: ISO code of currency + * balance: current balance + * + * + * any other fields are optional but can be useful: + * image: logo or account specific thing + * color: any associated color. + * + * @return array + */ + public function getAccounts(): array + { + // cache for an hour: + $cache = new CacheProperties; + $cache->addProperty('bunq.get-accounts'); + $cache->addProperty(date('dmy h')); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + Log::debug('Now in getAccounts()'); + $sessionToken = $this->startSession(); + $userId = $this->getUserInformation($sessionToken); + // get list of Bunq accounts: + $accounts = $this->getMonetaryAccounts($sessionToken, $userId); + $return = []; + /** @var MonetaryAccountBank $account */ + foreach ($accounts as $account) { + $current = [ + 'id' => $account->getId(), + 'name' => $account->getDescription(), + 'currency' => $account->getCurrency(), + 'balance' => $account->getBalance()->getValue(), + 'color' => $account->getSetting()->getColor(), + ]; + /** @var Alias $alias */ + foreach ($account->getAliases() as $alias) { + if ('IBAN' === $alias->getType()) { + $current['number'] = $alias->getValue(); + } + } + $return[] = $current; + } + $cache->store($return); + + $this->closeSession($sessionToken); + + return $return; + } + + /** + * 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; + $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data; + $request = new DeleteDeviceSessionRequest(); + $request->setSecret($apiKey); + $request->setPrivateKey($privateKey); + $request->setServerPublicKey($serverPublicKey); + $request->setSessionToken($sessionToken); + $request->call(); + + return; + } + + /** + * @param SessionToken $sessionToken + * @param int $userId + * + * @return Collection + */ + private function getMonetaryAccounts(SessionToken $sessionToken, int $userId): Collection + { + $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data; + $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data; + $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data; + $request = new ListMonetaryAccountRequest; + + $request->setSessionToken($sessionToken); + $request->setSecret($apiKey); + $request->setServerPublicKey($serverPublicKey); + $request->setPrivateKey($privateKey); + $request->setUserId($userId); + $request->call(); + + return $request->getMonetaryAccounts(); + } + + /** + * @param SessionToken $sessionToken + * + * @return int + * + * @throws FireflyException + */ + private function getUserInformation(SessionToken $sessionToken): int + { + $apiKey = Preferences::getForUser($this->user, 'bunq_api_key')->data; + $serverPublicKey = Preferences::getForUser($this->user, 'bunq_server_public_key')->data; + $privateKey = Preferences::getForUser($this->user, 'bunq_private_key')->data; + $request = new ListUserRequest; + $request->setSessionToken($sessionToken); + $request->setSecret($apiKey); + $request->setServerPublicKey($serverPublicKey); + $request->setPrivateKey($privateKey); + $request->call(); + // return the first that isn't null? + $company = $request->getUserCompany(); + if ($company->getId() > 0) { + return $company->getId(); + } + $user = $request->getUserPerson(); + if ($user->getId() > 0) { + return $user->getId(); + } + throw new FireflyException('Expected user or company from Bunq, but got neither.'); + } + + /** + * @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; + $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->setPrivateKey($privateKey); + $request->setInstallationToken($installationToken); + $request->call(); + $sessionToken = $request->getSessionToken(); + Log::debug(sprintf('Now have got session token: %s', serialize($sessionToken))); + + return $sessionToken; + } +} diff --git a/app/Support/Import/Prerequisites/SpectrePrerequisites.php b/app/Support/Import/Prerequisites/SpectrePrerequisites.php index a55c0f87b6..d67960b139 100644 --- a/app/Support/Import/Prerequisites/SpectrePrerequisites.php +++ b/app/Support/Import/Prerequisites/SpectrePrerequisites.php @@ -1,6 +1,6 @@ user, 'spectre_private_key', null); - if (null === $preference) { - Log::debug('private key is null'); - // create key pair - $this->createKeyPair(); - } - $preference = Preferences::getForUser($this->user, 'spectre_private_key', null); - Log::debug('Return private key for user'); - - return $preference->data; - } - /** * Get a public key from the users preferences. * @@ -197,33 +173,4 @@ class SpectrePrerequisites implements PrerequisitesInterface return $preference->data; } - - /** - * Request users server remote IP. Let's assume this value will not change any time soon. - * - * @return string - * - * @throws FireflyException - */ - private function getRemoteIp(): string - { - $preference = Preferences::getForUser($this->user, 'external_ip', null); - if (null === $preference) { - try { - $response = Requests::get('https://api.ipify.org'); - } catch (Requests_Exception $e) { - throw new FireflyException(sprintf('Could not retrieve external IP: %s', $e->getMessage())); - } - if (200 !== $response->status_code) { - throw new FireflyException(sprintf('Could not retrieve external IP: %d %s', $response->status_code, $response->body)); - } - $serverIp = $response->body; - Preferences::setForUser($this->user, 'external_ip', $serverIp); - - return $serverIp; - } - - return $preference->data; - } - } diff --git a/config/firefly.php b/config/firefly.php index b4219692d4..f0b20f60ff 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -41,10 +41,12 @@ return [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], 'import_formats' => [ - 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + 'spectre' => '', ], 'import_configurators' => [ 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + 'spectre' => 'FireflyIII\Import\Configurator\SpectreConfigurator', ], 'import_processors' => [ 'csv' => 'FireflyIII\Import\FileProcessor\CsvProcessor', diff --git a/resources/lang/en_US/bank.php b/resources/lang/en_US/bank.php index 369f72e145..1c970e1e7e 100644 --- a/resources/lang/en_US/bank.php +++ b/resources/lang/en_US/bank.php @@ -3,12 +3,14 @@ declare(strict_types=1); return [ - 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', - 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', + 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', // Spectre: - 'spectre_title' => 'Import using Spectre', - 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', - 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', - 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', + 'spectre_title' => 'Import using Spectre', + 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', + 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', + 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', + 'spectre_select_country_title' => 'Select a country', + 'spectre_select_country_text' => 'Firefly III has a large selection of banks and sites from which Spectre can download transactional data. These banks are sorted by country. Please not that there is a "Fake Country" for when you wish to test something. If you wish to import from other financial tools, please use the imaginary country called "Other financial applications". By default, Spectre only allows you to download data from fake banks. Make sure your status is "Live" on your Dashboard if you wish to download from real banks.', ]; diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 9e3af6052c..bb3f1fa635 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -190,6 +190,7 @@ return [ 'service_secret' => 'Service secret', 'app_secret' => 'App secret', 'public_key' => 'Public key', + 'country_code' => 'Country code', 'due_date' => 'Due date', diff --git a/resources/views/import/spectre/select-bank.twig b/resources/views/import/spectre/select-bank.twig new file mode 100644 index 0000000000..af0a8d56f4 --- /dev/null +++ b/resources/views/import/spectre/select-bank.twig @@ -0,0 +1,42 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('bank.spectre_select_bank_title') }}

+
+
+
+
+

+ {{ trans('bank.spectre_select_bank_title') }} +

+
+
+ +
+
+ {{ ExpandedForm.select('bank_code', data.providers, null)|raw }} +
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %} diff --git a/resources/views/import/spectre/select-country.twig b/resources/views/import/spectre/select-country.twig new file mode 100644 index 0000000000..5c4f36a199 --- /dev/null +++ b/resources/views/import/spectre/select-country.twig @@ -0,0 +1,42 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('bank.spectre_select_country_title') }}

+
+
+
+
+

+ {{ trans('bank.spectre_select_country_text') }} +

+
+
+ +
+
+ {{ ExpandedForm.select('country_code', data.countries, null)|raw }} +
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %} diff --git a/routes/web.php b/routes/web.php index 86b29cb942..ff61ef929a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -420,8 +420,9 @@ Route::group( Route::get('bank/{bank}/prerequisites', ['uses' => 'Import\BankController@prerequisites', 'as' => 'bank.prerequisites']); Route::post('bank/{bank}/prerequisites', ['uses' => 'Import\BankController@postPrerequisites', 'as' => 'bank.prerequisites.post']); - Route::get('bank/{bank}/form', ['uses' => 'Import\BankController@form', 'as' => 'bank.form']); - Route::post('bank/{bank}/form', ['uses' => 'Import\BankController@postForm', 'as' => 'bank.form.post']); + Route::get('bank/{bank}/create', ['uses' => 'Import\BankController@createJob', 'as' => 'bank.create-job']); + Route:: get('bank/{bank}/configure/{importJob}', ['uses' => 'Import\BankController@configure', 'as' => 'bank.configure']); + Route::post('bank/{bank}/configure/{importJob}', ['uses' => 'Import\BankController@postConfigure', 'as' => 'bank.configure.post']); } ); From 2365fb69b4463ea066aff8868dbc7e29bb2669ba Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 9 Dec 2017 20:02:26 +0100 Subject: [PATCH 4/5] Can handle some mandatory fields (not all). --- .../Controllers/Import/BankController.php | 2 +- .../Configurator/SpectreConfigurator.php | 27 +++-- .../Configuration/Spectre/InputMandatory.php | 100 ++++++++++++++++++ .../Configuration/Spectre/SelectCountry.php | 4 +- .../{SelectBank.php => SelectProvider.php} | 11 +- resources/lang/en_US/bank.php | 21 ++-- .../views/import/spectre/input-fields.twig | 65 ++++++++++++ .../views/import/spectre/select-country.twig | 2 +- ...{select-bank.twig => select-provider.twig} | 4 +- 9 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 app/Support/Import/Configuration/Spectre/InputMandatory.php rename app/Support/Import/Configuration/Spectre/{SelectBank.php => SelectProvider.php} (81%) create mode 100644 resources/views/import/spectre/input-fields.twig rename resources/views/import/spectre/{select-bank.twig => select-provider.twig} (91%) diff --git a/app/Http/Controllers/Import/BankController.php b/app/Http/Controllers/Import/BankController.php index 0bf671d4dc..125f8968d3 100644 --- a/app/Http/Controllers/Import/BankController.php +++ b/app/Http/Controllers/Import/BankController.php @@ -52,7 +52,7 @@ class BankController extends Controller } $importJob = $repository->create($bank); - return redirect(route('import.bank.configure', [$bank, $importJob->key])); + return redirect(route('import.file.configure', [$importJob->key])); } /** diff --git a/app/Import/Configurator/SpectreConfigurator.php b/app/Import/Configurator/SpectreConfigurator.php index 9a8d295101..da118f8363 100644 --- a/app/Import/Configurator/SpectreConfigurator.php +++ b/app/Import/Configurator/SpectreConfigurator.php @@ -25,7 +25,8 @@ namespace FireflyIII\Import\Configurator; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; use FireflyIII\Support\Import\Configuration\ConfigurationInterface; -use FireflyIII\Support\Import\Configuration\Spectre\SelectBank; +use FireflyIII\Support\Import\Configuration\Spectre\SelectProvider; +use FireflyIII\Support\Import\Configuration\Spectre\InputMandatory; use FireflyIII\Support\Import\Configuration\Spectre\SelectCountry; use Log; @@ -97,8 +98,11 @@ class SpectreConfigurator implements ConfiguratorInterface if (!$this->job->configuration['selected-country']) { return 'import.spectre.select-country'; } - if (!$this->job->configuration['selected-bank']) { - return 'import.spectre.select-bank'; + if (!$this->job->configuration['selected-provider']) { + return 'import.spectre.select-provider'; + } + if (!$this->job->configuration['has-input-mandatory']) { + return 'import.spectre.input-fields'; } throw new FireflyException('No view for state'); @@ -119,13 +123,14 @@ class SpectreConfigurator implements ConfiguratorInterface */ public function isJobConfigured(): bool { - $config = $this->job->configuration; - $config['selected-country'] = $config['selected-country'] ?? false; - $config['selected-bank'] = $config['selected-bank'] ?? false; - $this->job->configuration = $config; + $config = $this->job->configuration; + $config['selected-country'] = $config['selected-country'] ?? false; + $config['selected-provider'] = $config['selected-provider'] ?? false; + $config['has-input-mandatory'] = $config['has-input-mandatory'] ?? false; + $this->job->configuration = $config; $this->job->save(); - if ($config['selected-country'] && $config['selected-bank'] && false) { + if ($config['selected-country'] && $config['selected-provider'] && $config['has-input-mandatory'] && false) { return true; } @@ -159,9 +164,11 @@ class SpectreConfigurator implements ConfiguratorInterface case !$this->job->configuration['selected-country']: $class = SelectCountry::class; break; - case !$this->job->configuration['selected-bank']: - $class = SelectBank::class; + case !$this->job->configuration['selected-provider']: + $class = SelectProvider::class; break; + case !$this->job->configuration['has-input-mandatory']: + $class = InputMandatory::class; default: break; } diff --git a/app/Support/Import/Configuration/Spectre/InputMandatory.php b/app/Support/Import/Configuration/Spectre/InputMandatory.php new file mode 100644 index 0000000000..818f21878c --- /dev/null +++ b/app/Support/Import/Configuration/Spectre/InputMandatory.php @@ -0,0 +1,100 @@ +job->configuration; + $providerId = $config['provider']; + $provider = SpectreProvider::where('spectre_id', $providerId)->first(); + if (is_null($provider)) { + throw new FireflyException(sprintf('Cannot find Spectre provider with ID #%d', $providerId)); + } + $fields = $provider->data['required_fields'] ?? []; + $positions = []; + // Obtain a list of columns + foreach ($fields as $key => $row) { + $positions[$key] = $row['position']; + } + array_multisort($positions, SORT_ASC, $fields); + $country = SelectCountry::$allCountries[$config['country']] ?? $config['country']; + + return compact('provider', 'country', 'fields'); + } + + /** + * Return possible warning to user. + * + * @return string + */ + public function getWarningMessage(): string + { + return ''; + } + + /** + * @param ImportJob $job + * + * @return ConfigurationInterface + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + $config = $this->job->configuration; + $providerId = $config['provider']; + $provider = SpectreProvider::where('spectre_id', $providerId)->first(); + if (is_null($provider)) { + throw new FireflyException(sprintf('Cannot find Spectre provider with ID #%d', $providerId)); + } + $mandatory = []; + $fields = $provider->data['required_fields'] ?? []; + foreach ($fields as $field) { + $name = $field['name']; + $mandatory[$name] = app('crypt')->encrypt($data[$name]) ?? null; + } + + // store in config of job: + $config['mandatory-fields'] = $mandatory; + $config['has-input-mandatory'] = true; + $this->job->configuration = $config; + $this->job->save(); + + // try to grab login for this job. See what happens? + // fire job that creates login object. user is redirected to "wait here" page (status page). Page should + // refresh and go back to interactive when user is supposed to enter SMS code or something. + // otherwise start downloading stuff + + return true; + } +} \ No newline at end of file diff --git a/app/Support/Import/Configuration/Spectre/SelectCountry.php b/app/Support/Import/Configuration/Spectre/SelectCountry.php index de49d9545d..e34c15b4e7 100644 --- a/app/Support/Import/Configuration/Spectre/SelectCountry.php +++ b/app/Support/Import/Configuration/Spectre/SelectCountry.php @@ -13,7 +13,7 @@ use FireflyIII\Support\Import\Configuration\ConfigurationInterface; */ class SelectCountry implements ConfigurationInterface { - private $allCountries + public static $allCountries = [ 'AF' => 'Afghanistan', 'AX' => 'Aland Islands', @@ -282,7 +282,7 @@ class SelectCountry implements ConfigurationInterface $countries = []; /** @var SpectreProvider $provider */ foreach ($providers as $provider) { - $countries[$provider->country_code] = $this->allCountries[$provider->country_code] ?? $provider->country_code; + $countries[$provider->country_code] = self::$allCountries[$provider->country_code] ?? $provider->country_code; } asort($countries); diff --git a/app/Support/Import/Configuration/Spectre/SelectBank.php b/app/Support/Import/Configuration/Spectre/SelectProvider.php similarity index 81% rename from app/Support/Import/Configuration/Spectre/SelectBank.php rename to app/Support/Import/Configuration/Spectre/SelectProvider.php index 1a9622f710..ec41ed78c5 100644 --- a/app/Support/Import/Configuration/Spectre/SelectBank.php +++ b/app/Support/Import/Configuration/Spectre/SelectProvider.php @@ -9,9 +9,9 @@ use FireflyIII\Models\SpectreProvider; use FireflyIII\Support\Import\Configuration\ConfigurationInterface; /** - * Class SelectBank + * Class SelectProvider */ -class SelectBank implements ConfigurationInterface +class SelectProvider implements ConfigurationInterface { /** @var ImportJob */ private $job; @@ -32,8 +32,9 @@ class SelectBank implements ConfigurationInterface $name = $provider->data['name']; $providers[$providerId] = $name; } + $country = SelectCountry::$allCountries[$config['country']] ?? $config['country']; - return compact('providers'); + return compact('providers', 'country'); } /** @@ -66,8 +67,8 @@ class SelectBank implements ConfigurationInterface public function storeConfiguration(array $data): bool { $config = $this->job->configuration; - $config['bank'] = intval($data['bank_code']) ?? 0; // default to fake country. - $config['selected-bank'] = true; + $config['provider'] = intval($data['provider_code']) ?? 0; // default to fake country. + $config['selected-provider'] = true; $this->job->configuration = $config; $this->job->save(); diff --git a/resources/lang/en_US/bank.php b/resources/lang/en_US/bank.php index 1c970e1e7e..34e8812920 100644 --- a/resources/lang/en_US/bank.php +++ b/resources/lang/en_US/bank.php @@ -3,14 +3,19 @@ declare(strict_types=1); return [ - 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', - 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', + 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', // Spectre: - 'spectre_title' => 'Import using Spectre', - 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', - 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', - 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', - 'spectre_select_country_title' => 'Select a country', - 'spectre_select_country_text' => 'Firefly III has a large selection of banks and sites from which Spectre can download transactional data. These banks are sorted by country. Please not that there is a "Fake Country" for when you wish to test something. If you wish to import from other financial tools, please use the imaginary country called "Other financial applications". By default, Spectre only allows you to download data from fake banks. Make sure your status is "Live" on your Dashboard if you wish to download from real banks.', + 'spectre_title' => 'Import using Spectre', + 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', + 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', + 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', + 'spectre_select_country_title' => 'Select a country', + 'spectre_select_country_text' => 'Firefly III has a large selection of banks and sites from which Spectre can download transactional data. These banks are sorted by country. Please not that there is a "Fake Country" for when you wish to test something. If you wish to import from other financial tools, please use the imaginary country called "Other financial applications". By default, Spectre only allows you to download data from fake banks. Make sure your status is "Live" on your Dashboard if you wish to download from real banks.', + 'spectre_select_provider_title' => 'Select a bank', + 'spectre_select_provider_text' => 'Spectre supports the following banks or financial services grouped under :country. Please pick the one you wish to import from.', + 'spectre_input_fields_title' => 'Input mandatory fields', + 'spectre_input_fields_text' => 'The following fields are mandated by ":provider" (from :country).', + 'spectre_instructions_english' => 'These instructions are provided by Spectre for your convencience. They are in English:', ]; diff --git a/resources/views/import/spectre/input-fields.twig b/resources/views/import/spectre/input-fields.twig new file mode 100644 index 0000000000..de77ba5b3f --- /dev/null +++ b/resources/views/import/spectre/input-fields.twig @@ -0,0 +1,65 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('bank.spectre_input_fields_title') }}

+
+
+
+
+

+ {{ trans('bank.spectre_input_fields_text',{provider: data.provider.data.name, country: data.country})|raw }} +

+

+ {{ trans('bank.spectre_instructions_english') }} +

+

+ {{ data.provider.data.instruction|nl2br }} +

+
+
+ +
+
+ {% for field in data.fields %} + {# text, password, select, file #} + {% if field.nature == 'text' %} + {{ ExpandedForm.text(field.name,null, {label: field.english_name ~ ' ('~field.localized_name~')'}) }} + {% endif %} + {% if field.nature == 'password' %} + {{ ExpandedForm.password(field.name, {label: field.english_name ~ ' ('~field.localized_name~')'}) }} + {% endif %} + {% if field.nature == 'select' %} + DO NOT SUPPORT + {{ dump(field) }} + {% endif %} + {% if field.narture == 'file' %} + DO NOT SUPPORT + {{ dump(field) }} + {% endif %} + + {% endfor %} +
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %} diff --git a/resources/views/import/spectre/select-country.twig b/resources/views/import/spectre/select-country.twig index 5c4f36a199..ba4f4a0c59 100644 --- a/resources/views/import/spectre/select-country.twig +++ b/resources/views/import/spectre/select-country.twig @@ -16,7 +16,7 @@

- {{ trans('bank.spectre_select_country_text') }} + {{ trans('bank.spectre_select_country_text')|raw }}

diff --git a/resources/views/import/spectre/select-bank.twig b/resources/views/import/spectre/select-provider.twig similarity index 91% rename from resources/views/import/spectre/select-bank.twig rename to resources/views/import/spectre/select-provider.twig index af0a8d56f4..5be257d082 100644 --- a/resources/views/import/spectre/select-bank.twig +++ b/resources/views/import/spectre/select-provider.twig @@ -16,14 +16,14 @@

- {{ trans('bank.spectre_select_bank_title') }} + {{ trans('bank.spectre_select_provider_text',{country: data.country})|raw }}

- {{ ExpandedForm.select('bank_code', data.providers, null)|raw }} + {{ ExpandedForm.select('provider_code', data.providers, null)|raw }}