Files
MageSail/Magesail/Block/Adminhtml/Tunnel.php
T
snxraven ff9447f0af
CI / php (push) Successful in 1m1s
Add built-in service targets for SSH on 127.0.0.1 (ports 22 and 2223) and a
Custom Port option with ssh_custom_port validation in Admin.
List env.php-discovered services first; SSH shortcuts and Custom Port last.
Rename the custom option label to "Custom Port" and align tunnel labels/i18n.
Update tunnel UI (phtml/JS), EnvServicePortResolver, controller, tests, and docs.
2026-04-01 20:13:11 -05:00

309 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
public function hasEnvDiscoveredServices(): bool
{
return $this->envServicePortResolver->hasEnvDiscoveredServices();
}
/**
* @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(),
'sshCustomServiceKey' => EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM,
'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 (165535).'),
'sshPortRequired' => (string) __('Enter a valid SSH port (165535).'),
'runningPidTpl' => (string) __('Running (PID %1)', 'X'),
'clientLabel' => (string) __('Client:'),
],
];
}
public function getTunnelUiInitJson(): string
{
return $this->json->serialize($this->getTunnelUiInitConfig());
}
}