diff --git a/.sandstorm/changelog.md b/.sandstorm/changelog.md index 6ffcc52bc5..13b38d665b 100644 --- a/.sandstorm/changelog.md +++ b/.sandstorm/changelog.md @@ -1,4 +1,4 @@ -# 4.7.17.1 (API 0.9.2) +# 4.7.17.2 (API 0.9.2) - XSS bug in budget title. # 4.7.17 (API 0.9.2) diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index d78701e8aa..cdec1b1794 100644 --- a/.sandstorm/sandstorm-pkgdef.capnp +++ b/.sandstorm/sandstorm-pkgdef.capnp @@ -16,7 +16,7 @@ const pkgdef :Spk.PackageDefinition = ( manifest = ( appTitle = (defaultText = "Firefly III"), appVersion = 28, - appMarketingVersion = (defaultText = "4.7.17.1"), + appMarketingVersion = (defaultText = "4.7.17.2"), actions = [ # Define your "new document" handlers here. diff --git a/.travis.yml b/.travis.yml index 0a9aa870af..6716f0f425 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: required language: bash env: - - VERSION=4.7.17.1 + - VERSION=4.7.17.2 dist: xenial diff --git a/app/Support/Twig/Extension/Transaction.php b/app/Support/Twig/Extension/Transaction.php new file mode 100644 index 0000000000..8894eff09c --- /dev/null +++ b/app/Support/Twig/Extension/Transaction.php @@ -0,0 +1,426 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Twig\Extension; + +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Attachment; +use FireflyIII\Models\Transaction as TransactionModel; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use Lang; +use Log; +use Twig_Extension; + +/** + * Class Transaction. + */ +class Transaction extends Twig_Extension +{ + /** + * Can show the amount of a transaction, if that transaction has been collected by the journal collector. + * + * @param TransactionModel $transaction + * + * @return string + */ + public function amount(TransactionModel $transaction): string + { + // at this point amount is always negative. + $amount = bcmul(app('steam')->positive((string)$transaction->transaction_amount), '-1'); + $format = '%s'; + $coloured = true; + + if (TransactionType::RECONCILIATION === $transaction->transaction_type_type && 1 === bccomp((string)$transaction->transaction_amount, '0')) { + $amount = bcmul($amount, '-1'); + } + + if (TransactionType::DEPOSIT === $transaction->transaction_type_type) { + $amount = bcmul($amount, '-1'); + } + + if (TransactionType::TRANSFER === $transaction->transaction_type_type) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + if (TransactionType::OPENING_BALANCE === $transaction->transaction_type_type) { + $amount = (string)$transaction->transaction_amount; + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->transaction_currency_symbol; + $currency->decimal_places = $transaction->transaction_currency_dp; + $str = sprintf($format, app('amount')->formatAnything($currency, $amount, $coloured)); + + if (null !== $transaction->transaction_foreign_amount) { + $amount = bcmul(app('steam')->positive((string)$transaction->transaction_foreign_amount), '-1'); + if (TransactionType::DEPOSIT === $transaction->transaction_type_type) { + $amount = bcmul($amount, '-1'); + } + + if (TransactionType::TRANSFER === $transaction->transaction_type_type) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->foreign_currency_symbol; + $currency->decimal_places = $transaction->foreign_currency_dp; + $str .= ' (' . sprintf($format, app('amount')->formatAnything($currency, $amount, $coloured)) . ')'; + } + + return $str; + } + + /** + * @param array $transaction + * + * @return string + */ + public function amountArray(array $transaction): string + { + // first display amount: + $amount = (string)$transaction['amount']; + $fakeCurrency = new TransactionCurrency; + $fakeCurrency->decimal_places = $transaction['currency_decimal_places']; + $fakeCurrency->symbol = $transaction['currency_symbol']; + $string = app('amount')->formatAnything($fakeCurrency, $amount, true); + + // then display (if present) the foreign amount: + if (null !== $transaction['foreign_amount']) { + $amount = (string)$transaction['foreign_amount']; + $fakeCurrency = new TransactionCurrency; + $fakeCurrency->decimal_places = $transaction['foreign_currency_decimal_places']; + $fakeCurrency->symbol = $transaction['foreign_currency_symbol']; + $string .= ' (' . app('amount')->formatAnything($fakeCurrency, $amount, true) . ')'; + } + + return $string; + } + + /** + * + * @param TransactionModel $transaction + * + * @return string + */ + public function budgets(TransactionModel $transaction): string + { + $txt = ''; + // journal has a budget: + if (null !== $transaction->transaction_journal_budget_id) { + $name = $transaction->transaction_journal_budget_name; + $txt = sprintf('%s', route('budgets.show', [$transaction->transaction_journal_budget_id]), e($name), e($name)); + } + + // transaction has a budget + if (null !== $transaction->transaction_budget_id && '' === $txt) { + $name = $transaction->transaction_budget_name; + $txt = sprintf('%s', route('budgets.show', [$transaction->transaction_budget_id]), e($name), e($name)); + } + + if ('' === $txt) { + // see if the transaction has a budget: + $budgets = $transaction->budgets()->get(); + if (0 === $budgets->count()) { + $budgets = $transaction->transactionJournal()->first()->budgets()->get(); + } + if ($budgets->count() > 0) { + $str = []; + foreach ($budgets as $budget) { + $str[] = sprintf('%s', route('budgets.show', [$budget->id]), e($budget->name), e($budget->name)); + } + $txt = implode(', ', $str); + } + } + + return $txt; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function categories(TransactionModel $transaction): string + { + $txt = ''; + // journal has a category: + if (null !== $transaction->transaction_journal_category_id) { + $name = $transaction->transaction_journal_category_name; + $txt = sprintf('%s', route('categories.show', [$transaction->transaction_journal_category_id]), e($name), e($name)); + } + + // transaction has a category: + if (null !== $transaction->transaction_category_id && '' === $txt) { + $name = $transaction->transaction_category_name; + $txt = sprintf('%s', route('categories.show', [$transaction->transaction_category_id]), e($name), e($name)); + } + + if ('' === $txt) { + // see if the transaction has a category: + $categories = $transaction->categories()->get(); + if (0 === $categories->count()) { + $categories = $transaction->transactionJournal()->first()->categories()->get(); + } + if ($categories->count() > 0) { + $str = []; + foreach ($categories as $category) { + $str[] = sprintf('%s', route('categories.show', [$category->id]), e($category->name), e($category->name)); + } + + $txt = implode(', ', $str); + } + } + + return $txt; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function description(TransactionModel $transaction): string + { + $description = $transaction->description; + if ('' !== (string)$transaction->transaction_description) { + $description = $transaction->transaction_description . ' (' . $transaction->description . ')'; + } + + return $description; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function destinationAccount(TransactionModel $transaction): string + { + if (TransactionType::RECONCILIATION === $transaction->transaction_type_type) { + return '—'; + } + + $name = $transaction->account_name; + $iban = $transaction->account_iban; + $transactionId = (int)$transaction->account_id; + $type = $transaction->account_type; + + // name is present in object, use that one: + if (null !== $transaction->opposing_account_id && bccomp($transaction->transaction_amount, '0') === -1) { + $name = $transaction->opposing_account_name; + $transactionId = (int)$transaction->opposing_account_id; + $type = $transaction->opposing_account_type; + $iban = $transaction->opposing_account_iban; + } + + // Find the opposing account and use that one: + if (null === $transaction->opposing_account_id && bccomp($transaction->transaction_amount, '0') === -1) { + // if the amount is negative, find the opposing account and use that one: + $journalId = $transaction->journal_id; + /** @var TransactionModel $other */ + $other = TransactionModel + ::where('transaction_journal_id', $journalId) + ->where('transactions.id', '!=', $transaction->id) + ->where('amount', '=', bcmul($transaction->transaction_amount, '-1')) + ->where('identifier', $transaction->identifier) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); + if (null === $other) { + Log::error(sprintf('Cannot find other transaction for journal #%d', $journalId)); + + return ''; + } + $name = $other->name; + $transactionId = $other->account_id; + $type = $other->type; + } + + if (AccountType::CASH === $type) { + $txt = '(' . trans('firefly.cash') . ')'; + + return $txt; + } + + $txt = sprintf('%1$s', e($name), route('accounts.show', [$transactionId]), e($iban)); + + return $txt; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function hasAttachments(TransactionModel $transaction): string + { + $res = ''; + if (\is_int($transaction->attachmentCount) && $transaction->attachmentCount > 0) { + $res = sprintf( + '', Lang::choice( + 'firefly.nr_of_attachments', + $transaction->attachmentCount, ['count' => $transaction->attachmentCount] + ) + ); + } + if (null === $transaction->attachmentCount) { + $journalId = (int)$transaction->journal_id; + $count = Attachment::whereNull('deleted_at') + ->where('attachable_type', TransactionJournal::class) + ->where('attachable_id', $journalId) + ->count(); + if ($count > 0) { + $res = sprintf('', Lang::choice('firefly.nr_of_attachments', $count, ['count' => $count])); + } + } + + return $res; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function icon(TransactionModel $transaction): string + { + switch ($transaction->transaction_type_type) { + case TransactionType::WITHDRAWAL: + $txt = sprintf('', (string)trans('firefly.withdrawal')); + break; + case TransactionType::DEPOSIT: + $txt = sprintf('', (string)trans('firefly.deposit')); + break; + case TransactionType::TRANSFER: + $txt = sprintf('', (string)trans('firefly.transfer')); + break; + case TransactionType::OPENING_BALANCE: + $txt = sprintf('', (string)trans('firefly.opening_balance')); + break; + case TransactionType::RECONCILIATION: + $txt = sprintf('', (string)trans('firefly.reconciliation_transaction')); + break; + default: + $txt = ''; + break; + } + + return $txt; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function isReconciled(TransactionModel $transaction): string + { + $icon = ''; + if (1 === (int)$transaction->reconciled) { + $icon = ''; + } + + return $icon; + } + + /** + * Returns an icon when the transaction is a split transaction. + * + * @param TransactionModel $transaction + * + * @return string + */ + public function isSplit(TransactionModel $transaction): string + { + $res = ''; + if (true === $transaction->is_split) { + $res = ''; + } + + if (null === $transaction->is_split) { + $journalId = (int)$transaction->journal_id; + $count = TransactionModel::where('transaction_journal_id', $journalId)->whereNull('deleted_at')->count(); + if ($count > 2) { + $res = ''; + } + } + + return $res; + } + + /** + * @param TransactionModel $transaction + * + * @return string + */ + public function sourceAccount(TransactionModel $transaction): string + { + if (TransactionType::RECONCILIATION === $transaction->transaction_type_type) { + return '—'; + } + + // if the amount is negative, assume that the current account (the one in $transaction) is indeed the source account. + $name = $transaction->account_name; + $transactionId = (int)$transaction->account_id; + $type = $transaction->account_type; + $iban = $transaction->account_iban; + + // name is present in object, use that one: + if (null !== $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) { + $name = $transaction->opposing_account_name; + $transactionId = (int)$transaction->opposing_account_id; + $type = $transaction->opposing_account_type; + $iban = $transaction->opposing_account_iban; + } + // Find the opposing account and use that one: + if (null === $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) { + $journalId = $transaction->journal_id; + /** @var TransactionModel $other */ + $other = TransactionModel::where('transaction_journal_id', $journalId)->where('transactions.id', '!=', $transaction->id) + ->where('amount', '=', bcmul($transaction->transaction_amount, '-1'))->where( + 'identifier', + $transaction->identifier + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->first(['transactions.account_id', 'accounts.encrypted', 'accounts.name', 'account_types.type']); + $name = $other->name; + $transactionId = $other->account_id; + $type = $other->type; + } + + if (AccountType::CASH === $type) { + $txt = '(' . trans('firefly.cash') . ')'; + + return $txt; + } + + $txt = sprintf('%1$s', e($name), route('accounts.show', [$transactionId]), e($iban)); + + return $txt; + } +} diff --git a/changelog.md b/changelog.md index d2a140a6de..6b95e171a4 100644 --- a/changelog.md +++ b/changelog.md @@ -2,16 +2,9 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## 4.8.0 +## [4.7.17.2 (API 0.9.2)] - 2019-07-15 -Lots of changes -- No more export function -- updated api - - -## [4.7.17.1 (API 0.9.2)] - 2019-07-15 - -- XSS bug in budget title. +- XSS bug in budget title, found by [@dayn1ne](https://github.com/dayn1ne). ## [4.7.17 (API 0.9.2)] - 2019-03-17