Fossil

Check-in Differences
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From:

[27c7a491] Ignore errors in the clean target commands. (user: ashepilko tags: trunk, date: 2018-09-19 19:55:38)

To:

[9718f3b0] Version 2.6 (user: drh tags: trunk, release, version-2.6, date: 2018-05-04 12:56:42)

Changes to Dockerfile.

     1      1   ###
     2      2   #   Dockerfile for Fossil
     3      3   ###
     4         -FROM fedora:28
            4  +FROM fedora:24
     5      5   
     6      6   ### Now install some additional parts we will need for the build
     7         -RUN dnf update -y && dnf install -y gcc make tcl tcl-devel zlib-devel openssl-devel tar && dnf clean all && groupadd -r fossil -g 433 && useradd -u 431 -r -g fossil -d /opt/fossil -s /sbin/nologin -c "Fossil user" fossil
            7  +RUN dnf update -y && dnf install -y gcc make zlib-devel openssl-devel tar && dnf clean all && groupadd -r fossil -g 433 && useradd -u 431 -r -g fossil -d /opt/fossil -s /sbin/nologin -c "Fossil user" fossil
     8      8   
     9      9   ### If you want to build "trunk", change the next line accordingly.
    10     10   ENV FOSSIL_INSTALL_VERSION release
    11     11   
    12         -RUN curl "https://www.fossil-scm.org/index.html/tarball/fossil-src.tar.gz?name=fossil-src&uuid=${FOSSIL_INSTALL_VERSION}" | tar zx
    13         -RUN cd fossil-src && ./configure --disable-fusefs --json --with-th1-docs --with-th1-hooks --with-tcl=1 --with-tcl-stubs --with-tcl-private-stubs
           12  +RUN curl "http://core.tcl.tk/tcl/tarball/tcl-src.tar.gz?name=tcl-src&uuid=release" | tar zx
           13  +RUN cd tcl-src/unix && ./configure --prefix=/usr --disable-load && make && make install
           14  +RUN curl "http://www.fossil-scm.org/index.html/tarball/fossil-src.tar.gz?name=fossil-src&uuid=${FOSSIL_INSTALL_VERSION}" | tar zx
           15  +RUN cd fossil-src && ./configure --disable-fusefs --json --with-th1-docs --with-th1-hooks --with-tcl --with-tcl-stubs --with-tcl-private-stubs
    14     16   RUN cd fossil-src/src && mv main.c main.c.orig && sed s/\"now\"/0/ <main.c.orig >main.c
    15     17   RUN cd fossil-src && make && strip fossil && cp fossil /usr/bin && cd .. && rm -rf fossil-src && chmod a+rx /usr/bin/fossil && mkdir -p /opt/fossil && chown fossil:fossil /opt/fossil
    16     18   
    17     19   ### Build is done, remove modules no longer needed
    18         -RUN dnf remove -y gcc make zlib-devel tcl-devel openssl-devel tar && dnf clean all
           20  +RUN dnf remove -y gcc make zlib-devel openssl-devel tar && dnf clean all
    19     21   
    20     22   USER fossil
    21     23   
    22     24   ENV HOME /opt/fossil
    23     25   
    24     26   EXPOSE 8080
    25     27   
    26     28   CMD ["/usr/bin/fossil", "server", "--create", "--user", "admin", "/opt/fossil/repository.fossil"]

Changes to Makefile.classic.

    17     17   
    18     18   #### C Compiler and options for use in building executables that
    19     19   #    will run on the platform that is doing the build.  This is used
    20     20   #    to compile code-generator programs as part of the build process.
    21     21   #    See TCC below for the C compiler for building the finished binary.
    22     22   #
    23     23   BCC = gcc
    24         -BCCFLAGS = $(CFLAGS)
    25     24   
    26     25   #### The suffix to add to final executable file.  When cross-compiling
    27     26   #    to windows, make this ".exe".  Otherwise leave it blank.
    28     27   #
    29     28   E =
    30     29   
    31     30   #### C Compile and options for use in building executables that
................................................................................
    41     40   # To use the included miniz library
    42     41   # FOSSIL_ENABLE_MINIZ = 1
    43     42   # TCC += -DFOSSIL_ENABLE_MINIZ
    44     43   
    45     44   # To add support for HTTPS
    46     45   TCC += -DFOSSIL_ENABLE_SSL
    47     46   
    48         -# To enable legacy mv/rm support
    49         -TCC += -DFOSSIL_ENABLE_LEGACY_MV_RM=1
    50         -
    51     47   #### We sometimes add the -static option here so that we can build a
    52     48   #    static executable that will run in a chroot jail.
    53     49   #LIB = -static
    54     50   TCC += -DFOSSIL_DYNAMIC_BUILD=1
    55     51   
    56         -TCCFLAGS = $(CFLAGS)
    57         -
    58     52   #### Extra arguments for linking the finished binary.  Fossil needs
    59     53   #    to link against the Z-Lib compression library unless the miniz
    60     54   #    library in the source tree is being used.  There are no other
    61     55   #    required dependencies.
    62     56   ZLIB_LIB.0 = -lz
    63     57   ZLIB_LIB.1 =
    64     58   ZLIB_LIB.  = $(ZLIB_LIB.0)

Changes to Makefile.in.

    32     32   E = @EXEEXT@
    33     33   
    34     34   TCC = @CC@
    35     35   
    36     36   #### Tcl shell for use in running the fossil testsuite.  If you do not
    37     37   #    care about testing the end result, this can be blank.
    38     38   #
    39         -TCLSH = @TCLSH@
           39  +TCLSH = tclsh
    40     40   
    41     41   CFLAGS = @CFLAGS@
    42     42   LIB =	@LDFLAGS@ @EXTRA_LDFLAGS@ @LIBS@
    43         -BCCFLAGS =	@CPPFLAGS@ $(CFLAGS)
    44         -TCCFLAGS =	@EXTRA_CFLAGS@ @CPPFLAGS@ $(CFLAGS) -DHAVE_AUTOCONFIG_H -D_HAVE_SQLITE_CONFIG_H
           43  +BCCFLAGS =	@CPPFLAGS@ @CFLAGS@
           44  +TCCFLAGS =	@EXTRA_CFLAGS@ @CPPFLAGS@ @CFLAGS@ -DHAVE_AUTOCONFIG_H -D_HAVE_SQLITE_CONFIG_H
    45     45   INSTALLDIR = $(DESTDIR)@prefix@/bin
    46     46   USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@
    47     47   USE_LINENOISE = @USE_LINENOISE@
    48     48   USE_MMAN_H = @USE_MMAN_H@
    49     49   USE_SEE = @USE_SEE@
    50     50   FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@
    51     51   
    52     52   include $(SRCDIR)/main.mk
    53     53   
    54     54   distclean: clean
    55         -	-rm -f autoconfig.h config.log Makefile
           55  +	rm -f autoconfig.h config.log Makefile
    56     56   
    57     57   reconfig:
    58     58   	@AUTOREMAKE@
    59     59   
    60     60   # Automatically reconfigure whenever an autosetup file or one of the
    61     61   # make source files change.
    62     62   #

Deleted Makefile.osx-jaguar.

     1         -#!/usr/bin/make
     2         -#
     3         -# This is a specially modified version of the Makefile that will build
     4         -# Fossil on Mac OSX Jaguar (10.2) circa 2002.  This Makefile is used for
     5         -# testing on an old PPC iBook.  The use of this old platform helps to verify
     6         -# Fossil and SQLite running on big-endian hardware.
     7         -#
     8         -# To build with this Makefile, run:
     9         -#
    10         -#     make -f Makefile.osx-jaguar
    11         -#
    12         -#
    13         -# This is the top-level makefile for Fossil when the build is occurring
    14         -# on a unix platform.  This works out-of-the-box on most unix platforms.
    15         -# But you are free to vary some of the definitions if desired.
    16         -#
    17         -#### The toplevel directory of the source tree.  Fossil can be built
    18         -#    in a directory that is separate from the source tree.  Just change
    19         -#    the following to point from the build directory to the src/ folder.
    20         -#
    21         -SRCDIR = ./src
    22         -
    23         -#### The directory into which object code files should be written.
    24         -#    Having a "./" prefix in the value of this variable breaks our use of the
    25         -#    "makeheaders" tool when running make on the MinGW platform, apparently
    26         -#    due to some command line argument manipulation performed automatically
    27         -#    by the shell.
    28         -#
    29         -#
    30         -OBJDIR = bld
    31         -
    32         -#### C Compiler and options for use in building executables that
    33         -#    will run on the platform that is doing the build.  This is used
    34         -#    to compile code-generator programs as part of the build process.
    35         -#    See TCC below for the C compiler for building the finished binary.
    36         -#
    37         -BCC = cc
    38         -BCCFLAGS = $(CFLAGS)
    39         -
    40         -#### The suffix to add to final executable file.  When cross-compiling
    41         -#    to windows, make this ".exe".  Otherwise leave it blank.
    42         -#
    43         -E = 
    44         -
    45         -TCC = cc
    46         -TCCFLAGS = $(CFLAGS)
    47         -
    48         -#### Tcl shell for use in running the fossil testsuite.  If you do not
    49         -#    care about testing the end result, this can be blank.
    50         -#
    51         -TCLSH = tclsh
    52         -
    53         -# LIB =	  -lz
    54         -LIB = compat/zlib/libz.a
    55         -TCC +=	  -g -O0 -DHAVE_AUTOCONFIG_H
    56         -TCC += -Icompat/zlib
    57         -TCC += -DSQLITE_WITHOUT_ZONEMALLOC
    58         -TCC += -D_BSD_SOURCE=1
    59         -TCC += -DWITHOUT_ICONV
    60         -TCC += -Dsocklen_t=int
    61         -TCC += -DSQLITE_MAX_MMAP_SIZE=0
    62         -TCC += -DFOSSIL_ENABLE_LEGACY_MV_RM=1
    63         -INSTALLDIR = $(DESTDIR)/usr/local/bin
    64         -USE_SYSTEM_SQLITE = 
    65         -USE_LINENOISE = 1
    66         -# FOSSIL_ENABLE_TCL = @FOSSIL_ENABLE_TCL@
    67         -FOSSIL_ENABLE_TCL = 0
    68         -FOSSIL_ENABLE_MINIZ = 0
    69         -
    70         -include $(SRCDIR)/main.mk
    71         -
    72         -distclean: clean
    73         -	rm -f autoconfig.h config.log Makefile

Changes to VERSION.

     1         -2.7
            1  +2.6

Changes to auto.def.

     6      6       with-openssl:path|auto|tree|none
     7      7                            => {Look for OpenSSL in the given path, automatically, in the source tree, or none}
     8      8       with-miniz=0         => {Use miniz from the source tree}
     9      9       with-zlib:path|auto|tree
    10     10                            => {Look for zlib in the given path, automatically, or in the source tree}
    11     11       with-exec-rel-paths=0
    12     12                            => {Enable relative paths for external diff/gdiff}
    13         -    with-legacy-mv-rm=1  => {Enable legacy behavior for mv/rm (skip checkout files)}
           13  +    with-legacy-mv-rm=0  => {Enable legacy behavior for mv/rm (skip checkout files)}
    14     14       with-th1-docs=0      => {Enable TH1 for embedded documentation pages}
    15     15       with-th1-hooks=0     => {Enable TH1 hooks for commands and web pages}
    16     16       with-tcl:path        => {Enable Tcl integration, with Tcl in the specified path}
    17     17       with-tcl-stubs=0     => {Enable Tcl integration via stubs library mechanism}
    18     18       with-tcl-private-stubs=0
    19     19                            => {Enable Tcl integration via private stubs mechanism}
    20     20       with-mman=0          => {Enable use of POSIX memory APIs from "sys/mman.h"}
................................................................................
    31     31   cc-with {-includes {stdint.h inttypes.h}} {
    32     32       cc-check-types uint32_t uint16_t int16_t uint8_t
    33     33   }
    34     34   
    35     35   # Use pread/pwrite system calls in place of seek + read/write if possible
    36     36   define USE_PREAD [cc-check-functions pread]
    37     37   
    38         -# Find tclsh for the test suite.
    39         -#
    40         -# We can't use jimsh for this: the test suite uses features of Tcl that
    41         -# Jim doesn't support, either statically or due to the way it's built by
    42         -# autosetup.  For example, Jim supports `file normalize`, but only if
    43         -# you build it with HAVE_REALPATH, which won't ever be defined in this
    44         -# context because autosetup doesn't try to discover platform-specific
    45         -# details like that before it decides to build jimsh0.  Besides which,
    46         -# autosetup won't build jimsh0 at all if it can find tclsh itself.
    47         -# Ironically, this means we may right now be running under either jimsh0
    48         -# or a version of tclsh that we find unsuitable below!
           38  +# Find tclsh for the test suite. Can't yet use jimsh for this.
    49     39   cc-check-progs tclsh
    50         -set hbtd /usr/local/Cellar/tcl-tk
    51         -if {[string equal false [get-define TCLSH]]} {
    52         -    msg-result "WARNING: 'make test' will not run here."
    53         -} else {
    54         -    set v [exec /bin/sh -c "echo 'puts \$tcl_version' | tclsh"]
    55         -    if {[expr $v >= 8.6]} {
    56         -        msg-result "Found Tclsh version $v in the PATH."
    57         -        define TCLSH tclsh
    58         -    } elseif {[file isdirectory $hbtd]} {
    59         -        # This is a macOS system with the Homebrew version of Tcl/Tk
    60         -        # installed.  Select the newest version.  It won't normally be
    61         -        # in the PATH to avoid shadowing /usr/bin/tclsh, and even if it
    62         -        # were in the PATH, it's bad practice to put /usr/local/bin (the
    63         -        # Homebrew default) ahead of /usr/bin, especially given that
    64         -        # it's user-writeable by default with Homebrew.  Thus, we can be
    65         -        # pretty sure the only way to call it is with an absolute path.
    66         -        set v [exec ls -tr $hbtd | tail -1]
    67         -        set path "$hbtd/$v/bin/tclsh"
    68         -        define TCLSH $path
    69         -        msg-result "Using Homebrew Tcl/Tk version $path."
    70         -    } else {
    71         -        msg-result "WARNING: tclsh $v found; need >= 8.6 for 'make test'."
    72         -        define TCLSH false     ;# force "make test" failure via /usr/bin/false
    73         -    }
    74         -}
    75     40   
    76     41   define EXTRA_CFLAGS "-Wall"
    77     42   define EXTRA_LDFLAGS ""
    78     43   define USE_SYSTEM_SQLITE 0
    79     44   define USE_LINENOISE 0
    80     45   define FOSSIL_ENABLE_MINIZ 0
    81     46   define USE_MMAN_H 0
................................................................................
   124     89       # search for the system SQLite once with -ldl, and once without. If
   125     90       # the library can only be found with $extralibs set to -ldl, then
   126     91       # the code below will append -ldl to LIBS.
   127     92       #
   128     93       foreach extralibs {{} {-ldl}} {
   129     94   
   130     95         # Locate the system SQLite by searching for sqlite3_open(). Then check
   131         -      # if sqlite3_create_window_function can be found as well. If we can find open() but
   132         -      # not create_window_function(), then the system SQLite is too old to link against
           96  +      # if sqlite3_keyword_check() can be found as well. If we can find open() but
           97  +      # not keyword_check(), then the system SQLite is too old to link against
   133     98         # fossil.
   134     99         #
   135    100         if {[check-function-in-lib sqlite3_open sqlite3 $extralibs]} {
   136         -        if {![check-function-in-lib sqlite3_create_window_function sqlite3 $extralibs]} {
   137         -          user-error "system sqlite3 too old (require >= 3.25.0)"
          101  +        if {![check-function-in-lib sqlite3_keyword_check sqlite3 $extralibs]} {
          102  +          user-error "system sqlite3 too old (require >= 3.24.0)"
   138    103           }
   139    104   
   140    105           # Success. Update symbols and return.
   141    106           #
   142    107           define USE_SYSTEM_SQLITE 1
   143    108           define-append LIBS -lsqlite3
   144    109           define-append LIBS $extralibs
................................................................................
   167    132   }
   168    133   
   169    134   if {[string match *-solaris* [get-define host]]} {
   170    135       define-append EXTRA_CFLAGS {-D_XOPEN_SOURCE=500 -D__EXTENSIONS__}
   171    136   }
   172    137   
   173    138   if {[opt-bool fossil-debug]} {
          139  +    define-append EXTRA_CFLAGS -DFOSSIL_DEBUG
   174    140       define CFLAGS {-g -O0 -Wall}
   175         -    define-append CFLAGS -DFOSSIL_DEBUG
   176    141       msg-result "Debugging support enabled"
   177    142   }
   178    143   
   179    144   if {[opt-bool no-opt]} {
   180    145       define CFLAGS {-g -O0 -Wall}
   181    146       msg-result "Builting without compiler optimization"
   182    147   }
................................................................................
   200    165       # reading config.h first.
   201    166       define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_JSON
   202    167       define FOSSIL_ENABLE_JSON
   203    168       msg-result "JSON support enabled"
   204    169   }
   205    170   
   206    171   if {[opt-bool with-legacy-mv-rm]} {
   207         -    define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_LEGACY_MV_RM=1
          172  +    define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_LEGACY_MV_RM
   208    173       define FOSSIL_ENABLE_LEGACY_MV_RM
   209    174       msg-result "Legacy mv/rm support enabled"
   210    175   }
   211    176   
   212    177   if {[opt-bool with-exec-rel-paths]} {
   213    178       define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_EXEC_REL_PATHS
   214    179       define FOSSIL_ENABLE_EXEC_REL_PATHS
................................................................................
   511    476   cc-check-function-in-lib gethostbyname nsl
   512    477   if {![cc-check-function-in-lib socket {socket network}]} {
   513    478       # Last resort, may be Windows
   514    479       if {[is_mingw]} {
   515    480           define-append LIBS -lwsock32
   516    481       }
   517    482   }
   518         -cc-check-function-in-lib ns_name_uncompress resolv
   519    483   cc-check-functions utime
   520    484   cc-check-functions usleep
   521    485   cc-check-functions strchrnul
   522    486   cc-check-functions pledge
   523         -cc-check-functions backtrace
   524    487   
   525    488   # Check for getloadavg(), and if it doesn't exist, define FOSSIL_OMIT_LOAD_AVERAGE
   526    489   if {![cc-check-functions getloadavg]} {
   527    490     define FOSSIL_OMIT_LOAD_AVERAGE 1
   528    491     msg-result "Load average support unavailable"
   529    492   }
   530    493   
   531    494   # Check for getpassphrase() for Solaris 10 where getpass() truncates to 10 chars
   532    495   if {![cc-check-functions getpassphrase]} {
   533    496       # Haiku needs this
   534    497       cc-check-function-in-lib getpass bsd
   535    498   }
          499  +cc-check-function-in-lib dlopen dl
   536    500   cc-check-function-in-lib sin m
   537    501   
   538    502   # Check for the FuseFS library
   539    503   if {[opt-bool fusefs]} {
   540    504     if {[cc-check-function-in-lib fuse_mount fuse]} {
   541    505        define-append EXTRA_CFLAGS -DFOSSIL_HAVE_FUSEFS
   542    506        define FOSSIL_HAVE_FUSEFS 1
   543    507        define-append LIBS -lfuse
   544    508        msg-result "FuseFS support enabled"
   545    509     }
   546    510   }
   547    511   
   548         -# Finally, append -ldl to make sure it's the last in the list.
   549         -# The library order matters in case of static linking.
   550         -if {[check-function-in-lib dlopen dl]} {
   551         -    # Some platforms (*BSD) have the dl functions already in libc and no libdl.
   552         -    # In such case we can link directly without -ldl.
   553         -    define-append LIBS [get-define lib_dlopen]
   554         -}
   555         -
   556    512   make-template Makefile.in
   557    513   make-config-header autoconfig.h -auto {USE_* FOSSIL_*}

Changes to skins/ardoise/header.txt.

    40     40   if {[hascap oh]} {
    41     41     menulink /dir?ci=tip Files
    42     42   }
    43     43   if {[hascap o]} {
    44     44     menulink  /brlist Branches
    45     45     menulink  /taglist Tags
    46     46   }
    47         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    48         -  menulink /forum Forum
    49         -}
    50     47   if {[hascap r]} {
    51     48     menulink /ticket Tickets
    52     49   }
    53     50   if {[hascap j]} {
    54     51     menulink /wiki Wiki
    55     52   }
    56     53   if {[hascap o]} {

Changes to skins/black_and_white/header.txt.

    21     21   if {[anoncap oh]} {
    22     22     html "<a href='$home/tree?ci=tip'>Files</a>\n"
    23     23   }
    24     24   if {[anoncap o]} {
    25     25     html "<a href='$home/brlist'>Branches</a>\n"
    26     26     html "<a href='$home/taglist'>Tags</a>\n"
    27     27   }
    28         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    29         -  html "<a href='$home/forum'>Forum</a>\n"
    30         -}
    31     28   if {[anoncap r]} {
    32     29     html "<a href='$home/ticket'>Tickets</a>\n"
    33     30   }
    34     31   if {[anoncap j]} {
    35     32     html "<a href='$home/wiki'>Wiki</a>\n"
    36     33   }
    37     34   if {[hascap s]} {

Changes to skins/blitz/header.txt.

    44     44   if {[hascap oh]} {
    45     45     menulink /dir?ci=tip Files
    46     46   }
    47     47   if {[hascap o]} {
    48     48     menulink  /brlist Branches
    49     49     menulink  /taglist Tags
    50     50   }
    51         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    52         -  menulink /forum Forum
    53         -}
    54     51   if {[hascap r]} {
    55     52     menulink /ticket Tickets
    56     53   }
    57     54   if {[hascap j]} {
    58     55     menulink /wiki Wiki
    59     56   }
    60     57   if {[hascap o]} {

Changes to skins/blitz_no_logo/header.txt.

    41     41   if {[hascap oh]} {
    42     42     menulink /dir?ci=tip Files
    43     43   }
    44     44   if {[hascap o]} {
    45     45     menulink  /brlist Branches
    46     46     menulink  /taglist Tags
    47     47   }
    48         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    49         -  menulink /forum Forum
    50         -}
    51     48   if {[hascap r]} {
    52     49     menulink /ticket Tickets
    53     50   }
    54     51   if {[hascap j]} {
    55     52     menulink /wiki Wiki
    56     53   }
    57     54   if {[hascap o]} {

Changes to skins/bootstrap/header.txt.

    76     76                     }
    77     77                     if {[string compare $current_page "taglist"] == 0} {
    78     78                       html "<li class='active'><a href='$home/taglist'>Tags</a></li>\n"
    79     79                     } else {
    80     80                       html "<li><a href='$home/taglist'>Tags</a></li>\n"
    81     81                     }
    82     82                   }
    83         -                if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    84         -                  if {[string compare $current_page "forum"] == 0} {
    85         -                    html "<li class='active'><a href='$home/forum'>Forum</a></li>\n"
    86         -                  } else {
    87         -                    html "<li><a href='$home/forum'>Forum</a></li>\n"
    88         -                  }
    89         -                }
    90     83                   if {[hascap r]} {
    91     84                     if {[string compare $current_page "reportlist"] == 0} {
    92     85                       html "<li class='active'><a href='$home/reportlist'>Tickets</a></li>\n"
    93     86                     } else {
    94     87                       html "<li><a href='$home/reportlist'>Tickets</a></li>\n"
    95     88                     }
    96     89                   }

Changes to skins/default/css.txt.

     1      1   body {
     2      2       margin: 0 auto;
            3  +    min-width: 800px;
            4  +    padding: 0px 20px;
     3      5       background-color: white;
     4      6       font-family: sans-serif;
     5      7       font-size:14pt;
     6      8       -moz-text-size-adjust: none;
     7      9       -webkit-text-size-adjust: none;
     8     10       -mx-text-size-adjust: none;
     9     11   }
................................................................................
    12     14       color: #4183C4;
    13     15       text-decoration: none;
    14     16   }
    15     17   a:hover {
    16     18       color: #4183C4;
    17     19       text-decoration: underline;
    18     20   }
    19         -div.forumPosts a:visited {
    20         -    color: #6A7F94;
    21         -}
    22     21   
    23     22   hr {
    24     23       color: #eee;
    25     24   }
    26     25   
    27     26   .title {
    28     27       color: #4183C4;
    29     28       float:left;
           29  +    padding-top: 30px;
           30  +    padding-bottom: 10px;
    30     31   }
    31     32   .title h1 {
    32     33       display:inline;
    33     34   }
    34     35   .title h1:after {
    35     36       content: " / ";
    36     37       color: #777;
................................................................................
    70     71       display: inline-block;
    71     72       margin-right: 1em;
    72     73   }
    73     74   
    74     75   .status {
    75     76       float:right;
    76     77       font-size:.7em;
           78  +    padding-top:50px;
    77     79   }
    78     80   
    79     81   .mainmenu {
    80     82       font-size:.8em;
    81     83       clear:both;
           84  +    padding:10px;
    82     85       background:#eaeaea linear-gradient(#fafafa, #eaeaea) repeat-x;
    83     86       border:1px solid #eaeaea;
    84     87       border-radius:5px;
    85         -    overflow-x: auto;
    86         -    overflow-y: hidden;
    87         -    white-space: nowrap;
    88         -    z-index: 21;  /* just above hbdrop */
    89     88   }
    90     89   
    91     90   .mainmenu a {
           91  +    padding: 10px 20px;
    92     92       text-decoration:none;
    93     93       color: #777;
    94     94       border-right:1px solid #eaeaea;
    95     95   }
    96     96   .mainmenu a.active,
    97     97   .mainmenu a:hover {
    98     98       color: #000;
    99     99       border-bottom:2px solid #D26911;
   100    100   }
   101    101   
   102         -div#hbdrop {
   103         -    background-color: white;
   104         -    border: 1px solid black;
   105         -    border-top: white;
   106         -    border-radius: 0 0 0.5em 0.5em;
   107         -    display: none;
   108         -    font-size: 80%;
   109         -    left: 2em;
   110         -    width: 90%;
   111         -    padding-right: 1em;
   112         -    position: absolute;
   113         -    z-index: 20;  /* just below mainmenu, but above timeline bubbles */
   114         -}
   115         -
   116    102   .submenu {
   117    103       font-size: .7em;
          104  +    margin-top: 10px;
   118    105       padding: 10px;
   119    106       border-bottom: 1px solid #ccc;
   120    107   }
   121    108   
   122    109   .submenu a, .submenu label {
   123    110       padding: 10px 11px;
   124    111       text-decoration:none;
................................................................................
   141    128   .udiff, .sbsdiff {
   142    129       font-size: .85em !important;
   143    130       overflow: auto;
   144    131       border: 1px solid #ccc;
   145    132       border-radius: 5px;
   146    133   }
   147    134   .content blockquote {
   148         -    background-color: rgba(65, 131, 196, 0.1);
   149         -    border-left: 3px solid #254769;
   150         -    padding: 0.1em;
   151         -    padding-left: 1em;
          135  +    padding: 0 15px;
   152    136   }
   153    137   
   154    138   table.report {
   155    139       cursor: auto;
   156    140       border-radius: 5px;
   157    141       border: 1px solid #ccc;
   158    142       margin: 1em 0;
................................................................................
   214    198   div.timelineDate {
   215    199       font-weight: bold;
   216    200       white-space: nowrap;
   217    201   }
   218    202   span.submenuctrl, span.submenuctrl input, select.submenuctrl {
   219    203     color: #777;
   220    204   }
   221         -span.submenuctrl {
   222         -  white-space: nowrap;
   223         -}
   224         -div.submenu label {
   225         -  white-space: nowrap;
   226         -}
   227         -
   228         -@media screen and (max-width: 600px) {
   229         -  /* Spacing for mobile */
   230         -  body {
   231         -    padding-left: 4px;
   232         -    padding-right: 4px;
   233         -  }
   234         -  .title {
   235         -    padding-top: 0px;
   236         -    padding-bottom: 0px;
   237         -  }
   238         -  .status {padding-top: 0px;}
   239         -  .mainmenu a {
   240         -    padding: 10px 10px;
   241         -  }
   242         -  .mainmenu {
   243         -    padding: 10px;
   244         -  }
   245         -  .desktoponly {
   246         -    display: none;
   247         -  }
   248         -}
   249         -@media screen and (min-width: 600px) {
   250         -  /* Spacing for desktop */
   251         -  body {
   252         -    padding-left: 20px;
   253         -    padding-right: 20px;
   254         -  }
   255         -  .title {
   256         -    padding-top: 10px;
   257         -    padding-bottom: 10px;
   258         -  }
   259         -  .status {padding-top: 30px;}
   260         -  .mainmenu a {
   261         -    padding: 10px 20px;
   262         -  }
   263         -  .mainmenu {
   264         -    padding: 10px;
   265         -  }
   266         -}
   267         -@media screen and (max-width: 1200px) {
   268         -  /* Special declarations for narrow desktop or wide mobile */
   269         -  .wideonly {
   270         -    display: none;
   271         -  }
   272         -}

Changes to skins/default/footer.txt.

     1      1   <div class="footer">
     2      2   This page was generated in about
     3      3   <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
     4      4   Fossil $release_version $manifest_version $manifest_date
     5      5   </div>
     6         -<script nonce="$nonce">
     7         -<th1>styleScript</th1>
     8         -</script>

Changes to skins/default/header.txt.

     6      6    } else {
     7      7      html "<a href='$home/login'>Login</a>\n"
     8      8    }
     9      9       </th1></div>
    10     10   </div>
    11     11   <div class="mainmenu">
    12     12   <th1>
    13         -proc menulink {url name cls} {
           13  +proc menulink {url name} {
    14     14     upvar current_page current
    15     15     upvar home home
    16     16     if {[string range $url 0 [string length $current]] eq "/$current"} {
    17         -    html "<a href='$home$url' class='active $cls'>$name</a>\n"
           17  +    html "<a href='$home$url' class='active'>$name</a>\n"
    18     18     } else {
    19         -    html "<a href='$home$url' class='$cls'>$name</a>\n"
           19  +    html "<a href='$home$url'>$name</a>\n"
    20     20     }
    21     21   }
    22         -html "<a href='#'>&#9776;</a>"
    23         -menulink $index_page Home {}
           22  +menulink $index_page Home
    24     23   if {[anycap jor]} {
    25         -  menulink /timeline Timeline {}
           24  +  menulink /timeline Timeline
    26     25   }
    27     26   if {[hascap oh]} {
    28         -  menulink /dir?ci=tip Files desktoponly
           27  +  menulink /dir?ci=tip Files
    29     28   }
    30     29   if {[hascap o]} {
    31         -  menulink  /brlist Branches desktoponly
    32         -  menulink  /taglist Tags wideonly
    33         -}
    34         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    35         -  menulink /forum Forum wideonly
           30  +  menulink  /brlist Branches
           31  +  menulink  /taglist Tags
    36     32   }
    37     33   if {[hascap r]} {
    38         -  menulink /ticket Tickets wideonly
           34  +  menulink /ticket Tickets
    39     35   }
    40     36   if {[hascap j]} {
    41         -  menulink /wiki Wiki wideonly
           37  +  menulink /wiki Wiki
    42     38   }
    43     39   if {[hascap s]} {
    44         -  menulink /setup Admin {}
           40  +  menulink /setup Admin
    45     41   } elseif {[hascap a]} {
    46         -  menulink /setup_ulist Users {}
           42  +  menulink /setup_ulist Users
    47     43   }
    48     44   </th1></div>
    49         -<div id='hbdrop'></div>

Deleted skins/default/js.txt.

     1         -/*
     2         -** Copyright © 2018 Warren Young
     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         -** Contact: wyoung on the Fossil forum, https://fossil-scm.org/forum/
    13         -**
    14         -*******************************************************************************
    15         -**
    16         -** This file contains the JS code specific to the Fossil default skin.
    17         -** Currently, the only thing this does is handle clicks on its hamburger
    18         -** menu button.
    19         -*/
    20         -(function() {
    21         -  var panel = document.getElementById("hbdrop");
    22         -  if (!panel) return;   // site admin might've nuked it
    23         -  var panelBorder = panel.style.border;
    24         -  var animate = panel.style.hasOwnProperty('transition');
    25         -  var animMS = 400;
    26         -
    27         -  // Calculate panel height despite its being hidden at call time.
    28         -  // Based on https://stackoverflow.com/a/29047447/142454
    29         -  var panelHeight;  // computed on sitemap load
    30         -  function calculatePanelHeight() {
    31         -    // Get initial panel styles so we can restore them below.
    32         -    var es   = window.getComputedStyle(panel),
    33         -        edis = es.display,
    34         -        epos = es.position,
    35         -        evis = es.visibility;
    36         -
    37         -    // Restyle the panel so we can measure its height while invisible.
    38         -    panel.style.visibility = 'hidden';
    39         -    panel.style.position   = 'absolute';
    40         -    panel.style.display    = 'block';
    41         -    panelHeight = panel.offsetHeight + 'px';
    42         -
    43         -    // Revert styles now that job is done.
    44         -    panel.style.display    = edis;
    45         -    panel.style.position   = epos;
    46         -    panel.style.visibility = evis;
    47         -  }
    48         -
    49         -  // Show the panel by changing the panel height, which kicks off the
    50         -  // slide-open/closed transition set up in the XHR onload handler.
    51         -  //
    52         -  // Schedule the change for a near-future time in case this is the
    53         -  // first call, where the div was initially invisible.  If we were
    54         -  // to change the panel's visibility and height at the same time
    55         -  // instead, that would prevent the browser from seeing the height
    56         -  // change as a state transition, so it'd skip the CSS transition:
    57         -  //
    58         -  // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples
    59         -  function showPanel() {
    60         -    if (animate) {
    61         -      setTimeout(function() {
    62         -        panel.style.maxHeight = panelHeight;
    63         -        panel.style.border    = panelBorder;
    64         -      }, 40);   // 25ms is insufficient with Firefox 62
    65         -    }
    66         -    else {
    67         -      panel.style.display = 'block';
    68         -    }
    69         -  }
    70         -
    71         -  // Return true if the panel is showing.
    72         -  function panelShowing() {
    73         -    if (animate) {
    74         -      return panel.style.maxHeight == panelHeight;
    75         -    }
    76         -    else {
    77         -      return panel.style.display == 'block';
    78         -    }
    79         -  }
    80         -
    81         -  // Click handler for the hamburger button.
    82         -  var needSitemapHTML = true;
    83         -  document.querySelector("div.mainmenu > a").onclick = function() {
    84         -    if (panelShowing()) {
    85         -      // Transition back to hidden state.
    86         -      if (animate) {
    87         -        panel.style.maxHeight = '0';
    88         -        setTimeout(function() {
    89         -          // Browsers show a 1px high border line when maxHeight == 0,
    90         -          // our "hidden" state, so hide the borders in that state, too.
    91         -          panel.style.border = 'none';
    92         -        }, animMS);
    93         -      }
    94         -      else {
    95         -        panel.style.display = 'none';
    96         -      }
    97         -    }
    98         -    else if (needSitemapHTML) {
    99         -      // Only get it once per page load: it isn't likely to
   100         -      // change on us.
   101         -      var xhr = new XMLHttpRequest();
   102         -      xhr.onload = function() {
   103         -        var doc = xhr.responseXML;
   104         -        if (doc) {
   105         -          var sm = doc.querySelector("ul#sitemap");
   106         -          if (sm && xhr.status == 200) {
   107         -            // Got sitemap.  Insert it into the drop-down panel.
   108         -            needSitemapHTML = false;
   109         -            panel.innerHTML = sm.outerHTML;
   110         -
   111         -            // Display the panel
   112         -            if (animate) {
   113         -              // Set up a CSS transition to animate the panel open and
   114         -              // closed.  Only needs to be done once per page load.
   115         -              // Based on https://stackoverflow.com/a/29047447/142454
   116         -              calculatePanelHeight();
   117         -              panel.style.transition = 'max-height ' + animMS +
   118         -                  'ms ease-in-out';
   119         -              panel.style.overflowY  = 'hidden';
   120         -              panel.style.maxHeight  = '0';
   121         -              showPanel();
   122         -            }
   123         -            panel.style.display = 'block';
   124         -          }
   125         -        }
   126         -        // else, can't parse response as HTML or XML
   127         -      }
   128         -      xhr.open("POST", "$home/sitemap");   // note the TH1 substitution!
   129         -      xhr.responseType = "document";
   130         -      xhr.send("popup=1");
   131         -    }
   132         -    else {
   133         -      showPanel();   // just show what we built above
   134         -    }
   135         -    return false;  // prevent browser from acting on <a> click
   136         -  }
   137         -})();

Changes to skins/eagle/header.txt.

    69     69        if {[info exists login]} {
    70     70          puts "Logged in as $login"
    71     71        } else {
    72     72          puts "Not logged in"
    73     73        }
    74     74     </th1></nobr><small><div id="clock"></div></small></div>
    75     75   </div>
    76         -<th1>html "<script nonce='$nonce'>"</th1>
           76  +<script>
    77     77   function updateClock(){
    78     78     var e = document.getElementById("clock");
    79     79     if(e){
    80     80       var d = new Date();
    81     81       function f(n) {
    82     82         return n < 10 ? '0' + n : n;
    83     83       }
    84     84       e.innerHTML = d.getUTCFullYear()+ '-' +
    85     85         f(d.getUTCMonth() + 1) + '-' +
    86     86         f(d.getUTCDate())      + ' ' +
    87     87         f(d.getUTCHours())     + ':' +
    88     88         f(d.getUTCMinutes());
    89         -    setTimeout(updateClock,(60-d.getUTCSeconds())*1000);
           89  +    setTimeout("updateClock();",(60-d.getUTCSeconds())*1000);
    90     90     }
    91     91   }
    92     92   updateClock();
    93     93   </script>
    94     94   <div class="mainmenu">
    95     95   <th1>
    96     96   proc menulink {url name} {
................................................................................
   105    105   if {[anoncap oh]} {
   106    106     menulink /dir?ci=tip Files
   107    107   }
   108    108   if {[anoncap o]} {
   109    109     menulink /brlist Branches
   110    110     menulink /taglist Tags
   111    111   }
   112         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
   113         -  menulink /forum Forum
   114         -}
   115    112   if {[anoncap r]} {
   116    113     menulink /ticket Tickets
   117    114   }
   118    115   if {[anoncap j]} {
   119    116     menulink /wiki Wiki
   120    117   }
   121    118   menulink /sitemap More...

Changes to skins/enhanced1/header.txt.

    69     69        if {[info exists login]} {
    70     70          puts "Logged in as $login"
    71     71        } else {
    72     72          puts "Not logged in"
    73     73        }
    74     74     </th1></nobr><small><div id="clock"></div></small></div>
    75     75   </div>
    76         -<th1>html "<script nonce='$nonce'>"</th1>
           76  +<script>
    77     77   function updateClock(){
    78     78     var e = document.getElementById("clock");
    79     79     if(e){
    80     80       var d = new Date();
    81     81       function f(n) {
    82     82         return n < 10 ? '0' + n : n;
    83     83       }
    84     84       e.innerHTML = d.getUTCFullYear()+ '-' +
    85     85         f(d.getUTCMonth() + 1) + '-' +
    86     86         f(d.getUTCDate())      + ' ' +
    87     87         f(d.getUTCHours())     + ':' +
    88     88         f(d.getUTCMinutes());
    89         -    setTimeout(updateClock,(60-d.getUTCSeconds())*1000);
           89  +    setTimeout("updateClock();",(60-d.getUTCSeconds())*1000);
    90     90     }
    91     91   }
    92     92   updateClock();
    93     93   </script>
    94     94   <div class="mainmenu">
    95     95   <th1>
    96     96   proc menulink {url name} {
................................................................................
   105    105   if {[anoncap oh]} {
   106    106     menulink /dir?ci=tip Files
   107    107   }
   108    108   if {[anoncap o]} {
   109    109     menulink /brlist Branches
   110    110     menulink /taglist Tags
   111    111   }
   112         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
   113         -  menulink /forum Forum
   114         -}
   115    112   if {[anoncap r]} {
   116    113     menulink /ticket Tickets
   117    114   }
   118    115   if {[anoncap j]} {
   119    116     menulink /wiki Wiki
   120    117   }
   121    118   if {[hascap s]} {

Changes to skins/khaki/header.txt.

    19     19   if {[anoncap oh]} {
    20     20     html "<a href='$home/tree?ci=tip'>Files</a>\n"
    21     21   }
    22     22   if {[anoncap o]} {
    23     23     html "<a href='$home/brlist'>Branches</a>\n"
    24     24     html "<a href='$home/taglist'>Tags</a>\n"
    25     25   }
    26         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    27         -  html "<a href='$home/forum'>Forum</a>\n"
    28         -}
    29     26   if {[anoncap r]} {
    30     27     html "<a href='$home/ticket'>Tickets</a>\n"
    31     28   }
    32     29   if {[anoncap j]} {
    33     30     html "<a href='$home/wiki'>Wiki</a>\n"
    34     31   }
    35     32   if {[hascap s]} {

Changes to skins/original/header.txt.

    20     20   if {[anoncap oh]} {
    21     21     html "<a href='$home/tree?ci=tip'>Files</a>\n"
    22     22   }
    23     23   if {[anoncap o]} {
    24     24     html "<a href='$home/brlist'>Branches</a>\n"
    25     25     html "<a href='$home/taglist'>Tags</a>\n"
    26     26   }
    27         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    28         -  html "<a href='$home/forum'>Forum</a>\n"
    29         -}
    30     27   if {[anoncap r]} {
    31     28     html "<a href='$home/ticket'>Tickets</a>\n"
    32     29   }
    33     30   if {[anoncap j]} {
    34     31     html "<a href='$home/wiki'>Wiki</a>\n"
    35     32   }
    36     33   if {[hascap s]} {

Changes to skins/plain_gray/details.txt.

     1      1   timeline-arrowheads:        1
     2         -timeline-circle-nodes:      1
            2  +timeline-circle-nodes:      0
     3      3   timeline-color-graph-lines: 0
     4      4   white-foreground:           0

Changes to skins/plain_gray/header.txt.

    17     17   if {[anoncap oh]} {
    18     18     html "<a href='$home/tree?ci=tip'>Files</a>\n"
    19     19   }
    20     20   if {[anoncap o]} {
    21     21     html "<a href='$home/brlist'>Branches</a>\n"
    22     22     html "<a href='$home/taglist'>Tags</a>\n"
    23     23   }
    24         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    25         -  html "<a href='$home/forum'>Forum</a>\n"
    26         -}
    27     24   if {[anoncap r]} {
    28     25     html "<a href='$home/ticket'>Tickets</a>\n"
    29     26   }
    30     27   if {[anoncap j]} {
    31     28     html "<a href='$home/wiki'>Wiki</a>\n"
    32     29   }
    33     30   if {[hascap s]} {

Changes to skins/rounded1/header.txt.

    21     21   if {[anoncap oh]} {
    22     22     html "<a href='$home/tree?ci=tip'>Files</a>\n"
    23     23   }
    24     24   if {[anoncap o]} {
    25     25     html "<a href='$home/brlist'>Branches</a>\n"
    26     26     html "<a href='$home/taglist'>Tags</a>\n"
    27     27   }
    28         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
    29         -  html "<a href='$home/forum'>Forum</a>\n"
    30         -}
    31     28   if {[anoncap r]} {
    32     29     html "<a href='$home/ticket'>Tickets</a>\n"
    33     30   }
    34     31   if {[anoncap j]} {
    35     32     html "<a href='$home/wiki'>Wiki</a>\n"
    36     33   }
    37     34   if {[hascap s]} {

Changes to skins/xekri/header.txt.

    69     69        if {[info exists login]} {
    70     70          puts "Logged in as $login"
    71     71        } else {
    72     72          puts "Not logged in"
    73     73        }
    74     74     </th1></nobr><small><div id="clock"></div></small></div>
    75     75   </div>
    76         -<th1>html "<script nonce='$nonce'>"</th1>
           76  +<script>
    77     77   function updateClock(){
    78     78     var e = document.getElementById("clock");
    79     79     if(e){
    80     80       var d = new Date();
    81     81       function f(n) {
    82     82         return n < 10 ? '0' + n : n;
    83     83       }
    84     84       e.innerHTML = d.getUTCFullYear()+ '-' +
    85     85         f(d.getUTCMonth() + 1) + '-' +
    86     86         f(d.getUTCDate())      + ' ' +
    87     87         f(d.getUTCHours())     + ':' +
    88     88         f(d.getUTCMinutes());
    89         -    setTimeout(updateClock,(60-d.getUTCSeconds())*1000);
           89  +    setTimeout("updateClock();",(60-d.getUTCSeconds())*1000);
    90     90     }
    91     91   }
    92     92   updateClock();
    93     93   </script>
    94     94   <div class="mainmenu">
    95     95   <th1>
    96     96   proc menulink {url name} {
................................................................................
   109    109   if {[anoncap oh]} {
   110    110     menulink /dir?ci=tip Files
   111    111   }
   112    112   if {[anoncap o]} {
   113    113     menulink  /brlist Branches
   114    114     menulink  /taglist Tags
   115    115   }
   116         -if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
   117         -  menulink /forum Forum
   118         -}
   119    116   if {[anoncap r]} {
   120    117     menulink /ticket Tickets
   121    118   }
   122    119   if {[anoncap j]} {
   123    120     menulink /wiki Wiki
   124    121   }
   125    122     menulink /sitemap More...

Changes to src/add.c.

   251    251   ** Make arrangements to add one or more files or directories to the
   252    252   ** current checkout at the next commit.
   253    253   **
   254    254   ** When adding files or directories recursively, filenames that begin
   255    255   ** with "." are excluded by default.  To include such files, add
   256    256   ** the "--dotfiles" option to the command-line.
   257    257   **
   258         -** The --ignore and --clean options are comma-separated lists of glob patterns
          258  +** The --ignore and --clean options are comma-separate lists of glob patterns
   259    259   ** for files to be excluded.  Example:  '*.o,*.obj,*.exe'  If the --ignore
   260    260   ** option does not appear on the command line then the "ignore-glob" setting
   261    261   ** is used.  If the --clean option does not appear on the command line then
   262    262   ** the "clean-glob" setting is used.
   263    263   **
   264    264   ** If files are attempted to be added explicitly on the command line which
   265    265   ** match "ignore-glob", a confirmation is asked first. This can be prevented
................................................................................
   857    857   void mv_cmd(void){
   858    858     int i;
   859    859     int vid;
   860    860     int moveFiles;
   861    861     int dryRunFlag;
   862    862     int softFlag;
   863    863     int hardFlag;
   864         -  int origType;
   865         -  int destType;
   866    864     char *zDest;
   867    865     Blob dest;
   868    866     Stmt q;
   869    867   
   870    868     db_must_be_within_tree();
   871    869     dryRunFlag = find_option("dry-run","n",0)!=0;
   872    870     softFlag = find_option("soft",0,0)!=0;
................................................................................
   873    871     hardFlag = find_option("hard",0,0)!=0;
   874    872   
   875    873     /* We should be done with options.. */
   876    874     verify_all_options();
   877    875   
   878    876     vid = db_lget_int("checkout", 0);
   879    877     if( vid==0 ){
   880         -    fossil_fatal("no checkout in which to rename files");
          878  +    fossil_fatal("no checkout rename files in");
   881    879     }
   882    880     if( g.argc<4 ){
   883    881       usage("OLDNAME NEWNAME");
   884    882     }
   885    883     zDest = g.argv[g.argc-1];
   886    884     db_begin_transaction();
   887    885     if( g.argv[1][0]=='r' ){ /* i.e. "rename" */
................................................................................
   900    898     file_tree_name(zDest, &dest, 0, 1);
   901    899     db_multi_exec(
   902    900       "UPDATE vfile SET origname=pathname WHERE origname IS NULL;"
   903    901     );
   904    902     db_multi_exec(
   905    903       "CREATE TEMP TABLE mv(f TEXT UNIQUE ON CONFLICT IGNORE, t TEXT);"
   906    904     );
   907         -  if( g.argc!=4 ){
   908         -    origType = -1;
   909         -  }else{
   910         -    origType = (file_isdir(g.argv[2], RepoFILE) == 1);
   911         -  }
   912         -  destType = file_isdir(zDest, RepoFILE);
   913         -  if( origType==-1 && destType!=1 ){
   914         -    usage("OLDNAME NEWNAME");
   915         -  }else if( origType==1 && destType==2 ){
   916         -    fossil_fatal("cannot rename '%s' to '%s' since another file named"
   917         -                 " '%s' exists", g.argv[2], zDest, zDest);
   918         -  }else if( origType==0 && destType!=1 ){
          905  +  if( file_isdir(zDest, RepoFILE)!=1 ){
   919    906       Blob orig;
          907  +    if( g.argc!=4 ){
          908  +      usage("OLDNAME NEWNAME");
          909  +    }
   920    910       file_tree_name(g.argv[2], &orig, 0, 1);
   921    911       db_multi_exec(
   922    912         "INSERT INTO mv VALUES(%B,%B)", &orig, &dest
   923    913       );
   924    914     }else{
   925    915       if( blob_eq(&dest, ".") ){
   926    916         blob_reset(&dest);
................................................................................
   944    934         );
   945    935         while( db_step(&q)==SQLITE_ROW ){
   946    936           const char *zPath = db_column_text(&q, 0);
   947    937           int nPath = db_column_bytes(&q, 0);
   948    938           const char *zTail;
   949    939           if( nPath==nOrig ){
   950    940             zTail = file_tail(zPath);
   951         -        }else if( destType==1 ){
   952         -          zTail = &zPath[nOrig-strlen(file_tail(zOrig))];
   953    941           }else{
   954    942             zTail = &zPath[nOrig+1];
   955    943           }
   956    944           db_multi_exec(
   957    945             "INSERT INTO mv VALUES('%q','%q%q')",
   958    946             zPath, blob_str(&dest), zTail
   959    947           );

Deleted src/alerts.c.

     1         -/*
     2         -** Copyright (c) 2018 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         -** Logic for email notification, also known as "alerts".
    19         -**
    20         -** Are you looking for the code that reads and writes the internet
    21         -** email protocol?  That is not here.  See the "smtp.c" file instead.
    22         -** Yes, the choice of source code filenames is not the greatest, but
    23         -** it is not so bad that changing them seems justified.
    24         -*/ 
    25         -#include "config.h"
    26         -#include "alerts.h"
    27         -#include <assert.h>
    28         -#include <time.h>
    29         -
    30         -/*
    31         -** Maximum size of the subscriberCode blob, in bytes
    32         -*/
    33         -#define SUBSCRIBER_CODE_SZ 32
    34         -
    35         -/*
    36         -** SQL code to implement the tables needed by the email notification
    37         -** system.
    38         -*/
    39         -static const char zAlertInit[] =
    40         -@ DROP TABLE IF EXISTS repository.subscriber;
    41         -@ -- Subscribers are distinct from users.  A person can have a log-in in
    42         -@ -- the USER table without being a subscriber.  Or a person can be a
    43         -@ -- subscriber without having a USER table entry.  Or they can have both.
    44         -@ -- In the last case the suname column points from the subscriber entry
    45         -@ -- to the USER entry.
    46         -@ --
    47         -@ -- The ssub field is a string where each character indicates a particular
    48         -@ -- type of event to subscribe to.  Choices:
    49         -@ --     a - Announcements
    50         -@ --     c - Check-ins
    51         -@ --     t - Ticket changes
    52         -@ --     w - Wiki changes
    53         -@ -- Probably different codes will be added in the future.  In the future
    54         -@ -- we might also add a separate table that allows subscribing to email
    55         -@ -- notifications for specific branches or tags or tickets.
    56         -@ --
    57         -@ CREATE TABLE repository.subscriber(
    58         -@   subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID.  Internal use
    59         -@   subscriberCode BLOB DEFAULT (randomblob(32)) UNIQUE, -- UUID for subscriber
    60         -@   semail TEXT UNIQUE COLLATE nocase,-- email address
    61         -@   suname TEXT,                      -- corresponding USER entry
    62         -@   sverified BOOLEAN DEFAULT true,   -- email address verified
    63         -@   sdonotcall BOOLEAN,               -- true for Do Not Call 
    64         -@   sdigest BOOLEAN,                  -- true for daily digests only
    65         -@   ssub TEXT,                        -- baseline subscriptions
    66         -@   sctime INTDATE,                   -- When this entry was created. unixtime
    67         -@   mtime INTDATE,                    -- Last change.  unixtime
    68         -@   smip TEXT                         -- IP address of last change
    69         -@ );
    70         -@ CREATE INDEX repository.subscriberUname
    71         -@   ON subscriber(suname) WHERE suname IS NOT NULL;
    72         -@ 
    73         -@ DROP TABLE IF EXISTS repository.pending_alert;
    74         -@ -- Email notifications that need to be sent.
    75         -@ --
    76         -@ -- The first character of the eventid determines the event type.
    77         -@ -- Remaining characters determine the specific event.  For example,
    78         -@ -- 'c4413' means check-in with rid=4413.
    79         -@ --
    80         -@ CREATE TABLE repository.pending_alert(
    81         -@   eventid TEXT PRIMARY KEY,         -- Object that changed
    82         -@   sentSep BOOLEAN DEFAULT false,    -- individual alert sent
    83         -@   sentDigest BOOLEAN DEFAULT false, -- digest alert sent
    84         -@   sentMod BOOLEAN DEFAULT false     -- pending moderation alert sent
    85         -@ ) WITHOUT ROWID;
    86         -@ 
    87         -@ DROP TABLE IF EXISTS repository.alert_bounce;
    88         -@ -- Record bounced emails.  If too many bounces are received within
    89         -@ -- some defined time range, then cancel the subscription.  Older
    90         -@ -- entries are periodically purged.
    91         -@ --
    92         -@ CREATE TABLE repository.alert_bounce(
    93         -@   subscriberId INTEGER, -- to whom the email was sent.
    94         -@   sendTime INTEGER,     -- seconds since 1970 when email was sent
    95         -@   rcvdTime INTEGER      -- seconds since 1970 when bounce was received
    96         -@ );
    97         -;
    98         -
    99         -/*
   100         -** Return true if the email notification tables exist.
   101         -*/
   102         -int alert_tables_exist(void){
   103         -  return db_table_exists("repository", "subscriber");
   104         -}
   105         -
   106         -/*
   107         -** Make sure the table needed for email notification exist in the repository.
   108         -**
   109         -** If the bOnlyIfEnabled option is true, then tables are only created
   110         -** if the email-send-method is something other than "off".
   111         -*/
   112         -void alert_schema(int bOnlyIfEnabled){
   113         -  if( !alert_tables_exist() ){
   114         -    if( bOnlyIfEnabled
   115         -     && fossil_strcmp(db_get("email-send-method","off"),"off")==0
   116         -    ){
   117         -      return;  /* Don't create table for disabled email */
   118         -    }
   119         -    db_multi_exec(zAlertInit/*works-like:""*/);
   120         -    alert_triggers_enable();
   121         -  }else if( !db_table_has_column("repository","pending_alert","sentMod") ){
   122         -    db_multi_exec(
   123         -      "ALTER TABLE repository.pending_alert"
   124         -      " ADD COLUMN sentMod BOOLEAN DEFAULT false;"
   125         -    );
   126         -  }
   127         -}
   128         -
   129         -/*
   130         -** Enable triggers that automatically populate the pending_alert
   131         -** table.
   132         -*/
   133         -void alert_triggers_enable(void){
   134         -  if( !db_table_exists("repository","pending_alert") ) return;
   135         -  db_multi_exec(
   136         -    "CREATE TRIGGER IF NOT EXISTS repository.alert_trigger1\n"
   137         -    "AFTER INSERT ON event BEGIN\n"
   138         -    "  INSERT INTO pending_alert(eventid)\n"
   139         -    "    SELECT printf('%%.1c%%d',new.type,new.objid) WHERE true\n"
   140         -    "    ON CONFLICT(eventId) DO NOTHING;\n"
   141         -    "END;"
   142         -  );
   143         -}
   144         -
   145         -/*
   146         -** Disable triggers the event_pending triggers.
   147         -**
   148         -** This must be called before rebuilding the EVENT table, for example
   149         -** via the "fossil rebuild" command.
   150         -*/
   151         -void alert_triggers_disable(void){
   152         -  db_multi_exec(
   153         -    "DROP TRIGGER IF EXISTS repository.alert_trigger1;\n"
   154         -    "DROP TRIGGER IF EXISTS repository.email_trigger1;\n" // Legacy
   155         -  );
   156         -}
   157         -
   158         -/*
   159         -** Return true if email alerts are active.
   160         -*/
   161         -int alert_enabled(void){
   162         -  if( !alert_tables_exist() ) return 0;
   163         -  if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
   164         -  return 1;
   165         -}
   166         -
   167         -/*
   168         -** If the subscriber table does not exist, then paint an error message
   169         -** web page and return true.
   170         -**
   171         -** If the subscriber table does exist, return 0 without doing anything.
   172         -*/
   173         -static int alert_webpages_disabled(void){
   174         -  if( alert_tables_exist() ) return 0;
   175         -  style_header("Email Alerts Are Disabled");
   176         -  @ <p>Email alerts are disabled on this server</p>
   177         -  style_footer();
   178         -  return 1;
   179         -}
   180         -
   181         -/*
   182         -** Insert a "Subscriber List" submenu link if the current user
   183         -** is an administrator.
   184         -*/
   185         -void alert_submenu_common(void){
   186         -  if( g.perm.Admin ){
   187         -    if( fossil_strcmp(g.zPath,"subscribers") ){
   188         -      style_submenu_element("List Subscribers","%R/subscribers");
   189         -    }
   190         -    if( fossil_strcmp(g.zPath,"subscribe") ){
   191         -      style_submenu_element("Add New Subscriber","%R/subscribe");
   192         -    }
   193         -  }
   194         -}
   195         -
   196         -
   197         -/*
   198         -** WEBPAGE: setup_notification
   199         -**
   200         -** Administrative page for configuring and controlling email notification.
   201         -** Normally accessible via the /Admin/Notification menu.
   202         -*/
   203         -void setup_notification(void){
   204         -  static const char *const azSendMethods[] = {
   205         -    "off",   "Disabled",
   206         -    "pipe",  "Pipe to a command",
   207         -    "db",    "Store in a database",
   208         -    "dir",   "Store in a directory",
   209         -    "relay", "SMTP relay"
   210         -  };
   211         -  login_check_credentials();
   212         -  if( !g.perm.Setup ){
   213         -    login_needed(0);
   214         -    return;
   215         -  }
   216         -  db_begin_transaction();
   217         -
   218         -  alert_submenu_common();
   219         -  style_submenu_element("Send Announcement","%R/announce");
   220         -  style_header("Email Notification Setup");
   221         -  @ <h1>Status</h1>
   222         -  @ <table class="label-value">
   223         -  if( alert_enabled() ){
   224         -    stats_for_email();
   225         -  }else{
   226         -    @ <th>Disabled</th>
   227         -  }
   228         -  @ </table>
   229         -  @ <hr>
   230         -  @ <h1> Configuration </h1>
   231         -  @ <form action="%R/setup_notification" method="post"><div>
   232         -  @ <input type="submit"  name="submit" value="Apply Changes" /><hr>
   233         -  login_insert_csrf_secret();
   234         -
   235         -  entry_attribute("Canonical Server URL", 40, "email-url",
   236         -                   "eurl", "", 0);
   237         -  @ <p><b>Required.</b>
   238         -  @ This is URL used as the basename for hyperlinks included in
   239         -  @ email alert text.  Omit the trailing "/".
   240         -  @ Suggested value: "%h(g.zBaseURL)"
   241         -  @ (Property: "email-url")</p>
   242         -  @ <hr>
   243         -
   244         -  entry_attribute("\"Return-Path\" email address", 20, "email-self",
   245         -                   "eself", "", 0);
   246         -  @ <p><b>Required.</b>
   247         -  @ This is the email to which email notification bounces should be sent.
   248         -  @ In cases where the email notification does not align with a specific
   249         -  @ Fossil login account (for example, digest messages), this is also
   250         -  @ the "From:" address of the email notification.
   251         -  @ The system administrator should arrange for emails sent to this address
   252         -  @ to be handed off to the "fossil email incoming" command so that Fossil
   253         -  @ can handle bounces. (Property: "email-self")</p>
   254         -  @ <hr>
   255         -
   256         -  entry_attribute("Repository Nickname", 16, "email-subname",
   257         -                   "enn", "", 0);
   258         -  @ <p><b>Required.</b>
   259         -  @ This is short name used to identifies the repository in the
   260         -  @ Subject: line of email alerts.  Traditionally this name is
   261         -  @ included in square brackets.  Examples: "[fossil-src]", "[sqlite-src]".
   262         -  @ (Property: "email-subname")</p>
   263         -  @ <hr>
   264         -
   265         -  multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
   266         -       "off", count(azSendMethods)/2, azSendMethods);
   267         -  @ <p>How to send email.  Requires auxiliary information from the fields
   268         -  @ that follow.  Hint: Use the <a href="%R/announce">/announce</a> page
   269         -  @ to send test message to debug this setting.
   270         -  @ (Property: "email-send-method")</p>
   271         -  alert_schema(1);
   272         -  entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
   273         -                   "ecmd", "sendmail -ti", 0);
   274         -  @ <p>When the send method is "pipe to a command", this is the command
   275         -  @ that is run.  Email messages are piped into the standard input of this
   276         -  @ command.  The command is expected to extract the sender address,
   277         -  @ recepient addresses, and subject from the header of the piped email
   278         -  @ text.  (Property: "email-send-command")</p>
   279         -
   280         -  entry_attribute("Store Emails In This Database", 60, "email-send-db",
   281         -                   "esdb", "", 0);
   282         -  @ <p>When the send method is "store in a databaes", each email message is
   283         -  @ stored in an SQLite database file with the name given here.
   284         -  @ (Property: "email-send-db")</p>
   285         -
   286         -  entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
   287         -                   "esdir", "", 0);
   288         -  @ <p>When the send method is "store in a directory", each email message is
   289         -  @ stored as a separate file in the directory shown here.
   290         -  @ (Property: "email-send-dir")</p>
   291         -
   292         -  entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
   293         -                   "esrh", "", 0);
   294         -  @ <p>When the send method is "SMTP relay", each email message is
   295         -  @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
   296         -  @ Agent" or "MSA" (rfc4409) at the hostname shown here.  Optionally
   297         -  @ append a colon and TCP port number (ex: smtp.example.com:587).
   298         -  @ The default TCP port number is 25.
   299         -  @ (Property: "email-send-relayhost")</p>
   300         -  @ <hr>
   301         -
   302         -  entry_attribute("Administrator email address", 40, "email-admin",
   303         -                   "eadmin", "", 0);
   304         -  @ <p>This is the email for the human administrator for the system.
   305         -  @ Abuse and trouble reports are send here.
   306         -  @ (Property: "email-admin")</p>
   307         -  @ <hr>
   308         -
   309         -  @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
   310         -  @ </div></form>
   311         -  db_end_transaction(0);
   312         -  style_footer();
   313         -}
   314         -
   315         -#if 0
   316         -/*
   317         -** Encode pMsg as MIME base64 and append it to pOut
   318         -*/
   319         -static void append_base64(Blob *pOut, Blob *pMsg){
   320         -  int n, i, k;
   321         -  char zBuf[100];
   322         -  n = blob_size(pMsg);
   323         -  for(i=0; i<n; i+=54){
   324         -    k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
   325         -    blob_append(pOut, zBuf, k);
   326         -    blob_append(pOut, "\r\n", 2);
   327         -  }
   328         -}
   329         -#endif
   330         -
   331         -/*
   332         -** Encode pMsg using the quoted-printable email encoding and
   333         -** append it onto pOut
   334         -*/
   335         -static void append_quoted(Blob *pOut, Blob *pMsg){
   336         -  char *zIn = blob_str(pMsg);
   337         -  char c;
   338         -  int iCol = 0;
   339         -  while( (c = *(zIn++))!=0 ){
   340         -    if( (c>='!' && c<='~' && c!='=' && c!=':')
   341         -     || (c==' ' && zIn[0]!='\r' && zIn[0]!='\n')
   342         -    ){
   343         -      blob_append_char(pOut, c);
   344         -      iCol++;
   345         -      if( iCol>=70 ){
   346         -        blob_append(pOut, "=\r\n", 3);
   347         -        iCol = 0;
   348         -      }
   349         -    }else if( c=='\r' && zIn[0]=='\n' ){
   350         -      zIn++;
   351         -      blob_append(pOut, "\r\n", 2);
   352         -      iCol = 0;
   353         -    }else if( c=='\n' ){
   354         -      blob_append(pOut, "\r\n", 2);
   355         -      iCol = 0;
   356         -    }else{
   357         -      char x[3];
   358         -      x[0] = '=';
   359         -      x[1] = "0123456789ABCDEF"[(c>>4)&0xf];
   360         -      x[2] = "0123456789ABCDEF"[c&0xf];
   361         -      blob_append(pOut, x, 3);
   362         -      iCol += 3;
   363         -    }
   364         -  }
   365         -}
   366         -
   367         -#if defined(_WIN32) || defined(WIN32)
   368         -# undef popen
   369         -# define popen _popen
   370         -# undef pclose
   371         -# define pclose _pclose
   372         -#endif
   373         -
   374         -#if INTERFACE
   375         -/*
   376         -** An instance of the following object is used to send emails.
   377         -*/
   378         -struct AlertSender {
   379         -  sqlite3 *db;               /* Database emails are sent to */
   380         -  sqlite3_stmt *pStmt;       /* Stmt to insert into the database */
   381         -  const char *zDest;         /* How to send email. */
   382         -  const char *zDb;           /* Name of database file */
   383         -  const char *zDir;          /* Directory in which to store as email files */
   384         -  const char *zCmd;          /* Command to run for each email */
   385         -  const char *zFrom;         /* Emails come from here */
   386         -  SmtpSession *pSmtp;        /* SMTP relay connection */
   387         -  Blob out;                  /* For zDest=="blob" */
   388         -  char *zErr;                /* Error message */
   389         -  u32 mFlags;                /* Flags */
   390         -  int bImmediateFail;        /* On any error, call fossil_fatal() */
   391         -};
   392         -
   393         -/* Allowed values for mFlags to alert_sender_new().
   394         -*/
   395         -#define ALERT_IMMEDIATE_FAIL   0x0001   /* Call fossil_fatal() on any error */
   396         -#define ALERT_TRACE            0x0002   /* Log sending process on console */
   397         -
   398         -#endif /* INTERFACE */
   399         -
   400         -/*
   401         -** Shutdown an emailer.  Clear all information other than the error message.
   402         -*/
   403         -static void emailerShutdown(AlertSender *p){
   404         -  sqlite3_finalize(p->pStmt);
   405         -  p->pStmt = 0;
   406         -  sqlite3_close(p->db);
   407         -  p->db = 0;
   408         -  p->zDb = 0;
   409         -  p->zDir = 0;
   410         -  p->zCmd = 0;
   411         -  if( p->pSmtp ){
   412         -    smtp_client_quit(p->pSmtp);
   413         -    smtp_session_free(p->pSmtp);
   414         -    p->pSmtp = 0;
   415         -  }
   416         -  blob_reset(&p->out);
   417         -}
   418         -
   419         -/*
   420         -** Put the AlertSender into an error state.
   421         -*/
   422         -static void emailerError(AlertSender *p, const char *zFormat, ...){
   423         -  va_list ap;
   424         -  fossil_free(p->zErr);
   425         -  va_start(ap, zFormat);
   426         -  p->zErr = vmprintf(zFormat, ap);
   427         -  va_end(ap);
   428         -  emailerShutdown(p);
   429         -  if( p->mFlags & ALERT_IMMEDIATE_FAIL ){
   430         -    fossil_fatal("%s", p->zErr);
   431         -  }
   432         -}
   433         -
   434         -/*
   435         -** Free an email sender object
   436         -*/
   437         -void alert_sender_free(AlertSender *p){
   438         -  if( p ){
   439         -    emailerShutdown(p);
   440         -    fossil_free(p->zErr);
   441         -    fossil_free(p);
   442         -  }
   443         -}
   444         -
   445         -/*
   446         -** Get an email setting value.  Report an error if not configured.
   447         -** Return 0 on success and one if there is an error.
   448         -*/
   449         -static int emailerGetSetting(
   450         -  AlertSender *p,        /* Where to report the error */
   451         -  const char **pzVal,    /* Write the setting value here */
   452         -  const char *zName      /* Name of the setting */
   453         -){
   454         -  const char *z = db_get(zName, 0);
   455         -  int rc = 0;
   456         -  if( z==0 || z[0]==0 ){
   457         -    emailerError(p, "missing \"%s\" setting", zName);
   458         -    rc = 1;
   459         -  }else{
   460         -    *pzVal = z;
   461         -  }
   462         -  return rc;
   463         -}
   464         -
   465         -/*
   466         -** Create a new AlertSender object.
   467         -**
   468         -** The method used for sending email is determined by various email-*
   469         -** settings, and especially email-send-method.  The repository
   470         -** email-send-method can be overridden by the zAltDest argument to
   471         -** cause a different sending mechanism to be used.  Pass "stdout" to
   472         -** zAltDest to cause all emails to be printed to the console for
   473         -** debugging purposes.
   474         -**
   475         -** The AlertSender object returned must be freed using alert_sender_free().
   476         -*/
   477         -AlertSender *alert_sender_new(const char *zAltDest, u32 mFlags){
   478         -  AlertSender *p;
   479         -
   480         -  p = fossil_malloc(sizeof(*p));
   481         -  memset(p, 0, sizeof(*p));
   482         -  blob_init(&p->out, 0, 0);
   483         -  p->mFlags = mFlags;
   484         -  if( zAltDest ){
   485         -    p->zDest = zAltDest;
   486         -  }else{
   487         -    p->zDest = db_get("email-send-method","off");
   488         -  }
   489         -  if( fossil_strcmp(p->zDest,"off")==0 ) return p;
   490         -  if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
   491         -  if( fossil_strcmp(p->zDest,"db")==0 ){
   492         -    char *zErr;
   493         -    int rc;
   494         -    if( emailerGetSetting(p, &p->zDb, "email-send-db") ) return p;
   495         -    rc = sqlite3_open(p->zDb, &p->db);
   496         -    if( rc ){
   497         -      emailerError(p, "unable to open output database file \"%s\": %s",
   498         -                   p->zDb, sqlite3_errmsg(p->db));
   499         -      return p;
   500         -    }
   501         -    rc = sqlite3_exec(p->db, "CREATE TABLE IF NOT EXISTS email(\n"
   502         -                          "  emailid INTEGER PRIMARY KEY,\n"
   503         -                          "  msg TEXT\n);", 0, 0, &zErr);
   504         -    if( zErr ){
   505         -      emailerError(p, "CREATE TABLE failed with \"%s\"", zErr);
   506         -      sqlite3_free(zErr);
   507         -      return p;
   508         -    }
   509         -    rc = sqlite3_prepare_v2(p->db, "INSERT INTO email(msg) VALUES(?1)", -1,
   510         -                            &p->pStmt, 0);
   511         -    if( rc ){
   512         -      emailerError(p, "cannot prepare INSERT statement: %s",
   513         -                 sqlite3_errmsg(p->db));
   514         -      return p;
   515         -    }
   516         -  }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
   517         -    emailerGetSetting(p, &p->zCmd, "email-send-command");
   518         -  }else if( fossil_strcmp(p->zDest, "dir")==0 ){
   519         -    emailerGetSetting(p, &p->zDir, "email-send-dir");
   520         -  }else if( fossil_strcmp(p->zDest, "blob")==0 ){
   521         -    blob_init(&p->out, 0, 0);
   522         -  }else if( fossil_strcmp(p->zDest, "relay")==0 ){
   523         -    const char *zRelay = 0;
   524         -    emailerGetSetting(p, &zRelay, "email-send-relayhost");
   525         -    if( zRelay ){
   526         -      u32 smtpFlags = SMTP_DIRECT;
   527         -      if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
   528         -      p->pSmtp = smtp_session_new(p->zFrom, zRelay, smtpFlags);
   529         -      smtp_client_startup(p->pSmtp);
   530         -    }
   531         -  }
   532         -  return p;
   533         -}
   534         -
   535         -/*
   536         -** Scan the header of the email message in pMsg looking for the
   537         -** (first) occurrance of zField.  Fill pValue with the content of
   538         -** that field.
   539         -**
   540         -** This routine initializes pValue.  Any prior content of pValue is
   541         -** discarded (leaked).
   542         -**
   543         -** Return non-zero on success.  Return 0 if no instance of the header
   544         -** is found.
   545         -*/
   546         -int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){
   547         -  int nField = (int)strlen(zField);
   548         -  Blob line;
   549         -  blob_rewind(pMsg);
   550         -  blob_init(pValue,0,0);
   551         -  while( blob_line(pMsg, &line) ){
   552         -    int n, i;
   553         -    char *z;
   554         -    blob_trim(&line);
   555         -    n = blob_size(&line);
   556         -    if( n==0 ) return 0;
   557         -    if( n<nField+1 ) continue;
   558         -    z = blob_buffer(&line);
   559         -    if( sqlite3_strnicmp(z, zField, nField)==0 && z[nField]==':' ){
   560         -      for(i=nField+1; i<n && fossil_isspace(z[i]); i++){}
   561         -      blob_init(pValue, z+i, n-i);
   562         -      while( blob_line(pMsg, &line) ){
   563         -        blob_trim(&line);
   564         -        n = blob_size(&line);
   565         -        if( n==0 ) break;
   566         -        z = blob_buffer(&line);
   567         -        if( !fossil_isspace(z[0]) ) break;
   568         -        for(i=1; i<n && fossil_isspace(z[i]); i++){}
   569         -        blob_append(pValue, " ", 1);
   570         -        blob_append(pValue, z+i, n-i);
   571         -      }
   572         -      return 1;
   573         -    }
   574         -  }
   575         -  return 0;
   576         -}
   577         -
   578         -/*
   579         -** Make a copy of the input string up to but not including the
   580         -** first cTerm character.
   581         -**
   582         -** Verify that the string really that is to be copied really is a
   583         -** valid email address.  If it is not, then return NULL.
   584         -**
   585         -** This routine is more restrictive than necessary.  It does not
   586         -** allow comments, IP address, quoted strings, or certain uncommon
   587         -** characters.  The only non-alphanumerics allowed in the local
   588         -** part are "_", "+", "-" and "+".
   589         -*/
   590         -char *email_copy_addr(const char *z, char cTerm ){
   591         -  int i;
   592         -  int nAt = 0;
   593         -  int nDot = 0;
   594         -  char c;
   595         -  if( z[0]=='.' ) return 0;  /* Local part cannot begin with "." */
   596         -  for(i=0; (c = z[i])!=0 && c!=cTerm; i++){
   597         -    if( fossil_isalnum(c) ){
   598         -      /* Alphanumerics are always ok */
   599         -    }else if( c=='@' ){
   600         -      if( nAt ) return 0;   /* Only a single "@"  allowed */
   601         -      if( i>64 ) return 0;  /* Local part too big */
   602         -      nAt = 1;
   603         -      nDot = 0;
   604         -      if( i==0 ) return 0;  /* Disallow empty local part */
   605         -      if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */
   606         -      if( z[i+1]=='.' || z[i+1]=='-' ){
   607         -        return 0; /* Domain cannot begin with "." or "-" */
   608         -      }
   609         -    }else if( c=='-' ){
   610         -      if( z[i+1]==cTerm ) return 0;  /* Last character cannot be "-" */
   611         -    }else if( c=='.' ){
   612         -      if( z[i+1]=='.' ) return 0;  /* Do not allow ".." */
   613         -      if( z[i+1]==cTerm ) return 0;  /* Domain may not end with . */
   614         -      nDot++;
   615         -    }else if( (c=='_' || c=='+') && nAt==0 ){
   616         -      /* _ and + are ok in the local part */
   617         -    }else{
   618         -      return 0;   /* Anything else is an error */
   619         -    }
   620         -  }
   621         -  if( c!=cTerm ) return 0;    /* Missing terminator */
   622         -  if( nAt==0 ) return 0;      /* No "@" found anywhere */
   623         -  if( nDot==0 ) return 0;     /* No "." in the domain */
   624         -
   625         -  /* If we reach this point, the email address is valid */
   626         -  return mprintf("%.*s", i, z);
   627         -}
   628         -
   629         -/*
   630         -** Scan the input string for a valid email address enclosed in <...>
   631         -** If the string contains one or more email addresses, extract the first
   632         -** one into memory obtained from mprintf() and return a pointer to it.
   633         -** If no valid email address can be found, return NULL.
   634         -*/
   635         -char *alert_find_emailaddr(const char *zIn){
   636         -  char *zOut = 0;
   637         -  while( zIn!=0 ){
   638         -     zIn = (const char*)strchr(zIn, '<');
   639         -     if( zIn==0 ) break;
   640         -     zIn++;
   641         -     zOut = email_copy_addr(zIn, '>');
   642         -     if( zOut!=0 ) break;
   643         -  }
   644         -  return zOut;
   645         -}
   646         -
   647         -/*
   648         -** SQL function:  find_emailaddr(X)
   649         -**
   650         -** Return the first valid email address of the form <...> in input string
   651         -** X.  Or return NULL if not found.
   652         -*/
   653         -void alert_find_emailaddr_func(
   654         -  sqlite3_context *context,
   655         -  int argc,
   656         -  sqlite3_value **argv
   657         -){
   658         -  const char *zIn = (const char*)sqlite3_value_text(argv[0]);
   659         -  char *zOut = alert_find_emailaddr(zIn);
   660         -  if( zOut ){
   661         -    sqlite3_result_text(context, zOut, -1, fossil_free);
   662         -  }
   663         -}
   664         -
   665         -/*
   666         -** Return the hostname portion of an email address - the part following
   667         -** the @
   668         -*/
   669         -char *alert_hostname(const char *zAddr){
   670         -  char *z = strchr(zAddr, '@');
   671         -  if( z ){
   672         -    z++;
   673         -  }else{
   674         -    z = (char*)zAddr;
   675         -  }
   676         -  return z;
   677         -}
   678         -
   679         -/*
   680         -** Return a pointer to a fake email mailbox name that corresponds
   681         -** to human-readable name zFromName.  The fake mailbox name is based
   682         -** on a hash.  No huge problems arise if there is a hash collisions,
   683         -** but it is still better if collisions can be avoided.
   684         -**
   685         -** The returned string is held in a static buffer and is overwritten
   686         -** by each subsequent call to this routine.
   687         -*/
   688         -static char *alert_mailbox_name(const char *zFromName){
   689         -  static char zHash[20];
   690         -  unsigned int x = 0;
   691         -  int n = 0;
   692         -  while( zFromName[0] ){
   693         -    n++;
   694         -    x = x*1103515245 + 12345 + ((unsigned char*)zFromName)[0];
   695         -    zFromName++;
   696         -  }
   697         -  sqlite3_snprintf(sizeof(zHash), zHash,
   698         -      "noreply%x%08x", n, x);
   699         -  return zHash;
   700         -}
   701         -
   702         -/*
   703         -** COMMAND: test-mailbox-hashname
   704         -**
   705         -** Usage: %fossil test-mailbox-hashname HUMAN-NAME ...
   706         -**
   707         -** Return the mailbox hash name corresponding to each human-readable
   708         -** name on the command line.  This is a test interface for the
   709         -** alert_mailbox_name() function.
   710         -*/
   711         -void alert_test_mailbox_hashname(void){
   712         -  int i;
   713         -  for(i=2; i<g.argc; i++){
   714         -    fossil_print("%30s: %s\n", g.argv[i], alert_mailbox_name(g.argv[i]));
   715         -  }
   716         -}
   717         -
   718         -/*
   719         -** Extract all To: header values from the email header supplied.
   720         -** Store them in the array list.
   721         -*/
   722         -void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){
   723         -  int nTo = 0;
   724         -  char **azTo = 0;
   725         -  Blob v;
   726         -  char *z, *zAddr;
   727         -  int i;
   728         -  
   729         -  email_header_value(pMsg, "to", &v);
   730         -  z = blob_str(&v);
   731         -  for(i=0; z[i]; i++){
   732         -    if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){
   733         -      azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) );
   734         -      azTo[nTo++] = zAddr;
   735         -    }
   736         -  }
   737         -  *pnTo = nTo;
   738         -  *pazTo = azTo;
   739         -}
   740         -
   741         -/*
   742         -** Free a list of To addresses obtained from a prior call to 
   743         -** email_header_to()
   744         -*/
   745         -void email_header_to_free(int nTo, char **azTo){
   746         -  int i;
   747         -  for(i=0; i<nTo; i++) fossil_free(azTo[i]);
   748         -  fossil_free(azTo);
   749         -}
   750         -
   751         -/*
   752         -** Send a single email message.
   753         -**
   754         -** The recepient(s) must be specified using  "To:" or "Cc:" or "Bcc:" fields
   755         -** in the header.  Likewise, the header must contains a "Subject:" line.
   756         -** The header might also include fields like "Message-Id:" or
   757         -** "In-Reply-To:".
   758         -**
   759         -** This routine will add fields to the header as follows:
   760         -**
   761         -**     From:
   762         -**     Date:
   763         -**     Message-Id:
   764         -**     Content-Type:
   765         -**     Content-Transfer-Encoding:
   766         -**     MIME-Version:
   767         -**     X-Fossil-From:
   768         -**     
   769         -** The caller maintains ownership of the input Blobs.  This routine will
   770         -** read the Blobs and send them onward to the email system, but it will
   771         -** not free them.
   772         -**
   773         -** The Message-Id: field is added if there is not already a Message-Id
   774         -** in the pHdr parameter.
   775         -**
   776         -** If the zFromName argument is not NULL, then it should be a human-readable
   777         -** name or handle for the sender.  In that case, "From:" becomes a made-up
   778         -** email address based on a hash of zFromName and the domain of email-self,
   779         -** and an additional "X-Fossil-From:" field is inserted with the email-self
   780         -** address.  Downstream software might use the X-Fossil-From header to set
   781         -** the envelope-from address of the email.  If zFromName is a NULL pointer, 
   782         -** then the "From:" is set to the email-self value and X-Fossil-From is
   783         -** omitted.
   784         -*/
   785         -void alert_send(
   786         -  AlertSender *p,           /* Emailer context */
   787         -  Blob *pHdr,               /* Email header (incomplete) */
   788         -  Blob *pBody,              /* Email body */
   789         -  const char *zFromName     /* Optional human-readable name of sender */
   790         -){
   791         -  Blob all, *pOut;
   792         -  u64 r1, r2;
   793         -  if( p->mFlags & ALERT_TRACE ){
   794         -    fossil_print("Sending email\n");
   795         -  }
   796         -  if( fossil_strcmp(p->zDest, "off")==0 ){
   797         -    return;
   798         -  }
   799         -  if( fossil_strcmp(p->zDest, "blob")==0 ){
   800         -    pOut = &p->out;
   801         -    if( blob_size(pOut) ){
   802         -      blob_appendf(pOut, "%.72c\n", '=');
   803         -    }
   804         -  }else{
   805         -    blob_init(&all, 0, 0);
   806         -    pOut = &all;
   807         -  }
   808         -  blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
   809         -  if( zFromName ){
   810         -    blob_appendf(pOut, "From: %s <%s@%s>\r\n",
   811         -       zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
   812         -    blob_appendf(pOut, "X-Fossil-From: <%s>\r\n", p->zFrom);
   813         -  }else{
   814         -    blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
   815         -  }
   816         -  blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
   817         -  if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
   818         -    /* Message-id format:  "<$(date)x$(random)@$(from-host)>" where $(date) is
   819         -    ** the current unix-time in hex, $(random) is a 64-bit random number,
   820         -    ** and $(from) is the domain part of the email-self setting. */
   821         -    sqlite3_randomness(sizeof(r1), &r1);
   822         -    r2 = time(0);
   823         -    blob_appendf(pOut, "Message-Id: <%llxx%016llx@%s>\r\n",
   824         -                 r2, r1, alert_hostname(p->zFrom));
   825         -  }
   826         -  blob_add_final_newline(pBody);
   827         -  blob_appendf(pOut, "MIME-Version: 1.0\r\n");
   828         -  blob_appendf(pOut, "Content-Type: text/plain; charset=\"UTF-8\"\r\n");
   829         -#if 0
   830         -  blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n");
   831         -  append_base64(pOut, pBody);
   832         -#else
   833         -  blob_appendf(pOut, "Content-Transfer-Encoding: quoted-printable\r\n\r\n");
   834         -  append_quoted(pOut, pBody);
   835         -#endif
   836         -  if( p->pStmt ){
   837         -    int i, rc;
   838         -    sqlite3_bind_text(p->pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT);
   839         -    for(i=0; i<100 && sqlite3_step(p->pStmt)==SQLITE_BUSY; i++){
   840         -      sqlite3_sleep(10);
   841         -    }
   842         -    rc = sqlite3_reset(p->pStmt);
   843         -    if( rc!=SQLITE_OK ){
   844         -      emailerError(p, "Failed to insert email message into output queue.\n"
   845         -                      "%s", sqlite3_errmsg(p->db));
   846         -    }
   847         -  }else if( p->zCmd ){
   848         -    FILE *out = popen(p->zCmd, "w");
   849         -    if( out ){
   850         -      fwrite(blob_buffer(&all), 1, blob_size(&all), out);
   851         -      fclose(out);
   852         -    }else{
   853         -      emailerError(p, "Could not open output pipe \"%s\"", p->zCmd);
   854         -    }
   855         -  }else if( p->zDir ){
   856         -    char *zFile = file_time_tempname(p->zDir, ".email");
   857         -    blob_write_to_file(&all, zFile);
   858         -    fossil_free(zFile);
   859         -  }else if( p->pSmtp ){
   860         -    char **azTo = 0;
   861         -    int nTo = 0;
   862         -    email_header_to(pHdr, &nTo, &azTo);
   863         -    if( nTo>0 ){
   864         -      smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all));
   865         -      email_header_to_free(nTo, azTo);
   866         -    }
   867         -  }else if( strcmp(p->zDest, "stdout")==0 ){
   868         -    char **azTo = 0;
   869         -    int nTo = 0;
   870         -    int i;
   871         -    email_header_to(pHdr, &nTo, &azTo);
   872         -    for(i=0; i<nTo; i++){
   873         -      fossil_print("X-To-Test-%d: [%s]\r\n", i, azTo[i]);
   874         -    }
   875         -    email_header_to_free(nTo, azTo);
   876         -    blob_add_final_newline(&all);
   877         -    fossil_print("%s", blob_str(&all));
   878         -  }
   879         -  blob_reset(&all);
   880         -}
   881         -
   882         -/*
   883         -** SETTING: email-send-method         width=5 default=off
   884         -** Determine the method used to send email.  Allowed values are
   885         -** "off", "relay", "pipe", "dir", "db", and "stdout".  The "off" value
   886         -** means no email is ever sent.  The "relay" value means emails are sent
   887         -** to an Mail Sending Agent using SMTP located at email-send-relayhost.
   888         -** The "pipe" value means email messages are piped into a command 
   889         -** determined by the email-send-command setting. The "dir" value means
   890         -** emails are written to individual files in a directory determined
   891         -** by the email-send-dir setting.  The "db" value means that emails
   892         -** are added to an SQLite database named by the* email-send-db setting.
   893         -** The "stdout" value writes email text to standard output, for debugging.
   894         -*/
   895         -/*
   896         -** SETTING: email-send-command       width=40
   897         -** This is a command to which outbound email content is piped when the
   898         -** email-send-method is set to "pipe".  The command must extract
   899         -** recipient, sender, subject, and all other relevant information
   900         -** from the email header.
   901         -*/
   902         -/*
   903         -** SETTING: email-send-dir           width=40
   904         -** This is a directory into which outbound emails are written as individual
   905         -** files if the email-send-method is set to "dir".
   906         -*/
   907         -/*
   908         -** SETTING: email-send-db            width=40
   909         -** This is an SQLite database file into which outbound emails are written
   910         -** if the email-send-method is set to "db".
   911         -*/
   912         -/*
   913         -** SETTING: email-self               width=40
   914         -** This is the email address for the repository.  Outbound emails add
   915         -** this email address as the "From:" field.
   916         -*/
   917         -/*
   918         -** SETTING: email-send-relayhost      width=40
   919         -** This is the hostname and TCP port to which output email messages
   920         -** are sent when email-send-method is "relay".  There should be an
   921         -** SMTP server configured as a Mail Submission Agent listening on the
   922         -** designated host and port and all times.
   923         -*/
   924         -
   925         -
   926         -/*
   927         -** COMMAND: alerts
   928         -** 
   929         -** Usage: %fossil alerts SUBCOMMAND ARGS...
   930         -**
   931         -** Subcommands:
   932         -**
   933         -**    pending                 Show all pending alerts.  Useful for debugging.
   934         -**
   935         -**    reset                   Hard reset of all email notification tables
   936         -**                            in the repository.  This erases all subscription
   937         -**                            information.  ** Use with extreme care **
   938         -**
   939         -**    send                    Compose and send pending email alerts.
   940         -**                            Some installations may want to do this via
   941         -**                            a cron-job to make sure alerts are sent
   942         -**                            in a timely manner.
   943         -**                            Options:
   944         -**
   945         -**                               --digest     Send digests
   946         -**                               --test       Write to standard output
   947         -**
   948         -**    settings [NAME VALUE]   With no arguments, list all email settings.
   949         -**                            Or change the value of a single email setting.
   950         -**
   951         -**    status                  Report on the status of the email alert
   952         -**                            subsystem
   953         -**
   954         -**    subscribers [PATTERN]   List all subscribers matching PATTERN.
   955         -**
   956         -**    test-message TO [OPTS]  Send a single email message using whatever
   957         -**                            email sending mechanism is currently configured.
   958         -**                            Use this for testing the email notification
   959         -**                            configuration.  Options:
   960         -**
   961         -**                              --body FILENAME
   962         -**                              --smtp-trace
   963         -**                              --stdout
   964         -**                              --subject|-S SUBJECT
   965         -**
   966         -**    unsubscribe EMAIL       Remove a single subscriber with the given EMAIL.
   967         -*/
   968         -void alert_cmd(void){
   969         -  const char *zCmd;
   970         -  int nCmd;
   971         -  db_find_and_open_repository(0, 0);
   972         -  alert_schema(0);
   973         -  zCmd = g.argc>=3 ? g.argv[2] : "x";
   974         -  nCmd = (int)strlen(zCmd);
   975         -  if( strncmp(zCmd, "pending", nCmd)==0 ){
   976         -    Stmt q;
   977         -    verify_all_options();
   978         -    if( g.argc!=3 ) usage("pending");
   979         -    db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod"
   980         -                  "  FROM pending_alert");
   981         -    while( db_step(&q)==SQLITE_ROW ){
   982         -      fossil_print("%10s %7s %10s %7s\n",
   983         -         db_column_text(&q,0),
   984         -         db_column_int(&q,1) ? "sentSep" : "",
   985         -         db_column_int(&q,2) ? "sentDigest" : "",
   986         -         db_column_int(&q,3) ? "sentMod" : "");
   987         -    }
   988         -    db_finalize(&q);
   989         -  }else
   990         -  if( strncmp(zCmd, "reset", nCmd)==0 ){
   991         -    int c;
   992         -    int bForce = find_option("force","f",0)!=0;
   993         -    verify_all_options();
   994         -    if( bForce ){
   995         -      c = 'y';
   996         -    }else{
   997         -      Blob yn;
   998         -      fossil_print(
   999         -          "This will erase all content in the repository tables, thus\n"
  1000         -          "deleting all subscriber information.  The information will be\n"
  1001         -          "unrecoverable.\n");
  1002         -      prompt_user("Continue? (y/N) ", &yn);
  1003         -      c = blob_str(&yn)[0];
  1004         -      blob_reset(&yn);
  1005         -    }
  1006         -    if( c=='y' ){
  1007         -      alert_triggers_disable();
  1008         -      db_multi_exec(
  1009         -        "DROP TABLE IF EXISTS subscriber;\n"
  1010         -        "DROP TABLE IF EXISTS pending_alert;\n"
  1011         -        "DROP TABLE IF EXISTS alert_bounce;\n"
  1012         -        /* Legacy */
  1013         -        "DROP TABLE IF EXISTS alert_pending;\n"
  1014         -        "DROP TABLE IF EXISTS subscription;\n"
  1015         -      );
  1016         -      alert_schema(0);
  1017         -    }
  1018         -  }else
  1019         -  if( strncmp(zCmd, "send", nCmd)==0 ){
  1020         -    u32 eFlags = 0;
  1021         -    if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
  1022         -    if( find_option("test",0,0)!=0 ){
  1023         -      eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT;
  1024         -    }
  1025         -    verify_all_options();
  1026         -    alert_send_alerts(eFlags);
  1027         -  }else
  1028         -  if( strncmp(zCmd, "settings", nCmd)==0 ){
  1029         -    int isGlobal = find_option("global",0,0)!=0;
  1030         -    int nSetting;
  1031         -    const Setting *pSetting = setting_info(&nSetting);
  1032         -    db_open_config(1, 0);
  1033         -    verify_all_options();
  1034         -    if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
  1035         -    if( g.argc==5 ){
  1036         -      const char *zLabel = g.argv[3];
  1037         -      if( strncmp(zLabel, "email-", 6)!=0
  1038         -       || (pSetting = db_find_setting(zLabel, 1))==0 ){
  1039         -        fossil_fatal("not a valid email setting: \"%s\"", zLabel);
  1040         -      }
  1041         -      db_set(pSetting->name, g.argv[4], isGlobal);
  1042         -      g.argc = 3;
  1043         -    }
  1044         -    pSetting = setting_info(&nSetting);
  1045         -    for(; nSetting>0; nSetting--, pSetting++ ){
  1046         -      if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
  1047         -      print_setting(pSetting);
  1048         -    }
  1049         -  }else
  1050         -  if( strncmp(zCmd, "status", nCmd)==0 ){
  1051         -    int nSetting, n;
  1052         -    static const char *zFmt = "%-29s %d\n";
  1053         -    const Setting *pSetting = setting_info(&nSetting);
  1054         -    db_open_config(1, 0);
  1055         -    verify_all_options();
  1056         -    if( g.argc!=3 ) usage("status");
  1057         -    pSetting = setting_info(&nSetting);
  1058         -    for(; nSetting>0; nSetting--, pSetting++ ){
  1059         -      if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
  1060         -      print_setting(pSetting);
  1061         -    }
  1062         -    n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep");
  1063         -    fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n);
  1064         -    n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest");
  1065         -    fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n);
  1066         -    n = db_int(0,"SELECT count(*) FROM subscriber");
  1067         -    fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n);
  1068         -    n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified"
  1069         -                   " AND NOT sdonotcall AND length(ssub)>1");
  1070         -    fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n);
  1071         -  }else
  1072         -  if( strncmp(zCmd, "subscribers", nCmd)==0 ){
  1073         -    Stmt q;
  1074         -    verify_all_options();
  1075         -    if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
  1076         -    if( g.argc==4 ){
  1077         -      char *zPattern = g.argv[3];
  1078         -      db_prepare(&q,
  1079         -        "SELECT semail FROM subscriber"
  1080         -        " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'"
  1081         -        "  OR semail GLOB '*%q*' or suname GLOB '*%q*'"
  1082         -        " ORDER BY semail",
  1083         -        zPattern, zPattern, zPattern, zPattern);
  1084         -    }else{
  1085         -      db_prepare(&q,
  1086         -        "SELECT semail FROM subscriber"
  1087         -        " ORDER BY semail");
  1088         -    }
  1089         -    while( db_step(&q)==SQLITE_ROW ){
  1090         -      fossil_print("%s\n", db_column_text(&q, 0));
  1091         -    }
  1092         -    db_finalize(&q);
  1093         -  }else
  1094         -  if( strncmp(zCmd, "test-message", nCmd)==0 ){
  1095         -    Blob prompt, body, hdr;
  1096         -    const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
  1097         -    int i;
  1098         -    u32 mFlags = ALERT_IMMEDIATE_FAIL;
  1099         -    const char *zSubject = find_option("subject", "S", 1);
  1100         -    const char *zSource = find_option("body", 0, 1);
  1101         -    AlertSender *pSender;
  1102         -    if( find_option("smtp-trace",0,0)!=0 ) mFlags |= ALERT_TRACE;
  1103         -    verify_all_options();
  1104         -    blob_init(&prompt, 0, 0);
  1105         -    blob_init(&body, 0, 0);
  1106         -    blob_init(&hdr, 0, 0);
  1107         -    blob_appendf(&hdr,"To: ");
  1108         -    for(i=3; i<g.argc; i++){
  1109         -      if( i>3 ) blob_append(&hdr, ", ", 2);
  1110         -      blob_appendf(&hdr, "<%s>", g.argv[i]);
  1111         -    }
  1112         -    blob_append(&hdr,"\r\n",2);
  1113         -    if( zSubject==0 ) zSubject = "fossil alerts test-message";
  1114         -    blob_appendf(&hdr, "Subject: %s\r\n", zSubject);
  1115         -    if( zSource ){
  1116         -      blob_read_from_file(&body, zSource, ExtFILE);
  1117         -    }else{
  1118         -      prompt_for_user_comment(&body, &prompt);
  1119         -    }
  1120         -    blob_add_final_newline(&body);
  1121         -    pSender = alert_sender_new(zDest, mFlags);
  1122         -    alert_send(pSender, &hdr, &body, 0);
  1123         -    alert_sender_free(pSender);
  1124         -    blob_reset(&hdr);
  1125         -    blob_reset(&body);
  1126         -    blob_reset(&prompt);
  1127         -  }else
  1128         -  if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
  1129         -    verify_all_options();
  1130         -    if( g.argc!=4 ) usage("unsubscribe EMAIL");
  1131         -    db_multi_exec(
  1132         -      "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
  1133         -  }else
  1134         -  {
  1135         -    usage("pending|reset|send|setting|status|"
  1136         -          "subscribers|test-message|unsubscribe");
  1137         -  }
  1138         -}
  1139         -
  1140         -/*
  1141         -** Do error checking on a submitted subscription form.  Return TRUE
  1142         -** if the submission is valid.  Return false if any problems are seen.
  1143         -*/
  1144         -static int subscribe_error_check(
  1145         -  int *peErr,           /* Type of error */
  1146         -  char **pzErr,         /* Error message text */
  1147         -  int needCaptcha       /* True if captcha check needed */
  1148         -){
  1149         -  const char *zEAddr;
  1150         -  int i, j, n;
  1151         -  char c;
  1152         -
  1153         -  *peErr = 0;
  1154         -  *pzErr = 0;
  1155         -
  1156         -  /* Check the validity of the email address.
  1157         -  **
  1158         -  **  (1) Exactly one '@' character.
  1159         -  **  (2) No other characters besides [a-zA-Z0-9._+-]
  1160         -  **
  1161         -  **  The local part is currently more restrictive than RFC 5322 allows:
  1162         -  **  https://stackoverflow.com/a/2049510/142454  We will expand this as
  1163         -  **  necessary.
  1164         -  */
  1165         -  zEAddr = P("e");
  1166         -  if( zEAddr==0 ) return 0;
  1167         -  for(i=j=n=0; (c = zEAddr[i])!=0; i++){
  1168         -    if( c=='@' ){
  1169         -      n = i;
  1170         -      j++;
  1171         -      continue;
  1172         -    }
  1173         -    if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' && c!='+' ){
  1174         -      *peErr = 1;
  1175         -      *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
  1176         -                   c, c);
  1177         -      return 0;
  1178         -    }
  1179         -  }
  1180         -  if( j!=1 ){
  1181         -    *peErr = 1;
  1182         -    *pzErr = mprintf("email address should contain exactly one '@'");
  1183         -    return 0;
  1184         -  }
  1185         -  if( n<1 ){
  1186         -    *peErr = 1;
  1187         -    *pzErr = mprintf("name missing before '@' in email address");
  1188         -    return 0;
  1189         -  }
  1190         -  if( n>i-5 ){
  1191         -    *peErr = 1;
  1192         -    *pzErr = mprintf("email domain too short");
  1193         -     return 0;
  1194         -  }
  1195         -
  1196         -  /* Verify the captcha */
  1197         -  if( needCaptcha && !captcha_is_correct(1) ){
  1198         -    *peErr = 2;
  1199         -    *pzErr = mprintf("incorrect security code");
  1200         -    return 0;
  1201         -  }
  1202         -
  1203         -  /* Check to make sure the email address is available for reuse */
  1204         -  if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
  1205         -    *peErr = 1;
  1206         -    *pzErr = mprintf("this email address is used by someone else");
  1207         -    return 0;
  1208         -  }
  1209         -
  1210         -  /* If we reach this point, all is well */
  1211         -  return 1;
  1212         -}
  1213         -
  1214         -/*
  1215         -** Text of email message sent in order to confirm a subscription.
  1216         -*/
  1217         -static const char zConfirmMsg[] = 
  1218         -@ Someone has signed you up for email alerts on the Fossil repository
  1219         -@ at %s.
  1220         -@
  1221         -@ To confirm your subscription and begin receiving alerts, click on
  1222         -@ the following hyperlink:
  1223         -@
  1224         -@    %s/alerts/%s
  1225         -@
  1226         -@ Save the hyperlink above!  You can reuse this same hyperlink to
  1227         -@ unsubscribe or to change the kinds of alerts you receive.
  1228         -@
  1229         -@ If you do not want to subscribe, you can simply ignore this message.
  1230         -@ You will not be contacted again.
  1231         -@
  1232         -;
  1233         -
  1234         -/*
  1235         -** Append the text of an email confirmation message to the given
  1236         -** Blob.  The security code is in zCode.
  1237         -*/
  1238         -void alert_append_confirmation_message(Blob *pMsg, const char *zCode){
  1239         -  blob_appendf(pMsg, zConfirmMsg/*works-like:"%s%s%s"*/,
  1240         -                   g.zBaseURL, g.zBaseURL, zCode);
  1241         -}
  1242         -
  1243         -/*
  1244         -** WEBPAGE: subscribe
  1245         -**
  1246         -** Allow users to subscribe to email notifications.
  1247         -**
  1248         -** This page is usually run by users who are not logged in.
  1249         -** A logged-in user can add email notifications on the /alerts page.
  1250         -** Access to this page by a logged in user (other than an
  1251         -** administrator) results in a redirect to the /alerts page.
  1252         -**
  1253         -** Administrators can visit this page in order to sign up other
  1254         -** users.
  1255         -**
  1256         -** The Alerts permission ("7") is required to access this
  1257         -** page.  To allow anonymous passers-by to sign up for email
  1258         -** notification, set Email-Alerts on user "nobody" or "anonymous".
  1259         -*/
  1260         -void subscribe_page(void){
  1261         -  int needCaptcha;
  1262         -  unsigned int uSeed;
  1263         -  const char *zDecoded;
  1264         -  char *zCaptcha = 0;
  1265         -  char *zErr = 0;
  1266         -  int eErr = 0;
  1267         -  int di;
  1268         -
  1269         -  if( alert_webpages_disabled() ) return;
  1270         -  login_check_credentials();
  1271         -  if( !g.perm.EmailAlert ){
  1272         -    login_needed(g.anon.EmailAlert);
  1273         -    return;
  1274         -  }
  1275         -  if( login_is_individual()
  1276         -   && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
  1277         -  ){
  1278         -    /* This person is already signed up for email alerts.  Jump
  1279         -    ** to the screen that lets them edit their alert preferences.
  1280         -    ** Except, administrators can create subscriptions for others so
  1281         -    ** do not jump for them.
  1282         -    */
  1283         -    if( g.perm.Admin ){
  1284         -      /* Admins get a link to admin their own account, but they
  1285         -      ** stay on this page so that they can create subscriptions
  1286         -      ** for other people. */
  1287         -      style_submenu_element("My Subscription","%R/alerts");
  1288         -    }else{
  1289         -      /* Everybody else jumps to the page to administer their own
  1290         -      ** account only. */
  1291         -      cgi_redirectf("%R/alerts");
  1292         -      return;
  1293         -    }
  1294         -  }
  1295         -  alert_submenu_common();
  1296         -  needCaptcha = !login_is_individual();
  1297         -  if( P("submit")
  1298         -   && cgi_csrf_safe(1)
  1299         -   && subscribe_error_check(&eErr,&zErr,needCaptcha)
  1300         -  ){
  1301         -    /* A validated request for a new subscription has been received. */
  1302         -    char ssub[20];
  1303         -    const char *zEAddr = P("e");
  1304         -    sqlite3_int64 id;   /* New subscriber Id */
  1305         -    const char *zCode;  /* New subscriber code (in hex) */
  1306         -    int nsub = 0;
  1307         -    const char *suname = PT("suname");
  1308         -    if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin;
  1309         -    if( suname && suname[0]==0 ) suname = 0;
  1310         -    if( PB("sa") ) ssub[nsub++] = 'a';
  1311         -    if( g.perm.Read && PB("sc") )    ssub[nsub++] = 'c';
  1312         -    if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
  1313         -    if( g.perm.RdTkt && PB("st") )   ssub[nsub++] = 't';
  1314         -    if( g.perm.RdWiki && PB("sw") )  ssub[nsub++] = 'w';
  1315         -    ssub[nsub] = 0;
  1316         -    db_multi_exec(
  1317         -      "INSERT INTO subscriber(semail,suname,"
  1318         -      "  sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
  1319         -      "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
  1320         -      /* semail */    zEAddr,
  1321         -      /* suname */    suname,
  1322         -      /* sverified */ needCaptcha==0,
  1323         -      /* sdigest */   PB("di"),
  1324         -      /* ssub */      ssub,
  1325         -      /* smip */      g.zIpAddr
  1326         -    );
  1327         -    id = db_last_insert_rowid();
  1328         -    zCode = db_text(0,
  1329         -         "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld",
  1330         -         id);
  1331         -    if( !needCaptcha ){
  1332         -      /* The new subscription has been added on behalf of a logged-in user.
  1333         -      ** No verification is required.  Jump immediately to /alerts page.
  1334         -      */
  1335         -      cgi_redirectf("%R/alerts/%s", zCode);
  1336         -      return;
  1337         -    }else{
  1338         -      /* We need to send a verification email */
  1339         -      Blob hdr, body;
  1340         -      AlertSender *pSender = alert_sender_new(0,0);
  1341         -      blob_init(&hdr,0,0);
  1342         -      blob_init(&body,0,0);
  1343         -      blob_appendf(&hdr, "To: <%s>\n", zEAddr);
  1344         -      blob_appendf(&hdr, "Subject: Subscription verification\n");
  1345         -      alert_append_confirmation_message(&body, zCode);
  1346         -      alert_send(pSender, &hdr, &body, 0);
  1347         -      style_header("Email Alert Verification");
  1348         -      if( pSender->zErr ){
  1349         -        @ <h1>Internal Error</h1>
  1350         -        @ <p>The following internal error was encountered while trying
  1351         -        @ to send the confirmation email:
  1352         -        @ <blockquote><pre>
  1353         -        @ %h(pSender->zErr)
  1354         -        @ </pre></blockquote>
  1355         -      }else{
  1356         -        @ <p>An email has been sent to "%h(zEAddr)". That email contains a
  1357         -        @ hyperlink that you must click on in order to activate your
  1358         -        @ subscription.</p>
  1359         -      }
  1360         -      alert_sender_free(pSender);
  1361         -      style_footer();
  1362         -    }
  1363         -    return;
  1364         -  }
  1365         -  style_header("Signup For Email Alerts");
  1366         -  if( P("submit")==0 ){
  1367         -    /* If this is the first visit to this page (if this HTTP request did not
  1368         -    ** come from a prior Submit of the form) then default all of the
  1369         -    ** subscription options to "on" */
  1370         -    cgi_set_parameter_nocopy("sa","1",1);
  1371         -    if( g.perm.Read )    cgi_set_parameter_nocopy("sc","1",1);
  1372         -    if( g.perm.RdForum ) cgi_set_parameter_nocopy("sf","1",1);
  1373         -    if( g.perm.RdTkt )   cgi_set_parameter_nocopy("st","1",1);
  1374         -    if( g.perm.RdWiki )  cgi_set_parameter_nocopy("sw","1",1);
  1375         -  }
  1376         -  @ <p>To receive email notifications for changes to this
  1377         -  @ repository, fill out the form below and press "Submit" button.</p>
  1378         -  form_begin(0, "%R/subscribe");
  1379         -  @ <table class="subscribe">
  1380         -  @ <tr>
  1381         -  @  <td class="form_label">Email&nbsp;Address:</td>
  1382         -  @  <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
  1383         -  @ <tr>
  1384         -  if( eErr==1 ){
  1385         -    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1386         -  }
  1387         -  @ </tr>
  1388         -  if( needCaptcha ){
  1389         -    uSeed = captcha_seed();
  1390         -    zDecoded = captcha_decode(uSeed);
  1391         -    zCaptcha = captcha_render(zDecoded);
  1392         -    @ <tr>
  1393         -    @  <td class="form_label">Security Code:</td>
  1394         -    @  <td><input type="text" name="captcha" value="" size="30">
  1395         -    @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
  1396         -    @ </tr>
  1397         -    if( eErr==2 ){
  1398         -      @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1399         -    }
  1400         -    @ </tr>
  1401         -  }
  1402         -  if( g.perm.Admin ){
  1403         -    @ <tr>
  1404         -    @  <td class="form_label">User:</td>
  1405         -    @  <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
  1406         -    @  size="30"></td>
  1407         -    @ </tr>
  1408         -    if( eErr==3 ){
  1409         -      @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1410         -    }
  1411         -    @ </tr>
  1412         -  }
  1413         -  @ <tr>
  1414         -  @  <td class="form_label">Topics:</td>
  1415         -  @  <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
  1416         -  @  Announcements</label><br>
  1417         -  if( g.perm.Read ){
  1418         -    @  <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
  1419         -    @  Check-ins</label><br>
  1420         -  }
  1421         -  if( g.perm.RdForum ){
  1422         -    @  <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
  1423         -    @  Forum Posts</label><br>
  1424         -  }
  1425         -  if( g.perm.RdTkt ){
  1426         -    @  <label><input type="checkbox" name="st" %s(PCK("st"))> \
  1427         -    @  Ticket changes</label><br>
  1428         -  }
  1429         -  if( g.perm.RdWiki ){
  1430         -    @  <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
  1431         -    @  Wiki</label><br>
  1432         -  }
  1433         -  di = PB("di");
  1434         -  @ </td></tr>
  1435         -  @ <tr>
  1436         -  @  <td class="form_label">Delivery:</td>
  1437         -  @  <td><select size="1" name="di">
  1438         -  @     <option value="0" %s(di?"":"selected")>Individual Emails</option>
  1439         -  @     <option value="1" %s(di?"selected":"")>Daily Digest</option>
  1440         -  @     </select></td>
  1441         -  @ </tr>
  1442         -  if( g.perm.Admin ){
  1443         -    @ <tr>
  1444         -    @  <td class="form_label">Admin Options:</td><td>
  1445         -    @  <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
  1446         -    @  Verified</label><br>
  1447         -    @  <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
  1448         -    @  Do not call</label></td></tr>
  1449         -  }
  1450         -  @ <tr>
  1451         -  @  <td></td>
  1452         -  if( needCaptcha && !alert_enabled() ){
  1453         -    @  <td><input type="submit" name="submit" value="Submit" disabled>
  1454         -    @  (Email current disabled)</td>
  1455         -  }else{
  1456         -    @  <td><input type="submit" name="submit" value="Submit"></td>
  1457         -  }
  1458         -  @ </tr>
  1459         -  @ </table>
  1460         -  if( needCaptcha ){
  1461         -    @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  1462         -    @ %h(zCaptcha)
  1463         -    @ </pre>
  1464         -    @ Enter the 8 characters above in the "Security Code" box
  1465         -    @ </td></tr></table></div>
  1466         -  }
  1467         -  @ </form>
  1468         -  fossil_free(zErr);
  1469         -  style_footer();
  1470         -}
  1471         -
  1472         -/*
  1473         -** Either shutdown or completely delete a subscription entry given
  1474         -** by the hex value zName.  Then paint a webpage that explains that
  1475         -** the entry has been removed.
  1476         -*/
  1477         -static void alert_unsubscribe(const char *zName){
  1478         -  char *zEmail;
  1479         -  zEmail = db_text(0, "SELECT semail FROM subscriber"
  1480         -                      " WHERE subscriberCode=hextoblob(%Q)", zName);
  1481         -  if( zEmail==0 ){
  1482         -    style_header("Unsubscribe Fail");
  1483         -    @ <p>Unable to locate a subscriber with the requested key</p>
  1484         -  }else{
  1485         -    db_multi_exec(
  1486         -      "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
  1487         -      zName
  1488         -    );
  1489         -    style_header("Unsubscribed");
  1490         -    @ <p>The "%h(zEmail)" email address has been delisted.
  1491         -    @ All traces of that email address have been removed</p>
  1492         -  }
  1493         -  style_footer();
  1494         -  return;
  1495         -}
  1496         -
  1497         -/*
  1498         -** WEBPAGE: alerts
  1499         -**
  1500         -** Edit email alert and notification settings.
  1501         -**
  1502         -** The subscriber is identified in either of two ways:
  1503         -**
  1504         -**    (1)  The name= query parameter contains the subscriberCode.
  1505         -**         
  1506         -**    (2)  The user is logged into an account other than "nobody" or
  1507         -**         "anonymous".  In that case the notification settings
  1508         -**         associated with that account can be edited without needing
  1509         -**         to know the subscriber code.
  1510         -*/
  1511         -void alert_page(void){
  1512         -  const char *zName = P("name");
  1513         -  Stmt q;
  1514         -  int sa, sc, sf, st, sw;
  1515         -  int sdigest, sdonotcall, sverified;
  1516         -  const char *ssub;
  1517         -  const char *semail;
  1518         -  const char *smip;
  1519         -  const char *suname;
  1520         -  const char *mtime;
  1521         -  const char *sctime;
  1522         -  int eErr = 0;
  1523         -  char *zErr = 0;
  1524         -
  1525         -  if( alert_webpages_disabled() ) return;
  1526         -  login_check_credentials();
  1527         -  if( !g.perm.EmailAlert ){
  1528         -    login_needed(g.anon.EmailAlert);
  1529         -    return;
  1530         -  }
  1531         -  if( zName==0 && login_is_individual() ){
  1532         -    zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
  1533         -                       " WHERE suname=%Q", g.zLogin);
  1534         -  }
  1535         -  if( zName==0 || !validate16(zName, -1) ){
  1536         -    cgi_redirect("subscribe");
  1537         -    return;
  1538         -  }
  1539         -  alert_submenu_common();
  1540         -  if( P("submit")!=0 && cgi_csrf_safe(1) ){
  1541         -    int sdonotcall = PB("sdonotcall");
  1542         -    int sdigest = PB("sdigest");
  1543         -    char ssub[10];
  1544         -    int nsub = 0;
  1545         -    if( PB("sa") )                   ssub[nsub++] = 'a';
  1546         -    if( g.perm.Read && PB("sc") )    ssub[nsub++] = 'c';
  1547         -    if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
  1548         -    if( g.perm.RdTkt && PB("st") )   ssub[nsub++] = 't';
  1549         -    if( g.perm.RdWiki && PB("sw") )  ssub[nsub++] = 'w';
  1550         -    ssub[nsub] = 0;
  1551         -    if( g.perm.Admin ){
  1552         -      const char *suname = PT("suname");
  1553         -      int sverified = PB("sverified");
  1554         -      if( suname && suname[0]==0 ) suname = 0;
  1555         -      db_multi_exec(
  1556         -        "UPDATE subscriber SET"
  1557         -        " sdonotcall=%d,"
  1558         -        " sdigest=%d,"
  1559         -        " ssub=%Q,"
  1560         -        " mtime=strftime('%%s','now'),"
  1561         -        " smip=%Q,"
  1562         -        " suname=%Q,"
  1563         -        " sverified=%d"
  1564         -        " WHERE subscriberCode=hextoblob(%Q)",
  1565         -        sdonotcall,
  1566         -        sdigest,
  1567         -        ssub,
  1568         -        g.zIpAddr,
  1569         -        suname,
  1570         -        sverified,
  1571         -        zName
  1572         -      );
  1573         -    }else{
  1574         -      db_multi_exec(
  1575         -        "UPDATE subscriber SET"
  1576         -        " sdonotcall=%d,"
  1577         -        " sdigest=%d,"
  1578         -        " ssub=%Q,"
  1579         -        " mtime=strftime('%%s','now'),"
  1580         -        " smip=%Q"
  1581         -        " WHERE subscriberCode=hextoblob(%Q)",
  1582         -        sdonotcall,
  1583         -        sdigest,
  1584         -        ssub,
  1585         -        g.zIpAddr,
  1586         -        zName
  1587         -      );
  1588         -    }
  1589         -  }
  1590         -  if( P("delete")!=0 && cgi_csrf_safe(1) ){
  1591         -    if( !PB("dodelete") ){
  1592         -      eErr = 9;
  1593         -      zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
  1594         -                     " unsubscribe");
  1595         -    }else{
  1596         -      alert_unsubscribe(zName);
  1597         -      return;
  1598         -    }
  1599         -  }
  1600         -  db_prepare(&q,
  1601         -    "SELECT"
  1602         -    "  semail,"                       /* 0 */
  1603         -    "  sverified,"                    /* 1 */
  1604         -    "  sdonotcall,"                   /* 2 */
  1605         -    "  sdigest,"                      /* 3 */
  1606         -    "  ssub,"                         /* 4 */
  1607         -    "  smip,"                         /* 5 */
  1608         -    "  suname,"                       /* 6 */
  1609         -    "  datetime(mtime,'unixepoch'),"  /* 7 */
  1610         -    "  datetime(sctime,'unixepoch')"  /* 8 */
  1611         -    " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
  1612         -  if( db_step(&q)!=SQLITE_ROW ){
  1613         -    db_finalize(&q);
  1614         -    cgi_redirect("subscribe");
  1615         -    return;
  1616         -  }
  1617         -  style_header("Update Subscription");
  1618         -  semail = db_column_text(&q, 0);
  1619         -  sverified = db_column_int(&q, 1);
  1620         -  sdonotcall = db_column_int(&q, 2);
  1621         -  sdigest = db_column_int(&q, 3);
  1622         -  ssub = db_column_text(&q, 4);
  1623         -  sa = strchr(ssub,'a')!=0;
  1624         -  sc = strchr(ssub,'c')!=0;
  1625         -  sf = strchr(ssub,'f')!=0;
  1626         -  st = strchr(ssub,'t')!=0;
  1627         -  sw = strchr(ssub,'w')!=0;
  1628         -  smip = db_column_text(&q, 5);
  1629         -  suname = db_column_text(&q, 6);
  1630         -  mtime = db_column_text(&q, 7);
  1631         -  sctime = db_column_text(&q, 8);
  1632         -  if( !g.perm.Admin && !sverified ){
  1633         -    db_multi_exec(
  1634         -      "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
  1635         -      zName);
  1636         -    @ <h1>Your email alert subscription has been verified!</h1>
  1637         -    @ <p>Use the form below to update your subscription information.</p>
  1638         -    @ <p>Hint:  Bookmark this page so that you can more easily update
  1639         -    @ your subscription information in the future</p>
  1640         -  }else{
  1641         -    @ <p>Make changes to the email subscription shown below and
  1642         -    @ press "Submit".</p>
  1643         -  }
  1644         -  form_begin(0, "%R/alerts");
  1645         -  @ <input type="hidden" name="name" value="%h(zName)">
  1646         -  @ <table class="subscribe">
  1647         -  @ <tr>
  1648         -  @  <td class="form_label">Email&nbsp;Address:</td>
  1649         -  @  <td>%h(semail)</td>
  1650         -  @ </tr>
  1651         -  if( g.perm.Admin ){
  1652         -    @ <tr>
  1653         -    @  <td class='form_label'>Created:</td>
  1654         -    @  <td>%h(sctime)</td>
  1655         -    @ </tr>
  1656         -    @ <tr>
  1657         -    @  <td class='form_label'>Last Modified:</td>
  1658         -    @  <td>%h(mtime)</td>
  1659         -    @ </tr>
  1660         -    @ <tr>
  1661         -    @  <td class='form_label'>IP Address:</td>
  1662         -    @  <td>%h(smip)</td>
  1663         -    @ </tr>
  1664         -    @ <tr>
  1665         -    @  <td class="form_label">User:</td>
  1666         -    @  <td><input type="text" name="suname" value="%h(suname?suname:"")" \
  1667         -    @  size="30"></td>
  1668         -    @ </tr>
  1669         -  }
  1670         -  @ <tr>
  1671         -  @  <td class="form_label">Topics:</td>
  1672         -  @  <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
  1673         -  @  Announcements</label><br>
  1674         -  if( g.perm.Read ){
  1675         -    @  <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
  1676         -    @  Check-ins</label><br>
  1677         -  }
  1678         -  if( g.perm.RdForum ){
  1679         -    @  <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
  1680         -    @  Forum Posts</label><br>
  1681         -  }
  1682         -  if( g.perm.RdTkt ){
  1683         -    @  <label><input type="checkbox" name="st" %s(st?"checked":"")>\
  1684         -    @  Ticket changes</label><br>
  1685         -  }
  1686         -  if( g.perm.RdWiki ){
  1687         -    @  <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
  1688         -    @  Wiki</label>
  1689         -  }
  1690         -  @ </td></tr>
  1691         -  @ <tr>
  1692         -  @  <td class="form_label">Delivery:</td>
  1693         -  @  <td><select size="1" name="sdigest">
  1694         -  @     <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
  1695         -  @     <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
  1696         -  @     </select></td>
  1697         -  @ </tr>
  1698         -#if 0
  1699         -  @  <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
  1700         -  @  Daily digest only</label><br>
  1701         -#endif
  1702         -  if( g.perm.Admin ){
  1703         -    @ <tr>
  1704         -    @  <td class="form_label">Admin Options:</td><td>
  1705         -    @  <label><input type="checkbox" name="sdonotcall" \
  1706         -    @  %s(sdonotcall?"checked":"")> Do not call</label><br>
  1707         -    @  <label><input type="checkbox" name="sverified" \
  1708         -    @  %s(sverified?"checked":"")>\
  1709         -    @  Verified</label></td></tr>
  1710         -  }
  1711         -  if( eErr==9 ){
  1712         -    @ <tr>
  1713         -    @  <td class="form_label">Verify:</td><td>
  1714         -    @  <label><input type="checkbox" name="dodelete">
  1715         -    @  Unsubscribe</label>
  1716         -    @ <span class="loginError">&larr; %h(zErr)</span>
  1717         -    @ </td></tr>
  1718         -  }
  1719         -  @ <tr>
  1720         -  @  <td></td>
  1721         -  @  <td><input type="submit" name="submit" value="Submit">
  1722         -  @  <input type="submit" name="delete" value="Unsubscribe">
  1723         -  @ </tr>
  1724         -  @ </table>
  1725         -  @ </form>
  1726         -  fossil_free(zErr);
  1727         -  db_finalize(&q);
  1728         -  style_footer();
  1729         -}
  1730         -
  1731         -/* This is the message that gets sent to describe how to change
  1732         -** or modify a subscription
  1733         -*/
  1734         -static const char zUnsubMsg[] = 
  1735         -@ To changes your subscription settings at %s visit this link:
  1736         -@
  1737         -@    %s/alerts/%s
  1738         -@
  1739         -@ To completely unsubscribe from %s, visit the following link:
  1740         -@
  1741         -@    %s/unsubscribe/%s
  1742         -;
  1743         -
  1744         -/*
  1745         -** WEBPAGE: unsubscribe
  1746         -**
  1747         -** Users visit this page to be delisted from email alerts.
  1748         -**
  1749         -** If a valid subscriber code is supplied in the name= query parameter,
  1750         -** then that subscriber is delisted.
  1751         -**
  1752         -** Otherwise, If the users is logged in, then they are redirected
  1753         -** to the /alerts page where they have an unsubscribe button.
  1754         -**
  1755         -** Non-logged-in users with no name= query parameter are invited to enter
  1756         -** an email address to which will be sent the unsubscribe link that
  1757         -** contains the correct subscriber code.
  1758         -*/
  1759         -void unsubscribe_page(void){
  1760         -  const char *zName = P("name");
  1761         -  char *zErr = 0;
  1762         -  int eErr = 0;
  1763         -  unsigned int uSeed;
  1764         -  const char *zDecoded;
  1765         -  char *zCaptcha = 0;
  1766         -  int dx;
  1767         -  int bSubmit;
  1768         -  const char *zEAddr;
  1769         -  char *zCode = 0;
  1770         -
  1771         -  /* If a valid subscriber code is supplied, then unsubscribe immediately.
  1772         -  */
  1773         -  if( zName 
  1774         -   && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
  1775         -                zName)
  1776         -  ){
  1777         -    alert_unsubscribe(zName);
  1778         -    return;
  1779         -  }
  1780         -
  1781         -  /* Logged in users are redirected to the /alerts page */
  1782         -  login_check_credentials();
  1783         -  if( login_is_individual() ){
  1784         -    cgi_redirectf("%R/alerts");
  1785         -    return;
  1786         -  }
  1787         -
  1788         -  zEAddr = PD("e","");
  1789         -  dx = atoi(PD("dx","0"));
  1790         -  bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1);
  1791         -  if( bSubmit ){
  1792         -    if( !captcha_is_correct(1) ){
  1793         -      eErr = 2;
  1794         -      zErr = mprintf("enter the security code shown below");
  1795         -      bSubmit = 0;
  1796         -    }
  1797         -  }
  1798         -  if( bSubmit ){
  1799         -    zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber"
  1800         -                      " WHERE semail=%Q", zEAddr);
  1801         -    if( zCode==0 ){
  1802         -      eErr = 1;
  1803         -      zErr = mprintf("not a valid email address");
  1804         -      bSubmit = 0;
  1805         -    }
  1806         -  }
  1807         -  if( bSubmit ){
  1808         -    /* If we get this far, it means that a valid unsubscribe request has
  1809         -    ** been submitted.  Send the appropriate email. */
  1810         -    Blob hdr, body;
  1811         -    AlertSender *pSender = alert_sender_new(0,0);
  1812         -    blob_init(&hdr,0,0);
  1813         -    blob_init(&body,0,0);
  1814         -    blob_appendf(&hdr, "To: <%s>\r\n", zEAddr);
  1815         -    blob_appendf(&hdr, "Subject: Unsubscribe Instructions\r\n");
  1816         -    blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
  1817         -                  g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
  1818         -    alert_send(pSender, &hdr, &body, 0);
  1819         -    style_header("Unsubscribe Instructions Sent");
  1820         -    if( pSender->zErr ){
  1821         -      @ <h1>Internal Error</h1>
  1822         -      @ <p>The following error was encountered while trying to send an
  1823         -      @ email to %h(zEAddr):
  1824         -      @ <blockquote><pre>
  1825         -      @ %h(pSender->zErr)
  1826         -      @ </pre></blockquote>
  1827         -    }else{
  1828         -      @ <p>An email has been sent to "%h(zEAddr)" that explains how to
  1829         -      @ unsubscribe and/or modify your subscription settings</p>
  1830         -    }
  1831         -    alert_sender_free(pSender);
  1832         -    style_footer();
  1833         -    return;
  1834         -  }  
  1835         -
  1836         -  /* Non-logged-in users have to enter an email address to which is
  1837         -  ** sent a message containing the unsubscribe link.
  1838         -  */
  1839         -  style_header("Unsubscribe Request");
  1840         -  @ <p>Fill out the form below to request an email message that will
  1841         -  @ explain how to unsubscribe and/or change your subscription settings.</p>
  1842         -  @
  1843         -  form_begin(0, "%R/unsubscribe");
  1844         -  @ <table class="subscribe">
  1845         -  @ <tr>
  1846         -  @  <td class="form_label">Email&nbsp;Address:</td>
  1847         -  @  <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
  1848         -  if( eErr==1 ){
  1849         -    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
  1850         -  }
  1851         -  @ </tr>
  1852         -  uSeed = captcha_seed();
  1853         -  zDecoded = captcha_decode(uSeed);
  1854         -  zCaptcha = captcha_render(zDecoded);
  1855         -  @ <tr>
  1856         -  @  <td class="form_label">Security Code:</td>
  1857         -  @  <td><input type="text" name="captcha" value="" size="30">
  1858         -  @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
  1859         -  if( eErr==2 ){
  1860         -    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
  1861         -  }
  1862         -  @ </tr>
  1863         -  @ <tr>
  1864         -  @  <td class="form_label">Options:</td>
  1865         -  @  <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\
  1866         -  @  Modify subscription</label><br>
  1867         -  @  <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
  1868         -  @  Completely unsubscribe</label><br>
  1869         -  @ <tr>
  1870         -  @  <td></td>
  1871         -  @  <td><input type="submit" name="submit" value="Submit"></td>
  1872         -  @ </tr>
  1873         -  @ </table>
  1874         -  @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  1875         -  @ %h(zCaptcha)
  1876         -  @ </pre>
  1877         -  @ Enter the 8 characters above in the "Security Code" box
  1878         -  @ </td></tr></table></div>
  1879         -  @ </form>
  1880         -  fossil_free(zErr);
  1881         -  style_footer();
  1882         -}
  1883         -
  1884         -/*
  1885         -** WEBPAGE: subscribers
  1886         -**
  1887         -** This page, accessible to administrators only,
  1888         -** shows a list of email notification email addresses.
  1889         -** Clicking on an email takes one to the /alerts page
  1890         -** for that email where the delivery settings can be
  1891         -** modified.
  1892         -*/
  1893         -void subscriber_list_page(void){
  1894         -  Blob sql;
  1895         -  Stmt q;
  1896         -  sqlite3_int64 iNow;
  1897         -  if( alert_webpages_disabled() ) return;
  1898         -  login_check_credentials();
  1899         -  if( !g.perm.Admin ){
  1900         -    login_needed(0);
  1901         -    return;
  1902         -  }
  1903         -  alert_submenu_common();
  1904         -  style_header("Subscriber List");
  1905         -  blob_init(&sql, 0, 0);
  1906         -  blob_append_sql(&sql,
  1907         -    "SELECT hex(subscriberCode),"          /* 0 */
  1908         -    "       semail,"                       /* 1 */
  1909         -    "       ssub,"                         /* 2 */
  1910         -    "       suname,"                       /* 3 */
  1911         -    "       sverified,"                    /* 4 */
  1912         -    "       sdigest,"                      /* 5 */
  1913         -    "       mtime,"                        /* 6 */
  1914         -    "       date(sctime,'unixepoch')"      /* 7 */
  1915         -    " FROM subscriber"
  1916         -  );
  1917         -  if( P("only")!=0 ){
  1918         -    blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
  1919         -    style_submenu_element("Show All","%R/subscribers");
  1920         -  }
  1921         -  blob_append_sql(&sql," ORDER BY mtime DESC");
  1922         -  db_prepare_blob(&q, &sql);
  1923         -  iNow = time(0);
  1924         -  @ <table border='1' class='sortable' \
  1925         -  @ data-init-sort='6' data-column-types='tttttKt'>
  1926         -  @ <thead>
  1927         -  @ <tr>
  1928         -  @ <th>Email
  1929         -  @ <th>Events
  1930         -  @ <th>Digest-Only?
  1931         -  @ <th>User
  1932         -  @ <th>Verified?
  1933         -  @ <th>Last change
  1934         -  @ <th>Created
  1935         -  @ </tr>
  1936         -  @ </thead><tbody>
  1937         -  while( db_step(&q)==SQLITE_ROW ){
  1938         -    sqlite3_int64 iMtime = db_column_int64(&q, 6);
  1939         -    double rAge = (iNow - iMtime)/86400.0;
  1940         -    @ <tr>
  1941         -    @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
  1942         -    @ %h(db_column_text(&q,1))</a></td>
  1943         -    @ <td>%h(db_column_text(&q,2))</td>
  1944         -    @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
  1945         -    @ <td>%h(db_column_text(&q,3))</td>
  1946         -    @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
  1947         -    @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
  1948         -    @ <td>%h(db_column_text(&q,7))</td>
  1949         -    @ </tr>
  1950         -  }
  1951         -  @ </tbody></table>
  1952         -  db_finalize(&q);
  1953         -  style_table_sorter();
  1954         -  style_footer();
  1955         -}
  1956         -
  1957         -#if LOCAL_INTERFACE
  1958         -/*
  1959         -** A single event that might appear in an alert is recorded as an
  1960         -** instance of the following object.
  1961         -*/
  1962         -struct EmailEvent {
  1963         -  int type;          /* 'c', 'f', 'm', 't', 'w' */
  1964         -  int needMod;       /* Pending moderator approval */
  1965         -  Blob hdr;          /* Header content, for forum entries */
  1966         -  Blob txt;          /* Text description to appear in an alert */
  1967         -  char *zFromName;   /* Human name of the sender */
  1968         -  EmailEvent *pNext; /* Next in chronological order */
  1969         -};
  1970         -#endif
  1971         -
  1972         -/*
  1973         -** Free a linked list of EmailEvent objects
  1974         -*/
  1975         -void alert_free_eventlist(EmailEvent *p){
  1976         -  while( p ){
  1977         -    EmailEvent *pNext = p->pNext;
  1978         -    blob_reset(&p->txt);
  1979         -    blob_reset(&p->hdr);
  1980         -    fossil_free(p->zFromName);
  1981         -    fossil_free(p);
  1982         -    p = pNext;
  1983         -  }
  1984         -}
  1985         -
  1986         -/*
  1987         -** Compute and return a linked list of EmailEvent objects
  1988         -** corresponding to the current content of the temp.wantalert
  1989         -** table which should be defined as follows:
  1990         -**
  1991         -**     CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN);
  1992         -*/
  1993         -EmailEvent *alert_compute_event_text(int *pnEvent, int doDigest){
  1994         -  Stmt q;
  1995         -  EmailEvent *p;
  1996         -  EmailEvent anchor;
  1997         -  EmailEvent *pLast;
  1998         -  const char *zUrl = db_get("email-url","http://localhost:8080");
  1999         -  const char *zFrom;
  2000         -  const char *zSub;
  2001         -
  2002         -
  2003         -  /* First do non-forum post events */
  2004         -  db_prepare(&q,
  2005         -    "SELECT"
  2006         -    " blob.uuid,"                /* 0 */
  2007         -    " datetime(event.mtime),"    /* 1 */
  2008         -    " coalesce(ecomment,comment)"
  2009         -    "  || ' (user: ' || coalesce(euser,user,'?')"
  2010         -    "  || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
  2011         -    "      FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
  2012         -    "              FROM tag, tagxref"
  2013         -    "             WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
  2014         -    "               AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
  2015         -    "  || ')' as comment,"       /* 2 */
  2016         -    " wantalert.eventId,"        /* 3 */
  2017         -    " wantalert.needMod"         /* 4 */
  2018         -    " FROM temp.wantalert, event, blob"
  2019         -    " WHERE blob.rid=event.objid"
  2020         -    "   AND event.objid=substr(wantalert.eventId,2)+0"
  2021         -    "   AND (%d OR eventId NOT GLOB 'f*')"
  2022         -    " ORDER BY event.mtime",
  2023         -    doDigest
  2024         -  );
  2025         -  memset(&anchor, 0, sizeof(anchor));
  2026         -  pLast = &anchor;
  2027         -  *pnEvent = 0;
  2028         -  while( db_step(&q)==SQLITE_ROW ){
  2029         -    const char *zType = "";
  2030         -    p = fossil_malloc( sizeof(EmailEvent) );
  2031         -    pLast->pNext = p;
  2032         -    pLast = p;
  2033         -    p->type = db_column_text(&q, 3)[0];
  2034         -    p->needMod = db_column_int(&q, 4);
  2035         -    p->zFromName = 0;
  2036         -    p->pNext = 0;
  2037         -    switch( p->type ){
  2038         -      case 'c':  zType = "Check-In";        break;
  2039         -      case 'f':  zType = "Forum post";      break;
  2040         -      case 't':  zType = "Wiki Edit";       break;
  2041         -      case 'w':  zType = "Ticket Change";   break;
  2042         -    }
  2043         -    blob_init(&p->hdr, 0, 0);
  2044         -    blob_init(&p->txt, 0, 0);
  2045         -    blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
  2046         -      db_column_text(&q,1),
  2047         -      zType,
  2048         -      db_column_text(&q,2),
  2049         -      zUrl,
  2050         -      db_column_text(&q,0)
  2051         -    );
  2052         -    if( p->needMod ){
  2053         -      blob_appendf(&p->txt,
  2054         -        "** Pending moderator approval (%s/modreq) **\n",
  2055         -        zUrl
  2056         -      );
  2057         -    }
  2058         -    (*pnEvent)++;
  2059         -  }
  2060         -  db_finalize(&q);
  2061         -
  2062         -  /* Early-out if forumpost is not a table in this repository */
  2063         -  if( !db_table_exists("repository","forumpost") ){
  2064         -    return anchor.pNext;
  2065         -  }
  2066         -
  2067         -  /* For digests, the previous loop also handled forumposts already */
  2068         -  if( doDigest ){
  2069         -    return anchor.pNext;
  2070         -  }
  2071         -
  2072         -  /* If we reach this point, it means that forumposts exist and this
  2073         -  ** is a normal email alert.  Construct full-text forum post alerts
  2074         -  ** using a format that enables them to be sent as separate emails.
  2075         -  */
  2076         -  db_prepare(&q,
  2077         -    "SELECT"
  2078         -    " forumpost.fpid,"                                      /* 0 */
  2079         -    " (SELECT uuid FROM blob WHERE rid=forumpost.fpid),"    /* 1 */
  2080         -    " datetime(event.mtime),"                               /* 2 */
  2081         -    " substr(comment,instr(comment,':')+2),"                /* 3 */
  2082         -    " (SELECT uuid FROM blob WHERE rid=forumpost.firt),"    /* 4 */
  2083         -    " wantalert.needMod,"                                   /* 5 */
  2084         -    " coalesce(trim(substr(info,1,instr(info,'<')-1)),euser,user)"   /* 6 */
  2085         -    " FROM temp.wantalert, event, forumpost"
  2086         -    "      LEFT JOIN user ON (login=coalesce(euser,user))"
  2087         -    " WHERE event.objid=substr(wantalert.eventId,2)+0"
  2088         -    "   AND eventId GLOB 'f*'"
  2089         -    "   AND forumpost.fpid=event.objid"
  2090         -    " ORDER BY event.mtime"
  2091         -  );
  2092         -  zFrom = db_get("email-self",0);
  2093         -  zSub = db_get("email-subname","");
  2094         -  while( db_step(&q)==SQLITE_ROW ){
  2095         -    Manifest *pPost = manifest_get(db_column_int(&q,0), CFTYPE_FORUM, 0);
  2096         -    const char *zIrt;
  2097         -    const char *zUuid;
  2098         -    const char *zTitle;
  2099         -    const char *z;
  2100         -    if( pPost==0 ) continue;
  2101         -    p = fossil_malloc( sizeof(EmailEvent) );
  2102         -    pLast->pNext = p;
  2103         -    pLast = p;
  2104         -    p->type = 'f';
  2105         -    p->needMod = db_column_int(&q, 5);
  2106         -    z = db_column_text(&q,6);
  2107         -    p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
  2108         -    p->pNext = 0;
  2109         -    blob_init(&p->hdr, 0, 0);
  2110         -    zUuid = db_column_text(&q, 1);
  2111         -    zTitle = db_column_text(&q, 3);
  2112         -    if( p->needMod ){
  2113         -      blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n",
  2114         -                   zSub, zTitle);
  2115         -    }else{
  2116         -      blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle);
  2117         -      blob_appendf(&p->hdr, "Message-Id: <%.32s@%s>\r\n", 
  2118         -                   zUuid, alert_hostname(zFrom));
  2119         -      zIrt = db_column_text(&q, 4);
  2120         -      if( zIrt && zIrt[0] ){
  2121         -        blob_appendf(&p->hdr, "In-Reply-To: <%.32s@%s>\r\n",
  2122         -                     zIrt, alert_hostname(zFrom));
  2123         -      }
  2124         -    }
  2125         -    blob_init(&p->txt, 0, 0);
  2126         -    if( p->needMod ){
  2127         -      blob_appendf(&p->txt,
  2128         -        "** Pending moderator approval (%s/modreq) **\n",
  2129         -        zUrl
  2130         -      );
  2131         -    }
  2132         -    blob_appendf(&p->txt,
  2133         -      "Forum post by %s on %s\n",
  2134         -      pPost->zUser, db_column_text(&q, 2));
  2135         -    blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid);
  2136         -    blob_append(&p->txt, pPost->zWiki, -1);
  2137         -    manifest_destroy(pPost);
  2138         -    (*pnEvent)++;
  2139         -  }
  2140         -  db_finalize(&q);
  2141         -
  2142         -  return anchor.pNext;
  2143         -}
  2144         -
  2145         -/*
  2146         -** Put a header on an alert email
  2147         -*/
  2148         -void email_header(Blob *pOut){
  2149         -  blob_appendf(pOut,
  2150         -    "This is an automated email reporting changes "
  2151         -    "on Fossil repository %s (%s/timeline)\n",
  2152         -    db_get("email-subname","(unknown)"),
  2153         -    db_get("email-url","http://localhost:8080"));
  2154         -}
  2155         -
  2156         -/*
  2157         -** Append the "unsubscribe" notification and other footer text to
  2158         -** the end of an email alert being assemblied in pOut.
  2159         -*/
  2160         -void alert_footer(Blob *pOut){
  2161         -  blob_appendf(pOut, "\n-- \nTo unsubscribe: %s/unsubscribe\n",
  2162         -     db_get("email-url","http://localhost:8080"));
  2163         -}
  2164         -
  2165         -/*
  2166         -** COMMAND:  test-alert
  2167         -**
  2168         -** Usage: %fossil test-alert EVENTID ...
  2169         -**
  2170         -** Generate the text of an email alert for all of the EVENTIDs
  2171         -** listed on the command-line.  Or if no events are listed on the
  2172         -** command line, generate text for all events named in the
  2173         -** pending_alert table.
  2174         -**
  2175         -** This command is intended for testing and debugging the logic
  2176         -** that generates email alert text.
  2177         -**
  2178         -** Options:
  2179         -**
  2180         -**      --digest           Generate digest alert text
  2181         -**      --needmod          Assume all events are pending moderator approval
  2182         -*/
  2183         -void test_alert_cmd(void){
  2184         -  Blob out;
  2185         -  int nEvent;
  2186         -  int needMod;
  2187         -  int doDigest;
  2188         -  EmailEvent *pEvent, *p;
  2189         -
  2190         -  doDigest = find_option("digest",0,0)!=0;
  2191         -  needMod = find_option("needmod",0,0)!=0;
  2192         -  db_find_and_open_repository(0, 0);
  2193         -  verify_all_options();
  2194         -  db_begin_transaction();
  2195         -  alert_schema(0);
  2196         -  db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)");
  2197         -  if( g.argc==2 ){
  2198         -    db_multi_exec(
  2199         -      "INSERT INTO wantalert(eventId,needMod)"
  2200         -      " SELECT eventid, %d FROM pending_alert", needMod);
  2201         -  }else{
  2202         -    int i;
  2203         -    for(i=2; i<g.argc; i++){
  2204         -      db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)",
  2205         -           g.argv[i], needMod);
  2206         -    }
  2207         -  }
  2208         -  blob_init(&out, 0, 0);
  2209         -  email_header(&out);
  2210         -  pEvent = alert_compute_event_text(&nEvent, doDigest);
  2211         -  for(p=pEvent; p; p=p->pNext){
  2212         -    blob_append(&out, "\n", 1);
  2213         -    if( blob_size(&p->hdr) ){
  2214         -      blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr));
  2215         -      blob_append(&out, "\n", 1);
  2216         -    }
  2217         -    blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
  2218         -  }
  2219         -  alert_free_eventlist(pEvent);
  2220         -  alert_footer(&out);
  2221         -  fossil_print("%s", blob_str(&out));
  2222         -  blob_reset(&out);
  2223         -  db_end_transaction(0);
  2224         -}
  2225         -
  2226         -/*
  2227         -** COMMAND:  test-add-alerts
  2228         -**
  2229         -** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ...
  2230         -**
  2231         -** Add one or more events to the pending_alert queue.  Use this
  2232         -** command during testing to force email notifications for specific
  2233         -** events.
  2234         -**
  2235         -** EVENTIDs are text.  The first character is 'c', 'f', 't', or 'w'
  2236         -** for check-in, forum, ticket, or wiki.  The remaining text is a
  2237         -** integer that references the EVENT.OBJID value for the event.
  2238         -** Run /timeline?showid to see these OBJID values.
  2239         -**
  2240         -** Options:
  2241         -**
  2242         -**    --backoffice        Run alert_backoffice() after all alerts have
  2243         -**                        been added.  This will cause the alerts to be
  2244         -**                        sent out with the SENDALERT_TRACE option.
  2245         -**
  2246         -**    --debug             Like --backoffice, but add the SENDALERT_STDOUT
  2247         -**                        so that emails are printed to standard output
  2248         -**                        rather than being sent.
  2249         -**
  2250         -**    --digest            Process emails using SENDALERT_DIGEST
  2251         -*/
  2252         -void test_add_alert_cmd(void){
  2253         -  int i;
  2254         -  int doAuto = find_option("backoffice",0,0)!=0;
  2255         -  unsigned mFlags = 0;
  2256         -  if( find_option("debug",0,0)!=0 ){
  2257         -    doAuto = 1;
  2258         -    mFlags = SENDALERT_STDOUT;
  2259         -  }
  2260         -  if( find_option("digest",0,0)!=0 ){
  2261         -    mFlags |= SENDALERT_DIGEST;
  2262         -  }
  2263         -  db_find_and_open_repository(0, 0);
  2264         -  verify_all_options();
  2265         -  db_begin_write();
  2266         -  alert_schema(0);
  2267         -  for(i=2; i<g.argc; i++){
  2268         -    db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
  2269         -  }
  2270         -  db_end_transaction(0);
  2271         -  if( doAuto ){
  2272         -    alert_backoffice(SENDALERT_TRACE|mFlags);
  2273         -  }
  2274         -}
  2275         -
  2276         -#if INTERFACE
  2277         -/*
  2278         -** Flags for alert_send_alerts()
  2279         -*/
  2280         -#define SENDALERT_DIGEST      0x0001    /* Send a digest */
  2281         -#define SENDALERT_PRESERVE    0x0002    /* Do not mark the task as done */
  2282         -#define SENDALERT_STDOUT      0x0004    /* Print emails instead of sending */
  2283         -#define SENDALERT_TRACE       0x0008    /* Trace operation for debugging */
  2284         -
  2285         -#endif /* INTERFACE */
  2286         -
  2287         -/*
  2288         -** Send alert emails to subscribers.
  2289         -**
  2290         -** This procedure is run by either the backoffice, or in response to the
  2291         -** "fossil alerts send" command.  Details of operation are controlled by
  2292         -** the flags parameter.
  2293         -**
  2294         -** Here is a summary of what happens:
  2295         -**
  2296         -**   (1) Create a TEMP table wantalert(eventId,needMod) and fill it with
  2297         -**       all the events that we want to send alerts about.  The needMod
  2298         -**       flags is set if and only if the event is still awaiting
  2299         -**       moderator approval.  Events with the needMod flag are only
  2300         -**       shown to users that have moderator privileges.
  2301         -**
  2302         -**   (2) Call alert_compute_event_text() to compute a list of EmailEvent
  2303         -**       objects that describe all events about which we want to send
  2304         -**       alerts.
  2305         -**
  2306         -**   (3) Loop over all subscribers.  Compose and send one or more email
  2307         -**       messages to each subscriber that describe the events for
  2308         -**       which the subscriber has expressed interest and has
  2309         -**       appropriate privileges.
  2310         -**
  2311         -**   (4) Update the pending_alerts table to indicate that alerts have been
  2312         -**       sent.
  2313         -**
  2314         -** Update 2018-08-09:  Do step (3) before step (4).  Update the
  2315         -** pending_alerts table *before* the emails are sent.  That way, if
  2316         -** the process malfunctions or crashes, some notifications may never
  2317         -** be sent.  But that is better than some recurring bug causing
  2318         -** subscribers to be flooded with repeated notifications every 60
  2319         -** seconds!
  2320         -*/
  2321         -void alert_send_alerts(u32 flags){
  2322         -  EmailEvent *pEvents, *p;
  2323         -  int nEvent = 0;
  2324         -  Stmt q;
  2325         -  const char *zDigest = "false";
  2326         -  Blob hdr, body;
  2327         -  const char *zUrl;
  2328         -  const char *zRepoName;
  2329         -  const char *zFrom;
  2330         -  const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;
  2331         -  AlertSender *pSender = 0;
  2332         -  u32 senderFlags = 0;
  2333         -
  2334         -  if( g.fSqlTrace ) fossil_trace("-- BEGIN alert_send_alerts(%u)\n", flags);
  2335         -  alert_schema(0);
  2336         -  if( !alert_enabled() ) goto send_alert_done;
  2337         -  zUrl = db_get("email-url",0);
  2338         -  if( zUrl==0 ) goto send_alert_done;
  2339         -  zRepoName = db_get("email-subname",0);
  2340         -  if( zRepoName==0 ) goto send_alert_done;
  2341         -  zFrom = db_get("email-self",0);
  2342         -  if( zFrom==0 ) goto send_alert_done;
  2343         -  if( flags & SENDALERT_TRACE ){
  2344         -    senderFlags |= ALERT_TRACE;
  2345         -  }
  2346         -  pSender = alert_sender_new(zDest, senderFlags);
  2347         -
  2348         -  /* Step (1):  Compute the alerts that need sending
  2349         -  */
  2350         -  db_multi_exec(
  2351         -    "DROP TABLE IF EXISTS temp.wantalert;"
  2352         -    "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);"
  2353         -  );
  2354         -  if( flags & SENDALERT_DIGEST ){
  2355         -    /* Unmoderated changes are never sent as part of a digest */
  2356         -    db_multi_exec(
  2357         -      "INSERT INTO wantalert(eventId,needMod)"
  2358         -      " SELECT eventid, 0"
  2359         -      "   FROM pending_alert"
  2360         -      "  WHERE sentDigest IS FALSE"
  2361         -      "    AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));"
  2362         -    );
  2363         -    zDigest = "true";
  2364         -  }else{
  2365         -    /* Immediate alerts might include events that are subject to
  2366         -    ** moderator approval */
  2367         -    db_multi_exec(
  2368         -      "INSERT INTO wantalert(eventId,needMod,sentMod)"
  2369         -      " SELECT eventid,"
  2370         -      "        EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2)),"
  2371         -      "        sentMod"
  2372         -      "   FROM pending_alert"
  2373         -      "  WHERE sentSep IS FALSE;"
  2374         -      "DELETE FROM wantalert WHERE needMod AND sentMod;"
  2375         -    );
  2376         -  }
  2377         -
  2378         -  /* Step 2: compute EmailEvent objects for every notification that
  2379         -  ** needs sending.
  2380         -  */
  2381         -  pEvents = alert_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0);
  2382         -  if( nEvent==0 ) goto send_alert_done;
  2383         -
  2384         -  /* Step 4a: Update the pending_alerts table to designate the
  2385         -  ** alerts as having all been sent.  This is done *before* step (3)
  2386         -  ** so that a crash will not cause alerts to be sent multiple times.
  2387         -  ** Better a missed alert than being spammed with hundreds of alerts
  2388         -  ** due to a bug.
  2389         -  */
  2390         -  if( (flags & SENDALERT_PRESERVE)==0 ){
  2391         -    if( flags & SENDALERT_DIGEST ){
  2392         -      db_multi_exec(
  2393         -        "UPDATE pending_alert SET sentDigest=true"
  2394         -        " WHERE eventid IN (SELECT eventid FROM wantalert);"
  2395         -      );
  2396         -    }else{
  2397         -      db_multi_exec(
  2398         -        "UPDATE pending_alert SET sentSep=true"
  2399         -        " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);"
  2400         -        "UPDATE pending_alert SET sentMod=true"
  2401         -        " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);"
  2402         -      );
  2403         -    }
  2404         -  }
  2405         -
  2406         -  /* Step 3: Loop over subscribers.  Send alerts
  2407         -  */
  2408         -  blob_init(&hdr, 0, 0);
  2409         -  blob_init(&body, 0, 0);
  2410         -  db_prepare(&q,
  2411         -     "SELECT"
  2412         -     " hex(subscriberCode),"  /* 0 */
  2413         -     " semail,"               /* 1 */
  2414         -     " ssub,"                 /* 2 */
  2415         -     " fullcap(user.cap)"     /* 3 */
  2416         -     " FROM subscriber LEFT JOIN user ON (login=suname)"
  2417         -     " WHERE sverified AND NOT sdonotcall"
  2418         -     "  AND sdigest IS %s",
  2419         -     zDigest/*safe-for-%s*/
  2420         -  );
  2421         -  while( db_step(&q)==SQLITE_ROW ){
  2422         -    const char *zCode = db_column_text(&q, 0);
  2423         -    const char *zSub = db_column_text(&q, 2);
  2424         -    const char *zEmail = db_column_text(&q, 1);
  2425         -    const char *zCap = db_column_text(&q, 3);
  2426         -    int nHit = 0;
  2427         -    for(p=pEvents; p; p=p->pNext){
  2428         -      if( strchr(zSub,p->type)==0 ) continue;
  2429         -      if( p->needMod ){
  2430         -        /* For events that require moderator approval, only send an alert
  2431         -        ** if the recipient is a moderator for that type of event */
  2432         -        char xType = '*';
  2433         -        switch( p->type ){
  2434         -          case 'f':  xType = '5';  break;
  2435         -          case 't':  xType = 'q';  break;
  2436         -          case 'w':  xType = 'l';  break;
  2437         -        }
  2438         -        if( strchr(zCap,xType)==0 ) continue;
  2439         -      }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
  2440         -        /* Setup and admin users can get any notification that does not
  2441         -        ** require moderation */
  2442         -      }else{
  2443         -        /* Other users only see the alert if they have sufficient
  2444         -        ** privilege to view the event itself */
  2445         -        char xType = '*';
  2446         -        switch( p->type ){
  2447         -          case 'c':  xType = 'o';  break;
  2448         -          case 'f':  xType = '2';  break;
  2449         -          case 't':  xType = 'r';  break;
  2450         -          case 'w':  xType = 'j';  break;
  2451         -        }
  2452         -        if( strchr(zCap,xType)==0 ) continue;
  2453         -      }
  2454         -      if( blob_size(&p->hdr)>0 ){
  2455         -        /* This alert should be sent as a separate email */
  2456         -        Blob fhdr, fbody;
  2457         -        blob_init(&fhdr, 0, 0);
  2458         -        blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
  2459         -        blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
  2460         -        blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
  2461         -        blob_appendf(&fbody, "\n-- \nSubscription info: %s/alerts/%s\n",
  2462         -           zUrl, zCode);
  2463         -        alert_send(pSender,&fhdr,&fbody,p->zFromName);
  2464         -        blob_reset(&fhdr);
  2465         -        blob_reset(&fbody);
  2466         -      }else{
  2467         -        /* Events other than forum posts are gathered together into
  2468         -        ** a single email message */
  2469         -        if( nHit==0 ){
  2470         -          blob_appendf(&hdr,"To: <%s>\r\n", zEmail);
  2471         -          blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName);
  2472         -          blob_appendf(&body,
  2473         -            "This is an automated email sent by the Fossil repository "
  2474         -            "at %s to report changes.\n",
  2475         -            zUrl
  2476         -          );
  2477         -        }
  2478         -        nHit++;
  2479         -        blob_append(&body, "\n", 1);
  2480         -        blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
  2481         -      }
  2482         -    }
  2483         -    if( nHit==0 ) continue;
  2484         -    blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
  2485         -         zUrl, zCode);
  2486         -    alert_send(pSender,&hdr,&body,0);
  2487         -    blob_truncate(&hdr, 0);
  2488         -    blob_truncate(&body, 0);
  2489         -  }
  2490         -  blob_reset(&hdr);
  2491         -  blob_reset(&body);
  2492         -  db_finalize(&q);
  2493         -  alert_free_eventlist(pEvents);
  2494         -
  2495         -  /* Step 4b: Update the pending_alerts table to remove all of the
  2496         -  ** alerts that have been completely sent.
  2497         -  */
  2498         -  db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep;");
  2499         -
  2500         -send_alert_done:
  2501         -  alert_sender_free(pSender);
  2502         -  if( g.fSqlTrace ) fossil_trace("-- END alert_send_alerts(%u)\n", flags);
  2503         -}
  2504         -
  2505         -/*
  2506         -** Do backoffice processing for email notifications.  In other words,
  2507         -** check to see if any email notifications need to occur, and then
  2508         -** do them.
  2509         -**
  2510         -** This routine is intended to run in the background, after webpages.
  2511         -**
  2512         -** The mFlags option is zero or more of the SENDALERT_* flags.  Normally
  2513         -** this flag is zero, but the test-set-alert command sets it to
  2514         -** SENDALERT_TRACE.
  2515         -*/
  2516         -void alert_backoffice(u32 mFlags){
  2517         -  int iJulianDay;
  2518         -  if( !alert_tables_exist() ) return;
  2519         -  alert_send_alerts(mFlags);
  2520         -  iJulianDay = db_int(0, "SELECT julianday('now')");
  2521         -  if( iJulianDay>db_get_int("email-last-digest",0) ){
  2522         -    db_set_int("email-last-digest",iJulianDay,0);
  2523         -    alert_send_alerts(SENDALERT_DIGEST|mFlags);
  2524         -  }
  2525         -}
  2526         -
  2527         -/*
  2528         -** WEBPAGE: contact_admin
  2529         -**
  2530         -** A web-form to send an email message to the repository administrator,
  2531         -** or (with appropriate permissions) to anybody.
  2532         -*/
  2533         -void contact_admin_page(void){
  2534         -  const char *zAdminEmail = db_get("email-admin",0);
  2535         -  unsigned int uSeed = 0;
  2536         -  const char *zDecoded;
  2537         -  char *zCaptcha = 0;
  2538         -
  2539         -  login_check_credentials();
  2540         -  if( zAdminEmail==0 || zAdminEmail[0]==0 ){
  2541         -    style_header("Outbound Email Disabled");
  2542         -    @ <p>Outbound email is disabled on this repository
  2543         -    style_footer();
  2544         -    return;
  2545         -  }
  2546         -  if( P("submit")!=0 
  2547         -   && P("subject")!=0
  2548         -   && P("msg")!=0
  2549         -   && P("from")!=0
  2550         -   && cgi_csrf_safe(1)
  2551         -   && captcha_is_correct(0)
  2552         -  ){
  2553         -    Blob hdr, body;
  2554         -    AlertSender *pSender = alert_sender_new(0,0);
  2555         -    blob_init(&hdr, 0, 0);
  2556         -    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s administrator message\r\n",
  2557         -                 zAdminEmail, db_get("email-subname","Fossil Repo"));
  2558         -    blob_init(&body, 0, 0);
  2559         -    blob_appendf(&body, "Message from [%s]\n", PT("from")/*safe-for-%s*/);
  2560         -    blob_appendf(&body, "Subject: [%s]\n\n", PT("subject")/*safe-for-%s*/);
  2561         -    blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  2562         -    alert_send(pSender, &hdr, &body, 0);
  2563         -    style_header("Message Sent");
  2564         -    if( pSender->zErr ){
  2565         -      @ <h1>Internal Error</h1>
  2566         -      @ <p>The following error was reported by the system:
  2567         -      @ <blockquote><pre>
  2568         -      @ %h(pSender->zErr)
  2569         -      @ </pre></blockquote>
  2570         -    }else{
  2571         -      @ <p>Your message has been sent to the repository administrator.
  2572         -      @ Thank you for your input.</p>
  2573         -    }
  2574         -    alert_sender_free(pSender);
  2575         -    style_footer();
  2576         -    return;
  2577         -  }
  2578         -  if( captcha_needed() ){
  2579         -    uSeed = captcha_seed();
  2580         -    zDecoded = captcha_decode(uSeed);
  2581         -    zCaptcha = captcha_render(zDecoded);
  2582         -  }
  2583         -  style_header("Message To Administrator");
  2584         -  form_begin(0, "%R/contact_admin");
  2585         -  @ <p>Enter a message to the repository administrator below:</p>
  2586         -  @ <table class="subscribe">
  2587         -  if( zCaptcha ){
  2588         -    @ <tr>
  2589         -    @  <td class="form_label">Security&nbsp;Code:</td>
  2590         -    @  <td><input type="text" name="captcha" value="" size="10">
  2591         -    @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
  2592         -    @ </tr>
  2593         -  }
  2594         -  @ <tr>
  2595         -  @  <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
  2596         -  @  <td><input type="text" name="from" value="%h(PT("from"))" size="30"></td>
  2597         -  @ </tr>
  2598         -  @ <tr>
  2599         -  @  <td class="form_label">Subject:</td>
  2600         -  @  <td><input type="text" name="subject" value="%h(PT("subject"))"\
  2601         -  @  size="80"></td>
  2602         -  @ </tr>
  2603         -  @ <tr>
  2604         -  @  <td class="form_label">Message:</td>
  2605         -  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
  2606         -  @ %h(PT("msg"))</textarea>
  2607         -  @ </tr>
  2608         -  @ <tr>
  2609         -  @   <td></td>
  2610         -  @   <td><input type="submit" name="submit" value="Send Message">
  2611         -  @ </tr>
  2612         -  @ </table>
  2613         -  if( zCaptcha ){
  2614         -    @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  2615         -    @ %h(zCaptcha)
  2616         -    @ </pre>
  2617         -    @ Enter the 8 characters above in the "Security Code" box
  2618         -    @ </td></tr></table></div>
  2619         -  }
  2620         -  @ </form>
  2621         -  style_footer();
  2622         -}
  2623         -
  2624         -/*
  2625         -** Send an annoucement message described by query parameter.
  2626         -** Permission to do this has already been verified.
  2627         -*/
  2628         -static char *alert_send_announcement(void){
  2629         -  AlertSender *pSender;
  2630         -  char *zErr;
  2631         -  const char *zTo = PT("to");
  2632         -  char *zSubject = PT("subject");
  2633         -  int bAll = PB("all");
  2634         -  int bAA = PB("aa");
  2635         -  const char *zSub = db_get("email-subname", "[Fossil Repo]");
  2636         -  int bTest2 = fossil_strcmp(P("name"),"test2")==0;
  2637         -  Blob hdr, body;
  2638         -  blob_init(&body, 0, 0);
  2639         -  blob_init(&hdr, 0, 0);
  2640         -  blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  2641         -  pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
  2642         -  if( zTo[0] ){
  2643         -    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
  2644         -    alert_send(pSender, &hdr, &body, 0);
  2645         -  }
  2646         -  if( bAll || bAA ){
  2647         -    Stmt q;
  2648         -    int nUsed = blob_size(&body);
  2649         -    const char *zURL =  db_get("email-url",0);
  2650         -    db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
  2651         -                   " WHERE sverified AND NOT sdonotcall %s",
  2652         -                   bAll ? "" : " AND ssub LIKE '%a%'");
  2653         -    while( db_step(&q)==SQLITE_ROW ){
  2654         -      const char *zCode = db_column_text(&q, 1);
  2655         -      zTo = db_column_text(&q, 0);
  2656         -      blob_truncate(&hdr, 0);
  2657         -      blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
  2658         -      if( zURL ){
  2659         -        blob_truncate(&body, nUsed);
  2660         -        blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
  2661         -           zURL, zCode);
  2662         -      }
  2663         -      alert_send(pSender, &hdr, &body, 0);
  2664         -    }
  2665         -    db_finalize(&q);
  2666         -  }
  2667         -  if( bTest2 ){
  2668         -    /* If the URL is /announce/test2 instead of just /announce, then no
  2669         -    ** email is actually sent.  Instead, the text of the email that would
  2670         -    ** have been sent is displayed in the result window. */
  2671         -    @ <pre style='border: 2px solid blue; padding: 1ex'>
  2672         -    @ %h(blob_str(&pSender->out))
  2673         -    @ </pre>
  2674         -  }
  2675         -  zErr = pSender->zErr;
  2676         -  pSender->zErr = 0;
  2677         -  alert_sender_free(pSender);
  2678         -  return zErr;
  2679         -}
  2680         -
  2681         -
  2682         -/*
  2683         -** WEBPAGE: announce
  2684         -**
  2685         -** A web-form, available to users with the "Send-Announcement" or "A"
  2686         -** capability, that allows one to send announcements to whomever
  2687         -** has subscribed to receive announcements.  The administrator can
  2688         -** also send a message to an arbitrary email address and/or to all
  2689         -** subscribers regardless of whether or not they have elected to
  2690         -** receive announcements.
  2691         -*/
  2692         -void announce_page(void){
  2693         -  login_check_credentials();
  2694         -  if( !g.perm.Announce ){
  2695         -    login_needed(0);
  2696         -    return;
  2697         -  }
  2698         -  if( fossil_strcmp(P("name"),"test1")==0 ){
  2699         -    /* Visit the /announce/test1 page to see the CGI variables */
  2700         -    @ <p style='border: 1px solid black; padding: 1ex;'>
  2701         -    cgi_print_all(0, 0);
  2702         -    @ </p>
  2703         -  }else
  2704         -  if( P("submit")!=0 && cgi_csrf_safe(1) ){
  2705         -    char *zErr = alert_send_announcement();
  2706         -    style_header("Announcement Sent");
  2707         -    if( zErr ){
  2708         -      @ <h1>Internal Error</h1>
  2709         -      @ <p>The following error was reported by the system:
  2710         -      @ <blockquote><pre>
  2711         -      @ %h(zErr)
  2712         -      @ </pre></blockquote>
  2713         -    }else{
  2714         -      @ <p>The announcement has been sent.</p>
  2715         -    }
  2716         -    style_footer();    
  2717         -    return;
  2718         -  }
  2719         -  style_header("Send Announcement");
  2720         -  @ <form method="POST">
  2721         -  @ <table class="subscribe">
  2722         -  if( g.perm.Admin ){
  2723         -    int aa = PB("aa");
  2724         -    int all = PB("all");
  2725         -    const char *aack = aa ? "checked" : "";
  2726         -    const char *allck = all ? "checked" : "";
  2727         -    @ <tr>
  2728         -    @  <td class="form_label">To:</td>
  2729         -    @  <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
  2730         -    @  <label><input type="checkbox" name="aa" %s(aack)> \
  2731         -    @  All "announcement" subscribers</label> \
  2732         -    @  <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
  2733         -    @  <label><input type="checkbox" name="all" %s(allck)> \
  2734         -    @  All subscribers</label> \
  2735         -    @  <a href="%R/subscribers" target="_blank">(list)</a><br></td>
  2736         -    @ </tr>
  2737         -  }
  2738         -  @ <tr>
  2739         -  @  <td class="form_label">Subject:</td>
  2740         -  @  <td><input type="text" name="subject" value="%h(PT("subject"))"\
  2741         -  @  size="80"></td>
  2742         -  @ </tr>
  2743         -  @ <tr>
  2744         -  @  <td class="form_label">Message:</td>
  2745         -  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
  2746         -  @ %h(PT("msg"))</textarea>
  2747         -  @ </tr>
  2748         -  @ <tr>
  2749         -  @   <td></td>
  2750         -  @   <td><input type="submit" name="submit" value="Send Message">
  2751         -  @ </tr>
  2752         -  @ </table>
  2753         -  @ </form>
  2754         -  style_footer();
  2755         -}

Changes to src/attach.c.

   104    104       }else if( type==2 ){
   105    105         zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
   106    106       }else{
   107    107         zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
   108    108       }
   109    109       @ <li><p>
   110    110       @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>
   111         -    moderation_pending_www(attachid);
          111  +    if( moderation_pending(attachid) ){
          112  +      @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
          113  +    }
   112    114       @ <br /><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
   113         -    @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br>
          115  +    @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br />
   114    116       if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
   115    117       if( zComment && zComment[0] ){
   116    118         @ %!W(zComment)<br />
   117    119       }
   118    120       if( zPage==0 && zTkt==0 && zTechNote==0 ){
   119    121         if( zSrc==0 || zSrc[0]==0 ){
   120    122           zSrc = "Deleted from";
................................................................................
   373    375       zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
   374    376                             zTkt, zTkt);
   375    377     }
   376    378     if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
   377    379     if( P("cancel") ){
   378    380       cgi_redirect(zFrom);
   379    381     }
   380         -  if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
          382  +  if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){
   381    383       int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
   382    384                           (zPage!=0 && wiki_need_moderation(0));
   383    385       const char *zComment = PD("comment", "");
   384    386       attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
   385    387       cgi_redirect(zFrom);
   386    388     }
   387    389     style_header("Add Attachment");
................................................................................
   560    562     @ <div class="section">Overview</div>
   561    563     @ <p><table class="label-value">
   562    564     @ <tr><th>Artifact&nbsp;ID:</th>
   563    565     @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
   564    566     if( g.perm.Setup ){
   565    567       @ (%d(rid))
   566    568     }
   567         -  modPending = moderation_pending_www(rid);
          569  +  modPending = moderation_pending(rid);
          570  +  if( modPending ){
          571  +    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
          572  +  }
   568    573     if( zTktUuid ){
   569    574       @ <tr><th>Ticket:</th>
   570    575       @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
   571    576     }
   572    577     if( zTNUuid ){
   573    578       @ <tr><th>Tech Note:</th>
   574    579       @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>

Deleted src/backoffice.c.

     1         -/*
     2         -** Copyright (c) 2018 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 used to manage a background processes that
    19         -** occur after user interaction with the repository.  Examples of
    20         -** backoffice processing includes:
    21         -**
    22         -**    *  Sending alerts and notifications
    23         -**    *  Processing the email queue
    24         -**    *  Automatically syncing to peer repositories
    25         -**
    26         -** Backoffice processing is automatically started whenever there are
    27         -** changes to the repository.  The backoffice process dies off after
    28         -** a period of inactivity.
    29         -**
    30         -** Steps are taken to ensure that only a single backoffice process is
    31         -** running at a time.  Otherwise, there could be race conditions that
    32         -** cause adverse effects such as multiple alerts for the same changes.
    33         -**
    34         -** At the same time, we do not want a backoffice process to run forever.
    35         -** Backoffice processes should die off after doing whatever work they need
    36         -** to do.  In this way, we avoid having lots of idle processes in the
    37         -** process table, doing nothing on rarely accessed repositories, and
    38         -** if the Fossil binary is updated on a system, the backoffice processes
    39         -** will restart using the new binary automatically.
    40         -**
    41         -** At any point in time there should be at most two backoffice processes.
    42         -** There is a main process that is doing the actually work, and there is
    43         -** a second stand-by process that is waiting for the main process to finish
    44         -** and that will become the main process after a delay.
    45         -**
    46         -** After any successful web page reply, the backoffice_check_if_needed()
    47         -** routine is called.  That routine checks to see if both one or both of
    48         -** the backoffice processes are already running.  That routine remembers the
    49         -** status in a global variable.
    50         -**
    51         -** Later, after the repository database is closed, the
    52         -** backoffice_run_if_needed() routine is called.  If the prior call
    53         -** to backoffice_check_if_needed() indicated that backoffice processing
    54         -** might be required, the run_if_needed() attempts to kick off a backoffice
    55         -** process.
    56         -**
    57         -** All work performance by the backoffice is in the backoffice_work()
    58         -** routine.
    59         -*/
    60         -#if defined(_WIN32)
    61         -# if defined(_WIN32_WINNT)
    62         -#  undef _WIN32_WINNT
    63         -# endif
    64         -# define _WIN32_WINNT 0x501
    65         -#endif
    66         -#include "config.h"
    67         -#include "backoffice.h"
    68         -#include <time.h>
    69         -#if defined(_WIN32)
    70         -# include <windows.h>
    71         -# include <stdio.h>
    72         -# include <process.h>
    73         -# if defined(__MINGW32__)
    74         -#  include <wchar.h>
    75         -# endif
    76         -# define GETPID (int)GetCurrentProcessId
    77         -#else
    78         -# include <unistd.h>
    79         -# include <sys/types.h>
    80         -# include <signal.h>
    81         -# include <errno.h>
    82         -# include <fcntl.h>
    83         -# define GETPID getpid
    84         -#endif
    85         -
    86         -/*
    87         -** The BKOFCE_LEASE_TIME is the amount of time for which a single backoffice
    88         -** processing run is valid.  Each backoffice run monopolizes the lease for
    89         -** at least this amount of time.  Hopefully all backoffice processing is
    90         -** finished much faster than this - usually in less than a second.  But
    91         -** regardless of how long each invocation lasts, successive backoffice runs
    92         -** must be spaced out by at least this much time.
    93         -*/
    94         -#define BKOFCE_LEASE_TIME   60    /* Length of lease validity in seconds */
    95         -
    96         -#if LOCAL_INTERFACE
    97         -/*
    98         -** An instance of the following object describes a lease on the backoffice
    99         -** processing timeslot.  This lease is used to help ensure that no more than
   100         -** one process is running backoffice at a time.
   101         -*/
   102         -struct Lease {
   103         -  sqlite3_uint64 idCurrent; /* process ID for the current lease holder */
   104         -  sqlite3_uint64 tmCurrent; /* Expiration of the current lease */
   105         -  sqlite3_uint64 idNext;    /* process ID for the next lease holder on queue */
   106         -  sqlite3_uint64 tmNext;    /* Expiration of the next lease */
   107         -};
   108         -#endif
   109         -
   110         -/***************************************************************************
   111         -** Local state variables
   112         -**
   113         -** Set to prevent backoffice processing from ever entering sleep or
   114         -** otherwise taking a long time to complete.  Set this when a user-visible
   115         -** process might need to wait for backoffice to complete.
   116         -*/
   117         -static int backofficeNoDelay = 0;
   118         -
   119         -/* This variable is set to the name of a database on which backoffice
   120         -** should run if backoffice process is needed.  It is set by the
   121         -** backoffice_check_if_needed() routine which must be run while the database
   122         -** file is open.  Later, after the database is closed, the
   123         -** backoffice_run_if_needed() will consult this variable to see if it
   124         -** should be a no-op.
   125         -*/
   126         -static char *backofficeDb = 0;
   127         -
   128         -/* End of state variables
   129         -****************************************************************************/
   130         -
   131         -/*
   132         -** This function emits a diagnostic message related to the processing in
   133         -** this module.
   134         -*/
   135         -#if defined(_WIN32)
   136         -# define BKOFCE_ALWAYS_TRACE   (1)
   137         -extern void sqlite3_win32_write_debug(const char *, int);
   138         -#else
   139         -# define BKOFCE_ALWAYS_TRACE   (0)
   140         -#endif
   141         -static void backofficeTrace(const char *zFormat, ...){
   142         -  char *zMsg = 0;
   143         -  if( BKOFCE_ALWAYS_TRACE || g.fAnyTrace ){
   144         -    va_list ap;
   145         -    va_start(ap, zFormat);
   146         -    zMsg = sqlite3_vmprintf(zFormat, ap);
   147         -    va_end(ap);
   148         -#if defined(_WIN32)
   149         -    sqlite3_win32_write_debug(zMsg, -1);
   150         -#endif
   151         -  }
   152         -  if( g.fAnyTrace ) fprintf(stderr, "%s", zMsg);
   153         -  if( zMsg ) sqlite3_free(zMsg);
   154         -}
   155         -
   156         -/*
   157         -** Do not allow backoffice processes to sleep waiting on a timeslot.
   158         -** They must either do their work immediately or exit.
   159         -**
   160         -** In a perfect world, this interface would not exist, as there would
   161         -** never be a problem with waiting backoffice threads.  But in some cases
   162         -** a backoffice will delay a UI thread, so we don't want them to run for
   163         -** longer than needed.
   164         -*/
   165         -void backoffice_no_delay(void){
   166         -  backofficeNoDelay = 1;
   167         -}
   168         -
   169         -/*
   170         -** Sleeps for the specified number of milliseconds -OR- until interrupted
   171         -** by another thread (if supported by the underlying platform).  Non-zero
   172         -** will be returned if the sleep was interrupted.
   173         -*/
   174         -static int backofficeSleep(int milliseconds){
   175         -#if defined(_WIN32)
   176         -  assert( milliseconds>=0 );
   177         -  if( SleepEx((DWORD)milliseconds, TRUE)==WAIT_IO_COMPLETION ){
   178         -    return 1;
   179         -  }
   180         -#else
   181         -  sqlite3_sleep(milliseconds);
   182         -#endif
   183         -  return 0;
   184         -}
   185         -
   186         -/*
   187         -** Parse a unsigned 64-bit integer from a string.  Return a pointer
   188         -** to the character of z[] that occurs after the integer.
   189         -*/
   190         -static const char *backofficeParseInt(const char *z, sqlite3_uint64 *pVal){
   191         -  *pVal = 0;
   192         -  if( z==0 ) return 0;
   193         -  while( fossil_isspace(z[0]) ){ z++; }
   194         -  while( fossil_isdigit(z[0]) ){
   195         -    *pVal = (*pVal)*10 + z[0] - '0';
   196         -    z++;
   197         -  }
   198         -  return z;
   199         -}
   200         -
   201         -/*
   202         -** Read the "backoffice" property and parse it into a Lease object.
   203         -**
   204         -** The backoffice property should consist of four integers:
   205         -**
   206         -**    (1)  Process ID for the active backoffice process.
   207         -**    (2)  Time (seconds since 1970) for when the active backoffice
   208         -**         lease expires.
   209         -**    (3)  Process ID for the on-deck backoffice process.
   210         -**    (4)  Time when the on-deck process should expire.
   211         -**
   212         -** No other process should start active backoffice processing until
   213         -** process (1) no longer exists and the current time exceeds (2).
   214         -*/
   215         -static void backofficeReadLease(Lease *pLease){
   216         -  Stmt q;
   217         -  memset(pLease, 0, sizeof(*pLease));
   218         -  db_prepare(&q, "SELECT value FROM repository.config"
   219         -                 " WHERE name='backoffice'");
   220         -  if( db_step(&q)==SQLITE_ROW ){
   221         -    const char *z = db_column_text(&q,0);
   222         -    z = backofficeParseInt(z, &pLease->idCurrent);
   223         -    z = backofficeParseInt(z, &pLease->tmCurrent);
   224         -    z = backofficeParseInt(z, &pLease->idNext);
   225         -    backofficeParseInt(z, &pLease->tmNext);
   226         -  }
   227         -  db_finalize(&q);
   228         -}
   229         -
   230         -/*
   231         -** Return a string that describes how long it has been since the
   232         -** last backoffice run.  The string is obtained from fossil_malloc().
   233         -*/
   234         -char *backoffice_last_run(void){
   235         -  Lease x;
   236         -  sqlite3_uint64 tmNow;
   237         -  double rAge;
   238         -  backofficeReadLease(&x);
   239         -  tmNow = time(0);
   240         -  if( x.tmCurrent==0 ){
   241         -    return fossil_strdup("never");
   242         -  }
   243         -  if( tmNow<=(x.tmCurrent-BKOFCE_LEASE_TIME) ){
   244         -    return fossil_strdup("moments ago");
   245         -  }
   246         -  rAge = (tmNow - (x.tmCurrent-BKOFCE_LEASE_TIME))/86400.0;
   247         -  return mprintf("%z ago", human_readable_age(rAge));
   248         -}
   249         -
   250         -/*
   251         -** Write a lease to the backoffice property
   252         -*/
   253         -static void backofficeWriteLease(Lease *pLease){
   254         -  db_multi_exec(
   255         -    "REPLACE INTO repository.config(name,value,mtime)"
   256         -    " VALUES('backoffice','%lld %lld %lld %lld',now())",
   257         -    pLease->idCurrent, pLease->tmCurrent,
   258         -    pLease->idNext, pLease->tmNext);
   259         -}
   260         -
   261         -/*
   262         -** Check to see if the specified Win32 process is still alive.  It
   263         -** should be noted that even if this function returns non-zero, the
   264         -** process may die before another operation on it can be completed.
   265         -*/
   266         -#if defined(_WIN32)
   267         -#ifndef PROCESS_QUERY_LIMITED_INFORMATION
   268         -#  define PROCESS_QUERY_LIMITED_INFORMATION  (0x1000)
   269         -#endif
   270         -static int backofficeWin32ProcessExists(DWORD dwProcessId){
   271         -  HANDLE hProcess;
   272         -  hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,FALSE,dwProcessId);
   273         -  if( hProcess==NULL ) return 0;
   274         -  CloseHandle(hProcess);
   275         -  return 1;
   276         -}
   277         -#endif
   278         -
   279         -/*
   280         -** Check to see if the process identified by pid is alive.  If
   281         -** we cannot prove the the process is dead, return true.
   282         -*/
   283         -static int backofficeProcessExists(sqlite3_uint64 pid){
   284         -#if defined(_WIN32)
   285         -  return pid>0 && backofficeWin32ProcessExists((DWORD)pid)!=0;
   286         -#else
   287         -  return pid>0 && kill((pid_t)pid, 0)==0;
   288         -#endif 
   289         -}
   290         -
   291         -/*
   292         -** Check to see if the process identified by pid has finished.  If
   293         -** we cannot prove the the process is still running, return true.
   294         -*/
   295         -static int backofficeProcessDone(sqlite3_uint64 pid){
   296         -#if defined(_WIN32)
   297         -  return pid<=0 || backofficeWin32ProcessExists((DWORD)pid)==0;
   298         -#else
   299         -  return pid<=0 || kill((pid_t)pid, 0)!=0;
   300         -#endif 
   301         -}
   302         -
   303         -/*
   304         -** Return a process id number for the current process
   305         -*/
   306         -static sqlite3_uint64 backofficeProcessId(void){
   307         -  return (sqlite3_uint64)GETPID();
   308         -}
   309         -
   310         -
   311         -/*
   312         -** COMMAND: test-process-id
   313         -**
   314         -** Usage: %fossil [--sleep N] PROCESS-ID ...
   315         -**
   316         -** Show the current process id, and also tell whether or not all other
   317         -** processes IDs on the command line are running or not.  If the --sleep N
   318         -** option is provide, then sleep for N seconds before exiting.
   319         -*/
   320         -void test_process_id_command(void){
   321         -  const char *zSleep = find_option("sleep",0,1);
   322         -  int i;
   323         -  verify_all_options();
   324         -  fossil_print("ProcessID for this process: %lld\n", backofficeProcessId());
   325         -  if( zSleep ) sqlite3_sleep(1000*atoi(zSleep));
   326         -  for(i=2; i<g.argc; i++){
   327         -    sqlite3_uint64 x = (sqlite3_uint64)atoi(g.argv[i]);
   328         -    fossil_print("ProcessId %lld: exists %d done %d\n",
   329         -                 x, backofficeProcessExists(x),
   330         -                    backofficeProcessDone(x));
   331         -  }
   332         -}
   333         -
   334         -/*
   335         -** COMMAND: test-backoffice-lease
   336         -**
   337         -** Usage: %fossil test-backoffice-lease
   338         -**
   339         -** Print out information about the backoffice "lease" entry in the
   340         -** config table that controls whether or not backoffice should run.
   341         -*/
   342         -void test_backoffice_lease(void){
   343         -  sqlite3_int64 tmNow = time(0);
   344         -  Lease x;
   345         -  const char *zLease;
   346         -  db_find_and_open_repository(0,0);
   347         -  verify_all_options();
   348         -  zLease = db_get("backoffice","");
   349         -  fossil_print("now:        %lld\n", tmNow);
   350         -  fossil_print("lease:      \"%s\"\n", zLease);
   351         -  backofficeReadLease(&x);
   352         -  fossil_print("idCurrent:  %-20lld", x.idCurrent);
   353         -  if( backofficeProcessExists(x.idCurrent) ) fossil_print(" (exists)");
   354         -  if( backofficeProcessDone(x.idCurrent) ) fossil_print(" (done)");
   355         -  fossil_print("\n");
   356         -  fossil_print("tmCurrent:  %-20lld", x.tmCurrent);
   357         -  if( x.tmCurrent>0 ){
   358         -    fossil_print(" (now%+d)\n",x.tmCurrent-tmNow);
   359         -  }else{
   360         -    fossil_print("\n");
   361         -  }
   362         -  fossil_print("idNext:     %-20lld", x.idNext);
   363         -  if( backofficeProcessExists(x.idNext) ) fossil_print(" (exists)");
   364         -  if( backofficeProcessDone(x.idNext) ) fossil_print(" (done)");
   365         -  fossil_print("\n");
   366         -  fossil_print("tmNext:     %-20lld", x.tmNext);
   367         -  if( x.tmNext>0 ){
   368         -    fossil_print(" (now%+d)\n",x.tmNext-tmNow);
   369         -  }else{
   370         -    fossil_print("\n");
   371         -  }
   372         -}
   373         -
   374         -/*
   375         -** If backoffice processing is needed set the backofficeDb variable to the
   376         -** name of the database file.  If no backoffice processing is needed,
   377         -** this routine makes no changes to state.
   378         -*/
   379         -void backoffice_check_if_needed(void){
   380         -  Lease x;
   381         -  sqlite3_uint64 tmNow;
   382         -
   383         -  if( backofficeDb ) return;
   384         -  if( g.zRepositoryName==0 ) return;
   385         -  if( g.db==0 ) return;
   386         -  if( !db_table_exists("repository","config") ) return;
   387         -  tmNow = time(0);
   388         -  backofficeReadLease(&x);
   389         -  if( x.tmNext>=tmNow && backofficeProcessExists(x.idNext) ){
   390         -    /* Another backoffice process is already queued up to run.  This
   391         -    ** process does not need to do any backoffice work. */
   392         -    return;
   393         -  }else{
   394         -    /* We need to run backup to be (at a minimum) on-deck */
   395         -    backofficeDb = fossil_strdup(g.zRepositoryName);
   396         -  }
   397         -}
   398         -
   399         -/*
   400         -** Check for errors prior to running backoffice_thread() or backoffice_run().
   401         -*/
   402         -static void backoffice_error_check_one(int *pOnce){
   403         -  if( *pOnce ){
   404         -    fossil_panic("multiple calls to backoffice()");
   405         -  }
   406         -  *pOnce = 1;
   407         -  if( g.db==0 ){
   408         -    fossil_panic("database not open for backoffice processing");
   409         -  }
   410         -  if( db_transaction_nesting_depth()!=0 ){
   411         -    fossil_panic("transaction %s not closed prior to backoffice processing",
   412         -                 db_transaction_start_point());
   413         -  }
   414         -}
   415         -
   416         -/* This is the main loop for backoffice processing.
   417         -**
   418         -** If another process is already working as the current backoffice and
   419         -** the on-deck backoffice, then this routine returns very quickly
   420         -** without doing any work.
   421         -**
   422         -** If no backoffice processes are running at all, this routine becomes
   423         -** the main backoffice.
   424         -**
   425         -** If a primary backoffice is running, but a on-deck backoffice is
   426         -** needed, this routine becomes that on-desk backoffice.
   427         -*/
   428         -static void backoffice_thread(void){
   429         -  Lease x;
   430         -  sqlite3_uint64 tmNow;
   431         -  sqlite3_uint64 idSelf;
   432         -  int lastWarning = 0;
   433         -  int warningDelay = 30;
   434         -  static int once = 0;
   435         -
   436         -  backoffice_error_check_one(&once);
   437         -  idSelf = backofficeProcessId();
   438         -  while(1){
   439         -    tmNow = time(0);
   440         -    db_begin_write();
   441         -    backofficeReadLease(&x);
   442         -    if( x.tmNext>=tmNow
   443         -     && x.idNext!=idSelf
   444         -     && backofficeProcessExists(x.idNext)
   445         -    ){
   446         -      /* Another backoffice process is already queued up to run.  This
   447         -      ** process does not need to do any backoffice work and can stop
   448         -      ** immediately. */
   449         -      db_end_transaction(0);
   450         -      break;
   451         -    }
   452         -    if( x.tmCurrent<tmNow && backofficeProcessDone(x.idCurrent) ){
   453         -      /* This process can start doing backoffice work immediately */
   454         -      x.idCurrent = idSelf;
   455         -      x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
   456         -      x.idNext = 0;
   457         -      x.tmNext = 0;
   458         -      backofficeWriteLease(&x);
   459         -      db_end_transaction(0);
   460         -      backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
   461         -                      GETPID());
   462         -      backoffice_work();
   463         -      break;
   464         -    }
   465         -    if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",0) ){
   466         -      /* If the no-delay flag is set, exit immediately rather than queuing
   467         -      ** up.  Assume that some future request will come along and handle any
   468         -      ** necessary backoffice work. */
   469         -      db_end_transaction(0);
   470         -      break;
   471         -    }
   472         -    /* This process needs to queue up and wait for the current lease
   473         -    ** to expire before continuing. */
   474         -    x.idNext = idSelf;
   475         -    x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
   476         -    backofficeWriteLease(&x);
   477         -    db_end_transaction(0);
   478         -    backofficeTrace("/***** Backoffice On-deck %d *****/\n",  GETPID());
   479         -    if( x.tmCurrent >= tmNow ){
   480         -      if( backofficeSleep(1000*(x.tmCurrent - tmNow + 1)) ){
   481         -        /* The sleep was interrupted by a signal from another thread. */
   482         -        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
   483         -        db_end_transaction(0);
   484         -        break;
   485         -      }
   486         -    }else{
   487         -      if( lastWarning+warningDelay < tmNow ){
   488         -        fossil_warning(
   489         -           "backoffice process %lld still running after %d seconds",
   490         -           x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
   491         -        lastWarning = tmNow;
   492         -        warningDelay *= 2;
   493         -      }
   494         -      if( backofficeSleep(1000) ){
   495         -        /* The sleep was interrupted by a signal from another thread. */
   496         -        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
   497         -        db_end_transaction(0);
   498         -        break;
   499         -      }
   500         -    }
   501         -  }
   502         -  return;
   503         -}
   504         -
   505         -/*
   506         -** This routine runs to do the backoffice processing.  When adding new
   507         -** backoffice processing tasks, add them here.
   508         -*/
   509         -void backoffice_work(void){
   510         -  /* Log the backoffice run for testing purposes.  For production deployments
   511         -  ** the "backoffice-logfile" property should be unset and the following code
   512         -  ** should be a no-op. */
   513         -  char *zLog = db_get("backoffice-logfile",0);
   514         -  if( zLog && zLog[0] ){
   515         -    FILE *pLog = fossil_fopen(zLog, "a");
   516         -    if( pLog ){
   517         -      char *zDate = db_text(0, "SELECT datetime('now');");
   518         -      fprintf(pLog, "%s (%d) backoffice running\n", zDate, GETPID());
   519         -      fclose(pLog);
   520         -    }
   521         -  }
   522         -
   523         -  /* Here is where the actual work of the backoffice happens */
   524         -  alert_backoffice(0);
   525         -  smtp_cleanup();
   526         -}
   527         -
   528         -/*
   529         -** COMMAND: backoffice
   530         -**
   531         -** Usage: backoffice [-R repository]
   532         -**
   533         -** Run backoffice processing.  This might be done by a cron job or
   534         -** similar to make sure backoffice processing happens periodically.
   535         -*/
   536         -void backoffice_command(void){
   537         -  if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1;
   538         -  db_find_and_open_repository(0,0);
   539         -  verify_all_options();
   540         -  backoffice_thread();
   541         -}
   542         -
   543         -/*
   544         -** This is the main interface to backoffice from the rest of the system.
   545         -** This routine launches either backoffice_thread() directly or as a
   546         -** subprocess.
   547         -*/
   548         -void backoffice_run_if_needed(void){
   549         -  if( backofficeDb==0 ) return;
   550         -  if( strcmp(backofficeDb,"x")==0 ) return;
   551         -  if( g.db ) return;
   552         -  if( g.repositoryOpen ) return;
   553         -#if defined(_WIN32)
   554         -  {
   555         -    int i;
   556         -    intptr_t x;
   557         -    char *argv[4];
   558         -    wchar_t *ax[5];
   559         -    argv[0] = g.nameOfExe;
   560         -    argv[1] = "backoffice";
   561         -    argv[2] = "-R";
   562         -    argv[3] = backofficeDb;
   563         -    ax[4] = 0;
   564         -    for(i=0; i<=3; i++) ax[i] = fossil_utf8_to_unicode(argv[i]);
   565         -    x = _wspawnv(_P_NOWAIT, ax[0], (const wchar_t * const *)ax);
   566         -    for(i=0; i<=3; i++) fossil_unicode_free(ax[i]);
   567         -    backofficeTrace(
   568         -      "/***** Subprocess %d creates backoffice child %lu *****/\n",
   569         -      GETPID(), GetProcessId((HANDLE)x));
   570         -    if( x>=0 ) return;
   571         -  }
   572         -#else /* unix */
   573         -  {
   574         -    pid_t pid = fork();
   575         -    if( pid>0 ){
   576         -      /* This is the parent in a successful fork().  Return immediately. */
   577         -      backofficeTrace(
   578         -        "/***** Subprocess %d creates backoffice child %d *****/\n",
   579         -        GETPID(), (int)pid);
   580         -      return;
   581         -    }
   582         -    if( pid==0 ){
   583         -      /* This is the child of a successful fork().  Run backoffice. */
   584         -      int i;
   585         -      setsid();
   586         -      for(i=0; i<=2; i++){
   587         -        close(i);
   588         -        open("/dev/null", O_RDWR);
   589         -      }
   590         -      for(i=3; i<100; i++){ close(i); }
   591         -      db_open_repository(backofficeDb);
   592         -      backofficeDb = "x";
   593         -      backoffice_thread();
   594         -      db_close(1);
   595         -      backofficeTrace("/***** Backoffice Child %d exits *****/\n", GETPID());
   596         -      exit(0);
   597         -    }
   598         -    fossil_warning("backoffice process %d fork failed, errno %d", GETPID(),
   599         -                   errno);
   600         -  }
   601         -#endif
   602         -  /* Fork() failed or is unavailable.  Run backoffice in this process, but
   603         -  ** do so with the no-delay setting.
   604         -  */
   605         -  backofficeNoDelay = 1;
   606         -  db_open_repository(backofficeDb);
   607         -  backofficeDb = "x";
   608         -  backoffice_thread();
   609         -  db_close(1);
   610         -}

Changes to src/blob.c.

   292    292       }
   293    293     }
   294    294     memcpy(&pBlob->aData[pBlob->nUsed], aData, nData);
   295    295     pBlob->nUsed += nData;
   296    296     pBlob->aData[pBlob->nUsed] = 0;   /* Blobs are always nul-terminated */
   297    297   }
   298    298   
   299         -/*
   300         -** Append a single character to the blob
   301         -*/
   302         -void blob_append_char(Blob *pBlob, char c){
   303         -  if( pBlob->nUsed+1 >= pBlob->nAlloc ){
   304         -    pBlob->xRealloc(pBlob, pBlob->nUsed + pBlob->nAlloc + 100);
   305         -    if( pBlob->nUsed + 1 >= pBlob->nAlloc ){
   306         -      blob_panic();
   307         -    }
   308         -  }
   309         -  pBlob->aData[pBlob->nUsed++] = c;    
   310         -}
   311         -
   312    299   /*
   313    300   ** Copy a blob
   314    301   */
   315    302   void blob_copy(Blob *pTo, Blob *pFrom){
   316    303     blob_is_init(pFrom);
   317    304     blob_zero(pTo);
   318    305     blob_append(pTo, blob_buffer(pFrom), blob_size(pFrom));
................................................................................
   320    307   
   321    308   /*
   322    309   ** Return a pointer to a null-terminated string for a blob.
   323    310   */
   324    311   char *blob_str(Blob *p){
   325    312     blob_is_init(p);
   326    313     if( p->nUsed==0 ){
   327         -    blob_append_char(p, 0); /* NOTE: Changes nUsed. */
          314  +    blob_append(p, "", 1); /* NOTE: Changes nUsed. */
   328    315       p->nUsed = 0;
   329    316     }
   330    317     if( p->aData[p->nUsed]!=0 ){
   331    318       blob_materialize(p);
   332    319     }
   333    320     return p->aData;
   334    321   }
................................................................................
   337    324   ** Return a pointer to a null-terminated string for a blob that has
   338    325   ** been created using blob_append_sql() and not blob_appendf().  If
   339    326   ** text was ever added using blob_appendf() then throw an error.
   340    327   */
   341    328   char *blob_sql_text(Blob *p){
   342    329     blob_is_init(p);
   343    330     if( (p->blobFlags & BLOBFLAG_NotSQL) ){
   344         -    fossil_panic("use of blob_appendf() to construct SQL text");
          331  +    fossil_fatal("Internal error: Use of blob_appendf() to construct SQL text");
   345    332     }
   346    333     return blob_str(p);
   347    334   }
   348    335   
   349    336   
   350    337   /*
   351    338   ** Return a pointer to a null-terminated string for a blob.
................................................................................
   492    479   /*
   493    480   ** Rewind the cursor on a blob back to the beginning.
   494    481   */
   495    482   void blob_rewind(Blob *p){
   496    483     p->iCursor = 0;
   497    484   }
   498    485   
   499         -/*
   500         -** Truncate a blob back to zero length
   501         -*/
   502         -void blob_truncate(Blob *p, int sz){
   503         -  if( sz>=0 && sz<p->nUsed ) p->nUsed = sz;
   504         -}
   505         -
   506    486   /*
   507    487   ** Seek the cursor in a blob to the indicated offset.
   508    488   */
   509    489   int blob_seek(Blob *p, int offset, int whence){
   510    490     if( whence==BLOB_SEEK_SET ){
   511    491       p->iCursor = offset;
   512    492     }else if( whence==BLOB_SEEK_CUR ){
................................................................................
   672    652     }
   673    653     if( pTo ){
   674    654       blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor);
   675    655     }
   676    656     pFrom->iCursor = i;
   677    657   }
   678    658   
   679         -/*
   680         -** Ensure that the text in pBlob ends with '\n'
   681         -*/
   682         -void blob_add_final_newline(Blob *pBlob){
   683         -  if( pBlob->nUsed<=0 ) return;
   684         -  if( pBlob->aData[pBlob->nUsed-1]!='\n' ){
   685         -    blob_append_char(pBlob, '\n');
   686         -  }
   687         -}
   688         -
   689    659   /*
   690    660   ** Return true if the blob contains a valid base16 identifier artifact hash.
   691    661   **
   692    662   ** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the
   693    663   ** hash is valid.  Both of these are non-zero and therefore "true".
   694    664   ** If the hash is not valid, then HNAME_ERROR is returned, which is zero or
   695    665   ** false.
................................................................................
  1265   1235                      zIn, blob_str(&bad), c);
  1266   1236       }
  1267   1237       if( !needEscape && !fossil_isalnum(c) && c!='/' && c!='.' && c!='_' ){
  1268   1238         needEscape = 1;
  1269   1239       }
  1270   1240     }
  1271   1241     if( n>0 && !fossil_isspace(z[n-1]) ){
  1272         -    blob_append_char(pBlob, ' ');
         1242  +    blob_append(pBlob, " ", 1);
  1273   1243     }
  1274         -  if( needEscape ) blob_append_char(pBlob, cQuote);
         1244  +  if( needEscape ) blob_append(pBlob, &cQuote, 1);
  1275   1245     if( zIn[0]=='-' ) blob_append(pBlob, "./", 2);
  1276   1246     blob_append(pBlob, zIn, -1);
  1277         -  if( needEscape ) blob_append_char(pBlob, cQuote);
         1247  +  if( needEscape ) blob_append(pBlob, &cQuote, 1);
  1278   1248   }
  1279   1249   
  1280   1250   /*
  1281   1251   ** A read(2)-like impl for the Blob class. Reads (copies) up to nLen
  1282   1252   ** bytes from pIn, starting at position pIn->iCursor, and copies them
  1283   1253   ** to pDest (which must be valid memory at least nLen bytes long).
  1284   1254   **
................................................................................
  1341   1311           /* swap bytes of unicode representation */
  1342   1312           char zTemp = zUtf8[--i];
  1343   1313           zUtf8[i] = zUtf8[i-1];
  1344   1314           zUtf8[--i] = zTemp;
  1345   1315         }
  1346   1316       }
  1347   1317       /* Make sure the blob contains two terminating 0-bytes */
  1348         -    blob_append_char(pBlob, 0);
         1318  +    blob_append(pBlob, "", 1);
  1349   1319       zUtf8 = blob_str(pBlob) + bomSize;
  1350   1320       zUtf8 = fossil_unicode_to_utf8(zUtf8);
  1351   1321       blob_set_dynamic(pBlob, zUtf8);
  1352   1322     }else if( useMbcs && invalid_utf8(pBlob) ){
  1353   1323   #if defined(_WIN32) || defined(__CYGWIN__)
  1354   1324       zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob));
  1355   1325       blob_reset(pBlob);

Changes to src/branch.c.

   266    266   ** COMMAND: branch
   267    267   **
   268    268   ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS?
   269    269   **
   270    270   ** Run various subcommands to manage branches of the open repository or
   271    271   ** of the repository identified by the -R or --repository option.
   272    272   **
   273         -**    fossil branch current
   274         -**
   275         -**        Print the name of the branch for the current check-out
   276         -**
   277         -**    fossil branch info BRANCH-NAME
   278         -**
   279         -**        Print information about a branch
   280         -**
   281         -**    fossil branch list|ls ?-a|--all|-c|--closed?
   282         -**
   283         -**        List all branches.  Use -a or --all to list all branches and
   284         -**        -c or --closed to list all closed branches.  The default is to
   285         -**        show only open branches.
   286         -**
   287    273   **    fossil branch new BRANCH-NAME BASIS ?OPTIONS?
   288    274   **
   289    275   **        Create a new branch BRANCH-NAME off of check-in BASIS.
   290    276   **        Supported options for this subcommand include:
   291    277   **        --private             branch is private (i.e., remains local)
   292    278   **        --bgcolor COLOR       use COLOR instead of automatic background
   293    279   **        --nosign              do not sign contents on this branch
................................................................................
   296    282   **
   297    283   **        DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
   298    284   **        year-month-day form, it may be truncated, the "T" may be
   299    285   **        replaced by a space, and it may also name a timezone offset
   300    286   **        from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
   301    287   **        Either no timezone suffix or "Z" means UTC.
   302    288   **
          289  +**    fossil branch list|ls ?-a|--all|-c|--closed?
          290  +**
          291  +**        List all branches.  Use -a or --all to list all branches and
          292  +**        -c or --closed to list all closed branches.  The default is to
          293  +**        show only open branches.
          294  +**
          295  +**    fossil branch info BRANCH-NAME
          296  +**
          297  +**        Print information about a branch
          298  +**
   303    299   ** Options:
   304    300   **    -R|--repository FILE       Run commands on repository FILE
   305         -**
   306         -** Summary:
   307         -**    fossil branch current
   308         -**    fossil branch info BRANCHNAME
   309         -**    fossil branch [list|ls]
   310         -**    fossil branch new
   311    301   */
   312    302   void branch_cmd(void){
   313    303     int n;
   314    304     const char *zCmd = "list";
   315    305     db_find_and_open_repository(0, 0);
   316    306     if( g.argc>=3 ) zCmd = g.argv[2];
   317    307     n = strlen(zCmd);
   318         -  if( strncmp(zCmd,"current",n)==0 ){
   319         -    if( !g.localOpen ){
   320         -      fossil_fatal("not within an open checkout");
   321         -    }else{
   322         -      int vid = db_lget_int("checkout", 0);
   323         -      char *zCurrent = db_text(0, "SELECT value FROM tagxref"
   324         -                            " WHERE rid=%d AND tagid=%d", vid, TAG_BRANCH);
   325         -      fossil_print("%s\n", zCurrent);
   326         -      fossil_free(zCurrent);
   327         -    }
   328         -  }else if( strncmp(zCmd,"info",n)==0 ){
   329         -    int i;
   330         -    for(i=3; i<g.argc; i++){
   331         -      const char *zBrName = g.argv[i];
   332         -      int rid = branch_is_open(zBrName);
   333         -      if( rid==0 ){
   334         -        fossil_print("%s: not an open branch\n", zBrName);
   335         -      }else{
   336         -        const char *zUuid = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",rid);
   337         -        const char *zDate = db_text(0,
   338         -          "SELECT datetime(mtime,toLocal()) FROM event"
   339         -          " WHERE objid=%d", rid);
   340         -        fossil_print("%s: open as of %s on %.16s\n", zBrName, zDate, zUuid);
   341         -      }
   342         -    }
          308  +  if( strncmp(zCmd,"new",n)==0 ){
          309  +    branch_new();
   343    310     }else if( (strncmp(zCmd,"list",n)==0)||(strncmp(zCmd, "ls", n)==0) ){
   344    311       Stmt q;
   345    312       int vid;
   346    313       char *zCurrent = 0;
   347    314       int brFlags = BRL_OPEN_ONLY;
   348    315       if( find_option("all","a",0)!=0 ) brFlags = BRL_BOTH;
   349    316       if( find_option("closed","c",0)!=0 ) brFlags = BRL_CLOSED_ONLY;
................................................................................
   356    323       branch_prepare_list_query(&q, brFlags);
   357    324       while( db_step(&q)==SQLITE_ROW ){
   358    325         const char *zBr = db_column_text(&q, 0);
   359    326         int isCur = zCurrent!=0 && fossil_strcmp(zCurrent,zBr)==0;
   360    327         fossil_print("%s%s\n", (isCur ? "* " : "  "), zBr);
   361    328       }
   362    329       db_finalize(&q);
   363         -  }else if( strncmp(zCmd,"new",n)==0 ){
   364         -    branch_new();
          330  +  }else if( strncmp(zCmd,"info",n)==0 ){
          331  +    int i;
          332  +    for(i=3; i<g.argc; i++){
          333  +      const char *zBrName = g.argv[i];
          334  +      int rid = branch_is_open(zBrName);
          335  +      if( rid==0 ){
          336  +        fossil_print("%s: not an open branch\n", zBrName);
          337  +      }else{
          338  +        const char *zUuid = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",rid);
          339  +        const char *zDate = db_text(0,
          340  +          "SELECT datetime(mtime,toLocal()) FROM event"
          341  +          " WHERE objid=%d", rid);
          342  +        fossil_print("%s: open as of %s on %.16s\n", zBrName, zDate, zUuid);
          343  +      }
          344  +    }
   365    345     }else{
   366    346       fossil_fatal("branch subcommand should be one of: "
   367         -                 "current info list ls new");
          347  +                 "info list ls new");
   368    348     }
   369    349   }
   370    350   
   371    351   static const char brlistQuery[] =
   372    352   @ SELECT
   373    353   @   tagxref.value,
   374    354   @   max(event.mtime),

Changes to src/browse.c.

   119    119   **    type=TYPE        TYPE=flat: use this display
   120    120   **                     TYPE=tree: use the /tree display instead
   121    121   */
   122    122   void page_dir(void){
   123    123     char *zD = fossil_strdup(P("name"));
   124    124     int nD = zD ? strlen(zD)+1 : 0;
   125    125     int mxLen;
   126         -  int n;
          126  +  int nCol, nRow;
          127  +  int cnt, i;
   127    128     char *zPrefix;
   128    129     Stmt q;
   129    130     const char *zCI = P("ci");
   130    131     int rid = 0;
   131    132     char *zUuid = 0;
   132    133     Blob dirname;
   133    134     Manifest *pM = 0;
................................................................................
   266    267       );
   267    268     }
   268    269   
   269    270     /* Generate a multi-column table listing the contents of zD[]
   270    271     ** directory.
   271    272     */
   272    273     mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
   273         -  n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/");
          274  +  cnt = db_int(0, "SELECT count(*) FROM localfiles /*scan*/");
   274    275     if( mxLen<12 ) mxLen = 12;
   275         -  mxLen += (mxLen+9)/10;
          276  +  nCol = 100/mxLen;
          277  +  if( nCol<1 ) nCol = 1;
          278  +  if( nCol>5 ) nCol = 5;
          279  +  nRow = (cnt+nCol-1)/nCol;
   276    280     db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/");
   277         -  @ <div class="columns" style="columns: %d(mxLen)ex %d(n);">
   278         -  @ <ul class="browser">
          281  +  @ <table class="browser"><tr><td class="browser"><ul class="browser">
          282  +  i = 0;
   279    283     while( db_step(&q)==SQLITE_ROW ){
   280    284       const char *zFN;
          285  +    if( i==nRow ){
          286  +      @ </ul></td><td class="browser"><ul class="browser">
          287  +      i = 0;
          288  +    }
          289  +    i++;
   281    290       zFN = db_column_text(&q, 0);
   282    291       if( zFN[0]=='/' ){
   283    292         zFN++;
   284    293         @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
   285    294       }else{
   286    295         const char *zLink;
   287    296         if( zCI ){
................................................................................
   291    300           zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
   292    301         }
   293    302         @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
   294    303       }
   295    304     }
   296    305     db_finalize(&q);
   297    306     manifest_destroy(pM);
   298         -  @ </ul></div>
          307  +  @ </ul></td></tr></table>
   299    308   
   300    309     /* If the directory contains a readme file, then display its content below
   301    310     ** the list of files
   302    311     */
   303    312     db_prepare(&q,
   304    313       "SELECT x, u FROM localfiles"
   305    314       " WHERE x COLLATE nocase IN"

Changes to src/bundle.c.

    58     58     char *zErrMsg = 0;
    59     59     char *zSql;
    60     60     if( !doInit && file_size(zFile, ExtFILE)<0 ){
    61     61       fossil_fatal("no such file: %s", zFile);
    62     62     }
    63     63     assert( g.db );
    64     64     zSql = sqlite3_mprintf("ATTACH %Q AS %Q", zFile, zBName);
    65         -  if( zSql==0 ) fossil_panic("out of memory");
           65  +  if( zSql==0 ) fossil_fatal("out of memory");
    66     66     rc = sqlite3_exec(g.db, zSql, 0, 0, &zErrMsg);
    67     67     sqlite3_free(zSql);
    68     68     if( rc!=SQLITE_OK || zErrMsg ){
    69     69       if( zErrMsg==0 ) zErrMsg = (char*)sqlite3_errmsg(g.db);
    70     70       fossil_fatal("not a valid bundle: %s", zFile);
    71     71     }
    72     72     if( doInit ){
    73     73       db_multi_exec(zBundleInit /*works-like:"%w%w"*/, zBName, zBName);
    74     74     }else{
    75     75       sqlite3_stmt *pStmt;
    76     76       zSql = sqlite3_mprintf("SELECT bcname, bcvalue"
    77     77                              "  FROM \"%w\".bconfig", zBName);
    78         -    if( zSql==0 ) fossil_panic("out of memory");
           78  +    if( zSql==0 ) fossil_fatal("out of memory");
    79     79       rc = sqlite3_prepare(g.db, zSql, -1, &pStmt, 0);
    80     80       if( rc ) fossil_fatal("not a valid bundle: %s", zFile);
    81     81       sqlite3_free(zSql);
    82     82       sqlite3_finalize(pStmt);
    83     83       zSql = sqlite3_mprintf("SELECT blobid, uuid, sz, delta, notes, data"
    84     84                              "  FROM \"%w\".bblob", zBName);
    85         -    if( zSql==0 ) fossil_panic("out of memory");
           85  +    if( zSql==0 ) fossil_fatal("out of memory");
    86     86       rc = sqlite3_prepare(g.db, zSql, -1, &pStmt, 0);
    87     87       if( rc ) fossil_fatal("not a valid bundle: %s", zFile);
    88     88       sqlite3_free(zSql);
    89     89       sqlite3_finalize(pStmt);
    90     90     }
    91     91   }
    92     92   

Changes to src/cache.c.

    59     59     }
    60     60     rc = sqlite3_open(zDbName, &db);
    61     61     fossil_free(zDbName);
    62     62     if( rc ){
    63     63       sqlite3_close(db);
    64     64       return 0;
    65     65     }
    66         -  sqlite3_busy_timeout(db, 5000);
    67         -  if( sqlite3_table_column_metadata(db,0,"blob","key",0,0,0,0,0)!=SQLITE_OK ){
    68         -    rc = sqlite3_exec(db,
    69         -       "PRAGMA page_size=8192;"
    70         -       "CREATE TABLE IF NOT EXISTS blob(id INTEGER PRIMARY KEY, data BLOB);"
    71         -       "CREATE TABLE IF NOT EXISTS cache("
    72         -         "key TEXT PRIMARY KEY,"     /* Key used to access the cache */
    73         -         "id INT REFERENCES blob,"   /* The cache content */
    74         -         "sz INT,"                   /* Size of content in bytes */
    75         -         "tm INT,"                   /* Last access time (unix timestampe) */
    76         -         "nref INT"                  /* Number of uses */
    77         -       ");"
    78         -       "CREATE TRIGGER IF NOT EXISTS cacheDel AFTER DELETE ON cache BEGIN"
    79         -       "  DELETE FROM blob WHERE id=OLD.id;"
    80         -       "END;",
    81         -       0, 0, 0
    82         -    );
    83         -    if( rc!=SQLITE_OK ){
    84         -      sqlite3_close(db);
    85         -      return 0;
    86         -    }
           66  +  rc = sqlite3_exec(db,
           67  +     "PRAGMA page_size=8192;"
           68  +     "CREATE TABLE IF NOT EXISTS blob(id INTEGER PRIMARY KEY, data BLOB);"
           69  +     "CREATE TABLE IF NOT EXISTS cache("
           70  +       "key TEXT PRIMARY KEY,"     /* Key used to access the cache */
           71  +       "id INT REFERENCES blob,"   /* The cache content */
           72  +       "sz INT,"                   /* Size of content in bytes */
           73  +       "tm INT,"                   /* Last access time (unix timestampe) */
           74  +       "nref INT"                  /* Number of uses */
           75  +     ");"
           76  +     "CREATE TRIGGER IF NOT EXISTS cacheDel AFTER DELETE ON cache BEGIN"
           77  +     "  DELETE FROM blob WHERE id=OLD.id;"
           78  +     "END;",
           79  +     0, 0, 0);
           80  +  if( rc!=SQLITE_OK ){
           81  +    sqlite3_close(db);
           82  +    return 0;
    87     83     }
    88     84     return db;
    89     85   }
    90     86   
    91     87   /*
    92     88   ** Attempt to construct a prepared statement for the cache database.
    93     89   */

Deleted src/capabilities.c.

     1         -/*
     2         -** Copyright (c) 2018 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 used managing user capability strings.
    19         -*/
    20         -#include "config.h"
    21         -#include "capabilities.h"
    22         -#include <assert.h>
    23         -
    24         -#if INTERFACE
    25         -/*
    26         -** A capability string object holds all defined capabilities in a
    27         -** vector format that is subject to boolean operations.
    28         -*/
    29         -struct CapabilityString {
    30         -  unsigned char x[128];
    31         -};
    32         -#endif
    33         -
    34         -/*
    35         -** Add capabilities to a CapabilityString.  If pIn is NULL, then create
    36         -** a new capability string.
    37         -**
    38         -** Call capability_free() on the allocated CapabilityString object to
    39         -** deallocate.
    40         -*/
    41         -CapabilityString *capability_add(CapabilityString *pIn, const char *zCap){
    42         -  int c;
    43         -  int i;
    44         -  if( pIn==0 ){
    45         -    pIn = fossil_malloc( sizeof(*pIn) );
    46         -    memset(pIn, 0, sizeof(*pIn));
    47         -  }
    48         -  if( zCap ){
    49         -    for(i=0; (c = zCap[i])!=0; i++){
    50         -      if( c>='0' && c<='z' ) pIn->x[c] = 1;
    51         -    }
    52         -  }
    53         -  return pIn;
    54         -}
    55         -
    56         -/*
    57         -** Remove capabilities from a CapabilityString.
    58         -*/
    59         -CapabilityString *capability_remove(CapabilityString *pIn, const char *zCap){
    60         -  int c;
    61         -  int i;
    62         -  if( pIn==0 ){
    63         -    pIn = fossil_malloc( sizeof(*pIn) );
    64         -    memset(pIn, 0, sizeof(*pIn));
    65         -  }
    66         -  if( zCap ){
    67         -    for(i=0; (c = zCap[i])!=0; i++){
    68         -      if( c>='0' && c<='z' ) pIn->x[c] = 0;
    69         -    }
    70         -  }
    71         -  return pIn;
    72         -}
    73         -
    74         -/*
    75         -** Return true if any of the capabilities in zNeeded are found in pCap
    76         -*/
    77         -int capability_has_any(CapabilityString *p, const char *zNeeded){
    78         -  if( p==0 ) return 0;
    79         -  if( zNeeded==0 ) return 0;
    80         -  while( zNeeded[0] ){
    81         -    int c = zNeeded[0];
    82         -    if( fossil_isalnum(c) && p->x[c] ) return 1;
    83         -    zNeeded++;
    84         -  }
    85         -  return 0;
    86         -}
    87         -
    88         -/*
    89         -** Delete a CapabilityString object.
    90         -*/
    91         -void capability_free(CapabilityString *p){
    92         -  fossil_free(p);
    93         -}
    94         -
    95         -/*
    96         -** Expand the capability string by including all capabilities for 
    97         -** special users "nobody" and "anonymous".  Also include "reader"
    98         -** if "u" is present and "developer" if "v" is present.
    99         -*/
   100         -void capability_expand(CapabilityString *pIn){
   101         -  static char *zNobody = 0;
   102         -  static char *zAnon = 0;
   103         -  static char *zReader = 0;
   104         -  static char *zDev = 0;
   105         -  int doneV = 0;
   106         -
   107         -  if( pIn==0 ){
   108         -    fossil_free(zNobody); zNobody = 0;
   109         -    fossil_free(zAnon);   zAnon = 0;
   110         -    fossil_free(zReader); zReader = 0;
   111         -    fossil_free(zDev);    zDev = 0;
   112         -    return;
   113         -  }
   114         -  if( zNobody==0 ){
   115         -    zNobody = db_text(0, "SELECT cap FROM user WHERE login='nobody'");
   116         -    zAnon = db_text(0, "SELECT cap FROM user WHERE login='anonymous'");
   117         -    zReader = db_text(0, "SELECT cap FROM user WHERE login='reader'");
   118         -    zDev = db_text(0, "SELECT cap FROM user WHERE login='developer'");
   119         -  }
   120         -  pIn = capability_add(pIn, zAnon);
   121         -  pIn = capability_add(pIn, zNobody);
   122         -  if( pIn->x['v'] ){
   123         -    pIn = capability_add(pIn, zDev);
   124         -    doneV = 1;
   125         -  }
   126         -  if( pIn->x['u'] ){
   127         -    pIn = capability_add(pIn, zReader);
   128         -    if( pIn->x['v'] && !doneV ){
   129         -      pIn = capability_add(pIn, zDev);
   130         -    }
   131         -  }
   132         -}
   133         -
   134         -/*
   135         -** Render a capability string in canonical string format.  Space to hold
   136         -** the returned string is obtained from fossil_malloc() can should be freed
   137         -** by the caller.
   138         -*/
   139         -char *capability_string(CapabilityString *p){
   140         -  Blob out;
   141         -  int i;
   142         -  int j = 0;
   143         -  char buf[100];
   144         -  blob_init(&out, 0, 0);
   145         -  for(i='a'; i<='z'; i++){
   146         -    if( p->x[i] ) buf[j++] = i;
   147         -  }
   148         -  for(i='0'; i<='9'; i++){
   149         -    if( p->x[i] ) buf[j++] = i;
   150         -  }
   151         -  for(i='A'; i<='Z'; i++){
   152         -    if( p->x[i] ) buf[j++] = i;
   153         -  }
   154         -  buf[j] = 0;
   155         -  return fossil_strdup(buf);
   156         -}
   157         -
   158         -/*
   159         -** The next two routines implement an aggregate SQL function that
   160         -** takes multiple capability strings and in the end returns their
   161         -** union.  Example usage:
   162         -**
   163         -**    SELECT capunion(cap) FROM user WHERE login IN ('nobody','anonymous');
   164         -*/
   165         -void capability_union_step(
   166         -  sqlite3_context *context,
   167         -  int argc,
   168         -  sqlite3_value **argv
   169         -){
   170         -  CapabilityString *p;
   171         -  const char *zIn;
   172         -
   173         -  zIn = (const char*)sqlite3_value_text(argv[0]);
   174         -  if( zIn==0 ) return;
   175         -  p = (CapabilityString*)sqlite3_aggregate_context(context, sizeof(*p));
   176         -  p = capability_add(p, zIn);
   177         -}
   178         -void capability_union_finalize(sqlite3_context *context){
   179         -  CapabilityString *p;
   180         -  p = sqlite3_aggregate_context(context, 0);
   181         -  if( p ){
   182         -    char *zOut = capability_string(p);
   183         -    sqlite3_result_text(context, zOut, -1, fossil_free);
   184         -  }
   185         -}
   186         -
   187         -/*
   188         -** The next routines takes the raw USER.CAP field and expands it with
   189         -** capabilities from special users.  Example:
   190         -**
   191         -**   SELECT fullcap(cap) FROM user WHERE login=?1
   192         -*/
   193         -void capability_fullcap(
   194         -  sqlite3_context *context,
   195         -  int argc,
   196         -  sqlite3_value **argv
   197         -){
   198         -  CapabilityString *p;
   199         -  const char *zIn;
   200         -  char *zOut;
   201         -
   202         -  zIn = (const char*)sqlite3_value_text(argv[0]);
   203         -  if( zIn==0 ) zIn = "";
   204         -  p = capability_add(0, zIn);
   205         -  capability_expand(p);
   206         -  zOut = capability_string(p);
   207         -  sqlite3_result_text(context, zOut, -1, fossil_free);
   208         -  capability_free(p);
   209         -}
   210         -
   211         -#if INTERFACE
   212         -/*
   213         -** Capabilities are grouped into "classes" as follows:
   214         -*/
   215         -#define CAPCLASS_CODE  0x0001
   216         -#define CAPCLASS_WIKI  0x0002
   217         -#define CAPCLASS_TKT   0x0004
   218         -#define CAPCLASS_FORUM 0x0008
   219         -#define CAPCLASS_DATA  0x0010
   220         -#define CAPCLASS_ALERT 0x0020
   221         -#define CAPCLASS_OTHER 0x0040
   222         -#define CAPCLASS_SUPER 0x0080
   223         -#define CAPCLASS_ALL   0xffff
   224         -#endif /* INTERFACE */
   225         -
   226         -
   227         -/*
   228         -** The following structure holds descriptions of the various capabilities.
   229         -*/
   230         -static struct Caps {
   231         -  char cCap;              /* The capability letter */
   232         -  unsigned short eClass;  /* The "class" for this capability */
   233         -  char *zAbbrev;          /* Abbreviated mnemonic name */
   234         -  char *zOneLiner;        /* One-line summary */
   235         -} aCap[] = {
   236         -  { 'a', CAPCLASS_SUPER,
   237         -    "Admin", "Create and delete users" },
   238         -  { 'b', CAPCLASS_WIKI|CAPCLASS_TKT,
   239         -    "Attach", "Add attchments to wiki or tickets" },
   240         -  { 'c', CAPCLASS_TKT,
   241         -    "Append-Tkt", "Append to existing tickets" },
   242         -  { 'd', CAPCLASS_WIKI|CAPCLASS_TKT,
   243         -    "Delete", "Delete wiki or tickets" },
   244         -  { 'e', CAPCLASS_DATA,
   245         -    "View-PII", "View sensitive info such as email addresses" },
   246         -  { 'f', CAPCLASS_WIKI,
   247         -    "New-Wiki", "Create new wiki pages" },
   248         -  { 'g', CAPCLASS_DATA,
   249         -    "Clone", "Clone the repository" },
   250         -  { 'h', CAPCLASS_OTHER,
   251         -    "Hyperlinks", "Show hyperlinks to detailed repository history" },
   252         -  { 'i', CAPCLASS_CODE,
   253         -    "Check-In", "Check-in code changes" },
   254         -  { 'j', CAPCLASS_WIKI,
   255         -    "Read-Wiki", "View wiki pages" },
   256         -  { 'k', CAPCLASS_WIKI,
   257         -    "Write-Wiki", "Edit wiki pages" },
   258         -  { 'l', CAPCLASS_WIKI|CAPCLASS_SUPER,
   259         -    "Mod-Wiki", "Moderator for wiki pages" },
   260         -  { 'm', CAPCLASS_WIKI,
   261         -    "Append-Wiki", "Append to wiki pages" },
   262         -  { 'n', CAPCLASS_TKT,
   263         -    "New-Tkt", "Create new tickets" },
   264         -  { 'o', CAPCLASS_CODE,
   265         -    "Check-Out", "Check out code" },
   266         -  { 'p', CAPCLASS_OTHER,
   267         -    "Password", "Change your own password" },
   268         -  { 'q', CAPCLASS_TKT|CAPCLASS_SUPER,
   269         -    "Mod-Tkt", "Moderate tickets" },
   270         -  { 'r', CAPCLASS_TKT,
   271         -    "Read-Tkt", "View tickets" },
   272         -  { 's', CAPCLASS_SUPER,
   273         -    "Superuser", "Setup and configure the respository" },
   274         -  { 't', CAPCLASS_TKT,
   275         -    "Reports", "Create new ticket report formats" },
   276         -  { 'u', CAPCLASS_OTHER,
   277         -    "Reader", "Inherit all the capabilities of the \"reader\" user" },
   278         -  { 'v', CAPCLASS_OTHER,
   279         -    "Developer", "Inherit all capabilities of the \"developer\" user" },
   280         -  { 'w', CAPCLASS_TKT,
   281         -    "Write-Tkt", "Edit tickets" },
   282         -  { 'x', CAPCLASS_DATA,
   283         -    "Private", "Push and/or pull private branches" },
   284         -  { 'y', CAPCLASS_SUPER,
   285         -    "Write-UV", "Push unversioned content" },
   286         -  { 'z', CAPCLASS_CODE,
   287         -    "Zip-Download", "Download a ZIP archive, tarball, or SQL archive" },
   288         -  { '2', CAPCLASS_FORUM,
   289         -    "Forum-Read", "Read forum posts by others" },
   290         -  { '3', CAPCLASS_FORUM,
   291         -    "Forum-Write", "Create new forum messages" },
   292         -  { '4', CAPCLASS_FORUM,
   293         -    "Forum-Trusted", "Create forum messages that bypass moderation" },
   294         -  { '5', CAPCLASS_FORUM|CAPCLASS_SUPER,
   295         -    "Forum-Mod", "Moderator for forum messages" },
   296         -  { '6', CAPCLASS_FORUM|CAPCLASS_SUPER,
   297         -    "Forum-Admin", "Set or remove capability '4' from other users" },
   298         -  { '7', CAPCLASS_ALERT,
   299         -    "Alerts", "Sign up for email alerts" },
   300         -  { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER,
   301         -    "Announce", "Send announcements to all subscribers" },
   302         -  { 'D', CAPCLASS_OTHER,
   303         -    "Debug", "Enable debugging features" },
   304         -};
   305         -
   306         -
   307         -/*
   308         -** Generate HTML that lists all of the capability letters together with
   309         -** a brief summary of what each letter means.
   310         -*/
   311         -void capabilities_table(unsigned mClass){
   312         -  int i;
   313         -  @ <table>
   314         -  for(i=0; i<sizeof(aCap)/sizeof(aCap[0]); i++){
   315         -    if( (aCap[i].eClass & mClass)==0 ) continue;
   316         -    @ <tr><th valign="top">%c(aCap[i].cCap)</th>
   317         -    @  <td><i>%h(aCap[i].zAbbrev):</i> %h(aCap[i].zOneLiner)</td></tr>
   318         -  }
   319         -  @ </table>
   320         -}
   321         -
   322         -/*
   323         -** Generate a "capability summary table" that shows the major capabilities
   324         -** against the various user categories.
   325         -*/
   326         -void capability_summary(void){
   327         -  Stmt q;
   328         -  db_prepare(&q,
   329         -    "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
   330         -                       "('developer',4))"
   331         -    " SELECT id, fullcap(user.cap),seq,1"
   332         -    "   FROM t LEFT JOIN user ON t.id=user.login"
   333         -    " UNION ALL"
   334         -    " SELECT 'New User Default', fullcap(%Q), 10, 1"
   335         -    " UNION ALL"
   336         -    " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user"
   337         -    " WHERE cap NOT GLOB '*[as]*'"
   338         -    " UNION ALL"
   339         -    " SELECT 'Adminstator', fullcap(capunion(cap)), 30, count(*) FROM user"
   340         -    " WHERE cap GLOB '*[as]*'"
   341         -    " ORDER BY 3 ASC",
   342         -    db_get("default-perms","")
   343         -  );
   344         -  @ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
   345         -  @ <tr><th>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
   346         -  @ <th>Unversioned Content</th></tr>
   347         -  while( db_step(&q)==SQLITE_ROW ){
   348         -    const char *zId = db_column_text(&q, 0);
   349         -    const char *zCap = db_column_text(&q, 1);
   350         -    int n = db_column_int(&q, 3);
   351         -    int eType;
   352         -    static const char *azType[] = { "off", "read", "write" };
   353         -    static const char *azClass[] = { "capsumOff", "capsumRead", "capsumWrite" };
   354         -
   355         -    if( n==0 ) continue;
   356         -
   357         -    /* Code */
   358         -    if( db_column_int(&q,2)<10 ){
   359         -      @ <tr><th align="right"><tt>"%h(zId)"</tt></th>
   360         -    }else if( n>1 ){
   361         -      @ <tr><th align="right">%d(n) %h(zId)s</th>
   362         -    }else{
   363         -      @ <tr><th align="right">%h(zId)</th>
   364         -    }
   365         -    if( sqlite3_strglob("*[asi]*",zCap)==0 ){
   366         -      eType = 2;
   367         -    }else if( sqlite3_strglob("*[oz]*",zCap)==0 ){
   368         -      eType = 1;
   369         -    }else{
   370         -      eType = 0;
   371         -    }
   372         -    @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
   373         -
   374         -    /* Forum */
   375         -    if( sqlite3_strglob("*[as3456]*",zCap)==0 ){
   376         -      eType = 2;
   377         -    }else if( sqlite3_strglob("*2*",zCap)==0 ){
   378         -      eType = 1;
   379         -    }else{
   380         -      eType = 0;
   381         -    }
   382         -    @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
   383         -
   384         -    /* Ticket */
   385         -    if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){
   386         -      eType = 2;
   387         -    }else if( sqlite3_strglob("*r*",zCap)==0 ){
   388         -      eType = 1;
   389         -    }else{
   390         -      eType = 0;
   391         -    }
   392         -    @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
   393         -
   394         -    /* Wiki */
   395         -    if( sqlite3_strglob("*[asdfklm]*",zCap)==0 ){
   396         -      eType = 2;
   397         -    }else if( sqlite3_strglob("*j*",zCap)==0 ){
   398         -      eType = 1;
   399         -    }else{
   400         -      eType = 0;
   401         -    }
   402         -    @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
   403         -
   404         -    /* Unversioned */
   405         -    if( sqlite3_strglob("*y*",zCap)==0 ){
   406         -      eType = 2;
   407         -    }else if( sqlite3_strglob("*[ioas]*",zCap)==0 ){
   408         -      eType = 1;
   409         -    }else{
   410         -      eType = 0;
   411         -    }
   412         -    @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
   413         -
   414         -  }
   415         -  db_finalize(&q);
   416         -  @ </table>
   417         -}

Changes to src/captcha.c.

   495    495   **
   496    496   ** If no captcha is required or if the correct captcha is supplied, return
   497    497   ** true (non-zero).
   498    498   **
   499    499   ** The query parameters examined are "captchaseed" for the seed value and
   500    500   ** "captcha" for text that the user types in response to the captcha prompt.
   501    501   */
   502         -int captcha_is_correct(int bAlwaysNeeded){
          502  +int captcha_is_correct(void){
   503    503     const char *zSeed;
   504    504     const char *zEntered;
   505    505     const char *zDecode;
   506    506     char z[30];
   507    507     int i;
   508         -  if( !bAlwaysNeeded && !captcha_needed() ){
          508  +  if( !captcha_needed() ){
   509    509       return 1;  /* No captcha needed */
   510    510     }
   511    511     zSeed = P("captchaseed");
   512    512     if( zSeed==0 ) return 0;
   513    513     zEntered = P("captcha");
   514    514     if( zEntered==0 || strlen(zEntered)!=8 ) return 0;
   515    515     zDecode = captcha_decode((unsigned int)atoi(zSeed));
................................................................................
   537    537     const char *zDecoded;
   538    538     char *zCaptcha;
   539    539   
   540    540     if( !captcha_needed() ) return;
   541    541     uSeed = captcha_seed();
   542    542     zDecoded = captcha_decode(uSeed);
   543    543     zCaptcha = captcha_render(zDecoded);
   544         -  @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
          544  +  @ <div class="captcha"><table class="captcha"><tr><td><pre>
   545    545     @ %h(zCaptcha)
   546    546     @ </pre>
   547    547     @ Enter security code shown above:
   548    548     @ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
   549    549     @ <input type="text" name="captcha" size=8 />
   550    550     if( showButton ){
   551    551       @ <input type="submit" value="Submit">
................................................................................
   591    591         return 0;
   592    592       }
   593    593     }
   594    594   #endif
   595    595     zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
   596    596     zCookieValue = P(zCookieName);
   597    597     if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
   598         -  if( captcha_is_correct(0) ){
          598  +  if( captcha_is_correct() ){
   599    599       cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
   600    600       return 0;
   601    601     }
   602    602   
   603    603     /* This appears to be a spider.  Offer the captcha */
   604    604     style_header("Verification");
   605    605     @ <form method='POST' action='%s(g.zPath)'>

Changes to src/cgi.c.

    54     54   ** does the same except "y" is returned in place of NULL if there is not match.
    55     55   */
    56     56   #define P(x)        cgi_parameter((x),0)
    57     57   #define PD(x,y)     cgi_parameter((x),(y))
    58     58   #define PT(x)       cgi_parameter_trimmed((x),0)
    59     59   #define PDT(x,y)    cgi_parameter_trimmed((x),(y))
    60     60   #define PB(x)       cgi_parameter_boolean(x)
    61         -#define PCK(x)      cgi_parameter_checked(x,1)
    62         -#define PIF(x,y)    cgi_parameter_checked(x,y)
    63     61   
    64     62   
    65     63   /*
    66     64   ** Destinations for output text.
    67     65   */
    68     66   #define CGI_HEADER   0
    69     67   #define CGI_BODY     1
................................................................................
   198    196   ** Append text to the header of an HTTP reply
   199    197   */
   200    198   void cgi_append_header(const char *zLine){
   201    199     blob_append(&extraHeader, zLine, -1);
   202    200   }
   203    201   
   204    202   /*
   205         -** Set a cookie by queuing up the appropriate HTTP header output. If
   206         -** !g.isHTTP, this is a no-op.
          203  +** Set a cookie.
   207    204   **
   208    205   ** Zero lifetime implies a session cookie.
   209    206   */
   210    207   void cgi_set_cookie(
   211    208     const char *zName,    /* Name of the cookie */
   212    209     const char *zValue,   /* Value of the cookie.  Automatically escaped */
   213    210     const char *zPath,    /* Path cookie applies to.  NULL means "/" */
   214    211     int lifetime          /* Expiration of the cookie in seconds from now */
   215    212   ){
   216         -  char const *zSecure = "";
   217         -  if(!g.isHTTP) return /* e.g. JSON CLI mode, where g.zTop is not set */;
   218         -  else if( zPath==0 ){
          213  +  char *zSecure = "";
          214  +  if( zPath==0 ){
   219    215       zPath = g.zTop;
   220    216       if( zPath[0]==0 ) zPath = "/";
   221    217     }
   222    218     if( g.zBaseURL!=0 && strncmp(g.zBaseURL, "https:", 6)==0 ){
   223    219       zSecure = " secure;";
   224    220     }
   225    221     if( lifetime>0 ){
................................................................................
   234    230   }
   235    231   
   236    232   
   237    233   /*
   238    234   ** Return true if the response should be sent with Content-Encoding: gzip.
   239    235   */
   240    236   static int is_gzippable(void){
   241         -  if( g.fNoHttpCompress ) return 0;
   242    237     if( strstr(PD("HTTP_ACCEPT_ENCODING", ""), "gzip")==0 ) return 0;
   243    238     return strncmp(zContentType, "text/", 5)==0
   244    239       || sqlite3_strglob("application/*xml", zContentType)==0
   245    240       || sqlite3_strglob("application/*javascript", zContentType)==0;
   246    241   }
   247    242   
   248    243   /*
................................................................................
   337    332         if( size>0 ){
   338    333           fwrite(blob_buffer(&cgiContent[i]), 1, size, g.httpOut);
   339    334         }
   340    335       }
   341    336     }
   342    337     fflush(g.httpOut);
   343    338     CGIDEBUG(("DONE\n"));
   344         -
   345         -  /* After the webpage has been sent, do any useful background
   346         -  ** processing.
   347         -  */
   348         -  g.cgiOutput = 2;
   349         -  if( g.db!=0 && iReplyStatus==200 ){
   350         -    backoffice_check_if_needed();
   351         -  }
   352    339   }
   353    340   
   354    341   /*
   355    342   ** Do a redirect request to the URL given in the argument.
   356    343   **
   357    344   ** The URL must be relative to the base of the fossil server.
   358    345   */
................................................................................
   887    874     if( z==0 ){
   888    875       if( pLog ) fclose(pLog);
   889    876       pLog = 0;
   890    877       return;
   891    878     }
   892    879     if( pLog==0 ){
   893    880       char zFile[50];
   894         -#if defined(_WIN32)
   895    881       unsigned r;
   896    882       sqlite3_randomness(sizeof(r), &r);
   897    883       sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%08x.txt", r);
   898         -#else
   899         -    sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%05d.txt", getpid());
   900         -#endif
   901    884       pLog = fossil_fopen(zFile, "wb");
   902    885       if( pLog ){
   903    886         fprintf(stderr, "# open log on %s\n", zFile);
   904    887       }else{
   905    888         fprintf(stderr, "# failed to open %s\n", zFile);
   906    889         return;
   907    890       }
................................................................................
  1108   1091     }
  1109   1092     CGIDEBUG(("no-match [%s]\n", zName));
  1110   1093     return zDefault;
  1111   1094   }
  1112   1095   
  1113   1096   /*
  1114   1097   ** Return the value of a CGI parameter with leading and trailing
  1115         -** spaces removed and with internal \r\n changed to just \n
         1098  +** spaces removed.
  1116   1099   */
  1117   1100   char *cgi_parameter_trimmed(const char *zName, const char *zDefault){
  1118   1101     const char *zIn;
  1119         -  char *zOut, c;
  1120         -  int i, j;
         1102  +  char *zOut;
         1103  +  int i;
  1121   1104     zIn = cgi_parameter(zName, 0);
  1122   1105     if( zIn==0 ) zIn = zDefault;
  1123         -  if( zIn==0 ) return 0;
  1124   1106     while( fossil_isspace(zIn[0]) ) zIn++;
  1125   1107     zOut = fossil_strdup(zIn);
  1126         -  for(i=j=0; (c = zOut[i])!=0; i++){
  1127         -    if( c=='\r' && zOut[i+1]=='\n' ) continue;
  1128         -    zOut[j++] = c;
  1129         -  }
  1130         -  zOut[j] = 0;
  1131         -  while( j>0 && fossil_isspace(zOut[j-1]) ) zOut[--j] = 0;
         1108  +  for(i=0; zOut[i]; i++){}
         1109  +  while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0;
  1132   1110     return zOut;
  1133   1111   }
  1134   1112   
  1135   1113   /*
  1136   1114   ** Return true if the CGI parameter zName exists and is not equal to 0,
  1137   1115   ** or "no" or "off".
  1138   1116   */
  1139   1117   int cgi_parameter_boolean(const char *zName){
  1140   1118     const char *zIn = cgi_parameter(zName, 0);
  1141   1119     if( zIn==0 ) return 0;
  1142   1120     return zIn[0]==0 || is_truth(zIn);
  1143   1121   }
  1144   1122   
  1145         -/*
  1146         -** Return either an empty string "" or the string "checked" depending
  1147         -** on whether or not parameter zName has value iValue.  If parameter
  1148         -** zName does not exist, that is assumed to be the same as value 0.
  1149         -**
  1150         -** This routine implements the PCK(x) and PIF(x,y) macros.  The PIF(x,y)
  1151         -** macro generateds " checked" if the value of parameter x equals integer y.
  1152         -** PCK(x) is the same as PIF(x,1).  These macros are used to generate
  1153         -** the "checked" attribute on checkbox and radio controls of forms.
  1154         -*/
  1155         -const char *cgi_parameter_checked(const char *zName, int iValue){
  1156         -  const char *zIn = cgi_parameter(zName,0);
  1157         -  int x;
  1158         -  if( zIn==0 ){
  1159         -    x = 0;
  1160         -  }else if( !fossil_isdigit(zIn[0]) ){
  1161         -    x = is_truth(zIn);
  1162         -  }else{
  1163         -    x = atoi(zIn);
  1164         -  }
  1165         -  return x==iValue ? "checked" : "";
  1166         -}
  1167         -
  1168   1123   /*
  1169   1124   ** Return the name of the i-th CGI parameter.  Return NULL if there
  1170   1125   ** are fewer than i registered CGI parameters.
  1171   1126   */
  1172   1127   const char *cgi_parameter_name(int i){
  1173   1128     if( i>=0 && i<nUsedQP ){
  1174   1129       return aParamQP[i].zName;
................................................................................
  1224   1179   
  1225   1180   /*
  1226   1181   ** Print all query parameters on standard output.  Format the
  1227   1182   ** parameters as HTML.  This is used for testing and debugging.
  1228   1183   **
  1229   1184   ** Omit the values of the cookies unless showAll is true.
  1230   1185   */
  1231         -void cgi_print_all(int showAll, int onConsole){
         1186  +void cgi_print_all(int showAll){
  1232   1187     int i;
  1233   1188     cgi_parameter("","");  /* Force the parameters into sorted order */
  1234   1189     for(i=0; i<nUsedQP; i++){
  1235   1190       const char *zName = aParamQP[i].zName;
  1236   1191       if( !showAll ){
  1237   1192         if( fossil_stricmp("HTTP_COOKIE",zName)==0 ) continue;
  1238   1193         if( fossil_strnicmp("fossil-",zName,7)==0 ) continue;
  1239   1194       }
  1240         -    if( onConsole ){
  1241         -      fossil_trace("%s = %s\n", zName, aParamQP[i].zValue);
  1242         -    }else{
  1243         -      cgi_printf("%h = %h  <br />\n", zName, aParamQP[i].zValue);
  1244         -    }
         1195  +    cgi_printf("%h = %h  <br />\n", zName, aParamQP[i].zValue);
  1245   1196     }
  1246   1197   }
  1247   1198   
  1248   1199   /*
  1249   1200   ** Export all untagged query parameters (but not cookies or environment
  1250   1201   ** variables) as hidden values of a form.
  1251   1202   */
................................................................................
  1386   1337       zInput++;
  1387   1338       while( fossil_isspace(*zInput) ){ zInput++; }
  1388   1339     }
  1389   1340     if( zLeftOver ){ *zLeftOver = zInput; }
  1390   1341     return zResult;
  1391   1342   }
  1392   1343   
  1393         -/*
  1394         -** Determine the IP address on the other side of a connection.
  1395         -** Return a pointer to a string.  Or return 0 if unable.
  1396         -**
  1397         -** The string is held in a static buffer that is overwritten on
  1398         -** each call.
  1399         -*/
  1400         -char *cgi_remote_ip(int fd){
  1401         -#if 0
  1402         -  static char zIp[100];
  1403         -  struct sockaddr_in6 addr;
  1404         -  socklen_t sz = sizeof(addr);
  1405         -  if( getpeername(fd, &addr, &sz) ) return 0;
  1406         -  zIp[0] = 0;
  1407         -  if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){
  1408         -    return 0;
  1409         -  }
  1410         -  return zIp;
  1411         -#else
  1412         -  struct sockaddr_in remoteName;
  1413         -  socklen_t size = sizeof(struct sockaddr_in);
  1414         -  if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0;
  1415         -  return inet_ntoa(remoteName.sin_addr);
  1416         -#endif
  1417         -}
  1418         -
  1419   1344   /*
  1420   1345   ** This routine handles a single HTTP request which is coming in on
  1421   1346   ** g.httpIn and which replies on g.httpOut
  1422   1347   **
  1423   1348   ** The HTTP request is read from g.httpIn and is used to initialize
  1424   1349   ** entries in the cgi_parameter() hash, as if those entries were
  1425   1350   ** environment variables.  A call to cgi_init() completes
  1426   1351   ** the setup.  Once all the setup is finished, this procedure returns
  1427   1352   ** and subsequent code handles the actual generation of the webpage.
  1428   1353   */
  1429   1354   void cgi_handle_http_request(const char *zIpAddr){
  1430   1355     char *z, *zToken;
  1431   1356     int i;
         1357  +  struct sockaddr_in remoteName;
         1358  +  socklen_t size = sizeof(struct sockaddr_in);
  1432   1359     char zLine[2000];     /* A single line of input. */
  1433   1360     g.fullHttpReply = 1;
  1434   1361     if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){
  1435   1362       malformed_request("missing HTTP header");
  1436   1363     }
  1437   1364     blob_append(&g.httpHeader, zLine, -1);
  1438   1365     cgi_trace(zLine);
................................................................................
  1452   1379     }
  1453   1380     cgi_setenv("REQUEST_URI", zToken);
  1454   1381     cgi_setenv("SCRIPT_NAME", "");
  1455   1382     for(i=0; zToken[i] && zToken[i]!='?'; i++){}
  1456   1383     if( zToken[i] ) zToken[i++] = 0;
  1457   1384     cgi_setenv("PATH_INFO", zToken);
  1458   1385     cgi_setenv("QUERY_STRING", &zToken[i]);
  1459         -  if( zIpAddr==0 ){
  1460         -    zIpAddr = cgi_remote_ip(fileno(g.httpIn));
         1386  +  if( zIpAddr==0 &&
         1387  +        getpeername(fileno(g.httpIn), (struct sockaddr*)&remoteName,
         1388  +                                &size)>=0
         1389  +  ){
         1390  +    zIpAddr = inet_ntoa(remoteName.sin_addr);
  1461   1391     }
  1462   1392     if( zIpAddr ){
  1463   1393       cgi_setenv("REMOTE_ADDR", zIpAddr);
  1464   1394       g.zIpAddr = mprintf("%s", zIpAddr);
  1465   1395     }
  1466   1396   
  1467   1397     /* Get all the optional fields that follow the first line.
................................................................................
  1917   1847             int nErr = 0, fd;
  1918   1848             close(0);
  1919   1849             fd = dup(connection);
  1920   1850             if( fd!=0 ) nErr++;
  1921   1851             close(1);
  1922   1852             fd = dup(connection);
  1923   1853             if( fd!=1 ) nErr++;
  1924         -          if( 0 && !g.fAnyTrace ){
         1854  +          if( !g.fAnyTrace ){
  1925   1855               close(2);
  1926   1856               fd = dup(connection);
  1927   1857               if( fd!=2 ) nErr++;
  1928   1858             }
  1929   1859             close(connection);
  1930   1860             g.nPendingRequest = nchildren+1;
  1931   1861             g.nRequest = nRequest+1;
................................................................................
  1935   1865       }
  1936   1866       /* Bury dead children */
  1937   1867       if( nchildren ){
  1938   1868         while(1){
  1939   1869           int iStatus = 0;
  1940   1870           pid_t x = waitpid(-1, &iStatus, WNOHANG);
  1941   1871           if( x<=0 ) break;
  1942         -        if( WIFSIGNALED(iStatus) && g.fAnyTrace ){
  1943         -          fprintf(stderr, "/***** Child %d exited on signal %d (%s) *****/\n",
  1944         -                  x, WTERMSIG(iStatus), strsignal(WTERMSIG(iStatus)));
  1945         -        }
  1946   1872           nchildren--;
  1947   1873         }
  1948   1874       }  
  1949   1875     }
  1950   1876     /* NOT REACHED */
  1951   1877     fossil_exit(1);
  1952   1878   #endif
................................................................................
  1964   1890       {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
  1965   1891        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 0};
  1966   1892   
  1967   1893   
  1968   1894   /*
  1969   1895   ** Returns an RFC822-formatted time string suitable for HTTP headers.
  1970   1896   ** The timezone is always GMT.  The value returned is always a
  1971         -** string obtained from mprintf() and must be freed using fossil_free()
  1972         -** to avoid a memory leak.
         1897  +** string obtained from mprintf() and must be freed using free() to
         1898  +** avoid a memory leak.
  1973   1899   **
  1974   1900   ** See http://www.faqs.org/rfcs/rfc822.html, section 5
  1975   1901   ** and http://www.faqs.org/rfcs/rfc2616.html, section 3.3.
  1976   1902   */
  1977   1903   char *cgi_rfc822_datestamp(time_t now){
  1978   1904     struct tm *pTm;
  1979   1905     pTm = gmtime(&now);
  1980   1906     if( pTm==0 ){
  1981   1907       return mprintf("");
  1982   1908     }else{
  1983         -    return mprintf("%s, %d %s %02d %02d:%02d:%02d +0000",
         1909  +    return mprintf("%s, %d %s %02d %02d:%02d:%02d GMT",
  1984   1910                      azDays[pTm->tm_wday], pTm->tm_mday, azMonths[pTm->tm_mon],
  1985   1911                      pTm->tm_year+1900, pTm->tm_hour, pTm->tm_min, pTm->tm_sec);
  1986   1912     }
  1987   1913   }
  1988   1914   
  1989   1915   /*
  1990   1916   ** Parse an RFC822-formatted timestamp as we'd expect from HTTP and return

Changes to src/checkin.c.

  1190   1190   #if defined(__CYGWIN__)
  1191   1191       zEditor = fossil_utf8_to_path(zEditor, 0);
  1192   1192       blob_add_cr(pPrompt);
  1193   1193   #endif
  1194   1194     }
  1195   1195   #endif
  1196   1196     if( zEditor==0 ){
  1197         -    if( blob_size(pPrompt)>0 ){
  1198         -      blob_append(pPrompt,
  1199         -         "#\n"
  1200         -         "# Since no default text editor is set using EDITOR or VISUAL\n"
  1201         -         "# environment variables or the \"fossil set editor\" command,\n"
  1202         -         "# and because no comment was specified using the \"-m\" or \"-M\"\n"
  1203         -         "# command-line options, you will need to enter the comment below.\n"
  1204         -         "# Type \".\" on a line by itself when you are done:\n", -1);
  1205         -    }
         1197  +    blob_append(pPrompt,
         1198  +       "#\n"
         1199  +       "# Since no default text editor is set using EDITOR or VISUAL\n"
         1200  +       "# environment variables or the \"fossil set editor\" command,\n"
         1201  +       "# and because no comment was specified using the \"-m\" or \"-M\"\n"
         1202  +       "# command-line options, you will need to enter the comment below.\n"
         1203  +       "# Type \".\" on a line by itself when you are done:\n", -1);
  1206   1204       zFile = mprintf("-");
  1207   1205     }else{
  1208   1206       Blob fname;
  1209   1207       blob_zero(&fname);
  1210   1208       if( g.zLocalRoot!=0 ){
  1211   1209         file_relative_name(g.zLocalRoot, &fname, 1);
  1212   1210         zFile = db_text(0, "SELECT '%qci-comment-'||hex(randomblob(6))||'.txt'",
................................................................................
  1216   1214         zFile = mprintf("%s", blob_str(&fname));
  1217   1215       }
  1218   1216       blob_reset(&fname);
  1219   1217     }
  1220   1218   #if defined(_WIN32)
  1221   1219     blob_add_cr(pPrompt);
  1222   1220   #endif
  1223         -  if( blob_size(pPrompt)>0 ) blob_write_to_file(pPrompt, zFile);
         1221  +  blob_write_to_file(pPrompt, zFile);
  1224   1222     if( zEditor ){
  1225   1223       zCmd = mprintf("%s \"%s\"", zEditor, zFile);
  1226   1224       fossil_print("%s\n", zCmd);
  1227   1225       if( fossil_system(zCmd) ){
  1228   1226         fossil_fatal("editor aborted: \"%s\"", zCmd);
  1229   1227       }
  1230   1228   
................................................................................
  2123   2121     if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
  2124   2122     useCksum = db_get_boolean("repo-cksum", 1);
  2125   2123     outputManifest = db_get_manifest_setting();
  2126   2124     verify_all_options();
  2127   2125   
  2128   2126     /* Do not allow the creation of a new branch using an existing open
  2129   2127     ** branch name unless the --force flag is used */
  2130         -  if( sCiInfo.zBranch!=0
  2131         -   && !forceFlag
  2132         -   && fossil_strcmp(sCiInfo.zBranch,"private")!=0
  2133         -   && branch_is_open(sCiInfo.zBranch)
  2134         -  ){
         2128  +  if( sCiInfo.zBranch!=0 && !forceFlag && branch_is_open(sCiInfo.zBranch) ){
  2135   2129       fossil_fatal("an open branch named \"%s\" already exists - use --force"
  2136   2130                    " to override", sCiInfo.zBranch);
  2137   2131     }
  2138   2132   
  2139   2133     /* Escape special characters in tags and put all tags in sorted order */
  2140   2134     if( nTag ){
  2141   2135       int i;

Changes to src/checkout.c.

   289    289         return;
   290    290       }
   291    291     }else{
   292    292       zVers = g.argv[2];
   293    293     }
   294    294     vid = load_vfile(zVers, forceMissingFlag);
   295    295     if( prior==vid ){
   296         -    db_end_transaction(0);
   297    296       return;
   298    297     }
   299    298     if( !keepFlag ){
   300    299       uncheckout(prior);
   301    300     }
   302    301     db_multi_exec("DELETE FROM vfile WHERE vid!=%d", vid);
   303    302     if( !keepFlag ){

Changes to src/clone.c.

   107    107   **
   108    108   ** By default, your current login name is used to create the default
   109    109   ** admin user. This can be overridden using the -A|--admin-user
   110    110   ** parameter.
   111    111   **
   112    112   ** Options:
   113    113   **    --admin-user|-A USERNAME   Make USERNAME the administrator
   114         -**    --nocompress               Omit extra delta compression
   115    114   **    --once                     Don't remember the URI.
   116    115   **    --private                  Also clone private branches
   117    116   **    --ssl-identity FILENAME    Use the SSL identity if requested by the server
   118    117   **    --ssh-command|-c SSH       Use SSH as the "ssh" command
   119    118   **    --httpauth|-B USER:PASS    Add HTTP Basic Authorization to requests
   120    119   **    -u|--unversioned           Also sync unversioned content
   121    120   **    -v|--verbose               Show more statistics in output
................................................................................
   125    124   void clone_cmd(void){
   126    125     char *zPassword;
   127    126     const char *zDefaultUser;   /* Optional name of the default user */
   128    127     const char *zHttpAuth;      /* HTTP Authorization user:pass information */
   129    128     int nErr = 0;
   130    129     int urlFlags = URL_PROMPT_PW | URL_REMEMBER;
   131    130     int syncFlags = SYNC_CLONE;
   132         -  int noCompress = find_option("nocompress",0,0)!=0;
   133    131   
   134    132     /* Also clone private branches */
   135    133     if( find_option("private",0,0)!=0 ) syncFlags |= SYNC_PRIVATE;
   136    134     if( find_option("once",0,0)!=0) urlFlags &= ~URL_REMEMBER;
   137    135     if( find_option("verbose","v",0)!=0) syncFlags |= SYNC_VERBOSE;
   138    136     if( find_option("unversioned","u",0)!=0 ) syncFlags |= SYNC_UNVERSIONED;
   139    137     zHttpAuth = find_option("httpauth","B",1);
................................................................................
   211    209         fossil_fatal("server returned an error - clone aborted");
   212    210       }
   213    211       db_open_repository(g.argv[3]);
   214    212     }
   215    213     db_begin_transaction();
   216    214     fossil_print("Rebuilding repository meta-data...\n");
   217    215     rebuild_db(0, 1, 0);
   218         -  if( !noCompress ){
   219         -    fossil_print("Extra delta compression... "); fflush(stdout);
   220         -    extra_deltification();
   221         -    fossil_print("\n");
   222         -  }
          216  +  fossil_print("Extra delta compression... "); fflush(stdout);
          217  +  extra_deltification();
   223    218     db_end_transaction(0);
   224         -  fossil_print("Vacuuming the database... "); fflush(stdout);
          219  +  fossil_print("\nVacuuming the database... "); fflush(stdout);
   225    220     if( db_int(0, "PRAGMA page_count")>1000
   226    221      && db_int(0, "PRAGMA page_size")<8192 ){
   227    222        db_multi_exec("PRAGMA page_size=8192;");
   228    223     }
   229    224     db_multi_exec("VACUUM");
   230    225     fossil_print("\nproject-id: %s\n", db_get("project-code", 0));
   231    226     fossil_print("server-id:  %s\n", db_get("server-code", 0));

Changes to src/codecheck1.c.

    37     37   */
    38     38   #include <stdio.h>
    39     39   #include <stdlib.h>
    40     40   #include <ctype.h>
    41     41   #include <string.h>
    42     42   #include <assert.h>
    43     43   
    44         -/*
    45         -** Debugging switch
    46         -*/
    47         -static int eVerbose = 0;
    48         -
    49     44   /*
    50     45   ** Malloc, aborting if it fails.
    51     46   */
    52     47   void *safe_malloc(int nByte){
    53     48     void *x = malloc(nByte);
    54     49     if( x==0 ){
    55     50       fprintf(stderr, "failed to allocate %d bytes\n", nByte);
................................................................................
   202    197   ** Return the first non-whitespace characters in z[]
   203    198   */
   204    199   static const char *skip_space(const char *z){
   205    200     while( isspace(z[0]) ){ z++; }
   206    201     return z;
   207    202   }
   208    203   
   209         -/*
   210         -** Remove excess whitespace and nested "()" from string z.
   211         -*/
   212         -static char *simplify_expr(char *z){
   213         -  int n = (int)strlen(z);
   214         -  while( n>0 ){
   215         -    if( isspace(z[0]) ){
   216         -      z++;
   217         -      n--;
   218         -      continue;
   219         -    }
   220         -    if( z[0]=='(' && z[n-1]==')' ){
   221         -      z++;
   222         -      n -= 2;
   223         -      continue;
   224         -    }
   225         -    break;
   226         -  }
   227         -  z[n] = 0;
   228         -  return z;
   229         -}
   230         -
   231    204   /*
   232    205   ** Return true if the input is a string literal.
   233    206   */
   234    207   static int is_string_lit(const char *z){
   235    208     int nu1, nu2;
   236    209     z = next_non_whitespace(z, &nu1, &nu2);
   237    210     if( strcmp(z, "NULL")==0 ) return 1;
................................................................................
   293    266     "db_setting_inop_rhs",
   294    267   };
   295    268   
   296    269   /*
   297    270   ** Return true if the input is an argument that is safe to use with %s
   298    271   ** while building an SQL statement.
   299    272   */
   300         -static int is_sql_safe(const char *z){
          273  +static int is_s_safe(const char *z){
   301    274     int len, eType;
   302    275     int i;
   303    276   
   304    277     /* A string literal is safe for use with %s */
   305    278     if( is_string_lit(z) ) return 1;
   306    279   
   307    280     /* Certain functions are guaranteed to return a string that is safe
................................................................................
   323    296     /* If the "safe-for-%s" comment appears in the argument, then
   324    297     ** let it through */
   325    298     if( strstr(z, "/*safe-for-%s*/")!=0 ) return 1;
   326    299   
   327    300     return 0;
   328    301   }
   329    302   
   330         -/*
   331         -** Return true if the input is an argument that is never safe for use
   332         -** with %s.
   333         -*/
   334         -static int never_safe(const char *z){
   335         -  if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
   336         -  if( z[0]=='P' ){
   337         -    if( strncmp(z,"PIF(",4)==0 ) return 0;
   338         -    if( strncmp(z,"PCK(",4)==0 ) return 0;
   339         -    return 1;
   340         -  }
   341         -  if( strncmp(z,"cgi_param",9)==0 ) return 1;
   342         -  return 0;
   343         -}
   344         -
   345    303   /*
   346    304   ** Processing flags
   347    305   */
   348         -#define FMT_SQL   0x00001     /* Generates SQL text */
   349         -#define FMT_HTML  0x00002     /* Generates HTML text */
   350         -#define FMT_URL   0x00004     /* Generates URLs */
   351         -#define FMT_SAFE  0x00008     /* Always safe for %s */
          306  +#define FMT_NO_S   0x00001     /* Do not allow %s substitutions */
   352    307   
   353    308   /*
   354    309   ** A list of internal Fossil interfaces that take a printf-style format
   355    310   ** string.
   356    311   */
   357    312   struct {
   358    313     const char *zFName;    /* Name of the function */
   359    314     int iFmtArg;           /* Index of format argument.  Leftmost is 1. */
   360    315     unsigned fmtFlags;     /* Processing flags */
   361    316   } aFmtFunc[] = {
   362    317     { "admin_log",               1, 0 },
   363         -  { "blob_append_sql",         2, FMT_SQL },
          318  +  { "blob_append_sql",         2, FMT_NO_S },
   364    319     { "blob_appendf",            2, 0 },
   365         -  { "cgi_debug",               1, FMT_SAFE },
   366         -  { "cgi_panic",               1, FMT_SAFE },
   367         -  { "cgi_printf",              1, FMT_HTML },
   368         -  { "cgi_redirectf",           1, FMT_URL },
   369         -  { "chref",                   2, FMT_URL },
   370         -  { "db_blob",                 2, FMT_SQL },
   371         -  { "db_debug",                1, FMT_SQL },
   372         -  { "db_double",               2, FMT_SQL },
          320  +  { "cgi_debug",               1, 0 },
          321  +  { "cgi_panic",               1, 0 },
          322  +  { "cgi_printf",              1, 0 },
          323  +  { "cgi_redirectf",           1, 0 },
          324  +  { "chref",                   2, 0 },
          325  +  { "db_blob",                 2, FMT_NO_S },
          326  +  { "db_debug",                1, FMT_NO_S },
          327  +  { "db_double",               2, FMT_NO_S },
   373    328     { "db_err",                  1, 0 },
   374         -  { "db_exists",               1, FMT_SQL },
          329  +  { "db_exists",               1, FMT_NO_S },
   375    330     { "db_get_mprintf",          2, 0 },
   376         -  { "db_int",                  2, FMT_SQL },
   377         -  { "db_int64",                2, FMT_SQL },
   378         -  { "db_multi_exec",           1, FMT_SQL },
   379         -  { "db_optional_sql",         2, FMT_SQL },
   380         -  { "db_prepare",              2, FMT_SQL },
   381         -  { "db_prepare_ignore_error", 2, FMT_SQL },
          331  +  { "db_int",                  2, FMT_NO_S },
          332  +  { "db_int64",                2, FMT_NO_S },
          333  +  { "db_multi_exec",           1, FMT_NO_S },
          334  +  { "db_optional_sql",         2, FMT_NO_S },
          335  +  { "db_prepare",              2, FMT_NO_S },
          336  +  { "db_prepare_ignore_error", 2, FMT_NO_S },
   382    337     { "db_set_mprintf",          3, 0 },
   383         -  { "db_static_prepare",       2, FMT_SQL },
   384         -  { "db_text",                 2, FMT_SQL },
          338  +  { "db_static_prepare",       2, FMT_NO_S },
          339  +  { "db_text",                 2, FMT_NO_S },
   385    340     { "db_unset_mprintf",        2, 0 },
   386         -  { "form_begin",              2, FMT_URL },
   387         -  { "fossil_error",            2, FMT_SAFE },
   388         -  { "fossil_errorlog",         1, FMT_SAFE },
   389         -  { "fossil_fatal",            1, FMT_SAFE },
   390         -  { "fossil_fatal_recursive",  1, FMT_SAFE },
   391         -  { "fossil_panic",            1, FMT_SAFE },
   392         -  { "fossil_print",            1, FMT_SAFE },
   393         -  { "fossil_trace",            1, FMT_SAFE },
   394         -  { "fossil_warning",          1, FMT_SAFE },
   395         -  { "href",                    1, FMT_URL },
          341  +  { "form_begin",              2, 0 },
          342  +  { "fossil_error",            2, 0 },
          343  +  { "fossil_errorlog",         1, 0 },
          344  +  { "fossil_fatal",            1, 0 },
          345  +  { "fossil_fatal_recursive",  1, 0 },
          346  +  { "fossil_panic",            1, 0 },
          347  +  { "fossil_print",            1, 0 },
          348  +  { "fossil_trace",            1, 0 },
          349  +  { "fossil_warning",          1, 0 },
          350  +  { "href",                    1, 0 },
   396    351     { "json_new_string_f",       1, 0 },
   397    352     { "json_set_err",            2, 0 },
   398    353     { "json_warn",               2, 0 },
   399    354     { "mprintf",                 1, 0 },
   400    355     { "socket_set_errmsg",       1, 0 },
   401    356     { "ssl_set_errmsg",          1, 0 },
   402         -  { "style_header",            1, FMT_HTML },
   403         -  { "style_js_onload",         1, FMT_HTML },
   404         -  { "style_set_current_page",  1, FMT_URL },
   405         -  { "style_submenu_element",   2, FMT_URL },
   406         -  { "style_submenu_sql",       3, FMT_SQL },
   407         -  { "webpage_error",           1, FMT_SAFE },
   408         -  { "xhref",                   2, FMT_URL },
          357  +  { "style_header",            1, 0 },
          358  +  { "style_set_current_page",  1, 0 },
          359  +  { "style_submenu_element",   2, 0 },
          360  +  { "style_submenu_sql",       3, 0 },
          361  +  { "webpage_error",           1, 0 },
          362  +  { "xhref",                   2, 0 },
   409    363   };
   410    364   
   411    365   /*
   412    366   ** Determine if the indentifier zIdent of length nIndent is a Fossil
   413    367   ** internal interface that uses a printf-style argument.  Return zero if not.
   414    368   ** Return the index of the format string if true with the left-most
   415    369   ** argument having an index of 1.
................................................................................
   508    462     zCopy = safe_malloc( len + 1 );
   509    463     memcpy(zCopy, zStart+1, len);
   510    464     zCopy[len] = 0;
   511    465     azArg = 0;
   512    466     nArg = 0;
   513    467     z = zCopy;
   514    468     while( z[0] ){
   515         -    char cEnd;
   516    469       len = distance_to(z, ',');
   517         -    cEnd = z[len];
          470  +    azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1));
          471  +    azArg[nArg++] = skip_space(z);
          472  +    if( z[len]==0 ) break;
   518    473       z[len] = 0;
   519         -    azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1));
   520         -    azArg[nArg++] = simplify_expr(z);
   521         -    if( cEnd==0 ) break;
          474  +    for(i=len-1; i>0 && isspace(z[i]); i--){ z[i] = 0; }
   522    475       z += len + 1;
   523    476     }
   524    477     acType = (char*)&azArg[nArg];
   525    478     if( fmtArg>nArg ){
   526    479       printf("%s:%d: too few arguments to %.*s()\n",
   527    480              zFilename, lnFCall, szFName, zFCall);
   528    481       nErr++;
................................................................................
   537    490       }else if( (k = formatArgCount(zFmt, nArg, acType))>=0
   538    491                && nArg!=fmtArg+k ){
   539    492         printf("%s:%d: too %s arguments to %.*s() "
   540    493                "- got %d and expected %d\n",
   541    494                zFilename, lnFCall, (nArg<fmtArg+k ? "few" : "many"),
   542    495                szFName, zFCall, nArg, fmtArg+k);
   543    496         nErr++;
   544         -    }else if( (fmtFlags & FMT_SAFE)==0 ){
          497  +    }else if( fmtFlags & FMT_NO_S ){
   545    498         for(i=0; i<nArg && i<k; i++){
   546         -        if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b') ){
   547         -          const char *zExpr = azArg[fmtArg+i];
   548         -          if( never_safe(zExpr) ){
   549         -            printf("%s:%d: Argument %d to %.*s() is not safe for"
   550         -                   " a query parameter\n",
   551         -               zFilename, lnFCall, i+fmtArg, szFName, zFCall);
   552         -             nErr++;
   553         -   
   554         -          }else if( (fmtFlags & FMT_SQL)!=0 && !is_sql_safe(zExpr) ){
   555         -            printf("%s:%d: Argument %d to %.*s() not safe for SQL\n",
   556         -               zFilename, lnFCall, i+fmtArg, szFName, zFCall);
   557         -             nErr++;
   558         -          }
          499  +        if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b')
          500  +         && !is_s_safe(azArg[fmtArg+i])
          501  +        ){
          502  +           printf("%s:%d: Argument %d to %.*s() not safe for SQL\n",
          503  +             zFilename, lnFCall, i+fmtArg, szFName, zFCall);
          504  +           nErr++;
   559    505           }
   560    506         }
   561    507       }
   562    508     }
   563    509     if( nErr ){
   564    510       for(i=0; i<nArg; i++){
   565    511         printf("   arg[%d]: %s\n", i, azArg[i]);
   566    512       }
   567         -  }else if( eVerbose>1 ){
   568         -    printf("%s:%d: %.*s() ok for %d arguments\n",
   569         -      zFilename, lnFCall, szFName, zFCall, nArg);
   570    513     }
          514  +
   571    515     free((char*)azArg);
   572    516     free(zCopy);
   573    517     return nErr;
   574    518   }
   575    519   
   576    520   
   577    521   /*
................................................................................
   617    561     }
   618    562     return nErr;
   619    563   }
   620    564   
   621    565   /*
   622    566   ** Check for format-string design rule violations on all files listed
   623    567   ** on the command-line.
   624         -**
   625         -** The eVerbose global variable is incremented with each "-v" argument.
   626    568   */
   627    569   int main(int argc, char **argv){
   628    570     int i;
   629    571     int nErr = 0;
   630    572     for(i=1; i<argc; i++){
   631         -    char *zFile;
   632         -    if( strcmp(argv[i],"-v")==0 ){
   633         -      eVerbose++;
   634         -      continue;
   635         -    }
   636         -    if( eVerbose>0 ) printf("Processing %s...\n", argv[i]);
   637         -    zFile = read_file(argv[i]);
          573  +    char *zFile = read_file(argv[i]);
   638    574       nErr += scan_file(argv[i], zFile);
   639    575       free(zFile);
   640    576     }
   641    577     return nErr;
   642    578   }

Changes to src/configure.c.

    34     34   #define CONFIGSET_TKT       0x000004     /* Ticket configuration */
    35     35   #define CONFIGSET_PROJ      0x000008     /* Project name */
    36     36   #define CONFIGSET_SHUN      0x000010     /* Shun settings */
    37     37   #define CONFIGSET_USER      0x000020     /* The USER table */
    38     38   #define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
    39     39   #define CONFIGSET_XFER      0x000080     /* Transfer configuration */
    40     40   #define CONFIGSET_ALIAS     0x000100     /* URL Aliases */
    41         -#define CONFIGSET_SCRIBER   0x000200     /* Email subscribers */
    42         -#define CONFIGSET_ALL       0x0003ff     /* Everything */
           41  +
           42  +#define CONFIGSET_ALL       0x0001ff     /* Everything */
    43     43   
    44     44   #define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
    45     45   
    46     46   /*
    47     47   ** This mask is used for the common TH1 configuration settings (i.e. those
    48     48   ** that are not specific to one particular subsystem, such as the transfer
    49     49   ** subsystem).
................................................................................
    56     56   ** Names of the configuration sets
    57     57   */
    58     58   static struct {
    59     59     const char *zName;   /* Name of the configuration set */
    60     60     int groupMask;       /* Mask for that configuration set */
    61     61     const char *zHelp;   /* What it does */
    62     62   } aGroupName[] = {
    63         -  { "/email",       CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
    64         -  { "/project",     CONFIGSET_PROJ,  "Project name and description"         },
    65         -  { "/skin",        CONFIGSET_SKIN | CONFIGSET_CSS,
    66         -                                     "Web interface appearance settings"    },
    67         -  { "/css",         CONFIGSET_CSS,   "Style sheet"                          },
    68         -  { "/shun",        CONFIGSET_SHUN,  "List of shunned artifacts"            },
    69         -  { "/ticket",      CONFIGSET_TKT,   "Ticket setup",                        },
    70         -  { "/user",        CONFIGSET_USER,  "Users and privilege settings"         },
    71         -  { "/xfer",        CONFIGSET_XFER,  "Transfer setup",                      },
    72         -  { "/alias",       CONFIGSET_ALIAS, "URL Aliases",                         },
    73         -  { "/subscriber",  CONFIGSET_SCRIBER,"Email notification subscriber list"  },
    74         -  { "/all",         CONFIGSET_ALL,   "All of the above"                     },
           63  +  { "/email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
           64  +  { "/project",      CONFIGSET_PROJ,  "Project name and description"         },
           65  +  { "/skin",         CONFIGSET_SKIN | CONFIGSET_CSS,
           66  +                                      "Web interface appearance settings"    },
           67  +  { "/css",          CONFIGSET_CSS,   "Style sheet"                          },
           68  +  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
           69  +  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
           70  +  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
           71  +  { "/xfer",         CONFIGSET_XFER,  "Transfer setup",                      },
           72  +  { "/alias",        CONFIGSET_ALIAS, "URL Aliases",                         },
           73  +  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
    75     74   };
    76     75   
    77     76   
    78     77   /*
    79     78   ** The following is a list of settings that we are willing to
    80     79   ** transfer.
    81     80   **
................................................................................
    97     96     { "background-image",       CONFIGSET_SKIN },
    98     97     { "timeline-block-markup",  CONFIGSET_SKIN },
    99     98     { "timeline-max-comment",   CONFIGSET_SKIN },
   100     99     { "timeline-plaintext",     CONFIGSET_SKIN },
   101    100     { "adunit",                 CONFIGSET_SKIN },
   102    101     { "adunit-omit-if-admin",   CONFIGSET_SKIN },
   103    102     { "adunit-omit-if-user",    CONFIGSET_SKIN },
   104         -  { "sitemap-docidx",         CONFIGSET_SKIN },
   105         -  { "sitemap-download",       CONFIGSET_SKIN },
   106         -  { "sitemap-license",        CONFIGSET_SKIN },
   107         -  { "sitemap-contact",        CONFIGSET_SKIN },
   108    103   
   109    104   #ifdef FOSSIL_ENABLE_TH1_DOCS
   110    105     { "th1-docs",               CONFIGSET_TH1 },
   111    106   #endif
   112    107   #ifdef FOSSIL_ENABLE_TH1_HOOKS
   113    108     { "th1-hooks",              CONFIGSET_TH1 },
   114    109   #endif
................................................................................
   160    155   
   161    156     { "@concealed",             CONFIGSET_ADDR },
   162    157   
   163    158     { "@shun",                  CONFIGSET_SHUN },
   164    159   
   165    160     { "@alias",                 CONFIGSET_ALIAS },
   166    161   
   167         -  { "@subscriber",            CONFIGSET_SCRIBER },
   168         -
   169    162     { "xfer-common-script",     CONFIGSET_XFER },
   170    163     { "xfer-push-script",       CONFIGSET_XFER },
   171    164     { "xfer-commit-script",     CONFIGSET_XFER },
   172    165     { "xfer-ticket-script",     CONFIGSET_XFER },
   173    166   
   174    167   };
   175    168   static int iConfig = 0;
................................................................................
   218    211     return blob_sql_text(&x);
   219    212   }
   220    213   
   221    214   /*
   222    215   ** Return the mask for the named configuration parameter if it can be
   223    216   ** safely exported.  Return 0 if the parameter is not safe to export.
   224    217   **
   225         -** "Safe" in the previous paragraph means the permission is granted to
          218  +** "Safe" in the previous paragraph means the permission is created to
   226    219   ** export the property.  In other words, the requesting side has presented
   227    220   ** login credentials and has sufficient capabilities to access the requested
   228    221   ** information.
   229    222   */
   230    223   int configure_is_exportable(const char *zName){
   231    224     int i;
   232    225     int n = strlen(zName);
................................................................................
   234    227       zName++;
   235    228       n -= 2;
   236    229     }
   237    230     for(i=0; i<count(aConfig); i++){
   238    231       if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
   239    232         int m = aConfig[i].groupMask;
   240    233         if( !g.perm.Admin ){
   241         -        m &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER);
          234  +        m &= ~CONFIGSET_USER;
   242    235         }
   243    236         if( !g.perm.RdAddr ){
   244    237           m &= ~CONFIGSET_ADDR;
   245    238         }
   246    239         return m;
   247    240       }
   248    241     }
................................................................................
   317    310   ** sync session.
   318    311   **
   319    312   ** Mask consists of one or more CONFIGSET_* values ORed together, to
   320    313   ** designate what types of configuration we are allowed to receive.
   321    314   **
   322    315   ** NEW FORMAT:
   323    316   **
   324         -** zName is one of:
   325         -**
   326         -**     "/config", "/user",  "/shun", "/reportfmt", "/concealed",
   327         -**     "/subscriber",
   328         -**
          317  +** zName is one of "/config", "/user", "/shun", "/reportfmt", or "/concealed".
   329    318   ** zName indicates the table that holds the configuration information being
   330    319   ** transferred.  pContent is a string that consist of alternating Fossil
   331    320   ** and SQL tokens.  The First token is a timestamp in seconds since 1970.
   332    321   ** The second token is a primary key for the table identified by zName.  If
   333    322   ** The entry with the corresponding primary key exists and has a more recent
   334    323   ** mtime, then nothing happens.  If the entry does not exist or if it has
   335    324   ** an older mtime, then the content described by subsequent token pairs is
................................................................................
   341    330   **    NAME        CONTENT
   342    331   **    -------     -----------------------------------------------------------
   343    332   **    /config     $MTIME $NAME value $VALUE
   344    333   **    /user       $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE
   345    334   **    /shun       $MTIME $UUID scom $VALUE
   346    335   **    /reportfmt  $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE
   347    336   **    /concealed  $MTIME $HASH content $VALUE
   348         -**    /subscriber $SMTIME $SEMAIL suname $V ...
          337  +**
          338  +** OLD FORMAT:
          339  +**
          340  +** The old format is retained for backwards compatibility, but is deprecated.
          341  +** The cutover from old format to new was on 2011-04-25.  After sufficient
          342  +** time has passed, support for the old format will be removed.
          343  +** Update: Support for the old format was removed on 2017-09-20.
          344  +**
          345  +** zName is either the NAME of an element of the CONFIG table, or else
          346  +** one of the special names "@shun", "@reportfmt", "@user", or "@concealed".
          347  +** If zName is a CONFIG table name, then CONTENT replaces (overwrites) the
          348  +** element in the CONFIG table.  For one of the @-labels, CONTENT is raw
          349  +** SQL that is evaluated.  Note that the raw SQL in CONTENT might not
          350  +** insert directly into the target table but might instead use a proxy
          351  +** table like _fer_reportfmt or _xfer_user.  Such tables must be created
          352  +** ahead of time using configure_prepare_to_receive().  Then after multiple
          353  +** calls to this routine, configure_finalize_receive() to transfer the
          354  +** information received into the true target table.
   349    355   */
   350    356   void configure_receive(const char *zName, Blob *pContent, int groupMask){
   351         -  int checkMask;   /* Masks for which we must first check existance of tables */
   352         -
   353         -  checkMask = CONFIGSET_SCRIBER;
   354    357     if( zName[0]=='/' ){
   355    358       /* The new format */
   356         -    char *azToken[24];
          359  +    char *azToken[12];
   357    360       int nToken = 0;
   358    361       int ii, jj;
   359    362       int thisMask;
   360    363       Blob name, value, sql;
   361    364       static const struct receiveType {
   362         -      const char *zName;         /* Configuration key for this table */
   363         -      const char *zPrimKey;      /* Primary key column */
   364         -      int nField;                /* Number of data fields */
   365         -      const char *azField[6];    /* Names of the data fields */
          365  +      const char *zName;
          366  +      const char *zPrimKey;
          367  +      int nField;
          368  +      const char *azField[4];
   366    369       } aType[] = {
   367         -      { "/config",    "name",  1, { "value", 0,0,0,0,0 }           },
   368         -      { "@user",      "login", 4, { "pw","cap","info","photo",0,0} },
   369         -      { "@shun",      "uuid",  1, { "scom", 0,0,0,0,0}             },
   370         -      { "@reportfmt", "title", 3, { "owner","cols","sqlcode",0,0,0}},
   371         -      { "@concealed", "hash",  1, { "content", 0,0,0,0,0 }         },
   372         -      { "@subscriber","semail",6,
   373         -         { "suname","sdigest","sdonotcall","ssub","sctime","smip"}         },
          370  +      { "/config",    "name",  1, { "value", 0, 0, 0 }              },
          371  +      { "@user",      "login", 4, { "pw", "cap", "info", "photo" }  },
          372  +      { "@shun",      "uuid",  1, { "scom", 0, 0, 0 }               },
          373  +      { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } },
          374  +      { "@concealed", "hash",  1, { "content", 0, 0, 0 }            },
   374    375       };
   375         -
   376         -    /* Locate the receiveType in aType[ii] */
   377    376       for(ii=0; ii<count(aType); ii++){
   378    377         if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
   379    378       }
   380    379       if( ii>=count(aType) ) return;
   381         -
   382    380       while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
   383    381         char *z = blob_terminate(&name);
   384    382         if( !safeSql(z) ) return;
   385    383         if( nToken>0 ){
   386    384           for(jj=0; jj<aType[ii].nField; jj++){
   387    385             if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break;
   388    386           }
................................................................................
   389    387           if( jj>=aType[ii].nField ) continue;
   390    388         }else{
   391    389           if( !safeInt(z) ) return;
   392    390         }
   393    391         azToken[nToken++] = z;
   394    392         azToken[nToken++] = z = blob_terminate(&value);
   395    393         if( !safeSql(z) ) return;
   396         -      if( nToken>=count(azToken)-1 ) break;
          394  +      if( nToken>=count(azToken) ) break;
   397    395       }
   398    396       if( nToken<2 ) return;
   399    397       if( aType[ii].zName[0]=='/' ){
   400    398         thisMask = configure_is_exportable(azToken[1]);
   401    399       }else{
   402    400         thisMask = configure_is_exportable(aType[ii].zName);
   403    401       }
   404    402       if( (thisMask & groupMask)==0 ) return;
   405         -    if( (thisMask & checkMask)!=0 ){
   406         -      if( (thisMask & CONFIGSET_SCRIBER)!=0 ){
   407         -        alert_schema(1);
   408         -      }
   409         -      checkMask &= ~thisMask;
   410         -    }
   411    403   
   412    404       blob_zero(&sql);
   413    405       if( groupMask & CONFIGSET_OVERWRITE ){
   414    406         if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
   415    407           db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]);
   416    408           configHasBeenReset |= thisMask;
   417    409         }
   418    410         blob_append_sql(&sql, "REPLACE INTO ");
   419    411       }else{
   420    412         blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
   421    413       }
   422         -    blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
   423         -         &zName[1], aType[ii].zPrimKey);
   424         -    if( fossil_stricmp(zName,"/subscriber") ) alert_schema(0);
          414  +    blob_append_sql(&sql, "\"%w\"(\"%w\", mtime", &zName[1], aType[ii].zPrimKey);
   425    415       for(jj=2; jj<nToken; jj+=2){
   426    416          blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
   427    417       }
   428    418       blob_append_sql(&sql,") VALUES(%s,%s",
   429         -       azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
          419  +       azToken[1] /*safe-for-%s*/, azToken[0] /*safe-for-%s*/);
   430    420       for(jj=2; jj<nToken; jj+=2){
   431    421          blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
   432    422       }
   433    423       db_multi_exec("%s)", blob_sql_text(&sql));
   434    424       if( db_changes()==0 ){
   435    425         blob_reset(&sql);
   436    426         blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
................................................................................
   582    572         blob_appendf(pOut, "config /config %d\n%s\n",
   583    573                      blob_size(&rec), blob_str(&rec));
   584    574         nCard++;
   585    575         blob_reset(&rec);
   586    576       }
   587    577       db_finalize(&q);
   588    578     }
   589         -  if( (groupMask & CONFIGSET_SCRIBER)!=0
   590         -   && db_table_exists("repository","subscriber")
   591         -  ){
   592         -    db_prepare(&q, "SELECT mtime, quote(semail),"
   593         -                   " quote(suname), quote(sdigest),"
   594         -                   " quote(sdonotcall), quote(ssub),"
   595         -                   " quote(sctime), quote(smip)"
   596         -                   " FROM subscriber WHERE sverified"
   597         -                   " AND mtime>=%lld", iStart);
   598         -    while( db_step(&q)==SQLITE_ROW ){
   599         -      blob_appendf(&rec,
   600         -        "%lld %s suname %s sdigest %s sdonotcall %s ssub %s"
   601         -        " sctime %s smip %s",
   602         -        db_column_int64(&q, 0), /* mtime */
   603         -        db_column_text(&q, 1),  /* semail (PK) */
   604         -        db_column_text(&q, 2),  /* suname */
   605         -        db_column_text(&q, 3),  /* sdigest */
   606         -        db_column_text(&q, 4),  /* sdonotcall */
   607         -        db_column_text(&q, 5),  /* ssub */
   608         -        db_column_text(&q, 6),  /* sctime */
   609         -        db_column_text(&q, 7)   /* smip */
   610         -      );
   611         -      blob_appendf(pOut, "config /subscriber %d\n%s\n",
   612         -                   blob_size(&rec), blob_str(&rec));
   613         -      nCard++;
   614         -      blob_reset(&rec);
   615         -    }
   616         -    db_finalize(&q);
   617         -  }
   618    579     db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
   619    580                    " WHERE name=:name AND mtime>=%lld", iStart);
   620    581     for(ii=0; ii<count(aConfig); ii++){
   621    582       if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
   622    583         db_bind_text(&q, ":name", aConfig[ii].zName);
   623    584         while( db_step(&q)==SQLITE_ROW ){
   624    585           blob_appendf(&rec,"%s %s value %s",
................................................................................
   649    610       if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){
   650    611         return aGroupName[i].groupMask;
   651    612       }
   652    613     }
   653    614     if( notFoundIsFatal ){
   654    615       fossil_print("Available configuration areas:\n");
   655    616       for(i=0; i<count(aGroupName); i++){
   656         -      fossil_print("  %-13s %s\n",
   657         -            &aGroupName[i].zName[1], aGroupName[i].zHelp);
          617  +      fossil_print("  %-10s %s\n", &aGroupName[i].zName[1], aGroupName[i].zHelp);
   658    618       }
   659    619       fossil_fatal("no such configuration area: \"%s\"", z);
   660    620     }
   661    621     return 0;
   662    622   }
   663    623   
   664    624   /*
................................................................................
   693    653   **
   694    654   ** Where METHOD is one of: export import merge pull push reset.  All methods
   695    655   ** accept the -R or --repository option to specify a repository.
   696    656   **
   697    657   **    %fossil configuration export AREA FILENAME
   698    658   **
   699    659   **         Write to FILENAME exported configuration information for AREA.
   700         -**         AREA can be one of:
   701         -**
   702         -**             all email project shun skin ticket user alias subscriber
          660  +**         AREA can be one of:  all email project shun skin ticket user alias
   703    661   **
   704    662   **    %fossil configuration import FILENAME
   705    663   **
   706    664   **         Read a configuration from FILENAME, overwriting the current
   707    665   **         configuration.
   708    666   **
   709    667   **    %fossil configuration merge FILENAME
................................................................................
   832    790         }else if( fossil_strcmp(zName,"@user")==0 ){
   833    791           db_multi_exec("DELETE FROM user");
   834    792           db_create_default_users(0, 0);
   835    793         }else if( fossil_strcmp(zName,"@concealed")==0 ){
   836    794           db_multi_exec("DELETE FROM concealed");
   837    795         }else if( fossil_strcmp(zName,"@shun")==0 ){
   838    796           db_multi_exec("DELETE FROM shun");
   839         -      }else if( fossil_strcmp(zName,"@subscriber")==0 ){
   840         -        if( db_table_exists("repository","subscriber") ){
   841         -          db_multi_exec("DELETE FROM subscriber");
   842         -        }
   843         -      }else if( fossil_strcmp(zName,"@forum")==0 ){
   844         -        if( db_table_exists("repository","forumpost") ){
   845         -          db_multi_exec("DELETE FROM forumpost");
   846         -          db_multi_exec("DELETE FROM forumthread");
   847         -        }
   848    797         }else if( fossil_strcmp(zName,"@reportfmt")==0 ){
   849    798           db_multi_exec("DELETE FROM reportfmt");
   850    799           assert( strchr(zRepositorySchemaDefaultReports,'%')==0 );
   851    800           db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/);
   852    801         }
   853    802       }
   854    803       db_end_transaction(0);

Changes to src/db.c.

    68     68   #endif /* INTERFACE */
    69     69   const struct Stmt empty_Stmt = empty_Stmt_m;
    70     70   
    71     71   /*
    72     72   ** Call this routine when a database error occurs.
    73     73   */
    74     74   static void db_err(const char *zFormat, ...){
           75  +  static int rcLooping = 0;
    75     76     va_list ap;
    76     77     char *z;
           78  +  int rc = 1;
           79  +  if( rcLooping ) exit(rcLooping);
    77     80     va_start(ap, zFormat);
    78     81     z = vmprintf(zFormat, ap);
    79     82     va_end(ap);
    80     83   #ifdef FOSSIL_ENABLE_JSON
    81     84     if( g.json.isJsonMode ){
    82     85       json_err( 0, z, 1 );
           86  +    if( g.isHTTP ){
           87  +      rc = 0 /* avoid HTTP 500 */;
           88  +    }
    83     89     }
    84     90     else
    85     91   #endif /* FOSSIL_ENABLE_JSON */
    86         -  if( g.xferPanic && g.cgiOutput==1 ){
           92  +  if( g.xferPanic ){
    87     93       cgi_reset_content();
    88     94       @ error Database\serror:\s%F(z)
           95  +      cgi_reply();
           96  +  }
           97  +  else if( g.cgiOutput ){
           98  +    g.cgiOutput = 0;
           99  +    cgi_printf("<h1>Database Error</h1>\n<p>%h</p>\n", z);
    89    100       cgi_reply();
          101  +  }else{
          102  +    fprintf(stderr, "%s: %s\n", g.argv[0], z);
    90    103     }
    91         -  fossil_panic("Database error: %s", z);
          104  +  free(z);
          105  +  rcLooping = rc;
          106  +  db_force_rollback();
          107  +  fossil_exit(rc);
    92    108   }
    93    109   
    94    110   /*
    95    111   ** All static variable that a used by only this file are gathered into
    96    112   ** the following structure.
    97    113   */
    98    114   static struct DbLocalData {
................................................................................
   106    122       int (*xHook)(void);         /* Functions to call at db_end_transaction() */
   107    123       int sequence;               /* Call functions in sequence order */
   108    124     } aHook[5];
   109    125     char *azDeleteOnFail[3];  /* Files to delete on a failure */
   110    126     char *azBeforeCommit[5];  /* Commands to run prior to COMMIT */
   111    127     int nBeforeCommit;        /* Number of entries in azBeforeCommit */
   112    128     int nPriorChanges;        /* sqlite3_total_changes() at transaction start */
   113         -  const char *zStartFile;   /* File in which transaction was started */
   114         -  int iStartLine;           /* Line of zStartFile where transaction started */
   115    129   } db = {0, 0, 0, 0, 0, 0, };
   116    130   
   117    131   /*
   118    132   ** Arrange for the given file to be deleted on a failure.
   119    133   */
   120    134   void db_delete_on_failure(const char *zFilename){
   121    135     assert( db.nDeleteOnFail<count(db.azDeleteOnFail) );
   122    136     db.azDeleteOnFail[db.nDeleteOnFail++] = fossil_strdup(zFilename);
   123    137   }
   124    138   
   125         -/*
   126         -** Return the transaction nesting depth.  0 means we are currently
   127         -** not in a transaction.
   128         -*/
   129         -int db_transaction_nesting_depth(void){
   130         -  return db.nBegin;
   131         -}
   132         -
   133         -/*
   134         -** Return a pointer to a string that is the code point where the
   135         -** current transaction was started.
   136         -*/
   137         -char *db_transaction_start_point(void){
   138         -  return mprintf("%s:%d", db.zStartFile, db.iStartLine);
   139         -}
   140         -
   141    139   /*
   142    140   ** This routine is called by the SQLite commit-hook mechanism
   143    141   ** just prior to each commit.  All this routine does is verify
   144    142   ** that nBegin really is zero.  That insures that transactions
   145    143   ** cannot commit by any means other than by calling db_end_transaction()
   146    144   ** below.
   147    145   **
................................................................................
   152    150       fossil_panic("illegal commit attempt");
   153    151       return 1;
   154    152     }
   155    153     return 0;
   156    154   }
   157    155   
   158    156   /*
   159         -** Silently add the filename and line number as parameter to each
   160         -** db_begin_transaction call.
          157  +** Begin and end a nested transaction
   161    158   */
   162         -#if INTERFACE
   163         -#define db_begin_transaction()    db_begin_transaction_real(__FILE__,__LINE__)
   164         -#define db_begin_write()          db_begin_write_real(__FILE__,__LINE__)
   165         -#define db_commit_transaction()   db_end_transaction(0)
   166         -#define db_rollback_transaction() db_end_transaction(1)
   167         -#endif
   168         -
   169         -/*
   170         -** Begin a nested transaction
   171         -*/
   172         -void db_begin_transaction_real(const char *zStartFile, int iStartLine){
          159  +void db_begin_transaction(void){
   173    160     if( db.nBegin==0 ){
   174    161       db_multi_exec("BEGIN");
   175    162       sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
   176    163       db.nPriorChanges = sqlite3_total_changes(g.db);
   177         -    db.doRollback = 0;
   178         -    db.zStartFile = zStartFile;
   179         -    db.iStartLine = iStartLine;
   180    164     }
   181    165     db.nBegin++;
   182    166   }
   183         -/*
   184         -** Begin a new transaction for writing.
   185         -*/
   186         -void db_begin_write_real(const char *zStartFile, int iStartLine){
   187         -  if( db.nBegin==0 ){
   188         -    db_multi_exec("BEGIN IMMEDIATE");
   189         -    sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
   190         -    db.nPriorChanges = sqlite3_total_changes(g.db);
   191         -    db.doRollback = 0;
   192         -    db.zStartFile = zStartFile;
   193         -    db.iStartLine = iStartLine;
   194         -  }else{
   195         -    fossil_warning("read txn at %s:%d might cause SQLITE_BUSY "
   196         -       "for the write txn at %s:%d",
   197         -       db.zStartFile, db.iStartLine, zStartFile, iStartLine);
   198         -  }
   199         -  db.nBegin++;
   200         -}
   201         -
   202         -/* End a transaction previously started using db_begin_transaction()
   203         -** or db_begin_write().
   204         -*/
   205    167   void db_end_transaction(int rollbackFlag){
   206    168     if( g.db==0 ) return;
   207         -  if( db.nBegin<=0 ){
   208         -    fossil_warning("Extra call to db_end_transaction");
   209         -    return;
   210         -  }
   211         -  if( rollbackFlag ){
   212         -    db.doRollback = 1;
   213         -    if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n");
   214         -  }
          169  +  if( db.nBegin<=0 ) return;
          170  +  if( rollbackFlag ) db.doRollback = 1;
   215    171     db.nBegin--;
   216    172     if( db.nBegin==0 ){
   217    173       int i;
   218    174       if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){
   219    175         i = 0;
   220    176         while( db.nBeforeCommit ){
   221    177           db.nBeforeCommit--;
................................................................................
   222    178           sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0);
   223    179           sqlite3_free(db.azBeforeCommit[i]);
   224    180           i++;
   225    181         }
   226    182         leaf_do_pending_checks();
   227    183       }
   228    184       for(i=0; db.doRollback==0 && i<db.nCommitHook; i++){
   229         -      int rc = db.aHook[i].xHook();
   230         -      if( rc ){
   231         -        db.doRollback = 1;
   232         -        if( g.fSqlTrace ) fossil_trace("-- ROLLBACK due to aHook[%d]\n", i);
   233         -      }
          185  +      db.doRollback |= db.aHook[i].xHook();
   234    186       }
   235    187       while( db.pAllStmt ){
   236    188         db_finalize(db.pAllStmt);
   237    189       }
   238    190       db_multi_exec("%s", db.doRollback ? "ROLLBACK" : "COMMIT");
   239    191       db.doRollback = 0;
   240    192     }
................................................................................
   318    270     va_end(ap);
   319    271     zSql = blob_str(&pStmt->sql);
   320    272     db.nPrepare++;
   321    273     if( flags & DB_PREPARE_PERSISTENT ){
   322    274       prepFlags = SQLITE_PREPARE_PERSISTENT;
   323    275     }
   324    276     rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, 0);
   325         -  if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){
          277  +  if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)!=0 ){
   326    278       db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
   327    279     }
   328         -  pStmt->pNext = db.pAllStmt;
   329         -  pStmt->pPrev = 0;
   330         -  if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt;
   331         -  db.pAllStmt = pStmt;
          280  +  pStmt->pNext = pStmt->pPrev = 0;
   332    281     pStmt->nStep = 0;
   333    282     pStmt->rc = rc;
   334    283     return rc;
   335    284   }
   336    285   int db_prepare(Stmt *pStmt, const char *zFormat, ...){
   337    286     int rc;
   338    287     va_list ap;
................................................................................
   345    294     int rc;
   346    295     va_list ap;
   347    296     va_start(ap, zFormat);
   348    297     rc = db_vprepare(pStmt, DB_PREPARE_IGNORE_ERROR, zFormat, ap);
   349    298     va_end(ap);
   350    299     return rc;
   351    300   }
   352         -
   353         -/* This variant of db_prepare() checks to see if the statement has
   354         -** already been prepared, and if it has it becomes a no-op.
   355         -*/
   356    301   int db_static_prepare(Stmt *pStmt, const char *zFormat, ...){
   357    302     int rc = SQLITE_OK;
   358    303     if( blob_size(&pStmt->sql)==0 ){
   359    304       va_list ap;
   360    305       va_start(ap, zFormat);
   361    306       rc = db_vprepare(pStmt, DB_PREPARE_PERSISTENT, zFormat, ap);
          307  +    pStmt->pNext = db.pAllStmt;
          308  +    pStmt->pPrev = 0;
          309  +    if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt;
          310  +    db.pAllStmt = pStmt;
   362    311       va_end(ap);
   363    312     }
   364    313     return rc;
   365    314   }
   366         -
   367         -/* Prepare a statement using text placed inside a Blob
   368         -** using blob_append_sql().
   369         -*/
   370         -int db_prepare_blob(Stmt *pStmt, Blob *pSql){
   371         -  int rc;
   372         -  char *zSql;
   373         -  pStmt->sql = *pSql;
   374         -  blob_init(pSql, 0, 0);
   375         -  zSql = blob_sql_text(&pStmt->sql);
   376         -  db.nPrepare++;
   377         -  rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0);
   378         -  if( rc!=0 ){
   379         -    db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
   380         -  }
   381         -  pStmt->pNext = pStmt->pPrev = 0;
   382         -  pStmt->nStep = 0;
   383         -  pStmt->rc = rc;
   384         -  return rc;
   385         -}
   386         -
   387    315   
   388    316   /*
   389    317   ** Return the index of a bind parameter
   390    318   */
   391    319   static int paramIdx(Stmt *pStmt, const char *zParamName){
   392    320     int i = sqlite3_bind_parameter_index(pStmt->pStmt, zParamName);
   393    321     if( i==0 ){
................................................................................
   474    402     db_stats(pStmt);
   475    403     rc = sqlite3_reset(pStmt->pStmt);
   476    404     db_check_result(rc);
   477    405     return rc;
   478    406   }
   479    407   int db_finalize(Stmt *pStmt){
   480    408     int rc;
          409  +  db_stats(pStmt);
          410  +  blob_reset(&pStmt->sql);
          411  +  rc = sqlite3_finalize(pStmt->pStmt);
          412  +  db_check_result(rc);
          413  +  pStmt->pStmt = 0;
   481    414     if( pStmt->pNext ){
   482    415       pStmt->pNext->pPrev = pStmt->pPrev;
   483    416     }
   484    417     if( pStmt->pPrev ){
   485    418       pStmt->pPrev->pNext = pStmt->pNext;
   486    419     }else if( db.pAllStmt==pStmt ){
   487    420       db.pAllStmt = pStmt->pNext;
   488    421     }
   489    422     pStmt->pNext = 0;
   490    423     pStmt->pPrev = 0;
   491         -  db_stats(pStmt);
   492         -  blob_reset(&pStmt->sql);
   493         -  rc = sqlite3_finalize(pStmt->pStmt);
   494         -  db_check_result(rc);
   495         -  pStmt->pStmt = 0;
   496    424     return rc;
   497    425   }
   498    426   
   499    427   /*
   500    428   ** Return the rowid of the most recent insert
   501    429   */
   502    430   int db_last_insert_rowid(void){
   503    431     i64 x = sqlite3_last_insert_rowid(g.db);
   504    432     if( x<0 || x>(i64)2147483647 ){
   505         -    fossil_panic("rowid out of range (0..2147483647)");
          433  +    fossil_fatal("rowid out of range (0..2147483647)");
   506    434     }
   507    435     return (int)x;
   508    436   }
   509    437   
   510    438   /*
   511    439   ** Return the number of rows that were changed by the most recent
   512    440   ** INSERT, UPDATE, or DELETE.  Auxiliary changes caused by triggers
................................................................................
   550    478   char *db_column_malloc(Stmt *pStmt, int N){
   551    479     return mprintf("%s", db_column_text(pStmt, N));
   552    480   }
   553    481   void db_column_blob(Stmt *pStmt, int N, Blob *pBlob){
   554    482     blob_append(pBlob, sqlite3_column_blob(pStmt->pStmt, N),
   555    483                 sqlite3_column_bytes(pStmt->pStmt, N));
   556    484   }
   557         -Blob db_column_text_as_blob(Stmt *pStmt, int N){
   558         -  Blob x;
   559         -  blob_init(&x, (char*)sqlite3_column_text(pStmt->pStmt,N),
   560         -            sqlite3_column_bytes(pStmt->pStmt,N));
   561         -  return x;
   562         -}
   563    485   
   564    486   /*
   565    487   ** Initialize a blob to an ephemeral copy of the content of a
   566    488   ** column in the current row.  The data in the blob will become
   567    489   ** invalid when the statement is stepped or reset.
   568    490   */
   569    491   void db_ephemeral_blob(Stmt *pStmt, int N, Blob *pBlob){
................................................................................
   951    873     if( g.fTimeFormat==1 ){
   952    874       sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC);
   953    875     }else{
   954    876       sqlite3_result_text(context, "utc", -1, SQLITE_STATIC);
   955    877     }
   956    878   }
   957    879   
   958         -/*
   959         -** If the input is a hexadecimal string, convert that string into a BLOB.
   960         -** If the input is not a hexadecimal string, return NULL.
   961         -*/
   962         -void db_hextoblob(
   963         -  sqlite3_context *context,
   964         -  int argc,
   965         -  sqlite3_value **argv
   966         -){
   967         -  const unsigned char *zIn = sqlite3_value_text(argv[0]);
   968         -  int nIn = sqlite3_value_bytes(argv[0]);
   969         -  unsigned char *zOut;
   970         -  if( zIn==0 ) return;
   971         -  if( nIn&1 ) return;
   972         -  if( !validate16((const char*)zIn, nIn) ) return;
   973         -  zOut = sqlite3_malloc64( nIn/2 + 1 );
   974         -  if( zOut==0 ){
   975         -    sqlite3_result_error_nomem(context);
   976         -    return;
   977         -  }
   978         -  decode16(zIn, zOut, nIn);
   979         -  sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free);
   980         -}
   981    880   
   982    881   /*
   983    882   ** Register the SQL functions that are useful both to the internal
   984    883   ** representation and to the "fossil sql" command.
   985    884   */
   986    885   void db_add_aux_functions(sqlite3 *db){
   987    886     sqlite3_create_function(db, "checkin_mtime", 2, SQLITE_UTF8, 0,
................................................................................
   992    891                             db_sym2rid_function, 0, 0);
   993    892     sqlite3_create_function(db, "now", 0, SQLITE_UTF8, 0,
   994    893                             db_now_function, 0, 0);
   995    894     sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
   996    895                             db_tolocal_function, 0, 0);
   997    896     sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
   998    897                             db_fromlocal_function, 0, 0);
   999         -  sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
  1000         -                          db_hextoblob, 0, 0);
  1001         -  sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0,
  1002         -                          0, capability_union_step, capability_union_finalize);
  1003         -  sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0,
  1004         -                          capability_fullcap, 0, 0);
  1005         -  sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0,
  1006         -                          alert_find_emailaddr_func, 0, 0);
  1007    898   }
  1008    899   
  1009    900   #if USE_SEE
  1010    901   /*
  1011    902   ** This is a pointer to the saved database encryption key string.
  1012    903   */
  1013    904   static char *zSavedKey = 0;
................................................................................
  1046    937     size_t blobSize = 0;
  1047    938   
  1048    939     blobSize = blob_size(pKey);
  1049    940     if( blobSize==0 ) return;
  1050    941     fossil_get_page_size(&pageSize);
  1051    942     assert( pageSize>0 );
  1052    943     if( blobSize>pageSize ){
  1053         -    fossil_panic("key blob too large: %u versus %u", blobSize, pageSize);
          944  +    fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize);
  1054    945     }
  1055    946     p = fossil_secure_alloc_page(&n);
  1056    947     assert( p!=NULL );
  1057    948     assert( n==pageSize );
  1058    949     assert( n>=blobSize );
  1059    950     memcpy(p, blob_str(pKey), blobSize);
  1060    951     zSavedKey = p;
................................................................................
  1080    971   ){
  1081    972     if( zSavedKey!=NULL ){
  1082    973       size_t blobSize = blob_size(pKey);
  1083    974       if( blobSize==0 ){
  1084    975         db_unsave_encryption_key();
  1085    976       }else{
  1086    977         if( blobSize>savedKeySize ){
  1087         -        fossil_panic("key blob too large: %u versus %u",
          978  +        fossil_fatal("key blob too large: %u versus %u",
  1088    979                        blobSize, savedKeySize);
  1089    980         }
  1090    981         fossil_secure_zero(zSavedKey, savedKeySize);
  1091    982         memcpy(zSavedKey, blob_str(pKey), blobSize);
  1092    983       }
  1093    984     }else{
  1094    985       db_save_encryption_key(pKey);
................................................................................
  1110   1001     size_t n = 0;
  1111   1002     size_t pageSize = 0;
  1112   1003     HANDLE hProcess = NULL;
  1113   1004   
  1114   1005     fossil_get_page_size(&pageSize);
  1115   1006     assert( pageSize>0 );
  1116   1007     if( nSize>pageSize ){
  1117         -    fossil_panic("key too large: %u versus %u", nSize, pageSize);
         1008  +    fossil_fatal("key too large: %u versus %u", nSize, pageSize);
  1118   1009     }
  1119   1010     p = fossil_secure_alloc_page(&n);
  1120   1011     assert( p!=NULL );
  1121   1012     assert( n==pageSize );
  1122   1013     assert( n>=nSize );
  1123   1014     hProcess = OpenProcess(PROCESS_VM_READ, FALSE, processId);
  1124   1015     if( hProcess!=NULL ){
................................................................................
  1126   1017       if( ReadProcessMemory(hProcess, pAddress, p, nSize, &nRead) ){
  1127   1018         CloseHandle(hProcess);
  1128   1019         if( nRead==nSize ){
  1129   1020           db_unsave_encryption_key();
  1130   1021           zSavedKey = p;
  1131   1022           savedKeySize = n;
  1132   1023         }else{
  1133         -        fossil_panic("bad size read, %u out of %u bytes at %p from pid %lu",
         1024  +        fossil_fatal("bad size read, %u out of %u bytes at %p from pid %lu",
  1134   1025                        nRead, nSize, pAddress, processId);
  1135   1026         }
  1136   1027       }else{
  1137   1028         CloseHandle(hProcess);
  1138         -      fossil_panic("failed read, %u bytes at %p from pid %lu: %lu", nSize,
         1029  +      fossil_fatal("failed read, %u bytes at %p from pid %lu: %lu", nSize,
  1139   1030                      pAddress, processId, GetLastError());
  1140   1031       }
  1141   1032     }else{
  1142         -    fossil_panic("failed to open pid %lu: %lu", processId, GetLastError());
         1033  +    fossil_fatal("failed to open pid %lu: %lu", processId, GetLastError());
  1143   1034     }
  1144   1035   }
  1145   1036   #endif /* defined(_WIN32) */
  1146   1037   #endif /* USE_SEE */
  1147   1038   
  1148   1039   /*
  1149   1040   ** If the database file zDbFile has a name that suggests that it is
................................................................................
  1199   1090   ** connection.  An error results in process abort.
  1200   1091   */
  1201   1092   LOCAL sqlite3 *db_open(const char *zDbName){
  1202   1093     int rc;
  1203   1094     sqlite3 *db;
  1204   1095   
  1205   1096     if( g.fSqlTrace ) fossil_trace("-- sqlite3_open: [%s]\n", zDbName);
  1206         -  if( strcmp(zDbName, g.nameOfExe)==0 ){
  1207         -    extern int sqlite3_appendvfs_init(
  1208         -      sqlite3 *, char **, const sqlite3_api_routines *
  1209         -    );
  1210         -    sqlite3_appendvfs_init(0,0,0);
  1211         -    g.zVfsName = "apndvfs";
  1212         -  }
  1213   1097     rc = sqlite3_open_v2(
  1214   1098          zDbName, &db,
  1215   1099          SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
  1216   1100          g.zVfsName
  1217   1101     );
  1218   1102     if( rc!=SQLITE_OK ){
  1219   1103       db_err("[%s]: %s", zDbName, sqlite3_errmsg(db));
................................................................................
  1281   1165   ** the database connection.
  1282   1166   **
  1283   1167   ** After calling this routine, db_database_slot(zLabel) should
  1284   1168   ** return 0.
  1285   1169   */
  1286   1170   void db_set_main_schemaname(sqlite3 *db, const char *zLabel){
  1287   1171     if( sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, zLabel) ){
  1288         -    fossil_panic("Fossil requires a version of SQLite that supports the "
         1172  +    fossil_fatal("Fossil requires a version of SQLite that supports the "
  1289   1173                    "SQLITE_DBCONFIG_MAINDBNAME interface.");
  1290   1174     }
  1291   1175   }
  1292   1176   
  1293   1177   /*
  1294   1178   ** Return the slot number for database zLabel.  The first database
  1295   1179   ** opened is slot 0.  The "temp" database is slot 1.  Attached databases
................................................................................
  1338   1222       g.zConfigDbName = 0;
  1339   1223     }else if( g.dbConfig ){
  1340   1224       sqlite3_wal_checkpoint(g.dbConfig, 0);
  1341   1225       sqlite3_close(g.dbConfig);
  1342   1226       g.dbConfig = 0;
  1343   1227       g.zConfigDbName = 0;
  1344   1228     }else if( g.db && 0==iSlot ){
  1345         -    int rc;
  1346   1229       sqlite3_wal_checkpoint(g.db, 0);
  1347         -    rc = sqlite3_close(g.db);
  1348         -    if( g.fSqlTrace ) fossil_trace("-- db_close_config(%d)\n", rc);
         1230  +    sqlite3_close(g.db);
  1349   1231       g.db = 0;
  1350   1232       g.zConfigDbName = 0;
  1351   1233     }
  1352   1234   }
  1353   1235   
  1354   1236   /*
  1355   1237   ** Open the user database in "~/.fossil".  Create the database anew if
................................................................................
  1382   1264           char *zPath = fossil_getenv("HOMEPATH");
  1383   1265           if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath);
  1384   1266         }
  1385   1267       }
  1386   1268     }
  1387   1269     if( zHome==0 ){
  1388   1270       if( isOptional ) return 0;
  1389         -    fossil_panic("cannot locate home directory - please set the "
         1271  +    fossil_fatal("cannot locate home directory - please set the "
  1390   1272                    "FOSSIL_HOME, LOCALAPPDATA, APPDATA, or HOMEPATH "
  1391   1273                    "environment variables");
  1392   1274     }
  1393   1275   #else
  1394   1276     if( zHome==0 ){
  1395   1277       zHome = fossil_getenv("HOME");
  1396   1278     }
  1397   1279     if( zHome==0 ){
  1398   1280       if( isOptional ) return 0;
  1399         -    fossil_panic("cannot locate home directory - please set the "
         1281  +    fossil_fatal("cannot locate home directory - please set the "
  1400   1282                    "FOSSIL_HOME or HOME environment variables");
  1401   1283     }
  1402   1284   #endif
  1403   1285     if( file_isdir(zHome, ExtFILE)!=1 ){
  1404   1286       if( isOptional ) return 0;
  1405         -    fossil_panic("invalid home directory: %s", zHome);
         1287  +    fossil_fatal("invalid home directory: %s", zHome);
  1406   1288     }
  1407   1289   #if defined(_WIN32) || defined(__CYGWIN__)
  1408   1290     /* . filenames give some window systems problems and many apps problems */
  1409   1291     zDbName = mprintf("%//_fossil", zHome);
  1410   1292   #else
  1411   1293     zDbName = mprintf("%s/.fossil", zHome);
  1412   1294   #endif
  1413   1295     if( file_size(zDbName, ExtFILE)<1024*3 ){
  1414   1296       if( file_access(zHome, W_OK) ){
  1415   1297         if( isOptional ) return 0;
  1416         -      fossil_panic("home directory %s must be writeable", zHome);
         1298  +      fossil_fatal("home directory %s must be writeable", zHome);
  1417   1299       }
  1418   1300       db_init_database(zDbName, zConfigSchema, (char*)0);
  1419   1301     }
  1420   1302     if( file_access(zDbName, W_OK) ){
  1421   1303       if( isOptional ) return 0;
  1422         -    fossil_panic("configuration file %s must be writeable", zDbName);
         1304  +    fossil_fatal("configuration file %s must be writeable", zDbName);
  1423   1305     }
  1424   1306     if( useAttach ){
  1425   1307       db_open_or_attach(zDbName, "configdb");
  1426   1308       g.dbConfig = 0;
  1427   1309     }else{
  1428   1310       g.dbConfig = db_open(zDbName);
  1429   1311       db_set_main_schemaname(g.dbConfig, "configdb");
................................................................................
  1620   1502       }
  1621   1503     }
  1622   1504     if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){
  1623   1505       if( file_access(zDbName, F_OK) ){
  1624   1506   #ifdef FOSSIL_ENABLE_JSON
  1625   1507         g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
  1626   1508   #endif
  1627         -      fossil_fatal("repository does not exist or"
         1509  +      fossil_panic("repository does not exist or"
  1628   1510                      " is in an unreadable directory: %s", zDbName);
  1629   1511       }else if( file_access(zDbName, R_OK) ){
  1630   1512   #ifdef FOSSIL_ENABLE_JSON
  1631   1513         g.json.resultCode = FSL_JSON_E_DENIED;
  1632   1514   #endif
  1633         -      fossil_fatal("read permission denied for repository %s", zDbName);
         1515  +      fossil_panic("read permission denied for repository %s", zDbName);
  1634   1516       }else{
  1635   1517   #ifdef FOSSIL_ENABLE_JSON
  1636   1518         g.json.resultCode = FSL_JSON_E_DB_NOT_VALID;
  1637   1519   #endif
  1638         -      fossil_fatal("not a valid repository: %s", zDbName);
         1520  +      fossil_panic("not a valid repository: %s", zDbName);
  1639   1521       }
  1640   1522     }
  1641   1523     g.zRepositoryName = mprintf("%s", zDbName);
  1642   1524     db_open_or_attach(g.zRepositoryName, "repository");
  1643   1525     g.repositoryOpen = 1;
  1644         -  sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION,
  1645         -                       &g.iRepoDataVers);
  1646   1526     /* Cache "allow-symlinks" option, because we'll need it on every stat call */
  1647   1527     g.allowSymlinks = db_get_boolean("allow-symlinks",
  1648   1528                                      db_allow_symlinks_by_default());
  1649   1529     g.zAuxSchema = db_get("aux-schema","");
  1650   1530     g.eHashPolicy = db_get_int("hash-policy",-1);
  1651   1531     if( g.eHashPolicy<0 ){
  1652   1532       g.eHashPolicy = hname_default_policy();
................................................................................
  1655   1535   
  1656   1536     /* Make a change to the CHECK constraint on the BLOB table for
  1657   1537     ** version 2.0 and later.
  1658   1538     */
  1659   1539     rebuild_schema_update_2_0();   /* Do the Fossil-2.0 schema updates */
  1660   1540   }
  1661   1541   
  1662         -/*
  1663         -** Return true if there have been any changes to the repository
  1664         -** database since it was opened.
  1665         -**
  1666         -** Changes to "config" and "localdb" and "temp" do not count.
  1667         -** This routine only returns true if there have been changes
  1668         -** to "repository".
  1669         -*/
  1670         -int db_repository_has_changed(void){
  1671         -  unsigned int v;
  1672         -  if( !g.repositoryOpen ) return 0;
  1673         -  sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, &v);
  1674         -  return g.iRepoDataVers != v;               
  1675         -}
  1676         -
  1677   1542   /*
  1678   1543   ** Flags for the db_find_and_open_repository() function.
  1679   1544   */
  1680   1545   #if INTERFACE
  1681   1546   #define OPEN_OK_NOT_FOUND    0x001      /* Do not error out if not found */
  1682   1547   #define OPEN_ANY_SCHEMA      0x002      /* Do not error if schema is wrong */
  1683   1548   #endif
................................................................................
  1837   1702       sqlite3_status(SQLITE_STATUS_PAGECACHE_OVERFLOW, &cur, &hiwtr, 0);
  1838   1703       fprintf(stderr, "-- PCACHE_OVFLOW          %10d %10d\n", cur, hiwtr);
  1839   1704       fprintf(stderr, "-- prepared statements    %10d\n", db.nPrepare);
  1840   1705     }
  1841   1706     while( db.pAllStmt ){
  1842   1707       db_finalize(db.pAllStmt);
  1843   1708     }
  1844         -  if( db.nBegin && reportErrors ){
  1845         -    fossil_warning("Transaction started at %s:%d never commits",
  1846         -                   db.zStartFile, db.iStartLine);
  1847         -    db_end_transaction(1);
  1848         -  }
         1709  +  db_end_transaction(1);
  1849   1710     pStmt = 0;
  1850         -  g.dbIgnoreErrors++; /* Stop "database locked" warnings from PRAGMA optimize */
         1711  +  g.dbIgnoreErrors++;  /* Stop "database locked" warnings from PRAGMA optimize */
  1851   1712     sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0);
  1852   1713     g.dbIgnoreErrors--;
  1853   1714     db_close_config();
  1854   1715   
  1855   1716     /* If the localdb has a lot of unused free space,
  1856   1717     ** then VACUUM it as we shut down.
  1857   1718     */
................................................................................
  1863   1724       }
  1864   1725     }
  1865   1726   
  1866   1727     if( g.db ){
  1867   1728       int rc;
  1868   1729       sqlite3_wal_checkpoint(g.db, 0);
  1869   1730       rc = sqlite3_close(g.db);
  1870         -    if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc);
  1871   1731       if( rc==SQLITE_BUSY && reportErrors ){
  1872   1732         while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
  1873   1733           fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
  1874   1734         }
  1875   1735       }
  1876   1736       g.db = 0;
  1877   1737     }
  1878   1738     g.repositoryOpen = 0;
  1879   1739     g.localOpen = 0;
  1880   1740     assert( g.dbConfig==0 );
  1881   1741     assert( g.zConfigDbName==0 );
  1882         -  backoffice_run_if_needed();
  1883   1742   }
  1884   1743   
  1885         -/*
  1886         -** Close the database as quickly as possible without unnecessary processing.
  1887         -*/
  1888         -void db_panic_close(void){
  1889         -  if( g.db ){
  1890         -    int rc;
  1891         -    sqlite3_wal_checkpoint(g.db, 0);
  1892         -    rc = sqlite3_close(g.db);
  1893         -    if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc);
  1894         -  }
  1895         -  g.db = 0;
  1896         -  g.repositoryOpen = 0;
  1897         -  g.localOpen = 0;
  1898         -}
  1899   1744   
  1900   1745   /*
  1901   1746   ** Create a new empty repository database with the given name.
  1902   1747   **
  1903   1748   ** Only the schema is initialized.  The required VAR tables entries
  1904   1749   ** are not set by this routine and must be set separately in order
  1905   1750   ** to make the new file a valid database.
................................................................................
  2896   2741     info_cmd();
  2897   2742   }
  2898   2743   
  2899   2744   /*
  2900   2745   ** Print the current value of a setting identified by the pSetting
  2901   2746   ** pointer.
  2902   2747   */
  2903         -void print_setting(const Setting *pSetting){
         2748  +static void print_setting(const Setting *pSetting){
  2904   2749     Stmt q;
  2905   2750     if( g.repositoryOpen ){
  2906   2751       db_prepare(&q,
  2907   2752          "SELECT '(local)', value FROM config WHERE name=%Q"
  2908   2753          " UNION ALL "
  2909   2754          "SELECT '(global)', value FROM global_config WHERE name=%Q",
  2910   2755          pSetting->name, pSetting->name
................................................................................
  3022   2867   */
  3023   2868   /*
  3024   2869   ** SETTING: autosync-tries  width=16 default=1
  3025   2870   ** If autosync is enabled setting this to a value greater
  3026   2871   ** than zero will cause autosync to try no more than this
  3027   2872   ** number of attempts if there is a sync failure.
  3028   2873   */
  3029         -/*
  3030         -** SETTING: backoffice-nodelay boolean default=off
  3031         -** If backoffice-nodelay is true, then the backoffice processing
  3032         -** will never invoke sleep().  If it has nothing useful to do,
  3033         -** it simply exits.
  3034         -*/
  3035         -/*
  3036         -** SETTING: backoffice-logfile width=40
  3037         -** If backoffice-logfile is not an empty string and is a valid
  3038         -** filename, then a one-line message is appended to that file
  3039         -** every time the backoffice runs.  This can be used for debugging,
  3040         -** to ensure that backoffice is running appropriately.
  3041         -*/
  3042   2874   /*
  3043   2875   ** SETTING: binary-glob     width=40 versionable block-text
  3044   2876   ** The VALUE of this setting is a comma or newline-separated list of
  3045   2877   ** GLOB patterns that should be treated as binary files
  3046   2878   ** for committing and merging purposes.  Example: *.jpg
  3047   2879   */
  3048   2880   #if defined(_WIN32)||defined(__CYGWIN__)||defined(__DARWIN__)

Changes to src/default_css.txt.

   182    182   }
   183    183   span.infoTag {
   184    184     font-weight: bold;
   185    185   }
   186    186   span.wikiTagCancelled {
   187    187     text-decoration: line-through;
   188    188   }
   189         -div.columns {
   190         -  padding: 0 2em 0 2em;
   191         -  max-width: 1000px;
          189  +table.browser {
          190  +  width: 100%;
          191  +  border: 0;
   192    192   }
   193         -div.columns > ul {
   194         -  margin: 0;
   195         -  padding: 0 0 0 1em;
   196         -}
   197         -div.columns > ul li:first-child {
   198         -  margin-top:0px;
   199         -}
   200         -div.columns li {
   201         -  break-inside: avoid;
          193  +td.browser {
          194  +  width: 24%;
          195  +  vertical-align: top;
   202    196   }
   203    197   .filetree {
   204    198     margin: 1em 0;
   205    199     line-height: 1.5;
   206    200   }
   207    201   .filetree > ul {
   208    202     display: inline-block;
................................................................................
   304    298   table.captcha {
   305    299     margin: auto;
   306    300     padding: 10px;
   307    301     border-width: 4px;
   308    302     border-style: double;
   309    303     border-color: black;
   310    304   }
   311         -pre.captcha {
   312         -  font-size: 50%;
   313         -}
   314    305   td.login_out_label {
   315    306     text-align: center;
   316    307   }
   317    308   span.loginError {
   318    309     color: red;
   319    310   }
   320    311   span.note {
................................................................................
   356    347     text-align: center;
   357    348     padding-right: 15px;
   358    349   }
   359    350   td.usetupListCon {
   360    351     text-align: left
   361    352   }
   362    353   div.ueditCapBox {
          354  +  float: left;
   363    355     margin-right: 20px;
   364    356     margin-bottom: 20px;
   365    357   }
   366    358   td.usetupEditLabel {
   367    359     text-align: right;
   368    360     vertical-align: top;
   369    361     white-space: nowrap;
................................................................................
   540    532     word-wrap: break-word;
   541    533   }
   542    534   pre.th1error {
   543    535     white-space: pre-wrap;
   544    536     word-wrap: break-word;
   545    537     color: red;
   546    538   }
   547         -pre.textPlain {
   548         -  white-space: pre-wrap;
   549         -  word-wrap: break-word;
   550         -}
   551    539   .statistics-report-graph-line {
   552    540     background-color: #446979;
   553    541   }
   554    542   .statistics-report-table-events th {
   555    543     padding: 0 1em 0 1em;
   556    544   }
   557    545   .statistics-report-table-events td {
................................................................................
   641    629     display: none;
   642    630   }
   643    631   table.label-value th {
   644    632     vertical-align: top;
   645    633     text-align: right;
   646    634     padding: 0.2ex 1ex;
   647    635   }
   648         -table.forum_post {
   649         -  margin-top: 1ex;
   650         -  margin-bottom: 1ex;
   651         -  margin-left: 0;
   652         -  margin-right: 0;
   653         -  border-spacing: 0;
   654         -}
   655         -span.forum_author {
   656         -  color: #888;
   657         -  font-size: 75%;
   658         -}
   659         -span.forum_author::after {
   660         -  content: " | ";
   661         -}
   662         -span.forum_age {
   663         -  color: #888;
   664         -  font-size: 85%;
   665         -}
   666         -span.forum_buttons {
   667         -  font-size: 85%;
   668         -}
   669         -span.forum_buttons::before {
   670         -  color: #888;
   671         -  content: " | ";
   672         -}
   673         -span.forum_npost {
   674         -  color: #888;
   675         -  font-size: 75%;
   676         -}
   677         -table.forumeditform td {
   678         -  vertical-align: top;
   679         -  border-collapse: collapse;
   680         -  padding: 1px;
   681         -}
   682         -div.forum_body p {
   683         -  margin-top: 0;
   684         -}
   685         -td.form_label {
   686         -  vertical-align: top;
   687         -  text-align: right;
   688         -}
   689         -.debug {
   690         -  background-color: #ffc;
   691         -  border: 2px solid #ff0;
   692         -}
   693         -div.forumEdit {
   694         -  border: 1px solid black;
   695         -  padding-left: 1ex;
   696         -  padding-right: 1ex;
   697         -}
   698         -div.forumHier, div.forumTime {
   699         -  border: 1px solid black;
   700         -  padding-left: 1ex;
   701         -  padding-right: 1ex;
   702         -  margin-top: 1ex;
   703         -}
   704         -div.forumSel {
   705         -  background-color: #cef;
   706         -}
   707         -div.forumObs {
   708         -  color: #bbb;
   709         -}
   710         -#capabilitySummary {
   711         -  text-align: center;
   712         -}
   713         -#capabilitySummary td {
   714         -  padding-left: 3ex;
   715         -  padding-right: 3ex;
   716         -}
   717         -#capabilitySummary th {
   718         -  padding-left: 1ex;
   719         -  padding-right: 1ex;
   720         -}
   721         -.capsumOff {
   722         -  background-color: #bbb;
   723         -}
   724         -.capsumRead {
   725         -  background-color: #bfb;
   726         -}
   727         -.capsumWrite {
   728         -  background-color: #ffb;
   729         -}
   730         -label {
   731         -  white-space: nowrap;
   732         -}

Changes to src/diff.c.

  2214   2214     int rid;               /* Artifact ID of the file being annotated */
  2215   2215     int fnid;              /* Filename ID */
  2216   2216     Stmt q;                /* Query returning all ancestor versions */
  2217   2217     int cnt = 0;           /* Number of versions analyzed */
  2218   2218     int iLimit;            /* Maximum number of versions to analyze */
  2219   2219     sqlite3_int64 mxTime;  /* Halt at this time if not already complete */
  2220   2220   
  2221         -  memset(p, 0, sizeof(*p));
  2222         -
  2223   2221     if( zLimit ){
  2224   2222       if( strcmp(zLimit,"none")==0 ){
  2225   2223         iLimit = 0;
  2226   2224         mxTime = 0;
  2227   2225       }else if( sqlite3_strglob("*[0-9]s", zLimit)==0 ){
  2228   2226         iLimit = 0;
  2229   2227         mxTime = current_time_in_milliseconds() + 1000.0*atof(zLimit);
................................................................................
  2235   2233     }else{
  2236   2234       /* Default limit is as much as we can do in 1.000 seconds */
  2237   2235       iLimit = 0;
  2238   2236       mxTime = current_time_in_milliseconds()+1000;
  2239   2237     }
  2240   2238     db_begin_transaction();
  2241   2239   
  2242         -  /* Get the artifact ID for the check-in begin analyzed */
         2240  +  /* Get the artificate ID for the check-in begin analyzed */
  2243   2241     if( zRevision ){
  2244   2242       cid = name_to_typed_rid(zRevision, "ci");
  2245   2243     }else{
  2246   2244       db_must_be_within_tree();
  2247   2245       cid = db_lget_int("checkout", 0);
  2248   2246     }
  2249   2247     origid = zOrigin ? name_to_typed_rid(zOrigin, "ci") : 0;
................................................................................
  2308   2306         blob_to_utf8_no_bom(&step, 0);
  2309   2307         annotation_step(p, &step, p->nVers-1, annFlags);
  2310   2308         blob_reset(&step);
  2311   2309       }
  2312   2310       p->nVers++;
  2313   2311       cnt++;
  2314   2312     }
  2315         -
  2316         -  if( p->nVers==0 ){
  2317         -    if( zRevision ){
  2318         -      fossil_fatal("file %s does not exist in check-in %s", zFilename, zRevision);
  2319         -    }else{
  2320         -      fossil_fatal("no history for file: %s", zFilename);
  2321         -    }
  2322         -  }
  2323         -
  2324   2313     db_finalize(&q);
  2325   2314     db_end_transaction(0);
  2326   2315   }
  2327   2316   
  2328   2317   /*
  2329   2318   ** Return a color from a gradient.
  2330   2319   */

Changes to src/diffcmd.c.

   834    834   **   --command PROG             External diff program - overrides "diff-command"
   835    835   **   --context|-c N             Use N lines of context
   836    836   **   --diff-binary BOOL         Include binary files when using external commands
   837    837   **   --exec-abs-paths           Force absolute path names with external commands.
   838    838   **   --exec-rel-paths           Force relative path names with external commands.
   839    839   **   --from|-r VERSION          Select VERSION as source for the diff
   840    840   **   --internal|-i              Use internal diff logic
   841         -**   --new-file|-N              Show complete text of added and deleted files
   842    841   **   --numstat                  Show only the number of lines delete and added
   843    842   **   --side-by-side|-y          Side-by-side diff
   844    843   **   --strip-trailing-cr        Strip trailing CR
   845    844   **   --tk                       Launch a Tcl/Tk GUI for display
   846    845   **   --to VERSION               Select VERSION as target for the diff
   847    846   **   --undo                     Diff against the "undo" buffer
   848    847   **   --unified                  Unified diff

Changes to src/dispatch.c.

   347    347         }else{
   348    348           @ <blockquote>
   349    349           help_to_html(pCmd->zHelp, cgi_output_blob());
   350    350           @ </blockquote>
   351    351         }
   352    352       }
   353    353     }else{
   354         -    int i;
          354  +    int i, j, n;
   355    355   
   356    356       style_header("Help");
   357    357   
   358    358       @ <a name='commands'></a>
   359    359       @ <h1>Available commands:</h1>
   360         -    @ <div class="columns" style="column-width: 12ex;">
   361         -    @ <ul>
   362         -    for(i=0; i<MX_COMMAND; i++){
          360  +    @ <table border="0"><tr>
          361  +    for(i=j=0; i<MX_COMMAND; i++){
          362  +      const char *z = aCommand[i].zName;
          363  +      if( '/'==*z || strncmp(z,"test",4)==0 ) continue;
          364  +      if( (aCommand[i].eCmdFlags & CMDFLAG_SETTING)!=0 ) continue;
          365  +      j++;
          366  +    }
          367  +    n = (j+5)/6;
          368  +    for(i=j=0; i<MX_COMMAND; i++){
   363    369         const char *z = aCommand[i].zName;
   364    370         const char *zBoldOn  = aCommand[i].eCmdFlags&CMDFLAG_1ST_TIER?"<b>" :"";
   365    371         const char *zBoldOff = aCommand[i].eCmdFlags&CMDFLAG_1ST_TIER?"</b>":"";
   366    372         if( '/'==*z || strncmp(z,"test",4)==0 ) continue;
   367    373         if( (aCommand[i].eCmdFlags & CMDFLAG_SETTING)!=0 ) continue;
          374  +      if( j==0 ){
          375  +        @ <td valign="top"><ul>
          376  +      }
   368    377         @ <li><a href="%R/help?cmd=%s(z)">%s(zBoldOn)%s(z)%s(zBoldOff)</a></li>
          378  +      j++;
          379  +      if( j>=n ){
          380  +        @ </ul></td>
          381  +        j = 0;
          382  +      }
   369    383       }
   370         -    @ </ui></div>
          384  +    if( j>0 ){
          385  +      @ </ul></td>
          386  +    }
          387  +    @ </tr></table>
   371    388   
   372    389       @ <a name='webpages'></a>
   373    390       @ <h1>Available web UI pages:</h1>
   374         -    @ <div class="columns" style="column-width: 18ex;">
   375         -    @ <ul>
   376         -    for(i=0; i<MX_COMMAND; i++){
          391  +    @ <table border="0"><tr>
          392  +    for(i=j=0; i<MX_COMMAND; i++){
          393  +      const char *z = aCommand[i].zName;
          394  +      if( '/'!=*z ) continue;
          395  +      j++;
          396  +    }
          397  +    n = (j+4)/5;
          398  +    for(i=j=0; i<MX_COMMAND; i++){
   377    399         const char *z = aCommand[i].zName;
   378    400         if( '/'!=*z ) continue;
          401  +      if( j==0 ){
          402  +        @ <td valign="top"><ul>
          403  +      }
   379    404         if( aCommand[i].zHelp[0] ){
   380    405           @ <li><a href="%R/help?cmd=%s(z)">%s(z+1)</a></li>
   381    406         }else{
   382    407           @ <li>%s(z+1)</li>
   383    408         }
          409  +      j++;
          410  +      if( j>=n ){
          411  +        @ </ul></td>
          412  +        j = 0;
          413  +      }
          414  +    }
          415  +    if( j>0 ){
          416  +      @ </ul></td>
   384    417       }
   385         -    @ </ul></div>
          418  +    @ </tr></table>
   386    419   
   387    420       @ <a name='unsupported'></a>
   388    421       @ <h1>Unsupported commands:</h1>
   389         -    @ <div class="columns" style="column-width: 20ex;">
   390         -    @ <ul>
   391         -    for(i=0; i<MX_COMMAND; i++){
          422  +    @ <table border="0"><tr>
          423  +    for(i=j=0; i<MX_COMMAND; i++){
          424  +      const char *z = aCommand[i].zName;
          425  +      if( strncmp(z,"test",4)!=0 ) continue;
          426  +      j++;
          427  +    }
          428  +    n = (j+3)/4;
          429  +    for(i=j=0; i<MX_COMMAND; i++){
   392    430         const char *z = aCommand[i].zName;
   393    431         if( strncmp(z,"test",4)!=0 ) continue;
          432  +      if( j==0 ){
          433  +        @ <td valign="top"><ul>
          434  +      }
   394    435         if( aCommand[i].zHelp[0] ){
   395    436           @ <li><a href="%R/help?cmd=%s(z)">%s(z)</a></li>
   396    437         }else{
   397    438           @ <li>%s(z)</li>
   398    439         }
          440  +      j++;
          441  +      if( j>=n ){
          442  +        @ </ul></td>
          443  +        j = 0;
          444  +      }
          445  +    }
          446  +    if( j>0 ){
          447  +      @ </ul></td>
   399    448       }
   400         -    @ </ul></div>
          449  +    @ </tr></table>
   401    450   
   402    451       @ <a name='settings'></a>
   403    452       @ <h1>Settings:</h1>
   404         -    @ <div class="columns" style="column-width: 20ex;">
   405         -    @ <ul>
   406         -    for(i=0; i<MX_COMMAND; i++){
          453  +    @ <table border="0"><tr>
          454  +    for(i=j=0; i<MX_COMMAND; i++){
          455  +      if( (aCommand[i].eCmdFlags & CMDFLAG_SETTING)==0 ) continue;
          456  +      j++;
          457  +    }
          458  +    n = (j+4)/5;
          459  +    for(i=j=0; i<MX_COMMAND; i++){
   407    460         const char *z = aCommand[i].zName;
   408    461         if( (aCommand[i].eCmdFlags & CMDFLAG_SETTING)==0 ) continue;
          462  +      if( j==0 ){
          463  +        @ <td valign="top"><ul>
          464  +      }
   409    465         if( aCommand[i].zHelp[0] ){
   410    466           @ <li><a href="%R/help?cmd=%s(z)">%s(z)</a></li>
   411    467         }else{
   412    468           @ <li>%s(z)</li>
   413    469         }
          470  +      j++;
          471  +      if( j>=n ){
          472  +        @ </ul></td>
          473  +        j = 0;
          474  +      }
          475  +    }
          476  +    if( j>0 ){
          477  +      @ </ul></td>
   414    478       }
   415         -    @ </ul></div>
          479  +    @ </tr></table>
   416    480   
   417    481     }
   418    482     style_footer();
   419    483   }
   420    484   
   421    485   /*
   422    486   ** WEBPAGE: test-all-help

Changes to src/doc.c.

   296    296   ** Verify that all entries in the aMime[] table are in sorted order.
   297    297   ** Abort with a fatal error if any is out-of-order.
   298    298   */
   299    299   static void mimetype_verify(void){
   300    300     int i;
   301    301     for(i=1; i<count(aMime); i++){
   302    302       if( fossil_strcmp(aMime[i-1].zSuffix,aMime[i].zSuffix)>=0 ){
   303         -      fossil_panic("mimetypes out of sequence: %s before %s",
          303  +      fossil_fatal("mimetypes out of sequence: %s before %s",
   304    304                      aMime[i-1].zSuffix, aMime[i].zSuffix);
   305    305       }
   306    306     }
   307    307   }
   308    308   
   309    309   /*
   310    310   ** Guess the mime-type of a document based on its name.
................................................................................
   465    465   ** Look for a file named zName in the check-in with RID=vid.  Load the content
   466    466   ** of that file into pContent and return the RID for the file.  Or return 0
   467    467   ** if the file is not found or could not be loaded.
   468    468   */
   469    469   int doc_load_content(int vid, const char *zName, Blob *pContent){
   470    470     int writable = db_is_writeable("repository");
   471    471     int rid;   /* The RID of the file being loaded */
   472         -  if( writable ){
   473         -    db_end_transaction(0);
   474         -    db_begin_write();
   475         -  }
   476    472     if( !db_table_exists("repository", "vcache") || !writable ){
   477    473       db_multi_exec(
   478    474         "CREATE %s TABLE IF NOT EXISTS vcache(\n"
   479    475         "  vid INTEGER,         -- check-in ID\n"
   480    476         "  fname TEXT,          -- filename\n"
   481    477         "  rid INTEGER,         -- artifact ID\n"
   482    478         "  PRIMARY KEY(vid,fname)\n"
................................................................................
   666    662         zFullpath = mprintf("%s/%s", g.zLocalRoot, zName);
   667    663         if( file_isfile(zFullpath, RepoFILE)
   668    664          && blob_read_from_file(&filebody, zFullpath, RepoFILE)>0 ){
   669    665           rid = 1;  /* Fake RID just to get the loop to end */
   670    666         }
   671    667         fossil_free(zFullpath);
   672    668       }else{
   673         -      vid = symbolic_name_to_rid(zCheckin, "ci");
   674         -      rid = vid>0 ? doc_load_content(vid, zName, &filebody) : 0;
          669  +      vid = name_to_typed_rid(zCheckin, "ci");
          670  +      rid = doc_load_content(vid, zName, &filebody);
   675    671       }
   676    672     }
   677    673     g.zPath = mprintf("%s/%s", g.zPath, zPathSuffix);
   678    674     if( rid==0 ) goto doc_not_found;
   679    675     blob_to_utf8_no_bom(&filebody, 0);
   680    676   
   681    677     /* The file is now contained in the filebody blob.  Deliver the
................................................................................
   766    762     cgi_set_status(404, "Not Found");
   767    763     style_header("Not Found");
   768    764     @ <p>Document %h(zOrigName) not found
   769    765     if( fossil_strcmp(zCheckin,"ckout")!=0 ){
   770    766       @ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin)</a>
   771    767     }
   772    768     style_footer();
          769  +  db_end_transaction(0);
   773    770     return;
   774    771   }
   775    772   
   776    773   /*
   777    774   ** The default logo.
   778    775   */
   779    776   static const unsigned char aLogo[] = {

Changes to src/encode.c.

   432    432   /*
   433    433   ** The characters used for HTTP base64 encoding.
   434    434   */
   435    435   static unsigned char zBase[] =
   436    436     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
   437    437   
   438    438   /*
   439         -** Translate nData bytes of content from zData into
   440         -** ((nData+2)/3)*4) bytes of base64 encoded content and
   441         -** put the result in z64.  Add a zero-terminator at the end.
          439  +** Encode a string using a base-64 encoding.
          440  +** The encoding can be reversed using the <b>decode64</b> function.
          441  +**
          442  +** Space to hold the result comes from malloc().
   442    443   */
   443         -int translateBase64(const char *zData, int nData, char *z64){
          444  +char *encode64(const char *zData, int nData){
          445  +  char *z64;
   444    446     int i, n;
          447  +
          448  +  if( nData<=0 ){
          449  +    nData = strlen(zData);
          450  +  }
          451  +  z64 = fossil_malloc( (nData*4)/3 + 8 );
   445    452     for(i=n=0; i+2<nData; i+=3){
   446    453       z64[n++] = zBase[ (zData[i]>>2) & 0x3f ];
   447    454       z64[n++] = zBase[ ((zData[i]<<4) & 0x30) | ((zData[i+1]>>4) & 0x0f) ];
   448    455       z64[n++] = zBase[ ((zData[i+1]<<2) & 0x3c) | ((zData[i+2]>>6) & 0x03) ];
   449    456       z64[n++] = zBase[ zData[i+2] & 0x3f ];
   450    457     }
   451    458     if( i+1<nData ){
................................................................................
   456    463     }else if( i<nData ){
   457    464       z64[n++] = zBase[ (zData[i]>>2) & 0x3f ];
   458    465       z64[n++] = zBase[ ((zData[i]<<4) & 0x30) ];
   459    466       z64[n++] = '=';
   460    467       z64[n++] = '=';
   461    468     }
   462    469     z64[n] = 0;
   463         -  return n;
   464         -}
   465         -
   466         -/*
   467         -** Encode a string using a base-64 encoding.
   468         -** The encoding can be reversed using the <b>decode64</b> function.
   469         -**
   470         -** Space to hold the result comes from malloc().
   471         -*/
   472         -char *encode64(const char *zData, int nData){
   473         -  char *z64;
   474         -  if( nData<=0 ){
   475         -    nData = strlen(zData);
   476         -  }
   477         -  z64 = fossil_malloc( (nData*4)/3 + 8 );
   478         -  translateBase64(zData, nData, z64);
   479    470     return z64;
   480    471   }
   481    472   
   482    473   /*
   483    474   ** COMMAND: test-encode64
   484    475   **
   485    476   ** Usage: %fossil test-encode64 STRING
................................................................................
   490    481     for(i=2; i<g.argc; i++){
   491    482       z = encode64(g.argv[i], -1);
   492    483       fossil_print("%s\n", z);
   493    484       free(z);
   494    485     }
   495    486   }
   496    487   
   497         -
   498         -/* Decode base64 text.  Write the output into zData.  The caller
   499         -** must ensure that zData is large enough.  It is ok for z64 and
   500         -** zData to be the same buffer.  In other words, it is ok to decode
   501         -** in-place.  A zero terminator is always placed at the end of zData.
   502         -*/
   503         -void decodeBase64(const char *z64, int *pnByte, char *zData){
   504         -  const unsigned char *zIn = (const unsigned char*)z64;
   505         -  int i, j, k;
   506         -  int x[4];
   507         -  static int isInit = 0;
   508         -  static signed char trans[256];
   509         -
   510         -  if( !isInit ){
   511         -    for(i=0; i<256; i++){ trans[i] = -1; }
   512         -    for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
   513         -    isInit = 1;
   514         -  }
   515         -  for(j=k=0; zIn[0]; zIn++){
   516         -    int v = trans[zIn[0]];
   517         -    if( v>=0 ){
   518         -      x[k++] = v;
   519         -      if( k==4 ){
   520         -        zData[j++] = ((x[0]<<2) & 0xfc) | ((x[1]>>4) & 0x03);
   521         -        zData[j++] = ((x[1]<<4) & 0xf0) | ((x[2]>>2) & 0x0f);
   522         -        zData[j++] = ((x[2]<<6) & 0xc0) | (x[3] & 0x3f);
   523         -        k = 0;
   524         -      }
   525         -    }
   526         -  }
   527         -  if( k>=2 ){
   528         -    zData[j++] = ((x[0]<<2) & 0xfc) | ((x[1]>>4) & 0x03);
   529         -    if( k==3 ){
   530         -      zData[j++] = ((x[1]<<4) & 0xf0) | ((x[2]>>2) & 0x0f);
   531         -    }
   532         -  }
   533         -  zData[j] = 0;
   534         -  *pnByte = j;
   535         -}
   536         -
   537    488   
   538    489   /*
   539    490   ** This function treats its input as a base-64 string and returns the
   540    491   ** decoded value of that string.  Characters of input that are not
   541    492   ** valid base-64 characters (such as spaces and newlines) are ignored.
   542    493   **
   543    494   ** Space to hold the decoded string is obtained from malloc().
   544    495   **
   545    496   ** The number of bytes decoded is returned in *pnByte
   546    497   */
   547    498   char *decode64(const char *z64, int *pnByte){
   548    499     char *zData;
   549         -  int n64 = (int)strlen(z64);
          500  +  int n64;
          501  +  int i, j;
          502  +  int a, b, c, d;
          503  +  static int isInit = 0;
          504  +  static int trans[128];
          505  +
          506  +  if( !isInit ){
          507  +    for(i=0; i<128; i++){ trans[i] = 0; }
          508  +    for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
          509  +    isInit = 1;
          510  +  }
          511  +  n64 = strlen(z64);
   550    512     while( n64>0 && z64[n64-1]=='=' ) n64--;
   551    513     zData = fossil_malloc( (n64*3)/4 + 4 );
   552         -  decodeBase64(z64, pnByte, zData);
          514  +  for(i=j=0; i+3<n64; i+=4){
          515  +    a = trans[z64[i] & 0x7f];
          516  +    b = trans[z64[i+1] & 0x7f];
          517  +    c = trans[z64[i+2] & 0x7f];
          518  +    d = trans[z64[i+3] & 0x7f];
          519  +    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
          520  +    zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
          521  +    zData[j++] = ((c<<6) & 0xc0) | (d & 0x3f);
          522  +  }
          523  +  if( i+2<n64 ){
          524  +    a = trans[z64[i] & 0x7f];
          525  +    b = trans[z64[i+1] & 0x7f];
          526  +    c = trans[z64[i+2] & 0x7f];
          527  +    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
          528  +    zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
          529  +  }else if( i+1<n64 ){
          530  +    a = trans[z64[i] & 0x7f];
          531  +    b = trans[z64[i+1] & 0x7f];
          532  +    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
          533  +  }
          534  +  zData[j] = 0;
          535  +  *pnByte = j;
   553    536     return zData;
   554    537   }
   555    538   
   556    539   /*
   557    540   ** COMMAND: test-decode64
   558    541   **
   559    542   ** Usage: %fossil test-decode64 STRING
................................................................................
   560    543   */
   561    544   void test_decode64_cmd(void){
   562    545     char *z;
   563    546     int i, n;
   564    547     for(i=2; i<g.argc; i++){
   565    548       z = decode64(g.argv[i], &n);
   566    549       fossil_print("%d: %s\n", n, z);
   567         -    fossil_free(z);
          550  +    free(z);
   568    551     }
   569    552   }
   570    553   
   571    554   /*
   572    555   ** The base-16 encoding using the following characters:
   573    556   **
   574    557   **         0123456789abcdef
................................................................................
   640    623   
   641    624   /*
   642    625   ** Return true if the input string contains only valid base-16 digits.
   643    626   ** If any invalid characters appear in the string, return false.
   644    627   */
   645    628   int validate16(const char *zIn, int nIn){
   646    629     int i;
   647         -  if( nIn<0 ) nIn = (int)strlen(zIn);
   648    630     for(i=0; i<nIn; i++, zIn++){
   649    631       if( zDecode[zIn[0]&0xff]>63 ){
   650    632         return zIn[0]==0;
   651    633       }
   652    634     }
   653    635     return 1;
   654    636   }
................................................................................
   661    643   void canonical16(char *z, int n){
   662    644     while( *z && n-- ){
   663    645       *z = zEncode[zDecode[(*z)&0x7f]&0x1f];
   664    646       z++;
   665    647     }
   666    648   }
   667    649   
   668         -/*
   669         -** Decode a string encoded using "quoted-printable".
   670         -**
   671         -**   (1)  "=" followed by two hex digits becomes a single
   672         -**        byte specified by the two digits
   673         -**
   674         -** The decoding is done in-place.
   675         -*/
   676         -void decodeQuotedPrintable(char *z, int *pnByte){
   677         -  int i, j, c;
   678         -  for(i=j=0; (c = z[i])!=0; i++){
   679         -    if( c=='=' ){
   680         -      if( z[i+1]!='\r' ){
   681         -        decode16((unsigned char*)&z[i+1], (unsigned char*)&z[j], 2);
   682         -        j++;
   683         -      }
   684         -      i += 2;
   685         -    }else{
   686         -      z[j++] = c;
   687         -    }
   688         -  }
   689         -  if( pnByte ) *pnByte = j;
   690         -  z[j] = 0;
   691         -}
   692         -
   693    650   /* Randomness used for XOR-ing by the obscure() and unobscure() routines */
   694    651   static const unsigned char aObscurer[16] = {
   695    652       0xa7, 0x21, 0x31, 0xe3, 0x2a, 0x50, 0x2c, 0x86,
   696    653       0x4c, 0xa4, 0x52, 0x25, 0xff, 0x49, 0x35, 0x85
   697    654   };
   698    655   
   699    656   

Changes to src/etag.c.

   124    124     if( strcmp(zIfNoneMatch,zETag)!=0 ) return;
   125    125   
   126    126     /* If we get this far, it means that the content has
   127    127     ** not changed and we can do a 304 reply */
   128    128     cgi_reset_content();
   129    129     cgi_set_status(304, "Not Modified");
   130    130     cgi_reply();
   131         -  db_close(0);
   132    131     fossil_exit(0);
   133    132   }
   134    133   
   135    134   /*
   136    135   ** Accept a new Last-Modified time.  This routine should be called by
   137    136   ** page generators that know a valid last-modified time.  This routine
   138    137   ** might generate a 304 Not Modified reply and exit(), never returning.
   139    138   ** Or, if not, it will cause a Last-Modified: header to be included in the
   140    139   ** reply.
   141    140   */
   142    141   void etag_last_modified(sqlite3_int64 mtime){
   143    142     const char *zIfModifiedSince;
   144         -  sqlite3_int64 x;
          143  +  sqlite3_int64 x, exeMtime;
   145    144     assert( iEtagMtime==0 );   /* Only call this routine once */
   146    145     assert( mtime>0 );         /* Only call with a valid mtime */
   147    146     iEtagMtime = mtime;
   148    147   
   149    148     /* Check to see the If-Modified-Since constraint is satisfied */
   150    149     zIfModifiedSince = P("HTTP_IF_MODIFIED_SINCE");
   151    150     if( zIfModifiedSince==0 ) return;
   152    151     x = cgi_rfc822_parsedate(zIfModifiedSince);
   153         -  if( x<mtime ) return;
          152  +  if( x<=0 || x>mtime ) return;
   154    153   
   155         -#if 0  
   156    154     /* If the Fossil executable is more recent than If-Modified-Since,
   157    155     ** go ahead and regenerate the resource. */
   158         -  if( file_mtime(g.nameOfExe, ExtFILE)>x ) return;
   159         -#endif
          156  +  exeMtime = file_mtime(g.nameOfExe, ExtFILE);
          157  +  if( exeMtime>x ) return;
   160    158   
   161    159     /* If we reach this point, it means that the resource has not changed
   162    160     ** and that we should generate a 304 Not Modified reply */
   163    161     cgi_reset_content();
   164    162     cgi_set_status(304, "Not Modified");
   165    163     cgi_reply();
   166         -  db_close(0);
   167    164     fossil_exit(0);
   168    165   }
   169    166   
   170    167   /* Return the ETag, if there is one.
   171    168   */
   172    169   const char *etag_tag(void){
   173    170     return zETag;

Changes to src/event.c.

   585    585   
   586    586     user_select();
   587    587     if (event_commit_common(rid, zId, blob_str(pContent), zETime,
   588    588         zMimeType, zComment, zTags, zClr)==0 ){
   589    589   #ifdef FOSSIL_ENABLE_JSON
   590    590       g.json.resultCode = FSL_JSON_E_ASSERT;
   591    591   #endif
   592         -    fossil_panic("Internal error: Fossil tried to make an "
          592  +    fossil_fatal("Internal error: Fossil tried to make an "
   593    593                    "invalid artifact for the technote.");
   594    594     }
   595    595   }

Changes to src/export.c.

   598    598     }
   599    599     db_finalize(&q);
   600    600     db_finalize(&q2);
   601    601     db_finalize(&q3);
   602    602   
   603    603     /* Output the commit records.
   604    604     */
   605         -  topological_sort_checkins(0);
   606    605     db_prepare(&q,
   607    606       "SELECT strftime('%%s',mtime), objid, coalesce(ecomment,comment),"
   608    607       "       coalesce(euser,user),"
   609    608       "       (SELECT value FROM tagxref WHERE rid=objid AND tagid=%d)"
   610         -    "  FROM toponode, event"
   611         -    " WHERE toponode.tid=event.objid"
   612         -    "   AND event.type='ci'"
   613         -    "   AND NOT EXISTS (SELECT 1 FROM oldcommit WHERE toponode.tid=rid)"
   614         -    " ORDER BY toponode.tseq ASC",
          609  +    "  FROM event"
          610  +    " WHERE type='ci' AND NOT EXISTS (SELECT 1 FROM oldcommit WHERE objid=rid)"
          611  +    " ORDER BY mtime ASC",
   615    612       TAG_BRANCH
   616    613     );
   617    614     db_prepare(&q2, "INSERT INTO oldcommit VALUES (:rid)");
   618    615     while( db_step(&q)==SQLITE_ROW ){
   619    616       Stmt q4;
   620    617       const char *zSecondsSince1970 = db_column_text(&q, 0);
   621    618       int ckinId = db_column_int(&q, 1);
................................................................................
   624    621       const char *zBranch = db_column_text(&q, 4);
   625    622       char *zMark;
   626    623   
   627    624       bag_insert(&vers, ckinId);
   628    625       db_bind_int(&q2, ":rid", ckinId);
   629    626       db_step(&q2);
   630    627       db_reset(&q2);
   631         -    if( zBranch==0 || fossil_strcmp(zBranch, "trunk")==0 ){
   632         -      zBranch = gexport.zTrunkName;
   633         -    }
          628  +    if( zBranch==0 || fossil_strcmp(zBranch, "trunk")==0 ) zBranch = gexport.zTrunkName;
   634    629       zMark = mark_name_from_rid(ckinId, &unused_mark);
   635    630       printf("commit refs/heads/");
   636    631       print_ref(zBranch);
   637    632       printf("\nmark %s\n", zMark);
   638    633       free(zMark);
   639    634       printf("committer");
   640    635       print_person(zUser);
................................................................................
   740    735       if( ferror(f)!=0 || fclose(f)!=0 ){
   741    736         fossil_fatal("error while writing %s", markfile_out);
   742    737       }
   743    738     }
   744    739     bag_clear(&blobs);
   745    740     bag_clear(&vers);
   746    741   }
   747         -
   748         -/*
   749         -** Construct the temporary table toposort as follows:
   750         -**
   751         -**     CREATE TEMP TABLE toponode(
   752         -**        tid INTEGER PRIMARY KEY,   -- Check-in id
   753         -**        tseq INT                   -- integer total order on check-ins.
   754         -**     );
   755         -**
   756         -** This table contains all check-ins of the repository in topological
   757         -** order.  "Topological order" means that every parent check-in comes
   758         -** before all of its children.  Topological order is *almost* the same
   759         -** thing as "ORDER BY event.mtime".  Differences only arrise when there
   760         -** are timewarps.  In as much as Git hates timewarps, we have to compute
   761         -** a correct topological order when doing an export.
   762         -**
   763         -** Since mtime is a usually already nearly in topological order, the
   764         -** algorithm is to start with mtime, then make adjustments as necessary
   765         -** for timewarps.  This is not a great algorithm for the general case,
   766         -** but it is very fast for the overwhelmingly common case where there
   767         -** are few timewarps.
   768         -*/
   769         -int topological_sort_checkins(int bVerbose){
   770         -  int nChange = 0;
   771         -  Stmt q1;
   772         -  Stmt chng;
   773         -  db_multi_exec(
   774         -    "CREATE TEMP TABLE toponode(\n"
   775         -    "  tid INTEGER PRIMARY KEY,\n"
   776         -    "  tseq INT\n"
   777         -    ");\n"
   778         -    "INSERT INTO toponode(tid,tseq) "
   779         -    " SELECT objid, CAST(mtime*8640000 AS int) FROM event WHERE type='ci';\n"
   780         -    "CREATE TEMP TABLE topolink(\n"
   781         -    "  tparent INT,\n"
   782         -    "  tchild INT,\n"
   783         -    "  PRIMARY KEY(tparent,tchild)\n"
   784         -    ") WITHOUT ROWID;"
   785         -    "INSERT INTO topolink(tparent,tchild)"
   786         -    "  SELECT pid, cid FROM plink;\n"
   787         -    "CREATE INDEX topolink_child ON topolink(tchild);\n"
   788         -  );
   789         -
   790         -  /* Find a timewarp instance */
   791         -  db_prepare(&q1,
   792         -    "SELECT P.tseq, C.tid, C.tseq\n"
   793         -    "  FROM toponode P, toponode C, topolink X\n"
   794         -    " WHERE X.tparent=P.tid\n"
   795         -    "   AND X.tchild=C.tid\n"
   796         -    "   AND P.tseq>=C.tseq;"
   797         -  );
   798         -
   799         -  /* Update the timestamp on :tid to have value :tseq */
   800         -  db_prepare(&chng,
   801         -    "UPDATE toponode SET tseq=:tseq WHERE tid=:tid"
   802         -  );
   803         -
   804         -  while( db_step(&q1)==SQLITE_ROW ){
   805         -    i64 iParentTime = db_column_int64(&q1, 0);
   806         -    int iChild = db_column_int(&q1, 1);
   807         -    i64 iChildTime = db_column_int64(&q1, 2);
   808         -    nChange++;
   809         -    if( nChange>10000 ){
   810         -      fossil_fatal("failed to fix all timewarps after 100000 attempts");
   811         -    }
   812         -    db_reset(&q1);
   813         -    db_bind_int64(&chng, ":tid", iChild);
   814         -    db_bind_int64(&chng, ":tseq", iParentTime+1);
   815         -    db_step(&chng);
   816         -    db_reset(&chng);
   817         -    if( bVerbose ){
   818         -      fossil_print("moving %d from %lld to %lld\n",
   819         -                   iChild, iChildTime, iParentTime+1);
   820         -    }
   821         -  }
   822         -
   823         -  db_finalize(&q1);
   824         -  db_finalize(&chng);
   825         -  return nChange;
   826         -}
   827         -
   828         -/*
   829         -** COMMAND: test-topological-sort
   830         -**
   831         -** Invoke the topological_sort_checkins() interface for testing
   832         -** purposes.
   833         -*/
   834         -void test_topological_sort(void){
   835         -  int n;
   836         -  db_find_and_open_repository(0, 0);
   837         -  n = topological_sort_checkins(1);
   838         -  fossil_print("%d reorderings required\n", n);
   839         -}

Changes to src/file.c.

    20     20   #include "config.h"
    21     21   #include <sys/types.h>
    22     22   #include <sys/stat.h>
    23     23   #include <unistd.h>
    24     24   #include <stdio.h>
    25     25   #include <string.h>
    26     26   #include <errno.h>
    27         -#include <time.h>
    28     27   #include "file.h"
    29     28   
    30     29   /*
    31     30   ** On Windows, include the Platform SDK header file.
    32     31   */
    33     32   #ifdef _WIN32
    34     33   # include <direct.h>
................................................................................
   895    894   */
   896    895   void file_getcwd(char *zBuf, int nBuf){
   897    896   #ifdef _WIN32
   898    897     win32_getcwd(zBuf, nBuf);
   899    898   #else
   900    899     if( getcwd(zBuf, nBuf-1)==0 ){
   901    900       if( errno==ERANGE ){
   902         -      fossil_panic("pwd too big: max %d", nBuf-1);
          901  +      fossil_fatal("pwd too big: max %d", nBuf-1);
   903    902       }else{
   904         -      fossil_panic("cannot find current working directory; %s",
          903  +      fossil_fatal("cannot find current working directory; %s",
   905    904                      strerror(errno));
   906    905       }
   907    906     }
   908    907   #endif
   909    908   }
   910    909   
   911    910   /*
................................................................................
  1467   1466     z = blob_buffer(pBuf);
  1468   1467     for(i=0; z[i]; i++) if( z[i]=='\\' ) z[i] = '/';
  1469   1468   #else
  1470   1469     fossil_path_free((char *)azDirs[0]);
  1471   1470   #endif
  1472   1471   }
  1473   1472   
  1474         -/*
  1475         -** Compute a temporary filename in zDir.  The filename is based on
  1476         -** the current time.
  1477         -*/
  1478         -char *file_time_tempname(const char *zDir, const char *zSuffix){
  1479         -  struct tm *tm;
  1480         -  unsigned int r;
  1481         -  static unsigned int cnt = 0;
  1482         -  time_t t;
  1483         -  t = time(0);
  1484         -  tm = gmtime(&t);
  1485         -  sqlite3_randomness(sizeof(r), &r);
  1486         -  return mprintf("%s/%04d%02d%02d%02d%02d%02d%04d%06d%s",
  1487         -      zDir, tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
  1488         -            tm->tm_hour, tm->tm_min, tm->tm_sec, cnt++, r%1000000, zSuffix);
  1489         -}
  1490         -
  1491   1473   
  1492   1474   /*
  1493   1475   ** COMMAND: test-tempname
  1494         -** Usage:  fossil test-name [--time SUFFIX] BASENAME ...
         1476  +** Usage:  fossil test-name BASENAME ...
  1495   1477   **
  1496         -** Generate temporary filenames derived from BASENAME.  Use the --time
  1497         -** option to generate temp names based on the time of day.
         1478  +** Generate temporary filenames derived from BASENAME
  1498   1479   */
  1499   1480   void file_test_tempname(void){
  1500   1481     int i;
  1501         -  const char *zSuffix = find_option("time",0,1);
  1502   1482     Blob x = BLOB_INITIALIZER;
  1503         -  char *z;
  1504         -  verify_all_options();
  1505   1483     for(i=2; i<g.argc; i++){
  1506         -    if( zSuffix ){
  1507         -      z = file_time_tempname(g.argv[i], zSuffix);
  1508         -      fossil_print("%s\n", z);
  1509         -      fossil_free(z);
  1510         -    }else{
  1511         -      file_tempname(&x, g.argv[i]);
  1512         -      fossil_print("%s\n", blob_str(&x));
  1513         -      blob_reset(&x);
  1514         -    }
         1484  +    file_tempname(&x, g.argv[i]);
         1485  +    fossil_print("%s\n", blob_str(&x));
         1486  +    blob_reset(&x);
  1515   1487     }
  1516   1488   }
  1517   1489   
  1518   1490   
  1519   1491   /*
  1520   1492   ** Return true if a file named zName exists and has identical content
  1521   1493   ** to the blob pContent.  If zName does not exist or if the content is
................................................................................
  1640   1612   #if defined(_WIN32) || defined(__CYGWIN__)
  1641   1613     if( z[0]=='/' && fossil_isalpha(z[1]) && z[2]==':' && z[3]=='/' ) z++;
  1642   1614   #else
  1643   1615     while( z[0]=='/' && z[1]=='/' ) z++;
  1644   1616   #endif
  1645   1617     return z;
  1646   1618   }
  1647         -
  1648         -/*
  1649         -** Count the number of objects (files and subdirectores) in a given
  1650         -** directory.  Return the count.  Return -1 of the object is not a
  1651         -** directory.
  1652         -*/
  1653         -int file_directory_size(const char *zDir, const char *zGlob, int omitDotFiles){
  1654         -  void *zNative;
  1655         -  DIR *d;
  1656         -  int n = -1;
  1657         -  zNative = fossil_utf8_to_path(zDir,1);
  1658         -  d = opendir(zNative);
  1659         -  if( d ){
  1660         -    struct dirent *pEntry;
  1661         -    n = 0;
  1662         -    while( (pEntry=readdir(d))!=0 ){
  1663         -      if( pEntry->d_name[0]==0 ) continue;
  1664         -      if( omitDotFiles && pEntry->d_name[0]=='.' ) continue;
  1665         -      if( zGlob ){
  1666         -        char *zUtf8 = fossil_path_to_utf8(pEntry->d_name);
  1667         -        int rc = sqlite3_strglob(zGlob, zUtf8);
  1668         -        fossil_path_free(zUtf8);
  1669         -        if( rc ) continue;
  1670         -      }
  1671         -      n++;
  1672         -    }
  1673         -    closedir(d);
  1674         -  }
  1675         -  fossil_path_free(zNative);
  1676         -  return n;
  1677         -}
  1678         -
  1679         -/*
  1680         -** COMMAND: test-dir-size
  1681         -**
  1682         -** Usage: %fossil test-dir-size NAME [GLOB] [--nodots]
  1683         -**
  1684         -** Return the number of objects in the directory NAME.  If GLOB is
  1685         -** provided, then only count objects that match the GLOB pattern.
  1686         -** if --nodots is specified, omit files that begin with ".".
  1687         -*/
  1688         -void test_dir_size_cmd(void){
  1689         -  int omitDotFiles = find_option("nodots",0,0)!=0;
  1690         -  const char *zGlob;
  1691         -  const char *zDir;
  1692         -  verify_all_options();
  1693         -  if( g.argc!=3 && g.argc!=4 ){
  1694         -    usage("NAME [GLOB] [-nodots]");
  1695         -  }
  1696         -  zDir = g.argv[2];
  1697         -  zGlob = g.argc==4 ? g.argv[3] : 0;
  1698         -  fossil_print("%d\n", file_directory_size(zDir, zGlob, omitDotFiles));
  1699         -}

Deleted src/forum.c.

     1         -/*
     2         -** Copyright (c) 2018 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 used to generate the user forum.
    19         -*/
    20         -#include "config.h"
    21         -#include <assert.h>
    22         -#include "forum.h"
    23         -
    24         -/*
    25         -** Default to using Markdown markup
    26         -*/
    27         -#define DEFAULT_FORUM_MIMETYPE  "text/x-markdown"
    28         -
    29         -#if INTERFACE
    30         -/*
    31         -** Each instance of the following object represents a single message - 
    32         -** either the initial post, an edit to a post, a reply, or an edit to
    33         -** a reply.
    34         -*/
    35         -struct ForumEntry {
    36         -  int fpid;              /* rid for this entry */
    37         -  int fprev;             /* zero if initial entry.  non-zero if an edit */
    38         -  int firt;              /* This entry replies to firt */
    39         -  int mfirt;             /* Root in-reply-to */
    40         -  char *zUuid;           /* Artifact hash */
    41         -  ForumEntry *pLeaf;     /* Most recent edit for this entry */
    42         -  ForumEntry *pEdit;     /* This entry is an edit of pEditee */
    43         -  ForumEntry *pNext;     /* Next in chronological order */
    44         -  ForumEntry *pPrev;     /* Previous in chronological order */
    45         -  ForumEntry *pDisplay;  /* Next in display order */
    46         -  int nIndent;           /* Number of levels of indentation for this entry */
    47         -};
    48         -
    49         -/*
    50         -** A single instance of the following tracks all entries for a thread.
    51         -*/
    52         -struct ForumThread {
    53         -  ForumEntry *pFirst;    /* First entry in chronological order */
    54         -  ForumEntry *pLast;     /* Last entry in chronological order */
    55         -  ForumEntry *pDisplay;  /* Entries in display order */
    56         -  ForumEntry *pTail;     /* Last on the display list */
    57         -};
    58         -#endif /* INTERFACE */
    59         -
    60         -/*
    61         -** Delete a complete ForumThread and all its entries.
    62         -*/
    63         -static void forumthread_delete(ForumThread *pThread){
    64         -  ForumEntry *pEntry, *pNext;
    65         -  for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){
    66         -    pNext = pEntry->pNext;
    67         -    fossil_free(pEntry->zUuid);
    68         -    fossil_free(pEntry);
    69         -  }
    70         -  fossil_free(pThread);
    71         -}
    72         -
    73         -#if 0 /* not used */
    74         -/*
    75         -** Search a ForumEntry list forwards looking for the entry with fpid
    76         -*/
    77         -static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){
    78         -  while( p && p->fpid!=fpid ) p = p->pNext;
    79         -  return p;
    80         -}
    81         -#endif
    82         -
    83         -/*
    84         -** Search backwards for a ForumEntry
    85         -*/
    86         -static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){
    87         -  while( p && p->fpid!=fpid ) p = p->pPrev;
    88         -  return p;
    89         -}
    90         -
    91         -/*
    92         -** Add an entry to the display list
    93         -*/
    94         -static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){
    95         -  if( pThread->pDisplay==0 ){
    96         -    pThread->pDisplay = p;
    97         -  }else{
    98         -    pThread->pTail->pDisplay = p;
    99         -  }
   100         -  pThread->pTail = p;
   101         -}
   102         -
   103         -/*
   104         -** Extend the display list for pThread by adding all entries that
   105         -** reference fpid.  The first such entry will be no earlier then
   106         -** entry "p".
   107         -*/
   108         -static void forumthread_display_order(
   109         -  ForumThread *pThread,
   110         -  ForumEntry *p,
   111         -  int fpid,
   112         -  int nIndent
   113         -){
   114         -  while( p ){
   115         -    if( p->fprev==0 && p->mfirt==fpid ){
   116         -      p->nIndent = nIndent;
   117         -      forumentry_add_to_display(pThread, p);
   118         -      forumthread_display_order(pThread, p->pNext, p->fpid, nIndent+1);
   119         -    }
   120         -    p = p->pNext;
   121         -  }
   122         -}
   123         -
   124         -/*
   125         -** Construct a ForumThread object given the root record id.
   126         -*/
   127         -static ForumThread *forumthread_create(int froot, int computeHierarchy){
   128         -  ForumThread *pThread;
   129         -  ForumEntry *pEntry;
   130         -  Stmt q;
   131         -  pThread = fossil_malloc( sizeof(*pThread) );
   132         -  memset(pThread, 0, sizeof(*pThread));
   133         -  db_prepare(&q,
   134         -     "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)"
   135         -     "  FROM forumpost"
   136         -     " WHERE froot=%d ORDER BY fmtime",
   137         -     froot
   138         -  );
   139         -  while( db_step(&q)==SQLITE_ROW ){
   140         -    pEntry = fossil_malloc( sizeof(*pEntry) );
   141         -    memset(pEntry, 0, sizeof(*pEntry));
   142         -    pEntry->fpid = db_column_int(&q, 0);
   143         -    pEntry->firt = db_column_int(&q, 1);
   144         -    pEntry->fprev = db_column_int(&q, 2);
   145         -    pEntry->zUuid = fossil_strdup(db_column_text(&q,3));
   146         -    pEntry->mfirt = pEntry->firt;
   147         -    pEntry->pPrev = pThread->pLast;
   148         -    pEntry->pNext = 0;
   149         -    if( pThread->pLast==0 ){
   150         -      pThread->pFirst = pEntry;
   151         -    }else{
   152         -      pThread->pLast->pNext = pEntry;
   153         -    }
   154         -    pThread->pLast = pEntry;
   155         -  }
   156         -  db_finalize(&q);
   157         -
   158         -  /* Establish which entries are the latest edit.  After this loop
   159         -  ** completes, entries that have non-NULL pLeaf should not be
   160         -  ** displayed.
   161         -  */
   162         -  for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){
   163         -    if( pEntry->fprev ){
   164         -      ForumEntry *pBase = 0, *p;
   165         -      p = forumentry_backward(pEntry->pPrev, pEntry->fprev);
   166         -      pEntry->pEdit = p;
   167         -      while( p ){
   168         -        pBase = p;
   169         -        p->pLeaf = pEntry;
   170         -        p = pBase->pEdit;
   171         -      }
   172         -      for(p=pEntry->pNext; p; p=p->pNext){
   173         -        if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid;
   174         -      }
   175         -    }
   176         -  }
   177         -
   178         -  if( computeHierarchy ){
   179         -    /* Compute the hierarchical display order */
   180         -    pEntry = pThread->pFirst;
   181         -    pEntry->nIndent = 1;
   182         -    forumentry_add_to_display(pThread, pEntry);
   183         -    forumthread_display_order(pThread, pEntry, pEntry->fpid, 2);
   184         -  }
   185         -
   186         -  /* Return the result */
   187         -  return pThread;
   188         -}
   189         -
   190         -/*
   191         -** COMMAND: test-forumthread
   192         -**
   193         -** Usage: %fossil test-forumthread THREADID
   194         -**
   195         -** Display a summary of all messages on a thread.
   196         -*/
   197         -void forumthread_cmd(void){
   198         -  int fpid;
   199         -  int froot;
   200         -  const char *zName;
   201         -  ForumThread *pThread;
   202         -  ForumEntry *p;
   203         -
   204         -  db_find_and_open_repository(0,0);
   205         -  verify_all_options();
   206         -  if( g.argc!=3 ) usage("THREADID");
   207         -  zName = g.argv[2];
   208         -  fpid = symbolic_name_to_rid(zName, "f");
   209         -  if( fpid<=0 ){
   210         -    fossil_fatal("Unknown or ambiguous forum id: \"%s\"", zName);
   211         -  }
   212         -  froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
   213         -  if( froot==0 ){
   214         -    fossil_fatal("Not a forum post: \"%s\"", zName);
   215         -  }
   216         -  fossil_print("fpid  = %d\n", fpid);
   217         -  fossil_print("froot = %d\n", froot);
   218         -  pThread = forumthread_create(froot, 1);
   219         -  fossil_print("Chronological:\n");
   220         -           /*   123456789 123456789 123456789 123456789 123456789  */
   221         -  fossil_print("     fpid      firt     fprev     mfirt     pLeaf\n");
   222         -  for(p=pThread->pFirst; p; p=p->pNext){
   223         -    fossil_print("%9d %9d %9d %9d %9d\n",
   224         -       p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0);
   225         -  }
   226         -  fossil_print("\nDisplay\n");
   227         -  for(p=pThread->pDisplay; p; p=p->pDisplay){
   228         -    fossil_print("%*s", (p->nIndent-1)*3, "");
   229         -    if( p->pLeaf ){
   230         -      fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid);
   231         -    }else{
   232         -      fossil_print("%d\n", p->fpid);
   233         -    }
   234         -  }
   235         -  forumthread_delete(pThread);
   236         -}
   237         -
   238         -/*
   239         -** Render a forum post for display
   240         -*/
   241         -void forum_render(
   242         -  const char *zTitle,         /* The title.  Might be NULL for no title */
   243         -  const char *zMimetype,      /* Mimetype of the message */
   244         -  const char *zContent,       /* Content of the message */
   245         -  const char *zClass          /* Put in a <div> if not NULL */
   246         -){
   247         -  if( zClass ){
   248         -    @ <div class='%s(zClass)'>
   249         -  }
   250         -  if( zTitle ){
   251         -    if( zTitle[0] ){
   252         -      @ <h1>%h(zTitle)</h1>
   253         -    }else{
   254         -      @ <h1><i>Deleted</i></h1>
   255         -    }
   256         -  }
   257         -  if( zContent && zContent[0] ){
   258         -    Blob x;
   259         -    blob_init(&x, 0, 0);
   260         -    blob_append(&x, zContent, -1);
   261         -    wiki_render_by_mimetype(&x, zMimetype);
   262         -    blob_reset(&x);
   263         -  }else{
   264         -    @ <i>Deleted</i>
   265         -  }
   266         -  if( zClass ){
   267         -    @ </div>
   268         -  }
   269         -}
   270         -
   271         -/*
   272         -** Display all posts in a forum thread in chronological order
   273         -*/
   274         -static void forum_display_chronological(int froot, int target){
   275         -  ForumThread *pThread = forumthread_create(froot, 0);
   276         -  ForumEntry *p;
   277         -  int notAnon = login_is_individual();
   278         -  for(p=pThread->pFirst; p; p=p->pNext){
   279         -    char *zDate;
   280         -    Manifest *pPost;
   281         -    int isPrivate;        /* True for posts awaiting moderation */
   282         -    int sameUser;         /* True if author is also the reader */
   283         -
   284         -    pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
   285         -    if( pPost==0 ) continue;
   286         -    if( p->fpid==target ){
   287         -      @ <div id="forum%d(p->fpid)" class="forumTime forumSel">
   288         -    }else if( p->pLeaf!=0 ){
   289         -      @ <div id="forum%d(p->fpid)" class="forumTime forumObs">
   290         -    }else{
   291         -      @ <div id="forum%d(p->fpid)" class="forumTime">
   292         -    }
   293         -    if( pPost->zThreadTitle ){
   294         -      @ <h1>%h(pPost->zThreadTitle)</h1>
   295         -    }
   296         -    zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
   297         -    @ <p>By %h(pPost->zUser) on %h(zDate) (%d(p->fpid))
   298         -    fossil_free(zDate);
   299         -    if( p->pEdit ){
   300         -      @ edit of %z(href("%R/forumpost/%S?t",p->pEdit->zUuid))%d(p->fprev)</a>
   301         -    }
   302         -    if( p->firt ){
   303         -      ForumEntry *pIrt = p->pPrev;
   304         -      while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
   305         -      if( pIrt ){
   306         -        @ reply to %z(href("%R/forumpost/%S?t",pIrt->zUuid))%d(p->firt)</a>
   307         -      }
   308         -    }
   309         -    if( p->pLeaf ){
   310         -      @ updated by %z(href("%R/forumpost/%S?t",p->pLeaf->zUuid))\
   311         -      @ %d(p->pLeaf->fpid)</a>
   312         -    }
   313         -    if( g.perm.Debug ){
   314         -      @ <span class="debug">\
   315         -      @ <a href="%R/artifact/%h(p->zUuid)">artifact</a></span>
   316         -    }
   317         -    if( p->fpid!=target ){
   318         -      @ %z(href("%R/forumpost/%S?t",p->zUuid))[link]</a>
   319         -    }
   320         -    isPrivate = content_is_private(p->fpid);
   321         -    sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   322         -    if( isPrivate && !g.perm.ModForum && !sameUser ){
   323         -      @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
   324         -    }else{
   325         -      forum_render(0, pPost->zMimetype, pPost->zWiki, 0);
   326         -    }
   327         -    if( g.perm.WrForum && p->pLeaf==0 ){
   328         -      int sameUser = login_is_individual()
   329         -                     && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   330         -      @ <p><form action="%R/forumedit" method="POST">
   331         -      @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
   332         -      if( !isPrivate ){
   333         -        /* Reply and Edit are only available if the post has already
   334         -        ** been approved */
   335         -        @ <input type="submit" name="reply" value="Reply">
   336         -        if( g.perm.Admin || sameUser ){
   337         -          @ <input type="submit" name="edit" value="Edit">
   338         -          @ <input type="submit" name="nullout" value="Delete">
   339         -        }
   340         -      }else if( g.perm.ModForum ){
   341         -        /* Provide moderators with moderation buttons for posts that
   342         -        ** are pending moderation */
   343         -        @ <input type="submit" name="approve" value="Approve">
   344         -        @ <input type="submit" name="reject" value="Reject">
   345         -      }else if( sameUser ){
   346         -        /* A post that is pending moderation can be deleted by the
   347         -        ** person who originally submitted the post */
   348         -        @ <input type="submit" name="reject" value="Delete">
   349         -      }
   350         -      @ </form></p>
   351         -    }
   352         -    manifest_destroy(pPost);
   353         -    @ </div>
   354         -  }
   355         -  forumthread_delete(pThread);
   356         -}
   357         -
   358         -/*
   359         -** Display all messages in a forumthread with indentation.
   360         -*/
   361         -static int forum_display_hierarchical(int froot, int target){
   362         -  ForumThread *pThread;
   363         -  ForumEntry *p;
   364         -  Manifest *pPost, *pOPost;
   365         -  int fpid;
   366         -  const char *zUuid;
   367         -  char *zDate;
   368         -  const char *zSel;
   369         -  int notAnon = login_is_individual();
   370         -
   371         -  pThread = forumthread_create(froot, 1);
   372         -  for(p=pThread->pFirst; p; p=p->pNext){
   373         -    if( p->fpid==target ){
   374         -      while( p->pEdit ) p = p->pEdit;
   375         -      target = p->fpid;
   376         -      break;
   377         -    }
   378         -  }
   379         -  for(p=pThread->pDisplay; p; p=p->pDisplay){
   380         -    int isPrivate;         /* True for posts awaiting moderation */
   381         -    int sameUser;          /* True if reader is also the poster */
   382         -    pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
   383         -    if( p->pLeaf ){
   384         -      fpid = p->pLeaf->fpid;
   385         -      zUuid = p->pLeaf->zUuid;
   386         -      pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
   387         -    }else{
   388         -      fpid = p->fpid;
   389         -      zUuid = p->zUuid;
   390         -      pPost = pOPost;
   391         -    }
   392         -    zSel = p->fpid==target ? " forumSel" : "";
   393         -    if( p->nIndent==1 ){
   394         -      @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'>
   395         -    }else{
   396         -      @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \
   397         -      @ style='margin-left: %d((p->nIndent-1)*3)ex;'>
   398         -    }
   399         -    pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
   400         -    if( pPost==0 ) continue;
   401         -    if( pPost->zThreadTitle ){
   402         -      @ <h1>%h(pPost->zThreadTitle)</h1>
   403         -    }
   404         -    zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate);
   405         -    @ <p>By %h(pOPost->zUser) on %h(zDate)
   406         -    fossil_free(zDate);
   407         -    if( g.perm.Debug ){
   408         -      @ <span class="debug">\
   409         -      @ <a href="%R/artifact/%h(p->zUuid)">(%d(p->fpid))</a></span>
   410         -    }
   411         -    if( p->pLeaf ){
   412         -      zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
   413         -      if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){
   414         -        @ and edited on %h(zDate)
   415         -      }else{
   416         -        @ as edited by %h(pPost->zUser) on %h(zDate)
   417         -      }
   418         -      fossil_free(zDate);
   419         -      if( g.perm.Debug ){
   420         -        @ <span class="debug">\
   421         -        @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">(%d(fpid))</a></span>
   422         -      }
   423         -      manifest_destroy(pOPost);
   424         -    }
   425         -    if( fpid!=target ){
   426         -      @ %z(href("%R/forumpost/%S",zUuid))[link]</a>
   427         -    }
   428         -    isPrivate = content_is_private(fpid);
   429         -    sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   430         -    if( isPrivate && !g.perm.ModForum && !sameUser ){
   431         -      @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
   432         -    }else{
   433         -      forum_render(0, pPost->zMimetype, pPost->zWiki, 0);
   434         -    }
   435         -    if( g.perm.WrForum ){
   436         -      @ <p><form action="%R/forumedit" method="POST">
   437         -      @ <input type="hidden" name="fpid" value="%s(zUuid)">
   438         -      if( !isPrivate ){
   439         -        /* Reply and Edit are only available if the post has already
   440         -        ** been approved */
   441         -        @ <input type="submit" name="reply" value="Reply">
   442         -        if( g.perm.Admin || sameUser ){
   443         -          @ <input type="submit" name="edit" value="Edit">
   444         -          @ <input type="submit" name="nullout" value="Delete">
   445         -        }
   446         -      }else if( g.perm.ModForum ){
   447         -        /* Provide moderators with moderation buttons for posts that
   448         -        ** are pending moderation */
   449         -        @ <input type="submit" name="approve" value="Approve">
   450         -        @ <input type="submit" name="reject" value="Reject">
   451         -      }else if( sameUser ){
   452         -        /* A post that is pending moderation can be deleted by the
   453         -        ** person who originally submitted the post */
   454         -        @ <input type="submit" name="reject" value="Delete">
   455         -      }
   456         -      @ </form></p>
   457         -    }
   458         -    manifest_destroy(pPost);
   459         -    @ </div>
   460         -  }
   461         -  forumthread_delete(pThread);
   462         -  return target;
   463         -}
   464         -
   465         -/*
   466         -** WEBPAGE: forumpost
   467         -**
   468         -** Show a single forum posting. The posting is shown in context with
   469         -** it's entire thread.  The selected posting is enclosed within
   470         -** <div class='forumSel'>...</div>.  Javascript is used to move the
   471         -** selected posting into view after the page loads.
   472         -**
   473         -** Query parameters:
   474         -**
   475         -**   name=X        REQUIRED.  The hash of the post to display
   476         -**   t             Show a chronologic listing instead of hierarchical
   477         -*/
   478         -void forumpost_page(void){
   479         -  forumthread_page();
   480         -}
   481         -
   482         -/*
   483         -** WEBPAGE: forumthread
   484         -**
   485         -** Show all forum messages associated with a particular message thread.
   486         -** The result is basically the same as /forumpost except that none of
   487         -** the postings in the thread are selected.
   488         -**
   489         -** Query parameters:
   490         -**
   491         -**   name=X        REQUIRED.  The hash of any post of the thread.
   492         -**   t             Show a chronologic listing instead of hierarchical
   493         -*/
   494         -void forumthread_page(void){
   495         -  int fpid;
   496         -  int froot;
   497         -  const char *zName = P("name");
   498         -  login_check_credentials();
   499         -  if( !g.perm.RdForum ){
   500         -    login_needed(g.anon.RdForum);
   501         -    return;
   502         -  }
   503         -  if( zName==0 ){
   504         -    webpage_error("Missing \"name=\" query parameter");
   505         -  }
   506         -  fpid = symbolic_name_to_rid(zName, "f");
   507         -  if( fpid<=0 ){
   508         -    webpage_error("Unknown or ambiguous forum id: \"%s\"", zName);
   509         -  }
   510         -  style_header("Forum");
   511         -  froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
   512         -  if( froot==0 ){
   513         -    webpage_error("Not a forum post: \"%s\"", zName);
   514         -  }
   515         -  if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
   516         -  if( P("t") ){
   517         -    if( g.perm.Debug ){
   518         -      style_submenu_element("Hierarchical", "%R/%s/%s", g.zPath, zName);
   519         -    }                          
   520         -    forum_display_chronological(froot, fpid);
   521         -  }else{
   522         -    if( g.perm.Debug ){
   523         -      style_submenu_element("Chronological", "%R/%s/%s?t", g.zPath, zName);
   524         -    }                          
   525         -    forum_display_hierarchical(froot, fpid);
   526         -  }
   527         -  style_load_js("forum.js");
   528         -  style_footer();
   529         -}
   530         -
   531         -/*
   532         -** Return true if a forum post should be moderated.
   533         -*/
   534         -static int forum_need_moderation(void){
   535         -  if( P("domod") ) return 1;
   536         -  if( g.perm.WrTForum ) return 0;
   537         -  if( g.perm.ModForum ) return 0;
   538         -  return 1;
   539         -}
   540         -
   541         -/*
   542         -** Add a new Forum Post artifact to the repository.
   543         -**
   544         -** Return true if a redirect occurs.
   545         -*/
   546         -static int forum_post(
   547         -  const char *zTitle,          /* Title.  NULL for replies */
   548         -  int iInReplyTo,              /* Post replying to.  0 for new threads */
   549         -  int iEdit,                   /* Post being edited, or zero for a new post */
   550         -  const char *zUser,           /* Username.  NULL means use login name */
   551         -  const char *zMimetype,       /* Mimetype of content. */
   552         -  const char *zContent         /* Content */
   553         -){
   554         -  char *zDate;
   555         -  char *zI;
   556         -  char *zG;
   557         -  int iBasis;
   558         -  Blob x, cksum, formatCheck, errMsg;
   559         -  Manifest *pPost;
   560         -
   561         -  schema_forum();
   562         -  if( iInReplyTo==0 && iEdit>0 ){
   563         -    iBasis = iEdit;
   564         -    iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
   565         -  }else{
   566         -    iBasis = iInReplyTo;
   567         -  }
   568         -  webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
   569         -  blob_init(&x, 0, 0);
   570         -  zDate = date_in_standard_format("now");
   571         -  blob_appendf(&x, "D %s\n", zDate);
   572         -  fossil_free(zDate);
   573         -  zG = db_text(0, 
   574         -     "SELECT uuid FROM blob, forumpost"
   575         -     " WHERE blob.rid==forumpost.froot"
   576         -     "   AND forumpost.fpid=%d", iBasis);
   577         -  if( zG ){
   578         -    blob_appendf(&x, "G %s\n", zG);
   579         -    fossil_free(zG);
   580         -  }
   581         -  if( zTitle ){
   582         -    blob_appendf(&x, "H %F\n", zTitle);
   583         -  }
   584         -  zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo);
   585         -  if( zI ){
   586         -    blob_appendf(&x, "I %s\n", zI);
   587         -    fossil_free(zI);
   588         -  }
   589         -  if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
   590         -    blob_appendf(&x, "N %s\n", zMimetype);
   591         -  }
   592         -  if( iEdit>0 ){
   593         -    char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
   594         -    if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
   595         -    blob_appendf(&x, "P %s\n", zP);
   596         -    fossil_free(zP);
   597         -  }
   598         -  if( zUser==0 ){
   599         -    if( login_is_nobody() ){
   600         -      zUser = "anonymous";
   601         -    }else{
   602         -      zUser = login_name();
   603         -    }
   604         -  }
   605         -  blob_appendf(&x, "U %F\n", zUser);
   606         -  blob_appendf(&x, "W %d\n%s\n", strlen(zContent), zContent);
   607         -  md5sum_blob(&x, &cksum);
   608         -  blob_appendf(&x, "Z %b\n", &cksum);
   609         -  blob_reset(&cksum);
   610         -
   611         -  /* Verify that the artifact we are creating is well-formed */
   612         -  blob_init(&formatCheck, 0, 0);
   613         -  blob_init(&errMsg, 0, 0);
   614         -  blob_copy(&formatCheck, &x);
   615         -  pPost = manifest_parse(&formatCheck, 0, &errMsg);
   616         -  if( pPost==0 ){
   617         -    webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
   618         -  }
   619         -  webpage_assert( pPost->type==CFTYPE_FORUM );
   620         -  manifest_destroy(pPost);
   621         -
   622         -  if( P("dryrun") ){
   623         -    @ <div class='debug'>
   624         -    @ This is the artifact that would have been generated:
   625         -    @ <pre>%h(blob_str(&x))</pre>
   626         -    @ </div>
   627         -    blob_reset(&x);
   628         -    return 0;
   629         -  }else{
   630         -    int nrid = wiki_put(&x, 0, forum_need_moderation());
   631         -    cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid));
   632         -    return 1;
   633         -  }
   634         -}
   635         -
   636         -/*
   637         -** Paint the form elements for entering a Forum post
   638         -*/
   639         -static void forum_entry_widget(
   640         -  const char *zTitle,
   641         -  const char *zMimetype,
   642         -  const char *zContent
   643         -){
   644         -  if( zTitle ){
   645         -    @ Title: <input type="input" name="title" value="%h(zTitle)" size="50"><br>
   646         -  }
   647         -  @ Markup style:
   648         -  mimetype_option_menu(zMimetype);
   649         -  @ <br><textarea name="content" class="wikiedit" cols="80" \
   650         -  @ rows="25" wrap="virtual">%h(zContent)</textarea><br>
   651         -}
   652         -
   653         -/*
   654         -** WEBPAGE: forumnew
   655         -** WEBPAGE: forumedit
   656         -**
   657         -** Start a new thread on the forum or reply to an existing thread.
   658         -** But first prompt to see if the user would like to log in.
   659         -*/
   660         -void forum_page_init(void){
   661         -  int isEdit;
   662         -  char *zGoto;
   663         -  login_check_credentials();
   664         -  if( !g.perm.WrForum ){
   665         -    login_needed(g.anon.WrForum);
   666         -    return;
   667         -  }
   668         -  if( sqlite3_strglob("*edit*", g.zPath)==0 ){
   669         -    zGoto = mprintf("%R/forume2?fpid=%S",PD("fpid",""));
   670         -    isEdit = 1;
   671         -  }else{
   672         -    zGoto = mprintf("%R/forume1");
   673         -    isEdit = 0;
   674         -  }
   675         -  if( login_is_individual() ){
   676         -    if( isEdit ){
   677         -      forumedit_page();
   678         -    }else{
   679         -      forumnew_page();
   680         -    }
   681         -    return;
   682         -  }
   683         -  style_header("%h As Anonymous?", isEdit ? "Reply" : "Post");
   684         -  @ <p>You are not logged in.
   685         -  @ <p><table border="0" cellpadding="10">
   686         -  @ <tr><td>
   687         -  @ <form action="%s(zGoto)" method="POST">
   688         -  @ <input type="submit" value="Remain Anonymous">
   689         -  @ </form>
   690         -  @ <td>Post to the forum anonymously
   691         -  if( login_self_register_available(0) ){
   692         -    @ <tr><td>
   693         -    @ <form action="%R/register" method="POST">
   694         -    @ <input type="hidden" name="g" value="%s(zGoto)">
   695         -    @ <input type="submit" value="Create An Account">
   696         -    @ </form>
   697         -    @ <td>Create a new account and post using that new account
   698         -  }
   699         -  @ <tr><td>
   700         -  @ <form action="%R/login" method="POST">
   701         -  @ <input type="hidden" name="g" value="%s(zGoto)">
   702         -  @ <input type="hidden" name="noanon" value="1">
   703         -  @ <input type="submit" value="Login">
   704         -  @ </form>
   705         -  @ <td>Log into an existing account
   706         -  @ </table>
   707         -  style_footer();
   708         -  fossil_free(zGoto);
   709         -}
   710         -
   711         -/*
   712         -** Write the "From: USER" line on the webpage.
   713         -*/
   714         -static void forum_from_line(void){
   715         -  if( login_is_nobody() ){
   716         -    @ From: anonymous<br>
   717         -  }else{
   718         -    @ From: %h(login_name())<br>
   719         -  }
   720         -}
   721         -
   722         -/*
   723         -** WEBPAGE: forume1
   724         -**
   725         -** Start a new forum thread.
   726         -*/
   727         -void forumnew_page(void){
   728         -  const char *zTitle = PDT("title","");
   729         -  const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
   730         -  const char *zContent = PDT("content","");
   731         -  login_check_credentials();
   732         -  if( !g.perm.WrForum ){
   733         -    login_needed(g.anon.WrForum);
   734         -    return;
   735         -  }
   736         -  if( P("submit") ){
   737         -    if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent) ) return;
   738         -  }
   739         -  if( P("preview") ){
   740         -    @ <h1>Preview:</h1>
   741         -    forum_render(zTitle, zMimetype, zContent, "forumEdit");
   742         -  }
   743         -  style_header("New Forum Thread");
   744         -  @ <form action="%R/forume1" method="POST">
   745         -  @ <h1>New Message:</h1>
   746         -  forum_from_line();
   747         -  forum_entry_widget(zTitle, zMimetype, zContent);
   748         -  @ <input type="submit" name="preview" value="Preview">
   749         -  if( P("preview") ){
   750         -    @ <input type="submit" name="submit" value="Submit">
   751         -  }else{
   752         -    @ <input type="submit" name="submit" value="Submit" disabled>
   753         -  }
   754         -  if( g.perm.Debug ){
   755         -    /* For the test-forumnew page add these extra debugging controls */
   756         -    @ <div class="debug">
   757         -    @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
   758         -    @ Dry run</label>
   759         -    @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
   760         -    @ Require moderator approval</label>
   761         -    @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
   762         -    @ Show query parameters</label>
   763         -    @ </div>
   764         -  }
   765         -  @ </form>
   766         -  style_footer();
   767         -}
   768         -
   769         -/*
   770         -** WEBPAGE: forume2
   771         -**
   772         -** Edit an existing forum message.
   773         -** Query parameters:
   774         -**
   775         -**   fpid=X        Hash of the post to be editted.  REQUIRED
   776         -*/
   777         -void forumedit_page(void){
   778         -  int fpid;
   779         -  Manifest *pPost = 0;
   780         -  const char *zMimetype = 0;
   781         -  const char *zContent = 0;
   782         -  const char *zTitle = 0;
   783         -  int isCsrfSafe;
   784         -  int isDelete = 0;
   785         -
   786         -  login_check_credentials();
   787         -  if( !g.perm.WrForum ){
   788         -    login_needed(g.anon.WrForum);
   789         -    return;
   790         -  }
   791         -  fpid = symbolic_name_to_rid(PD("fpid",""), "f");
   792         -  if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){
   793         -    webpage_error("Missing or invalid fpid query parameter");
   794         -  }
   795         -  if( P("cancel") ){
   796         -    cgi_redirectf("%R/forumpost/%S",P("fpid"));
   797         -    return;
   798         -  }
   799         -  isCsrfSafe = cgi_csrf_safe(1);
   800         -  if( g.perm.ModForum && isCsrfSafe ){
   801         -    if( P("approve") ){
   802         -      moderation_approve(fpid);
   803         -      cgi_redirectf("%R/forumpost/%S",P("fpid"));
   804         -      return;
   805         -    }
   806         -    if( P("reject") ){
   807         -      char *zParent = 
   808         -        db_text(0,
   809         -          "SELECT uuid FROM forumpost, blob"
   810         -          " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
   811         -          fpid
   812         -        );
   813         -      moderation_disapprove(fpid);
   814         -      if( zParent ){
   815         -        cgi_redirectf("%R/forumpost/%S",zParent);
   816         -      }else{
   817         -        cgi_redirectf("%R/forum");
   818         -      }
   819         -      return;
   820         -    }
   821         -  }
   822         -  isDelete = P("nullout")!=0;
   823         -  if( P("submit") && isCsrfSafe ){
   824         -    int done = 1;
   825         -    const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
   826         -    const char *zContent = PDT("content","");
   827         -    if( P("reply") ){
   828         -      done = forum_post(0, fpid, 0, 0, zMimetype, zContent);
   829         -    }else if( P("edit") || isDelete ){
   830         -      done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent);
   831         -    }else{
   832         -      webpage_error("Missing 'reply' query parameter");
   833         -    }
   834         -    if( done ) return;
   835         -  }
   836         -  if( isDelete ){
   837         -    zMimetype = "text/x-fossil-wiki";
   838         -    zContent = "";
   839         -    if( pPost->zThreadTitle ) zTitle = "";
   840         -    style_header("Delete %s", zTitle ? "Post" : "Reply");
   841         -    @ <h1>Original Post:</h1>
   842         -    forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
   843         -                 "forumEdit");
   844         -    @ <h1>Change Into:</h1>
   845         -    forum_render(zTitle, zMimetype, zContent,"forumEdit");
   846         -    @ <form action="%R/forume2" method="POST">
   847         -    @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
   848         -    @ <input type="hidden" name="nullout" value="1">
   849         -    @ <input type="hidden" name="mimetype" value="%h(zMimetype)">
   850         -    @ <input type="hidden" name="content" value="%h(zContent)">
   851         -    if( zTitle ){
   852         -      @ <input type="hidden" name="title" value="%h(zTitle)">
   853         -    }
   854         -  }else if( P("edit") ){
   855         -    /* Provide an edit to the fpid post */
   856         -    zMimetype = P("mimetype");
   857         -    zContent = PT("content");
   858         -    zTitle = P("title");
   859         -    if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki);
   860         -    if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype);
   861         -    if( zTitle==0 && pPost->zThreadTitle!=0 ){
   862         -      zTitle = fossil_strdup(pPost->zThreadTitle);
   863         -    }
   864         -    style_header("Edit %s", zTitle ? "Post" : "Reply");
   865         -    @ <h1>Original Post:</h1>
   866         -    forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
   867         -                 "forumEdit");
   868         -    if( P("preview") ){
   869         -      @ <h1>Preview of Edited Post:</h1>
   870         -      forum_render(zTitle, zMimetype, zContent,"forumEdit");
   871         -    }
   872         -    @ <h1>Revised Message:</h1>
   873         -    @ <form action="%R/forume2" method="POST">
   874         -    @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
   875         -    @ <input type="hidden" name="edit" value="1">
   876         -    forum_from_line();
   877         -    forum_entry_widget(zTitle, zMimetype, zContent);
   878         -  }else{
   879         -    /* Reply */
   880         -    zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
   881         -    zContent = PDT("content","");
   882         -    style_header("Reply");
   883         -    @ <h1>Replying To:</h1>
   884         -    forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit");
   885         -    if( P("preview") ){
   886         -      @ <h1>Preview:</h1>
   887         -      forum_render(0, zMimetype,zContent, "forumEdit");
   888         -    }
   889         -    @ <h1>Enter Reply:</h1>
   890         -    @ <form action="%R/forume2" method="POST">
   891         -    @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
   892         -    @ <input type="hidden" name="reply" value="1">
   893         -    forum_from_line();
   894         -    forum_entry_widget(0, zMimetype, zContent);
   895         -  }
   896         -  if( !isDelete ){
   897         -    @ <input type="submit" name="preview" value="Preview">
   898         -  }
   899         -  @ <input type="submit" name="cancel" value="Cancel">
   900         -  if( P("preview") || isDelete ){
   901         -    @ <input type="submit" name="submit" value="Submit">
   902         -  }
   903         -  if( g.perm.Debug ){
   904         -    /* For the test-forumnew page add these extra debugging controls */
   905         -    @ <div class="debug">
   906         -    @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
   907         -    @ Dry run</label>
   908         -    @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
   909         -    @ Require moderator approval</label>
   910         -    @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
   911         -    @ Show query parameters</label>
   912         -    @ </div>
   913         -  }
   914         -  @ </form>
   915         -  style_footer();
   916         -}
   917         -
   918         -/*
   919         -** WEBPAGE: forum
   920         -**
   921         -** The main page for the forum feature.  Show a list of recent forum
   922         -** threads.  Also show a search box at the top if search is enabled,
   923         -** and a button for creating a new thread, if enabled.
   924         -**
   925         -** Query parameters:
   926         -**
   927         -**    n=N             The number of threads to show on each page
   928         -**    x=X             Skip the first X threads
   929         -*/
   930         -void forum_main_page(void){
   931         -  Stmt q;
   932         -  int iLimit, iOfst, iCnt;
   933         -  int srchFlags;
   934         -  login_check_credentials();
   935         -  srchFlags = search_restrict(SRCH_FORUM);
   936         -  if( !g.perm.RdForum ){
   937         -    login_needed(g.anon.RdForum);
   938         -    return;
   939         -  }
   940         -  style_header("Forum");
   941         -  if( g.perm.WrForum ){
   942         -    style_submenu_element("New Message","%R/forumnew");
   943         -  }
   944         -  if( g.perm.ModForum && moderation_needed() ){
   945         -    style_submenu_element("Moderation Requests", "%R/modreq");
   946         -  }
   947         -  if( (srchFlags & SRCH_FORUM)!=0 ){
   948         -    if( search_screen(SRCH_FORUM, 0) ){
   949         -      style_submenu_element("Recent Threads","%R/forum");
   950         -      style_footer();
   951         -      return;
   952         -    }
   953         -  }
   954         -  iLimit = atoi(PD("n","25"));
   955         -  iOfst = atoi(PD("x","0"));
   956         -  iCnt = 0;
   957         -  if( db_table_exists("repository","forumpost") ){
   958         -    db_prepare(&q,
   959         -      "WITH thread(age,duration,cnt,root,last) AS ("
   960         -      "  SELECT"
   961         -      "    julianday('now') - max(fmtime),"
   962         -      "    max(fmtime) - min(fmtime),"
   963         -      "    sum(fprev IS NULL),"
   964         -      "    froot,"
   965         -      "    (SELECT fpid FROM forumpost AS y"
   966         -      "      WHERE y.froot=x.froot %s"
   967         -      "      ORDER BY y.fmtime DESC LIMIT 1)"
   968         -      "  FROM forumpost AS x"
   969         -      "  WHERE %s"
   970         -      "  GROUP BY froot"
   971         -      "  ORDER BY 1 LIMIT %d OFFSET %d"
   972         -      ")"
   973         -      "SELECT"
   974         -      "  thread.age,"                                         /* 0 */
   975         -      "  thread.duration,"                                    /* 1 */
   976         -      "  thread.cnt,"                                         /* 2 */
   977         -      "  blob.uuid,"                                          /* 3 */
   978         -      "  substr(event.comment,instr(event.comment,':')+1),"   /* 4 */
   979         -      "  thread.last"                                         /* 5 */
   980         -      " FROM thread, blob, event"
   981         -      " WHERE blob.rid=thread.last"
   982         -      "  AND event.objid=thread.last"
   983         -      " ORDER BY 1;",
   984         -      g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/,
   985         -      g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/,
   986         -      iLimit+1, iOfst
   987         -    );
   988         -    while( db_step(&q)==SQLITE_ROW ){
   989         -      char *zAge = human_readable_age(db_column_double(&q,0));
   990         -      int nMsg = db_column_int(&q, 2);
   991         -      const char *zUuid = db_column_text(&q, 3);
   992         -      const char *zTitle = db_column_text(&q, 4);
   993         -      if( iCnt==0 ){
   994         -        if( iOfst>0 ){
   995         -          @ <h1>Threads at least %s(zAge) old</h1>
   996         -        }else{
   997         -          @ <h1>Most recent threads</h1>
   998         -        }
   999         -        @ <div class='forumPosts fileage'><table width="100%%">
  1000         -        if( iOfst>0 ){
  1001         -          if( iOfst>iLimit ){
  1002         -            @ <tr><td colspan="3">\
  1003         -            @ %z(href("%R/forum?x=%d&n=%d",iOfst-iLimit,iLimit))\
  1004         -            @ &uarr; Newer...</a></td></tr>
  1005         -          }else{
  1006         -            @ <tr><td colspan="3">%z(href("%R/forum?n=%d",iLimit))\
  1007         -            @ &uarr; Newer...</a></td></tr>
  1008         -          }
  1009         -        }
  1010         -      }
  1011         -      iCnt++;
  1012         -      if( iCnt>iLimit ){
  1013         -        @ <tr><td colspan="3">\
  1014         -        @ %z(href("%R/forum?x=%d&n=%d",iOfst+iLimit,iLimit))\
  1015         -        @ &darr; Older...</a></td></tr>
  1016         -        fossil_free(zAge);
  1017         -        break;
  1018         -      }
  1019         -      @ <tr><td>%h(zAge) ago</td>
  1020         -      @ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a></td>
  1021         -      @ <td>\
  1022         -      if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){
  1023         -        @ <span class="modpending">\
  1024         -        @ Awaiting Moderator Approval</span><br>
  1025         -      }
  1026         -      if( nMsg<2 ){
  1027         -        @ no replies</td>
  1028         -      }else{
  1029         -        char *zDuration = human_readable_age(db_column_double(&q,1));
  1030         -        @ %d(nMsg) posts spanning %h(zDuration)</td>
  1031         -        fossil_free(zDuration);
  1032         -      }
  1033         -      @ </tr>
  1034         -      fossil_free(zAge);
  1035         -    }
  1036         -    db_finalize(&q);
  1037         -  }
  1038         -  if( iCnt>0 ){
  1039         -    @ </table></div>
  1040         -  }else{
  1041         -    @ <h1>No forum posts found</h1>
  1042         -  }
  1043         -  style_footer();
  1044         -}

Deleted src/forum.js.

     1         -(function(){
     2         -  function absoluteY(obj){
     3         -    var top = 0;
     4         -    if( obj.offsetParent ){
     5         -      do{
     6         -        top += obj.offsetTop;
     7         -      }while( obj = obj.offsetParent );
     8         -    }
     9         -    return top;
    10         -  }
    11         -  var x = document.getElementsByClassName('forumSel');
    12         -  if(x[0]){
    13         -    var w = window.innerHeight;
    14         -    var h = x[0].scrollHeight;
    15         -    var y = absoluteY(x[0]);
    16         -    if( w>h ) y = y + (h-w)/2;
    17         -    if( y>0 ) window.scrollTo(0, y);
    18         -  }
    19         -}())

Changes to src/graph.c.

   532    532         }
   533    533       }
   534    534       mask = BIT(pRow->iRail);
   535    535       pRow->railInUse |= mask;
   536    536       if( pRow->pChild ){
   537    537         assignChildrenToRail(pRow);
   538    538       }else if( !omitDescenders && count_nonbranch_children(pRow->rid)!=0 ){
   539         -      if( !pRow->timeWarp ) riser_to_top(pRow);
          539  +      riser_to_top(pRow);
   540    540       }
   541    541       if( pParent ){
   542    542         for(pLoop=pParent->pPrev; pLoop && pLoop!=pRow; pLoop=pLoop->pPrev){
   543    543           pLoop->railInUse |= mask;
   544    544         }
   545    545       }
   546    546     }

Changes to src/graph.js.

   105    105       line = elems.line;
   106    106       mArrow = elems.arrow_merge_r;
   107    107       mLine = elems.line_merge;
   108    108       wArrow = elems.arrow_warp;
   109    109       wLine = elems.line_warp;
   110    110     
   111    111       var minRailPitch = Math.ceil((node.w+line.w)/2 + mArrow.w + 1);
   112         -    if( window.innerWidth<400 ){
   113         -      railPitch = minRailPitch;
          112  +    if( tx.iRailPitch>0 ){
          113  +      railPitch = tx.iRailPitch;
   114    114       }else{
   115         -      if( tx.iRailPitch>0 ){
   116         -        railPitch = tx.iRailPitch;
   117         -      }else{
   118         -        railPitch = elems.rail.w;
   119         -        railPitch -= Math.floor((tx.nrail-1)*(railPitch-minRailPitch)/21);
   120         -      }
   121         -      railPitch = Math.max(railPitch, minRailPitch);
          115  +      railPitch = elems.rail.w;
          116  +      railPitch -= Math.floor((tx.nrail-1)*(railPitch-minRailPitch)/21);
   122    117       }
          118  +    railPitch = Math.max(railPitch, minRailPitch);
   123    119     
   124    120       if( tx.nomo ){
   125    121         mergeOffset = 0;
   126    122       }else{
   127    123         mergeOffset = railPitch-minRailPitch-mLine.w;
   128    124         mergeOffset = Math.min(mergeOffset, elems.mergeoffset.w);
   129    125         mergeOffset = mergeOffset>0 ? mergeOffset + line.w/2 : 0;
................................................................................
   378    374     for(i=0; i<lx.length; i++){
   379    375       if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
   380    376     }
   381    377     lx = topObj.getElementsByClassName('timelineCompactComment');
   382    378     for(i=0; i<lx.length; i++){
   383    379       if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
   384    380     }
   385         -  if( window.innerWidth<400 ){
   386         -    /* On narrow displays, shift the date from the first column to the
   387         -    ** third column, to make the first column narrower */
   388         -    lx = topObj.getElementsByClassName('timelineDateRow');
   389         -    for(i=0; i<lx.length; i++){
   390         -      var rx = lx[i];
   391         -      if( rx.getAttribute('data-reordered') ) break;
   392         -      rx.setAttribute('data-reordered',1);
   393         -      rx.appendChild(rx.firstChild);
   394         -      rx.insertBefore(rx.childNodes[1],rx.firstChild);
   395         -    }
   396         -  }
   397    381   }
   398    382     
   399    383   /* Look for all timeline-data-NN objects.  Load each one and draw
   400    384   ** a graph for each one.
   401    385   */
   402    386   (function(){
   403    387     var i;

Changes to src/http_socket.c.

    22     22   ** at a time.  State information is stored in static variables.  The identity
    23     23   ** of the server is held in global variables that are set by url_parse().
    24     24   **
    25     25   ** Low-level sockets are abstracted out into this module because they
    26     26   ** are handled different on Unix and windows.
    27     27   */
    28     28   #if defined(_WIN32)
    29         -# if defined(_WIN32_WINNT)
    30         -#  undef _WIN32_WINNT
    31         -# endif
    32     29   # define _WIN32_WINNT 0x501
    33     30   #endif
    34     31   #ifndef __EXTENSIONS__
    35     32   # define __EXTENSIONS__ 1  /* IPv6 won't compile on Solaris without this */
    36     33   #endif
    37     34   #include "config.h"
    38     35   #include "http_socket.h"
................................................................................
    80     77     socketErrMsg = vmprintf(zFormat, ap);
    81     78     va_end(ap);
    82     79   }
    83     80   
    84     81   /*
    85     82   ** Return the current socket error message
    86     83   */
    87         -char *socket_errmsg(void){
    88         -  char *zResult = socketErrMsg;
    89         -  socketErrMsg = 0;
    90         -  return zResult;
           84  +const char *socket_errmsg(void){
           85  +  return socketErrMsg;
    91     86   }
    92     87   
    93     88   /*
    94     89   ** Call this routine once before any other use of the socket interface.
    95     90   ** This routine does initial configuration of the socket module.
    96     91   */
    97     92   void socket_global_init(void){
................................................................................
   135    130     }
   136    131   }
   137    132   
   138    133   /*
   139    134   ** Open a socket connection.  The identify of the server is determined
   140    135   ** by pUrlData
   141    136   **
   142         -**    pUrlData->name       Name of the server.  Ex: www.fossil-scm.org
   143         -**    pUrlData->port       TCP/IP port to use.  Ex: 80
          137  +**    pUrlDAta->name       Name of the server.  Ex: www.fossil-scm.org
          138  +**    pUrlDAta->port       TCP/IP port to use.  Ex: 80
   144    139   **
   145    140   ** Return the number of errors.
   146    141   */
   147    142   int socket_open(UrlData *pUrlData){
   148    143     int rc = 0;
   149    144     struct addrinfo *ai = 0;
   150    145     struct addrinfo *p;
................................................................................
   193    188     if( ai ) freeaddrinfo(ai);
   194    189     return rc;
   195    190   }
   196    191   
   197    192   /*
   198    193   ** Send content out over the open socket connection.
   199    194   */
   200         -size_t socket_send(void *NotUsed, const void *pContent, size_t N){
          195  +size_t socket_send(void *NotUsed, void *pContent, size_t N){
   201    196     size_t sent;
   202    197     size_t total = 0;
   203    198     while( N>0 ){
   204    199       sent = send(iSocket, pContent, N, 0);
   205    200       if( sent<=0 ) break;
   206    201       total += sent;
   207    202       N -= sent;
................................................................................
   208    203       pContent = (void*)&((char*)pContent)[sent];
   209    204     }
   210    205     return total;
   211    206   }
   212    207   
   213    208   /*
   214    209   ** Receive content back from the open socket connection.
   215         -** Return the number of bytes read.
   216         -**
   217         -** When bDontBlock is false, this function blocks until all N bytes
   218         -** have been read.
   219    210   */
   220         -size_t socket_receive(void *NotUsed, void *pContent, size_t N, int bDontBlock){
          211  +size_t socket_receive(void *NotUsed, void *pContent, size_t N){
   221    212     ssize_t got;
   222    213     size_t total = 0;
   223         -  int flags = 0;
   224         -#ifdef MSG_DONTWAIT
   225         -  if( bDontBlock ) flags |= MSG_DONTWAIT;
   226         -#endif
   227    214     while( N>0 ){
   228    215       /* WinXP fails for large values of N.  So limit it to 64KiB. */
   229         -    got = recv(iSocket, pContent, N>65536 ? 65536 : N, flags);
          216  +    got = recv(iSocket, pContent, N>65536 ? 65536 : N, 0);
   230    217       if( got<=0 ) break;
   231    218       total += (size_t)got;
   232    219       N -= (size_t)got;
   233    220       pContent = (void*)&((char*)pContent)[got];
   234    221     }
   235    222     return total;
   236    223   }

Changes to src/http_ssl.c.

   109    109       if( zCaSetting==0 || zCaSetting[0]=='\0' ){
   110    110         /* CA location not specified, use platform's default certificate store */
   111    111         X509_STORE_set_default_paths(SSL_CTX_get_cert_store(sslCtx));
   112    112       }else{
   113    113         /* User has specified a CA location, make sure it exists and use it */
   114    114         switch( file_isdir(zCaSetting, ExtFILE) ){
   115    115           case 0: { /* doesn't exist */
   116         -          fossil_panic("ssl-ca-location is set to '%s', "
          116  +          fossil_fatal("ssl-ca-location is set to '%s', "
   117    117                 "but is not a file or directory", zCaSetting);
   118    118             break;
   119    119           }
   120    120           case 1: { /* directory */
   121    121             zCaDirectory = zCaSetting;
   122    122             break;
   123    123           }
   124    124           case 2: { /* file */
   125    125             zCaFile = zCaSetting;
   126    126             break;
   127    127           }
   128    128         }
   129    129         if( SSL_CTX_load_verify_locations(sslCtx, zCaFile, zCaDirectory)==0 ){
   130         -        fossil_panic("Failed to use CA root certificates from "
          130  +        fossil_fatal("Failed to use CA root certificates from "
   131    131             "ssl-ca-location '%s'", zCaSetting);
   132    132         }
   133    133       }
   134    134   
   135    135       /* Load client SSL identity, preferring the filename specified on the
   136    136       ** command line */
   137    137       if( g.zSSLIdentity!=0 ){
................................................................................
   139    139       }else{
   140    140         identityFile = db_get("ssl-identity", 0);
   141    141       }
   142    142       if( identityFile!=0 && identityFile[0]!='\0' ){
   143    143         if( SSL_CTX_use_certificate_file(sslCtx,identityFile,SSL_FILETYPE_PEM)!=1
   144    144          || SSL_CTX_use_PrivateKey_file(sslCtx,identityFile,SSL_FILETYPE_PEM)!=1
   145    145         ){
   146         -        fossil_panic("Could not load SSL identity from %s", identityFile);
          146  +        fossil_fatal("Could not load SSL identity from %s", identityFile);
   147    147         }
   148    148       }
   149    149       /* Register a callback to tell the user what to do when the server asks
   150    150       ** for a cert */
   151    151       SSL_CTX_set_client_cert_cb(sslCtx, ssl_client_cert_callback);
   152    152   
   153    153       sslIsInit = 1;

Changes to src/http_transport.c.

   125    125       zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name);
   126    126       blob_append_escaped_arg(&zCmd, zHost);
   127    127       fossil_free(zHost);
   128    128     }else{
   129    129       blob_append_escaped_arg(&zCmd, pUrlData->name);
   130    130     }
   131    131     if( !is_safe_fossil_command(pUrlData->fossil) ){
   132         -    fossil_panic("the ssh:// URL is asking to run an unsafe command [%s] on "
          132  +    fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
   133    133                    "the server.", pUrlData->fossil);
   134    134     }
   135    135     blob_append_escaped_arg(&zCmd, pUrlData->fossil);
   136    136     blob_append(&zCmd, " test-http", 10);
   137    137     if( pUrlData->path && pUrlData->path[0] ){
   138    138       blob_append_escaped_arg(&zCmd, pUrlData->path);
   139    139     }else{
   140         -    fossil_panic("ssh:// URI does not specify a path to the repository");
          140  +    fossil_fatal("ssh:// URI does not specify a path to the repository");
   141    141     }
   142    142     if( g.fSshTrace ){
   143    143       fossil_print("%s\n", blob_str(&zCmd));  /* Show the whole SSH command */
   144    144     }
   145    145     popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid);
   146    146     if( sshPid==0 ){
   147    147       socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd);
................................................................................
   179    179         sqlite3_randomness(sizeof(iRandId), &iRandId);
   180    180         transport.zOutFile = mprintf("%s-%llu-out.http",
   181    181                                          g.zRepositoryName, iRandId);
   182    182         transport.zInFile = mprintf("%s-%llu-in.http",
   183    183                                          g.zRepositoryName, iRandId);
   184    184         transport.pFile = fossil_fopen(transport.zOutFile, "wb");
   185    185         if( transport.pFile==0 ){
   186         -        fossil_panic("cannot output temporary file: %s", transport.zOutFile);
          186  +        fossil_fatal("cannot output temporary file: %s", transport.zOutFile);
   187    187         }
   188    188         transport.isOpen = 1;
   189    189       }else{
   190    190         rc = socket_open(pUrlData);
   191    191         if( rc==0 ) transport.isOpen = 1;
   192    192       }
   193    193     }
................................................................................
   267    267   ** This routine is called when the outbound message is complete and
   268    268   ** it is time to being receiving a reply.
   269    269   */
   270    270   void transport_flip(UrlData *pUrlData){
   271    271     if( pUrlData->isFile ){
   272    272       char *zCmd;
   273    273       fclose(transport.pFile);
   274         -    zCmd = mprintf("\"%s\" http --in \"%s\" --out \"%s\" --ipaddr 127.0.0.1"
   275         -                   " \"%s\" --localauth",
          274  +    zCmd = mprintf("\"%s\" http \"%s\" \"%s\" 127.0.0.1 \"%s\" --localauth",
   276    275          g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
   277    276       );
   278    277       fossil_system(zCmd);
   279    278       free(zCmd);
   280    279       transport.pFile = fossil_fopen(transport.zInFile, "rb");
   281    280     }
   282    281   }
................................................................................
   324    323       got = ssl_receive(0, zBuf, N);
   325    324       #else
   326    325       got = 0;
   327    326       #endif
   328    327     }else if( pUrlData->isFile ){
   329    328       got = fread(zBuf, 1, N, transport.pFile);
   330    329     }else{
   331         -    got = socket_receive(0, zBuf, N, 0);
          330  +    got = socket_receive(0, zBuf, N);
   332    331     }
   333    332     /* printf("received %d of %d bytes\n", got, N); fflush(stdout); */
   334    333     if( transport.pLog ){
   335    334       fwrite(zBuf, 1, got, transport.pLog);
   336    335       fflush(transport.pLog);
   337    336     }
   338    337     return got;

Changes to src/import.c.

   604    604         fossil_free(gg.aData); gg.aData = 0;
   605    605         gg.nData = atoi(&zLine[5]);
   606    606         if( gg.nData ){
   607    607           int got;
   608    608           gg.aData = fossil_malloc( gg.nData+1 );
   609    609           got = fread(gg.aData, 1, gg.nData, pIn);
   610    610           if( got!=gg.nData ){
   611         -          fossil_panic("short read: got %d of %d bytes", got, gg.nData);
          611  +          fossil_fatal("short read: got %d of %d bytes", got, gg.nData);
   612    612           }
   613    613           gg.aData[got] = '\0';
   614    614           if( gg.zComment==0 &&
   615    615               (gg.xFinish==finish_commit || gg.xFinish==finish_tag) ){
   616    616   	  /* Strip trailing newline, it's appended to the comment. */
   617    617   	  if( gg.aData[got-1] == '\n' )
   618    618   	    gg.aData[got-1] = '\0';

Changes to src/info.c.

   933    933     @ <div class="section">Overview</div>
   934    934     @ <p><table class="label-value">
   935    935     @ <tr><th>Artifact&nbsp;ID:</th>
   936    936     @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
   937    937     if( g.perm.Setup ){
   938    938       @ (%d(rid))
   939    939     }
   940         -  modPending = moderation_pending_www(rid);
          940  +  modPending = moderation_pending(rid);
          941  +  if( modPending ){
          942  +    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
          943  +  }
   941    944     @ </td></tr>
   942    945     @ <tr><th>Page&nbsp;Name:</th><td>%h(pWiki->zWikiTitle)</td></tr>
   943    946     @ <tr><th>Date:</th><td>
   944    947     hyperlink_to_date(zDate, "</td></tr>");
   945    948     @ <tr><th>Original&nbsp;User:</th><td>
   946    949     hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
   947    950     if( pWiki->zMimetype ){
................................................................................
   975    978     @ <div class="section">Content</div>
   976    979     blob_init(&wiki, pWiki->zWiki, -1);
   977    980     wiki_render_by_mimetype(&wiki, pWiki->zMimetype);
   978    981     blob_reset(&wiki);
   979    982     manifest_destroy(pWiki);
   980    983     style_footer();
   981    984   }
          985  +
          986  +/*
          987  +** Show a webpage error message
          988  +*/
          989  +void webpage_error(const char *zFormat, ...){
          990  +  va_list ap;
          991  +  const char *z;
          992  +  va_start(ap, zFormat);
          993  +  z = vmprintf(zFormat, ap);
          994  +  va_end(ap);
          995  +  style_header("URL Error");
          996  +  @ <h1>Error</h1>
          997  +  @ <p>%h(z)</p>
          998  +  style_footer();
          999  +}
   982   1000   
   983   1001   /*
   984   1002   ** Find an check-in based on query parameter zParam and parse its
   985   1003   ** manifest.  Return the number of errors.
   986   1004   */
   987   1005   static Manifest *vdiff_parse_manifest(const char *zParam, int *pRid){
   988   1006     int rid;
................................................................................
  1226   1244   #define OBJTYPE_WIKI       0x0004
  1227   1245   #define OBJTYPE_TICKET     0x0008
  1228   1246   #define OBJTYPE_ATTACHMENT 0x0010
  1229   1247   #define OBJTYPE_EVENT      0x0020
  1230   1248   #define OBJTYPE_TAG        0x0040
  1231   1249   #define OBJTYPE_SYMLINK    0x0080
  1232   1250   #define OBJTYPE_EXE        0x0100
  1233         -#define OBJTYPE_FORUM      0x0200
  1234   1251   
  1235   1252   /*
  1236   1253   ** Possible flags for the second parameter to
  1237   1254   ** object_description()
  1238   1255   */
  1239   1256   #define OBJDESC_DETAIL      0x0001   /* more detail */
  1240   1257   #endif
................................................................................
  1419   1436           if( eventTagId != 0) {
  1420   1437             @ Instance of technote
  1421   1438             objType |= OBJTYPE_EVENT;
  1422   1439             hyperlink_to_event_tagid(db_column_int(&q, 5));
  1423   1440           }else{
  1424   1441             @ Attachment to technote
  1425   1442           }
  1426         -      }else if( zType[0]=='f' ){
  1427         -        objType |= OBJTYPE_FORUM;
  1428         -        @ Forum post
  1429   1443         }else{
  1430   1444           @ Tag referencing
  1431   1445         }
  1432   1446         if( zType[0]!='e' || eventTagId == 0){
  1433   1447           hyperlink_to_uuid(zUuid);
  1434   1448         }
  1435   1449         @ - %!W(zCom) by
................................................................................
  1891   1905         if( iEnd<iStart ) iEnd = iStart;
  1892   1906         db_multi_exec(
  1893   1907           "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
  1894   1908         );
  1895   1909         iStart = iEnd = atoi(&zLn[i++]);
  1896   1910       }while( zLn[i] && iStart && iEnd );
  1897   1911     }
  1898         -  db_prepare(&q, "SELECT min(iStart), max(iEnd) FROM lnos");
         1912  +  db_prepare(&q, "SELECT min(iStart), iEnd FROM lnos");
  1899   1913     if( db_step(&q)==SQLITE_ROW ){
  1900   1914       iStart = db_column_int(&q, 0);
  1901   1915       iEnd = db_column_int(&q, 1);
  1902   1916       iTop = iStart - 15 + (iEnd-iStart)/4;
  1903   1917       if( iTop>iStart - 2 ) iTop = iStart-2;
  1904   1918     }
  1905   1919     db_finalize(&q);
................................................................................
  2232   2246     @ <div class="section">Overview</div>
  2233   2247     @ <p><table class="label-value">
  2234   2248     @ <tr><th>Artifact&nbsp;ID:</th>
  2235   2249     @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
  2236   2250     if( g.perm.Setup ){
  2237   2251       @ (%d(rid))
  2238   2252     }
  2239         -  modPending = moderation_pending_www(rid);
         2253  +  modPending = moderation_pending(rid);
         2254  +  if( modPending ){
         2255  +    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
         2256  +  }
  2240   2257     @ <tr><th>Ticket:</th>
  2241   2258     @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a>
  2242   2259     if( zTktTitle ){
  2243   2260           @<br />%h(zTktTitle)
  2244   2261     }
  2245   2262     @</td></tr>
  2246   2263     @ <tr><th>User&nbsp;&amp;&nbsp;Date:</th><td>
................................................................................
  2346   2363     }else
  2347   2364     if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){
  2348   2365       ci_page();
  2349   2366     }else
  2350   2367     if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){
  2351   2368       ainfo_page();
  2352   2369     }else
  2353         -  if( db_table_exists("repository","forumpost")
  2354         -   && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid)
  2355         -  ){
  2356         -    forumthread_page();
  2357         -  }else
  2358   2370     {
  2359   2371       artifact_page();
  2360   2372     }
  2361   2373   }
  2362   2374   
  2363   2375   /*
  2364   2376   ** Do a comment comparison.

Changes to src/json.c.

   994    994         break;
   995    995       }
   996    996       inFile = (0==strcmp("-",jfile))
   997    997         ? stdin
   998    998         : fossil_fopen(jfile,"rb");
   999    999       if(!inFile){
  1000   1000         g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED;
  1001         -      fossil_panic("Could not open JSON file [%s].",jfile)
         1001  +      fossil_fatal("Could not open JSON file [%s].",jfile)
  1002   1002           /* Does not return. */
  1003   1003           ;
  1004   1004       }
  1005   1005       cgi_parse_POST_JSON(inFile, 0);
  1006   1006       if( stdin != inFile ){
  1007   1007         fclose(inFile);
  1008   1008       }
................................................................................
  1888   1888     ADD(WrTkt,"editTicket");
  1889   1889     ADD(ModTkt,"moderateTicket");
  1890   1890     ADD(Attach,"attachFile");
  1891   1891     ADD(TktFmt,"createTicketReport");
  1892   1892     ADD(RdAddr,"readPrivate");
  1893   1893     ADD(Zip,"zip");
  1894   1894     ADD(Private,"xferPrivate");
  1895         -  ADD(WrUnver,"writeUnversioned");
  1896         -  ADD(RdForum,"readForum");
  1897         -  ADD(WrForum,"writeForum");
  1898         -  ADD(WrTForum,"writeTrustedForum");
  1899         -  ADD(ModForum,"moderateForum");
  1900         -  ADD(AdminForum,"adminForum");
  1901         -  ADD(EmailAlert,"emailAlert");
  1902         -  ADD(Announce,"announce");
  1903         -  ADD(Debug,"debug");
  1904   1895   #undef ADD
  1905   1896     return payload;
  1906   1897   }
  1907   1898   
  1908   1899   /*
  1909   1900   ** Implementation of the /json/stat page/command.
  1910   1901   **

Changes to src/json_branch.c.

    66     66     cson_value * payV;
    67     67     cson_object * pay;
    68     68     cson_value * listV;
    69     69     cson_array * list;
    70     70     char const * range = NULL;
    71     71     int branchListFlags = BRL_OPEN_ONLY;
    72     72     char * sawConversionError = NULL;
    73         -  Stmt q = empty_Stmt;
           73  +  Stmt q;
    74     74     if( !g.perm.Read ){
    75     75       json_set_err(FSL_JSON_E_DENIED,
    76     76                    "Requires 'o' permissions.");
    77     77       return NULL;
    78     78     }
    79     79     payV = cson_value_new_object();
    80     80     pay = cson_value_get_object(payV);
................................................................................
   141    141                                      __FILE__,__LINE__);
   142    142       }
   143    143     }
   144    144     if( sawConversionError ){
   145    145       json_warn(FSL_JSON_W_COL_TO_JSON_FAILED,"%s",sawConversionError);
   146    146       free(sawConversionError);
   147    147     }
   148         -  db_finalize(&q);
   149    148     return payV;
   150    149   }
   151    150   
   152    151   /*
   153    152   ** Parameters for the create-branch operation.
   154    153   */
   155    154   typedef struct BranchCreateOptions{
................................................................................
   287    286   
   288    287     blob_appendf(&branch, "U %F\n", g.zLogin);
   289    288     md5sum_blob(&branch, &mcksum);
   290    289     blob_appendf(&branch, "Z %b\n", &mcksum);
   291    290   
   292    291     brid = content_put(&branch);
   293    292     if( brid==0 ){
   294         -    fossil_panic("Problem committing manifest: %s", g.zErrMsg);
          293  +    fossil_fatal("Problem committing manifest: %s", g.zErrMsg);
   295    294     }
   296    295     db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
   297    296     if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
   298         -    fossil_panic("%s", g.zErrMsg);
          297  +    fossil_fatal("%s", g.zErrMsg);
   299    298     }
   300    299     assert( blob_is_reset(&branch) );
   301    300     content_deltify(rootid, &brid, 1, 0);
   302    301     if( zNewRid ){
   303    302       *zNewRid = brid;
   304    303     }
   305    304   

Changes to src/json_login.c.

   122    122       }
   123    123     }
   124    124   
   125    125   #if 0
   126    126     {
   127    127       /* only for debugging the PD()-incorrect-result problem */
   128    128       cson_object * o = NULL;
   129         -    uid = login_search_uid( &name, pw );
          129  +    uid = login_search_uid( name, pw );
   130    130       payload = cson_value_new_object();
   131    131       o = cson_value_get_object(payload);
   132    132       cson_object_set( o, "n", cson_value_new_string(name,strlen(name)));
   133    133       cson_object_set( o, "p", cson_value_new_string(pw,strlen(pw)));
   134    134       return payload;
   135    135     }
   136    136   #endif
   137    137     uid = anonSeed
   138    138       ? login_is_valid_anonymous(name, pw, anonSeed)
   139         -    : login_search_uid(&name, pw)
          139  +    : login_search_uid(name, pw)
   140    140       ;
   141    141     if( !uid ){
   142    142       g.json.resultCode = preciseErrors
   143    143         ? FSL_JSON_E_LOGIN_FAILED_NOTFOUND
   144    144         : FSL_JSON_E_LOGIN_FAILED;
   145    145       return NULL;
   146    146     }else{

Changes to src/json_status.c.

   166    166         case -4:  zLabel = "INTEGRATE  ";  break;
   167    167       }
   168    168       blob_append(report, zPrefix, nPrefix);
   169    169       blob_appendf(report, "%s %s\n", zLabel, db_column_text(&q, 0));
   170    170     }
   171    171     db_finalize(&q);
   172    172     if( nErr ){
   173         -    fossil_panic("aborting due to prior errors");
          173  +    fossil_fatal("aborting due to prior errors");
   174    174     }
   175    175   #endif
   176    176     return cson_object_value( oPay );
   177    177   }
   178    178   
   179    179   #endif /* FOSSIL_ENABLE_JSON */

Changes to src/json_timeline.c.

    33     33   static const JsonPageDef JsonPageDefs_Timeline[] = {
    34     34   /* the short forms are only enabled in CLI mode, to avoid
    35     35      that we end up with HTTP clients using 3 different names
    36     36      for the same requests.
    37     37   */
    38     38   {"branch", json_timeline_branch, 0},
    39     39   {"checkin", json_timeline_ci, 0},
    40         -{"event", json_timeline_event, 0},
    41     40   {"ticket", json_timeline_ticket, 0},
    42     41   {"wiki", json_timeline_wiki, 0},
    43     42   /* Last entry MUST have a NULL name. */
    44     43   {NULL,NULL,0}
    45     44   };
    46     45   
    47     46   
................................................................................
   317    316   ** or 0 for defaults.
   318    317   */
   319    318   cson_value * json_get_changed_files(int rid, int flags){
   320    319     cson_value * rowsV = NULL;
   321    320     cson_array * rows = NULL;
   322    321     Stmt q = empty_Stmt;
   323    322     db_prepare(&q,
   324         -         "SELECT (pid<=0) AS isnew,"
   325         -         "       (fid==0) AS isdel,"
   326         -         "       (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
   327         -         "       (SELECT uuid FROM blob WHERE rid=fid) as uuid,"
   328         -         "       (SELECT uuid FROM blob WHERE rid=pid) as parent,"
   329         -         "       blob.size as size"
   330         -         "  FROM mlink"
   331         -         " LEFT JOIN blob ON blob.rid=fid"
   332         -         " WHERE mid=%d AND pid!=fid"
   333         -         " AND NOT mlink.isaux"
   334         -         " ORDER BY name /*sort*/",
   335         -         rid
   336         -         );
          323  +           "SELECT (pid==0) AS isnew,"
          324  +           "       (fid==0) AS isdel,"
          325  +           "       (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
          326  +           "       blob.uuid as uuid,"
          327  +           "       (SELECT uuid FROM blob WHERE rid=pid) as parent,"
          328  +           "       blob.size as size"
          329  +           "  FROM mlink, blob"
          330  +           " WHERE mid=%d AND pid!=fid"
          331  +           " AND blob.rid=fid AND NOT mlink.isaux"
          332  +           " ORDER BY name /*sort*/",
          333  +             rid
          334  +             );
   337    335     while( (SQLITE_ROW == db_step(&q)) ){
   338    336       cson_value * rowV = cson_value_new_object();
   339    337       cson_object * row = cson_value_get_object(rowV);
   340    338       int const isNew = db_column_int(&q,0);
   341    339       int const isDel = db_column_int(&q,1);
   342    340       char * zDownload = NULL;
   343    341       if(!rowsV){
................................................................................
   519    517     cson_value_free(payV);
   520    518     payV = NULL;
   521    519     ok:
   522    520     db_finalize(&q);
   523    521     return payV;
   524    522   }
   525    523   
   526         -/*
   527         -** Implementation of /json/timeline/event.
   528         -**
   529         -*/
   530         -cson_value * json_timeline_event(){
   531         -  /* This code is 95% the same as json_timeline_ci(), by the way. */
   532         -  cson_value * payV = NULL;
   533         -  cson_object * pay = NULL;
   534         -  cson_array * list = NULL;
   535         -  int check = 0;
   536         -  Stmt q = empty_Stmt;
   537         -  Blob sql = empty_blob;
   538         -  if( !g.perm.RdWiki ){
   539         -    json_set_err( FSL_JSON_E_DENIED, "Event timeline requires 'j' access.");
   540         -    return NULL;
   541         -  }
   542         -  payV = cson_value_new_object();
   543         -  pay = cson_value_get_object(payV);
   544         -  check = json_timeline_setup_sql( "e", &sql, pay );
   545         -  if(check){
   546         -    json_set_err(check, "Query initialization failed.");
   547         -    goto error;
   548         -  }
   549         -
   550         -#if 0
   551         -  /* only for testing! */
   552         -  cson_object_set(pay, "timelineSql", cson_value_new_string(blob_buffer(&sql),strlen(blob_buffer(&sql))));
   553         -#endif
   554         -  db_multi_exec("%s", blob_buffer(&sql) /*safe-for-%s*/);
   555         -  blob_reset(&sql);
   556         -  db_prepare(&q, "SELECT"
   557         -             /* For events, the name is generally more useful than
   558         -                the uuid, but the uuid is unambiguous and can be used
   559         -                with commands like 'artifact'. */
   560         -             " substr((SELECT tagname FROM tag AS tn WHERE tn.tagid=json_timeline.tagId AND tagname LIKE 'event-%%'),7) AS name,"
   561         -             " uuid as uuid,"
   562         -             " mtime AS timestamp,"
   563         -             " comment AS comment, "
   564         -             " user AS user,"
   565         -             " eventType AS eventType"
   566         -             " FROM json_timeline"
   567         -             " ORDER BY rowid");
   568         -  list = cson_new_array();
   569         -  json_stmt_to_array_of_obj(&q, list);
   570         -  cson_object_set(pay, "timeline", cson_array_value(list));
   571         -  goto ok;
   572         -  error:
   573         -  assert( 0 != g.json.resultCode );
   574         -  cson_value_free(payV);
   575         -  payV = NULL;
   576         -  ok:
   577         -  db_finalize(&q);
   578         -  blob_reset(&sql);
   579         -  return payV;
   580         -}
   581         -
   582    524   /*
   583    525   ** Implementation of /json/timeline/wiki.
   584    526   **
   585    527   */
   586    528   cson_value * json_timeline_wiki(){
   587    529     /* This code is 95% the same as json_timeline_ci(), by the way. */
   588    530     cson_value * payV = NULL;

Changes to src/login.c.

   204    204   }
   205    205   
   206    206   /*
   207    207   ** Searches for the user ID matching the given name and password.
   208    208   ** On success it returns a positive value. On error it returns 0.
   209    209   ** On serious (DB-level) error it will probably exit.
   210    210   **
   211         -** zUsername uses double indirection because we may re-point *zUsername
   212         -** at a C string allocated with fossil_strdup() if you pass an email
   213         -** address instead and we find that address in the user table's info
   214         -** field, which is expected to contain a string of the form "Human Name
   215         -** <human@example.com>".  In that case, *zUsername will point to that
   216         -** user's actual login name on return, causing a leak unless the caller
   217         -** is diligent enough to check whether its pointer was re-pointed.
   218         -**
   219    211   ** zPassword may be either the plain-text form or the encrypted
   220    212   ** form of the user's password.
   221    213   */
   222         -int login_search_uid(const char **pzUsername, const char *zPasswd){
   223         -  char *zSha1Pw = sha1_shared_secret(zPasswd, *pzUsername, 0);
   224         -  int uid = db_int(0,
   225         -    "SELECT uid FROM user"
   226         -    " WHERE login=%Q"
   227         -    "   AND length(cap)>0 AND length(pw)>0"
   228         -    "   AND login NOT IN ('anonymous','nobody','developer','reader')"
   229         -    "   AND (pw=%Q OR (length(pw)<>40 AND pw=%Q))"
   230         -    "   AND (info NOT LIKE '%%expires 20%%'"
   231         -    "      OR substr(info,instr(lower(info),'expires')+8,10)>datetime('now'))",
   232         -    *pzUsername, zSha1Pw, zPasswd
   233         -  );
   234         -
   235         -  /* If we did not find a login on the first attempt, and the username
   236         -  ** looks like an email address, then perhaps the user entered their
   237         -  ** email address instead of their login.  Try again to match the user
   238         -  ** against email addresses contained in the "info" field.
   239         -  */
   240         -  if( uid==0 && strchr(*pzUsername,'@')!=0 ){
   241         -    Stmt q;
   242         -    db_prepare(&q,
   243         -      "SELECT login FROM user"
   244         -      " WHERE find_emailaddr(info)=%Q"
   245         -      "   AND instr(login,'@')==0",
   246         -      *pzUsername
   247         -    );
   248         -    while( db_step(&q)==SQLITE_ROW ){
   249         -      const char *zLogin = db_column_text(&q,0);
   250         -      if( (uid = login_search_uid(&zLogin, zPasswd) ) != 0 ){
   251         -        *pzUsername = fossil_strdup(zLogin);
   252         -        break;
   253         -      }
   254         -    }
   255         -    db_finalize(&q);
   256         -  }    
          214  +int login_search_uid(const char *zUsername, const char *zPasswd){
          215  +  char *zSha1Pw = sha1_shared_secret(zPasswd, zUsername, 0);
          216  +  int const uid =
          217  +      db_int(0,
          218  +             "SELECT uid FROM user"
          219  +             " WHERE login=%Q"
          220  +             "   AND length(cap)>0 AND length(pw)>0"
          221  +             "   AND login NOT IN ('anonymous','nobody','developer','reader')"
          222  +             "   AND (pw=%Q OR (length(pw)<>40 AND pw=%Q))"
          223  +             "   AND (info NOT LIKE '%%expires 20%%'"
          224  +             "      OR substr(info,instr(lower(info),'expires')+8,10)>datetime('now'))",
          225  +             zUsername, zSha1Pw, zPasswd
          226  +             );
   257    227     free(zSha1Pw);
   258    228     return uid;
   259    229   }
   260    230   
   261    231   /*
   262    232   ** Generates a login cookie value for a non-anonymous user.
   263    233   **
................................................................................
   499    469     if( zReferer==0 ) return 0;
   500    470     zPattern = mprintf("%s/login*", g.zBaseURL);
   501    471     rc = sqlite3_strglob(zPattern, zReferer)==0;
   502    472     fossil_free(zPattern);
   503    473     return rc;
   504    474   }
   505    475   
   506         -/*
   507         -** Return TRUE if self-registration is available.  If the zNeeded
   508         -** argument is not NULL, then only return true if self-registration is
   509         -** available and any of the capabilities named in zNeeded are available
   510         -** to self-registered users.
   511         -*/
   512         -int login_self_register_available(const char *zNeeded){
   513         -  CapabilityString *pCap;
   514         -  int rc;
   515         -  if( !db_get_boolean("self-register",0) ) return 0;
   516         -  if( zNeeded==0 ) return 1;
   517         -  pCap = capability_add(0, db_get("default-perms",""));
   518         -  capability_expand(pCap);
   519         -  rc = capability_has_any(pCap, zNeeded);
   520         -  capability_free(pCap);
   521         -  return rc;
   522         -}
   523         -
   524    476   /*
   525    477   ** There used to be a page named "my" that was designed to show information
   526    478   ** about a specific user.  The "my" page was linked from the "Logged in as USER"
   527    479   ** line on the title bar.  The "my" page was never completed so it is now
   528    480   ** removed.  Use this page as a placeholder in older installations.
   529    481   **
   530    482   ** WEBPAGE: login
................................................................................
   544    496     const char *zGoto = P("g");
   545    497     int anonFlag;                /* Login as "anonymous" would be useful */
   546    498     char *zErrMsg = "";
   547    499     int uid;                     /* User id logged in user */
   548    500     char *zSha1Pw;
   549    501     const char *zIpAddr;         /* IP address of requestor */
   550    502     const char *zReferer;
   551         -  int noAnon = P("noanon")!=0;
   552    503   
   553    504     login_check_credentials();
   554    505     if( login_wants_https_redirect() ){
   555    506       const char *zQS = P("QUERY_STRING");
   556    507       if( P("redir")!=0 ){
   557    508         style_header("Insecure Connection");
   558    509         @ <h1>Unable To Establish An Encrypted Connection</h1>
................................................................................
   568    519         return;
   569    520       }
   570    521       if( zQS==0 ){
   571    522         zQS = "?redir=1";
   572    523       }else if( zQS[0]!=0 ){
   573    524         zQS = mprintf("?%s&redir=1", zQS);
   574    525       }
   575         -    cgi_redirectf("%s%T%s", g.zHttpsURL, P("PATH_INFO"), zQS);
          526  +    cgi_redirectf("%s%s%s", g.zHttpsURL, P("PATH_INFO"), zQS);
   576    527       return;
   577    528     }
   578    529     sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
   579    530                     constant_time_cmp_function, 0, 0);
   580    531     zUsername = P("u");
   581    532     zPasswd = P("p");
   582    533     anonFlag = g.zLogin==0 && PB("anon");
................................................................................
   584    535     /* Handle log-out requests */
   585    536     if( P("out") ){
   586    537       login_clear_login_data();
   587    538       redirect_to_g();
   588    539       return;
   589    540     }
   590    541   
   591         -  /* Redirect for create-new-account requests */
   592         -  if( P("self") ){
   593         -    cgi_redirectf("%R/register");
   594         -    return;
   595         -  }
   596         -
   597    542     /* Deal with password-change requests */
   598    543     if( g.perm.Password && zPasswd
   599    544      && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
   600    545     ){
   601    546       /* If there is not a "real" login, we cannot change any password. */
   602    547       if( g.zLogin ){
   603    548         /* The user requests a password change */
................................................................................
   660    605       login_set_anon_cookie(zIpAddr, NULL);
   661    606       record_login_attempt("anonymous", zIpAddr, 1);
   662    607       redirect_to_g();
   663    608     }
   664    609     if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
   665    610       /* Attempting to log in as a user other than anonymous.
   666    611       */
   667         -    uid = login_search_uid(&zUsername, zPasswd);
          612  +    uid = login_search_uid(zUsername, zPasswd);
   668    613       if( uid<=0 ){
   669    614         sleep(1);
   670    615         zErrMsg =
   671    616            @ <p><span class="loginError">
   672    617            @ You entered an unknown user or an incorrect password.
   673    618            @ </span></p>
   674    619         ;
................................................................................
   684    629         login_set_user_cookie(zUsername, uid, NULL);
   685    630         redirect_to_g();
   686    631       }
   687    632     }
   688    633     style_header("Login/Logout");
   689    634     style_adunit_config(ADUNIT_OFF);
   690    635     @ %s(zErrMsg)
   691         -  if( zGoto && !noAnon ){
          636  +  if( zGoto ){
   692    637       char *zAbbrev = fossil_strdup(zGoto);
   693    638       int i;
   694    639       for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
   695    640       zAbbrev[i] = 0;
   696    641       if( g.zLogin ){
   697    642         @ <p>Use a different login with greater privilege than <b>%h(g.zLogin)</b>
   698    643         @ to access <b>%h(zAbbrev)</b>.
................................................................................
   718    663     }
   719    664     if( anonFlag ){
   720    665       @ <input type="hidden" name="anon" value="1" />
   721    666     }
   722    667     if( g.zLogin ){
   723    668       @ <p>Currently logged in as <b>%h(g.zLogin)</b>.
   724    669       @ <input ty