Compare commits

..

12 Commits

Author SHA1 Message Date
github-actions[bot]
84b4ab0cc2 Merge pull request #11112 from firefly-iii/release-1761470095
🤖 Automatically merge the PR into the develop branch.
2025-10-26 10:15:03 +01:00
JC5
eb808a0fbc 🤖 Auto commit for release 'develop' on 2025-10-26 2025-10-26 10:14:55 +01:00
James Cole
d25ae50d30 Fix routes. 2025-10-26 10:09:59 +01:00
github-actions[bot]
c43821e29c Merge pull request #11111 from firefly-iii/release-1761469189
🤖 Automatically merge the PR into the develop branch.
2025-10-26 09:59:56 +01:00
JC5
b7570b2651 🤖 Auto commit for release 'develop' on 2025-10-26 2025-10-26 09:59:49 +01:00
James Cole
cb77609f27 Add disclaimer. 2025-10-26 09:55:51 +01:00
James Cole
99651bb61e Catch some exceptions. 2025-10-26 09:53:19 +01:00
James Cole
bf1e14f66d Explain about user agents. 2025-10-26 09:45:00 +01:00
github-actions[bot]
d0e55804f8 Merge pull request #11110 from firefly-iii/release-1761467960
🤖 Automatically merge the PR into the develop branch.
2025-10-26 09:39:29 +01:00
JC5
0a9715b8c1 🤖 Auto commit for release 'develop' on 2025-10-26 2025-10-26 09:39:20 +01:00
James Cole
61390e67f6 Add debug info. 2025-10-26 09:34:40 +01:00
James Cole
536d25980f Fix issues related to #11109 2025-10-26 09:31:07 +01:00
11 changed files with 174 additions and 59 deletions

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\Exceptions\FireflyException;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -75,6 +76,9 @@ class DestroyController extends Controller
if ($exchangeRate instanceof CurrencyExchangeRate) {
$this->repository->deleteRate($exchangeRate);
}
if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new FireflyException('Bla');
}
return response()->json([], 204);
}

View File

@@ -82,7 +82,6 @@ class UpdateController extends Controller
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))

View File

