directoryList->getPath(DirectoryList::VAR_DIR) . '/' . self::FILENAME; } /** * @return array{version: int, tunnels: list>} */ public function loadData(): array { $path = $this->getFilePath(); if (!\is_readable($path)) { $this->migrateFromLegacyIfNeeded(); } if (!\is_readable($path)) { return ['version' => self::CURRENT_VERSION, 'tunnels' => []]; } $raw = file_get_contents($path); if ($raw === false || trim($raw) === '') { return ['version' => self::CURRENT_VERSION, 'tunnels' => []]; } try { /** @var array $data */ $data = $this->json->unserialize($raw); } catch (\InvalidArgumentException) { return ['version' => self::CURRENT_VERSION, 'tunnels' => []]; } if (!isset($data['tunnels']) || !\is_array($data['tunnels'])) { $data['tunnels'] = []; } if (\count($data['tunnels']) === 0) { $this->migrateFromLegacyIfNeeded(); if (\is_readable($path)) { $raw2 = file_get_contents($path); if ($raw2 !== false && trim($raw2) !== '') { try { /** @var array $data2 */ $data2 = $this->json->unserialize($raw2); if (isset($data2['tunnels']) && \is_array($data2['tunnels'])) { $data = $data2; } } catch (\InvalidArgumentException) { // keep empty } } } } if (!isset($data['tunnels']) || !\is_array($data['tunnels'])) { return ['version' => self::CURRENT_VERSION, 'tunnels' => []]; } return [ 'version' => (int) ($data['version'] ?? self::CURRENT_VERSION), 'tunnels' => array_values($data['tunnels']), ]; } /** * @return list> */ public function listTunnels(): array { $out = []; foreach ($this->loadData()['tunnels'] as $t) { $out[] = $this->normalizeTunnelRecord($t); } return $out; } /** * @return array|null */ public function getById(string $tunnelId): ?array { foreach ($this->listTunnels() as $t) { if (($t['id'] ?? '') === $tunnelId) { return $this->normalizeTunnelRecord($t); } } return null; } /** * @return array|null */ public function getByStoreCode(string $storeCode): ?array { foreach ($this->listTunnels() as $t) { if (($t['store_code'] ?? '') === $storeCode) { return $this->normalizeTunnelRecord($t); } } return null; } /** * Match HTTP_HOST (normalized) against tunnel base URL hosts. * * @return array|null */ public function findByRequestHost(string $requestHost): ?array { $host = self::hostFromHttpHost($requestHost); if ($host === null) { return null; } foreach ($this->listTunnels() as $t) { $t = $this->normalizeTunnelRecord($t); foreach ([$t['unsecure_base_url'] ?? '', $t['secure_base_url'] ?? ''] as $baseUrl) { if ($baseUrl === '') { continue; } $configured = self::hostFromBaseUrl((string) $baseUrl); if ($configured !== null && strcasecmp($host, $configured) === 0) { return $t; } } } return null; } public static function hostFromHttpHost(?string $httpHost): ?string { if ($httpHost === null || $httpHost === '') { return null; } $parsed = parse_url('http://' . $httpHost); if ($parsed === false || !isset($parsed['host'])) { return null; } return $parsed['host']; } public static function hostFromBaseUrl(string $baseUrl): ?string { $parsed = parse_url($baseUrl); if ($parsed === false || !isset($parsed['host'])) { return null; } return $parsed['host']; } /** * @param array $tunnel */ public function saveTunnel(array $tunnel): void { if (!isset($tunnel['id']) || (string) $tunnel['id'] === '') { throw new \InvalidArgumentException('Tunnel id is required.'); } $data = $this->loadData(); $tunnel = $this->normalizeTunnelRecord($tunnel); $found = false; foreach ($data['tunnels'] as $i => $existing) { if (($existing['id'] ?? '') === $tunnel['id']) { $data['tunnels'][$i] = $tunnel; $found = true; break; } } if (!$found) { $data['tunnels'][] = $tunnel; } $data['version'] = self::CURRENT_VERSION; $this->writeData($data); } public function deleteTunnel(string $tunnelId): void { $data = $this->loadData(); $data['tunnels'] = array_values(array_filter( $data['tunnels'], static fn ($t) => ($t['id'] ?? '') !== $tunnelId )); $this->writeData($data); } public function clearAllNginxPendingReloadFlags(): void { $data = $this->loadData(); $changed = false; foreach ($data['tunnels'] as $i => $t) { if (!empty($t['nginx_pending_reload'])) { $data['tunnels'][$i]['nginx_pending_reload'] = false; $changed = true; } } if ($changed) { $this->writeData($data); } } public function anyTunnelHasNginxPendingReload(): bool { foreach ($this->listTunnels() as $t) { if (!empty($t['nginx_pending_reload'])) { return true; } } return false; } public function clearNginxPendingReloadFlagForTunnel(string $tunnelId): void { $data = $this->loadData(); $changed = false; foreach ($data['tunnels'] as $i => $t) { if (($t['id'] ?? '') === $tunnelId && !empty($t['nginx_pending_reload'])) { $data['tunnels'][$i]['nginx_pending_reload'] = false; $changed = true; break; } } if ($changed) { $this->writeData($data); } } /** * @param array{version: int, tunnels: list>} $data */ private function writeData(array $data): void { $path = $this->getFilePath(); $payload = $this->json->serialize($data); if (file_put_contents($path, $payload, LOCK_EX) === false) { throw new \RuntimeException('Could not write tunnel registry file.'); } } private function migrateFromLegacyIfNeeded(): void { if ($this->hasNonEmptyRegistry()) { return; } $legacy = $this->legacyTunnelStoreState->read(); if ($legacy === null) { return; } $port = filter_var($this->scopeConfig->getValue(self::XML_PORT) ?: 80, FILTER_VALIDATE_INT); if (!$port || $port < 1 || $port > 65535) { $port = 80; } $secure = (bool) $this->scopeConfig->getValue(self::XML_MODE); $websiteId = 0; $groupId = 0; try { $store = $this->storeRepository->get($legacy['store_code']); $websiteId = (int) $store->getWebsiteId(); $groupId = (int) $store->getStoreGroupId(); } catch (\Throwable $e) { $this->logger->warning('MageSail legacy migration: could not load store: ' . $e->getMessage()); } $tunnel = [ 'id' => self::MIGRATED_LEGACY_TUNNEL_ID, 'label' => (string) __('Migrated tunnel'), 'website_id' => $websiteId, 'group_id' => $groupId, 'store_id' => (int) $legacy['store_id'], 'store_code' => (string) $legacy['store_code'], 'unsecure_base_url' => (string) $legacy['unsecure_base_url'], 'secure_base_url' => (string) $legacy['secure_base_url'], 'use_secure_urls' => (bool) ($legacy['use_secure_urls'] ?? false), 'local_port' => $port, 'secure' => $secure, 'nginx_pending_reload' => (bool) ($legacy['nginx_pending_reload'] ?? false), ]; $tunnel = $this->normalizeTunnelRecord($tunnel); try { $this->writeData(['version' => self::CURRENT_VERSION, 'tunnels' => [$tunnel]]); $this->migrateLegacyProcessFilesIfPresent(); $this->legacyTunnelStoreState->delete(); } catch (\Throwable $e) { $this->logger->error('MageSail legacy migration failed: ' . $e->getMessage()); } } private function hasNonEmptyRegistry(): bool { $path = $this->getFilePath(); if (!\is_readable($path)) { return false; } $raw = file_get_contents($path); if ($raw === false || trim($raw) === '') { return false; } try { /** @var array $data */ $data = $this->json->unserialize($raw); } catch (\InvalidArgumentException) { return false; } return isset($data['tunnels']) && \is_array($data['tunnels']) && \count($data['tunnels']) > 0; } private function migrateLegacyProcessFilesIfPresent(): void { $id = self::MIGRATED_LEGACY_TUNNEL_ID; $legacyPid = $this->tunnelProcessPaths->getLegacyPidFilePath(); $newPid = $this->tunnelProcessPaths->getPidFilePath($id); if (\is_file($legacyPid) && !\is_file($newPid)) { @rename($legacyPid, $newPid); } $legacyKey = $this->tunnelProcessPaths->getLegacyKeyFilePath(); $newKey = $this->tunnelProcessPaths->getKeyFilePath($id); if (\is_file($legacyKey) && !\is_file($newKey)) { @rename($legacyKey, $newKey); } } /** * @param array $t * @return array */ private function normalizeTunnelRecord(array $t): array { return [ 'id' => (string) ($t['id'] ?? ''), 'label' => (string) ($t['label'] ?? ''), 'website_id' => (int) ($t['website_id'] ?? 0), 'group_id' => (int) ($t['group_id'] ?? 0), 'store_id' => (int) ($t['store_id'] ?? 0), 'store_code' => (string) ($t['store_code'] ?? ''), 'unsecure_base_url' => (string) ($t['unsecure_base_url'] ?? ''), 'secure_base_url' => (string) ($t['secure_base_url'] ?? ''), 'use_secure_urls' => (bool) ($t['use_secure_urls'] ?? false), 'local_port' => (int) ($t['local_port'] ?? 80), 'secure' => (bool) ($t['secure'] ?? false), 'nginx_pending_reload' => (bool) ($t['nginx_pending_reload'] ?? false), ]; } }