CI / php (push) Successful in 2m53s
- Add EnvServicePortResolver (DeploymentConfig) for MySQL, Redis frontends, session Redis, AMQP, http_cache_hosts, optional search - Extend tunnel registry with tunnel_type, service_key, service metadata; provisionServiceTunnel without store/NGINX map; teardown guards - Admin: Type (Website|Service), service dropdown, list Type column; JS toggle - TunnelManager/start-server: forward to resolved host:port - Tests: EnvServicePortResolver + service teardown; test stubs for Magento/PSR - docs: admin-ui Type/Service and security notes
302 lines
9.8 KiB
PHP
302 lines
9.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace MageSail\Magesail\Block\Adminhtml;
|
||
|
||
use Magento\Backend\Block\Template;
|
||
use Magento\Backend\Block\Template\Context;
|
||
use Magento\Framework\App\Config\ScopeConfigInterface;
|
||
use Magento\Framework\Serialize\Serializer\Json;
|
||
use Magento\Store\Model\StoreManagerInterface;
|
||
use MageSail\Magesail\Model\EnvServicePortResolver;
|
||
use MageSail\Magesail\Model\TunnelRegistry;
|
||
use MageSail\Magesail\Model\TunnelStatus;
|
||
use MageSail\Magesail\Model\TunnelStoreProvisioner;
|
||
|
||
class Tunnel extends Template
|
||
{
|
||
private const LOG_TAIL_BYTES = 8192;
|
||
private const LOG_TAIL_LINES = 40;
|
||
|
||
private const XML_MODE = 'magesail/settings/mode';
|
||
|
||
public function __construct(
|
||
Context $context,
|
||
private readonly TunnelStatus $tunnelStatus,
|
||
private readonly TunnelStoreProvisioner $tunnelStoreProvisioner,
|
||
private readonly StoreManagerInterface $storeManager,
|
||
private readonly ScopeConfigInterface $scopeConfig,
|
||
private readonly TunnelRegistry $tunnelRegistry,
|
||
private readonly EnvServicePortResolver $envServicePortResolver,
|
||
private readonly Json $json,
|
||
array $data = []
|
||
) {
|
||
parent::__construct($context, $data);
|
||
}
|
||
|
||
/**
|
||
* @return list<array{tunnel_id: string, label: string, store_code: string, local_port: int, running: bool, pid: int|null, key: string|null}>
|
||
*/
|
||
public function getAllTunnelStatuses(): array
|
||
{
|
||
return $this->tunnelStatus->getAllTunnelStatuses();
|
||
}
|
||
|
||
/**
|
||
* @return list<array<string, mixed>>
|
||
*/
|
||
public function listTunnelsFromRegistry(): array
|
||
{
|
||
return $this->tunnelRegistry->listTunnels();
|
||
}
|
||
|
||
public function hasAnyNginxPendingReload(): bool
|
||
{
|
||
return $this->tunnelRegistry->anyTunnelHasNginxPendingReload();
|
||
}
|
||
|
||
/**
|
||
* Preset ports shown first in Admin (443 is typical for Holesail + HTTPS upstream).
|
||
*
|
||
* @return list<int>
|
||
*/
|
||
public function getLocalPortPresetValues(): array
|
||
{
|
||
return [443, 80, 8080];
|
||
}
|
||
|
||
/**
|
||
* Initial Add-tunnel UI: preset key '443'|'80'|'8080'|'custom' and optional custom port when not a preset.
|
||
*
|
||
* @return array{preset: string, custom_port: int}
|
||
*/
|
||
public function getInitialLocalPortUiState(): array
|
||
{
|
||
$presets = $this->getLocalPortPresetValues();
|
||
$default = 443;
|
||
foreach ($presets as $p) {
|
||
if ($p === $default) {
|
||
return ['preset' => (string) $p, 'custom_port' => $default];
|
||
}
|
||
}
|
||
return ['preset' => 'custom', 'custom_port' => $default];
|
||
}
|
||
|
||
public function getDefaultHolesailSecure(): bool
|
||
{
|
||
return (bool) $this->scopeConfig->getValue(self::XML_MODE);
|
||
}
|
||
|
||
/**
|
||
* TCP endpoints discovered from env.php (for Service tunnel type).
|
||
*
|
||
* @return list<array{service_key: string, label: string, host: string, port: int}>
|
||
*/
|
||
public function getDiscoveredServices(): array
|
||
{
|
||
return $this->envServicePortResolver->getDiscoveredServices();
|
||
}
|
||
|
||
/**
|
||
* @return list<array{value: string, label: string}>
|
||
*/
|
||
public function getWebsiteOptions(): array
|
||
{
|
||
$options = [];
|
||
foreach ($this->storeManager->getWebsites() as $website) {
|
||
$options[] = [
|
||
'value' => (string) $website->getId(),
|
||
'label' => $website->getName() . ' (' . $website->getCode() . ')',
|
||
];
|
||
}
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* JSON map: websiteId string => list of {id, name}
|
||
*/
|
||
public function getStoreGroupsByWebsiteJson(): string
|
||
{
|
||
$map = [];
|
||
foreach ($this->storeManager->getWebsites() as $website) {
|
||
$wid = (string) $website->getId();
|
||
$map[$wid] = [];
|
||
foreach ($website->getGroups() as $group) {
|
||
$map[$wid][] = [
|
||
'id' => (string) $group->getId(),
|
||
'name' => $group->getName() . ' (' . $group->getCode() . ')',
|
||
];
|
||
}
|
||
}
|
||
return $this->json->serialize($map);
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
*/
|
||
public function getTunnelStoreState(): ?array
|
||
{
|
||
return $this->tunnelStoreProvisioner->getPersistedState();
|
||
}
|
||
|
||
public function getTunnelStoreCodeConstant(): string
|
||
{
|
||
return TunnelStoreProvisioner::STORE_CODE;
|
||
}
|
||
|
||
/**
|
||
* Strip scheme for display so Admin shows hostname-only; persisted URLs stay full in state.
|
||
*/
|
||
/**
|
||
* @param array<string, mixed> $row
|
||
*/
|
||
public function getTunnelRowHostDisplay(array $row): string
|
||
{
|
||
if (($row['tunnel_type'] ?? 'website') === 'service') {
|
||
$h = trim((string) ($row['service_target_host'] ?? ''));
|
||
$p = (int) ($row['local_port'] ?? 0);
|
||
if ($h !== '' && $p > 0) {
|
||
return $h . ':' . $p;
|
||
}
|
||
return (string) ($row['service_label'] ?? '');
|
||
}
|
||
|
||
return $this->getTunnelHostDisplay($row['unsecure_base_url'] ?? null);
|
||
}
|
||
|
||
public function getTunnelHostDisplay(?string $baseUrl): string
|
||
{
|
||
if ($baseUrl === null || $baseUrl === '') {
|
||
return '';
|
||
}
|
||
$parts = parse_url($baseUrl);
|
||
if ($parts === false || empty($parts['host'])) {
|
||
return $baseUrl;
|
||
}
|
||
$out = $parts['host'];
|
||
if (!empty($parts['port'])) {
|
||
$out .= ':' . $parts['port'];
|
||
}
|
||
if (!empty($parts['path']) && $parts['path'] !== '/' && $parts['path'] !== '') {
|
||
$out .= rtrim($parts['path'], '/');
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
public function getStatusUrl(): string
|
||
{
|
||
return $this->getUrl('*/*/status');
|
||
}
|
||
|
||
public function getIndexUrl(): string
|
||
{
|
||
return $this->getUrl('*/*/index');
|
||
}
|
||
|
||
public function getLogtailUrl(): string
|
||
{
|
||
return $this->getUrl('*/*/logtail');
|
||
}
|
||
|
||
public function getFormKey(): string
|
||
{
|
||
return $this->formKey->getFormKey();
|
||
}
|
||
|
||
public function getLogFilePathForTunnel(string $tunnelId): string
|
||
{
|
||
return $this->tunnelStatus->getLogFilePath($tunnelId);
|
||
}
|
||
|
||
/**
|
||
* @return string[]
|
||
*/
|
||
public function getRecentLogLinesForTunnel(string $tunnelId): array
|
||
{
|
||
$path = $this->tunnelStatus->getLogFilePath($tunnelId);
|
||
if (!\is_readable($path)) {
|
||
return [];
|
||
}
|
||
$size = filesize($path);
|
||
if ($size === false || $size === 0) {
|
||
return [];
|
||
}
|
||
$fp = fopen($path, 'rb');
|
||
if ($fp === false) {
|
||
return [];
|
||
}
|
||
$start = max(0, $size - self::LOG_TAIL_BYTES);
|
||
fseek($fp, $start);
|
||
if ($start > 0) {
|
||
fgets($fp);
|
||
}
|
||
$chunk = stream_get_contents($fp) ?: '';
|
||
fclose($fp);
|
||
$lines = preg_split('/\R/', $chunk) ?: [];
|
||
$lines = array_values(array_filter($lines, static fn ($l) => $l !== ''));
|
||
if (\count($lines) > self::LOG_TAIL_LINES) {
|
||
$lines = \array_slice($lines, -self::LOG_TAIL_LINES);
|
||
}
|
||
return $lines;
|
||
}
|
||
|
||
/**
|
||
* Config for RequireJS magesail-tunnel-ui (URLs, form key, i18n, store groups).
|
||
*
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function getTunnelUiInitConfig(): array
|
||
{
|
||
$groupsJson = $this->getStoreGroupsByWebsiteJson();
|
||
$groups = json_decode($groupsJson, true);
|
||
|
||
return [
|
||
'statusUrl' => $this->getStatusUrl(),
|
||
'indexUrl' => $this->getIndexUrl(),
|
||
'logtailUrl' => $this->getLogtailUrl(),
|
||
'formKey' => $this->getFormKey(),
|
||
'groupsByWebsite' => \is_array($groups) ? $groups : [],
|
||
'defaultTab' => \count($this->listTunnelsFromRegistry()) === 0 ? 'add' : 'list',
|
||
'discoveredServices' => $this->envServicePortResolver->getDiscoveredServices(),
|
||
'translations' => [
|
||
'copied' => (string) __('Copied!'),
|
||
'copyFailed' => (string) __('Copy failed'),
|
||
'copyStoreCode' => (string) __('Copy store code'),
|
||
'copyKey' => (string) __('Copy key'),
|
||
'copyCommand' => (string) __('Copy command'),
|
||
'copyLog' => (string) __('Copy log'),
|
||
'copy' => (string) __('Copy'),
|
||
'selectWebsiteFirst' => (string) __('— Select website first —'),
|
||
'selectDash' => (string) __('— Select —'),
|
||
'stopFailed' => (string) __('Stop failed'),
|
||
'removeFailed' => (string) __('Remove failed'),
|
||
'removeConfirm' => (string) __(
|
||
'Remove this tunnel, delete its store view, and update the NGINX map?'
|
||
),
|
||
'removeConfirmService' => (string) __(
|
||
'Remove this service tunnel? No store view will be deleted.'
|
||
),
|
||
'typeWebsite' => (string) __('Website'),
|
||
'typeService' => (string) __('Service'),
|
||
'tunnelType' => (string) __('Type'),
|
||
'serviceToExpose' => (string) __('Service to expose'),
|
||
'noServicesInEnv' => (string) __(
|
||
'No TCP services were found in env.php. Check db, cache, session, queue, and http_cache_hosts.'
|
||
),
|
||
'selectService' => (string) __('— Select a service —'),
|
||
'selectWebsiteGroup' => (string) __('Select website and store group.'),
|
||
'enterHostname' => (string) __('Enter the tunnel hostname.'),
|
||
'invalidPort' => (string) __('Enter a valid local port (1–65535).'),
|
||
'runningPidTpl' => (string) __('Running (PID %1)', 'X'),
|
||
'clientLabel' => (string) __('Client:'),
|
||
],
|
||
];
|
||
}
|
||
|
||
public function getTunnelUiInitJson(): string
|
||
{
|
||
return $this->json->serialize($this->getTunnelUiInitConfig());
|
||
}
|
||
}
|