clrghouz/app/Classes/BBS/Server.php
2024-05-28 12:37:52 +10:00

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));
}
}