Fix start and upgrade commands

This commit is contained in:
James Cole
2023-04-16 07:33:12 +02:00
parent 05e307136c
commit e6a84ab387
19 changed files with 391 additions and 274 deletions

View File

@@ -0,0 +1,230 @@
<?php
/*
* DecryptDatabase.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade;
use Crypt;
use DB;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Preference;
use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Log;
use JsonException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use stdClass;
/**
* Class DecryptDatabase
*/
class DecryptDatabase extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Decrypts the database.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:decrypt-all';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->line('Going to decrypt the database.');
$tables = [
'accounts' => ['name', 'iban'],
'attachments' => ['filename', 'mime', 'title', 'description'],
'bills' => ['name', 'match'],
'budgets' => ['name'],
'categories' => ['name'],
'piggy_banks' => ['name'],
'preferences' => ['data'],
'tags' => ['tag', 'description'],
'transaction_journals' => ['description'],
'transactions' => ['description'],
'journal_links' => ['comment'],
];
/**
* @var string $table
* @var array $fields
*/
foreach ($tables as $table => $fields) {
$this->decryptTable($table, $fields);
}
$this->info('Done!');
return 0;
}
/**
* @param string $table
* @param array $fields
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function decryptTable(string $table, array $fields): void
{
if ($this->isDecrypted($table)) {
$this->info(sprintf('No decryption required for table "%s".', $table));
return;
}
foreach ($fields as $field) {
$this->decryptField($table, $field);
}
$this->line(sprintf('Decrypted the data in table "%s".', $table));
// mark as decrypted:
$configName = sprintf('is_decrypted_%s', $table);
app('fireflyconfig')->set($configName, true);
}
/**
* @param string $table
*
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isDecrypted(string $table): bool
{
$configName = sprintf('is_decrypted_%s', $table);
$configVar = null;
try {
$configVar = app('fireflyconfig')->get($configName, false);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
* @param string $table
* @param string $field
*/
private function decryptField(string $table, string $field): void
{
$rows = DB::table($table)->get(['id', $field]);
/** @var stdClass $row */
foreach ($rows as $row) {
$this->decryptRow($table, $field, $row);
}
}
/**
* @param string $table
* @param string $field
* @param stdClass $row
*/
private function decryptRow(string $table, string $field, stdClass $row): void
{
$original = $row->$field;
if (null === $original) {
return;
}
$id = (int)$row->id;
$value = '';
try {
$value = $this->tryDecrypt($original);
} catch (FireflyException $e) {
$message = sprintf('Could not decrypt field "%s" in row #%d of table "%s": %s', $field, $id, $table, $e->getMessage());
$this->error($message);
Log::error($message);
Log::error($e->getTraceAsString());
}
// A separate routine for preferences table:
if ('preferences' === $table) {
$this->decryptPreferencesRow($id, $value);
return;
}
if ($value !== $original) {
DB::table($table)->where('id', $id)->update([$field => $value]);
}
}
/**
* Tries to decrypt data. Will only throw an exception when the MAC is invalid.
*
* @param mixed $value
*
* @return string
* @throws FireflyException
*/
private function tryDecrypt($value)
{
try {
$value = Crypt::decrypt($value);
} catch (DecryptException $e) {
if ('The MAC is invalid.' === $e->getMessage()) {
throw new FireflyException($e->getMessage(), 0, $e);
}
}
return $value;
}
/**
* @param int $id
* @param string $value
*/
private function decryptPreferencesRow(int $id, string $value): void
{
// try to json_decrypt the value.
try {
$newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value;
} catch (JsonException $e) {
$message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage());
$this->error($message);
app('log')->warning($message);
app('log')->warning($value);
app('log')->warning($e->getTraceAsString());
return;
}
/** @var Preference $object */
$object = Preference::find((int)$id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
/*
* FixPostgresSequences.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade;
use DB;
use Illuminate\Console\Command;
/**
* Class FixPostgresSequences
*/
class FixPostgresSequences extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes issues with PostgreSQL sequences.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-pgsql-sequences';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
if (DB::connection()->getName() !== 'pgsql') {
$this->info('Command executed successfully.');
return 0;
}
$this->line('Going to verify PostgreSQL table sequences.');
$tablesToCheck = [
'2fa_tokens',
'account_meta',
'account_types',
'accounts',
'attachments',
'auto_budgets',
'available_budgets',
'bills',
'budget_limits',
'budget_transaction',
'budget_transaction_journal',
'budgets',
'categories',
'category_transaction',
'category_transaction_journal',
'configuration',
'currency_exchange_rates',
'failed_jobs',
'group_journals',
'jobs',
'journal_links',
'journal_meta',
'limit_repetitions',
'link_types',
'locations',
'migrations',
'notes',
'oauth_clients',
'oauth_personal_access_clients',
'object_groups',
'permissions',
'piggy_bank_events',
'piggy_bank_repetitions',
'piggy_banks',
'preferences',
'recurrences',
'recurrences_meta',
'recurrences_repetitions',
'recurrences_transactions',
'roles',
'rt_meta',
'rule_actions',
'rule_groups',
'rule_triggers',
'rules',
'tag_transaction_journal',
'tags',
'transaction_currencies',
'transaction_groups',
'transaction_journals',
'transaction_types',
'transactions',
'users',
'webhook_attempts',
'webhook_messages',
'webhooks',
];
foreach ($tablesToCheck as $tableToCheck) {
$this->info(sprintf('Checking the next id sequence for table "%s".', $tableToCheck));
$highestId = DB::table($tableToCheck)->select(DB::raw('MAX(id)'))->first();
$nextId = DB::table($tableToCheck)->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))->first();
if (null === $nextId) {
$this->line(sprintf('nextval is NULL for table "%s", go to next table.', $tableToCheck));
continue;
}
if ($nextId->nextval < $highestId->max) {
DB::select(sprintf('SELECT setval(\'%s_id_seq\', %d)', $tableToCheck, $highestId->max));
$highestId = DB::table($tableToCheck)->select(DB::raw('MAX(id)'))->first();
$nextId = DB::table($tableToCheck)->select(DB::raw(sprintf('nextval(\'%s_id_seq\')', $tableToCheck)))->first();
if ($nextId->nextval > $highestId->max) {
$this->info(sprintf('Table "%s" autoincrement corrected.', $tableToCheck));
}
if ($nextId->nextval <= $highestId->max) {
$this->warn(sprintf('Arff! The nextval sequence is still all screwed up on table "%s".', $tableToCheck));
}
}
if ($nextId->nextval >= $highestId->max) {
$this->info(sprintf('Table "%s" autoincrement is correct.', $tableToCheck));
}
}
return 0;
}
}

