Compare commits

..

10 Commits

Author SHA1 Message Date
github-actions[bot] 3de7cfd3bf Merge pull request #12181 from firefly-iii/release-1777003029
🤖 Automatically merge the PR into the develop branch.
2026-04-24 05:57:17 +02:00
JC5 a6fd805202 🤖 Auto commit for release 'develop' on 2026-04-24 2026-04-24 05:57:09 +02:00
James Cole 541e1ac7b4 And now fix them for real. 2026-04-24 05:51:44 +02:00
James Cole 1c6b8af3a3 Fix Mago issues. 2026-04-24 05:45:42 +02:00
James Cole b33b22e0b3 Let's add some authentication before I am drowned in reports. 2026-04-24 05:37:55 +02:00
James Cole 30cce327e8 Add helpers and fix missing string. 2026-04-24 05:34:40 +02:00
James Cole 994e3dae18 Merge pull request #12179 from tasnim0tantawi/feature/client-side-password-verification-on-register
implement password validation JS script
2026-04-24 05:09:19 +02:00
tasnim0tantawi b065150968 implement password validation JS script 2026-04-23 22:08:24 +03:00
James Cole d18e9ffad0 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-04-23 03:08:53 +02:00
James Cole fad5438909 Fix https://github.com/firefly-iii/firefly-iii/issues/12170 2026-04-23 03:07:03 +02:00
31 changed files with 360 additions and 147 deletions
+1
View File
@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
## 2026
- tasnim0tantawi
- Joe Longendyke
- Daniel Holøien
- Matthew Grove
@@ -158,7 +158,10 @@ final class TagController extends Controller
'currency_id' => (string) $foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
$response[$foreignKey]['difference'] = bcadd(
(string) $response[$foreignKey]['difference'],
Steam::positive($journal['foreign_amount'])
);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference'];
}
}
@@ -155,7 +155,10 @@ final class TagController extends Controller
'currency_id' => (string) $foreignCurrencyId,
'currency_code' => $journal['foreign_currency_code'],
];
$response[$foreignKey]['difference'] = bcadd((string) $response[$foreignKey]['difference'], Steam::positive($journal['foreign_amount']));
$response[$foreignKey]['difference'] = bcadd(
(string) $response[$foreignKey]['difference'],
Steam::positive($journal['foreign_amount'])
);
$response[$foreignKey]['difference_float'] = (float) $response[$foreignKey]['difference']; // intentional float
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
/*
* helpers.php
* Copyright (c) 2026 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);
use FireflyIII\Exceptions\FireflyException;
use function Safe\mb_ord;
use function Safe\preg_match;
use function Safe\preg_replace_callback;
if (!function_exists('env_default_when_empty')) {
/**
* @return null|mixed
*/
function env_default_when_empty(mixed $value, bool|int|string|null $default = null): mixed
{
if (null === $value) {
return $default;
}
if ('' === $value) {
return $default;
}
return $value;
}
}
if (!function_exists('string_is_equal')) {
function string_is_equal(string $left, string $right): bool
{
return $left === $right;
}
}
if (!function_exists('blade_escape_js')) {
function blade_escape_js(string $string): string
{
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if (0 === preg_match('//u', $string)) {
throw new FireflyException('The string to escape is not a valid UTF-8 string.');
}
return preg_replace_callback(
'#[^a-zA-Z0-9,\._]#Su',
static function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
$short = match ($char) {
'\\' => '\\\\',
'/' => '\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
default => false
};
if ($short) {
return $short;
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10_000 > $codepoint) {
return \sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10_000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return \sprintf('\u%04X\u%04X', $high, $low);
},
$string
);
}
}
@@ -255,7 +255,10 @@ final class IndexController extends Controller
if (count($bill['paid_dates']) < count($bill['pay_dates'])) {
$count = count($bill['pay_dates']) - count($bill['paid_dates']);
if ($count > 0) {
$avg = bcdiv(bcadd((string) $bill['amount_min'], (string) $bill['amount_max']), '2');
$avg = bcdiv(
bcadd((string) $bill['amount_min'], (string) $bill['amount_max']),
'2'
);
$avg = bcmul($avg, (string) $count);
$sums[$groupOrder][$currencyId]['total_left_to_pay'] = bcadd($sums[$groupOrder][$currencyId]['total_left_to_pay'], $avg);
Log::debug(
@@ -198,7 +198,13 @@ final class BudgetLimitController extends Controller
if ($request->expectsJson()) {
$array = $limit->toArray();
// add some extra metadata:
$spentArr = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);
$spentArr = $this->opsRepository->sumExpenses(
$limit->start_date,
$limit->end_date,
null,
new Collection()->push($budget),
$currency
);
$array['spent'] = $spentArr[$currency->id]['sum'] ?? '0';
$array['left_formatted'] = Amount::formatAnything($limit->transactionCurrency, bcadd($array['spent'], (string) $array['amount']));
$array['amount_formatted'] = Amount::formatAnything($limit->transactionCurrency, $limit['amount']);
@@ -245,13 +245,7 @@ final class IndexController extends Controller
$inPast = $limitPeriod->startsBefore(now()) && $limitPeriod->endsBefore(now());
$currency = $limit->transactionCurrency ?? $primaryCurrency;
$amount = Steam::bcround($limit->amount, $currency->decimal_places);
$spent = $this->opsRepository->sumExpenses(
$limit->start_date,
$limit->end_date,
null,
new Collection()->push($budget),
$currency
);
$spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection()->push($budget), $currency);
$spentAmount = $spent[$currency->id]['sum'] ?? '0';
$array['budgeted'][] = [
'id' => $limit->id,
@@ -289,7 +283,10 @@ final class IndexController extends Controller
if (array_key_exists($currency->id, $spentArr) && array_key_exists('sum', $spentArr[$currency->id])) {
$array['spent'][$currency->id]['spent'] = $spentArr[$currency->id]['sum'];
$array['spent'][$currency->id]['spent_outside'] = bcmul(bcsub($spentInLimits[$currency->id], $spentArr[$currency->id]['sum']), '-1');
$array['spent'][$currency->id]['spent_outside'] = bcmul(
bcsub($spentInLimits[$currency->id], $spentArr[$currency->id]['sum']),
'-1'
);
$array['spent'][$currency->id]['currency_id'] = $currency->id;
$array['spent'][$currency->id]['currency_symbol'] = $currency->symbol;
$array['spent'][$currency->id]['currency_decimal_places'] = $currency->decimal_places;
@@ -539,7 +539,13 @@ final class BudgetController extends Controller
}
// get spent amount in this period for this currency.
$sum = $this->opsRepository->sumExpenses($currentStart, $currentEnd, $accounts, new Collection()->push($budget), $currency);
$sum = $this->opsRepository->sumExpenses(
$currentStart,
$currentEnd,
$accounts,
new Collection()->push($budget),
$currency
);
$amount = Steam::positive($sum[$currency->id]['sum'] ?? '0');
$chartData[0]['entries'][$title] = Steam::bcround($amount, $currency->decimal_places);
@@ -115,6 +115,9 @@ final class OAuthController extends Controller
public function listClients(): JsonResponse
{
if (!auth()->check()) {
return response()->json([]);
}
Log::debug('Now in listClients()');
// Retrieving all the OAuth app clients that belong to the user...
$clients = auth()->user()->oauthApps()->where('revoked', false)->get();
+1 -1
View File
@@ -63,7 +63,7 @@ class SecureHeaders
"base-uri 'self'",
// "form-action 'self'", // safe
"font-src 'self' data:",
sprintf("connect-src 'self' %s", $trackingScriptSrc),
sprintf("connect-src 'self' https://api.pwnedpasswords.com %s", $trackingScriptSrc),
sprintf("img-src 'self' data: 'nonce-%1s' ", $nonce),
"manifest-src 'self'",
];
+14 -2
View File
@@ -122,7 +122,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
// if has one, calculate expenses and use that as a base.
$repository = app(OperationsRepositoryInterface::class);
$repository->setUser($autoBudget->budget->user);
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection()->push($autoBudget->budget), $autoBudget->transactionCurrency);
$spent = $repository->sumExpenses(
$previousStart,
$previousEnd,
null,
new Collection()->push($autoBudget->budget),
$autoBudget->transactionCurrency
);
$currencyId = $autoBudget->transaction_currency_id;
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
@@ -212,7 +218,13 @@ class CreateAutoBudgetLimits implements ShouldQueue
// if has one, calculate expenses and use that as a base.
$repository = app(OperationsRepositoryInterface::class);
$repository->setUser($autoBudget->budget->user);
$spent = $repository->sumExpenses($previousStart, $previousEnd, null, new Collection()->push($autoBudget->budget), $autoBudget->transactionCurrency);
$spent = $repository->sumExpenses(
$previousStart,
$previousEnd,
null,
new Collection()->push($autoBudget->budget),
$autoBudget->transactionCurrency
);
$currencyId = $autoBudget->transaction_currency_id;
$spentAmount = $spent[$currencyId]['sum'] ?? '0';
Log::debug(sprintf('Spent in previous budget period (%s-%s) is %s', $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $spentAmount));
@@ -98,7 +98,7 @@ trait ModifiesPiggyBanks
$maxAmount = $leftOnAccount;
Log::debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d H:i:s')));
Log::debug(sprintf('Saved so far: %s', $savedSoFar));
Log::debug(sprintf('Saved so far : %s', $savedSoFar));
if (0 !== bccomp($piggyBank->target_amount, '0')) {
$leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar);
@@ -110,7 +110,7 @@ trait ModifiesPiggyBanks
$compare = bccomp($amount, (string) $maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
Log::debug(sprintf('Compare %s to %s <= 0? %d, so canAddAmount is %s', $amount, $maxAmount, $compare, var_export($result, true)));
return $result;
}
+5 -4
View File
@@ -54,8 +54,9 @@ class IsEnoughInAccounts implements ValidationRule
$accounts = $this->data['accounts'];
foreach ($accounts as $info) {
$account = $repository->find((int) $info['account_id']);
$amount = $info['current_amount'] ?? '0';
$account = $repository->find((int) $info['account_id']);
$amount = $info['current_amount'] ?? '0';
$savedSoFar = $piggyRepos->getCurrentAmount($this->piggyBank, $account);
if (null === $account) {
$fail('validation.no_asset_account')->translate();
@@ -64,8 +65,8 @@ class IsEnoughInAccounts implements ValidationRule
if ('' === $amount || 0 === bccomp($amount, '0')) {
continue;
}
$diff = bcsub($amount, $piggyRepos->getCurrentAmount($this->piggyBank, $account));
if (1 === bccomp($diff, '0') && !$piggyRepos->canAddAmount($this->piggyBank, $account, $amount)) {
$diff = bcsub($amount, $savedSoFar);
if (1 === bccomp($diff, '0') && !$piggyRepos->canAddAmount($this->piggyBank, $account, $diff)) {
$fail('validation.cannot_add_piggy_amount')->translate();
}
}
@@ -222,7 +222,14 @@ trait AugumentData
$currentEnd->addMonth();
}
// primary currency amount.
$expenses = $opsRepository->sumExpenses($currentStart, $currentEnd, null, $budgetCollection, $entry->transactionCurrency, $this->convertToPrimary);
$expenses = $opsRepository->sumExpenses(
$currentStart,
$currentEnd,
null,
$budgetCollection,
$entry->transactionCurrency,
$this->convertToPrimary
);
$spent = $expenses[$currency->id]['sum'] ?? '0';
$entry->pc_spent = $spent;
@@ -354,7 +354,10 @@ class RecurringEnrichment implements EnrichmentInterface
/** @var RecurrenceRepetition $repetition */
foreach ($set as $repetition) {
$recurrence = $this->collection->filter(static fn (Recurrence $item): bool => (int) $item->id === (int) $repetition->recurrence_id)->first();
$recurrence = $this->collection
->filter(static fn (Recurrence $item): bool => (int) $item->id === (int) $repetition->recurrence_id)
->first()
;
$fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
$recurrenceId = (int) $repetition->recurrence_id;
$repId = (int) $repetition->id;
+2 -2
View File
@@ -522,7 +522,7 @@ class OperatorQuerySearch implements SearchInterface
$stringMethod = 'str_contains';
}
if (StringPosition::IS === $stringPosition) {
$stringMethod = 'stringIsEqual';
$stringMethod = 'string_is_equal';
}
// get accounts:
@@ -619,7 +619,7 @@ class OperatorQuerySearch implements SearchInterface
$stringMethod = 'str_contains';
}
if (StringPosition::IS === $stringPosition) {
$stringMethod = 'stringIsEqual';
$stringMethod = 'string_is_equal';
}
// search for accounts:
-22
View File
@@ -63,29 +63,7 @@ use PragmaRX\Google2FALaravel\Middleware as MFAMiddleware;
bcscale(12);
if (!function_exists('envDefaultWhenEmpty')) {
/**
*
* @return mixed|null
*/
function envDefaultWhenEmpty(mixed $value, string | int | bool | null $default = null): mixed
{
if(null === $value) {
return $default;
}
if('' === $value) {
return $default;
}
return $value;
}
}
if (!function_exists('stringIsEqual')) {
function stringIsEqual(string $left, string $right): bool
{
return $left === $right;
}
}
$app = Application::configure(basePath: dirname(__DIR__))
->withRouting(
+3
View File
@@ -135,6 +135,9 @@
"suggest": {},
"autoload": {
"files": [
"app/Helpers/Functions/helpers.php"
],
"psr-4": {
"FireflyIII\\": "app/",
"Domain\\": "domain/",
Generated
+6 -6
View File
@@ -11511,16 +11511,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "14.1.3",
"version": "14.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "24dc6fcf9f2a983de5b3f1199fb01e88d68e7474"
"reference": "13dd8d33c6a23da100acec635cd351bfa7ff1a98"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/24dc6fcf9f2a983de5b3f1199fb01e88d68e7474",
"reference": "24dc6fcf9f2a983de5b3f1199fb01e88d68e7474",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/13dd8d33c6a23da100acec635cd351bfa7ff1a98",
"reference": "13dd8d33c6a23da100acec635cd351bfa7ff1a98",
"shasum": ""
},
"require": {
@@ -11576,7 +11576,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.1.3"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.1.4"
},
"funding": [
{
@@ -11596,7 +11596,7 @@
"type": "tidelift"
}
],
"time": "2026-04-18T05:41:54+00:00"
"time": "2026-04-23T17:00:20+00:00"
},
{
"name": "phpunit/php-file-iterator",
+5 -5
View File
@@ -36,12 +36,12 @@ use Illuminate\Support\Facades\URL;
use Spatie\Html\Facades\Html;
return [
'name' => envDefaultWhenEmpty(env('APP_NAME'), 'Firefly III'),
'env' => envDefaultWhenEmpty(env('APP_ENV'), 'production'),
'name' => env_default_when_empty(env('APP_NAME'), 'Firefly III'),
'env' => env_default_when_empty(env('APP_ENV'), 'production'),
'debug' => env('APP_DEBUG', false),
'url' => envDefaultWhenEmpty(env('APP_URL'), 'http://localhost'),
'timezone' => envDefaultWhenEmpty(env('TZ'), 'UTC'),
'locale' => envDefaultWhenEmpty(env('DEFAULT_LANGUAGE'), 'en_US'),
'url' => env_default_when_empty(env('APP_URL'), 'http://localhost'),
'timezone' => env_default_when_empty(env('TZ'), 'UTC'),
'locale' => env_default_when_empty(env('DEFAULT_LANGUAGE'), 'en_US'),
'fallback_locale' => 'en_US',
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
+2 -2
View File
@@ -37,10 +37,10 @@ return [
*/
'defaults' => [
'guard' => envDefaultWhenEmpty(env('AUTHENTICATION_GUARD'), 'web'),
'guard' => env_default_when_empty(env('AUTHENTICATION_GUARD'), 'web'),
'passwords' => 'users',
],
'guard_header' => envDefaultWhenEmpty(env('AUTHENTICATION_GUARD_HEADER'), 'REMOTE_USER'),
'guard_header' => env_default_when_empty(env('AUTHENTICATION_GUARD_HEADER'), 'REMOTE_USER'),
'guard_email' => env('AUTHENTICATION_GUARD_EMAIL'),
/*
+1 -1
View File
@@ -36,7 +36,7 @@ return [
|
*/
'default' => envDefaultWhenEmpty(env('CACHE_DRIVER'), 'file'),
'default' => env_default_when_empty(env('CACHE_DRIVER'), 'file'),
/*
|--------------------------------------------------------------------------
+20 -20
View File
@@ -49,7 +49,7 @@ $mysql_ssl_ciphers = env('MYSQL_SSL_CIPHER');
$mysql_ssl_verify = env('MYSQL_SSL_VERIFY_SERVER_CERT');
$mySqlSSLOptions = [];
$useSSL = envDefaultWhenEmpty(env('MYSQL_USE_SSL'), false);
$useSSL = env_default_when_empty(env('MYSQL_USE_SSL'), false);
if (false !== $useSSL && null !== $useSSL && '' !== $useSSL) {
if (null !== $mysql_ssl_ca_dir) {
$mySqlSSLOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $mysql_ssl_ca_dir;
@@ -72,19 +72,19 @@ if (false !== $useSSL && null !== $useSSL && '' !== $useSSL) {
}
return [
'default' => envDefaultWhenEmpty(env('DB_CONNECTION'), 'mysql'),
'default' => env_default_when_empty(env('DB_CONNECTION'), 'mysql'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => envDefaultWhenEmpty(env('DB_DATABASE'), storage_path('database/database.sqlite')),
'database' => env_default_when_empty(env('DB_DATABASE'), storage_path('database/database.sqlite')),
'prefix' => '',
],
'mysql' => [
'driver' => 'mysql',
'host' => envDefaultWhenEmpty(env('DB_HOST'), $host),
'port' => envDefaultWhenEmpty(env('DB_PORT'), $port),
'database' => envDefaultWhenEmpty(env('DB_DATABASE'), $database),
'username' => envDefaultWhenEmpty(env('DB_USERNAME'), $username),
'host' => env_default_when_empty(env('DB_HOST'), $host),
'port' => env_default_when_empty(env('DB_PORT'), $port),
'database' => env_default_when_empty(env('DB_DATABASE'), $database),
'username' => env_default_when_empty(env('DB_USERNAME'), $username),
'password' => env('DB_PASSWORD', $password),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
@@ -96,16 +96,16 @@ return [
],
'pgsql' => [
'driver' => 'pgsql',
'host' => envDefaultWhenEmpty(env('DB_HOST'), $host),
'port' => envDefaultWhenEmpty(env('DB_PORT'), $port),
'database' => envDefaultWhenEmpty(env('DB_DATABASE'), $database),
'username' => envDefaultWhenEmpty(env('DB_USERNAME'), $username),
'host' => env_default_when_empty(env('DB_HOST'), $host),
'port' => env_default_when_empty(env('DB_PORT'), $port),
'database' => env_default_when_empty(env('DB_DATABASE'), $database),
'username' => env_default_when_empty(env('DB_USERNAME'), $username),
'password' => env('DB_PASSWORD', $password),
'charset' => 'utf8',
'prefix' => '',
'search_path' => envDefaultWhenEmpty(env('PGSQL_SCHEMA'), 'public'),
'schema' => envDefaultWhenEmpty(env('PGSQL_SCHEMA'), 'public'),
'sslmode' => envDefaultWhenEmpty(env('PGSQL_SSL_MODE'), 'prefer'),
'search_path' => env_default_when_empty(env('PGSQL_SCHEMA'), 'public'),
'schema' => env_default_when_empty(env('PGSQL_SCHEMA'), 'public'),
'sslmode' => env_default_when_empty(env('PGSQL_SSL_MODE'), 'prefer'),
'sslcert' => env('PGSQL_SSL_CERT'),
'sslkey' => env('PGSQL_SSL_KEY'),
'sslrootcert' => env('PGSQL_SSL_ROOT_CERT'),
@@ -139,21 +139,21 @@ return [
// 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
],
'default' => [
'scheme' => envDefaultWhenEmpty(env('REDIS_SCHEME'), 'tcp'),
'scheme' => env_default_when_empty(env('REDIS_SCHEME'), 'tcp'),
'url' => env('REDIS_URL'),
'path' => env('REDIS_PATH'),
'host' => envDefaultWhenEmpty(env('REDIS_HOST'), '127.0.0.1'),
'port' => envDefaultWhenEmpty(env('REDIS_PORT'), 6379),
'host' => env_default_when_empty(env('REDIS_HOST'), '127.0.0.1'),
'port' => env_default_when_empty(env('REDIS_PORT'), 6379),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'scheme' => envDefaultWhenEmpty(env('REDIS_SCHEME'), 'tcp'),
'scheme' => env_default_when_empty(env('REDIS_SCHEME'), 'tcp'),
'url' => env('REDIS_URL'),
'path' => env('REDIS_PATH'),
'host' => envDefaultWhenEmpty(env('REDIS_HOST'), '127.0.0.1'),
'port' => envDefaultWhenEmpty(env('REDIS_PORT'), 6379),
'host' => env_default_when_empty(env('REDIS_HOST'), '127.0.0.1'),
'port' => env_default_when_empty(env('REDIS_PORT'), 6379),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_CACHE_DB', '1'),
+10 -10
View File
@@ -75,18 +75,18 @@ return [
'webhooks' => true,
'handle_debts' => true,
'expression_engine' => true,
'running_balance_column' => (bool)envDefaultWhenEmpty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
'running_balance_column' => (bool)env_default_when_empty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2026-04-22',
'build_time' => 1776876664,
'version' => 'develop/2026-04-24',
'build_time' => 1777003029,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.
// Docker build info, if present:
'is_docker' => env('IS_DOCKER', false),
'base_image_build' => envDefaultWhenEmpty(env('BASE_IMAGE_BUILD'), '(unknown)'),
'base_image_date' => envDefaultWhenEmpty(env('BASE_IMAGE_DATE'), '(unknown)'),
'base_image_build' => env_default_when_empty(env('BASE_IMAGE_BUILD'), '(unknown)'),
'base_image_date' => env_default_when_empty(env('BASE_IMAGE_DATE'), '(unknown)'),
'is_local_dev' => env('IS_LOCAL_DEV', false),
// generic settings
@@ -112,8 +112,8 @@ return [
'tracker_url' => env('TRACKER_URL', ''),
// authentication settings
'authentication_guard' => envDefaultWhenEmpty(env('AUTHENTICATION_GUARD'), 'web'),
'custom_logout_url' => envDefaultWhenEmpty(env('CUSTOM_LOGOUT_URL'), ''),
'authentication_guard' => env_default_when_empty(env('AUTHENTICATION_GUARD'), 'web'),
'custom_logout_url' => env_default_when_empty(env('CUSTOM_LOGOUT_URL'), ''),
// static config (cannot be changed by user)
'update_endpoint' => 'https://version.firefly-iii.org/index.json',
@@ -194,8 +194,8 @@ return [
'convertToPrimary' => false,
],
'default_currency' => 'EUR',
'default_language' => envDefaultWhenEmpty(env('DEFAULT_LANGUAGE'), 'en_US'),
'default_locale' => envDefaultWhenEmpty(env('DEFAULT_LOCALE'), 'equal'),
'default_language' => env_default_when_empty(env('DEFAULT_LANGUAGE'), 'en_US'),
'default_locale' => env_default_when_empty(env('DEFAULT_LOCALE'), 'equal'),
// account types that may have or set a currency
'valid_currency_account_types' => [
@@ -224,7 +224,7 @@ return [
'available_dark_modes' => ['light', 'dark', 'browser'],
'bill_reminder_periods' => [90, 30, 14, 7, 0],
'valid_view_ranges' => ['1D', '1W', '1M', '3M', '6M', '1Y'],
'valid_url_protocols' => envDefaultWhenEmpty(env('VALID_URL_PROTOCOLS'), 'http,https,ftp,ftps,mailto'), // no longer used, only for default.
'valid_url_protocols' => env_default_when_empty(env('VALID_URL_PROTOCOLS'), 'http,https,ftp,ftps,mailto'), // no longer used, only for default.
'allowedMimes' => [
// plain files
'text/plain',
+14 -14
View File
@@ -34,7 +34,7 @@ $validChannels = ['single', 'papertrail', 'stdout', 'daily', 'syslog', 'err
$validAuditChannels = ['audit_papertrail', 'audit_stdout', 'audit_stdout', 'audit_daily', 'audit_syslog', 'audit_errorlog'];
// which settings did the user set, if any?
$defaultLogChannel = (string) envDefaultWhenEmpty(env('LOG_CHANNEL'), 'stack');
$defaultLogChannel = (string) env_default_when_empty(env('LOG_CHANNEL'), 'stack');
$auditLogChannel = (string) env('AUDIT_LOG_CHANNEL');
if ('stack' === $defaultLogChannel) {
@@ -60,8 +60,8 @@ return [
|
*/
'default' => envDefaultWhenEmpty(env('LOG_CHANNEL'), 'stack'),
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'default' => env_default_when_empty(env('LOG_CHANNEL'), 'stack'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
/*
|--------------------------------------------------------------------------
| Log Channels
@@ -93,11 +93,11 @@ return [
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
],
'papertrail' => [
'driver' => 'monolog',
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
'handler' => SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_HOST'),
@@ -107,21 +107,21 @@ return [
'stdout' => [
'driver' => 'single',
'path' => 'php://stdout',
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/ff3-'.PHP_SAPI.'.log'),
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
'days' => 7,
],
'syslog' => [
'driver' => 'syslog',
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
],
'errorlog' => [
'driver' => 'errorlog',
'level' => envDefaultWhenEmpty(env('APP_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('APP_LOG_LEVEL'), 'info'),
],
/*
@@ -130,7 +130,7 @@ return [
*/
'audit_papertrail' => [
'driver' => 'monolog',
'level' => envDefaultWhenEmpty(env('AUDIT_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('AUDIT_LOG_LEVEL'), 'info'),
'handler' => SyslogUdpHandler::class,
'tap' => [AuditLogger::class],
'handler_with' => [
@@ -142,24 +142,24 @@ return [
'driver' => 'single',
'path' => 'php://stdout',
'tap' => [AuditLogger::class],
'level' => envDefaultWhenEmpty(env('AUDIT_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('AUDIT_LOG_LEVEL'), 'info'),
],
'audit_daily' => [
'driver' => 'daily',
'path' => storage_path('logs/ff3-audit.log'),
'tap' => [AuditLogger::class],
'level' => envDefaultWhenEmpty(env('AUDIT_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('AUDIT_LOG_LEVEL'), 'info'),
'days' => 90,
],
'audit_syslog' => [
'driver' => 'syslog',
'tap' => [AuditLogger::class],
'level' => envDefaultWhenEmpty(env('AUDIT_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('AUDIT_LOG_LEVEL'), 'info'),
],
'audit_errorlog' => [
'driver' => 'errorlog',
'tap' => [AuditLogger::class],
'level' => envDefaultWhenEmpty(env('AUDIT_LOG_LEVEL'), 'info'),
'level' => env_default_when_empty(env('AUDIT_LOG_LEVEL'), 'info'),
],
],
];
+7 -7
View File
@@ -34,16 +34,16 @@ return [
| and used as needed; however, this mailer will be used by default.
|
*/
'default' => envDefaultWhenEmpty(env('MAIL_MAILER'), 'log'),
'default' => env_default_when_empty(env('MAIL_MAILER'), 'log'),
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => envDefaultWhenEmpty(env('MAIL_HOST'), 'smtp.mailtrap.io'),
'host' => env_default_when_empty(env('MAIL_HOST'), 'smtp.mailtrap.io'),
'port' => (int) env('MAIL_PORT', 2525),
'encryption' => envDefaultWhenEmpty(env('MAIL_ENCRYPTION'), 'tls'),
'username' => envDefaultWhenEmpty(env('MAIL_USERNAME'), 'user@example.com'),
'password' => envDefaultWhenEmpty(env('MAIL_PASSWORD'), 'password'),
'encryption' => env_default_when_empty(env('MAIL_ENCRYPTION'), 'tls'),
'username' => env_default_when_empty(env('MAIL_USERNAME'), 'user@example.com'),
'password' => env_default_when_empty(env('MAIL_PASSWORD'), 'password'),
'timeout' => null,
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
@@ -73,7 +73,7 @@ return [
'sendmail' => [
'transport' => 'sendmail',
'path' => envDefaultWhenEmpty(env('MAIL_SENDMAIL_COMMAND'), '/usr/sbin/sendmail -bs'),
'path' => env_default_when_empty(env('MAIL_SENDMAIL_COMMAND'), '/usr/sbin/sendmail -bs'),
],
'log' => [
'transport' => 'log',
@@ -91,7 +91,7 @@ return [
],
],
'from' => ['address' => envDefaultWhenEmpty(env('MAIL_FROM'), 'changeme@example.com'), 'name' => 'Firefly III Mailer'],
'from' => ['address' => env_default_when_empty(env('MAIL_FROM'), 'changeme@example.com'), 'name' => 'Firefly III Mailer'],
'markdown' => [
'theme' => 'default',
+1 -1
View File
@@ -15,7 +15,7 @@ return [
|
*/
'guard' => envDefaultWhenEmpty(env('AUTHENTICATION_GUARD'), 'web'),
'guard' => env_default_when_empty(env('AUTHENTICATION_GUARD'), 'web'),
/*
|--------------------------------------------------------------------------
+25 -28
View File
@@ -3315,9 +3315,9 @@
"license": "MIT"
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5337,9 +5337,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.343",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz",
"integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==",
"version": "1.5.344",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
"dev": true,
"license": "ISC"
},
@@ -5394,14 +5394,14 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
"version": "5.21.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
"integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.0"
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
@@ -6646,9 +6646,9 @@
}
},
"node_modules/i18next": {
"version": "26.0.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.6.tgz",
"integrity": "sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg==",
"version": "26.0.7",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.7.tgz",
"integrity": "sha512-f7tL/iw0VQsx4nC5oNxBM2RjM8alNys5KzyiQTU6A9TI5TI89py4/Ez1cKFvHiLWsvzOXvuGUES+Kk/A2WiANQ==",
"funding": [
{
"type": "individual",
@@ -6664,9 +6664,6 @@
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2"
},
"peerDependencies": {
"typescript": "^5 || ^6"
},
@@ -6686,9 +6683,9 @@
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.5.tgz",
"integrity": "sha512-QaWHnsxieEDcqKe+vo/RFqpiIFRi/KBqlOSPcUlvinBaISCeiTRCbtrazHAjtHtsLC66oDsROAH8frWkQzfMMQ==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.6.tgz",
"integrity": "sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.1.0"
@@ -10762,9 +10759,9 @@
}
},
"node_modules/terser": {
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"version": "5.46.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",
"integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -10781,9 +10778,9 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz",
"integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11983,9 +11980,9 @@
}
},
"node_modules/webpack/node_modules/webpack-sources": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
"integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz",
"integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==",
"dev": true,
"license": "MIT",
"engines": {
+1
View File
@@ -23,6 +23,7 @@
declare(strict_types=1);
return [
'verifying_password' => 'Verifying password...',
'invalid_account_list' => 'Invalid account type list entry ":value"',
'invalid_transaction_type_list' => 'Invalid transaction type list',
'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.',
+86 -3
View File
@@ -21,6 +21,10 @@
</div>
@endif
<div id="client-errors" class="alert alert-danger" role="alert" style="display:none;">
<ul id="client-errors-list"></ul>
</div>
<div class="card">
<div class="card-body register-card-body">
<p class="login-box-msg">{{ trans('firefly.register_new_account') }}</p>
@@ -35,11 +39,11 @@
</div>
<div class="input-group mb-3">
<input type="password" autocomplete="new-password" required class="form-control"
placeholder="{{ trans('form.password') }}" name="password"/>
placeholder="{{ trans('form.password') }}" minlength="16" name="password"/>
<div class="input-group-text"> <em class="fa-solid fa-lock"></em> </div>
</div>
<div class="input-group mb-3">
<input type="password" autocomplete="new-password" required class="form-control"
<input type="password" autocomplete="new-password" minlength="16" required class="form-control"
placeholder="{{ trans('form.password_confirmation') }}" name="password_confirmation"/>
<div class="input-group-text"> <em class="fa-solid fa-lock"></em> </div>
</div>
@@ -76,5 +80,84 @@
@endsection
@section('scripts')
@vite(['src/pages/dashboard/dashboard.js'])
<script nonce="{{ $JS_NONCE }}">
(function () {
const form = document.querySelector('form[action="{{ route('register') }}"]');
const errorBox = document.getElementById('client-errors');
const errorList = document.getElementById('client-errors-list');
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.textContent;
function showErrors(errors) {
errorList.innerHTML = errors.map(function(e) { return '<li>' + e + '</li>'; }).join('');
errorBox.style.display = 'block';
errorBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
async function sha1Hex(str) {
const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(str));
return Array.from(new Uint8Array(buf))
.map(function(b) { return b.toString(16).padStart(2, '0'); })
.join('')
.toUpperCase();
}
async function isPwned(password) {
const hash = await sha1Hex(password);
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const res = await fetch('https://api.pwnedpasswords.com/range/' + prefix, {
headers: { 'Add-Padding': 'true' }
});
if (!res.ok) { return false; }
const text = await res.text();
return text.toUpperCase().split('\n').some(function(line) {
return line.split(':')[0] === suffix;
});
}
form.addEventListener('submit', async function (e) {
e.preventDefault();
errorBox.style.display = 'none';
const password = form.querySelector('[name="password"]').value;
const confirm = form.querySelector('[name="password_confirmation"]').value;
const verify = form.querySelector('[name="verify_password"]');
const errors = [];
if (password.length < 16) {
errors.push('{{ blade_escape_js((string)trans('validation.min.string', ['attribute' => 'password', 'min' => 16])) }}');
}
if (password !== confirm) {
errors.push('{{ blade_escape_js(trans('validation.confirmed', ['attribute' => 'password'])) }}');
}
if (errors.length > 0) {
showErrors(errors);
return;
}
if (verify && verify.checked) {
submitBtn.disabled = true;
submitBtn.textContent = '{{ blade_escape_js(trans('validation.verifying_password')) }}';
try {
if (await isPwned(password)) {
errors.push('{{ blade_escape_js(trans('validation.secure_password')) }}');
}
} catch (_) {
// network failure — let server validate
}
submitBtn.disabled = false;
submitBtn.textContent = originalBtnText;
}
if (errors.length > 0) {
showErrors(errors);
return;
}
form.submit();
});
})();
</script>
@endsection
+3 -2
View File
@@ -32,8 +32,9 @@ if (!defined('DATEFORMAT')) {
// new Passport routes.
Route::group(
[
'as' => 'passport.',
'prefix' => 'oauth',
'as' => 'passport.',
'prefix' => 'oauth',
'middleware' => ['user-full-auth'],
// 'namespace' => 'FireflyIII\Http\Controllers\OAuth',
],
function (): void {