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/Console/Commands/VerifyDatabase.php (8 issues)

Labels
1
<?php
2
declare(strict_types=1);
3
/**
4
 * VerifyDatabase.php
5
 * Copyright (c) 2017 [email protected]
6
 *
7
 * This file is part of Firefly III.
8
 *
9
 * Firefly III is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * Firefly III is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
21
 */
22
23
namespace FireflyIII\Console\Commands;
24
25
use Crypt;
26
use DB;
27
use FireflyIII\Models\Account;
28
use FireflyIII\Models\AccountType;
29
use FireflyIII\Models\Budget;
30
use FireflyIII\Models\LinkType;
31
use FireflyIII\Models\PiggyBankEvent;
32
use FireflyIII\Models\Transaction;
33
use FireflyIII\Models\TransactionJournal;
34
use FireflyIII\Models\TransactionType;
35
use FireflyIII\Repositories\User\UserRepositoryInterface;
36
use FireflyIII\User;
37
use Illuminate\Console\Command;
38
use Illuminate\Contracts\Encryption\DecryptException;
39
use Illuminate\Database\Eloquent\Builder;
40
use Preferences;
41
use Schema;
42
use stdClass;
43
44
/**
45
 * Class VerifyDatabase.
46
 *
47
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
48
 */
49
class VerifyDatabase extends Command
50
{
51
    /**
52
     * The console command description.
53
     *
54
     * @var string
55
     */
56
    protected $description = 'Will verify your database.';
57
    /**
58
     * The name and signature of the console command.
59
     *
60
     * @var string
61
     */
62
    protected $signature = 'firefly:verify';
63
64
    /**
65
     * Execute the console command.
66
     */
67
    public function handle()
68
    {
69
        // if table does not exist, return false
70
        if (!Schema::hasTable('users')) {
71
            return;
72
        }
73
74
        $this->reportObject('budget');
75
        $this->reportObject('category');
76
        $this->reportObject('tag');
77
        $this->reportAccounts();
78
        $this->reportBudgetLimits();
79
        $this->reportSum();
80
        $this->reportJournals();
81
        $this->reportTransactions();
82
        $this->reportDeletedAccounts();
83
        $this->reportNoTransactions();
84
        $this->reportTransfersBudgets();
85
        $this->reportIncorrectJournals();
86
        $this->repairPiggyBanks();
87
        $this->createLinkTypes();
88
        $this->createAccessTokens();
89
        $this->fixDoubleAmounts();
90
    }
91
92
    /**
93
     * Create user access tokens, if not present already.
94
     */
95
    private function createAccessTokens()
96
    {
97
        $count = 0;
98
        $users = User::get();
99
        /** @var User $user */
100
        foreach ($users as $user) {
101
            $pref = Preferences::getForUser($user, 'access_token', null);
0 ignored issues
show
The method getForUser() does not exist on FireflyIII\Support\Facades\Preferences. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
            /** @scrutinizer ignore-call */ 
102
            $pref = Preferences::getForUser($user, 'access_token', null);
Loading history...
102
            if (null === $pref) {
103
                $token = $user->generateAccessToken();
104
                Preferences::setForUser($user, 'access_token', $token);
0 ignored issues
show
The method setForUser() does not exist on FireflyIII\Support\Facades\Preferences. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

104
                Preferences::/** @scrutinizer ignore-call */ 
105
                             setForUser($user, 'access_token', $token);
Loading history...
105
                $this->line(sprintf('Generated access token for user %s', $user->email));
106
                ++$count;
107
            }
108
        }
109
        if (0 === $count) {
110
            $this->info('All access tokens OK!');
111
        }
112
    }
113
114
    /**
115
     * Create default link types if necessary.
116
     */
117
    private function createLinkTypes()
118
    {
119
        $count = 0;
120
        $set   = [
121
            'Related'       => ['relates to', 'relates to'],
122
            'Refund'        => ['(partially) refunds', 'is (partially) refunded by'],
123
            'Paid'          => ['(partially) pays for', 'is (partially) paid for by'],
124
            'Reimbursement' => ['(partially) reimburses', 'is (partially) reimbursed by'],
125
        ];
126
        foreach ($set as $name => $values) {
127
            $link = LinkType::where('name', $name)->where('outward', $values[0])->where('inward', $values[1])->first();
128
            if (null === $link) {
129
                $link          = new LinkType;
130
                $link->name    = $name;
131
                $link->outward = $values[0];
132
                $link->inward  = $values[1];
133
                ++$count;
134
            }
135
            $link->editable = false;
136
            $link->save();
137
        }
138
        if (0 === $count) {
139
            $this->info('All link types OK!');
140
        }
141
    }
142
143
    private function fixDoubleAmounts()
144
    {
145
        $count = 0;
146
        // get invalid journals
147
        $errored  = [];
148
        $journals = DB::table('transactions')
149
                      ->groupBy('transaction_journal_id')
0 ignored issues
show
'transaction_journal_id' of type string is incompatible with the type array expected by parameter $groups of Illuminate\Database\Query\Builder::groupBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

149
                      ->groupBy(/** @scrutinizer ignore-type */ 'transaction_journal_id')
Loading history...
150
                      ->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')]);
151
        /** @var stdClass $entry */
152
        foreach ($journals as $entry) {
153
            if (0 !== bccomp((string)$entry->the_sum, '0')) {
154
                $errored[] = $entry->transaction_journal_id;
155
            }
156
        }
157
        foreach ($errored as $journalId) {
158
            // select and update:
159
            $res = Transaction::whereNull('deleted_at')->where('transaction_journal_id', $journalId)->groupBy('amount')->get([DB::raw('MIN(id) as first_id')]);
160
            $ids = $res->pluck('first_id')->toArray();
161
            DB::table('transactions')->whereIn('id', $ids)->update(['amount' => DB::raw('amount * -1')]);
162
            ++$count;
163
            // report about it
164
            /** @var TransactionJournal $journal */
165
            $journal = TransactionJournal::find($journalId);
166
            if (null === $journal) {
167
                continue;
168
            }
169
            if (TransactionType::OPENING_BALANCE === $journal->transactionType->type) {
0 ignored issues
show
The property type does not seem to exist on FireflyIII\Models\TransactionType. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
170
                $this->error(
171
                    sprintf(
172
                        'Transaction #%d was stored incorrectly. One of your asset accounts may show the wrong balance. Please visit /transactions/show/%d to verify the opening balance.',
173
                        $journalId, $journalId
174
                    )
175
                );
176
            }
177
            if (TransactionType::OPENING_BALANCE !== $journal->transactionType->type) {
178
                $this->error(
179
                    sprintf(
180
                        'Transaction #%d was stored incorrectly. Could be that the transaction shows the wrong amount. Please visit /transactions/show/%d to verify the opening balance.',
181
                        $journalId, $journalId
182
                    )
183
                );
184
            }
185
        }
186
        if (0 === $count) {
187
            $this->info('Amount integrity OK!');
188
        }
189
190
        return;
191
    }
192
193
    /**
194
     * Eeport (and fix) piggy banks. Make sure there are only transfers linked to piggy bank events.
195
     */
196
    private function repairPiggyBanks(): void
197
    {
198
        $set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
199
        $set->each(
200
            function (PiggyBankEvent $event) {
201
                if (null === $event->transaction_journal_id) {
0 ignored issues
show
The property transaction_journal_id does not exist on FireflyIII\Models\PiggyBankEvent. Did you mean transactionJournal?
Loading history...
202
                    return true;
203
                }
204
                /** @var TransactionJournal $journal */
205
                $journal = $event->transactionJournal()->first();
206
                if (null === $journal) {
207
                    return true;
208
                }
209
210
                $type = $journal->transactionType->type;
0 ignored issues
show
The property type does not seem to exist on FireflyIII\Models\TransactionType. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
211
                if (TransactionType::TRANSFER !== $type) {
212
                    $event->transaction_journal_id = null;
213
                    $event->save();
214
                    $this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
0 ignored issues
show
The property piggy_bank_id does not seem to exist on FireflyIII\Models\PiggyBankEvent. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
215
                }
216
217
                return true;
218
            }
219
        );
220
221
        return;
222
    }
223
224
    /**
225
     * Reports on accounts with no transactions.
226
     */
227
    private function reportAccounts()
228
    {
229
        $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
230
                      ->leftJoin('users', 'accounts.user_id', '=', 'users.id')
231
                      ->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
232
                      ->whereNull('transactions.account_id')
233
                      ->get(
234
                          ['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']
235
                      );
236
237
        /** @var stdClass $entry */
238
        foreach ($set as $entry) {
239
            $name = $entry->name;
240
            $line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
241
            $line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $name);
242
            $this->line($line);
243
        }
244
    }
