teardownPersistedState(); } /** * Remove tunnel store and state file (idempotent). */ public function teardownPersistedState(): void { $state = $this->tunnelStoreState->read(); $this->tunnelStoreState->delete(); if ($state === null) { $this->flushConfigCache(); return; } $store = $this->storeFactory->create(); $this->storeResource->load($store, $state['store_id']); if (!$store->getId()) { $this->flushConfigCache(); return; } $websiteId = (int) $store->getWebsiteId(); try { $website = $this->storeManager->getWebsite($websiteId); } catch (LocalizedException $e) { $this->logger->warning('MageSail tunnel store teardown: ' . $e->getMessage()); $this->flushConfigCache(); return; } $defaultStoreId = (int) $website->getDefaultStoreId(); if ((int) $store->getId() === $defaultStoreId) { $this->logger->warning( 'MageSail: tunnel store is the website default; skipping delete. Change default store view in Admin.' ); $this->flushConfigCache(); return; } try { $hostname = $this->nginxMapManager->extractHostname($state['unsecure_base_url']); $this->nginxMapManager->removeTunnelMapping($hostname); $validation = $this->nginxMapManager->validateAndReload(); if (!$validation['valid']) { $this->logger->warning('NGINX config validation failed after tunnel map removal: ' . ($validation['error'] ?? 'unknown')); } elseif (!$validation['reloaded']) { $this->logger->warning('NGINX config valid but reload failed after map removal: ' . ($validation['error'] ?? 'unknown')); } } catch (LocalizedException $e) { $this->logger->warning('NGINX global map removal skipped: ' . $e->getMessage()); } try { $this->storeResource->delete($store); $this->storeRepository->clean(); $this->storeManager->reinitStores(); } catch (\Throwable $e) { $this->logger->error('MageSail tunnel store delete failed: ' . $e->getMessage()); } $this->flushConfigCache(); } /** * @return array{store_id: int, store_code: string, nginx_reloaded: bool} * @throws LocalizedException */ public function provision(string $unsecureBase, string $secureBase, bool $useSecureUrls): array { $unsecureBase = $this->normalizeBaseUrl($unsecureBase); $secureBase = $this->normalizeBaseUrl($secureBase !== '' ? $secureBase : $this->deriveSecureFromUnsecure($unsecureBase)); $this->assertValidUrl($unsecureBase); $this->assertValidUrl($secureBase); $default = $this->storeManager->getDefaultStoreView(); if ($default === null) { throw new LocalizedException(__('No default store view is configured.')); } $websiteId = (int) $default->getWebsiteId(); $groupId = (int) $default->getStoreGroupId(); $store = $this->loadOrCreateStore($websiteId, $groupId); $storeId = (int) $store->getId(); $this->configWriter->save(self::PATH_UNSECURE_BASE, $unsecureBase, ScopeInterface::SCOPE_STORES, $storeId); $this->configWriter->save(self::PATH_SECURE_BASE, $secureBase, ScopeInterface::SCOPE_STORES, $storeId); $useSecure = $useSecureUrls ? '1' : '0'; $this->configWriter->save(self::PATH_USE_IN_FRONTEND, $useSecure, ScopeInterface::SCOPE_STORES, $storeId); $this->configWriter->save(self::PATH_USE_IN_ADMIN, $useSecure, ScopeInterface::SCOPE_STORES, $storeId); $disableRedirect = (bool) $this->scopeConfig->getValue('magesail/nginx/disable_base_url_redirect'); if ($disableRedirect) { $this->configWriter->save('web/url/redirect_to_base', '0', ScopeInterface::SCOPE_STORES, $storeId); } $nginxReloaded = true; try { $hostname = $this->nginxMapManager->extractHostname($unsecureBase); $this->nginxMapManager->addTunnelMapping($hostname, self::STORE_CODE); $validation = $this->nginxMapManager->validateAndReload(); if (!$validation['valid']) { $msg = 'NGINX config validation failed after tunnel map update: ' . ($validation['error'] ?? 'unknown'); $this->logger->error($msg); throw new LocalizedException(__('Tunnel store was not finalized: NGINX map validation failed: %1', $validation['error'] ?? 'unknown')); } $nginxReloaded = !empty($validation['reloaded']); if (!$nginxReloaded) { $this->logger->warning( 'NGINX map updated for tunnel but reload failed (reload nginx manually): ' . ($validation['error'] ?? 'unknown') ); } } catch (LocalizedException $e) { $this->logger->error('NGINX global map update failed: ' . $e->getMessage()); throw $e; } $this->tunnelStoreState->write([ 'store_id' => $storeId, 'store_code' => self::STORE_CODE, 'unsecure_base_url' => $unsecureBase, 'secure_base_url' => $secureBase, 'use_secure_urls' => $useSecureUrls, 'nginx_pending_reload' => !$nginxReloaded, ]); $this->storeRepository->clean(); $this->storeManager->reinitStores(); $this->flushConfigCache(); return [ 'store_id' => $storeId, 'store_code' => self::STORE_CODE, 'nginx_reloaded' => $nginxReloaded, ]; } /** * @return array{store_id: int, store_code: string, unsecure_base_url: string, secure_base_url: string, use_secure_urls: bool, nginx_pending_reload?: bool}|null */ public function getPersistedState(): ?array { return $this->tunnelStoreState->read(); } /** * Call after nginx reload succeeds (e.g. Admin NGINX map "reload") to clear the tunnel "reload nginx" banner. */ public function clearNginxPendingReloadFlag(): void { $state = $this->tunnelStoreState->read(); if ($state === null || !($state['nginx_pending_reload'] ?? false)) { return; } $state['nginx_pending_reload'] = false; $this->tunnelStoreState->write($state); } private function loadOrCreateStore(int $websiteId, int $groupId): \Magento\Store\Api\Data\StoreInterface { try { $existing = $this->storeRepository->get(self::STORE_CODE); $store = $existing; $store->setName((string) __('MageSail Tunnel')); $store->setWebsiteId($websiteId); $store->setStoreGroupId($groupId); $store->setIsActive(true); $this->storeResource->save($store); return $store; } catch (NoSuchEntityException) { $store = $this->storeFactory->create(); $store->setCode(self::STORE_CODE); $store->setName((string) __('MageSail Tunnel')); $store->setWebsiteId($websiteId); $store->setStoreGroupId($groupId); $store->setIsActive(true); $this->storeResource->save($store); return $store; } } private function normalizeBaseUrl(string $url): string { return rtrim(trim($url), '/') . '/'; } private function deriveSecureFromUnsecure(string $unsecureBase): string { if (stripos($unsecureBase, 'http://') === 0) { return 'https://' . substr($unsecureBase, \strlen('http://')); } return $unsecureBase; } private function assertValidUrl(string $url): void { if (filter_var($url, FILTER_VALIDATE_URL) === false) { throw new LocalizedException(__('Invalid base URL: %1', $url)); } $scheme = parse_url($url, PHP_URL_SCHEME); if (!\in_array($scheme, ['http', 'https'], true)) { throw new LocalizedException(__('Base URL must use http or https: %1', $url)); } } private function flushConfigCache(): void { $this->typeList->cleanType('config'); $this->typeList->cleanType('full_page'); $this->reinitableConfig->reinit(); } }