Fossil

Artifact Content
Login

Artifact 65a555d0d4fb846be9b943d0dc36142281bc5722d114f086274bba3726026466:


     1  /*
     2  ** Copyright (c) 2006,2007 D. Richard Hipp
     3  **
     4  ** This program is free software; you can redistribute it and/or
     5  ** modify it under the terms of the Simplified BSD License (also
     6  ** known as the "2-Clause License" or "FreeBSD License".)
     7  
     8  ** This program is distributed in the hope that it will be useful,
     9  ** but without any warranty; without even the implied warranty of
    10  ** merchantability or fitness for a particular purpose.
    11  **
    12  ** Author contact information:
    13  **   drh@hwaci.com
    14  **   http://www.hwaci.com/drh/
    15  **
    16  *******************************************************************************
    17  **
    18  ** This file contains code to implement the basic web page look and feel.
    19  **
    20  */
    21  #include "VERSION.h"
    22  #include "config.h"
    23  #include "style.h"
    24  
    25  /*
    26  ** Elements of the submenu are collected into the following
    27  ** structure and displayed below the main menu.
    28  **
    29  ** Populate these structure with calls to
    30  **
    31  **      style_submenu_element()
    32  **      style_submenu_entry()
    33  **      style_submenu_checkbox()
    34  **      style_submenu_binary()
    35  **      style_submenu_multichoice()
    36  **      style_submenu_sql()
    37  **
    38  ** prior to calling style_footer().  The style_footer() routine
    39  ** will generate the appropriate HTML text just below the main
    40  ** menu.
    41  */
    42  static struct Submenu {
    43    const char *zLabel;        /* Button label */
    44    const char *zLink;         /* Jump to this link when button is pressed */
    45  } aSubmenu[30];
    46  static int nSubmenu = 0;     /* Number of buttons */
    47  static struct SubmenuCtrl {
    48    const char *zName;           /* Form query parameter */
    49    const char *zLabel;          /* Label.  Might be NULL for FF_MULTI */
    50    unsigned char eType;         /* FF_ENTRY, FF_MULTI, FF_CHECKBOX */
    51    unsigned char eVisible;      /* STYLE_NORMAL or STYLE_DISABLED */
    52    short int iSize;             /* Width for FF_ENTRY.  Count for FF_MULTI */
    53    const char *const *azChoice; /* value/display pairs for FF_MULTI */
    54    const char *zFalse;          /* FF_BINARY label when false */
    55    const char *zJS;             /* Javascript to run on toggle */
    56  } aSubmenuCtrl[20];
    57  static int nSubmenuCtrl = 0;
    58  #define FF_ENTRY    1          /* Text entry box */
    59  #define FF_MULTI    2          /* Combobox.  Multiple choices. */
    60  #define FF_BINARY   3          /* Control for binary query parameter */
    61  #define FF_CHECKBOX 4          /* Check-box */
    62  
    63  #if INTERFACE
    64  #define STYLE_NORMAL   0       /* Normal display of control */
    65  #define STYLE_DISABLED 1       /* Control is disabled */
    66  #endif /* INTERFACE */
    67  
    68  /*
    69  ** Remember that the header has been generated.  The footer is omitted
    70  ** if an error occurs before the header.
    71  */
    72  static int headerHasBeenGenerated = 0;
    73  
    74  /*
    75  ** remember, if a sidebox was used
    76  */
    77  static int sideboxUsed = 0;
    78  
    79  /*
    80  ** Ad-unit styles.
    81  */
    82  static unsigned adUnitFlags = 0;
    83  
    84  /*
    85  ** Submenu disable flag
    86  */
    87  static int submenuEnable = 1;
    88  
    89  /*
    90  ** Flags for various javascript files needed prior to </body>
    91  */
    92  static int needHrefJs = 0;      /* href.js */
    93  static int needSortJs = 0;      /* sorttable.js */
    94  static int needGraphJs = 0;     /* graph.js */
    95  static int needCopyBtnJs = 0;   /* copybtn.js */
    96  static int needAccordionJs = 0; /* accordion.js */
    97  
    98  /*
    99  ** Extra JS added to the end of the file.
   100  */
   101  static Blob blobOnLoad = BLOB_INITIALIZER;
   102  
   103  /*
   104  ** Generate and return a anchor tag like this:
   105  **
   106  **        <a href="URL">
   107  **  or    <a id="ID">
   108  **
   109  ** The form of the anchor tag is determined by the g.javascriptHyperlink
   110  ** variable.  The href="URL" form is used if g.javascriptHyperlink is false.
   111  ** If g.javascriptHyperlink is true then the
   112  ** id="ID" form is used and javascript is generated in the footer to cause
   113  ** href values to be inserted after the page has loaded.  If
   114  ** g.perm.History is false, then the <a id="ID"> form is still
   115  ** generated but the javascript is not generated so the links never
   116  ** activate.
   117  **
   118  ** If the user lacks the Hyperlink (h) property and the "auto-hyperlink"
   119  ** setting is true, then g.perm.Hyperlink is changed from 0 to 1 and
   120  ** g.javascriptHyperlink is set to 1.  The g.javascriptHyperlink defaults
   121  ** to 0 and only changes to one if the user lacks the Hyperlink (h) property
   122  ** and the "auto-hyperlink" setting is enabled.
   123  **
   124  ** Filling in the href="URL" using javascript is a defense against bots.
   125  **
   126  ** The name of this routine is deliberately kept short so that can be
   127  ** easily used within @-lines.  Example:
   128  **
   129  **      @ %z(href("%R/artifact/%s",zUuid))%h(zFN)</a>
   130  **
   131  ** Note %z format.  The string returned by this function is always
   132  ** obtained from fossil_malloc() so rendering it with %z will reclaim
   133  ** that memory space.
   134  **
   135  ** There are three versions of this routine:
   136  **
   137  **    (1)   href() does a plain hyperlink
   138  **    (2)   xhref() adds extra attribute text
   139  **    (3)   chref() adds a class name
   140  **
   141  ** g.perm.Hyperlink is true if the user has the Hyperlink (h) property.
   142  ** Most logged in users should have this property, since we can assume
   143  ** that a logged in user is not a bot.  Only "nobody" lacks g.perm.Hyperlink,
   144  ** typically.
   145  */
   146  char *xhref(const char *zExtra, const char *zFormat, ...){
   147    char *zUrl;
   148    va_list ap;
   149    va_start(ap, zFormat);
   150    zUrl = vmprintf(zFormat, ap);
   151    va_end(ap);
   152    if( g.perm.Hyperlink && !g.javascriptHyperlink ){
   153      char *zHUrl;
   154      if( zExtra ){
   155        zHUrl = mprintf("<a %s href=\"%h\">", zExtra, zUrl);
   156      }else{
   157        zHUrl = mprintf("<a href=\"%h\">", zUrl);
   158      }
   159      fossil_free(zUrl);
   160      return zHUrl;
   161    }
   162    needHrefJs = 1;
   163    if( zExtra==0 ){
   164      return mprintf("<a data-href='%z' href='%R/honeypot'>", zUrl);
   165    }else{
   166      return mprintf("<a %s data-href='%z' href='%R/honeypot'>",
   167                     zExtra, zUrl);
   168    }
   169  }
   170  char *chref(const char *zExtra, const char *zFormat, ...){
   171    char *zUrl;
   172    va_list ap;
   173    va_start(ap, zFormat);
   174    zUrl = vmprintf(zFormat, ap);
   175    va_end(ap);
   176    if( g.perm.Hyperlink && !g.javascriptHyperlink ){
   177      char *zHUrl = mprintf("<a class=\"%s\" href=\"%h\">", zExtra, zUrl);
   178      fossil_free(zUrl);
   179      return zHUrl;
   180    }
   181    needHrefJs = 1;
   182    return mprintf("<a class='%s' data-href='%z' href='%R/honeypot'>",
   183                   zExtra, zUrl);
   184  }
   185  char *href(const char *zFormat, ...){
   186    char *zUrl;
   187    va_list ap;
   188    va_start(ap, zFormat);
   189    zUrl = vmprintf(zFormat, ap);
   190    va_end(ap);
   191    if( g.perm.Hyperlink && !g.javascriptHyperlink ){
   192      char *zHUrl = mprintf("<a href=\"%h\">", zUrl);
   193      fossil_free(zUrl);
   194      return zHUrl;
   195    }
   196    needHrefJs = 1;
   197    return mprintf("<a data-href='%s' href='%R/honeypot'>",
   198                    zUrl);
   199  }
   200  
   201  /*
   202  ** Generate <form method="post" action=ARG>.  The ARG value is inserted
   203  ** by javascript.
   204  */
   205  void form_begin(const char *zOtherArgs, const char *zAction, ...){
   206    char *zLink;
   207    va_list ap;
   208    if( zOtherArgs==0 ) zOtherArgs = "";
   209    va_start(ap, zAction);
   210    zLink = vmprintf(zAction, ap);
   211    va_end(ap);
   212    if( g.perm.Hyperlink && !g.javascriptHyperlink ){
   213      @ <form method="POST" action="%z(zLink)" %s(zOtherArgs)>
   214    }else{
   215      needHrefJs = 1;
   216      @ <form method="POST" data-action='%s(zLink)' action='%R/login' \
   217      @ %s(zOtherArgs)>
   218    }
   219  }
   220  
   221  /*
   222  ** Add a new element to the submenu
   223  */
   224  void style_submenu_element(
   225    const char *zLabel,
   226    const char *zLink,
   227    ...
   228  ){
   229    va_list ap;
   230    assert( nSubmenu < count(aSubmenu) );
   231    aSubmenu[nSubmenu].zLabel = zLabel;
   232    va_start(ap, zLink);
   233    aSubmenu[nSubmenu].zLink = vmprintf(zLink, ap);
   234    va_end(ap);
   235    nSubmenu++;
   236  }
   237  void style_submenu_entry(
   238    const char *zName,       /* Query parameter name */
   239    const char *zLabel,      /* Label before the entry box */
   240    int iSize,               /* Size of the entry box */
   241    int eVisible             /* Visible or disabled */
   242  ){
   243    assert( nSubmenuCtrl < count(aSubmenuCtrl) );
   244    aSubmenuCtrl[nSubmenuCtrl].zName = zName;
   245    aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
   246    aSubmenuCtrl[nSubmenuCtrl].iSize = iSize;
   247    aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
   248    aSubmenuCtrl[nSubmenuCtrl].eType = FF_ENTRY;
   249    nSubmenuCtrl++;
   250  }
   251  void style_submenu_checkbox(
   252    const char *zName,       /* Query parameter name */
   253    const char *zLabel,      /* Label to display after the checkbox */
   254    int eVisible,            /* Visible or disabled */
   255    const char *zJS          /* Optional javascript to run on toggle */
   256  ){
   257    assert( nSubmenuCtrl < count(aSubmenuCtrl) );
   258    aSubmenuCtrl[nSubmenuCtrl].zName = zName;
   259    aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
   260    aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
   261    aSubmenuCtrl[nSubmenuCtrl].zJS = zJS;
   262    aSubmenuCtrl[nSubmenuCtrl].eType = FF_CHECKBOX;
   263    nSubmenuCtrl++;
   264  }
   265  void style_submenu_binary(
   266    const char *zName,       /* Query parameter name */
   267    const char *zTrue,       /* Label to show when parameter is true */
   268    const char *zFalse,      /* Label to show when the parameter is false */
   269    int eVisible             /* Visible or disabled */
   270  ){
   271    assert( nSubmenuCtrl < count(aSubmenuCtrl) );
   272    aSubmenuCtrl[nSubmenuCtrl].zName = zName;
   273    aSubmenuCtrl[nSubmenuCtrl].zLabel = zTrue;
   274    aSubmenuCtrl[nSubmenuCtrl].zFalse = zFalse;
   275    aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
   276    aSubmenuCtrl[nSubmenuCtrl].eType = FF_BINARY;
   277    nSubmenuCtrl++;
   278  }
   279  void style_submenu_multichoice(
   280    const char *zName,           /* Query parameter name */
   281    int nChoice,                 /* Number of options */
   282    const char *const *azChoice, /* value/display pairs.  2*nChoice entries */
   283    int eVisible                 /* Visible or disabled */
   284  ){
   285    assert( nSubmenuCtrl < count(aSubmenuCtrl) );
   286    aSubmenuCtrl[nSubmenuCtrl].zName = zName;
   287    aSubmenuCtrl[nSubmenuCtrl].iSize = nChoice;
   288    aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
   289    aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
   290    aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
   291    nSubmenuCtrl++;
   292  }
   293  void style_submenu_sql(
   294    const char *zName,       /* Query parameter name */
   295    const char *zLabel,      /* Label on the control */
   296    const char *zFormat,     /* Format string for SQL command for choices */
   297    ...                      /* Arguments to the format string */
   298  ){
   299    Stmt q;
   300    int n = 0;
   301    int nAlloc = 0;
   302    char **az = 0;
   303    va_list ap;
   304  
   305    va_start(ap, zFormat);
   306    db_vprepare(&q, 0, zFormat, ap);
   307    va_end(ap);
   308    while( SQLITE_ROW==db_step(&q) ){
   309      if( n+2>=nAlloc ){
   310        nAlloc += nAlloc + 20;
   311        az = fossil_realloc(az, sizeof(char*)*nAlloc);
   312      }
   313      az[n++] = fossil_strdup(db_column_text(&q,0));
   314      az[n++] = fossil_strdup(db_column_text(&q,1));
   315    }
   316    db_finalize(&q);
   317    if( n>0 ){
   318      aSubmenuCtrl[nSubmenuCtrl].zName = zName;
   319      aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
   320      aSubmenuCtrl[nSubmenuCtrl].iSize = n/2;
   321      aSubmenuCtrl[nSubmenuCtrl].azChoice = (const char *const *)az;
   322      aSubmenuCtrl[nSubmenuCtrl].eVisible = STYLE_NORMAL;
   323      aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
   324      nSubmenuCtrl++;
   325    }
   326  }
   327  
   328  /*
   329  ** Disable or enable the submenu
   330  */
   331  void style_submenu_enable(int onOff){
   332    submenuEnable = onOff;
   333  }
   334  
   335  
   336  /*
   337  ** Compare two submenu items for sorting purposes
   338  */
   339  static int submenuCompare(const void *a, const void *b){
   340    const struct Submenu *A = (const struct Submenu*)a;
   341    const struct Submenu *B = (const struct Submenu*)b;
   342    return fossil_strcmp(A->zLabel, B->zLabel);
   343  }
   344  
   345  /* Use this for the $current_page variable if it is not NULL.  If it
   346  ** is NULL then use g.zPath.
   347  */
   348  static char *local_zCurrentPage = 0;
   349  
   350  /*
   351  ** Set the desired $current_page to something other than g.zPath
   352  */
   353  void style_set_current_page(const char *zFormat, ...){
   354    fossil_free(local_zCurrentPage);
   355    if( zFormat==0 ){
   356      local_zCurrentPage = 0;
   357    }else{
   358      va_list ap;
   359      va_start(ap, zFormat);
   360      local_zCurrentPage = vmprintf(zFormat, ap);
   361      va_end(ap);
   362    }
   363  }
   364  
   365  /*
   366  ** Create a TH1 variable containing the URL for the specified config
   367  ** resource. The resulting variable name will be of the form
   368  ** $[zVarPrefix]_url.
   369  */
   370  static void url_var(
   371    const char *zVarPrefix,
   372    const char *zConfigName,
   373    const char *zPageName
   374  ){
   375    char *zVarName = mprintf("%s_url", zVarPrefix);
   376    char *zUrl = 0;              /* stylesheet URL */
   377    int hasBuiltin = 0;          /* true for built-in page-specific CSS */
   378  
   379    if(0==strcmp("css",zConfigName)){
   380      /* Account for page-specific CSS, appending a /{{g.zPath}} to the
   381      ** url only if we have a corresponding built-in page-specific CSS
   382      ** file. Do not append it to all pages because we would
   383      ** effectively cache-bust all pages which do not have
   384      ** page-specific CSS. */
   385      char * zBuiltin = mprintf("style.%s.css", g.zPath);
   386      hasBuiltin = builtin_file(zBuiltin,0)!=0;
   387      fossil_free(zBuiltin);
   388    }
   389    zUrl = mprintf("%R/%s%s%s?id=%x", zPageName,
   390                   hasBuiltin ? "/" : "", hasBuiltin ? g.zPath : "",
   391                   skin_id(zConfigName));
   392    Th_Store(zVarName, zUrl);
   393    fossil_free(zUrl);
   394    fossil_free(zVarName);
   395  }
   396  
   397  /*
   398  ** Create a TH1 variable containing the URL for the specified config image.
   399  ** The resulting variable name will be of the form $[zImageName]_image_url.
   400  */
   401  static void image_url_var(const char *zImageName){
   402    char *zVarPrefix = mprintf("%s_image", zImageName);
   403    char *zConfigName = mprintf("%s-image", zImageName);
   404    url_var(zVarPrefix, zConfigName, zImageName);
   405    free(zVarPrefix);
   406    free(zConfigName);
   407  }
   408  
   409  /*
   410  ** Output TEXT with a click-to-copy button next to it. Loads the copybtn.js
   411  ** Javascript module, and generates HTML elements with the following IDs:
   412  **
   413  **    TARGETID:       The <span> wrapper around TEXT.
   414  **    copy-TARGETID:  The <span> for the copy button.
   415  **
   416  ** If the FLIPPED argument is non-zero, the copy button is displayed after TEXT.
   417  **
   418  ** The COPYLENGTH argument defines the length of the substring of TEXT copied to
   419  ** clipboard:
   420  **
   421  **    <= 0:   No limit (default if the argument is omitted).
   422  **    >= 3:   Truncate TEXT after COPYLENGTH (single-byte) characters.
   423  **       1:   Use the "hash-digits" setting as the limit.
   424  **       2:   Use the length appropriate for URLs as the limit (defined at
   425  **            compile-time by FOSSIL_HASH_DIGITS_URL, defaults to 16).
   426  */
   427  char *style_copy_button(
   428    int bOutputCGI,         /* Don't return result, but send to cgi_printf(). */
   429    const char *zTargetId,  /* The TARGETID argument. */
   430    int bFlipped,           /* The FLIPPED argument. */
   431    int cchLength,          /* The COPYLENGTH argument. */
   432    const char *zTextFmt,   /* Formatting of the TEXT argument (htmlized). */
   433    ...                     /* Formatting parameters of the TEXT argument. */
   434  ){
   435    va_list ap;
   436    char *zText;
   437    char *zResult = 0;
   438    va_start(ap,zTextFmt);
   439    zText = vmprintf(zTextFmt/*works-like:?*/,ap);
   440    va_end(ap);
   441    if( cchLength==1 ) cchLength = hash_digits(0);
   442    else if( cchLength==2 ) cchLength = hash_digits(1);
   443    if( !bFlipped ){
   444      const char *zBtnFmt =
   445        "<span class=\"nobr\">"
   446        "<span "
   447        "class=\"copy-button\" "
   448        "id=\"copy-%h\" "
   449        "data-copytarget=\"%h\" "
   450        "data-copylength=\"%d\">"
   451        "</span>"
   452        "<span id=\"%h\">"
   453        "%s"
   454        "</span>"
   455        "</span>";
   456      if( bOutputCGI ){
   457        cgi_printf(
   458                    zBtnFmt/*works-like:"%h%h%d%h%s"*/,
   459                    zTargetId,zTargetId,cchLength,zTargetId,zText);
   460      }else{
   461        zResult = mprintf(
   462                    zBtnFmt/*works-like:"%h%h%d%h%s"*/,
   463                    zTargetId,zTargetId,cchLength,zTargetId,zText);
   464      }
   465    }else{
   466      const char *zBtnFmt =
   467        "<span class=\"nobr\">"
   468        "<span id=\"%h\">"
   469        "%s"
   470        "</span>"
   471        "<span "
   472        "class=\"copy-button copy-button-flipped\" "
   473        "id=\"copy-%h\" "
   474        "data-copytarget=\"%h\" "
   475        "data-copylength=\"%d\">"
   476        "</span>"
   477        "</span>";
   478      if( bOutputCGI ){
   479        cgi_printf(
   480                    zBtnFmt/*works-like:"%h%s%h%h%d"*/,
   481                    zTargetId,zText,zTargetId,zTargetId,cchLength);
   482      }else{
   483        zResult = mprintf(
   484                    zBtnFmt/*works-like:"%h%s%h%h%d"*/,
   485                    zTargetId,zText,zTargetId,zTargetId,cchLength);
   486      }
   487    }
   488    free(zText);
   489    style_copybutton_control();
   490    return zResult;
   491  }
   492  
   493  /*
   494  ** Return a random nonce that is stored in static space.  For a particular
   495  ** run, the same nonce is always returned.
   496  */
   497  char *style_nonce(void){
   498    static char zNonce[52];
   499    if( zNonce[0]==0 ){
   500      unsigned char zSeed[24];
   501      sqlite3_randomness(24, zSeed);
   502      encode16(zSeed,(unsigned char*)zNonce,24);
   503    }
   504    return zNonce;
   505  }
   506  
   507  /*
   508  ** Return the default Content Security Policy (CSP) string.
   509  ** If the toHeader argument is true, then also add the
   510  ** CSP to the HTTP reply header.
   511  **
   512  ** The CSP comes from the "default-csp" setting if it exists and
   513  ** is non-empty.  If that setting is an empty string, then the following
   514  ** default is used instead:
   515  **
   516  **     default-src 'self' data:;
   517  **     script-src 'self' 'nonce-$nonce';
   518  **     style-src 'self' 'unsafe-inline';
   519  **
   520  ** The text '$nonce' is replaced by style_nonce() if and whereever it
   521  ** occurs in the input string.
   522  **
   523  ** The string returned is obtained from fossil_malloc() and
   524  ** should be released by the caller.
   525  */
   526  char *style_csp(int toHeader){
527 static const char zBackupCSP[] = 528 "default-src 'self' data:; " 529 "script-src 'self' 'nonce-$nonce'; " 530 "style-src 'self' 'unsafe-inline'";
531 const char *zFormat = db_get("default-csp",""); 532 Blob csp; 533 char *zNonce; 534 char *zCsp; 535 if( zFormat[0]==0 ){ 536 zFormat = zBackupCSP; 537 } 538 blob_init(&csp, 0, 0); 539 while( zFormat[0] && (zNonce = strstr(zFormat,"$nonce"))!=0 ){ 540 blob_append(&csp, zFormat, (int)(zNonce - zFormat)); 541 blob_append(&csp, style_nonce(), -1); 542 zFormat = zNonce + 6; 543 } 544 blob_append(&csp, zFormat, -1); 545 zCsp = blob_str(&csp); 546 if( toHeader ){ 547 cgi_printf_header("Content-Security-Policy: %s\r\n", zCsp); 548 } 549 return zCsp; 550 } 551 552 /* 553 ** Default HTML page header text through <body>. If the repository-specific 554 ** header template lacks a <body> tag, then all of the following is 555 ** prepended. 556 */ 557 static char zDfltHeader[] = 558 @ <html> 559 @ <head> 560 @ <base href="$baseurl/$current_page" /> 561 @ <meta http-equiv="Content-Security-Policy" content="$default_csp" /> 562 @ <meta name="viewport" content="width=device-width, initial-scale=1.0"> 563 @ <title>$<project_name>: $<title></title> 564 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" \ 565 @ href="$home/timeline.rss" /> 566 @ <link rel="stylesheet" href="$stylesheet_url" type="text/css" /> 567 @ </head> 568 @ <body> 569 ; 570 571 /* 572 ** Initialize all the default TH1 variables 573 */ 574 static void style_init_th1_vars(const char *zTitle){ 575 const char *zNonce = style_nonce(); 576 char *zDfltCsp; 577 578 zDfltCsp = style_csp(1); 579 /* 580 ** Do not overwrite the TH1 variable "default_csp" if it exists, as this 581 ** allows it to be properly overridden via the TH1 setup script (i.e. it 582 ** is evaluated before the header is rendered). 583 */ 584 Th_MaybeStore("default_csp", zDfltCsp); 585 fossil_free(zDfltCsp); 586 Th_Store("nonce", zNonce); 587 Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); 588 Th_Store("project_description", db_get("project-description","")); 589 if( zTitle ) Th_Store("title", zTitle); 590 Th_Store("baseurl", g.zBaseURL); 591 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); 592 Th_Store("home", g.zTop); 593 Th_Store("index_page", db_get("index-page","/home")); 594 if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); 595 Th_Store("current_page", local_zCurrentPage); 596 Th_Store("csrf_token", g.zCsrfToken); 597 Th_Store("release_version", RELEASE_VERSION); 598 Th_Store("manifest_version", MANIFEST_VERSION); 599 Th_Store("manifest_date", MANIFEST_DATE); 600 Th_Store("compiler_name", COMPILER_NAME); 601 url_var("stylesheet", "css", "style.css"); 602 image_url_var("logo"); 603 image_url_var("background"); 604 if( !login_is_nobody() ){ 605 Th_Store("login", g.zLogin); 606 } 607 } 608 609 /* 610 ** Draw the header. 611 */ 612 void style_header(const char *zTitleFormat, ...){ 613 va_list ap; 614 char *zTitle; 615 const char *zHeader = skin_get("header"); 616 login_check_credentials(); 617 618 va_start(ap, zTitleFormat); 619 zTitle = vmprintf(zTitleFormat, ap); 620 va_end(ap); 621 622 cgi_destination(CGI_HEADER); 623 624 @ <!DOCTYPE html> 625 626 if( g.thTrace ) Th_Trace("BEGIN_HEADER<br />\n", -1); 627 628 /* Generate the header up through the main menu */ 629 style_init_th1_vars(zTitle); 630 if( sqlite3_strlike("%<body%", zHeader, 0)!=0 ){ 631 Th_Render(zDfltHeader); 632 } 633 if( g.thTrace ) Th_Trace("BEGIN_HEADER_SCRIPT<br />\n", -1); 634 Th_Render(zHeader); 635 if( g.thTrace ) Th_Trace("END_HEADER<br />\n", -1); 636 Th_Unstore("title"); /* Avoid collisions with ticket field names */ 637 cgi_destination(CGI_BODY); 638 g.cgiOutput = 1; 639 headerHasBeenGenerated = 1; 640 sideboxUsed = 0; 641 if( g.perm.Debug && P("showqp") ){ 642 @ <div class="debug"> 643 cgi_print_all(0, 0); 644 @ </div> 645 } 646 } 647 648 #if INTERFACE 649 /* Allowed parameters for style_adunit() */ 650 #define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */ 651 #define ADUNIT_RIGHT_OK 0x0002 /* Right-side vertical ads ok here */ 652 #endif 653 654 /* 655 ** Various page implementations can invoke this interface to let the 656 ** style manager know what kinds of ads are appropriate for this page. 657 */ 658 void style_adunit_config(unsigned int mFlags){ 659 adUnitFlags = mFlags; 660 } 661 662 /* 663 ** Return the text of an ad-unit, if one should be rendered. Return 664 ** NULL if no ad-unit is desired. 665 ** 666 ** The *pAdFlag value might be set to ADUNIT_RIGHT_OK if this is 667 ** a right-hand vertical ad. 668 */ 669 static const char *style_adunit_text(unsigned int *pAdFlag){ 670 const char *zAd = 0; 671 *pAdFlag = 0; 672 if( adUnitFlags & ADUNIT_OFF ) return 0; /* Disallow ads on this page */ 673 if( db_get_boolean("adunit-disable",0) ) return 0; 674 if( g.perm.Admin && db_get_boolean("adunit-omit-if-admin",0) ){ 675 return 0; 676 } 677 if( !login_is_nobody() 678 && fossil_strcmp(g.zLogin,"anonymous")!=0 679 && db_get_boolean("adunit-omit-if-user",0) 680 ){ 681 return 0; 682 } 683 if( (adUnitFlags & ADUNIT_RIGHT_OK)!=0 684 && !fossil_all_whitespace(zAd = db_get("adunit-right", 0)) 685 && !cgi_body_contains("<table") 686 ){ 687 *pAdFlag = ADUNIT_RIGHT_OK; 688 return zAd; 689 }else if( !fossil_all_whitespace(zAd = db_get("adunit",0)) ){ 690 return zAd; 691 } 692 return 0; 693 } 694 695 /* 696 ** Indicate that the table-sorting javascript is needed. 697 */ 698 void style_table_sorter(void){ 699 needSortJs = 1; 700 } 701 702 /* 703 ** Indicate that the accordion javascript is needed. 704 */ 705 void style_accordion(void){ 706 needAccordionJs = 1; 707 } 708 709 /* 710 ** Indicate that the timeline graph javascript is needed. 711 */ 712 void style_graph_generator(void){ 713 needGraphJs = 1; 714 } 715 716 /* 717 ** Indicate that the copy button javascript is needed. 718 */ 719 void style_copybutton_control(void){ 720 needCopyBtnJs = 1; 721 } 722 723 /* 724 ** Generate code to load a single javascript file 725 */ 726 void style_load_one_js_file(const char *zFile){ 727 @ <script src='%R/builtin/%s(zFile)?id=%S(fossil_exe_id())'></script> 728 } 729 730 /* 731 ** All extra JS files to load. 732 */ 733 static const char *azJsToLoad[4]; 734 static int nJsToLoad = 0; 735 736 /* 737 ** Register a new JS file to load at the end of the document. 738 */ 739 void style_load_js(const char *zName){ 740 int i; 741 for(i=0; i<nJsToLoad; i++){ 742 if( fossil_strcmp(zName, azJsToLoad[i])==0 ) return; 743 } 744 if( nJsToLoad>=sizeof(azJsToLoad)/sizeof(azJsToLoad[0]) ){ 745 fossil_panic("too many JS files"); 746 } 747 azJsToLoad[nJsToLoad++] = zName; 748 } 749 750 /* 751 ** Generate code to load all required javascript files. 752 */ 753 static void style_load_all_js_files(void){ 754 int i; 755 if( needHrefJs ){ 756 int nDelay = db_get_int("auto-hyperlink-delay",0); 757 int bMouseover = db_get_boolean("auto-hyperlink-mouseover",0); 758 @ <script id='href-data' type='application/json'>\ 759 @ {"delay":%d(nDelay),"mouseover":%d(bMouseover)}</script> 760 } 761 @ <script nonce="%h(style_nonce())"> 762 @ function debugMsg(msg){ 763 @ var n = document.getElementById("debugMsg"); 764 @ if(n){n.textContent=msg;} 765 @ } 766 if( needHrefJs ){ 767 cgi_append_content(builtin_text("href.js"),-1); 768 } 769 if( needSortJs ){ 770 cgi_append_content(builtin_text("sorttable.js"),-1); 771 } 772 if( needGraphJs ){ 773 cgi_append_content(builtin_text("graph.js"),-1); 774 } 775 if( needCopyBtnJs ){ 776 cgi_append_content(builtin_text("copybtn.js"),-1); 777 } 778 if( needAccordionJs ){ 779 cgi_append_content(builtin_text("accordion.js"),-1); 780 } 781 for(i=0; i<nJsToLoad; i++){ 782 cgi_append_content(builtin_text(azJsToLoad[i]),-1); 783 } 784 if( blob_size(&blobOnLoad)>0 ){ 785 @ window.onload = function(){ 786 cgi_append_content(blob_buffer(&blobOnLoad), blob_size(&blobOnLoad)); 787 cgi_append_content("\n}\n", -1); 788 } 789 @ </script> 790 } 791 792 /* 793 ** Extra JS to run after all content is loaded. 794 */ 795 void style_js_onload(const char *zFormat, ...){ 796 va_list ap; 797 va_start(ap, zFormat); 798 blob_vappendf(&blobOnLoad, zFormat, ap); 799 va_end(ap); 800 } 801 802 /* 803 ** Draw the footer at the bottom of the page. 804 */ 805 void style_footer(void){ 806 const char *zFooter; 807 const char *zAd = 0; 808 unsigned int mAdFlags = 0; 809 810 if( !headerHasBeenGenerated ) return; 811 812 /* Go back and put the submenu at the top of the page. We delay the 813 ** creation of the submenu until the end so that we can add elements 814 ** to the submenu while generating page text. 815 */ 816 cgi_destination(CGI_HEADER); 817 if( submenuEnable && nSubmenu+nSubmenuCtrl>0 ){ 818 int i; 819 if( nSubmenuCtrl ){ 820 @ <form id='f01' method='GET' action='%R/%s(g.zPath)'> 821 @ <input type='hidden' name='udc' value='1'> 822 cgi_tag_query_parameter("udc"); 823 } 824 @ <div class="submenu"> 825 if( nSubmenu>0 ){ 826 qsort(aSubmenu, nSubmenu, sizeof(aSubmenu[0]), submenuCompare); 827 for(i=0; i<nSubmenu; i++){ 828 struct Submenu *p = &aSubmenu[i]; 829 if( p->zLink==0 ){ 830 @ <span class="label">%h(p->zLabel)</span> 831 }else{ 832 @ <a class="label" href="%h(p->zLink)">%h(p->zLabel)</a> 833 } 834 } 835 } 836 for(i=0; i<nSubmenuCtrl; i++){ 837 const char *zQPN = aSubmenuCtrl[i].zName; 838 const char *zDisabled = ""; 839 const char *zXtraClass = ""; 840 if( aSubmenuCtrl[i].eVisible & STYLE_DISABLED ){ 841 zDisabled = " disabled"; 842 }else if( zQPN ){ 843 cgi_tag_query_parameter(zQPN); 844 } 845 switch( aSubmenuCtrl[i].eType ){ 846 case FF_ENTRY: 847 @ <span class='submenuctrl%s(zXtraClass)'>\ 848 @ &nbsp;%h(aSubmenuCtrl[i].zLabel)\ 849 @ <input type='text' name='%s(zQPN)' value='%h(PD(zQPN, ""))' \ 850 if( aSubmenuCtrl[i].iSize<0 ){ 851 @ size='%d(-aSubmenuCtrl[i].iSize)' \ 852 }else if( aSubmenuCtrl[i].iSize>0 ){ 853 @ size='%d(aSubmenuCtrl[i].iSize)' \ 854 @ maxlength='%d(aSubmenuCtrl[i].iSize)' \ 855 } 856 @ id='submenuctrl-%d(i)'%s(zDisabled)></span> 857 break; 858 case FF_MULTI: { 859 int j; 860 const char *zVal = P(zQPN); 861 if( zXtraClass[0] ){ 862 @ <span class='%s(zXtraClass+1)'> 863 } 864 if( aSubmenuCtrl[i].zLabel ){ 865 @ &nbsp;%h(aSubmenuCtrl[i].zLabel)\ 866 } 867 @ <select class='submenuctrl' size='1' name='%s(zQPN)' \ 868 @ id='submenuctrl-%d(i)'%s(zDisabled)> 869 for(j=0; j<aSubmenuCtrl[i].iSize*2; j+=2){ 870 const char *zQPV = aSubmenuCtrl[i].azChoice[j]; 871 @ <option value='%h(zQPV)'\ 872 if( fossil_strcmp(zVal, zQPV)==0 ){ 873 @ selected\ 874 } 875 @ >%h(aSubmenuCtrl[i].azChoice[j+1])</option> 876 } 877 @ </select> 878 if( zXtraClass[0] ){ 879 @ </span> 880 } 881 break; 882 } 883 case FF_BINARY: { 884 int isTrue = PB(zQPN); 885 @ <select class='submenuctrl%s(zXtraClass)' size='1' \ 886 @ name='%s(zQPN)' id='submenuctrl-%d(i)'%s(zDisabled)> 887 @ <option value='1'\ 888 if( isTrue ){ 889 @ selected\ 890 } 891 @ >%h(aSubmenuCtrl[i].zLabel)</option> 892 @ <option value='0'\ 893 if( !isTrue ){ 894 @ selected\ 895 } 896 @ >%h(aSubmenuCtrl[i].zFalse)</option> 897 @ </select> 898 break; 899 } 900 case FF_CHECKBOX: { 901 @ <label class='submenuctrl submenuckbox%s(zXtraClass)'>\ 902 @ <input type='checkbox' name='%s(zQPN)' id='submenuctrl-%d(i)' \ 903 if( PB(zQPN) ){ 904 @ checked \ 905 } 906 if( aSubmenuCtrl[i].zJS ){ 907 @ data-ctrl='%s(aSubmenuCtrl[i].zJS)'%s(zDisabled)>\ 908 }else{ 909 @ %s(zDisabled)>\ 910 } 911 @ %h(aSubmenuCtrl[i].zLabel)</label> 912 break; 913 } 914 } 915 } 916 @ </div> 917 if( nSubmenuCtrl ){ 918 cgi_query_parameters_to_hidden(); 919 cgi_tag_query_parameter(0); 920 @ </form> 921 style_load_one_js_file("menu.js"); 922 } 923 } 924 925 zAd = style_adunit_text(&mAdFlags); 926 if( (mAdFlags & ADUNIT_RIGHT_OK)!=0 ){ 927 @ <div class="content adunit_right_container"> 928 @ <div class="adunit_right"> 929 cgi_append_content(zAd, -1); 930 @ </div> 931 }else{ 932 if( zAd ){ 933 @ <div class="adunit_banner"> 934 cgi_append_content(zAd, -1); 935 @ </div> 936 } 937 @ <div class="content"><span id="debugMsg"></span> 938 } 939 cgi_destination(CGI_BODY); 940 941 if( sideboxUsed ){ 942 /* Put the footer at the bottom of the page. 943 ** the additional clear/both is needed to extend the content 944 ** part to the end of an optional sidebox. 945 */ 946 @ <div class="endContent"></div> 947 } 948 @ </div> 949 950 951 952 zFooter = skin_get("footer"); 953 if( sqlite3_strlike("%</body>%", zFooter, 0)==0 ){ 954 style_load_all_js_files(); 955 } 956 if( g.thTrace ) Th_Trace("BEGIN_FOOTER<br />\n", -1); 957 Th_Render(zFooter); 958 if( g.thTrace ) Th_Trace("END_FOOTER<br />\n", -1); 959 960 /* Render trace log if TH1 tracing is enabled. */ 961 if( g.thTrace ){ 962 cgi_append_content("<span class=\"thTrace\"><hr />\n", -1); 963 cgi_append_content(blob_str(&g.thLog), blob_size(&g.thLog)); 964 cgi_append_content("</span>\n", -1); 965 } 966 967 /* Add document end mark if it was not in the footer */ 968 if( sqlite3_strlike("%</body>%", zFooter, 0)!=0 ){ 969 style_load_all_js_files(); 970 @ </body> 971 @ </html> 972 } 973 } 974 975 /* 976 ** Begin a side-box on the right-hand side of a page. The title and 977 ** the width of the box are given as arguments. The width is usually 978 ** a percentage of total screen width. 979 */ 980 void style_sidebox_begin(const char *zTitle, const char *zWidth){ 981 sideboxUsed = 1; 982 @ <div class="sidebox" style="width:%s(zWidth)"> 983 @ <div class="sideboxTitle">%h(zTitle)</div> 984 } 985 986 /* End the side-box 987 */ 988 void style_sidebox_end(void){ 989 @ </div> 990 } 991 992 /* 993 ** Search string zCss for zSelector. 994 ** 995 ** Return true if found. Return false if not found 996 */ 997 static int containsSelector(const char *zCss, const char *zSelector){ 998 const char *z; 999 int n; 1000 int selectorLen = (int)strlen(zSelector); 1001 1002 for(z=zCss; *z; z+=selectorLen){ 1003 z = strstr(z, zSelector); 1004 if( z==0 ) return 0; 1005 if( z!=zCss ){ 1006 for( n=-1; z+n!=zCss && fossil_isspace(z[n]); n--); 1007 if( z+n!=zCss && z[n]!=',' && z[n]!= '}' && z[n]!='/' ) continue; 1008 } 1009 for( n=selectorLen; z[n] && fossil_isspace(z[n]); n++ ); 1010 if( z[n]==',' || z[n]=='{' || z[n]=='/' ) return 1; 1011 } 1012 return 0; 1013 } 1014 1015 /* 1016 ** COMMAND: test-contains-selector 1017 ** 1018 ** Usage: %fossil test-contains-selector FILENAME SELECTOR 1019 ** 1020 ** Determine if the CSS stylesheet FILENAME contains SELECTOR. 1021 ** 1022 ** Note that as of 2020-05-28, the default rules are always emitted, 1023 ** so the containsSelector() logic is no longer applied when emitting 1024 ** style.css. It is unclear whether this test command is now obsolete 1025 ** or whether it may still serve a purpose. 1026 */ 1027 void contains_selector_cmd(void){ 1028 int found; 1029 char *zSelector; 1030 Blob css; 1031 if( g.argc!=4 ) usage("FILENAME SELECTOR"); 1032 blob_read_from_file(&css, g.argv[2], ExtFILE); 1033 zSelector = g.argv[3]; 1034 found = containsSelector(blob_str(&css), zSelector); 1035 fossil_print("%s %s\n", zSelector, found ? "found" : "not found"); 1036 blob_reset(&css); 1037 } 1038 1039 /* 1040 ** WEBPAGE: script.js 1041 ** 1042 ** Return the "Javascript" content for the current skin (if there is any) 1043 */ 1044 void page_script_js(void){ 1045 const char *zScript = skin_get("js"); 1046 if( P("test") ){ 1047 /* Render the script as plain-text for testing purposes, if the "test" 1048 ** query parameter is present */ 1049 cgi_set_content_type("text/plain"); 1050 }else{ 1051 /* Default behavior is to return javascript */ 1052 cgi_set_content_type("application/javascript"); 1053 } 1054 style_init_th1_vars(0); 1055 Th_Render(zScript?zScript:""); 1056 } 1057 1058 /* 1059 ** If one of the "name" or "page" URL parameters (in that order) 1060 ** is set then this function looks for page/page group-specific 1061 ** CSS and (if found) appends it to pOut, else it is a no-op. 1062 */ 1063 static void page_style_css_append_page_style(Blob *pOut){ 1064 const char *zPage = PD("name",P("page")); 1065 char * zFile; 1066 int nFile = 0; 1067 const char *zBuiltin; 1068 1069 if(zPage==0 || zPage[0]==0){ 1070 return; 1071 } 1072 zFile = mprintf("style.%s.css", zPage); 1073 zBuiltin = (const char *)builtin_file(zFile, &nFile); 1074 if(nFile>0){ 1075 blob_appendf(pOut, 1076 "\n/***********************************************************\n" 1077 "** Start of page-specific CSS for page %s...\n" 1078 "***********************************************************/\n", 1079 zPage); 1080 blob_append(pOut, zBuiltin, nFile); 1081 blob_appendf(pOut, 1082 "\n/***********************************************************\n" 1083 "** End of page-specific CSS for page %s.\n" 1084 "***********************************************************/\n", 1085 zPage); 1086 fossil_free(zFile); 1087 return; 1088 } 1089 /* Potential TODO: check for aliases/page groups. e.g. group all 1090 ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As 1091 ** of this writing, doing so would only shave a few kb from 1092 ** default.css. */ 1093 fossil_free(zFile); 1094 } 1095 1096 /* 1097 ** WEBPAGE: style.css 1098 ** 1099 ** Return the style sheet. 1100 */ 1101 void page_style_css(void){ 1102 Blob css = empty_blob; 1103 int i; 1104 const char * zDefaults; 1105 1106 cgi_set_content_type("text/css"); 1107 /* Emit all default rules... */ 1108 zDefaults = (const char*)builtin_file("default.css", &i); 1109 blob_append(&css, zDefaults, i); 1110 /* Page-specific CSS, if any... */ 1111 page_style_css_append_page_style(&css); 1112 blob_append(&css, 1113 "\n/***********************************************************\n" 1114 "** All CSS which follows is supplied by the repository \"skin\".\n" 1115 "***********************************************************/\n", 1116 -1); 1117 blob_append(&css,skin_get("css"),-1); 1118 /* Process through TH1 in order to give an opportunity to substitute 1119 ** variables such as $baseurl. 1120 */ 1121 Th_Store("baseurl", g.zBaseURL); 1122 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); 1123 Th_Store("home", g.zTop); 1124 image_url_var("logo"); 1125 image_url_var("background"); 1126 Th_Render(blob_str(&css)); 1127 1128 /* Tell CGI that the content returned by this page is considered cacheable */ 1129 g.isConst = 1; 1130 } 1131 1132 /* 1133 ** WEBPAGE: builtin 1134 ** URL: builtin/FILENAME 1135 ** 1136 ** Return the built-in text given by FILENAME. This is used internally 1137 ** by many Fossil web pages to load built-in javascript files. 1138 ** 1139 ** If the id= query parameter is present, then Fossil assumes that the 1140 ** result is immutable and sets a very large cache retention time (1 year). 1141 */ 1142 void page_builtin_text(void){ 1143 Blob out; 1144 const char *zName = P("name"); 1145 const char *zTxt = 0; 1146 const char *zId = P("id"); 1147 int nId; 1148 if( zName ) zTxt = builtin_text(zName); 1149 if( zTxt==0 ){ 1150 cgi_set_status(404, "Not Found"); 1151 @ File "%h(zName)" not found 1152 return; 1153 } 1154 if( sqlite3_strglob("*.js", zName)==0 ){ 1155 cgi_set_content_type("application/javascript"); 1156 }else{ 1157 cgi_set_content_type("text/plain"); 1158 } 1159 if( zId && (nId = (int)strlen(zId))>=8 && strncmp(zId,MANIFEST_UUID,nId)==0 ){ 1160 g.isConst = 1; 1161 }else{ 1162 etag_check(0,0); 1163 } 1164 blob_init(&out, zTxt, -1); 1165 cgi_set_content(&out); 1166 } 1167 1168 /* 1169 ** All possible capabilities 1170 */ 1171 static const char allCap[] = 1172 "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL"; 1173 1174 /* 1175 ** Compute the current login capabilities 1176 */ 1177 static char *find_capabilities(char *zCap){ 1178 int i, j; 1179 char c; 1180 for(i=j=0; (c = allCap[j])!=0; j++){ 1181 if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; 1182 } 1183 zCap[i] = 0; 1184 return zCap; 1185 } 1186 1187 /* 1188 ** Compute the current login capabilities that were 1189 ** contributed by Anonymous 1190 */ 1191 static char *find_anon_capabilities(char *zCap){ 1192 int i, j; 1193 char c; 1194 for(i=j=0; (c = allCap[j])!=0; j++){ 1195 if( login_has_capability(&c, 1, LOGIN_ANON) 1196 && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; 1197 } 1198 zCap[i] = 0; 1199 return zCap; 1200 } 1201 1202 /* 1203 ** WEBPAGE: test_env 1204 ** 1205 ** Display CGI-variables and other aspects of the run-time 1206 ** environment, for debugging and trouble-shooting purposes. 1207 */ 1208 void page_test_env(void){ 1209 webpage_error(""); 1210 } 1211 1212 /* 1213 ** WEBPAGE: honeypot 1214 ** This page is a honeypot for spiders and bots. 1215 */ 1216 void honeypot_page(void){ 1217 cgi_set_status(403, "Forbidden"); 1218 @ <p>Please enable javascript or log in to see this content</p> 1219 } 1220 1221 /* 1222 ** Webpages that encounter an error due to missing or incorrect 1223 ** query parameters can jump to this routine to render an error 1224 ** message screen. 1225 ** 1226 ** For administators, or if the test_env_enable setting is true, then 1227 ** details of the request environment are displayed. Otherwise, just 1228 ** the error message is shown. 1229 ** 1230 ** If zFormat is an empty string, then this is the /test_env page. 1231 */ 1232 void webpage_error(const char *zFormat, ...){ 1233 int showAll; 1234 char *zErr = 0; 1235 int isAuth = 0; 1236 char zCap[100]; 1237 1238 login_check_credentials(); 1239 if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ 1240 isAuth = 1; 1241 } 1242 cgi_load_environment(); 1243 if( zFormat[0] ){ 1244 va_list ap; 1245 va_start(ap, zFormat); 1246 zErr = vmprintf(zFormat, ap); 1247 va_end(ap); 1248 style_header("Bad Request"); 1249 @ <h1>/%h(g.zPath): %h(zErr)</h1> 1250 showAll = 0; 1251 cgi_set_status(500, "Bad Request"); 1252 }else if( !isAuth ){ 1253 login_needed(0); 1254 return; 1255 }else{ 1256 style_header("Environment Test"); 1257 showAll = PB("showall"); 1258 style_submenu_checkbox("showall", "Cookies", 0, 0); 1259 style_submenu_element("Stats", "%R/stat"); 1260 } 1261 1262 if( isAuth ){ 1263 #if !defined(_WIN32) 1264 @ uid=%d(getuid()), gid=%d(getgid())<br /> 1265 #endif 1266 @ g.zBaseURL = %h(g.zBaseURL)<br /> 1267 @ g.zHttpsURL = %h(g.zHttpsURL)<br /> 1268 @ g.zTop = %h(g.zTop)<br /> 1269 @ g.zPath = %h(g.zPath)<br /> 1270 @ g.userUid = %d(g.userUid)<br /> 1271 @ g.zLogin = %h(g.zLogin)<br /> 1272 @ g.isHuman = %d(g.isHuman)<br /> 1273 if( g.nRequest ){ 1274 @ g.nRequest = %d(g.nRequest)<br /> 1275 } 1276 if( g.nPendingRequest>1 ){ 1277 @ g.nPendingRequest = %d(g.nPendingRequest)<br /> 1278 } 1279 @ capabilities = %s(find_capabilities(zCap))<br /> 1280 if( zCap[0] ){ 1281 @ anonymous-adds = %s(find_anon_capabilities(zCap))<br /> 1282 } 1283 @ g.zRepositoryName = %h(g.zRepositoryName)<br /> 1284 @ load_average() = %f(load_average())<br /> 1285 @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br /> 1286 @ fossil_exe_id() = %h(fossil_exe_id())<br /> 1287 @ <hr /> 1288 P("HTTP_USER_AGENT"); 1289 cgi_print_all(showAll, 0); 1290 if( showAll && blob_size(&g.httpHeader)>0 ){ 1291 @ <hr /> 1292 @ <pre> 1293 @ %h(blob_str(&g.httpHeader)) 1294 @ </pre> 1295 } 1296 } 1297 style_footer(); 1298 if( zErr ){ 1299 cgi_reply(); 1300 fossil_exit(1); 1301 } 1302 } 1303 1304 /* 1305 ** Generate a Not Yet Implemented error page. 1306 */ 1307 void webpage_not_yet_implemented(void){ 1308 webpage_error("Not yet implemented"); 1309 } 1310 1311 /* 1312 ** Generate a webpage for a webpage_assert(). 1313 */ 1314 void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){ 1315 fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr); 1316 cgi_reset_content(); 1317 webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr); 1318 } 1319 1320 #if INTERFACE 1321 # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);} 1322 #endif 1323 1324 /* 1325 ** Returns a pseudo-random input field ID, for use in associating an 1326 ** ID-less input field with a label. The memory is owned by the 1327 ** caller. 1328 */ 1329 static char * style_next_input_id(){ 1330 static int inputID = 0; 1331 ++inputID; 1332 return mprintf("input-id-%d", inputID); 1333 } 1334 1335 /* 1336 ** Outputs a labeled checkbox element. zWrapperId is an optional ID 1337 ** value for the containing element (see below). zFieldName is the 1338 ** form element name. zLabel is the label for the checkbox. zValue is 1339 ** the optional value for the checkbox. zTip is an optional tooltip, 1340 ** which gets set as the "title" attribute of the outermost 1341 ** element. If isChecked is true, the checkbox gets the "checked" 1342 ** attribute set, else it is not. 1343 ** 1344 ** Resulting structure: 1345 ** 1346 ** <span class='input-with-label' title={{zTip}} id={{zWrapperId}}> 1347 ** <input type='checkbox' name={{zFieldName}} value={{zValue}} 1348 ** id='A RANDOM VALUE' 1349 ** {{isChecked ? " checked : ""}}/> 1350 ** <label for='ID OF THE INPUT FIELD'>{{zLabel}}</label> 1351 ** </span> 1352 ** 1353 ** zLabel, and zValue are required. zFieldName, zWrapperId, and zTip 1354 ** are may be NULL or empty. 1355 ** 1356 ** Be sure that the input-with-label CSS class is defined sensibly, in 1357 ** particular, having its display:inline-block is useful for alignment 1358 ** purposes. 1359 */ 1360 void style_labeled_checkbox(const char * zWrapperId, 1361 const char *zFieldName, const char * zLabel, 1362 const char * zValue, int isChecked, 1363 const char * zTip){ 1364 char * zLabelID = style_next_input_id(); 1365 CX("<span class='input-with-label'"); 1366 if(zTip && *zTip){ 1367 CX(" title='%h'", zTip); 1368 } 1369 if(zWrapperId && *zWrapperId){ 1370 CX(" id='%s'",zWrapperId); 1371 } 1372 CX("><input type='checkbox' id='%s' ", zLabelID); 1373 if(zFieldName && *zFieldName){ 1374 CX("name='%s' ",zFieldName); 1375 } 1376 CX("value='%T'%s/>", 1377 zValue ? zValue : "", isChecked ? " checked" : ""); 1378 CX("<label for='%s'>%h</label></span>", zLabelID, zLabel); 1379 fossil_free(zLabelID); 1380 } 1381 1382 /* 1383 ** Outputs a SELECT list from a compile-time list of integers. 1384 ** The vargs must be a list of (const char *, int) pairs, terminated 1385 ** with a single NULL. Each pair is interpreted as... 1386 ** 1387 ** If the (const char *) is NULL, it is the end of the list, else 1388 ** a new OPTION entry is created. If the string is empty, the 1389 ** label and value of the OPTION is the integer part of the pair. 1390 ** If the string is not empty, it becomes the label and the integer 1391 ** the value. If that value == selectedValue then that OPTION 1392 ** element gets the 'selected' attribute. 1393 ** 1394 ** Note that the pairs are not in (int, const char *) order because 1395 ** there is no well-known integer value which we can definitively use 1396 ** as a list terminator. 1397 ** 1398 ** zWrapperId is an optional ID value for the containing element (see 1399 ** below). 1400 ** 1401 ** zFieldName is the value of the form element's name attribute. Note 1402 ** that fossil prefers underscores over '-' for separators in form 1403 ** element names. 1404 ** 1405 ** zLabel is an optional string to use as a "label" for the element 1406 ** (see below). 1407 ** 1408 ** zTooltip is an optional value for the SELECT's title attribute. 1409 ** 1410 ** The structure of the emitted HTML is: 1411 ** 1412 ** <span class='input-with-label' title={{zToolTip}} id={{zWrapperId}}> 1413 ** <label for='SELECT ELEMENT ID'>{{zLabel}}</label> 1414 ** <select id='RANDOM ID' name={{zFieldName}}>...</select> 1415 ** </span> 1416 ** 1417 ** Example: 1418 ** 1419 ** style_select_list_int("my-grapes", "my_grapes", "Grapes", 1420 ** "Select the number of grapes", 1421 ** atoi(PD("my_field","0")), 1422 ** "", 1, "2", 2, "Three", 3, 1423 ** NULL); 1424 ** 1425 */ 1426 void style_select_list_int(const char * zWrapperId, 1427 const char *zFieldName, const char * zLabel, 1428 const char * zToolTip, int selectedVal, 1429 ... ){ 1430 char * zLabelID = style_next_input_id(); 1431 va_list vargs; 1432 1433 va_start(vargs,selectedVal); 1434 CX("<span class='input-with-label'"); 1435 if(zToolTip && *zToolTip){ 1436 CX(" title='%h'",zToolTip); 1437 } 1438 if(zWrapperId && *zWrapperId){ 1439 CX(" id='%s'",zWrapperId); 1440 } 1441 CX(">"); 1442 if(zLabel && *zLabel){ 1443 CX("<label label='%s'>%h</label>", zLabelID, zLabel); 1444 } 1445 CX("<select name='%s' id='%s'>",zFieldName, zLabelID); 1446 while(1){ 1447 const char * zOption = va_arg(vargs,char *); 1448 int v; 1449 if(NULL==zOption){ 1450 break; 1451 } 1452 v = va_arg(vargs,int); 1453 CX("<option value='%d'%s>", 1454 v, v==selectedVal ? " selected" : ""); 1455 if(*zOption){ 1456 CX("%s", zOption); 1457 }else{ 1458 CX("%d",v); 1459 } 1460 CX("</option>\n"); 1461 } 1462 CX("</select>\n"); 1463 CX("</span>\n"); 1464 va_end(vargs); 1465 fossil_free(zLabelID); 1466 } 1467 1468 /* 1469 ** The C-string counterpart of style_select_list_int(), this variant 1470 ** differs only in that its variadic arguments are C-strings in pairs 1471 ** of (optionLabel, optionValue). If a given optionLabel is an empty 1472 ** string, the corresponding optionValue is used as its label. If any 1473 ** given value matches zSelectedVal, that option gets preselected. If 1474 ** no options match zSelectedVal then the first entry is selected by 1475 ** default. 1476 ** 1477 ** Any of (zWrapperId, zTooltip, zSelectedVal) may be NULL or empty. 1478 ** 1479 ** Example: 1480 ** 1481 ** style_select_list_str("my-grapes", "my_grapes", "Grapes", 1482 ** "Select the number of grapes", 1483 ** P("my_field"), 1484 ** "1", "One", "2", "Two", "", "3", 1485 ** NULL); 1486 */ 1487 void style_select_list_str(const char * zWrapperId, 1488 const char *zFieldName, const char * zLabel, 1489 const char * zToolTip, char const * zSelectedVal, 1490 ... ){ 1491 char * zLabelID = style_next_input_id(); 1492 va_list vargs; 1493 1494 va_start(vargs,zSelectedVal); 1495 if(!zSelectedVal){ 1496 zSelectedVal = __FILE__/*some string we'll never match*/; 1497 } 1498 CX("<span class='input-with-label'"); 1499 if(zToolTip && *zToolTip){ 1500 CX(" title='%h'",zToolTip); 1501 } 1502 if(zWrapperId && *zWrapperId){ 1503 CX(" id='%s'",zWrapperId); 1504 } 1505 CX(">"); 1506 if(zLabel && *zLabel){ 1507 CX("<label for='%s'>%h</label>", zLabelID, zLabel); 1508 } 1509 CX("<select name='%s' id='%s'>",zFieldName, zLabelID); 1510 while(1){ 1511 const char * zLabel = va_arg(vargs,char *); 1512 const char * zVal; 1513 if(NULL==zLabel){ 1514 break; 1515 } 1516 zVal = va_arg(vargs,char *); 1517 CX("<option value='%T'%s>", 1518 zVal, 0==fossil_strcmp(zVal, zSelectedVal) ? " selected" : ""); 1519 if(*zLabel){ 1520 CX("%s", zLabel); 1521 }else{ 1522 CX("%h",zVal); 1523 } 1524 CX("</option>\n"); 1525 } 1526 CX("</select>\n"); 1527 CX("</span>\n"); 1528 va_end(vargs); 1529 fossil_free(zLabelID); 1530 } 1531 1532 1533 /* 1534 ** The first time this is called, it emits code to install and 1535 ** bootstrap the window.fossil object, using the built-in file 1536 ** fossil.bootstrap.js (not to be confused with bootstrap.js). 1537 ** 1538 ** Subsequent calls are no-ops. 1539 ** 1540 ** If passed a true value, it emits the contents directly to the page 1541 ** output, else it emits a script tag with a src=builtin/... to load 1542 ** the script. It always outputs a small pre-bootstrap element in its 1543 ** own script tag to initialize parts which need C-runtime-level 1544 ** information, before loading the main fossil.bootstrap.js either 1545 ** inline or via a <script src=...>, as specified by the first 1546 ** argument. 1547 */ 1548 void style_emit_script_fossil_bootstrap(int asInline){ 1549 static int once = 0; 1550 if(0==once++){ 1551 /* Set up the generic/app-agnostic parts of window.fossil 1552 ** which require C-level state... */ 1553 style_emit_script_tag(0,0); 1554 CX("(function(){\n" 1555 "if(!window.fossil) window.fossil={};\n" 1556 "window.fossil.version = %!j;\n" 1557 /* fossil.rootPath is the top-most CGI/server path, 1558 ** including a trailing slash. */ 1559 "window.fossil.rootPath = %!j+'/';\n", 1560 get_version(), g.zTop); 1561 /* fossil.config = {...various config-level options...} */ 1562 CX("window.fossil.config = {" 1563 "hashDigits: %d, hashDigitsUrl: %d" 1564 "};\n", hash_digits(0), hash_digits(1)); 1565 #if 0 1566 /* Is it safe to emit the CSRF token here? Some pages add it 1567 ** as a hidden form field. */ 1568 if(g.zCsrfToken[0]!=0){ 1569 CX("window.fossil.csrfToken = %!j;\n", 1570 g.zCsrfToken); 1571 } 1572 #endif 1573 /* 1574 ** fossil.page holds info about the current page. This is also 1575 ** where the current page "should" store any of its own 1576 ** page-specific state, and it is reserved for that purpose. 1577 */ 1578 CX("window.fossil.page = {" 1579 "name:\"%T\"" 1580 "};\n", g.zPath); 1581 CX("})();\n"); 1582 /* The remaining fossil object bootstrap code is not dependent on 1583 ** C-runtime state... */ 1584 if(asInline){ 1585 CX("%s\n", builtin_text("fossil.bootstrap.js")); 1586 } 1587 style_emit_script_tag(1,0); 1588 if(asInline==0){ 1589 style_emit_script_builtin(0, "fossil.bootstrap.js"); 1590 } 1591 } 1592 } 1593 1594 /* 1595 ** If passed 0 as its first argument, it emits a script opener tag 1596 ** with this request's nonce. If passed non-0 it emits a script 1597 ** closing tag. Mnemonic for remembering the order in which to pass 0 1598 ** or 1 as the first argument to this function: 0 comes before 1. 1599 ** 1600 ** If passed 0 as its first argument and a non-NULL/non-empty zSrc, 1601 ** then it instead emits: 1602 ** 1603 ** <script src='%R/{{zSrc}}'></script> 1604 ** 1605 ** zSrc is always assumed to be a repository-relative path without 1606 ** a leading slash, and has %R/ prepended to it. 1607 ** 1608 ** Meaning that no follow-up call to pass a non-0 first argument 1609 ** to close the tag. zSrc is ignored if the first argument is not 1610 ** 0. 1611 */ 1612 void style_emit_script_tag(int isCloser, const char * zSrc){ 1613 if(0==isCloser){ 1614 if(zSrc!=0 && zSrc[0]!=0){ 1615 CX("<script src='%R/%T'></script>\n", zSrc); 1616 }else{ 1617 CX("<script nonce='%s'>", style_nonce()); 1618 } 1619 }else{ 1620 CX("</script>\n"); 1621 } 1622 } 1623 1624 /* 1625 ** Emits a script tag which uses content from a builtin script file. 1626 ** 1627 ** If asInline is true, it is emitted directly as an opening tag, the 1628 ** content of the zName builtin file, and a closing tag. 1629 ** 1630 ** If it is false, a script tag loading it via 1631 ** src=builtin/{{zName}}?cache=XYZ is emitted, where XYZ is a 1632 ** build-time-dependent cache-buster value. 1633 */ 1634 void style_emit_script_builtin(int asInline, char const * zName){ 1635 if(asInline){ 1636 style_emit_script_tag(0,0); 1637 CX("%s", builtin_text(zName)); 1638 style_emit_script_tag(1,0); 1639 }else{ 1640 char * zFullName = mprintf("builtin/%s",zName); 1641 const char * zHash = fossil_exe_id(); 1642 CX("<script src='%R/%T?cache=%.8s'></script>\n", 1643 zFullName, zHash); 1644 fossil_free(zFullName); 1645 } 1646 } 1647 1648 /* 1649 ** The first time this is called it emits the JS code from the 1650 ** built-in file fossil.fossil.js. Subsequent calls are no-ops. 1651 ** 1652 ** If passed a true value, it emits the contents directly 1653 ** to the page output, else it emits a script tag with a 1654 ** src=builtin/... to load the script. 1655 ** 1656 ** Note that this code relies on that loaded via 1657 ** style_emit_script_fossil_bootstrap() but it does not call that 1658 ** routine. 1659 */ 1660 void style_emit_script_fetch(int asInline){ 1661 static int once = 0; 1662 if(0==once++){ 1663 style_emit_script_builtin(asInline, "fossil.fetch.js"); 1664 } 1665 } 1666 1667 /* 1668 ** The first time this is called it emits the JS code from the 1669 ** built-in file fossil.dom.js. Subsequent calls are no-ops. 1670 ** 1671 ** If passed a true value, it emits the contents directly 1672 ** to the page output, else it emits a script tag with a 1673 ** src=builtin/... to load the script. 1674 ** 1675 ** Note that this code relies on that loaded via 1676 ** style_emit_script_fossil_bootstrap(), but it does not call that 1677 ** routine. 1678 */ 1679 void style_emit_script_dom(int asInline){ 1680 static int once = 0; 1681 if(0==once++){ 1682 style_emit_script_builtin(asInline, "fossil.dom.js"); 1683 } 1684 } 1685 1686 /* 1687 ** The first time this is called, it calls style_emit_script_dom(), 1688 ** passing it the given asInline value, and emits the JS code from the 1689 ** built-in file fossil.tabs.js. Subsequent calls are no-ops. 1690 ** 1691 ** If passed a true value, it emits the contents directly 1692 ** to the page output, else it emits a script tag with a 1693 ** src=builtin/... to load the script. 1694 */ 1695 void style_emit_script_tabs(int asInline){ 1696 static int once = 0; 1697 if(0==once++){ 1698 style_emit_script_dom(asInline); 1699 style_emit_script_builtin(asInline, "fossil.tabs.js"); 1700 } 1701 } 1702 1703 /* 1704 ** The first time this is called it emits the JS code from the 1705 ** built-in file fossil.confirmer.js. Subsequent calls are no-ops. 1706 ** 1707 ** If passed a true value, it emits the contents directly 1708 ** to the page output, else it emits a script tag with a 1709 ** src=builtin/... to load the script. 1710 */ 1711 void style_emit_script_confirmer(int asInline){ 1712 static int once = 0; 1713 if(0==once++){ 1714 style_emit_script_builtin(asInline, "fossil.confirmer.js"); 1715 } 1716 }