245
246
    /**
247
     * Reports on budgets with no budget limits (which makes them pointless).
248
     */
249
    private function reportBudgetLimits()
250
    {
251
        $set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
252
                     ->leftJoin('users', 'budgets.user_id', '=', 'users.id')
253
                     ->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
254
                     ->whereNull('budget_limits.id')
255
                     ->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
256
257
        /** @var Budget $entry */
258
        foreach ($set as $entry) {
259
            $line = sprintf(
260
                'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
261
                $entry->user_id,
262
                $entry->email,
263
                $entry->id,
264
                $entry->name
265
            );
266
            $this->line($line);
267
        }
268
    }
269
270
    /**
271
     * Reports on deleted accounts that still have not deleted transactions or journals attached to them.
272
     */
273
    private function reportDeletedAccounts()
274
    {
275
        $set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
276
                      ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
277
                      ->whereNotNull('accounts.deleted_at')
278
                      ->whereNotNull('transactions.id')
279
                      ->where(
280
                          function (Builder $q) {
281
                              $q->whereNull('transactions.deleted_at');
282
                              $q->orWhereNull('transaction_journals.deleted_at');
283
                          }
284
                      )
285
                      ->get(
286
                          ['accounts.id as account_id', 'accounts.deleted_at as account_deleted_at', 'transactions.id as transaction_id',
287
                           'transactions.deleted_at as transaction_deleted_at', 'transaction_journals.id as journal_id',
288
                           'transaction_journals.deleted_at as journal_deleted_at',]
289
                      );
290
        /** @var stdClass $entry */
291
        foreach ($set as $entry) {
292
            $date = $entry->transaction_deleted_at ?? $entry->journal_deleted_at;
293
            $this->error(
294
                'Error: Account #' . $entry->account_id . ' should have been deleted, but has not.' .
295
                ' Find it in the table called "accounts" and change the "deleted_at" field to: "' . $date . '"'
296
            );
297
        }
298
    }
