GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 37b02e...ebbbe1 )
by James
08:59
created

app/Import/Routine/SpectreRoutine.php (1 issue)

1
<?php
2
/**
3
 * SpectreRoutine.php
4
 * Copyright (c) 2017 [email protected]
5
 *
6
 * This file is part of Firefly III.
7
 *
8
 * Firefly III is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * Firefly III is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace FireflyIII\Import\Routine;
24
25
use Carbon\Carbon;
26
use DB;
27
use Exception;
28
use FireflyIII\Exceptions\FireflyException;
29
use FireflyIII\Import\Object\ImportJournal;
30
use FireflyIII\Import\Storage\ImportStorage;
31
use FireflyIII\Models\ImportJob;
32
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
33
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
34
use FireflyIII\Services\Spectre\Exception\SpectreException;
35
use FireflyIII\Services\Spectre\Object\Account;
36
use FireflyIII\Services\Spectre\Object\Customer;
37
use FireflyIII\Services\Spectre\Object\Login;
38
use FireflyIII\Services\Spectre\Object\Token;
39
use FireflyIII\Services\Spectre\Object\Transaction;
40
use FireflyIII\Services\Spectre\Request\CreateTokenRequest;
41
use FireflyIII\Services\Spectre\Request\ListAccountsRequest;
42
use FireflyIII\Services\Spectre\Request\ListCustomersRequest;
43
use FireflyIII\Services\Spectre\Request\ListLoginsRequest;
44
use FireflyIII\Services\Spectre\Request\ListTransactionsRequest;
45
use FireflyIII\Services\Spectre\Request\NewCustomerRequest;
46
use Illuminate\Support\Collection;
47
use Log;
48
use Preferences;
49
50
/**
51
 * Class FileRoutine
52
 */
