getRequest()->getParam('action'); $wantJson = $this->isTunnelAjaxRequest(); if ($action === 'start') { return $this->handleStart($wantJson); } if ($action === 'stop') { return $this->handleStop($wantJson); } if ($action === 'delete') { return $this->handleDelete($wantJson); } $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); $resultPage->setActiveMenu('MageSail_Magesail::tunnel_index'); $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, 'tunnels' => $this->tunnelStatus->getAllTunnelStatuses(), ], $extra)); return $json; } private function handleStart(bool $wantJson) { if (!$this->isAllowedTunnelManage()) { $msg = (string) __('You are not allowed to manage tunnels.'); if ($wantJson) { return $this->jsonPayload(false, $msg); } $this->messageManager->addErrorMessage($msg); return $this->_redirect('*/*/index'); } $tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', '')); if ($tunnelId !== '') { return $this->handleStartExisting($tunnelId, $wantJson); } return $this->handleStartNew($wantJson); } private function handleStartExisting(string $tunnelId, bool $wantJson) { if ($this->tunnelStatus->isRunning($tunnelId)) { if ($wantJson) { return $this->jsonPayload(false, (string) __('This tunnel is already running.')); } $this->messageManager->addErrorMessage(__('This tunnel is already running.')); return $this->_redirect('*/*/index'); } $result = $this->tunnelManager->start($tunnelId); if ($result['ok']) { $this->magesailLog->add(sprintf('Tunnel started %s PID %s', $tunnelId, $result['pid'] ?? '')); $msg = (string) __( 'Tunnel started (PID: %1). Key will appear once ready.', $result['pid'] ?? '' ); if ($wantJson) { return $this->jsonPayload(true, $msg); } $this->messageManager->addSuccessMessage($msg); return $this->_redirect('*/*/index'); } $msg = match ($result['error'] ?? '') { 'unknown_tunnel' => (string) __('Unknown tunnel.'), 'already_running' => (string) __('Tunnel is already running.'), 'invalid_port' => (string) __('Invalid local port for this tunnel.'), 'could_not_create_var_dir' => (string) __('Could not create var directory for tunnel state.'), 'script_not_found' => (string) __( 'start-server.js was not found. Set Scripts directory in Configuration (expected: %1).', $result['detail'] ?? '' ), 'node_modules_missing' => (string) __( 'Run npm install in the scripts directory (missing node_modules under %1).', $result['detail'] ?? '' ), '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 handleStartNew(bool $wantJson) { $label = trim((string) $this->getRequest()->getParam('tunnel_label', '')); $websiteId = (int) $this->getRequest()->getParam('website_id', 0); $groupId = (int) $this->getRequest()->getParam('group_id', 0); $tunnelBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_base_url', '')); $tunnelSecureBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_secure_base_url', '')); $useSecureUrls = $this->isTruthyParam($this->getRequest()->getParam('tunnel_use_secure_urls')); $localPort = filter_var($this->getRequest()->getParam('local_port'), FILTER_VALIDATE_INT); $holesailSecureParam = $this->getRequest()->getParam('tunnel_holesail_secure'); $secureHolesail = $holesailSecureParam === null ? (bool) $this->scopeConfig->getValue('magesail/settings/mode') : $this->isTruthyParam($holesailSecureParam); if ($websiteId <= 0 || $groupId <= 0) { $msg = (string) __('Select a website and store group.'); if ($wantJson) { return $this->jsonPayload(false, $msg); } $this->messageManager->addErrorMessage($msg); return $this->_redirect('*/*/index'); } 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'); } if (!$localPort || $localPort < 1 || $localPort > 65535) { $msg = (string) __('Invalid local port (1–65535).'); if ($wantJson) { return $this->jsonPayload(false, $msg); } $this->messageManager->addErrorMessage($msg); return $this->_redirect('*/*/index'); } try { $provision = $this->tunnelStoreProvisioner->provisionNewTunnel( $label, $websiteId, $groupId, $tunnelBaseUrl, $tunnelSecureBaseUrl, $useSecureUrls, (int) $localPort, $secureHolesail ); } 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'); } $newTunnelId = $provision['tunnel_id']; $result = $this->tunnelManager->start($newTunnelId); if (!$result['ok']) { $this->tunnelStoreProvisioner->teardownTunnel($newTunnelId); } if ($result['ok']) { $this->magesailLog->add(sprintf( 'Tunnel started %s PID %s port %s', $newTunnelId, $result['pid'] ?? '', $localPort )); $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'); } $msg = match ($result['error'] ?? '') { 'already_running' => (string) __('Tunnel is already running.'), 'invalid_port' => (string) __('Invalid local port.'), 'could_not_create_var_dir' => (string) __('Could not create var directory for tunnel state.'), '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) { if (!$this->isAllowedTunnelManage()) { $msg = (string) __('You are not allowed to manage tunnels.'); if ($wantJson) { return $this->jsonPayload(false, $msg); } $this->messageManager->addErrorMessage($msg); return $this->_redirect('*/*/index'); } $tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', '')); if ($tunnelId === '') { if ($wantJson) { return $this->jsonPayload(false, (string) __('Missing tunnel id.')); } $this->messageManager->addErrorMessage(__('Missing tunnel id.')); return $this->_redirect('*/*/index'); } $stop = $this->tunnelManager->stop($tunnelId); if ($stop['ok']) { $this->magesailLog->add(sprintf('Tunnel stopped %s', $tunnelId)); if ($wantJson) { return $this->jsonPayload(true, (string) __('Tunnel stopped.'), [ 'message' => (string) __('Tunnel stopped.'), ]); } $this->messageManager->addSuccessMessage(__('Tunnel stopped.')); } else { if (($stop['error'] ?? '') === 'not_running') { if ($wantJson) { return $this->jsonPayload(true, (string) __('No process running for this tunnel.')); } $this->messageManager->addNoticeMessage(__('No process running for this tunnel.')); } 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 handleDelete(bool $wantJson) { if (!$this->isAllowedTunnelManage()) { $msg = (string) __('You are not allowed to manage tunnels.'); if ($wantJson) { return $this->jsonPayload(false, $msg); } $this->messageManager->addErrorMessage($msg); return $this->_redirect('*/*/index'); } $tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', '')); if ($tunnelId === '') { if ($wantJson) { return $this->jsonPayload(false, (string) __('Missing tunnel id.')); } $this->messageManager->addErrorMessage(__('Missing tunnel id.')); return $this->_redirect('*/*/index'); } $this->tunnelManager->stop($tunnelId); $this->tunnelStoreProvisioner->teardownTunnel($tunnelId); $this->magesailLog->add(sprintf('Tunnel removed %s', $tunnelId)); if ($wantJson) { return $this->jsonPayload(true, (string) __('Tunnel removed.')); } $this->messageManager->addSuccessMessage(__('Tunnel removed.')); return $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->isAllowedTunnelUi(); } private function isAllowedTunnelUi(): bool { return $this->_authorization->isAllowed('MageSail_Magesail::tunnel') || $this->_authorization->isAllowed('MageSail_Magesail::tunnel_view') || $this->_authorization->isAllowed('MageSail_Magesail::tunnel_manage'); } private function isAllowedTunnelManage(): bool { return $this->_authorization->isAllowed('MageSail_Magesail::tunnel') || $this->_authorization->isAllowed('MageSail_Magesail::tunnel_manage'); } }