299
300
    /**
301
     * Report on journals with bad account types linked to them.
302
     */
303
    private function reportIncorrectJournals()
304
    {
305
        $configuration = [
306
            // a withdrawal can not have revenue account:
307
            TransactionType::WITHDRAWAL => [AccountType::REVENUE],
308
            // deposit cannot have an expense account:
309
            TransactionType::DEPOSIT    => [AccountType::EXPENSE],
310
            // transfer cannot have either:
311
            TransactionType::TRANSFER   => [AccountType::EXPENSE, AccountType::REVENUE],
312
        ];
313
        foreach ($configuration as $transactionType => $accountTypes) {
314
            $set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
315
                                     ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
316
                                     ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
317
                                     ->leftJoin('account_types', 'account_types.id', 'accounts.account_type_id')
318
                                     ->leftJoin('users', 'users.id', '=', 'transaction_journals.user_id')
319
                                     ->where('transaction_types.type', $transactionType)
320
                                     ->whereIn('account_types.type', $accountTypes)
321
                                     ->whereNull('transaction_journals.deleted_at')
322
                                     ->get(
323
                                         ['transaction_journals.id', 'transaction_journals.user_id', 'users.email', 'account_types.type as a_type',
324
                                          'transaction_types.type',]
325
                                     );
326
            foreach ($set as $entry) {
327
                $this->error(
328
                    sprintf(
329
                        'Transaction journal #%d (user #%d, %s) is of type "%s" but ' .
330
                        'is linked to a "%s". The transaction journal should be recreated.',
331
                        $entry->id,
332
                        $entry->user_id,
333
                        $entry->email,
334
                        $entry->type,
335
                        $entry->a_type
336
                    )
337
                );
338
            }
339
        }
340
    }
341
342
    /**
343
     * Any deleted transaction journals that have transactions that are NOT deleted:.
344
     */
345
    private function reportJournals()
346
    {
347
        $count = 0;
348
        $set   = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
349
                                   ->whereNotNull('transaction_journals.deleted_at')// USE THIS
350
                                   ->whereNull('transactions.deleted_at')
351
                                   ->whereNotNull('transactions.id')
352
                                   ->get(
353
                                       [
354
                                           'transaction_journals.id as journal_id',
355
                                           'transaction_journals.description',
356
                                           'transaction_journals.deleted_at as journal_deleted',
357
                                           'transactions.id as transaction_id',
358
                                           'transactions.deleted_at as transaction_deleted_at',]
359
                                   );
360
        /** @var stdClass $entry */
361
        foreach ($set as $entry) {
362
            $this->error(
363
                'Error: Transaction #' . $entry->transaction_id . ' should have been deleted, but has not.' .
364
                ' Find it in the table called "transactions" and change the "deleted_at" field to: "' . $entry->journal_deleted . '"'
365
            );
366
            ++$count;
367
        }
368
        if (0 === $count) {
369
            $this->info('No orphaned transactions!');
370
        }
371
    }
372
373
    /**
374
     * Report on journals without transactions.
375
     */
376
    private function reportNoTransactions()
