1231 lines
34 KiB
PHP
1231 lines
34 KiB
PHP
<?php
|
|
|
|
namespace App\Classes\BBS;
|
|
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
|
|
use App\Classes\BBS\Control\Telnet;
|
|
use App\Classes\BBS\Exceptions\{ActionMissingInputsException,InvalidPasswordException,ParentNotFoundException};
|
|
use App\Classes\BBS\Frame\Action;
|
|
use App\Classes\BBS\Page\{Ansi,Viewdata};
|
|
use App\Classes\BBS\Server\{Ansitex,Videotex};
|
|
use App\Classes\Sock\{SocketClient,SocketException};
|
|
use App\Models\User;
|
|
|
|
abstract class Server {
|
|
/** @var SocketClient Remote connection resource */
|
|
protected SocketClient $client;
|
|
|
|
/** @var Page Our page object that is specific to a server type */
|
|
protected Page $po;
|
|
|
|
/** @var int Our send/receive timeout, when interacting with the remote */
|
|
protected const TIMEOUT = 30;
|
|
|
|
// Size of Bottom Line Pollution
|
|
/** @deprecated ? */
|
|
protected $blp = 0;
|
|
|
|
// Whats on the baseline currently
|
|
/** @deprecated ? */
|
|
protected $baseline = '';
|
|
|
|
// Fields in the frame
|
|
/** @deprecated - move to page ? */
|
|
public $fields = [];
|
|
|
|
/* PROTOCOL SPECIFIC ACTIONS */
|
|
|
|
/**
|
|
* Move the cursor via the shortest path.
|
|
*
|
|
* @param $x
|
|
* @param $y
|
|
* @return string
|
|
*/
|
|
abstract function moveCursor($x,$y): string;
|
|
|
|
/**
|
|
* Send a message to the baseline
|
|
*
|
|
* @note "$this->blp" remembers how many chars are there, so that they can be replaced with next call
|
|
* @param string $text
|
|
* @param bool $reposition
|
|
*/
|
|
abstract function sendBaseline(string $text,bool $reposition=FALSE);
|
|
|
|
/* METHODS */
|
|
|
|
public function __construct()
|
|
{
|
|
switch (get_class($this)) {
|
|
case Ansitex::class;
|
|
$this->po = new Ansi(config('bbs.welcome'));
|
|
break;
|
|
|
|
case Videotex::class;
|
|
$this->po = new Viewdata(config('bbs.welcome'));
|
|
break;
|
|
|
|
default:
|
|
throw new \Exception('Unknown server type: '.get_class($this));
|
|
|
|
}
|
|
|
|
$this->fields = collect();
|
|
}
|
|
|
|
/**
|
|
* Setup the session with the client
|
|
*
|
|
* @param SocketClient $client
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
protected function init(SocketClient $client)
|
|
{
|
|
$this->client = $client;
|
|
|
|
define('MODE_BL', 1); // Typing a * command on the baseline
|
|
define('MODE_FIELD', 2); // typing into an input field
|
|
define('MODE_WARPTO', 3); // awaiting selection of a timewarp
|
|
define('MODE_COMPLETE', 4); // Entry of data is complete ..
|
|
define('MODE_SUBMITRF', 5); // asking if should send or not.
|
|
define('MODE_RFSENT', 6);
|
|
define('MODE_RFERROR', 7);
|
|
define('MODE_RFNOTSENT', 8);
|
|
|
|
define('ACTION_RELOAD', 1);
|
|
define('ACTION_GOTO', 2);
|
|
define('ACTION_BACK', 3);
|
|
define('ACTION_NEXT', 4);
|
|
define('ACTION_INFO', 5);
|
|
define('ACTION_TERMINATE', 6);
|
|
define('ACTION_SUBMITRF', 7); // Offer to submit a response frame
|
|
define('ACTION_STAR', 8);
|
|
define('ACTION_EDIT', 9); // Edit current frame
|
|
|
|
/** @deprecated */
|
|
define('CONTROL_METHOD', 2); // Send input to an external method
|
|
/** @deprecated */
|
|
define('CONTROL_EDIT', 3); // Controller to edit frame
|
|
|
|
// Status messages
|
|
define('MSG_SENDORNOT', GREEN.'KEY 1 TO SEND, 2 NOT TO SEND');
|
|
define('MSG_SENT', GREEN.'MESSAGE SENT - KEY '.HASH.' TO CONTINUE');
|
|
define('MSG_NOTSENT', GREEN.'MESSAGE NOT SENT - KEY '.HASH.' TO CONTINUE');
|
|
|
|
define('ERR_DATABASE', RED.'UNAVAILABLE AT PRESENT - PLS TRY LATER');
|
|
define('ERR_NOTSENT', RED.'MESSAGE NOT SENT DUE TO AN ERROR');
|
|
define('ERR_PRIVATE', WHITE.'PRIVATE PAGE'.GREEN.SPACE.'- FOR EXPLANATION *37'.HASH.'..');
|
|
define('ERR_ROUTE', WHITE.'MISTAKE?'.GREEN.SPACE.'TRY AGAIN OR TELL US ON *08');
|
|
define('ERR_PAGE',ERR_ROUTE);
|
|
define('ERR_USER_ALREADYMEMBER', RED.'ALREADY MEMBER OF CUG');
|
|
define('ERR_SYSTEM', RED.'SYSTEM ERROR');
|
|
|
|
define('MSG_TIMEWARP_ON', WHITE.'TIMEWARP ON'.GREEN.SPACE.'VIEW INFO WITH *02');
|
|
define('MSG_TIMEWARP_OFF', WHITE.'TIMEWARP OFF'.GREEN.SPACE.'VIEWING DATE IS FIXED');
|
|
define('MSG_TIMEWARP_TO', GREEN.'TIMEWARP TO %s');
|
|
define('MSG_TIMEWARP', WHITE.'OTHER VERSIONS EXIST'.GREEN.SPACE.'KEY *02 TO VIEW');
|
|
|
|
// Setup session
|
|
$this->client->send(Telnet::send_iac('do_suppress_goahead'),static::TIMEOUT);
|
|
$this->client->send(Telnet::send_iac('wont_linemode'),static::TIMEOUT);
|
|
$this->client->send(Telnet::send_iac('will_echo'),static::TIMEOUT);
|
|
$this->client->send(Telnet::send_iac('will_xmit_binary'),static::TIMEOUT);
|
|
//$this->client->send(Telnet::send_iac('are_you_there'),static::TIMEOUT); // AYT
|
|
$this->client->send(Telnet::send_iac('do_opt_termtype').Telnet::send_iac('sn_start').chr(Telnet::TCP_OPT_TERMTYPE).chr(Telnet::TCP_OPT_ECHO).Telnet::send_iac('sn_end'),static::TIMEOUT); // Request Term Type
|
|
|
|
$this->client->send(CLS.COFF,static::TIMEOUT);
|
|
$this->client->send('Press a key...',static::TIMEOUT);
|
|
}
|
|
|
|
/**
|
|
* Write something to the system log.
|
|
*
|
|
* @param string $mode
|
|
* @param string $message
|
|
* @param array $data
|
|
*/
|
|
public function log(string $mode,string $message,array $data=[])
|
|
{
|
|
Log::channel('bbs')->$mode($message,$data);
|
|
}
|
|
|
|
/**
|
|
* Connection handler
|
|
*
|
|
* @param SocketClient $client
|
|
* @return int|null
|
|
* @throws SocketException
|
|
*/
|
|
public function onConnect(SocketClient $client): ?int
|
|
{
|
|
$pid = pcntl_fork();
|
|
|
|
if ($pid === -1)
|
|
throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process');
|
|
|
|
// Parent return ready for next connection
|
|
elseif (! $pid)
|
|
return NULL;
|
|
|
|
$this->log('info',sprintf('%s:+ New connection, thread [%d] created',static::LOGKEY,$pid));
|
|
Log::channel('bbs')->withContext(['pid'=>getmypid()]);
|
|
|
|
$this->init($client);
|
|
$this->session();
|
|
$this->client->close();
|
|
exit(0);
|
|
}
|
|
|
|
/**
|
|
* The core of the BBS functionality
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
protected function session(): void
|
|
{
|
|
/**
|
|
* - NULL means no action
|
|
* - ACTION_* is the action
|
|
*/
|
|
$action = ACTION_GOTO; // Initial action
|
|
|
|
/**
|
|
* State of the current action
|
|
* - NULL means we are not doing anything
|
|
* - MODE_* is the mode
|
|
*
|
|
* @var ?int $mode
|
|
*/
|
|
$mode = NULL;
|
|
|
|
/** Variable holding our current key timeout value */
|
|
$inkey_timeout = 5;
|
|
|
|
/**
|
|
* Current Session Object that describe the terminal that the user has connected on
|
|
* - SessionViewdata - for ViewData sessions
|
|
* - SessionAnsitex - for ANSItex sessions
|
|
* @type {SessionAnsitex|SessionViewdata|null}
|
|
*
|
|
var so = null;
|
|
|
|
/**
|
|
* Current input field being edited when a frame has input fields
|
|
* - NULL means we are not inputting on a field
|
|
* @type {number|null}
|
|
*
|
|
var fn = null;
|
|
|
|
/**
|
|
* Current Input Field.
|
|
* @type {object}
|
|
*
|
|
var cf = null;
|
|
|
|
/**
|
|
* User has hit the inactivity timeout without any input
|
|
* @type {boolean}
|
|
*/
|
|
$timeout = FALSE;
|
|
|
|
/** Time the user hit the inactivity timeout */
|
|
$timer = time();
|
|
|
|
/**
|
|
* Current Control Method
|
|
* @type {null}
|
|
*
|
|
var cc = null;
|
|
|
|
/**
|
|
* We are receiving an extended key sequence (like a function key)
|
|
*/
|
|
$extendedkey = '';
|
|
|
|
/**
|
|
* ESC key sequence received
|
|
*/
|
|
$esc = FALSE;
|
|
|
|
$timewarp = FALSE; // Is timewarp active.
|
|
$timewarpalt = FALSE; // Alternative timewarp frame to get
|
|
|
|
/**
|
|
* Our current control method stack to execute
|
|
*/
|
|
$control = collect();
|
|
|
|
/** Current command being entered */
|
|
$cmd = '';
|
|
|
|
/** Current logged in user */
|
|
$user = new User;
|
|
|
|
$current = []; // Attributes about the current page @deprecate ? - store this information in Page::class
|
|
$current['prevmode'] = FALSE; // Previous mode - in case we need to go back to MODE_FIELD @deprecate ?
|
|
|
|
// Our BBS session loop
|
|
while ($action !== ACTION_TERMINATE) {
|
|
/** @var string $read The current input character */
|
|
$read = NULL;
|
|
$esc = FALSE;
|
|
|
|
try {
|
|
// If we have no action, read from the terminal
|
|
if ($action === NULL) {
|
|
// If a special key sequence is coming...
|
|
while (($esc || is_null($read)) && ($action !== ACTION_TERMINATE)) {
|
|
// Read a character from the client session
|
|
$read = $this->client->read($inkey_timeout,1);
|
|
|
|
// Handle ESC keys
|
|
// We are entering a special keyboard char.
|
|
if ($read === ESC) {
|
|
$this->log('debug',sprintf('%s:- READ SPECIAL KEY COMING',static::LOGKEY));
|
|
$esc = TRUE;
|
|
// We reduce our timeout, and assume the key is a function key. If the user pressed ESC we'll process that later
|
|
$inkey_timeout = 1;
|
|
|
|
// If we got the ESC, but no [ then re-put the ESC in the read, we loose the current key
|
|
// @todo We loose the current pressed key
|
|
} elseif ($esc && ! $extendedkey && $read !== '[') {
|
|
$this->log('alert',sprintf('%s:- READ SPECIAL KEY ABANDONED [%s] (%x)',static::LOGKEY,$read,ord($read)));
|
|
$esc = FALSE;
|
|
$inkey_timeout = self::TIMEOUT;
|
|
$read = ESC;
|
|
|
|
// Recognise when the ESC sequence has ended (with a ~ or ;)
|
|
} elseif ($esc && $extendedkey && ($read === '~' || $read === ';' || is_null($read))) {
|
|
switch ($extendedkey) {
|
|
case '[15': $read = FALSE; break; // F5
|
|
case '[17': $read = FALSE; break; // F6
|
|
case '[18': $read = FALSE; break; // F7
|
|
case '[19': $read = FALSE; break; // F8
|
|
case '[20': $read = FALSE; break; // F9
|
|
case '[21': $read = chr(26); break; // F10
|
|
case '[23': $read = FALSE; break; // F11
|
|
case '[24': $read = FALSE; break; // F12
|
|
default:
|
|
$this->log('alert',sprintf('%s:- READ UNKNOWN KEY [%s] (%x)',static::LOGKEY,$extendedkey,ord($extendedkey)));
|
|
$read = '';
|
|
}
|
|
|
|
$esc = FALSE;
|
|
$extendedkey = '';
|
|
$inkey_timeout = self::TIMEOUT;
|
|
|
|
// Record the character as an extended key
|
|
} elseif ($esc) {
|
|
$this->log('alert',sprintf('%s:- READ SPECIAL KEY [%s] (%x)',static::LOGKEY,$read,ord($read)));
|
|
|
|
$extendedkey .= $read;
|
|
$read = FALSE;
|
|
}
|
|
|
|
// Calculate idle timeouts
|
|
// If the user has exemption H we dont worry about timeout
|
|
if (is_null($read) && ((! $user->exists) || (! $user->hasExemption(User::FLAG_H)))) {
|
|
$this->log('debug',sprintf('%s:+ Empty read, evaluating timeouts',static::LOGKEY));
|
|
|
|
$idletime = config(sprintf('bbs.%s',($user->exists ? 'inactive_login' : 'inactive_nologin')));
|
|
|
|
// Terminate the user if they have been inactive too long.
|
|
if (time() > $timer+$idletime*1.5) {
|
|
$this->sendBaseline(RED.'INACTIVE');
|
|
$action = ACTION_TERMINATE;
|
|
$mode = NULL;
|
|
|
|
$this->log('alert',sprintf('%s:+ User INACTIVE - terminating...',static::LOGKEY));
|
|
|
|
// Idle warning - due to inactivity.
|
|
} elseif (time() > $timer+$idletime) {
|
|
$timeout = TRUE;
|
|
$this->sendBaseline(RED.'INACTIVITY DISCONNECT PENDING');
|
|
|
|
/*
|
|
if (cf) {
|
|
so.gotoxy(cf.x+cf.value.length,cf.y);
|
|
so.attr(cf.attribute);
|
|
}
|
|
*/
|
|
}
|
|
|
|
} else {
|
|
// If the user become active during inactivity, clear the baseline message
|
|
if ($timeout) {
|
|
echo 'so.baselineClear(false)';
|
|
|
|
/*
|
|
if (cf) {
|
|
so.gotoxy(cf.x+cf.value.length,cf.y);
|
|
so.attr(cf.attribute);
|
|
}
|
|
*/
|
|
}
|
|
|
|
$timer = time();
|
|
$timeout = FALSE;
|
|
}
|
|
|
|
// If we are in a control, we need to break here so that the control takes the input
|
|
if ($control->count())
|
|
break;
|
|
}
|
|
|
|
$this->log('debug',
|
|
sprintf('%s:+ Got: %s [%02x]: Mode: [%02x], Action: [%02x], Control: [%d]',
|
|
static::LOGKEY,
|
|
((ord($read) < 32) || (ord($read) > 250)) ? '.' : $read,
|
|
ord($read),
|
|
$mode,
|
|
$action,
|
|
$control->count()));
|
|
|
|
// Handle telnet IAC commands
|
|
if ((ord($read) === Telnet::TCP_IAC) && ((! $control->last()) || ($control->last()->name !== Telnet::class))) {
|
|
$this->log('debug',sprintf('%s:- We got a TELNET command',static::LOGKEY));
|
|
|
|
// Process telnet IAC commands
|
|
$control->push(Control::factory(Telnet::class,$this));
|
|
}
|
|
}
|
|
|
|
// Run CONTROL, only if we are not on the bottom line
|
|
if (($mode !== MODE_BL) && $control->count()) {
|
|
$this->log('debug',sprintf('%s:= Start CONTROL: Going to method: %s',static::LOGKEY,get_class($control->last())));
|
|
|
|
/*
|
|
// Capture our state when we enter this method.
|
|
if (! array_key_exists('control',$control->last()->state)) {
|
|
$control->last()->state['control'] = $control;
|
|
$control->last()->state['action'] = $action;
|
|
}
|
|
|
|
$control->last()->state['mode'] = $mode;
|
|
//$action = NULL;
|
|
*/
|
|
|
|
// Pass Control to Method
|
|
// @todo do we need $current?
|
|
$read = $control->last()->handle($read,$current);
|
|
//$mode = $control->last()->state['mode'];
|
|
|
|
if ($control->last()->complete) {
|
|
$this->log('info',sprintf('%s:- Complete CONTROL: %s',static::LOGKEY,get_class($control->last())));
|
|
$save = $control->pop();
|
|
|
|
/*
|
|
if ($control->count()) {
|
|
$control = $control->last()->state['control'];
|
|
|
|
} else {
|
|
$mode = $save->state['mode'];
|
|
$action = $save->state['action'];
|
|
$control = FALSE;
|
|
}
|
|
*/
|
|
}
|
|
|
|
$this->log('debug',sprintf('%s:= End CONTROL: Read %s [%02x]: Mode: [%02x], Action: [%02x], Control: [%d]',
|
|
static::LOGKEY,
|
|
((ord($read) < 32) || (ord($read) > 250)) ? '.' : $read,
|
|
ord($read),
|
|
$mode,
|
|
$action,
|
|
$control->count()));
|
|
}
|
|
|
|
$this->log('debug',sprintf('%s:= Start MODE: [%02x]',static::LOGKEY,$mode));
|
|
switch ($mode) {
|
|
// Key presses during field input.
|
|
case MODE_FIELD:
|
|
$cmd = '';
|
|
$action = NULL;
|
|
|
|
switch ($this->po->type) {
|
|
// Login frame.
|
|
case Page::FRAMETYPE_LOGIN:
|
|
switch ($read) {
|
|
case CR:
|
|
case HASH:
|
|
// If we are the main login screen, see if it is a new user
|
|
if ($this->po->isCUG(0)) {
|
|
if (strtolower($this->po->field_current->value) === 'new') {
|
|
$action = ACTION_GOTO;
|
|
$this->po->goto(config('bbs.register'));
|
|
$read = NULL;
|
|
$mode = NULL;
|
|
|
|
break 2;
|
|
}
|
|
}
|
|
|
|
//dump('not a public CUG');
|
|
|
|
break;
|
|
}
|
|
|
|
// Response frame.
|
|
case Page::FRAMETYPE_RESPONSE:
|
|
switch ($read) {
|
|
// End of field entry.
|
|
case CR:
|
|
case HASH:
|
|
// For response frames, see if we have any field actions
|
|
// We'll use the submit key's method (1) and execute any pre-action on fields
|
|
try {
|
|
if ($msg=$frame_submit_method->preSubmitField($this,$this->po->field_current)) {
|
|
$this->sendBaseline(RED.strtoupper($msg));
|
|
|
|
// Next Field
|
|
} else
|
|
$this->po->fieldNext();
|
|
|
|
} catch (\Exception $e) {
|
|
$this->log('alert',(sprintf('Pre field exception [%s] for [%s] on page [%s]',$e->getMessage(),$this->po->field_current->name,$this->po->page)));
|
|
$this->sendBaseline(RED.'PRE FIELD ERROR');
|
|
}
|
|
|
|
if ($x=$this->po->field_current) {
|
|
$this->client->send($this->moveCursor($x->X,$x->y).$this->po->attr($x->attribute),static::TIMEOUT);
|
|
$mode = MODE_FIELD;
|
|
|
|
// Finished all editable fields.
|
|
} else {
|
|
$action = ACTION_SUBMITRF;
|
|
}
|
|
|
|
break;
|
|
|
|
case STAR:
|
|
$current['prevmode'] = MODE_FIELD;
|
|
$action = ACTION_STAR;
|
|
|
|
break;
|
|
|
|
case KEY_DELETE:
|
|
if ($this->po->field_current->delete())
|
|
$this->client->send(LEFT.$this->po->field_current->pad.LEFT,static::TIMEOUT);
|
|
|
|
break;
|
|
|
|
case ESC:
|
|
break;
|
|
|
|
// Record Data Entry
|
|
default:
|
|
if (ord($read) > 31 && $this->po->field_current->append($read))
|
|
$this->client->send($this->po->field_current->mask ?: $read,static::TIMEOUT);
|
|
}
|
|
|
|
break;
|
|
|
|
// Other Frame Types - Shouldnt get here.
|
|
default:
|
|
$this->client->close();
|
|
|
|
throw new \Exception('Shouldnt get here',500);
|
|
}
|
|
|
|
break;
|
|
|
|
// Form submission: 1 to send, 2 not to send.
|
|
case MODE_SUBMITRF:
|
|
switch ($read) {
|
|
case '1':
|
|
// If we are in a control method, complete it
|
|
if ($control->count()) {
|
|
$control->last()->process();
|
|
|
|
} elseif ($this->po->isRoute(1)) {
|
|
$this->sendBaseline(RED.'NO ACTION PERFORMED');
|
|
$mode = MODE_RFSENT;
|
|
|
|
} elseif ($frame_submit_method) {
|
|
$frame_submit_method->fields_input = $this->po->fields_input;
|
|
|
|
try {
|
|
$result = $frame_submit_method->handle();
|
|
|
|
// Is this a user logging in?
|
|
if (
|
|
(($frame_submit_method instanceof Action\Login) || ($frame_submit_method instanceof Action\Register))
|
|
&& $result)
|
|
{
|
|
$user = $frame_submit_method->user;
|
|
$this->po->resetHistory();
|
|
$this->log('info',sprintf('User [%s] logged in',$user->name));
|
|
$this->po->next();
|
|
$this->po->showheader = TRUE;
|
|
$action = ACTION_GOTO;
|
|
$mode = NULL;
|
|
}
|
|
|
|
} catch (ActionMissingInputsException $e) {
|
|
$this->log('alert',sprintf('Missing [%s] on page [%s]',$e->getMessage(),$this->po->page));
|
|
|
|
$this->sendBaseline(RED.'MISSING DETAILS, TRY AGAIN *00');
|
|
$mode = NULL;
|
|
$action = NULL;
|
|
|
|
} catch (InvalidPasswordException $e) {
|
|
$this->sendBaseline(RED.'INVALID PASSWORD, TRY AGAIN *00');
|
|
$mode = NULL;
|
|
$action = NULL;
|
|
|
|
} catch (ModelNotFoundException $e) {
|
|
$this->sendBaseline(RED.'USER NOT FOUND, TRY AGAIN *00');
|
|
|
|
$mode = NULL;
|
|
$action = NULL;
|
|
|
|
} catch (\Exception $e) {
|
|
$this->log('error',sprintf('Exception [%s]during action: %s on line %d in %s',get_class($e),$e->getMessage(),$e->getLine(),$e->getFile()));
|
|
$this->sendBaseline(RED.'UNCAUGHT EXCEPTION, TRY AGAIN *00');
|
|
$mode = NULL;
|
|
$action = NULL;
|
|
}
|
|
|
|
$frame_submit_method = NULL;
|
|
|
|
} else {
|
|
$this->sendBaseline(RED.'NO method exists...');
|
|
|
|
$mode = MODE_RFSENT;
|
|
}
|
|
|
|
break;
|
|
|
|
case '2':
|
|
// // For response frames, see if we have any field actions
|
|
// We'll use key2 method and execute any post-undo on fields
|
|
// @todo Check if HASH is a valid next destination
|
|
$frame_submit_method = NULL;
|
|
$this->sendBaseline(MSG_NOTSENT);
|
|
$mode = MODE_RFNOTSENT;
|
|
|
|
// If a Control method was rejected, we can clear it
|
|
if ($control->count()) {
|
|
$save = $control->pop();
|
|
|
|
/*
|
|
if ($control->count()) {
|
|
//$control = $control->last()->state['control'];
|
|
|
|
} else {
|
|
$mode = $save->state['mode'];
|
|
$action = $save->state['action'];
|
|
$control = FALSE;
|
|
}
|
|
*/
|
|
}
|
|
|
|
break;
|
|
|
|
case STAR:
|
|
$action = ACTION_STAR;
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
// Response form ERROR
|
|
case MODE_RFERROR:
|
|
// Response form after NOT sending
|
|
case MODE_RFNOTSENT:
|
|
// Response form after Sent processing
|
|
case MODE_RFSENT:
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
|
|
if ($read === HASH) {
|
|
if ($this->po->isRoute(2)) {
|
|
$this->po->route(2);
|
|
|
|
} elseif ($this->po->haveNext()) {
|
|
$this->po->next();
|
|
|
|
} elseif ($this->po->isRoute(0)) {
|
|
$this->po->route(0);
|
|
|
|
// No further routes defined, go home.
|
|
} else {
|
|
$this->po->goto(0);
|
|
}
|
|
|
|
$action = ACTION_GOTO;
|
|
|
|
} elseif ($read === STAR) {
|
|
$action = ACTION_STAR;
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
// List of alternate frames has been presented
|
|
case MODE_WARPTO:
|
|
// @todo If we are in a control, we need to terminate it.
|
|
// @todo only enable warp for information frames
|
|
if (is_numeric($read) AND $read) {
|
|
$timewarpalt = $alts->get($read-1)->id;
|
|
$action = ACTION_GOTO;
|
|
|
|
} elseif ($read === '0') {
|
|
$action = ACTION_RELOAD;
|
|
}
|
|
|
|
break;
|
|
|
|
// Not doing anything in particular.
|
|
case MODE_COMPLETE:
|
|
case FALSE:
|
|
$this->log('debug','Idle');
|
|
$cmd = '';
|
|
|
|
switch ($read) {
|
|
case HASH:
|
|
$action = ACTION_NEXT;
|
|
break;
|
|
|
|
case STAR:
|
|
$action = ACTION_STAR;
|
|
break;
|
|
|
|
// Frame Routing
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
if ($this->po->isRoute($read)) {
|
|
$this->po->route($read);
|
|
$action = ACTION_GOTO;
|
|
|
|
} else {
|
|
$this->sendBaseline(ERR_ROUTE);
|
|
$mode = $action = NULL;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
// Currently accepting baseline input after a * was received
|
|
case MODE_BL:
|
|
// if it's a number, continue entry
|
|
if (strpos('0123456789',$read) !== FALSE) {
|
|
$this->client->send($read,static::TIMEOUT);
|
|
$this->blp++;
|
|
$cmd .= $read;
|
|
}
|
|
|
|
// If its a backspace, delete last input
|
|
if (($read === KEY_DELETE) && strlen($cmd)) {
|
|
$this->client->send(BS.' '.BS,static::TIMEOUT);
|
|
$this->blp--;
|
|
$cmd = substr($cmd,0,-1);
|
|
}
|
|
|
|
// if we hit a special numeric command, deal with it.
|
|
// Refresh page
|
|
if ($cmd === '00') {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$action = ACTION_RELOAD;
|
|
$cmd = '';
|
|
$mode = NULL;
|
|
|
|
break;
|
|
}
|
|
|
|
// Toggle Timewarp Mode
|
|
if ($cmd === '01') {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$timewarp = !$timewarp;
|
|
$this->sendBaseline(($timewarp ? MSG_TIMEWARP_ON : MSG_TIMEWARP_OFF));
|
|
$cmd = '';
|
|
$mode = NULL;
|
|
|
|
/*
|
|
if ($current['prevmode'] === MODE_FIELD) {
|
|
$mode = $current['prevmode'];
|
|
$current['prevmode'] = FALSE;
|
|
|
|
if ($x=$this->po->field_current()) {
|
|
// @todo This WHITE should be removed, and the color set to whatever is in the frame
|
|
$this->client->send($this->moveCursor($x->x+strlen($this->po->field_currentCurrentInput()),$x->y).CON.WHITE,static::TIMEOUT);
|
|
}
|
|
}
|
|
*/
|
|
|
|
break;
|
|
}
|
|
|
|
// Present Timewarp Frames
|
|
if ($cmd === '02') {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$mode = NULL;
|
|
$cmd = '';
|
|
|
|
if ($timewarp && $user->exists) {
|
|
$action = ACTION_INFO;
|
|
|
|
break;
|
|
}
|
|
|
|
$this->log('alert','Refusing timewarp for anonymous user: '.$this->po->page);
|
|
$this->sendBaseline(ERR_ROUTE);
|
|
}
|
|
|
|
// Invalid system pages.
|
|
if (preg_match('/^0[1367]/',$cmd)) {
|
|
$this->log('alert','Invalid System Page: '.$cmd);
|
|
|
|
$mode = NULL;
|
|
$cmd = '';
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$this->sendBaseline(ERR_ROUTE);
|
|
}
|
|
|
|
// Edit frame
|
|
// Catch if we are going to edit a child frame
|
|
if (preg_match('/^04/',$cmd) && preg_match('/^[a-z]$/',$read)) {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$this->po->goto(substr($cmd,2),$read);
|
|
$mode = NULL;
|
|
$cmd = '';
|
|
|
|
$action = ACTION_EDIT;
|
|
|
|
break;
|
|
}
|
|
|
|
// Bookmark page
|
|
if ($cmd === '05') {
|
|
$this->sendBaseline(RED.'NOT IMPLEMENTED YET?');
|
|
$mode = NULL;
|
|
|
|
break;
|
|
}
|
|
|
|
// Report a problem
|
|
if ($cmd === '08') {
|
|
$this->sendBaseline(RED.'NOT IMPLEMENTED YET?');
|
|
$mode = NULL;
|
|
|
|
break;
|
|
}
|
|
|
|
// Reload page
|
|
if ($cmd === '09') {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$action = ACTION_GOTO;
|
|
$cmd = '';
|
|
|
|
break;
|
|
}
|
|
|
|
// Another star aborts the command.
|
|
if ($read === STAR) {
|
|
$action = NULL;
|
|
$this->sendBaseline(Arr::get($current,'baseline',''));
|
|
$cmd = '';
|
|
|
|
if ($current['prevmode'] === MODE_FIELD) {
|
|
$mode = $current['prevmode'];
|
|
$current['prevmode'] = FALSE;
|
|
|
|
if (! ($x=$this->po->field_current))
|
|
$this->po->fieldPrev();
|
|
|
|
if ($x=$this->po->field_current) {
|
|
$this->client->send($this->moveCursor($x->x,$x->y).CON.$this->po->attr($x->attribute),static::TIMEOUT);
|
|
$this->client->send(str_repeat($x->pad,$x->size),static::TIMEOUT);
|
|
$this->client->send($this->moveCursor($x->x,$x->y),static::TIMEOUT);
|
|
$x->clear();
|
|
}
|
|
|
|
} else {
|
|
$mode = FALSE;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Complete request
|
|
if (($read === HASH) || ($read === CR)) {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$timewarpalt = FALSE;
|
|
|
|
// If input is in a control, terminate it
|
|
if ($control) {
|
|
$control->pop();
|
|
//$control = FALSE;
|
|
|
|
// Our method count should be zero
|
|
if ($control->count()) {
|
|
dump($control);
|
|
throw new \Exception('Method count should be zero, but its not...',500);
|
|
}
|
|
}
|
|
|
|
// Nothing typed between * and #
|
|
// *# means go back
|
|
if ($cmd === '') {
|
|
$action = ACTION_BACK;
|
|
|
|
// Edit Frame
|
|
} elseif (preg_match('/^04/',$cmd)) {
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$action = ACTION_EDIT;
|
|
$this->po->goto(substr($cmd,2) ?: $this->po->frame);
|
|
|
|
} else {
|
|
$this->po->goto($cmd);
|
|
$action = ACTION_GOTO;
|
|
}
|
|
|
|
// Clear the command, we are finished processing
|
|
$cmd = '';
|
|
$mode = FALSE;
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
$this->log('debug','Not sure what we were doing?');
|
|
}
|
|
|
|
$this->log('debug',sprintf('%s:= End MODE: Read %s [%02x]: Mode: [%02x], Action: [%02x], Control: [%d]',
|
|
static::LOGKEY,
|
|
((ord($read) < 32) || (ord($read) > 250)) ? '.' : $read,
|
|
ord($read),
|
|
$mode,
|
|
$action,
|
|
$control->count()));
|
|
|
|
// This section performs some action if it is deemed necessary
|
|
$this->log('debug',sprintf('%s:= Start ACTION: [%02x]',static::LOGKEY,$action));
|
|
switch ($action) {
|
|
case ACTION_STAR:
|
|
// If there is something on the baseline, lets preserve it
|
|
if ($this->blp) {
|
|
$current['baseline'] = $this->baseline;
|
|
}
|
|
|
|
$this->sendBaseline(GREEN.STAR,TRUE);
|
|
$this->client->send(CON,static::TIMEOUT);
|
|
$action = NULL;
|
|
$mode = MODE_BL;
|
|
|
|
break;
|
|
|
|
case ACTION_SUBMITRF:
|
|
$action = NULL;
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
$this->sendBaseline(MSG_SENDORNOT);
|
|
$mode = MODE_SUBMITRF;
|
|
|
|
break;
|
|
|
|
// Edit Frame
|
|
case ACTION_EDIT:
|
|
$this->log('debug','Editing frame:',[$this->po->page]);
|
|
|
|
$next_fo = NULL;
|
|
// If we are editing a different frame, load it
|
|
try {
|
|
$this->po->goto($this->po->frame,$this->po->index);
|
|
|
|
} catch (ModelNotFoundException $e) {
|
|
try {
|
|
$this->po->new($this->po->frame,$this->po->index);
|
|
|
|
} catch (ParentNotFoundException $e) {
|
|
$this->sendBaseline('PARENT_NOT_FOUND');
|
|
$action = NULL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
//$control = CONTROL_EDIT;
|
|
$control->push(Control::factory('editframe',$this,['fo'=>$next_fo]));
|
|
//$control->last()->state['control'] = $control;
|
|
//$control->last()->state['action'] = FALSE;
|
|
//$control->last()->state['mode'] = MODE_FIELD;
|
|
$action = NULL;
|
|
*/
|
|
|
|
break;
|
|
|
|
// Go Backwards
|
|
case ACTION_BACK:
|
|
// If there is no next page, we'll refresh the current page.
|
|
if ($this->po->back())
|
|
$this->log('debug','Backing up to:'.$this->po->page);
|
|
|
|
// Go to next index frame.
|
|
case ACTION_NEXT:
|
|
// We need this extra test in case we come from ACTION_BACK
|
|
if ($action === ACTION_NEXT)
|
|
$this->po->next();
|
|
|
|
// Look for requested page - charge for it to be loaded.
|
|
case ACTION_GOTO:
|
|
// If our target frame is 0, we'll go to our home page
|
|
if (($this->po->frame === 0) && ($this->po->index === 'a'))
|
|
$this->po->goto(config('bbs.'.($user->exists ? 'home' : 'welcome')));
|
|
|
|
// If we wanted a "Searching..." message, this is where to put it.
|
|
try {
|
|
// Store our next frame in a temporary var while we determine if it can be displayed
|
|
$timewarpalt
|
|
? $this->po->get($timewarpalt)
|
|
: $this->po->load();
|
|
|
|
$this->log('debug',sprintf('Fetched frame: [%s] (%d)',$this->po->page,$this->po->id));
|
|
|
|
} catch (ModelNotFoundException $e) {
|
|
$this->log('alert',sprintf('Frame doesnt exist: [%s]',$this->po->page));
|
|
|
|
// @todo Make sure parent frame exists, or display error
|
|
$this->sendBaseline(ERR_PAGE);
|
|
$mode = $action = NULL;
|
|
|
|
break;
|
|
}
|
|
|
|
// Is there a user logged in
|
|
if ($user->exists) {
|
|
if ($this->po->public && $this->po->access) {
|
|
if (($this->po->type === Page::FRAMETYPE_LOGIN) && $user->isMemberCUG($this->po->cug)) {
|
|
$this->sendBaseline(ERR_USER_ALREADYMEMBER);
|
|
$this->po->back();
|
|
$mode = $action = NULL;
|
|
$this->log('alert',sprintf('Frame Denied - Already Member: [%s] (%d)',$this->po->page,$this->po->id));
|
|
|
|
break;
|
|
}
|
|
|
|
// If this is a login frame and the user is already a member.
|
|
} else {
|
|
if (! $this->po->isOwner($user)) {
|
|
if (! $this->po->access) {
|
|
$this->sendBaseline(ERR_PAGE);
|
|
$this->log('alert',sprintf('Frame Denied - In Accessible: [%s] (%d)',$this->po->page,$this->po->id));
|
|
$this->po->back();
|
|
$mode = $action = NULL;
|
|
|
|
break;
|
|
}
|
|
|
|
if (! $user->isMemberCUG($this->po->cug)) {
|
|
$this->sendBaseline(ERR_PRIVATE);
|
|
$this->po->back();
|
|
$mode = $action = NULL;
|
|
$this->log('alert',sprintf('Frame Denied - Not in CUG [%d]: [%s] (%d)',$this->po->cug,$this->po->page,$this->po->id));
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// Is this a public frame in CUG 0?
|
|
if ((! $this->po->isCUG(0)) || (! $this->po->public)) {
|
|
$this->sendBaseline(ERR_PAGE);
|
|
$this->po->back();
|
|
$mode = $action = NULL;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$timewarpalt = NULL;
|
|
|
|
// Build our page for rendering
|
|
$this->po->build();
|
|
|
|
// drop into
|
|
case ACTION_RELOAD:
|
|
// Clear the baseline history
|
|
$this->sendBaseline('');
|
|
$current['baseline'] = '';
|
|
|
|
$output = ($this->po->cls ? CLS : HOME).$this->po;
|
|
|
|
if ($timewarpalt)
|
|
$this->sendBaseline(sprintf(MSG_TIMEWARP_TO,$this->po->created_at->format('Y-m-d H:i:s')));
|
|
|
|
switch ($this->po->type) {
|
|
default:
|
|
// Standard Frame
|
|
case Page::FRAMETYPE_INFO:
|
|
$this->client->send($output,static::TIMEOUT);
|
|
$mode = $action = NULL;
|
|
|
|
break;
|
|
|
|
// Login Frame.
|
|
case Page::FRAMETYPE_LOGIN:
|
|
$this->client->send($output,static::TIMEOUT);
|
|
$action = NULL;
|
|
$output = '';
|
|
|
|
// If this is the registration page
|
|
if ($this->po->page === config('bbs.register').'a') {
|
|
//$control = CONTROL_METHOD;
|
|
$control->push(Control::factory('register',$this));
|
|
/*
|
|
$control->last()->state['control'] = $control;
|
|
$control->last()->state['action'] = FALSE;
|
|
$control->last()->state['mode'] = MODE_FIELD;
|
|
*/
|
|
}
|
|
|
|
// Active Frame. Prestel uses this for a Response Frame.
|
|
case Page::FRAMETYPE_RESPONSE:
|
|
$this->client->send($output,static::TIMEOUT);
|
|
|
|
// Our submit method
|
|
$frame_submit_method = $this->po->method(1);
|
|
|
|
if ($this->po->fields_input->count()) {
|
|
$this->po->fieldReset();
|
|
|
|
if ($x=$this->po->fieldNext()) {
|
|
$mode = MODE_FIELD;
|
|
$this->client->send($this->moveCursor($x->x,$x->y).CON.$this->po->attr($x->attribute),static::TIMEOUT);
|
|
|
|
// There were no editable fields.
|
|
} else {
|
|
$mode = MODE_COMPLETE;
|
|
$this->client->send(COFF,static::TIMEOUT);
|
|
}
|
|
|
|
} else {
|
|
$mode = NULL;
|
|
}
|
|
|
|
$action = NULL;
|
|
|
|
break;
|
|
|
|
// External Frame - run by a control.
|
|
case Page::FRAMETYPE_EXTERNAL:
|
|
$external = explode(' ',(string)$this->po);
|
|
$x = Control::factory(array_shift($external),$this,$external);
|
|
|
|
if (! $x)
|
|
{
|
|
$this->sendBaseline(ERR_PAGE);
|
|
$mode = $action = NULL;
|
|
break;
|
|
}
|
|
|
|
$control->push($x);
|
|
//$control = CONTROL_METHOD;
|
|
$action = NULL;
|
|
|
|
break;
|
|
|
|
// Terminate Frame
|
|
case Page::FRAMETYPE_TERMINATE:
|
|
$this->client->send($output,static::TIMEOUT);
|
|
$action = ACTION_TERMINATE;
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
// Timewarp Mode
|
|
case ACTION_INFO:
|
|
$mode = $action = NULL;
|
|
$cmd = '';
|
|
$y = 0;
|
|
|
|
$output = $this->moveCursor(0,$y++).WHITE.NEWBG.RED.'TIMEWARP INFO FOR Pg.'.BLUE.$this->po->page.WHITE;
|
|
$output .= $this->moveCursor(0,$y++).WHITE.NEWBG.BLUE.'Dated : ' .substr($this->po->created_at->format('j F Y').str_repeat(' ',27),0,27);
|
|
|
|
$alts = $this->po->alts();
|
|
|
|
if (count($alts)) {
|
|
$n = 1;
|
|
|
|
$output .= $this->moveCursor(0,$y++).WHITE.NEWBG.RED.'ALTERNATIVE VERSIONS:'.str_repeat(' ',16);
|
|
|
|
foreach ($alts as $o) {
|
|
$date = $o->created_at->format('d M Y');
|
|
|
|
$line = WHITE.NEWBG;
|
|
|
|
if ($timewarp) {
|
|
$line .= RED.$n++;
|
|
}
|
|
|
|
$line .= BLUE.$date.' '.$o->note;
|
|
|
|
$output .= $this->moveCursor(0,$y++).$line.str_repeat(' ',$this->po->width-$this->po->strlenv($line));
|
|
}
|
|
|
|
if ($timewarp) {
|
|
$mode = MODE_WARPTO;
|
|
}
|
|
|
|
}
|
|
|
|
$this->client->send($output,static::TIMEOUT);
|
|
|
|
break;
|
|
}
|
|
|
|
$this->log('debug',sprintf('%s:= End ACTION: Read %s [%02x]: Mode: [%02x], Action: [%02x], Control: [%d]',
|
|
static::LOGKEY,
|
|
((ord($read) < 32) || (ord($read) > 250)) ? '.' : $read,
|
|
ord($read),
|
|
$mode,
|
|
$action,
|
|
$control->count()));
|
|
|
|
/*
|
|
// Did the client disconnect
|
|
if ($read === NULL || socket_last_error()) {
|
|
$this->log('debug',sprintf('Client Disconnected: %s',$this->client->address_remote),['read'=>$read,'socket_last_error'=>socket_strerror(socket_last_error())]);
|
|
$this->client->close();
|
|
|
|
return;
|
|
}
|
|
*/
|
|
|
|
// @todo Turn cursor on
|
|
|
|
// Something bad happened. We'll log it and then disconnect.
|
|
} catch (\Exception $e) {
|
|
$this->log('error',sprintf('! ERROR: %s (%s)',$e->getMessage(),get_class($e)),['line'=>$e->getLine(),'file'=>$e->getFile()]);
|
|
|
|
$this->sendBaseline(ERR_SYSTEM);
|
|
$action = $mode = NULL;
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
$this->log('debug',sprintf('Disconnected: %s',$this->client->address_remote));
|
|
}
|
|
} |