BBS ported from vbbs
This commit is contained in:
parent
364815e8af
commit
b43784574c
101
app/Classes/BBS/Control.php
Normal file
101
app/Classes/BBS/Control.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS;
|
||||
|
||||
use App\Classes\BBS\Control\EditFrame;
|
||||
use App\Classes\BBS\Control\Register;
|
||||
use App\Classes\BBS\Control\Telnet;
|
||||
|
||||
abstract class Control
|
||||
{
|
||||
const prefix = 'App\Classes\Control\\';
|
||||
|
||||
// Has this control class finished with input
|
||||
protected bool $complete = FALSE;
|
||||
|
||||
// The server object that is running this control class
|
||||
protected Server $so;
|
||||
|
||||
/**
|
||||
* What is the state of the server outside of this control.
|
||||
* Should only contain
|
||||
* + mode = Mode to follow outside of the control method
|
||||
* + action = Action to run after leaving the control method
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $state = [];
|
||||
|
||||
abstract public function handle(string $read): string;
|
||||
|
||||
public static function factory(string $name,Server $so,array $args=[])
|
||||
{
|
||||
switch ($name) {
|
||||
case 'editframe':
|
||||
return new EditFrame($so,$args);
|
||||
|
||||
case 'register':
|
||||
return new Register($so);
|
||||
|
||||
case 'telnet':
|
||||
return new Telnet($so);
|
||||
|
||||
default:
|
||||
$c = (class_exists($name)) ? $name : self::prefix.$name;
|
||||
$o = class_exists($c) ? new $c($so,$args) : NULL;
|
||||
|
||||
$so->log('debug',sprintf(($o ? 'Executing: %s' : 'Class doesnt exist: %s'),$c));
|
||||
|
||||
return $o;
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(Server $so,array $args=[])
|
||||
{
|
||||
$this->so = $so;
|
||||
|
||||
// Boot control, preparing anything before keyboard entry
|
||||
$this->boot();
|
||||
|
||||
$this->so->log('info',sprintf('Initialised control %s',get_class($this)));
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'complete':
|
||||
return $this->complete;
|
||||
|
||||
case 'name':
|
||||
return get_class($this);
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('%s:! Unknown key: %s',static::LOGKEY,$key));
|
||||
}
|
||||
}
|
||||
// Default boot method if a child class doesnt have one.
|
||||
|
||||
protected function boot()
|
||||
{
|
||||
$this->state['mode'] = FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has control completed?
|
||||
* @deprecated use $this->complete;
|
||||
*/
|
||||
public function complete()
|
||||
{
|
||||
return $this->complete;
|
||||
}
|
||||
|
||||
/**
|
||||
* If completing an Action frame, this will be called to submit the data.
|
||||
*
|
||||
* Ideally this should be overridden in a child class.
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$this->complete = TRUE;
|
||||
}
|
||||
}
|
198
app/Classes/BBS/Control/EditFrame.php
Normal file
198
app/Classes/BBS/Control/EditFrame.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Control;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Classes\BBS\Control;
|
||||
use App\Classes\BBS\Frame;
|
||||
use App\Classes\BBS\Server;
|
||||
|
||||
/**
|
||||
* Class Edit Frame handles frame editing
|
||||
*
|
||||
* @package App\Classes\Control
|
||||
*/
|
||||
class EditFrame extends Control
|
||||
{
|
||||
private $x = 1;
|
||||
private $y = 1;
|
||||
|
||||
// The frame applicable for this control (not the current rendered frame, thats in $so)
|
||||
protected $fo = NULL;
|
||||
|
||||
public function __construct(Server $so,array $args=[])
|
||||
{
|
||||
if (! $args OR ! Arr::get($args,'fo') OR (! $args['fo'] instanceof Frame))
|
||||
throw new \Exception('Missing frame to Edit');
|
||||
|
||||
$this->fo = $args['fo'];
|
||||
|
||||
parent::__construct($so);
|
||||
}
|
||||
|
||||
protected function boot()
|
||||
{
|
||||
// Clear screen and setup edit.
|
||||
$this->so->co->send(CLS.HOME.DOWN.CON);
|
||||
|
||||
// @todo Add page number + "EDIT" (prob only required for login pages which dont show page num)
|
||||
$this->so->co->send($this->fo->raw().$this->so->moveCursor(1,2));
|
||||
|
||||
$this->updateBaseline();
|
||||
}
|
||||
|
||||
public function handle(string $read): string
|
||||
{
|
||||
static $esc = FALSE;
|
||||
static $brace = FALSE;
|
||||
static $out = '';
|
||||
static $key = '';
|
||||
|
||||
$out .= $read;
|
||||
|
||||
switch ($read)
|
||||
{
|
||||
case 'A':
|
||||
if ($esc AND $brace)
|
||||
{
|
||||
$this->y--;
|
||||
if ($this->y < 1) {
|
||||
$this->y = 1;
|
||||
$out = '';
|
||||
}
|
||||
|
||||
$brace = $esc = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'B':
|
||||
if ($esc AND $brace)
|
||||
{
|
||||
$this->y++;
|
||||
if ($this->y > $this->fo->frame_length()) {
|
||||
$this->y = $this->fo->frame_length();
|
||||
$out = '';
|
||||
}
|
||||
|
||||
$brace =$esc = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'C':
|
||||
if ($esc AND $brace)
|
||||
{
|
||||
$this->x++;
|
||||
if ($this->x > $this->fo->frame_width()) {
|
||||
$this->x = $this->fo->frame_width();
|
||||
$out = '';
|
||||
}
|
||||
|
||||
$brace =$esc = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'D':
|
||||
if ($esc AND $brace)
|
||||
{
|
||||
$this->x--;
|
||||
if ($this->x < 1) {
|
||||
$this->x = 1;
|
||||
$out = '';
|
||||
}
|
||||
|
||||
$brace = $esc = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case '[':
|
||||
if ($esc)
|
||||
$brace = TRUE;
|
||||
break;
|
||||
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '0':
|
||||
if ($esc AND $brace) {
|
||||
$key .= $read;
|
||||
} else {
|
||||
$this->x++;
|
||||
}
|
||||
break;
|
||||
|
||||
case '~':
|
||||
if ($esc AND $brace)
|
||||
{
|
||||
switch ($key)
|
||||
{
|
||||
// F9 Pressed
|
||||
case 20:
|
||||
break;
|
||||
|
||||
// F10 Pressed
|
||||
case 21:
|
||||
$this->complete = TRUE;
|
||||
$this->state = ['action'=>ACTION_GOTO,'mode'=>NULL];
|
||||
break;
|
||||
}
|
||||
|
||||
$brace = $esc = FALSE;
|
||||
$key = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ESC;
|
||||
$esc = TRUE;
|
||||
break;
|
||||
|
||||
case LF: $this->y++; break;
|
||||
case CR; $this->x = 1; break;
|
||||
|
||||
default:
|
||||
if ($esc)
|
||||
$esc = FALSE;
|
||||
|
||||
$this->x++;
|
||||
}
|
||||
|
||||
if (! $esc)
|
||||
{
|
||||
printf(" . SENDING OUT: %s\n",$out);
|
||||
$this->so->co->send($out);
|
||||
$this->updateBaseline();
|
||||
$out = '';
|
||||
}
|
||||
|
||||
printf(" . X:%d,Y:%d,C:%s,ESC:%s\n",
|
||||
$this->x,
|
||||
$this->y,
|
||||
(ord($read) < 32 ? '.' : $read),
|
||||
($esc AND $brace) ? 'TRUE' : 'FALSE');
|
||||
|
||||
return $read;
|
||||
}
|
||||
|
||||
public function updateBaseline()
|
||||
{
|
||||
$this->so->sendBaseline(
|
||||
$this->so->co,
|
||||
sprintf('%02.0f:%02.0f]%s'.RESET.'[',
|
||||
$this->y,
|
||||
$this->x,
|
||||
($this->fo->attr($this->x,$this->y) != '-' ? ESC.'['.$this->fo->attr($this->x,$this->y) : '').$this->fo->char($this->x,$this->y),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function process()
|
||||
{
|
||||
dump(__METHOD__);
|
||||
}
|
||||
}
|
158
app/Classes/BBS/Control/Register.php
Normal file
158
app/Classes/BBS/Control/Register.php
Normal file
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Control;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Classes\BBS\Control;
|
||||
use App\Mail\SendToken;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* Class Register handles registration
|
||||
*
|
||||
* @todo REMOVE the force .WHITE at the end of each sendBaseline()
|
||||
* @package App\Classes\Control
|
||||
*/
|
||||
class Register extends Control
|
||||
{
|
||||
private $data = [];
|
||||
|
||||
protected function boot()
|
||||
{
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Select User Name'.WHITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Registration Form Input
|
||||
*
|
||||
* This function assumes the form has 7 fields in a specific order.
|
||||
*
|
||||
* @todo Make this form more dynamic, or put some configuration in a config file, so that there is flexibility
|
||||
* in field placement.
|
||||
* @param string $read
|
||||
* @param array $current
|
||||
* @return string
|
||||
*/
|
||||
public function handle(string $read,array $current=[]): string
|
||||
{
|
||||
// Ignore LF (as a result of pressing ENTER)
|
||||
if ($read == LF)
|
||||
return '';
|
||||
|
||||
// If we got a # we'll be completing field input.
|
||||
if ($read == HASH OR $read == CR) {
|
||||
// Does our field have data...
|
||||
if ($x=$this->so->fo->getFieldCurrentInput()) {
|
||||
switch ($this->so->fo->getFieldId()) {
|
||||
// Username
|
||||
case 0:
|
||||
// See if the requested username already exists
|
||||
if (User::where('login',$x)->exists()) {
|
||||
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Enter Real Name'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Real Name
|
||||
case 1:
|
||||
//$this->data['name'] = $x;
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Enter Email Address'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Email Address
|
||||
case 2:
|
||||
if (Validator::make(['email'=>$x],[
|
||||
'email'=>'email',
|
||||
])->fails()) {
|
||||
$this->so->sendBaseline($this->so->co,RED.'INVALID EMAIL ADDRESS'.WHITE);
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// See if the requested email already exists
|
||||
if (User::where('email',$x)->exists()) {
|
||||
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->data['email'] = $x;
|
||||
$this->data['token'] = sprintf('%06.0f',rand(0,999999));
|
||||
|
||||
$this->so->sendBaseline($this->so->co,YELLOW.'PROCESSING...'.WHITE);
|
||||
Mail::to($this->data['email'])->sendNow(new SendToken($this->data['token']));
|
||||
|
||||
if (Mail::failures()) {
|
||||
dump('Failure?');
|
||||
|
||||
dump(Mail::failures());
|
||||
}
|
||||
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Enter Password'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Enter Password
|
||||
case 3:
|
||||
$this->data['password'] = $x;
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Confirm Password'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Confirm Password
|
||||
case 4:
|
||||
if ($this->data['password'] !== $x) {
|
||||
$this->so->sendBaseline($this->so->co,RED.'PASSWORD DOESNT MATCH, *09 TO START AGAIN'.WHITE);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Enter Location'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Enter Location
|
||||
case 5:
|
||||
$this->so->sendBaseline($this->so->co,GREEN.'Enter TOKEN emailed to you'.WHITE);
|
||||
|
||||
break;
|
||||
|
||||
// Enter Token
|
||||
case 6:
|
||||
if ($this->data['token'] !== $x) {
|
||||
$this->so->sendBaseline($this->so->co,RED.'TOKEN DOESNT MATCH, *09 TO START AGAIN'.WHITE);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->complete = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->so->sendBaseline($this->so->co,RED.'HUH?');
|
||||
}
|
||||
|
||||
} else {
|
||||
// If we are MODE_BL, we need to return the HASH, otherwise nothing.
|
||||
if (in_array($this->state['mode'],[MODE_BL,MODE_SUBMITRF,MODE_RFNOTSENT])) {
|
||||
return $read;
|
||||
|
||||
} else {
|
||||
$this->so->sendBaseline($this->so->co,RED.'FIELD REQUIRED...'.WHITE);
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $read;
|
||||
}
|
||||
}
|
199
app/Classes/BBS/Control/Telnet.php
Normal file
199
app/Classes/BBS/Control/Telnet.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Control;
|
||||
|
||||
use App\Classes\BBS\Control;
|
||||
|
||||
/**
|
||||
* Class Telnet
|
||||
*
|
||||
* This class looks after any telnet session commands
|
||||
*
|
||||
* TELNET http://pcmicro.com/netfoss/telnet.html
|
||||
*
|
||||
* @package App\Classes\Control
|
||||
*/
|
||||
final class Telnet extends Control
|
||||
{
|
||||
protected const LOGKEY = 'CT-';
|
||||
|
||||
/** @var int Data Byte */
|
||||
public const TCP_IAC = 0xff;
|
||||
/** @var int Indicates the demand that the other party stop performing, or confirmation that you are no
|
||||
longer expecting the other party to perform, the indicated option */
|
||||
public const TCP_DONT = 0xfe;
|
||||
/** @var int Indicates the request that the other party perform, or confirmation that you are expecting
|
||||
the other party to perform, the indicated option. */
|
||||
public const TCP_DO = 0xfd;
|
||||
/** @var int Indicates the refusal to perform, or continue performing, the indicated option. */
|
||||
public const TCP_WONT = 0xfc;
|
||||
/** @var int Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option. */
|
||||
public const TCP_WILL = 0xfb;
|
||||
/** @var int Indicates that what follows is sub-negotiation of the indicated option. */
|
||||
public const TCP_SB = 0xfa;
|
||||
|
||||
/** @var int The GA signal. */
|
||||
public const TCP_GA = 0xf9;
|
||||
/** @var int Erase Line. */
|
||||
public const TCP_EL = 0xf8;
|
||||
/** @var int Erase character. */
|
||||
public const TCP_EC = 0xf7;
|
||||
/** @var int Are you there? */
|
||||
public const TCP_AYT = 0xf6;
|
||||
/** @var int About output */
|
||||
public const TCP_AO = 0xf5;
|
||||
/** @var int Interrupt Process. */
|
||||
public const TCP_IP = 0xf4;
|
||||
/** @var int Break. */
|
||||
public const TCP_BREAK = 0xf3;
|
||||
/** @var int The data stream portion of a Synch. This should always be accompanied by a TCP Urgent notification. */
|
||||
public const TCP_DM = 0xf2;
|
||||
/** @var int No operation. */
|
||||
public const TCP_NOPT = 0xf1;
|
||||
/** @var int End of sub-negotiation parameters. */
|
||||
public const TCP_SE = 0xf0;
|
||||
|
||||
public const TCP_BINARY = 0x00;
|
||||
public const TCP_OPT_ECHO = 0x01;
|
||||
public const TCP_OPT_SUP_GOAHEAD = 0x03;
|
||||
public const TCP_OPT_TERMTYPE = 0x18;
|
||||
public const TCP_OPT_WINDOWSIZE = 0x1f;
|
||||
public const TCP_OPT_LINEMODE = 0x22;
|
||||
|
||||
private bool $option = FALSE;
|
||||
private string $note;
|
||||
private string $terminal = '';
|
||||
|
||||
public static function send_iac($key): string
|
||||
{
|
||||
$send = chr(self::TCP_IAC);
|
||||
|
||||
switch ($key) {
|
||||
case 'are_you_there':
|
||||
$send .= chr(self::TCP_AYT);
|
||||
break;
|
||||
|
||||
case 'do_echo':
|
||||
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_ECHO);
|
||||
break;
|
||||
case 'dont_echo':
|
||||
$send .= chr(self::TCP_DONT).chr(self::TCP_OPT_ECHO);
|
||||
break;
|
||||
case 'will_echo':
|
||||
$send .= chr(self::TCP_WILL).chr(self::TCP_OPT_ECHO);
|
||||
break;
|
||||
case 'wont_echo':
|
||||
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_ECHO);
|
||||
break;
|
||||
|
||||
case 'do_opt_termtype':
|
||||
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_TERMTYPE);
|
||||
break;
|
||||
|
||||
case 'do_suppress_goahead':
|
||||
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_SUP_GOAHEAD);
|
||||
break;
|
||||
|
||||
case 'sn_end':
|
||||
$send .= chr(self::TCP_SE);
|
||||
break;
|
||||
|
||||
case 'sn_start':
|
||||
$send .= chr(self::TCP_SB);
|
||||
break;
|
||||
|
||||
case 'wont_linemode':
|
||||
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_LINEMODE);
|
||||
break;
|
||||
|
||||
case 'will_xmit_binary':
|
||||
$send .= chr(self::TCP_WILL).chr(self::TCP_BINARY);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('%s:! Unknown key: %s',$key));
|
||||
}
|
||||
|
||||
return $send;
|
||||
}
|
||||
|
||||
public function handle(string $read): string
|
||||
{
|
||||
$this->so->log('debug',sprintf('%s:+ Session Char [%02x] (%c)',self::LOGKEY,ord($read),$read),['complete'=>$this->complete,'option'=>$this->option]);
|
||||
|
||||
switch (ord($read)) {
|
||||
// Command being sent.
|
||||
case self::TCP_IAC:
|
||||
$this->complete = FALSE;
|
||||
$this->note = 'IAC ';
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_SB:
|
||||
$this->option = TRUE;
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_SE:
|
||||
$this->option = FALSE;
|
||||
$this->complete = TRUE;
|
||||
$this->so->log('debug',sprintf('%s:%% Session Terminal: %s',self::LOGKEY,$this->terminal));
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_DO:
|
||||
$this->note .= 'DO ';
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_WILL:
|
||||
$this->note .= 'WILL ';
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_WONT:
|
||||
$this->note .= 'WONT ';
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_OPT_TERMTYPE:
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_OPT_ECHO:
|
||||
$this->note .= 'ECHO';
|
||||
$this->complete = TRUE;
|
||||
|
||||
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_OPT_SUP_GOAHEAD:
|
||||
$this->note .= 'SUPPRESS GO AHEAD';
|
||||
$this->complete = TRUE;
|
||||
|
||||
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
|
||||
|
||||
break;
|
||||
|
||||
case self::TCP_OPT_WINDOWSIZE:
|
||||
$this->note .= 'WINDOWSIZE';
|
||||
$this->complete = TRUE;
|
||||
|
||||
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($this->option && $read)
|
||||
$this->terminal .= $read;
|
||||
else
|
||||
$this->so->log('debug',sprintf('%s:= Unhandled char in session_init: [%02x] (%c)',self::LOGKEY,ord($read),$read));
|
||||
}
|
||||
|
||||
if ($this->complete)
|
||||
$this->so->log('debug',sprintf('%s:= TELNET control COMPLETE',self::LOGKEY));
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
56
app/Classes/BBS/Control/Test.php
Normal file
56
app/Classes/BBS/Control/Test.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Control;
|
||||
|
||||
use App\Classes\BBS\Control;
|
||||
|
||||
/**
|
||||
* Class Test
|
||||
*
|
||||
* This is a test class for Control Validation Processing
|
||||
*
|
||||
* @package App\Classes\Control
|
||||
*/
|
||||
class Test extends Control
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
$this->so->co->send(CLS.HOME.DOWN.CON);
|
||||
|
||||
$this->so->co->send('Press 1, or 2, or 4, 0 to end.');
|
||||
}
|
||||
|
||||
// @todo *00/09 doesnt work
|
||||
public function handle(string $read): string
|
||||
{
|
||||
switch ($read)
|
||||
{
|
||||
case 0:
|
||||
$this->complete = TRUE;
|
||||
$read = '';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$this->so->co->send('You pressed ONE.');
|
||||
$read = '';
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$this->so->co->send('You pressed TWO.');
|
||||
$read = '';
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$this->so->co->send('You pressed THREE.');
|
||||
$read = '';
|
||||
break;
|
||||
|
||||
case 4:
|
||||
$this->so->co->send('You pressed FOUR.');
|
||||
$read = '';
|
||||
break;
|
||||
}
|
||||
|
||||
return $read;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ActionMissingInputsException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/InvalidPasswordException.php
Normal file
9
app/Classes/BBS/Exceptions/InvalidPasswordException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidPasswordException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/NoRouteException.php
Normal file
9
app/Classes/BBS/Exceptions/NoRouteException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NoRouteException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/ParentNotFoundException.php
Normal file
9
app/Classes/BBS/Exceptions/ParentNotFoundException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ParentNotFoundException extends Exception
|
||||
{
|
||||
}
|
73
app/Classes/BBS/Frame/Action.php
Normal file
73
app/Classes/BBS/Frame/Action.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Frame;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
|
||||
use App\Classes\BBS\Frame\Action\{Login,Register};
|
||||
use App\Classes\BBS\Server;
|
||||
use App\Models\User;
|
||||
|
||||
abstract class Action
|
||||
{
|
||||
private Collection $fields_input;
|
||||
|
||||
protected User $uo;
|
||||
|
||||
public const actions = [
|
||||
'login' => Login::class,
|
||||
'register' => Register::class,
|
||||
];
|
||||
|
||||
protected const fields = [];
|
||||
|
||||
abstract public function handle(): bool;
|
||||
abstract public function preSubmitField(Server $server,Field $field): ?string;
|
||||
|
||||
public static function factory(string $class): self
|
||||
{
|
||||
if (array_key_exists($class,self::actions)) {
|
||||
$class = self::actions[$class];
|
||||
return new $class;
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Call to action [%s] doesnt have a class to execute',$class));
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fields_input':
|
||||
return $this->{$key};
|
||||
|
||||
default:
|
||||
if (($x=$this->fields_input->search(function($item) use ($key) { return $item->name === $key; })) !== FALSE)
|
||||
return $this->fields_input->get($x)->value;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
public function __set(string $key,mixed $value): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fields_input':
|
||||
$this->{$key} = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
if (! isset($this->fields_input))
|
||||
throw new \Exception(sprintf('Missing fields_input in [%s]',get_class($this)));
|
||||
|
||||
// First field data element is user, the second is the password
|
||||
if (count($x=collect(static::fields)->diff($this->fields_input->pluck('name'))))
|
||||
throw new ActionMissingInputsException(sprintf('Login missing %s',$x->join(',')));
|
||||
}
|
||||
}
|
50
app/Classes/BBS/Frame/Action/Login.php
Normal file
50
app/Classes/BBS/Frame/Action/Login.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Frame\Action;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
use App\Classes\BBS\Exceptions\{ActionMissingInputsException,InvalidPasswordException};
|
||||
use App\Classes\BBS\Frame\{Action,Field};
|
||||
use App\Classes\BBS\Server;
|
||||
use App\Models\User;
|
||||
|
||||
class Login extends Action
|
||||
{
|
||||
protected const fields = ['USER','PASS'];
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'user': return $this->uo;
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user logins
|
||||
*
|
||||
* @return bool
|
||||
* @throws ActionMissingInputsException
|
||||
* @throws InvalidPasswordException
|
||||
*/
|
||||
public function handle(): bool
|
||||
{
|
||||
parent::init();
|
||||
|
||||
$this->uo = User::where('name',$this->USER)->orWhere('alias',$this->USER)->firstOrFail();
|
||||
|
||||
if (! Hash::check($this->PASS,$this->uo->password))
|
||||
throw new InvalidPasswordException(sprintf('Password doesnt match for [%s]',$this->USER));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function preSubmitField(Server $server,Field $field): ?string
|
||||
{
|
||||
// Noop
|
||||
return NULL;
|
||||
}
|
||||
}
|
112
app/Classes/BBS/Frame/Action/Register.php
Normal file
112
app/Classes/BBS/Frame/Action/Register.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Frame\Action;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Classes\BBS\Frame\{Action,Field};
|
||||
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
|
||||
use App\Classes\BBS\Server;
|
||||
use App\Mail\BBS\SendToken;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* Class Register
|
||||
* This handles the data received for account registration
|
||||
*
|
||||
* @package App\Classes\Frame\Action
|
||||
*/
|
||||
class Register extends Action
|
||||
{
|
||||
protected const fields = ['EMAIL','USER','PASS','FULLNAME','TOKEN'];
|
||||
|
||||
private string $token = '';
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'user': return $this->uo;
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user logins
|
||||
*
|
||||
* @return bool
|
||||
* @throws ActionMissingInputsException
|
||||
*/
|
||||
public function handle(): bool
|
||||
{
|
||||
parent::init();
|
||||
|
||||
$this->uo = new User;
|
||||
|
||||
$this->uo->name = $this->fields_input->where('name','FULLNAME')->first()->value;
|
||||
$this->uo->email = $this->fields_input->where('name','EMAIL')->first()->value;
|
||||
$this->uo->email_verified_at = Carbon::now();
|
||||
|
||||
$this->uo->password = Hash::make($x=$this->fields_input->where('name','PASS')->first()->value);
|
||||
$this->uo->active = TRUE;
|
||||
$this->uo->last_on = Carbon::now();
|
||||
$this->uo->alias = $this->fields_input->where('name','USER')->first()->value;
|
||||
|
||||
$this->uo->save();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function preSubmitField(Server $server,Field $field): ?string
|
||||
{
|
||||
switch ($field->name) {
|
||||
// Send a token
|
||||
case 'EMAIL':
|
||||
// Make sure we got an email address
|
||||
if (Validator::make(['email'=>$field->value],[
|
||||
'email'=>'email',
|
||||
])->fails()) {
|
||||
return 'INVALID EMAIL ADDRESS';
|
||||
}
|
||||
|
||||
// See if the requested email already exists
|
||||
if (User::where('email',$field->value)->exists())
|
||||
return 'USER ALREADY EXISTS';
|
||||
|
||||
Log::info(sprintf('Sending token to [%s]',$field->value));
|
||||
$server->sendBaseline(RED.'SENDING TOKEN...');
|
||||
|
||||
$this->token = sprintf('%06.0f',rand(0,999999));
|
||||
$sent = Mail::to($field->value)->send(new SendToken($this->token));
|
||||
$server->sendBaseline(RED.'SENT');
|
||||
|
||||
break;
|
||||
|
||||
case 'USER':
|
||||
if (str_contains($field->value,' '))
|
||||
return 'NO SPACES IN USER NAMES';
|
||||
|
||||
// See if the requested username already exists
|
||||
if (User::where('alias',$field->value)->exists())
|
||||
return 'USER ALREADY EXISTS';
|
||||
|
||||
// Clear the baseline from EMAIL entry
|
||||
$server->sendBaseline('');
|
||||
|
||||
break;
|
||||
|
||||
case 'TOKEN':
|
||||
if ($field->value !== $this->token)
|
||||
return 'INVALID TOKEN';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
}
|
290
app/Classes/BBS/Frame/Char.php
Normal file
290
app/Classes/BBS/Frame/Char.php
Normal file
@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Frame;
|
||||
|
||||
use App\Classes\BBS\Page\{Ansi,Viewdata};
|
||||
use App\Models\BBS\Mode;
|
||||
|
||||
class Char {
|
||||
/** @var int|null Attributes for the character (ie: color) */
|
||||
private ?int $attr;
|
||||
/** @var string|null Character to be shown */
|
||||
private ?string $ch;
|
||||
|
||||
public function __construct(string $ch=NULL,int $attr=NULL)
|
||||
{
|
||||
$this->ch = $ch;
|
||||
$this->attr = $attr;
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'attr': return $this->attr;
|
||||
case 'ch': return $this->ch;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key:'.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __isset($key): bool
|
||||
{
|
||||
return isset($this->{$key});
|
||||
}
|
||||
|
||||
public function __set(string $key,mixed $value): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'ch':
|
||||
if (strlen($value) !== 1)
|
||||
throw new \Exception(sprintf('CH can only be 1 char: [%s]',$value));
|
||||
|
||||
$this->{$key} = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key:'.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('%04x [%s]|',$this->attr,$this->ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the color codes required to draw the current character
|
||||
*
|
||||
* @param Mode $mo Service we are rendering for
|
||||
* @param int|null $last last rendered char
|
||||
* @param bool $debug debug mode
|
||||
* @return string|NULL
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function attr(Mode $mo,int $last=NULL,bool $debug=FALSE): string|NULL
|
||||
{
|
||||
$ansi = collect();
|
||||
|
||||
if ($debug)
|
||||
dump('- last:'.$last.', this:'.$this->attr);
|
||||
|
||||
switch ($mo->name) {
|
||||
case 'ansi':
|
||||
if ($debug) {
|
||||
dump(' - this BG_BLACK:'.($this->attr & Ansi::BG_BLACK));
|
||||
dump(' - last BG_BLACK:'.($last & Ansi::BG_BLACK));
|
||||
|
||||
dump(' - this HIGH:'.($this->attr & Ansi::HIGH));
|
||||
dump(' - last HIGH:'.($last & Ansi::HIGH));
|
||||
|
||||
dump(' - this BLINK:'.($this->attr & Ansi::BLINK));
|
||||
dump(' - last BLINK:'.($last & Ansi::BLINK));
|
||||
}
|
||||
|
||||
// If high was in the last, and we dont have high now, we need 0, but we need to turn back on flash if it was there
|
||||
// If flash was in the last, and we dont have flash now, we need to 0 but we need to turn on high if it was there
|
||||
$reset = FALSE;
|
||||
if ((($this->attr & Ansi::BG_BLACK) && (! ($last & Ansi::BG_BLACK)))
|
||||
|| ((! ($this->attr & Ansi::BLINK)) && ($last & Ansi::BLINK))
|
||||
|| ((! ($this->attr & Ansi::HIGH)) && ($last & Ansi::HIGH)))
|
||||
{
|
||||
$ansi->push(Ansi::I_CLEAR_CODE);
|
||||
$reset = TRUE;
|
||||
$last = Ansi::BG_BLACK|Ansi::LIGHTGRAY;
|
||||
}
|
||||
|
||||
if (($this->attr & Ansi::HIGH)
|
||||
&& ((($this->attr & Ansi::HIGH) !== ($last & Ansi::HIGH)) || ($reset && ($last & Ansi::HIGH)))) {
|
||||
$ansi->push(Ansi::I_HIGH_CODE);
|
||||
}
|
||||
|
||||
if (($this->attr & Ansi::BLINK)
|
||||
&& ((($this->attr & Ansi::BLINK) !== ($last & Ansi::BLINK)) || ($reset && ($last & Ansi::BLINK)))) {
|
||||
$ansi->push(Ansi::I_BLINK_CODE);
|
||||
}
|
||||
|
||||
$c = ($this->attr & 0x07);
|
||||
$l = ($last & 0x07);
|
||||
|
||||
// Foreground
|
||||
switch ($c) {
|
||||
case Ansi::BLACK:
|
||||
$r = Ansi::FG_BLACK_CODE;
|
||||
break;
|
||||
case Ansi::RED:
|
||||
$r = Ansi::FG_RED_CODE;
|
||||
break;
|
||||
case Ansi::GREEN:
|
||||
$r = Ansi::FG_GREEN_CODE;
|
||||
break;
|
||||
case Ansi::BROWN:
|
||||
$r = Ansi::FG_BROWN_CODE;
|
||||
break;
|
||||
case Ansi::BLUE:
|
||||
$r = Ansi::FG_BLUE_CODE;
|
||||
break;
|
||||
case Ansi::MAGENTA:
|
||||
$r = Ansi::FG_MAGENTA_CODE;
|
||||
break;
|
||||
case Ansi::CYAN:
|
||||
$r = Ansi::FG_CYAN_CODE;
|
||||
break;
|
||||
case Ansi::LIGHTGRAY:
|
||||
$r = Ansi::FG_LIGHTGRAY_CODE;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($r && ($c !== $l))
|
||||
$ansi->push($r);
|
||||
|
||||
// Background
|
||||
if ($this->attr & 0x70) {
|
||||
$c = ($this->attr & 0x70);
|
||||
$l = ($last & 0x70);
|
||||
|
||||
switch ($this->attr & 0x70) {
|
||||
case Ansi::BG_BLACK:
|
||||
$r = Ansi::BG_BLACK_CODE;
|
||||
break;
|
||||
case Ansi::BG_RED:
|
||||
$r = Ansi::BG_RED_CODE;
|
||||
break;
|
||||
case Ansi::BG_GREEN:
|
||||
$r = Ansi::BG_GREEN_CODE;
|
||||
break;
|
||||
case Ansi::BG_BROWN:
|
||||
$r = Ansi::BG_BROWN_CODE;
|
||||
break;
|
||||
case Ansi::BG_BLUE:
|
||||
$r = Ansi::BG_BLUE_CODE;
|
||||
break;
|
||||
case Ansi::BG_MAGENTA:
|
||||
$r = Ansi::BG_MAGENTA_CODE;
|
||||
break;
|
||||
case Ansi::BG_CYAN:
|
||||
$r = Ansi::BG_CYAN_CODE;
|
||||
break;
|
||||
case Ansi::BG_LIGHTGRAY:
|
||||
$r = Ansi::BG_LIGHTGRAY_CODE;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($r && ($c !== $l))
|
||||
$ansi->push($r);
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump([' - ansi:' =>$ansi]);
|
||||
|
||||
return $ansi->count() ? sprintf('%s[%sm',($debug ? '': "\x1b"),$ansi->join(';')) : NULL;
|
||||
|
||||
case 'viewdata':
|
||||
if ($debug)
|
||||
dump(sprintf('Last: %02x, Attr: %02x',$last,$this->attr));
|
||||
|
||||
switch ($this->attr) {
|
||||
// \x08
|
||||
case Viewdata::BLINK:
|
||||
$r = Viewdata::I_BLINK_CODE;
|
||||
break;
|
||||
// \x09
|
||||
case Viewdata::STEADY:
|
||||
$r = Viewdata::I_STEADY;
|
||||
break;
|
||||
// \x0c
|
||||
case Viewdata::NORMAL:
|
||||
$r = Viewdata::I_NORMAL;
|
||||
break;
|
||||
// \x0d
|
||||
case Viewdata::DOUBLE:
|
||||
$r = Viewdata::I_DOUBLE_CODE;
|
||||
break;
|
||||
// \x18
|
||||
case Viewdata::CONCEAL:
|
||||
$r = Viewdata::I_CONCEAL;
|
||||
break;
|
||||
// \x19
|
||||
case Viewdata::BLOCKS:
|
||||
$r = Viewdata::I_BLOCKS;
|
||||
break;
|
||||
// \x1a
|
||||
case Viewdata::SEPARATED:
|
||||
$r = Viewdata::I_SEPARATED;
|
||||
break;
|
||||
// \x1c
|
||||
case Viewdata::BLACKBACK:
|
||||
$r = Viewdata::I_BLACKBACK;
|
||||
break;
|
||||
// \x1d
|
||||
case Viewdata::NEWBACK:
|
||||
$r = Viewdata::I_NEWBACK;
|
||||
break;
|
||||
// \x1e
|
||||
case Viewdata::HOLD:
|
||||
$r = Viewdata::I_HOLD;
|
||||
break;
|
||||
// \x1f
|
||||
case Viewdata::RELEASE:
|
||||
$r = Viewdata::I_REVEAL;
|
||||
break;
|
||||
|
||||
// Not handled
|
||||
// \x0a-b,\x0e-f,\x1b
|
||||
case 0xff00:
|
||||
dump($this->attr);
|
||||
break;
|
||||
|
||||
default:
|
||||
$mosiac = ($this->attr & Viewdata::MOSIAC);
|
||||
$c = ($this->attr & 0x07);
|
||||
|
||||
if ($debug)
|
||||
dump(sprintf('Last: %02x, Attr: %02x, Color: %02x',$last,$this->attr,$c));
|
||||
|
||||
// Color control \x00-\x07, \x10-\x17
|
||||
switch ($c) {
|
||||
/*
|
||||
case Viewdata::BLACK:
|
||||
$r = Viewdata::FG_BLACK_CODE;
|
||||
break;
|
||||
*/
|
||||
case Viewdata::RED:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_RED_CODE : Viewdata::FG_RED_CODE;
|
||||
break;
|
||||
case Viewdata::GREEN:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_GREEN_CODE : Viewdata::FG_GREEN_CODE;
|
||||
break;
|
||||
case Viewdata::YELLOW:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_YELLOW_CODE : Viewdata::FG_YELLOW_CODE;
|
||||
break;
|
||||
case Viewdata::BLUE:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_BLUE_CODE : Viewdata::FG_BLUE_CODE;
|
||||
break;
|
||||
case Viewdata::MAGENTA:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_MAGENTA_CODE : Viewdata::FG_MAGENTA_CODE;
|
||||
break;
|
||||
case Viewdata::CYAN:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_CYAN_CODE : Viewdata::FG_CYAN_CODE;
|
||||
break;
|
||||
case Viewdata::WHITE:
|
||||
$r = $mosiac ? Viewdata::MOSIAC_WHITE_CODE : Viewdata::FG_WHITE_CODE;
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($debug)
|
||||
dump('Not a color?:'.$c);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump(sprintf('= result: ESC[%s](%02x) for [%s]',chr($r),$r,$this->ch));
|
||||
|
||||
return chr($r);
|
||||
|
||||
default:
|
||||
throw new \Exception($this->type.': has not been implemented');
|
||||
}
|
||||
}
|
||||
}
|
110
app/Classes/BBS/Frame/Field.php
Normal file
110
app/Classes/BBS/Frame/Field.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Frame;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
final class Field
|
||||
{
|
||||
private array $attributes = [];
|
||||
|
||||
private const attributes = [
|
||||
'attribute', // Color attribute when rendering values
|
||||
'pad', // Pad character remaining characters up to length
|
||||
'size', // Size of the field
|
||||
'name', // Field name
|
||||
'type', // Type of field
|
||||
'value', // Current value
|
||||
'x', // X position in the frame
|
||||
'y', // Y position in the frame
|
||||
];
|
||||
|
||||
/** @var string[] Attributes that should be masked */
|
||||
private const mask = [
|
||||
'p',
|
||||
];
|
||||
|
||||
private const mask_attribute = '*';
|
||||
|
||||
public function __construct(array $values)
|
||||
{
|
||||
array_walk($values,function($value,$key) {
|
||||
$this->{$key} = $value;
|
||||
});
|
||||
}
|
||||
|
||||
public function __get($key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'can_add':
|
||||
return strlen($this->value) < $this->size;
|
||||
|
||||
case 'mask':
|
||||
return in_array($this->type,self::mask) ? '*' : NULL;
|
||||
|
||||
case 'X':
|
||||
return $this->x+strlen($this->value);
|
||||
|
||||
default:
|
||||
return Arr::get($this->attributes,$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __isset($key): bool
|
||||
{
|
||||
return isset($this->attributes[$key]);
|
||||
}
|
||||
|
||||
public function __set($key,$value): void
|
||||
{
|
||||
if (! in_array($key,self::attributes))
|
||||
throw new \Exception('Unknown attribute key:'.$key);
|
||||
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a char to the value, only if there is space to do so
|
||||
*
|
||||
* @param string $char
|
||||
* @return bool
|
||||
*/
|
||||
public function append(string $char): bool
|
||||
{
|
||||
if (is_null($this->value))
|
||||
$this->clear();
|
||||
|
||||
if ($this->can_add) {
|
||||
$this->value .= $char;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the field value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a character from the value, only if there are chars to do so
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if (strlen($this->value)) {
|
||||
$this->value = substr($this->value,0,-1);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
628
app/Classes/BBS/Page.php
Normal file
628
app/Classes/BBS/Page.php
Normal file
@ -0,0 +1,628 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Classes\BBS\Exceptions\{NoRouteException,ParentNotFoundException};
|
||||
use App\Classes\BBS\Frame\{Action,Field};
|
||||
use App\Models\BBS\{Frame,Mode};
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* The current page object
|
||||
*
|
||||
* @property page The full page number requested
|
||||
*/
|
||||
abstract class Page
|
||||
{
|
||||
/**
|
||||
* Color attributes can fit in an int
|
||||
* + Bit 0-2 off = Black foreground
|
||||
* + Foreground colors bits (0-2)
|
||||
* + High Intensity 1 bit (3)
|
||||
* + Bit 4-6 off = Black background
|
||||
* + Background colors bits (4-6)
|
||||
* + Flash 1 bit (7)
|
||||
*/
|
||||
public const BLINK = 1<<7; /* blink bit */
|
||||
public const HIGH = 1<<3; /* high intensity (bright) foreground bit */
|
||||
|
||||
/* foreground colors */
|
||||
public const BLACK = 0; /* dark colors (HIGH bit unset) */
|
||||
public const BLUE = 1;
|
||||
public const GREEN = 2;
|
||||
public const CYAN = 3;
|
||||
public const RED = 4;
|
||||
public const MAGENTA = 5;
|
||||
public const BROWN = 6;
|
||||
public const LIGHTGRAY = 7;
|
||||
public const DARKGRAY = self::HIGH | self::BLACK; /* light colors (HIGH bit set) */
|
||||
public const LIGHTBLUE = self::HIGH | self::BLUE;
|
||||
public const LIGHTGREEN = self::HIGH | self::GREEN;
|
||||
public const LIGHTCYAN = self::HIGH | self::CYAN;
|
||||
public const LIGHTRED = self::HIGH | self::RED;
|
||||
public const LIGHTMAGENTA = self::HIGH | self::MAGENTA;
|
||||
public const YELLOW = self::HIGH | self::BROWN;
|
||||
public const WHITE = self::HIGH | self::LIGHTGRAY;
|
||||
|
||||
public const BG_BLACK = 0x100; /* special value for ansi() */
|
||||
public const BG_BLUE = (self::BLUE<<4);
|
||||
public const BG_GREEN = (self::GREEN<<4);
|
||||
public const BG_CYAN = (self::CYAN<<4);
|
||||
public const BG_RED = (self::RED<<4);
|
||||
public const BG_MAGENTA = (self::MAGENTA<<4);
|
||||
public const BG_BROWN = (self::BROWN<<4);
|
||||
public const BG_LIGHTGRAY = (self::LIGHTGRAY<<4);
|
||||
|
||||
public const FRAMETYPE_INFO = 'i';
|
||||
public const FRAMETYPE_ACTION = 'a';
|
||||
public const FRAMETYPE_RESPONSE = 'r';
|
||||
public const FRAMETYPE_LOGIN = 'l';
|
||||
public const FRAMETYPE_TERMINATE = 't';
|
||||
public const FRAMETYPE_EXTERNAL = 'x';
|
||||
|
||||
private int $frame;
|
||||
private string $index;
|
||||
|
||||
/** @var Mode Our BBS mode model object */
|
||||
protected Mode $mo;
|
||||
|
||||
/** @var Frame|null Our frame model object */
|
||||
private ?Frame $fo = NULL;
|
||||
|
||||
/** @var Collection Users page retrieval history */
|
||||
private Collection $history;
|
||||
|
||||
/* Our window layout */
|
||||
protected Window $layout;
|
||||
private Window $content;
|
||||
private Window $header;
|
||||
private Window $provider;
|
||||
private Window $pagenum;
|
||||
private Window $unit;
|
||||
private bool $showheader = FALSE;
|
||||
|
||||
/** @var array Our compiled page */
|
||||
protected array $build;
|
||||
|
||||
/* Fields */
|
||||
// Current field being edited
|
||||
private ?int $field_active = NULL;
|
||||
/** @var Collection Dynamic fields that are pre-populated with system data */
|
||||
protected Collection $fields_dynamic;
|
||||
/** @var Collection Input fields take input from the user */
|
||||
protected Collection $fields_input;
|
||||
|
||||
protected bool $debug;
|
||||
|
||||
abstract public function attr(array $field): string;
|
||||
|
||||
abstract public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array;
|
||||
|
||||
abstract public static function strlenv($text): int;
|
||||
|
||||
public function __construct(int $frame,string $index='a',bool $debug=FALSE)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
|
||||
$this->layout = new Window(1,1,static::FRAME_WIDTH,static::FRAME_HEIGHT+1,'LAYOUT',NULL,$debug);
|
||||
|
||||
$this->header = new Window(1,1,static::FRAME_WIDTH,1,'HEADER',$this->layout,$debug);
|
||||
//dump(['this'=>get_class($this),'header_from'=>$this->header->x,'header_to'=>$this->header->bx,'width'=>$this->header->width]);
|
||||
|
||||
// Provider can use all its space
|
||||
$this->provider = new Window(1,1,static::FRAME_PROVIDER_LENGTH,1,'PROVIDER',$this->header,$debug);
|
||||
//dump(['this'=>get_class($this),'provider_from'=>$this->provider->x,'provider_to'=>$this->provider->bx,'width'=>$this->provider->width]);
|
||||
|
||||
// Page number is prefixed with a color change (if required, otherwise a space)
|
||||
$this->pagenum = new Window($this->provider->bx+1,1,static::FRAME_PAGE_LENGTH,1,'#',$this->header,$debug);
|
||||
//dump(['this'=>get_class($this),'pagenum_from'=>$this->pagenum->x,'pagenum_to'=>$this->pagenum->bx,'width'=>$this->pagenum->width]);
|
||||
|
||||
// Unit is prefixed with a color change (required, since a different color to page)
|
||||
$this->unit = new Window($this->pagenum->bx+1,1,static::FRAME_COST_LENGTH,1,'$',$this->header,$debug);
|
||||
//dump(['this'=>get_class($this),'unit_from'=>$this->unit->x,'unit_to'=>$this->unit->bx,'width'=>$this->unit->width]);
|
||||
|
||||
$this->content = new Window(1,2,static::FRAME_WIDTH,static::FRAME_HEIGHT,'CONTENT',$this->layout,$debug);
|
||||
|
||||
$this->resetHistory();
|
||||
$this->clear();
|
||||
$this->goto($frame,$index);
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'access' :
|
||||
case 'id' :
|
||||
case 'cls':
|
||||
case 'cost':
|
||||
case 'created_at':
|
||||
case 'public' :
|
||||
case 'type' :
|
||||
return $this->fo?->{$key};
|
||||
|
||||
case 'cug': return $this->fo?->cug_id;
|
||||
|
||||
case 'frame':
|
||||
case 'index':
|
||||
return $this->{$key};
|
||||
|
||||
case 'next': return ($this->index < 'z') ? chr(ord($this->index)+1) : $this->index;
|
||||
case 'prev': return ($this->index > 'a') ? chr(ord($this->index)-1) : $this->index;
|
||||
|
||||
case 'page': return sprintf('%d%s',$this->frame,$this->index);
|
||||
|
||||
case 'height': return $this->layout->height;
|
||||
case 'width': return $this->layout->width;
|
||||
|
||||
case 'fields_input': return $this->fields_input;
|
||||
|
||||
case 'field_current': return (! is_null($this->field_active)) ? $this->fields_input->get($this->field_active): NULL;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __set(string $key,mixed $value): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'showheader':
|
||||
$this->{$key} = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->display()->join("");
|
||||
}
|
||||
|
||||
/* METHODS */
|
||||
|
||||
/**
|
||||
* Return a list of alternative versions of this frame.
|
||||
*
|
||||
* @todo: Need to adjust to not include access=0 frames unless owner
|
||||
*/
|
||||
public function alts(): Collection
|
||||
{
|
||||
return Frame::where('frame',$this->frame)
|
||||
->where('index',$this->index)
|
||||
->where('id','<>',$this->fo->id)
|
||||
->where('mode_id',$this->id)
|
||||
->where('access',1)
|
||||
->limit(9)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function atcode(string $name,int $length,mixed $pad=' '): string
|
||||
{
|
||||
switch ($name) {
|
||||
case 'NODE':
|
||||
$result = '00010001';
|
||||
break;
|
||||
|
||||
case 'DATETIME':
|
||||
$result = Carbon::now()->toRfc822String();
|
||||
break;
|
||||
|
||||
case 'DATE':
|
||||
$result = Carbon::now()->format('Y-m-d');
|
||||
break;
|
||||
|
||||
case 'TIME':
|
||||
$result = Carbon::now()->format('H:ia');
|
||||
break;
|
||||
|
||||
default:
|
||||
$result = $name;
|
||||
}
|
||||
|
||||
if (strlen($result) < abs($length) && $pad)
|
||||
$result = ($length < 0)
|
||||
? Str::padLeft($result,abs($length),$pad)
|
||||
: Str::padRight($result,abs($length),$pad);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* History go back to previous page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function back(): bool
|
||||
{
|
||||
if ($this->history->count() > 1) {
|
||||
$this->history->pop();
|
||||
$this->fo = $this->history->last();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a page, extracting fields and formatting into our Window objects
|
||||
*
|
||||
* @param bool $force
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function build(bool $force=FALSE): array
|
||||
{
|
||||
if ($this->build && ! $force)
|
||||
throw new \Exception('Refusing to build without force.');
|
||||
|
||||
$this->load();
|
||||
$test = FALSE;
|
||||
|
||||
$this->provider->content = $this->parse(($test ? chr(0x02).'T'.chr(0x03).'B'.chr(0x04) : 'TB').'A'.($test ? ' - 12345678901234567890123456789012345678901234567890123456' : ''),static::FRAME_PROVIDER_LENGTH,$this->provider->y,$this->provider->x);
|
||||
$this->pagenum->content = $this->parse($this->color_page.($test ? '123456789012345a' : $this->page),static::FRAME_SPACE+static::FRAME_PAGE_LENGTH,$this->pagenum->y,$this->pagenum->x);
|
||||
$this->unit->content = $this->parse($this->color_unit.Str::padLeft(($this->cost+($test ? 1234 : 0)).'c',static::FRAME_COST_LENGTH-1,' '),static::FRAME_SPACE+static::FRAME_COST_LENGTH,$this->unit->y,$this->unit->x);
|
||||
$this->content->content = $this->parse($this->fo->content,static::FRAME_WIDTH,$this->content->y,$this->content->x);
|
||||
|
||||
$this->header->visible = ($this->showheader || $test);
|
||||
|
||||
$this->build_system_fields();
|
||||
$this->build = $this->layout->build(1,1,$this->debug);
|
||||
|
||||
// Add our dynamic values
|
||||
$fields = $this->fields_dynamic->filter(function($item) { return $item->value; });
|
||||
|
||||
Log::channel('bbs')->debug(sprintf('There are [%d] dynamic fields to populate',$fields->count()));
|
||||
if ($fields->count())
|
||||
$this->fields_insert($fields);
|
||||
|
||||
// Add our input fields
|
||||
$fields = $this->fields_input->filter(function($item) { return is_null($item->value); });
|
||||
|
||||
Log::channel('bbs')->debug(sprintf('There are [%d] input fields to setup',$fields->count()));
|
||||
if ($fields->count())
|
||||
$this->fields_insert($fields);
|
||||
|
||||
return $this->build;
|
||||
}
|
||||
|
||||
// @todo To complete - some of these came from SBBS and are not valid here
|
||||
private function build_system_fields(): void
|
||||
{
|
||||
// Fields we can process automatically
|
||||
$auto = ['NODE','DATETIME','DATE','TIME','REALNAME','BBS'];
|
||||
|
||||
$df = $this->fields_dynamic->filter(function($item) { return is_null($item->value); });
|
||||
|
||||
if (! $df->count())
|
||||
return;
|
||||
|
||||
foreach ($df as $field) {
|
||||
if (in_array($field->name,$auto))
|
||||
$this->field_dynamic($field->name,$this->atcode($field->name,$field->size,$field->pad));
|
||||
}
|
||||
}
|
||||
|
||||
private function clear(): void
|
||||
{
|
||||
$this->build = [];
|
||||
$this->fields_dynamic = collect();
|
||||
$this->fields_input = collect();
|
||||
$this->fieldReset();
|
||||
}
|
||||
|
||||
// Insert our *_field data (if it is set)
|
||||
public function display(): Collection
|
||||
{
|
||||
if (! $this->build)
|
||||
throw new \Exception('Page not ready');
|
||||
|
||||
// build
|
||||
$display = $this->build;
|
||||
|
||||
// populate dynamic fields - refresh dynamic fields if 09, otherwise show previous compiled with 00
|
||||
// check if there are any dynamic fields with no values
|
||||
|
||||
switch ($this->mo->name) {
|
||||
case 'ansi':
|
||||
$new_line = NULL;
|
||||
$shownullchars = TRUE;
|
||||
break;
|
||||
|
||||
case 'viewdata':
|
||||
$new_line = static::BG_BLACK|static::WHITE;
|
||||
$shownullchars = FALSE;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('Dont know how to display a [%s] page',$this->mo->name));
|
||||
}
|
||||
|
||||
$result = collect();
|
||||
$last = $new_line;
|
||||
|
||||
if ($this->debug)
|
||||
dump(['page-width'=>$this->width,'page-height'=>$this->height]);
|
||||
|
||||
// render
|
||||
for ($y=1;$y<=$this->height;$y++) {
|
||||
$line = '';
|
||||
|
||||
if ($new_line)
|
||||
$last = $new_line;
|
||||
|
||||
if ($this->debug)
|
||||
dump('============== ['.$y.'] ===============');
|
||||
|
||||
$x = 1;
|
||||
while ($x <= $this->width) {
|
||||
if ($this->debug)
|
||||
dump('* CELL : y:'.$y.', x:'.$x);
|
||||
|
||||
// The current char value
|
||||
$char = (isset($display[$y]) && isset($display[$y][$x])) ? $display[$y][$x] : NULL;
|
||||
|
||||
if ($this->debug)
|
||||
dump(' - CHAR : '.(! is_null($char) ? $char->ch : 'undefined').', ATTR:'.(! is_null($char) ? $char->attr : 'undefined').', LAST:'.$last);
|
||||
|
||||
if ($this->debug) {
|
||||
dump('-------- ['.$x.'] ------');
|
||||
dump('y:'.$y.',x:'.$x.', attr:'.(! is_null($char) ? $char->attr : 'undefined'));
|
||||
}
|
||||
|
||||
// Only write a new attribute if it has changed (and not Videotex)
|
||||
if ($last !== $char->attr) {
|
||||
// The current attribute for this character
|
||||
$attr = is_null($char) ? NULL : $char->attr($this->mo,$last,$this->debug);
|
||||
|
||||
switch ($this->mo->name) {
|
||||
case 'ansi':
|
||||
// If the attribute is null, we'll write our default attribute
|
||||
if (is_null($attr))
|
||||
$line .= ''; #static::BG_BLACK|static::LIGHTGRAY;
|
||||
else
|
||||
$line .= (! is_null($attr)) ? $attr : '';
|
||||
|
||||
break;
|
||||
|
||||
case 'viewdata':
|
||||
// If the attribute is null, we'll ignore it since we are drawing a character
|
||||
if (! is_null($attr)) {
|
||||
if ($this->debug)
|
||||
dump(sprintf('= SEND attr:%02x, last: %02x [%s] (%s)',ord($attr),$last,$char->ch,serialize($attr)));
|
||||
$line .= "\x1b".$attr;
|
||||
//$x++;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('[%s] has not been implemented',$this->mo->name));
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_null($char->ch)) {
|
||||
if ($this->debug)
|
||||
dump(' = SEND CHAR :'.$char->ch.', attr:'.$char->attr.', last:'.$last);
|
||||
|
||||
$line .= $char->ch;
|
||||
|
||||
} else if ($shownullchars || ((is_null($char->ch) && is_null($char->attr)))) {
|
||||
if ($this->debug)
|
||||
dump(' = CHAR UNDEFINED');
|
||||
$line .= ' ';
|
||||
}
|
||||
|
||||
$last = $char->attr;
|
||||
$x++;
|
||||
}
|
||||
|
||||
if ($this->debug)
|
||||
dump(['line'=>$line]);
|
||||
|
||||
$result->push($line);
|
||||
|
||||
if ($this->debug && ($y > $this->debug))
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a dynamic field with a value
|
||||
*
|
||||
* @param $name
|
||||
* @param $value
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function field_dynamic($name,$value): void
|
||||
{
|
||||
if (($x=$this->fields_dynamic->search(function($item) use ($name) { return $item->name === $name; })) !== FALSE) {
|
||||
$field = $this->fields_dynamic->get($x);
|
||||
|
||||
// Store our value
|
||||
$field->value = $value;
|
||||
|
||||
} else {
|
||||
throw new \Exception(sprintf('Dynamic field: [%s], doesnt exist?',$name));
|
||||
}
|
||||
}
|
||||
|
||||
private function fields_insert($fields) {
|
||||
foreach ($fields as $field) {
|
||||
if (is_null($field->value))
|
||||
continue;
|
||||
|
||||
$content = str_split($field->value);
|
||||
$y = $field->y;
|
||||
$x = $field->x;
|
||||
|
||||
for ($x;$x < $field->x+abs($field->size);$x++) {
|
||||
$index = $x-$field->x;
|
||||
|
||||
if (isset($content[$index]))
|
||||
$this->build[$y][$x]->ch = ($field->type !== 'p') ? $content[$index] : '*';
|
||||
else
|
||||
$this->build[$y][$x]->ch = $field->pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function fieldReset(): void
|
||||
{
|
||||
$this->field_active = NULL;
|
||||
|
||||
foreach ($this->fields_input as $field)
|
||||
$field->value = NULL;
|
||||
}
|
||||
|
||||
public function fieldNext(): Field|NULL
|
||||
{
|
||||
if ($this->fields_input->count()) {
|
||||
if (is_null($this->field_active))
|
||||
$this->field_active = 0;
|
||||
else
|
||||
$this->field_active++;
|
||||
|
||||
return $this->fields_input->get($this->field_active);
|
||||
|
||||
} else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a frame by it's ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
public function get(int $id): void
|
||||
{
|
||||
$this->po->findOrFail($id);
|
||||
$this->frame = $this->po->frame;
|
||||
$this->index = $this->po->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to a specific frame
|
||||
*
|
||||
* @param int $frame
|
||||
* @param string $index
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function goto(int $frame,string $index='a'): void
|
||||
{
|
||||
if (strlen($index) !== 1)
|
||||
throw new \Exception('Invalid index:'.$index);
|
||||
|
||||
$this->frame = $frame;
|
||||
$this->index = $index;
|
||||
$this->fo = NULL;
|
||||
}
|
||||
|
||||
public function haveNext(): bool
|
||||
{
|
||||
return $this->fo
|
||||
? Frame::where('frame',$this->frame)
|
||||
->where('index',$this->next)
|
||||
->where('mode_id',$this->fo->mode_id)
|
||||
->exists()
|
||||
: FALSE;
|
||||
}
|
||||
|
||||
public function isCug(int $cug): bool
|
||||
{
|
||||
return $this->cug === $cug;
|
||||
}
|
||||
// @todo To implement
|
||||
|
||||
public function isOwner(User $o): bool
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function isRoute(int $route): bool
|
||||
{
|
||||
return is_numeric($this->fo->{sprintf('r%d',$route)});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a frame, throw a model not found exception if it doesnt exist
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load(): void
|
||||
{
|
||||
$this->fo = Frame::where('mode_id',$this->mo->id)
|
||||
->where('frame',$this->frame)
|
||||
->where('index',$this->index)
|
||||
->orderBy('created_at','DESC')
|
||||
->firstOrFail();
|
||||
|
||||
$this->history->push($this->fo);
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
public function method(int $route): ?Action
|
||||
{
|
||||
if (($x=($this->fo->{sprintf('r%d',$route)})) && (! $this->isRoute($route)))
|
||||
return Action::factory($x);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
public function new(int $frame,string $index='a'): void
|
||||
{
|
||||
$this->frame = $frame;
|
||||
$this->index = $index;
|
||||
$this->fo = new Frame;
|
||||
|
||||
// Make sure parent frame exists
|
||||
if (($this->index !== 'a') && (! Frame::where('frame',$this->frame)->where('index',$this->prev)->where('mode',$this->mo->id)->exists()))
|
||||
throw new ParentNotFoundException(sprintf('Parent %d%s doesnt exist',$frame,$index));
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
$this->index = $this->next;
|
||||
$this->fo = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a user's history
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetHistory(): void
|
||||
{
|
||||
$this->history = collect();
|
||||
}
|
||||
|
||||
public function route(int $route): void
|
||||
{
|
||||
if ($this->isRoute($route)) {
|
||||
$this->frame = (int)$this->fo->{sprintf('r%d',$route)};
|
||||
$this->index = 'a';
|
||||
$this->fo = NULL;
|
||||
|
||||
} else {
|
||||
throw new NoRouteException('Invalid route '.$route);
|
||||
}
|
||||
}
|
||||
|
||||
public function prev(): void
|
||||
{
|
||||
$this->index = $this->prev;
|
||||
$this->fo = NULL;
|
||||
}
|
||||
}
|
433
app/Classes/BBS/Page/Ansi.php
Normal file
433
app/Classes/BBS/Page/Ansi.php
Normal file
@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Page;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\BBS\Frame\{Char,Field};
|
||||
use App\Classes\BBS\Page;
|
||||
use App\Models\BBS\Mode;
|
||||
|
||||
class Ansi extends Page
|
||||
{
|
||||
protected const FRAME_WIDTH = 80;
|
||||
protected const FRAME_HEIGHT = 22;
|
||||
protected const FRAME_PROVIDER_LENGTH = 55;
|
||||
protected const FRAME_PAGE_LENGTH = 17; // Full space for page number + space at beginning (as would be displayed by viewdata)
|
||||
protected const FRAME_COST_LENGTH = 8; // Full space for cost + space at beginning (as would be displayed by viewdata)
|
||||
protected const FRAME_SPACE = 1; // Since colors dont take a space, this is to buffer a space
|
||||
|
||||
public const ESC = 27;
|
||||
public const I_CLEAR_CODE = 0;
|
||||
public const I_HIGH_CODE = 1;
|
||||
public const I_BLINK_CODE = 5;
|
||||
public const FG_WHITE_CODE = self::FG_LIGHTGRAY_CODE;
|
||||
public const FG_YELLOW_CODE = self::FG_BROWN_CODE;
|
||||
public const FG_BLACK_CODE = 30;
|
||||
public const FG_RED_CODE = 31;
|
||||
public const FG_GREEN_CODE = 32;
|
||||
public const FG_BROWN_CODE = 33;
|
||||
public const FG_BLUE_CODE = 34;
|
||||
public const FG_MAGENTA_CODE = 35;
|
||||
public const FG_CYAN_CODE = 36;
|
||||
public const FG_LIGHTGRAY_CODE = 37;
|
||||
public const BG_BLACK_CODE = 40;
|
||||
public const BG_RED_CODE = 41;
|
||||
public const BG_GREEN_CODE = 42;
|
||||
public const BG_BROWN_CODE = 43;
|
||||
public const BG_YELLOW_CODE = self::BG_BROWN_CODE;
|
||||
public const BG_BLUE_CODE = 44;
|
||||
public const BG_MAGENTA_CODE = 45;
|
||||
public const BG_CYAN_CODE = 46;
|
||||
public const BG_LIGHTGRAY_CODE = 47;
|
||||
|
||||
public static function strlenv($text): int
|
||||
{
|
||||
return strlen($text ? preg_replace('/'.ESC.'\[[0-9;?]+[a-zA-Z]/','',$text) : $text);
|
||||
}
|
||||
|
||||
public function __construct(int $frame,string $index='a')
|
||||
{
|
||||
parent::__construct($frame,$index);
|
||||
|
||||
$this->mo = Mode::where('name','Ansi')->single();
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'color_page':
|
||||
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_WHITE_CODE);
|
||||
case 'color_unit':
|
||||
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_GREEN_CODE);
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
public function attr(array $field): string
|
||||
{
|
||||
return sprintf('%s[%d;%d;%dm',ESC,$field['i'],$field['f'],$field['b']);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function converts ANSI text into an array of attributes
|
||||
*
|
||||
* We include the attribute for every character, so that if a window is placed on top of this window, the edges
|
||||
* render correctly.
|
||||
*
|
||||
* @param string $contents Our ANSI content to convert
|
||||
* @param int $width Canvas width before we wrap to the next line
|
||||
* @param int $yoffset fields offset when rendered (based on main window)
|
||||
* @param int $xoffset fields offset when rendered (based on main window)
|
||||
* @param int|null $debug Enable debug mode
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$lines = collect(explode("\r\n",$contents));
|
||||
if ($debug)
|
||||
dump(['lines'=>$lines]);
|
||||
|
||||
$i = 0; // Intensity
|
||||
$bg = self::BG_BLACK; // Background color
|
||||
$fg = self::LIGHTGRAY; // Foreground color
|
||||
$attr = $fg + $bg + $i; // Attribute int
|
||||
$default = ['i'=>0,'f'=>self::FG_LIGHTGRAY_CODE,'b'=>self::BG_BLACK_CODE];
|
||||
|
||||
$y = 0; // Line
|
||||
$saved_x = NULL; // Cursor saved
|
||||
$saved_y = NULL; // Cursor saved
|
||||
|
||||
$ansi = $default; // Our current attribute used for input fields
|
||||
|
||||
while ($lines->count() > 0) {
|
||||
$x = 0;
|
||||
$line = $lines->shift();
|
||||
|
||||
$result[$y+1] = [];
|
||||
|
||||
if ($this->debug) dump(['next line'=>$line,'length'=>strlen($line)]);
|
||||
|
||||
if (is_numeric($debug) && ($y > $debug)) {
|
||||
dump(['exiting'=>serialize($debug)]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
while (strlen($line) > 0) {
|
||||
if ($debug)
|
||||
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
|
||||
|
||||
if ($x >= $width) {
|
||||
$x = 0;
|
||||
$y++;
|
||||
}
|
||||
|
||||
/* parse an attribute sequence*/
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[((\d+)+(;(\d+)+)*)m/U',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
// Are values separated by ;
|
||||
$m = array_map(function($item) { return (int)$item; },explode(';',$m[0]));
|
||||
// Sort our numbers
|
||||
sort($m);
|
||||
|
||||
// Reset
|
||||
if ($m[0] === self::I_CLEAR_CODE) {
|
||||
$bg = self::BG_BLACK;
|
||||
$fg = self::LIGHTGRAY;
|
||||
$i = 0;
|
||||
$ansi = $default;
|
||||
array_shift($m);
|
||||
}
|
||||
|
||||
// High Intensity
|
||||
if (count($m) && ($m[0] === self::I_HIGH_CODE)) {
|
||||
$i += ((($i === 0) || ($i === self::BLINK)) ? self::HIGH : 0);
|
||||
$ansi['i'] = self::I_HIGH_CODE;
|
||||
array_shift($m);
|
||||
}
|
||||
|
||||
// Blink
|
||||
if (count($m) && ($m[0] === self::I_BLINK_CODE)) {
|
||||
$i += ((($i === 0) || ($i === self::HIGH)) ? self::BLINK : 0);
|
||||
array_shift($m);
|
||||
}
|
||||
|
||||
// Foreground
|
||||
if (count($m) && ($m[0] >= self::FG_BLACK_CODE) && ($m[0] <= self::FG_LIGHTGRAY_CODE)) {
|
||||
$ansi['f'] = $m[0];
|
||||
|
||||
switch (array_shift($m)) {
|
||||
case self::FG_BLACK_CODE:
|
||||
$fg = self::BLACK;
|
||||
break;
|
||||
|
||||
case self::FG_RED_CODE:
|
||||
$fg = self::RED;
|
||||
break;
|
||||
|
||||
case self::FG_GREEN_CODE:
|
||||
$fg = self::GREEN;
|
||||
break;
|
||||
|
||||
case self::FG_YELLOW_CODE:
|
||||
$fg = self::BROWN;
|
||||
break;
|
||||
|
||||
case self::FG_BLUE_CODE:
|
||||
$fg = self::BLUE;
|
||||
break;
|
||||
|
||||
case self::FG_MAGENTA_CODE:
|
||||
$fg = self::MAGENTA;
|
||||
break;
|
||||
|
||||
case self::FG_CYAN_CODE:
|
||||
$fg = self::CYAN;
|
||||
break;
|
||||
|
||||
case self::FG_LIGHTGRAY_CODE:
|
||||
$fg = self::LIGHTGRAY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
if (count($m) && ($m[0] >= self::BG_BLACK_CODE) && ($m[0] <= self::BG_LIGHTGRAY_CODE)) {
|
||||
$ansi['b'] = $m[0];
|
||||
|
||||
switch (array_shift($m)) {
|
||||
case self::BG_BLACK_CODE:
|
||||
$bg = self::BG_BLACK;
|
||||
break;
|
||||
|
||||
case self::BG_RED_CODE:
|
||||
$bg = self::BG_RED;
|
||||
break;
|
||||
|
||||
case self::BG_GREEN_CODE:
|
||||
$bg = self::BG_GREEN;
|
||||
break;
|
||||
|
||||
case self::BG_BROWN_CODE:
|
||||
$bg = self::BG_BROWN;
|
||||
break;
|
||||
|
||||
case self::BG_BLUE_CODE:
|
||||
$bg = self::BG_BLUE;
|
||||
break;
|
||||
|
||||
case self::BG_MAGENTA_CODE:
|
||||
$bg = self::BG_MAGENTA;
|
||||
break;
|
||||
|
||||
case self::BG_CYAN_CODE:
|
||||
$bg = self::BG_CYAN;
|
||||
break;
|
||||
|
||||
case self::BG_LIGHTGRAY_CODE:
|
||||
$bg = self::BG_LIGHTGRAY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$attr = $bg + $fg + $i;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* parse absolute character position */
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[(\d*);?(\d*)[Hf]/',$line,$m);
|
||||
if (count($m)) {
|
||||
dump(['Hf'=>$m]); // @todo Remove once validated
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
$y = (int)array_shift($m);
|
||||
|
||||
if (count($m))
|
||||
$x = (int)array_shift($m)-1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ignore an invalid sequence */
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[\?7h/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* parse positional sequences */
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[(\d+)([A-D])/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
switch ($m[1]) {
|
||||
/* parse an up positional sequence */
|
||||
case 'A':
|
||||
$y -= ($m[0] < 1) ? 0 : $m[0];
|
||||
break;
|
||||
|
||||
/* parse a down positional sequence */
|
||||
case 'B':
|
||||
$y += ($m[0] < 1) ? 0 : $m[0];
|
||||
break;
|
||||
|
||||
/* parse a forward positional sequence */
|
||||
case 'C':
|
||||
$x += ($m[0] < 1) ? 0 : $m[0];
|
||||
break;
|
||||
|
||||
/* parse a backward positional sequence */
|
||||
case 'D':
|
||||
$x -= ($m[0] < 1) ? 0 : $m[0];
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* parse a clear screen sequence - we ignore them */
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[2J/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* parse cursor sequences */
|
||||
$m = [];
|
||||
preg_match('/^\x1b\[([su])/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
switch ($m[0]) {
|
||||
/* parse save cursor sequence */
|
||||
case 's':
|
||||
$saved_x = $x;
|
||||
$saved_y = $y;
|
||||
break;
|
||||
|
||||
/* parse restore cursor sequence */
|
||||
case 'u':
|
||||
$x = $saved_x;
|
||||
$y = $saved_y;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* parse an input field */
|
||||
// Input field 'FIELD;valueTYPE;input char'
|
||||
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
|
||||
$m = [];
|
||||
preg_match('/^\x1b_([A-Z]+;[0-9a-z]+)([;]?.+)?\x1b\\\/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
// We are interested in our field match
|
||||
$f = explode(';',array_shift($m));
|
||||
|
||||
// First value is the field name
|
||||
$field = array_shift($f);
|
||||
|
||||
// Second value is the length/type of the field, nnX nn=size in chars, X=type (lower case)
|
||||
$c = [];
|
||||
preg_match('/([0-9]+)([a-z])/',$xx=array_shift($f),$c);
|
||||
if (! count($c)) {
|
||||
Log::channel('bbs')->alert(sprintf('! IF FAILED PARSING FIELD LENGTH/TYPE [%02dx%02d] (%s)',$y,$x,$xx));
|
||||
break;
|
||||
}
|
||||
|
||||
// Third field is the char to use
|
||||
$fieldpad = count($f) ? array_shift($f) : '.';
|
||||
Log::channel('bbs')->info(sprintf('- IF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$c[2],$c[1],$fieldpad));
|
||||
|
||||
// Any remaining fields are junk
|
||||
if (count($f))
|
||||
Log::channel('bbs')->alert(sprintf('! IGNORING ADDITIONAL IF FIELDS [%02dx%02d] (%s)',$y,$x,join('',$f)));
|
||||
|
||||
// If we are padding our field with a char, we need to add that back to $line
|
||||
// @todo validate if this goes beyond our width (and if scrolling not enabled)
|
||||
if ($c[1])
|
||||
$line = str_repeat($fieldpad,$c[1]).$line;
|
||||
|
||||
$this->fields_input->push(new Field([
|
||||
'attribute' => $ansi,
|
||||
'name' => $field,
|
||||
'pad' => $fieldpad,
|
||||
'size' => $c[1],
|
||||
'type' => $c[2],
|
||||
'value' => NULL,
|
||||
'x' => $x+$xoffset,
|
||||
'y' => $y+$yoffset,
|
||||
]));
|
||||
}
|
||||
|
||||
/* parse dynamic value field */
|
||||
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
|
||||
$m = [];
|
||||
preg_match('/^\x1bX([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?\x1b\\\/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
|
||||
// We are interested in our field match
|
||||
$f = explode(';',array_shift($m));
|
||||
$pad = Arr::get($f,2,' ');
|
||||
|
||||
Log::channel('bbs')->info(sprintf('- DF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$f[0],$f[1],$pad));
|
||||
// If we are padding our field with a char, we need to add that back to line
|
||||
// @todo validate if this goes beyond our width (and if scrolling not enabled)
|
||||
$line = str_repeat($pad,abs($f[1])).$line;
|
||||
|
||||
$this->fields_dynamic->push(new Field([
|
||||
'name' => $f[0],
|
||||
'pad' => $pad,
|
||||
'type' => NULL,
|
||||
'size' => $f[1],
|
||||
'value' => NULL,
|
||||
'x' => $x+$xoffset,
|
||||
'y' => $y+$yoffset,
|
||||
]));
|
||||
}
|
||||
|
||||
/* set character and attribute */
|
||||
$ch = $line[0];
|
||||
$line = substr($line,1);
|
||||
|
||||
/* validate position */
|
||||
if ($y < 0)
|
||||
$y = 0;
|
||||
if ($x < 0)
|
||||
$x = 0;
|
||||
|
||||
if ($attr === null)
|
||||
throw new \Exception('Attribute is null?');
|
||||
|
||||
$result[$y+1][$x+1] = new Char($ch,$attr);
|
||||
|
||||
$x++;
|
||||
}
|
||||
|
||||
// If we got a self::BG_BLACK|self::LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
|
||||
if (($attr === self::BG_BLACK|self::LIGHTGRAY) && isset($result[$y+1][$x]) && ($result[$y+1][$x]->attr !== $attr))
|
||||
$result[$y+1][$x+1] = new Char(NULL,$attr);
|
||||
|
||||
$y++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
370
app/Classes/BBS/Page/Viewdata.php
Normal file
370
app/Classes/BBS/Page/Viewdata.php
Normal file
@ -0,0 +1,370 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Page;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Classes\BBS\Frame\{Char,Field};
|
||||
use App\Classes\BBS\Page;
|
||||
use App\Models\BBS\Mode;
|
||||
|
||||
class Viewdata extends Page
|
||||
{
|
||||
protected const FRAME_WIDTH = 40;
|
||||
protected const FRAME_HEIGHT = 22;
|
||||
protected const FRAME_PROVIDER_LENGTH = 23;
|
||||
protected const FRAME_PAGE_LENGTH = 11; // Spec is 9+1 - including our color code.
|
||||
protected const FRAME_COST_LENGTH = 6; // including our color code
|
||||
protected const FRAME_SPACE = 0; // Since colors take a space, this is not needed
|
||||
|
||||
public const MOSIAC = 0x10;
|
||||
// Toggles
|
||||
public const CONCEAL = 0x20;
|
||||
public const REVEAL = 0x2000; // @temp Turns off Conceal
|
||||
|
||||
public const SEPARATED = 0x40;
|
||||
public const BLOCKS = 0x4000; // @temp Turns off Separated
|
||||
|
||||
public const STEADY = 0x8000; // @temp (turn off flash)
|
||||
|
||||
public const DOUBLE = 0x100;
|
||||
public const NORMAL = 0x1000; // @temp Turns off Double Height
|
||||
|
||||
public const HOLD = 0x200;
|
||||
public const RELEASE = 0x20000; // @temp turns off Hold
|
||||
|
||||
public const NEWBACK = 0x400;
|
||||
public const BLACKBACK = 0x800;
|
||||
|
||||
//public const ESC = 27;
|
||||
//public const I_CLEAR_CODE = 0;
|
||||
//public const I_HIGH_CODE = 1;
|
||||
|
||||
public const FG_BLACK_CODE = 0x40;
|
||||
public const FG_RED_CODE = 0x41;
|
||||
public const FG_GREEN_CODE = 0x42;
|
||||
public const FG_YELLOW_CODE = 0x43;
|
||||
public const FG_BLUE_CODE = 0x44;
|
||||
public const FG_MAGENTA_CODE = 0x45;
|
||||
public const FG_CYAN_CODE = 0x46;
|
||||
public const FG_WHITE_CODE = 0x47;
|
||||
public const I_BLINK_CODE = 0x48;
|
||||
public const I_STEADY = 0x49;
|
||||
public const I_NORMAL = 0x4c;
|
||||
public const I_DOUBLE_CODE = 0x4d;
|
||||
public const I_CONCEAL = 0x58;
|
||||
public const I_BLOCKS = 0x59;
|
||||
public const I_SEPARATED = 0x5a;
|
||||
public const I_BLACKBACK = 0x5c;
|
||||
public const I_NEWBACK = 0x5d;
|
||||
public const I_HOLD = 0x5e;
|
||||
public const I_REVEAL = 0x5f;
|
||||
|
||||
public const RED = 1;
|
||||
//public const GREEN = 2;
|
||||
public const YELLOW = 3;
|
||||
public const BLUE = 4;
|
||||
//public const MAGENTA = 5;
|
||||
public const CYAN = 6;
|
||||
public const WHITE = 7;
|
||||
public const MOSIAC_RED_CODE = 0x51;
|
||||
public const MOSIAC_GREEN_CODE = 0x52;
|
||||
public const MOSIAC_YELLOW_CODE = 0x53;
|
||||
public const MOSIAC_BLUE_CODE = 0x54;
|
||||
public const MOSIAC_MAGENTA_CODE = 0x55;
|
||||
public const MOSIAC_CYAN_CODE = 0x56;
|
||||
public const MOSIAC_WHITE_CODE = 0x57; // W
|
||||
|
||||
public const input_map = [
|
||||
'd' => 'DATE',
|
||||
'e' => 'EMAIL',
|
||||
'f' => 'FULLNAME',
|
||||
'n' => 'USER',
|
||||
'p' => 'PASS',
|
||||
't' => 'TIME',
|
||||
'y' => 'NODE',
|
||||
'z' => 'TOKEN',
|
||||
];
|
||||
|
||||
public static function strlenv($text):int
|
||||
{
|
||||
return strlen($text)-substr_count($text,ESC);
|
||||
}
|
||||
|
||||
public function __construct(int $frame,string $index='a')
|
||||
{
|
||||
parent::__construct($frame,$index);
|
||||
|
||||
$this->mo = Mode::where('name','Viewdata')->single();
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'color_page':
|
||||
return chr(self::WHITE);
|
||||
case 'color_unit':
|
||||
return chr(self::GREEN);
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
public function attr(array $field): string
|
||||
{
|
||||
// Noop
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* This function converts Viewtex BIN data into an array of attributes
|
||||
*
|
||||
* With viewdata, a character is used/display regardless of whether it is a control character, or an actual display
|
||||
* character.
|
||||
*
|
||||
* @param string $contents Our ANSI content to convert
|
||||
* @param int $width Canvas width before we wrap to the next line
|
||||
* @param int $yoffset fields offset when rendered (based on main window)
|
||||
* @param int $xoffset fields offset when rendered (based on main window)
|
||||
* @param int|null $debug Enable debug mode
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$lines = collect(explode("\r\n",$contents));
|
||||
if ($debug)
|
||||
dump(['lines'=>$lines]);
|
||||
|
||||
$i = 0; // Intensity
|
||||
$bg = self::BG_BLACK; // Background color
|
||||
$fg = self::WHITE; // Foreground color
|
||||
$new_line = $fg + $bg + $i; // Attribute int
|
||||
|
||||
// Attribute state on a new line
|
||||
$attr = $new_line;
|
||||
|
||||
$y = 0;
|
||||
while ($lines->count() > 0) {
|
||||
$x = 0;
|
||||
$line = $lines->shift();
|
||||
|
||||
$result[$y+1] = [];
|
||||
|
||||
if ($this->debug)
|
||||
dump(['next line'=>$line,'length'=>strlen($line)]);
|
||||
|
||||
while (strlen($line) > 0) {
|
||||
if ($debug)
|
||||
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
|
||||
|
||||
if ($x >= $width) {
|
||||
$x = 0;
|
||||
// Each new line, we reset the attrs
|
||||
$attr = $new_line;
|
||||
$y++;
|
||||
}
|
||||
|
||||
/* parse control codes */
|
||||
$m = [];
|
||||
preg_match('/^([\x00-\x09\x0c-\x1a\x1c-\x1f])/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen(array_shift($m)));
|
||||
$attr = 0;
|
||||
|
||||
switch ($xx=ord(array_shift($m))) {
|
||||
case 0x00:
|
||||
$attr += self::BLACK;
|
||||
break;
|
||||
case 0x01:
|
||||
$attr += self::RED;
|
||||
break;
|
||||
case 0x02:
|
||||
$attr += self::GREEN;
|
||||
break;
|
||||
case 0x03:
|
||||
$attr += self::YELLOW;
|
||||
break;
|
||||
case 0x04:
|
||||
$attr += self::BLUE;
|
||||
break;
|
||||
case 0x05:
|
||||
$attr += self::MAGENTA;
|
||||
break;
|
||||
case 0x06:
|
||||
$attr += self::CYAN;
|
||||
break;
|
||||
case 0x07:
|
||||
$attr += self::WHITE;
|
||||
break;
|
||||
case 0x08:
|
||||
$attr = self::BLINK;
|
||||
break;
|
||||
case 0x09:
|
||||
$attr = self::STEADY;
|
||||
break;
|
||||
/*
|
||||
case 0x0a:
|
||||
//$attr = self::ENDBOX; // End Box (Unused?)
|
||||
break;
|
||||
case 0x0b:
|
||||
//$attr = self::STARTBOX; // Start Box (Unused?)
|
||||
break;
|
||||
*/
|
||||
case 0x0c:
|
||||
$attr = self::NORMAL;
|
||||
break;
|
||||
case 0x0d:
|
||||
$attr = self::DOUBLE;
|
||||
break;
|
||||
case 0x0e:
|
||||
$attr = self::NORMAL; // @todo Double Width (Unused)?
|
||||
break;
|
||||
case 0x0f:
|
||||
$attr = self::NORMAL; // @todo Double Width (Unused?)
|
||||
break;
|
||||
case 0x10:
|
||||
$attr = self::MOSIAC|self::BLACK;
|
||||
break;
|
||||
case 0x11:
|
||||
$attr = self::MOSIAC|self::RED;
|
||||
break;
|
||||
case 0x12:
|
||||
$attr = self::MOSIAC|self::GREEN;
|
||||
break;
|
||||
case 0x13:
|
||||
$attr = self::MOSIAC|self::YELLOW;
|
||||
break;
|
||||
case 0x14:
|
||||
$attr = self::MOSIAC|self::BLUE;
|
||||
break;
|
||||
case 0x15:
|
||||
$attr = self::MOSIAC|self::MAGENTA;
|
||||
break;
|
||||
case 0x16:
|
||||
$attr = self::MOSIAC|self::CYAN;
|
||||
break;
|
||||
case 0x17:
|
||||
$attr = self::MOSIAC|self::WHITE;
|
||||
break;
|
||||
case 0x18:
|
||||
$attr = self::CONCEAL;
|
||||
break;
|
||||
case 0x19:
|
||||
$attr = self::BLOCKS;
|
||||
break;
|
||||
case 0x1a:
|
||||
$attr = self::SEPARATED;
|
||||
break;
|
||||
/*
|
||||
// We are using this for field input
|
||||
case 0x1b:
|
||||
//$attr = self::NORMAL; // CSI
|
||||
break;
|
||||
*/
|
||||
case 0x1c:
|
||||
$attr = self::BLACKBACK; // Black Background
|
||||
break;
|
||||
case 0x1d:
|
||||
$attr = self::NEWBACK; // New Background
|
||||
break;
|
||||
case 0x1e:
|
||||
$attr = self::HOLD; // Mosiac Hold
|
||||
break;
|
||||
case 0x1f:
|
||||
$attr = self::RELEASE; // Mosiac Release
|
||||
break;
|
||||
|
||||
// Catch all for other codes
|
||||
default:
|
||||
dump(['char'=>$xx]);
|
||||
$attr = 0xff00;
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump(sprintf('- got control code [%02x] at [%02dx%02d]',$attr,$y,$x));
|
||||
|
||||
$result[$y+1][$x+1] = new Char(NULL,$attr);
|
||||
$x++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* For response frames, a dialogue field is signalled by a CLS (0x0c) followed by a number of dialogue
|
||||
* characters [a-z]. The field ends by the first different character from the initial dialogue character.
|
||||
* The CLS is a "privileged space" and the dialogue characters defined the dialogue field.
|
||||
*
|
||||
* Standard dialogue characters:
|
||||
* + n = name
|
||||
* + t = telephone number
|
||||
* + d = date and time
|
||||
* + a = address
|
||||
* + anything else free form, typically 'f' is used
|
||||
*
|
||||
* Source: Prestel Bulk Update Technical Specification
|
||||
*/
|
||||
|
||||
/* parse an input field */
|
||||
// Since 0x0c is double, we'll use good ol' ESC 0x1b
|
||||
$m = [];
|
||||
preg_match('/^([\x1b|\x9b])([a-z])\2+/',$line,$m);
|
||||
if (count($m)) {
|
||||
$line = substr($line,strlen($m[0]));
|
||||
$len = strlen(substr($m[0],1));
|
||||
|
||||
$field = new Field([
|
||||
'attribute' => [],
|
||||
'name' => Arr::get(self::input_map,$m[2],$m[2]),
|
||||
'pad' => '.',
|
||||
'size' => $len,
|
||||
'type' => $m[2],
|
||||
'value' => NULL,
|
||||
'x' => $x+$xoffset,
|
||||
'y' => $y+$yoffset,
|
||||
]);
|
||||
|
||||
(($m[1] === "\x1b") ? $this->fields_input : $this->fields_dynamic)->push($field);
|
||||
|
||||
$result[$y+1][++$x] = new Char(' ',$attr); // The \x1b|\x9b is the privileged space.
|
||||
|
||||
for ($xx=0;$xx<$len;$xx++)
|
||||
$result[$y+1][$x+1+$xx] = new Char('.',$attr);
|
||||
|
||||
$x += $len;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* set character and attribute */
|
||||
$ch = $line[0];
|
||||
$line = substr($line,1);
|
||||
|
||||
if ($debug)
|
||||
dump(sprintf('Storing [%02xx%02x] [%s] with [%02x]',$y,$x,$ch,$attr));
|
||||
|
||||
/* validate position */
|
||||
if ($y < 0)
|
||||
$y = 0;
|
||||
if ($x < 0)
|
||||
$x = 0;
|
||||
|
||||
if ($attr === null)
|
||||
throw new \Exception('Attribute is null?');
|
||||
|
||||
$result[$y+1][$x+1] = new Char($ch,$attr);
|
||||
|
||||
$x++;
|
||||
}
|
||||
|
||||
// Each new line, we reset the attrs
|
||||
$attr = $new_line;
|
||||
$y++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
1231
app/Classes/BBS/Server.php
Normal file
1231
app/Classes/BBS/Server.php
Normal file
File diff suppressed because it is too large
Load Diff
87
app/Classes/BBS/Server/Ansitex.php
Normal file
87
app/Classes/BBS/Server/Ansitex.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Server;
|
||||
|
||||
use App\Classes\BBS\Server as AbstractServer;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
|
||||
class Ansitex extends AbstractServer
|
||||
{
|
||||
protected const LOGKEY = 'BAS';
|
||||
|
||||
/* CONSTS */
|
||||
|
||||
public const PORT = 23;
|
||||
|
||||
protected function init(SocketClient $client)
|
||||
{
|
||||
define('ESC', chr(27));
|
||||
define('CON', ESC.'[?25h'); // Cursor On
|
||||
define('COFF', ESC.'[?25l'); // Cursor Off
|
||||
define('CSAVE', ESC.'[s'); // Save Cursor position
|
||||
define('CRESTORE',ESC.'[u'); // Restore to saved position
|
||||
define('HOME', ESC.'[0;0f');
|
||||
define('LEFT', ESC.'[D'); // Move Cursor
|
||||
define('RIGHT', ESC.'[C'); // Move Cursor
|
||||
define('DOWN', ESC.'[B'); // Move Cursor
|
||||
define('UP', ESC.'[A'); // Move Cursor
|
||||
define('CR', chr(13));
|
||||
define('LF', chr(10));
|
||||
define('BS', chr(8));
|
||||
define('CLS', ESC.'[2J');
|
||||
define('HASH', '#'); // Enter
|
||||
define('STAR', '*'); // Star Entry
|
||||
define('SPACE', ' '); // Space (for compatibility with Videotex)
|
||||
|
||||
// NOTE: This consts are effective output
|
||||
define('RESET', ESC.'[0;39;49m');
|
||||
define('RED', ESC.'[0;31m');
|
||||
define('GREEN', ESC.'[0;32m');
|
||||
define('YELLOW', ESC.'[1;33m');
|
||||
define('BLUE', ESC.'[0;34m');
|
||||
define('MAGENTA', ESC.'[0;35m');
|
||||
define('CYAN', ESC.'[0;36m');
|
||||
define('WHITE', ESC.'[1;37m');
|
||||
define('NEWBG', '');
|
||||
|
||||
// Compatibility attributes (to Videotex).
|
||||
define('R_RED', RED.SPACE);
|
||||
define('R_GREEN', GREEN.SPACE);
|
||||
define('R_YELLOW', YELLOW.SPACE);
|
||||
define('R_BLUE', BLUE.SPACE);
|
||||
define('R_MAGENTA', MAGENTA.SPACE);
|
||||
define('R_CYAN', CYAN.SPACE);
|
||||
define('R_WHITE', WHITE.SPACE);
|
||||
//define('FLASH',chr(8));
|
||||
|
||||
// Keyboard presses
|
||||
// @todo Check where these are used vs the keys defined above?
|
||||
define('KEY_DELETE', chr(8));
|
||||
define('KEY_LEFT', chr(136));
|
||||
define('KEY_RIGHT', chr(137));
|
||||
define('KEY_DOWN', chr(138));
|
||||
define('KEY_UP', chr(139));
|
||||
|
||||
parent::init($client);
|
||||
}
|
||||
|
||||
function moveCursor($x,$y): string
|
||||
{
|
||||
return ESC.'['.$y.';'.$x.'f';
|
||||
}
|
||||
|
||||
// Abstract function
|
||||
public function sendBaseline(string $text,bool $reposition=FALSE)
|
||||
{
|
||||
$this->client->send(CSAVE.ESC.'[24;0f'.RESET.SPACE.$text.
|
||||
($this->blp > $this->po->strlenv(SPACE.$text)
|
||||
? str_repeat(' ',$this->blp-$this->po->strlenv(SPACE.$text)).
|
||||
($reposition ? ESC.'[24;0f'.str_repeat(RIGHT,$this->po->strlenv(SPACE.$text)) : CRESTORE)
|
||||
: ($reposition ? '' : CRESTORE)),
|
||||
static::TIMEOUT
|
||||
);
|
||||
|
||||
$this->blp = $this->po->strlenv(SPACE.$text);
|
||||
$this->baseline = $text;
|
||||
}
|
||||
}
|
91
app/Classes/BBS/Server/Videotex.php
Normal file
91
app/Classes/BBS/Server/Videotex.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Server;
|
||||
|
||||
use App\Classes\BBS\Server as AbstractServer;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
|
||||
class Videotex extends AbstractServer
|
||||
{
|
||||
protected const LOGKEY = 'BVS';
|
||||
|
||||
/* CONSTS */
|
||||
|
||||
public const PORT = 516;
|
||||
|
||||
protected function init(SocketClient $client)
|
||||
{
|
||||
define('ESC', chr(27));
|
||||
define('CON', chr(17)); // Cursor On
|
||||
define('COFF', chr(20)); // Cursor Off
|
||||
define('HOME', chr(30));
|
||||
define('LEFT', chr(8)); // Move Cursor
|
||||
define('RIGHT', chr(9)); // Move Cursor
|
||||
define('DOWN', chr(10)); // Move Cursor
|
||||
define('UP', chr(11)); // Move Cursor
|
||||
define('CR', chr(13));
|
||||
define('LF', chr(10));
|
||||
define('CLS', chr(12));
|
||||
define('HASH', '_'); // Enter
|
||||
define('STAR', '*'); // Star Entry
|
||||
define('SPACE', ''); // Space
|
||||
|
||||
// NOTE: This consts are effective output
|
||||
define('RESET', '');
|
||||
define('RED', ESC.'A');
|
||||
define('GREEN', ESC.'B');
|
||||
define('YELLOW', ESC.'C');
|
||||
define('BLUE', ESC.'D');
|
||||
define('MAGENTA', ESC.'E');
|
||||
define('CYAN', ESC.'F');
|
||||
define('WHITE', ESC.'G');
|
||||
define('NEWBG', ESC.']');
|
||||
|
||||
// Raw attributes - used when storing frames.
|
||||
define('R_RED', chr(1));
|
||||
define('R_GREEN', chr(2));
|
||||
define('R_YELLOW', chr(3));
|
||||
define('R_BLUE', chr(4));
|
||||
define('R_MAGENTA', chr(5));
|
||||
define('R_CYAN', chr(6));
|
||||
define('R_WHITE', chr(7));
|
||||
define('FLASH', chr(8));
|
||||
|
||||
define('KEY_DELETE', chr(0x7f));
|
||||
define('KEY_LEFT', chr(0x08));
|
||||
define('KEY_RIGHT', chr(0x09));
|
||||
define('KEY_DOWN', chr(0x0a));
|
||||
define('KEY_UP', chr(0x0b));
|
||||
|
||||
parent::init($client);
|
||||
}
|
||||
|
||||
public function moveCursor($x,$y): string
|
||||
{
|
||||
// Take the shortest path.
|
||||
if ($y < 12) {
|
||||
return HOME.
|
||||
(($x < 21)
|
||||
? str_repeat(DOWN,$y-1).str_repeat(RIGHT,$x)
|
||||
: str_repeat(DOWN,$y).str_repeat(LEFT,40-$x));
|
||||
|
||||
} else {
|
||||
return HOME.str_repeat(UP,24-$y+1).
|
||||
(($x < 21)
|
||||
? str_repeat(RIGHT,$x)
|
||||
: str_repeat(LEFT,40-$x));
|
||||
}
|
||||
}
|
||||
|
||||
public function sendBaseline(string $text,bool $reposition=FALSE) {
|
||||
$this->client->send(HOME.UP.$text.
|
||||
($this->blp > $this->po->strlenv($text)
|
||||
? str_repeat(' ',$this->blp-$this->po->strlenv($text)).
|
||||
($reposition ? HOME.UP.str_repeat(RIGHT,$this->po->strlenv($text)) : '')
|
||||
: ''),
|
||||
static::TIMEOUT
|
||||
);
|
||||
|
||||
$this->blp = $this->po->strlenv($text);
|
||||
}
|
||||
}
|
365
app/Classes/BBS/Window.php
Normal file
365
app/Classes/BBS/Window.php
Normal file
@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\BBS\Frame\Char;
|
||||
|
||||
/**
|
||||
* Windows are elements of a Page object
|
||||
*
|
||||
* @param int $x - (int) starting x of it's parent [1..]
|
||||
* @param int $y - (int) starting y of it's parent [1..]
|
||||
* @param int $width - (int) full width of the window (text content will be smaller if there are scroll bars/boarder)
|
||||
* @param int $height - (int) full height of the window (text content will be smaller if there are scroll bars/boarder)
|
||||
* @param string $name - (string) internal name for the window (useful for debugging)
|
||||
* @param Window $parent - (object) parent of this window
|
||||
* @param bool $debug - (int) debug mode, which fills the window with debug content
|
||||
*
|
||||
* Pages have the following attributes:
|
||||
* - bx/by - (int) right/bottom most boundary of the window representing the start + width/height of the window
|
||||
* - child - (array) children in this window
|
||||
* - height - (int) Window's height
|
||||
* - name - (string) Windows name (useful for internal debugging)
|
||||
* - parent - (object) Parent that this window belongs to
|
||||
* - x/y - (int) start position of the window
|
||||
* - visible - (bool) whether this window is visible
|
||||
* - width - (int) Window's width
|
||||
* - z - (int) Window's depth indicator
|
||||
*
|
||||
* Windows have the following public functions
|
||||
* - build - Compile the frame for rendering
|
||||
* - debug - Useful for debugging with properties of this Window
|
||||
* - draw - Draw a part of this Window
|
||||
*/
|
||||
class Window
|
||||
{
|
||||
/** @var int X offset of parent that the canvas starts [1..width] */
|
||||
private int $x;
|
||||
/** @var int Y offset of parent that the canvas starts [1..height] */
|
||||
private int $y;
|
||||
/** @var int Window top-bottom position, higher z is shown [0..] */
|
||||
private int $z = 0;
|
||||
/** @var int When canvas width > width, this is the offset we display [0..] */
|
||||
private int $ox = 0;
|
||||
/** @var int When canvas height > height, this is the offset we display [0..] */
|
||||
private int $oy = 0;
|
||||
/** @var int Display Width + (1 char if scrollbars = true) */
|
||||
private int $width;
|
||||
/** @var int Display Height */
|
||||
private int $height;
|
||||
/** @var int Width of Canvas (default display width) */
|
||||
private int $canvaswidth;
|
||||
/** @var int Height of Canvas (default display height) */
|
||||
private int $canvasheight;
|
||||
/** @var array Window content - starting at 0,0 = 1,1 */
|
||||
public array $content = [];
|
||||
/** @var bool Window visible */
|
||||
private bool $visible = TRUE;
|
||||
/** @var string Window name */
|
||||
private string $name;
|
||||
/** @var bool Can this frame move outside the parent */
|
||||
private bool $checkbounds = TRUE;
|
||||
/** @var bool Can the content scroll vertically (takes up 1 line) [AUTO DETERMINE IF canvas > width] */
|
||||
private bool $v_scroll = TRUE;
|
||||
/** @var bool Can the content scroll horizontally (takes up 1 char) [AUTO DETERMINE IF canvas > height] */
|
||||
private bool $h_scroll = FALSE;
|
||||
/** @var int|bool Overflowed content is rendered with the next page */
|
||||
private bool $pageable = FALSE;
|
||||
private Page|Window|NULL $parent;
|
||||
private Collection $child;
|
||||
private bool $debug;
|
||||
|
||||
/*
|
||||
Validation to implement:
|
||||
+ X BOUNDARY
|
||||
- x cannot be < parent.x if checkbounds is true [when moving window]
|
||||
- x+width(-1 if h_scroll is true) cannot be greater than parent.width if checkbounds is true
|
||||
- v_scroll must be true for canvaswidth > width
|
||||
- when scrolling ox cannot be > width-x
|
||||
- when layout.pageable is true, next page will only have windows included that have a y in the range
|
||||
ie: if height is 44 (window is 22), next page is 23-44 and will only include children where y=23-44
|
||||
+ Y BOUNDARY
|
||||
- y cannot be < parent.y if checkbounds is true [when moving window]
|
||||
- y+height(-1 if v_scroll is true) cannot be greater than parent.height if checkbounds is true
|
||||
- h_scroll must be true for canvasheight > height
|
||||
- when scrolling oy cannot be > height-y
|
||||
- when layout.pageable is true, children height cannot be greater than parent.height - y.
|
||||
*/
|
||||
public function __construct(int $x,int $y,int $width,int $height,string $name,Window|Page $parent=NULL,bool $debug=FALSE) {
|
||||
$this->x = $x;
|
||||
$this->y = $y;
|
||||
$this->name = $name;
|
||||
$this->parent = $parent;
|
||||
$this->debug = $debug;
|
||||
$this->child = collect();
|
||||
|
||||
if ($parent instanceof self) {
|
||||
$this->z = $parent->child->count()+1;
|
||||
$this->parent = $parent;
|
||||
|
||||
$this->parent->child->push($this);
|
||||
|
||||
// Check that our height/widths is not outside our parent
|
||||
if (($this->x < 1) || ($width > $this->parent->width))
|
||||
throw new \Exception(sprintf('Window: %s width [%d] is beyond our parent\'s width [%d].',$name,$width,$this->parent->width));
|
||||
if (($x > $this->parent->bx) || ($x+$width-1 > $this->parent->bx))
|
||||
throw new \Exception(sprintf('Window: %s start x [%d] and width [%d] is beyond our parent\'s end x [%d].',$name,$x,$width,$this->parent->bx));
|
||||
|
||||
if (($this->y < 1) || ($height > $this->parent->height))
|
||||
throw new \Exception(sprintf('Window: %s height [%d] is beyond our parent\'s height [%d].',$name,$height,$this->parent->height));
|
||||
if (($y > $this->parent->by) || ($y+$height-1 > $this->parent->by))
|
||||
throw new \Exception(sprintf('Window: %s start y [%d] and height [%d] is beyond our parent\'s end y [%s].',$name,$y,$height,$this->parent->by));
|
||||
|
||||
} elseif ($parent instanceof Page) {
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
$this->width = $this->canvaswidth = $width;
|
||||
$this->height = $this->canvasheight = $height;
|
||||
|
||||
if ($debug) {
|
||||
$this->canvaswidth = $width*2;
|
||||
$this->canvasheight = $height*2;
|
||||
}
|
||||
|
||||
// Fill with data
|
||||
for($y=1;$y<=$this->canvasheight;$y++) {
|
||||
for($x=1;$x<=$this->canvaswidth;$x++) {
|
||||
if (! isset($this->content[$y]))
|
||||
$this->content[$y] = [];
|
||||
|
||||
$this->content[$y][$x] = $debug
|
||||
? new Char((($x > $this->width) || ($y > $this->height)) ? strtoupper($this->name[0]) : strtolower($this->name[0]))
|
||||
: new Char();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function __get($key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'bx': return $this->x+$this->width-1;
|
||||
case 'by': return $this->y+$this->height-1;
|
||||
case 'checkbounds': return $this->checkbounds;
|
||||
case 'child':
|
||||
return $this->child->sort(function($a,$b) {return ($a->z < $b->z) ? -1 : (($b->z < $a->z) ? 1 : 0); });
|
||||
case 'name':
|
||||
return $this->name;
|
||||
case 'height':
|
||||
case 'parent':
|
||||
case 'visible':
|
||||
case 'width':
|
||||
case 'x':
|
||||
case 'y':
|
||||
case 'z':
|
||||
return $this->{$key};
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
public function __set($key,$value): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'child':
|
||||
if ($value instanceof self)
|
||||
$this->child->push($value);
|
||||
else
|
||||
throw new \Exception('child not an instance of Window()');
|
||||
break;
|
||||
|
||||
case 'content':
|
||||
$this->content = $value;
|
||||
break;
|
||||
|
||||
case 'parent':
|
||||
if ($this->parent)
|
||||
throw new \Exception('parent already DEFINED');
|
||||
else
|
||||
$this->parent = $value;
|
||||
break;
|
||||
|
||||
case 'visible':
|
||||
$this->visible = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build this window, returning an array of Char that will be rendered by Page
|
||||
*
|
||||
* @param int $xoffset - (int) This windows x position for its parent
|
||||
* @param int $yoffset - (int) This windows y position for its parent
|
||||
* @param bool $debug - (int) debug mode, which fills the window with debug content
|
||||
* @return array
|
||||
*/
|
||||
public function build(int $xoffset,int $yoffset,bool $debug=FALSE): array
|
||||
{
|
||||
$display = [];
|
||||
|
||||
if ($debug) {
|
||||
dump('********* ['.$this->name.'] *********');
|
||||
dump('name :'.$this->name);
|
||||
dump('xoff :'.$xoffset);
|
||||
dump('yoff :'.$yoffset);
|
||||
dump('x :'.$this->x);
|
||||
dump('bx :'.$this->bx);
|
||||
dump('ox :'.$this->ox);
|
||||
dump('y :'.$this->y);
|
||||
dump('by :'.$this->by);
|
||||
dump('oy :'.$this->oy);
|
||||
dump('lines :'.count(array_keys($this->content)));
|
||||
//dump('content:'.join('',$this->content[1]));
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump('-------------');
|
||||
|
||||
for ($y=1;$y<=$this->height;$y++) {
|
||||
if ($debug)
|
||||
echo sprintf('%02d',$y).':';
|
||||
|
||||
$sy = $this->y-1+$y+$yoffset-1;
|
||||
|
||||
for ($x=1;$x<=$this->width;$x++) {
|
||||
if ($debug)
|
||||
dump('- Checking :'.$this->name.', y:'.($y+$this->oy).', x:'.($x+$this->ox));
|
||||
|
||||
$sx = $this->x-1+$x+$xoffset-1;
|
||||
if (! isset($display[$sy]))
|
||||
$display[$sy] = [];
|
||||
|
||||
if (isset($this->content[$y+$this->oy]) && isset($this->content[$y+$this->oy][$x+$this->ox])) {
|
||||
$display[$sy][$sx] = $this->content[$y+$this->oy][$x+$this->ox];
|
||||
|
||||
if ($debug)
|
||||
dump('- storing in y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
|
||||
|
||||
} else {
|
||||
$display[$sy][$sx] = new Char();
|
||||
|
||||
if ($debug)
|
||||
dump('- nothing for y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump('---');
|
||||
}
|
||||
|
||||
if ($debug)
|
||||
dump('----LOOKING AT CHILDREN NOW---------');
|
||||
|
||||
if ($debug) {
|
||||
dump('Window:'.$this->name.', has ['.$this->child->filter(function($child) { return $child->visible; })->count().'] children');
|
||||
|
||||
$this->child->each(function($child) {
|
||||
dump(' - child:'.$child->name.', visible:'.$child->visible);
|
||||
});
|
||||
}
|
||||
|
||||
// Fill the array with our values
|
||||
foreach ($this->child->filter(function($child) { return $child->visible; }) as $child) {
|
||||
if ($debug) {
|
||||
dump('=========== ['.$child->name.'] =============');
|
||||
dump('xoff :'.$xoffset);
|
||||
dump('yoff :'.$yoffset);
|
||||
dump('x :'.$this->x);
|
||||
dump('y :'.$this->y);
|
||||
}
|
||||
|
||||
$draw = $child->build($this->x+$xoffset-1,$this->y+$yoffset-1,$debug);
|
||||
|
||||
if ($debug)
|
||||
dump('draw y:'.join(',',array_keys($draw)));
|
||||
|
||||
foreach (array_keys($draw) as $y) {
|
||||
foreach (array_keys($draw[$y]) as $x) {
|
||||
if (! isset($display[$y]))
|
||||
$display[$y] = [];
|
||||
|
||||
$display[$y][$x] = $draw[$y][$x];
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
//dump('draw 1:'.join(',',array_keys($draw[1])));
|
||||
dump('=========== END ['.$child->name.'] =============');
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
dump('this->name:'.$this->name);
|
||||
dump('this->y:'.$this->y);
|
||||
dump('display now:'.join(',',array_values($display[$this->y])));
|
||||
dump('********* END ['.$this->name.'] *********');
|
||||
|
||||
foreach ($display as $y => $data) {
|
||||
dump(sprintf("%02d:%s (%d)\r\n",$y,join('',$data),count($data)));
|
||||
}
|
||||
}
|
||||
|
||||
return $display;
|
||||
}
|
||||
|
||||
public function xdebug(string $text) {
|
||||
return '- '.$text.': '.$this->name.'('.$this->x.'->'.($this->bx).') width:'.$this->width.' ['.$this->y.'=>'.$this->by.'] with z:'.$this->z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this window
|
||||
*
|
||||
* @param $start - (int) Starting x position
|
||||
* @param $end - (int) Ending x position
|
||||
* @param $y - (int) Line to render
|
||||
* @param $color - (bool) Whether to include color
|
||||
* @returns {{x: number, content: string}}
|
||||
*/
|
||||
public function xdraw($start,$end,$y,$color): array
|
||||
{
|
||||
$content = '';
|
||||
|
||||
for ($x=$start;$x<=$end;$x++) {
|
||||
$rx = $this->ox+$x;
|
||||
$ry = $this->oy+$y;
|
||||
|
||||
// Check if we have an attribute to draw
|
||||
if (! (isset($this->content[$ry])) || ! (isset($this->content[$ry][$rx]))) {
|
||||
$content += ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($color === NULL || $color === true) {
|
||||
// Only write a new attribute if it has changed
|
||||
if (($this->last === NULL) || ($this->last !== $this->content[$ry][$rx]->attr)) {
|
||||
$this->last = $this->content[$ry][$rx]->attr;
|
||||
|
||||
$content += ($this->last === null ? BG_BLACK|LIGHTGRAY : $this->last);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$content += ($this->content[$ry][$rx]->ch !== null ? $this->content[$ry][$rx]->ch : ' ');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
dump($e);
|
||||
dump('---');
|
||||
dump('x:'.($x-$this->x));
|
||||
dump('y:'.($y-$this->y));
|
||||
dump('ox:'.$this->ox);
|
||||
dump('oy:'.$this->oy);
|
||||
dump('$rx:'.$rx);
|
||||
dump('$ry:'.$ry);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
return ['content'=>$content, 'x'=>$end - $start + 1];
|
||||
}
|
||||
}
|
104
app/Console/Commands/BBS/FrameImport.php
Normal file
104
app/Console/Commands/BBS/FrameImport.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\BBS;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
use App\Models\BBS\{Frame,Mode};
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class FrameImport extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'frame:import {frame} {file} '.
|
||||
'{--index=a : The frame index }'.
|
||||
'{--access=0 : Is frame accessible }'.
|
||||
'{--public=0 : Is frame limited to CUG }'.
|
||||
'{--cost=0 : Frame Cost }'.
|
||||
'{--mode=Ansi : Frame Emulation Mode }'.
|
||||
'{--replace : Replace existing frame}'.
|
||||
'{--type=i : Frame Type}'.
|
||||
'{--title= : Frame Title}'.
|
||||
'{--keys= : Key Destinations [0,1,2,3,4,5,6,7,8,9]}'.
|
||||
'{--trim= : Trim off header (first n chars)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import frames into the database. The frames should be in binary format.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! is_numeric($this->argument('frame')))
|
||||
throw new \Exception('Frame is not numeric: '.$this->argument('frame'));
|
||||
|
||||
if ((strlen($this->option('index')) !== 1) || (! preg_match('/^[a-z]$/',$this->option('index'))))
|
||||
throw new \Exception('Subframe failed validation');
|
||||
|
||||
if (! file_exists($this->argument('file')))
|
||||
throw new \Exception('File not found: '.$this->argument('file'));
|
||||
|
||||
$mo = Mode::where('name',$this->option('mode'))->firstOrFail();
|
||||
|
||||
$o = new Frame;
|
||||
if ($this->option('replace')) {
|
||||
try {
|
||||
$o = $o->where('frame',$this->argument('frame'))
|
||||
->where('index',$this->option('index'))
|
||||
->where('mode_id',$mo->id)
|
||||
->orderBy('created_at','DESC')
|
||||
->firstOrNew();
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$this->error('Frame not found to replace: '.$this->argument('frame').$this->option('index'));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$o->frame = $this->argument('frame');
|
||||
$o->index = $this->option('index');
|
||||
$o->mode_id = $mo->id;
|
||||
$o->access = $this->option('access');
|
||||
$o->public = $this->option('public');
|
||||
$o->cost = $this->option('cost');
|
||||
$o->type = $this->option('type');
|
||||
$o->title = $this->option('title');
|
||||
|
||||
$keys = [];
|
||||
if ($this->option('keys'))
|
||||
$keys = explode(',',$this->option('keys'));
|
||||
|
||||
foreach (range(0,9) as $key) {
|
||||
$index = sprintf('r%d',$key);
|
||||
|
||||
$o->{$index} = (($x=Arr::get($keys,$key,NULL)) === "null") ? NULL : $x;
|
||||
}
|
||||
|
||||
// We need to escape any back slashes, so they dont get interpretted as hex
|
||||
$o->content = $this->option('trim')
|
||||
? substr(file_get_contents($this->argument('file')),$this->option('trim'))
|
||||
: file_get_contents($this->argument('file'));
|
||||
|
||||
// If we have 0x1aSAUCE, we'll discard the sauce.
|
||||
if ($x = strpos($o->content,chr(0x1a).'SAUCE')) {
|
||||
$o->content = substr($o->content,0,$x-1).chr(0x0a);
|
||||
}
|
||||
|
||||
$o->save();
|
||||
|
||||
$this->info(sprintf('Saved frame: [%s] as [%s] with [%d]',$o->page,$mo->name,$o->id));
|
||||
}
|
||||
}
|
128
app/Console/Commands/BBS/Start.php
Normal file
128
app/Console/Commands/BBS/Start.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Viewdata/Videotex Server
|
||||
*
|
||||
* Inspired by Rob O'Donnell at irrelevant.com
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands\BBS;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\BBS\Server\{Ansitex,Videotex};
|
||||
use App\Classes\Sock\{SocketException,SocketServer};
|
||||
use App\Models\Mode;
|
||||
use App\Models\Setup;
|
||||
|
||||
class Start extends Command
|
||||
{
|
||||
private const LOGKEY = 'CBS';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bbs:start {--mode=VideoTex : Server Mode Profile}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Start BBS Server';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
* @throws SocketException
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Log::channel('bbs')->info(sprintf('%s:+ BBS Server Starting (%d)',self::LOGKEY,getmypid()));
|
||||
$o = Setup::findOrFail(config('app.id'));
|
||||
|
||||
$start = collect();
|
||||
|
||||
if (TRUE || $o->ansitex_active)
|
||||
$start->put('ansitex',[
|
||||
'address'=>$o->ansitex_bind,
|
||||
'port'=>$o->ansitex_port,
|
||||
'proto'=>SOCK_STREAM,
|
||||
'class'=>new Ansitex,
|
||||
]);
|
||||
|
||||
if (TRUE || $o->viewdata_active)
|
||||
$start->put('videotex',[
|
||||
'address'=>$o->videotex_bind,
|
||||
'port'=>$o->videotex_port,
|
||||
'proto'=>SOCK_STREAM,
|
||||
'class'=>new Videotex,
|
||||
]);
|
||||
|
||||
$children = collect();
|
||||
|
||||
Log::channel('bbs')->debug(sprintf('%s:# Servers [%d]',self::LOGKEY,$start->count()));
|
||||
|
||||
if (! $start->count()) {
|
||||
Log::channel('bbs')->alert(sprintf('%s:! No servers configured to start',self::LOGKEY));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//pcntl_signal(SIGCHLD,SIG_IGN);
|
||||
|
||||
foreach ($start as $item => $config) {
|
||||
Log::channel('bbs')->debug(sprintf('%s:- Starting [%s] (%d)',self::LOGKEY,$item,getmypid()));
|
||||
|
||||
$pid = pcntl_fork();
|
||||
|
||||
if ($pid === -1)
|
||||
die('could not fork');
|
||||
|
||||
// We are the child
|
||||
if (! $pid) {
|
||||
Log::channel('bbs')->withContext(['pid'=>getmypid()]);
|
||||
Log::channel('bbs')->info(sprintf('%s:= Started [%s]',self::LOGKEY,$item));
|
||||
|
||||
$server = new SocketServer($config['port'],$config['address'],$config['proto']);
|
||||
$server->handler = [$config['class'],'onConnect'];
|
||||
|
||||
try {
|
||||
$server->listen();
|
||||
|
||||
} catch (SocketException $e) {
|
||||
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
|
||||
Log::channel('bbs')->debug(sprintf('%s:! Server Terminated [%s]',self::LOGKEY,$item));
|
||||
else
|
||||
Log::channel('bbs')->emergency(sprintf('%s:! Uncaught Message: %s',self::LOGKEY,$e->getMessage()));
|
||||
}
|
||||
|
||||
Log::channel('bbs')->info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item));
|
||||
|
||||
// Child finished we need to get out of this loop.
|
||||
exit;
|
||||
}
|
||||
|
||||
Log::channel('bbs')->debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid));
|
||||
$children->put($pid,$item);
|
||||
}
|
||||
|
||||
// Wait for children to exit
|
||||
while ($x=$children->count()) {
|
||||
// Wait for children to finish
|
||||
$exited = pcntl_wait($status);
|
||||
|
||||
if ($exited < 0)
|
||||
abort(500,sprintf('Something strange for status: [%s] (%d)',pcntl_wifsignaled($status) ? pcntl_wtermsig($status) : 'unknown',$exited));
|
||||
|
||||
Log::channel('bbs')->info(sprintf('%s:= Exited: #%d [%s]',self::LOGKEY,$x,$children->pull($exited)));
|
||||
}
|
||||
|
||||
// Done
|
||||
Log::channel('bbs')->debug(sprintf('%s:= Finished.',self::LOGKEY));
|
||||
}
|
||||
}
|
40
app/Mail/BBS/SendToken.php
Normal file
40
app/Mail/BBS/SendToken.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\BBS;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
use App\Models\Service;
|
||||
|
||||
class SendToken extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $token = '';
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->markdown('email.bbs.sendtoken')
|
||||
->subject('Token to complete registration')
|
||||
->with(['token'=>$this->token]);
|
||||
}
|
||||
}
|
21
app/Models/BBS/CUG.php
Normal file
21
app/Models/BBS/CUG.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BBS;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CUG extends Model
|
||||
{
|
||||
protected $table = 'cugs';
|
||||
|
||||
public function isMember(CUG $o)
|
||||
{
|
||||
while ($o)
|
||||
{
|
||||
if (! $this->parent_id OR $o->id == $this->parent_id)
|
||||
return TRUE;
|
||||
|
||||
$o = $this::findOrFail($o->parent_id);
|
||||
}
|
||||
}
|
||||
}
|
92
app/Models/BBS/Frame.php
Normal file
92
app/Models/BBS/Frame.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BBS;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
use App\Casts\CompressedString;
|
||||
|
||||
/*
|
||||
* Frames belong to a CUG, and if no cug present, they are assigned to CUG 2, the "null CUG" to which all users
|
||||
* have access. In our implementation, we are using CUG 0 for this purpose.
|
||||
*
|
||||
* Users can see a frame if:
|
||||
* + The User Access is "Y"
|
||||
* + If the frame is assigned to a CUG, then only if the user is a member of that CUG can they access
|
||||
* + If the User Access is "N", then only the IP owner can access the frame.
|
||||
*
|
||||
* Frame types:
|
||||
* + I, i or space = information frame
|
||||
* + A, a, R or r = response frame
|
||||
*/
|
||||
|
||||
class Frame extends Model
|
||||
{
|
||||
protected $casts = [
|
||||
'content' => CompressedString::class,
|
||||
];
|
||||
|
||||
public $cache_content = '';
|
||||
|
||||
public function cug()
|
||||
{
|
||||
return $this->belongsTo(CUG::class);
|
||||
}
|
||||
|
||||
/*
|
||||
public function route()
|
||||
{
|
||||
return $this->hasOne(FrameMeta::class);
|
||||
}
|
||||
|
||||
protected static function boot() {
|
||||
parent::boot();
|
||||
|
||||
static::addGlobalScope('order', function (Builder $builder) {
|
||||
$builder->orderBy('created_at','DESC');
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* For cockroachDB, content is a "resource stream"
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function xgetContentAttribute()
|
||||
{
|
||||
// For stream resources, we need to cache this result.
|
||||
if (! $this->cache_content) {
|
||||
$this->cache_content = is_resource($this->attributes['content'])
|
||||
? stream_get_contents($this->attributes['content'])
|
||||
: $this->attributes['content'];
|
||||
}
|
||||
|
||||
return $this->cache_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Page Number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPageAttribute()
|
||||
{
|
||||
return $this->frame.$this->index;
|
||||
}
|
||||
|
||||
public function hasFlag(string $flag)
|
||||
{
|
||||
// @todo When flags is in the DB update this.
|
||||
return isset($this->flags) ? in_array($flag,$this->flags,FALSE) : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame Types
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
return $this->type ?: 'i';
|
||||
}
|
||||
}
|
13
app/Models/BBS/Mode.php
Normal file
13
app/Models/BBS/Mode.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BBS;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Mode extends Model
|
||||
{
|
||||
public function getNameAttribute(string $val): string
|
||||
{
|
||||
return strtolower($val);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
use App\Classes\BBS\Server\{Ansitex,Videotex};
|
||||
use App\Classes\Protocol\{Binkp,DNS,EMSI};
|
||||
|
||||
/**
|
||||
@ -48,6 +49,8 @@ class Setup extends Model
|
||||
case 'binkp_bind':
|
||||
case 'dns_bind':
|
||||
case 'emsi_bind':
|
||||
case 'ansitex_bind':
|
||||
case 'videotex_bind':
|
||||
return Arr::get($this->servers,str_replace('_','.',$key),self::BIND);
|
||||
|
||||
case 'binkp_port':
|
||||
@ -57,6 +60,11 @@ class Setup extends Model
|
||||
case 'emsi_port':
|
||||
return Arr::get($this->servers,str_replace('_','.',$key),EMSI::PORT);
|
||||
|
||||
case 'ansitex_port':
|
||||
return Arr::get($this->servers,str_replace('_','.',$key),Ansitex::PORT);
|
||||
case 'videotex_port':
|
||||
return Arr::get($this->servers,str_replace('_','.',$key),Videotex::PORT);
|
||||
|
||||
case 'options_options':
|
||||
return Arr::get($this->options,'options');
|
||||
|
||||
|
@ -29,6 +29,9 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use HasFactory,Notifiable,HasApiTokens,UserSwitch,ScopeActive;
|
||||
|
||||
/** @var int Enables users have unlimited online time in the BBS */
|
||||
public const FLAG_H = 1<<12;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
@ -91,6 +94,23 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
->get();
|
||||
}
|
||||
|
||||
public function hasExemption(int $flag): bool
|
||||
{
|
||||
// @todo To implement
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the user is already a member of the chosen network
|
||||
*
|
||||
* @param Domain $o
|
||||
* @return bool
|
||||
*/
|
||||
public function isMember(Domain $o): bool
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this user a ZC of a domain?
|
||||
*
|
||||
|
13
config/bbs.php
Normal file
13
config/bbs.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BBS Configuration items
|
||||
*/
|
||||
return [
|
||||
'home' => env('BBS_HOME', 1),
|
||||
'inactive_login' => env('BBS_INACTIVE_LOGIN',300),
|
||||
'inactive_nologin' => env('BBS_INACTIVE_NOLOGIN',60),
|
||||
'login' => env('BBS_LOGIN', 98),
|
||||
'register' => env('BBS_REGISTER', 981),
|
||||
'welcome' => env('BBS_WELCOME', 980),
|
||||
];
|
@ -54,6 +54,13 @@ return [
|
||||
'days' => 93,
|
||||
],
|
||||
|
||||
'bbs' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/ansitex.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => 14,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
|
118
database/migrations/2023_07_31_104758_bbs.php
Normal file
118
database/migrations/2023_07_31_104758_bbs.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->down();
|
||||
|
||||
Schema::create('modes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
$table->string('name',16);
|
||||
$table->string('note',255)->nullable();
|
||||
|
||||
$table->unique(['name']);
|
||||
});
|
||||
|
||||
Schema::create('cugs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
$table->string('name', 16);
|
||||
$table->string('description', 255)->nullable();
|
||||
|
||||
$table->integer('parent_id')->nullable();
|
||||
$table->foreign('parent_id')->references('id')->on('cugs');
|
||||
$table->unique(['name']);
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('cug_users', function (Blueprint $table) {
|
||||
$table->integer('cug_id')->unsigned();
|
||||
$table->foreign('cug_id')->references('id')->on('cugs');
|
||||
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
|
||||
$table->boolean('owner');
|
||||
|
||||
$table->unique(['user_id','cug_id']);
|
||||
});
|
||||
|
||||
Schema::create('frames', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
|
||||
$table->integer('frame')->unsigned();
|
||||
$table->char('index', 1);
|
||||
|
||||
$table->string('r0')->nullable();
|
||||
$table->string('r1')->nullable();
|
||||
$table->string('r2')->nullable();
|
||||
$table->string('r3')->nullable();
|
||||
$table->string('r4')->nullable();
|
||||
$table->string('r5')->nullable();
|
||||
$table->string('r6')->nullable();
|
||||
$table->string('r7')->nullable();
|
||||
$table->string('r8')->nullable();
|
||||
$table->string('r9')->nullable();
|
||||
|
||||
$table->string('type', 2);
|
||||
|
||||
$table->smallInteger('cost')->default(0);
|
||||
$table->boolean('access')->default(FALSE);
|
||||
$table->boolean('public')->default(FALSE);
|
||||
|
||||
$table->binary('content');
|
||||
$table->string('title',16)->nullable();
|
||||
$table->string('note', 255)->nullable();
|
||||
|
||||
$table->boolean('cls')->default(TRUE);
|
||||
//$table->unique(['frame','index','mode_id']); // Not needed since we have timewarp
|
||||
|
||||
$table->integer('mode_id')->unsigned();
|
||||
$table->foreign('mode_id')->references('id')->on('modes');
|
||||
|
||||
$table->integer('cug_id')->unsigned()->default(0);
|
||||
$table->foreign('cug_id')->references('id')->on('cugs');
|
||||
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
DB::statement("
|
||||
CREATE VIEW frame_view AS
|
||||
(
|
||||
SELECT F.id, F.frame || F.index as page,F.type,F.access,F.public,F.cls,C.name as cug,M.name as mode,F.cost,F.title,
|
||||
F.r0,F.r1,F.r2,F.r3,F.r4,F.r5,F.r6,F.r7,F.r8,F.r9
|
||||
FROM frames F
|
||||
LEFT JOIN cugs C ON C.id=F.cug_id
|
||||
LEFT JOIN modes M ON M.id=F.mode_id
|
||||
ORDER BY
|
||||
F.mode_id,F.frame,F.index
|
||||
)
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::statement('DROP VIEW IF EXISTS frame_view');
|
||||
Schema::dropIfExists('frames');
|
||||
Schema::dropIfExists('cug_users');
|
||||
Schema::dropIfExists('cugs');
|
||||
Schema::dropIfExists('modes');
|
||||
}
|
||||
};
|
42
database/seeders/BBSModes.php
Normal file
42
database/seeders/BBSModes.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BBSModes extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('modes')
|
||||
->insert([
|
||||
'created_at'=>Carbon::now(),
|
||||
'updated_at'=>Carbon::now(),
|
||||
'name'=>'Viewdata',
|
||||
'note'=>'Original 40x25 Viewdata'
|
||||
]);
|
||||
|
||||
DB::table('modes')
|
||||
->insert([
|
||||
'created_at'=>Carbon::now(),
|
||||
'updated_at'=>Carbon::now(),
|
||||
'name'=>'Ansi',
|
||||
'note'=>'ANSItex 80x25 mode, ANSI + Videotex'
|
||||
]);
|
||||
|
||||
DB::table('cugs')
|
||||
->insert([
|
||||
'id'=>0,
|
||||
'created_at'=>Carbon::now(),
|
||||
'updated_at'=>Carbon::now(),
|
||||
'name'=>'All Users',
|
||||
'description'=>'These frames are visible to all users. All frames belong to this CUG, unless specified.',
|
||||
]);
|
||||
}
|
||||
}
|
14
resources/views/email/bbs/sendtoken.blade.php
Normal file
14
resources/views/email/bbs/sendtoken.blade.php
Normal file
@ -0,0 +1,14 @@
|
||||
@component('mail::message')
|
||||
# New User Token
|
||||
|
||||
Use this token to sign into ANSItex
|
||||
|
||||
If this email is a surprise to you, you can ignore it.
|
||||
|
||||
@component('mail::panel')
|
||||
TOKEN: <b>{{ $token }}</b>
|
||||
@endcomponent
|
||||
|
||||
Thanks,
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
Loading…
Reference in New Issue
Block a user