/*
** Copyright (c) 2006,2007 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 code to implement the basic web page look and feel.
**
*/
#include "VERSION.h"
#include "config.h"
#include "style.h"
/*
** Elements of the submenu are collected into the following
** structure and displayed below the main menu.
**
** Populate these structure with calls to
**
** style_submenu_element()
** style_submenu_entry()
** style_submenu_checkbox()
** style_submenu_binary()
** style_submenu_multichoice()
** style_submenu_sql()
**
** prior to calling style_finish_page(). The style_finish_page() routine
** will generate the appropriate HTML text just below the main
** menu.
*/
static struct Submenu {
const char *zLabel; /* Button label */
const char *zLink; /* Jump to this link when button is pressed */
} aSubmenu[30];
static int nSubmenu = 0; /* Number of buttons */
static struct SubmenuCtrl {
const char *zName; /* Form query parameter */
const char *zLabel; /* Label. Might be NULL for FF_MULTI */
unsigned char eType; /* FF_ENTRY, FF_MULTI, FF_CHECKBOX */
unsigned char eVisible; /* STYLE_NORMAL or STYLE_DISABLED */
short int iSize; /* Width for FF_ENTRY. Count for FF_MULTI */
const char *const *azChoice; /* value/display pairs for FF_MULTI */
const char *zFalse; /* FF_BINARY label when false */
const char *zJS; /* Javascript to run on toggle */
} aSubmenuCtrl[20];
static int nSubmenuCtrl = 0;
#define FF_ENTRY 1 /* Text entry box */
#define FF_MULTI 2 /* Combobox. Multiple choices. */
#define FF_BINARY 3 /* Control for binary query parameter */
#define FF_CHECKBOX 4 /* Check-box */
#if INTERFACE
#define STYLE_NORMAL 0 /* Normal display of control */
#define STYLE_DISABLED 1 /* Control is disabled */
#endif /* INTERFACE */
/*
** Remember that the header has been generated. The footer is omitted
** if an error occurs before the header.
*/
static int headerHasBeenGenerated = 0;
/*
** remember, if a sidebox was used
*/
static int sideboxUsed = 0;
/*
** Ad-unit styles.
*/
static unsigned adUnitFlags = 0;
/*
** Submenu disable flag
*/
static int submenuEnable = 1;
/*
** Disable content-security-policy.
** Warning: Do not disable the CSP without careful consideration!
*/
static int disableCSP = 0;
/*
** Flags for various javascript files needed prior to </body>
*/
static int needHrefJs = 0; /* href.js */
/*
** Extra JS added to the end of the file.
*/
static Blob blobOnLoad = BLOB_INITIALIZER;
/*
** Generate and return a anchor tag like this:
**
** <a href="URL">
** or <a id="ID">
**
** The form of the anchor tag is determined by the g.javascriptHyperlink
** and g.perm.Hyperlink variables.
**
** g.perm.Hyperlink g.javascriptHyperlink Returned anchor format
** ---------------- --------------------- ------------------------
** 0 0 (empty string)
** 0 1 (empty string)
** 1 0 <a href="URL">
** 1 1 <a data-href="URL">
**
** No anchor tag is generated if g.perm.Hyperlink is false.
** The href="URL" form is used if g.javascriptHyperlink is false.
** If g.javascriptHyperlink is true then the data-href="URL" and
** href="/honeypot" is generated and javascript is added to the footer
** to cause data-href values to be inserted into href
** after the page has loaded. The use of the data-href="URL" form
** instead of href="URL" is a defense against bots.
**
** If the user lacks the Hyperlink (h) property and the "auto-hyperlink"
** setting is true, then g.perm.Hyperlink is changed from 0 to 1 and
** g.javascriptHyperlink is set to 1 by login_check_credentials(). Thus
** the g.perm.Hyperlink property will be true even if the user does not
** have the "h" privilege if the "auto-hyperlink" setting is true.
**
** User has "h" auto-hyperlink g.perm.Hyperlink g.javascriptHyperlink
** ------------ -------------- ---------------- ---------------------
** 0 0 0 0
** 1 0 1 0
** 0 1 1 1
** 1 1 1 0
**
** So, in other words, tracing input configuration to final actions we have:
**
** User has "h" auto-hyperlink Returned anchor format
** ------------ -------------- ----------------------
** 0 0 (empty string)
** 1 0 <a href="URL">
** 0 1 <a data-href="URL">
** 1 1 <a href="URL">
**
** The name of these routines are deliberately kept short so that can be
** easily used within @-lines. Example:
**
** @ %z(href("%R/artifact/%s",zUuid))%h(zFN)</a>
**
** Note %z format. The string returned by this function is always
** obtained from fossil_malloc() so rendering it with %z will reclaim
** that memory space.
**
** There are three versions of this routine:
**
** (1) href() does a plain hyperlink
** (2) xhref() adds extra attribute text
** (3) chref() adds a class name
**
** g.perm.Hyperlink is true if the user has the Hyperlink (h) property.
** Most logged in users should have this property, since we can assume
** that a logged in user is not a bot. Only "nobody" lacks g.perm.Hyperlink,
** typically.
*/
char *xhref(const char *zExtra, const char *zFormat, ...){
char *zUrl;
va_list ap;
if( !g.perm.Hyperlink ) return fossil_strdup("");
va_start(ap, zFormat);
zUrl = vmprintf(zFormat, ap);
va_end(ap);
if( !g.javascriptHyperlink ){
char *zHUrl;
if( zExtra ){
zHUrl = mprintf("<a %s href=\"%h\">", zExtra, zUrl);
}else{
zHUrl = mprintf("<a href=\"%h\">", zUrl);
}
fossil_free(zUrl);
return zHUrl;
}
needHrefJs = 1;
if( zExtra==0 ){
return mprintf("<a data-href='%z' href='%R/honeypot'>", zUrl);
}else{
return mprintf("<a %s data-href='%z' href='%R/honeypot'>",
zExtra, zUrl);
}
}
char *chref(const char *zExtra, const char *zFormat, ...){
char *zUrl;
va_list ap;
if( !g.perm.Hyperlink ) return fossil_strdup("");
va_start(ap, zFormat);
zUrl = vmprintf(zFormat, ap);
va_end(ap);
if( !g.javascriptHyperlink ){
char *zHUrl = mprintf("<a class=\"%s\" href=\"%h\">", zExtra, zUrl);
fossil_free(zUrl);
return zHUrl;
}
needHrefJs = 1;
return mprintf("<a class='%s' data-href='%z' href='%R/honeypot'>",
zExtra, zUrl);
}
char *href(const char *zFormat, ...){
char *zUrl;
va_list ap;
if( !g.perm.Hyperlink ) return fossil_strdup("");
va_start(ap, zFormat);
zUrl = vmprintf(zFormat, ap);
va_end(ap);
if( !g.javascriptHyperlink ){
char *zHUrl = mprintf("<a href=\"%h\">", zUrl);
fossil_free(zUrl);
return zHUrl;
}
needHrefJs = 1;
return mprintf("<a data-href='%s' href='%R/honeypot'>",
zUrl);
}
/*
** Generate <form method="post" action=ARG>. The ARG value is determined
** by the arguments.
**
** As a defense against robots, the action=ARG might instead by data-action=ARG
** and javascript (href.js) added to the page so that the data-action= is
** changed into action= after the page loads. Whether or not this happens
** depends on if the user has the "h" privilege and whether or not the
** auto-hyperlink setting is on. These setings determine the values of
** variables g.perm.Hyperlink and g.javascriptHyperlink.
**
** User has "h" auto-hyperlink g.perm.Hyperlink g.javascriptHyperlink
** ------------ -------------- ---------------- ---------------------
** 1: 0 0 0 0
** 2: 1 0 1 0
** 3: 0 1 1 1
** 4: 1 1 1 0
**
** The data-action=ARG form is used for cases 1 and 3. In case 1, the href.js
** javascript is omitted and so the form is effectively disabled.
*/
void form_begin(const char *zOtherArgs, const char *zAction, ...){
char *zLink;
va_list ap;
if( zOtherArgs==0 ) zOtherArgs = "";
va_start(ap, zAction);
zLink = vmprintf(zAction, ap);
va_end(ap);
if( g.perm.Hyperlink ){
@ <form method="POST" action="%z(zLink)" %s(zOtherArgs)>
}else{
needHrefJs = 1;
@ <form method="POST" data-action='%s(zLink)' action='%R/login' \
@ %s(zOtherArgs)>
}
}
/*
** Add a new element to the submenu
*/
void style_submenu_element(
const char *zLabel,
const char *zLink,
...
){
va_list ap;
assert( nSubmenu < count(aSubmenu) );
aSubmenu[nSubmenu].zLabel = zLabel;
va_start(ap, zLink);
aSubmenu[nSubmenu].zLink = vmprintf(zLink, ap);
va_end(ap);
nSubmenu++;
}
void style_submenu_entry(
const char *zName, /* Query parameter name */
const char *zLabel, /* Label before the entry box */
int iSize, /* Size of the entry box */
int eVisible /* Visible or disabled */
){
assert( nSubmenuCtrl < count(aSubmenuCtrl) );
aSubmenuCtrl[nSubmenuCtrl].zName = zName;
aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
aSubmenuCtrl[nSubmenuCtrl].iSize = iSize;
aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
aSubmenuCtrl[nSubmenuCtrl].eType = FF_ENTRY;
nSubmenuCtrl++;
}
void style_submenu_checkbox(
const char *zName, /* Query parameter name */
const char *zLabel, /* Label to display after the checkbox */
int eVisible, /* Visible or disabled */
const char *zJS /* Optional javascript to run on toggle */
){
assert( nSubmenuCtrl < count(aSubmenuCtrl) );
aSubmenuCtrl[nSubmenuCtrl].zName = zName;
aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
aSubmenuCtrl[nSubmenuCtrl].zJS = zJS;
aSubmenuCtrl[nSubmenuCtrl].eType = FF_CHECKBOX;
nSubmenuCtrl++;
}
void style_submenu_binary(
const char *zName, /* Query parameter name */
const char *zTrue, /* Label to show when parameter is true */
const char *zFalse, /* Label to show when the parameter is false */
int eVisible /* Visible or disabled */
){
assert( nSubmenuCtrl < count(aSubmenuCtrl) );
aSubmenuCtrl[nSubmenuCtrl].zName = zName;
aSubmenuCtrl[nSubmenuCtrl].zLabel = zTrue;
aSubmenuCtrl[nSubmenuCtrl].zFalse = zFalse;
aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
aSubmenuCtrl[nSubmenuCtrl].eType = FF_BINARY;
nSubmenuCtrl++;
}
void style_submenu_multichoice(
const char *zName, /* Query parameter name */
int nChoice, /* Number of options */
const char *const *azChoice, /* value/display pairs. 2*nChoice entries */
int eVisible /* Visible or disabled */
){
assert( nSubmenuCtrl < count(aSubmenuCtrl) );
aSubmenuCtrl[nSubmenuCtrl].zName = zName;
aSubmenuCtrl[nSubmenuCtrl].iSize = nChoice;
aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
nSubmenuCtrl++;
}
void style_submenu_sql(
const char *zName, /* Query parameter name */
const char *zLabel, /* Label on the control */
const char *zFormat, /* Format string for SQL command for choices */
... /* Arguments to the format string */
){
Stmt q;
int n = 0;
int nAlloc = 0;
char **az = 0;
va_list ap;
va_start(ap, zFormat);
db_vprepare(&q, 0, zFormat, ap);
va_end(ap);
while( SQLITE_ROW==db_step(&q) ){
if( n+2>=nAlloc ){
nAlloc += nAlloc + 20;
az = fossil_realloc(az, sizeof(char*)*nAlloc);
}
az[n++] = fossil_strdup(db_column_text(&q,0));
az[n++] = fossil_strdup(db_column_text(&q,1));
}
db_finalize(&q);
if( n>0 ){
aSubmenuCtrl[nSubmenuCtrl].zName = zName;
aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
aSubmenuCtrl[nSubmenuCtrl].iSize = n/2;
aSubmenuCtrl[nSubmenuCtrl].azChoice = (const char *const *)az;
aSubmenuCtrl[nSubmenuCtrl].eVisible = STYLE_NORMAL;
aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
nSubmenuCtrl++;
}
}
/*
** Disable or enable the submenu
*/
void style_submenu_enable(int onOff){
submenuEnable = onOff;
}
/*
** Compare two submenu items for sorting purposes
*/
static int submenuCompare(const void *a, const void *b){
const struct Submenu *A = (const struct Submenu*)a;
const struct Submenu *B = (const struct Submenu*)b;
return fossil_strcmp(A->zLabel, B->zLabel);
}
/* Use this for the $current_page variable if it is not NULL. If it
** is NULL then use g.zPath.
*/
static char *local_zCurrentPage = 0;
/*
** Set the desired $current_page to something other than g.zPath
*/
void style_set_current_page(const char *zFormat, ...){
fossil_free(local_zCurrentPage);
if( zFormat==0 ){
local_zCurrentPage = 0;
}else{
va_list ap;
va_start(ap, zFormat);
local_zCurrentPage = vmprintf(zFormat, ap);
va_end(ap);
}
}
/* Use this for the $base_href_suffix variable if it is not NULL.
** If it is NULL then use g.zRelReqURI
*/
static const char *local_zBaseHrefSuffix = 0;
/*
** Set the desired $base_href_suffix to something other than g.zRelReqURI
*/
void style_set_base_href_suffix(const char *zFormat, ...){
fossil_free( (char*)local_zBaseHrefSuffix );
if( zFormat==0 ){
local_zBaseHrefSuffix = 0;
}else{
char *z;
va_list ap;
va_start(ap, zFormat);
z = vmprintf(zFormat, ap);
va_end(ap);
local_zBaseHrefSuffix = escape_quotes( z );
if( local_zBaseHrefSuffix!=z ) fossil_free( z );
}
}
/*
** Create a TH1 variable containing the URL for the stylesheet.
**
** The name of the new variable will be "stylesheet_url".
**
** The value will be a URL for accessing the appropriate stylesheet.
** This URL will include query parameters such as "id=" and "once&skin="
** to cause the correct stylesheet to be loaded after a skin change
** or after a change to the stylesheet.
*/
static void stylesheet_url_var(void){
char *zBuiltin; /* Auxiliary page-specific CSS page */
Blob url; /* The URL */
/* Initialize the URL to its baseline */
url = empty_blob;
blob_appendf(&url, "%R/style.css");
/* If page-specific CSS exists for the current page, then append
** the pathname for the page-specific CSS. The default CSS is
**
** /style.css
**
** But for the "/wikiedit" page (to name but one example), we
** append a path as follows:
**
** /style.css/wikiedit
**
** The /style.css page (implemented below) will detect this extra "wikiedit"
** path information and include the page-specific CSS along with the
** default CSS when it delivers the page.
*/
zBuiltin = mprintf("style.%s.css", g.zPath);
if( builtin_file(zBuiltin,0)!=0 ){
blob_appendf(&url, "/%s", g.zPath);
}
fossil_free(zBuiltin);
/* Add query parameters that will change whenever the skin changes
** or after any updates to the CSS files
*/
blob_appendf(&url, "?id=%x", skin_id("css"));
if( P("once")!=0 && P("skin")!=0 ){
blob_appendf(&url, "&skin=%s&once", skin_in_use());
}
/* Generate the CSS URL variable */
Th_Store("stylesheet_url", blob_str(&url));
blob_reset(&url);
}
/*
** Create a TH1 variable containing the URL for the specified image.
** The resulting variable name will be of the form $[zImageName]_image_url.
** The value will be a URL that includes an id= query parameter that
** changes if the underlying resource changes or if a different skin
** is selected.
*/
static void image_url_var(const char *zImageName){
char *zVarName; /* Name of the new TH1 variable */
char *zResource; /* Name of CONFIG entry holding content */
char *zUrl; /* The URL */
zResource = mprintf("%s-image", zImageName);
zUrl = mprintf("%R/%s?id=%x", zImageName, skin_id(zResource));
free(zResource);
zVarName = mprintf("%s_image_url", zImageName);
Th_Store(zVarName, zUrl);
free(zVarName);
free(zUrl);
}
/*
** Output TEXT with a click-to-copy button next to it. Loads the copybtn.js
** Javascript module, and generates HTML elements with the following IDs:
**
** TARGETID: The <span> wrapper around TEXT.
** copy-TARGETID: The <span> for the copy button.
**
** If the FLIPPED argument is non-zero, the copy button is displayed after TEXT.
**
** The COPYLENGTH argument defines the length of the substring of TEXT copied to
** clipboard:
**
** <= 0: No limit (default if the argument is omitted).
** >= 3: Truncate TEXT after COPYLENGTH (single-byte) characters.
** 1: Use the "hash-digits" setting as the limit.
** 2: Use the length appropriate for URLs as the limit (defined at
** compile-time by FOSSIL_HASH_DIGITS_URL, defaults to 16).
*/
char *style_copy_button(
int bOutputCGI, /* Don't return result, but send to cgi_printf(). */
const char *zTargetId, /* The TARGETID argument. */
int bFlipped, /* The FLIPPED argument. */
int cchLength, /* The COPYLENGTH argument. */
const char *zTextFmt, /* Formatting of the TEXT argument (htmlized). */
... /* Formatting parameters of the TEXT argument. */
){
va_list ap;
char *zText;
char *zResult = 0;
va_start(ap,zTextFmt);
zText = vmprintf(zTextFmt/*works-like:?*/,ap);
va_end(ap);
if( cchLength==1 ) cchLength = hash_digits(0);
else if( cchLength==2 ) cchLength = hash_digits(1);
if( !bFlipped ){
const char *zBtnFmt =
"<span class=\"nobr\">"
"<span "
"class=\"copy-button\" "
"id=\"copy-%h\" "
"data-copytarget=\"%h\" "
"data-copylength=\"%d\">"
"</span>"
"<span id=\"%h\">"
"%s"
"</span>"
"</span>";
if( bOutputCGI ){
cgi_printf(
zBtnFmt/*works-like:"%h%h%d%h%s"*/,
zTargetId,zTargetId,cchLength,zTargetId,zText);
}else{
zResult = mprintf(
zBtnFmt/*works-like:"%h%h%d%h%s"*/,
zTargetId,zTargetId,cchLength,zTargetId,zText);
}
}else{
const char *zBtnFmt =
"<span class=\"nobr\">"
"<span id=\"%h\">"
"%s"
"</span>"
"<span "
"class=\"copy-button copy-button-flipped\" "
"id=\"copy-%h\" "
"data-copytarget=\"%h\" "
"data-copylength=\"%d\">"
"</span>"
"</span>";
if( bOutputCGI ){
cgi_printf(
zBtnFmt/*works-like:"%h%s%h%h%d"*/,
zTargetId,zText,zTargetId,zTargetId,cchLength);
}else{
zResult = mprintf(
zBtnFmt/*works-like:"%h%s%h%h%d"*/,
zTargetId,zText,zTargetId,zTargetId,cchLength);
}
}
free(zText);
builtin_request_js("copybtn.js");
return zResult;
}
/*
** Return a random nonce that is stored in static space. For a particular
** run, the same nonce is always returned.
*/
char *style_nonce(void){
static char zNonce[52];
if( zNonce[0]==0 ){
unsigned char zSeed[24];
sqlite3_randomness(24, zSeed);
encode16(zSeed,(unsigned char*)zNonce,24);
}
return zNonce;
}
/*
** Return the default Content Security Policy (CSP) string.
** If the toHeader argument is true, then also add the
** CSP to the HTTP reply header.
**
** The CSP comes from the "default-csp" setting if it exists and
** is non-empty. If that setting is an empty string, then the following
** default is used instead:
**
** default-src 'self' data:;
** script-src 'self' 'nonce-$nonce';
** style-src 'self' 'unsafe-inline';
** img-src * data:;
**
** The text '$nonce' is replaced by style_nonce() if and whereever it
** occurs in the input string.
**
** The string returned is obtained from fossil_malloc() and
** should be released by the caller.
*/
char *style_csp(int toHeader){
static const char zBackupCSP[] =
"default-src 'self' data:; "
"script-src 'self' 'nonce-$nonce'; "
"style-src 'self' 'unsafe-inline'; "
"img-src * data:";
const char *zFormat;
Blob csp;
char *zNonce;
char *zCsp;
int i;
if( disableCSP ) return fossil_strdup("");
zFormat = db_get("default-csp","");
if( zFormat[0]==0 ){
zFormat = zBackupCSP;
}
blob_init(&csp, 0, 0);
while( zFormat[0] && (zNonce = strstr(zFormat,"$nonce"))!=0 ){
blob_append(&csp, zFormat, (int)(zNonce - zFormat));
blob_append(&csp, style_nonce(), -1);
zFormat = zNonce + 6;
}
blob_append(&csp, zFormat, -1);
zCsp = blob_str(&csp);
/* No whitespace other than actual space characters allowed in the CSP
** string. See https://fossil-scm.org/forum/forumpost/d29e3af43c */
for(i=0; zCsp[i]; i++){ if( fossil_isspace(zCsp[i]) ) zCsp[i] = ' '; }
if( toHeader ){
cgi_printf_header("Content-Security-Policy: %s\r\n", zCsp);
}
return zCsp;
}
/*
** Disable content security policy for the current page.
** WARNING: Do not do this lightly!
**
** This routine must be called before the CSP is sued by
** style_header().
*/
void style_disable_csp(void){
disableCSP = 1;
}
/*
** Default HTML page header text through <body>. If the repository-specific
** header template lacks a <body> tag, then all of the following is
** prepended.
*/
static const char zDfltHeader[] =
@ <html>
@ <head>
@ <meta charset="UTF-8">
@ <base href="$baseurl/$base_href_suffix" />
@ <meta http-equiv="Content-Security-Policy" content="$default_csp" />
@ <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ <title>$<project_name>: $<title></title>
@ <link rel="alternate" type="application/rss+xml" title="RSS Feed" \
@ href="$home/timeline.rss" />
@ <link rel="stylesheet" href="$stylesheet_url" type="text/css" />
@ </head>
@ <body class="$current_feature wpn-$webpagename">
;
/*
** Returns the default page header.
*/
const char *get_default_header(){
return zDfltHeader;
}
/*
** The default TCL list that defines the main menu.
*/
static const char zDfltMainMenu[] =
@ Home /home * {}
@ Timeline /timeline {o r j} {}
@ Files /dir?ci=tip oh desktoponly
@ Branches /brlist o wideonly
@ Tags /taglist o wideonly
@ Forum /forum {@2 3 4 5 6} wideonly
@ Chat /chat C wideonly
@ Tickets /ticket r wideonly
@ Wiki /wiki j wideonly
@ Admin /setup {a s} desktoponly
@ Logout /logout L wideonly
@ Login /login !L wideonly
;
/*
** Return the default menu
*/
const char *style_default_mainmenu(void){
return zDfltMainMenu;
}
/*
** Given a URL path, extract the first element as a "feature" name,
** used as the <body class="FEATURE"> value by default, though
** later-running code may override this, typically to group multiple
** Fossil UI URLs into a single "feature" so you can have per-feature
** CSS rules.
**
** For example, "body.forum div.markdown blockquote" targets only
** block quotes made in forum posts, leaving other Markdown quotes
** alone. Because feature class "forum" groups /forummain, /forumpost,
** and /forume2, it works across all renderings of Markdown to HTML
** within the Fossil forum feature.
*/
static const char* feature_from_page_path(const char *zPath){
const char* zSlash = strchr(zPath, '/');
if (zSlash) {
return fossil_strndup(zPath, zSlash - zPath);
} else {
return zPath;
}
}
/*
** Override the value of the TH1 variable current_feature, its default
** set by feature_from_page_path(). We do not call this from
** style_init_th1_vars() because that uses Th_MaybeStore() instead to
** allow webpage implementations to call this before style_header()
** to override that "maybe" default with something better.
*/
void style_set_current_feature(const char* zFeature){
Th_Store("current_feature", zFeature);
}
/*
** Returns the current mainmenu value from either the --mainmenu flag
** (handled by the server/ui/cgi commands), the "mainmenu" config
** setting, or style_default_mainmenu(), in that order, returning the
** first of those which is defined.
*/
const char*style_get_mainmenu(){
static const char *zMenu = 0;
if(!zMenu){
if(g.zMainMenuFile){
Blob b = empty_blob;
blob_read_from_file(&b, g.zMainMenuFile, ExtFILE);
zMenu = blob_str(&b);
}else{
zMenu = db_get("mainmenu", style_default_mainmenu());
}
}
return zMenu;
}
/*
** Initialize all the default TH1 variables
*/
static void style_init_th1_vars(const char *zTitle){
const char *zNonce = style_nonce();
char *zDfltCsp;
zDfltCsp = style_csp(1);
/*
** Do not overwrite the TH1 variable "default_csp" if it exists, as this
** allows it to be properly overridden via the TH1 setup script (i.e. it
** is evaluated before the header is rendered).
*/
Th_MaybeStore("default_csp", zDfltCsp);
fossil_free(zDfltCsp);
Th_Store("nonce", zNonce);
Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
Th_Store("project_description", db_get("project-description",""));
if( zTitle ) Th_Store("title", zTitle);
Th_Store("baseurl", g.zBaseURL);
Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
Th_Store("home", g.zTop);
Th_Store("index_page", db_get("index-page","/home"));
if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
Th_Store("current_page", local_zCurrentPage);
if( !local_zBaseHrefSuffix ) style_set_base_href_suffix("%s",g.zRelReqURI);
Th_Store("base_href_suffix", local_zBaseHrefSuffix);
Th_Store("webpagename", escape_quotes(g.zPath));
Th_Store("csrf_token", g.zCsrfToken);
Th_Store("release_version", RELEASE_VERSION);
Th_Store("manifest_version", MANIFEST_VERSION);
Th_Store("manifest_date", MANIFEST_DATE);
Th_Store("compiler_name", COMPILER_NAME);
Th_Store("mainmenu", style_get_mainmenu());
stylesheet_url_var();
image_url_var("logo");
image_url_var("background");
if( !login_is_nobody() ){
Th_Store("login", g.zLogin);
}
Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
}
/*
** Draw the header.
*/
void style_header(const char *zTitleFormat, ...){
va_list ap;
char *zTitle;
const char *zHeader = skin_get("header");
login_check_credentials();
va_start(ap, zTitleFormat);
zTitle = vmprintf(zTitleFormat, ap);
va_end(ap);
cgi_destination(CGI_HEADER);
@ <!DOCTYPE html>
if( g.thTrace ) Th_Trace("BEGIN_HEADER<br />\n", -1);
/* Generate the header up through the main menu */
style_init_th1_vars(zTitle);
if( sqlite3_strlike("%<body%", zHeader, 0)!=0 ){
Th_Render(zDfltHeader);
}
if( g.thTrace ) Th_Trace("BEGIN_HEADER_SCRIPT<br />\n", -1);
Th_Render(zHeader);
if( g.thTrace ) Th_Trace("END_HEADER<br />\n", -1);
Th_Unstore("title"); /* Avoid collisions with ticket field names */
cgi_destination(CGI_BODY);
g.cgiOutput = 1;
headerHasBeenGenerated = 1;
sideboxUsed = 0;
if( g.perm.Debug && P("showqp") ){
@ <div class="debug">
cgi_print_all(0, 0);
@ </div>
}
}
#if INTERFACE
/* Allowed parameters for style_adunit() */
#define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */
#define ADUNIT_RIGHT_OK 0x0002 /* Right-side vertical ads ok here */
#endif
/*
** Various page implementations can invoke this interface to let the
** style manager know what kinds of ads are appropriate for this page.
*/
void style_adunit_config(unsigned int mFlags){
adUnitFlags = mFlags;
}
/*
** Return the text of an ad-unit, if one should be rendered. Return
** NULL if no ad-unit is desired.
**
** The *pAdFlag value might be set to ADUNIT_RIGHT_OK if this is
** a right-hand vertical ad.
*/
static const char *style_adunit_text(unsigned int *pAdFlag){
const char *zAd = 0;
*pAdFlag = 0;
if( adUnitFlags & ADUNIT_OFF ) return 0; /* Disallow ads on this page */
if( db_get_boolean("adunit-disable",0) ) return 0;
if( g.perm.Admin && db_get_boolean("adunit-omit-if-admin",0) ){
return 0;
}
if( !login_is_nobody()
&& fossil_strcmp(g.zLogin,"anonymous")!=0
&& db_get_boolean("adunit-omit-if-user",0)
){
return 0;
}
if( (adUnitFlags & ADUNIT_RIGHT_OK)!=0
&& !fossil_all_whitespace(zAd = db_get("adunit-right", 0))
&& !cgi_body_contains("<table")
){
*pAdFlag = ADUNIT_RIGHT_OK;
return zAd;
}else if( !fossil_all_whitespace(zAd = db_get("adunit",0)) ){
return zAd;
}
return 0;
}
/*
** Indicate that the table-sorting javascript is needed.
*/
void style_table_sorter(void){
builtin_request_js("sorttable.js");
}
/*
** Generate code to load all required javascript files.
*/
static void style_load_all_js_files(void){
if( needHrefJs && g.perm.Hyperlink ){
int nDelay = db_get_int("auto-hyperlink-delay",0);
int bMouseover = db_get_boolean("auto-hyperlink-mouseover",0)
&& sqlite3_strglob("*Android*",PD("HTTP_USER_AGENT",""));
@ <script id='href-data' type='application/json'>\
@ {"delay":%d(nDelay),"mouseover":%d(bMouseover)}</script>
}
@ <script nonce="%h(style_nonce())">/* style.c:%d(__LINE__) */
@ function debugMsg(msg){
@ var n = document.getElementById("debugMsg");
@ if(n){n.textContent=msg;}
@ }
if( needHrefJs && g.perm.Hyperlink ){
@ /* href.js */
cgi_append_content(builtin_text("href.js"),-1);
}
if( blob_size(&blobOnLoad)>0 ){
@ window.onload = function(){
cgi_append_content(blob_buffer(&blobOnLoad), blob_size(&blobOnLoad));
cgi_append_content("\n}\n", -1);
}
@ </script>
builtin_fulfill_js_requests();
}
/*
** Invoke this routine after all of the content for a webpage has been
** generated. This routine should be called once for every webpage, at
** or near the end of page generation. This routine does the following:
**
** * Populates the header of the page, including setting up appropriate
** submenu elements. The header generation is deferred until this point
** so that we know that all style_submenu_element() and similar have
** been received.
**
** * Finalizes the page content.
**
** * Appends the footer.
*/
void style_finish_page(){
const char *zFooter;
const char *zAd = 0;
unsigned int mAdFlags = 0;
if( !headerHasBeenGenerated ) return;
/* Go back and put the submenu at the top of the page. We delay the
** creation of the submenu until the end so that we can add elements
** to the submenu while generating page text.
*/
cgi_destination(CGI_HEADER);
if( submenuEnable && nSubmenu+nSubmenuCtrl>0 ){
int i;
if( nSubmenuCtrl ){
@ <form id='f01' method='GET' action='%R/%s(g.zPath)'>
@ <input type='hidden' name='udc' value='1'>
cgi_tag_query_parameter("udc");
}
@ <div class="submenu">
if( nSubmenu>0 ){
qsort(aSubmenu, nSubmenu, sizeof(aSubmenu[0]), submenuCompare);
for(i=0; i<nSubmenu; i++){
struct Submenu *p = &aSubmenu[i];
/* switching away from the %h formatting below might be dangerous
** because some places use %s to compose zLabel and zLink;
** e.g. /rptview page
*/
if( p->zLink==0 ){
@ <span class="label">%h(p->zLabel)</span>
}else{
@ <a class="label" href="%h(p->zLink)">%h(p->zLabel)</a>
}
}
}
for(i=0; i<nSubmenuCtrl; i++){
const char *zQPN = aSubmenuCtrl[i].zName;
const char *zDisabled = "";
const char *zXtraClass = "";
if( aSubmenuCtrl[i].eVisible & STYLE_DISABLED ){
zDisabled = " disabled";
}else if( zQPN ){
cgi_tag_query_parameter(zQPN);
}
switch( aSubmenuCtrl[i].eType ){
case FF_ENTRY:
@ <span class='submenuctrl%s(zXtraClass)'>\
@ %h(aSubmenuCtrl[i].zLabel)\
@ <input type='text' name='%s(zQPN)' value='%h(PD(zQPN, ""))' \
if( aSubmenuCtrl[i].iSize<0 ){
@ size='%d(-aSubmenuCtrl[i].iSize)' \
}else if( aSubmenuCtrl[i].iSize>0 ){
@ size='%d(aSubmenuCtrl[i].iSize)' \
@ maxlength='%d(aSubmenuCtrl[i].iSize)' \
}
@ id='submenuctrl-%d(i)'%s(zDisabled)></span>
break;
case FF_MULTI: {
int j;
const char *zVal = P(zQPN);
if( zXtraClass[0] ){
@ <span class='%s(zXtraClass+1)'>
}
if( aSubmenuCtrl[i].zLabel ){
@ %h(aSubmenuCtrl[i].zLabel)\
}
@ <select class='submenuctrl' size='1' name='%s(zQPN)' \
@ id='submenuctrl-%d(i)'%s(zDisabled)>
for(j=0; j<aSubmenuCtrl[i].iSize*2; j+=2){
const char *zQPV = aSubmenuCtrl[i].azChoice[j];
@ <option value='%h(zQPV)'\
if( fossil_strcmp(zVal, zQPV)==0 ){
@ selected\
}
@ >%h(aSubmenuCtrl[i].azChoice[j+1])</option>
}
@ </select>
if( zXtraClass[0] ){
@ </span>
}
break;
}
case FF_BINARY: {
int isTrue = PB(zQPN);
@ <select class='submenuctrl%s(zXtraClass)' size='1' \
@ name='%s(zQPN)' id='submenuctrl-%d(i)'%s(zDisabled)>
@ <option value='1'\
if( isTrue ){
@ selected\
}
@ >%h(aSubmenuCtrl[i].zLabel)</option>
@ <option value='0'\
if( !isTrue ){
@ selected\
}
@ >%h(aSubmenuCtrl[i].zFalse)</option>
@ </select>
break;
}
case FF_CHECKBOX: {
@ <label class='submenuctrl submenuckbox%s(zXtraClass)'>\
@ <input type='checkbox' name='%s(zQPN)' id='submenuctrl-%d(i)' \
if( PB(zQPN) ){
@ checked \
}
if( aSubmenuCtrl[i].zJS ){
@ data-ctrl='%s(aSubmenuCtrl[i].zJS)'%s(zDisabled)>\
}else{
@ %s(zDisabled)>\
}
@ %h(aSubmenuCtrl[i].zLabel)</label>
break;
}
}
}
@ </div>
if( nSubmenuCtrl ){
cgi_query_parameters_to_hidden();
cgi_tag_query_parameter(0);
@ </form>
builtin_request_js("menu.js");
}
}
zAd = style_adunit_text(&mAdFlags);
if( (mAdFlags & ADUNIT_RIGHT_OK)!=0 ){
@ <div class="content adunit_right_container">
@ <div class="adunit_right">
cgi_append_content(zAd, -1);
@ </div>
}else if( zAd ){
@ <div class="adunit_banner">
cgi_append_content(zAd, -1);
@ </div>
}
@ <div class="content"><span id="debugMsg"></span>
cgi_destination(CGI_BODY);
if( sideboxUsed ){
@ <div class="endContent"></div>
}
@ </div>
/* Put the footer at the bottom of the page. */
zFooter = skin_get("footer");
if( sqlite3_strlike("%</body>%", zFooter, 0)==0 ){
style_load_all_js_files();
}
if( g.thTrace ) Th_Trace("BEGIN_FOOTER<br />\n", -1);
Th_Render(zFooter);
if( g.thTrace ) Th_Trace("END_FOOTER<br />\n", -1);
/* Render trace log if TH1 tracing is enabled. */
if( g.thTrace ){
cgi_append_content("<span class=\"thTrace\"><hr />\n", -1);
cgi_append_content(blob_str(&g.thLog), blob_size(&g.thLog));
cgi_append_content("</span>\n", -1);
}
/* Add document end mark if it was not in the footer */
if( sqlite3_strlike("%</body>%", zFooter, 0)!=0 ){
style_load_all_js_files();
@ </body>
@ </html>
}
/* Update the user display prefs cookie if it was modified during
** this request.
*/
cookie_render();
}
/*
** Begin a side-box on the right-hand side of a page. The title and
** the width of the box are given as arguments. The width is usually
** a percentage of total screen width.
*/
void style_sidebox_begin(const char *zTitle, const char *zWidth){
sideboxUsed = 1;
@ <div class="sidebox" style="width:%s(zWidth)">
@ <div class="sideboxTitle">%h(zTitle)</div>
}
/* End the side-box
*/
void style_sidebox_end(void){
@ </div>
}
/*
** Search string zCss for zSelector.
**
** Return true if found. Return false if not found
*/
static int containsSelector(const char *zCss, const char *zSelector){
const char *z;
int n;
int selectorLen = (int)strlen(zSelector);
for(z=zCss; *z; z+=selectorLen){
z = strstr(z, zSelector);
if( z==0 ) return 0;
if( z!=zCss ){
for( n=-1; z+n!=zCss && fossil_isspace(z[n]); n--);
if( z+n!=zCss && z[n]!=',' && z[n]!= '}' && z[n]!='/' ) continue;
}
for( n=selectorLen; z[n] && fossil_isspace(z[n]); n++ );
if( z[n]==',' || z[n]=='{' || z[n]=='/' ) return 1;
}
return 0;
}
/*
** COMMAND: test-contains-selector
**
** Usage: %fossil test-contains-selector FILENAME SELECTOR
**
** Determine if the CSS stylesheet FILENAME contains SELECTOR.
**
** Note that as of 2020-05-28, the default rules are always emitted,
** so the containsSelector() logic is no longer applied when emitting
** style.css. It is unclear whether this test command is now obsolete
** or whether it may still serve a purpose.
*/
void contains_selector_cmd(void){
int found;
char *zSelector;
Blob css;
if( g.argc!=4 ) usage("FILENAME SELECTOR");
blob_read_from_file(&css, g.argv[2], ExtFILE);
zSelector = g.argv[3];
found = containsSelector(blob_str(&css), zSelector);
fossil_print("%s %s\n", zSelector, found ? "found" : "not found");
blob_reset(&css);
}
/*
** WEBPAGE: script.js
**
** Return the "Javascript" content for the current skin (if there is any)
*/
void page_script_js(void){
const char *zScript = skin_get("js");
if( P("test") ){
/* Render the script as plain-text for testing purposes, if the "test"
** query parameter is present */
cgi_set_content_type("text/plain");
}else{
/* Default behavior is to return javascript */
cgi_set_content_type("application/javascript");
}
style_init_th1_vars(0);
Th_Render(zScript?zScript:"");
}
/*
** Check for "name" or "page" query parameters on an /style.css
** page request. If present, then page-specific CSS is requested,
** so add that CSS to pOut. If the "name" and "page" query parameters
** are omitted, then pOut is unchnaged.
*/
static void page_style_css_append_page_style(Blob *pOut){
const char *zPage = PD("name",P("page"));
char * zFile;
int nFile = 0;
const char *zBuiltin;
if(zPage==0 || zPage[0]==0){
return;
}
zFile = mprintf("style.%s.css", zPage);
zBuiltin = (const char *)builtin_file(zFile, &nFile);
if(nFile>0){
blob_appendf(pOut,
"\n/***********************************************************\n"
"** Page-specific CSS for \"%s\"\n"
"***********************************************************/\n",
zPage);
blob_append(pOut, zBuiltin, nFile);
fossil_free(zFile);
return;
}
/* Potential TODO: check for aliases/page groups. e.g. group all
** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As
** of this writing, doing so would only shave a few kb from
** default.css. */
fossil_free(zFile);
}
/*
** WEBPAGE: style.css
**
** Return the style sheet. The style sheet is assemblied from
** multiple sources, in order:
**
** (1) The built-in "default.css" style sheet containing basic defaults.
**
** (2) The page-specific style sheet taken from the built-in
** called "PAGENAME.css" where PAGENAME is the value of the name=
** or page= query parameters. If neither name= nor page= exist,
** then this section is a no-op.
**
** (3) The skin-specific "css.txt" file, if there one.
**
** All of (1), (2), and (3) above (or as many as exist) are concatenated.
** The result is then run through TH1 with the following variables set:
**
** * $basename
** * $secureurl
** * $home
** * $logo
** * $background
**
** The output from TH1 becomes the style sheet. Fossil always reports
** that the style sheet is cacheable.
*/
void page_style_css(void){
Blob css = empty_blob;
int i;
const char * zDefaults;
const char *zSkin;
cgi_set_content_type("text/css");
etag_check(0, 0);
/* Emit all default rules... */
zDefaults = (const char*)builtin_file("default.css", &i);
blob_append(&css, zDefaults, i);
/* Page-specific CSS, if any... */
page_style_css_append_page_style(&css);
zSkin = skin_in_use();
if( zSkin==0 ) zSkin = "this repository";
blob_appendf(&css,
"\n/***********************************************************\n"
"** Skin-specific CSS for %s\n"
"***********************************************************/\n",
zSkin);
blob_append(&css,skin_get("css"),-1);
/* Process through TH1 in order to give an opportunity to substitute
** variables such as $baseurl.
*/
Th_Store("baseurl", g.zBaseURL);
Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
Th_Store("home", g.zTop);
image_url_var("logo");
image_url_var("background");
Th_Render(blob_str(&css));
/* Tell CGI that the content returned by this page is considered cacheable */
g.isConst = 1;
}
/*
** All possible capabilities
*/
static const char allCap[] =
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL";
/*
** Compute the current login capabilities
*/
static char *find_capabilities(char *zCap){
int i, j;
char c;
for(i=j=0; (c = allCap[j])!=0; j++){
if( login_has_capability(&c, 1, 0) ) zCap[i++] = c;
}
zCap[i] = 0;
return zCap;
}
/*
** Compute the current login capabilities that were
** contributed by Anonymous
*/
static char *find_anon_capabilities(char *zCap){
int i, j;
char c;
for(i=j=0; (c = allCap[j])!=0; j++){
if( login_has_capability(&c, 1, LOGIN_ANON)
&& !login_has_capability(&c, 1, 0) ) zCap[i++] = c;
}
zCap[i] = 0;
return zCap;
}
/*
** WEBPAGE: test_env
**
** Display CGI-variables and other aspects of the run-time
** environment, for debugging and trouble-shooting purposes.
*/
void page_test_env(void){
webpage_error("");
}
/*
** WEBPAGE: honeypot
** This page is a honeypot for spiders and bots.
*/
void honeypot_page(void){
style_header("I think you are a robot");
@ <p>You seem like a robot.</p>
@
@ <p>Is this wrong? Are you really a human? If so, please prove it
@ by <a href="%R/login">logging in</a>.
if( g.anon.Hyperlink ){
@ You can <a href="%R/login?anon=1">log in anonymously</a> if you
@ prefer.
}
@ <p>Sorry for the inconvenience. The point of this is to prevent
@ robots from following the countless of hyperlinks in this site and
@ soaking up all the available CPU time and network bandwidth.
style_finish_page();
}
/*
** Webpages that encounter an error due to missing or incorrect
** query parameters can jump to this routine to render an error
** message screen.
**
** For administators, or if the test_env_enable setting is true, then
** details of the request environment are displayed. Otherwise, just
** the error message is shown.
**
** If zFormat is an empty string, then this is the /test_env page.
*/
void webpage_error(const char *zFormat, ...){
int showAll;
char *zErr = 0;
int isAuth = 0;
char zCap[100];
login_check_credentials();
if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
isAuth = 1;
}
cgi_load_environment();
style_set_current_feature(zFormat[0]==0 ? "test" : "error");
if( zFormat[0] ){
va_list ap;
va_start(ap, zFormat);
zErr = vmprintf(zFormat, ap);
va_end(ap);
style_header("Bad Request");
@ <h1>/%h(g.zPath): %h(zErr)</h1>
showAll = 0;
cgi_set_status(500, "Bad Request");
}else if( !isAuth ){
login_needed(0);
return;
}else{
style_header("Environment Test");
showAll = PB("showall");
style_submenu_checkbox("showall", "Cookies", 0, 0);
style_submenu_element("Stats", "%R/stat");
}
if( isAuth ){
#if !defined(_WIN32)
@ uid=%d(getuid()), gid=%d(getgid())<br />
#endif
@ g.zBaseURL = %h(g.zBaseURL)<br />
@ g.zHttpsURL = %h(g.zHttpsURL)<br />
@ g.zRelReqURI = %h(g.zRelReqURI)<br />
@ g.zTop = %h(g.zTop)<br />
@ g.zPath = %h(g.zPath)<br />
@ g.userUid = %d(g.userUid)<br />
@ g.zLogin = %h(g.zLogin)<br />
@ g.isHuman = %d(g.isHuman)<br />
@ g.javascriptHyperlink = %d(g.javascriptHyperlink)<br />
if( g.nRequest ){
@ g.nRequest = %d(g.nRequest)<br />
}
if( g.nPendingRequest>1 ){
@ g.nPendingRequest = %d(g.nPendingRequest)<br />
}
@ capabilities = %s(find_capabilities(zCap))<br />
if( zCap[0] ){
@ anonymous-adds = %s(find_anon_capabilities(zCap))<br />
}
@ g.zRepositoryName = %h(g.zRepositoryName)<br />
@ load_average() = %f(load_average())<br />
#ifndef _WIN32
@ RSS = %.2f(fossil_rss()/1000000.0) MB</br />
#endif
@ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br />
@ fossil_exe_id() = %h(fossil_exe_id())<br />
@ <hr />
P("HTTP_USER_AGENT");
P("SERVER_SOFTWARE");
cgi_print_all(showAll, 0);
if( showAll && blob_size(&g.httpHeader)>0 ){
@ <hr />
@ <pre>
@ %h(blob_str(&g.httpHeader))
@ </pre>
}
}
if( zErr && zErr[0] ){
style_finish_page();
cgi_reply();
fossil_exit(1);
}else{
style_finish_page();
}
}
/*
** Generate a Not Yet Implemented error page.
*/
void webpage_not_yet_implemented(void){
webpage_error("Not yet implemented");
}
/*
** Generate a webpage for a webpage_assert().
*/
void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){
fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr);
cgi_reset_content();
webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr);
}
/*
** Issue a 404 Not Found error for a webpage
*/
void webpage_notfound_error(const char *zFormat, ...){
char *zMsg;
va_list ap;
if( zFormat ){
va_start(ap, zFormat);
zMsg = vmprintf(zFormat, ap);
va_end(ap);
}else{
zMsg = "Not Found";
}
style_set_current_feature("enotfound");
style_header("Not Found");
@ <p>%h(zMsg)</p>
cgi_set_status(404, "Not Found");
style_finish_page();
}
#if INTERFACE
# define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
#endif
/*
** Returns a pseudo-random input field ID, for use in associating an
** ID-less input field with a label. The memory is owned by the
** caller.
*/
static char * style_next_input_id(){
static int inputID = 0;
++inputID;
return mprintf("input-id-%d", inputID);
}
/*
** Outputs a labeled checkbox element. zWrapperId is an optional ID
** value for the containing element (see below). zFieldName is the
** form element name. zLabel is the label for the checkbox. zValue is
** the optional value for the checkbox. zTip is an optional tooltip,
** which gets set as the "title" attribute of the outermost
** element. If isChecked is true, the checkbox gets the "checked"
** attribute set, else it is not.
**
** Resulting structure:
**
** <div class='input-with-label' title={{zTip}} id={{zWrapperId}}>
** <input type='checkbox' name={{zFieldName}} value={{zValue}}
** id='A RANDOM VALUE'
** {{isChecked ? " checked : ""}}/>
** <label for='ID OF THE INPUT FIELD'>{{zLabel}}</label>
** </div>
**
** zLabel, and zValue are required. zFieldName, zWrapperId, and zTip
** are may be NULL or empty.
**
** Be sure that the input-with-label CSS class is defined sensibly, in
** particular, having its display:inline-block is useful for alignment
** purposes.
*/
void style_labeled_checkbox(const char * zWrapperId,
const char *zFieldName, const char * zLabel,
const char * zValue, int isChecked,
const char * zTip){
char * zLabelID = style_next_input_id();
CX("<div class='input-with-label'");
if(zTip && *zTip){
CX(" title='%h'", zTip);
}
if(zWrapperId && *zWrapperId){
CX(" id='%s'",zWrapperId);
}
CX("><input type='checkbox' id='%s' ", zLabelID);
if(zFieldName && *zFieldName){
CX("name='%s' ",zFieldName);
}
CX("value='%T'%s/>",
zValue ? zValue : "", isChecked ? " checked" : "");
CX("<label for='%s'>%h</label></div>", zLabelID, zLabel);
fossil_free(zLabelID);
}
/*
** Outputs a SELECT list from a compile-time list of integers.
** The vargs must be a list of (const char *, int) pairs, terminated
** with a single NULL. Each pair is interpreted as...
**
** If the (const char *) is NULL, it is the end of the list, else
** a new OPTION entry is created. If the string is empty, the
** label and value of the OPTION is the integer part of the pair.
** If the string is not empty, it becomes the label and the integer
** the value. If that value == selectedValue then that OPTION
** element gets the 'selected' attribute.
**
** Note that the pairs are not in (int, const char *) order because
** there is no well-known integer value which we can definitively use
** as a list terminator.
**
** zWrapperId is an optional ID value for the containing element (see
** below).
**
** zFieldName is the value of the form element's name attribute. Note
** that fossil prefers underscores over '-' for separators in form
** element names.
**
** zLabel is an optional string to use as a "label" for the element
** (see below).
**
** zTooltip is an optional value for the SELECT's title attribute.
**
** The structure of the emitted HTML is:
**
** <div class='input-with-label' title={{zToolTip}} id={{zWrapperId}}>
** <label for='SELECT ELEMENT ID'>{{zLabel}}</label>
** <select id='RANDOM ID' name={{zFieldName}}>...</select>
** </div>
**
** Example:
**
** style_select_list_int("my-grapes", "my_grapes", "Grapes",
** "Select the number of grapes",
** atoi(PD("my_field","0")),
** "", 1, "2", 2, "Three", 3,
** NULL);
**
*/
void style_select_list_int(const char * zWrapperId,
const char *zFieldName, const char * zLabel,
const char * zToolTip, int selectedVal,
... ){
char * zLabelID = style_next_input_id();
va_list vargs;
va_start(vargs,selectedVal);
CX("<div class='input-with-label'");
if(zToolTip && *zToolTip){
CX(" title='%h'",zToolTip);
}
if(zWrapperId && *zWrapperId){
CX(" id='%s'",zWrapperId);
}
CX(">");
if(zLabel && *zLabel){
CX("<label for='%s'>%h</label>", zLabelID, zLabel);
}
CX("<select name='%s' id='%s'>",zFieldName, zLabelID);
while(1){
const char * zOption = va_arg(vargs,char *);
int v;
if(NULL==zOption){
break;
}
v = va_arg(vargs,int);
CX("<option value='%d'%s>",
v, v==selectedVal ? " selected" : "");
if(*zOption){
CX("%s", zOption);
}else{
CX("%d",v);
}
CX("</option>\n");
}
CX("</select>\n");
CX("</div>\n");
va_end(vargs);
fossil_free(zLabelID);
}
/*
** The C-string counterpart of style_select_list_int(), this variant
** differs only in that its variadic arguments are C-strings in pairs
** of (optionLabel, optionValue). If a given optionLabel is an empty
** string, the corresponding optionValue is used as its label. If any
** given value matches zSelectedVal, that option gets preselected. If
** no options match zSelectedVal then the first entry is selected by
** default.
**
** Any of (zWrapperId, zTooltip, zSelectedVal) may be NULL or empty.
**
** Example:
**
** style_select_list_str("my-grapes", "my_grapes", "Grapes",
** "Select the number of grapes",
** P("my_field"),
** "1", "One", "2", "Two", "", "3",
** NULL);
*/
void style_select_list_str(const char * zWrapperId,
const char *zFieldName, const char * zLabel,
const char * zToolTip, char const * zSelectedVal,
... ){
char * zLabelID = style_next_input_id();
va_list vargs;
va_start(vargs,zSelectedVal);
if(!zSelectedVal){
zSelectedVal = __FILE__/*some string we'll never match*/;
}
CX("<div class='input-with-label'");
if(zToolTip && *zToolTip){
CX(" title='%h'",zToolTip);
}
if(zWrapperId && *zWrapperId){
CX(" id='%s'",zWrapperId);
}
CX(">");
if(zLabel && *zLabel){
CX("<label for='%s'>%h</label>", zLabelID, zLabel);
}
CX("<select name='%s' id='%s'>",zFieldName, zLabelID);
while(1){
const char * zLabel = va_arg(vargs,char *);
const char * zVal;
if(NULL==zLabel){
break;
}
zVal = va_arg(vargs,char *);
CX("<option value='%T'%s>",
zVal, 0==fossil_strcmp(zVal, zSelectedVal) ? " selected" : "");
if(*zLabel){
CX("%s", zLabel);
}else{
CX("%h",zVal);
}
CX("</option>\n");
}
CX("</select>\n");
CX("</div>\n");
va_end(vargs);
fossil_free(zLabelID);
}
/*
** Generate a <script> with an appropriate nonce.
**
** zOrigin and iLine are the source code filename and line number
** that generated this request.
*/
void style_script_begin(const char *zOrigin, int iLine){
const char *z;
for(z=zOrigin; z[0]!=0; z++){
if( z[0]=='/' || z[0]=='\\' ){
zOrigin = z+1;
}
}
CX("<script nonce='%s'>/* %s:%d */\n", style_nonce(), zOrigin, iLine);
}
/* Generate the closing </script> tag
*/
void style_script_end(void){
CX("</script>\n");
}
/*
** Emits a NOSCRIPT tag with an error message stating that JS is
** required for the current page. This "should" be called near the top
** of pages which *require* JS. The inner DIV has the CSS class
** 'error' and can be styled via a (noscript > .error) CSS selector.
*/
void style_emit_noscript_for_js_page(void){
CX("<noscript><div class='error'>"
"This page requires JavaScript (ES2015, a.k.a. ES6, or newer)."
"</div></noscript>");
}