377
    {
378
        $count = 0;
379
        $set   = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
380
                                   ->groupBy('transaction_journals.id')
0 ignored issues
show
'transaction_journals.id' of type string is incompatible with the type array expected by parameter $groups of Illuminate\Database\Query\Builder::groupBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

380
                                   ->groupBy(/** @scrutinizer ignore-type */ 'transaction_journals.id')
Loading history...
381
                                   ->whereNull('transactions.transaction_journal_id')
382
                                   ->get(['transaction_journals.id']);
383
384
        foreach ($set as $entry) {
385
            $this->error(
386
                'Error: Journal #' . $entry->id . ' has zero transactions. Open table "transaction_journals" and delete the entry with id #' . $entry->id
387
            );
388
            ++$count;
389
        }
390
        if (0 === $count) {
391
            $this->info('No orphaned journals!');
392
        }
393
    }
394
395
    /**
396
     * Report on things with no linked journals.
397
     *
398
     * @param string $name
399
     */
400
    private function reportObject(string $name)
401
    {
402
        $plural = str_plural($name);
403
        $class  = sprintf('FireflyIII\Models\%s', ucfirst($name));
404
        $field  = 'tag' === $name ? 'tag' : 'name';
405
        $set    = $class::leftJoin($name . '_transaction_journal', $plural . '.id', '=', $name . '_transaction_journal.' . $name . '_id')
406
                        ->leftJoin('users', $plural . '.user_id', '=', 'users.id')
407
                        ->distinct()
408
                        ->whereNull($name . '_transaction_journal.' . $name . '_id')
409
                        ->whereNull($plural . '.deleted_at')
410
                        ->get([$plural . '.id', $plural . '.' . $field . ' as name', $plural . '.user_id', 'users.email']);
411
412
        /** @var stdClass $entry */
413
        foreach ($set as $entry) {
414
            $objName = $entry->name;
415
            try {
416
                $objName = Crypt::decrypt($objName);
417
            } catch (DecryptException $e) {
418
                // it probably was not encrypted.
419
            }
420
421
            $line = sprintf(
422
                'User #%d (%s) has %s #%d ("%s") which has no transactions.',
423
                $entry->user_id,
424
                $entry->email,
425
                $name,
426
                $entry->id,
427
                $objName
428
            );
429
            $this->line($line);
430
        }
431
    }
432
433
    /**
434
     * Reports for each user when the sum of their transactions is not zero.
435
     */
436
    private function reportSum()
437
    {
438
        /** @var UserRepositoryInterface $userRepository */
439
        $userRepository = app(UserRepositoryInterface::class);
440
441
        /** @var User $user */
442
        foreach ($userRepository->all() as $user) {
443
            $sum = (string)$user->transactions()->sum('amount');
444
            if (0 !== bccomp($sum, '0')) {
445
                $this->error('Error: Transactions for user #' . $user->id . ' (' . $user->email . ') are off by ' . $sum . '!');
446
            } else {
447
                $this->info(sprintf('Amount integrity OK for user #%d', $user->id));
448
            }
449
        }
450
    }
451
452
    /**
453
     * Reports on deleted transactions that are connected to a not deleted journal.
454
     */
455
    private function reportTransactions()
456
    {
457
        $set = Transaction::leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
458
                          ->whereNotNull('transactions.deleted_at')
459
                          ->whereNull('transaction_journals.deleted_at')
460
                          ->get(
461
                              ['transactions.id as transaction_id', 'transactions.deleted_at as transaction_deleted', 'transaction_journals.id as journal_id',
462
                               'transaction_journals.deleted_at',]
463
                          );
464
        /** @var stdClass $entry */
465
        foreach ($set as $entry) {
466
            $this->error(
467
                'Error: Transaction journal #' . $entry->journal_id . ' should have been deleted, but has not.' .
468
                ' Find it in the table called "transaction_journals" and change the "deleted_at" field to: "' . $entry->transaction_deleted . '"'
469
            );
470
        }
471
    }
472
473
    /**
474
     * Report on transfers that have budgets.
475
     */
476
    private function reportTransfersBudgets()
477
    {
478
        $set = TransactionJournal::distinct()
479
                                 ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
480
                                 ->leftJoin('budget_transaction_journal', 'transaction_journals.id', '=', 'budget_transaction_journal.transaction_journal_id')
481
                                 ->whereIn('transaction_types.type', [TransactionType::TRANSFER, TransactionType::DEPOSIT])
482
                                 ->whereNotNull('budget_transaction_journal.budget_id')->get(['transaction_journals.*']);
483
484
        /** @var TransactionJournal $entry */
485
        foreach ($set as $entry) {
486
            $this->error(
487
                sprintf(
488
                    'Error: Transaction journal #%d is a %s, but has a budget. Edit it without changing anything, so the budget will be removed.',
489
                    $entry->id,
490
                    $entry->transactionType->type
491
                )
492
            );
493
        }
494
    }
495
}
496