/*
** Copyright (c) 2008 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
** drh@hwaci.com
** http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains an interface between the TH scripting language
** (an independent project) and fossil.
*/
#include "config.h"
#include "th_main.h"
/*
** Interfaces to register the scripting language extensions.
*/
int register_tcl(Jim_Interp *interp, void *pContext); /* th_tcl.c */
/*
** Generate a TH1 trace message if debugging is enabled.
*/
void Th_Trace(const char *zFormat, ...){
va_list ap;
va_start(ap, zFormat);
blob_vappendf(&g.thLog, zFormat, ap);
va_end(ap);
}
/*
** True if output is enabled. False if disabled.
*/
static long enableOutput = 1;
/*
** TH command: enable_output BOOLEAN
**
** Enable or disable the puts and hputs commands.
*/
static int enableOutputCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "BOOLEAN");
return JIM_ERR;
}
return Jim_GetLong(interp, argv[1], &enableOutput);
}
/*
** Send text to the appropriate output: Either to the console
** or to the CGI reply buffer.
*/
static void sendText(const char *z, int n, int encode){
if( enableOutput && n ){
if( n<0 ) n = strlen(z);
if( encode ){
z = htmlize(z, n);
n = strlen(z);
}
if( g.cgiOutput ){
cgi_append_content(z, n);
}else{
fwrite(z, 1, n, stdout);
fflush(stdout);
}
if( encode ) free((char*)z);
}
}
static void sendTextObj(Jim_Obj *objPtr, int encode)
{
sendText(Jim_String(objPtr), Jim_Length(objPtr), encode);
}
/*
** TH command: puts STRING
**
** Output STRING as HTML
*/
static int putsCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
sendText(Jim_String(argv[1]), -1, 1);
return JIM_OK;
}
/*
** TH command: html STRING
**
** Output STRING unchanged
*/
static int htmlCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
sendText(Jim_String(argv[1]), -1, 0);
return JIM_OK;
}
/*
** TH command: wiki STRING
**
** Render the input string as wiki.
*/
static int wikiCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
if( enableOutput ){
Blob src;
blob_init(&src, Jim_String(argv[1]), Jim_Length(argv[1]));
wiki_convert(&src, 0, WIKI_INLINE);
blob_reset(&src);
}
return JIM_OK;
}
/*
** TH command: htmlize STRING
**
** Escape all characters of STRING which have special meaning in HTML.
** Return a new string result.
*/
static int htmlizeCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
char *zOut;
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
zOut = htmlize(Jim_String(argv[1]), Jim_Length(argv[1]));
Jim_SetResultString(interp, zOut, -1);
free(zOut);
return JIM_OK;
}
/*
** TH command: date
**
** Return a string which is the current time and date. If the
** -local option is used, the date appears using localtime instead
** of UTC.
*/
static int dateCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
char *zOut;
if( argc>=2 && Jim_CompareStringImmediate(interp, argv[1], "-local")) {
zOut = db_text("??", "SELECT datetime('now','localtime')");
}else{
zOut = db_text("??", "SELECT datetime('now')");
}
Jim_SetResultString(interp, zOut, -1);
free(zOut);
return JIM_OK;
}
/*
** TH command: hascap STRING
**
** Return true if the user has all of the capabilities listed in STRING.
*/
static int hascapCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
int rc;
const char *str;
int len;
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
str = Jim_GetString(argv[1], &len);
rc = login_has_capability(str, len);
if( g.thTrace ){
Th_Trace("[hascap %#h] => %d<br />\n", len, str, rc);
}
Jim_SetResultInt(interp, rc);
return JIM_OK;
}
/*
** TH command: anycap STRING
**
** Return true if the user has any one of the capabilities listed in STRING.
*/
static int anycapCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
int rc = 0;
int i;
const char *str;
int len;
if( argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING");
return JIM_ERR;
}
str = Jim_GetString(argv[1], &len);
for(i=0; rc==0 && i<len; i++){
rc = login_has_capability(&str[i],1);
}
if( g.thTrace ){
Th_Trace("[hascap %#h] => %d<br />\n", len, str, rc);
}
Jim_SetResultInt(interp, rc);
return JIM_OK;
}
/*
** TH1 command: combobox NAME TEXT-LIST NUMLINES
**
** Generate an HTML combobox. NAME is both the name of the
** CGI parameter and the name of a variable that contains the
** currently selected value. TEXT-LIST is a list of possible
** values for the combobox. NUMLINES is 1 for a true combobox.
** If NUMLINES is greater than one then the display is a listbox
** with the number of lines given.
*/
static int comboboxCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
if( argc!=4 ){
Jim_WrongNumArgs(interp, 1, argv, "NAME TEXT-LIST NUMLINES");
return JIM_ERR;
}
if( enableOutput ){
long height;
char *z, *zH;
int nElem;
int i;
Jim_Obj *objPtr;
Jim_Obj *varObjPtr;
if( Jim_GetLong(interp, argv[3], &height) ) return JIM_ERR;
nElem = Jim_ListLength(interp, argv[2]);
varObjPtr = Jim_GetVariable(g.interp, argv[1], JIM_NONE);
z = mprintf("<select name=\"%z\" size=\"%d\">",
htmlize(Jim_String(varObjPtr), Jim_Length(varObjPtr)), height);
sendText(z, -1, 0);
free(z);
for(i=0; i<nElem; i++){
Jim_ListIndex(interp, argv[2], i, &objPtr, JIM_NONE);
zH = htmlize(Jim_String(objPtr), Jim_Length(objPtr));
if( varObjPtr && Jim_StringEqObj(varObjPtr, objPtr)) {
z = mprintf("<option value=\"%s\" selected=\"selected\">%s</option>",
zH, zH);
}else{
z = mprintf("<option value=\"%s\">%s</option>", zH, zH);
}
free(zH);
sendText(z, -1, 0);
free(z);
}
sendText("</select>", -1, 0);
}
return JIM_OK;
}
/*
** TH1 command: linecount STRING MAX MIN
**
** Return one more than the number of \n characters in STRING. But
** never return less than MIN or more than MAX.
*/
static int linecntCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
const char *z;
int size, n, i;
jim_wide iMin, iMax;
if( argc!=4 ){
Jim_WrongNumArgs(interp, 1, argv, "STRING MAX MIN");
return JIM_ERR;
}
if( Jim_GetWide(interp, argv[2], &iMax) ) return JIM_ERR;
if( Jim_GetWide(interp, argv[3], &iMin) ) return JIM_ERR;
z = Jim_GetString(argv[1], &size);
for(n=1, i=0; i<size; i++){
if( z[i]=='\n' ){
n++;
if( n>=iMax ) break;
}
}
if( n<iMin ) n = iMin;
if( n>iMax ) n = iMax;
Jim_SetResultInt(interp, n);
return JIM_OK;
}
/*
** TH1 command: repository ?BOOLEAN?
**
** Return the fully qualified file name of the open repository or an empty
** string if one is not currently open. Optionally, it will attempt to open
** the repository if the boolean argument is non-zero.
*/
static int repositoryCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
long openRepository;
if( argc!=1 && argc!=2 ){
Jim_WrongNumArgs(interp, 1, argv, "BOOLEAN");
return JIM_ERR;
}
if( argc==2 ){
if( Jim_GetLong(interp, argv[1], &openRepository) != JIM_OK){
return JIM_ERR;
}
if( openRepository ) db_find_and_open_repository(OPEN_OK_NOT_FOUND, 0);
}
Jim_SetResultString(interp, g.zRepositoryName, -1);
return JIM_OK;
}
/*
** Make sure the interpreter has been initialized. Initialize it if
** it has not been already.
**
** The interpreter is stored in the g.interp global variable.
*/
void Th_FossilInit(void){
static struct _Command {
const char *zName;
Jim_CmdProc xProc;
} aCommand[] = {
{"anycap", anycapCmd, },
{"combobox", comboboxCmd, },
{"enable_output", enableOutputCmd, },
{"linecount", linecntCmd, },
{"hascap", hascapCmd, },
{"htmlize", htmlizeCmd, },
{"date", dateCmd, },
{"html", htmlCmd, },
{"puts", putsCmd, },
{"wiki", wikiCmd, },
{"repository", repositoryCmd, },
{0, 0}
};
if( g.interp==0 ){
int i;
/* Create and initialize the interpreter */
g.interp = Jim_CreateInterp();
Jim_RegisterCoreCommands(g.interp);
/* Register static extensions */
Jim_InitStaticExtensions(g.interp);
#ifdef FOSSIL_ENABLE_TCL
if( getenv("FOSSIL_ENABLE_TCL")!=0 || db_get_boolean("tcl", 0) ){
register_tcl(g.interp, &g.tcl); /* Tcl integration commands. */
}
#endif
for(i=0; i<sizeof(aCommand)/sizeof(aCommand[0]); i++){
if ( !aCommand[i].zName || !aCommand[i].xProc ) continue;
Jim_CreateCommand(g.interp, aCommand[i].zName, aCommand[i].xProc, NULL,
NULL);
}
}
}
/*
** Store a string value in a variable in the interpreter.
*/
void Th_Store(const char *zName, const char *zValue){
Th_FossilInit();
if( zValue ){
if( g.thTrace ){
Th_Trace("set %h {%h}<br />\n", zName, zValue);
}
Jim_SetVariableStrWithStr(g.interp, zName, zValue);
}
}
/*
** Unset a variable.
*/
void Th_Unstore(const char *zName){
if( g.interp ){
Jim_Obj *nameObjPtr = Jim_NewStringObj(g.interp, zName, -1);
Jim_UnsetVariable(g.interp, nameObjPtr, JIM_NONE);
Jim_FreeNewObj(g.interp, nameObjPtr);
}
}
/*
** Retrieve a string value (variable) from the interpreter. If no such
** variable exists, return NULL.
*/
const char *Th_Fetch(const char *zName){
Jim_Obj *objPtr;
Th_FossilInit();
objPtr = Jim_GetVariableStr(g.interp, zName, JIM_NONE);
return objPtr ? Jim_String(objPtr) : NULL;
}
/**
* Like Th_Fetch() except the variable name may not be null terminated.
* Instead, the length of the name is supplied as 'namelen'.
*/
const char *Th_GetVar(Jim_Interp *interp, const char *name, int namelen){
Jim_Obj *nameObjPtr, *varObjPtr;
nameObjPtr = Jim_NewStringObj(interp, name, namelen);
Jim_IncrRefCount(nameObjPtr);
varObjPtr = Jim_GetVariable(interp, nameObjPtr, 0);
Jim_DecrRefCount(interp, nameObjPtr);
return varObjPtr ? Jim_String(varObjPtr) : NULL;
}
/*
** Return true if the string begins with the TH1 begin-script
** tag: <th1>.
*/
static int isBeginScriptTag(const char *z){
/* XXX: Should we also allow <tcl>? */
return z[0]=='<'
&& (z[1]=='t' || z[1]=='T')
&& (z[2]=='h' || z[2]=='H')
&& z[3]=='1'
&& z[4]=='>';
}
/*
** Return true if the string begins with the TH1 end-script
** tag: </th1>.
*/
static int isEndScriptTag(const char *z){
/* XXX: Should we also allow </tcl>? */
return z[0]=='<'
&& z[1]=='/'
&& (z[2]=='t' || z[2]=='T')
&& (z[3]=='h' || z[3]=='H')
&& z[4]=='1'
&& z[5]=='>';
}
/*
** If string z[0...] contains a valid variable name, return
** the number of characters in that name. Otherwise, return 0.
*/
static int validVarName(const char *z){
int i = 0;
int inBracket = 0;
if( z[0]=='<' ){
inBracket = 1;
z++;
}
if( z[0]==':' && z[1]==':' && fossil_isalpha(z[2]) ){
z += 3;
i += 3;
}else if( fossil_isalpha(z[0]) ){
z ++;
i += 1;
}else{
return 0;
}
while( fossil_isalnum(z[0]) || z[0]=='_' ){
z++;
i++;
}
if( inBracket ){
if( z[0]!='>' ) return 0;
i += 2;
}
return i;
}
/*
** The z[] input contains text mixed with TH1 scripts.
** The TH1 scripts are contained within <th1>...</th1>.
** TH1 variables are $aaa or $<aaa>. The first form of
** variable is literal. The second is run through htmlize
** before being inserted.
**
** This routine processes the template and writes the results
** on either stdout or into CGI.
*/
int Th_Render(const char *z){
int i = 0;
int n;
int rc = JIM_OK;
const char *zResult;
Th_FossilInit();
while( z[i] ){
if( z[i]=='$' && (n = validVarName(&z[i+1]))>0 ){
const char *zVar;
int nVar;
int encode = 1;
sendText(z, i, 0);
if( z[i+1]=='<' ){
/* Variables of the form $<aaa> are html escaped */
zVar = &z[i+2];
nVar = n-2;
}else{
/* Variables of the form $aaa are output raw */
zVar = &z[i+1];
nVar = n;
encode = 0;
}
zResult = Th_GetVar(g.interp, zVar, nVar);
z += i+1+n;
i = 0;
if (zResult) {
sendText(zResult, -1, encode);
}
}else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
Jim_Obj *objPtr;
sendText(z, i, 0);
z += i+5;
for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
/* XXX: Would be nice to record the source location in case of error */
objPtr = Jim_NewStringObj(g.interp, z, i);
rc = Jim_EvalObj(g.interp, objPtr);
if( rc!=JIM_OK ) break;
z += i;
if( z[0] ){ z += 6; }
i = 0;
}else{
i++;
}
}
if( rc==JIM_ERR ){
sendText("<hr><p class=\"thmainError\">ERROR: ", -1, 0);
sendTextObj(Jim_GetResult(g.interp), 1);
sendText("</p>", -1, 0);
}else{
sendText(z, i, 0);
}
return rc;
}
/*
** COMMAND: test-script-render
*/
void test_script_render(void){
Blob in;
if( g.argc<3 ){
usage("FILE");
}
db_open_config(0); /* Needed for global "tcl" setting. */
blob_zero(&in);
blob_read_from_file(&in, g.argv[2]);
Th_Render(blob_str(&in));
}