isTokenValid(new CsrfToken('doctor.flush_log', $token))) { $this->flashError('action.csrf.error'); return $this->redirectToRoute('doctor'); } $csrfTokenManager->refreshToken('doctor.flush_log'); $logfile = $this->getLogFilename(); if (file_exists($logfile)) { if (!is_writable($logfile)) { $this->flashError('action.delete.error', 'Logfile cannot be written'); } else { if (false === file_put_contents($logfile, '')) { $this->flashError('action.delete.error', 'Failed writing to logfile'); } else { $this->flashSuccess('action.delete.success'); } } } return $this->redirectToRoute('doctor'); } #[Route(path: '', name: 'doctor', methods: ['GET'])] public function index(): Response { $logLines = 100; $canDeleteLogfile = $this->isGranted('system_configuration') && is_writable($this->getLogFilename()); $page = new PageSetup('Doctor'); $page->setHelp('doctor.html'); $latestRelease = $this->getNextUpdateVersion(); if (\is_array($latestRelease) && \array_key_exists('version', $latestRelease)) { if (version_compare(Constants::VERSION, (string) $latestRelease['version']) >= 0) { $latestRelease = null; } } return $this->render('doctor/index.html.twig', [ 'page_setup' => $page, 'modules' => get_loaded_extensions(), 'environment' => $this->kernelEnvironment, 'info' => $this->getPhpInfo(), 'settings' => $this->getIniSettings(), 'extensions' => $this->getLoadedExtensions(), 'directories' => $this->getFilePermissions(), 'log_delete' => $canDeleteLogfile, 'logs' => $this->getLog(), 'logLines' => $logLines, 'logSize' => $this->getLogSize(), 'composer' => $this->getComposerPackages(), 'release' => $latestRelease, 'opcache' => $this->getOpcacheConfiguration() ]); } /** * @return array{enabled: bool, status: false|array} */ private function getOpcacheConfiguration(): array { $status = \function_exists('opcache_get_status') ? opcache_get_status() : false; $enabled = \is_array($status) && $status['opcache_enabled']; if ($enabled && \array_key_exists('scripts', $status)) { unset($status['scripts']); } return [ 'enabled' => $enabled, 'status' => $status, ]; } /** * @return array */ private function getComposerPackages(): array { /** @var array $versions */ $versions = []; $rootPackage = InstalledVersions::getRootPackage()['name']; foreach (InstalledVersions::getInstalledPackages() as $package) { $versions[$package] = InstalledVersions::getPrettyVersion($package); } // remove kimai from the package list $versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool { if ($name === $rootPackage) { return false; } if ($version === null || $version === '*') { return false; } return true; }, ARRAY_FILTER_USE_BOTH); ksort($versions); return $versions; } /** * @return array */ private function getLoadedExtensions(): array { $results = []; foreach (self::REQUIRED_EXTENSIONS as $extName) { $results[$extName] = false; if (\extension_loaded($extName)) { $results[$extName] = true; } } return $results; } private function getLogSize(): int { $logfile = $this->getLogFilename(); return file_exists($logfile) ? filesize($logfile) : 0; } private function getLogFilename(): string { $logfileName = 'var/log/' . $this->kernelEnvironment . '.log'; return $this->projectDirectory . '/' . $logfileName; } private function getLog(int $lines = 100): array { $logfile = $this->getLogFilename(); if (!file_exists($logfile)) { return ['Missing logfile']; } if (!is_readable($logfile)) { return ['ATTENTION: Cannot read log file']; } $file = new \SplFileObject($logfile, 'r'); if ($file->getSize() === 0) { return ['Empty logfile']; } $file->seek($file->getSize()); $last_line = $file->key(); while ($last_line - $lines < 0) { $lines--; } $iterator = new \LimitIterator($file, $last_line - $lines, $last_line); try { $result = iterator_to_array($iterator); } catch (\Exception $ex) { $result = ['ATTENTION: Failed reading log file']; } if (!is_writable($logfile)) { $result[] = 'ATTENTION: Logfile is not writable'; } return $result; } private function getFilePermissions(): array { $testPaths = []; $baseDir = $this->projectDirectory . DIRECTORY_SEPARATOR; foreach (self::DIRECTORIES_WRITABLE as $path) { $fullPath = $baseDir . $path; $fullUri = realpath($fullPath); if ($fullUri === false && !file_exists($fullPath)) { @mkdir($fullPath); clearstatcache(true); $fullUri = realpath($fullPath); } $testPaths[] = $fullUri; } $results = []; $testPaths[] = $this->fileHelper->getDataDirectory(); foreach ($testPaths as $fullUri) { $fullUri = rtrim($fullUri, DIRECTORY_SEPARATOR); $tmp = str_replace($baseDir, '', $fullUri) . DIRECTORY_SEPARATOR; if (is_readable($fullUri) && is_writable($fullUri)) { $results[$tmp] = true; } else { $results[$tmp] = false; } } return $results; } private function getIniSettings(): array { $ini = [ 'memory_limit', 'session.gc_maxlifetime', 'max_execution_time', 'date.timezone', 'allow_url_fopen', 'default_charset', 'default_mimetype', 'display_errors', 'error_log', 'error_reporting', 'log_errors', 'open_basedir', 'post_max_size', 'sys_temp_dir', 'date.timezone', 'session.gc_maxlifetime', 'disable_functions', 'opcache.enable', 'opcache.memory_consumption', 'opcache.interned_strings_buffer', 'opcache.max_accelerated_files', 'opcache.validate_timestamps', ]; $settings = []; foreach ($ini as $name) { try { $settings[$name] = \ini_get($name); } catch (\Exception $ex) { $settings[$name] = "Couldn't load ini setting: " . $ex->getMessage(); } } return $settings; } /** * @author https://php.net/manual/en/function.phpinfo.php#117961 * @return array */ private function getPhpInfo(): array { $plainText = function ($input): string { return trim(html_entity_decode(strip_tags($input))); }; ob_start(); phpinfo(1); $phpinfo = ['phpinfo' => []]; if (preg_match_all( '#(?:(?:)?(.*?)(?:<\/a>)?<\/h2>)|' . '(?:(.*?)\s*(?:(.*?)\s*(?:(.*?)\s*)?)?)#s', ob_get_clean(), $matches, PREG_SET_ORDER )) { foreach ($matches as $match) { $fn = $plainText; if (isset($match[2]) && isset($match[3])) { $keys1 = array_keys($phpinfo); $phpinfo[end($keys1)][$fn($match[2])] = isset($match[4]) ? [$fn($match[3]), $fn($match[4])] : $fn($match[3]); } else { $keys1 = array_keys($phpinfo); $phpinfo[end($keys1)][] = $fn($match[2]); // @phpstan-ignore-line } } } $phpInfo = $phpinfo['phpinfo']; array_pop($phpInfo); array_shift($phpInfo); return $phpInfo; } /** * @return array{'version': string, 'date': \DateTimeInterface, 'url': string, 'download': string, 'content': string}|null */ private function getNextUpdateVersion(): ?array { return $this->cache->get('kimai.update_release', function (ItemInterface $item) { // we cache the result, no matter if the call failed: at the end, this is "just" // an update note but an expensive call $item->expiresAfter(86400); // one day try { $version = new ReleaseVersion(); return $version->getLatestReleaseFromGithub(true); } catch (\Exception $ex) { // something failed, retry tomorrow } return null; }); } }