Compare commits

...

31 Commits

Author SHA1 Message Date
github-actions[bot]
f36da26cc3 Merge pull request #11997 from firefly-iii/release-1774090121
🤖 Automatically merge the PR into the develop branch.
2026-03-21 11:48:48 +01:00
JC5
5983a8eb6d 🤖 Auto commit for release 'develop' on 2026-03-21 2026-03-21 11:48:41 +01:00
James Cole
b4a8a219ff Fix https://github.com/firefly-iii/firefly-iii/issues/11995 2026-03-21 11:42:55 +01:00
github-actions[bot]
4190c4d243 Merge pull request #11994 from firefly-iii/develop
🤖 Automatically merge the PR into the main branch.
2026-03-21 07:44:44 +01:00
github-actions[bot]
70cbbc1523 Merge pull request #11993 from firefly-iii/release-1774075474
🤖 Automatically merge the PR into the develop branch.
2026-03-21 07:44:40 +01:00
JC5
c724f13501 🤖 Auto commit for release 'v6.5.7' on 2026-03-21 2026-03-21 07:44:34 +01:00
James Cole
5f01a83b43 Fix phpstan issues. 2026-03-21 07:36:52 +01:00
James Cole
53c13d221d Clean up API routes. 2026-03-21 07:27:10 +01:00
James Cole
266cd7d8d0 Update changelog and html rendering. 2026-03-21 07:01:42 +01:00
github-actions[bot]
7c09278c8e Merge pull request #11992 from firefly-iii/release-1774047683
🤖 Automatically merge the PR into the develop branch.
2026-03-21 00:01:32 +01:00
JC5
21af34c65a 🤖 Auto commit for release 'develop' on 2026-03-21 2026-03-21 00:01:23 +01:00
James Cole
594c04b121 Add date. 2026-03-20 23:55:46 +01:00
James Cole
c50408249b Update changelog for the next release. 2026-03-20 23:52:53 +01:00
James Cole
b05a38c0e2 So let's make this absolutely clear. 2026-03-20 23:48:42 +01:00
James Cole
0bb1afdf6c Fuck these AI agents. 2026-03-20 23:36:57 +01:00
James Cole
547b83b36e Call dedicated method. 2026-03-20 15:43:04 +01:00
James Cole
134d8c8cf6 Merge branch 'main' into develop 2026-03-20 10:42:05 +01:00
James Cole
94144a407d Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Services/Internal/Recalculate/PrimaryAmountRecalculationService.php
2026-03-20 08:49:29 +01:00
James Cole
15e29d133a Expand the pull request template 2026-03-20 08:48:18 +01:00
James Cole
21f9be6504 Rewrite calculation service for #11982
- moved reset methods to this class
- Add method `recalculateForGroupAndCurrency` that accepts a $limitCurrency as third argument.
- Add `recalculateAccountsForCurrency` and `calculateTransactionsForCurrency`

`recalculateForGroupAndCurrency` can recalculate any foreign amount back to the primary currency (just like the class was already capable of) BUT limits the action to foreign amounts in $limitCurrency. This greatly reduces the rework necessary. This means the method can be safely called when changing currency exchange rates.

Not all methods in `recalculateForGroupAndCurrency` actually care about the given $limitCurrency but the methods that don't aren't doing much anyway, so it's OK to recalculate everything even though its not necessary.
2026-03-20 07:17:33 +01:00
James Cole
d514792f4d Add new handler for currency exchange rate events #11982 2026-03-20 07:14:34 +01:00
James Cole
5894695ad6 Can also collect budgets for user groups #11982 2026-03-20 07:14:19 +01:00
James Cole
7004c9aaf5 Add missing link to user group in currency exchange rate #11982 2026-03-20 07:14:10 +01:00
James Cole
1893a33d84 Move methods to calculation service for #11982 2026-03-20 07:13:54 +01:00
James Cole
d345b31cd4 Catch missing keys in old data for #11982 2026-03-20 07:13:38 +01:00
James Cole
a27642024d Add events for #11982 2026-03-20 07:13:26 +01:00
James Cole
c23ad831d0 New events for https://github.com/firefly-iii/firefly-iii/issues/11982 2026-03-20 06:35:39 +01:00
James Cole
d50c283973 Merge pull request #11988 from firefly-iii/dependabot/composer/composer-bc2e42b409
Bump league/commonmark from 2.8.1 to 2.8.2 in the composer group across 1 directory
2026-03-20 06:04:05 +01:00
dependabot[bot]
9ea3519585 Bump league/commonmark in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [league/commonmark](https://github.com/thephpleague/commonmark).


Updates `league/commonmark` from 2.8.1 to 2.8.2
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.8/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.8.1...2.8.2)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-version: 2.8.2
  dependency-type: direct:production
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 05:02:47 +00:00
James Cole
ddb5bc6038 Merge pull request #11985 from firefly-iii/dependabot/composer/composer-9e6dd9d4c4
Bump phpseclib/phpseclib from 3.0.49 to 3.0.50 in the composer group across 1 directory
2026-03-20 06:02:02 +01:00
dependabot[bot]
caadef7c64 Bump phpseclib/phpseclib in the composer group across 1 directory
Bumps the composer group with 1 update in the / directory: [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib).


Updates `phpseclib/phpseclib` from 3.0.49 to 3.0.50
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.49...3.0.50)

---
updated-dependencies:
- dependency-name: phpseclib/phpseclib
  dependency-version: 3.0.50
  dependency-type: indirect
  dependency-group: composer
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 17:14:44 +00:00
30 changed files with 776 additions and 391 deletions

View File

