ApiController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. * @param ServerRequest $request
  238. * @return JsonResponse
  239. * @throws \Exception
  240. */
  241. public function deleteSiteAction(ServerRequest $request): JsonResponse
  242. {
  243. $this->assertIpAllowed();
  244. $organizationId = $this->getOrganizationId($request);
  245. $params = $request->getQueryParams();
  246. $hard = (isset($params['hard']) && $params['hard']);
  247. if ($hard) {
  248. $this->preventIfIsDubious();
  249. $this->preventOnMissingConfirmationToken($organizationId);
  250. }
  251. $rootUid = $this->siteController->deleteSiteAction($organizationId, $hard, true, true);
  252. $this->logger->info(sprintf(
  253. "OtAdmin API: The website with root uid " . $rootUid . " has been soft-deleted " .
  254. " (organization: " . $organizationId . ")"));
  255. $msg = $hard ?
  256. "The website with root uid " . $rootUid . " has been soft-deleted. Use the /site/undelete route to restore it." :
  257. "The website with root uid " . $rootUid . " has been hard-deleted.";
  258. return new JsonResponse(
  259. [
  260. 'organization_id' => $organizationId,
  261. 'msg' => $msg,
  262. 'root_uid' => $rootUid
  263. ]
  264. );
  265. }
  266. /**
  267. * -- Target of the route 'site_undelete' --
  268. * >> Requires a query param named 'organization-id' (int)
  269. *
  270. * Restore a soft-deleted organization's website
  271. *
  272. * @param ServerRequest $request
  273. * @return JsonResponse
  274. * @throws \Exception
  275. */
  276. public function undeleteSiteAction(ServerRequest $request): JsonResponse
  277. {
  278. $this->assertIpAllowed();
  279. $organizationId = $this->getOrganizationId($request);
  280. $rootUid = $this->siteController->undeleteSiteAction($organizationId);
  281. $this->logger->info(sprintf(
  282. "OtAdmin API: The website with root uid " . $rootUid . " has been restored " .
  283. " (organization: " . $organizationId . ")"));
  284. return new JsonResponse(
  285. [
  286. 'organization_id' => $organizationId,
  287. 'msg' => "The website with root uid " . $rootUid . " has been restored",
  288. 'root_uid' => $rootUid
  289. ]
  290. );
  291. }
  292. /**
  293. * -- Target of the route 'site_clearcache' --
  294. * >> Requires a query param named 'organization-id' (int)
  295. *
  296. * Clear the cache of the organization's website
  297. *
  298. * @param ServerRequest $request
  299. * @return JsonResponse
  300. * @throws \Exception
  301. */
  302. public function clearSiteCacheAction(ServerRequest $request): JsonResponse
  303. {
  304. $this->assertIpAllowed();
  305. $organizationId = $this->getOrganizationId($request);
  306. $queryParams = $request->getQueryParams();
  307. $clearAll = (isset($queryParams['all']) && $queryParams['all']);;
  308. $rootUid = $this->siteController->clearSiteCacheAction($organizationId, $clearAll);
  309. return new JsonResponse(
  310. [
  311. 'organization_id' => $organizationId,
  312. 'msg' => "The cache has been cleared for the website with root uid " . $rootUid . "",
  313. 'root_uid' => $rootUid
  314. ]
  315. );
  316. }
  317. /**
  318. * -- Target of the route 'site_setdomain' --
  319. * >> Requires a query param named 'organization-id' (int)
  320. * and a parameter named 'domain' (string)
  321. *
  322. * Set a new domain for the organization website
  323. *
  324. * @param ServerRequest $request
  325. * @return JsonResponse
  326. * @throws \Exception
  327. */
  328. public function setSiteCustomDomainAction(ServerRequest $request): JsonResponse
  329. {
  330. $this->assertIpAllowed();
  331. $organizationId = $this->getOrganizationId($request);
  332. $queryParams = $request->getQueryParams();
  333. $domain = $queryParams['domain'];
  334. if (!$domain) {
  335. throw new \RuntimeException("Missing 'domain' parameter");
  336. }
  337. $redirect = (isset($queryParams['redirect']) && $queryParams['redirect']);
  338. $rootUid = $this->siteController->setSiteCustomDomainAction($organizationId, $domain, $redirect);
  339. return new JsonResponse(
  340. [
  341. 'organization_id' => $organizationId,
  342. 'msg' => "The cache has been cleared for the website with root uid " . $rootUid . "",
  343. 'root_uid' => $rootUid
  344. ]
  345. );
  346. }
  347. /**
  348. * -- Target of the route 'site_resetperms' --
  349. * >> Requires a query param named 'organization-id' (int)
  350. *
  351. * Reset the permissions of the website be users (admin, editors...)
  352. *
  353. * @param ServerRequest $request
  354. * @return JsonResponse
  355. * @throws \Exception
  356. */
  357. public function resetBeUserPermsAction(ServerRequest $request): JsonResponse
  358. {
  359. $this->assertIpAllowed();
  360. $organizationId = $this->getOrganizationId($request);
  361. $rootUid = $this->siteController->resetBeUserPermsAction($organizationId);
  362. return new JsonResponse(
  363. [
  364. 'organization_id' => $organizationId,
  365. 'msg' => "The website with root uid " . $rootUid . " had its be users permissions reset",
  366. 'root_uid' => $rootUid
  367. ]
  368. );
  369. }
  370. /**
  371. * -- Target of the route 'site_status' --
  372. * >> Requires a query param named 'organization-id' (int)
  373. *
  374. * Returns the current status of the website
  375. *
  376. * @param ServerRequest $request
  377. * @param SiteController $siteController
  378. * @return JsonResponse
  379. * @throws Exception
  380. * @throws InvalidWebsiteConfigurationException
  381. * @throws NoSuchOrganizationException
  382. * @throws NoSuchRecordException
  383. * @throws NoSuchWebsiteException
  384. */
  385. public function getSiteStatusAction(
  386. ServerRequest $request
  387. ): JsonResponse
  388. {
  389. $this->assertIpAllowed();
  390. $organizationId = $this->getOrganizationId($request);
  391. $queryParams = $request->getQueryParams();
  392. $full = (isset($queryParams['full']) && $queryParams['full']);
  393. $status = $this->siteController->getSiteStatusAction($organizationId, $full);
  394. return new JsonResponse($status->toArray());
  395. }
  396. /**
  397. * -- Target of the route 'scan' --
  398. *
  399. * Scan the whole Typo3 database and return the results
  400. *
  401. * @param ServerRequest $request
  402. * @return JsonResponse
  403. * @throws \Exception
  404. */
  405. public function scanAllAction(ServerRequest $request): JsonResponse
  406. {
  407. $this->assertIpAllowed();
  408. $queryParams = $request->getQueryParams();
  409. $full = (isset($queryParams['full']) && $queryParams['full']);
  410. $results = $this->siteController->scanAllAction($full);
  411. return new JsonResponse($results);
  412. }
  413. }