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