Login
init.fossi1ish at [39121d2e23]
Login

File th1ish/cgi/init.fossi1ish artifact 098243045a part of check-in 39121d2e23


/**

*/

assert 'function' === typename api.loadModule
api.cgi || api.loadModule('cgiish')
//api.import.path.suffix.push('.fossi1ish')
const DIR = Fossil.file.dirPart(Fossil.file.canonicalName(__FILE))
api.import('fossil-extend')
api.import(DIR+'extend-cgiish')

//print(Fossil.file.dirPart(Fossil.file.canonicalName(__FILE)))
api.cgi.config.resourceRootDir = DIR

api.cgi.config.showPageSourceLinks = true
api.cgi.config.defaultSkinName = 'default'

/**
    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. Propagates an exception on serious
    errors but catches errors thrown by func()
*/
api.cgi.runFuncAsPage = proc(func){
    const ob = api.ob
    const obLevel = ob.level()
    var saveSession = false
    var rc = catch{
        //assert func inherits
        assert func inherits api.prototypes.Function
        if('function' === typename func.render){
            toss func.render(this)
        }else{
            toss func(this)
        }
    }
    ob.push()

    const httpCode = this.httpStatus()
    if(rc inherits api.Exception){
        saveSession = false
        rc = object{
            exception: this.scrubException(rc)
        }
        this.httpStatus(500, "Caught Exception")
        while(ob.level() > obLevel ){
            ob.pop()
        }
    }else if(rc){ 
        rc = object{
            body: rc
        }
    }else if( (httpCode>=500) && (httpCode<600) ){
        saveSession = false
        rc = undefined
        while(ob.level() > obLevel ){
            ob.pop()
        }
        print("It's dead, Jim.");
    }

    if(rc){
        ob.clear()
        this.setContentType('application/json')
        this.printJSON(rc)
    }
    saveSession && this.savePathSession()
    this.fsl && this.fsl.finalize()
    this.send()
    exit 0
}


/**
    A helper routine for pages which want to show their source code
    if the user provides the showSrc=1 parameter. If that is set,
    it outputs the given file and returns true, otherwise it
    outputs a link to the current page with showSrc=1 set and returns
    false.
*/
api.cgi.handleShowSource = proc(file, showLink = false){
    const ce = argv.callee
    (undefined != ce.result) && return ce.result
    if(+this.getVar('showSrc',0)){
        $out {<pre class='code'>} this.htmlEscape(slurpFile((file))) {</pre>}
        return ce.result = true
    }
    showLink && $out {<div>[<a href='?showSrc=1'>soure code for this page</a>]</div>}
    return ce.result = false
}

/**
    Expects obj to be an Object. Each local property in obj
    with a non-undefined values gets appended in the form:

    key1=val1&key2=val2&...

    each key/value gets urlencoded. Values which ===undefined
    are skipped.

    Returns '' if ('object'!==typename obj).

    Returns '' if obj has no properties to 
*/
api.cgi.objToUrlParams = proc(obj){
    ('object' === typename obj) || return ''
    const ce = argv.callee
    const buf = (ce.buffer || (ce.buffer=api.Buffer(100)))
    var count = 0
    const cgi = this
    const propFunc = (ce.propFunc || (ce.propFunc=proc(k,v){
        (undefined === v) && return
        count += 1
        (1!==count) && buf.append('&')
        buf.append(cgi.urlencode(k),'=',cgi.urlencode(''+v))
    }))
    buf.reset()
    obj.eachProperty(propFunc)
    return count ? buf.toString() : ''
}

/**
  Returns the root CGI path suffixed by appName.
*/
api.cgi.getAppUrlPath = proc(appName){
    return this.cgiRoot + appName
}


/**
  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.
*/
api.cgi.cache = proc(){
    const ce = argv.callee
    const c = (ce._cache || (ce._cache = object{}))
    const argc = argv.length()
    argc || return c
    (1==argc) && return c.(argv.0)
    return c.(argv.0) = argv.1
}

/**
    Returns a shared fossil instance created using
    Fossil.createContext(). It is created, and its repo
    (possibly) opened on the first call and returned
    as-is on subsequent calls.
*/
api.cgi.getFossilInstance = proc(){
    const ce = argv.callee
    return ce.fsl || (ce.fsl = Fossil.createContext())
}


api.cgi.util = object {
    assertHasRepo: proc(){
        const F = api.cgi.getFossilInstance()
        assert F
        assert F.db
        assert F.db.repo
        return F.db.repo
    },
    assertHasCheckout: proc(){
        const F = api.cgi.getFossilInstance()
        assert F
        assert F.db
        assert F.db.checkout
        return F.db.checkout
    }
}

api.cgi.resolveResourceName = proc(baseName){
    return this.config.resourceRootDir+baseName
}

