. */ declare(strict_types=1); namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use FireflyIII\Enums\AccountTypeEnum; use FireflyIII\Enums\TransactionTypeEnum; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Middleware\IsDemoUser; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Preferences; use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Controllers\GetConfigurationData; use FireflyIII\Support\Models\AccountBalanceCalculator; use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; use Illuminate\View\View; use Monolog\Handler\RotatingFileHandler; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class DebugController */ class DebugController extends Controller { use GetConfigurationData; /** * DebugController constructor. */ public function __construct() { parent::__construct(); $this->middleware(IsDemoUser::class)->except(['displayError']); } public function routes(Request $request): never { if (!auth()->user()->hasRole('owner')) { throw new NotFoundHttpException(); } /** @var iterable $routes */ $routes = Route::getRoutes(); if ('true' === $request->get('api')) { $collection = []; $i = 0; echo 'PATHS="'; /** @var \Illuminate\Routing\Route $route */ foreach ($routes as $route) { ++$i; // skip API and other routes. if (!str_starts_with($route->uri(), 'api/v1') ) { continue; } // skip non GET routes if (!in_array('GET', $route->methods(), true)) { continue; } // no name route: if (null === $route->getName()) { var_dump($route); exit; } echo substr($route->uri(), 3); if (0 === $i % 5) { echo '"
PATHS="${PATHS},'; } if (0 !== $i % 5) { echo ','; } } exit; } $return = []; /** @var \Illuminate\Routing\Route $route */ foreach ($routes as $route) { // skip API and other routes. if ( str_starts_with($route->uri(), 'api') || str_starts_with($route->uri(), '_debugbar') || str_starts_with($route->uri(), '_ignition') || str_starts_with($route->uri(), 'oauth') || str_starts_with($route->uri(), 'chart') || str_starts_with($route->uri(), 'v1/jscript') || str_starts_with($route->uri(), 'v2/jscript') || str_starts_with($route->uri(), 'json') || str_starts_with($route->uri(), 'sanctum') ) { continue; } // skip non GET routes if (!in_array('GET', $route->methods(), true)) { continue; } // no name route: if (null === $route->getName()) { var_dump($route); exit; } if (!str_contains($route->uri(), '{')) { $return[$route->getName()] = route($route->getName()); continue; } $params = []; foreach ($route->parameterNames() as $name) { $params[] = $this->getParameter($name); } $return[$route->getName()] = route($route->getName(), $params); } $count = 0; echo '
'; echo '

Routes

'; echo sprintf('

%s

', $count); foreach ($return as $name => $path) { echo sprintf('%2$s
', $path, $name).PHP_EOL; ++$count; if (0 === $count % 10) { echo '
'; echo sprintf('

%s

', $count); } } exit; } /** * Show all possible errors. * * @throws FireflyException */ 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::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::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.'); throw new FireflyException('A very simple test error.'); } /** * Clear log and session. * * @return Redirector|RedirectResponse * * @throws FireflyException */ public function flush(Request $request) { Preferences::mark(); $request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range', 'temp-mfa-secret', 'temp-mfa-codes']); Artisan::call('cache:clear'); Artisan::call('config:clear'); Artisan::call('route:clear'); Artisan::call('view:clear'); // also do some recalculations. Artisan::call('correction:recalculates-liabilities'); AccountBalanceCalculator::recalculateAll(false); try { Artisan::call('twig:clean'); } catch (\Exception $e) { // intentional generic exception throw new FireflyException($e->getMessage(), 0, $e); } Artisan::call('view:clear'); return redirect(route('index')); } /** * Show debug info. * * @return Factory|View * * @throws FireflyException */ public function index() { $table = $this->generateTable(); $table = str_replace(["\n", "\t", ' '], '', $table); $now = now(config('app.timezone'))->format('Y-m-d H:i:s'); // get latest log file: $logger = Log::driver(); // PHPstan doesn't recognize the method because of its polymorphic nature. $handlers = $logger->getHandlers(); // @phpstan-ignore-line $logContent = ''; foreach ($handlers as $handler) { if ($handler instanceof RotatingFileHandler) { $logFile = $handler->getUrl(); if (null !== $logFile && file_exists($logFile)) { $logContent = file_get_contents($logFile); } } } if ('' !== $logContent) { // last few lines $logContent = 'Truncated from this point <----|'.substr((string) $logContent, -16384); } return view('debug', compact('table', 'now', 'logContent')); } private function generateTable(): string { // system information: $system = $this->getSystemInformation(); $docker = $this->getBuildInfo(); $app = $this->getAppInfo(); $user = $this->getUserInfo(); return (string) view('partials.debug-table', compact('system', 'docker', 'app', 'user')); } private function getSystemInformation(): array { $maxFileSize = Steam::phpBytes((string) ini_get('upload_max_filesize')); $maxPostSize = Steam::phpBytes((string) ini_get('post_max_size')); $drivers = DB::availableDrivers(); $currentDriver = DB::getDriverName(); return [ 'db_version' => app('fireflyconfig')->get('db_version', 1)->data, 'php_version' => PHP_VERSION, 'php_os' => PHP_OS, 'uname' => php_uname('m'), 'interface' => \PHP_SAPI, 'bits' => \PHP_INT_SIZE * 8, 'bcscale' => bcscale(), 'display_errors' => ini_get('display_errors'), 'error_reporting' => $this->errorReporting((int) ini_get('error_reporting')), 'upload_size' => min($maxFileSize, $maxPostSize), 'all_drivers' => $drivers, 'current_driver' => $currentDriver, ]; } private function getBuildInfo(): array { $return = [ 'is_docker' => env('IS_DOCKER', false), // @phpstan-ignore-line 'build' => '(unknown)', 'build_date' => '(unknown)', 'base_build' => '(unknown)', 'base_build_date' => '(unknown)', ]; try { if (file_exists('/var/www/counter-main.txt')) { $return['build'] = trim((string) file_get_contents('/var/www/counter-main.txt')); app('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()); } try { if (file_exists('/var/www/build-date-main.txt')) { $return['build_date'] = trim((string) 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()); } if ('' !== (string) env('BASE_IMAGE_BUILD')) { // @phpstan-ignore-line $return['base_build'] = env('BASE_IMAGE_BUILD'); // @phpstan-ignore-line } if ('' !== (string) env('BASE_IMAGE_DATE')) { // @phpstan-ignore-line $return['base_build_date'] = env('BASE_IMAGE_DATE'); // @phpstan-ignore-line } return $return; } private function getAppInfo(): array { $userGuard = config('auth.defaults.guard'); $config = app('fireflyconfig')->get('last_rt_job', 0); $lastTime = (int) $config->data; $lastCronjob = 'never'; $lastCronjobAgo = 'never'; if ($lastTime > 0) { $carbon = Carbon::createFromTimestamp($lastTime); $lastCronjob = $carbon->format('Y-m-d H:i:s'); $lastCronjobAgo = $carbon->locale('en')->diffForHumans(); // @phpstan-ignore-line } return [ 'debug' => var_export(config('app.debug'), true), 'audit_log_channel' => envNonEmpty('AUDIT_LOG_CHANNEL', '(empty)'), 'default_language' => (string) config('firefly.default_language'), 'default_locale' => (string) config('firefly.default_locale'), 'remote_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_header') : 'N/A', 'remote_mail_header' => 'remote_user_guard' === $userGuard ? config('auth.guard_email') : 'N/A', 'stateful_domains' => implode(', ', config('sanctum.stateful')), // the dates for the cron job are based on the recurring cron job's times. // any of the cron jobs will do, they always run at the same time. // but this job is the oldest, so the biggest chance it ran once 'last_cronjob' => $lastCronjob, 'last_cronjob_ago' => $lastCronjobAgo, ]; } private function getUserInfo(): array { $userFlags = $this->getUserFlags(); // user info $userAgent = request()->header('user-agent'); // set languages, see what happens: $original = setlocale(LC_ALL, '0'); $localeAttempts = []; $parts = Steam::getLocaleArray(Steam::getLocale()); foreach ($parts as $code) { $code = trim($code); app('log')->debug(sprintf('Trying to set %s', $code)); $result = setlocale(LC_ALL, $code); $localeAttempts[$code] = $result === $code; } setlocale(LC_ALL, (string) $original); return [ 'user_id' => auth()->user()->id, 'user_count' => User::count(), 'user_flags' => $userFlags, 'user_agent' => $userAgent, 'native' => Amount::getNativeCurrency(), 'convert_to_native' => Amount::convertToNative(), 'locale_attempts' => $localeAttempts, 'locale' => Steam::getLocale(), 'language' => Steam::getLanguage(), 'view_range' => Preferences::get('viewRange', '1M')->data, ]; } private function getUserFlags(): string { $flags = []; /** @var User $user */ $user = auth()->user(); // has liabilities if ($user->accounts()->accountTypeIn([AccountTypeEnum::DEBT->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::MORTGAGE->value])->count() > 0) { $flags[] = ':credit_card:'; } // has piggies $repository = app(PiggyBankRepositoryInterface::class); $repository->setUser($user); if ($repository->getPiggyBanks()->count() > 0) { $flags[] = ':pig:'; } // has stored reconciliations $type = TransactionType::whereType(TransactionTypeEnum::RECONCILIATION->value)->first(); if ($user->transactionJournals()->where('transaction_type_id', $type->id)->count() > 0) { $flags[] = ':ledger:'; } // has used importer? // has rules if ($user->rules()->count() > 0) { $flags[] = ':wrench:'; } // has recurring transactions if ($user->recurrences()->count() > 0) { $flags[] = ':clock130:'; } // has groups if ($user->objectGroups()->count() > 0) { $flags[] = ':bookmark_tabs:'; } // uses bills if ($user->bills()->count() > 0) { $flags[] = ':email:'; } return implode(' ', $flags); } /** * Flash all types of messages. * * @return Redirector|RedirectResponse */ public function testFlash(Request $request) { $request->session()->flash('success', 'This is a success message.'); $request->session()->flash('info', 'This is an info message.'); $request->session()->flash('warning', 'This is a warning.'); $request->session()->flash('error', 'This is an error!'); return redirect(route('home')); } private function getParameter(string $name): string { switch ($name) { default: throw new FireflyException(sprintf('Unknown parameter "%s"', $name)); case 'cliToken': case 'token': case 'code': case 'oldAddressHash': return 'fake-token'; case 'objectType': return 'asset'; case 'account': return '1'; case 'start_date': return '20241201'; case 'end_date': return '20241231'; case 'attachment': return '1'; case 'bill': return '1'; case 'budget': return '1'; case 'budgetLimit': return '1'; case 'category': return '1'; case 'currency': return '1'; case 'fromCurrencyCode': return 'EUR'; case 'toCurrencyCode': return 'USD'; case 'accountList': return '1,6'; case 'budgetList': return '1,2'; case 'categoryList': return '1,2'; case 'doubleList': return '1,2'; case 'tagList': return '1,2'; case 'tag': return '1'; case 'piggyBank': return '1'; case 'objectGroup': return '1'; case 'route': return 'accounts'; case 'specificPage': return 'show'; case 'recurrence': return '1'; case 'tj': return '1'; case 'reportType': return 'default'; case 'ruleGroup': return '1'; case 'rule': return '1'; case 'tagOrId': return '1'; case 'transactionGroup': return '1'; case 'journalList': return '1,2'; case 'transactionType': return 'withdrawal'; case 'journalLink': return '1'; case 'webhook': return '1'; case 'user': return '1'; case 'linkType': return '1'; case 'userGroup': return '1'; case 'date': return '20241201'; } } }