Fossil

Check-in [f59c9ecb]
Login

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

Overview
Comment:Merge updates from trunk.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | cmake-ide
Files: files | file ages | folders
SHA3-256:f59c9ecb5eb25a149dbf07fc872f4c1b974425e2b835ea29f4c3d0e9fdf3a5a6
User & Date: ashepilko 2018-09-14 19:13:10
Context
2018-09-14
21:15
Describe the debugging options. check-in: d364933a user: ashepilko tags: cmake-ide
19:13
Merge updates from trunk. check-in: f59c9ecb user: ashepilko tags: cmake-ide
12:53
Update the change log. check-in: d78f1f3c user: drh tags: trunk
2018-08-08
01:27
Fix the MSVC specific link flags. check-in: 27c4e7b0 user: ashepilko tags: cmake-ide
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

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     43   BCCFLAGS =	@CPPFLAGS@ $(CFLAGS)
    44     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@

Changes to auto.def.

    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. Can't yet use jimsh for this.
           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!
    39     49   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  +}
    40     75   
    41     76   define EXTRA_CFLAGS "-Wall"
    42     77   define EXTRA_LDFLAGS ""
    43     78   define USE_SYSTEM_SQLITE 0
    44     79   define USE_LINENOISE 0
    45     80   define FOSSIL_ENABLE_MINIZ 0
    46     81   define USE_MMAN_H 0

Changes to skins/ardoise/header.txt.

    39     39   }
    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  +}
           47  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           48  +  menulink /forum Forum
    46     49   }
    47     50   if {[hascap r]} {
    48     51     menulink /ticket Tickets
    49     52   }
    50     53   if {[hascap j]} {
    51     54     menulink /wiki Wiki
    52     55   }

Changes to skins/black_and_white/header.txt.

    20     20   }
    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  +}
           28  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           29  +  html "<a href='$home/forum'>Forum</a>\n"
    27     30   }
    28     31   if {[anoncap r]} {
    29     32     html "<a href='$home/ticket'>Tickets</a>\n"
    30     33   }
    31     34   if {[anoncap j]} {
    32     35     html "<a href='$home/wiki'>Wiki</a>\n"
    33     36   }

Changes to skins/blitz/header.txt.

    43     43   }
    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  +}
           51  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           52  +  menulink /forum Forum
    50     53   }
    51     54   if {[hascap r]} {
    52     55     menulink /ticket Tickets
    53     56   }
    54     57   if {[hascap j]} {
    55     58     menulink /wiki Wiki
    56     59   }

Changes to skins/blitz_no_logo/header.txt.

    40     40   }
    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  +}
           48  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           49  +  menulink /forum Forum
    47     50   }
    48     51   if {[hascap r]} {
    49     52     menulink /ticket Tickets
    50     53   }
    51     54   if {[hascap j]} {
    52     55     menulink /wiki Wiki
    53     56   }

Changes to skins/bootstrap/header.txt.

    75     75                       html "<li><a href='$home/brlist'>Branches</a></li>\n"
    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  +                }
           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  +                  }
    82     89                   }
    83     90                   if {[hascap r]} {
    84     91                     if {[string compare $current_page "reportlist"] == 0} {
    85     92                       html "<li class='active'><a href='$home/reportlist'>Tickets</a></li>\n"
    86     93                     } else {
    87     94                       html "<li><a href='$home/reportlist'>Tickets</a></li>\n"
    88     95                     }

Changes to skins/default/css.txt.

     1      1   body {
     2      2       margin: 0 auto;
     3         -    min-width: 800px;
     4         -    padding: 0px 20px;
     5      3       background-color: white;
     6      4       font-family: sans-serif;
     7      5       font-size:14pt;
     8      6       -moz-text-size-adjust: none;
     9      7       -webkit-text-size-adjust: none;
    10      8       -mx-text-size-adjust: none;
    11      9   }
................................................................................
    14     12       color: #4183C4;
    15     13       text-decoration: none;
    16     14   }
    17     15   a:hover {
    18     16       color: #4183C4;
    19     17       text-decoration: underline;
    20     18   }
           19  +div.forumPosts a:visited {
           20  +    color: #6A7F94;
           21  +}
    21     22   
    22     23   hr {
    23     24       color: #eee;
    24     25   }
    25     26   
    26     27   .title {
    27     28       color: #4183C4;
    28     29       float:left;
    29         -    padding-top: 30px;
    30         -    padding-bottom: 10px;
    31     30   }
    32     31   .title h1 {
    33     32       display:inline;
    34     33   }
    35     34   .title h1:after {
    36     35       content: " / ";
    37     36       color: #777;
................................................................................
    71     70       display: inline-block;
    72     71       margin-right: 1em;
    73     72   }
    74     73   
    75     74   .status {
    76     75       float:right;
    77     76       font-size:.7em;
    78         -    padding-top:50px;
    79     77   }
    80     78   
    81     79   .mainmenu {
    82     80       font-size:.8em;
    83     81       clear:both;
    84         -    padding:10px;
    85     82       background:#eaeaea linear-gradient(#fafafa, #eaeaea) repeat-x;
    86     83       border:1px solid #eaeaea;
    87     84       border-radius:5px;
           85  +    overflow-x: auto;
           86  +    overflow-y: hidden;
           87  +    white-space: nowrap;
           88  +    z-index: 21;  /* just above hbdrop */
    88     89   }
    89     90   
    90     91   .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  +
          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  +}
   101    115   
   102    116   .submenu {
   103    117       font-size: .7em;
   104         -    margin-top: 10px;
   105    118       padding: 10px;
   106    119       border-bottom: 1px solid #ccc;
   107    120   }
   108    121   
   109    122   .submenu a, .submenu label {
   110    123       padding: 10px 11px;
   111    124       text-decoration:none;
................................................................................
   197    210   }
   198    211   div.timelineDate {
   199    212       font-weight: bold;
   200    213       white-space: nowrap;
   201    214   }
   202    215   span.submenuctrl, span.submenuctrl input, select.submenuctrl {
   203    216     color: #777;
          217  +}
          218  +span.submenuctrl {
          219  +  white-space: nowrap;
          220  +}
          221  +div.submenu label {
          222  +  white-space: nowrap;
          223  +}
          224  +
          225  +@media screen and (max-width: 600px) {
          226  +  /* Spacing for mobile */
          227  +  body {
          228  +    padding-left: 4px;
          229  +    padding-right: 4px;
          230  +  }
          231  +  .title {
          232  +    padding-top: 0px;
          233  +    padding-bottom: 0px;
          234  +  }
          235  +  .status {padding-top: 0px;}
          236  +  .mainmenu a {
          237  +    padding: 10px 10px;
          238  +  }
          239  +  .mainmenu {
          240  +    padding: 10px;
          241  +  }
          242  +  .desktoponly {
          243  +    display: none;
          244  +  }
          245  +}
          246  +@media screen and (min-width: 600px) {
          247  +  /* Spacing for desktop */
          248  +  body {
          249  +    padding-left: 20px;
          250  +    padding-right: 20px;
          251  +  }
          252  +  .title {
          253  +    padding-top: 10px;
          254  +    padding-bottom: 10px;
          255  +  }
          256  +  .status {padding-top: 30px;}
          257  +  .mainmenu a {
          258  +    padding: 10px 20px;
          259  +  }
          260  +  .mainmenu {
          261  +    padding: 10px;
          262  +  }
          263  +}
          264  +@media screen and (max-width: 1200px) {
          265  +  /* Special declarations for narrow desktop or wide mobile */
          266  +  .wideonly {
          267  +    display: none;
          268  +  }
   204    269   }

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

Added 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         -<script>
           76  +<th1>html "<script nonce='$nonce'>"</th1>
    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} {
................................................................................
   104    104   }
   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  +}
          112  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
          113  +  menulink /forum Forum
   111    114   }
   112    115   if {[anoncap r]} {
   113    116     menulink /ticket Tickets
   114    117   }
   115    118   if {[anoncap j]} {
   116    119     menulink /wiki Wiki
   117    120   }

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         -<script>
           76  +<th1>html "<script nonce='$nonce'>"</th1>
    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} {
................................................................................
   104    104   }
   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  +}
          112  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
          113  +  menulink /forum Forum
   111    114   }
   112    115   if {[anoncap r]} {
   113    116     menulink /ticket Tickets
   114    117   }
   115    118   if {[anoncap j]} {
   116    119     menulink /wiki Wiki
   117    120   }

Changes to skins/khaki/header.txt.

    18     18   }
    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  +}
           26  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           27  +  html "<a href='$home/forum'>Forum</a>\n"
    25     28   }
    26     29   if {[anoncap r]} {
    27     30     html "<a href='$home/ticket'>Tickets</a>\n"
    28     31   }
    29     32   if {[anoncap j]} {
    30     33     html "<a href='$home/wiki'>Wiki</a>\n"
    31     34   }

Changes to skins/original/header.txt.

    19     19   }
    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  +}
           27  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           28  +  html "<a href='$home/forum'>Forum</a>\n"
    26     29   }
    27     30   if {[anoncap r]} {
    28     31     html "<a href='$home/ticket'>Tickets</a>\n"
    29     32   }
    30     33   if {[anoncap j]} {
    31     34     html "<a href='$home/wiki'>Wiki</a>\n"
    32     35   }

Changes to skins/plain_gray/details.txt.

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

Changes to skins/plain_gray/header.txt.

    16     16   }
    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  +}
           24  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           25  +  html "<a href='$home/forum'>Forum</a>\n"
    23     26   }
    24     27   if {[anoncap r]} {
    25     28     html "<a href='$home/ticket'>Tickets</a>\n"
    26     29   }
    27     30   if {[anoncap j]} {
    28     31     html "<a href='$home/wiki'>Wiki</a>\n"
    29     32   }

Changes to skins/rounded1/header.txt.

    20     20   }
    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  +}
           28  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
           29  +  html "<a href='$home/forum'>Forum</a>\n"
    27     30   }
    28     31   if {[anoncap r]} {
    29     32     html "<a href='$home/ticket'>Tickets</a>\n"
    30     33   }
    31     34   if {[anoncap j]} {
    32     35     html "<a href='$home/wiki'>Wiki</a>\n"
    33     36   }

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         -<script>
           76  +<th1>html "<script nonce='$nonce'>"</th1>
    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} {
................................................................................
   108    108   }
   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  +}
          116  +if {[anycap 23456] || [anoncap 2] || [anoncap 3]} {
          117  +  menulink /forum Forum
   115    118   }
   116    119   if {[anoncap r]} {
   117    120     menulink /ticket Tickets
   118    121   }
   119    122   if {[anoncap j]} {
   120    123     menulink /wiki Wiki
   121    124   }

Name change from src/email.c to src/alerts.c.

     1      1   /*
     2         -** Copyright (c) 2007 D. Richard Hipp
            2  +** Copyright (c) 2018 D. Richard Hipp
     3      3   **
     4      4   ** This program is free software; you can redistribute it and/or
     5      5   ** modify it under the terms of the Simplified BSD License (also
     6      6   ** known as the "2-Clause License" or "FreeBSD License".)
     7      7   **
     8      8   ** This program is distributed in the hope that it will be useful,
     9      9   ** but without any warranty; without even the implied warranty of
................................................................................
    12     12   ** Author contact information:
    13     13   **   drh@hwaci.com
    14     14   **   http://www.hwaci.com/drh/
    15     15   **
    16     16   *******************************************************************************
    17     17   **
    18     18   ** Logic for email notification, also known as "alerts".
    19         -*/
           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  +*/ 
    20     25   #include "config.h"
    21         -#include "email.h"
           26  +#include "alerts.h"
    22     27   #include <assert.h>
    23     28   #include <time.h>
    24     29   
    25     30   /*
    26     31   ** Maximum size of the subscriberCode blob, in bytes
    27     32   */
    28     33   #define SUBSCRIBER_CODE_SZ 32
    29     34   
    30     35   /*
    31     36   ** SQL code to implement the tables needed by the email notification
    32     37   ** system.
    33     38   */
    34         -static const char zEmailInit[] =
           39  +static const char zAlertInit[] =
    35     40   @ DROP TABLE IF EXISTS repository.subscriber;
    36     41   @ -- Subscribers are distinct from users.  A person can have a log-in in
    37     42   @ -- the USER table without being a subscriber.  Or a person can be a
    38     43   @ -- subscriber without having a USER table entry.  Or they can have both.
    39     44   @ -- In the last case the suname column points from the subscriber entry
    40     45   @ -- to the USER entry.
    41     46   @ --
................................................................................
    70     75   @ --
    71     76   @ -- The first character of the eventid determines the event type.
    72     77   @ -- Remaining characters determine the specific event.  For example,
    73     78   @ -- 'c4413' means check-in with rid=4413.
    74     79   @ --
    75     80   @ CREATE TABLE repository.pending_alert(
    76     81   @   eventid TEXT PRIMARY KEY,         -- Object that changed
    77         -@   sentSep BOOLEAN DEFAULT false,    -- individual emails sent
    78         -@   sentDigest BOOLEAN DEFAULT false  -- digest emails sent
           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
    79     85   @ ) WITHOUT ROWID;
    80     86   @ 
    81         -@ DROP TABLE IF EXISTS repository.email_bounce;
           87  +@ DROP TABLE IF EXISTS repository.alert_bounce;
    82     88   @ -- Record bounced emails.  If too many bounces are received within
    83     89   @ -- some defined time range, then cancel the subscription.  Older
    84     90   @ -- entries are periodically purged.
    85     91   @ --
    86         -@ CREATE TABLE repository.email_bounce(
           92  +@ CREATE TABLE repository.alert_bounce(
    87     93   @   subscriberId INTEGER, -- to whom the email was sent.
    88     94   @   sendTime INTEGER,     -- seconds since 1970 when email was sent
    89     95   @   rcvdTime INTEGER      -- seconds since 1970 when bounce was received
    90     96   @ );
    91     97   ;
    92     98   
    93     99   /*
    94    100   ** Return true if the email notification tables exist.
    95    101   */
    96         -int email_tables_exist(void){
          102  +int alert_tables_exist(void){
    97    103     return db_table_exists("repository", "subscriber");
    98    104   }
    99    105   
   100    106   /*
   101    107   ** Make sure the table needed for email notification exist in the repository.
   102    108   **
   103    109   ** If the bOnlyIfEnabled option is true, then tables are only created
   104    110   ** if the email-send-method is something other than "off".
   105    111   */
   106         -void email_schema(int bOnlyIfEnabled){
   107         -  if( !email_tables_exist() ){
          112  +void alert_schema(int bOnlyIfEnabled){
          113  +  if( !alert_tables_exist() ){
   108    114       if( bOnlyIfEnabled
   109    115        && fossil_strcmp(db_get("email-send-method","off"),"off")==0
   110    116       ){
   111    117         return;  /* Don't create table for disabled email */
   112    118       }
   113         -    db_multi_exec(zEmailInit/*works-like:""*/);
   114         -    email_triggers_enable();
          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  +    );
   115    126     }
   116    127   }
   117    128   
   118    129   /*
   119    130   ** Enable triggers that automatically populate the pending_alert
   120    131   ** table.
   121    132   */
   122         -void email_triggers_enable(void){
          133  +void alert_triggers_enable(void){
   123    134     if( !db_table_exists("repository","pending_alert") ) return;
   124    135     db_multi_exec(
   125         -    "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n"
          136  +    "CREATE TRIGGER IF NOT EXISTS repository.alert_trigger1\n"
   126    137       "AFTER INSERT ON event BEGIN\n"
   127    138       "  INSERT INTO pending_alert(eventid)\n"
   128    139       "    SELECT printf('%%.1c%%d',new.type,new.objid) WHERE true\n"
   129    140       "    ON CONFLICT(eventId) DO NOTHING;\n"
   130    141       "END;"
   131    142     );
   132    143   }
................................................................................
   133    144   
   134    145   /*
   135    146   ** Disable triggers the event_pending triggers.
   136    147   **
   137    148   ** This must be called before rebuilding the EVENT table, for example
   138    149   ** via the "fossil rebuild" command.
   139    150   */
   140         -void email_triggers_disable(void){
          151  +void alert_triggers_disable(void){
   141    152     db_multi_exec(
   142         -    "DROP TRIGGER IF EXISTS repository.email_trigger1;\n"
          153  +    "DROP TRIGGER IF EXISTS repository.alert_trigger1;\n"
          154  +    "DROP TRIGGER IF EXISTS repository.email_trigger1;\n" // Legacy
   143    155     );
   144    156   }
   145    157   
   146    158   /*
   147    159   ** Return true if email alerts are active.
   148    160   */
   149         -int email_enabled(void){
   150         -  if( !email_tables_exist() ) return 0;
          161  +int alert_enabled(void){
          162  +  if( !alert_tables_exist() ) return 0;
   151    163     if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
   152    164     return 1;
   153    165   }
   154    166   
   155    167   /*
   156    168   ** If the subscriber table does not exist, then paint an error message
   157    169   ** web page and return true.
   158    170   **
   159    171   ** If the subscriber table does exist, return 0 without doing anything.
   160    172   */
   161         -static int email_webpages_disabled(void){
   162         -  if( email_tables_exist() ) return 0;
          173  +static int alert_webpages_disabled(void){
          174  +  if( alert_tables_exist() ) return 0;
   163    175     style_header("Email Alerts Are Disabled");
   164    176     @ <p>Email alerts are disabled on this server</p>
   165    177     style_footer();
   166    178     return 1;
   167    179   }
   168    180   
   169    181   /*
   170    182   ** Insert a "Subscriber List" submenu link if the current user
   171    183   ** is an administrator.
   172    184   */
   173         -void email_submenu_common(void){
          185  +void alert_submenu_common(void){
   174    186     if( g.perm.Admin ){
   175    187       if( fossil_strcmp(g.zPath,"subscribers") ){
   176    188         style_submenu_element("List Subscribers","%R/subscribers");
   177    189       }
   178    190       if( fossil_strcmp(g.zPath,"subscribe") ){
   179    191         style_submenu_element("Add New Subscriber","%R/subscribe");
   180    192       }
................................................................................
   199    211     login_check_credentials();
   200    212     if( !g.perm.Setup ){
   201    213       login_needed(0);
   202    214       return;
   203    215     }
   204    216     db_begin_transaction();
   205    217   
   206         -  email_submenu_common();
          218  +  alert_submenu_common();
   207    219     style_submenu_element("Send Announcement","%R/announce");
   208    220     style_header("Email Notification Setup");
   209    221     @ <h1>Status</h1>
   210    222     @ <table class="label-value">
   211         -  if( email_enabled() ){
          223  +  if( alert_enabled() ){
   212    224       stats_for_email();
   213    225     }else{
   214    226       @ <th>Disabled</th>
   215    227     }
   216    228     @ </table>
   217    229     @ <hr>
   218    230     @ <h1> Configuration </h1>
................................................................................
   225    237     @ <p><b>Required.</b>
   226    238     @ This is URL used as the basename for hyperlinks included in
   227    239     @ email alert text.  Omit the trailing "/".
   228    240     @ Suggested value: "%h(g.zBaseURL)"
   229    241     @ (Property: "email-url")</p>
   230    242     @ <hr>
   231    243   
   232         -  entry_attribute("\"From\" email address", 20, "email-self",
          244  +  entry_attribute("\"Return-Path\" email address", 20, "email-self",
   233    245                      "eself", "", 0);
   234    246     @ <p><b>Required.</b>
   235         -  @ This is the email from which email notifications are sent.  The
   236         -  @ system administrator should arrange for emails sent to this address
          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
   237    252     @ to be handed off to the "fossil email incoming" command so that Fossil
   238    253     @ can handle bounces. (Property: "email-self")</p>
   239    254     @ <hr>
   240    255   
   241    256     entry_attribute("Repository Nickname", 16, "email-subname",
   242    257                      "enn", "", 0);
   243    258     @ <p><b>Required.</b>
................................................................................
   249    264   
   250    265     multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
   251    266          "off", count(azSendMethods)/2, azSendMethods);
   252    267     @ <p>How to send email.  Requires auxiliary information from the fields
   253    268     @ that follow.  Hint: Use the <a href="%R/announce">/announce</a> page
   254    269     @ to send test message to debug this setting.
   255    270     @ (Property: "email-send-method")</p>
   256         -  email_schema(1);
          271  +  alert_schema(1);
   257    272     entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
   258         -                   "ecmd", "sendmail -t", 0);
          273  +                   "ecmd", "sendmail -ti", 0);
   259    274     @ <p>When the send method is "pipe to a command", this is the command
   260    275     @ that is run.  Email messages are piped into the standard input of this
   261    276     @ command.  The command is expected to extract the sender address,
   262    277     @ recepient addresses, and subject from the header of the piped email
   263    278     @ text.  (Property: "email-send-command")</p>
   264    279   
   265    280     entry_attribute("Store Emails In This Database", 60, "email-send-db",
................................................................................
   287    302     entry_attribute("Administrator email address", 40, "email-admin",
   288    303                      "eadmin", "", 0);
   289    304     @ <p>This is the email for the human administrator for the system.
   290    305     @ Abuse and trouble reports are send here.
   291    306     @ (Property: "email-admin")</p>
   292    307     @ <hr>
   293    308   
   294         -  entry_attribute("Inbound email directory", 40, "email-receive-dir",
   295         -                   "erdir", "", 0);
   296         -  @ <p>Inbound emails can be stored in a directory for analysis as
   297         -  @ a debugging aid.  Put the name of that directory in this entry box.
   298         -  @ Disable saving of inbound email by making this an empty string.
   299         -  @ Abuse and trouble reports are send here.
   300         -  @ (Property: "email-receive-dir")</p>
   301         -  @ <hr>
   302    309     @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
   303    310     @ </div></form>
   304    311     db_end_transaction(0);
   305    312     style_footer();
   306    313   }
   307    314   
   308    315   #if 0
................................................................................
   364    371   # define pclose _pclose
   365    372   #endif
   366    373   
   367    374   #if INTERFACE
   368    375   /*
   369    376   ** An instance of the following object is used to send emails.
   370    377   */
   371         -struct EmailSender {
          378  +struct AlertSender {
   372    379     sqlite3 *db;               /* Database emails are sent to */
   373    380     sqlite3_stmt *pStmt;       /* Stmt to insert into the database */
   374    381     const char *zDest;         /* How to send email. */
   375    382     const char *zDb;           /* Name of database file */
   376    383     const char *zDir;          /* Directory in which to store as email files */
   377    384     const char *zCmd;          /* Command to run for each email */
   378    385     const char *zFrom;         /* Emails come from here */
................................................................................
   379    386     SmtpSession *pSmtp;        /* SMTP relay connection */
   380    387     Blob out;                  /* For zDest=="blob" */
   381    388     char *zErr;                /* Error message */
   382    389     u32 mFlags;                /* Flags */
   383    390     int bImmediateFail;        /* On any error, call fossil_fatal() */
   384    391   };
   385    392   
   386         -/* Allowed values for mFlags to email_sender_new().
          393  +/* Allowed values for mFlags to alert_sender_new().
   387    394   */
   388         -#define EMAIL_IMMEDIATE_FAIL   0x0001   /* Call fossil_fatal() on any error */
   389         -#define EMAIL_TRACE            0x0002   /* Log sending process on console */
          395  +#define ALERT_IMMEDIATE_FAIL   0x0001   /* Call fossil_fatal() on any error */
          396  +#define ALERT_TRACE            0x0002   /* Log sending process on console */
   390    397   
   391    398   #endif /* INTERFACE */
   392    399   
   393    400   /*
   394    401   ** Shutdown an emailer.  Clear all information other than the error message.
   395    402   */
   396         -static void emailerShutdown(EmailSender *p){
          403  +static void emailerShutdown(AlertSender *p){
   397    404     sqlite3_finalize(p->pStmt);
   398    405     p->pStmt = 0;
   399    406     sqlite3_close(p->db);
   400    407     p->db = 0;
   401    408     p->zDb = 0;
   402    409     p->zDir = 0;
   403    410     p->zCmd = 0;
................................................................................
   406    413       smtp_session_free(p->pSmtp);
   407    414       p->pSmtp = 0;
   408    415     }
   409    416     blob_reset(&p->out);
   410    417   }
   411    418   
   412    419   /*
   413         -** Put the EmailSender into an error state.
          420  +** Put the AlertSender into an error state.
   414    421   */
   415         -static void emailerError(EmailSender *p, const char *zFormat, ...){
          422  +static void emailerError(AlertSender *p, const char *zFormat, ...){
   416    423     va_list ap;
   417    424     fossil_free(p->zErr);
   418    425     va_start(ap, zFormat);
   419    426     p->zErr = vmprintf(zFormat, ap);
   420    427     va_end(ap);
   421    428     emailerShutdown(p);
   422         -  if( p->mFlags & EMAIL_IMMEDIATE_FAIL ){
          429  +  if( p->mFlags & ALERT_IMMEDIATE_FAIL ){
   423    430       fossil_fatal("%s", p->zErr);
   424    431     }
   425    432   }
   426    433   
   427    434   /*
   428    435   ** Free an email sender object
   429    436   */
   430         -void email_sender_free(EmailSender *p){
          437  +void alert_sender_free(AlertSender *p){
   431    438     if( p ){
   432    439       emailerShutdown(p);
   433    440       fossil_free(p->zErr);
   434    441       fossil_free(p);
   435    442     }
   436    443   }
   437    444   
   438    445   /*
   439    446   ** Get an email setting value.  Report an error if not configured.
   440    447   ** Return 0 on success and one if there is an error.
   441    448   */
   442    449   static int emailerGetSetting(
   443         -  EmailSender *p,        /* Where to report the error */
          450  +  AlertSender *p,        /* Where to report the error */
   444    451     const char **pzVal,    /* Write the setting value here */
   445    452     const char *zName      /* Name of the setting */
   446    453   ){
   447    454     const char *z = db_get(zName, 0);
   448    455     int rc = 0;
   449    456     if( z==0 || z[0]==0 ){
   450    457       emailerError(p, "missing \"%s\" setting", zName);
................................................................................
   452    459     }else{
   453    460       *pzVal = z;
   454    461     }
   455    462     return rc;
   456    463   }
   457    464   
   458    465   /*
   459         -** Create a new EmailSender object.
          466  +** Create a new AlertSender object.
   460    467   **
   461    468   ** The method used for sending email is determined by various email-*
   462    469   ** settings, and especially email-send-method.  The repository
   463    470   ** email-send-method can be overridden by the zAltDest argument to
   464    471   ** cause a different sending mechanism to be used.  Pass "stdout" to
   465    472   ** zAltDest to cause all emails to be printed to the console for
   466    473   ** debugging purposes.
   467    474   **
   468         -** The EmailSender object returned must be freed using email_sender_free().
          475  +** The AlertSender object returned must be freed using alert_sender_free().
   469    476   */
   470         -EmailSender *email_sender_new(const char *zAltDest, u32 mFlags){
   471         -  EmailSender *p;
          477  +AlertSender *alert_sender_new(const char *zAltDest, u32 mFlags){
          478  +  AlertSender *p;
   472    479   
   473    480     p = fossil_malloc(sizeof(*p));
   474    481     memset(p, 0, sizeof(*p));
   475    482     blob_init(&p->out, 0, 0);
   476    483     p->mFlags = mFlags;
   477    484     if( zAltDest ){
   478    485       p->zDest = zAltDest;
................................................................................
   513    520     }else if( fossil_strcmp(p->zDest, "blob")==0 ){
   514    521       blob_init(&p->out, 0, 0);
   515    522     }else if( fossil_strcmp(p->zDest, "relay")==0 ){
   516    523       const char *zRelay = 0;
   517    524       emailerGetSetting(p, &zRelay, "email-send-relayhost");
   518    525       if( zRelay ){
   519    526         u32 smtpFlags = SMTP_DIRECT;
   520         -      if( mFlags & EMAIL_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
          527  +      if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
   521    528         p->pSmtp = smtp_session_new(p->zFrom, zRelay, smtpFlags);
   522    529         smtp_client_startup(p->pSmtp);
   523    530       }
   524    531     }
   525    532     return p;
   526    533   }
   527    534   
................................................................................
   566    573       }
   567    574     }
   568    575     return 0;
   569    576   }
   570    577   
   571    578   /*
   572    579   ** Make a copy of the input string up to but not including the
   573         -** first ">" character.
          580  +** first cTerm character.
   574    581   **
   575    582   ** Verify that the string really that is to be copied really is a
   576    583   ** valid email address.  If it is not, then return NULL.
   577    584   **
   578    585   ** This routine is more restrictive than necessary.  It does not
   579    586   ** allow comments, IP address, quoted strings, or certain uncommon
   580    587   ** characters.  The only non-alphanumerics allowed in the local
   581    588   ** part are "_", "+", "-" and "+".
   582    589   */
   583         -char *email_copy_addr(const char *z){
          590  +char *email_copy_addr(const char *z, char cTerm ){
   584    591     int i;
   585    592     int nAt = 0;
   586    593     int nDot = 0;
   587    594     char c;
   588    595     if( z[0]=='.' ) return 0;  /* Local part cannot begin with "." */
   589         -  for(i=0; (c = z[i])!=0 && c!='>'; i++){
          596  +  for(i=0; (c = z[i])!=0 && c!=cTerm; i++){
   590    597       if( fossil_isalnum(c) ){
   591    598         /* Alphanumerics are always ok */
   592    599       }else if( c=='@' ){
   593    600         if( nAt ) return 0;   /* Only a single "@"  allowed */
   594    601         if( i>64 ) return 0;  /* Local part too big */
   595    602         nAt = 1;
   596    603         nDot = 0;
   597    604         if( i==0 ) return 0;  /* Disallow empty local part */
   598    605         if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */
   599    606         if( z[i+1]=='.' || z[i+1]=='-' ){
   600    607           return 0; /* Domain cannot begin with "." or "-" */
   601    608         }
   602    609       }else if( c=='-' ){
   603         -      if( z[i+1]=='>' ) return 0;  /* Last character cannot be "-" */
          610  +      if( z[i+1]==cTerm ) return 0;  /* Last character cannot be "-" */
   604    611       }else if( c=='.' ){
   605    612         if( z[i+1]=='.' ) return 0;  /* Do not allow ".." */
   606         -      if( z[i+1]=='>' ) return 0;  /* Domain may not end with . */
          613  +      if( z[i+1]==cTerm ) return 0;  /* Domain may not end with . */
   607    614         nDot++;
   608    615       }else if( (c=='_' || c=='+') && nAt==0 ){
   609    616         /* _ and + are ok in the local part */
   610    617       }else{
   611    618         return 0;   /* Anything else is an error */
   612    619       }
   613    620     }
   614         -  if( c!='>' ) return 0;      /* Missing final ">" */
          621  +  if( c!=cTerm ) return 0;    /* Missing terminator */
   615    622     if( nAt==0 ) return 0;      /* No "@" found anywhere */
   616    623     if( nDot==0 ) return 0;     /* No "." in the domain */
   617    624   
   618    625     /* If we reach this point, the email address is valid */
   619    626     return mprintf("%.*s", i, z);
   620    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  +}
   621    717   
   622    718   /*
   623    719   ** Extract all To: header values from the email header supplied.
   624    720   ** Store them in the array list.
   625    721   */
   626    722   void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){
   627    723     int nTo = 0;
................................................................................
   629    725     Blob v;
   630    726     char *z, *zAddr;
   631    727     int i;
   632    728     
   633    729     email_header_value(pMsg, "to", &v);
   634    730     z = blob_str(&v);
   635    731     for(i=0; z[i]; i++){
   636         -    if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){
          732  +    if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){
   637    733         azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) );
   638    734         azTo[nTo++] = zAddr;
   639    735       }
   640    736     }
   641    737     *pnTo = nTo;
   642    738     *pazTo = azTo;
   643    739   }
................................................................................
   663    759   ** This routine will add fields to the header as follows:
   664    760   **
   665    761   **     From:
   666    762   **     Date:
   667    763   **     Message-Id:
   668    764   **     Content-Type:
   669    765   **     Content-Transfer-Encoding:
          766  +**     MIME-Version:
          767  +**     X-Fossil-From:
   670    768   **     
   671    769   ** The caller maintains ownership of the input Blobs.  This routine will
   672    770   ** read the Blobs and send them onward to the email system, but it will
   673    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.
   674    784   */
   675         -void email_send(EmailSender *p, Blob *pHdr, Blob *pBody){
          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  +){
   676    791     Blob all, *pOut;
   677    792     u64 r1, r2;
   678         -  if( p->mFlags & EMAIL_TRACE ){
          793  +  if( p->mFlags & ALERT_TRACE ){
   679    794       fossil_print("Sending email\n");
   680    795     }
   681    796     if( fossil_strcmp(p->zDest, "off")==0 ){
   682    797       return;
   683    798     }
   684    799     if( fossil_strcmp(p->zDest, "blob")==0 ){
   685    800       pOut = &p->out;
................................................................................
   687    802         blob_appendf(pOut, "%.72c\n", '=');
   688    803       }
   689    804     }else{
   690    805       blob_init(&all, 0, 0);
   691    806       pOut = &all;
   692    807     }
   693    808     blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
   694         -  blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
          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  +  }
   695    816     blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
   696         -  /* Message-id format:  "<$(date)x$(random).$(from)>" where $(date) is
   697         -  ** the current unix-time in hex, $(random) is a 64-bit random number,
   698         -  ** and $(from) is the sender. */
   699         -  sqlite3_randomness(sizeof(r1), &r1);
   700         -  r2 = time(0);
   701         -  blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom);
          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  +  }
   702    826     blob_add_final_newline(pBody);
   703         -  blob_appendf(pOut,"Content-Type: text/plain\r\n");
          827  +  blob_appendf(pOut, "MIME-Version: 1.0\r\n");
          828  +  blob_appendf(pOut, "Content-Type: text/plain; charset=\"UTF-8\"\r\n");
   704    829   #if 0
   705    830     blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n");
   706    831     append_base64(pOut, pBody);
   707    832   #else
   708    833     blob_appendf(pOut, "Content-Transfer-Encoding: quoted-printable\r\n\r\n");
   709    834     append_quoted(pOut, pBody);
   710    835   #endif
................................................................................
   750    875       email_header_to_free(nTo, azTo);
   751    876       blob_add_final_newline(&all);
   752    877       fossil_print("%s", blob_str(&all));
   753    878     }
   754    879     blob_reset(&all);
   755    880   }
   756    881   
   757         -/*
   758         -** Analyze and act on a received email.
   759         -**
   760         -** This routine takes ownership of the Blob parameter and is responsible
   761         -** for freeing that blob when it is done with it.
   762         -**
   763         -** This routine acts on all email messages received from the
   764         -** "fossil email inbound" command.
   765         -*/
   766         -void email_receive(Blob *pMsg){
   767         -  /* To Do:  Look for bounce messages and possibly disable subscriptions */
   768         -  blob_reset(pMsg);
   769         -}
   770         -
   771    882   /*
   772    883   ** SETTING: email-send-method         width=5 default=off
   773    884   ** Determine the method used to send email.  Allowed values are
   774    885   ** "off", "relay", "pipe", "dir", "db", and "stdout".  The "off" value
   775    886   ** means no email is ever sent.  The "relay" value means emails are sent
   776    887   ** to an Mail Sending Agent using SMTP located at email-send-relayhost.
   777    888   ** The "pipe" value means email messages are piped into a command 
................................................................................
   799    910   ** if the email-send-method is set to "db".
   800    911   */
   801    912   /*
   802    913   ** SETTING: email-self               width=40
   803    914   ** This is the email address for the repository.  Outbound emails add
   804    915   ** this email address as the "From:" field.
   805    916   */
   806         -/*
   807         -** SETTING: email-receive-dir         width=40
   808         -** Inbound email messages are saved as separate files in this directory,
   809         -** for debugging analysis.  Disable saving of inbound emails omitting
   810         -** this setting, or making it an empty string.
   811         -*/
   812    917   /*
   813    918   ** SETTING: email-send-relayhost      width=40
   814    919   ** This is the hostname and TCP port to which output email messages
   815    920   ** are sent when email-send-method is "relay".  There should be an
   816    921   ** SMTP server configured as a Mail Submission Agent listening on the
   817    922   ** designated host and port and all times.
   818    923   */
   819    924   
   820    925   
   821    926   /*
   822         -** COMMAND: email
          927  +** COMMAND: alerts
   823    928   ** 
   824         -** Usage: %fossil email SUBCOMMAND ARGS...
          929  +** Usage: %fossil alerts SUBCOMMAND ARGS...
   825    930   **
   826    931   ** Subcommands:
   827    932   **
   828         -**    exec                    Compose and send pending email alerts.
          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.
   829    940   **                            Some installations may want to do this via
   830    941   **                            a cron-job to make sure alerts are sent
   831    942   **                            in a timely manner.
   832    943   **                            Options:
   833    944   **
   834    945   **                               --digest     Send digests
   835         -**                               --test       Resets to standard output
          946  +**                               --test       Write to standard output
   836    947   **
   837         -**    inbound [FILE]          Receive an inbound email message.  This message
   838         -**                            is analyzed to see if it is a bounce, and if
   839         -**                            necessary, subscribers may be disabled.
          948  +**    settings [NAME VALUE]   With no arguments, list all email settings.
          949  +**                            Or change the value of a single email setting.
   840    950   **
   841         -**    reset                   Hard reset of all email notification tables
   842         -**                            in the repository.  This erases all subscription
   843         -**                            information.  Use with extreme care.
          951  +**    status                  Report on the status of the email alert
          952  +**                            subsystem
   844    953   **
   845         -**    send TO [OPTIONS]       Send a single email message using whatever
          954  +**    subscribers [PATTERN]   List all subscribers matching PATTERN.
          955  +**
          956  +**    test-message TO [OPTS]  Send a single email message using whatever
   846    957   **                            email sending mechanism is currently configured.
   847         -**                            Use this for testing the email configuration.
   848         -**                            Options:
          958  +**                            Use this for testing the email notification
          959  +**                            configuration.  Options:
   849    960   **
   850    961   **                              --body FILENAME
   851    962   **                              --smtp-trace
   852    963   **                              --stdout
   853    964   **                              --subject|-S SUBJECT
   854    965   **
   855         -**    settings [NAME VALUE]   With no arguments, list all email settings.
   856         -**                            Or change the value of a single email setting.
   857         -**
   858         -**    subscribers [PATTERN]   List all subscribers matching PATTERN.
   859         -**
   860    966   **    unsubscribe EMAIL       Remove a single subscriber with the given EMAIL.
   861    967   */
   862         -void email_cmd(void){
          968  +void alert_cmd(void){
   863    969     const char *zCmd;
   864    970     int nCmd;
   865    971     db_find_and_open_repository(0, 0);
   866         -  email_schema(0);
          972  +  alert_schema(0);
   867    973     zCmd = g.argc>=3 ? g.argv[2] : "x";
   868    974     nCmd = (int)strlen(zCmd);
   869         -  if( strncmp(zCmd, "exec", nCmd)==0 ){
   870         -    u32 eFlags = 0;
   871         -    if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
   872         -    if( find_option("test",0,0)!=0 ){
   873         -      eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT;
   874         -    }
          975  +  if( strncmp(zCmd, "pending", nCmd)==0 ){
          976  +    Stmt q;
   875    977       verify_all_options();
   876         -    email_send_alerts(eFlags);
   877         -  }else
   878         -  if( strncmp(zCmd, "inbound", nCmd)==0 ){
   879         -    Blob email;
   880         -    const char *zInboundDir = db_get("email-receive-dir","");
   881         -    verify_all_options();
   882         -    if( g.argc!=3 && g.argc!=4 ){
   883         -      usage("inbound [FILE]");
          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" : "");
   884    987       }
   885         -    blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE);
   886         -    if( zInboundDir[0] ){
   887         -      char *zFN = file_time_tempname(zInboundDir,".email");
   888         -      blob_write_to_file(&email, zFN);
   889         -      fossil_free(zFN);
   890         -    }
   891         -    email_receive(&email);
          988  +    db_finalize(&q);
   892    989     }else
   893    990     if( strncmp(zCmd, "reset", nCmd)==0 ){
   894    991       int c;
   895    992       int bForce = find_option("force","f",0)!=0;
   896    993       verify_all_options();
   897    994       if( bForce ){
   898    995         c = 'y';
................................................................................
   903   1000             "deleting all subscriber information.  The information will be\n"
   904   1001             "unrecoverable.\n");
   905   1002         prompt_user("Continue? (y/N) ", &yn);
   906   1003         c = blob_str(&yn)[0];
   907   1004         blob_reset(&yn);
   908   1005       }
   909   1006       if( c=='y' ){
   910         -      email_triggers_disable();
         1007  +      alert_triggers_disable();
   911   1008         db_multi_exec(
   912   1009           "DROP TABLE IF EXISTS subscriber;\n"
   913   1010           "DROP TABLE IF EXISTS pending_alert;\n"
   914         -        "DROP TABLE IF EXISTS email_bounce;\n"
         1011  +        "DROP TABLE IF EXISTS alert_bounce;\n"
   915   1012           /* Legacy */
   916         -        "DROP TABLE IF EXISTS email_pending;\n"
         1013  +        "DROP TABLE IF EXISTS alert_pending;\n"
   917   1014           "DROP TABLE IF EXISTS subscription;\n"
   918   1015         );
   919         -      email_schema(0);
         1016  +      alert_schema(0);
   920   1017       }
   921   1018     }else
   922   1019     if( strncmp(zCmd, "send", nCmd)==0 ){
   923         -    Blob prompt, body, hdr;
   924         -    const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
   925         -    int i;
   926         -    u32 mFlags = EMAIL_IMMEDIATE_FAIL;
   927         -    const char *zSubject = find_option("subject", "S", 1);
   928         -    const char *zSource = find_option("body", 0, 1);
   929         -    EmailSender *pSender;
   930         -    if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE;
         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  +    }
   931   1025       verify_all_options();
   932         -    blob_init(&prompt, 0, 0);
   933         -    blob_init(&body, 0, 0);
   934         -    blob_init(&hdr, 0, 0);
   935         -    blob_appendf(&hdr,"To: ");
   936         -    for(i=3; i<g.argc; i++){
   937         -      if( i>3 ) blob_append(&hdr, ", ", 2);
   938         -      blob_appendf(&hdr, "<%s>", g.argv[i]);
   939         -    }
   940         -    blob_append(&hdr,"\r\n",2);
   941         -    if( zSubject ){
   942         -      blob_appendf(&hdr, "Subject: %s\r\n", zSubject);
   943         -    }
   944         -    if( zSource ){
   945         -      blob_read_from_file(&body, zSource, ExtFILE);
   946         -    }else{
   947         -      prompt_for_user_comment(&body, &prompt);
   948         -    }
   949         -    blob_add_final_newline(&body);
   950         -    pSender = email_sender_new(zDest, mFlags);
   951         -    email_send(pSender, &hdr, &body);
   952         -    email_sender_free(pSender);
   953         -    blob_reset(&hdr);
   954         -    blob_reset(&body);
   955         -    blob_reset(&prompt);
         1026  +    alert_send_alerts(eFlags);
   956   1027     }else
   957   1028     if( strncmp(zCmd, "settings", nCmd)==0 ){
   958   1029       int isGlobal = find_option("global",0,0)!=0;
   959   1030       int nSetting;
   960   1031       const Setting *pSetting = setting_info(&nSetting);
   961   1032       db_open_config(1, 0);
   962   1033       verify_all_options();
................................................................................
   972   1043       }
   973   1044       pSetting = setting_info(&nSetting);
   974   1045       for(; nSetting>0; nSetting--, pSetting++ ){
   975   1046         if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
   976   1047         print_setting(pSetting);
   977   1048       }
   978   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
   979   1072     if( strncmp(zCmd, "subscribers", nCmd)==0 ){
   980   1073       Stmt q;
   981   1074       verify_all_options();
   982   1075       if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
   983   1076       if( g.argc==4 ){
   984   1077         char *zPattern = g.argv[3];
   985   1078         db_prepare(&q,
................................................................................
   993   1086           "SELECT semail FROM subscriber"
   994   1087           " ORDER BY semail");
   995   1088       }
   996   1089       while( db_step(&q)==SQLITE_ROW ){
   997   1090         fossil_print("%s\n", db_column_text(&q, 0));
   998   1091       }
   999   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);
  1000   1127     }else
  1001   1128     if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
  1002   1129       verify_all_options();
  1003   1130       if( g.argc!=4 ) usage("unsubscribe EMAIL");
  1004   1131       db_multi_exec(
  1005   1132         "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
  1006   1133     }else
  1007   1134     {
  1008         -    usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
         1135  +    usage("pending|reset|send|setting|status|"
         1136  +          "subscribers|test-message|unsubscribe");
  1009   1137     }
  1010   1138   }
  1011   1139   
  1012   1140   /*
  1013   1141   ** Do error checking on a submitted subscription form.  Return TRUE
  1014   1142   ** if the submission is valid.  Return false if any problems are seen.
  1015   1143   */
................................................................................
  1024   1152   
  1025   1153     *peErr = 0;
  1026   1154     *pzErr = 0;
  1027   1155   
  1028   1156     /* Check the validity of the email address.
  1029   1157     **
  1030   1158     **  (1) Exactly one '@' character.
  1031         -  **  (2) No other characters besides [a-zA-Z0-9._-]
         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.
  1032   1164     */
  1033   1165     zEAddr = P("e");
  1034   1166     if( zEAddr==0 ) return 0;
  1035   1167     for(i=j=n=0; (c = zEAddr[i])!=0; i++){
  1036   1168       if( c=='@' ){
  1037   1169         n = i;
  1038   1170         j++;
  1039   1171         continue;
  1040   1172       }
  1041         -    if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){
         1173  +    if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' && c!='+' ){
  1042   1174         *peErr = 1;
  1043   1175         *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
  1044   1176                      c, c);
  1045   1177         return 0;
  1046   1178       }
  1047   1179     }
  1048   1180     if( j!=1 ){
................................................................................
  1094   1226   @ Save the hyperlink above!  You can reuse this same hyperlink to
  1095   1227   @ unsubscribe or to change the kinds of alerts you receive.
  1096   1228   @
  1097   1229   @ If you do not want to subscribe, you can simply ignore this message.
  1098   1230   @ You will not be contacted again.
  1099   1231   @
  1100   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  +}
  1101   1242   
  1102   1243   /*
  1103   1244   ** WEBPAGE: subscribe
  1104   1245   **
  1105   1246   ** Allow users to subscribe to email notifications.
  1106   1247   **
  1107   1248   ** This page is usually run by users who are not logged in.
................................................................................
  1108   1249   ** A logged-in user can add email notifications on the /alerts page.
  1109   1250   ** Access to this page by a logged in user (other than an
  1110   1251   ** administrator) results in a redirect to the /alerts page.
  1111   1252   **
  1112   1253   ** Administrators can visit this page in order to sign up other
  1113   1254   ** users.
  1114   1255   **
  1115         -** The Email-Alerts permission ("7") is required to access this
         1256  +** The Alerts permission ("7") is required to access this
  1116   1257   ** page.  To allow anonymous passers-by to sign up for email
  1117   1258   ** notification, set Email-Alerts on user "nobody" or "anonymous".
  1118   1259   */
  1119   1260   void subscribe_page(void){
  1120   1261     int needCaptcha;
  1121   1262     unsigned int uSeed;
  1122   1263     const char *zDecoded;
  1123   1264     char *zCaptcha = 0;
  1124   1265     char *zErr = 0;
  1125   1266     int eErr = 0;
         1267  +  int di;
  1126   1268   
  1127         -  if( email_webpages_disabled() ) return;
         1269  +  if( alert_webpages_disabled() ) return;
  1128   1270     login_check_credentials();
  1129   1271     if( !g.perm.EmailAlert ){
  1130   1272       login_needed(g.anon.EmailAlert);
  1131   1273       return;
  1132   1274     }
  1133   1275     if( login_is_individual()
  1134   1276      && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
................................................................................
  1146   1288       }else{
  1147   1289         /* Everybody else jumps to the page to administer their own
  1148   1290         ** account only. */
  1149   1291         cgi_redirectf("%R/alerts");
  1150   1292         return;
  1151   1293       }
  1152   1294     }
  1153         -  email_submenu_common();
         1295  +  alert_submenu_common();
  1154   1296     needCaptcha = !login_is_individual();
  1155   1297     if( P("submit")
  1156   1298      && cgi_csrf_safe(1)
  1157   1299      && subscribe_error_check(&eErr,&zErr,needCaptcha)
  1158   1300     ){
  1159   1301       /* A validated request for a new subscription has been received. */
  1160   1302       char ssub[20];
................................................................................
  1191   1333         ** No verification is required.  Jump immediately to /alerts page.
  1192   1334         */
  1193   1335         cgi_redirectf("%R/alerts/%s", zCode);
  1194   1336         return;
  1195   1337       }else{
  1196   1338         /* We need to send a verification email */
  1197   1339         Blob hdr, body;
  1198         -      EmailSender *pSender = email_sender_new(0,0);
         1340  +      AlertSender *pSender = alert_sender_new(0,0);
  1199   1341         blob_init(&hdr,0,0);
  1200   1342         blob_init(&body,0,0);
  1201   1343         blob_appendf(&hdr, "To: <%s>\n", zEAddr);
  1202   1344         blob_appendf(&hdr, "Subject: Subscription verification\n");
  1203         -      blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
  1204         -                   g.zBaseURL, g.zBaseURL, zCode);
  1205         -      email_send(pSender, &hdr, &body);
         1345  +      alert_append_confirmation_message(&body, zCode);
         1346  +      alert_send(pSender, &hdr, &body, 0);
  1206   1347         style_header("Email Alert Verification");
  1207   1348         if( pSender->zErr ){
  1208   1349           @ <h1>Internal Error</h1>
  1209   1350           @ <p>The following internal error was encountered while trying
  1210   1351           @ to send the confirmation email:
  1211   1352           @ <blockquote><pre>
  1212   1353           @ %h(pSender->zErr)
  1213   1354           @ </pre></blockquote>
  1214   1355         }else{
  1215   1356           @ <p>An email has been sent to "%h(zEAddr)". That email contains a
  1216   1357           @ hyperlink that you must click on in order to activate your
  1217   1358           @ subscription.</p>
  1218   1359         }
  1219         -      email_sender_free(pSender);
         1360  +      alert_sender_free(pSender);
  1220   1361         style_footer();
  1221   1362       }
  1222   1363       return;
  1223   1364     }
  1224   1365     style_header("Signup For Email Alerts");
  1225   1366     if( P("submit")==0 ){
  1226   1367       /* If this is the first visit to this page (if this HTTP request did not
................................................................................
  1235   1376     @ <p>To receive email notifications for changes to this
  1236   1377     @ repository, fill out the form below and press "Submit" button.</p>
  1237   1378     form_begin(0, "%R/subscribe");
  1238   1379     @ <table class="subscribe">
  1239   1380     @ <tr>
  1240   1381     @  <td class="form_label">Email&nbsp;Address:</td>
  1241   1382     @  <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
         1383  +  @ <tr>
  1242   1384     if( eErr==1 ){
  1243         -    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1385  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1244   1386     }
  1245   1387     @ </tr>
  1246   1388     if( needCaptcha ){
  1247   1389       uSeed = captcha_seed();
  1248   1390       zDecoded = captcha_decode(uSeed);
  1249   1391       zCaptcha = captcha_render(zDecoded);
  1250   1392       @ <tr>
  1251   1393       @  <td class="form_label">Security Code:</td>
  1252   1394       @  <td><input type="text" name="captcha" value="" size="30">
  1253   1395       @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
         1396  +    @ </tr>
  1254   1397       if( eErr==2 ){
  1255         -      @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1398  +      @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1256   1399       }
  1257   1400       @ </tr>
  1258   1401     }
  1259   1402     if( g.perm.Admin ){
  1260   1403       @ <tr>
  1261   1404       @  <td class="form_label">User:</td>
  1262   1405       @  <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
  1263   1406       @  size="30"></td>
         1407  +    @ </tr>
  1264   1408       if( eErr==3 ){
  1265         -      @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1409  +      @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
  1266   1410       }
  1267   1411       @ </tr>
  1268   1412     }
  1269   1413     @ <tr>
  1270         -  @  <td class="form_label">Options:</td>
         1414  +  @  <td class="form_label">Topics:</td>
  1271   1415     @  <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
  1272   1416     @  Announcements</label><br>
  1273   1417     if( g.perm.Read ){
  1274   1418       @  <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
  1275   1419       @  Check-ins</label><br>
  1276   1420     }
  1277   1421     if( g.perm.RdForum ){
................................................................................
  1282   1426       @  <label><input type="checkbox" name="st" %s(PCK("st"))> \
  1283   1427       @  Ticket changes</label><br>
  1284   1428     }
  1285   1429     if( g.perm.RdWiki ){
  1286   1430       @  <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
  1287   1431       @  Wiki</label><br>
  1288   1432     }
  1289         -  @  <label><input type="checkbox" name="di" %s(PCK("di"))> \
  1290         -  @  Daily digest only</label><br>
         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>
  1291   1442     if( g.perm.Admin ){
         1443  +    @ <tr>
         1444  +    @  <td class="form_label">Admin Options:</td><td>
  1292   1445       @  <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
  1293   1446       @  Verified</label><br>
  1294   1447       @  <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
  1295         -    @  Do not call</label><br>
         1448  +    @  Do not call</label></td></tr>
  1296   1449     }
  1297         -  @ </td>
  1298         -  @ </tr>
  1299   1450     @ <tr>
  1300   1451     @  <td></td>
  1301         -  if( needCaptcha && !email_enabled() ){
         1452  +  if( needCaptcha && !alert_enabled() ){
  1302   1453       @  <td><input type="submit" name="submit" value="Submit" disabled>
  1303   1454       @  (Email current disabled)</td>
  1304   1455     }else{
  1305   1456       @  <td><input type="submit" name="submit" value="Submit"></td>
  1306   1457     }
  1307   1458     @ </tr>
  1308   1459     @ </table>
  1309   1460     if( needCaptcha ){
  1310         -    @ <div class="captcha"><table class="captcha"><tr><td><pre>
         1461  +    @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  1311   1462       @ %h(zCaptcha)
  1312   1463       @ </pre>
  1313   1464       @ Enter the 8 characters above in the "Security Code" box
  1314   1465       @ </td></tr></table></div>
  1315   1466     }
  1316   1467     @ </form>
  1317   1468     fossil_free(zErr);
................................................................................
  1319   1470   }
  1320   1471   
  1321   1472   /*
  1322   1473   ** Either shutdown or completely delete a subscription entry given
  1323   1474   ** by the hex value zName.  Then paint a webpage that explains that
  1324   1475   ** the entry has been removed.
  1325   1476   */
  1326         -static void email_unsubscribe(const char *zName){
         1477  +static void alert_unsubscribe(const char *zName){
  1327   1478     char *zEmail;
  1328   1479     zEmail = db_text(0, "SELECT semail FROM subscriber"
  1329   1480                         " WHERE subscriberCode=hextoblob(%Q)", zName);
  1330   1481     if( zEmail==0 ){
  1331   1482       style_header("Unsubscribe Fail");
  1332   1483       @ <p>Unable to locate a subscriber with the requested key</p>
  1333   1484     }else{
................................................................................
  1353   1504   **    (1)  The name= query parameter contains the subscriberCode.
  1354   1505   **         
  1355   1506   **    (2)  The user is logged into an account other than "nobody" or
  1356   1507   **         "anonymous".  In that case the notification settings
  1357   1508   **         associated with that account can be edited without needing
  1358   1509   **         to know the subscriber code.
  1359   1510   */
  1360         -void alerts_page(void){
         1511  +void alert_page(void){
  1361   1512     const char *zName = P("name");
  1362   1513     Stmt q;
  1363   1514     int sa, sc, sf, st, sw;
  1364   1515     int sdigest, sdonotcall, sverified;
  1365   1516     const char *ssub;
  1366   1517     const char *semail;
  1367   1518     const char *smip;
  1368   1519     const char *suname;
  1369   1520     const char *mtime;
  1370   1521     const char *sctime;
  1371   1522     int eErr = 0;
  1372   1523     char *zErr = 0;
  1373   1524   
  1374         -  if( email_webpages_disabled() ) return;
         1525  +  if( alert_webpages_disabled() ) return;
  1375   1526     login_check_credentials();
         1527  +  if( !g.perm.EmailAlert ){
         1528  +    login_needed(g.anon.EmailAlert);
         1529  +    return;
         1530  +  }
  1376   1531     if( zName==0 && login_is_individual() ){
  1377   1532       zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
  1378   1533                          " WHERE suname=%Q", g.zLogin);
  1379   1534     }
  1380   1535     if( zName==0 || !validate16(zName, -1) ){
  1381   1536       cgi_redirect("subscribe");
  1382   1537       return;
  1383   1538     }
  1384         -  email_submenu_common();
         1539  +  alert_submenu_common();
  1385   1540     if( P("submit")!=0 && cgi_csrf_safe(1) ){
  1386   1541       int sdonotcall = PB("sdonotcall");
  1387   1542       int sdigest = PB("sdigest");
  1388   1543       char ssub[10];
  1389   1544       int nsub = 0;
  1390   1545       if( PB("sa") )                   ssub[nsub++] = 'a';
  1391   1546       if( g.perm.Read && PB("sc") )    ssub[nsub++] = 'c';
................................................................................
  1431   1586           zName
  1432   1587         );
  1433   1588       }
  1434   1589     }
  1435   1590     if( P("delete")!=0 && cgi_csrf_safe(1) ){
  1436   1591       if( !PB("dodelete") ){
  1437   1592         eErr = 9;
  1438         -      zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to"
         1593  +      zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
  1439   1594                        " unsubscribe");
  1440   1595       }else{
  1441         -      email_unsubscribe(zName);
         1596  +      alert_unsubscribe(zName);
  1442   1597         return;
  1443   1598       }
  1444   1599     }
  1445   1600     db_prepare(&q,
  1446   1601       "SELECT"
  1447   1602       "  semail,"                       /* 0 */
  1448   1603       "  sverified,"                    /* 1 */
................................................................................
  1509   1664       @ <tr>
  1510   1665       @  <td class="form_label">User:</td>
  1511   1666       @  <td><input type="text" name="suname" value="%h(suname?suname:"")" \
  1512   1667       @  size="30"></td>
  1513   1668       @ </tr>
  1514   1669     }
  1515   1670     @ <tr>
  1516         -  @  <td class="form_label">Options:</td>
         1671  +  @  <td class="form_label">Topics:</td>
  1517   1672     @  <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
  1518   1673     @  Announcements</label><br>
  1519   1674     if( g.perm.Read ){
  1520   1675       @  <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
  1521   1676       @  Check-ins</label><br>
  1522   1677     }
  1523   1678     if( g.perm.RdForum ){
................................................................................
  1526   1681     }
  1527   1682     if( g.perm.RdTkt ){
  1528   1683       @  <label><input type="checkbox" name="st" %s(st?"checked":"")>\
  1529   1684       @  Ticket changes</label><br>
  1530   1685     }
  1531   1686     if( g.perm.RdWiki ){
  1532   1687       @  <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
  1533         -    @  Wiki</label><br>
         1688  +    @  Wiki</label>
  1534   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
  1535   1699     @  <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
  1536   1700     @  Daily digest only</label><br>
         1701  +#endif
  1537   1702     if( g.perm.Admin ){
         1703  +    @ <tr>
         1704  +    @  <td class="form_label">Admin Options:</td><td>
  1538   1705       @  <label><input type="checkbox" name="sdonotcall" \
  1539   1706       @  %s(sdonotcall?"checked":"")> Do not call</label><br>
  1540   1707       @  <label><input type="checkbox" name="sverified" \
  1541   1708       @  %s(sverified?"checked":"")>\
  1542         -    @  Verified</label><br>
         1709  +    @  Verified</label></td></tr>
  1543   1710     }
  1544         -  @  <label><input type="checkbox" name="dodelete">
  1545         -  @  Unsubscribe</label> \
  1546   1711     if( eErr==9 ){
  1547         -    @ <span class="loginError">&larr; %h(zErr)</span>\
         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>
  1548   1718     }
  1549         -  @ <br>
  1550         -  @ </td></tr>
  1551   1719     @ <tr>
  1552   1720     @  <td></td>
  1553   1721     @  <td><input type="submit" name="submit" value="Submit">
  1554   1722     @  <input type="submit" name="delete" value="Unsubscribe">
  1555   1723     @ </tr>
  1556   1724     @ </table>
  1557   1725     @ </form>
................................................................................
  1602   1770   
  1603   1771     /* If a valid subscriber code is supplied, then unsubscribe immediately.
  1604   1772     */
  1605   1773     if( zName 
  1606   1774      && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
  1607   1775                   zName)
  1608   1776     ){
  1609         -    email_unsubscribe(zName);
         1777  +    alert_unsubscribe(zName);
  1610   1778       return;
  1611   1779     }
  1612   1780   
  1613   1781     /* Logged in users are redirected to the /alerts page */
  1614   1782     login_check_credentials();
  1615   1783     if( login_is_individual() ){
  1616   1784       cgi_redirectf("%R/alerts");
................................................................................
  1636   1804         bSubmit = 0;
  1637   1805       }
  1638   1806     }
  1639   1807     if( bSubmit ){
  1640   1808       /* If we get this far, it means that a valid unsubscribe request has
  1641   1809       ** been submitted.  Send the appropriate email. */
  1642   1810       Blob hdr, body;
  1643         -    EmailSender *pSender = email_sender_new(0,0);
         1811  +    AlertSender *pSender = alert_sender_new(0,0);
  1644   1812       blob_init(&hdr,0,0);
  1645   1813       blob_init(&body,0,0);
  1646   1814       blob_appendf(&hdr, "To: <%s>\r\n", zEAddr);
  1647   1815       blob_appendf(&hdr, "Subject: Unsubscribe Instructions\r\n");
  1648   1816       blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
  1649   1817                     g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
  1650         -    email_send(pSender, &hdr, &body);
         1818  +    alert_send(pSender, &hdr, &body, 0);
  1651   1819       style_header("Unsubscribe Instructions Sent");
  1652   1820       if( pSender->zErr ){
  1653   1821         @ <h1>Internal Error</h1>
  1654   1822         @ <p>The following error was encountered while trying to send an
  1655   1823         @ email to %h(zEAddr):
  1656   1824         @ <blockquote><pre>
  1657   1825         @ %h(pSender->zErr)
  1658   1826         @ </pre></blockquote>
  1659   1827       }else{
  1660   1828         @ <p>An email has been sent to "%h(zEAddr)" that explains how to
  1661   1829         @ unsubscribe and/or modify your subscription settings</p>
  1662   1830       }
  1663         -    email_sender_free(pSender);
         1831  +    alert_sender_free(pSender);
  1664   1832       style_footer();
  1665   1833       return;
  1666   1834     }  
  1667   1835   
  1668   1836     /* Non-logged-in users have to enter an email address to which is
  1669   1837     ** sent a message containing the unsubscribe link.
  1670   1838     */
................................................................................
  1699   1867     @  <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
  1700   1868     @  Completely unsubscribe</label><br>
  1701   1869     @ <tr>
  1702   1870     @  <td></td>
  1703   1871     @  <td><input type="submit" name="submit" value="Submit"></td>
  1704   1872     @ </tr>
  1705   1873     @ </table>
  1706         -  @ <div class="captcha"><table class="captcha"><tr><td><pre>
         1874  +  @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  1707   1875     @ %h(zCaptcha)
  1708   1876     @ </pre>
  1709   1877     @ Enter the 8 characters above in the "Security Code" box
  1710   1878     @ </td></tr></table></div>
  1711   1879     @ </form>
  1712   1880     fossil_free(zErr);
  1713   1881     style_footer();
................................................................................
  1721   1889   ** Clicking on an email takes one to the /alerts page
  1722   1890   ** for that email where the delivery settings can be
  1723   1891   ** modified.
  1724   1892   */
  1725   1893   void subscriber_list_page(void){
  1726   1894     Blob sql;
  1727   1895     Stmt q;
  1728         -  double rNow;
  1729         -  if( email_webpages_disabled() ) return;
         1896  +  sqlite3_int64 iNow;
         1897  +  if( alert_webpages_disabled() ) return;
  1730   1898     login_check_credentials();
  1731   1899     if( !g.perm.Admin ){
  1732   1900       login_needed(0);
  1733   1901       return;
  1734   1902     }
  1735         -  email_submenu_common();
         1903  +  alert_submenu_common();
  1736   1904     style_header("Subscriber List");
  1737   1905     blob_init(&sql, 0, 0);
  1738   1906     blob_append_sql(&sql,
  1739   1907       "SELECT hex(subscriberCode),"          /* 0 */
  1740   1908       "       semail,"                       /* 1 */
  1741   1909       "       ssub,"                         /* 2 */
  1742   1910       "       suname,"                       /* 3 */
  1743   1911       "       sverified,"                    /* 4 */
  1744   1912       "       sdigest,"                      /* 5 */
  1745         -    "       date(sctime,'unixepoch'),"     /* 6 */
  1746         -    "       julianday(mtime,'unixepoch')"  /* 7 */
         1913  +    "       mtime,"                        /* 6 */
         1914  +    "       date(sctime,'unixepoch')"      /* 7 */
  1747   1915       " FROM subscriber"
  1748   1916     );
  1749   1917     if( P("only")!=0 ){
  1750   1918       blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
  1751   1919       style_submenu_element("Show All","%R/subscribers");
  1752   1920     }
         1921  +  blob_append_sql(&sql," ORDER BY mtime DESC");
  1753   1922     db_prepare_blob(&q, &sql);
  1754         -  rNow = db_double(0.0,"SELECT julianday('now')");
  1755         -  @ <table border="1">
         1923  +  iNow = time(0);
         1924  +  @ <table border='1' class='sortable' \
         1925  +  @ data-init-sort='6' data-column-types='tttttKt'>
         1926  +  @ <thead>
  1756   1927     @ <tr>
  1757   1928     @ <th>Email
  1758   1929     @ <th>Events
  1759   1930     @ <th>Digest-Only?
  1760   1931     @ <th>User
  1761   1932     @ <th>Verified?
  1762   1933     @ <th>Last change
  1763   1934     @ <th>Created
  1764   1935     @ </tr>
         1936  +  @ </thead><tbody>
  1765   1937     while( db_step(&q)==SQLITE_ROW ){
  1766         -    double rAge = rNow - db_column_double(&q, 7);
         1938  +    sqlite3_int64 iMtime = db_column_int64(&q, 6);
         1939  +    double rAge = (iNow - iMtime)/86400.0;
  1767   1940       @ <tr>
  1768   1941       @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
  1769   1942       @ %h(db_column_text(&q,1))</a></td>
  1770   1943       @ <td>%h(db_column_text(&q,2))</td>
  1771   1944       @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
  1772   1945       @ <td>%h(db_column_text(&q,3))</td>
  1773   1946       @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
  1774         -    @ <td>%z(human_readable_age(rAge))</td>
  1775         -    @ <td>%h(db_column_text(&q,6))</td>
         1947  +    @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
         1948  +    @ <td>%h(db_column_text(&q,7))</td>
  1776   1949       @ </tr>
  1777   1950     }
  1778         -  @ </table>
         1951  +  @ </tbody></table>
  1779   1952     db_finalize(&q);
         1953  +  style_table_sorter();
  1780   1954     style_footer();
  1781   1955   }
  1782   1956   
  1783   1957   #if LOCAL_INTERFACE
  1784   1958   /*
  1785   1959   ** A single event that might appear in an alert is recorded as an
  1786   1960   ** instance of the following object.
  1787   1961   */
  1788   1962   struct EmailEvent {
  1789         -  int type;          /* 'c', 't', 'w', etc. */
         1963  +  int type;          /* 'c', 'f', 'm', 't', 'w' */
         1964  +  int needMod;       /* Pending moderator approval */
         1965  +  Blob hdr;          /* Header content, for forum entries */
  1790   1966     Blob txt;          /* Text description to appear in an alert */
         1967  +  char *zFromName;   /* Human name of the sender */
  1791   1968     EmailEvent *pNext; /* Next in chronological order */
  1792   1969   };
  1793   1970   #endif
  1794   1971   
  1795   1972   /*
  1796   1973   ** Free a linked list of EmailEvent objects
  1797   1974   */
  1798         -void email_free_eventlist(EmailEvent *p){
         1975  +void alert_free_eventlist(EmailEvent *p){
  1799   1976     while( p ){
  1800   1977       EmailEvent *pNext = p->pNext;
  1801   1978       blob_reset(&p->txt);
         1979  +    blob_reset(&p->hdr);
         1980  +    fossil_free(p->zFromName);
  1802   1981       fossil_free(p);
  1803   1982       p = pNext;
  1804   1983     }
  1805   1984   }
  1806   1985   
  1807   1986   /*
  1808   1987   ** Compute and return a linked list of EmailEvent objects
  1809   1988   ** corresponding to the current content of the temp.wantalert
  1810   1989   ** table which should be defined as follows:
  1811   1990   **
  1812         -**     CREATE TEMP TABLE wantalert(eventId TEXT);
         1991  +**     CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN);
  1813   1992   */
  1814         -EmailEvent *email_compute_event_text(int *pnEvent){
         1993  +EmailEvent *alert_compute_event_text(int *pnEvent, int doDigest){
  1815   1994     Stmt q;
  1816   1995     EmailEvent *p;
  1817   1996     EmailEvent anchor;
  1818   1997     EmailEvent *pLast;
  1819   1998     const char *zUrl = db_get("email-url","http://localhost:8080");
         1999  +  const char *zFrom;
         2000  +  const char *zSub;
  1820   2001   
         2002  +
         2003  +  /* First do non-forum post events */
  1821   2004     db_prepare(&q,
  1822   2005       "SELECT"
  1823         -    " blob.uuid,"  /* 0 */
  1824         -    " datetime(event.mtime),"  /* 1 */
         2006  +    " blob.uuid,"                /* 0 */
         2007  +    " datetime(event.mtime),"    /* 1 */
  1825   2008       " coalesce(ecomment,comment)"
  1826   2009       "  || ' (user: ' || coalesce(euser,user,'?')"
  1827   2010       "  || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
  1828   2011       "      FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
  1829   2012       "              FROM tag, tagxref"
  1830   2013       "             WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
  1831   2014       "               AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
  1832         -    "  || ')' as comment,"  /* 2 */
  1833         -    " tagxref.value AS branch,"  /* 3 */
  1834         -    " wantalert.eventId"     /* 4 */
  1835         -    " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob"
  1836         -    "  LEFT JOIN tagxref ON tagxref.tagid=tag.tagid"
  1837         -    "                       AND tagxref.tagtype>0"
  1838         -    "                       AND tagxref.rid=blob.rid"
         2015  +    "  || ')' as comment,"       /* 2 */
         2016  +    " wantalert.eventId,"        /* 3 */
         2017  +    " wantalert.needMod"         /* 4 */
         2018  +    " FROM temp.wantalert, event, blob"
  1839   2019       " WHERE blob.rid=event.objid"
  1840         -    "   AND tag.tagname='branch'"
  1841   2020       "   AND event.objid=substr(wantalert.eventId,2)+0"
  1842         -    " ORDER BY event.mtime"
         2021  +    "   AND (%d OR eventId NOT GLOB 'f*')"
         2022  +    " ORDER BY event.mtime",
         2023  +    doDigest
  1843   2024     );
  1844   2025     memset(&anchor, 0, sizeof(anchor));
  1845   2026     pLast = &anchor;
  1846   2027     *pnEvent = 0;
  1847   2028     while( db_step(&q)==SQLITE_ROW ){
  1848   2029       const char *zType = "";
  1849   2030       p = fossil_malloc( sizeof(EmailEvent) );
  1850   2031       pLast->pNext = p;
  1851   2032       pLast = p;
  1852         -    p->type = db_column_text(&q, 4)[0];
         2033  +    p->type = db_column_text(&q, 3)[0];
         2034  +    p->needMod = db_column_int(&q, 4);
         2035  +    p->zFromName = 0;
  1853   2036       p->pNext = 0;
  1854   2037       switch( p->type ){
  1855   2038         case 'c':  zType = "Check-In";        break;
         2039  +      case 'f':  zType = "Forum post";      break;
  1856   2040         case 't':  zType = "Wiki Edit";       break;
  1857   2041         case 'w':  zType = "Ticket Change";   break;
  1858   2042       }
         2043  +    blob_init(&p->hdr, 0, 0);
  1859   2044       blob_init(&p->txt, 0, 0);
  1860   2045       blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
  1861   2046         db_column_text(&q,1),
  1862   2047         zType,
  1863   2048         db_column_text(&q,2),
  1864   2049         zUrl,
  1865   2050         db_column_text(&q,0)
  1866   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);
  1867   2138       (*pnEvent)++;
  1868   2139     }
  1869   2140     db_finalize(&q);
         2141  +
  1870   2142     return anchor.pNext;
  1871   2143   }
  1872   2144   
  1873   2145   /*
  1874   2146   ** Put a header on an alert email
  1875   2147   */
  1876   2148   void email_header(Blob *pOut){
................................................................................
  1881   2153       db_get("email-url","http://localhost:8080"));
  1882   2154   }
  1883   2155   
  1884   2156   /*
  1885   2157   ** Append the "unsubscribe" notification and other footer text to
  1886   2158   ** the end of an email alert being assemblied in pOut.
  1887   2159   */
  1888         -void email_footer(Blob *pOut){
         2160  +void alert_footer(Blob *pOut){
  1889   2161     blob_appendf(pOut, "\n-- \nTo unsubscribe: %s/unsubscribe\n",
  1890   2162        db_get("email-url","http://localhost:8080"));
  1891   2163   }
  1892   2164   
  1893   2165   /*
  1894   2166   ** COMMAND:  test-alert
  1895   2167   **
................................................................................
  1898   2170   ** Generate the text of an email alert for all of the EVENTIDs
  1899   2171   ** listed on the command-line.  Or if no events are listed on the
  1900   2172   ** command line, generate text for all events named in the
  1901   2173   ** pending_alert table.
  1902   2174   **
  1903   2175   ** This command is intended for testing and debugging the logic
  1904   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
  1905   2182   */
  1906   2183   void test_alert_cmd(void){
  1907   2184     Blob out;
  1908   2185     int nEvent;
         2186  +  int needMod;
         2187  +  int doDigest;
  1909   2188     EmailEvent *pEvent, *p;
  1910   2189   
         2190  +  doDigest = find_option("digest",0,0)!=0;
         2191  +  needMod = find_option("needmod",0,0)!=0;
  1911   2192     db_find_and_open_repository(0, 0);
  1912   2193     verify_all_options();
  1913   2194     db_begin_transaction();
  1914         -  email_schema(0);
  1915         -  db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)");
         2195  +  alert_schema(0);
         2196  +  db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)");
  1916   2197     if( g.argc==2 ){
  1917         -    db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert");
         2198  +    db_multi_exec(
         2199  +      "INSERT INTO wantalert(eventId,needMod)"
         2200  +      " SELECT eventid, %d FROM pending_alert", needMod);
  1918   2201     }else{
  1919   2202       int i;
  1920   2203       for(i=2; i<g.argc; i++){
  1921         -      db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]);
         2204  +      db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)",
         2205  +           g.argv[i], needMod);
  1922   2206       }
  1923   2207     }
  1924   2208     blob_init(&out, 0, 0);
  1925   2209     email_header(&out);
  1926         -  pEvent = email_compute_event_text(&nEvent);
         2210  +  pEvent = alert_compute_event_text(&nEvent, doDigest);
  1927   2211     for(p=pEvent; p; p=p->pNext){
  1928   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  +    }
  1929   2217       blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
  1930   2218     }
  1931         -  email_free_eventlist(pEvent);
  1932         -  email_footer(&out);
         2219  +  alert_free_eventlist(pEvent);
         2220  +  alert_footer(&out);
  1933   2221     fossil_print("%s", blob_str(&out));
  1934   2222     blob_reset(&out);
  1935   2223     db_end_transaction(0);
  1936   2224   }
  1937   2225   
  1938   2226   /*
  1939   2227   ** COMMAND:  test-add-alerts
  1940   2228   **
  1941         -** Usage: %fossil test-add-alerts [--backoffice] EVENTID ...
         2229  +** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ...
  1942   2230   **
  1943   2231   ** Add one or more events to the pending_alert queue.  Use this
  1944   2232   ** command during testing to force email notifications for specific
  1945   2233   ** events.
  1946   2234   **
  1947         -** EVENTIDs are text.  The first character is 'c', 'w', or 't'
  1948         -** for check-in, wiki, or ticket.  The remaining text is a
         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
  1949   2237   ** integer that references the EVENT.OBJID value for the event.
  1950   2238   ** Run /timeline?showid to see these OBJID values.
  1951   2239   **
  1952         -** If the --backoffice option is included, then email_backoffice() is run
  1953         -** after all alerts have been added.  This will cause the alerts to
  1954         -** be sent out with the SENDALERT_TRACE option.
         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
  1955   2251   */
  1956   2252   void test_add_alert_cmd(void){
  1957   2253     int i;
  1958   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  +  }
  1959   2263     db_find_and_open_repository(0, 0);
  1960   2264     verify_all_options();
  1961   2265     db_begin_write();
  1962         -  email_schema(0);
         2266  +  alert_schema(0);
  1963   2267     for(i=2; i<g.argc; i++){
  1964   2268       db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
  1965   2269     }
  1966   2270     db_end_transaction(0);
  1967   2271     if( doAuto ){
  1968         -    email_backoffice(SENDALERT_TRACE);
         2272  +    alert_backoffice(SENDALERT_TRACE|mFlags);
  1969   2273     }
  1970   2274   }
  1971   2275   
  1972   2276   #if INTERFACE
  1973   2277   /*
  1974         -** Flags for email_send_alerts()
         2278  +** Flags for alert_send_alerts()
  1975   2279   */
  1976   2280   #define SENDALERT_DIGEST      0x0001    /* Send a digest */
  1977   2281   #define SENDALERT_PRESERVE    0x0002    /* Do not mark the task as done */
  1978   2282   #define SENDALERT_STDOUT      0x0004    /* Print emails instead of sending */
  1979   2283   #define SENDALERT_TRACE       0x0008    /* Trace operation for debugging */
  1980   2284   
  1981   2285   #endif /* INTERFACE */
  1982   2286   
  1983   2287   /*
  1984         -** Send alert emails to all subscribers.
         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!
  1985   2320   */
  1986         -void email_send_alerts(u32 flags){
         2321  +void alert_send_alerts(u32 flags){
  1987   2322     EmailEvent *pEvents, *p;
  1988   2323     int nEvent = 0;
  1989   2324     Stmt q;
  1990   2325     const char *zDigest = "false";
  1991   2326     Blob hdr, body;
  1992   2327     const char *zUrl;
  1993   2328     const char *zRepoName;
  1994   2329     const char *zFrom;
  1995   2330     const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;
  1996         -  EmailSender *pSender = 0;
         2331  +  AlertSender *pSender = 0;
  1997   2332     u32 senderFlags = 0;
  1998   2333   
  1999         -  if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags);
  2000         -  db_begin_transaction();
  2001         -  if( !email_enabled() ) goto send_alerts_done;
         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;
  2002   2337     zUrl = db_get("email-url",0);
  2003         -  if( zUrl==0 ) goto send_alerts_done;
         2338  +  if( zUrl==0 ) goto send_alert_done;
  2004   2339     zRepoName = db_get("email-subname",0);
  2005         -  if( zRepoName==0 ) goto send_alerts_done;
         2340  +  if( zRepoName==0 ) goto send_alert_done;
  2006   2341     zFrom = db_get("email-self",0);
  2007         -  if( zFrom==0 ) goto send_alerts_done;
         2342  +  if( zFrom==0 ) goto send_alert_done;
  2008   2343     if( flags & SENDALERT_TRACE ){
  2009         -    senderFlags |= EMAIL_TRACE;
         2344  +    senderFlags |= ALERT_TRACE;
  2010   2345     }
  2011         -  pSender = email_sender_new(zDest, senderFlags);
         2346  +  pSender = alert_sender_new(zDest, senderFlags);
         2347  +
         2348  +  /* Step (1):  Compute the alerts that need sending
         2349  +  */
  2012   2350     db_multi_exec(
  2013   2351       "DROP TABLE IF EXISTS temp.wantalert;"
  2014         -    "CREATE TEMP TABLE wantalert(eventId TEXT);"
         2352  +    "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);"
  2015   2353     );
  2016   2354     if( flags & SENDALERT_DIGEST ){
         2355  +    /* Unmoderated changes are never sent as part of a digest */
  2017   2356       db_multi_exec(
  2018         -      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
         2357  +      "INSERT INTO wantalert(eventId,needMod)"
         2358  +      " SELECT eventid, 0"
         2359  +      "   FROM pending_alert"
  2019   2360         "  WHERE sentDigest IS FALSE"
         2361  +      "    AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));"
  2020   2362       );
  2021   2363       zDigest = "true";
  2022   2364     }else{
         2365  +    /* Immediate alerts might include events that are subject to
         2366  +    ** moderator approval */
  2023   2367       db_multi_exec(
  2024         -      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
  2025         -      "  WHERE sentSep IS FALSE"
         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;"
  2026   2375       );
  2027   2376     }
  2028         -  pEvents = email_compute_event_text(&nEvent);
  2029         -  if( nEvent==0 ) goto send_alerts_done;
         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  +  */
  2030   2408     blob_init(&hdr, 0, 0);
  2031   2409     blob_init(&body, 0, 0);
  2032   2410     db_prepare(&q,
  2033   2411        "SELECT"
  2034   2412        " hex(subscriberCode),"  /* 0 */
  2035   2413        " semail,"               /* 1 */
  2036         -     " ssub"                  /* 2 */
  2037         -     " FROM subscriber"
         2414  +     " ssub,"                 /* 2 */
         2415  +     " fullcap(user.cap)"     /* 3 */
         2416  +     " FROM subscriber LEFT JOIN user ON (login=suname)"
  2038   2417        " WHERE sverified AND NOT sdonotcall"
  2039   2418        "  AND sdigest IS %s",
  2040   2419        zDigest/*safe-for-%s*/
  2041   2420     );
  2042   2421     while( db_step(&q)==SQLITE_ROW ){
  2043   2422       const char *zCode = db_column_text(&q, 0);
  2044   2423       const char *zSub = db_column_text(&q, 2);
  2045   2424       const char *zEmail = db_column_text(&q, 1);
         2425  +    const char *zCap = db_column_text(&q, 3);
  2046   2426       int nHit = 0;
  2047   2427       for(p=pEvents; p; p=p->pNext){
  2048   2428         if( strchr(zSub,p->type)==0 ) continue;
  2049         -      if( nHit==0 ){
  2050         -        blob_appendf(&hdr,"To: <%s>\r\n", zEmail);
  2051         -        blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName);
  2052         -        blob_appendf(&body,
  2053         -          "This is an automated email sent by the Fossil repository "
  2054         -          "at %s to report changes.\n",
  2055         -          zUrl
  2056         -        );
         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;
  2057   2453         }
  2058         -      nHit++;
  2059         -      blob_append(&body, "\n", 1);
  2060         -      blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
         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  +      }
  2061   2482       }
  2062   2483       if( nHit==0 ) continue;
  2063   2484       blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
  2064   2485            zUrl, zCode);
  2065         -    email_send(pSender,&hdr,&body);
         2486  +    alert_send(pSender,&hdr,&body,0);
  2066   2487       blob_truncate(&hdr, 0);
  2067   2488       blob_truncate(&body, 0);
  2068   2489     }
  2069   2490     blob_reset(&hdr);
  2070   2491     blob_reset(&body);
  2071   2492     db_finalize(&q);
  2072         -  email_free_eventlist(pEvents);
  2073         -  if( (flags & SENDALERT_PRESERVE)==0 ){
  2074         -    if( flags & SENDALERT_DIGEST ){
  2075         -      db_multi_exec("UPDATE pending_alert SET sentDigest=true");
  2076         -    }else{
  2077         -      db_multi_exec("UPDATE pending_alert SET sentSep=true");
  2078         -    }
  2079         -    db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
  2080         -  }
  2081         -send_alerts_done:
  2082         -  email_sender_free(pSender);
  2083         -  if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags);
  2084         -  db_end_transaction(0);
         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);
  2085   2503   }
  2086   2504   
  2087   2505   /*
  2088   2506   ** Do backoffice processing for email notifications.  In other words,
  2089   2507   ** check to see if any email notifications need to occur, and then
  2090   2508   ** do them.
  2091   2509   **
  2092   2510   ** This routine is intended to run in the background, after webpages.
  2093   2511   **
  2094   2512   ** The mFlags option is zero or more of the SENDALERT_* flags.  Normally
  2095   2513   ** this flag is zero, but the test-set-alert command sets it to
  2096   2514   ** SENDALERT_TRACE.
  2097   2515   */
  2098         -void email_backoffice(u32 mFlags){
         2516  +void alert_backoffice(u32 mFlags){
  2099   2517     int iJulianDay;
  2100         -  if( !email_tables_exist() ) return;
  2101         -  email_send_alerts(mFlags);
         2518  +  if( !alert_tables_exist() ) return;
         2519  +  alert_send_alerts(mFlags);
  2102   2520     iJulianDay = db_int(0, "SELECT julianday('now')");
  2103   2521     if( iJulianDay>db_get_int("email-last-digest",0) ){
  2104   2522       db_set_int("email-last-digest",iJulianDay,0);
  2105         -    email_send_alerts(SENDALERT_DIGEST|mFlags);
         2523  +    alert_send_alerts(SENDALERT_DIGEST|mFlags);
  2106   2524     }
  2107   2525   }
  2108   2526   
  2109   2527   /*
  2110   2528   ** WEBPAGE: contact_admin
  2111   2529   **
  2112   2530   ** A web-form to send an email message to the repository administrator,
................................................................................
  2129   2547      && P("subject")!=0
  2130   2548      && P("msg")!=0
  2131   2549      && P("from")!=0
  2132   2550      && cgi_csrf_safe(1)
  2133   2551      && captcha_is_correct(0)
  2134   2552     ){
  2135   2553       Blob hdr, body;
  2136         -    EmailSender *pSender = email_sender_new(0,0);
         2554  +    AlertSender *pSender = alert_sender_new(0,0);
  2137   2555       blob_init(&hdr, 0, 0);
  2138   2556       blob_appendf(&hdr, "To: <%s>\r\nSubject: %s administrator message\r\n",
  2139   2557                    zAdminEmail, db_get("email-subname","Fossil Repo"));
  2140   2558       blob_init(&body, 0, 0);
  2141   2559       blob_appendf(&body, "Message from [%s]\n", PT("from")/*safe-for-%s*/);
  2142   2560       blob_appendf(&body, "Subject: [%s]\n\n", PT("subject")/*safe-for-%s*/);
  2143   2561       blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  2144         -    email_send(pSender, &hdr, &body);
         2562  +    alert_send(pSender, &hdr, &body, 0);
  2145   2563       style_header("Message Sent");
  2146   2564       if( pSender->zErr ){
  2147   2565         @ <h1>Internal Error</h1>
  2148   2566         @ <p>The following error was reported by the system:
  2149   2567         @ <blockquote><pre>
  2150   2568         @ %h(pSender->zErr)
  2151   2569         @ </pre></blockquote>
  2152   2570       }else{
  2153   2571         @ <p>Your message has been sent to the repository administrator.
  2154   2572         @ Thank you for your input.</p>
  2155   2573       }
  2156         -    email_sender_free(pSender);
         2574  +    alert_sender_free(pSender);
  2157   2575       style_footer();
  2158   2576       return;
  2159   2577     }
  2160   2578     if( captcha_needed() ){
  2161   2579       uSeed = captcha_seed();
  2162   2580       zDecoded = captcha_decode(uSeed);
  2163   2581       zCaptcha = captcha_render(zDecoded);
................................................................................
  2189   2607     @ </tr>
  2190   2608     @ <tr>
  2191   2609     @   <td></td>
  2192   2610     @   <td><input type="submit" name="submit" value="Send Message">
  2193   2611     @ </tr>
  2194   2612     @ </table>
  2195   2613     if( zCaptcha ){
  2196         -    @ <div class="captcha"><table class="captcha"><tr><td><pre>
         2614  +    @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  2197   2615       @ %h(zCaptcha)
  2198   2616       @ </pre>
  2199   2617       @ Enter the 8 characters above in the "Security Code" box
  2200   2618       @ </td></tr></table></div>
  2201   2619     }
  2202   2620     @ </form>
  2203   2621     style_footer();
  2204   2622   }
  2205   2623   
  2206   2624   /*
  2207   2625   ** Send an annoucement message described by query parameter.
  2208   2626   ** Permission to do this has already been verified.
  2209   2627   */
  2210         -static char *email_send_announcement(void){
  2211         -  EmailSender *pSender;
         2628  +static char *alert_send_announcement(void){
         2629  +  AlertSender *pSender;
  2212   2630     char *zErr;
  2213   2631     const char *zTo = PT("to");
  2214   2632     char *zSubject = PT("subject");
  2215   2633     int bAll = PB("all");
  2216   2634     int bAA = PB("aa");
  2217   2635     const char *zSub = db_get("email-subname", "[Fossil Repo]");
  2218   2636     int bTest2 = fossil_strcmp(P("name"),"test2")==0;
  2219   2637     Blob hdr, body;
  2220   2638     blob_init(&body, 0, 0);
  2221   2639     blob_init(&hdr, 0, 0);
  2222   2640     blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  2223         -  pSender = email_sender_new(bTest2 ? "blob" : 0, 0);
         2641  +  pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
  2224   2642     if( zTo[0] ){
  2225   2643       blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
  2226         -    email_send(pSender, &hdr, &body);
         2644  +    alert_send(pSender, &hdr, &body, 0);
  2227   2645     }
  2228   2646     if( bAll || bAA ){
  2229   2647       Stmt q;
  2230   2648       int nUsed = blob_size(&body);
  2231   2649       const char *zURL =  db_get("email-url",0);
  2232   2650       db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
  2233   2651                      " WHERE sverified AND NOT sdonotcall %s",
................................................................................
  2238   2656         blob_truncate(&hdr, 0);
  2239   2657         blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
  2240   2658         if( zURL ){
  2241   2659           blob_truncate(&body, nUsed);
  2242   2660           blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
  2243   2661              zURL, zCode);
  2244   2662         }
  2245         -      email_send(pSender, &hdr, &body);
         2663  +      alert_send(pSender, &hdr, &body, 0);
  2246   2664       }
  2247   2665       db_finalize(&q);
  2248   2666     }
  2249   2667     if( bTest2 ){
  2250   2668       /* If the URL is /announce/test2 instead of just /announce, then no
  2251   2669       ** email is actually sent.  Instead, the text of the email that would
  2252   2670       ** have been sent is displayed in the result window. */
  2253   2671       @ <pre style='border: 2px solid blue; padding: 1ex'>
  2254   2672       @ %h(blob_str(&pSender->out))
  2255   2673       @ </pre>
  2256   2674     }
  2257   2675     zErr = pSender->zErr;
  2258   2676     pSender->zErr = 0;
  2259         -  email_sender_free(pSender);
         2677  +  alert_sender_free(pSender);
  2260   2678     return zErr;
  2261   2679   }
  2262   2680   
  2263   2681   
  2264   2682   /*
  2265   2683   ** WEBPAGE: announce
  2266   2684   **
................................................................................
  2280   2698     if( fossil_strcmp(P("name"),"test1")==0 ){
  2281   2699       /* Visit the /announce/test1 page to see the CGI variables */
  2282   2700       @ <p style='border: 1px solid black; padding: 1ex;'>
  2283   2701       cgi_print_all(0, 0);
  2284   2702       @ </p>
  2285   2703     }else
  2286   2704     if( P("submit")!=0 && cgi_csrf_safe(1) ){
  2287         -    char *zErr = email_send_announcement();
         2705  +    char *zErr = alert_send_announcement();
  2288   2706       style_header("Announcement Sent");
  2289   2707       if( zErr ){
  2290   2708         @ <h1>Internal Error</h1>
  2291   2709         @ <p>The following error was reported by the system:
  2292   2710         @ <blockquote><pre>
  2293   2711         @ %h(zErr)
  2294   2712         @ </pre></blockquote>

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         -    if( moderation_pending(attachid) ){
   112         -      @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
   113         -    }
          111  +    moderation_pending_www(attachid);
   114    112       @ <br /><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
   115         -    @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br />
          113  +    @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br>
   116    114       if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
   117    115       if( zComment && zComment[0] ){
   118    116         @ %!W(zComment)<br />
   119    117       }
   120    118       if( zPage==0 && zTkt==0 && zTechNote==0 ){
   121    119         if( zSrc==0 || zSrc[0]==0 ){
   122    120           zSrc = "Deleted from";
................................................................................
   562    560     @ <div class="section">Overview</div>
   563    561     @ <p><table class="label-value">
   564    562     @ <tr><th>Artifact&nbsp;ID:</th>
   565    563     @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
   566    564     if( g.perm.Setup ){
   567    565       @ (%d(rid))
   568    566     }
   569         -  modPending = moderation_pending(rid);
   570         -  if( modPending ){
   571         -    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
   572         -  }
          567  +  modPending = moderation_pending_www(rid);
   573    568     if( zTktUuid ){
   574    569       @ <tr><th>Ticket:</th>
   575    570       @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
   576    571     }
   577    572     if( zTNUuid ){
   578    573       @ <tr><th>Tech Note:</th>
   579    574       @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>

Changes to src/backoffice.c.

    33     33   **
    34     34   ** At the same time, we do not want a backoffice process to run forever.
    35     35   ** Backoffice processes should die off after doing whatever work they need
    36     36   ** to do.  In this way, we avoid having lots of idle processes in the
    37     37   ** process table, doing nothing on rarely accessed repositories, and
    38     38   ** if the Fossil binary is updated on a system, the backoffice processes
    39     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.
    40     59   */
           60  +#if defined(_WIN32)
           61  +# if defined(_WIN32_WINNT)
           62  +#  undef _WIN32_WINNT
           63  +# endif
           64  +# define _WIN32_WINNT 0x501
           65  +#endif
    41     66   #include "config.h"
    42     67   #include "backoffice.h"
    43     68   #include <time.h>
    44     69   #if defined(_WIN32)
    45     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
    46     77   #else
    47     78   # include <unistd.h>
    48     79   # include <sys/types.h>
    49     80   # include <signal.h>
           81  +# include <errno.h>
           82  +# include <fcntl.h>
           83  +# define GETPID getpid
    50     84   #endif
    51     85   
    52     86   /*
    53     87   ** The BKOFCE_LEASE_TIME is the amount of time for which a single backoffice
    54     88   ** processing run is valid.  Each backoffice run monopolizes the lease for
    55     89   ** at least this amount of time.  Hopefully all backoffice processing is
    56     90   ** finished much faster than this - usually in less than a second.  But
    57         -** regardless of how fast each invocations run, successive backoffice runs
           91  +** regardless of how long each invocation lasts, successive backoffice runs
    58     92   ** must be spaced out by at least this much time.
    59     93   */
    60         -#define BKOFCE_LEASE_TIME   60    /* Length of lease validity */
           94  +#define BKOFCE_LEASE_TIME   60    /* Length of lease validity in seconds */
    61     95   
    62     96   #if LOCAL_INTERFACE
    63     97   /*
    64     98   ** An instance of the following object describes a lease on the backoffice
    65     99   ** processing timeslot.  This lease is used to help ensure that no more than
    66         -** one processing is running backoffice at a time.
          100  +** one process is running backoffice at a time.
    67    101   */
    68    102   struct Lease {
    69         -  sqlite3_uint64 idCurrent;   /* ID for the current lease holder */
    70         -  sqlite3_uint64 tmCurrent;   /* Expiration of the current lease */
    71         -  sqlite3_uint64 idNext;      /* ID for the next lease holder on queue */
    72         -  sqlite3_uint64 tmNext;      /* Expiration of the next 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 */
    73    107   };
    74    108   #endif
    75    109   
    76         -/*
    77         -** Set to prevent backoffice processing from every entering sleep or
          110  +/***************************************************************************
          111  +** Local state variables
          112  +**
          113  +** Set to prevent backoffice processing from ever entering sleep or
    78    114   ** otherwise taking a long time to complete.  Set this when a user-visible
    79    115   ** process might need to wait for backoffice to complete.
    80    116   */
    81    117   static int backofficeNoDelay = 0;
    82    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  +****************************************************************************/
    83    130   
    84    131   /*
    85         -** Disable the backoffice
          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.
    86    164   */
    87    165   void backoffice_no_delay(void){
    88    166     backofficeNoDelay = 1;
    89    167   }
    90    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  +}
    91    185   
    92    186   /*
    93    187   ** Parse a unsigned 64-bit integer from a string.  Return a pointer
    94    188   ** to the character of z[] that occurs after the integer.
    95    189   */
    96    190   static const char *backofficeParseInt(const char *z, sqlite3_uint64 *pVal){
    97    191     *pVal = 0;
................................................................................
   102    196       z++;
   103    197     }
   104    198     return z;
   105    199   }
   106    200   
   107    201   /*
   108    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).
   109    214   */
   110    215   static void backofficeReadLease(Lease *pLease){
   111    216     Stmt q;
   112    217     memset(pLease, 0, sizeof(*pLease));
   113    218     db_prepare(&q, "SELECT value FROM repository.config"
   114    219                    " WHERE name='backoffice'");
   115    220     if( db_step(&q)==SQLITE_ROW ){
................................................................................
   117    222       z = backofficeParseInt(z, &pLease->idCurrent);
   118    223       z = backofficeParseInt(z, &pLease->tmCurrent);
   119    224       z = backofficeParseInt(z, &pLease->idNext);
   120    225       backofficeParseInt(z, &pLease->tmNext);
   121    226     }
   122    227     db_finalize(&q);
   123    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  +}
   124    249   
   125    250   /*
   126    251   ** Write a lease to the backoffice property
   127    252   */
   128    253   static void backofficeWriteLease(Lease *pLease){
   129    254     db_multi_exec(
   130    255       "REPLACE INTO repository.config(name,value,mtime)"
   131    256       " VALUES('backoffice','%lld %lld %lld %lld',now())",
   132    257       pLease->idCurrent, pLease->tmCurrent,
   133    258       pLease->idNext, pLease->tmNext);
   134    259   }
   135    260   
   136    261   /*
   137         -** Check to see if the process identified by selfId is alive.  If
          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
   138    281   ** we cannot prove the the process is dead, return true.
   139    282   */
   140    283   static int backofficeProcessExists(sqlite3_uint64 pid){
   141    284   #if defined(_WIN32)
   142         -  return 1;
          285  +  return pid>0 && backofficeWin32ProcessExists((DWORD)pid)!=0;
   143    286   #else
   144    287     return pid>0 && kill((pid_t)pid, 0)==0;
   145    288   #endif 
   146    289   }
   147    290   
   148    291   /*
   149         -** Check to see if the process identified by selfId has finished.  If
          292  +** Check to see if the process identified by pid has finished.  If
   150    293   ** we cannot prove the the process is still running, return true.
   151    294   */
   152    295   static int backofficeProcessDone(sqlite3_uint64 pid){
   153    296   #if defined(_WIN32)
   154         -  return 1;
          297  +  return pid<=0 || backofficeWin32ProcessExists((DWORD)pid)==0;
   155    298   #else
   156    299     return pid<=0 || kill((pid_t)pid, 0)!=0;
   157    300   #endif 
   158    301   }
   159    302   
   160    303   /*
   161    304   ** Return a process id number for the current process
   162    305   */
   163    306   static sqlite3_uint64 backofficeProcessId(void){
   164         -#if defined(_WIN32)
   165         -  return (sqlite3_uint64)GetCurrentProcessId();
   166         -#else
   167         -  return (sqlite3_uint64)getpid();
   168         -#endif
   169         -}
   170         -
   171         -/*
   172         -** Set an alarm to cause the process to exit after "x" seconds.  This
   173         -** prevents any kind of bug from keeping a backoffice process running
   174         -** indefinitely.
   175         -*/
   176         -#if !defined(_WIN32)
   177         -static void backofficeSigalrmHandler(int x){
   178         -  fossil_panic("backoffice timeout");
   179         -}
   180         -#endif
   181         -static void backofficeTimeout(int x){
   182         -#if !defined(_WIN32)
   183         -  signal(SIGALRM, backofficeSigalrmHandler);
   184         -  alarm(x);
   185         -#endif
          307  +  return (sqlite3_uint64)GETPID();
   186    308   }
   187    309   
   188    310   
   189    311   /*
   190    312   ** COMMAND: test-process-id
   191    313   **
   192    314   ** Usage: %fossil [--sleep N] PROCESS-ID ...
................................................................................
   205    327       sqlite3_uint64 x = (sqlite3_uint64)atoi(g.argv[i]);
   206    328       fossil_print("ProcessId %lld: exists %d done %d\n",
   207    329                    x, backofficeProcessExists(x),
   208    330                       backofficeProcessDone(x));
   209    331     }
   210    332   }
   211    333   
   212         -/* This is the main public interface to the backoffice.  A process invokes this
   213         -** routine in an attempt to become the backoffice.  If another process is
   214         -** already working as the backoffice, this routine returns very quickly
   215         -** without doing any work - allowing the other process to continue.  But
   216         -** if no other processes are currently operating as the backoffice, this
   217         -** routine enters a loop to do background work periodically.
          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.
   218    378   */
   219         -void backoffice_run(void){
          379  +void backoffice_check_if_needed(void){
   220    380     Lease x;
   221    381     sqlite3_uint64 tmNow;
   222         -  sqlite3_uint64 idSelf;
   223         -  int lastWarning = 0;
   224         -  int warningDelay = 30;
   225         -  static int once = 0;
          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  +}
   226    398   
   227         -  if( once ){
   228         -    fossil_panic("multiple calls to backoffice_run()");
          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()");
   229    405     }
   230         -  once = 1;
          406  +  *pOnce = 1;
   231    407     if( g.db==0 ){
   232    408       fossil_panic("database not open for backoffice processing");
   233    409     }
   234    410     if( db_transaction_nesting_depth()!=0 ){
   235    411       fossil_panic("transaction %s not closed prior to backoffice processing",
   236    412                    db_transaction_start_point());
   237    413     }
   238         -  backofficeTimeout(BKOFCE_LEASE_TIME*2);
          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);
   239    437     idSelf = backofficeProcessId();
   240    438     while(1){
   241    439       tmNow = time(0);
   242    440       db_begin_write();
   243    441       backofficeReadLease(&x);
   244    442       if( x.tmNext>=tmNow
   245    443        && x.idNext!=idSelf
................................................................................
   255    453         /* This process can start doing backoffice work immediately */
   256    454         x.idCurrent = idSelf;
   257    455         x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
   258    456         x.idNext = 0;
   259    457         x.tmNext = 0;
   260    458         backofficeWriteLease(&x);
   261    459         db_end_transaction(0);
   262         -      if( g.fAnyTrace ){
   263         -        fprintf(stderr, "/***** Begin Backoffice Processing %d *****/\n",
   264         -                        getpid());
   265         -      }
          460  +      backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
          461  +                      GETPID());
   266    462         backoffice_work();
   267    463         break;
   268    464       }
   269         -    if( backofficeNoDelay ){
          465  +    if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",0) ){
   270    466         /* If the no-delay flag is set, exit immediately rather than queuing
   271    467         ** up.  Assume that some future request will come along and handle any
   272    468         ** necessary backoffice work. */
   273    469         db_end_transaction(0);
   274    470         break;
   275    471       }
   276    472       /* This process needs to queue up and wait for the current lease
   277    473       ** to expire before continuing. */
   278    474       x.idNext = idSelf;
   279    475       x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
   280    476       backofficeWriteLease(&x);
   281    477       db_end_transaction(0);
   282         -    if( g.fAnyTrace ){
   283         -      fprintf(stderr, "/***** Backoffice On-deck %d *****/\n",  getpid());
   284         -    }
          478  +    backofficeTrace("/***** Backoffice On-deck %d *****/\n",  GETPID());
   285    479       if( x.tmCurrent >= tmNow ){
   286         -      sqlite3_sleep(1000*(x.tmCurrent - tmNow + 1));
          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  +      }
   287    486       }else{
   288    487         if( lastWarning+warningDelay < tmNow ){
   289    488           fossil_warning(
   290    489              "backoffice process %lld still running after %d seconds",
   291    490              x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
   292    491           lastWarning = tmNow;
   293    492           warningDelay *= 2;
   294    493         }
   295         -      sqlite3_sleep(1000);
          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  +      }
   296    500       }
   297    501     }
   298    502     return;
   299    503   }
   300    504   
   301    505   /*
   302    506   ** This routine runs to do the backoffice processing.  When adding new
   303    507   ** backoffice processing tasks, add them here.
   304    508   */
   305    509   void backoffice_work(void){
   306         -  email_backoffice(0);
          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();
   307    541   }
   308    542   
   309    543   /*
   310         -** COMMAND: test-backoffice
   311         -**
   312         -** Usage: test-backoffice
   313         -**
   314         -** Run backoffice processing
          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.
   315    547   */
   316         -void test_backoffice_command(void){
   317         -  db_find_and_open_repository(0,0);
   318         -  backoffice_run();
          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);
   319    610   }

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

Added 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.

   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>
          544  +  @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
   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">

Changes to src/cgi.c.

   343    343     CGIDEBUG(("DONE\n"));
   344    344   
   345    345     /* After the webpage has been sent, do any useful background
   346    346     ** processing.
   347    347     */
   348    348     g.cgiOutput = 2;
   349    349     if( g.db!=0 && iReplyStatus==200 ){
   350         -    fclose(g.httpOut);
   351         -#ifdef _WIN32
   352         -    g.httpOut = fossil_fopen("NUL", "wb");
   353         -#else
   354         -    g.httpOut = fossil_fopen("/dev/null", "wb");
   355         -#endif
   356         -    if( g.httpOut==0 ){
   357         -      fossil_warning("failed ot open /dev/null");
   358         -    }else{
   359         -      backoffice_run();
   360         -    }
          350  +    backoffice_check_if_needed();
   361    351     }
   362    352   }
   363    353   
   364    354   /*
   365    355   ** Do a redirect request to the URL given in the argument.
   366    356   **
   367    357   ** The URL must be relative to the base of the fossil server.
................................................................................
  1118   1108     }
  1119   1109     CGIDEBUG(("no-match [%s]\n", zName));
  1120   1110     return zDefault;
  1121   1111   }
  1122   1112   
  1123   1113   /*
  1124   1114   ** Return the value of a CGI parameter with leading and trailing
  1125         -** spaces removed.
         1115  +** spaces removed and with internal \r\n changed to just \n
  1126   1116   */
  1127   1117   char *cgi_parameter_trimmed(const char *zName, const char *zDefault){
  1128   1118     const char *zIn;
  1129         -  char *zOut;
  1130         -  int i;
         1119  +  char *zOut, c;
         1120  +  int i, j;
  1131   1121     zIn = cgi_parameter(zName, 0);
  1132   1122     if( zIn==0 ) zIn = zDefault;
  1133   1123     if( zIn==0 ) return 0;
  1134   1124     while( fossil_isspace(zIn[0]) ) zIn++;
  1135   1125     zOut = fossil_strdup(zIn);
  1136         -  for(i=0; zOut[i]; i++){}
  1137         -  while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0;
         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;
  1138   1132     return zOut;
  1139   1133   }
  1140   1134   
  1141   1135   /*
  1142   1136   ** Return true if the CGI parameter zName exists and is not equal to 0,
  1143   1137   ** or "no" or "off".
  1144   1138   */

Changes to src/checkin.c.

  2123   2123     if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
  2124   2124     useCksum = db_get_boolean("repo-cksum", 1);
  2125   2125     outputManifest = db_get_manifest_setting();
  2126   2126     verify_all_options();
  2127   2127   
  2128   2128     /* Do not allow the creation of a new branch using an existing open
  2129   2129     ** branch name unless the --force flag is used */
  2130         -  if( sCiInfo.zBranch!=0 && !forceFlag && branch_is_open(sCiInfo.zBranch) ){
         2130  +  if( sCiInfo.zBranch!=0
         2131  +   && !forceFlag
         2132  +   && fossil_strcmp(sCiInfo.zBranch,"private")!=0
         2133  +   && branch_is_open(sCiInfo.zBranch)
         2134  +  ){
  2131   2135       fossil_fatal("an open branch named \"%s\" already exists - use --force"
  2132   2136                    " to override", sCiInfo.zBranch);
  2133   2137     }
  2134   2138   
  2135   2139     /* Escape special characters in tags and put all tags in sorted order */
  2136   2140     if( nTag ){
  2137   2141       int i;

Changes to src/codecheck1.c.

   396    396     { "json_new_string_f",       1, 0 },
   397    397     { "json_set_err",            2, 0 },
   398    398     { "json_warn",               2, 0 },
   399    399     { "mprintf",                 1, 0 },
   400    400     { "socket_set_errmsg",       1, 0 },
   401    401     { "ssl_set_errmsg",          1, 0 },
   402    402     { "style_header",            1, FMT_HTML },
          403  +  { "style_js_onload",         1, FMT_HTML },
   403    404     { "style_set_current_page",  1, FMT_URL },
   404    405     { "style_submenu_element",   2, FMT_URL },
   405    406     { "style_submenu_sql",       3, FMT_SQL },
   406    407     { "webpage_error",           1, FMT_SAFE },
   407    408     { "xhref",                   2, FMT_URL },
   408    409   };
   409    410   

Changes to src/configure.c.

    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     41   #define CONFIGSET_SCRIBER   0x000200     /* Email subscribers */
    42         -#define CONFIGSET_FORUM     0x000400     /* Forum posts */
    43         -
    44         -#define CONFIGSET_ALL       0x0007ff     /* Everything */
           42  +#define CONFIGSET_ALL       0x0003ff     /* Everything */
    45     43   
    46     44   #define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
    47     45   
    48     46   /*
    49     47   ** This mask is used for the common TH1 configuration settings (i.e. those
    50     48   ** that are not specific to one particular subsystem, such as the transfer
    51     49   ** subsystem).
................................................................................
    68     66                                        "Web interface appearance settings"    },
    69     67     { "/css",         CONFIGSET_CSS,   "Style sheet"                          },
    70     68     { "/shun",        CONFIGSET_SHUN,  "List of shunned artifacts"            },
    71     69     { "/ticket",      CONFIGSET_TKT,   "Ticket setup",                        },
    72     70     { "/user",        CONFIGSET_USER,  "Users and privilege settings"         },
    73     71     { "/xfer",        CONFIGSET_XFER,  "Transfer setup",                      },
    74     72     { "/alias",       CONFIGSET_ALIAS, "URL Aliases",                         },
    75         -  { "/subscriber",  CONFIGSET_SCRIBER,"Email notification subscriber list" },
    76         -/*{ "/forum",       CONFIGSET_FORUM, "Forum posts",                         },*/
           73  +  { "/subscriber",  CONFIGSET_SCRIBER,"Email notification subscriber list"  },
    77     74     { "/all",         CONFIGSET_ALL,   "All of the above"                     },
    78     75   };
    79     76   
    80     77   
    81     78   /*
    82     79   ** The following is a list of settings that we are willing to
    83     80   ** transfer.
................................................................................
   100     97     { "background-image",       CONFIGSET_SKIN },
   101     98     { "timeline-block-markup",  CONFIGSET_SKIN },
   102     99     { "timeline-max-comment",   CONFIGSET_SKIN },
   103    100     { "timeline-plaintext",     CONFIGSET_SKIN },
   104    101     { "adunit",                 CONFIGSET_SKIN },
   105    102     { "adunit-omit-if-admin",   CONFIGSET_SKIN },
   106    103     { "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 },
   107    108   
   108    109   #ifdef FOSSIL_ENABLE_TH1_DOCS
   109    110     { "th1-docs",               CONFIGSET_TH1 },
   110    111   #endif
   111    112   #ifdef FOSSIL_ENABLE_TH1_HOOKS
   112    113     { "th1-hooks",              CONFIGSET_TH1 },
   113    114   #endif
................................................................................
   235    236     }
   236    237     for(i=0; i<count(aConfig); i++){
   237    238       if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
   238    239         int m = aConfig[i].groupMask;
   239    240         if( !g.perm.Admin ){
   240    241           m &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER);
   241    242         }
   242         -      if( !g.perm.RdForum ){
   243         -        m &= ~(CONFIGSET_FORUM);
   244         -      }
   245    243         if( !g.perm.RdAddr ){
   246    244           m &= ~CONFIGSET_ADDR;
   247    245         }
   248    246         return m;
   249    247       }
   250    248     }
   251    249     if( strncmp(zName, "walias:/", 8)==0 ){
................................................................................
   402    400         thisMask = configure_is_exportable(azToken[1]);
   403    401       }else{
   404    402         thisMask = configure_is_exportable(aType[ii].zName);
   405    403       }
   406    404       if( (thisMask & groupMask)==0 ) return;
   407    405       if( (thisMask & checkMask)!=0 ){
   408    406         if( (thisMask & CONFIGSET_SCRIBER)!=0 ){
   409         -        email_schema(1);
          407  +        alert_schema(1);
   410    408         }
   411    409         checkMask &= ~thisMask;
   412    410       }
   413    411   
   414    412       blob_zero(&sql);
   415    413       if( groupMask & CONFIGSET_OVERWRITE ){
   416    414         if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
................................................................................
   419    417         }
   420    418         blob_append_sql(&sql, "REPLACE INTO ");
   421    419       }else{
   422    420         blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
   423    421       }
   424    422       blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
   425    423            &zName[1], aType[ii].zPrimKey);
   426         -    if( fossil_stricmp(zName,"/subscriber") ) email_schema(0);
          424  +    if( fossil_stricmp(zName,"/subscriber") ) alert_schema(0);
   427    425       for(jj=2; jj<nToken; jj+=2){
   428    426          blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
   429    427       }
   430    428       blob_append_sql(&sql,") VALUES(%s,%s",
   431    429          azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
   432    430       for(jj=2; jj<nToken; jj+=2){
   433    431          blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
................................................................................
   695    693   **
   696    694   ** Where METHOD is one of: export import merge pull push reset.  All methods
   697    695   ** accept the -R or --repository option to specify a repository.
   698    696   **
   699    697   **    %fossil configuration export AREA FILENAME
   700    698   **
   701    699   **         Write to FILENAME exported configuration information for AREA.
   702         -**         AREA can be one of:  all email project shun skin ticket user alias
          700  +**         AREA can be one of:
          701  +**
          702  +**             all email project shun skin ticket user alias subscriber
   703    703   **
   704    704   **    %fossil configuration import FILENAME
   705    705   **
   706    706   **         Read a configuration from FILENAME, overwriting the current
   707    707   **         configuration.
   708    708   **
   709    709   **    %fossil configuration merge FILENAME

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;
    76     75     va_list ap;
    77     76     char *z;
    78         -  int rc = 1;
    79         -  if( rcLooping ) exit(rcLooping);
    80     77     va_start(ap, zFormat);
    81     78     z = vmprintf(zFormat, ap);
    82     79     va_end(ap);
    83     80   #ifdef FOSSIL_ENABLE_JSON
    84     81     if( g.json.isJsonMode ){
    85     82       json_err( 0, z, 1 );
    86         -    if( g.isHTTP ){
    87         -      rc = 0 /* avoid HTTP 500 */;
    88         -    }
    89     83     }
    90     84     else
    91     85   #endif /* FOSSIL_ENABLE_JSON */
    92         -  if( g.xferPanic ){
           86  +  if( g.xferPanic && g.cgiOutput==1 ){
    93     87       cgi_reset_content();
    94     88       @ 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);
   100     89       cgi_reply();
   101         -  }else{
   102         -    fprintf(stderr, "%s: %s\n", g.argv[0], z);
   103     90     }
   104         -  free(z);
   105         -  rcLooping = rc;
   106         -  db_force_rollback();
   107         -  fossil_exit(rc);
           91  +  fossil_panic("Database error: %s", z);
   108     92   }
   109     93   
   110     94   /*
   111     95   ** All static variable that a used by only this file are gathered into
   112     96   ** the following structure.
   113     97   */
   114     98   static struct DbLocalData {
................................................................................
   490    474     db_stats(pStmt);
   491    475     rc = sqlite3_reset(pStmt->pStmt);
   492    476     db_check_result(rc);
   493    477     return rc;
   494    478   }
   495    479   int db_finalize(Stmt *pStmt){
   496    480     int rc;
   497         -  db_stats(pStmt);
   498         -  blob_reset(&pStmt->sql);
   499         -  rc = sqlite3_finalize(pStmt->pStmt);
   500         -  db_check_result(rc);
   501         -  pStmt->pStmt = 0;
   502    481     if( pStmt->pNext ){
   503    482       pStmt->pNext->pPrev = pStmt->pPrev;
   504    483     }
   505    484     if( pStmt->pPrev ){
   506    485       pStmt->pPrev->pNext = pStmt->pNext;
   507    486     }else if( db.pAllStmt==pStmt ){
   508    487       db.pAllStmt = pStmt->pNext;
   509    488     }
   510    489     pStmt->pNext = 0;
   511    490     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;
   512    496     return rc;
   513    497   }
   514    498   
   515    499   /*
   516    500   ** Return the rowid of the most recent insert
   517    501   */
   518    502   int db_last_insert_rowid(void){
................................................................................
   982    966   ){
   983    967     const unsigned char *zIn = sqlite3_value_text(argv[0]);
   984    968     int nIn = sqlite3_value_bytes(argv[0]);
   985    969     unsigned char *zOut;
   986    970     if( zIn==0 ) return;
   987    971     if( nIn&1 ) return;
   988    972     if( !validate16((const char*)zIn, nIn) ) return;
   989         -  zOut = sqlite3_malloc64( nIn/2 );
          973  +  zOut = sqlite3_malloc64( nIn/2 + 1 );
   990    974     if( zOut==0 ){
   991    975       sqlite3_result_error_nomem(context);
   992    976       return;
   993    977     }
   994    978     decode16(zIn, zOut, nIn);
   995    979     sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free);
   996    980   }
................................................................................
  1010    994                             db_now_function, 0, 0);
  1011    995     sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
  1012    996                             db_tolocal_function, 0, 0);
  1013    997     sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
  1014    998                             db_fromlocal_function, 0, 0);
  1015    999     sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
  1016   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);
  1017   1007   }
  1018   1008   
  1019   1009   #if USE_SEE
  1020   1010   /*
  1021   1011   ** This is a pointer to the saved database encryption key string.
  1022   1012   */
  1023   1013   static char *zSavedKey = 0;
................................................................................
  1630   1620       }
  1631   1621     }
  1632   1622     if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){
  1633   1623       if( file_access(zDbName, F_OK) ){
  1634   1624   #ifdef FOSSIL_ENABLE_JSON
  1635   1625         g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
  1636   1626   #endif
  1637         -      fossil_panic("repository does not exist or"
         1627  +      fossil_fatal("repository does not exist or"
  1638   1628                      " is in an unreadable directory: %s", zDbName);
  1639   1629       }else if( file_access(zDbName, R_OK) ){
  1640   1630   #ifdef FOSSIL_ENABLE_JSON
  1641   1631         g.json.resultCode = FSL_JSON_E_DENIED;
  1642   1632   #endif
  1643         -      fossil_panic("read permission denied for repository %s", zDbName);
         1633  +      fossil_fatal("read permission denied for repository %s", zDbName);
  1644   1634       }else{
  1645   1635   #ifdef FOSSIL_ENABLE_JSON
  1646   1636         g.json.resultCode = FSL_JSON_E_DB_NOT_VALID;
  1647   1637   #endif
  1648         -      fossil_panic("not a valid repository: %s", zDbName);
         1638  +      fossil_fatal("not a valid repository: %s", zDbName);
  1649   1639       }
  1650   1640     }
  1651   1641     g.zRepositoryName = mprintf("%s", zDbName);
  1652   1642     db_open_or_attach(g.zRepositoryName, "repository");
  1653   1643     g.repositoryOpen = 1;
  1654   1644     sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION,
  1655   1645                          &g.iRepoDataVers);
................................................................................
  1723   1713     }
  1724   1714   rep_not_found:
  1725   1715     if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){
  1726   1716   #ifdef FOSSIL_ENABLE_JSON
  1727   1717       g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
  1728   1718   #endif
  1729   1719       if( nArgUsed==0 ){
  1730         -      fossil_panic("use --repository or -R to specify the repository database");
         1720  +      fossil_fatal("use --repository or -R to specify the repository database");
  1731   1721       }else{
  1732         -      fossil_panic("specify the repository name as a command-line argument");
         1722  +      fossil_fatal("specify the repository name as a command-line argument");
  1733   1723       }
  1734   1724     }
  1735   1725   }
  1736   1726   
  1737   1727   /*
  1738   1728   ** Return TRUE if the schema is out-of-date
  1739   1729   */
................................................................................
  1885   1875       }
  1886   1876       g.db = 0;
  1887   1877     }
  1888   1878     g.repositoryOpen = 0;
  1889   1879     g.localOpen = 0;
  1890   1880     assert( g.dbConfig==0 );
  1891   1881     assert( g.zConfigDbName==0 );
         1882  +  backoffice_run_if_needed();
  1892   1883   }
  1893   1884   
  1894   1885   /*
  1895   1886   ** Close the database as quickly as possible without unnecessary processing.
  1896   1887   */
  1897   1888   void db_panic_close(void){
  1898   1889     if( g.db ){
................................................................................
  3031   3022   */
  3032   3023   /*
  3033   3024   ** SETTING: autosync-tries  width=16 default=1
  3034   3025   ** If autosync is enabled setting this to a value greater
  3035   3026   ** than zero will cause autosync to try no more than this
  3036   3027   ** number of attempts if there is a sync failure.
  3037   3028   */
         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  +*/
  3038   3042   /*
  3039   3043   ** SETTING: binary-glob     width=40 versionable block-text
  3040   3044   ** The VALUE of this setting is a comma or newline-separated list of
  3041   3045   ** GLOB patterns that should be treated as binary files
  3042   3046   ** for committing and merging purposes.  Example: *.jpg
  3043   3047   */
  3044   3048   #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         -table.browser {
   190         -  width: 100%;
   191         -  border: 0;
          189  +div.columns {
          190  +  padding: 0 2em 0 2em;
          191  +  max-width: 1000px;
   192    192   }
   193         -td.browser {
   194         -  width: 24%;
   195         -  vertical-align: top;
          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;
   196    202   }
   197    203   .filetree {
   198    204     margin: 1em 0;
   199    205     line-height: 1.5;
   200    206   }
   201    207   .filetree > ul {
   202    208     display: inline-block;
................................................................................
   298    304   table.captcha {
   299    305     margin: auto;
   300    306     padding: 10px;
   301    307     border-width: 4px;
   302    308     border-style: double;
   303    309     border-color: black;
   304    310   }
          311  +pre.captcha {
          312  +  font-size: 50%;
          313  +}
   305    314   td.login_out_label {
   306    315     text-align: center;
   307    316   }
   308    317   span.loginError {
   309    318     color: red;
   310    319   }
   311    320   span.note {
................................................................................
   347    356     text-align: center;
   348    357     padding-right: 15px;
   349    358   }
   350    359   td.usetupListCon {
   351    360     text-align: left
   352    361   }
   353    362   div.ueditCapBox {
   354         -  float: left;
   355    363     margin-right: 20px;
   356    364     margin-bottom: 20px;
   357    365   }
   358    366   td.usetupEditLabel {
   359    367     text-align: right;
   360    368     vertical-align: top;
   361    369     white-space: nowrap;
................................................................................
   532    540     word-wrap: break-word;
   533    541   }
   534    542   pre.th1error {
   535    543     white-space: pre-wrap;
   536    544     word-wrap: break-word;
   537    545     color: red;
   538    546   }
          547  +pre.textPlain {
          548  +  white-space: pre-wrap;
          549  +  word-wrap: break-word;
          550  +}
   539    551   .statistics-report-graph-line {
   540    552     background-color: #446979;
   541    553   }
   542    554   .statistics-report-table-events th {
   543    555     padding: 0 1em 0 1em;
   544    556   }
   545    557   .statistics-report-table-events td {
................................................................................
   670    682   div.forum_body p {
   671    683     margin-top: 0;
   672    684   }
   673    685   td.form_label {
   674    686     vertical-align: top;
   675    687     text-align: right;
   676    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/dispatch.c.

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

Changes to src/doc.c.

   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  +  }
   472    476     if( !db_table_exists("repository", "vcache") || !writable ){
   473    477       db_multi_exec(
   474    478         "CREATE %s TABLE IF NOT EXISTS vcache(\n"
   475    479         "  vid INTEGER,         -- check-in ID\n"
   476    480         "  fname TEXT,          -- filename\n"
   477    481         "  rid INTEGER,         -- artifact ID\n"
   478    482         "  PRIMARY KEY(vid,fname)\n"

Changes to src/forum.c.

    18     18   ** This file contains code used to generate the user forum.
    19     19   */
    20     20   #include "config.h"
    21     21   #include <assert.h>
    22     22   #include "forum.h"
    23     23   
    24     24   /*
    25         -** The schema for the tables that manage the forum, if forum is
    26         -** enabled.
           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.
    27    713   */
    28         -static const char zForumInit[] = 
    29         -@ CREATE TABLE repository.forumpost(
    30         -@   mpostid INTEGER PRIMARY KEY,  -- unique id for each post (local)
    31         -@   mposthash TEXT,               -- uuid for this post
    32         -@   mthreadid INTEGER,            -- thread to which this post belongs
    33         -@   uname TEXT,                   -- name of user
    34         -@   mtime REAL,                   -- julian day number
    35         -@   mstatus TEXT,                 -- status.  NULL=ok. 'mod'=pending moderation
    36         -@   mimetype TEXT,                -- Mimetype for mbody
    37         -@   ipaddr TEXT,                  -- IP address of post origin
    38         -@   inreplyto INT,                -- Parent posting
    39         -@   mbody TEXT                    -- Content of the post
    40         -@ );
    41         -@ CREATE INDEX repository.forumpost_x1 ON
    42         -@   forumpost(inreplyto,mtime);
    43         -@ CREATE TABLE repository.forumthread(
    44         -@   mthreadid INTEGER PRIMARY KEY,
    45         -@   mthreadhash TEXT,             -- uuid for this thread
    46         -@   mtitle TEXT,                  -- Title or subject line
    47         -@   mtime REAL,                   -- Most recent update
    48         -@   npost INT                     -- Number of posts on this thread
    49         -@ );
    50         -;
          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  +}
    51    721   
    52    722   /*
    53         -** Create the forum tables in the schema if they do not already
    54         -** exist.
          723  +** WEBPAGE: forume1
          724  +**
          725  +** Start a new forum thread.
    55    726   */
    56         -static void forum_verify_schema(void){
    57         -  if( !db_table_exists("repository","forumpost") ){
    58         -    db_multi_exec(zForumInit /*works-like:""*/);
          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;
    59    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();
    60    767   }
    61    768   
    62    769   /*
    63         -** WEBPAGE: forum
    64         -** URL: /forum
          770  +** WEBPAGE: forume2
          771  +**
          772  +** Edit an existing forum message.
    65    773   ** Query parameters:
    66    774   **
    67         -**    item=N             Show post N and its replies
    68         -**    
          775  +**   fpid=X        Hash of the post to be editted.  REQUIRED
    69    776   */
    70         -void forum_page(void){
    71         -  int itemId;
    72         -  Stmt q;
    73         -  int i;
          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;
    74    785   
    75    786     login_check_credentials();
    76         -  if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
    77         -  forum_verify_schema();
    78         -  style_header("Forum");
    79         -  itemId = atoi(PD("item","0"));
    80         -  if( itemId>0 ){
    81         -    int iUp;
    82         -    double rNow;
    83         -    style_submenu_element("Topics", "%R/forum");
    84         -    iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId);
    85         -    if( iUp ){
    86         -      style_submenu_element("Parent", "%R/forum?item=%d", iUp);
          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;
    87    805       }
    88         -    rNow = db_double(0.0, "SELECT julianday('now')");
    89         -    /* Show the post given by itemId and all its descendents */
    90         -    db_prepare(&q,
    91         -      "WITH RECURSIVE"
    92         -      " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS ("
    93         -      "    SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody,"
    94         -      "           0, mtime FROM forumpost WHERE mpostid=%d"
    95         -      "  UNION"
    96         -      "  SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr,"
    97         -      "         f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime"
    98         -      "    FROM forumpost AS f, post AS p"
    99         -      "   WHERE f.inreplyto=p.id"
   100         -      "   ORDER BY xdepth DESC, xtime ASC"
   101         -      ") SELECT * FROM post;",
   102         -      itemId
   103         -    );
   104         -    while( db_step(&q)==SQLITE_ROW ){
   105         -      int id = db_column_int(&q, 0);
   106         -      const char *zUser = db_column_text(&q, 1);
   107         -      const char *zMime = db_column_text(&q, 3);
   108         -      int iDepth = db_column_int(&q, 7);
   109         -      double rMTime = db_column_double(&q, 8);
   110         -      char *zAge = db_timespan_name(rNow - rMTime);
   111         -      Blob body;
   112         -      @ <!-- Forum post %d(id) -->
   113         -      @ <table class="forum_post">
   114         -      @ <tr>
   115         -      @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2">
   116         -      @ <td><span class="forum_author">%h(zUser)</span>
   117         -      @ <span class="forum_age">%s(zAge) ago</span>
   118         -      sqlite3_free(zAge);
   119         -      if( g.perm.WrForum ){
   120         -        @ <span class="forum_buttons">
   121         -        if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){
   122         -          @ <a href='%R/forumedit?item=%d(id)'>Edit</a>
   123         -        }
   124         -        @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a>
   125         -        @ </span>
          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");
   126    818         }
   127         -      @ </tr>
   128         -      @ <tr><td><div class="forum_body">
   129         -      blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6));
   130         -      wiki_render_by_mimetype(&body, zMime);
   131         -      blob_reset(&body);
   132         -      @ </div></td></tr>
   133         -      @ </table>
          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");
   134    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);
   135    878     }else{
   136         -    /* If we reach this point, that means the users wants a list of
   137         -    ** recent threads.
   138         -    */
   139         -    i = 0;
   140         -    db_prepare(&q,
   141         -      "SELECT a.mtitle, a.npost, b.mpostid"
   142         -      "  FROM forumthread AS a, forumpost AS b "
   143         -      " WHERE a.mthreadid=b.mthreadid"
   144         -      "   AND b.inreplyto IS NULL"
   145         -      " ORDER BY a.mtime DESC LIMIT 40"
   146         -    );
   147         -    if( g.perm.WrForum ){
   148         -      style_submenu_element("New", "%R/forumedit");
          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");
   149    888       }
   150         -    @ <h1>Recent Forum Threads</h1>
   151         -    while( db_step(&q)==SQLITE_ROW ){
   152         -      int n = db_column_int(&q,1);
   153         -      int itemid = db_column_int(&q,2);
   154         -      const char *zTitle = db_column_text(&q,0);
   155         -      if( (i++)==0 ){
   156         -        @ <ol>
   157         -      }
   158         -      @ <li><span class="forum_title">
   159         -      @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span>
   160         -      @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li>
   161         -    }
   162         -    if( i ){
   163         -      @ </ol>
   164         -    }
          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>
   165    913     }
          914  +  @ </form>
   166    915     style_footer();
   167    916   }
   168    917   
   169    918   /*
   170         -** Use content in CGI parameters "s" (subject), "b" (body), and
   171         -** "mimetype" (mimetype) to create a new forum entry.
   172         -** Return the id of the new forum entry.
          919  +** WEBPAGE: forum
   173    920   **
   174         -** If any problems occur, return 0 and set *pzErr to a description of
   175         -** the problem.
   176         -**
   177         -** Cases:
   178         -**
   179         -**    itemId==0 && parentId==0        Starting a new thread.
   180         -**    itemId==0 && parentId>0         New reply to parentId
   181         -**    itemId>0 && parentId==0         Edit existing post itemId
   182         -*/
   183         -static int forum_post(int itemId, int parentId, char **pzErr){
   184         -  const char *zSubject = 0;
   185         -  int threadId;
   186         -  double rNow = db_double(0.0, "SELECT julianday('now')");
   187         -  const char *zMime = wiki_filter_mimetypes(P("mimetype"));
   188         -  if( itemId==0 && parentId==0 ){
   189         -    /* Start a new thread.  Subject required. */
   190         -    sqlite3_uint64 r1, r2;
   191         -    zSubject = PT("s");
   192         -    if( zSubject==0 || zSubject[0]==0 ){
   193         -      *pzErr = "\"Subject\" required to start a new thread";
   194         -      return 0;
   195         -    }
   196         -    sqlite3_randomness(sizeof(r1), &r1);
   197         -    sqlite3_randomness(sizeof(r2), &r2);
   198         -    db_multi_exec(
   199         -      "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)"
   200         -      "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)",
   201         -      zSubject, rNow
   202         -    );
   203         -    threadId = db_last_insert_rowid();
   204         -  }else{
   205         -    threadId = db_int(0, "SELECT mthreadid FROM forumpost"
   206         -                         " WHERE mpostid=%d", itemId ? itemId : parentId);
   207         -  }
   208         -  if( itemId ){
   209         -    if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
   210         -                  " WHERE mpostid=%d", itemId) ){
   211         -      db_multi_exec(
   212         -        "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d",
   213         -        PT("s"), threadId
   214         -      );
   215         -    }
   216         -    db_multi_exec(
   217         -       "UPDATE forumpost SET"
   218         -       " mtime=%!.17g,"
   219         -       " mimetype=%Q,"
   220         -       " ipaddr=%Q,"
   221         -       " mbody=%Q"
   222         -       " WHERE mpostid=%d",
   223         -       rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId
   224         -    );
   225         -  }else{
   226         -    db_multi_exec(
   227         -       "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime,"
   228         -       "  mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES"
   229         -       "  (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)",
   230         -       threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b"));
   231         -    itemId = db_last_insert_rowid();
   232         -  }
   233         -  if( zSubject==0 ){
   234         -    db_multi_exec(
   235         -      "UPDATE forumthread SET mtime=%!.17g, npost=npost+1"
   236         -      " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)",
   237         -      rNow, itemId
   238         -    );
   239         -  }
   240         -  return itemId;
   241         -}
   242         -
   243         -/*
   244         -** WEBPAGE: forumedit
          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.
   245    924   **
   246    925   ** Query parameters:
   247    926   **
   248         -**    replyto=N      Enter a reply to forum item N
   249         -**    item=N         Edit item N
   250         -**    s=SUBJECT      Subject. New thread only. Omitted for replies
   251         -**    b=BODY         Body of the post
   252         -**    m=MIMETYPE     Mimetype for the body of the post
   253         -**    x              Submit changes
   254         -**    p              Preview changes
          927  +**    n=N             The number of threads to show on each page
          928  +**    x=X             Skip the first X threads
   255    929   */
   256         -void forum_edit_page(void){
   257         -  int itemId;
   258         -  int parentId;
   259         -  char *zErr = 0;
   260         -  const char *zMime;
   261         -  const char *zSub;
   262         -
          930  +void forum_main_page(void){
          931  +  Stmt q;
          932  +  int iLimit, iOfst, iCnt;
          933  +  int srchFlags;
   263    934     login_check_credentials();
   264         -  if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
   265         -  forum_verify_schema();
   266         -  itemId = atoi(PD("item","0"));
   267         -  parentId = atoi(PD("replyto","0"));
   268         -  if( P("cancel")!=0 ){
   269         -    cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId);
          935  +  srchFlags = search_restrict(SRCH_FORUM);
          936  +  if( !g.perm.RdForum ){
          937  +    login_needed(g.anon.RdForum);
   270    938       return;
   271    939     }
   272         -  if( P("x")!=0 && cgi_csrf_safe(1) ){
   273         -    itemId = forum_post(itemId,parentId,&zErr);
   274         -    if( itemId ){
   275         -      cgi_redirectf("%R/forum?item=%d",itemId);
          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();
   276    951         return;
   277    952       }
   278    953     }
   279         -  if( itemId && (P("mimetype")==0 || P("b")==0) ){
   280         -    Stmt q;
   281         -    db_prepare(&q, "SELECT mimetype, mbody FROM forumpost"
   282         -                   " WHERE mpostid=%d", itemId);
   283         -    if( db_step(&q)==SQLITE_ROW ){
   284         -      if( P("mimetype")==0 ){
   285         -        cgi_set_query_parameter("mimetype", db_column_text(&q, 0));
          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);
   286   1032         }
   287         -      if( P("b")==0 ){
   288         -        cgi_set_query_parameter("b", db_column_text(&q, 1));
   289         -      }
         1033  +      @ </tr>
         1034  +      fossil_free(zAge);
   290   1035       }
   291   1036       db_finalize(&q);
   292   1037     }
   293         -  zMime = wiki_filter_mimetypes(P("mimetype"));
   294         -  if( itemId>0 ){
   295         -    style_header("Edit Forum Post");
   296         -  }else if( parentId>0 ){
   297         -    style_header("Comment On Forum Post");
         1038  +  if( iCnt>0 ){
         1039  +    @ </table></div>
   298   1040     }else{
   299         -    style_header("New Forum Thread");
         1041  +    @ <h1>No forum posts found</h1>
   300   1042     }
   301         -  @ <form action="%R/forumedit" method="POST">
   302         -  if( itemId ){
   303         -    @ <input type="hidden" name="item" value="%d(itemId)">
   304         -  }
   305         -  if( parentId ){
   306         -    @ <input type="hidden" name="replyto" value="%d(parentId)">
   307         -  }
   308         -  if( P("p") ){
   309         -    Blob x;
   310         -    @ <div class="forumpreview">
   311         -    if( P("s") ){
   312         -      @ <h1>%h(PT("s"))</h1>
   313         -    }
   314         -    @ <div class="forumpreviewbody">
   315         -    blob_init(&x, PT("b"), -1);
   316         -    wiki_render_by_mimetype(&x, PT("mimetype"));
   317         -    blob_reset(&x);
   318         -    @ </div>
   319         -    @ </div>
   320         -    @ <hr>
   321         -  }
   322         -  @ <table border="0" class="forumeditform"> 
   323         -  if( zErr ){
   324         -    @ <tr><td colspan="2">
   325         -    @ <span class='forumFormErr'>%h(zErr)</span>
   326         -  }
   327         -  if( (itemId==0 && parentId==0)
   328         -   || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
   329         -                           " WHERE mpostid=%d", itemId))
   330         -  ){
   331         -    zSub = PT("s");
   332         -    if( zSub==0 && itemId ){
   333         -      zSub = db_text("",
   334         -         "SELECT mtitle FROM forumthread"
   335         -         " WHERE mthreadid=(SELECT mthreadid FROM forumpost"
   336         -                          "  WHERE mpostid=%d)", itemId);
   337         -    }
   338         -    @ <tr><td>Subject:</td>
   339         -    @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'>
   340         -  }
   341         -  @ <tr><td>Markup:</td><td>
   342         -  mimetype_option_menu(zMime);
   343         -  @ <tr><td>Comment:</td><td>
   344         -  @ <textarea name="b" class="wikiedit" cols="80"\
   345         -  @  rows="20" wrap="virtual">%h(PD("b",""))</textarea></td>
   346         -  @ <tr><td></td><td>
   347         -  @ <input type="submit" name="p" value="Preview">
   348         -  if( P("p")!=0 ){
   349         -    @ <input type="submit" name="x" value="Submit">
   350         -  }
   351         -  @ <input type="submit" name="cancel" value="Cancel">
   352         -  @ </table>
   353         -  @ </form>
   354   1043     style_footer();
   355   1044   }

Added 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.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( tx.iRailPitch>0 ){
   113         -      railPitch = tx.iRailPitch;
          112  +    if( window.innerWidth<400 ){
          113  +      railPitch = minRailPitch;
   114    114       }else{
   115         -      railPitch = elems.rail.w;
   116         -      railPitch -= Math.floor((tx.nrail-1)*(railPitch-minRailPitch)/21);
          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);
   117    122       }
   118         -    railPitch = Math.max(railPitch, minRailPitch);
   119    123     
   120    124       if( tx.nomo ){
   121    125         mergeOffset = 0;
   122    126       }else{
   123    127         mergeOffset = railPitch-minRailPitch-mLine.w;
   124    128         mergeOffset = Math.min(mergeOffset, elems.mergeoffset.w);
   125    129         mergeOffset = mergeOffset>0 ? mergeOffset + line.w/2 : 0;
................................................................................
   373    377     var i;
   374    378     for(i=0; i<lx.length; i++){
   375    379       if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
   376    380     }
   377    381     lx = topObj.getElementsByClassName('timelineCompactComment');
   378    382     for(i=0; i<lx.length; i++){
   379    383       if( lx[i].hasAttribute('data-id') ) lx[i].onclick = toggleDetail;
          384  +  }
          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  +    }
   380    396     }
   381    397   }
   382    398     
   383    399   /* Look for all timeline-data-NN objects.  Load each one and draw
   384    400   ** a graph for each one.
   385    401   */
   386    402   (function(){

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
    29     32   # define _WIN32_WINNT 0x501
    30     33   #endif
    31     34   #ifndef __EXTENSIONS__
    32     35   # define __EXTENSIONS__ 1  /* IPv6 won't compile on Solaris without this */
    33     36   #endif
    34     37   #include "config.h"
    35     38   #include "http_socket.h"

Changes to src/http_transport.c.

   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    274       zCmd = mprintf("\"%s\" http --in \"%s\" --out \"%s\" --ipaddr 127.0.0.1"
   275         -                   " \"%s\" --localauth --nodelay",
          275  +                   " \"%s\" --localauth",
   276    276          g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
   277    277       );
   278    278       fossil_system(zCmd);
   279    279       free(zCmd);
   280    280       transport.pFile = fossil_fopen(transport.zInFile, "rb");
   281    281     }
   282    282   }

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(rid);
   941         -  if( modPending ){
   942         -    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
   943         -  }
          940  +  modPending = moderation_pending_www(rid);
   944    941     @ </td></tr>
   945    942     @ <tr><th>Page&nbsp;Name:</th><td>%h(pWiki->zWikiTitle)</td></tr>
   946    943     @ <tr><th>Date:</th><td>
   947    944     hyperlink_to_date(zDate, "</td></tr>");
   948    945     @ <tr><th>Original&nbsp;User:</th><td>
   949    946     hyperlink_to_user(pWiki->zUser, zDate, "</td></tr>");
   950    947     if( pWiki->zMimetype ){
................................................................................
   976    973   
   977    974   
   978    975     @ <div class="section">Content</div>
   979    976     blob_init(&wiki, pWiki->zWiki, -1);
   980    977     wiki_render_by_mimetype(&wiki, pWiki->zMimetype);
   981    978     blob_reset(&wiki);
   982    979     manifest_destroy(pWiki);
   983         -  style_footer();
   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    980     style_footer();
   999    981   }
  1000    982   
  1001    983   /*
  1002    984   ** Find an check-in based on query parameter zParam and parse its
  1003    985   ** manifest.  Return the number of errors.
  1004    986   */
................................................................................
  1244   1226   #define OBJTYPE_WIKI       0x0004
  1245   1227   #define OBJTYPE_TICKET     0x0008
  1246   1228   #define OBJTYPE_ATTACHMENT 0x0010
  1247   1229   #define OBJTYPE_EVENT      0x0020
  1248   1230   #define OBJTYPE_TAG        0x0040
  1249   1231   #define OBJTYPE_SYMLINK    0x0080
  1250   1232   #define OBJTYPE_EXE        0x0100
         1233  +#define OBJTYPE_FORUM      0x0200
  1251   1234   
  1252   1235   /*
  1253   1236   ** Possible flags for the second parameter to
  1254   1237   ** object_description()
  1255   1238   */
  1256   1239   #define OBJDESC_DETAIL      0x0001   /* more detail */
  1257   1240   #endif
................................................................................
  1436   1419           if( eventTagId != 0) {
  1437   1420             @ Instance of technote
  1438   1421             objType |= OBJTYPE_EVENT;
  1439   1422             hyperlink_to_event_tagid(db_column_int(&q, 5));
  1440   1423           }else{
  1441   1424             @ Attachment to technote
  1442   1425           }
         1426  +      }else if( zType[0]=='f' ){
         1427  +        objType |= OBJTYPE_FORUM;
         1428  +        @ Forum post
  1443   1429         }else{
  1444   1430           @ Tag referencing
  1445   1431         }
  1446   1432         if( zType[0]!='e' || eventTagId == 0){
  1447   1433           hyperlink_to_uuid(zUuid);
  1448   1434         }
  1449   1435         @ - %!W(zCom) by
................................................................................
  2246   2232     @ <div class="section">Overview</div>
  2247   2233     @ <p><table class="label-value">
  2248   2234     @ <tr><th>Artifact&nbsp;ID:</th>
  2249   2235     @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
  2250   2236     if( g.perm.Setup ){
  2251   2237       @ (%d(rid))
  2252   2238     }
  2253         -  modPending = moderation_pending(rid);
  2254         -  if( modPending ){
  2255         -    @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
  2256         -  }
         2239  +  modPending = moderation_pending_www(rid);
  2257   2240     @ <tr><th>Ticket:</th>
  2258   2241     @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a>
  2259   2242     if( zTktTitle ){
  2260   2243           @<br />%h(zTktTitle)
  2261   2244     }
  2262   2245     @</td></tr>
  2263   2246     @ <tr><th>User&nbsp;&amp;&nbsp;Date:</th><td>
................................................................................
  2362   2345       ci_page();
  2363   2346     }else
  2364   2347     if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){
  2365   2348       ci_page();
  2366   2349     }else
  2367   2350     if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){
  2368   2351       ainfo_page();
         2352  +  }else
         2353  +  if( db_table_exists("repository","forumpost")
         2354  +   && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid)
         2355  +  ){
         2356  +    forumthread_page();
  2369   2357     }else
  2370   2358     {
  2371   2359       artifact_page();
  2372   2360     }
  2373   2361   }
  2374   2362   
  2375   2363   /*

Changes to src/json.c.

  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");
  1895   1904   #undef ADD
  1896   1905     return payload;
  1897   1906   }
  1898   1907   
  1899   1908   /*
  1900   1909   ** Implementation of the /json/stat page/command.
  1901   1910   **

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_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},
    40     41   {"ticket", json_timeline_ticket, 0},
    41     42   {"wiki", json_timeline_wiki, 0},
    42     43   /* Last entry MUST have a NULL name. */
    43     44   {NULL,NULL,0}
    44     45   };
    45     46   
    46     47   
................................................................................
   517    518     assert( 0 != g.json.resultCode );
   518    519     cson_value_free(payV);
   519    520     payV = NULL;
   520    521     ok:
   521    522     db_finalize(&q);
   522    523     return payV;
   523    524   }
          525  +
          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  +}
   524    581   
   525    582   /*
   526    583   ** Implementation of /json/timeline/wiki.
   527    584   **
   528    585   */
   529    586   cson_value * json_timeline_wiki(){
   530    587     /* This code is 95% the same as json_timeline_ci(), by the way. */

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  +**
   211    219   ** zPassword may be either the plain-text form or the encrypted
   212    220   ** form of the user's password.
   213    221   */
   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         -             );
          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  +  }    
   227    257     free(zSha1Pw);
   228    258     return uid;
   229    259   }
   230    260   
   231    261   /*
   232    262   ** Generates a login cookie value for a non-anonymous user.
   233    263   **
................................................................................
   468    498     int rc;
   469    499     if( zReferer==0 ) return 0;
   470    500     zPattern = mprintf("%s/login*", g.zBaseURL);
   471    501     rc = sqlite3_strglob(zPattern, zReferer)==0;
   472    502     fossil_free(zPattern);
   473    503     return rc;
   474    504   }
          505  +
          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  +}
   475    523   
   476    524   /*
   477    525   ** There used to be a page named "my" that was designed to show information
   478    526   ** about a specific user.  The "my" page was linked from the "Logged in as USER"
   479    527   ** line on the title bar.  The "my" page was never completed so it is now
   480    528   ** removed.  Use this page as a placeholder in older installations.
   481    529   **
................................................................................
   496    544     const char *zGoto = P("g");
   497    545     int anonFlag;                /* Login as "anonymous" would be useful */
   498    546     char *zErrMsg = "";
   499    547     int uid;                     /* User id logged in user */
   500    548     char *zSha1Pw;
   501    549     const char *zIpAddr;         /* IP address of requestor */
   502    550     const char *zReferer;
          551  +  int noAnon = P("noanon")!=0;
   503    552   
   504    553     login_check_credentials();
   505    554     if( login_wants_https_redirect() ){
   506    555       const char *zQS = P("QUERY_STRING");
   507    556       if( P("redir")!=0 ){
   508    557         style_header("Insecure Connection");
   509    558         @ <h1>Unable To Establish An Encrypted Connection</h1>
................................................................................
   534    583   
   535    584     /* Handle log-out requests */
   536    585     if( P("out") ){
   537    586       login_clear_login_data();
   538    587       redirect_to_g();
   539    588       return;
   540    589     }
          590  +
          591  +  /* Redirect for create-new-account requests */
          592  +  if( P("self") ){
          593  +    cgi_redirectf("%R/register");
          594  +    return;
          595  +  }
   541    596   
   542    597     /* Deal with password-change requests */
   543    598     if( g.perm.Password && zPasswd
   544    599      && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
   545    600     ){
   546    601       /* If there is not a "real" login, we cannot change any password. */
   547    602       if( g.zLogin ){
................................................................................
   605    660       login_set_anon_cookie(zIpAddr, NULL);
   606    661       record_login_attempt("anonymous", zIpAddr, 1);
   607    662       redirect_to_g();
   608    663     }
   609    664     if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
   610    665       /* Attempting to log in as a user other than anonymous.
   611    666       */
   612         -    uid = login_search_uid(zUsername, zPasswd);
          667  +    uid = login_search_uid(&zUsername, zPasswd);
   613    668       if( uid<=0 ){
   614    669         sleep(1);
   615    670         zErrMsg =
   616    671            @ <p><span class="loginError">
   617    672            @ You entered an unknown user or an incorrect password.
   618    673            @ </span></p>
   619    674         ;
................................................................................
   629    684         login_set_user_cookie(zUsername, uid, NULL);
   630    685         redirect_to_g();
   631    686       }
   632    687     }
   633    688     style_header("Login/Logout");
   634    689     style_adunit_config(ADUNIT_OFF);
   635    690     @ %s(zErrMsg)
   636         -  if( zGoto ){
          691  +  if( zGoto && !noAnon ){
   637    692       char *zAbbrev = fossil_strdup(zGoto);
   638    693       int i;
   639    694       for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
   640    695       zAbbrev[i] = 0;
   641    696       if( g.zLogin ){
   642    697         @ <p>Use a different login with greater privilege than <b>%h(g.zLogin)</b>
   643    698         @ to access <b>%h(zAbbrev)</b>.
................................................................................
   672    727       @ <tr>
   673    728       @   <td class="form_label">User ID:</td>
   674    729       if( anonFlag ){
   675    730         @ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td>
   676    731       }else{
   677    732         @ <td><input type="text" id="u" name="u" value="" size="30" /></td>
   678    733       }
          734  +    @ </tr>
          735  +    @ <tr>
          736  +    @  <td class="form_label">Password:</td>
          737  +    @  <td><input type="password" id="p" name="p" value="" size="30" /></td>
          738  +    @ </tr>
   679    739       if( P("HTTPS")==0 ){
   680         -      @ <td width="15"><td rowspan="3">
   681         -      @ <p class='securityWarning'>
   682         -      @ Warning: Your password will be sent in the clear over an
          740  +      @ <tr><td class="form_label">Warning:</td>
          741  +      @ <td><span class='securityWarning'>
          742  +      @ Your password will be sent in the clear over an
   683    743         @ unencrypted connection.
   684    744         if( g.sslNotAvailable ){
   685    745           @ No encrypted connection is available on this server.
   686    746         }else{
   687    747           @ Consider logging in at
   688    748           @ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
   689    749         }
   690         -      @ </p>
          750  +      @ </span></td></tr>
   691    751       }
   692         -    @ </tr>
   693         -    @ <tr>
   694         -    @  <td class="form_label">Password:</td>
   695         -    @   <td><input type="password" id="p" name="p" value="" size="30" /></td>
   696         -    @ </tr>
   697    752       if( g.zLogin==0 && (anonFlag || zGoto==0) ){
   698    753         zAnonPw = db_text(0, "SELECT pw FROM user"
   699    754                              " WHERE login='anonymous'"
   700    755                              "   AND cap!=''");
   701    756       }
   702    757       @ <tr>
   703    758       @   <td></td>
   704         -    @   <td><input type="submit" name="in" value="Login">
          759  +    @   <td><input type="submit" name="in" value="Login"></td>
   705    760       @ </tr>
          761  +    if( !noAnon && login_self_register_available(0) ){
          762  +      @ <tr>
          763  +      @   <td></td>
          764  +      @   <td><input type="submit" name="self" value="Create A New Account">
          765  +      @ </tr>
          766  +    }
   706    767       @ </table>
   707         -    @ <p>Pressing the Login button grants permission to store a cookie.</p>
   708         -    if( db_get_boolean("self-register", 0) ){
   709         -      @ <p>If you do not have an account, you can
   710         -      @ <a href="%R/register?g=%T(P("G"))">create one</a>.
   711         -    }
   712         -    if( zAnonPw ){
          768  +    if( zAnonPw && !noAnon ){
   713    769         unsigned int uSeed = captcha_seed();
   714    770         const char *zDecoded = captcha_decode(uSeed);
   715    771         int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
   716    772         char *zCaptcha = captcha_render(zDecoded);
   717    773     
   718    774         @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
   719    775         @ Visitors may enter <b>anonymous</b> as the user-ID with
   720    776         @ the 8-character hexadecimal password shown below:</p>
   721         -      @ <div class="captcha"><table class="captcha"><tr><td><pre>
          777  +      @ <div class="captcha"><table class="captcha"><tr><td>\
          778  +      @ <pre class="captcha">
   722    779         @ %h(zCaptcha)
   723    780         @ </pre></td></tr></table>
   724    781         if( bAutoCaptcha ) {
   725    782            @ <input type="button" value="Fill out captcha" id='autofillButton' \
   726    783            @ data-af='%s(zDecoded)' />
   727    784            style_load_one_js_file("login.js");
   728    785         }
   729    786         @ </div>
   730    787         free(zCaptcha);
   731    788       }
   732    789       @ </form>
   733    790     }
   734         -  if( login_is_individual() && g.perm.Password ){
   735         -    if( email_enabled() ){
          791  +  if( login_is_individual() ){
          792  +    if( g.perm.EmailAlert && alert_enabled() ){
   736    793         @ <hr>
   737    794         @ <p>Configure <a href="%R/alerts">Email Alerts</a>
   738    795         @ for user <b>%h(g.zLogin)</b></p>
   739    796       }
   740         -    @ <hr />
   741         -    @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
   742         -    form_begin(0, "%R/login");
   743         -    @ <table>
   744         -    @ <tr><td class="form_label">Old Password:</td>
   745         -    @ <td><input type="password" name="p" size="30" /></td></tr>
   746         -    @ <tr><td class="form_label">New Password:</td>
   747         -    @ <td><input type="password" name="n1" size="30" /></td></tr>
   748         -    @ <tr><td class="form_label">Repeat New Password:</td>
   749         -    @ <td><input type="password" name="n2" size="30" /></td></tr>
   750         -    @ <tr><td></td>
   751         -    @ <td><input type="submit" value="Change Password" /></td></tr>
   752         -    @ </table>
   753         -    @ </form>
          797  +    if( g.perm.Password ){
          798  +      @ <hr>
          799  +      @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
          800  +      form_begin(0, "%R/login");
          801  +      @ <table>
          802  +      @ <tr><td class="form_label">Old Password:</td>
          803  +      @ <td><input type="password" name="p" size="30" /></td></tr>
          804  +      @ <tr><td class="form_label">New Password:</td>
          805  +      @ <td><input type="password" name="n1" size="30" /></td></tr>
          806  +      @ <tr><td class="form_label">Repeat New Password:</td>
          807  +      @ <td><input type="password" name="n2" size="30" /></td></tr>
          808  +      @ <tr><td></td>
          809  +      @ <td><input type="submit" value="Change Password" /></td></tr>
          810  +      @ </table>
          811  +      @ </form>
          812  +    }
   754    813     }
   755    814     style_footer();
   756    815   }
   757    816   
   758    817   /*
   759    818   ** Attempt to find login credentials for user zLogin on a peer repository
   760    819   ** with project code zCode.  Transfer those credentials to the local
................................................................................
   901    960       zDecode[i] = 0;
   902    961       zUsername = zDecode;
   903    962       zPasswd = &zDecode[i+1];
   904    963   
   905    964       /* Attempting to log in as the user provided by HTTP
   906    965       ** basic auth
   907    966       */
   908         -    uid = login_search_uid(zUsername, zPasswd);
          967  +    uid = login_search_uid(&zUsername, zPasswd);
   909    968       if( uid>0 ){
   910    969         record_login_attempt(zUsername, zIpAddr, 1);
   911    970       }else{
   912    971         record_login_attempt(zUsername, zIpAddr, 0);
   913    972   
   914    973         /* The user attempted to login specifically with HTTP basic
   915    974         ** auth, but provided invalid credentials. Inform them of
................................................................................
  1132   1191     /* If the public-pages glob pattern is defined and REQUEST_URI matches
  1133   1192     ** one of the globs in public-pages, then also add in all default-perms
  1134   1193     ** permissions.
  1135   1194     */
  1136   1195     zPublicPages = db_get("public-pages",0);
  1137   1196     if( zPublicPages!=0 ){
  1138   1197       Glob *pGlob = glob_create(zPublicPages);
  1139         -    if( glob_match(pGlob, PD("REQUEST_URI","no-match")) ){
         1198  +    const char *zUri = PD("REQUEST_URI","");
         1199  +    zUri += (int)strlen(g.zTop);
         1200  +    if( glob_match(pGlob, zUri) ){
  1140   1201         login_set_capabilities(db_get("default-perms","u"), 0);
  1141   1202       }
  1142   1203       glob_free(pGlob);
  1143   1204     }
  1144   1205   }
  1145   1206   
  1146   1207   /*
................................................................................
  1200   1261                                p->RdWiki = p->WrWiki = p->NewWiki =
  1201   1262                                p->ApndWiki = p->Hyperlink = p->Clone =
  1202   1263                                p->NewTkt = p->Password = p->RdAddr =
  1203   1264                                p->TktFmt = p->Attach = p->ApndTkt =
  1204   1265                                p->ModWiki = p->ModTkt = p->Delete =
  1205   1266                                p->RdForum = p->WrForum = p->ModForum =
  1206   1267                                p->WrTForum = p->AdminForum =
  1207         -                             p->EmailAlert = p->Announce =
         1268  +                             p->EmailAlert = p->Announce = p->Debug =
  1208   1269                                p->WrUnver = p->Private = 1;
  1209   1270                                /* Fall thru into Read/Write */
  1210   1271         case 'i':   p->Read = p->Write = 1;                      break;
  1211   1272         case 'o':   p->Read = 1;                                 break;
  1212   1273         case 'z':   p->Zip = 1;                                  break;
  1213   1274   
  1214   1275         case 'd':   p->Delete = 1;                               break;
................................................................................
  1240   1301         case '3':   p->WrForum = 1;
  1241   1302         case '2':   p->RdForum = 1;                              break;
  1242   1303   
  1243   1304         case '7':   p->EmailAlert = 1;                           break;
  1244   1305         case 'A':   p->Announce = 1;                             break;
  1245   1306         case 'D':   p->Debug = 1;                                break;
  1246   1307   
  1247         -      /* The "u" privileges is a little different.  It recursively
         1308  +      /* The "u" privilege recursively
  1248   1309         ** inherits all privileges of the user named "reader" */
  1249   1310         case 'u': {
  1250         -        if( (flags & LOGIN_IGNORE_UV)==0 ){
         1311  +        if( p->XReader==0 ){
  1251   1312             const char *zUser;
         1313  +          p->XReader = 1;
  1252   1314             zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");
  1253         -          login_set_capabilities(zUser, flags | LOGIN_IGNORE_UV);
         1315  +          login_set_capabilities(zUser, flags);
  1254   1316           }
  1255   1317           break;
  1256   1318         }
  1257   1319   
  1258         -      /* The "v" privileges is a little different.  It recursively
         1320  +      /* The "v" privilege recursively
  1259   1321         ** inherits all privileges of the user named "developer" */
  1260   1322         case 'v': {
  1261         -        if( (flags & LOGIN_IGNORE_UV)==0 ){
         1323  +        if( p->XDeveloper==0 ){
  1262   1324             const char *zDev;
         1325  +          p->XDeveloper = 1;
  1263   1326             zDev = db_text("", "SELECT cap FROM user WHERE login='developer'");
  1264         -          login_set_capabilities(zDev, flags | LOGIN_IGNORE_UV);
         1327  +          login_set_capabilities(zDev, flags);
  1265   1328           }
  1266   1329           break;
  1267   1330         }
  1268   1331       }
  1269   1332     }
  1270   1333   }
  1271   1334   
................................................................................
  1457   1520   /*
  1458   1521   ** WEBPAGE: register
  1459   1522   **
  1460   1523   ** Page to allow users to self-register.  The "self-register" setting
  1461   1524   ** must be enabled for this page to operate.
  1462   1525   */
  1463   1526   void register_page(void){
  1464         -  const char *zUsername, *zPasswd, *zConfirm, *zContact, *zCS, *zPw, *zCap;
         1527  +  const char *zUserID, *zPasswd, *zConfirm, *zEAddr;
         1528  +  const char *zDName;
  1465   1529     unsigned int uSeed;
  1466   1530     const char *zDecoded;
  1467   1531     char *zCaptcha;
         1532  +  int iErrLine = -1;
         1533  +  const char *zErr = 0;
         1534  +  char *zPerms;             /* Permissions for the default user */
         1535  +  int canDoAlerts = 0;      /* True if receiving email alerts is possible */
         1536  +  int doAlerts = 0;         /* True if subscription is wanted too */
  1468   1537     if( !db_get_boolean("self-register", 0) ){
  1469   1538       style_header("Registration not possible");
  1470   1539       @ <p>This project does not allow user self-registration. Please contact the
  1471   1540       @ project administrator to obtain an account.</p>
  1472   1541       style_footer();
  1473   1542       return;
  1474   1543     }
  1475         -
  1476         -  style_header("Register");
  1477         -  zUsername = P("u");
  1478         -  zPasswd = P("p");
  1479         -  zConfirm = P("cp");
  1480         -  zContact = P("c");
  1481         -  zCap = P("cap");
  1482         -  zCS = P("cs"); /* Captcha Secret */
  1483         -
  1484         -  /* Try to make any sense from user input. */
  1485         -  if( P("new") ){
  1486         -    if( zCS==0 ) fossil_redirect_home();  /* Forged request */
  1487         -    zPw = captcha_decode((unsigned int)atoi(zCS));
  1488         -    if( !(zUsername && zPasswd && zConfirm && zContact) ){
  1489         -      @ <p><span class="loginError">
  1490         -      @ All fields are obligatory.
  1491         -      @ </span></p>
  1492         -    }else if( strlen(zPasswd) < 6){
  1493         -      @ <p><span class="loginError">
  1494         -      @ Password too weak.
  1495         -      @ </span></p>
  1496         -    }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
  1497         -      @ <p><span class="loginError">
  1498         -      @ The two copies of your new passwords do not match.
  1499         -      @ </span></p>
  1500         -    }else if( fossil_stricmp(zPw, zCap)!=0 ){
  1501         -      @ <p><span class="loginError">
  1502         -      @ Captcha text invalid.
  1503         -      @ </span></p>
  1504         -    }else{
  1505         -      /* This almost is stupid copy-paste of code from user.c:user_cmd(). */
  1506         -      Blob passwd, login, caps, contact;
  1507         -
  1508         -      blob_init(&login, zUsername, -1);
  1509         -      blob_init(&contact, zContact, -1);
  1510         -      blob_init(&caps, db_get("default-perms", "u"), -1);
  1511         -      blob_init(&passwd, zPasswd, -1);
  1512         -
  1513         -      if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){
  1514         -        /* Here lies the reason I don't use zErrMsg - it would not substitute
  1515         -         * this %s(zUsername), or at least I don't know how to force it to.*/
  1516         -        @ <p><span class="loginError">
  1517         -        @ %h(zUsername) already exists.
  1518         -        @ </span></p>
  1519         -      }else{
  1520         -        char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
  1521         -        int uid;
  1522         -        db_multi_exec(
  1523         -            "INSERT INTO user(login,pw,cap,info,mtime)"
  1524         -            "VALUES(%B,%Q,%B,%B,strftime('%%s','now'))",
  1525         -            &login, zPw, &caps, &contact
  1526         -            );
  1527         -        free(zPw);
  1528         -
  1529         -        /* The user is registered, now just log him in. */
  1530         -        uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUsername);
  1531         -        login_set_user_cookie( zUsername, uid, NULL );
         1544  +  zPerms = db_get("default-perms","u");
         1545  +
         1546  +  /* Prompt the user for email alerts if this repository is configured for
         1547  +  ** email alerts and if the default permissions include "7" */
         1548  +  canDoAlerts = alert_tables_exist() && db_int(0,
         1549  +    "SELECT fullcap(%Q) GLOB '*7*'", zPerms
         1550  +  );
         1551  +  doAlerts = canDoAlerts && atoi(PD("alerts","1"))!=0;
         1552  +
         1553  +  zUserID = PDT("u","");
         1554  +  zPasswd = PDT("p","");
         1555  +  zConfirm = PDT("cp","");
         1556  +  zEAddr = PDT("ea","");
         1557  +  zDName = PDT("dn","");
         1558  +
         1559  +  /* Verify user imputs */
         1560  +  if( P("new")==0 || !cgi_csrf_safe(1) ){
         1561  +    /* This is not a valid form submission.  Fall through into
         1562  +    ** the form display */
         1563  +  }else if( !captcha_is_correct(1) ){
         1564  +    iErrLine = 6;
         1565  +    zErr = "Incorrect CAPTCHA";
         1566  +  }else if( strlen(zUserID)<3 ){
         1567  +    iErrLine = 1;
         1568  +    zErr = "User ID too short. Must be at least 3 characters.";
         1569  +  }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){
         1570  +    iErrLine = 1;
         1571  +    zErr = "User ID may not contain spaces or special characters.";
         1572  +  }else if( zDName[0]==0 ){
         1573  +    iErrLine = 2;
         1574  +    zErr = "Required";
         1575  +  }else if( zEAddr[0]==0 ){
         1576  +    iErrLine = 3;
         1577  +    zErr = "Required";
         1578  +  }else if( email_copy_addr(zEAddr,0)==0 ){
         1579  +    iErrLine = 3;
         1580  +    zErr = "Not a valid email address";
         1581  +  }else if( strlen(zPasswd)<6 ){
         1582  +    iErrLine = 4;
         1583  +    zErr = "Password must be at least 6 characters long";
         1584  +  }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
         1585  +    iErrLine = 5;
         1586  +    zErr = "Passwords do not match";
         1587  +  }else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zUserID) ){
         1588  +    iErrLine = 1;
         1589  +    zErr = "This User ID is already taken. Choose something different.";
         1590  +  }else if(
         1591  +      /* If the email is found anywhere in USER.INFO... */
         1592  +      db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr)
         1593  +    ||
         1594  +      /* Or if the email is a verify subscriber email with an associated
         1595  +      ** user... */
         1596  +      db_exists(
         1597  +        "SELECT 1 FROM subscriber WHERE semail=%Q AND suname IS NOT NULL"
         1598  +        " AND sverified",zEAddr)
         1599  +   ){
         1600  +    iErrLine = 3;
         1601  +    zErr = "This email address is already claimed by another user";
         1602  +  }else{
         1603  +    /* If all of the tests above have passed, that means that the submitted
         1604  +    ** form contains valid data and we can proceed to create the new login */
         1605  +    Blob sql;
         1606  +    int uid;
         1607  +    char *zPass = sha1_shared_secret(zPasswd, zUserID, 0);
         1608  +    blob_init(&sql, 0, 0);
         1609  +    blob_append_sql(&sql,
         1610  +       "INSERT INTO user(login,pw,cap,info,mtime)\n"
         1611  +       "VALUES(%Q,%Q,%Q,"
         1612  +       "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())",
         1613  +       zUserID, zPass, zPerms, zDName, zEAddr, g.zIpAddr);
         1614  +    fossil_free(zPass);
         1615  +    db_multi_exec("%s", blob_sql_text(&sql));
         1616  +    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID);
         1617  +    login_set_user_cookie(zUserID, uid, NULL);
         1618  +    if( doAlerts ){
         1619  +      /* Also make the new user a subscriber. */
         1620  +      Blob hdr, body;
         1621  +      AlertSender *pSender;
         1622  +      sqlite3_int64 id;   /* New subscriber Id */
         1623  +      const char *zCode;  /* New subscriber code (in hex) */
         1624  +      const char *zGoto = P("g");
         1625  +      int nsub = 0;
         1626  +      char ssub[20];
         1627  +      ssub[nsub++] = 'a';
         1628  +      if( g.perm.Read )    ssub[nsub++] = 'c';
         1629  +      if( g.perm.RdForum ) ssub[nsub++] = 'f';
         1630  +      if( g.perm.RdTkt )   ssub[nsub++] = 't';
         1631  +      if( g.perm.RdWiki )  ssub[nsub++] = 'w';
         1632  +      ssub[nsub] = 0;
         1633  +      /* Also add the user to the subscriber table. */
         1634  +      db_multi_exec(
         1635  +        "INSERT INTO subscriber(semail,suname,"
         1636  +        "  sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
         1637  +        " VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)"
         1638  +        " ON CONFLICT(semail) DO UPDATE"
         1639  +        "   SET suname=excluded.suname",
         1640  +        /* semail */    zEAddr,
         1641  +        /* suname */    zUserID,
         1642  +        /* sverified */ 0,
         1643  +        /* sdigest */   0,
         1644  +        /* ssub */      ssub,
         1645  +        /* smip */      g.zIpAddr
         1646  +      );
         1647  +      id = db_last_insert_rowid();
         1648  +      if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q"
         1649  +                    "  AND sverified", zEAddr) ){
         1650  +        /* This the case where the user was formerly a verified subscriber
         1651  +        ** and here they have also registered as a user as well.  It is
         1652  +        ** not necessary to repeat the verfication step */
  1532   1653           redirect_to_g();
  1533         -
         1654  +      }
         1655  +      zCode = db_text(0,
         1656  +           "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld",
         1657  +           id);
         1658  +      /* A verification email */
         1659  +      pSender = alert_sender_new(0,0);
         1660  +      blob_init(&hdr,0,0);
         1661  +      blob_init(&body,0,0);
         1662  +      blob_appendf(&hdr, "To: <%s>\n", zEAddr);
         1663  +      blob_appendf(&hdr, "Subject: Subscription verification\n");
         1664  +      alert_append_confirmation_message(&body, zCode);
         1665  +      alert_send(pSender, &hdr, &body, 0);
         1666  +      style_header("Email Verification");
         1667  +      if( pSender->zErr ){
         1668  +        @ <h1>Internal Error</h1>
         1669  +        @ <p>The following internal error was encountered while trying
         1670  +        @ to send the confirmation email:
         1671  +        @ <blockquote><pre>
         1672  +        @ %h(pSender->zErr)
         1673  +        @ </pre></blockquote>
         1674  +      }else{
         1675  +        @ <p>An email has been sent to "%h(zEAddr)". That email contains a
         1676  +        @ hyperlink that you must click on in order to activate your
         1677  +        @ subscription.</p>
         1678  +      }
         1679  +      alert_sender_free(pSender);
         1680  +      if( zGoto ){
         1681  +        @ <p><a href='%h(zGoto)'>Continue</a>
  1534   1682         }
         1683  +      style_footer();
         1684  +      return;
  1535   1685       }
         1686  +    redirect_to_g();
  1536   1687     }
  1537   1688   
  1538   1689     /* Prepare the captcha. */
  1539   1690     uSeed = captcha_seed();
  1540   1691     zDecoded = captcha_decode(uSeed);
  1541   1692     zCaptcha = captcha_render(zDecoded);
  1542   1693   
         1694  +  style_header("Register");
  1543   1695     /* Print out the registration form. */
  1544   1696     form_begin(0, "%R/register");
  1545   1697     if( P("g") ){
  1546   1698       @ <input type="hidden" name="g" value="%h(P("g"))" />
  1547   1699     }
  1548         -  @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
         1700  +  @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" />
  1549   1701     @ <table class="login_out">
  1550   1702     @ <tr>
  1551   1703     @   <td class="form_label" align="right">User ID:</td>
  1552         -  @   <td><input type="text" id="u" name="u" value="" size="30" /></td>
         1704  +  @   <td><input type="text" name="u" value="%h(zUserID)" size="30"></td>
         1705  +  @
         1706  +  if( iErrLine==1 ){
         1707  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1708  +  }
         1709  +  @ <tr>
         1710  +  @   <td class="form_label" align="right">Display Name:</td>
         1711  +  @   <td><input type="text" name="dn" value="%h(zDName)" size="30"></td>
         1712  +  @ </tr>
         1713  +  if( iErrLine==2 ){
         1714  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1715  +  }
         1716  +  @ </tr>
         1717  +  @ <tr>
         1718  +  @   <td class="form_label" align="right">Email Address:</td>
         1719  +  @   <td><input type="text" name="ea" value="%h(zEAddr)" size="30"></td>
  1553   1720     @ </tr>
         1721  +  if( iErrLine==3 ){
         1722  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1723  +  }
         1724  +  if( canDoAlerts ){
         1725  +    int a = atoi(PD("alerts","1"));
         1726  +    @ <tr>
         1727  +    @   <td class="form_label" align="right">Email&nbsp;Alerts?</td>
         1728  +    @   <td><select size='1' name='alerts'>
         1729  +    @       <option value="1" %s(a?"selected":"")>Yes</option>
         1730  +    @       <option value="0" %s(!a?"selected":"")>No</option>
         1731  +    @   </select></td></tr>
         1732  +  }
  1554   1733     @ <tr>
  1555   1734     @   <td class="form_label" align="right">Password:</td>
  1556         -  @   <td><input type="password" id="p" name="p" value="" size="30" /></td>
  1557         -  @ </tr>
         1735  +  @   <td><input type="password" name="p" value="%h(zPasswd)" size="30"></td>
  1558   1736     @ <tr>
  1559         -  @   <td class="form_label" align="right">Confirm password:</td>
  1560         -  @   <td><input type="password" id="cp" name="cp" value="" size="30" /></td>
  1561         -  @ </tr>
         1737  +  if( iErrLine==4 ){
         1738  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1739  +  }
  1562   1740     @ <tr>
  1563         -  @   <td class="form_label" align="right">Contact info:</td>
  1564         -  @   <td><input type="text" id="c" name="c" value="" size="30" /></td>
         1741  +  @   <td class="form_label" align="right">Confirm:</td>
         1742  +  @   <td><input type="password" name="cp" value="%h(zConfirm)" size="30"></td>
  1565   1743     @ </tr>
         1744  +  if( iErrLine==5 ){
         1745  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1746  +  }
  1566   1747     @ <tr>
  1567         -  @   <td class="form_label" align="right">Captcha text (below):</td>
  1568         -  @   <td><input type="text" id="cap" name="cap" value="" size="30" /></td>
         1748  +  @   <td class="form_label" align="right">Captcha:</td>
         1749  +  @   <td><input type="text" name="captcha" value="" size="30"></td>
  1569   1750     @ </tr>
         1751  +  if( iErrLine==6 ){
         1752  +    @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
         1753  +  }
  1570   1754     @ <tr><td></td>
  1571   1755     @ <td><input type="submit" name="new" value="Register" /></td></tr>
  1572   1756     @ </table>
  1573         -  @ <div class="captcha"><table class="captcha"><tr><td><pre>
         1757  +  @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
  1574   1758     @ %h(zCaptcha)
  1575         -  @ </pre></td></tr></table></div>
         1759  +  @ </pre>
         1760  +  @ Enter this 8-letter code in the "Captcha" box above.
         1761  +  @ </td></tr></table></div>
  1576   1762     @ </form>
  1577   1763     style_footer();
  1578   1764   
  1579   1765     free(zCaptcha);
  1580   1766   }
  1581   1767   
  1582   1768   /*

Changes to src/main.c.

    18     18   ** This module codes the main() procedure that runs first when the
    19     19   ** program is invoked.
    20     20   */
    21     21   #include "VERSION.h"
    22     22   #include "config.h"
    23     23   #if defined(_WIN32)
    24     24   #  include <windows.h>
           25  +#  include <io.h>
           26  +#  define isatty(h) _isatty(h)
           27  +#  define GETPID (int)GetCurrentProcessId
    25     28   #endif
    26     29   #include "main.h"
    27     30   #include <string.h>
    28     31   #include <time.h>
    29     32   #include <fcntl.h>
    30     33   #include <sys/types.h>
    31     34   #include <sys/stat.h>
    32     35   #include <stdlib.h> /* atexit() */
    33     36   #if !defined(_WIN32)
    34     37   #  include <errno.h> /* errno global */
           38  +#  include <unistd.h>
           39  +#  include <signal.h>
           40  +#  define GETPID getpid
    35     41   #endif
    36     42   #ifdef FOSSIL_ENABLE_SSL
    37     43   #  include "openssl/crypto.h"
    38     44   #endif
    39     45   #if defined(FOSSIL_ENABLE_MINIZ)
    40     46   #  define MINIZ_HEADER_FILE_ONLY
    41     47   #  include "miniz.c"
................................................................................
    88     94     char Zip;              /* z: download zipped artifact via /zip URL */
    89     95     char Private;          /* x: can send and receive private content */
    90     96     char WrUnver;          /* y: can push unversioned content */
    91     97     char RdForum;          /* 2: Read forum posts */
    92     98     char WrForum;          /* 3: Create new forum posts */
    93     99     char WrTForum;         /* 4: Post to forums not subject to moderation */
    94    100     char ModForum;         /* 5: Moderate (approve or reject) forum posts */
    95         -  char AdminForum;       /* 6: Edit forum posts by other users */
          101  +  char AdminForum;       /* 6: Set or remove capability 4 on other users */
    96    102     char EmailAlert;       /* 7: Sign up for email notifications */
    97    103     char Announce;         /* A: Send announcements */
    98    104     char Debug;            /* D: show extra Fossil debugging features */
          105  +  /* These last two are included to block infinite recursion */
          106  +  char XReader;          /* u: Inherit all privileges of "reader" */
          107  +  char XDeveloper;       /* v: Inherit all privileges of "developer" */
    99    108   };
   100    109   
   101    110   #ifdef FOSSIL_ENABLE_TCL
   102    111   /*
   103    112   ** All Tcl related context information is in this structure.  This structure
   104    113   ** definition has been copied from and should be kept in sync with the one in
   105    114   ** "th_tcl.c".
................................................................................
   597    606   #endif
   598    607   int main(int argc, char **argv)
   599    608   #endif
   600    609   {
   601    610     const char *zCmdName = "unknown";
   602    611     const CmdOrPage *pCmd = 0;
   603    612     int rc;
          613  +
          614  +#if !defined(_WIN32_WCE)
          615  +  if( fossil_getenv("FOSSIL_BREAK") ){
          616  +    if( isatty(0) && isatty(2) ){
          617  +      fprintf(stderr,
          618  +          "attach debugger to process %d and press any key to continue.\n",
          619  +          GETPID());
          620  +      fgetc(stdin);
          621  +    }else{
          622  +#if defined(_WIN32) || defined(WIN32)
          623  +      DebugBreak();
          624  +#elif defined(SIGTRAP)
          625  +      raise(SIGTRAP);
          626  +#endif
          627  +    }
          628  +  }
          629  +#endif
   604    630   
   605    631     fossil_limit_memory(1);
   606    632     if( sqlite3_libversion_number()<3014000 ){
   607    633       fossil_panic("Unsuitable SQLite version %s, must be at least 3.14.0",
   608    634                    sqlite3_libversion());
   609    635     }
   610    636     sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
................................................................................
   638    664       g.zVfsName = fossil_getenv("FOSSIL_VFS");
   639    665     }
   640    666     if( g.zVfsName ){
   641    667       sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName);
   642    668       if( pVfs ){
   643    669         sqlite3_vfs_register(pVfs, 1);
   644    670       }else{
   645         -      fossil_panic("no such VFS: \"%s\"", g.zVfsName);
          671  +      fossil_fatal("no such VFS: \"%s\"", g.zVfsName);
   646    672       }
   647    673     }
   648    674     if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){
   649    675       zCmdName = "cgi";
   650    676       g.isHTTP = 1;
   651    677     }else if( g.argc<2 && !fossilExeHasAppendedRepo() ){
   652    678       fossil_print(
................................................................................
   689    715       g.zLogin = find_option("user", "U", 1);
   690    716       g.zSSLIdentity = find_option("ssl-identity", 0, 1);
   691    717       g.zErrlog = find_option("errorlog", 0, 1);
   692    718       fossil_init_flags_from_options();
   693    719       if( find_option("utc",0,0) ) g.fTimeFormat = 1;
   694    720       if( find_option("localtime",0,0) ) g.fTimeFormat = 2;
   695    721       if( zChdir && file_chdir(zChdir, 0) ){
   696         -      fossil_panic("unable to change directories to %s", zChdir);
          722  +      fossil_fatal("unable to change directories to %s", zChdir);
   697    723       }
   698    724       if( find_option("help",0,0)!=0 ){
   699    725         /* If --help is found anywhere on the command line, translate the command
   700    726          * to "fossil help cmdname" where "cmdname" is the first argument that
   701    727          * does not begin with a "-" character.  If all arguments start with "-",
   702    728          * translate to "fossil help argv[1] argv[2]...". */
   703    729         int i, nNewArgc;
................................................................................
   754    780         rc = Th_CommandHook(zCmdName, 0);
   755    781       }else{
   756    782         rc = TH_OK;
   757    783       }
   758    784       if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
   759    785         if( rc==TH_OK || rc==TH_RETURN ){
   760    786   #endif
   761         -        fossil_panic("%s: unknown command: %s\n"
          787  +        fossil_fatal("%s: unknown command: %s\n"
   762    788                        "%s: use \"help\" for more information",
   763    789                        g.argv[0], zCmdName, g.argv[0]);
   764    790   #ifdef FOSSIL_ENABLE_TH1_HOOKS
   765    791         }
   766    792         if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
   767    793           Th_CommandNotify(zCmdName, 0);
   768    794         }
................................................................................
   819    845     return 0;
   820    846   }
   821    847   
   822    848   /*
   823    849   ** Print a usage comment and quit
   824    850   */
   825    851   void usage(const char *zFormat){
   826         -  fossil_panic("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
          852  +  fossil_fatal("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
   827    853   }
   828    854   
   829    855   /*
   830    856   ** Remove n elements from g.argv beginning with the i-th element.
   831    857   */
   832    858   static void remove_from_argv(int i, int n){
   833    859     int j;
................................................................................
   937    963   ** Any remaining command-line argument begins with "-" print
   938    964   ** an error message and quit.
   939    965   */
   940    966   void verify_all_options(void){
   941    967     int i;
   942    968     for(i=1; i<g.argc; i++){
   943    969       if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){
   944         -      fossil_panic(
          970  +      fossil_fatal(
   945    971           "unrecognized command-line option, or missing argument: %s",
   946    972           g.argv[i]);
   947    973       }
   948    974     }
   949    975   }
   950    976   
   951    977   /*
................................................................................
  1156   1182       if( strncmp(g.zTop, "http://", 7)==0 ){
  1157   1183         /* it is HTTP, replace prefix with HTTPS. */
  1158   1184         g.zHttpsURL = mprintf("https://%s", &g.zTop[7]);
  1159   1185       }else if( strncmp(g.zTop, "https://", 8)==0 ){
  1160   1186         /* it is already HTTPS, use it. */
  1161   1187         g.zHttpsURL = mprintf("%s", g.zTop);
  1162   1188       }else{
  1163         -      fossil_panic("argument to --baseurl should be 'http://host/path'"
         1189  +      fossil_fatal("argument to --baseurl should be 'http://host/path'"
  1164   1190                      " or 'https://host/path'");
  1165   1191       }
  1166   1192       for(i=n=0; (c = g.zTop[i])!=0; i++){
  1167   1193         if( c=='/' ){
  1168   1194           n++;
  1169   1195           if( n==3 ){
  1170   1196             g.zTop += i;
  1171   1197             break;
  1172   1198           }
  1173   1199         }
  1174   1200       }
  1175   1201       if( g.zTop==g.zBaseURL ){
  1176         -      fossil_panic("argument to --baseurl should be 'http://host/path'"
         1202  +      fossil_fatal("argument to --baseurl should be 'http://host/path'"
  1177   1203                      " or 'https://host/path'");
  1178   1204       }
  1179   1205       if( g.zTop[1]==0 ) g.zTop++;
  1180   1206     }else{
  1181   1207       zHost = PD("HTTP_HOST","");
  1182   1208       zMode = PD("HTTPS","off");
  1183   1209       zCur = PD("SCRIPT_NAME","/");
................................................................................
  1258   1284             }
  1259   1285             zDir[i] = '/';
  1260   1286           }
  1261   1287           zRepo = &zDir[i];
  1262   1288         }
  1263   1289       }
  1264   1290       if( stat(zRepo, &sStat)!=0 ){
  1265         -      fossil_panic("cannot stat() repository: %s", zRepo);
         1291  +      fossil_fatal("cannot stat() repository: %s", zRepo);
  1266   1292       }
  1267   1293       i = setgid(sStat.st_gid);
  1268   1294       i = i || setuid(sStat.st_uid);
  1269   1295       if(i){
  1270         -      fossil_panic("setgid/uid() failed with errno %d", errno);
         1296  +      fossil_fatal("setgid/uid() failed with errno %d", errno);
  1271   1297       }
  1272   1298       if( g.db==0 && file_isfile(zRepo, ExtFILE) ){
  1273   1299         db_open_repository(zRepo);
  1274   1300       }
  1275   1301     }
  1276   1302   #endif
  1277   1303     return zRepo;
................................................................................
  1327   1353       vfile_scan(&base, blob_size(&base), 0, 0, 0);
  1328   1354       db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'");
  1329   1355       allRepo = 0;
  1330   1356     }
  1331   1357     @ <html>
  1332   1358     @ <head>
  1333   1359     @ <base href="%s(g.zBaseURL)/" />
         1360  +  @ <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1334   1361     @ <title>Repository List</title>
  1335   1362     @ </head>
  1336   1363     @ <body>
  1337   1364     n = db_int(0, "SELECT count(*) FROM sfile");
  1338   1365     if( n>0 ){
  1339   1366       Stmt q;
  1340   1367       sqlite3_int64 iNow, iMTime;
................................................................................
  1646   1673           }else{
  1647   1674   #ifdef FOSSIL_ENABLE_JSON
  1648   1675             if(g.json.isJsonMode){
  1649   1676               json_err(FSL_JSON_E_RESOURCE_NOT_FOUND,NULL,1);
  1650   1677               return;
  1651   1678             }
  1652   1679   #endif
         1680  +          @ <html><head>
         1681  +          @ <meta name="viewport" \
         1682  +          @ content="width=device-width, initial-scale=1.0">
         1683  +          @ </head><body>
  1653   1684             @ <h1>Not Found</h1>
         1685  +          @ </body>
  1654   1686             cgi_set_status(404, "not found");
  1655   1687             cgi_reply();
  1656   1688           }
  1657   1689           return;
  1658   1690         }
  1659   1691         break;
  1660   1692       }
................................................................................
  2249   2281     LPVOID *ppAddress,   /* The extracted pointer value. */
  2250   2282     SIZE_T *pnSize       /* The extracted size value. */
  2251   2283   ){
  2252   2284     unsigned int nSize = 0;
  2253   2285     if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){
  2254   2286       *pnSize = (SIZE_T)nSize;
  2255   2287     }else{
  2256         -    fossil_panic("failed to parse pid key");
         2288  +    fossil_fatal("failed to parse pid key");
  2257   2289     }
  2258   2290   }
  2259   2291   #endif
  2260   2292   
  2261   2293   /*
  2262   2294   ** COMMAND: http*
  2263   2295   **
................................................................................
  2700   2732                               zBrowser, zIpAddr, zInitPage);
  2701   2733       }
  2702   2734     }
  2703   2735     if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY;
  2704   2736     if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT;
  2705   2737     db_close(1);
  2706   2738     if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){
  2707         -    fossil_panic("unable to listen on TCP socket %d", iPort);
         2739  +    fossil_fatal("unable to listen on TCP socket %d", iPort);
  2708   2740     }
  2709   2741     if( zMaxLatency ){
  2710   2742       signal(SIGALRM, sigalrm_handler);
  2711   2743       alarm(atoi(zMaxLatency));
  2712   2744     }
  2713   2745     g.httpIn = stdin;
  2714   2746     g.httpOut = stdout;
................................................................................
  2804   2836   ** the administrator only.
  2805   2837   **
  2806   2838   **     case=1           Issue a fossil_warning() while generating the page.
  2807   2839   **     case=2           Extra db_begin_transaction()
  2808   2840   **     case=3           Extra db_end_transaction()
  2809   2841   **     case=4           Error during SQL processing
  2810   2842   **     case=5           Call the segfault handler
         2843  +**     case=6           Call webpage_assert()
         2844  +**     case=7           Call webpage_error()
  2811   2845   */
  2812   2846   void test_warning_page(void){
  2813   2847     int iCase = atoi(PD("case","0"));
  2814   2848     int i;
  2815   2849     login_check_credentials();
  2816   2850     if( !g.perm.Setup && !g.perm.Admin ){
  2817   2851       login_needed(0);
................................................................................
  2821   2855     style_submenu_element("Error Log","%R/errorlog");
  2822   2856     if( iCase<1 || iCase>4 ){
  2823   2857       @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
  2824   2858       @ by clicking on one of the following cases:
  2825   2859     }else{
  2826   2860       @ <p>This is the test page for case=%d(iCase).  All possible cases:
  2827   2861     }
  2828         -  for(i=1; i<=5; i++){
         2862  +  for(i=1; i<=7; i++){
  2829   2863       @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
  2830   2864     }
  2831   2865     @ </p>
  2832   2866     @ <p><ol>
  2833   2867     @ <li value='1'> Call fossil_warning()
  2834   2868     if( iCase==1 ){
  2835   2869       fossil_warning("Test warning message from /test-warning");
................................................................................
  2850   2884       sqlite3_log(SQLITE_ERROR, "Test warning message during SQL");
  2851   2885       db_finalize(&q);
  2852   2886     }
  2853   2887     @ <li value='5'> simulate segfault handling
  2854   2888     if( iCase==5 ){
  2855   2889       sigsegv_handler(0);
  2856   2890     }
         2891  +  @ <li value='6'> call webpage_assert(0)
         2892  +  if( iCase==6 ){
         2893  +    webpage_assert( 5==7 );
         2894  +  }
         2895  +  @ <li value='7'> call webpage_error()"
         2896  +  if( iCase==7 ){
         2897  +    cgi_reset_content();
         2898  +    webpage_error("Case 7 from /test-warning");
         2899  +  }
  2857   2900     @ </ol>
  2858   2901     @ <p>End of test</p>
  2859   2902     style_footer();
  2860   2903   }

Changes to src/main.mk.

     9      9   #
    10     10   # This file is included by primary Makefile.
    11     11   #
    12     12   
    13     13   XBCC = $(BCC) $(BCCFLAGS)
    14     14   XTCC = $(TCC) -I. -I$(SRCDIR) -I$(OBJDIR) $(TCCFLAGS)
    15     15   
           16  +TESTFLAGS := -quiet
    16     17   
    17     18   SRC = \
    18     19     $(SRCDIR)/add.c \
           20  +  $(SRCDIR)/alerts.c \
    19     21     $(SRCDIR)/allrepo.c \
    20     22     $(SRCDIR)/attach.c \
    21     23     $(SRCDIR)/backoffice.c \
    22     24     $(SRCDIR)/bag.c \
    23     25     $(SRCDIR)/bisect.c \
    24     26     $(SRCDIR)/blob.c \
    25     27     $(SRCDIR)/branch.c \
    26     28     $(SRCDIR)/browse.c \
    27     29     $(SRCDIR)/builtin.c \
    28     30     $(SRCDIR)/bundle.c \
    29     31     $(SRCDIR)/cache.c \
           32  +  $(SRCDIR)/capabilities.c \
    30     33     $(SRCDIR)/captcha.c \
    31     34     $(SRCDIR)/cgi.c \
    32     35     $(SRCDIR)/checkin.c \
    33     36     $(SRCDIR)/checkout.c \
    34     37     $(SRCDIR)/clearsign.c \
    35     38     $(SRCDIR)/clone.c \
    36     39     $(SRCDIR)/comformat.c \
................................................................................
    41     44     $(SRCDIR)/delta.c \
    42     45     $(SRCDIR)/deltacmd.c \
    43     46     $(SRCDIR)/descendants.c \
    44     47     $(SRCDIR)/diff.c \
    45     48     $(SRCDIR)/diffcmd.c \
    46     49     $(SRCDIR)/dispatch.c \
    47     50     $(SRCDIR)/doc.c \
    48         -  $(SRCDIR)/email.c \
    49     51     $(SRCDIR)/encode.c \
    50     52     $(SRCDIR)/etag.c \
    51     53     $(SRCDIR)/event.c \
    52     54     $(SRCDIR)/export.c \
    53     55     $(SRCDIR)/file.c \
    54     56     $(SRCDIR)/finfo.c \
    55     57     $(SRCDIR)/foci.c \
................................................................................
   106    108     $(SRCDIR)/regexp.c \
   107    109     $(SRCDIR)/report.c \
   108    110     $(SRCDIR)/rss.c \
   109    111     $(SRCDIR)/schema.c \
   110    112     $(SRCDIR)/search.c \
   111    113     $(SRCDIR)/security_audit.c \
   112    114     $(SRCDIR)/setup.c \
          115  +  $(SRCDIR)/setupuser.c \
   113    116     $(SRCDIR)/sha1.c \
   114    117     $(SRCDIR)/sha1hard.c \
   115    118     $(SRCDIR)/sha3.c \
   116    119     $(SRCDIR)/shun.c \
   117    120     $(SRCDIR)/sitemap.c \
   118    121     $(SRCDIR)/skins.c \
   119    122     $(SRCDIR)/smtp.c \
................................................................................
   173    176     $(SRCDIR)/../skins/bootstrap/details.txt \
   174    177     $(SRCDIR)/../skins/bootstrap/footer.txt \
   175    178     $(SRCDIR)/../skins/bootstrap/header.txt \
   176    179     $(SRCDIR)/../skins/default/css.txt \
   177    180     $(SRCDIR)/../skins/default/details.txt \
   178    181     $(SRCDIR)/../skins/default/footer.txt \
   179    182     $(SRCDIR)/../skins/default/header.txt \
          183  +  $(SRCDIR)/../skins/default/js.txt \
   180    184     $(SRCDIR)/../skins/eagle/css.txt \
   181    185     $(SRCDIR)/../skins/eagle/details.txt \
   182    186     $(SRCDIR)/../skins/eagle/footer.txt \
   183    187     $(SRCDIR)/../skins/eagle/header.txt \
   184    188     $(SRCDIR)/../skins/enhanced1/css.txt \
   185    189     $(SRCDIR)/../skins/enhanced1/details.txt \
   186    190     $(SRCDIR)/../skins/enhanced1/footer.txt \
................................................................................
   203    207     $(SRCDIR)/../skins/rounded1/header.txt \
   204    208     $(SRCDIR)/../skins/xekri/css.txt \
   205    209     $(SRCDIR)/../skins/xekri/details.txt \
   206    210     $(SRCDIR)/../skins/xekri/footer.txt \
   207    211     $(SRCDIR)/../skins/xekri/header.txt \
   208    212     $(SRCDIR)/ci_edit.js \
   209    213     $(SRCDIR)/diff.tcl \
          214  +  $(SRCDIR)/forum.js \
   210    215     $(SRCDIR)/graph.js \
   211    216     $(SRCDIR)/href.js \
   212    217     $(SRCDIR)/login.js \
   213    218     $(SRCDIR)/markdown.md \
   214    219     $(SRCDIR)/menu.js \
   215    220     $(SRCDIR)/sbsdiff.js \
   216    221     $(SRCDIR)/scroll.js \
................................................................................
   218    223     $(SRCDIR)/sorttable.js \
   219    224     $(SRCDIR)/tree.js \
   220    225     $(SRCDIR)/useredit.js \
   221    226     $(SRCDIR)/wiki.wiki
   222    227   
   223    228   TRANS_SRC = \
   224    229     $(OBJDIR)/add_.c \
          230  +  $(OBJDIR)/alerts_.c \
   225    231     $(OBJDIR)/allrepo_.c \
   226    232     $(OBJDIR)/attach_.c \
   227    233     $(OBJDIR)/backoffice_.c \
   228    234     $(OBJDIR)/bag_.c \
   229    235     $(OBJDIR)/bisect_.c \
   230    236     $(OBJDIR)/blob_.c \
   231    237     $(OBJDIR)/branch_.c \
   232    238     $(OBJDIR)/browse_.c \
   233    239     $(OBJDIR)/builtin_.c \
   234    240     $(OBJDIR)/bundle_.c \
   235    241     $(OBJDIR)/cache_.c \
          242  +  $(OBJDIR)/capabilities_.c \
   236    243     $(OBJDIR)/captcha_.c \
   237    244     $(OBJDIR)/cgi_.c \
   238    245     $(OBJDIR)/checkin_.c \
   239    246     $(OBJDIR)/checkout_.c \
   240    247     $(OBJDIR)/clearsign_.c \
   241    248     $(OBJDIR)/clone_.c \
   242    249     $(OBJDIR)/comformat_.c \
................................................................................
   247    254     $(OBJDIR)/delta_.c \
   248    255     $(OBJDIR)/deltacmd_.c \
   249    256     $(OBJDIR)/descendants_.c \
   250    257     $(OBJDIR)/diff_.c \
   251    258     $(OBJDIR)/diffcmd_.c \
   252    259     $(OBJDIR)/dispatch_.c \
   253    260     $(OBJDIR)/doc_.c \
   254         -  $(OBJDIR)/email_.c \
   255    261     $(OBJDIR)/encode_.c \
   256    262     $(OBJDIR)/etag_.c \
   257    263     $(OBJDIR)/event_.c \
   258    264     $(OBJDIR)/export_.c \
   259    265     $(OBJDIR)/file_.c \
   260    266     $(OBJDIR)/finfo_.c \
   261    267     $(OBJDIR)/foci_.c \
................................................................................
   312    318     $(OBJDIR)/regexp_.c \
   313    319     $(OBJDIR)/report_.c \
   314    320     $(OBJDIR)/rss_.c \
   315    321     $(OBJDIR)/schema_.c \
   316    322     $(OBJDIR)/search_.c \
   317    323     $(OBJDIR)/security_audit_.c \
   318    324     $(OBJDIR)/setup_.c \
          325  +  $(OBJDIR)/setupuser_.c \
   319    326     $(OBJDIR)/sha1_.c \
   320    327     $(OBJDIR)/sha1hard_.c \
   321    328     $(OBJDIR)/sha3_.c \
   322    329     $(OBJDIR)/shun_.c \
   323    330     $(OBJDIR)/sitemap_.c \
   324    331     $(OBJDIR)/skins_.c \
   325    332     $(OBJDIR)/smtp_.c \
................................................................................
   353    360     $(OBJDIR)/wysiwyg_.c \
   354    361     $(OBJDIR)/xfer_.c \
   355    362     $(OBJDIR)/xfersetup_.c \
   356    363     $(OBJDIR)/zip_.c
   357    364   
   358    365   OBJ = \
   359    366    $(OBJDIR)/add.o \
          367  + $(OBJDIR)/alerts.o \
   360    368    $(OBJDIR)/allrepo.o \
   361    369    $(OBJDIR)/attach.o \
   362    370    $(OBJDIR)/backoffice.o \
   363    371    $(OBJDIR)/bag.o \
   364    372    $(OBJDIR)/bisect.o \
   365    373    $(OBJDIR)/blob.o \
   366    374    $(OBJDIR)/branch.o \
   367    375    $(OBJDIR)/browse.o \
   368    376    $(OBJDIR)/builtin.o \
   369    377    $(OBJDIR)/bundle.o \
   370    378    $(OBJDIR)/cache.o \
          379  + $(OBJDIR)/capabilities.o \
   371    380    $(OBJDIR)/captcha.o \
   372    381    $(OBJDIR)/cgi.o \
   373    382    $(OBJDIR)/checkin.o \
   374    383    $(OBJDIR)/checkout.o \
   375    384    $(OBJDIR)/clearsign.o \
   376    385    $(OBJDIR)/clone.o \
   377    386    $(OBJDIR)/comformat.o \
................................................................................
   382    391    $(OBJDIR)/delta.o \
   383    392    $(OBJDIR)/deltacmd.o \
   384    393    $(OBJDIR)/descendants.o \
   385    394    $(OBJDIR)/diff.o \
   386    395    $(OBJDIR)/diffcmd.o \
   387    396    $(OBJDIR)/dispatch.o \
   388    397    $(OBJDIR)/doc.o \
   389         - $(OBJDIR)/email.o \
   390    398    $(OBJDIR)/encode.o \
   391    399    $(OBJDIR)/etag.o \
   392    400    $(OBJDIR)/event.o \
   393    401    $(OBJDIR)/export.o \
   394    402    $(OBJDIR)/file.o \
   395    403    $(OBJDIR)/finfo.o \
   396    404    $(OBJDIR)/foci.o \
................................................................................
   447    455    $(OBJDIR)/regexp.o \
   448    456    $(OBJDIR)/report.o \
   449    457    $(OBJDIR)/rss.o \
   450    458    $(OBJDIR)/schema.o \
   451    459    $(OBJDIR)/search.o \
   452    460    $(OBJDIR)/security_audit.o \
   453    461    $(OBJDIR)/setup.o \
          462  + $(OBJDIR)/setupuser.o \
   454    463    $(OBJDIR)/sha1.o \
   455    464    $(OBJDIR)/sha1hard.o \
   456    465    $(OBJDIR)/sha3.o \
   457    466    $(OBJDIR)/shun.o \
   458    467    $(OBJDIR)/sitemap.o \
   459    468    $(OBJDIR)/skins.o \
   460    469    $(OBJDIR)/smtp.o \
................................................................................
   537    546   #  -quiet    Hide most output from the terminal
   538    547   #  -strict   Treat known bugs as failures
   539    548   #
   540    549   # TESTFLAGS can also include names of specific test files to limit
   541    550   # the run to just those test cases.
   542    551   #
   543    552   test:	$(OBJDIR) $(APPNAME)
   544         -	$(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) -quiet $(TESTFLAGS)
          553  +	$(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) $(TESTFLAGS)
   545    554   
   546    555   $(OBJDIR)/VERSION.h:	$(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION $(OBJDIR)/mkversion
   547    556   	$(OBJDIR)/mkversion $(SRCDIR)/../manifest.uuid  $(SRCDIR)/../manifest  $(SRCDIR)/../VERSION >$(OBJDIR)/VERSION.h
   548    557   
   549    558   $(OBJDIR)/default_css.h:	$(SRCDIR)/default_css.txt $(OBJDIR)/mkcss
   550    559   	$(OBJDIR)/mkcss $(SRCDIR)/default_css.txt $(OBJDIR)/default_css.h
   551    560   
................................................................................
   686    695   	$(OBJDIR)/mkindex $(TRANS_SRC) >$@
   687    696   
   688    697   $(OBJDIR)/builtin_data.h: $(OBJDIR)/mkbuiltin $(EXTRA_FILES)
   689    698   	$(OBJDIR)/mkbuiltin --prefix $(SRCDIR)/ $(EXTRA_FILES) >$@
   690    699   
   691    700   $(OBJDIR)/headers:	$(OBJDIR)/page_index.h $(OBJDIR)/builtin_data.h $(OBJDIR)/default_css.h $(OBJDIR)/makeheaders $(OBJDIR)/VERSION.h
   692    701   	$(OBJDIR)/makeheaders $(OBJDIR)/add_.c:$(OBJDIR)/add.h \
          702  +	$(OBJDIR)/alerts_.c:$(OBJDIR)/alerts.h \
   693    703   	$(OBJDIR)/allrepo_.c:$(OBJDIR)/allrepo.h \
   694    704   	$(OBJDIR)/attach_.c:$(OBJDIR)/attach.h \
   695    705   	$(OBJDIR)/backoffice_.c:$(OBJDIR)/backoffice.h \
   696    706   	$(OBJDIR)/bag_.c:$(OBJDIR)/bag.h \
   697    707   	$(OBJDIR)/bisect_.c:$(OBJDIR)/bisect.h \
   698    708   	$(OBJDIR)/blob_.c:$(OBJDIR)/blob.h \
   699    709   	$(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \
   700    710   	$(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \
   701    711   	$(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \
   702    712   	$(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \
   703    713   	$(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \
          714  +	$(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \
   704    715   	$(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \
   705    716   	$(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \
   706    717   	$(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \
   707    718   	$(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \
   708    719   	$(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \
   709    720   	$(OBJDIR)/clone_.c:$(OBJDIR)/clone.h \
   710    721   	$(OBJDIR)/comformat_.c:$(OBJDIR)/comformat.h \
................................................................................
   715    726   	$(OBJDIR)/delta_.c:$(OBJDIR)/delta.h \
   716    727   	$(OBJDIR)/deltacmd_.c:$(OBJDIR)/deltacmd.h \
   717    728   	$(OBJDIR)/descendants_.c:$(OBJDIR)/descendants.h \
   718    729   	$(OBJDIR)/diff_.c:$(OBJDIR)/diff.h \
   719    730   	$(OBJDIR)/diffcmd_.c:$(OBJDIR)/diffcmd.h \
   720    731   	$(OBJDIR)/dispatch_.c:$(OBJDIR)/dispatch.h \
   721    732   	$(OBJDIR)/doc_.c:$(OBJDIR)/doc.h \
   722         -	$(OBJDIR)/email_.c:$(OBJDIR)/email.h \
   723    733   	$(OBJDIR)/encode_.c:$(OBJDIR)/encode.h \
   724    734   	$(OBJDIR)/etag_.c:$(OBJDIR)/etag.h \
   725    735   	$(OBJDIR)/event_.c:$(OBJDIR)/event.h \
   726    736   	$(OBJDIR)/export_.c:$(OBJDIR)/export.h \
   727    737   	$(OBJDIR)/file_.c:$(OBJDIR)/file.h \
   728    738   	$(OBJDIR)/finfo_.c:$(OBJDIR)/finfo.h \
   729    739   	$(OBJDIR)/foci_.c:$(OBJDIR)/foci.h \
................................................................................
   780    790   	$(OBJDIR)/regexp_.c:$(OBJDIR)/regexp.h \
   781    791   	$(OBJDIR)/report_.c:$(OBJDIR)/report.h \
   782    792   	$(OBJDIR)/rss_.c:$(OBJDIR)/rss.h \
   783    793   	$(OBJDIR)/schema_.c:$(OBJDIR)/schema.h \
   784    794   	$(OBJDIR)/search_.c:$(OBJDIR)/search.h \
   785    795   	$(OBJDIR)/security_audit_.c:$(OBJDIR)/security_audit.h \
   786    796   	$(OBJDIR)/setup_.c:$(OBJDIR)/setup.h \
          797  +	$(OBJDIR)/setupuser_.c:$(OBJDIR)/setupuser.h \
   787    798   	$(OBJDIR)/sha1_.c:$(OBJDIR)/sha1.h \
   788    799   	$(OBJDIR)/sha1hard_.c:$(OBJDIR)/sha1hard.h \
   789    800   	$(OBJDIR)/sha3_.c:$(OBJDIR)/sha3.h \
   790    801   	$(OBJDIR)/shun_.c:$(OBJDIR)/shun.h \
   791    802   	$(OBJDIR)/sitemap_.c:$(OBJDIR)/sitemap.h \
   792    803   	$(OBJDIR)/skins_.c:$(OBJDIR)/skins.h \
   793    804   	$(OBJDIR)/smtp_.c:$(OBJDIR)/smtp.h \
................................................................................
   832    843   $(OBJDIR)/add_.c:	$(SRCDIR)/add.c $(OBJDIR)/translate
   833    844   	$(OBJDIR)/translate $(SRCDIR)/add.c >$@
   834    845   
   835    846   $(OBJDIR)/add.o:	$(OBJDIR)/add_.c $(OBJDIR)/add.h $(SRCDIR)/config.h
   836    847   	$(XTCC) -o $(OBJDIR)/add.o -c $(OBJDIR)/add_.c
   837    848   
   838    849   $(OBJDIR)/add.h:	$(OBJDIR)/headers
          850  +
          851  +$(OBJDIR)/alerts_.c:	$(SRCDIR)/alerts.c $(OBJDIR)/translate
          852  +	$(OBJDIR)/translate $(SRCDIR)/alerts.c >$@
          853  +
          854  +$(OBJDIR)/alerts.o:	$(OBJDIR)/alerts_.c $(OBJDIR)/alerts.h $(SRCDIR)/config.h
          855  +	$(XTCC) -o $(OBJDIR)/alerts.o -c $(OBJDIR)/alerts_.c
          856  +
          857  +$(OBJDIR)/alerts.h:	$(OBJDIR)/headers
   839    858   
   840    859   $(OBJDIR)/allrepo_.c:	$(SRCDIR)/allrepo.c $(OBJDIR)/translate
   841    860   	$(OBJDIR)/translate $(SRCDIR)/allrepo.c >$@
   842    861   
   843    862   $(OBJDIR)/allrepo.o:	$(OBJDIR)/allrepo_.c $(OBJDIR)/allrepo.h $(SRCDIR)/config.h
   844    863   	$(XTCC) -o $(OBJDIR)/allrepo.o -c $(OBJDIR)/allrepo_.c
   845    864   
................................................................................
   920    939   $(OBJDIR)/cache_.c:	$(SRCDIR)/cache.c $(OBJDIR)/translate
   921    940   	$(OBJDIR)/translate $(SRCDIR)/cache.c >$@
   922    941   
   923    942   $(OBJDIR)/cache.o:	$(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h
   924    943   	$(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c
   925    944   
   926    945   $(OBJDIR)/cache.h:	$(OBJDIR)/headers
          946  +
          947  +$(OBJDIR)/capabilities_.c:	$(SRCDIR)/capabilities.c $(OBJDIR)/translate
          948  +	$(OBJDIR)/translate $(SRCDIR)/capabilities.c >$@
          949  +
          950  +$(OBJDIR)/capabilities.o:	$(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h
          951  +	$(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c
          952  +
          953  +$(OBJDIR)/capabilities.h:	$(OBJDIR)/headers
   927    954   
   928    955   $(OBJDIR)/captcha_.c:	$(SRCDIR)/captcha.c $(OBJDIR)/translate
   929    956   	$(OBJDIR)/translate $(SRCDIR)/captcha.c >$@
   930    957   
   931    958   $(OBJDIR)/captcha.o:	$(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h
   932    959   	$(XTCC) -o $(OBJDIR)/captcha.o -c $(OBJDIR)/captcha_.c
   933    960   
................................................................................
  1065   1092   	$(OBJDIR)/translate $(SRCDIR)/doc.c >$@
  1066   1093   
  1067   1094   $(OBJDIR)/doc.o:	$(OBJDIR)/doc_.c $(OBJDIR)/doc.h $(SRCDIR)/config.h
  1068   1095   	$(XTCC) -o $(OBJDIR)/doc.o -c $(OBJDIR)/doc_.c
  1069   1096   
  1070   1097   $(OBJDIR)/doc.h:	$(OBJDIR)/headers
  1071   1098   
  1072         -$(OBJDIR)/email_.c:	$(SRCDIR)/email.c $(OBJDIR)/translate
  1073         -	$(OBJDIR)/translate $(SRCDIR)/email.c >$@
  1074         -
  1075         -$(OBJDIR)/email.o:	$(OBJDIR)/email_.c $(OBJDIR)/email.h $(SRCDIR)/config.h
  1076         -	$(XTCC) -o $(OBJDIR)/email.o -c $(OBJDIR)/email_.c
  1077         -
  1078         -$(OBJDIR)/email.h:	$(OBJDIR)/headers
  1079         -
  1080   1099   $(OBJDIR)/encode_.c:	$(SRCDIR)/encode.c $(OBJDIR)/translate
  1081   1100   	$(OBJDIR)/translate $(SRCDIR)/encode.c >$@
  1082   1101   
  1083   1102   $(OBJDIR)/encode.o:	$(OBJDIR)/encode_.c $(OBJDIR)/encode.h $(SRCDIR)/config.h
  1084   1103   	$(XTCC) -o $(OBJDIR)/encode.o -c $(OBJDIR)/encode_.c
  1085   1104   
  1086   1105   $(OBJDIR)/encode.h:	$(OBJDIR)/headers
................................................................................
  1584   1603   $(OBJDIR)/setup_.c:	$(SRCDIR)/setup.c $(OBJDIR)/translate
  1585   1604   	$(OBJDIR)/translate $(SRCDIR)/setup.c >$@
  1586   1605   
  1587   1606   $(OBJDIR)/setup.o:	$(OBJDIR)/setup_.c $(OBJDIR)/setup.h $(SRCDIR)/config.h
  1588   1607   	$(XTCC) -o $(OBJDIR)/setup.o -c $(OBJDIR)/setup_.c
  1589   1608   
  1590   1609   $(OBJDIR)/setup.h:	$(OBJDIR)/headers
         1610  +
         1611  +$(OBJDIR)/setupuser_.c:	$(SRCDIR)/setupuser.c $(OBJDIR)/translate
         1612  +	$(OBJDIR)/translate $(SRCDIR)/setupuser.c >$@
         1613  +
         1614  +$(OBJDIR)/setupuser.o:	$(OBJDIR)/setupuser_.c $(OBJDIR)/setupuser.h $(SRCDIR)/config.h
         1615  +	$(XTCC) -o $(OBJDIR)/setupuser.o -c $(OBJDIR)/setupuser_.c
         1616  +
         1617  +$(OBJDIR)/setupuser.h:	$(OBJDIR)/headers
  1591   1618   
  1592   1619   $(OBJDIR)/sha1_.c:	$(SRCDIR)/sha1.c $(OBJDIR)/translate
  1593   1620   	$(OBJDIR)/translate $(SRCDIR)/sha1.c >$@
  1594   1621   
  1595   1622   $(OBJDIR)/sha1.o:	$(OBJDIR)/sha1_.c $(OBJDIR)/sha1.h $(SRCDIR)/config.h
  1596   1623   	$(XTCC) -o $(OBJDIR)/sha1.o -c $(OBJDIR)/sha1_.c
  1597   1624   

Changes to src/makemake.tcl.

    24     24   # project, simply add the basename to this list and rerun this script.
    25     25   #
    26     26   # Set the separate extra_files variable further down for how to add non-C
    27     27   # files, such as string and BLOB resources.
    28     28   #
    29     29   set src {
    30     30     add
           31  +  alerts
    31     32     allrepo
    32     33     attach
    33     34     backoffice
    34     35     bag
    35     36     bisect
    36     37     blob
    37     38     branch
    38     39     browse
    39     40     builtin
    40     41     bundle
    41     42     cache
           43  +  capabilities
    42     44     captcha
    43     45     cgi
    44     46     checkin
    45     47     checkout
    46     48     clearsign
    47     49     clone
    48     50     comformat
................................................................................
    53     55     delta
    54     56     deltacmd
    55     57     descendants
    56     58     diff
    57     59     diffcmd
    58     60     dispatch
    59     61     doc
    60         -  email
    61     62     encode
    62     63     etag
    63     64     event
    64     65     export
    65     66     file
    66     67     finfo
    67     68     foci
................................................................................
   117    118     regexp
   118    119     report
   119    120     rss
   120    121     schema
   121    122     search
   122    123     security_audit
   123    124     setup
          125  +  setupuser
   124    126     sha1
   125    127     sha1hard
   126    128     sha3
   127    129     shun
   128    130     sitemap
   129    131     skins
   130    132     smtp
................................................................................
   282    284   #
   283    285   # This file is included by primary Makefile.
   284    286   #
   285    287   
   286    288   XBCC = $(BCC) $(BCCFLAGS)
   287    289   XTCC = $(TCC) -I. -I$(SRCDIR) -I$(OBJDIR) $(TCCFLAGS)
   288    290   
          291  +TESTFLAGS := -quiet
   289    292   }
   290    293   writeln -nonewline "SRC ="
   291    294   foreach s [lsort $src] {
   292    295     writeln -nonewline " \\\n  \$(SRCDIR)/$s.c"
   293    296   }
   294    297   writeln "\n"
   295    298   writeln -nonewline "EXTRA_FILES ="
................................................................................
   357    360   #  -quiet    Hide most output from the terminal
   358    361   #  -strict   Treat known bugs as failures
   359    362   #
   360    363   # TESTFLAGS can also include names of specific test files to limit
   361    364   # the run to just those test cases.
   362    365   #
   363    366   test:	$(OBJDIR) $(APPNAME)
   364         -	$(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) -quiet $(TESTFLAGS)
          367  +	$(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME) $(TESTFLAGS)
   365    368   
   366    369   $(OBJDIR)/VERSION.h:	$(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION $(OBJDIR)/mkversion
   367    370   	$(OBJDIR)/mkversion $(SRCDIR)/../manifest.uuid \
   368    371   		$(SRCDIR)/../manifest \
   369    372   		$(SRCDIR)/../VERSION >$(OBJDIR)/VERSION.h
   370    373   
   371    374   $(OBJDIR)/default_css.h:	$(SRCDIR)/default_css.txt $(OBJDIR)/mkcss
................................................................................
   691    694   ZLIBCONFIG =
   692    695   ZLIBTARGETS =
   693    696   endif
   694    697   
   695    698   #### Disable creation of the OpenSSL shared libraries.  Also, disable support
   696    699   #    for both SSLv2 and SSLv3 (i.e. thereby forcing the use of TLS).
   697    700   #
   698         -SSLCONFIG += no-ssl2 no-ssl3 no-shared
          701  +SSLCONFIG += no-ssl2 no-ssl3 no-weak-ssl-ciphers no-shared
   699    702   
   700    703   #### When using zlib, make sure that OpenSSL is configured to use the zlib
   701    704   #    that Fossil knows about (i.e. the one within the source tree).
   702    705   #
   703    706   ifndef FOSSIL_ENABLE_MINIZ
   704    707   SSLCONFIG +=  --with-zlib-lib=$(PWD)/$(ZLIBDIR) --with-zlib-include=$(PWD)/$(ZLIBDIR) zlib
   705    708   endif
   706    709   
   707    710   #### The directories where the OpenSSL include and library files are located.
   708    711   #    The recommended usage here is to use the Sysinternals junction tool
   709    712   #    to create a hard link between an "openssl-1.x" sub-directory of the
   710    713   #    Fossil source code directory and the target OpenSSL source directory.
   711    714   #
   712         -OPENSSLDIR = $(SRCDIR)/../compat/openssl-1.0.2o
          715  +OPENSSLDIR = $(SRCDIR)/../compat/openssl-1.0.2p
   713    716   OPENSSLINCDIR = $(OPENSSLDIR)/include
   714    717   OPENSSLLIBDIR = $(OPENSSLDIR)
   715    718   
   716    719   #### Either the directory where the Tcl library is installed or the Tcl
   717    720   #    source code directory resides (depending on the value of the macro
   718    721   #    FOSSIL_TCL_SOURCE).  If this points to the Tcl install directory,
   719    722   #    this directory must have "include" and "lib" sub-directories.  If
................................................................................
  1573   1576   
  1574   1577   # Enable support for the SQLite Encryption Extension?
  1575   1578   !ifndef USE_SEE
  1576   1579   USE_SEE = 0
  1577   1580   !endif
  1578   1581   
  1579   1582   !if $(FOSSIL_ENABLE_SSL)!=0
  1580         -SSLDIR    = $(B)\compat\openssl-1.0.2o
         1583  +SSLDIR    = $(B)\compat\openssl-1.0.2p
  1581   1584   SSLINCDIR = $(SSLDIR)\inc32
  1582   1585   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1583   1586   SSLLIBDIR = $(SSLDIR)\out32dll
  1584   1587   !else
  1585   1588   SSLLIBDIR = $(SSLDIR)\out32
  1586   1589   !endif
  1587   1590   SSLLFLAGS = /nologo /opt:ref /debug
  1588   1591   SSLLIB    = ssleay32.lib libeay32.lib user32.lib gdi32.lib crypt32.lib
  1589   1592   !if "$(PLATFORM)"=="amd64" || "$(PLATFORM)"=="x64"
  1590   1593   !message Using 'x64' platform for OpenSSL...
  1591   1594   # BUGBUG (OpenSSL): Using "no-ssl*" here breaks the build.
  1592         -# SSLCONFIG = VC-WIN64A no-asm no-ssl2 no-ssl3
         1595  +# SSLCONFIG = VC-WIN64A no-asm no-ssl2 no-ssl3 no-weak-ssl-ciphers
  1593   1596   SSLCONFIG = VC-WIN64A no-asm
  1594   1597   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1595   1598   SSLCONFIG = $(SSLCONFIG) shared
  1596   1599   !else
  1597   1600   SSLCONFIG = $(SSLCONFIG) no-shared
  1598   1601   !endif
  1599   1602   SSLSETUP  = ms\do_win64a.bat
................................................................................
  1600   1603   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1601   1604   SSLNMAKE  = ms\ntdll.mak all
  1602   1605   !else
  1603   1606   SSLNMAKE  = ms\nt.mak all
  1604   1607   !endif
  1605   1608   # BUGBUG (OpenSSL): Using "OPENSSL_NO_SSL*" here breaks dynamic builds.
  1606   1609   !if $(FOSSIL_DYNAMIC_BUILD)==0
  1607         -SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3
         1610  +SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3 -DOPENSSL_NO_WEAK_SSL_CIPHERS
  1608   1611   !endif
  1609   1612   !elseif "$(PLATFORM)"=="ia64"
  1610   1613   !message Using 'ia64' platform for OpenSSL...
  1611   1614   # BUGBUG (OpenSSL): Using "no-ssl*" here breaks the build.
  1612         -# SSLCONFIG = VC-WIN64I no-asm no-ssl2 no-ssl3
         1615  +# SSLCONFIG = VC-WIN64I no-asm no-ssl2 no-ssl3 no-weak-ssl-ciphers
  1613   1616   SSLCONFIG = VC-WIN64I no-asm
  1614   1617   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1615   1618   SSLCONFIG = $(SSLCONFIG) shared
  1616   1619   !else
  1617   1620   SSLCONFIG = $(SSLCONFIG) no-shared
  1618   1621   !endif
  1619   1622   SSLSETUP  = ms\do_win64i.bat
................................................................................
  1620   1623   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1621   1624   SSLNMAKE  = ms\ntdll.mak all
  1622   1625   !else
  1623   1626   SSLNMAKE  = ms\nt.mak all
  1624   1627   !endif
  1625   1628   # BUGBUG (OpenSSL): Using "OPENSSL_NO_SSL*" here breaks dynamic builds.
  1626   1629   !if $(FOSSIL_DYNAMIC_BUILD)==0
  1627         -SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3
         1630  +SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3 -DOPENSSL_NO_WEAK_SSL_CIPHERS
  1628   1631   !endif
  1629   1632   !else
  1630   1633   !message Assuming 'x86' platform for OpenSSL...
  1631   1634   # BUGBUG (OpenSSL): Using "no-ssl*" here breaks the build.
  1632         -# SSLCONFIG = VC-WIN32 no-asm no-ssl2 no-ssl3
         1635  +# SSLCONFIG = VC-WIN32 no-asm no-ssl2 no-ssl3 no-weak-ssl-ciphers
  1633   1636   SSLCONFIG = VC-WIN32 no-asm
  1634   1637   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1635   1638   SSLCONFIG = $(SSLCONFIG) shared
  1636   1639   !else
  1637   1640   SSLCONFIG = $(SSLCONFIG) no-shared
  1638   1641   !endif
  1639   1642   SSLSETUP  = ms\do_ms.bat
................................................................................
  1640   1643   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1641   1644   SSLNMAKE  = ms\ntdll.mak all
  1642   1645   !else
  1643   1646   SSLNMAKE  = ms\nt.mak all
  1644   1647   !endif
  1645   1648   # BUGBUG (OpenSSL): Using "OPENSSL_NO_SSL*" here breaks dynamic builds.
  1646   1649   !if $(FOSSIL_DYNAMIC_BUILD)==0
  1647         -SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3
         1650  +SSLCFLAGS = -DOPENSSL_NO_SSL2 -DOPENSSL_NO_SSL3 -DOPENSSL_NO_WEAK_SSL_CIPHERS
  1648   1651   !endif
  1649   1652   !endif
  1650   1653   !endif
  1651   1654   
  1652   1655   !if $(FOSSIL_ENABLE_TCL)!=0
  1653   1656   TCLDIR    = $(B)\compat\tcl-8.6
  1654   1657   TCLSRCDIR = $(TCLDIR)
................................................................................
  1815   1818   writeln -nonewline "EXTRA_FILES   = "
  1816   1819   set i 0
  1817   1820   foreach s [lsort $extra_files] {
  1818   1821     if {$i > 0} {
  1819   1822       writeln " \\"
  1820   1823       writeln -nonewline "        "
  1821   1824     }
  1822         -  set s [file nativename $s]
         1825  +  set s [regsub -all / $s \\]
  1823   1826     writeln -nonewline "\"\$(SRCDIR)\\${s}\""; incr i
  1824   1827   }
  1825   1828   writeln "\n"
  1826   1829   set AdditionalObj [list shell sqlite3 th th_lang th_tcl cson_amalgamation]
  1827   1830   writeln -nonewline "OBJ   = "
  1828   1831   set i 0
  1829   1832   foreach s [lsort [concat $src $AdditionalObj]] {

Changes to src/manifest.c.

    32     32   #define CFTYPE_MANIFEST   1
    33     33   #define CFTYPE_CLUSTER    2
    34     34   #define CFTYPE_CONTROL    3
    35     35   #define CFTYPE_WIKI       4
    36     36   #define CFTYPE_TICKET     5
    37     37   #define CFTYPE_ATTACHMENT 6
    38     38   #define CFTYPE_EVENT      7
           39  +#define CFTYPE_FORUM      8
    39     40   
    40     41   /*
    41     42   ** File permissions used by Fossil internally.
    42     43   */
    43     44   #define PERM_REG          0     /*  regular file  */
    44     45   #define PERM_EXE          1     /*  executable    */
    45     46   #define PERM_LNK          2     /*  symlink       */
................................................................................
    74     75     char *zComment;       /* Decoded comment.  The C card. */
    75     76     double rDate;         /* Date and time from D card.  0.0 if no D card. */
    76     77     char *zUser;          /* Name of the user from the U card. */
    77     78     char *zRepoCksum;     /* MD5 checksum of the baseline content.  R card. */
    78     79     char *zWiki;          /* Text of the wiki page.  W card. */
    79     80     char *zWikiTitle;     /* Name of the wiki page. L card. */
    80     81     char *zMimetype;      /* Mime type of wiki or comment text.  N card.  */
           82  +  char *zThreadTitle;   /* The forum thread title. H card */
    81     83     double rEventDate;    /* Date of an event.  E card. */
    82     84     char *zEventId;       /* Artifact hash for an event.  E card. */
    83     85     char *zTicketUuid;    /* UUID for a ticket. K card. */
    84     86     char *zAttachName;    /* Filename of an attachment. A card. */
    85     87     char *zAttachSrc;     /* Artifact hash for document being attached. A card. */
    86     88     char *zAttachTarget;  /* Ticket or wiki that attachment applies to.  A card */
           89  +  char *zThreadRoot;    /* Thread root artifact.  G card */
           90  +  char *zInReplyTo;     /* Forum in-reply-to artifact.  I card */
    87     91     int nFile;            /* Number of F cards */
    88     92     int nFileAlloc;       /* Slots allocated in aFile[] */
    89     93     int iFile;            /* Index of current file in iterator */
    90     94     ManifestFile *aFile;  /* One entry for each F-card */
    91     95     int nParent;          /* Number of parents. */
    92     96     int nParentAlloc;     /* Slots allocated in azParent[] */
    93     97     char **azParent;      /* Hashes of parents.  One for each P card argument */
................................................................................
   110    114     int nFieldAlloc;      /* Slots allocated in aField[] */
   111    115     struct {
   112    116       char *zName;           /* Key or field name */
   113    117       char *zValue;          /* Value of the field */
   114    118     } *aField;            /* One for each J card */
   115    119   };
   116    120   #endif
          121  +
          122  +/*
          123  +** Allowed and required card types in each style of artifact
          124  +*/
          125  +static struct {
          126  +  const char *zAllowed;     /* Allowed cards.  Human-readable */
          127  +  const char *zRequired;    /* Required cards.  Human-readable */
          128  +} manifestCardTypes[] = {
          129  +  /*                           Allowed          Required    */
          130  +  /* CFTYPE_MANIFEST   1 */ { "BCDFNPQRTUZ",   "DZ"          },
          131  +                     /* Wants to be "CDUZ" ----^^^^
          132  +                     ** but we must limit for historical compatibility */
          133  +  /* CFTYPE_CLUSTER    2 */ { "MZ",            "MZ"          },
          134  +  /* CFTYPE_CONTROL    3 */ { "DTUZ",          "DTUZ"        },
          135  +  /* CFTYPE_WIKI       4 */ { "DLNPUWZ",       "DLUWZ"       },
          136  +  /* CFTYPE_TICKET     5 */ { "DJKUZ",         "DJKUZ"       },
          137  +  /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ",        "ADZ"         },
          138  +  /* CFTYPE_EVENT      7 */ { "CDENPTUWZ",     "DEWZ"        },
          139  +  /* CFTYPE_FORUM      8 */ { "DGHINPUWZ",     "DUWZ"        },
          140  +};
          141  +
          142  +/*
          143  +** Names of manifest types
          144  +*/
          145  +static const char *azNameOfMType[] = {
          146  +  "manifest",
          147  +  "cluster",
          148  +  "tag",
          149  +  "wiki",
          150  +  "ticket",
          151  +  "attachment",
          152  +  "technote",
          153  +  "forum post"
          154  +};
   117    155   
   118    156   /*
   119    157   ** A cache of parsed manifests.  This reduces the number of
   120    158   ** calls to manifest_parse() when doing a rebuild.
   121    159   */
   122    160   #define MX_MANIFEST_CACHE 6
   123    161   static struct {
................................................................................
   145    183       fossil_free(p->aField);
   146    184       fossil_free(p->aCherrypick);
   147    185       if( p->pBaseline ) manifest_destroy(p->pBaseline);
   148    186       memset(p, 0, sizeof(*p));
   149    187       fossil_free(p);
   150    188     }
   151    189   }
          190  +
          191  +/*
          192  +** Given a string of upper-case letters, compute a mask of the letters
          193  +** present.  For example,  "ABC" computes 0x0007.  "DE" gives 0x0018".
          194  +*/
          195  +static unsigned int manifest_card_mask(const char *z){
          196  +  unsigned int m = 0;
          197  +  char c;
          198  +  while( (c = *(z++))>='A' && c<='Z' ){
          199  +    m |= 1 << (c - 'A');
          200  +  }
          201  +  return m;
          202  +}
          203  +
          204  +/*
          205  +** Given an integer mask representing letters A-Z, return the
          206  +** letter which is the first bit set in the mask.  Example:
          207  +** 0x03520 gives 'F' since the F-bit is the lowest.
          208  +*/
          209  +static char maskToType(unsigned int x){
          210  +  char c = 'A';
          211  +  if( x==0 ) return '?';
          212  +  while( (x&1)==0 ){ x >>= 1; c++; }
          213  +  return c;
          214  +}
   152    215   
   153    216   /*
   154    217   ** Add an element to the manifest cache using LRU replacement.
   155    218   */
   156    219   void manifest_cache_insert(Manifest *p){
   157    220     while( p ){
   158    221       int i;
................................................................................
   350    413   ** Each card is divided into tokens by a single space character.
   351    414   ** The first token is a single upper-case letter which is the card type.
   352    415   ** The card type determines the other parameters to the card.
   353    416   ** Cards must occur in lexicographical order.
   354    417   */
   355    418   Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
   356    419     Manifest *p;
   357         -  int seenZ = 0;
   358    420     int i, lineNo=0;
   359    421     ManifestText x;
   360    422     char cPrevType = 0;
   361    423     char cType;
   362    424     char *z;
   363    425     int n;
   364    426     char *zUuid;
   365    427     int sz = 0;
   366         -  int isRepeat, hasSelfRefTag = 0;
          428  +  int isRepeat;
          429  +  int nSelfTag = 0;     /* Number of T cards referring to this manifest */
          430  +  int nSimpleTag = 0;   /* Number of T cards with "+" prefix */
   367    431     static Bag seen;
   368    432     const char *zErr = 0;
          433  +  unsigned int m;
          434  +  unsigned int seenCard = 0;   /* Which card types have been seen */
          435  +  char zErrBuf[100];           /* Write error messages here */
   369    436   
   370    437     if( rid==0 ){
   371    438       isRepeat = 1;
   372    439     }else if( bag_find(&seen, rid) ){
   373    440       isRepeat = 1;
   374    441     }else{
   375    442       isRepeat = 0;
................................................................................
   420    487     /* Begin parsing, card by card.
   421    488     */
   422    489     x.z = z;
   423    490     x.zEnd = &z[n];
   424    491     x.atEol = 1;
   425    492     while( (cType = next_card(&x))!=0 && cType>=cPrevType ){
   426    493       lineNo++;
          494  +    if( cType<'A' || cType>'Z' ) SYNTAX("bad card type");
          495  +    seenCard |= 1 << (cType-'A');
   427    496       switch( cType ){
   428    497         /*
   429    498         **     A <filename> <target> ?<source>?
   430    499         **
   431    500         ** Identifies an attachment to either a wiki page or a ticket.
   432    501         ** <source> is the artifact that is the attachment.  <source>
   433    502         ** is omitted to delete an attachment.  <target> is the name of
................................................................................
   452    521           }
   453    522           if( zSrc && !hname_validate(zSrc,nSrc) ){
   454    523             SYNTAX("invalid source on A-card");
   455    524           }
   456    525           p->zAttachName = (char*)file_tail(zName);
   457    526           p->zAttachSrc = zSrc;
   458    527           p->zAttachTarget = zTarget;
          528  +        p->type = CFTYPE_ATTACHMENT;
   459    529           break;
   460    530         }
   461    531   
   462    532         /*
   463    533         **    B <uuid>
   464    534         **
   465    535         ** A B-line gives the artifact hash for the baseline of a delta-manifest.
................................................................................
   467    537         case 'B': {
   468    538           if( p->zBaseline ) SYNTAX("more than one B-card");
   469    539           p->zBaseline = next_token(&x, &sz);
   470    540           if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
   471    541           if( !hname_validate(p->zBaseline,sz) ){
   472    542             SYNTAX("invalid hash on B-card");
   473    543           }
          544  +        p->type = CFTYPE_MANIFEST;
   474    545           break;
   475    546         }
   476    547   
   477    548   
   478    549         /*
   479    550         **     C <comment>
   480    551         **
................................................................................
   518    589           if( p->rEventDate>0.0 ) SYNTAX("more than one E-card");
   519    590           p->rEventDate = db_double(0.0,"SELECT julianday(%Q)", next_token(&x,0));
   520    591           if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
   521    592           p->zEventId = next_token(&x, &sz);
   522    593           if( !hname_validate(p->zEventId, sz) ){
   523    594             SYNTAX("malformed hash on E-card");
   524    595           }
          596  +        p->type = CFTYPE_EVENT;
   525    597           break;
   526    598         }
   527    599   
   528    600         /*
   529    601         **     F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
   530    602         **
   531    603         ** Identifies a file in a manifest.  Multiple F lines are
................................................................................
   563    635           p->aFile[i].zName = zName;
   564    636           p->aFile[i].zUuid = zUuid;
   565    637           p->aFile[i].zPerm = zPerm;
   566    638           p->aFile[i].zPrior = zPriorName;
   567    639           if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
   568    640             SYNTAX("incorrect F-card sort order");
   569    641           }
          642  +        p->type = CFTYPE_MANIFEST;
          643  +        break;
          644  +      }
          645  +
          646  +      /*
          647  +      **    G <hash>
          648  +      **
          649  +      ** A G-card identifies the initial root forum post for the thread
          650  +      ** of which this post is a part.  Forum posts only.
          651  +      */
          652  +      case 'G': {
          653  +        if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card");
          654  +        p->zThreadRoot = next_token(&x, &sz);
          655  +        if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card");
          656  +        if( !hname_validate(p->zThreadRoot,sz) ){
          657  +          SYNTAX("Invalid hash on G-card");
          658  +        }
          659  +        p->type = CFTYPE_FORUM;
          660  +        break;
          661  +      }
          662  +
          663  +      /*
          664  +      **     H <threadtitle>
          665  +      **
          666  +      ** The title for a forum thread.
          667  +      */
          668  +      case 'H': {
          669  +        if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card");
          670  +        p->zThreadTitle = next_token(&x,0);
          671  +        if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card");
          672  +        defossilize(p->zThreadTitle);
          673  +        p->type = CFTYPE_FORUM;
          674  +        break;
          675  +      }
          676  +
          677  +      /*
          678  +      **    I <hash>
          679  +      **
          680  +      ** A I-card identifies another forum post that the current forum post
          681  +      ** is in reply to.
          682  +      */
          683  +      case 'I': {
          684  +        if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card");
          685  +        p->zInReplyTo = next_token(&x, &sz);
          686  +        if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card");
          687  +        if( !hname_validate(p->zInReplyTo,sz) ){
          688  +          SYNTAX("Invalid hash on I-card");
          689  +        }
          690  +        p->type = CFTYPE_FORUM;
   570    691           break;
   571    692         }
   572    693   
   573    694         /*
   574    695         **     J <name> ?<value>?
   575    696         **
   576    697         ** Specifies a name value pair for ticket.  If the first character
................................................................................
   592    713           }
   593    714           i = p->nField++;
   594    715           p->aField[i].zName = zName;
   595    716           p->aField[i].zValue = zValue;
   596    717           if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
   597    718             SYNTAX("incorrect J-card sort order");
   598    719           }
          720  +        p->type = CFTYPE_TICKET;
   599    721           break;
   600    722         }
   601    723   
   602    724   
   603    725         /*
   604    726         **    K <uuid>
   605    727         **
................................................................................
   609    731         case 'K': {
   610    732           if( p->zTicketUuid!=0 ) SYNTAX("more than one K-card");
   611    733           p->zTicketUuid = next_token(&x, &sz);
   612    734           if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
   613    735           if( !validate16(p->zTicketUuid, sz) ){
   614    736             SYNTAX("invalid K-card UUID");
   615    737           }
          738  +        p->type = CFTYPE_TICKET;
   616    739           break;
   617    740         }
   618    741   
   619    742         /*
   620    743         **     L <wikititle>
   621    744         **
   622    745         ** The wiki page title is fossil-encoded.  There may be no more than
................................................................................
   626    749           if( p->zWikiTitle!=0 ) SYNTAX("more than one L-card");
   627    750           p->zWikiTitle = next_token(&x,0);
   628    751           if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
   629    752           defossilize(p->zWikiTitle);
   630    753           if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
   631    754             SYNTAX("L-card has malformed wiki name");
   632    755           }
          756  +        p->type = CFTYPE_WIKI;
   633    757           break;
   634    758         }
   635    759   
   636    760         /*
   637    761         **    M <hash>
   638    762         **
   639    763         ** An M-line identifies another artifact by its hash.  M-lines
................................................................................
   651    775                                    , p->nCChildAlloc*sizeof(p->azCChild[0]) );
   652    776           }
   653    777           i = p->nCChild++;
   654    778           p->azCChild[i] = zUuid;
   655    779           if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
   656    780             SYNTAX("M-card in the wrong order");
   657    781           }
          782  +        p->type = CFTYPE_CLUSTER;
   658    783           break;
   659    784         }
   660    785   
   661    786         /*
   662    787         **    N <uuid>
   663    788         **
   664    789         ** An N-line identifies the mimetype of wiki or comment text.
................................................................................
   715    840           p->aCherrypick = fossil_realloc(p->aCherrypick,
   716    841                                    p->nCherrypick*sizeof(p->aCherrypick[0]));
   717    842           p->aCherrypick[n].zCPTarget = zUuid;
   718    843           p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
   719    844           if( zUuid && !hname_validate(zUuid,sz) ){
   720    845             SYNTAX("invalid second hash on Q-card");
   721    846           }
          847  +        p->type = CFTYPE_MANIFEST;
   722    848           break;
   723    849         }
   724    850   
   725    851         /*
   726    852         **     R <md5sum>
   727    853         **
   728    854         ** Specify the MD5 checksum over the name and content of all files
................................................................................
   729    855         ** in the manifest.
   730    856         */
   731    857         case 'R': {
   732    858           if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
   733    859           p->zRepoCksum = next_token(&x, &sz);
   734    860           if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
   735    861           if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
          862  +        p->type = CFTYPE_MANIFEST;
   736    863           break;
   737    864         }
   738    865   
   739    866         /*
   740    867         **    T (+|*|-)<tagname> <uuid> ?<value>?
   741    868         **
   742    869         ** Create or cancel a tag or property.  The tagname is fossil-encoded.
................................................................................
   757    884           if( zName==0 ) SYNTAX("missing name on T-card");
   758    885           zUuid = next_token(&x, &sz);
   759    886           if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
   760    887           zValue = next_token(&x, 0);
   761    888           if( zValue ) defossilize(zValue);
   762    889           if( hname_validate(zUuid, sz) ){
   763    890             /* A valid artifact hash */
   764         -          if( p->zEventId ) SYNTAX("non-self-referential T-card in event");
   765    891           }else if( sz==1 && zUuid[0]=='*' ){
   766    892             zUuid = 0;
   767         -          hasSelfRefTag = 1;
   768         -          if( p->zEventId && zName[0]!='+' ){
   769         -            SYNTAX("propagating T-card in event");
   770         -          }
          893  +          nSelfTag++;
   771    894           }else{
   772    895             SYNTAX("malformed artifact hash on T-card");
   773    896           }
   774    897           defossilize(zName);
   775    898           if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){
   776    899             SYNTAX("T-card name does not begin with '-', '+', or '*'");
   777    900           }
          901  +        if( zName[0]=='+' ) nSimpleTag++;
   778    902           if( validate16(&zName[1], strlen(&zName[1])) ){
   779    903             /* Do not allow tags whose names look like a hash */
   780    904             SYNTAX("T-card name looks like a hexadecimal hash");
   781    905           }
   782    906           if( p->nTag>=p->nTagAlloc ){
   783    907             p->nTagAlloc = p->nTagAlloc*2 + 10;
   784    908             p->aTag = fossil_realloc(p->aTag, p->nTagAlloc*sizeof(p->aTag[0]) );
................................................................................
   856    980         ** Manifest.  It is not required for manifest only for historical
   857    981         ** compatibility reasons.
   858    982         */
   859    983         case 'Z': {
   860    984           zUuid = next_token(&x, &sz);
   861    985           if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
   862    986           if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
   863         -        seenZ = 1;
   864    987           break;
   865    988         }
   866    989         default: {
   867    990           SYNTAX("unrecognized card");
   868    991         }
   869    992       }
   870    993     }
   871    994     if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
   872    995   
   873         -  if( p->nCChild>0 ){
   874         -    if( p->zAttachName
   875         -     || p->zBaseline
   876         -     || p->zComment
   877         -     || p->rDate>0.0
   878         -     || p->zEventId
   879         -     || p->nFile>0
   880         -     || p->nField>0
   881         -     || p->zTicketUuid
   882         -     || p->zWikiTitle
   883         -     || p->zMimetype
   884         -     || p->nParent>0
   885         -     || p->nCherrypick>0
   886         -     || p->zRepoCksum
   887         -     || p->nTag>0
   888         -     || p->zUser
   889         -     || p->zWiki
   890         -    ){
   891         -      SYNTAX("cluster contains a card other than M- or Z-");
   892         -    }
   893         -    if( !seenZ ) SYNTAX("missing Z-card on cluster");
   894         -    p->type = CFTYPE_CLUSTER;
   895         -  }else if( p->zEventId ){
   896         -    if( p->zAttachName ) SYNTAX("A-card in event");
   897         -    if( p->zBaseline ) SYNTAX("B-card in event");
   898         -    if( p->rDate<=0.0 ) SYNTAX("missing date on event");
   899         -    if( p->nFile>0 ) SYNTAX("F-card in event");
   900         -    if( p->nField>0 ) SYNTAX("J-card in event");
   901         -    if( p->zTicketUuid ) SYNTAX("K-card in event");
   902         -    if( p->zWikiTitle!=0 ) SYNTAX("L-card in event");
   903         -    if( p->zRepoCksum ) SYNTAX("R-card in event");
   904         -    if( p->zWiki==0 ) SYNTAX("missing W-card on event");
   905         -    if( !seenZ ) SYNTAX("missing Z-card on event");
   906         -    p->type = CFTYPE_EVENT;
   907         -  }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){
   908         -    if( p->zAttachName ) SYNTAX("A-card in wiki");
   909         -    if( p->zBaseline ) SYNTAX("B-card in wiki");
   910         -    if( p->rDate<=0.0 ) SYNTAX("missing date on wiki");
   911         -    if( p->nFile>0 ) SYNTAX("F-card in wiki");
   912         -    if( p->nField>0 ) SYNTAX("J-card in wiki");
   913         -    if( p->zTicketUuid ) SYNTAX("K-card in wiki");
   914         -    if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki");
   915         -    if( p->zRepoCksum ) SYNTAX("R-card in wiki");
   916         -    if( p->nTag>0 ) SYNTAX("T-card in wiki");
   917         -    if( p->zWiki==0 ) SYNTAX("missing W-card on wiki");
   918         -    if( !seenZ ) SYNTAX("missing Z-card on wiki");
   919         -    p->type = CFTYPE_WIKI;
   920         -  }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline
   921         -      || p->nParent>0 ){
   922         -    if( p->zAttachName ) SYNTAX("A-card in manifest");
   923         -    if( p->rDate<=0.0 ) SYNTAX("missing date on manifest");
   924         -    if( p->nField>0 ) SYNTAX("J-card in manifest");
   925         -    if( p->zTicketUuid ) SYNTAX("K-card in manifest");
   926         -    p->type = CFTYPE_MANIFEST;
   927         -  }else if( p->nField>0 || p->zTicketUuid!=0 ){
   928         -    if( p->zAttachName ) SYNTAX("A-card in ticket");
   929         -    if( p->rDate<=0.0 ) SYNTAX("missing date on ticket");
   930         -    if( p->nField==0 ) SYNTAX("missing J-card on ticket");
   931         -    if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket");
   932         -    if( p->zMimetype) SYNTAX("N-card in ticket");
   933         -    if( p->nTag>0 ) SYNTAX("T-card in ticket");
   934         -    if( p->zUser==0 ) SYNTAX("missing U-card on ticket");
   935         -    if( !seenZ ) SYNTAX("missing Z-card on ticket");
   936         -    p->type = CFTYPE_TICKET;
   937         -  }else if( p->zAttachName ){
   938         -    if( p->rDate<=0.0 ) SYNTAX("missing date on attachment");
   939         -    if( p->nTag>0 ) SYNTAX("T-card in attachment");
   940         -    if( !seenZ ) SYNTAX("missing Z-card on attachment");
   941         -    p->type = CFTYPE_ATTACHMENT;
   942         -  }else{
   943         -    if( p->rDate<=0.0 ) SYNTAX("missing date on control");
   944         -    if( p->zMimetype ) SYNTAX("N-card in control");
   945         -    if( !seenZ ) SYNTAX("missing Z-card on control");
   946         -    p->type = CFTYPE_CONTROL;
   947         -  }
          996  +  /* If the artifact type has not yet been determined, then compute
          997  +  ** it now. */
          998  +  if( p->type==0 ){
          999  +    if( p->zComment!=0 || p->nFile>0 || p->nParent>0 ){
         1000  +      p->type = CFTYPE_MANIFEST;
         1001  +    }else{
         1002  +      p->type = CFTYPE_CONTROL;
         1003  +    }
         1004  +  }
         1005  +
         1006  +  /* Verify that no disallowed cards are present for this artifact type */
         1007  +  m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed);
         1008  +  if( seenCard & ~m ){
         1009  +    sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s",
         1010  +                     maskToType(seenCard & ~m),
         1011  +                     azNameOfMType[p->type-1]);
         1012  +    zErr = zErrBuf;
         1013  +    goto manifest_syntax_error;
         1014  +  }
         1015  +
         1016  +  /* Verify that all required cards are present for this artifact type */
         1017  +  m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired);
         1018  +  if( ~seenCard & m ){
         1019  +    sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s",
         1020  +                     maskToType(~seenCard & m),
         1021  +                     azNameOfMType[p->type-1]);
         1022  +    zErr = zErrBuf;
         1023  +    goto manifest_syntax_error;
         1024  +  }
         1025  +
         1026  +  /* Additional checks based on artifact type */
         1027  +  switch( p->type ){
         1028  +    case CFTYPE_CONTROL: {
         1029  +      if( nSelfTag ) SYNTAX("self-referential T-card in control artifact");
         1030  +      break;
         1031  +    }
         1032  +    case CFTYPE_EVENT: {
         1033  +      if( p->nTag!=nSelfTag ){
         1034  +        SYNTAX("non-self-referential T-card in technote");
         1035  +      }
         1036  +      if( p->nTag!=nSimpleTag ){
         1037  +        SYNTAX("T-card with '*' or '-' in technote");
         1038  +      }
         1039  +      break;
         1040  +    }
         1041  +    case CFTYPE_FORUM: {
         1042  +      if( p->zThreadTitle && p->zInReplyTo ){
         1043  +        SYNTAX("cannot have I-card and H-card in a forum post");
         1044  +      }
         1045  +      if( p->nParent>1 ) SYNTAX("too many arguments to P-card");
         1046  +      break;
         1047  +    }
         1048  +  }
         1049  +
   948   1050     md5sum_init();
   949   1051     if( !isRepeat ) g.parseCnt[p->type]++;
   950   1052     return p;
   951   1053   
   952   1054   manifest_syntax_error:
   953   1055     {
   954   1056       char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
   955   1057       if( zUuid ){
   956         -      blob_appendf(pErr, "manifest [%s] ", zUuid);
         1058  +      blob_appendf(pErr, "artifact [%s] ", zUuid);
   957   1059         fossil_free(zUuid);
   958   1060       }
   959   1061     }
   960   1062     if( zErr ){
   961   1063       blob_appendf(pErr, "line %d: %s", lineNo, zErr);
   962   1064     }else{
   963   1065       blob_appendf(pErr, "unknown error on line %d", lineNo);
................................................................................
  1013   1115   }
  1014   1116   
  1015   1117   /*
  1016   1118   ** COMMAND: test-parse-manifest
  1017   1119   **
  1018   1120   ** Usage: %fossil test-parse-manifest FILENAME ?N?
  1019   1121   **
  1020         -** Parse the manifest and discarded.  Use for testing only.
         1122  +** Parse the manifest(s) given on the command-line and report any
         1123  +** errors.  If the N argument is given, run the parsing N times.
  1021   1124   */
  1022   1125   void manifest_test_parse_cmd(void){
  1023   1126     Manifest *p;
  1024   1127     Blob b;
  1025   1128     int i;
  1026   1129     int n = 1;
  1027         -  sqlite3_open(":memory:", &g.db);
         1130  +  db_find_and_open_repository(0,0);
         1131  +  verify_all_options();
  1028   1132     if( g.argc!=3 && g.argc!=4 ){
  1029   1133       usage("FILENAME");
  1030   1134     }
  1031   1135     blob_read_from_file(&b, g.argv[2], ExtFILE);
  1032   1136     if( g.argc>3 ) n = atoi(g.argv[3]);
  1033   1137     for(i=0; i<n; i++){
  1034   1138       Blob b2;
................................................................................
  1037   1141       blob_zero(&err);
  1038   1142       p = manifest_parse(&b2, 0, &err);
  1039   1143       if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err));
  1040   1144       blob_reset(&err);
  1041   1145       manifest_destroy(p);
  1042   1146     }
  1043   1147   }
         1148  +
         1149  +/*
         1150  +** COMMAND: test-parse-all-blobs
         1151  +**
         1152  +** Usage: %fossil test-parse-all-blobs
         1153  +**
         1154  +** Parse all entries in the BLOB table that are believed to be non-data
         1155  +** artifacts and report any errors.  Run this test command on historical
         1156  +** repositories after making any changes to the manifest_parse()
         1157  +** implementation to confirm that the changes did not break anything.
         1158  +*/
         1159  +void manifest_test_parse_all_blobs_cmd(void){
         1160  +  Manifest *p;
         1161  +  Blob err;
         1162  +  Stmt q;
         1163  +  int nTest = 0;
         1164  +  int nErr = 0;
         1165  +  db_find_and_open_repository(0, 0);
         1166  +  verify_all_options();
         1167  +  db_prepare(&q, "SELECT DISTINCT objid FROM EVENT");
         1168  +  while( db_step(&q)==SQLITE_ROW ){
         1169  +    int id = db_column_int(&q,0);
         1170  +    fossil_print("Checking %d       \r", id);
         1171  +    nTest++;
         1172  +    fflush(stdout);
         1173  +    blob_init(&err, 0, 0);
         1174  +    p = manifest_get(id, CFTYPE_ANY, &err);
         1175  +    if( p==0 ){
         1176  +      fossil_print("%d ERROR: %s\n", id, blob_str(&err));
         1177  +      nErr++;
         1178  +    }
         1179  +    blob_reset(&err);
         1180  +    manifest_destroy(p);
         1181  +  }
         1182  +  db_finalize(&q);
         1183  +  fossil_print("%d tests with %d errors\n", nTest, nErr);
         1184  +}
  1044   1185   
  1045   1186   /*
  1046   1187   ** Fetch the baseline associated with the delta-manifest p.
  1047   1188   ** Return 0 on success.  If unable to parse the baseline,
  1048   1189   ** throw an error.  If the baseline is a manifest, throw an
  1049   1190   ** error if throwError is true, or record that p is an orphan
  1050   1191   ** and return 1 if throwError is false.
................................................................................
  1057   1198         if( !throwError ){
  1058   1199           db_multi_exec(
  1059   1200              "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)",
  1060   1201              p->rid, rid
  1061   1202           );
  1062   1203           return 1;
  1063   1204         }
  1064         -      fossil_panic("cannot access baseline manifest %S", p->zBaseline);
         1205  +      fossil_fatal("cannot access baseline manifest %S", p->zBaseline);
  1065   1206       }
  1066   1207     }
  1067   1208     return 0;
  1068   1209   }
  1069   1210   
  1070   1211   /*
  1071   1212   ** Rewind a manifest-file iterator back to the beginning of the manifest.
................................................................................
  2376   2517       if( blob_size(&comment)==0 ) blob_append(&comment, " ", 1);
  2377   2518       db_multi_exec(
  2378   2519         "REPLACE INTO event(type,mtime,objid,user,comment)"
  2379   2520         "VALUES('g',%.17g,%d,%Q,%Q)",
  2380   2521         p->rDate, rid, p->zUser, blob_str(&comment)+1
  2381   2522       );
  2382   2523       blob_reset(&comment);
         2524  +  }
         2525  +  if( p->type==CFTYPE_FORUM ){
         2526  +    int froot, fprev, firt;
         2527  +    char *zFType;
         2528  +    char *zTitle;
         2529  +    schema_forum();
         2530  +    froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid;
         2531  +    fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0;
         2532  +    firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0;
         2533  +    db_multi_exec(
         2534  +      "INSERT INTO forumpost(fpid,froot,fprev,firt,fmtime)"
         2535  +      "VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)",
         2536  +      p->rid, froot, fprev, firt, p->rDate
         2537  +    );
         2538  +    if( firt==0 ){
         2539  +      /* This is the start of a new thread, either the initial entry
         2540  +      ** or an edit of the initial entry. */
         2541  +      zTitle = p->zThreadTitle;
         2542  +      if( zTitle==0 || zTitle[0]==0 ){
         2543  +        zTitle = "(Deleted)";
         2544  +      }
         2545  +      zFType = fprev ? "Edit" : "Post";
         2546  +      db_multi_exec(
         2547  +        "REPLACE INTO event(type,mtime,objid,user,comment)"
         2548  +        "VALUES('f',%.17g,%d,%Q,'%q: %q')",
         2549  +        p->rDate, rid, p->zUser, zFType, zTitle
         2550  +      );
         2551  +      /*
         2552  +      ** If this edit is the most recent, then make it the title for
         2553  +      ** all other entries for the same thread
         2554  +      */
         2555  +      if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0"
         2556  +                     "   AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate)
         2557  +      ){
         2558  +        /* This entry establishes a new title for all entries on the thread */
         2559  +        db_multi_exec(
         2560  +          "UPDATE event"
         2561  +          " SET comment=substr(comment,1,instr(comment,':')) || ' %q'"
         2562  +          " WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)",
         2563  +          zTitle, froot
         2564  +        );
         2565  +      }
         2566  +    }else{
         2567  +      /* This is a reply to a prior post.  Take the title from the root. */
         2568  +      zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)"
         2569  +                          "  FROM event WHERE objid=%d", froot);
         2570  +      if( zTitle==0 ) zTitle = fossil_strdup("<i>Unknown</i>");
         2571  +      if( p->zWiki[0]==0 ){
         2572  +        zFType = "Delete reply";
         2573  +      }else if( fprev ){
         2574  +        zFType = "Edit reply";
         2575  +      }else{
         2576  +        zFType = "Reply";
         2577  +      }
         2578  +      db_multi_exec(
         2579  +        "REPLACE INTO event(type,mtime,objid,user,comment)"
         2580  +        "VALUES('f',%.17g,%d,%Q,'%q: %q')",
         2581  +        p->rDate, rid, p->zUser, zFType, zTitle
         2582  +      );
         2583  +      fossil_free(zTitle);
         2584  +    }
  2383   2585     }
  2384   2586     db_end_transaction(0);
  2385   2587     if( permitHooks ){
  2386   2588       rc = xfer_run_common_script();
  2387   2589       if( rc==TH_OK ){
  2388   2590         rc = xfer_run_script(zScript, zUuid, 0);
  2389   2591       }

Changes to src/markdown.md.

    91     91   
    92     92   > The first row is a header if followed by a horizontal rule or a blank line.
    93     93   
    94     94   > Placing **:** at the left, both, or right sides of a cell gives left-aligned,
    95     95   > centered, or right-aligned text, respectively.  By default, header cells are
    96     96   > centered, and body cells are left-aligned.
    97     97   
    98         -> The leftmost **\|** is required if the first column contains at least one
    99         -> blank cell.  The rightmost **\|** is optional.
           98  +> The leftmost or rightmost **\|** is required only if the first or last column,
           99  +> respectively, contains at least one blank cell.
   100    100   
   101    101   ## Miscellaneous ##
   102    102   
   103    103   > *   In-line images are made using **\!\[alt-text\]\(image-URL\)**.
   104    104   > *   Use HTML for advanced formatting such as forms.
   105    105   > *   **\<!--** HTML-style comments **-->** are supported.
   106    106   > *   Escape special characters (ex: **\[** **\(** **\|** **\***)

Changes to src/mkbuiltin.c.

    66     66   }
    67     67   
    68     68   /*
    69     69   ** There is an instance of the following for each file translated.
    70     70   */
    71     71   typedef struct Resource Resource;
    72     72   struct Resource {
    73         -  const char *zName;
           73  +  char *zName;
    74     74     int nByte;
    75     75     int idx;
    76     76   };
    77     77   
    78     78   typedef struct ResourceList ResourceList;
    79     79   struct ResourceList {
    80     80       Resource *aRes;
................................................................................
   337    337     printf("struct BuiltinFileTable {\n");
   338    338     printf("  const char *zName;\n");
   339    339     printf("  const unsigned char *pData;\n");
   340    340     printf("  int nByte;\n");
   341    341     printf("};\n");
   342    342     printf("static const BuiltinFileTable aBuiltinFiles[] = {\n");
   343    343     for(i=0; i<nRes; i++){
   344         -    const char *z = aRes[i].zName;
          344  +    char *z = aRes[i].zName;
   345    345       if( strlen(z)>=nPrefix ) z += nPrefix;
   346         -    while( z[0]=='.' || z[0]=='/' ){ z++; }
          346  +    while( z[0]=='.' || z[0]=='/' || z[0]=='\\' ){ z++; }
   347    347       aRes[i].zName = z;
          348  +    while( z[0] ){
          349  +      if( z[0]=='\\' ) z[0] = '/';
          350  +      z++;
          351  +    }
   348    352     }
   349    353     qsort(aRes, nRes, sizeof(aRes[0]), (QsortCompareFunc)compareResource);
   350    354     for(i=0; i<nRes; i++){
   351    355       printf("  { \"%s\", bidata%d, %d },\n",
   352    356              aRes[i].zName, aRes[i].idx, aRes[i].nByte);
   353    357     }
   354    358     printf("};\n");
   355    359     free_reslist(&resList);
   356    360     return nErr;
   357    361   }

Changes to src/moderate.c.

    52     52     if( rid==0 || !moderation_table_exists() ) return 0;
    53     53     db_static_prepare(&q, "SELECT 1 FROM modreq WHERE objid=:objid");
    54     54     db_bind_int(&q, ":objid", rid);
    55     55     rc = db_step(&q)==SQLITE_ROW;
    56     56     db_reset(&q);
    57     57     return rc;
    58     58   }
           59  +
           60  +/*
           61  +** If the rid object is being held for moderation, write out
           62  +** an "awaiting moderation" message and return true.
           63  +**
           64  +** If the object is not being held for moderation, simply return
           65  +** false without generating any output.
           66  +*/
           67  +int moderation_pending_www(int rid){
           68  +  int pending = moderation_pending(rid);
           69  +  if( pending ){
           70  +    @ <span class="modpending">(Awaiting Moderator Approval)</span>
           71  +  }
           72  +  return pending;
           73  +}
           74  +
           75  +
           76  +/*
           77  +** Return TRUE if there any pending moderation requests.
           78  +*/
           79  +int moderation_needed(void){
           80  +  if( !moderation_table_exists() ) return 0;
           81  +  return db_exists("SELECT 1 FROM modreq");
           82  +}
    59     83   
    60     84   /*
    61     85   ** Check to see if the object identified by RID is used for anything.
    62     86   */
    63     87   static int object_used(int rid){
    64     88     static const char *const aTabField[] = {
    65     89        "modreq",     "attachRid",
................................................................................
    99    123         "DELETE FROM delta WHERE rid=%d;"
   100    124         "DELETE FROM event WHERE objid=%d;"
   101    125         "DELETE FROM tagxref WHERE rid=%d;"
   102    126         "DELETE FROM private WHERE rid=%d;"
   103    127         "DELETE FROM attachment WHERE attachid=%d;",
   104    128         rid, rid, rid, rid, rid, rid
   105    129       );
          130  +    if( db_table_exists("repository","forumpost") ){
          131  +      db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid);
          132  +    }
   106    133       zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid);
   107    134       if( zTktid && zTktid[0] ){
   108    135         ticket_rebuild_entry(zTktid);
   109    136         fossil_free(zTktid);
   110    137       }
   111    138       attachRid = db_int(0, "SELECT attachRid FROM modreq WHERE objid=%d", rid);
   112    139       if( rid==objid ){
................................................................................
   142    169   ** Show all pending moderation request
   143    170   */
   144    171   void modreq_page(void){
   145    172     Blob sql;
   146    173     Stmt q;
   147    174   
   148    175     login_check_credentials();
   149         -  if( !g.perm.ModWiki && !g.perm.ModTkt ){
   150         -    login_needed(g.anon.ModWiki && g.anon.ModTkt);
          176  +  if( !g.perm.ModWiki && !g.perm.ModTkt && !g.perm.ModForum ){
          177  +    login_needed(g.anon.ModWiki && g.anon.ModTkt && g.anon.ModForum);
   151    178       return;
   152    179     }
   153    180     style_header("Pending Moderation Requests");
   154    181     @ <h2>All Pending Moderation Requests</h2>
   155    182     if( moderation_table_exists() ){
   156    183       blob_init(&sql, timeline_query_for_www(), -1);
   157    184       blob_append_sql(&sql,

Changes to src/name.c.

    92     92   **   *  "current"
    93     93   **   *  "prev" or "previous"
    94     94   **   *  "next"
    95     95   **
    96     96   ** Return the RID of the matching artifact.  Or return 0 if the name does not
    97     97   ** match any known object.  Or return -1 if the name is ambiguous.
    98     98   **
    99         -** The zType parameter specifies the type of artifact: ci, t, w, e, g.
           99  +** The zType parameter specifies the type of artifact: ci, t, w, e, g, f.
   100    100   ** If zType is NULL or "" or "*" then any type of artifact will serve.
   101    101   ** If zType is "br" then find the first check-in of the named branch
   102    102   ** rather than the last.
   103    103   ** zType is "ci" in most use cases since we are usually searching for
   104    104   ** a check-in.
   105    105   **
   106    106   ** Note that the input zTag for types "t" and "e" is the artifact hash of
   107         -** the ticket-change or event-change artifact, not the randomly generated
          107  +** the ticket-change or technote-change artifact, not the randomly generated
   108    108   ** hexadecimal identifier assigned to tickets and events.  Those identifiers
   109    109   ** live in a separate namespace.
   110    110   */
   111    111   int symbolic_name_to_rid(const char *zTag, const char *zType){
   112    112     int vid;
   113    113     int rid = 0;
   114    114     int nTag;
................................................................................
   601    601        "       coalesce(euser,user), coalesce(ecomment,comment)"
   602    602        "  FROM event WHERE objid=%d", rid);
   603    603     if( db_step(&q)==SQLITE_ROW ){
   604    604       const char *zType;
   605    605       switch( db_column_text(&q,0)[0] ){
   606    606         case 'c':  zType = "Check-in";       break;
   607    607         case 'w':  zType = "Wiki-edit";      break;
   608         -      case 'e':  zType = "Event";          break;
          608  +      case 'e':  zType = "Technote";       break;
          609  +      case 'f':  zType = "Forum-post";     break;
   609    610         case 't':  zType = "Ticket-change";  break;
   610    611         case 'g':  zType = "Tag-change";     break;
   611    612         default:   zType = "Unknown";        break;
   612    613       }
   613    614       fossil_print("type:       %s by %s on %s\n", zType, db_column_text(&q,2),
   614    615                    db_column_text(&q, 1));
   615    616       fossil_print("comment:    ");
................................................................................
   924    925       "       'attachment '||attachment.filename\n"
   925    926       "  FROM attachment, blob\n"
   926    927       " WHERE (blob.rid %s)\n"
   927    928       "   AND blob.rid NOT IN (SELECT rid FROM description)\n"
   928    929       "   AND blob.uuid=attachment.src",
   929    930       zWhere /*safe-for-%s*/
   930    931     );
          932  +
          933  +  /* Forum posts */
          934  +  if( db_table_exists("repository","forumpost") ){
          935  +    db_multi_exec(
          936  +      "INSERT OR IGNORE INTO description(rid,uuid,ctime,type,summary)\n"
          937  +      "SELECT postblob.rid, postblob.uuid, forumpost.fmtime, 'forumpost',\n"
          938  +      "       CASE WHEN fpid=froot THEN 'forum-post '\n"
          939  +      "            ELSE 'forum-reply-to ' END || substr(rootblob.uuid,1,14)\n"
          940  +      "  FROM forumpost, blob AS postblob, blob AS rootblob\n"
          941  +      " WHERE (forumpost.fpid %s)\n"
          942  +      "   AND postblob.rid=forumpost.fpid"
          943  +      "   AND rootblob.rid=forumpost.froot",
          944  +      zWhere /*safe-for-%s*/
          945  +    );
          946  +  }
   931    947   
   932    948     /* Everything else */
   933    949     db_multi_exec(
   934    950       "INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n"
   935    951       "SELECT blob.rid, blob.uuid,"
   936    952       "       CASE WHEN blob.size<0 THEN 'phantom' ELSE '' END,\n"
   937    953       "       'unknown'\n"

Changes to src/popen.c.

    23     23   #ifdef _WIN32
    24     24   #include <windows.h>
    25     25   #include <fcntl.h>
    26     26   /*
    27     27   ** Print a fatal error and quit.
    28     28   */
    29     29   static void win32_fatal_error(const char *zMsg){
    30         -  fossil_panic("%s", zMsg);
           30  +  fossil_fatal("%s", zMsg);
    31     31   }
    32     32   #else
    33     33   #include <signal.h>
    34     34   #include <sys/wait.h>
    35     35   #endif
    36     36   
    37     37   /*

Changes to src/printf.c.

  1079   1079     int rc = 1;
  1080   1080     char z[1000];
  1081   1081     static int once = 0;
  1082   1082   
  1083   1083     if( once ) exit(1);
  1084   1084     once = 1;
  1085   1085     mainInFatalError = 1;
  1086         -  db_force_rollback();
         1086  +  /* db_force_rollback(); */
  1087   1087     va_start(ap, zFormat);
  1088   1088     sqlite3_vsnprintf(sizeof(z),z,zFormat, ap);
  1089   1089     va_end(ap);
         1090  +  if( g.fAnyTrace ){
         1091  +    fprintf(stderr, "/***** panic on %d *****/\n", getpid());
         1092  +  }
  1090   1093     fossil_errorlog("panic: %s", z);
  1091   1094     rc = fossil_print_error(rc, z);
         1095  +  abort();
  1092   1096     exit(rc);
  1093   1097   }
  1094   1098   NORETURN void fossil_fatal(const char *zFormat, ...){
  1095   1099     char *z;
  1096   1100     int rc = 1;
  1097   1101     va_list ap;
  1098   1102     mainInFatalError = 1;

Changes to src/rebuild.c.

   356    356   
   357    357     bag_init(&bagDone);
   358    358     ttyOutput = doOut;
   359    359     processCnt = 0;
   360    360     if (ttyOutput && !g.fQuiet) {
   361    361       percent_complete(0);
   362    362     }
   363         -  email_triggers_disable();
          363  +  alert_triggers_disable();
   364    364     rebuild_update_schema();
   365    365     blob_init(&sql, 0, 0);
   366    366     db_prepare(&q,
   367    367        "SELECT name FROM sqlite_master /*scan*/"
   368    368        " WHERE type='table'"
   369    369        " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias',"
   370    370                          "'config','shun','private','reportfmt',"
   371    371                          "'concealed','accesslog','modreq',"
   372    372                          "'purgeevent','purgeitem','unversioned',"
   373         -                       "'subscriber','pending_alert','email_bounce')"
          373  +                       "'subscriber','pending_alert','alert_bounce')"
   374    374        " AND name NOT GLOB 'sqlite_*'"
   375    375        " AND name NOT GLOB 'fx_*'"
   376    376     );
   377    377     while( db_step(&q)==SQLITE_ROW ){
   378    378       blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0));
   379    379     }
   380    380     db_finalize(&q);
................................................................................
   447    447       percent_complete((processCnt*1000)/totalSize);
   448    448     }
   449    449     if( doClustering ) create_cluster();
   450    450     if( ttyOutput && !g.fQuiet && totalSize>0 ){
   451    451       processCnt += incrSize;
   452    452       percent_complete((processCnt*1000)/totalSize);
   453    453     }
   454         -  email_triggers_enable();
          454  +  alert_triggers_enable();
   455    455     if(!g.fQuiet && ttyOutput ){
   456    456       percent_complete(1000);
   457    457       fossil_print("\n");
   458    458     }
   459    459     return errCnt;
   460    460   }
   461    461   

Changes to src/schema.c.

   288    288   @ -- between the plink and tagxref tables, but it is a slower join for
   289    289   @ -- very large repositories (repositories with 100,000 or more check-ins)
   290    290   @ -- and so it makes sense to precompute the set of leaves.  There is
   291    291   @ -- one entry in the following table for each leaf.
   292    292   @ --
   293    293   @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
   294    294   @
   295         -@ -- Events used to generate a timeline
          295  +@ -- Events used to generate a timeline.  Type meanings:
          296  +@ --     ci    Check-ins
          297  +@ --     e     Technotes
          298  +@ --     f     Forum posts
          299  +@ --     g     Tags
          300  +@ --     t     Ticket changes
          301  +@ --     w     Wiki page edit
   296    302   @ --
   297    303   @ CREATE TABLE event(
   298         -@   type TEXT,                      -- Type of event: 'ci', 'w', 'e', 't', 'g'
          304  +@   type TEXT,                      -- Type of event: ci, e, f, g, t, w
   299    305   @   mtime DATETIME,                 -- Time of occurrence. Julian day.
   300    306   @   objid INTEGER PRIMARY KEY,      -- Associated record ID
   301    307   @   tagid INTEGER,                  -- Associated ticket or wiki name tag
   302    308   @   uid INTEGER REFERENCES user,    -- User who caused the event
   303    309   @   bgcolor TEXT,                   -- Color set by 'bgcolor' property
   304    310   @   euser TEXT,                     -- User set by 'user' property
   305    311   @   user TEXT,                      -- Name of the user
................................................................................
   540    546   @   UNIQUE(id, merge)
   541    547   @ );
   542    548   @
   543    549   @ -- Identifier for this file type.
   544    550   @ -- The integer is the same as 'FSLC'.
   545    551   @ PRAGMA application_id=252006674;
   546    552   ;
          553  +
          554  +/*
          555  +** The following table holds information about forum posts.  It
          556  +** is created on-demand whenever the manifest parser encounters
          557  +** a forum-post artifact.
          558  +*/
          559  +static const char zForumSchema[] =
          560  +@ CREATE TABLE repository.forumpost(
          561  +@   fpid INTEGER PRIMARY KEY,  -- BLOB.rid for the artifact
          562  +@   froot INT,                 -- fpid of the thread root
          563  +@   fprev INT,                 -- Previous version of this same post
          564  +@   firt INT,                  -- This post is in-reply-to
          565  +@   fmtime REAL                -- When posted.  Julian day
          566  +@ );
          567  +@ CREATE INDEX repository.forumthread ON forumpost(froot,fmtime);
          568  +;
          569  +
          570  +/* Create the forum-post schema if it does not already exist */
          571  +void schema_forum(void){
          572  +  if( !db_table_exists("repository","forumpost") ){
          573  +    db_multi_exec("%s",zForumSchema/*safe-for-%s*/);
          574  +  }
          575  +}

Changes to src/search.c.

   636    636   #if INTERFACE
   637    637   /* What to search for */
   638    638   #define SRCH_CKIN     0x0001    /* Search over check-in comments */
   639    639   #define SRCH_DOC      0x0002    /* Search over embedded documents */
   640    640   #define SRCH_TKT      0x0004    /* Search over tickets */
   641    641   #define SRCH_WIKI     0x0008    /* Search over wiki */
   642    642   #define SRCH_TECHNOTE 0x0010    /* Search over tech notes */
   643         -#define SRCH_ALL      0x001f    /* Search over everything */
          643  +#define SRCH_FORUM    0x0020    /* Search over forum messages */
          644  +#define SRCH_ALL      0x003f    /* Search over everything */
   644    645   #endif
   645    646   
   646    647   /*
   647    648   ** Remove bits from srchFlags which are disallowed by either the
   648    649   ** current server configuration or by user permissions.
   649    650   */
   650    651   unsigned int search_restrict(unsigned int srchFlags){
................................................................................
   652    653     static unsigned int knownBad = 0;
   653    654     static const struct { unsigned m; const char *zKey; } aSetng[] = {
   654    655        { SRCH_CKIN,     "search-ci"   },
   655    656        { SRCH_DOC,      "search-doc"  },
   656    657        { SRCH_TKT,      "search-tkt"  },
   657    658        { SRCH_WIKI,     "search-wiki" },
   658    659        { SRCH_TECHNOTE, "search-technote" },
          660  +     { SRCH_FORUM,    "search-forum" },
   659    661     };
   660    662     int i;
   661    663     if( g.perm.Read==0 )   srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE);
   662    664     if( g.perm.RdTkt==0 )  srchFlags &= ~(SRCH_TKT);
   663    665     if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI);
          666  +  if( g.perm.RdForum==0) srchFlags &= ~(SRCH_FORUM);
   664    667     for(i=0; i<count(aSetng); i++){
   665    668       unsigned int m = aSetng[i].m;
   666    669       if( (srchFlags & m)==0 ) continue;
   667    670       if( ((knownGood|knownBad) & m)!=0 ) continue;
   668    671       if( db_get_boolean(aSetng[i].zKey,0) ){
   669    672         knownGood |= m;
   670    673       }else{
................................................................................
   791    794         "         'e'||rid,"
   792    795         "         datetime(mtime),"
   793    796         "         search_snippet()"
   794    797         "    FROM technote"
   795    798         "   WHERE search_match('',body('e',rid,NULL));"
   796    799       );
   797    800     }
          801  +  if( (srchFlags & SRCH_FORUM)!=0 ){
          802  +    db_multi_exec(
          803  +      "INSERT INTO x(label,url,score,id,date,snip)"
          804  +      "  SELECT 'Forum '||comment,"
          805  +      "         '/forumpost/'||uuid,"
          806  +      "         search_score(),"
          807  +      "         'f'||rid,"
          808  +      "         datetime(event.mtime),"
          809  +      "         search_snippet()"
          810  +      "    FROM event JOIN blob on event.objid=blob.rid"
          811  +      "   WHERE search_match('',body('f',rid,NULL));"
          812  +    );
          813  +  }
   798    814   }
   799    815   
   800    816   /*
   801    817   ** Number of significant bits in a u32
   802    818   */
   803    819   static int nbits(u32 x){
   804    820     int n = 0;
................................................................................
   911    927       const char *zSep = " AND (";
   912    928       static const struct { unsigned m; char c; } aMask[] = {
   913    929          { SRCH_CKIN,     'c' },
   914    930          { SRCH_DOC,      'd' },
   915    931          { SRCH_TKT,      't' },
   916    932          { SRCH_WIKI,     'w' },
   917    933          { SRCH_TECHNOTE, 'e' },
          934  +       { SRCH_FORUM,    'f' },
   918    935       };
   919    936       int i;
   920    937       for(i=0; i<count(aMask); i++){
   921    938         if( srchFlags & aMask[i].m ){
   922    939           blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c);
   923    940           zSep = " OR ";
   924    941         }
................................................................................
  1047   1064   ** categories.  Any srchFlags with two or more bits set
  1048   1065   ** is treated like SRCH_ALL for display purposes.
  1049   1066   **
  1050   1067   ** This routine automatically restricts srchFlag according to user
  1051   1068   ** permissions and the server configuration.  The entry box is shown
  1052   1069   ** disabled if srchFlags is 0 after these restrictions are applied.
  1053   1070   **
  1054         -** If useYparam is true, then this routine also looks at the y= query
  1055         -** parameter for further search restrictions.
         1071  +** The mFlags value controls options:
         1072  +**
         1073  +**     0x01    If the y= query parameter is present, use it as an addition
         1074  +**             restriction what to search.
         1075  +**
         1076  +**     0x02    Show nothing if search is disabled.
         1077  +**
         1078  +** Return true if there are search results.
  1056   1079   */
  1057         -void search_screen(unsigned srchFlags, int useYparam){
         1080  +int search_screen(unsigned srchFlags, int mFlags){
  1058   1081     const char *zType = 0;
  1059   1082     const char *zClass = 0;
  1060   1083     const char *zDisable1;
  1061   1084     const char *zDisable2;
  1062   1085     const char *zPattern;
  1063   1086     int fDebug = PB("debug");
         1087  +  int haveResult = 0;
  1064   1088     srchFlags = search_restrict(srchFlags);
  1065   1089     switch( srchFlags ){
  1066   1090       case SRCH_CKIN:     zType = " Check-ins";  zClass = "Ckin"; break;
  1067   1091       case SRCH_DOC:      zType = " Docs";       zClass = "Doc";  break;
  1068   1092       case SRCH_TKT:      zType = " Tickets";    zClass = "Tkt";  break;
  1069   1093       case SRCH_WIKI:     zType = " Wiki";       zClass = "Wiki"; break;
  1070   1094       case SRCH_TECHNOTE: zType = " Tech Notes"; zClass = "Note"; break;
         1095  +    case SRCH_FORUM:    zType = " Forum";      zClass = "Frm";  break;
  1071   1096     }
  1072   1097     if( srchFlags==0 ){
         1098  +    if( mFlags & 0x02 ) return 0;
  1073   1099       zDisable1 = " disabled";
  1074   1100       zDisable2 = " disabled";
  1075   1101       zPattern = "";
  1076   1102     }else{
  1077   1103       zDisable1 = ""; /* Was: " autofocus" */
  1078   1104       zDisable2 = "";
  1079   1105       zPattern = PD("s","");
................................................................................
  1081   1107     @ <form method='GET' action='%R/%T(g.zPath)'>
  1082   1108     if( zClass ){
  1083   1109       @ <div class='searchForm searchForm%s(zClass)'>
  1084   1110     }else{
  1085   1111       @ <div class='searchForm'>
  1086   1112     }
  1087   1113     @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable1)>
  1088         -  if( useYparam && (srchFlags & (srchFlags-1))!=0 && useYparam ){
         1114  +  if( (mFlags & 0x01)!=0 && (srchFlags & (srchFlags-1))!=0 ){
  1089   1115       static const struct { char *z; char *zNm; unsigned m; } aY[] = {
  1090   1116          { "all",  "All",        SRCH_ALL      },
  1091   1117          { "c",    "Check-ins",  SRCH_CKIN     },
  1092   1118          { "d",    "Docs",       SRCH_DOC      },
  1093   1119          { "t",    "Tickets",    SRCH_TKT      },
  1094   1120          { "w",    "Wiki",       SRCH_WIKI     },
  1095   1121          { "e",    "Tech Notes", SRCH_TECHNOTE },
         1122  +       { "f",    "Forum",      SRCH_FORUM    },
  1096   1123       };
  1097   1124       const char *zY = PD("y","all");
  1098   1125       unsigned newFlags = srchFlags;
  1099   1126       int i;
  1100   1127       @ <select size='1' name='y'>
  1101   1128       for(i=0; i<count(aY); i++){
  1102   1129         if( (aY[i].m & srchFlags)==0 ) continue;
................................................................................
  1125   1152       }else{
  1126   1153         @ <div class='searchResult'>
  1127   1154       }
  1128   1155       if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){
  1129   1156         @ <p class='searchEmpty'>No matches for: <span>%h(zPattern)</span></p>
  1130   1157       }
  1131   1158       @ </div>
         1159  +    haveResult = 1;
  1132   1160     }
         1161  +  return haveResult;
  1133   1162   }
  1134   1163   
  1135   1164   /*
  1136   1165   ** WEBPAGE: search
  1137   1166   **
  1138   1167   ** Search for check-in comments, documents, tickets, or wiki that
  1139   1168   ** match a user-supplied pattern.
................................................................................
  1141   1170   **    s=PATTERN       Specify the full-text pattern to search for
  1142   1171   **    y=TYPE          What to search.
  1143   1172   **                      c -> check-ins
  1144   1173   **                      d -> documentation
  1145   1174   **                      t -> tickets
  1146   1175   **                      w -> wiki
  1147   1176   **                      e -> tech notes
         1177  +**                      f -> forum
  1148   1178   **                    all -> everything
  1149   1179   */
  1150   1180   void search_page(void){
  1151   1181     login_check_credentials();
  1152   1182     style_header("Search");
  1153   1183     search_screen(SRCH_ALL, 1);
  1154   1184     style_footer();
................................................................................
  1248   1278   ** full text search and/or for constructing a search result snippet.
  1249   1279   **
  1250   1280   **    cType:            d      Embedded documentation
  1251   1281   **                      w      Wiki page
  1252   1282   **                      c      Check-in comment
  1253   1283   **                      t      Ticket text
  1254   1284   **                      e      Tech note
         1285  +**                      f      Forum
  1255   1286   **
  1256   1287   **    rid               The RID of an artifact that defines the object
  1257   1288   **                      being searched.
  1258   1289   **
  1259   1290   **    zName             Name of the object being searched.  This is used
  1260   1291   **                      only to help figure out the mimetype (text/plain,
  1261   1292   **                      test/html, test/x-fossil-wiki, or text/x-markdown)
................................................................................
  1273   1304         Blob doc;
  1274   1305         content_get(rid, &doc);
  1275   1306         blob_to_utf8_no_bom(&doc, 0);
  1276   1307         get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
  1277   1308         blob_reset(&doc);
  1278   1309         break;
  1279   1310       }
         1311  +    case 'f':     /* Forum messages */
  1280   1312       case 'e':     /* Tech Notes */
  1281   1313       case 'w': {   /* Wiki */
  1282   1314         Manifest *pWiki = manifest_get(rid,
  1283         -          cType == 'e' ? CFTYPE_EVENT : CFTYPE_WIKI, 0);
         1315  +          cType == 'e' ? CFTYPE_EVENT :
         1316  +          cType == 'f' ? CFTYPE_FORUM : CFTYPE_WIKI, 0);
  1284   1317         Blob wiki;
  1285   1318         if( pWiki==0 ) break;
  1286         -      blob_init(&wiki, pWiki->zWiki, -1);
         1319  +      if( cType=='f' ){
         1320  +        blob_init(&wiki, 0, 0);
         1321  +        if( pWiki->zThreadTitle ){
         1322  +          blob_appendf(&wiki, "<h1>%h</h1>\n", pWiki->zThreadTitle);
         1323  +        }
         1324  +        blob_appendf(&wiki, "From %s:\n\n%s", pWiki->zUser, pWiki->zWiki);
         1325  +      }else{
         1326  +        blob_init(&wiki, pWiki->zWiki, -1);
         1327  +      }
  1287   1328         get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype),
  1288   1329                               pOut);
  1289   1330         blob_reset(&wiki);
  1290   1331         manifest_destroy(pWiki);
  1291   1332         break;
  1292   1333       }
  1293   1334       case 'c': {   /* Check-in Comments */
................................................................................
  1514   1555     );
  1515   1556     db_multi_exec(
  1516   1557       "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
  1517   1558       "  SELECT 't', tkt_id, 0 FROM ticket;"
  1518   1559     );
  1519   1560     db_multi_exec(
  1520   1561       "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)"
  1521         -    "  SELECT 'e', objid, comment, 0 FROM event WHERE type='e';"
         1562  +    "  SELECT type, objid, comment, 0 FROM event WHERE type IN ('e','f');"
  1522   1563     );
  1523   1564   }
  1524   1565   
  1525   1566   /*
  1526   1567   ** The document described by cType,rid,zName is about to be added or
  1527   1568   ** updated.  If the document has already been indexed, then unindex it
  1528   1569   ** now while we still have access to the old content.  Add the document
................................................................................
  1551   1592           cType, zName
  1552   1593         );
  1553   1594         db_multi_exec(
  1554   1595           "DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d",
  1555   1596           cType, zName, rid
  1556   1597         );
  1557   1598       }
         1599  +    /* All forum posts are always indexed */
  1558   1600     }
  1559   1601   }
  1560   1602   
  1561   1603   /*
  1562   1604   ** If the doc-glob and doc-br settings are valid for document search
  1563   1605   ** and if the latest check-in on doc-br is in the unindexed set of
  1564   1606   ** check-ins, then update all 'd' entries in FTSDOCS that have
................................................................................
  1679   1721       "            'Wiki: '||ftsdocs.name,"
  1680   1722       "            '/wiki?name='||urlencode(ftsdocs.name),"
  1681   1723       "            tagxref.mtime"
  1682   1724       "       FROM tagxref WHERE tagxref.rid=ftsdocs.rid)"
  1683   1725       " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed"
  1684   1726     );
  1685   1727   }
         1728  +
         1729  +/*
         1730  +** Deal with all of the unindexed 'f' terms in FTSDOCS
         1731  +*/
         1732  +static void search_update_forum_index(void){
         1733  +  db_multi_exec(
         1734  +    "INSERT INTO ftsidx(docid,title,body)"
         1735  +    " SELECT rowid, title('f',rid,NULL),body('f',rid,NULL) FROM ftsdocs"
         1736  +    "  WHERE type='f' AND NOT idxed;"
         1737  +  );
         1738  +  if( db_changes()==0 ) return;
         1739  +  db_multi_exec(
         1740  +    "UPDATE ftsdocs SET idxed=1, name=NULL,"
         1741  +    " (label,url,mtime) = "
         1742  +    "  (SELECT 'Forum '||event.comment,"
         1743  +    "          '/forumpost/'||blob.uuid,"
         1744  +    "          event.mtime"
         1745  +    "     FROM event, blob"
         1746  +    "    WHERE event.objid=ftsdocs.rid"
         1747  +    "      AND blob.rid=ftsdocs.rid)"
         1748  +    "WHERE ftsdocs.type='f' AND NOT ftsdocs.idxed"
         1749  +  );
         1750  +}
  1686   1751   
  1687   1752   /*
  1688   1753   ** Deal with all of the unindexed 'e' terms in FTSDOCS
  1689   1754   */
  1690   1755   static void search_update_technote_index(void){
  1691   1756     db_multi_exec(
  1692   1757       "INSERT INTO ftsidx(docid,title,body)"
................................................................................
  1726   1791     }
  1727   1792     if( srchFlags & SRCH_WIKI ){
  1728   1793       search_update_wiki_index();
  1729   1794     }
  1730   1795     if( srchFlags & SRCH_TECHNOTE ){
  1731   1796       search_update_technote_index();
  1732   1797     }
         1798  +  if( srchFlags & SRCH_FORUM ){
         1799  +    search_update_forum_index();
         1800  +  }
  1733   1801   }
  1734   1802   
  1735   1803   /*
  1736   1804   ** Construct, prepopulate, and then update the full-text index.
  1737   1805   */
  1738   1806   void search_rebuild_index(void){
  1739   1807     fossil_print("rebuilding the search index...");
................................................................................
  1778   1846     };
  1779   1847     static const struct { char *zSetting; char *zName; char *zSw; } aSetng[] = {
  1780   1848        { "search-ci",       "check-in search:",  "c" },
  1781   1849        { "search-doc",      "document search:",  "d" },
  1782   1850        { "search-tkt",      "ticket search:",    "t" },
  1783   1851        { "search-wiki",     "wiki search:",      "w" },
  1784   1852        { "search-technote", "tech note search:", "e" },
         1853  +     { "search-forum",    "forum search:",     "f" },
  1785   1854     };
  1786   1855     char *zSubCmd = 0;
  1787   1856     int i, j, n;
  1788   1857     int iCmd = 0;
  1789   1858     int iAction = 0;
  1790   1859     db_find_and_open_repository(0, 0);
  1791   1860     if( g.argc>2 ){

Changes to src/security_audit.c.

    56     56     @ <ol>
    57     57   
    58     58     /* Step 1:  Determine if the repository is public or private.  "Public"
    59     59     ** means that any anonymous user on the internet can access all content.
    60     60     ** "Private" repos require (non-anonymous) login to access all content,
    61     61     ** though some content may be accessible anonymously.
    62     62     */
    63         -  zAnonCap = db_text("", "SELECT group_concat(coalesce(cap,'')) FROM user"
    64         -                         " WHERE login IN ('anonymous','nobody')");
           63  +  zAnonCap = db_text("", "SELECT fullcap(NULL)");
    65     64     zPubPages = db_get("public-pages",0);
    66     65     if( hasAnyCap(zAnonCap,"as") ){
    67     66       @ <li><p>This repository is <big><b>Wildly INSECURE</b></big> because
    68     67       @ it grants administrator privileges to anonymous users.  You
    69     68       @ should <a href="takeitprivate">take this repository private</a>
    70     69       @ immediately!  Or, at least remove the Setup and Admin privileges
    71     70       @ for users "anonymous" and "login" on the
................................................................................
    76     75       @ <p>Fix this by <a href="takeitprivate">taking the repository private</a>
    77     76       @ or by removing the "y" permission from users "anonymous" and
    78     77       @ "nobody" on the <a href="setup_ulist">User Configuration</a> page.
    79     78     }else if( hasAnyCap(zAnonCap,"goz") ){
    80     79       @ <li><p>This repository is <big><b>PUBLIC</b></big>. All
    81     80       @ checked-in content can be accessed by anonymous users.
    82     81       @ <a href="takeitprivate">Take it private</a>.<p>
    83         -  }else if( !hasAnyCap(zAnonCap, "jry") && (zPubPages==0 || zPubPages[0]==0) ){
           82  +  }else if( !hasAnyCap(zAnonCap, "jrwy234567")
           83  +         && (zPubPages==0 || zPubPages[0]==0) ){
    84     84       @ <li><p>This repository is <big><b>Completely PRIVATE</b></big>.
    85     85       @ A valid login and password is required to access any content.
    86     86     }else{
    87     87       @ <li><p>This repository is <big><b>Mostly PRIVATE</b></big>.
    88     88       @ A valid login and password is usually required, however some
    89     89       @ content can be accessed anonymously:
    90     90       @ <ul>
    91     91       if( hasAnyCap(zAnonCap,"j") ){
    92     92         @ <li> Wiki pages
    93     93       }
    94     94       if( hasAnyCap(zAnonCap,"r") ){
    95     95         @ <li> Tickets
           96  +    }
           97  +    if( hasAnyCap(zAnonCap,"234567") ){
           98  +      @ <li> Forum posts
    96     99       }
    97    100       if( zPubPages && zPubPages[0] ){
    98    101         Glob *pGlob = glob_create(zPubPages);
    99    102         int i;
   100    103         @ <li> URLs that match any of these GLOB patterns:
   101    104         @ <ul>
   102    105         for(i=0; i<pGlob->nPattern; i++){
................................................................................
   124    127     /* Anonymous users should not be able to harvest email addresses
   125    128     ** from tickets.
   126    129     */
   127    130     if( hasAnyCap(zAnonCap, "e") ){
   128    131       @ <li><p><b>WARNING:</b>
   129    132       @ Anonymous users can view email addresses and other personally
   130    133       @ identifiable information on tickets.
   131         -    @ <p>Fix this by removing the "Email" privilege from users
          134  +    @ <p>Fix this by removing the "Email" privilege
          135  +    @ (<a href="setup_ucap_list">capability "e"</a>) from users
   132    136       @ "anonymous" and "nobody" on the
   133    137       @ <a href="setup_ulist">User Configuration</a> page.
   134    138     }
   135    139   
   136    140     /* Anonymous users probably should not be allowed to push content
   137    141     ** to the repository.
   138    142     */
   139    143     if( hasAnyCap(zAnonCap, "i") ){
   140    144       @ <li><p><b>WARNING:</b>
   141    145       @ Anonymous users can push new check-ins into the repository.
   142         -    @ <p>Fix this by removing the "Check-in" privilege from users
          146  +    @ <p>Fix this by removing the "Check-in" privilege
          147  +    @ (<a href="setup_ucap_list">capability</a> "i") from users
   143    148       @ "anonymous" and "nobody" on the
   144    149       @ <a href="setup_ulist">User Configuration</a> page.
   145    150     }
   146    151   
   147    152     /* Anonymous users probably should not be allowed act as moderators
   148    153     ** for wiki or tickets.
   149    154     */
   150         -  if( hasAnyCap(zAnonCap, "lq") ){
          155  +  if( hasAnyCap(zAnonCap, "lq5") ){
   151    156       @ <li><p><b>WARNING:</b>
   152         -    @ Anonymous users can act as moderators for wiki and/or tickets.
   153         -    @ This defeats the whole purpose of moderation.
   154         -    @ <p>Fix this by removing the "Mod-Wiki" and "Mod-Tkt"
   155         -    @ privilege from users "anonymous" and "nobody" on the
   156         -    @ <a href="setup_ulist">User Configuration</a> page.
          157  +    @ Anonymous users can act as moderators for wiki, tickets, or 
          158  +    @ forum posts. This defeats the whole purpose of moderation.
          159  +    @ <p>Fix this by removing the "Mod-Wiki", "Mod-Tkt", and "Mod-Forum"
          160  +    @ privileges (<a href="%R/setup_ucap_list">capabilities</a> "fq5")
          161  +    @ from users "anonymous" and "nobody"
          162  +    @ on the <a href="setup_ulist">User Configuration</a> page.
   157    163     }
   158    164   
   159    165     /* Anonymous users probably should not be allowed to delete
   160    166     ** wiki or tickets.
   161    167     */
   162    168     if( hasAnyCap(zAnonCap, "d") ){
   163    169       @ <li><p><b>WARNING:</b>
................................................................................
   172    178     */
   173    179     if( hasAnyCap(zAnonCap, "fk") ){
   174    180       if( db_get_boolean("modreq-wiki",0)==0 ){
   175    181         @ <li><p><b>WARNING:</b>
   176    182         @ Anonymous users can create or edit wiki without moderation.
   177    183         @ This can result in robots inserting lots of wiki spam into
   178    184         @ repository.
   179         -      @ <p>Fix this by removing the "New-Wiki" and "Write-Wiki"
          185  +      @ Fix this by removing the "New-Wiki" and "Write-Wiki"
   180    186         @ privileges from users "anonymous" and "nobody" on the
   181    187         @ <a href="setup_ulist">User Configuration</a> page or
   182    188         @ by enabling wiki moderation on the
   183    189         @ <a href="setup_modreq">Moderation Setup</a> page.
   184    190       }else{
   185    191         @ <li><p>
   186    192         @ Anonymous users can create or edit wiki, but moderator
   187    193         @ approval is required before the edits become permanent.
   188    194       }
   189    195     }
          196  +
          197  +  /* Anonymous users should not be able to create trusted forum
          198  +  ** posts.
          199  +  */
          200  +  if( hasAnyCap(zAnonCap, "456") ){
          201  +    @ <li><p><b>WARNING:</b>
          202  +    @ Anonymous users can create forum posts that are
          203  +    @ accepted into the permanent record without moderation.
          204  +    @ This can result in robots generating spam on forum posts.
          205  +    @ Fix this by removing the "WriteTrusted-Forum" privilege
          206  +    @ (<a href="setup_ucap_list">capabilities</a> "456") from
          207  +    @ users "anonymous" and "nobody" on the
          208  +    @ <a href="setup_ulist">User Configuration</a> page or
          209  +  }
          210  +
          211  +  /* Anonymous users should not be able to send announcements.
          212  +  */
          213  +  if( hasAnyCap(zAnonCap, "A") ){
          214  +    @ <li><p><b>WARNING:</b>
          215  +    @ Anonymous users can send announcements to anybody who is signed
          216  +    @ up to receive announcements.  This can result in spam.
          217  +    @ Fix this by removing the "Announce" privilege
          218  +    @ (<a href="setup_ucap_list">capability</a> "A") from
          219  +    @ users "anonymous" and "nobody" on the
          220  +    @ <a href="setup_ulist">User Configuration</a> page or
          221  +  }
   190    222   
   191    223     /* Administrative privilege should only be provided to
   192    224     ** specific individuals, not to entire classes of people.
   193    225     ** And not too many people should have administrator privilege.
   194    226     */
   195         -  z = db_text(0, "SELECT group_concat(login,' AND ') FROM user"
   196         -                 " WHERE cap GLOB '*[as]*'"
   197         -                 "   AND login in ('anonymous','nobody','reader','developer')");
          227  +  z = db_text(0,
          228  +    "SELECT group_concat("
          229  +                 "printf('<a href=''setup_uedit?id=%%d''>%%s</a>',uid,login),"
          230  +             "' and ')"
          231  +    " FROM user"
          232  +    " WHERE cap GLOB '*[as]*'"
          233  +    "   AND login in ('anonymous','nobody','reader','developer')"
          234  +  );
   198    235     if( z && z[0] ){
   199         -    @ <li><p>
   200         -    @ Administrative privilege is granted to an entire class of users
   201         -    @ (%h(z)).  Ideally, the Write-Unver privilege should only be
          236  +    @ <li><p><b>WARNING:</b>
          237  +    @ Administrative privilege ('a' or 's')
          238  +    @ is granted to an entire class of users: %s(z).
          239  +    @ Administrative privilege should only be
   202    240       @ granted to specific individuals.
   203    241     }
   204         -  n = db_int(0,"SELECT count(*) FROM user WHERE cap GLOB '*[as]*'");
          242  +  n = db_int(0,"SELECT count(*) FROM user WHERE fullcap(cap) GLOB '*[as]*'");
   205    243     if( n==0 ){
   206    244       @ <li><p>
   207    245       @ No users have administrator privilege.
   208    246     }else{
   209    247       z = db_text(0,
   210    248         "SELECT group_concat("
   211    249                    "printf('<a href=''setup_uedit?id=%%d''>%%s</a>',uid,login),"
   212    250                "', ')"
   213    251         " FROM user"
   214         -      " WHERE cap GLOB '*[as]*'"
          252  +      " WHERE fullcap(cap) GLOB '*[as]*'"
   215    253       );
   216    254       @ <li><p>
   217    255       @ Users with administrator privilege are: %s(z)
   218    256       fossil_free(z);
   219    257       if( n>3 ){
   220         -      @ <p><b>Caution</b>:
          258  +      @ <li><p><b>WARNING:</b>
   221    259         @ Administrator privilege is granted to
   222    260         @ <a href='setup_ulist?with=as'>%d(n) users</a>.
   223    261         @ Ideally, administator privilege ('s' or 'a') should only
   224    262         @ be granted to one or two users.
   225    263       }
   226    264     }
   227    265   
................................................................................
   234    272                    "printf('<a href=''setup_uedit?id=%%d''>%%s</a>',uid,login),"
   235    273                "' and ')"
   236    274       " FROM user"
   237    275       " WHERE cap GLOB '*y*'"
   238    276       "   AND login in ('anonymous','nobody','reader','developer')"
   239    277     );
   240    278     if( z && z[0] ){
   241         -    @ <li><p>
   242         -    @ The "Write-Unver" privilege is granted to an entire class of users
   243         -    @ (%s(z)).  Ideally, the Write-Unver privilege should only be
   244         -    @ granted to specific individuals.
          279  +    @ <li><p><b>WARNING:</b>
          280  +    @ The "Write-Unver" privilege is granted to an entire class of users: %s(z).
          281  +    @ The Write-Unver privilege should only be granted to specific individuals.
   245    282       fossil_free(z);
   246    283     }
   247    284     n = db_int(0,"SELECT count(*) FROM user WHERE cap GLOB '*y*'");
   248    285     if( n>0 ){
   249    286       z = db_text(0,
   250    287          "SELECT group_concat("
   251    288             "printf('<a href=''setup_uedit?id=%%d''>%%s</a>',uid,login),', ')"
   252         -       " FROM user WHERE cap GLOB '*y*'"
          289  +       " FROM user WHERE fullcap(cap) GLOB '*y*'"
   253    290       );
   254    291       @ <li><p>
   255    292       @ Users with "Write-Unver" privilege: %s(z)
   256    293       fossil_free(z);
   257    294       if( n>3 ){
   258    295         @ <p><b>Caution:</b>
   259    296         @ The "Write-Unver" privilege ('y') is granted to an excessive
................................................................................
   347    384       }else{
   348    385         fclose(pTest);
   349    386         @ <li><p>
   350    387         @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is
   351    388         @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
   352    389       }
   353    390     }
          391  +
          392  +  @ <li><p> User capability summary:
          393  +  capability_summary();
          394  +
          395  +  if( alert_enabled() ){
          396  +    @ <li><p> Email alert configuration summary:
          397  +    @ <table class="label-value">
          398  +    stats_for_email();
          399  +    @ </table>
          400  +  }else{
          401  +    @ <li><p> Email alerts are disabled
          402  +  }
   354    403   
   355    404     @ </ol>
   356    405     style_footer();
   357    406   }
   358    407   
   359    408   /*
   360    409   ** WEBPAGE: takeitprivate

Changes to src/setup.c.

   155    155       "Enter raw SQL commands");
   156    156     setup_menu_entry("TH1", "admin_th1",
   157    157       "Enter raw TH1 commands");
   158    158     @ </table>
   159    159   
   160    160     style_footer();
   161    161   }
   162         -
   163         -/*
   164         -** WEBPAGE: setup_ulist
   165         -**
   166         -** Show a list of users.  Clicking on any user jumps to the edit
   167         -** screen for that user.  Requires Admin privileges.
   168         -**
   169         -** Query parameters:
   170         -**
   171         -**   with=CAP         Only show users that have one or more capabilities in CAP.
   172         -*/
   173         -void setup_ulist(void){
   174         -  Stmt s;
   175         -  double rNow;
   176         -  const char *zWith = P("with");
   177         -
   178         -  login_check_credentials();
   179         -  if( !g.perm.Admin ){
   180         -    login_needed(0);
   181         -    return;
   182         -  }
   183         -
   184         -  if( zWith==0 || zWith[0]==0 ){
   185         -    style_submenu_element("Add", "setup_uedit");
   186         -    style_submenu_element("Log", "access_log");
   187         -    style_submenu_element("Help", "setup_ulist_notes");
   188         -    style_header("User List");
   189         -    @ <table border=1 cellpadding=2 cellspacing=0 class='userTable'>
   190         -    @ <thead><tr>
   191         -    @   <th>Category
   192         -    @   <th>Capabilities (<a href='%R/setup_ucap_list'>key</a>)
   193         -    @   <th>Info <th>Last Change</tr></thead>
   194         -    @ <tbody>
   195         -    db_prepare(&s,
   196         -       "SELECT uid, login, cap, date(mtime,'unixepoch')"
   197         -       "  FROM user"
   198         -       " WHERE login IN ('anonymous','nobody','developer','reader')"
   199         -       " ORDER BY login"
   200         -    );
   201         -    while( db_step(&s)==SQLITE_ROW ){
   202         -      int uid = db_column_int(&s, 0);
   203         -      const char *zLogin = db_column_text(&s, 1);
   204         -      const char *zCap = db_column_text(&s, 2);
   205         -      const char *zDate = db_column_text(&s, 4);
   206         -      @ <tr>
   207         -      @ <td><a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
   208         -      @ <td>%h(zCap)
   209         -
   210         -      if( fossil_strcmp(zLogin,"anonymous")==0 ){
   211         -        @ <td>All logged-in users
   212         -      }else if( fossil_strcmp(zLogin,"developer")==0 ){
   213         -        @ <td>Users with '<b>v</b>' capability
   214         -      }else if( fossil_strcmp(zLogin,"nobody")==0 ){
   215         -        @ <td>All users without login
   216         -      }else if( fossil_strcmp(zLogin,"reader")==0 ){
   217         -        @ <td>Users with '<b>u</b>' capability
   218         -      }else{
   219         -        @ <td>
   220         -      }
   221         -      if( zDate && zDate[0] ){
   222         -        @ <td>%h(zDate)
   223         -      }else{
   224         -        @ <td>
   225         -      }
   226         -      @ </tr>
   227         -    }
   228         -    db_finalize(&s);
   229         -  }else{
   230         -    style_header("Users With Capabilities \"%h\"", zWith);
   231         -  }
   232         -  @ </tbody></table>
   233         -  @ <div class='section'>Users</div>
   234         -  @ <table border=1 cellpadding=2 cellspacing=0 class='userTable sortable' \
   235         -  @  data-column-types='ktxTTK' data-init-sort='2'>
   236         -  @ <thead><tr>
   237         -  @ <th>Login Name<th>Caps<th>Info<th>Date<th>Expire<th>Last Login</tr></thead>
   238         -  @ <tbody>
   239         -  db_multi_exec(
   240         -    "CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL)"
   241         -    "WITHOUT ROWID;"
   242         -  );
   243         -  if( db_table_exists("repository","accesslog") ){
   244         -    db_multi_exec(
   245         -      "INSERT INTO lastAccess(uname, atime)"
   246         -      " SELECT uname, max(mtime) FROM ("
   247         -      "    SELECT uname, mtime FROM accesslog WHERE success"
   248         -      "    UNION ALL"
   249         -      "    SELECT login AS uname, rcvfrom.mtime AS mtime"
   250         -      "      FROM rcvfrom JOIN user USING(uid))"
   251         -      " GROUP BY 1;"
   252         -    );
   253         -  }
   254         -  if( zWith && zWith[0] ){
   255         -    zWith = mprintf(" AND cap GLOB '*[%q]*'", zWith);
   256         -  }else{
   257         -    zWith = "";
   258         -  }
   259         -  db_prepare(&s,
   260         -     "SELECT uid, login, cap, info, date(mtime,'unixepoch'),"
   261         -     "       lower(login) AS sortkey, "
   262         -     "       CASE WHEN info LIKE '%%expires 20%%'"
   263         -             "    THEN substr(info,instr(lower(info),'expires')+8,10)"
   264         -             "    END AS exp,"
   265         -             "atime"
   266         -     "  FROM user LEFT JOIN lastAccess ON login=uname"
   267         -     " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
   268         -     " ORDER BY sortkey", zWith/*safe-for-%s*/
   269         -  );
   270         -  rNow = db_double(0.0, "SELECT julianday('now');");
   271         -  while( db_step(&s)==SQLITE_ROW ){
   272         -    int uid = db_column_int(&s, 0);
   273         -    const char *zLogin = db_column_text(&s, 1);
   274         -    const char *zCap = db_column_text(&s, 2);
   275         -    const char *zInfo = db_column_text(&s, 3);
   276         -    const char *zDate = db_column_text(&s, 4);
   277         -    const char *zSortKey = db_column_text(&s,5);
   278         -    const char *zExp = db_column_text(&s,6);
   279         -    double rATime = db_column_double(&s,7);
   280         -    char *zAge = 0;
   281         -    if( rATime>0.0 ){
   282         -      zAge = human_readable_age(rNow - rATime);
   283         -    }
   284         -    @ <tr>
   285         -    @ <td data-sortkey='%h(zSortKey)'>\
   286         -    @ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
   287         -    @ <td>%h(zCap)
   288         -    @ <td>%h(zInfo)
   289         -    @ <td>%h(zDate?zDate:"")
   290         -    @ <td>%h(zExp?zExp:"")
   291         -    @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
   292         -    @ </tr>
   293         -    fossil_free(zAge);
   294         -  }
   295         -  @ </tbody></table>
   296         -  db_finalize(&s);
   297         -  style_table_sorter();
   298         -  style_footer();
   299         -}
   300         -
   301         -/*
   302         -** Render the user-capability table
   303         -*/
   304         -static void setup_usercap_table(void){
   305         -  @ <table>
   306         -     @ <tr><th valign="top">a</th>
   307         -     @   <td><i>Admin:</i> Create and delete users</td></tr>
   308         -     @ <tr><th valign="top">b</th>
   309         -     @   <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr>
   310         -     @ <tr><th valign="top">c</th>
   311         -     @   <td><i>Append-Tkt:</i> Append to tickets</td></tr>
   312         -     @ <tr><th valign="top">d</th>
   313         -     @   <td><i>Delete:</i> Delete wiki and tickets</td></tr>
   314         -     @ <tr><th valign="top">e</th>
   315         -     @   <td><i>View-PII:</i> \
   316         -     @ View sensitive data such as email addresses</td></tr>
   317         -     @ <tr><th valign="top">f</th>
   318         -     @   <td><i>New-Wiki:</i> Create new wiki pages</td></tr>
   319         -     @ <tr><th valign="top">g</th>
   320         -     @   <td><i>Clone:</i> Clone the repository</td></tr>
   321         -     @ <tr><th valign="top">h</th>
   322         -     @   <td><i>Hyperlinks:</i> Show hyperlinks to detailed
   323         -     @   repository history</td></tr>
   324         -     @ <tr><th valign="top">i</th>
   325         -     @   <td><i>Check-In:</i> Commit new versions in the repository</td></tr>
   326         -     @ <tr><th valign="top">j</th>
   327         -     @   <td><i>Read-Wiki:</i> View wiki pages</td></tr>
   328         -     @ <tr><th valign="top">k</th>
   329         -     @   <td><i>Write-Wiki:</i> Edit wiki pages</td></tr>
   330         -     @ <tr><th valign="top">l</th>
   331         -     @   <td><i>Mod-Wiki:</i> Moderator for wiki pages</td></tr>
   332         -     @ <tr><th valign="top">m</th>
   333         -     @   <td><i>Append-Wiki:</i> Append to wiki pages</td></tr>
   334         -     @ <tr><th valign="top">n</th>
   335         -     @   <td><i>New-Tkt:</i> Create new tickets</td></tr>
   336         -     @ <tr><th valign="top">o</th>
   337         -     @   <td><i>Check-Out:</i> Check out versions</td></tr>
   338         -     @ <tr><th valign="top">p</th>
   339         -     @   <td><i>Password:</i> Change your own password</td></tr>
   340         -     @ <tr><th valign="top">q</th>
   341         -     @   <td><i>Mod-Tkt:</i> Moderator for tickets</td></tr>
   342         -     @ <tr><th valign="top">r</th>
   343         -     @   <td><i>Read-Tkt:</i> View tickets</td></tr>
   344         -     @ <tr><th valign="top">s</th>
   345         -     @   <td><i>Setup/Super-user:</i> Setup and configure this website</td></tr>
   346         -     @ <tr><th valign="top">t</th>
   347         -     @   <td><i>Tkt-Report:</i> Create new bug summary reports</td></tr>
   348         -     @ <tr><th valign="top">u</th>
   349         -     @   <td><i>Reader:</i> Inherit privileges of
   350         -     @   user <tt>reader</tt></td></tr>
   351         -     @ <tr><th valign="top">v</th>
   352         -     @   <td><i>Developer:</i> Inherit privileges of
   353         -     @   user <tt>developer</tt></td></tr>
   354         -     @ <tr><th valign="top">w</th>
   355         -     @   <td><i>Write-Tkt:</i> Edit tickets</td></tr>
   356         -     @ <tr><th valign="top">x</th>
   357         -     @   <td><i>Private:</i> Push and/or pull private branches</td></tr>
   358         -     @ <tr><th valign="top">y</th>
   359         -     @   <td><i>Write-Unver:</i> Push unversioned files</td></tr>
   360         -     @ <tr><th valign="top">z</th>
   361         -     @   <td><i>Zip download:</i> Download a ZIP archive or tarball</td></tr>
   362         -     @ <tr><th valign="top">2</th>
   363         -     @   <td><i>Forum-Read:</i> Read forum posts by others </td></tr>
   364         -     @ <tr><th valign="top">3</th>
   365         -     @   <td><i>Forum-Append:</i> Add new forum posts</td></tr>
   366         -     @ <tr><th valign="top">4</th>
   367         -     @   <td><i>Forum-Trusted:</i> Add pre-approved forum posts </td></tr>
   368         -     @ <tr><th valign="top">5</th>
   369         -     @   <td><i>Forum-Moderator:</i> Approve or disapprove forum posts</td></tr>
   370         -     @ <tr><th valign="top">6</th>
   371         -     @   <td><i>Forum-Supervisor:</i> \
   372         -     @ Forum administrator
   373         -     @ <tr><th valign="top">7</th>
   374         -     @   <td><i>Email-Alerts:</i> Sign up for email nofications</td></tr>
   375         -     @ <tr><th valign="top">A</th>
   376         -     @   <td><i>Announce:</i> Send announcements</td></tr>
   377         -     @ <tr><th valign="top">D</th>
   378         -     @   <td><i>Debug:</i> Enable debugging features</td></tr>
   379         -  @ </table>
   380         -}
   381         -
   382         -/*
   383         -** WEBPAGE: setup_ulist_notes
   384         -**
   385         -** A documentation page showing notes about user configuration.  This
   386         -** information used to be a side-bar on the user list page, but has been
   387         -** factored out for improved presentation.
   388         -*/
   389         -void setup_ulist_notes(void){
   390         -  style_header("User Configuration Notes");
   391         -  @ <h1>User Configuration Notes:</h1>
   392         -  @ <ol>
   393         -  @ <li><p>
   394         -  @ Every user, logged in or not, inherits the privileges of
   395         -  @ <span class="usertype">nobody</span>.
   396         -  @ </p></li>
   397         -  @
   398         -  @ <li><p>
   399         -  @ Any human can login as <span class="usertype">anonymous</span> since the
   400         -  @ password is clearly displayed on the login page for them to type. The
   401         -  @ purpose of requiring anonymous to log in is to prevent access by spiders.
   402         -  @ Every logged-in user inherits the combined privileges of
   403         -  @ <span class="usertype">anonymous</span> and
   404         -  @ <span class="usertype">nobody</span>.
   405         -  @ </p></li>
   406         -  @
   407         -  @ <li><p>
   408         -  @ Users with privilege <span class="capability">u</span> inherit the combined
   409         -  @ privileges of <span class="usertype">reader</span>,
   410         -  @ <span class="usertype">anonymous</span>, and
   411         -  @ <span class="usertype">nobody</span>.
   412         -  @ </p></li>
   413         -  @
   414         -  @ <li><p>
   415         -  @ Users with privilege <span class="capability">v</span> inherit the combined
   416         -  @ privileges of <span class="usertype">developer</span>,
   417         -  @ <span class="usertype">anonymous</span>, and
   418         -  @ <span class="usertype">nobody</span>.
   419         -  @ </p></li>
   420         -  @
   421         -  @ <li><p>The permission flags are as follows:</p>
   422         -  setup_usercap_table();
   423         -  @ </li>
   424         -  @ </ol>
   425         -  style_footer();
   426         -}
   427         -
   428         -/*
   429         -** WEBPAGE: setup_ucap_list
   430         -**
   431         -** A documentation page showing the meaning of the various user capabilities
   432         -** code letters.
   433         -*/
   434         -void setup_ucap_list(void){
   435         -  style_header("User Capability Codes");
   436         -  setup_usercap_table();
   437         -  style_footer();
   438         -}
   439         -
   440         -/*
   441         -** Return true if zPw is a valid password string.  A valid
   442         -** password string is:
   443         -**
   444         -**  (1)  A zero-length string, or
   445         -**  (2)  a string that contains a character other than '*'.
   446         -*/
   447         -static int isValidPwString(const char *zPw){
   448         -  if( zPw==0 ) return 0;
   449         -  if( zPw[0]==0 ) return 1;
   450         -  while( zPw[0]=='*' ){ zPw++; }
   451         -  return zPw[0]!=0;
   452         -}
   453         -
   454         -/*
   455         -** WEBPAGE: setup_uedit
   456         -**
   457         -** Edit information about a user or create a new user.
   458         -** Requires Admin privileges.
   459         -*/
   460         -void user_edit(void){
   461         -  const char *zId, *zLogin, *zInfo, *zCap, *zPw;
   462         -  const char *zGroup;
   463         -  const char *zOldLogin;
   464         -  int doWrite;
   465         -  int uid, i;
   466         -  int higherUser = 0;  /* True if user being edited is SETUP and the */
   467         -                       /* user doing the editing is ADMIN.  Disallow editing */
   468         -  const char *inherit[128];
   469         -  int a[128];
   470         -  const char *oa[128];
   471         -
   472         -  /* Must have ADMIN privileges to access this page
   473         -  */
   474         -  login_check_credentials();
   475         -  if( !g.perm.Admin ){ login_needed(0); return; }
   476         -
   477         -  /* Check to see if an ADMIN user is trying to edit a SETUP account.
   478         -  ** Don't allow that.
   479         -  */
   480         -  zId = PD("id", "0");
   481         -  uid = atoi(zId);
   482         -  if( zId && !g.perm.Setup && uid>0 ){
   483         -    char *zOldCaps;
   484         -    zOldCaps = db_text(0, "SELECT cap FROM user WHERE uid=%d",uid);
   485         -    higherUser = zOldCaps && strchr(zOldCaps,'s');
   486         -  }
   487         -
   488         -  if( P("can") ){
   489         -    /* User pressed the cancel button */
   490         -    cgi_redirect(cgi_referer("setup_ulist"));
   491         -    return;
   492         -  }
   493         -
   494         -  /* If we have all the necessary information, write the new or
   495         -  ** modified user record.  After writing the user record, redirect
   496         -  ** to the page that displays a list of users.
   497         -  */
   498         -  doWrite = cgi_all("login","info","pw") && !higherUser && cgi_csrf_safe(1);
   499         -  if( doWrite ){
   500         -    char c;
   501         -    char zCap[70], zNm[4];
   502         -    zNm[0] = 'a';
   503         -    zNm[2] = 0;
   504         -    for(i=0, c='a'; c<='z'; c++){
   505         -      zNm[1] = c;
   506         -      a[c&0x7f] = (c!='s' || g.perm.Setup) && P(zNm)!=0;
   507         -      if( a[c&0x7f] ) zCap[i++] = c;
   508         -    }
   509         -    for(c='0'; c<='9'; c++){
   510         -      zNm[1] = c;
   511         -      a[c&0x7f] = P(zNm)!=0;
   512         -      if( a[c&0x7f] ) zCap[i++] = c;
   513         -    }
   514         -    for(c='A'; c<='Z'; c++){
   515         -      zNm[1] = c;
   516         -      a[c&0x7f] = P(zNm)!=0;
   517         -      if( a[c&0x7f] ) zCap[i++] = c;
   518         -    }
   519         -
   520         -    zCap[i] = 0;
   521         -    zPw = P("pw");
   522         -    zLogin = P("login");
   523         -    if( strlen(zLogin)==0 ){
   524         -      const char *zRef = cgi_referer("setup_ulist");
   525         -      style_header("User Creation Error");
   526         -      @ <span class="loginError">Empty login not allowed.</span>
   527         -      @
   528         -      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   529         -      @ [Bummer]</a></p>
   530         -      style_footer();
   531         -      return;
   532         -    }
   533         -    if( isValidPwString(zPw) ){
   534         -      zPw = sha1_shared_secret(zPw, zLogin, 0);
   535         -    }else{
   536         -      zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
   537         -    }
   538         -    zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
   539         -    if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d",zLogin,uid) ){
   540         -      const char *zRef = cgi_referer("setup_ulist");
   541         -      style_header("User Creation Error");
   542         -      @ <span class="loginError">Login "%h(zLogin)" is already used by
   543         -      @ a different user.</span>
   544         -      @
   545         -      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   546         -      @ [Bummer]</a></p>
   547         -      style_footer();
   548         -      return;
   549         -    }
   550         -    login_verify_csrf_secret();
   551         -    db_multi_exec(
   552         -       "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
   553         -       "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())",
   554         -      uid, zLogin, P("info"), zPw, zCap
   555         -    );
   556         -    setup_incr_cfgcnt();
   557         -    admin_log( "Updated user [%q] with capabilities [%q].",
   558         -               zLogin, zCap );
   559         -    if( atoi(PD("all","0"))>0 ){
   560         -      Blob sql;
   561         -      char *zErr = 0;
   562         -      blob_zero(&sql);
   563         -      if( zOldLogin==0 ){
   564         -        blob_appendf(&sql,
   565         -          "INSERT INTO user(login)"
   566         -          "  SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);",
   567         -          zLogin, zLogin
   568         -        );
   569         -        zOldLogin = zLogin;
   570         -      }
   571         -      blob_appendf(&sql,
   572         -        "UPDATE user SET login=%Q,"
   573         -        "  pw=coalesce(shared_secret(%Q,%Q,"
   574         -                "(SELECT value FROM config WHERE name='project-code')),pw),"
   575         -        "  info=%Q,"
   576         -        "  cap=%Q,"
   577         -        "  mtime=now()"
   578         -        " WHERE login=%Q;",
   579         -        zLogin, P("pw"), zLogin, P("info"), zCap,
   580         -        zOldLogin
   581         -      );
   582         -      login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
   583         -      blob_reset(&sql);
   584         -      admin_log( "Updated user [%q] in all login groups "
   585         -                 "with capabilities [%q].",
   586         -                 zLogin, zCap );
   587         -      if( zErr ){
   588         -        const char *zRef = cgi_referer("setup_ulist");
   589         -        style_header("User Change Error");
   590         -        admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
   591         -        @ <span class="loginError">%h(zErr)</span>
   592         -        @
   593         -        @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   594         -        @ [Bummer]</a></p>
   595         -        style_footer();
   596         -        return;
   597         -      }
   598         -    }
   599         -    cgi_redirect(cgi_referer("setup_ulist"));
   600         -    return;
   601         -  }
   602         -
   603         -  /* Load the existing information about the user, if any
   604         -  */
   605         -  zLogin = "";
   606         -  zInfo = "";
   607         -  zCap = "";
   608         -  zPw = "";
   609         -  for(i='a'; i<='z'; i++) oa[i] = "";
   610         -  for(i='0'; i<='9'; i++) oa[i] = "";
   611         -  for(i='A'; i<='Z'; i++) oa[i] = "";
   612         -  if( uid ){
   613         -    zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid);
   614         -    zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid);
   615         -    zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", uid);
   616         -    zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid);
   617         -    for(i=0; zCap[i]; i++){
   618         -      char c = zCap[i];
   619         -      if( (c>='a' && c<='z') || (c>='0' && c<='9') || (c>='A' && c<='Z') ){
   620         -        oa[c&0x7f] = " checked=\"checked\"";
   621         -      }
   622         -    }
   623         -  }
   624         -
   625         -  /* figure out inherited permissions */
   626         -  memset((char *)inherit, 0, sizeof(inherit));
   627         -  if( fossil_strcmp(zLogin, "developer") ){
   628         -    char *z1, *z2;
   629         -    z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='developer'");
   630         -    while( z1 && *z1 ){
   631         -      inherit[0x7f & *(z1++)] =
   632         -         "<span class=\"ueditInheritDeveloper\"><sub>[D]</sub></span>";
   633         -    }
   634         -    free(z2);
   635         -  }
   636         -  if( fossil_strcmp(zLogin, "reader") ){
   637         -    char *z1, *z2;
   638         -    z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='reader'");
   639         -    while( z1 && *z1 ){
   640         -      inherit[0x7f & *(z1++)] =
   641         -          "<span class=\"ueditInheritReader\"><sub>[R]</sub></span>";
   642         -    }
   643         -    free(z2);
   644         -  }
   645         -  if( fossil_strcmp(zLogin, "anonymous") ){
   646         -    char *z1, *z2;
   647         -    z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='anonymous'");
   648         -    while( z1 && *z1 ){
   649         -      inherit[0x7f & *(z1++)] =
   650         -           "<span class=\"ueditInheritAnonymous\"><sub>[A]</sub></span>";
   651         -    }
   652         -    free(z2);
   653         -  }
   654         -  if( fossil_strcmp(zLogin, "nobody") ){
   655         -    char *z1, *z2;
   656         -    z1 = z2 = db_text(0,"SELECT cap FROM user WHERE login='nobody'");
   657         -    while( z1 && *z1 ){
   658         -      inherit[0x7f & *(z1++)] =
   659         -           "<span class=\"ueditInheritNobody\"><sub>[N]</sub></span>";
   660         -    }
   661         -    free(z2);
   662         -  }
   663         -
   664         -  /* Begin generating the page
   665         -  */
   666         -  style_submenu_element("Cancel", "%s", cgi_referer("setup_ulist"));
   667         -  if( uid ){
   668         -    style_header("Edit User %h", zLogin);
   669         -    style_submenu_element("Access Log", "%R/access_log?u=%t", zLogin);
   670         -  }else{
   671         -    style_header("Add A New User");
   672         -  }
   673         -  @ <div class="ueditCapBox">
   674         -  @ <form action="%s(g.zPath)" method="post"><div>
   675         -  login_insert_csrf_secret();
   676         -  if( login_is_special(zLogin) ){
   677         -    @ <input type="hidden" name="login" value="%s(zLogin)">
   678         -    @ <input type="hidden" name="info" value="">
   679         -    @ <input type="hidden" name="pw" value="*">
   680         -  }
   681         -  @ <input type="hidden" name="referer" value="%h(cgi_referer("setup_ulist"))">
   682         -  @ <table>
   683         -  @ <tr>
   684         -  @   <td class="usetupEditLabel">User ID:</td>
   685         -