api.cgi.resolveResourcePath = proc(baseName){
    const pageName = this.resolveResourceName(baseName)
    const fn = api.import.path.search(pageName)
    /**
       Reminder to self: if this throws, ensure that search path has
       a "" somewhere in it so that "near-match" lookups (right
       path, wrong extension) work on my hoster!
    */
    if(!fn){
        const shortName = pageName.substr(this.config.resourceRootDir.length())
        throw api.Exception(Fossil.rc.NOT_FOUND, "Could not find resource "+shortName)
    }
    return fn
}

api.cgi.html = object {
    /**
        Returns an HTML Anchor tag ("A") with the given URL and label. The label
        defaults to the URL.
    */
    createAnchor: proc(url,label){
        ('string'===typename label) || (label = ''.concat(label))
        return "<a href='%1$s'>%2$s</a>".applyFormat(url, api.cgi.htmlEscape(label || url))
    },
    /**
        Assumes the given URL resolves a CSS content and outputs
        JavaScript code which will inject the stylesheet into the
        page header, such that it will get properly added to the
        page's CSS (and need not be injected directly as a STYLE tag
        into the current page's output). The advantage of this approach
        is that the url's destination (e.g. /download/css/...) can add
        caching-related headers.

        The generated script injects no new symbols and this can be called
        an arbitrary number of times.

        Credits go to:

        http://stackoverflow.com/questions/3744270/dynamically-loading-css
    */
    injectCssLink:proc(url){
        $api.cgi.out <<<EOF <script type='application/javascript'>(function(){
            var fileref = document.createElement("link");
            fileref.setAttribute("rel", "stylesheet");
            fileref.setAttribute("type", "text/css");
            fileref.setAttribute("href", 'EOF url <<<EOF');
            document.getElementsByTagName("head")[0].appendChild(fileref); 
        })()</script>EOF
    }
}/*api.cgi.html*/


/**
    If name's extension (the part after the LAST period) matches
    (case-insensitively) a known list of mime types, this returnsthat
    mime type string, else it returns the 2nd parameter's value.
*/
api.cgi.guessMimeType = proc(name, default = 'application/octet-stream'){
    const ce = argv.callee
    const map = (ce.map || (ce.map=scope{
        const map = api.Hash(117)
        map.insert('c', 'text/plain')
        map.insert('h', 'text/plain')
        map.insert('css', 'text/css')
        map.insert('html', 'text/html')
        map.insert('in', 'text/plain')
        map.insert('make', 'text/plain')
        map.insert('sh', 'text/plain')
        map.insert('txt', 'text/plain')
        map.insert('th1ish', 'text/plain')
        map.insert('fossi1ish', 'text/plain')
        map.insert('png', 'image/png')
        map.insert('jpg', 'image/jpeg')
        map.insert('jpeg', 'image/jpeg')
        map
    }))
    return (map.search(name.split('.').pop().toLower()) || default)
}

/**
    If filename's mimeType (as determined by the 2nd parameter
    or guessMimeType(filename)) belongs to a known set of
    "downloadable" mime types, a Content-Disposition header
    is added which hints to the browser that it should "download"
    instead of "render" the page. The "attachment's" name is
    the non-directory part of the given filename. Returns true if
    it outputs that header.
*/
api.cgi.handleContentDisposition = proc(filename, mimeType){
    assert 'string' === typename filename
    mimeType || (mimeType = this.guessMimeType(filename))
    assert 'string' === typename mimeType
    const ce = argv.callee
    const baseName = filename.split('/').pop()
    const map = (ce.map || (ce.map=object{
        {application/octet-stream}: true
    }))
    if(map.hasOwnProperty(mimeType)){
        cgi.setHeader("Content-Disposition", 'attachment; filename="'.concat(baseName, '"'))
        return true
    }
}


/**
    Expects name to be the base name of a script file under
    resolveResourcePath('widgets/'). It creates a DIV wrapper
    with two classes: fsl-widget and fsl-widget-name (the
    literal name passed to this function). In that DIV
    the widget is called. The widget gets all arguments
    passed to this function, and may expect the 2nd argument
    to contain an Object with configuration parameters.

    If an exception is thrown in the widget, it is displayed
    (scrubbed) in place of the widget's output.    
*/
api.cgi.renderWidget = proc(name){
    const fn = this.resolveResourcePath({widgets/}+name)
    $this.out "<div class='fsl-widget fsl-widget-" name "'>"
    api.ob.push()
    const oldRenderArgs = this.WIDGET_RENDER_ARGS
    this.WIDGET_RENDER_ARGS = argv
    const err = catch{
        api.import(fn)
    }
    this.WIDGET_RENDER_ARGS = oldRenderArgs
    if(err){
        api.ob.clear();
        $this.out {<div class='exception'>Exception in widget} " '" name "':<br/>"
        $this.out {<textarea rows='10' cols='80' class='exception'>}
        $this.out this.toJSONString(this.scrubException(err))
        $this.out {</textarea></div>}
    }
    api.ob.flush()
    api.ob.pop()
    $this.out {</div><!-- 'fsl-widget' -->}
}

api.import(DIR+'routing')