/**
*/
api.import('unit-common')
api.cgi || api.loadModule('cgiish')
assert api.cgi
assert Fossil
Fossil.client = object{}
/**
If passed no args, returns this instance's private cache
storage (an Object). If passed 1 arg, returns the value
of cache.(argv.0). Otherwise is stores the value of argv.1
into the cache using the key argv.0 and returns the
value it cached.
*/
Fossil.Context.cache = proc(){
const c = (this._cache || (this._cache = object{}))
const argc = argv.length()
argc || return c
(1==argc) && return c.(argv.0)
return c.(argv.0) = argv.1
}
Fossil.Context.getProjectName = proc(){
var rc = this.cache('project-name')
if(!rc){
rc = this.selectVal({
SELECT value FROM config WHERE name='project-name'
})
rc && this.cache('project-name', rc)
}
return rc
}
catch { /* Set up some extra environment... */
const R = api.cgi.request;
const uri = R.ENV.REQUEST_URI
uri && (api.cgi.cgiRoot = uri.split('.cgi').0+'.cgi/')
const serverName = R.ENV.SERVER_NAME
api.cgi.isLocalServer = (!) || (0<=serverName.indexOf(".local"))
}
api.cgi.config = object{
scrubExceptions: !api.cgi.isLocalServer,//true,
onlyDeepestException: true,//false
sessionCookieKey: 'fSession',
cookiePath: api.cgi.cgiRoot
}
/**
Removes "potentially security-relevant" properties from the
exception ex, recursively in all 'rethrown' exceptions. If
ex.rethrown is set and api.cgi.config.onlyDeepestException is
true, it returns that value, else return ex.
*/
api.cgi.scrubException = proc(ex){
if(this.config.scrubExceptions){
unset ex.script, ex.callStack //, ex.line, ex.column // security-relevant
}
//this.config.onlyDeepestException && return ex
ex.rethrown && argv.callee.call(this, ex.rethrown)
return (this.config.onlyDeepestException && ex.rethrown) ? ex.rethrown : ex
}
/**
Assumes obj is a legal value for the 'J' format specifier for Buffer.appendf()
and outputs that data using some default, unspecified JSON indentation setting.
*/
api.cgi.printJSON = proc(obj){
print("%1$2J".applyFormat(obj))
}
/**
Assumes obj is a legal value for the 'J' format specifier for Buffer.appendf()
and returns that data's JSON string form, unspecified JSON indentation setting.
*/
api.cgi.toJSONString = proc(obj){
return "%1$2J".applyFormat(obj)
}
/**
Returns a variable from the GET, POST, or COOKIES
data, in that order, or default if none of those
sets contain the given key.
*/
api.cgi.getVar = proc(name, default = undefined){
const R = this.request
var rc = default
if(R.GET && R.GET.hasOwnProperty(name)){
rc = R.GET.(name)
//CGI.setCookie('limit', resultLimit)
}else if(R.POST && R.POST.hasOwnProperty(name)){
rc = R.POST.(name)
}else if(R.COOKIES && R.COOKIES.hasOwnProperty(name)){
rc = R.COOKIES.(name)
}
return rc
}
api.cgi.getCookie = proc(name){
const c = this.request.COOKIES
return (c && c.hasOwnProperty(name)) ? c.(name) : undefined
}
/**
Returns an app-specific session object, for storing _small amounts_
of persistent data. The session is submitted as a single JSON-encoded
cookie, so the data should be small.
The session is loaded or initialized the first time this is called,
and each time it is called, the session's 'lifetime' property is
updated.
*/
api.cgi.getSession = proc(){
var s = argv.callee.session
if(!s){
s = this.getCookie(this.config.sessionCookieKey)
s && catch{
const x = s
s = null
s = api.json.parse(x)
}
s || (s = object{
sessionId: 1234,
startTime: time()
})
s.startTime || (s.startTime = time())
s.lifetime = time()-s.startTime
s.visitCount = 1 + (+s.visitCount)
argv.callee.session = s
}
return s
}
/**
Don't use this - it turns out that each sub-path sets its own
cookie, no matter what path we tell it to use.
Encodes api.cgi.getSession() to JSON and sets it as the
cookie named by api.cgi.config.sessionCookieKey.
*/
api.cgi.savePathSession = proc(){
var s = this.getSession()
this.setCookie(this.config.sessionCookieKey, object{
path: '/', // this.config.cookiePath,
value:"%1$J".applyFormat(s)
})
}
/**
Expects func() to be a page generation function. To return
JSON, the function must simply return an Object or Array.
To generate other output, the function must call
api.cgi.setContentType(), use api.io.output() (or its
convenience form print(), or similar) to generate output,
and must return a falsy value.
func() is passed the CGI module's "this" as its parameter.
exit()s on success.
*/
api.cgi.runFuncAsPage = proc(func){
api.ob.push()
var saveSession = false
var rc = catch{
assert 'function' === typename func
assert func inherits api.prototypes.Function
toss func(this)
}
if(rc inherits api.Exception){
saveSession = false
rc = object{
exception: this.scrubException(rc)
}
}else if(rc){
rc = object{
body: rc
}
}
if(rc){
api.ob.clear()
this.setContentType('application/json')
this.printJSON(rc)
}
saveSession && this.savePathSession()
exit api.cgi.send()
}
api.cgi.util = object {
assertHasRepo: proc(){
assert Fossil.cx
assert Fossil.cx.db
assert Fossil.cx.db.repo
return Fossil.cx.db.repo
},
assertHasCheckout: proc(){
assert Fossil.cx
assert Fossil.cx.db
assert Fossil.cx.db.checkout
return Fossil.cx.db.checkout
}
}
/**
Dispatches to one of api.cgi.pages' pages, depending on
the PATH_INFO. It either throws or exit()s, but never
returns successfully.
*/
api.cgi.dispatchPage = proc(){
const CGI = api.cgi
const R = CGI.request
this.util.assertHasRepo()
// Look for a CGI.pages entry matching PATH_INFO...
var ps = R.ENV.PATH_INFO_SPLIT
var next = CGI.pages
var rightMost = next
var leftMost = array[]
// th1ish bug: cloning of arrays returns a Function:
// var rightPath = ps ? clone ps : undefined
var rightPath = ps ? ps.slice()/*clone*/ : undefined
//print(typename ps, ps)
ps && for{var x, i = 0, key, psLen = [ps.length]}
{next && (i<psLen)}{i+=1}{
key = ps.(i)
next.hasOwnProperty(key) || continue
x = next.(key)
next = x || undefined
next && (rightMost = next)
leftMost.push(rightPath.shift())
}
var cb
if ('function' === typename rightMost){
cb = rightMost
}else{
if(ps && ps.length()){
throw api.Exception(Fossil.rc.NOT_FOUND, "Unknown page: "+R.ENV.PATH_INFO)
}else{
cb = CGI.pages._default
}
}
//print('cb',cb)
//R.ENV.rightPath = rightPath.join('/')
//R.ENV.leftPath = leftMost.join('/')
CGI.runFuncAsPage(cb)
throw __SRC+": cannot be reached"
}
api.cgi.createPageProxyForFile = proc(pageBaseName){
const f = proc(cgi){
const pf = api.import.path
const pageName = {pages/}+pageBaseName
const fn = pf.search(pageName)
fn || throw api.Exception(Fossil.rc.NOT_FOUND, "Could not find page "+pageName)
//print(pf.prefix, pf.suffix)
import(fn)
}.importSymbols('pageBaseName')
return f
}
api.cgi.pages = object {
foo: object{
bar:proc(cgi){
cgi.setContentType('text/plain')
print("should not be emitted")
api.ob.clear()
print(/*__SRC,*/"hi from", cgi.request.ENV.PATH_INFO)
}
},
err1: 'not a function',
cookie: proc(cgi){
cgi.setHeader('X-request-time', [time]);
cgi.setCookie('myCookie', true);
cgi.setCookie('yourCookie', object{
path: '/'
// domain: string,
httpOnly: true,
// secure: bool,
value:'1 2 3',
expires: [time] + (60*60*24)
});
cgi.setContentType('text/html')
assert 0 < [api.ob.level]
$print {<html><body><h1>Hi from cgiish!</h1>}
const R = cgi.request
$print {<textarea cols='80' rows='20'>} \
object {
timestamp: [time],
cookies: R.COOKIES,
//env: R.ENV,
get: R.GET
} \
{</textarea>}
$print {</body></html>}
},
manifest: api.cgi.createPageProxyForFile('manifest'),
snap: proc(cgi){
//const x = api.prototypes.Exception(3,"!!!");
//throw x ; //"Aw, snap! Google changed its APIs again!";
throw api.Exception(Fossil.rc.ERROR, "Aw, snap! Google v8 changed its APIs again!")
},
timeline: scope{
var pmain = api.cgi.createPageProxyForFile('timeline')
pmain.html = api.cgi.createPageProxyForFile('timeline-html')
pmain
}/*object{
text: api.cgi.createPageProxyForFile('timeline'),
html: api.cgi.createPageProxyForFile('timeline-html')
}*/,
_default: proc(cgi){
const R = cgi.request
const now = time()
//cgi.setCookie( 'default-page', object{value: now, path:'/foo/', httpOnly:true})
var rc = object {
timestamp: [time],
//cookies: R.COOKIES,
get: R.GET,
//ARGV: ARGV
cgiRoot: cgi.cgiRoot,
//session: cgi.getSession(),
'Fossil.rc': Fossil.rc
}
if(cgi.isLocalServer){
rc.privileged = object{
env: R.ENV
}
}
if(Fossil.cx && Fossil.cx.db && Fossil.cx.db.repo){
rc.context = "%1$p".applyFormat(Fossil.cx)
rc.repo = object{
trunkVersion: Fossil.cx.symToUuid("trunk"),
projectName: Fossil.cx.getProjectName()
}
}
return rc
}
}
unset api.cgi.createPageProxyForFile