View File

@@ -57,7 +57,8 @@ class UpgradeDatabase extends Command
{
$this->callInitialCommands();
$commands = [
// there are 14 upgrade commands.
'firefly-iii:fix-pgsql-sequences',
'firefly-iii:decrypt-all',
'firefly-iii:transaction-identifiers',
'firefly-iii:migrate-to-groups',
'firefly-iii:account-currencies',
@@ -75,41 +76,7 @@ class UpgradeDatabase extends Command
'firefly-iii:migrate-recurrence-type',
'firefly-iii:upgrade-liabilities',
'firefly-iii:liabilities-600',
// there are 16 verify commands.
'firefly-iii:fix-piggies',
'firefly-iii:create-link-types',
'firefly-iii:create-access-tokens',
'firefly-iii:remove-bills',
'firefly-iii:fix-negative-limits',
'firefly-iii:enable-currencies',
'firefly-iii:fix-transfer-budgets',
'firefly-iii:fix-uneven-amount',
'firefly-iii:delete-zero-amount',
'firefly-iii:delete-orphaned-transactions',
'firefly-iii:delete-empty-journals',
'firefly-iii:delete-empty-groups',
'firefly-iii:fix-account-types',
'firefly-iii:fix-account-order',
'firefly-iii:rename-meta-fields',
'firefly-iii:fix-ob-currencies',
'firefly-iii:fix-long-descriptions',
'firefly-iii:fix-recurring-transactions',
'firefly-iii:unify-group-accounts',
'firefly-iii:fix-transaction-types',
'firefly-iii:fix-frontpage-accounts',
'firefly-iii:fix-ibans',
'firefly-iii:create-group-memberships',
'firefly-iii:upgrade-group-information',
// two report commands
'firefly-iii:report-empty-objects',
'firefly-iii:report-sum',
'firefly-iii:restore-oauth-keys',
// instructions
'firefly:instructions update',
'firefly-iii:verify-security-alerts',
'firefly-iii:budget-limit-periods',
];
$args = [];
if ($this->option('force')) {
@@ -117,9 +84,7 @@ class UpgradeDatabase extends Command
}
foreach ($commands as $command) {
$this->line(sprintf('Now executing %s', $command));
Artisan::call($command, $args);
$result = Artisan::output();
echo $result;
$this->call($command, $args);
}
// set new DB version.
app('fireflyconfig')->set('db_version', (int)config('firefly.db_version'));
@@ -129,22 +94,19 @@ class UpgradeDatabase extends Command
return 0;
}
/**
* @return void
*/
private function callInitialCommands(): void
{
$this->line('Now seeding the database...');
Artisan::call('migrate', ['--seed' => true, '--force' => true]);
$result = Artisan::output();
echo $result;
$this->call('migrate', ['--seed' => true, '--force' => true]);
$this->line('Fix PostgreSQL sequences.');
Artisan::call('firefly-iii:fix-pgsql-sequences');
$result = Artisan::output();
echo $result;
$this->call('firefly-iii:fix-pgsql-sequences');
$this->line('Now decrypting the database (if necessary)...');
Artisan::call('firefly-iii:decrypt-all');
$result = Artisan::output();
echo $result;
$this->call('firefly-iii:decrypt-all');
$this->line('Done!');
}