@@ -1,25 +1,50 @@
<!--
Please TALK TO ME FIRST before you open a PR.
🙌 Thanks for contributing a pull request. Before you continue:
1. If you fix a problem that has no ticket, talk to me FIRST.
2. If you introduce new financial solutions or concepts, talk to me FIRST.
3. If your PR is more than 25 lines, talk to me FIRST.
4. If you used AI to write your PR, talk to me FIRST.
5. If you fix spelling or code comments, talk to me FIRST.
1. If you introduce new financial solutions or concepts, talk to me FIRST.
2. If your PR is more than 25 lines, talk to me FIRST.
3. If you fix spelling or code comments, talk to me FIRST.
Wanna talk to me? Open a GitHub Issue, Discussion, or send me an email: james@firefly-iii.org
Wanna talk to me? Open a GitHub Issue, Discussion, or email me: james@firefly-iii.org
See also: https://docs.firefly-iii.org/explanation/support/#contributing-code
👀 Please ensure you have taken a look at the contribution guidelines:
https://docs.firefly-iii.org/explanation/support/#contributing-code
Remember that your PR may be CLOSED:
1. If you do not refer to an existing issue, your PR will be CLOSED.
2. If you open a PR on the main branch, your PR will be CLOSED.
3. If you only fix a spelling error or code comment, your PR will be CLOSED.
Thanks again, and happy developing!
-->
@JC5
This PR fixes issue # <!-- mandatory field! -->.
#### Reference issues and PRs
<!--
Example: Fixes #1234. See also #3456.
-->
Changes in this pull request:
#### What does this implement/fix? Explain your changes.
-
-
-
#### AI usage disclosure
<!--
If AI tools were involved in creating this PR, please check all boxes that apply
below and make sure that you adhere to our Automated Contributions Policy:
https://docs.firefly-iii.org/explanation/support/#automated-contributions-policy
-->
I used AI assistance for:
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
- [ ] Test/benchmark generation
- [ ] Documentation (including examples)
- [ ] Research and understanding
#### Any other comments?
<!--
Thanks for contributing!
-->

2
.github/security.md vendored
View File

@@ -106,6 +106,8 @@ found with the full or partial support of AI coding agents, large language model
2. explain how the vulnerability can actually be abused by a nefarious third party, and
3. try to limit the verbosity of your report.
At the discretion of the maintainer of the developer, your report may be closed without resolve.
## Credits
This security policy is based on [Harbor](https://github.com/goharbor/harbor)'s security policy.

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\DestroyRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
@@ -59,11 +60,12 @@ final class DestroyController extends Controller
public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
{
$this->repository->deleteRates($from, $to);
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
return response()->json([], 204);
}
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
public function destroySingleByDate(Request $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($exchangeRate instanceof CurrencyExchangeRate) {
@@ -72,14 +74,19 @@ final class DestroyController extends Controller
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new FireflyException('Bla');
}
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
return response()->json([], 204);
}
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
public function destroySingleById(Request $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{
$from = $exchangeRate->fromCurrency;
$to = $exchangeRate->toCurrency;
$this->repository->deleteRate($exchangeRate);
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
return response()->json([], 204);
}
}

View File

@@ -30,6 +30,8 @@ use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequ
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -73,10 +75,12 @@ final class StoreController extends Controller
if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
event(new UpdatedCurrencyExchangeRate($rate));
}
if (!$object instanceof CurrencyExchangeRate) {
// store new
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
event(new CreatedCurrencyExchangeRate($rate));
}
$transformer = new ExchangeRateTransformer();
@@ -97,10 +101,12 @@ final class StoreController extends Controller
// update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing);
event(new UpdatedCurrencyExchangeRate($existing));
continue;
}
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
event(new CreatedCurrencyExchangeRate($new));
$collection->push($new);
}
@@ -124,11 +130,13 @@ final class StoreController extends Controller
// update existing rate.
$existing = $this->repository->updateExchangeRate($existing, $rate);
$collection->push($existing);
event(new UpdatedCurrencyExchangeRate($existing));
continue;
}
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
$collection->push($new);
event(new CreatedCurrencyExchangeRate($new));
}
$count = $collection->count();

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -66,7 +67,7 @@ final class UpdateController extends Controller
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
event(new UpdatedCurrencyExchangeRate($exchangeRate));
$transformer = new ExchangeRateTransformer();
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
@@ -77,6 +78,7 @@ final class UpdateController extends Controller
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
event(new UpdatedCurrencyExchangeRate($exchangeRate));
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);

View File

