. */ declare(strict_types=1); namespace FireflyIII\Support; use Carbon\Carbon; use Carbon\Exceptions\InvalidFormatException; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; /** * Class Steam. */ class Steam { /** * @deprecated */ public function balanceIgnoreVirtual(Account $account, Carbon $date): string { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $repository->setUser($account->user); $currencyId = (int) $repository->getMetaValue($account, 'currency_id'); $transactions = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', $currencyId) ->get(['transactions.amount'])->toArray() ; $nativeBalance = $this->sumTransactions($transactions, 'amount'); // get all balances in foreign currency: $transactions = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.foreign_currency_id', $currencyId) ->where('transactions.transaction_currency_id', '!=', $currencyId) ->get(['transactions.foreign_amount'])->toArray() ; $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); return bcadd($nativeBalance, $foreignBalance); } public function sumTransactions(array $transactions, string $key): string { $sum = '0'; /** @var array $transaction */ foreach ($transactions as $transaction) { $value = (string) ($transaction[$key] ?? '0'); $value = '' === $value ? '0' : $value; $sum = bcadd($sum, $value); } return $sum; } /** * Gets the balance for the given account during the whole range, using this format:. * * [yyyy-mm-dd] => 123,2 * * @throws FireflyException */ public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range'); $cache->addProperty(null !== $currency ? $currency->id : 0); $cache->addProperty($start); $cache->addProperty($end); if ($cache->has()) { return $cache->get(); } $start->subDay(); $end->addDay(); $balances = []; $formatted = $start->format('Y-m-d'); $startBalance = $this->balance($account, $start, $currency); $balances[$formatted] = $startBalance; if (null === $currency) { $repository = app(AccountRepositoryInterface::class); $repository->setUser($account->user); $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); } $currencyId = $currency->id; $start->addDay(); // query! $set = $account->transactions() ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) ->groupBy('transaction_journals.date') ->groupBy('transactions.transaction_currency_id') ->groupBy('transactions.foreign_currency_id') ->orderBy('transaction_journals.date', 'ASC') ->whereNull('transaction_journals.deleted_at') ->get( [ // @phpstan-ignore-line 'transaction_journals.date', 'transactions.transaction_currency_id', \DB::raw('SUM(transactions.amount) AS modified'), 'transactions.foreign_currency_id', \DB::raw('SUM(transactions.foreign_amount) AS modified_foreign'), ] ) ; $currentBalance = $startBalance; /** @var Transaction $entry */ foreach ($set as $entry) { // normal amount and foreign amount $modified = (string) (null === $entry->modified ? '0' : $entry->modified); $foreignModified = (string) (null === $entry->modified_foreign ? '0' : $entry->modified_foreign); $amount = '0'; if ($currencyId === (int) $entry->transaction_currency_id || 0 === $currencyId) { // use normal amount: $amount = $modified; } if ($currencyId === (int) $entry->foreign_currency_id) { // use foreign amount: $amount = $foreignModified; } // Log::debug(sprintf('Trying to add %s and %s.', var_export($currentBalance, true), var_export($amount, true))); $currentBalance = bcadd($currentBalance, $amount); $carbon = new Carbon($entry->date, config('app.timezone')); $date = $carbon->format('Y-m-d'); $balances[$date] = $currentBalance; } $cache->store($balances); return $balances; } public function balanceByTransactions(Account $account, Carbon $date, ?TransactionCurrency $currency): array { $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-by-transactions'); $cache->addProperty($date); $cache->addProperty(null !== $currency ? $currency->id : 0); if ($cache->has()) { return $cache->get(); } $query = $account->transactions() ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->orderBy('transaction_journals.date', 'desc') ->orderBy('transaction_journals.order', 'asc') ->orderBy('transaction_journals.description', 'desc') ->orderBy('transactions.amount', 'desc') ; if (null !== $currency) { $query->where('transactions.transaction_currency_id', $currency->id); $query->limit(1); $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after'])->first(); $key = (int) $result->transaction_currency_id; $return = [$key => $result->balance_after]; $cache->store($return); return $return; } $return = []; $result = $query->get(['transactions.transaction_currency_id', 'transactions.balance_after']); foreach ($result as $entry) { $key = (int) $entry->transaction_currency_id; if (array_key_exists($key, $return)) { continue; } $return[$key] = $entry->balance_after; } return $return; } /** * Gets balance at the end of current month by default * * @throws FireflyException */ public function balance(Account $account, Carbon $date, ?TransactionCurrency $currency = null): string { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance'); $cache->addProperty($date); $cache->addProperty(null !== $currency ? $currency->id : 0); if ($cache->has()) { return $cache->get(); } /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); if (null === $currency) { $currency = $repository->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); } // first part: get all balances in own currency: $transactions = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', $currency->id) ->get(['transactions.amount'])->toArray() ; $nativeBalance = $this->sumTransactions($transactions, 'amount'); // get all balances in foreign currency: $transactions = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.foreign_currency_id', $currency->id) ->where('transactions.transaction_currency_id', '!=', $currency->id) ->get(['transactions.foreign_amount'])->toArray() ; $foreignBalance = $this->sumTransactions($transactions, 'foreign_amount'); $balance = bcadd($nativeBalance, $foreignBalance); $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; $balance = bcadd($balance, $virtual); $cache->store($balance); return $balance; } /** * @throws FireflyException * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-in-range-converted'); $cache->addProperty($native->id); $cache->addProperty($start); $cache->addProperty($end); if ($cache->has()) { return $cache->get(); } Log::debug(sprintf('balanceInRangeConverted for account #%d to %s', $account->id, $native->code)); $start->subDay(); $end->addDay(); $balances = []; $formatted = $start->format('Y-m-d'); $currencies = []; $startBalance = $this->balanceConverted($account, $start, $native); // already converted to native amount $balances[$formatted] = $startBalance; Log::debug(sprintf('Start balance on %s is %s', $formatted, $startBalance)); Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); $converter = new ExchangeRateConverter(); // not sure why this is happening: $start->addDay(); // grab all transactions between start and end: $set = $account->transactions() ->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59')) ->orderBy('transaction_journals.date', 'ASC') ->whereNull('transaction_journals.deleted_at') ->get( [ 'transaction_journals.date', 'transactions.transaction_currency_id', 'transactions.amount', 'transactions.foreign_currency_id', 'transactions.foreign_amount', ] )->toArray() ; // loop the set and convert if necessary: $currentBalance = $startBalance; /** @var Transaction $transaction */ foreach ($set as $transaction) { $day = false; try { $day = Carbon::parse($transaction['date'], config('app.timezone')); } catch (InvalidFormatException $e) { Log::error(sprintf('Could not parse date "%s" in %s: %s', $transaction['date'], __METHOD__, $e->getMessage())); } if (false === $day) { $day = today(config('app.timezone')); } $format = $day->format('Y-m-d'); // if the transaction is in the expected currency, change nothing. if ((int) $transaction['transaction_currency_id'] === $native->id) { // change the current balance, set it to today, continue the loop. $currentBalance = bcadd($currentBalance, $transaction['amount']); $balances[$format] = $currentBalance; Log::debug(sprintf('%s: transaction in %s, new balance is %s.', $format, $native->code, $currentBalance)); continue; } // if foreign currency is in the expected currency, do nothing: if ((int) $transaction['foreign_currency_id'] === $native->id) { $currentBalance = bcadd($currentBalance, $transaction['foreign_amount']); $balances[$format] = $currentBalance; Log::debug(sprintf('%s: transaction in %s (foreign), new balance is %s.', $format, $native->code, $currentBalance)); continue; } // otherwise, convert 'amount' to the necessary currency: $currencyId = (int) $transaction['transaction_currency_id']; $currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId); $currencies[$currencyId] = $currency; $rate = $converter->getCurrencyRate($currency, $native, $day); $convertedAmount = bcmul($transaction['amount'], $rate); $currentBalance = bcadd($currentBalance, $convertedAmount); $balances[$format] = $currentBalance; Log::debug(sprintf( '%s: transaction in %s(!). Conversion rate is %s. %s %s = %s %s', $format, $currency->code, $rate, $currency->code, $transaction['amount'], $native->code, $convertedAmount )); } $cache->store($balances); $converter->summarize(); return $balances; } /** * selection of transactions * 1: all normal transactions. No foreign currency info. In $currency. Need conversion. * 2: all normal transactions. No foreign currency info. In $native. Need NO conversion. * 3: all normal transactions. No foreign currency info. In neither currency. Need conversion. * Then, select everything with foreign currency info: * 4. All transactions with foreign currency info in $native. Normal currency value is ignored. Do not need * conversion. * 5. All transactions with foreign currency info NOT in $native, but currency info in $currency. Need conversion. * 6. All transactions with foreign currency info NOT in $native, and currency info NOT in $currency. Need * conversion. * * Gets balance at the end of current month by default. Returns the balance converted * to the indicated currency ($native). * * @throws FireflyException * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function balanceConverted(Account $account, Carbon $date, TransactionCurrency $native): string { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); Log::debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code)); $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance'); $cache->addProperty($date); $cache->addProperty($native->id); if ($cache->has()) { Log::debug('Cached!'); return $cache->get(); } /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $currency = $repository->getAccountCurrency($account); $currency = null === $currency ? app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup) : $currency; if ($native->id === $currency->id) { Log::debug('No conversion necessary!'); return $this->balance($account, $date); } $new = []; $existing = []; $new[] = $account->transactions() // 1 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', $currency->id) ->whereNull('transactions.foreign_currency_id') ->get(['transaction_journals.date', 'transactions.amount'])->toArray() ; Log::debug(sprintf('%d transaction(s) in set #1', count($new[0]))); $existing[] = $account->transactions() // 2 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', $native->id) ->whereNull('transactions.foreign_currency_id') ->get(['transactions.amount'])->toArray() ; Log::debug(sprintf('%d transaction(s) in set #2', count($existing[0]))); $new[] = $account->transactions() // 3 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', '!=', $currency->id) ->where('transactions.transaction_currency_id', '!=', $native->id) ->whereNull('transactions.foreign_currency_id') ->get(['transaction_journals.date', 'transactions.amount'])->toArray() ; Log::debug(sprintf('%d transactions in set #3', count($new[1]))); $existing[] = $account->transactions() // 4 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.foreign_currency_id', $native->id) ->whereNotNull('transactions.foreign_amount') ->get(['transactions.foreign_amount'])->toArray() ; Log::debug(sprintf('%d transactions in set #4', count($existing[1]))); $new[] = $account->transactions()// 5 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', $currency->id) ->where('transactions.foreign_currency_id', '!=', $native->id) ->whereNotNull('transactions.foreign_amount') ->get(['transaction_journals.date', 'transactions.amount'])->toArray() ; Log::debug(sprintf('%d transactions in set #5', count($new[2]))); $new[] = $account->transactions()// 6 ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->where('transactions.transaction_currency_id', '!=', $currency->id) ->where('transactions.foreign_currency_id', '!=', $native->id) ->whereNotNull('transactions.foreign_amount') ->get(['transaction_journals.date', 'transactions.amount'])->toArray() ; Log::debug(sprintf('%d transactions in set #6', count($new[3]))); // process both sets of transactions. Of course, no need to convert set "existing". $balance = $this->sumTransactions($existing[0], 'amount'); $balance = bcadd($balance, $this->sumTransactions($existing[1], 'foreign_amount')); Log::debug(sprintf('Balance from set #2 and #4 is %f', $balance)); // need to convert the others. All sets use the "amount" value as their base (that's easy) // but we need to convert each transaction separately because the date difference may // incur huge currency changes. Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); $start = clone $date; $end = clone $date; $converter = new ExchangeRateConverter(); foreach ($new as $set) { foreach ($set as $transaction) { $currentDate = false; try { $currentDate = Carbon::parse($transaction['date'], config('app.timezone')); } catch (InvalidFormatException $e) { Log::error(sprintf('Could not parse date "%s" in %s', $transaction['date'], __METHOD__)); } if (false === $currentDate) { $currentDate = today(config('app.timezone')); } if ($currentDate->lte($start)) { $start = clone $currentDate; } } } unset($currentDate); $converter->prepare($currency, $native, $start, $end); foreach ($new as $set) { foreach ($set as $transaction) { $currentDate = false; try { $currentDate = Carbon::parse($transaction['date'], config('app.timezone')); } catch (InvalidFormatException $e) { Log::error(sprintf('Could not parse date "%s" in %s', $transaction['date'], __METHOD__)); } if (false === $currentDate) { $currentDate = today(config('app.timezone')); } $rate = $converter->getCurrencyRate($currency, $native, $currentDate); $convertedAmount = bcmul($transaction['amount'], $rate); $balance = bcadd($balance, $convertedAmount); } } // add virtual balance (also needs conversion) $virtual = null === $account->virtual_balance ? '0' : $account->virtual_balance; $virtual = $converter->convert($currency, $native, $account->created_at, $virtual); $balance = bcadd($balance, $virtual); $converter->summarize(); $cache->store($balance); $converter->summarize(); return $balance; } /** * This method always ignores the virtual balance. * * @throws FireflyException */ public function balancesByAccounts(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances'); $cache->addProperty($date); if ($cache->has()) { return $cache->get(); } // need to do this per account. $result = []; /** @var Account $account */ foreach ($accounts as $account) { $result[$account->id] = $this->balance($account, $date); } $cache->store($result); return $result; } /** * This method always ignores the virtual balance. * * @throws FireflyException */ public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances-converted'); $cache->addProperty($date); if ($cache->has()) { return $cache->get(); } // need to do this per account. $result = []; /** @var Account $account */ foreach ($accounts as $account) { $default = app('amount')->getDefaultCurrencyByUserGroup($account->user->userGroup); $result[$account->id] = [ 'balance' => $this->balance($account, $date), 'native_balance' => $this->balanceConverted($account, $date, $default), ]; } $cache->store($result); return $result; } /** * Same as above, but also groups per currency. */ public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); $ids = $accounts->pluck('id')->toArray(); // cache this property. $cache = new CacheProperties(); $cache->addProperty($ids); $cache->addProperty('balances-per-currency'); $cache->addProperty($date); if ($cache->has()) { return $cache->get(); } // need to do this per account. $result = []; /** @var Account $account */ foreach ($accounts as $account) { $result[$account->id] = $this->balancePerCurrency($account, $date); } $cache->store($result); return $result; } public function balancePerCurrency(Account $account, Carbon $date): array { // Log::warning(sprintf('Deprecated method %s, do not use.', __METHOD__)); // abuse chart properties: $cache = new CacheProperties(); $cache->addProperty($account->id); $cache->addProperty('balance-per-currency'); $cache->addProperty($date); if ($cache->has()) { return $cache->get(); } $query = $account->transactions() ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) ->groupBy('transactions.transaction_currency_id') ; $balances = $query->get(['transactions.transaction_currency_id', \DB::raw('SUM(transactions.amount) as sum_for_currency')]); // @phpstan-ignore-line $return = []; /** @var \stdClass $entry */ foreach ($balances as $entry) { $return[(int) $entry->transaction_currency_id] = (string) $entry->sum_for_currency; } $cache->store($return); return $return; } /** * https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers */ public function bcround(?string $number, int $precision = 0): string { if (null === $number) { return '0'; } if ('' === trim($number)) { return '0'; } // if the number contains "E", it's in scientific notation, so we need to convert it to a normal number first. if (false !== stripos($number, 'e')) { $number = sprintf('%.12f', $number); } // Log::debug(sprintf('Trying bcround("%s",%d)', $number, $precision)); if (str_contains($number, '.')) { if ('-' !== $number[0]) { return bcadd($number, '0.'.str_repeat('0', $precision).'5', $precision); } return bcsub($number, '0.'.str_repeat('0', $precision).'5', $precision); } return $number; } public function filterSpaces(string $string): string { $search = [ "\u{0001}", // start of heading "\u{0002}", // start of text "\u{0003}", // end of text "\u{0004}", // end of transmission "\u{0005}", // enquiry "\u{0006}", // ACK "\u{0007}", // BEL "\u{0008}", // backspace "\u{000E}", // shift out "\u{000F}", // shift in "\u{0010}", // data link escape "\u{0011}", // DC1 "\u{0012}", // DC2 "\u{0013}", // DC3 "\u{0014}", // DC4 "\u{0015}", // NAK "\u{0016}", // SYN "\u{0017}", // ETB "\u{0018}", // CAN "\u{0019}", // EM "\u{001A}", // SUB "\u{001B}", // escape "\u{001C}", // file separator "\u{001D}", // group separator "\u{001E}", // record separator "\u{001F}", // unit separator "\u{007F}", // DEL "\u{00A0}", // non-breaking space "\u{1680}", // ogham space mark "\u{180E}", // mongolian vowel separator "\u{2000}", // en quad "\u{2001}", // em quad "\u{2002}", // en space "\u{2003}", // em space "\u{2004}", // three-per-em space "\u{2005}", // four-per-em space "\u{2006}", // six-per-em space "\u{2007}", // figure space "\u{2008}", // punctuation space "\u{2009}", // thin space "\u{200A}", // hair space "\u{200B}", // zero width space "\u{202F}", // narrow no-break space "\u{3000}", // ideographic space "\u{FEFF}", // zero width no -break space "\x20", // plain old normal space, ' ', ]; // clear zalgo text $string = preg_replace('/(\pM{2})\pM+/u', '\1', $string); $string = preg_replace('/\s+/', '', $string); return str_replace($search, '', $string); } /** * @throws FireflyException */ public function getHostName(string $ipAddress): string { try { $hostName = gethostbyaddr($ipAddress); } catch (\Exception $e) { // intentional generic exception throw new FireflyException($e->getMessage(), 0, $e); } return (string) $hostName; } public function getLastActivities(array $accounts): array { $list = []; $set = auth()->user()->transactions() ->whereIn('transactions.account_id', $accounts) ->groupBy(['transactions.account_id', 'transaction_journals.user_id']) ->get(['transactions.account_id', \DB::raw('MAX(transaction_journals.date) AS max_date')]) // @phpstan-ignore-line ; /** @var Transaction $entry */ foreach ($set as $entry) { $date = new Carbon($entry->max_date, config('app.timezone')); $date->setTimezone(config('app.timezone')); $list[$entry->account_id] = $date; } return $list; } /** * Get user's locale. */ public function getLocale(): string // get preference { $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data; if (is_array($locale)) { $locale = 'equal'; } if ('equal' === $locale) { $locale = $this->getLanguage(); } $locale = (string) $locale; // Check for Windows to replace the locale correctly. if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) { $locale = str_replace('_', '-', $locale); } return $locale; } /** * Get user's language. * * @throws FireflyException */ public function getLanguage(): string // get preference { $preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data; if (!is_string($preference)) { throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference))); } return str_replace('-', '_', $preference); } public function getLocaleArray(string $locale): array { return [ sprintf('%s.utf8', $locale), sprintf('%s.UTF-8', $locale), ]; } /** * Returns the previous URL but refuses to send you to specific URLs. * * - outside domain * - to JS files, API or JSON routes * * Uses the session's previousUrl() function as inspired by GitHub user @z1r0- * * session()->previousUrl() uses getSafeUrl() so we can safely return it: */ public function getSafePreviousUrl(): string { // Log::debug(sprintf('getSafePreviousUrl: "%s"', session()->previousUrl())); return session()->previousUrl() ?? route('index'); } /** * Make sure URL is safe. */ public function getSafeUrl(string $unknownUrl, string $safeUrl): string { // Log::debug(sprintf('getSafeUrl(%s, %s)', $unknownUrl, $safeUrl)); $returnUrl = $safeUrl; $unknownHost = parse_url($unknownUrl, PHP_URL_HOST); $safeHost = parse_url($safeUrl, PHP_URL_HOST); if (null !== $unknownHost && $unknownHost === $safeHost) { $returnUrl = $unknownUrl; } // URL must not lead to weird pages $forbiddenWords = ['jscript', 'json', 'debug', 'serviceworker', 'offline', 'delete', '/login', '/attachments/view']; if (\Str::contains($returnUrl, $forbiddenWords)) { $returnUrl = $safeUrl; } return $returnUrl; } public function negative(string $amount): string { if ('' === $amount) { return '0'; } $amount = $this->floatalize($amount); if (1 === bccomp($amount, '0')) { $amount = bcmul($amount, '-1'); } return $amount; } /** * https://framework.zend.com/downloads/archives * * Convert a scientific notation to float * Additionally fixed a problem with PHP <= 5.2.x with big integers */ public function floatalize(string $value): string { $value = strtoupper($value); if (!str_contains($value, 'E')) { return $value; } Log::debug(sprintf('Floatalizing %s', $value)); $number = substr($value, 0, (int) strpos($value, 'E')); if (str_contains($number, '.')) { $post = strlen(substr($number, (int) strpos($number, '.') + 1)); $mantis = substr($value, (int) strpos($value, 'E') + 1); if ($mantis < 0) { $post += abs((int) $mantis); } // TODO careless float could break financial math. return number_format((float) $value, $post, '.', ''); } // TODO careless float could break financial math. return number_format((float) $value, 0, '.', ''); } public function opposite(?string $amount = null): ?string { if (null === $amount) { return null; } return bcmul($amount, '-1'); } public function phpBytes(string $string): int { $string = str_replace(['kb', 'mb', 'gb'], ['k', 'm', 'g'], strtolower($string)); if (false !== stripos($string, 'k')) { // has a K in it, remove the K and multiply by 1024. $bytes = bcmul(rtrim($string, 'k'), '1024'); return (int) $bytes; } if (false !== stripos($string, 'm')) { // has a M in it, remove the M and multiply by 1048576. $bytes = bcmul(rtrim($string, 'm'), '1048576'); return (int) $bytes; } if (false !== stripos($string, 'g')) { // has a G in it, remove the G and multiply by (1024)^3. $bytes = bcmul(rtrim($string, 'g'), '1073741824'); return (int) $bytes; } return (int) $string; } public function positive(string $amount): string { if ('' === $amount) { return '0'; } try { if (-1 === bccomp($amount, '0')) { $amount = bcmul($amount, '-1'); } } catch (\ValueError $e) { Log::error(sprintf('ValueError in Steam::positive("%s"): %s', $amount, $e->getMessage())); Log::error($e->getTraceAsString()); return '0'; } return $amount; } }