Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.27% covered (success)
86.27%
88 / 102
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
InstanceController
86.27% covered (success)
86.27%
88 / 102
66.67% covered (warning)
66.67%
2 / 3
32.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
83.33% covered (success)
83.33%
70 / 84
0.00% covered (danger)
0.00%
0 / 1
23.04
 delete
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3/**
4 * The Admin Instance Controller
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2023-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2023-10-28
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Administration\Api;
21
22use phpMyFAQ\Configuration\DatabaseConfiguration;
23use phpMyFAQ\Controller\AbstractController;
24use phpMyFAQ\Core\Exception;
25use phpMyFAQ\Database;
26use phpMyFAQ\Entity\InstanceEntity;
27use phpMyFAQ\Enums\PermissionType;
28use phpMyFAQ\Filesystem\Filesystem;
29use phpMyFAQ\Filter;
30use phpMyFAQ\Instance;
31use phpMyFAQ\Instance\Client;
32use phpMyFAQ\Instance\Setup;
33use phpMyFAQ\Session\Token;
34use phpMyFAQ\Translation;
35use phpMyFAQ\User;
36use Symfony\Component\HttpFoundation\JsonResponse;
37use Symfony\Component\HttpFoundation\Request;
38use Symfony\Component\HttpFoundation\Response;
39use Symfony\Component\Routing\Attribute\Route;
40
41final class InstanceController extends AbstractController
42{
43    public function __construct(
44        private readonly Instance $instance,
45    ) {
46        parent::__construct();
47    }
48
49    /**
50     * @throws Exception
51     * @throws \Exception
52     */
53    #[Route(path: 'instance/add', name: 'admin.api.instance.add', methods: ['POST'])]
54    public function add(Request $request): JsonResponse
55    {
56        $this->userHasPermission(PermissionType::INSTANCE_ADD);
57
58        $data = $this->getJsonObject($request);
59
60        if (!Token::getInstance($this->session)->verifyToken('add-instance', (string) ($data->csrf ?? ''))) {
61            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
62        }
63
64        $url = Filter::filterVar($data->url ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
65        $instance = Filter::filterVar($data->instance ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
66        $comment = Filter::filterVar($data->comment ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
67        $email = Filter::filterEmail($data->email ?? null);
68        $admin = Filter::filterVar($data->admin ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
69        $password = Filter::filterVar($data->password ?? null, FILTER_SANITIZE_SPECIAL_CHARS);
70
71        if (
72            $url === ''
73            || $url === null
74            || $instance === ''
75            || $instance === null
76            || $comment === ''
77            || $comment === null
78            || $email === ''
79            || $email === null
80            || $email === false
81            || $admin === ''
82            || $admin === null
83            || $password === ''
84            || $password === null
85        ) {
86            return $this->json(['error' => 'Cannot create instance.'], Response::HTTP_BAD_REQUEST);
87        }
88
89        $url = (string) $url;
90        $instance = (string) $instance;
91        $comment = (string) $comment;
92        $email = (string) $email;
93        $admin = (string) $admin;
94        $password = (string) $password;
95
96        $url = 'https://' . $url . '.' . $request->getHost();
97        if (!Filter::filterVar($url, FILTER_VALIDATE_URL)) {
98            return $this->json(['error' => 'Cannot create instance: wrong URL'], Response::HTTP_BAD_REQUEST);
99        }
100
101        $faqInstanceClient = new Client($this->configuration);
102        $faqInstanceClient->setFileSystem(new Filesystem());
103        if (!$faqInstanceClient->isValidClientUrl($url)) {
104            return $this->json(['error' => 'Cannot create instance: wrong URL'], Response::HTTP_BAD_REQUEST);
105        }
106
107        $data = new InstanceEntity();
108        $data->setUrl($url)->setInstance($instance)->setComment($comment);
109
110        $instanceId = $this->instance->create($data);
111
112        $faqInstanceClient = new Client($this->configuration);
113        $faqInstanceClient->createClient($this->instance);
114        $faqInstanceClient->setFileSystem(new Filesystem((string) PMF_ROOT_DIR));
115
116        $urlParts = parse_url($data->getUrl());
117        $hostname = $urlParts['host'] ?? '';
118
119        if ($faqInstanceClient->createClientFolder($hostname)) {
120            $clientDir = (string) PMF_ROOT_DIR . '/multisite/' . $hostname;
121            $clientSetup = new Setup();
122            $clientSetup->setRootDir($clientDir);
123            $databaseConfiguration = new DatabaseConfiguration((string) PMF_CONFIG_DIR . '/database.php');
124            $dbSetup = [
125                'dbServer' => $databaseConfiguration->getServer(),
126                'dbPort' => $databaseConfiguration->getPort(),
127                'dbUser' => $databaseConfiguration->getUser(),
128                'dbPassword' => $databaseConfiguration->getPassword(),
129                'dbDatabaseName' => $databaseConfiguration->getDatabase(),
130                'dbPrefix' => substr($hostname, offset: 0, length: (int) strpos($hostname, needle: '.')),
131                'dbType' => $databaseConfiguration->getType(),
132            ];
133
134            try {
135                $faqInstanceClient->copyConstantsFile($clientDir . '/constants.php');
136                $clientSetup->createDatabaseFile($dbSetup, '');
137
138                $faqInstanceClient->setClientUrl('https://' . $hostname);
139                $faqInstanceClient->createClientTables($dbSetup['dbPrefix']);
140
141                Database::setTablePrefix($dbSetup['dbPrefix']);
142
143                // add an admin account and rights
144                $user = new User($this->configuration);
145                $user->createUser($admin, $password, '', 1);
146                $user->setStatus('protected');
147                $instanceAdminData = [
148                    'display_name' => '',
149                    'email' => $email,
150                ];
151                $user->setUserData($instanceAdminData);
152
153                // Add an anonymous user account
154                $clientSetup->createAnonymousUser($this->configuration);
155            } catch (\Throwable $e) {
156                $this->instance->delete($instanceId);
157                $faqInstanceClient->deleteClientFolder('https://' . $hostname);
158
159                return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
160            } finally {
161                Database::setTablePrefix($databaseConfiguration->getPrefix());
162            }
163        }
164
165        if (!$faqInstanceClient->createClientFolder($hostname)) {
166            $this->instance->delete($instanceId);
167            return $this->json(['error' => 'Cannot create instance.'], Response::HTTP_BAD_REQUEST);
168        }
169
170        if (0 !== $instanceId) {
171            return $this->json(['added' => $instanceId, 'url' => $data->getUrl()], Response::HTTP_OK);
172        }
173
174        return $this->json(['error' => $instanceId], Response::HTTP_BAD_REQUEST);
175    }
176
177    /**
178     * @throws \Exception
179     */
180    #[Route(path: 'instance/delete', name: 'admin.api.instance.delete', methods: ['DELETE'])]
181    public function delete(Request $request): JsonResponse
182    {
183        $this->userHasPermission(PermissionType::INSTANCE_DELETE);
184
185        $data = $this->getJsonObject($request);
186
187        if (!Token::getInstance($this->session)->verifyToken('delete-instance', (string) ($data->csrf ?? ''))) {
188            return $this->json(['error' => Translation::get(key: 'msgNoPermission')], Response::HTTP_UNAUTHORIZED);
189        }
190
191        $instanceId = Filter::filterVar($data->instanceId ?? null, FILTER_VALIDATE_INT);
192
193        if ($instanceId !== false && null !== $instanceId) {
194            $client = new Client($this->configuration);
195            $client->setFileSystem(new Filesystem());
196            $clientData = $client->getById($instanceId);
197            if (!$client->isValidClientUrl((string) $clientData->url)) {
198                return $this->json(['error' => $instanceId], Response::HTTP_BAD_REQUEST);
199            }
200
201            if (
202                1 !== $instanceId
203                && $client->deleteClientFolder((string) $clientData->url)
204                && $client->delete($instanceId)
205            ) {
206                return $this->json(['deleted' => $instanceId], Response::HTTP_OK);
207            }
208
209            return $this->json(['error' => $instanceId], Response::HTTP_BAD_REQUEST);
210        }
211
212        return $this->json(['error' => $instanceId], Response::HTTP_BAD_REQUEST);
213    }
214}