PHP Classes

File: public_html/webdoors/dosdoors/index.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   public_html/webdoors/dosdoors/index.php   Download  
File: public_html/webdoors/dosdoors/index.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 26,234 bytes
 

Contents

Class file image Download
<?php /** * DOS Door Player * * This file can be included by routes or accessed directly. * When included, $doorId should be set by the calling code. */ use BinktermPHP\RouteHelper; use BinktermPHP\UserMeta; use BinktermPHP\I18n\LocaleResolver; use BinktermPHP\I18n\Translator; $user = RouteHelper::requireAuth(); $csrfUserId = (int)($user['user_id'] ?? $user['id'] ?? 0); $csrfToken = ''; if ($csrfUserId > 0) { try { $meta = new UserMeta(); $csrfToken = $meta->getValue($csrfUserId, 'csrf_token') ?? ''; } catch (\Throwable $e) {} } $translator = new Translator(); $localeResolver = new LocaleResolver($translator); $locale = $localeResolver->resolveLocale((string)($user['locale'] ?? ''), $user); $localeResolver->persistLocale($locale); $t = static function (string $key, string $fallback, array $params = [], string $namespace = 'common') use ($translator, $locale): string { $translated = $translator->translate($key, $params, $locale, [$namespace]); return $translated === $key ? $fallback : $translated; }; // If accessed directly, try to extract door ID from URL if (!isset($doorId)) { $requestUri = $_SERVER['REQUEST_URI']; $pattern = '#/webdoors/dosdoors/([^/]+)#'; preg_match($pattern, $requestUri, $matches); $doorId = $matches[1] ?? ''; // Clean the door ID (remove query string if present) $doorId = preg_replace('/\?.*$/', '', $doorId); } if (empty($doorId)) { http_response_code(404); echo htmlspecialchars($t('ui.dosdoor_player.error_no_door_specified', 'Error: No door ID specified'), ENT_QUOTES); exit; } ?> <!DOCTYPE html> <html lang="<?= htmlspecialchars($locale, ENT_QUOTES) ?>"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="csrf-token" content="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>"> <title><?= htmlspecialchars($t('ui.dosdoor_player.page_title', 'DOS Door Player'), ENT_QUOTES) ?></title> <link rel="stylesheet" href="/webdoors/terminal/assets/xterm.css"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; overflow: hidden; background: #000; } .terminal-controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 5px 10px; background: #1a1a2e; height: 35px; border-bottom: 1px solid #333; } #terminal-container { position: absolute; top: 35px; left: 0; right: 0; bottom: 0; background: #000; display: flex; align-items: flex-start; justify-content: center; overflow: hidden; } #terminal-container .xterm { margin-top: 6px; } /* Force terminal surface to pure black */ #terminal-container .xterm, #terminal-container .xterm-viewport, #terminal-container .xterm-screen, #terminal-container .xterm-screen canvas { background-color: #000 !important; } .connection-status { padding: 2px 8px; border-radius: 3px; font-size: 11px; font-family: monospace; } .status-disconnected { background-color: #dc3545; color: white; } .status-connecting { background-color: #ffc107; color: black; } .status-connected { background-color: #28a745; color: white; } .door-header { margin: 0; font-size: 0.9rem; color: #fff; font-family: monospace; justify-self: start; } #endSessionBtn { padding: 4px 12px; font-size: 12px; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer; justify-self: end; font-family: monospace; } #endSessionBtn:hover { background: #c82333; } #connectionStatus { justify-self: center; } .error-message { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1a1a2e; color: #ff5555; padding: 20px; border-radius: 5px; font-family: monospace; max-width: 80%; text-align: center; } /* Xterm helpers: keep off-screen, but do not break measurement logic */ .xterm-helpers { position: absolute !important; left: -9999em !important; top: 0 !important; opacity: 0 !important; pointer-events: none !important; } .xterm-helpers .xterm-helper-textarea { position: absolute !important; left: -9999em !important; top: 0 !important; opacity: 0 !important; pointer-events: none !important; } .xterm-char-measure-element { position: absolute !important; left: -9999em !important; top: 0 !important; visibility: hidden !important; } </style> </head> <body> <div class="terminal-controls"> <h5 class="door-header" id="doorTitle"><?= htmlspecialchars($t('ui.dosdoor_player.page_title', 'DOS Door Player'), ENT_QUOTES) ?></h5> <div id="connectionStatus" class="connection-status status-disconnected"> <?= htmlspecialchars($t('ui.dosdoor_player.status_prefix', 'Status:'), ENT_QUOTES) ?> <?= htmlspecialchars($t('ui.dosdoor_player.status_disconnected', 'Disconnected'), ENT_QUOTES) ?> </div> <button id="endSessionBtn"><?= htmlspecialchars($t('ui.dosdoor_player.end_session', 'End Session'), ENT_QUOTES) ?></button> </div> <div id="terminal-container"></div> <script src="/webdoors/terminal/assets/xterm.js"></script> <script> let term = null; const TERM_COLS = 80; const TERM_ROWS = 25; let socket = null; let sessionId = null; let wsPort = null; let wsToken = null; const doorId = <?php echo json_encode($doorId); ?>; const I18N = <?php echo json_encode([ 'statusPrefix' => $t('ui.dosdoor_player.status_prefix', 'Status:'), 'statusDisconnected' => $t('ui.dosdoor_player.status_disconnected', 'Disconnected'), 'statusLaunching' => $t('ui.dosdoor_player.status_launching', 'Launching...'), 'statusLaunchFailed' => $t('ui.dosdoor_player.status_launch_failed', 'Launch failed'), 'statusConnecting' => $t('ui.dosdoor_player.status_connecting', 'Connecting...'), 'statusConnected' => $t('ui.dosdoor_player.status_connected', 'Connected'), 'statusConnectionError' => $t('ui.dosdoor_player.status_connection_error', 'Connection error'), 'statusError' => $t('ui.dosdoor_player.status_error', 'Error'), 'launchingDoorLine' => $t('ui.dosdoor_player.launching_door_line', 'Launching door game...'), 'failedLaunchLine' => $t('ui.dosdoor_player.failed_launch_line', 'Failed to launch door session.'), 'connectingToPrefix' => $t('ui.dosdoor_player.connecting_to_prefix', 'Connecting to'), 'connectedLine' => $t('ui.dosdoor_player.connected_line', 'Connected!'), 'connectionClosedLine' => $t('ui.dosdoor_player.connection_closed_line', '[Connection closed]'), 'connectionErrorLine' => $t('ui.dosdoor_player.connection_error_line', '[Connection error]'), 'failedToConnectPrefix' => $t('ui.dosdoor_player.failed_to_connect_prefix', 'Failed to connect:'), 'confirmEndSession' => $t('ui.dosdoor_player.confirm_end_session', 'Are you sure you want to end this door session?'), 'failedEndSession' => $t('ui.dosdoor_player.failed_end_session', 'Failed to end session'), 'errorEndingSession' => $t('ui.dosdoor_player.error_ending_session', 'Error ending session'), 'errorNoDoorSpecified' => $t('ui.dosdoor_player.error_no_door_specified', 'Error: No door ID specified'), 'failedLaunchDoor' => $t('ui.dosdoor_player.failed_launch_door', 'Failed to launch door'), 'documentTitleSuffix' => $t('ui.dosdoor_player.document_title_suffix', 'DOS Door'), 'apiErrors' => [ 'errors.door.door_name_required' => $t('errors.door.door_name_required', 'Door name required', [], 'errors'), 'errors.door.admin_only' => $t('errors.door.admin_only', 'This door is restricted to administrators', [], 'errors'), 'errors.door.insufficient_credits' => $t('errors.door.insufficient_credits', 'Insufficient credits', [], 'errors'), 'errors.door.insufficient_credits_detail' => $t('errors.door.insufficient_credits_detail', 'This door costs {required} credits. You have {balance} credits.', [], 'errors'), 'errors.door.capacity_reached' => $t('errors.door.capacity_reached', 'Door is at capacity', [], 'errors'), 'errors.door.capacity_reached_detail' => $t('errors.door.capacity_reached_detail', 'This door is currently in use. Only {max_nodes} player(s) allowed at a time. Please try again later.', [], 'errors'), 'errors.door.launch_failed' => $t('errors.door.launch_failed', 'Failed to start door session', [], 'errors'), 'errors.door.session_id_required' => $t('errors.door.session_id_required', 'Session ID required', [], 'errors'), 'errors.door.session_unauthorized' => $t('errors.door.session_unauthorized', 'Unauthorized', [], 'errors'), 'errors.door.session_end_failed' => $t('errors.door.session_end_failed', 'Failed to end session', [], 'errors'), 'errors.door.session_get_failed' => $t('errors.door.session_get_failed', 'Failed to get session', [], 'errors'), ], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>; function interpolateTemplate(template, params) { if (!template || !params || typeof params !== 'object') { return template; } return String(template).replace(/\{(\w+)\}/g, (_, key) => { return Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : `{${key}}`; }); } function resolveApiError(payload, fallback) { if (payload && payload.error_code && I18N.apiErrors[payload.error_code]) { return interpolateTemplate(I18N.apiErrors[payload.error_code], payload); } if (payload && payload.error) { return String(payload.error); } return fallback; } // Initialize terminal function initTerminal() { console.log('[INIT] Starting terminal initialization'); const container = document.getElementById('terminal-container'); term = new Terminal({ cursorBlink: true, cols: TERM_COLS, rows: TERM_ROWS, fontSize: 16, fontFamily: 'Courier New, monospace', scrollback: 0, theme: { background: '#000000', foreground: '#AAAAAA', cursor: '#00FF00', black: '#000000', red: '#AA0000', green: '#00AA00', yellow: '#AA5500', blue: '#0000AA', magenta: '#AA00AA', cyan: '#00AAAA', white: '#AAAAAA', brightBlack: '#555555', brightRed: '#FF5555', brightGreen: '#55FF55', brightYellow: '#FFFF55', brightBlue: '#5555FF', brightMagenta: '#FF55FF', brightCyan: '#55FFFF', brightWhite: '#FFFFFF' }, convertEol: false }); console.log('[INIT] Terminal created, opening in container'); term.open(container); term.resize(TERM_COLS, TERM_ROWS); scheduleFixedTerminalSize(); // Handle terminal input term.onData((data) => { // Remap DEL (0x7f) to Backspace (0x08) for DOS compatibility if (data === '\x7f') data = '\x08'; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(data); } }); // Key handler: intercept extended keys before xterm generates ANSI sequences. // DOS apps running via Doorway expect Doorway Protocol (\x00 + IBM PC scan code) // for navigation/function keys, not ANSI escape sequences. term.attachCustomKeyEventHandler((e) => { if (e.type !== 'keydown') return true; // Extended key Doorway Protocol scan codes (no modifier) const doorwayKeys = { 'ArrowUp': 0x48, 'ArrowDown': 0x50, 'ArrowLeft': 0x4B, 'ArrowRight': 0x4D, 'Home': 0x47, 'End': 0x4F, 'PageUp': 0x49, 'PageDown': 0x51, 'Insert': 0x52, 'Delete': 0x53, 'F1': 0x3B, 'F2': 0x3C, 'F3': 0x3D, 'F4': 0x3E, 'F5': 0x3F, 'F6': 0x40, 'F7': 0x41, 'F8': 0x42, 'F9': 0x43, 'F10': 0x44, 'F11': 0x85, 'F12': 0x86, }; // Extended key Doorway Protocol scan codes (Ctrl modifier) const doorwayCtrlKeys = { 'ArrowLeft': 0x73, 'ArrowRight': 0x74, 'Home': 0x77, 'End': 0x75, 'PageUp': 0x84, 'PageDown': 0x76, }; if (!e.altKey) { let scanCode = null; if (e.ctrlKey && doorwayCtrlKeys[e.key] !== undefined) { scanCode = doorwayCtrlKeys[e.key]; } else if (!e.ctrlKey && doorwayKeys[e.key] !== undefined) { scanCode = doorwayKeys[e.key]; } if (scanCode !== null) { if (socket && socket.readyState === WebSocket.OPEN) { socket.send('\x00' + String.fromCharCode(scanCode)); } return false; // Suppress xterm's ANSI sequence output } } // Ctrl+key: prevent browser from capturing common combos (Ctrl+W, Ctrl+T, etc.) // and let xterm.js encode them as control characters if (e.ctrlKey && !e.altKey && e.key.length === 1) { e.preventDefault(); return true; // xterm handles the encoding } return true; }); term.onRender(() => { scheduleFixedTerminalSize(); }); } function updateStatus(message, state) { const statusDiv = document.getElementById('connectionStatus'); statusDiv.textContent = I18N.statusPrefix + ' ' + message; statusDiv.className = 'connection-status status-' + state; } function showError(message) { const container = document.getElementById('terminal-container'); const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.textContent = message; container.appendChild(errorDiv); } function launchDoorSession() { console.log('[LAUNCH] Launching door session for:', doorId); updateStatus(I18N.statusLaunching, 'connecting'); term.writeln('\x1b[1;33m' + I18N.launchingDoorLine + '\x1b[0m'); const formData = new FormData(); formData.append('door', doorId); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; return fetch('/api/door/launch', { method: 'POST', headers: csrfToken ? { 'X-CSRF-Token': csrfToken } : {}, body: formData }) .then(response => response.json()) .then(data => { console.log('[LAUNCH] Launch response:', data); if (!data.success) { throw new Error(resolveApiError(data, I18N.failedLaunchDoor)); } return data.session; }); } function connectToSession() { console.log('[CONNECT] connectToSession called, term exists:', !!term); // Get current session for this specific door fetch('/api/door/session?door=' + encodeURIComponent(doorId)) .then(response => response.json()) .then(data => { console.log('[CONNECT] Session data received:', data); if (!data.success || !data.session) { // No session exists, launch one console.log('[CONNECT] No session found, launching...'); return launchDoorSession().then(session => { data.session = session; return data; }); } return data; }) .then(data => { if (!data.session) { updateStatus(I18N.statusLaunchFailed, 'disconnected'); term.clear(); term.writeln('\x1b[1;31m' + I18N.failedLaunchLine + '\x1b[0m'); return; } sessionId = data.session.session_id; wsPort = data.session.ws_port; wsToken = data.session.ws_token; console.log('[TOKEN] Received token:', wsToken ? wsToken.substring(0, 16) + '...' : 'MISSING!'); const doorTitle = document.getElementById('doorTitle'); if (doorTitle && data.session.door_name) { doorTitle.textContent = data.session.door_name; document.title = data.session.door_name + ' - ' + I18N.documentTitleSuffix; } // Clear terminal initialization artifacts console.log('[CONNECT] Clearing terminal'); term.clear(); // Connect to WebSocket with authentication token updateStatus(I18N.statusConnecting, 'connecting'); term.writeln('\x1b[1;33m' + I18N.connectingToPrefix + ' ' + data.session.door_name + '...\x1b[0m'); // Use WebSocket URL from server (configured or auto-detected) const wsBaseUrl = data.session.ws_url || ('ws://' + window.location.hostname + ':' + wsPort); const wsUrl = wsBaseUrl + (wsToken ? '?token=' + encodeURIComponent(wsToken) : ''); console.log('[CONNECT] Connecting to WebSocket:', wsBaseUrl + ' (token present:', !!wsToken, ')'); socket = new WebSocket(wsUrl); socket.onopen = () => { updateStatus(I18N.statusConnected, 'connected'); term.writeln('\x1b[1;32m' + I18N.connectedLine + '\x1b[0m'); term.writeln(''); term.focus(); }; socket.onmessage = (event) => { term.write(event.data); }; socket.onclose = (event) => { updateStatus(I18N.statusDisconnected, 'disconnected'); term.writeln(''); term.writeln('\x1b[1;31m' + I18N.connectionClosedLine + '\x1b[0m'); }; socket.onerror = (error) => { updateStatus(I18N.statusConnectionError, 'disconnected'); term.writeln('\x1b[1;31m' + I18N.connectionErrorLine + '\x1b[0m'); console.error('WebSocket error:', error); }; }) .catch(error => { console.error('Failed to get session:', error); updateStatus(I18N.statusError, 'disconnected'); term.writeln('\x1b[1;31m' + I18N.failedToConnectPrefix + ' ' + error.message + '\x1b[0m'); }); } function endSession() { if (!sessionId) { return; } if (!confirm(I18N.confirmEndSession)) { return; } const csrfTokenEnd = document.querySelector('meta[name="csrf-token"]')?.content || ''; fetch('/api/door/end', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...(csrfTokenEnd ? { 'X-CSRF-Token': csrfTokenEnd } : {}) }, body: 'session_id=' + encodeURIComponent(sessionId) }) .then(response => response.json()) .then(data => { if (data.success) { if (socket) { socket.close(); } window.top.location.href = '/games'; } else { alert(resolveApiError(data, I18N.failedEndSession)); } }) .catch(error => { console.error('Failed to end session:', error); alert(I18N.errorEndingSession); }); } function setFixedTerminalSize() { if (!term || !term.element) { return; } const core = term._core; if (!core || !core._renderService || !core._renderService.dimensions) { return; } const dims = core._renderService.dimensions.css; if (!dims || !dims.cell) { return; } const width = Math.ceil(dims.cell.width * TERM_COLS); const height = Math.ceil(dims.cell.height * TERM_ROWS); term.element.style.width = width + 'px'; term.element.style.height = height + 'px'; } function scheduleFixedTerminalSize() { if (window.requestAnimationFrame) { window.requestAnimationFrame(setFixedTerminalSize); } else { setFixedTerminalSize(); } } // Initialize on page load window.addEventListener('DOMContentLoaded', () => { if (!doorId) { showError(I18N.errorNoDoorSpecified); updateStatus(I18N.statusError, 'disconnected'); return; } console.log('[INIT] Door ID:', doorId); initTerminal(); connectToSession(); }); window.addEventListener('resize', () => { scheduleFixedTerminalSize(); }); // End session button document.getElementById('endSessionBtn').addEventListener('click', endSession); // Clean up on page unload window.addEventListener('beforeunload', () => { if (socket) { socket.close(); } }); // Alt+key handling via capture phase - fires before browser menu/shortcut handling // attachCustomKeyEventHandler is too late on Windows; Alt is consumed first // Uses Doorway Protocol: \x00 + IBM PC scan code (standard for DOS BBS programs) document.addEventListener('keydown', function(e) { if (!e.altKey || e.ctrlKey || !term || !socket || socket.readyState !== WebSocket.OPEN) return; // IBM PC scan codes for Alt+letter (Doorway Protocol) const altLetterCodes = { 'a': 0x1E, 'b': 0x30, 'c': 0x2E, 'd': 0x20, 'e': 0x12, 'f': 0x21, 'g': 0x22, 'h': 0x23, 'i': 0x17, 'j': 0x24, 'k': 0x25, 'l': 0x26, 'm': 0x32, 'n': 0x31, 'o': 0x18, 'p': 0x19, 'q': 0x10, 'r': 0x13, 's': 0x1F, 't': 0x14, 'u': 0x16, 'v': 0x2F, 'w': 0x11, 'x': 0x2D, 'y': 0x15, 'z': 0x2C }; // IBM PC scan codes for Alt+digit (Doorway Protocol) const altDigitCodes = { '1': 0x78, '2': 0x79, '3': 0x7A, '4': 0x7B, '5': 0x7C, '6': 0x7D, '7': 0x7E, '8': 0x7F, '9': 0x80, '0': 0x81 }; let scanCode = null; if (e.code.startsWith('Key')) { const ch = e.code.slice(3).toLowerCase(); scanCode = altLetterCodes[ch] ?? null; } else if (e.code.startsWith('Digit')) { const digit = e.code.slice(5); scanCode = altDigitCodes[digit] ?? null; } if (scanCode !== null) { e.preventDefault(); e.stopPropagation(); const seq = '\x00' + String.fromCharCode(scanCode); console.log('[ALT] code=' + e.code + ' scanCode=0x' + scanCode.toString(16) + ' socketState=' + socket.readyState); socket.send(seq); } }, true); // true = capture phase, fires before browser handles it </script> </body> </html>