diff --git a/app/Import/Configuration/SpectreConfigurator.php b/app/Import/Configuration/SpectreConfigurator.php index 125d7e2bd5..36d93cdeca 100644 --- a/app/Import/Configuration/SpectreConfigurator.php +++ b/app/Import/Configuration/SpectreConfigurator.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Import\Configuration; use FireflyIII\Models\ImportJob; +use Log; /** * Class SpectreConfigurator. @@ -61,7 +62,8 @@ class SpectreConfigurator implements ConfiguratorInterface */ public function getNextData(): array { - // update config to tell Firefly we've redirected the user. + Log::debug('in getNextData(), user will be redirected next.'); + // update config to tell Firefly the user is redirected. $config = $this->job->configuration; $config['is-redirected'] = true; $this->job->configuration = $config; @@ -76,6 +78,9 @@ class SpectreConfigurator implements ConfiguratorInterface */ public function getNextView(): string { + Log::debug('Send user redirect view'); + + // sends the user to spectre. return 'import.spectre.redirect'; } @@ -94,11 +99,15 @@ class SpectreConfigurator implements ConfiguratorInterface */ public function isJobConfigured(): bool { + Log::debug('in isJobConfigured'); // job is configured (and can start) when token is empty: $config = $this->job->configuration; - if ($config['has-token'] === false) { + if ($config['has-token'] === false && $config['is-redirected'] === true) { + Log::debug('has-token is false, is-redirected is true, return true'); + return true; } + Log::debug('return false'); return false; } @@ -114,6 +123,8 @@ class SpectreConfigurator implements ConfiguratorInterface 'token-expires' => 0, 'token-url' => '', 'is-redirected' => false, + 'customer' => null, + 'login' => null, ]; diff --git a/app/Import/Routine/SpectreRoutine.php b/app/Import/Routine/SpectreRoutine.php index fbd12ae9c7..b1ee3b3673 100644 --- a/app/Import/Routine/SpectreRoutine.php +++ b/app/Import/Routine/SpectreRoutine.php @@ -24,9 +24,13 @@ namespace FireflyIII\Import\Routine; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; +use FireflyIII\Services\Spectre\Exception\DuplicatedCustomerException; use FireflyIII\Services\Spectre\Object\Customer; +use FireflyIII\Services\Spectre\Object\Login; use FireflyIII\Services\Spectre\Object\Token; use FireflyIII\Services\Spectre\Request\CreateTokenRequest; +use FireflyIII\Services\Spectre\Request\ListCustomersRequest; +use FireflyIII\Services\Spectre\Request\ListLoginsRequest; use FireflyIII\Services\Spectre\Request\NewCustomerRequest; use Illuminate\Support\Collection; use Log; @@ -82,13 +86,13 @@ class SpectreRoutine implements RoutineInterface /** * * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ public function run(): bool { if ('configured' !== $this->job->status) { - Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->job->status)); - - return false; + //Log::error(sprintf('Job %s is in state "%s" so it cannot be started.', $this->job->key, $this->job->status)); + //return false; } Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key)); set_time_limit(0); @@ -125,9 +129,49 @@ class SpectreRoutine implements RoutineInterface } $isRedirected = $config['is-redirected'] ?? false; if ($isRedirected === true) { + // update job to say it's running + $extended = $this->job->extended_status; + $this->job->status = 'running'; + $extended['steps'] = 100; + $extended['done'] = 1; + $this->job->extended_status = $extended; + $this->job->save(); + } + // is job running? + if ($this->job->status === 'running') { + + // list all logins: + $customer = $this->getCustomer(); + $request = new ListLoginsRequest($this->job->user); + $request->setCustomer($customer); + $request->call(); + $logins = $request->getLogins(); + /** @var Login $final */ + $final = null; + // loop logins, find the latest with no error in it: + $time = 0; + /** @var Login $login */ + foreach($logins as $login) { + $attempt = $login->getLastAttempt(); + $attemptTime = intval($attempt->getCreatedAt()->format('U')); + if($attemptTime > $time && is_null($attempt->getFailErrorClass())) { + $time = $attemptTime; + $final = $login; + } + } + if(is_null($final)) { + throw new FireflyException('No valid login attempt found.'); + } + var_dump($final); + + //var_dump($logins); + exit; + // assume user has "used" the token. // ... // now what? + Log::debug('Token has been used. User was redirected. Now check status with Spectre and respond?'); + throw new FireflyException('Application cannot handle this.'); } @@ -145,12 +189,31 @@ class SpectreRoutine implements RoutineInterface /** * @return Customer * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ protected function createCustomer(): Customer { $newCustomerRequest = new NewCustomerRequest($this->job->user); - $newCustomerRequest->call(); - $customer = $newCustomerRequest->getCustomer(); + $customer = null; + try { + $newCustomerRequest->call(); + $customer = $newCustomerRequest->getCustomer(); + } catch (DuplicatedCustomerException $e) { + // already exists, must fetch customer instead. + Log::warning('Customer exists already for user, fetch it.'); + } + if (is_null($customer)) { + $getCustomerRequest = new ListCustomersRequest($this->job->user); + $getCustomerRequest->call(); + $customers = $getCustomerRequest->getCustomers(); + /** @var Customer $current */ + foreach ($customers as $current) { + if ($current->getIdentifier() === 'default_ff3_customer') { + $customer = $current; + break; + } + } + } Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray()); @@ -160,15 +223,26 @@ class SpectreRoutine implements RoutineInterface /** * @return Customer - * @throws \FireflyIII\Exceptions\FireflyException + * @throws FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ protected function getCustomer(): Customer { - $preference = Preferences::getForUser($this->job->user, 'spectre_customer', null); - if (is_null($preference)) { - return $this->createCustomer(); + $config = $this->job->configuration; + if (!is_null($config['customer'])) { + $customer = new Customer($config['customer']); + + return $customer; } - $customer = new Customer($preference->data); + + $customer = $this->createCustomer(); + $config['customer'] = [ + 'id' => $customer->getId(), + 'identifier' => $customer->getIdentifier(), + 'secret' => $customer->getSecret(), + ]; + $this->job->configuration = $config; + $this->job->save(); return $customer; } @@ -179,6 +253,7 @@ class SpectreRoutine implements RoutineInterface * * @return Token * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ protected function getToken(Customer $customer, string $returnUri): Token { diff --git a/app/Services/Spectre/Exception/DuplicatedCustomerException.php b/app/Services/Spectre/Exception/DuplicatedCustomerException.php new file mode 100644 index 0000000000..a9a3a42996 --- /dev/null +++ b/app/Services/Spectre/Exception/DuplicatedCustomerException.php @@ -0,0 +1,32 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Exception; + +/** + * Class DuplicatedCustomerException + */ +class DuplicatedCustomerException extends SpectreException +{ + +} \ No newline at end of file diff --git a/app/Services/Spectre/Exception/SpectreException.php b/app/Services/Spectre/Exception/SpectreException.php new file mode 100644 index 0000000000..78200f7f08 --- /dev/null +++ b/app/Services/Spectre/Exception/SpectreException.php @@ -0,0 +1,34 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Exception; + +use Exception; + +/** + * Class SpectreException + */ +class SpectreException extends Exception +{ + +} \ No newline at end of file diff --git a/app/Services/Spectre/Object/Attempt.php b/app/Services/Spectre/Object/Attempt.php new file mode 100644 index 0000000000..71b77bbbbe --- /dev/null +++ b/app/Services/Spectre/Object/Attempt.php @@ -0,0 +1,168 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Object; + +use Carbon\Carbon; + +/** + * Class Attempt + */ +class Attempt extends SpectreObject +{ + /** @var string */ + private $apiMode; + /** @var int */ + private $apiVersion; + /** @var bool */ + private $automaticFetch; + /** @var bool */ + private $categorize; + /** @var Carbon */ + private $consentGivenAt; + /** @var array */ + private $consentTypes = []; + /** @var Carbon */ + private $createdAt; + /** @var array */ + private $customFields = []; + /** @var bool */ + private $dailyRefresh; + /** @var string */ + private $deviceType; + /** @var array */ + private $excludeAccounts = []; + /** @var Carbon */ + private $failAt; + /** @var string */ + private $failErrorClass; + /** @var string */ + private $failMessage; + /** @var string */ + private $fetchType; + /** @var bool */ + private $finished; + /** @var bool */ + private $finishedRecent; + /** @var Carbon */ + private $fromDate; + /** @var int */ + private $id; + /** @var bool */ + private $interactive; + /** @var string */ + private $locale; + /** @var bool */ + private $partial; + /** @var string */ + private $remoteIp; + /** @var bool */ + private $showConsentInformation; + /** @var array */ + private $stages = []; + /** @var bool */ + private $storeCredentials; + /** @var Carbon */ + private $successAt; + /** @var Carbon */ + private $toDate; + /** @var Carbon */ + private $updatedAt; + /** @var string */ + private $userAgent; // undocumented + + /** + * Attempt constructor. + * + * @param array $data + */ + public function __construct(array $data) + { + $this->apiMode = $data['api_mode']; + $this->apiVersion = $data['api_version']; + $this->automaticFetch = $data['automatic_fetch']; + $this->categorize = $data['categorize']; + $this->createdAt = new Carbon($data['created_at']); + $this->consentGivenAt = new Carbon($data['consent_given_at']); + $this->consentTypes = $data['consent_types']; + $this->customFields = $data['custom_fields']; + $this->dailyRefresh = $data['daily_refresh']; + $this->deviceType = $data['device_type']; + $this->userAgent = $data['user_agent'] ?? ''; + $this->remoteIp = $data['remote_ip']; + $this->excludeAccounts = $data['exclude_accounts']; + $this->failAt = new Carbon($data['fail_at']); + $this->failErrorClass = $data['fail_error_class']; + $this->failMessage = $data['fail_message']; + $this->fetchType = $data['fetch_type']; + $this->finished = $data['finished']; + $this->finishedRecent = $data['finished_recent']; + $this->fromDate = new Carbon($data['from_date']); + $this->id = $data['id']; + $this->interactive = $data['interactive']; + $this->locale = $data['locale']; + $this->partial = $data['partial']; + $this->showConsentInformation = $data['show_consent_confirmation']; + $this->stages = $data['stages'] ?? []; + $this->storeCredentials = $data['store_credentials']; + $this->successAt = new Carbon($data['success_at']); + $this->toDate = new Carbon($data['to_date']); + $this->updatedAt = new Carbon($data['updated_at']); + + } + + /** + * @return Carbon + */ + public function getCreatedAt(): Carbon + { + return $this->createdAt; + } + + /** + * @return Carbon + */ + public function getFailAt(): Carbon + { + return $this->failAt; + } + + /** + * @return null|string + */ + public function getFailErrorClass(): ?string + { + return $this->failErrorClass; + } + + /** + * @return null|string + */ + public function getFailMessage(): ?string + { + return $this->failMessage; + } + + + +} \ No newline at end of file diff --git a/app/Services/Spectre/Object/Holder.php b/app/Services/Spectre/Object/Holder.php new file mode 100644 index 0000000000..d1e2922f7b --- /dev/null +++ b/app/Services/Spectre/Object/Holder.php @@ -0,0 +1,40 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Object; + +/** + * Class Holder + */ +class Holder extends SpectreObject +{ + /** + * Holder constructor. + * + * @param array $data + */ + public function __construct(array $data) + { + + } +} \ No newline at end of file diff --git a/app/Services/Spectre/Object/Login.php b/app/Services/Spectre/Object/Login.php new file mode 100644 index 0000000000..73fa3e2c6b --- /dev/null +++ b/app/Services/Spectre/Object/Login.php @@ -0,0 +1,108 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Object; + +use Carbon\Carbon; + + +/** + * Class Login + */ +class Login extends SpectreObject +{ + /** @var Carbon */ + private $consentGivenAt; + /** @var array */ + private $consentTypes; + /** @var string */ + private $countryCode; + /** @var Carbon */ + private $createdAt; + /** @var int */ + private $customerId; + /** @var bool */ + private $dailyRefresh; + /** @var Holder */ + private $holderInfo; + /** @var int */ + private $id; + /** @var Attempt */ + private $lastAttempt; + /** @var Carbon */ + private $lastSuccessAt; + /** @var Carbon */ + private $nextRefreshPossibleAt; + /** @var string */ + private $providerCode; + /** @var int */ + private $providerId; + /** @var string */ + private $providerName; + /** @var bool */ + private $showConsentConfirmation; + /** @var string */ + private $status; + /** @var bool */ + private $storeCredentials; + /** @var Carbon */ + private $updatedAt; + + /** + * Login constructor. + * + * @param array $data + */ + public function __construct(array $data) + { + $this->consentGivenAt = new Carbon($data['consent_given_at']); + $this->consentTypes = $data['consent_types']; + $this->countryCode = $data['country_code']; + $this->createdAt = new Carbon($data['created_at']); + $this->updatedAt = new Carbon($data['updated_at']); + $this->customerId = $data['customer_id']; + $this->dailyRefresh = $data['daily_refresh']; + $this->holderInfo = new Holder($data['holder_info']); + $this->id = $data['id']; + $this->lastAttempt = new Attempt($data['last_attempt']); + $this->lastSuccessAt = new Carbon($data['last_success_at']); + $this->nextRefreshPossibleAt = new Carbon($data['next_refresh_possible_at']); + $this->providerCode = $data['provider_code']; + $this->providerId = $data['provider_id']; + $this->providerName = $data['provider_name']; + $this->showConsentConfirmation = $data['show_consent_confirmation']; + $this->status = $data['status']; + $this->storeCredentials = $data['store_credentials']; + + } + + /** + * @return Attempt + */ + public function getLastAttempt(): Attempt + { + return $this->lastAttempt; + } + + +} \ No newline at end of file diff --git a/app/Services/Spectre/Request/CreateTokenRequest.php b/app/Services/Spectre/Request/CreateTokenRequest.php index adacab4ae8..ff0925779c 100644 --- a/app/Services/Spectre/Request/CreateTokenRequest.php +++ b/app/Services/Spectre/Request/CreateTokenRequest.php @@ -44,6 +44,7 @@ class CreateTokenRequest extends SpectreRequest /** * * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ public function call(): void { diff --git a/app/Services/Spectre/Request/ListCustomersRequest.php b/app/Services/Spectre/Request/ListCustomersRequest.php new file mode 100644 index 0000000000..7395c5a37a --- /dev/null +++ b/app/Services/Spectre/Request/ListCustomersRequest.php @@ -0,0 +1,82 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use FireflyIII\Services\Spectre\Object\Customer; +use Log; + + +/** + * Class ListCustomersRequest + */ +class ListCustomersRequest extends SpectreRequest +{ + /** @var array */ + private $customers = []; + + /** + * + * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException + */ + public function call(): void + { + $hasNextPage = true; + $nextId = 0; + while ($hasNextPage) { + Log::debug(sprintf('Now calling ListCustomersRequest for next_id %d', $nextId)); + $parameters = ['from_id' => $nextId]; + $uri = '/api/v3/customers/?' . 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 customers: + foreach ($response['data'] as $customerArray) { + $this->customers[] = new Customer($customerArray); + } + } + } + + /** + * @return array + */ + public function getCustomers(): array + { + return $this->customers; + } + + +} \ No newline at end of file diff --git a/app/Services/Spectre/Request/ListLoginsRequest.php b/app/Services/Spectre/Request/ListLoginsRequest.php new file mode 100644 index 0000000000..c6444ec514 --- /dev/null +++ b/app/Services/Spectre/Request/ListLoginsRequest.php @@ -0,0 +1,91 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use FireflyIII\Services\Spectre\Object\Customer; +use FireflyIII\Services\Spectre\Object\Login; +use Log; +/** + * Class ListLoginsRequest + */ +class ListLoginsRequest extends SpectreRequest +{ + /** @var Customer */ + private $customer; + + /** @var array */ + private $logins = []; + + /** + * @return array + */ + public function getLogins(): array + { + return $this->logins; + } + + /** + * + */ + public function call(): void + { + $hasNextPage = true; + $nextId = 0; + while ($hasNextPage) { + Log::debug(sprintf('Now calling ListLoginsRequest for next_id %d', $nextId)); + $parameters = ['from_id' => $nextId, 'customer_id' => $this->customer->getId()]; + $uri = '/api/v3/logins/?' . 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 logins: + /** @var array $loginArray */ + foreach ($response['data'] as $loginArray) { + $this->logins[] = new Login($loginArray); + } + } + } + + /** + * @param Customer $customer + */ + public function setCustomer(Customer $customer): void + { + $this->customer = $customer; + } + + +} \ No newline at end of file diff --git a/app/Services/Spectre/Request/NewCustomerRequest.php b/app/Services/Spectre/Request/NewCustomerRequest.php index 0c6960be90..9e8140c20d 100644 --- a/app/Services/Spectre/Request/NewCustomerRequest.php +++ b/app/Services/Spectre/Request/NewCustomerRequest.php @@ -34,6 +34,7 @@ class NewCustomerRequest extends SpectreRequest /** * @throws \FireflyIII\Exceptions\FireflyException + * @throws \FireflyIII\Services\Spectre\Exception\SpectreException */ public function call(): void { diff --git a/app/Services/Spectre/Request/SpectreRequest.php b/app/Services/Spectre/Request/SpectreRequest.php index 8db025b6ba..b7e129ca0f 100644 --- a/app/Services/Spectre/Request/SpectreRequest.php +++ b/app/Services/Spectre/Request/SpectreRequest.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Services\Spectre\Request; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Services\Spectre\Exception\DuplicatedCustomerException; +use FireflyIII\Services\Spectre\Exception\SpectreException; use FireflyIII\User; use Log; use Requests; @@ -179,6 +181,7 @@ abstract class SpectreRequest * @return array * * @throws FireflyException + * @throws SpectreException */ protected function sendSignedSpectreGet(string $uri, array $data): array { @@ -222,6 +225,7 @@ abstract class SpectreRequest * @return array * * @throws FireflyException + * @throws SpectreException */ protected function sendSignedSpectrePost(string $uri, array $data): array { @@ -255,15 +259,21 @@ abstract class SpectreRequest * @param Requests_Response $response * * @throws FireflyException + * @throws SpectreException */ private function detectError(Requests_Response $response): void { $body = $response->body; $array = json_decode($body, true); if (isset($array['error_class'])) { - $message = $array['error_message'] ?? '(no message)'; + $message = $array['error_message'] ?? '(no message)'; + $errorClass = $array['error_class']; + $class = sprintf('\\FireflyIII\\Services\\Spectre\Exception\\%sException', $errorClass); + if (class_exists($class)) { + throw new $class($message); + } - throw new FireflyException(sprintf('Error of class %s: %s', $array['error_class'], $message)); + throw new FireflyException(sprintf('Error of class %s: %s', $errorClass, $message)); } $statusCode = intval($response->status_code);