Lines 84.88% 73 / 86
Methods 33.33% 2 / 6
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 index 84.61% 22 / 26 0.00% 0 / 1 4.06
 install 100.00% 15 / 15 100.00% 1 / 1 3
 update 85.71% 24 / 28 0.00% 0 / 1 7.14
 hasAdministratorSession 42.85% 3 / 7 0.00% 0 / 1 6.99
 getRelativeTokenFilePath 80.00% 4 / 5 0.00% 0 / 1 2.03
 render 100.00% 5 / 5 100.00% 1 / 1 1
40final class SetupController
41{
42    /**
43     * @throws TemplateException
44     * @throws \Exception
45     */
46    #[Route(path: '/setup', name: 'public.setup.update', methods: ['GET'])]
47    public function index(Request $request): Response
48    {
49        $system = new System();
50
51        if (!$system->checkInstallation()) {
52            return new Response('phpMyFAQ is already installed.', Response::HTTP_FORBIDDEN);
53        }
54
55        $installer = new Installer($system);
56
57        $checkBasicError = '';
58        try {
59            $installer->checkBasicStuff();
60        } catch (Exception $exception) {
61            $checkBasicError = $exception->getMessage();
62        }
63
64        try {
65            $installer->checkInitialRewriteBasePath($request);
66        } catch (Exception $exception) {
67            $checkBasicError = $exception->getMessage();
68        }
69
70        return $this->render('@setup/index.twig', [
71            'newVersion' => System::getVersion(),
72            'setupType' => 'Setup',
73            'currentYear' => date(format: 'Y'),
74            'currentLanguage' => 'en',
75            'documentationUrl' => System::getDocumentationUrl(),
76            'checkBasicError' => $checkBasicError,
77            'nonCriticalSettings' => $installer->checkNoncriticalSettings(),
78            'filePermissions' => $installer->checkFilesystemPermissions(),
79            'supportedDatabases' => $system->getSupportedSafeDatabases(),
80            'currentPath' => dirname(path: __DIR__, levels: 4),
81            'isLdapEnabled' => $installer->hasLdapSupport(),
82            'isElasticsearchEnabled' => $installer->hasElasticsearchSupport(),
83            'supportedTranslations' => LanguageCodes::getAllSupported(),
84        ]);
85    }
86
87    /**
88     * @throws TemplateException
89     * @throws \Exception
90     */
91    #[Route(path: '/setup/install', name: 'public.setup.install', methods: ['GET'])]
92    public function install(): Response
93    {
94        $system = new System();
95
96        if (!$system->checkInstallation()) {
97            return new Response('phpMyFAQ is already installed.', Response::HTTP_FORBIDDEN);
98        }
99
100        $installer = new Installer($system);
101
102        $installationError = '';
103
104        try {
105            $installer->startInstall();
106        } catch (Exception|AuthenticationException $exception) {
107            $installationError = $exception->getMessage();
108        }
109
110        return $this->render('@setup/install.twig', [
111            'newVersion' => System::getVersion(),
112            'setupType' => 'Setup',
113            'currentYear' => date(format: 'Y'),
114            'documentationUrl' => System::getDocumentationUrl(),
115            'installationError' => $installationError,
116        ]);
117    }
118
119    /**
120     * @throws TemplateException
121     * @throws Exception
122     * @throws \Exception
123     */
124    #[Route(path: '/update', name: 'public.update.index', methods: ['GET'])]
125    #[Route(path: '/update/', name: 'public.update.index.slash', methods: ['GET'])]
126    public function update(Request $request): Response
127    {
128        $currentStep = Filter::filterVar($request->query->get('step') ?? 1, FILTER_VALIDATE_INT);
129        if ($currentStep === null || $currentStep < 1 || $currentStep > 3) {
130            $currentStep = 1;
131        }
132
133        $configuration = Configuration::getConfigurationInstance();
134
135        $update = new Update(new System(), $configuration);
136
137        $checkBasicError = '';
138        try {
139            $update->checkInitialRewriteBasePath($request);
140        } catch (Exception $exception) {
141            $checkBasicError = $exception->getMessage();
142        }
143
144        $updateTokenError = '';
145        $updateTokenRequired = !$this->hasAdministratorSession($configuration);
146        if ($updateTokenRequired) {
147            try {
148                // The token itself is never rendered, it has to be read from the file system
149                new UpdateToken((string) PMF_CONFIG_DIR)->getOrCreate();
150            } catch (Exception $exception) {
151                $updateTokenError = $exception->getMessage();
152            }
153        }
154
155        return $this->render('@setup/update.twig', [
156            'currentStep' => $currentStep,
157            'installedVersion' => $configuration->getVersion(),
158            'newVersion' => System::getVersion(),
159            'checkBasicError' => $checkBasicError,
160            'currentYear' => date(format: 'Y'),
161            'documentationUrl' => System::getDocumentationUrl(),
162            'configTableNotAvailable' => $update->isConfigTableNotAvailable($configuration->getDb()),
163            'updateTokenRequired' => $updateTokenRequired,
164            'updateTokenError' => $updateTokenError,
165            'updateTokenFile' => $this->getRelativeTokenFilePath(),
166            'updateTokenLifetime' => (int) (UpdateToken::TOKEN_LIFETIME / 60),
167        ]);
168    }
169
170    /**
171     * Returns true if the update is started by a logged-in administrator. This is only
172     * possible as long as the database still matches what the new code expects, so the
173     * update wizard falls back to the update token whenever this returns false.
174     */
175    private function hasAdministratorSession(Configuration $configuration): bool
176    {
177        try {
178            $currentUser = CurrentUser::getCurrentUser($configuration);
179
180            if (!$currentUser->isLoggedIn()) {
181                return false;
182            }
183
184            return $currentUser->isSuperAdmin()
185            || $currentUser->perm->hasPermission($currentUser->getUserId(), PermissionType::CONFIGURATION_EDIT->value);
186        } catch (\Throwable) {
187            return false;
188        }
189    }
190
191    /**
192     * Returns the path of the token file relative to the phpMyFAQ root directory,
193     * so that we can tell the administrator where to find it.
194     */
195    private function getRelativeTokenFilePath(): string
196    {
197        $tokenFilePath = new UpdateToken((string) PMF_CONFIG_DIR)->getTokenFilePath();
198        $rootDir = (string) PMF_ROOT_DIR;
199
200        if (str_starts_with($tokenFilePath, $rootDir . DIRECTORY_SEPARATOR)) {
201            return substr($tokenFilePath, strlen($rootDir) + 1);
202        }
203
204        return $tokenFilePath;
205    }
206
207    /**
208     * Returns a Twig-rendered template as a response.
209     *
210     * @param array<string, mixed> $templateVars
211     * @throws Exception|LoaderError
212     */
213    public function render(string $pathToTwigFile, array $templateVars = [], ?Response $response = null): Response
214    {
215        $response ??= new Response();
216        $twigWrapper = new TwigWrapper((string) PMF_ROOT_DIR . '/assets/templates', true);
217        $templateWrapper = $twigWrapper->loadTemplate($pathToTwigFile);
218
219        $response->setContent($templateWrapper->render($templateVars));
220
221        return $response;
222    }
223}