259 lines
10 KiB
PHP
259 lines
10 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace MageSail\Magesail\Model;
|
|
|
|
use Magento\Framework\App\Cache\TypeListInterface;
|
|
use Magento\Framework\App\Config\ReinitableConfigInterface;
|
|
use Magento\Framework\App\Config\ScopeConfigInterface;
|
|
use Magento\Framework\App\Config\Storage\WriterInterface;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\Framework\Exception\NoSuchEntityException;
|
|
use Magento\Store\Api\StoreRepositoryInterface;
|
|
use Magento\Store\Model\ResourceModel\Store as StoreResource;
|
|
use Magento\Store\Model\ScopeInterface;
|
|
use Magento\Store\Model\StoreFactory;
|
|
use Magento\Store\Model\StoreManagerInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use MageSail\Magesail\Model\NginxGlobalMapManager;
|
|
|
|
/**
|
|
* Creates a dedicated store view with tunnel base URLs; tears it down when the tunnel is stopped.
|
|
*/
|
|
class TunnelStoreProvisioner
|
|
{
|
|
public const STORE_CODE = 'magesail_tunnel';
|
|
|
|
private const PATH_UNSECURE_BASE = 'web/unsecure/base_url';
|
|
private const PATH_SECURE_BASE = 'web/secure/base_url';
|
|
private const PATH_USE_IN_FRONTEND = 'web/secure/use_in_frontend';
|
|
private const PATH_USE_IN_ADMIN = 'web/secure/use_in_adminhtml';
|
|
|
|
public function __construct(
|
|
private readonly StoreManagerInterface $storeManager,
|
|
private readonly StoreFactory $storeFactory,
|
|
private readonly StoreResource $storeResource,
|
|
private readonly StoreRepositoryInterface $storeRepository,
|
|
private readonly WriterInterface $configWriter,
|
|
private readonly TypeListInterface $typeList,
|
|
private readonly ReinitableConfigInterface $reinitableConfig,
|
|
private readonly TunnelStoreState $tunnelStoreState,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly NginxGlobalMapManager $nginxMapManager,
|
|
private readonly ScopeConfigInterface $scopeConfig
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Remove store + state file after stale PID cleanup when auto-restart is disabled.
|
|
*/
|
|
public function teardownAfterStaleProcess(): void
|
|
{
|
|
$this->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();
|
|
}
|
|
}
|