ApiController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <?php
  2. declare(strict_types=1);
  3. namespace Opentalent\OtAdmin\Http;
  4. use Doctrine\DBAL\Driver\Exception;
  5. use Opentalent\OtAdmin\Controller\SiteController;
  6. use Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException;
  7. use Opentalent\OtCore\Exception\NoSuchOrganizationException;
  8. use Opentalent\OtCore\Exception\NoSuchRecordException;
  9. use Opentalent\OtCore\Exception\NoSuchWebsiteException;
  10. use Psr\Log\LoggerAwareInterface;
  11. use Psr\Log\LoggerAwareTrait;
  12. use Psr\Log\LoggerInterface;
  13. use TYPO3\CMS\Core\Http\JsonResponse;
  14. use TYPO3\CMS\Core\Http\ServerRequest;
  15. use TYPO3\CMS\Core\Utility\GeneralUtility;
  16. /**
  17. * Actions for Http API calls
  18. *
  19. * @package Opentalent\OtAdmin\Http
  20. */
  21. class ApiController implements LoggerAwareInterface
  22. {
  23. use LoggerAwareTrait;
  24. const PROD_FRONT_IP = "172.16.0.68";
  25. const PROD_V2_IP = "172.16.0.35";
  26. const PUBLIC_PRODFRONT_IP = "141.94.117.38";
  27. const PUBLIC_PROD_V2_IP = "141.94.117.35";
  28. const array ALLOWED_IPS = [
  29. '/^127\.0\.0\.[0-1]$/', // Localhost
  30. '/^localhost$/', // Localhost
  31. '/^10\.8\.0\.\d{1,3}$/', // 10.8.0.[0-255] - VPN
  32. '/^141\.94\.117\.((3[3-9])|(4\d)|(5\d)|(6[0-1]))$/', // 141.94.117.[33-61] - Opentalent hosts public ips
  33. '/^172\.16\.0.\d{1,3}$/', // 172.16.0.[0-255] - Opentalent hosts private ips
  34. '/^172\.20\.\d{1,3}\.\d{1,3}$/', // 172.20.[0-255].[0-255] - Docker
  35. ];
  36. private readonly SiteController $siteController;
  37. public function __construct() {
  38. $this->siteController = GeneralUtility::makeInstance(SiteController::class);
  39. }
  40. /**
  41. * Returns true if the client Ip is allowed
  42. *
  43. * @param string $clientIp
  44. * @return bool
  45. */
  46. public static function isIpAllowed(string $clientIp): bool
  47. {
  48. foreach (self::ALLOWED_IPS as $ipRule) {
  49. if (preg_match($ipRule, $clientIp)) {
  50. return true;
  51. }
  52. }
  53. return false;
  54. }
  55. /**
  56. * Check that the client Ip is allowed, else throw a Runtime error
  57. *
  58. * @return bool
  59. */
  60. private function assertIpAllowed(): bool
  61. {
  62. $clientIp = $_SERVER['REMOTE_ADDR'];
  63. if (!self::isIpAllowed($clientIp)){
  64. $route = $_REQUEST['route'];
  65. $this->logger->error(sprintf(
  66. "OtAdmin API: an attempt was made to call the route " .
  67. $route . " from an non-allowed IP (" . $clientIp . ")"));
  68. throw new \RuntimeException("Not allowed");
  69. }
  70. return true;
  71. }
  72. /**
  73. * Lève une erreur si l'environnement est la prod et que la requête provient d'un autre environnement, car
  74. * cette requête a probablement été envoyée à la prod par erreur.
  75. *
  76. * Permet de sécuriser certaines opérations destructives, comme la suppression d'organisation.
  77. *
  78. * @return void
  79. */
  80. private function preventIfIsDubious(): void
  81. {
  82. if (
  83. $_SERVER &&
  84. (
  85. ($_SERVER['SERVER_ADDR'] === self::PROD_FRONT_IP && $_SERVER['REMOTE_ADDR'] !== self::PROD_V2_IP) ||
  86. ($_SERVER['SERVER_ADDR'] === self::PUBLIC_PRODFRONT_IP && $_SERVER['REMOTE_ADDR'] !== self::PUBLIC_PROD_V2_IP)
  87. )
  88. ) {
  89. throw new \RuntimeException("Invalid client ip");
  90. }
  91. }
  92. /**
  93. * Lève une erreur si le token de confirmation n'a pas était ajouté, ou si sa valeur est invalide.
  94. *
  95. * Permet de sécuriser certaines opérations destructives, comme la suppression d'organisation.
  96. *
  97. * @param int $organizationId
  98. * @return void
  99. */
  100. private function preventOnMissingConfirmationToken(int $organizationId): void
  101. {
  102. $headers = getallheaders();
  103. if (
  104. !isset($headers['Confirmation-Token']) ||
  105. $headers['Confirmation-Token'] !== 'DEL-'.$organizationId.'-'.date('Ymd')
  106. ) {
  107. throw new \RuntimeException("Missing or invalid confirmation token");
  108. }
  109. }
  110. /**
  111. * Retrieve the organization's id from the given request parameters
  112. *
  113. * @param ServerRequest $request
  114. * @return int
  115. */
  116. private function getOrganizationId(ServerRequest $request): int
  117. {
  118. $params = $request->getQueryParams();
  119. $organizationId = $params['organization-id'];
  120. if (!$organizationId) {
  121. throw new \RuntimeException("Missing parameter: 'organization-id'");
  122. }
  123. return (int)$organizationId;
  124. }
  125. /**
  126. * -- Target of the route 'site_infos' --
  127. *
  128. * Return the main information about the organization's website
  129. *
  130. * @param ServerRequest $request
  131. * @return JsonResponse
  132. * @throws \Exception
  133. */
  134. public function getSiteInfosAction(
  135. ServerRequest $request,
  136. SiteController $siteController
  137. ): JsonResponse
  138. {
  139. $this->assertIpAllowed();
  140. $organizationId = $this->getOrganizationId($request);
  141. $infos = $siteController->getSiteInfosAction($organizationId);
  142. return new JsonResponse($infos);
  143. }
  144. /**
  145. * -- Target of the route 'site_create' --
  146. * >> Requires a query param named 'organization-id' (int)
  147. *
  148. * Create the organization's website
  149. *
  150. * @param ServerRequest $request
  151. * @return JsonResponse
  152. * @throws \Exception
  153. */
  154. public function createSiteAction(ServerRequest $request): JsonResponse
  155. {
  156. $this->assertIpAllowed();
  157. $organizationId = $this->getOrganizationId($request);
  158. $rootUid = $this->siteController->createSiteAction($organizationId);
  159. $this->logger->info(sprintf(
  160. "OtAdmin API: A new website has been created with root page uid=" . $rootUid .
  161. " for the organization " . $organizationId));
  162. return new JsonResponse(
  163. [
  164. 'organization_id' => $organizationId,
  165. 'msg' => "A new website has been created with root page uid=" . $rootUid,
  166. 'root_uid' => $rootUid
  167. ]
  168. );
  169. }
  170. /**
  171. * -- Target of the route 'site_update' --
  172. * >> Requires a query param named 'organization-id' (int)
  173. *
  174. * Update the settings of the organization's website
  175. *
  176. * @param ServerRequest $request
  177. * @return JsonResponse
  178. * @throws \Exception
  179. */
  180. public function updateSiteConstantsAction(ServerRequest $request): JsonResponse
  181. {
  182. $this->assertIpAllowed();
  183. $organizationId = $this->getOrganizationId($request);
  184. $deep = (isset($queryParams['deep']) && $queryParams['deep']);
  185. $rootUid = $this->siteController->updateSiteAction($organizationId, $deep);
  186. $this->logger->info(sprintf(
  187. "OtAdmin API: The website with root uid " . $rootUid . " has been updated " .
  188. " (organization: " . $organizationId . ")"));
  189. return new JsonResponse(
  190. [
  191. 'organization_id' => $organizationId,
  192. 'msg' => "The website with root uid " . $rootUid . " has been updated",
  193. 'root_uid' => $rootUid
  194. ]
  195. );
  196. }
  197. /**
  198. * -- Target of the route 'redirect_add' --
  199. * >> Requires query params named 'from-domain' (string) and 'to-domain' (string)
  200. *
  201. * Add or update a redirection from 'from-domain' to 'to-domain'
  202. *
  203. * @param ServerRequest $request
  204. * @return JsonResponse
  205. * @throws \Exception
  206. */
  207. public function addRedirectionAction(ServerRequest $request): JsonResponse
  208. {
  209. $this->assertIpAllowed();
  210. $fromDomain = (isset($queryParams['from-domain']) && $queryParams['from-domain']);
  211. $toDomain = (isset($queryParams['to-domain']) && $queryParams['to-domain']);
  212. $res = $this->siteController->addRedirection($fromDomain, $toDomain);
  213. if ($res === SiteController::REDIRECTION_UPDATED) {
  214. $msg = "An existing redirection has been updated ";
  215. } elseif ($res === SiteController::REDIRECTION_CREATED) {
  216. $msg = "A redirection has been added ";
  217. }
  218. $this->logger->info(sprintf(
  219. "OtAdmin API: " . $msg . " from " . $fromDomain . " to " . $toDomain
  220. ));
  221. return new JsonResponse(
  222. [
  223. 'msg' => $msg . " from " . $fromDomain . " to " . $toDomain,
  224. ]
  225. );
  226. }
  227. /**
  228. * -- Target of the route 'site_delete' --
  229. * >> Requires a query param named 'organization-id' (int)
  230. *
  231. * Proceeds to a soft-deletion of the organization's website
  232. *
  233. * In the case of a hard deletion, a special header is requested as a confirmation token. The header
  234. * shall be named 'Confirmation-Token' and its value shall be DEL-XXXX-YYYYMMDD, where XXXX is the id of
  235. * the organization owning the website, and YYYYMMDD is the date of the current day.
  236. *
  237. * /!\ Warning: this is a destructive operation
  238. *
  239. * @param ServerRequest $request
  240. * @return JsonResponse
  241. * @throws \Exception
  242. */
  243. public function deleteSiteAction(ServerRequest $request): JsonResponse
  244. {
  245. $this->assertIpAllowed();
  246. $organizationId = $this->getOrganizationId($request);
  247. $params = $request->getQueryParams();
  248. $hard = (isset($params['hard']) && $params['hard']);
  249. if ($hard) {
  250. $this->preventIfIsDubious();
  251. $this->preventOnMissingConfirmationToken($organizationId);
  252. }
  253. $rootUid = $this->siteController->deleteSiteAction($organizationId, $hard, true, true);
  254. $this->logger->info(sprintf(
  255. "OtAdmin API: The website with root uid " . $rootUid . " has been soft-deleted " .
  256. " (organization: " . $organizationId . ")"));
  257. $msg = $hard ?
  258. "The website with root uid " . $rootUid . " has been hard-deleted." :
  259. "The website with root uid " . $rootUid . " has been soft-deleted. Use the /site/undelete route to restore it.";
  260. return new JsonResponse(
  261. [
  262. 'organization_id' => $organizationId,
  263. 'msg' => $msg,
  264. 'root_uid' => $rootUid
  265. ]
  266. );
  267. }
  268. /**
  269. * -- Target of the route 'site_undelete' --
  270. * >> Requires a query param named 'organization-id' (int)
  271. *
  272. * Restore a soft-deleted organization's website
  273. *
  274. * @param ServerRequest $request
  275. * @return JsonResponse
  276. * @throws \Exception
  277. */
  278. public function undeleteSiteAction(ServerRequest $request): JsonResponse
  279. {
  280. $this->assertIpAllowed();
  281. $organizationId = $this->getOrganizationId($request);
  282. $rootUid = $this->siteController->undeleteSiteAction($organizationId);
  283. $this->logger->info(sprintf(
  284. "OtAdmin API: The website with root uid " . $rootUid . " has been restored " .
  285. " (organization: " . $organizationId . ")"));
  286. return new JsonResponse(
  287. [
  288. 'organization_id' => $organizationId,
  289. 'msg' => "The website with root uid " . $rootUid . " has been restored",
  290. 'root_uid' => $rootUid
  291. ]
  292. );
  293. }
  294. /**
  295. * -- Target of the route 'site_clearcache' --
  296. * >> Requires a query param named 'organization-id' (int)
  297. *
  298. * Clear the cache of the organization's website
  299. *
  300. * @param ServerRequest $request
  301. * @return JsonResponse
  302. * @throws \Exception
  303. */
  304. public function clearSiteCacheAction(ServerRequest $request): JsonResponse
  305. {
  306. $this->assertIpAllowed();
  307. $organizationId = $this->getOrganizationId($request);
  308. $queryParams = $request->getQueryParams();
  309. $clearAll = (isset($queryParams['all']) && $queryParams['all']);;
  310. $rootUid = $this->siteController->clearSiteCacheAction($organizationId, $clearAll);
  311. return new JsonResponse(
  312. [
  313. 'organization_id' => $organizationId,
  314. 'msg' => "The cache has been cleared for the website with root uid " . $rootUid . "",
  315. 'root_uid' => $rootUid
  316. ]
  317. );
  318. }
  319. /**
  320. * -- Target of the route 'site_setdomain' --
  321. * >> Requires a query param named 'organization-id' (int)
  322. * and a parameter named 'domain' (string)
  323. *
  324. * Set a new domain for the organization website
  325. *
  326. * @param ServerRequest $request
  327. * @return JsonResponse
  328. * @throws \Exception
  329. */
  330. public function setSiteCustomDomainAction(ServerRequest $request): JsonResponse
  331. {
  332. $this->assertIpAllowed();
  333. $organizationId = $this->getOrganizationId($request);
  334. $queryParams = $request->getQueryParams();
  335. $domain = $queryParams['domain'];
  336. if (!$domain) {
  337. throw new \RuntimeException("Missing 'domain' parameter");
  338. }
  339. $redirect = (isset($queryParams['redirect']) && $queryParams['redirect']);
  340. $rootUid = $this->siteController->setSiteCustomDomainAction($organizationId, $domain, $redirect);
  341. return new JsonResponse(
  342. [
  343. 'organization_id' => $organizationId,
  344. 'msg' => "The cache has been cleared for the website with root uid " . $rootUid . "",
  345. 'root_uid' => $rootUid
  346. ]
  347. );
  348. }
  349. /**
  350. * -- Target of the route 'site_resetperms' --
  351. * >> Requires a query param named 'organization-id' (int)
  352. *
  353. * Reset the permissions of the website be users (admin, editors...)
  354. *
  355. * @param ServerRequest $request
  356. * @return JsonResponse
  357. * @throws \Exception
  358. */
  359. public function resetBeUserPermsAction(ServerRequest $request): JsonResponse
  360. {
  361. $this->assertIpAllowed();
  362. $organizationId = $this->getOrganizationId($request);
  363. $rootUid = $this->siteController->resetBeUserPermsAction($organizationId);
  364. return new JsonResponse(
  365. [
  366. 'organization_id' => $organizationId,
  367. 'msg' => "The website with root uid " . $rootUid . " had its be users permissions reset",
  368. 'root_uid' => $rootUid
  369. ]
  370. );
  371. }
  372. /**
  373. * -- Target of the route 'site_status' --
  374. * >> Requires a query param named 'organization-id' (int)
  375. *
  376. * Returns the current status of the website
  377. *
  378. * @param ServerRequest $request
  379. * @param SiteController $siteController
  380. * @return JsonResponse
  381. * @throws Exception
  382. * @throws InvalidWebsiteConfigurationException
  383. * @throws NoSuchOrganizationException
  384. * @throws NoSuchRecordException
  385. * @throws NoSuchWebsiteException
  386. */
  387. public function getSiteStatusAction(
  388. ServerRequest $request
  389. ): JsonResponse
  390. {
  391. $this->assertIpAllowed();
  392. $organizationId = $this->getOrganizationId($request);
  393. $queryParams = $request->getQueryParams();
  394. $full = (isset($queryParams['full']) && $queryParams['full']);
  395. $status = $this->siteController->getSiteStatusAction($organizationId, $full);
  396. return new JsonResponse($status->toArray());
  397. }
  398. /**
  399. * -- Target of the route 'scan' --
  400. *
  401. * Scan the whole Typo3 database and return the results
  402. *
  403. * @param ServerRequest $request
  404. * @return JsonResponse
  405. * @throws \Exception
  406. */
  407. public function scanAllAction(ServerRequest $request): JsonResponse
  408. {
  409. $this->assertIpAllowed();
  410. $queryParams = $request->getQueryParams();
  411. $full = (isset($queryParams['full']) && $queryParams['full']);
  412. $results = $this->siteController->scanAllAction($full);
  413. return new JsonResponse($results);
  414. }
  415. /**
  416. * -- Target of the route 'delete-user-created-pages' --
  417. * >> Requires a query param named 'organization-id' (int)
  418. *
  419. * Delete all user-created pages for the organization's website
  420. *
  421. * /!\ Warning: this is a destructive operation
  422. *
  423. * @param ServerRequest $request
  424. * @return JsonResponse
  425. * @throws \Exception
  426. */
  427. public function deleteUserCreatedPagesAction(ServerRequest $request): JsonResponse
  428. {
  429. $this->assertIpAllowed();
  430. $organizationId = $this->getOrganizationId($request);
  431. $this->preventIfIsDubious();
  432. $this->preventOnMissingConfirmationToken($organizationId);
  433. $rootUid = $this->siteController->deleteUserCreatedPagesAction($organizationId);
  434. return new JsonResponse(
  435. [
  436. 'organization_id' => $organizationId,
  437. 'msg' => "The website with root uid " . $rootUid . " had its user-created pages deleted.",
  438. 'root_uid' => $rootUid
  439. ]
  440. );
  441. }
  442. }