@@ -28,9 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
@@ -41,7 +39,6 @@ use Illuminate\Validation\ValidationException;
final class DestroyController extends Controller
{
private CurrencyRepositoryInterface $repository;
private UserRepositoryInterface $userRepository;
/**
* CurrencyRepository constructor.
@@ -50,8 +47,7 @@ final class DestroyController extends Controller
{
parent::__construct();
$this->middleware(function ($request, $next) {
$this->repository = app(CurrencyRepositoryInterface::class);
$this->userRepository = app(UserRepositoryInterface::class);
$this->repository = app(CurrencyRepositoryInterface::class);
$this->repository->setUser(auth()->user());
return $next($request);
@@ -69,15 +65,8 @@ final class DestroyController extends Controller
*/
public function destroy(TransactionCurrency $currency): JsonResponse
{
/** @var User $admin */
$admin = auth()->user();
$rules = ['currency_code' => 'required'];
if (!$this->userRepository->hasRole($admin, 'owner')) {
// access denied:
$messages = ['currency_code' => '200005: You need the "owner" role to do this.'];
Validator::make([], $rules, $messages)->validate();
}
if ($this->repository->currencyInUse($currency)) {
$messages = ['currency_code' => '200006: Currency in use.'];
Validator::make([], $rules, $messages)->validate();

View File

@@ -32,7 +32,6 @@ use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
/**
* Class DestroyController
@@ -72,11 +71,6 @@ final class DestroyController extends Controller
if (false === $linkType->editable) {
throw new FireflyException('200020: Link type cannot be changed.');
}
if (false === auth()->user()->hasRole('owner')) {
Log::channel('audit')->warning('Non-owner user tries to delete a link type.');
response()->json([], 401);
}
$this->repository->destroy($linkType);
Preferences::mark();

View File

@@ -27,12 +27,10 @@ namespace FireflyIII\Api\V1\Controllers\Models\TransactionLinkType;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\StoreRequest;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Transformers\LinkTypeTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use League\Fractal\Resource\Item;
@@ -44,7 +42,6 @@ final class StoreController extends Controller
use TransactionFilter;
private LinkTypeRepositoryInterface $repository;
private UserRepositoryInterface $userRepository;
/**
* LinkTypeController constructor.
@@ -54,9 +51,8 @@ final class StoreController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->userRepository = app(UserRepositoryInterface::class);
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->repository->setUser($user);
return $next($request);
@@ -73,15 +69,6 @@ final class StoreController extends Controller
*/
public function store(StoreRequest $request): JsonResponse
{
/** @var User $admin */
$admin = auth()->user();
$rules = ['name' => 'required'];
if (!$this->userRepository->hasRole($admin, 'owner')) {
// access denied:
$messages = ['name' => '200005: You need the "owner" role to do this.'];
Validator::make([], $rules, $messages)->validate();
}
$data = $request->getAll();
// if currency ID is 0, find the currency by the code:
$linkType = $this->repository->store($data);

View File

@@ -29,12 +29,10 @@ use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\UpdateRequest;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\LinkType;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Transformers\LinkTypeTransformer;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use League\Fractal\Resource\Item;
@@ -46,7 +44,6 @@ final class UpdateController extends Controller
use TransactionFilter;
private LinkTypeRepositoryInterface $repository;
private UserRepositoryInterface $userRepository;
/**
* LinkTypeController constructor.
@@ -56,9 +53,8 @@ final class UpdateController extends Controller
parent::__construct();
$this->middleware(function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->userRepository = app(UserRepositoryInterface::class);
$user = auth()->user();
$this->repository = app(LinkTypeRepositoryInterface::class);
$this->repository->setUser($user);
return $next($request);
@@ -80,15 +76,6 @@ final class UpdateController extends Controller
throw new FireflyException('200020: Link type cannot be changed.');
}
/** @var User $admin */
$admin = auth()->user();
$rules = ['name' => 'required'];
if (!$this->userRepository->hasRole($admin, 'owner')) {
$messages = ['name' => '200005: You need the "owner" role to do this.'];
Validator::make([], $rules, $messages)->validate();
}
$data = $request->getAll();
$this->repository->update($linkType, $data);
$manager = $this->getManager();

View File

@@ -30,12 +30,10 @@ use FireflyIII\Enums\WebhookDelivery;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Binder\EitherConfigKey;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
/**
@@ -43,21 +41,6 @@ use Illuminate\Validation\ValidationException;
*/
final class ConfigurationController extends Controller
{
private UserRepositoryInterface $repository;
/**
* ConfigurationController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(function ($request, $next) {
$this->repository = app(UserRepositoryInterface::class);
return $next($request);
});
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getConfiguration
@@ -142,11 +125,6 @@ final class ConfigurationController extends Controller
*/
public function update(UpdateRequest $request, string $name): JsonResponse
{
$rules = ['value' => 'required'];
if (!$this->repository->hasRole(auth()->user(), 'owner')) {
$messages = ['value' => '200005: You need the "owner" role to do this.'];
Validator::make([], $rules, $messages)->validate();
}
$data = $request->getAll();
$shortName = str_replace('configuration.', '', $name);

View File

@@ -74,13 +74,9 @@ final class UserController extends Controller
return response()->json([], 500);
}
if ($this->repository->hasRole($admin, 'owner')) {
$this->repository->destroy($user);
$this->repository->destroy($user);
return response()->json([], 204);
}
throw new FireflyException('200025: No access to function.');
return response()->json([], 204);
}
/**

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* CreatedCurrencyExchangeRate.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/>.
*/
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
use FireflyIII\Events\Event;
use FireflyIII\Models\CurrencyExchangeRate;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CreatedCurrencyExchangeRate extends Event
{
use SerializesModels;
public function __construct(
public CurrencyExchangeRate $rate
) {
Log::debug(sprintf('CreatedCurrencyExchangeRate(#%d) Event', $rate->id));
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* DestroyedCurrencyExchangeRate.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/>.
*/
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DestroyedCurrencyExchangeRate extends Event
{
use SerializesModels;
public function __construct(
public TransactionCurrency $from,
public TransactionCurrency $to,
public UserGroup $userGroup
) {
Log::debug(sprintf('DestroyedCurrencyExchangeRate(%s, %s) Event', $from->code, $to->code));
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* UpdatedCurrencyExchangeRate.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/>.
*/
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
use FireflyIII\Events\Event;
use FireflyIII\Models\CurrencyExchangeRate;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class UpdatedCurrencyExchangeRate extends Event
{
use SerializesModels;
public function __construct(
public CurrencyExchangeRate $rate
) {
Log::debug(sprintf('UpdatedCurrencyExchangeRate(#%d) Event', $rate->id));
}
}

View File

@@ -83,7 +83,7 @@ final class RegisterController extends Controller
throw new FireflyException('Registration is currently not available :(');
}
$this->validator($request->only(['email', 'password']))->validate();
$this->validator($request->only(['email', 'password', 'password_confirmation']))->validate();
$user = $this->createUser($request->only(['email', 'password']));
Log::info(sprintf('Registered new user %s', $user->email));
$owner = new OwnerNotifiable();

View File

@@ -44,6 +44,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
@@ -108,7 +109,7 @@ final class DebugController extends Controller
Preferences::mark();
$request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range', 'temp-mfa-secret', 'temp-mfa-codes']);
Artisan::call('cache:clear');
Cache::clear();
Artisan::call('config:clear');
Artisan::call('route:clear');
Artisan::call('view:clear');

View File

@@ -0,0 +1,66 @@
<?php
/**
* IsAdmin.php
* Copyright (c) 2019 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\Http\Middleware;
use Closure;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Class IsAdmin.
*/
class IsAdminApi
{
/**
* Handle an incoming request. Must be admin.
*
* @param null|string $guard
*
* @return mixed
*/
public function handle(Request $request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
}
return response()->redirectTo(route('login'));
}
/** @var User $user */
$user = auth()->user();
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
if (!$repository->hasRole($user, 'owner')) {
throw new AuthorizationException();
}
return $next($request);
}
}

View File

@@ -59,7 +59,7 @@ class UpdatesAccountInformation implements ShouldQueue
/** @var RuleAction $action */
foreach ($rule->ruleActions as $action) {
// fix name:
if ($oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
if (array_key_exists('name', $oldData) && $oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
Log::debug(sprintf('Rule action #%d "%s" has old account name, replace with new.', $action->id, $action->action_type));
$action->action_value = $account->name;
$action->save();
@@ -105,21 +105,25 @@ class UpdatesAccountInformation implements ShouldQueue
/** @var RuleTrigger $trigger */
foreach ($rule->ruleTriggers as $trigger) {
// fix name:
if ($oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
if (array_key_exists('name', $oldData) && $oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
Log::debug(sprintf('Rule trigger #%d "%s" has old account name, replace with new.', $trigger->id, $trigger->trigger_type));
$trigger->trigger_value = $account->name;
$trigger->save();
++$fixed;
}
// fix IBAN:
if ($oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
if (array_key_exists('iban', $oldData) && $oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
Log::debug(sprintf('Rule trigger #%d "%s" has old account IBAN, replace with new.', $trigger->id, $trigger->trigger_type));
$trigger->trigger_value = $account->iban;
$trigger->save();
++$fixed;
}
// fix account number: // account_number
if ($oldData['account_number'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
if (
array_key_exists('account_number', $oldData)
&& $oldData['account_number'] === $trigger->trigger_value
&& in_array($trigger->trigger_type, $numberFields, true)
) {
Log::debug(sprintf('Rule trigger #%d "%s" has old account account_number, replace with new.', $trigger->id, $trigger->trigger_type));
$trigger->trigger_value = $account->iban;
$trigger->save();

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* ProcessesExchangeRates.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/>.
*/
namespace FireflyIII\Listeners\Model\CurrencyExchangeRate;
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Preferences;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ProcessesExchangeRates
{
public function handle(CreatedCurrencyExchangeRate|DestroyedCurrencyExchangeRate|UpdatedCurrencyExchangeRate $event): void
{
Preferences::mark();
Cache::clear();
if ($event instanceof DestroyedCurrencyExchangeRate) {
$this->handleCurrency($event->userGroup, $event->from);
$this->handleCurrency($event->userGroup, $event->to);
return;
}
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency);
$this->handleCurrency($event->rate->userGroup, $event->rate->toCurrency);
}
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency): void
{
$calculator = new PrimaryAmountRecalculationService();
if (Amount::convertToPrimary()) {
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s.', $currency->code));
$calculator->recalculateForGroupAndCurrency($userGroup, $currency);
// $calculator->recalculateForGroup($userGroup);
return;
}
Log::debug('Will NOT convert to primary currency.');
}
}

View File

@@ -25,46 +25,17 @@ declare(strict_types=1);
namespace FireflyIII\Listeners\System;
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
use FireflyIII\Models\Budget;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class RecalculatesPrimaryCurrencyAmounts
{
public function handle(UserGroupChangedPrimaryCurrency $event): void
{
// Reset the primary currency amounts for all objects that have it.
Log::debug('Resetting primary currency amounts for all objects.');
$tables = [
// !!! this array is also in the migration
'accounts' => ['native_virtual_balance'],
'available_budgets' => ['native_amount'],
'bills' => ['native_amount_min', 'native_amount_max'],
];
foreach ($tables as $table => $columns) {
Log::debug(sprintf('Now processing table "%s"', $table));
foreach ($columns as $column) {
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]);
}
}
$this->resetPiggyBanks($event->userGroup);
$this->resetBudgets($event->userGroup);
$this->resetTransactions($event->userGroup);
Log::debug('Have now reset all primary amounts to NULL.');
// fire laravel command to recalculate them all.
if (Amount::convertToPrimary()) {
Log::debug('Will now convert amounts to primary currency.');
$calculator = new PrimaryAmountRecalculationService();
$calculator->recalculate();
@@ -72,87 +43,4 @@ class RecalculatesPrimaryCurrencyAmounts
}
Log::debug('Will NOT convert to primary currency.');
}
private function resetBudget(Budget $budget): void
{
foreach ($budget->autoBudgets as $autoBudget) {
if ('' === (string) $autoBudget->native_amount) {
continue;
}
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
$autoBudget->native_amount = null;
$autoBudget->saveQuietly();
}
foreach ($budget->budgetlimits as $limit) {
if ('' !== (string) $limit->native_amount) {
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
$limit->native_amount = null;
$limit->saveQuietly();
}
}
}
private function resetBudgets(UserGroup $userGroup): void
{
$repository = app(BudgetRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getBudgets();
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
/** @var Budget $budget */
foreach ($set as $budget) {
$this->resetBudget($budget);
}
}
private function resetPiggyBank(PiggyBank $piggyBank): void
{
if ('' !== (string) $piggyBank->native_target_amount) {
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
$piggyBank->native_target_amount = null;
$piggyBank->saveQuietly();
}
foreach ($piggyBank->accounts as $account) {
if ('' !== (string) $account->pivot->native_current_amount) {
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
$account->pivot->native_current_amount = null;
$account->pivot->save();
}
}
foreach ($piggyBank->piggyBankEvents as $event) {
if ('' !== (string) $event->native_amount) {
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
$event->native_amount = null;
$event->saveQuietly();
}
}
}
private function resetPiggyBanks(UserGroup $userGroup): void
{
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$piggyBanks = $repository->getPiggyBanks();
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
$this->resetPiggyBank($piggyBank);
}
}
private function resetTransactions(UserGroup $userGroup): void
{
// custom query because of the potential size of this update.
$success = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (Builder $q): void {
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
})
->update(['native_amount' => null, 'native_foreign_amount' => null])
;
Log::debug(sprintf('Reset %d transactions.', $success));
}
}

View File

@@ -59,6 +59,11 @@ class CurrencyExchangeRate extends Model
return $this->belongsTo(User::class);
}
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class);
}
protected function casts(): array
{
return [

View File

@@ -368,6 +368,15 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function getBudgets(): Collection
{
if (null === $this->user) {
return $this->userGroup
->budgets()
->orderBy('order', 'ASC')
->orderBy('name', 'ASC')
->get()
;
}
return $this->user
->budgets()
->orderBy('order', 'ASC')

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Services\Internal\Recalculate;
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
use FireflyIII\Handlers\Observer\TransactionObserver;
use FireflyIII\Models\Account;
use FireflyIII\Models\AutoBudget;
@@ -36,14 +37,16 @@ use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\FireflyConfig;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Builder as DatabaseBuilder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -57,14 +60,52 @@ class PrimaryAmountRecalculationService
/** @var UserGroupRepositoryInterface $repository */
$repository = app(UserGroupRepositoryInterface::class);
Preferences::mark();
/** @var UserGroup $userGroup */
foreach ($repository->getAll() as $userGroup) {
Log::debug('Resetting primary currency amounts for all objects.');
$this->resetGenericTables($userGroup);
$this->resetPiggyBanks($userGroup);
$this->resetBudgets($userGroup);
$this->resetTransactions($userGroup);
Log::debug('Have now reset all primary amounts to NULL.');
$this->recalculateForGroup($userGroup);
}
}
public function recalculateForGroup(UserGroup $userGroup): void
{
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
// do a check with the group's currency so we can skip some stuff.
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
$this->recalculateAccounts($userGroup, $currency);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);
$this->recalculateAvailableBudgets($userGroup, $currency);
$this->recalculateBills($userGroup, $currency);
$this->calculateTransactions($userGroup, $currency);
}
public function recalculateForGroupAndCurrency(UserGroup $userGroup, TransactionCurrency $limitCurrency): void
{
// do a check with the group's currency so we can skip some stuff.
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
if ($limitCurrency->id === $currency->id) {
Log::debug(sprintf('Can skip recalculation because user requested the same currencies (%s).', $limitCurrency->code));
return;
}
$this->recalculateAccountsForCurrency($userGroup, $currency, $limitCurrency);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);
$this->recalculateAvailableBudgets($userGroup, $currency);
$this->recalculateBills($userGroup, $currency);
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
}
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
{
// custom query because of the potential size of this update.
@@ -86,7 +127,10 @@ class PrimaryAmountRecalculationService
->get(['transactions.id'])
;
TransactionObserver::$recalculate = false;
Log::debug(sprintf('Count of set is %d', $set->count()));
foreach ($set as $item) {
Log::debug(sprintf('Touch transaction #%d', $item->id));
// here we are.
/** @var null|Transaction $transaction */
$transaction = Transaction::find($item->id);
@@ -96,13 +140,42 @@ class PrimaryAmountRecalculationService
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
}
/**
* Only recalculate accounts that have a virtual balance.
* TODO this routine must filter on accounts that are NOT in the userGroup's currency.
*/
private function recalculateAccounts(UserGroup $userGroup): void
private function calculateTransactionsForCurrency(UserGroup $userGroup, TransactionCurrency $currency, TransactionCurrency $limitCurrency): void
{
$set = $userGroup
Log::debug(sprintf('Now in calculateTransactionsForCurrency(#%d, %s, %s)', $userGroup->id, $currency->code, $limitCurrency->code));
// custom query because of the potential size of this update.
$set = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (DatabaseBuilder $q1) use ($currency): void {
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
});
})
// must be in the limit currency.
->where('transactions.transaction_currency_id', $limitCurrency->id)
->orWhere('transactions.foreign_currency_id', $limitCurrency->id)
->get(['transactions.id'])
;
TransactionObserver::$recalculate = false;
Log::debug(sprintf('Count of set is %d', $set->count()));
foreach ($set as $item) {
Log::debug(sprintf('Touch transaction #%d', $item->id));
// here we are.
/** @var null|Transaction $transaction */
$transaction = Transaction::find($item->id);
$transaction?->touch();
}
TransactionObserver::$recalculate = true;
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
}
private function collectAccounts(UserGroup $userGroup): Collection
{
return $userGroup
->accounts()
->where(static function (EloquentBuilder $q): void {
$q->whereNotNull('virtual_balance');
@@ -117,14 +190,59 @@ class PrimaryAmountRecalculationService
})
->get()
;
}
/**
* Only recalculate accounts that have a virtual balance.
*/
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
{
Log::debug(sprintf('recalculateAccounts(#%d, %s)', $userGroup->id, $groupCurrency->code));
$set = $this->collectAccounts($userGroup);
/** @var Account $account */
foreach ($set as $account) {
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
if ($groupCurrency->id === $currencyId) {
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
continue;
}
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
$account->touch();
}
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
}
/**
* Only recalculate accounts that have a virtual balance.
*/
private function recalculateAccountsForCurrency(UserGroup $userGroup, TransactionCurrency $groupCurrency, TransactionCurrency $limitCurrency): void
{
Log::debug(sprintf('recalculateAccountsForCurrency(#%d, %s, %s)', $userGroup->id, $groupCurrency->code, $limitCurrency->code));
$set = $this->collectAccounts($userGroup);
/** @var Account $account */
foreach ($set as $account) {
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
if ($groupCurrency->id === $currencyId) {
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
continue;
}
if ($limitCurrency->id !== $currencyId) {
Log::debug(sprintf('Account "%s" is NOT in limit currency %s, skip.', $account->name, $limitCurrency->code));
continue;
}
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
// TODO it is bad form to call an event from an event but OK.
event(new UpdatedExistingAccount($account, []));
}
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
}
private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void
{
$set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
@@ -184,21 +302,6 @@ class PrimaryAmountRecalculationService
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
}
private function recalculateForGroup(UserGroup $userGroup): void
{
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
$this->recalculateAccounts($userGroup);
// do a check with the group's currency so we can skip some stuff.
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
$this->recalculatePiggyBanks($userGroup, $currency);
$this->recalculateBudgets($userGroup, $currency);
$this->recalculateAvailableBudgets($userGroup, $currency);
$this->recalculateBills($userGroup, $currency);
$this->calculateTransactions($userGroup, $currency);
}
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
{
$set = $piggyBank->piggyBankEvents()->get();
@@ -240,4 +343,104 @@ class PrimaryAmountRecalculationService
}
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
}
private function resetBudget(Budget $budget): void
{
foreach ($budget->autoBudgets as $autoBudget) {
if ('' === (string) $autoBudget->native_amount) {
continue;
}
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
$autoBudget->native_amount = null;
$autoBudget->saveQuietly();
}
foreach ($budget->budgetlimits as $limit) {
if ('' !== (string) $limit->native_amount) {
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
$limit->native_amount = null;
$limit->saveQuietly();
}
}
}
private function resetBudgets(UserGroup $userGroup): void
{
$repository = app(BudgetRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$set = $repository->getBudgets();
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
/** @var Budget $budget */
foreach ($set as $budget) {
$this->resetBudget($budget);
}
}
private function resetGenericTables(UserGroup $userGroup): void
{
$tables = [
// !!! this array is also in the migration
'accounts' => ['native_virtual_balance'],
'available_budgets' => ['native_amount'],
'bills' => ['native_amount_min', 'native_amount_max'],
];
foreach ($tables as $table => $columns) {
Log::debug(sprintf('Now processing table "%s"', $table));
foreach ($columns as $column) {
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
DB::table($table)->where('user_group_id', $userGroup->id)->update([$column => null]);
}
}
}
private function resetPiggyBank(PiggyBank $piggyBank): void
{
if ('' !== (string) $piggyBank->native_target_amount) {
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
$piggyBank->native_target_amount = null;
$piggyBank->saveQuietly();
}
foreach ($piggyBank->accounts as $account) {
if ('' !== (string) $account->pivot->native_current_amount) {
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
$account->pivot->native_current_amount = null;
$account->pivot->save();
}
}
foreach ($piggyBank->piggyBankEvents as $event) {
if ('' !== (string) $event->native_amount) {
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
$event->native_amount = null;
$event->saveQuietly();
}
}
}
private function resetPiggyBanks(UserGroup $userGroup): void
{
$repository = app(PiggyBankRepositoryInterface::class);
$repository->setUserGroup($userGroup);
$piggyBanks = $repository->getPiggyBanks();
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
$this->resetPiggyBank($piggyBank);
}
}
private function resetTransactions(UserGroup $userGroup): void
{
// custom query because of the potential size of this update.
$success = DB::table('transactions')
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('transaction_journals.user_group_id', $userGroup->id)
->where(static function (Builder $q): void {
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
})
->update(['native_amount' => null, 'native_foreign_amount' => null])
;
Log::debug(sprintf('Reset %d transactions.', $success));
}
}

View File

@@ -29,6 +29,7 @@ use FireflyIII\Http\Middleware\EncryptCookies;
use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Http\Middleware\InterestingMessage;
use FireflyIII\Http\Middleware\IsAdmin;
use FireflyIII\Http\Middleware\IsAdminApi;
use FireflyIII\Http\Middleware\Range;
use FireflyIII\Http\Middleware\RedirectIfAuthenticated;
use FireflyIII\Http\Middleware\SecureHeaders;
@@ -157,7 +158,7 @@ $app = Application::configure(basePath: dirname(__DIR__))
// This middleware is added to ensure that the user is not only logged in and
// authenticated (with MFA and everything), but also admin.
$middleware->appendToGroup('api-admin', [
IsAdmin::class,
IsAdminApi::class,
]);
$middleware->appendToGroup('admin', [
IsAdmin::class,

View File

@@ -3,38 +3,32 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## v6.5.7 - 2026-03-xx
### v6.5.8 - 2026-03-22
<!-- summary: If you can read this I forgot to update the summary! -->
<!-- summary: This release fixes a regression bug in user registration. -->
### Added
### Fixed
- Initial release.
- [Issue 11995](https://github.com/firefly-iii/firefly-iii/issues/11995) (User registration breaks on password validation) reported by @mikaelhm
### Changed
## v6.5.7 - 2026-03-21
- Initial release.
### Deprecated
- Initial release.
### Removed
- Initial release.
<!-- summary: There is a new security policy for AI-generated security advisories and of course, some interesting but annoying bugs fixed. -->
### Fixed
- [Issue 11964](https://github.com/firefly-iii/firefly-iii/issues/11964) ("Left to spend" is not taking into account non-main currency withdrawals (when displaying in primary currency)) reported by @absdjfh
- [Issue 11966](https://github.com/firefly-iii/firefly-iii/issues/11966) (Error when trying to export data in CSV (Export data via front end)) reported by @jgmm81
### Security
- Initial release.
- [Issue 11969](https://github.com/firefly-iii/firefly-iii/issues/11969) (Problem found when editing a multi-currency record, as well as details in the "Audit log entries") reported by @jgmm81
- [PR 11974](https://github.com/firefly-iii/firefly-iii/pull/11974) (Fix typo in SMTP server comment in .env.example) reported by @NorskNoobing
- [Discussion 11977](https://github.com/orgs/firefly-iii/discussions/11977) (CSP header `form-action 'self'` prevents form submission because it's a redirect) started by @superrio0187
- [Issue 11978](https://github.com/firefly-iii/firefly-iii/issues/11978) (Tags not associated with any record display incorrect information) reported by @jgmm81
- [Issue 11982](https://github.com/firefly-iii/firefly-iii/issues/11982) (Foreign currency account value in primary currency does not update after changing exchange rates) reported by @gattacus
- Remove old `zoomLevel` / `zoom_level` database references for tags, since they are no longer queries anyway.
### API
- Initial release.
- [Issue 11976](https://github.com/firefly-iii/firefly-iii/issues/11976) (New lines are removed from rule description when created using API POST) reported by @AlexRNL
## v6.5.6 - 2026-03-16

18
composer.lock generated
View File

@@ -5781,27 +5781,27 @@
},
{
"name": "rcrowe/twigbridge",
"version": "v0.14.6",
"version": "v0.14.7",
"source": {
"type": "git",
"url": "https://github.com/rcrowe/TwigBridge.git",
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45"
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/03a767c8d5c1d74d5f14e9fc754619a271822663",
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663",
"shasum": ""
},
"require": {
"illuminate/support": "^9|^10|^11|^12",
"illuminate/view": "^9|^10|^11|^12",
"illuminate/support": "^9|^10|^11|^12|^13",
"illuminate/view": "^9|^10|^11|^12|^13",
"php": "^8.1",
"twig/twig": "~3.21"
},
"require-dev": {
"ext-json": "*",
"laravel/framework": "^9|^10|^11|^12",
"laravel/framework": "^9|^10|^11|^12|^13",
"mockery/mockery": "^1.3.1",
"phpunit/phpunit": "^8.5.8 || ^9.3.7 || ^10.0 || ^11.0 || ^12.0",
"squizlabs/php_codesniffer": "^3.6"
@@ -5847,9 +5847,9 @@
],
"support": {
"issues": "https://github.com/rcrowe/TwigBridge/issues",
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.6"
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.7"
},
"time": "2025-08-20T11:25:49+00:00"
"time": "2026-03-20T16:59:18+00:00"
},
{
"name": "spatie/backtrace",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => (bool)envDefaultWhenEmpty(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-03-20',
'build_time' => 1773991310,
'version' => 'develop/2026-03-21',
'build_time' => 1774089937,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

236
package-lock.json generated
View File

@@ -2620,9 +2620,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.1.tgz",
"integrity": "sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ==",
"cpu": [
"arm"
],
@@ -2634,9 +2634,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.1.tgz",
"integrity": "sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw==",
"cpu": [
"arm64"
],
@@ -2648,9 +2648,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.1.tgz",
"integrity": "sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg==",
"cpu": [
"arm64"
],
@@ -2662,9 +2662,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.1.tgz",
"integrity": "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==",
"cpu": [
"x64"
],
@@ -2676,9 +2676,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.1.tgz",
"integrity": "sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ==",
"cpu": [
"arm64"
],
@@ -2690,9 +2690,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.1.tgz",
"integrity": "sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g==",
"cpu": [
"x64"
],
@@ -2704,9 +2704,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.1.tgz",
"integrity": "sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA==",
"cpu": [
"arm"
],
@@ -2718,9 +2718,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.1.tgz",
"integrity": "sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg==",
"cpu": [
"arm"
],
@@ -2732,9 +2732,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.1.tgz",
"integrity": "sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA==",
"cpu": [
"arm64"
],
@@ -2746,9 +2746,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.1.tgz",
"integrity": "sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA==",
"cpu": [
"arm64"
],
@@ -2760,9 +2760,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.1.tgz",
"integrity": "sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ==",
"cpu": [
"loong64"
],
@@ -2774,9 +2774,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.1.tgz",
"integrity": "sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g==",
"cpu": [
"loong64"
],
@@ -2788,9 +2788,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.1.tgz",
"integrity": "sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw==",
"cpu": [
"ppc64"
],
@@ -2802,9 +2802,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.1.tgz",
"integrity": "sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A==",
"cpu": [
"ppc64"
],
@@ -2816,9 +2816,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.1.tgz",
"integrity": "sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A==",
"cpu": [
"riscv64"
],
@@ -2830,9 +2830,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.1.tgz",
"integrity": "sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw==",
"cpu": [
"riscv64"
],
@@ -2844,9 +2844,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.1.tgz",
"integrity": "sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug==",
"cpu": [
"s390x"
],
@@ -2858,9 +2858,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.1.tgz",
"integrity": "sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w==",
"cpu": [
"x64"
],
@@ -2872,9 +2872,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.1.tgz",
"integrity": "sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ==",
"cpu": [
"x64"
],
@@ -2886,9 +2886,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.1.tgz",
"integrity": "sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA==",
"cpu": [
"x64"
],
@@ -2900,9 +2900,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.1.tgz",
"integrity": "sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ==",
"cpu": [
"arm64"
],
@@ -2914,9 +2914,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.1.tgz",
"integrity": "sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw==",
"cpu": [
"arm64"
],
@@ -2928,9 +2928,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.1.tgz",
"integrity": "sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg==",
"cpu": [
"ia32"
],
@@ -2942,9 +2942,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.1.tgz",
"integrity": "sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw==",
"cpu": [
"x64"
],
@@ -2956,9 +2956,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.1.tgz",
"integrity": "sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g==",
"cpu": [
"x64"
],
@@ -5066,9 +5066,9 @@
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5403,9 +5403,9 @@
}
},
"node_modules/cssnano/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"dev": true,
"license": "ISC",
"engines": {
@@ -7143,9 +7143,9 @@
}
},
"node_modules/i18next": {
"version": "25.8.20",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.20.tgz",
"integrity": "sha512-xjo9+lbX/P1tQt3xpO2rfJiBppNfUnNIPKgCvNsTKsvTOCro1Qr/geXVg1N47j5ScOSaXAPq8ET93raK3Rr06A==",
"version": "25.9.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.9.0.tgz",
"integrity": "sha512-mJ4rVRNWOTkqh5xnaGR6iMFT5vEw3Y2MTJhcjinR/7u8cRv6dAfC0ofuePh5fVPxoh395p6JdrJTStCcNW66gg==",
"funding": [
{
"type": "individual",
@@ -9156,9 +9156,9 @@
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"dev": true,
"license": "ISC",
"engines": {
@@ -10128,9 +10128,9 @@
}
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"version": "4.59.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.1.tgz",
"integrity": "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10144,31 +10144,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"@rollup/rollup-android-arm-eabi": "4.59.1",
"@rollup/rollup-android-arm64": "4.59.1",
"@rollup/rollup-darwin-arm64": "4.59.1",
"@rollup/rollup-darwin-x64": "4.59.1",
"@rollup/rollup-freebsd-arm64": "4.59.1",
"@rollup/rollup-freebsd-x64": "4.59.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
"@rollup/rollup-linux-arm-musleabihf": "4.59.1",
"@rollup/rollup-linux-arm64-gnu": "4.59.1",
"@rollup/rollup-linux-arm64-musl": "4.59.1",
"@rollup/rollup-linux-loong64-gnu": "4.59.1",
"@rollup/rollup-linux-loong64-musl": "4.59.1",
"@rollup/rollup-linux-ppc64-gnu": "4.59.1",
"@rollup/rollup-linux-ppc64-musl": "4.59.1",
"@rollup/rollup-linux-riscv64-gnu": "4.59.1",
"@rollup/rollup-linux-riscv64-musl": "4.59.1",
"@rollup/rollup-linux-s390x-gnu": "4.59.1",
"@rollup/rollup-linux-x64-gnu": "4.59.1",
"@rollup/rollup-linux-x64-musl": "4.59.1",
"@rollup/rollup-openbsd-x64": "4.59.1",
"@rollup/rollup-openharmony-arm64": "4.59.1",
"@rollup/rollup-win32-arm64-msvc": "4.59.1",
"@rollup/rollup-win32-ia32-msvc": "4.59.1",
"@rollup/rollup-win32-x64-gnu": "4.59.1",
"@rollup/rollup-win32-x64-msvc": "4.59.1",
"fsevents": "~2.3.2"
}
},
@@ -12422,9 +12422,9 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"

View File

@@ -20,6 +20,14 @@
export default class GenericObjectRenderer {
renderUrl(url, title, text) {
return `<a href="${url}" title="${title}">${text}</a>`;
return `<a href="${url}" title="${this.escapeHtml(title)}">${this.escapeHtml(text)}</a>`;
}
escapeHtml(unsafe) {
return unsafe
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
};
}

View File

@@ -636,12 +636,9 @@ Route::group(
],
static function (): void {
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::get('primary', ['uses' => 'ShowController@showPrimary', 'as' => 'show.primary']);
Route::get('default', ['uses' => 'ShowController@showPrimary', 'as' => 'show.default']);
Route::get('{currency_code}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
Route::post('{currency_code}/enable', ['uses' => 'UpdateController@enable', 'as' => 'enable']);
Route::post('{currency_code}/disable', ['uses' => 'UpdateController@disable', 'as' => 'disable']);
@@ -658,6 +655,22 @@ Route::group(
}
);
// Transaction currency API routes that require admin rights:
Route::group(
[
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionCurrency',
'prefix' => 'v1/currencies',
'as' => 'api.v1.currencies.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
}
);
// Transaction Links API routes:
Route::group(
[
@@ -683,11 +696,23 @@ Route::group(
],
static function (): void {
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::get('{linkType}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::get('{linkType}/transactions', ['uses' => 'ListController@transactions', 'as' => 'transactions']);
}
);
// Transaction Link Type API routes that need admin rights.
Route::group(
[
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionLinkType',
'prefix' => 'v1/link-types',
'as' => 'api.v1.link-types.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::put('{linkType}', ['uses' => 'UpdateController@update', 'as' => 'update']);
Route::delete('{linkType}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
Route::get('{linkType}/transactions', ['uses' => 'ListController@transactions', 'as' => 'transactions']);
}
);
@@ -727,10 +752,23 @@ Route::group(
],
static function (): void {
Route::get('', ['uses' => 'ConfigurationController@index', 'as' => 'index']);
Route::put('{dynamicConfigKey}', ['uses' => 'ConfigurationController@update', 'as' => 'update']);
Route::get('{eitherConfigKey}', ['uses' => 'ConfigurationController@show', 'as' => 'show']);
}
);
// Configuration API routes that need admin rights
Route::group(
[
'namespace' => 'FireflyIII\Api\V1\Controllers\System',
'prefix' => 'v1/configuration',
'as' => 'api.v1.configuration.',
'middleware' => ['api-admin'],
],
static function (): void {
Route::put('{dynamicConfigKey}', ['uses' => 'ConfigurationController@update', 'as' => 'update']);
}
);
// Users API routes:
Route::group(
[