sbbs/load/frame-page.js

604 lines
16 KiB
JavaScript

// Our frame object
function PageFrame() {
'use strict';
/* Frame type settings */
this.settings = {};
/* Frame Version */
this.version = 1;
/* Frame Number [0-9] */
this.frame = null;
/* Frame Index [a-z] */
this.index = null;
/* The Service Provider owning the frame. */
this.owner = 0;
/* The cost to view the frame @todo to implement */
this.cost = 0;
/* The frame content, base64 encoded */
this.content = '';
// Frame's owned by the system where:
// isPublic is FALSE - the user must be logged in to view it
// isPublic is TRUE - can be viewed by non-logged in users
// Frame's owned by Service Providers where:
// isPublic is FALSE - can only be viewed if a user is
// a member of the Service Providers CUG
// isPublic is TRUE - can be viewed by users (if logged in)
this.isPublic = false; // Is this frame accessible to non CUG users
// If FALSE user must be a member of the CUG to view the frame
// All users, including unauthenticated, are members of 'system' (owner = 0)
this.isAccessible = false; // Is this frame available to be accessed
// If FALSE, only the SP can view/edit the frame
this.type = FRAME_TYPE_INFO; // The frame type - see FRAME_TYPES above
this.key = [null,null,null,null,null,null,null,null,null,null]; // Key actions [0-9]
/**
* Determine if this frame is accessible to the current user
*/
Object.defineProperty(this,'accessible',{
get: function() {
log(LOG_DEBUG,'- Checking if user can access frame: '+this.page);
log(LOG_DEBUG,' - User: '+JSON.stringify(user.number));
log(LOG_DEBUG,' - Frame Owner: '+JSON.stringify(this.owner)+', System Frame: '+(this.pageowner === SYSTEM_OWNER));
// user.number 0 is unidentified user.
if (user.number) {
return (
(this.isAccessible && this.pageowner === SYSTEM_OWNER && ! this.isPublic) ||
(this.isAccessible && this.isPublic) ||
(this.isAccessible && ! this.isPublic && this.isMember) ||
(pageEditor(this.frame))
);
} else {
return (this.isAccessible && this.pageowner === SYSTEM_OWNER && this.isPublic);
}
}
});
/**
* Return the frames fields
*/
Object.defineProperty(this,'fields', {
get: function() {
return this.frame_fields;
}
});
/**
* Check if the user is already a member of the CUG
*/
Object.defineProperty(this,'isMember',{
get: function() {
log(LOG_DEBUG,'- Checking if user is a member of frame: '+this.page);
if (user.number) {
return (
(this.pageowner === SYSTEM_OWNER)
);
} else {
return false;
}
}
})
/**
* Return the page number
*/
Object.defineProperty(this,'page', {
get: function() {
if (this.frame === null || this.index === null) return null;
return this.frame.toString()+this.index;
}
});
Object.defineProperty(this,'pageowner', {
get: function() {
return pageOwner(this.frame).prefix;
}
})
Object.defineProperty(this,'pageownerlogo', {
get: function() {
return base64_decode(this.settings.ext === 'tex' ? pageOwner(this.frame).logoans : pageOwner(this.frame).logovtx);
}
})
}
/**
* Enable pulling out submitted value by its name
*
* @param key
* @returns {null|string|*}
*/
PageFrame.prototype.fieldValue = function(key) {
for each (var k in this.frame_fields) {
log(LOG_DEBUG,' - k:'+JSON.stringify(k));
if (k.fname === key) {
return k.fvalue;
}
}
return null;
}
/**
* Return the message for a index
*
* @param index
* @returns {string|*}
*/
PageFrame.prototype.getMessage = function(index) {
eval('var msg = this.settings.'+index);
return msg;
}
/**
* Load a frame
*
* @param filename
* @returns {null}
*/
PageFrame.prototype.load = function(filename) {
log(LOG_DEBUG,'Loading FRAME from: '+filename+'.'+this.settings.ext);
this.sendBaseline('LOADING');
f = new File(system.mods_dir+'ansitex/text/'+filename+'.'+this.settings.ext);
if (! f.exists || ! f.open('r')) {
return null;
}
try {
var load = JSON.parse(f.read());
for (property in SAVED_FRAME_ATTRS) {
this[SAVED_FRAME_ATTRS[property]] = load[SAVED_FRAME_ATTRS[property]];
}
// If the page doesnt match the filename, throw an error
if (this.page !== filename)
throw 'Frame doesnt match filename';
} catch (error) {
log(LOG_ERROR,'Frame error: '+error);
// Load our system error frame.
this.load(pageStr(FRAME_SYSTEM_ERROR));
return null;
}
log(LOG_DEBUG,'Loaded frame: ['+this.frame+']['+this.index+'] ('+this.page+')');
};
/**
* Load a message frame
*
* @param page
*/
PageFrame.prototype.loadMessage = function(page) {
this.frame = ''+page;
this.index = 'a';
this.owner = 1;
this.isPublic = true;
this.isAccessible = true;
// @todo Keys should map to next/previous/send, etc as indicated in the template frame.
this.key = [this.frame.substr(0,7)+'1',null,null,null,null,null,null,null,null,null];
// @todo validate that FRAME_TYPE_MESSAGE is a message template
this.type = FRAME_TYPE_MESSAGE;
// Load our message
var ma = new MsgAreas()
var area = ma.getArea(this.frame);
var msg = ma.getMessage(this.frame);
var msg_header;
if (! msg)
return undefined;
// @todo Search 1zzzzEE..., 1zzzz...
var to = viewdata ? new FrameViewdata() : new FrameAnsi();
to.load(MAIL_TEMPLATE_FRAME);
// @todo Check that this is a frame of type "m" and report error if not
// @todo Take the cost from the template
this.cost = 5;
if (! to) {
log(LOG_ERROR,'Echomail template missing :['+MAIL_TEMPLATE_FRAME+'] ?');
msg_header = 'TO: '+msg.to.substr(0,72)+"\n\r";
msg_header += 'FROM: '+msg.from.substr(0,72)+"\n\r";
msg_header += 'DATE: '+msg.date.substr(0,72)+"\n\r";
msg_header += 'SUBJECT: '+msg.subject.substr(0,72)+"\n\r";
} else {
// @todo change this to use atcode()
msg_header = base64_decode(to.content).replace(/@(.*)@/g,
function (str, code, offset, s) {
var length = code.split(':')[1];
switch(code.split(':')[0]) {
case 'DATE': return msg.date.substr(0,length);
case 'TO': return msg.to.substr(0,length);
case 'FROM': return msg.from.substr(0,length);
case 'SUBJECT': return msg.subject.substr(0,length);
}
}
);
}
//log(LOG_DEBUG,'Loaded message: '+msg_header+msg.content);
this.content = base64_encode(msg_header+msg.content);
// Update the user's pointers
var stats = ma.getUserStats(this.frame);
// if this message is to the user, and the msg number > scan_ptr and it is the next message on the user's new mail list
var newmsgs = area.newMsgsToMe();
var next;
log(LOG_DEBUG,'User has: '+newmsgs.length-1+' msgs to read to ME');
if (newmsgs.length) {
next = newmsgs[1];
//log(LOG_DEBUG,'- NEXT is: '+next.tags+', this is: '+msg.tags);
if (next && (next.tags === msg.tags)) {
log(LOG_DEBUG,'- Updating scan_ptr to: '+next.number);
stats.scan_ptr = next.number;
}
// Last message
next = newmsgs[0];
if (next !== undefined) {
log(LOG_DEBUG,'- LAST TO ME is: '+next.tags);
this.key[1] = area.page(next.tags);
}
// Next new message
next = newmsgs[2];
if (next !== undefined) {
log(LOG_DEBUG,'- NEXT TO ME is: '+next.tags);
this.key[2] = area.page(next.tags);
}
}
// if this message is the next message, update last_read
newmsgs = area.newMsgs();
log(LOG_DEBUG,'User has: '+newmsgs.length+' msgs to read');
if (newmsgs.length) {
next = newmsgs[0];
log(LOG_DEBUG,'- NEXT is: '+next.tags+', this is: '+msg.tags);
if (next.tags === msg.tags) {
log(LOG_DEBUG,'- Updating last_read to: '+next.number);
stats.last_read = next.number;
}
}
// Previous Message
x = area.MessagePrev(this.frame);
if (x)
this.key[4] = area.page(x.tags);
// Next Message
x = area.MessageNext(this.frame);
if (x)
this.key[6] = area.page(x.tags);
log(LOG_DEBUG,'Built frame: ['+this.frame+']['+this.index+'] ('+this.page+')');
}
/**
* Parse the page text, and return the frame as 2 arrays:
* + First array is all the characters and the position on the frame
* + Second array is the array of the control codes that changes the color of the character
*
* The purpose of this function is to convert any special char sequences there are interpreted directly by Ansitex
* Currently they are:
* + ESC _ <value>[;value] ESC \
*
* Additionally, for response frames, if the cursor is moved to a field, its to determine what attributes (eg: color)
* should apply for that field.
*
* @param text
* @param context
*/
PageFrame.prototype.parse = function(text,context) {
log(LOG_DEBUG,'Parsing Frame ...');
if (this.type === FRAME_TYPE_MESSAGE) {
log(LOG_DEBUG,'- Not parsing message frame');
return text;
}
/** Column
* @type {number}
*/
var c = 1;
/** Row - row 1 is the header
* @type {number}
*/
var r = 2;
/**
* The output to send to the screen
* @type {string}
*/
var output = '';
/**
* Fields within the frame
* @type {{
* ftype: fieldtype,
* flength: fieldlen,
* fchar: fieldchar,
* fname: field,
* r: r,
* c: c,
* attribute: {i:i,f:f,b:b},
* fvalue: '',
* }}
*/
this.frame_fields = [];
// If there is no text return
if (! text)
return output;
// Default Attributes
// @note - this has no affect for viewdata frames
/* Foreground */
var f = 39;
/* Background */
var b = 49;
/* Intensity */
var i = 0;
for(var p=0;p<text.length;p++) {
// Look for a special character until the end of the frame width
cte = (r*this.settings.FRAME_WIDTH - ((r-1)*this.settings.FRAME_WIDTH+c));
match = text.substr(p,cte).match(/[\r\n\x1b]/);
//log(LOG_DEBUG,'SPECIAL CHAR ['+r+'x'+c+'] ['+p+'-'+cte+'] for: '+match);
if (match === null || match.index) {
advance = match ? match.index : cte;
//log(LOG_DEBUG,'INCLUDE: '+text.substr(p,advance));
output += text.substr(p,advance);
p += advance;
c += advance;
advance = 0;
}
// If the frame is not big enough, fill it with spaces.
byte = text.charAt(p);
advance = 0;
switch (byte) {
// Carriage Return
case "\r":
// log(LOG_DEBUG,'CR');
c=1;
break;
// New line
case "\n":
// log(LOG_DEBUG,'LF');
r++;
break;
// ESC
case KEY_ESC:
advance = 1;
// Is the next byte something we know about
nextbyte = text.charAt(p+advance);
//log(LOG_DEBUG,'ESC ['+r+'x'+c+'] NEXT: '+nextbyte);
switch (nextbyte) {
// CSI
case '[':
advance++;
chars = '';
// Find our end CSI param in the next 50 chars
matches = text.substr(p+advance,50).match(/([0-9]+[;]?)+([a-zA-Z])/);
//log(LOG_DEBUG,'CSI ['+r+'x'+c+'] ADVANCE: '+advance+', MATCHES: '+matches+', STRING: '+text.substr(p+advance,50));
if (! matches) {
chars += nextbyte;
break;
}
advance += matches[0].length-1;
chars += nextbyte+matches[0];
//log(LOG_DEBUG,'CSI ['+r+'x'+c+'] ADVANCE: '+advance+', CHARS: '+chars+', CHARSLEN: '+chars.length);
switch (matches[2]) {
// Color CSIs
case 'm':
//log(LOG_DEBUG,'CSI m ['+r+'x'+c+'] MATCHES: '+matches[0]+', LENGTH : '+matches[0].length);
csi = matches[0].substr(0,matches[0].length-1).split(';');
var num = null;
for (num in csi) {
//log(LOG_DEBUG,'CSI m ['+r+'x'+c+'] NUM: '+num+', CSI: '+csi[num]);
// Intensity
if (csi[num] >= 0 && csi[num] <= 8) {
i = csi[num];
f = 39;
b = 49;
// Forground Color
} else if (csi[num] >= 30 && csi[num] <= 39) {
f = csi[num];
// Background Color
} else if (csi[num] >= 40 && csi[num] <= 49) {
b = num;
}
}
break;
// Advance characters
case 'C':
//log(LOG_DEBUG,'CSI C ['+r+'x'+c+'] CHARS: '+matches[1]);
c += parseInt(matches[1]); // Advance our position
break;
default:
log(LOG_DEBUG,'? CSI: '+matches[2]);
}
break;
case ' ':
log(LOG_DEBUG,'LOOSE ESC? ['+r+'x'+c+'] '+advance);
break;
// X - dynamic fields, terminated with ESC
// Dynamic fields are defined with ESC X FIELD;LENGTH;PAD ESC \
// @note FIELD & LENGTH are required
// @note Dynamic field is can only be DYNAMIC_FIELD_SIZE_MAX chars in size
case 'X':
//log(LOG_DEBUG,'DF ['+r+'x'+c+'] '+advance);
advance++;
// Find our end ST param in the next DYNAMIC_FIELD_SIZE_MAX chars
matches = text.substr(p+advance,DYNAMIC_FIELD_SIZE_MAX).match(/(([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?)\x1b\\/);
//log(LOG_DEBUG,'- DF ['+r+'x'+c+'] ADVANCE: '+advance+', MATCHES: '+JSON.stringify(matches)+', LENGTH: '+(matches ? matches[0].length : 0)+', STRING: '+text.substr(p+advance,50));
if (! matches) {
chars += nextbyte;
break;
}
advance += matches[0].length-1;
var df = matches[0].substr(0,matches[0].length-2).split(';');
//log(LOG_DEBUG,'- DF ['+r+'x'+c+'] ADVANCE: '+advance+', DF: '+df);
log(LOG_DEBUG,'- DF found at ['+r+'x'+(c-1)+'], Field: '+df[0]+', Length: '+df[1]+', Attrs: '+JSON.stringify({i:i,f:f,b:b}));
chars = atcode(df[0],df[1],df[2],context);
byte = '';
break;
// _ - has our fields that take input
case '_':
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] '+advance);
advance++;
// Find our end ST param in the next 50 chars
matches = text.substr(p+advance,50).match(/(([A-Z]+;[0-9a-z]+)([;]?.+)?)\x1b\\/);
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] ADVANCE: '+advance+', MATCHES: '+matches+', LENGTH: '+matches[0].length+', STRING: '+text.substr(p+advance,50));
if (! matches) {
chars += nextbyte;
break;
}
advance += matches[0].length-1;
// The last 2 chars of matches[0] are the ESC \
var sos = matches[0].substr(0,matches[0].length-2).split(';');
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] ADVANCE: '+advance+', SOS: '+sos);
var num = null;
var field = null;
var fieldlen = null;
for (num in sos) {
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM: '+num+', SOS: '+sos[num]);
switch (num) {
// First value is the field name
case '0':
field = sos[num];
break;
// Second value is the length/type of the field
case '1':
x = sos[num].match(/([0-9]+)([a-z])/);
if (! x) {
log(LOG_ERROR,'SOS FAILED PARSING FIELD LENGTH/TYPE. ['+r+'x'+c+'] '+sos[num]);
break;
}
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM CHARS: '+x[1]+', TYPE: '+x[2]);
fieldlen = x[1];
fieldtype = x[2];
break;
// Third field is the char to to use
case '2':
fieldchar = sos[num];
break;
default:
log(LOG_ERROR,'IGNORING ADDITIONAL SOS FIELDS. ['+r+'x'+c+'] '+sos[num]);
}
}
if (fieldlen) {
chars = fieldchar.repeat(fieldlen);
}
byte = '';
this.frame_fields.push({
ftype: fieldtype,
flength: fieldlen,
fchar: fieldchar,
fname: field,
r: r,
c: c,
attribute: {i:i,f:f,b:b},
fvalue: '',
});
log(LOG_DEBUG,'SOS Field found at ['+r+'x'+(c-1)+'], Type: '+fieldtype+', Length: '+fieldlen+', Attrs: '+JSON.stringify({i:i,f:f,b:b}));
break;
default:
log(LOG_DEBUG,'DEFAULT ['+r+'x'+c+'] '+advance);
}
break;
default:
c++;
}
output += byte;
if (advance) {
//log(LOG_DEBUG,'ADVANCE P ['+r+'x'+c+'] '+advance+', NEXT CHAR: '+text.charAt(p+advance)+' ('+text.charCodeAt(p+advance)+')');
output += chars;
p += advance;
}
if (c>this.settings.FRAME_WIDTH) {
c = 1;
r++;
}
/*
// @todo - If we are longer than FRAME_LENGTH, move the output into the next frame.
if (r>this.settings.FRAME_LENGTH) {
break;
}
*/
}
return output;
};