Files
MageSail/Magesail/Block/Adminhtml/Tunnel.php
T
snxraven 4ec4791f27
CI / php (push) Successful in 2m57s
feat(admin): copy buttons and shared clipboard helper
- Add magesail-clipboard.js (clipboard API + execCommand fallback)
- Tunnel: copy store code, key, Holesail command, log tail; wire polling UI
- How-To: copy per code block; Howto block + magesail-howto-copy.js
- NGINX map: copy raw map from textarea; translations in Map block
- Document copy UX in docs/admin-ui.md
2026-03-21 04:03:13 -05:00

261 lines
8.1 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\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 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);
}
/**
* @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.
*/
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',
'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?'
),
'selectWebsiteGroup' => (string) __('Select website and store group.'),
'enterHostname' => (string) __('Enter the tunnel hostname.'),
'invalidPort' => (string) __('Enter a valid local port (165535).'),
'runningPidTpl' => (string) __('Running (PID %1)', 'X'),
'clientLabel' => (string) __('Client:'),
],
];
}
public function getTunnelUiInitJson(): string
{
return $this->json->serialize($this->getTunnelUiInitConfig());
}
}