53
class SpectreRoutine implements RoutineInterface
54
{
55
    /** @var Collection */
56
    public $errors;
57
    /** @var Collection */
58
    public $journals;
59
    /** @var int */
60
    public $lines = 0;
61
    /** @var ImportJob */
62
    private $job;
63
64
    /** @var ImportJobRepositoryInterface */
65
    private $repository;
66
67
    /**
68
     * ImportRoutine constructor.
69
     */
70
    public function __construct()
71
    {
72
        $this->journals = new Collection;
73
        $this->errors   = new Collection;
74
    }
75
76
    /**
77
     * @return Collection
78
     */
79
    public function getErrors(): Collection
80
    {
81
        return $this->errors;
82
    }
83
84
    /**
85
     * @return Collection
86
     */
87
    public function getJournals(): Collection
88
    {
89
        return $this->journals;
90
    }
91
92
    /**
93
     * @return int
94
     */
95
    public function getLines(): int
96
    {
97
        return $this->lines;
98
    }
99
100
    /**
101
     * A Spectre job that ends up here is either "configured" or "running", and will be set to "running"
102
     * when it is "configured".
103
     *
104
     * Job has several stages, stored in extended status key 'stage'
105
     *
106
     * initial: just begun, nothing happened. action: get a customer and a token. Next status: has-token
107
     * has-token: redirect user to sandstorm, make user login. set job to: user-logged-in
108
     * user-logged-in: customer has an attempt. action: analyse/get attempt and go for next status.
109
     *                 if attempt failed: job status is error, save a warning somewhere?
110
     *                 if success, try to get accounts. Save in config key 'accounts'. set status: have-accounts and "configuring"
111
     *
112
     * have-accounts: make user link accounts and select accounts to import from.
113
     *
114
     * If job is "configuring" and stage "have-accounts" then present the accounts and make user link them to
115
     * own asset accounts. Store this mapping, set config to "have-account-mapping" and job status configured".
116
     *
117
     * have-account-mapping: start downloading transactions?
118
     *
119
     *
120
     * @return bool
121
     *
122
     * @throws FireflyException
123
     * @throws SpectreException
124
     * @throws \Illuminate\Container\EntryNotFoundException
125
     */
126
    public function run(): bool
127
    {
128
        if ('configured' === $this->getStatus()) {
129
            $this->repository->updateStatus($this->job, 'running');
130
        }
131
        Log::info(sprintf('Start with import job %s using Spectre.', $this->job->key));
132
        set_time_limit(0);
133
134
        // check if job has token first!
135
        $stage = $this->getConfig()['stage'] ?? 'unknown';
136
137
        switch ($stage) {
138
            case 'initial':
139
                // get customer and token:
140
                $this->runStageInitial();
141
                break;
142
            case 'has-token':
143
                // import routine does nothing at this point:
144
                break;
145
            case 'user-logged-in':
146
                $this->runStageLoggedIn();
147
                break;
148
            case 'have-account-mapping':
149
                $this->runStageHaveMapping();
150
                break;
151
            default:
152
                throw new FireflyException(sprintf('Cannot handle stage %s', $stage));
153
        }
154
155
        return true;
156
    }
157
158
    /**
159
     * @param ImportJob $job
160
     */
161
    public function setJob(ImportJob $job)
162
    {
163
        $this->job        = $job;
164
        $this->repository = app(ImportJobRepositoryInterface::class);
165
        $this->repository->setUser($job->user);
166
    }
167
168
    /**
169
     * @return Customer
170
     *
171
     * @throws \FireflyIII\Exceptions\FireflyException
172
     * @throws \FireflyIII\Services\Spectre\Exception\SpectreException
173
     * @throws \Illuminate\Container\EntryNotFoundException
174
     */
175
    protected function createCustomer(): Customer
176
    {
177
        $newCustomerRequest = new NewCustomerRequest($this->job->user);
178
        $customer           = null;
179
        try {
180
            $newCustomerRequest->call();
181
            $customer = $newCustomerRequest->getCustomer();
182
        } catch (Exception $e) {
183
            // already exists, must fetch customer instead.
184
            Log::warning(sprintf('Customer exists already for user, fetch it: %s', $e->getMessage()));
185
        }
186
        if (null === $customer) {
187
            $getCustomerRequest = new ListCustomersRequest($this->job->user);
188
            $getCustomerRequest->call();
189
            $customers = $getCustomerRequest->getCustomers();
190
            /** @var Customer $current */
191
            foreach ($customers as $current) {
192
                if ('default_ff3_customer' === $current->getIdentifier()) {
193
                    $customer = $current;
194
                    break;
195
                }
196
            }
197
        }
198
199
        Preferences::setForUser($this->job->user, 'spectre_customer', $customer->toArray());
200
201
        return $customer;
202
    }
203
204
    /**
205
     * @return Customer
206
     *
207
     * @throws FireflyException
208
     * @throws SpectreException
209
     * @throws \Illuminate\Container\EntryNotFoundException
210
     */
211
    protected function getCustomer(): Customer
212
    {
213
        $config = $this->getConfig();
214
        if (null !== $config['customer']) {
215
            $customer = new Customer($config['customer']);
216
217
            return $customer;
218
        }
219
220
        $customer           = $this->createCustomer();
221
        $config['customer'] = [
222
            'id'         => $customer->getId(),
223
            'identifier' => $customer->getIdentifier(),
224
            'secret'     => $customer->getSecret(),
225
        ];
226
        $this->setConfig($config);
227
228
        return $customer;
229
    }
230
231
    /**
232
     * @param Customer $customer
233
     * @param string   $returnUri
234
     *
235
     * @return Token
236
     *
237
     * @throws \FireflyIII\Exceptions\FireflyException
238
     * @throws \FireflyIII\Services\Spectre\Exception\SpectreException
239
     * @throws \Illuminate\Container\EntryNotFoundException
240
     */
241
    protected function getToken(Customer $customer, string $returnUri): Token
242
    {
243
        $request = new CreateTokenRequest($this->job->user);
244
        $request->setUri($returnUri);
245
        $request->setCustomer($customer);
246
        $request->call();
247
        Log::debug('Call to get token is finished');
248
249
        return $request->getToken();
250
    }
251
252
    /**
253
     * @throws FireflyException
254
     * @throws SpectreException
255
     * @throws \Illuminate\Container\EntryNotFoundException
256
     */
257
    protected function runStageInitial(): void
258
    {
259
        Log::debug('In runStageInitial()');
260
261
        // create customer if user does not have one:
262
        $customer = $this->getCustomer();
263
        Log::debug(sprintf('Customer ID is %s', $customer->getId()));
264
265
        // use customer to request a token:
266
        $uri   = route('import.status', [$this->job->key]);
267
        $token = $this->getToken($customer, $uri);
268
        Log::debug(sprintf('Token is %s', $token->getToken()));
269
270
        // update job, give it the token:
271
        $config                  = $this->getConfig();
272
        $config['has-token']     = true;
273
        $config['token']         = $token->getToken();
274
        $config['token-expires'] = $token->getExpiresAt()->format('U');
275
        $config['token-url']     = $token->getConnectUrl();
276
        $config['stage']         = 'has-token';
277
        $this->setConfig($config);
278
279
        Log::debug('Job config is now', $config);
280
281
        // update job, set status to "configuring".
282
        $this->setStatus('configuring');
283
        Log::debug(sprintf('Job status is now %s', $this->job->status));
284
        $this->addStep();
285
    }
286
287
    /**
288
     * @throws FireflyException
289
     * @throws SpectreException
290
     * @throws \Illuminate\Container\EntryNotFoundException
291
     */
292
    protected function runStageLoggedIn(): void
293
    {
294
        Log::debug('In runStageLoggedIn');
295
        // list all logins:
296
        $customer = $this->getCustomer();
297
        $request  = new ListLoginsRequest($this->job->user);
298
        $request->setCustomer($customer);
299
        $request->call();
300
301
        $logins = $request->getLogins();
302
        /** @var Login $final */
303
        $final = null;
304
        // loop logins, find the latest with no error in it:
305
        $time = 0;
306
        /** @var Login $login */
307
        foreach ($logins as $login) {
308
            $attempt     = $login->getLastAttempt();
309
            $attemptTime = (int)$attempt->getCreatedAt()->format('U');
310
            if ($attemptTime > $time && null === $attempt->getFailErrorClass()) {
311
                $time  = $attemptTime;
312
                $final = $login;
313
            }
314
        }
315
        if (null === $final) {
316
            Log::error('Could not find a valid login for this user.');
317
            $this->repository->addError($this->job, 0, 'Spectre connection failed. Did you use invalid credentials, press Cancel or failed the 2FA challenge?');
318
            $this->repository->setStatus($this->job, 'error');
319
320
            return;
321
        }
322
        $this->addStep();
323
324
        // list the users accounts using this login.
325
        $accountRequest = new ListAccountsRequest($this->job->user);
326
        $accountRequest->setLogin($login);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $login seems to be defined by a foreach iteration on line 307. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
327
        $accountRequest->call();
328
        $accounts = $accountRequest->getAccounts();
329
330
        // store accounts in job:
331
        $all = [];
332
        /** @var Account $account */
333
        foreach ($accounts as $account) {
334
            $all[] = $account->toArray();
335
        }
336
337
        // update job:
338
        $config             = $this->getConfig();
339
        $config['accounts'] = $all;
340
        $config['login']    = $login->toArray();
341
        $config['stage']    = 'have-accounts';
342
343
        $this->setConfig($config);
344
        $this->setStatus('configuring');
345
        $this->addStep();
346
    }
347
348
    /**
349
     * Shorthand method.
350
     */
351
    private function addStep()
352
    {
353
        $this->repository->addStepsDone($this->job, 1);
354
    }
355
356
    /**
357
     * Shorthand
358
     *
359
     * @param int $steps
360
     */
361
    private function addTotalSteps(int $steps)
362
    {
363
        $this->repository->addTotalSteps($this->job, $steps);
364
    }
365
366
    /**
367
     * @return array
368
     */
369
    private function getConfig(): array
370
    {
371
        return $this->repository->getConfiguration($this->job);
372
    }
373
374
    /**
375
     * Shorthand method.
376
     *
377
     * @return array
378
     */
379
    private function getExtendedStatus(): array
380
    {
381
        return $this->repository->getExtendedStatus($this->job);
382
    }
383
384
    /**
385
     * Shorthand method.
386
     *
387
     * @return string
388
     */
389
    private function getStatus(): string
390
    {
391
        return $this->repository->getStatus($this->job);
392
    }
393
394
    /**
395
     * @param array $all
396
     *
397
     * @throws FireflyException
398
     */
399
    private function importTransactions(array $all)
400
    {
401
        Log::debug('Going to import transactions');
402
        $collection = new Collection;
403
        // create import objects?
404
        foreach ($all as $accountId => $data) {
405
            Log::debug(sprintf('Now at account #%d', $accountId));
406
            /** @var Transaction $transaction */
407
            foreach ($data['transactions'] as $transaction) {
408
                Log::debug(sprintf('Now at transaction #%d', $transaction->getId()));
409
                /** @var Account $account */
410
                $account       = $data['account'];
411
                $importJournal = new ImportJournal;
412
                $importJournal->setUser($this->job->user);
413
                $importJournal->asset->setDefaultAccountId($data['import_id']);
414
                // call set value a bunch of times for various data entries:
415
                $tags   = [];
416
                $tags[] = $transaction->getMode();
417
                $tags[] = $transaction->getStatus();
418
                if ($transaction->isDuplicated()) {
419
                    $tags[] = 'possibly-duplicated';
420
                }
421
                $extra = $transaction->getExtra()->toArray();
422
                $notes = '';
423
                // double space for newline in Markdown.
424
                $notes .= (string)trans('import.imported_from_account', ['account' => $account->getName()]) . '  ' . "\n";
425
426
                foreach ($extra as $key => $value) {
427
                    switch ($key) {
428
                        case 'account_number':
429
                            $importJournal->setValue(['role' => 'account-number', 'value' => $value]);
430
                            break;
431
                        case 'original_category':
432
                        case 'original_subcategory':
433
                        case 'customer_category_code':
434
                        case 'customer_category_name':
435
                            $tags[] = $value;
436
                            break;
437
                        case 'payee':
438
                            $importJournal->setValue(['role' => 'opposing-name', 'value' => $value]);
439
                            break;
440
                        case 'original_amount':
441
                            $importJournal->setValue(['role' => 'amount_foreign', 'value' => $value]);
442
                            break;
443
                        case 'original_currency_code':
444
                            $importJournal->setValue(['role' => 'foreign-currency-code', 'value' => $value]);
445
                            break;
446
                        default:
447
                            $notes .= $key . ': ' . $value . '  '; // for newline in Markdown.
448
                    }
449
                }
450
                // hash
451
                $importJournal->setHash($transaction->getHash());
452
453
                // account ID (Firefly III account):
454
                $importJournal->setValue(['role' => 'account-id', 'value' => $data['import_id'], 'mapped' => $data['import_id']]);
455
456
                // description:
457
                $importJournal->setValue(['role' => 'description', 'value' => $transaction->getDescription()]);
458
459
                // date:
460
                $importJournal->setValue(['role' => 'date-transaction', 'value' => $transaction->getMadeOn()->toIso8601String()]);
461
462
                // amount
463
                $importJournal->setValue(['role' => 'amount', 'value' => $transaction->getAmount()]);
464
                $importJournal->setValue(['role' => 'currency-code', 'value' => $transaction->getCurrencyCode()]);
465
466
                // various meta fields:
467
                $importJournal->setValue(['role' => 'category-name', 'value' => $transaction->getCategory()]);
468
                $importJournal->setValue(['role' => 'note', 'value' => $notes]);
469
                $importJournal->setValue(['role' => 'tags-comma', 'value' => implode(',', $tags)]);
470
                $collection->push($importJournal);
471
            }
472
        }
473
        $this->addStep();
474
        Log::debug(sprintf('Going to try and store all %d them.', $collection->count()));
475
476
        $this->addTotalSteps(7 * $collection->count());
477
        // try to store them (seven steps per transaction)
478
        $storage = new ImportStorage;
479
480
        $storage->setJob($this->job);
481
        $storage->setDateFormat('Y-m-d\TH:i:sO');
482
        $storage->setObjects($collection);
483
        $storage->store();
484
        Log::info('Back in importTransactions()');
485
486
        // link to tag
487
        /** @var TagRepositoryInterface $repository */
488
        $repository = app(TagRepositoryInterface::class);
489
        $repository->setUser($this->job->user);
490
        $data            = [
491
            'tag'         => trans('import.import_with_key', ['key' => $this->job->key]),
492
            'date'        => new Carbon,
493
            'description' => null,
494
            'latitude'    => null,
495
            'longitude'   => null,
496
            'zoomLevel'   => null,
497
            'tagMode'     => 'nothing',
498
        ];
499
        $tag             = $repository->store($data);
500
        $extended        = $this->getExtendedStatus();
501
        $extended['tag'] = $tag->id;
502
        $this->setExtendedStatus($extended);
503
504
        Log::debug(sprintf('Created tag #%d ("%s")', $tag->id, $tag->tag));
505
        Log::debug('Looping journals...');
506
        $journalIds = $storage->journals->pluck('id')->toArray();
507
        $tagId      = $tag->id;
508
        $this->addTotalSteps(count($journalIds));
509
510
        foreach ($journalIds as $journalId) {
511
            Log::debug(sprintf('Linking journal #%d to tag #%d...', $journalId, $tagId));
512
            DB::table('tag_transaction_journal')->insert(['transaction_journal_id' => $journalId, 'tag_id' => $tagId]);
513
            $this->addStep();
514
        }
515
        Log::info(sprintf('Linked %d journals to tag #%d ("%s")', $storage->journals->count(), $tag->id, $tag->tag));
516
517
        // set status to "finished"?
518
        // update job:
519
        $this->setStatus('finished');
520
        $this->addStep();
521
522
    }
523
524
    /**
525
     * @throws FireflyException
526
     * @throws SpectreException
527
     * @throws \Illuminate\Container\EntryNotFoundException
528
     */
529
    private function runStageHaveMapping()
530
    {
531
        $config   = $this->getConfig();
532
        $accounts = $config['accounts'] ?? [];
533
        $all      = [];
534
        $count    = 0;
535
        /** @var array $accountArray */
536
        foreach ($accounts as $accountArray) {
537
            $account  = new Account($accountArray);
538
            $importId = (int)($config['accounts-mapped'][$account->getId()] ?? 0.0);
539
            $doImport = 0 !== $importId;
540
            if (!$doImport) {
541
                Log::debug(sprintf('Will NOT import from Spectre account #%d ("%s")', $account->getId(), $account->getName()));
542
                continue;
543
            }
544
            // grab all transactions
545
            $listTransactionsRequest = new ListTransactionsRequest($this->job->user);
546
            $listTransactionsRequest->setAccount($account);
547
            $listTransactionsRequest->call();
548
            $transactions           = $listTransactionsRequest->getTransactions();
549
            $all[$account->getId()] = [
550
                'account'      => $account,
551
                'import_id'    => $importId,
552
                'transactions' => $transactions,
553
            ];
554
            $count                  += count($transactions);
555
        }
556
        Log::debug(sprintf('Total number of transactions: %d', $count));
557
        $this->addStep();
558
559
        $this->importTransactions($all);
560
    }
561
562
    /**
563
     * Shorthand.
564
     *
565
     * @param array $config
566
     */
567
    private function setConfig(array $config): void
568
    {
569
        $this->repository->setConfiguration($this->job, $config);
570
571
    }
572
573
    /**
574
     * Shorthand method.
575
     *
576
     * @param array $extended
577
     */
578
    private function setExtendedStatus(array $extended): void
579
    {
580
        $this->repository->setExtendedStatus($this->job, $extended);
581
582
    }
583
584
    /**
585
     * Shorthand.
586
     *
587
     * @param string $status
588
     */
589
    private function setStatus(string $status): void
590
    {
591
        $this->repository->setStatus($this->job, $status);
592
    }
593
}
594