Files
MageSail/Magesail/Controller/Adminhtml/Tunnel/Index.php
T
snxraven 80981cfe62 feat(admin): accept hostname-only tunnel URLs (auto http/https)
TunnelStoreProvisioner expands bare hosts to http:// for unsecure and
https:// for optional secure; full URLs still work. Tunnel block strips
scheme for prefill display. Update tunnel template copy, Index error
text, and README/docs.
2026-03-21 00:11:47 -05:00

196 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
namespace MageSail\Magesail\Controller\Adminhtml\Tunnel;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
use MageSail\Magesail\Model\MagesailLog;
use MageSail\Magesail\Model\TunnelManager;
use MageSail\Magesail\Model\TunnelStatus;
use MageSail\Magesail\Model\TunnelStoreProvisioner;
class Index extends Action
{
public function __construct(
Context $context,
private readonly ScopeConfigInterface $scopeConfig,
private readonly TunnelManager $tunnelManager,
private readonly TunnelStatus $tunnelStatus,
private readonly MagesailLog $magesailLog,
private readonly TunnelStoreProvisioner $tunnelStoreProvisioner
) {
parent::__construct($context);
}
public function execute()
{
$action = $this->getRequest()->getParam('action');
$wantJson = $this->isTunnelAjaxRequest();
if ($action === 'start') {
return $this->handleStart($wantJson);
}
if ($action === 'stop') {
return $this->handleStop($wantJson);
}
$resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->setActiveMenu('MageSail_Magesail::tunnel');
$resultPage->getConfig()->getTitle()->prepend(__('Holesail Tunnel'));
return $resultPage;
}
private function isTunnelAjaxRequest(): bool
{
return strtolower((string) $this->getRequest()->getHeader('X-Requested-With')) === 'xmlhttprequest'
|| (string) $this->getRequest()->getParam('ajax') === '1';
}
private function jsonPayload(bool $ok, string $message = '', array $extra = [])
{
$json = $this->resultFactory->create(ResultFactory::TYPE_JSON);
$json->setData(array_merge([
'success' => $ok,
'message' => $message,
'status' => $this->tunnelStatus->getStatus(),
], $extra));
return $json;
}
private function handleStart(bool $wantJson)
{
if ($this->tunnelStatus->isRunning()) {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Tunnel is already running.'));
}
$this->messageManager->addErrorMessage(__('Tunnel is already running.'));
return $this->_redirect('*/*/index');
}
$secure = (bool) $this->scopeConfig->getValue('magesail/settings/mode');
$port = filter_var($this->scopeConfig->getValue('magesail/settings/port') ?: 80, FILTER_VALIDATE_INT);
if (!$port || !\in_array($port, [80, 443], true)) {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Invalid port. Must be 80 or 443.'));
}
$this->messageManager->addErrorMessage(__('Invalid port. Must be 80 or 443.'));
return $this->_redirect('*/*/index');
}
$tunnelBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_base_url', ''));
if ($tunnelBaseUrl === '') {
$msg = (string) __('Tunnel hostname is required (e.g. dev.myshop.local — must match your holesail-browser virtual host).');
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
$tunnelSecureBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_secure_base_url', ''));
$useSecureUrls = $this->isTruthyParam($this->getRequest()->getParam('tunnel_use_secure_urls'));
try {
$provision = $this->tunnelStoreProvisioner->provision($tunnelBaseUrl, $tunnelSecureBaseUrl, $useSecureUrls);
} catch (LocalizedException $e) {
$this->magesailLog->add('Tunnel store provision failed: ' . $e->getMessage());
if ($wantJson) {
return $this->jsonPayload(false, (string) $e->getMessage());
}
$this->messageManager->addErrorMessage($e->getMessage());
return $this->_redirect('*/*/index');
}
$result = $this->tunnelManager->start((int) $port, $secure);
if (!$result['ok']) {
$this->tunnelStoreProvisioner->teardownPersistedState();
}
if ($result['ok']) {
$this->magesailLog->add(sprintf('Tunnel started PID %s port %s', $result['pid'], $port));
$msg = (string) __(
'Tunnel started (PID: %1). Key will appear once ready.',
$result['pid']
);
$nginxReloadMsg = '';
if (empty($provision['nginx_reloaded'])) {
$nginxReloadMsg = (string) __(
'The NGINX global map was updated, but Magento could not reload nginx (common when the PHP user cannot signal the nginx master). ' .
'On the server run: sudo systemctl reload nginx, or set Reload wrapper script in Admin under Configuration for the Holesail Tunnel NGINX settings. ' .
'Until nginx reloads, your tunnel hostname may not route to the tunnel store.'
);
}
if ($wantJson) {
$extra = [];
if ($nginxReloadMsg !== '') {
$extra['nginx_reload_required'] = true;
$extra['nginx_reload_message'] = $nginxReloadMsg;
}
return $this->jsonPayload(true, $msg, $extra);
}
$this->messageManager->addSuccessMessage($msg);
if ($nginxReloadMsg !== '') {
$this->messageManager->addWarningMessage($nginxReloadMsg);
}
return $this->_redirect('*/*/index');
} else {
$msg = match ($result['error'] ?? '') {
'already_running' => (string) __('Tunnel is already running.'),
'invalid_port' => (string) __('Invalid port. Must be 80 or 443.'),
'pid_capture_failed' => (string) __('Failed to capture PID: %1', $result['detail'] ?? ''),
default => (string) __('Error starting tunnel: %1', $result['error'] ?? 'unknown'),
};
$this->magesailLog->add('Start failed: ' . ($result['error'] ?? 'unknown'));
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
}
private function handleStop(bool $wantJson)
{
$stop = $this->tunnelManager->stop();
$stoppedOrIdle = $stop['ok'] || (($stop['error'] ?? '') === 'not_running');
if ($stoppedOrIdle) {
$this->tunnelStoreProvisioner->teardownPersistedState();
}
if ($stop['ok']) {
$this->magesailLog->add('Tunnel stopped');
if ($wantJson) {
return $this->jsonPayload(true, (string) __('Tunnel stopped.'));
}
$this->messageManager->addSuccessMessage(__('Tunnel stopped.'));
} else {
if (($stop['error'] ?? '') === 'not_running') {
if ($wantJson) {
return $this->jsonPayload(true, (string) __('No tunnel running.'), ['status' => $this->tunnelStatus->getStatus()]);
}
$this->messageManager->addNoticeMessage(__('No tunnel running.'));
} else {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Error stopping tunnel: %1', $stop['error'] ?? ''));
}
$this->messageManager->addErrorMessage(__('Error stopping tunnel: %1', $stop['error'] ?? ''));
}
}
return $wantJson ? $this->jsonPayload($stop['ok'], '') : $this->_redirect('*/*/index');
}
private function isTruthyParam(mixed $value): bool
{
if ($value === true || $value === 1) {
return true;
}
if ($value === false || $value === null || $value === '') {
return false;
}
$s = strtolower(trim((string) $value));
return \in_array($s, ['1', 'true', 'on', 'yes'], true);
}
protected function _isAllowed()
{
return $this->_authorization->isAllowed('MageSail_Magesail::tunnel');
}
}