ApiController.php 14 KB

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