@@ -81,11 +81,11 @@ class DebugController extends Controller
*/
public function displayError(): void
{
app('log')->debug('This is a test message at the DEBUG level.');
app('log')->info('This is a test message at the INFO level.');
Log::debug('This is a test message at the DEBUG level.');
Log::info('This is a test message at the INFO level.');
Log::notice('This is a test message at the NOTICE level.');
app('log')->warning('This is a test message at the WARNING level.');
app('log')->error('This is a test message at the ERROR level.');
Log::warning('This is a test message at the WARNING level.');
Log::error('This is a test message at the ERROR level.');
Log::critical('This is a test message at the CRITICAL level.');
Log::alert('This is a test message at the ALERT level.');
Log::emergency('This is a test message at the EMERGENCY level.');
@@ -187,6 +187,8 @@ class DebugController extends Controller
return [
'php_version' => PHP_VERSION,
'php_os' => PHP_OS,
'build_time' => config('firefly.build_time'),
'build_time_nice' => Carbon::parse(config('firefly.build_time'), 'Europe/Amsterdam')->setTimezone('Europe/Amsterdam')->format('Y-m-d H:i:s e'),
'uname' => php_uname('m'),
'interface' => PHP_SAPI,
'bits' => PHP_INT_SIZE * 8,
@@ -212,11 +214,11 @@ class DebugController extends Controller
try {
if (file_exists('/var/www/counter-main.txt')) {
$return['build'] = trim(file_get_contents('/var/www/counter-main.txt'));
app('log')->debug(sprintf('build is now "%s"', $return['build']));
Log::debug(sprintf('build is now "%s"', $return['build']));
}
} catch (Exception $e) {
app('log')->debug('Could not check build counter, but thats ok.');
app('log')->warning($e->getMessage());
Log::debug('Could not check build counter, but thats ok.');
Log::warning($e->getMessage());
}
try {
@@ -224,8 +226,8 @@ class DebugController extends Controller
$return['build_date'] = trim(file_get_contents('/var/www/build-date-main.txt'));
}
} catch (Exception $e) {
app('log')->debug('Could not check build date, but thats ok.');
app('log')->warning($e->getMessage());
Log::debug('Could not check build date, but thats ok.');
Log::warning($e->getMessage());
}
if ('' !== (string) env('BASE_IMAGE_BUILD')) { // @phpstan-ignore-line
$return['base_build'] = env('BASE_IMAGE_BUILD'); // @phpstan-ignore-line
@@ -282,7 +284,7 @@ class DebugController extends Controller
$parts = Steam::getLocaleArray(Steam::getLocale());
foreach ($parts as $code) {
$code = trim($code);
app('log')->debug(sprintf('Trying to set %s', $code));
Log::debug(sprintf('Trying to set %s', $code));
$result = setlocale(LC_ALL, $code);
$localeAttempts[$code] = $result === $code;
}

View File

@@ -31,6 +31,8 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Safe\Exceptions\FilesystemException;
use Safe\Exceptions\JsonException;
use Symfony\Component\Mailer\Exception\TransportException;
use function Safe\file_get_contents;
@@ -125,11 +127,25 @@ class MailError extends Job implements ShouldQueue
if (!file_exists($file)) {
Log::debug(sprintf('Wrote new file in "%s"', $file));
file_put_contents($file, json_encode($limits, JSON_PRETTY_PRINT));
try {
file_put_contents($file, json_encode($limits, JSON_PRETTY_PRINT));
} catch (FilesystemException $e) {
Log::warning(sprintf('[a] Could not write file "%s": %s', $file, $e->getMessage()));
} catch (JsonException $e) {
Log::warning(sprintf('[b] Could not parse file "%s": %s', $file, $e->getMessage()));
}
}
if (file_exists($file)) {
Log::debug(sprintf('Read file in "%s"', $file));
$limits = json_decode(file_get_contents($file), true);
try {
$limits = json_decode(file_get_contents($file), true);
} catch (FilesystemException $e) {
Log::warning(sprintf('[c] Could not read file "%s": %s', $file, $e->getMessage()));
} catch (JsonException $e) {
Log::warning(sprintf('[d] Could not parse file "%s": %s', $file, $e->getMessage()));
}
}
// limit reached?
foreach ($types as $type => $info) {
@@ -157,7 +173,14 @@ class MailError extends Job implements ShouldQueue
}
++$limits[$type]['sent'];
}
file_put_contents($file, json_encode($limits, JSON_PRETTY_PRINT));
try {
file_put_contents($file, json_encode($limits, JSON_PRETTY_PRINT));
} catch (FilesystemException $e) {
Log::warning(sprintf('[c] Could not write file "%s": %s', $file, $e->getMessage()));
} catch (JsonException $e) {
Log::warning(sprintf('[c] Could not parse file "%s": %s', $file, $e->getMessage()));
}
Log::debug('No limits reached, return FALSE.');
return false;

View File

@@ -88,7 +88,7 @@ trait UserGroupTrait
public function setUserGroup(UserGroup $userGroup): void
{
if (null === $this->user) {
Log::warning(sprintf('User is not set in repository %s', static::class));
Log::warning(sprintf('User is not set in repository %s. This does not have to be a problem.', static::class));
}
$this->userGroup = $userGroup;
}

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2025-10-25',
'build_time' => 1761395572,
'version' => 'develop/2025-10-26',
'build_time' => 1761469988,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

View File

@@ -44,20 +44,29 @@
<div class="box-body no-padding">
<nav v-if="totalPages > 1">
<ul class="pagination">
<li v-if="1 === this.page" class="page-item disabled" aria-disabled="true" :aria-label="$t('pagination.previous')">
<li v-if="1 === this.page" class="page-item disabled" aria-disabled="true"
:aria-label="$t('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
<li class="page-item" v-if="1 !== this.page">
<a class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page-1)" rel="prev" :aria-label="$t('pagination.next')">&lsaquo;</a>
<a class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page-1)"
rel="prev" :aria-label="$t('pagination.next')">&lsaquo;</a>
</li>
<li v-for="item in this.totalPages" :class="item === page ? 'page-item active' : 'page-item'" aria-current="page">
<li v-for="item in this.totalPages"
:class="item === page ? 'page-item active' : 'page-item'" aria-current="page">
<span v-if="item === page" class="page-link" v-text="item"></span>
<a v-if="item !== page" class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + item" v-text="item"></a>
<a v-if="item !== page" class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + item"
v-text="item"></a>
</li>
<li v-if="totalPages !== page" class="page-item">
<a class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page+1)" rel="next" :aria-label="$t('pagination.next')">&rsaquo;</a>
<a class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page+1)"
rel="next" :aria-label="$t('pagination.next')">&rsaquo;</a>
</li>
<li v-if="totalPages === page" class="page-item disabled" aria-disabled="true" :aria-label="$t('pagination.next')">
<li v-if="totalPages === page" class="page-item disabled" aria-disabled="true"
:aria-label="$t('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
</ul>
@@ -97,9 +106,11 @@
>
</td>
<td>
<!-- (<span v-text="rate.rate_id"></span>) -->
<input type="number" class="form-control" min="0" step="any" v-model="rate.rate">
</td>
<td>
<!-- (<span v-text="rate.inverse_id"></span>) -->
<input type="number" class="form-control" min="0" step="any" v-model="rate.inverse">
</td>
<td>
@@ -122,20 +133,29 @@
<nav v-if="totalPages > 1">
<ul class="pagination">
<li v-if="1 === this.page" class="page-item disabled" aria-disabled="true" :aria-label="$t('pagination.previous')">
<li v-if="1 === this.page" class="page-item disabled" aria-disabled="true"
:aria-label="$t('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
<li class="page-item" v-if="1 !== this.page">
<a class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page-1)" rel="prev" :aria-label="$t('pagination.next')">&lsaquo;</a>
<a class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page-1)"
rel="prev" :aria-label="$t('pagination.next')">&lsaquo;</a>
</li>
<li v-for="item in this.totalPages" :class="item === page ? 'page-item active' : 'page-item'" aria-current="page">
<li v-for="item in this.totalPages"
:class="item === page ? 'page-item active' : 'page-item'" aria-current="page">
<span v-if="item === page" class="page-link" v-text="item"></span>
<a v-if="item !== page" class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + item" v-text="item"></a>
<a v-if="item !== page" class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + item"
v-text="item"></a>
</li>
<li v-if="totalPages !== page" class="page-item">
<a class="page-link" :href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page+1)" rel="next" :aria-label="$t('pagination.next')">&rsaquo;</a>
<a class="page-link"
:href="'/exchange-rates/'+from_code+'/'+to_code+'?page=' + (this.page+1)"
rel="next" :aria-label="$t('pagination.next')">&rsaquo;</a>
</li>
<li v-if="totalPages === page" class="page-item disabled" aria-disabled="true" :aria-label="$t('pagination.next')">
<li v-if="totalPages === page" class="page-item disabled" aria-disabled="true"
:aria-label="$t('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
</ul>
@@ -160,7 +180,8 @@
<label for="ffInput_date" class="col-sm-4 control-label"
v-text="$t('form.date')"></label>
<div class="col-sm-8">
<input class="form-control" type="date" name="date" id="ffInput_date" :disabled="posting"
<input class="form-control" type="date" name="date" id="ffInput_date"
:disabled="posting"
autocomplete="off" spellcheck="false" v-model="newDate">
</div>
</div>
@@ -168,16 +189,19 @@
<label for="ffInput_rate" class="col-sm-4 control-label"
v-text="$t('form.rate')"></label>
<div class="col-sm-8">
<input class="form-control" type="number" name="rate" id="ffInput_rate" :disabled="posting"
<input class="form-control" type="number" name="rate" id="ffInput_rate"
:disabled="posting"
autocomplete="off" spellcheck="false" v-model="newRate" step="any">
<p class="help-block" v-text="$t('firefly.help_rate_form', {from: from_code, to: to_code})">
<p class="help-block"
v-text="$t('firefly.help_rate_form', {from: from_code, to: to_code})">
</p>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="nodisablebutton btn pull-right btn-success" v-text="$t('firefly.save_new_rate')"></button>
<button type="submit" class="nodisablebutton btn pull-right btn-success"
v-text="$t('firefly.save_new_rate')"></button>
</div>
</div>
@@ -217,9 +241,9 @@ export default {
mounted() {
// get from and to code from URL
this.newDate = format(new Date, 'yyyy-MM-dd');
let parts = window.location.href.split('/');
this.from_code = parts[parts.length - 2];
this.to_code = parts[parts.length - 1];
let parts = window.location.pathname.split('/');
this.from_code = parts[parts.length - 2].toUpperCase();
this.to_code = parts[parts.length - 1].toUpperCase();
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
@@ -232,8 +256,8 @@ export default {
this.downloadRates(this.page);
},
methods: {
submitRate: function(e) {
if(e) e.preventDefault();
submitRate: function (e) {
if (e) e.preventDefault();
this.posting = true;
axios.post("./api/v1/exchange-rates", {
@@ -260,33 +284,74 @@ export default {
if (0 === parts.length) {
return;
}
console.log('These are the parts', parts);
if ('' !== this.rates[index].rate) {
//console.log('[a] Rate info is', this.rates[index]);
this.updating = true;
axios.put("./api/v1/exchange-rates/" + this.rates[index].rate_id, {rate: this.rates[index].rate})
.then(() => {
this.updating = false;
});
if (0 === parseInt(this.rates[index].rate_id)) {
console.log('[a] POST, not PUT.');
axios.post('./api/v1/exchange-rates',
{
from: this.from_code,
to: this.to_code,
rate: this.rates[index].rate,
date: this.rates[index].date_field
})
.then(() => {
this.updating = false;
});
}
if (0 !== parseInt(this.rates[index].rate_id)) {
console.log('[a] PUT, not POST.');
axios.put('./api/v1/exchange-rates/' + this.rates[index].rate_id, {rate: this.rates[index].rate})
.then(() => {
this.updating = false;
});
}
}
if ('' !== this.rates[index].inverse) {
//console.log('[b] Rate info is', this.rates[index]);
this.updating = true;
axios.put("./api/v1/exchange-rates/" + this.rates[index].inverse_id, {rate: this.rates[index].inverse})
.then(() => {
this.updating = false;
});
if (0 === parseInt(this.rates[index].inverse_id)) {
console.log('[b] POST, not PUT.');
// post, not put
axios.post('./api/v1/exchange-rates',
{
// remember, this is in reverse.
from: this.to_code,
to: this.from_code,
rate: this.rates[index].inverse,
date: this.rates[index].date_field
})
.then(() => {
this.updating = false;
});
}
if (0 !== parseInt(this.rates[index].inverse_id)) {
console.log('[b] PUT, not POST.');
axios.put('./api/v1/exchange-rates/' + this.rates[index].inverse_id, {rate: this.rates[index].inverse})
.then(() => {
this.updating = false;
});
}
}
},
deleteRate: function (index) {
// console.log(this.rates[index].key);
let parts = this.spliceKey(this.rates[index].key);
if (0 === parts.length) {
return;
}
// console.log(parts);
let rateId = parseInt(this.rates[index].rate_id);
let inverseId = parseInt(this.rates[index].inverse_id);
// delete A to B
axios.delete("./api/v1/exchange-rates/" + parts.from + '/' + parts.to + '/' + format(parts.date, 'yyyy-MM-dd'));
// delete B to A.
axios.delete("./api/v1/exchange-rates/" + parts.to + '/' + parts.from + '/' + format(parts.date, 'yyyy-MM-dd'));
if (rateId > 0) {
axios.delete('./api/v1/exchange-rates/' + rateId);
}
if (inverseId > 0) {
axios.delete('./api/v1/exchange-rates/' + inverseId);
}
this.rates.splice(index, 1);
},
@@ -307,6 +372,7 @@ export default {
};
},
downloadCurrencies: function () {
console.log('Now downloading currencies.');
this.loading = true;
axios.get("./api/v1/currencies/" + this.from_code).then((response) => {
this.from = {
@@ -327,31 +393,36 @@ export default {
downloadRates: function (page) {
this.tempRates = {};
this.loading = true;
axios.get("./api/v1/exchange-rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => {
console.log('Now downloading rates.', page);
axios.get('./api/v1/exchange-rates/' + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
console.log('Downloaded entry #' + i);
let current = response.data.data[i];
let date = new Date(current.attributes.date);
let from_code = current.attributes.from_currency_code;
let to_code = current.attributes.to_currency_code;
let from_code = current.attributes.from_currency_code.toUpperCase();
let to_code = current.attributes.to_currency_code.toUpperCase();
let rate = current.attributes.rate;
let inverse = '';
let rate_id = current.id;
let inverse_id = '0';
let key = from_code + '_' + to_code + '_' + format(date, 'yyyy-MM-dd');
// console.log('Key is now "' + key + '"');
console.log('Key is now "' + key + '"');
// perhaps the returned rate is actually the inverse rate.
if (from_code === this.to_code && to_code === this.from_code) {
// console.log('Inverse rate found!');
key = to_code + '_' + from_code + '_' + format(date, 'yyyy-MM-dd');
rate = '';
// new: set rate id to zero.
rate_id = '0';
inverse = current.attributes.rate;
inverse_id = current.id;
// console.log('Key updated to "' + key + '"');
console.log('Key updated to "' + key + '"');
}
if (!this.tempRates.hasOwnProperty(key)) {
console.log('New entry stored');
this.tempRates[key] = {
key: key,
date: date,
@@ -374,6 +445,7 @@ export default {
this.tempRates[key].rate = rate;
this.tempRates[key].rate_id = rate_id;
}
console.log('Found exchange rate #' + this.tempRates[key].rate_id + ' with inverse #' + this.tempRates[key].inverse_id);
}

View File

@@ -35,5 +35,5 @@ return [
'currencies-index' => 'Firefly III supports multiple currencies. Although it defaults to the Euro it can be set to the US Dollar and many other currencies. As you can see a small selection of currencies has been included but you can add your own if you wish to. Changing the default currency will not change the currency of existing transactions however: Firefly III supports the use of multiple currencies at the same time.',
'transactions-index' => 'These expenses, deposits and transfers are not particularly imaginative. They have been generated automatically.',
'piggy-banks-index' => 'As you can see, there are three piggy banks. Use the plus and minus buttons to influence the amount of money in each piggy bank. Click the name of the piggy bank to see the administration for each piggy bank.',
'profile-index' => 'Keep in mind that the demo site resets every four hours. Your access may be revoked at any time. This happens automatically and is not a bug.',
'profile-index' => 'Keep in mind that the demo site resets every four hours. Standard user agents (curl, Postman, wget) are blocked. Your access may be revoked at any time. This happens automatically and is not a bug.',
];

View File

@@ -32,6 +32,7 @@ return [
'webhook_account_info' => 'Cannot deliver account information for budget related webhooks.',
'webhook_transaction_info' => 'Cannot deliver transaction information for budget related webhooks.',
'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities',
'unique_currency_code' => 'This currency code is already in use',
'invalid_account_currency' => 'This account does not use the currency you have selected',
'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".',
'filter_must_be_in' => 'Filter ":filter" must be one of: :values',

View File

@@ -14,6 +14,10 @@
<td>Firefly III</td>
<td>{% if FF_IS_DEVELOP %}<!-- .Z9JBCmw64Zkx1pQw -->{% endif %}{% if not FF_IS_DEVELOP %}v{% endif %}{{ FF_VERSION }}</td>
</tr>
<tr>
<td>Build time</td>
<td>{{ system.build_time_nice }} ({{ system.build_time }})</td>
</tr>
{# PHP version + settings #}
<tr>
<td>PHP version</td>

View File

@@ -121,11 +121,11 @@ Route::group(
Route::post('logout', ['uses' => 'Auth\LoginController@logout', 'as' => 'logout']);
Route::get('flush', ['uses' => 'DebugController@flush', 'as' => 'flush']);
Route::get('routes', ['uses' => 'DebugController@routes', 'as' => 'routes']);
Route::get('debug', 'DebugController@index')->name('debug');
Route::get('debug/api-test', 'DebugController@apiTest')->name('api-test');
}
);
// For the two factor routes, the user must be logged in, but NOT 2FA. Account confirmation does not matter here.
Route::group(
['middleware' => 'user-logged-in-no-2fa', 'prefix' => 'two-factor', 'as' => 'two-factor.', 'namespace' => 'FireflyIII\Http\Controllers\Auth'],
@@ -137,6 +137,16 @@ Route::group(
// For all other routes, the user must be fully authenticated and have an activated account.
// For some other routes, it is only relevant that the user is authenticated.
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers'],
static function (): void {
Route::get('debug', 'DebugController@index')->name('debug');
Route::get('debug/api-test', 'DebugController@apiTest')->name('api-test');
}
);
// Home Controller.
Route::group(
['middleware' => ['user-full-auth'], 'namespace' => 'FireflyIII\Http\Controllers'],