Fossil SCM

Merge trunk

jan.nijtmans 2020-06-25 08:16 UTC cleanX merge
Commit 69b450c1e028c077db3030487fb6ab4e3bae5937d24c0c79cac9c3b5ee77711e
348 files changed +10 +2 +8 +1 -1 +59 -8 +17 -10 +35 -1 +4 -1 +1 -1 +1 -1 +2 -2 +1 -1 +4 -3 +12 -1 +14 +27 +163 -18 +19 +459 -199 +10 -39 +33 -28 +6 +23 -2 +6 +54 -55 +113 -15 +51 -10 +158 -79 +10 -2 +12 -12 +28 -12 +95 +296 -70 +210 -163 +210 -163 +1 -1 +18 -14 +37 -17 +17 -31 +4 -1 +16 -10 +48 -8 +4 -1 +413 -146 +11 -28 +12 -29 +80 -10 +79 -49 +2 -2 +658 -57 +260 -27 +29 -6 +57 -8 +17 -2 +42 -12 +41 -1 +174 -3 +27 +30 -20 +394 -61 +349 +1 +1 +74 +42 +8 +4 +9 -2 +66 +98 -5 +16 -2 +2 -1 +238 -123 +4 -4 +24 -17 +416 -305 +75 -35 +10 -1 +2 -5 +1 -1 +7 -8 +1 -1 +2 -1 +11 +1 -1 +154 -119 +6 -4 +293 -102 +93 -17 +15 -2 +8 -8 +215 -197 +113 -42 +208 -148 +30 -19 +70 -31 +25 -1 +17 -9 +207 -15 -168 +6 +95 -3 +4 -3 +366 -89 +40 -8 +29 -29 +100 -23 +120 -33 +4 -1 +11 -3 +20 -25 +8 -5 +26 -8 +118 -32 +160 -32 +130 -53 +54 -17 -19 +1340 -447 +4 -4 +4 +14 -2 +10 -2 +15 +23 -2 +25 +15229 -9140 +612 -230 +65 -61 +31 -20 +10 -2 +6 +561 -87 +220 +46 -45 +13 -11 +2 -2 +9 +20 -12 +53 -7 +255 -83 +154 -72 +24 -10 +27 -13 +52 -49 +79 -22 +86 -27 +19 -15 +7 -7 +53 +210 -7 +1 -1 +1 -1 +207 -98 +594 -171 +5 -5 +168 -63 +1 -1 +2 -2 +79 -82 +23 -5 +7 -1 +18 +4 -4 +2 -2 +1 -1 +72 +51 -22 +79 +32 -2 +10 +164 +9 -6 +1 -1 +2 +75 -33 +3 -2 +18 -10 +8 -5 +288 -288 +10 -10 +5 -1 +72 +3 -6 +38 -15 +93 -14 +107 -16 +1821 -1614 +58 -13 +13 -1 +1 -1 +11 -2 -4 +22 -16 +36 -39 +4 -4 +4 -4 +3 -3 +136 -2 +9 -4 +9 +64 +39 +6 +5 +81 +140 -2 +72 -26 +40 +4 -4 +1 -1 +3 +1 -1 +246 -103 +416 +123 -33 +1 -1 +55 -82 +1 -1 +3 -1 +30 +9 -1 +2 -9 +35 -67 +638 -244 +2 -1 +1 -1 +143 +199 -134 +5 -6 +3 -3 +109 +49 -38 +72 +1460 -12 +3 -3 +311 -311 +25 -76 +225 +22 +160 +8 +86 +45 +36 +44 +53 +53 +92 +22 +59 +7 +136 +214 +142 +32 +301 +332 +10 +38 +208 +102 +13 +27 -1 +55 +49 -24 +1 +18 -5 +2 -2 +49 -13 +22 -9 +4 -4 +18 -15 +148 +71 +137 +69 +200 +92 +242 +108 +231 +101 +221 +15 -15 +3 -2 -149 +19 +7 +16 +41 +3 +34 +26 +127 +5 +112 -119 +142 +122 +1 +18 +115 +52 +8 +69 +5 +146 +39 -18 +81 -17 +44 -69 +1 -1 +12 -5 +62 -19 +12 -3 +7 -7 +51 -243 +3 -3 +1 +26 -35 +2 -2 +6 -3 +4 -3
~ .editorconfig ~ Makefile.classic ~ Makefile.in ~ VERSION ~ auto.def ~ skins/README.md ~ skins/ardoise/css.txt ~ skins/blitz/css.txt ~ skins/blitz/ticket.txt ~ skins/blitz_no_logo/ticket.txt ~ skins/bootstrap/header.txt ~ skins/default/css.txt ~ skins/default/header.txt ~ skins/eagle/css.txt ~ skins/xekri/css.txt ~ src/accordion.js ~ src/add.c ~ src/ajax.c ~ src/alerts.c ~ src/allrepo.c ~ src/attach.c ~ src/backlink.c ~ src/backoffice.c ~ src/bag.c ~ src/bisect.c ~ src/blob.c ~ src/branch.c ~ src/browse.c ~ src/builtin.c ~ src/bundle.c ~ src/capabilities.c ~ src/captcha.c ~ src/cgi.c ~ src/checkin.c ~ src/checkin.c ~ src/checkout.c ~ src/clone.c ~ src/codecheck1.c ~ src/comformat.c ~ src/config.h ~ src/configure.c ~ src/content.c ~ src/cookies.c ~ src/db.c ~ src/default.css + src/default.css - src/default_css.txt ~ src/descendants.c ~ src/diff.c ~ src/diffcmd.c ~ src/dispatch.c ~ src/doc.c ~ src/encode.c ~ src/etag.c ~ src/event.c ~ src/export.c ~ src/extcgi.c ~ src/file.c ~ src/fileedit.c ~ src/finfo.c ~ src/forum.c ~ src/fossil.bootstrap.js ~ src/fossil.confirmer.js ~ src/fossil.dom.js ~ src/fossil.fetch.js ~ src/fossil.page.fileedit.js ~ src/fossil.storage.js ~ src/fossil.tabs.js ~ src/fshell.c ~ src/fuzz.c ~ src/graph.c ~ src/graph.js ~ src/http.c ~ src/http_ssl.c ~ src/http_transport.c ~ src/import.c ~ src/info.c ~ src/json.c ~ src/json_artifact.c ~ src/json_branch.c ~ src/json_finfo.c ~ src/json_login.c ~ src/json_tag.c ~ src/json_timeline.c ~ src/leaf.c ~ src/loadctrl.c ~ src/login.c ~ src/lookslike.c ~ src/main.c ~ src/main.mk ~ src/makeheaders.c ~ src/makeheaders.html ~ src/makemake.tcl ~ src/manifest.c ~ src/markdown.c ~ src/markdown.md ~ src/markdown_html.c ~ src/merge.c ~ src/merge3.c ~ src/mkbuiltin.c - src/mkcss.c ~ src/mkindex.c ~ src/mkversion.c ~ src/moderate.c ~ src/name.c ~ src/printf.c ~ src/purge.c ~ src/rebuild.c ~ src/regexp.c ~ src/repolist.c ~ src/report.c ~ src/rss.c ~ src/sbsdiff.js ~ src/schema.c ~ src/search.c ~ src/security_audit.c ~ src/setup.c ~ src/setupuser.c ~ src/sha1.c ~ src/shell.c ~ src/shun.c ~ src/sitemap.c ~ src/skins.c ~ src/smtp.c ~ src/sounds/0.wav ~ src/sounds/1.wav ~ src/sounds/2.wav ~ src/sounds/3.wav ~ src/sounds/4.wav ~ src/sounds/5.wav ~ src/sounds/6.wav ~ src/sounds/7.wav ~ src/sounds/8.wav ~ src/sounds/9.wav ~ src/sounds/README.md ~ src/sounds/a.wav ~ src/sounds/b.wav ~ src/sounds/c.wav ~ src/sounds/d.wav ~ src/sounds/e.wav ~ src/sounds/f.wav ~ src/sqlcmd.c ~ src/sqlcompattest.c ~ src/sqlite3.c ~ src/sqlite3.h ~ src/stash.c ~ src/stat.c ~ src/statrep.c ~ src/style.admin_log.css ~ src/style.c ~ src/style.fileedit.css ~ src/sync.c ~ src/tag.c ~ src/tar.c ~ src/terminal.c ~ src/th.c ~ src/th_main.c ~ src/timeline.c ~ src/tkt.c ~ src/tktsetup.c ~ src/undo.c ~ src/unicode.c ~ src/unversioned.c ~ src/update.c ~ src/url.c ~ src/user.c ~ src/utf8.c ~ src/util.c ~ src/verify.c ~ src/vfile.c ~ src/wiki.c ~ src/wikiformat.c ~ src/winhttp.c ~ src/xfer.c ~ src/xfersetup.c ~ src/zip.c ~ test/amend.test ~ test/cmdline.test ~ test/commit-warning.test ~ test/csp1.html ~ test/diff.test ~ test/fake-editor.tcl ~ test/fileStat.th1 ~ test/graph-test-2.md ~ test/json.test ~ test/markdown-test2.md ~ test/mv-rm.test ~ test/release-checklist.wiki ~ test/revert.test ~ test/set-manifest.test ~ test/settings.test ~ test/subdir with spaces/filename with spaces.txt ~ test/tester.tcl ~ test/th1-hooks.test ~ test/th1.test ~ test/unversioned.test ~ test/utf.test ~ test/wiki.test ~ tools/email-monitor.tcl ~ tools/email-sender.tcl ~ tools/encode_math.sh ~ tools/fossil-autocomplete.bash ~ tools/fossil-diff-log ~ tools/fossil-stress.tcl ~ tools/fossil_chat.tcl ~ win/Makefile.PellesCGMake ~ win/Makefile.dmc ~ win/Makefile.mingw ~ win/Makefile.mingw.mistachkin ~ win/Makefile.msc ~ win/buildmsvc.bat ~ www/aboutcgi.wiki ~ www/aboutdownload.wiki ~ www/adding_code.wiki - www/admin-v-setup.md ~ www/alerts.md ~ www/antibot.wiki ~ www/backoffice.md ~ www/blockchain.md ~ www/branching.wiki ~ www/build.wiki ~ www/caps/admin-v-setup.md + www/caps/admin-v-setup.md ~ www/caps/impl.md ~ www/caps/index.md ~ www/caps/login-groups.md ~ www/caps/ref.html ~ www/cgi.wiki ~ www/changes.wiki ~ www/checkin_names.wiki ~ www/chroot.md ~ www/concepts.wiki ~ www/contribute.wiki ~ www/css-tricks.md ~ www/custom_ticket.wiki ~ www/customskin.md ~ www/defcsp.md ~ www/embeddeddoc.wiki ~ www/encryptedrepos.wiki ~ www/env-opts.md ~ www/event.wiki ~ www/faq.wiki ~ www/fileedit-page.md ~ www/fileformat.wiki ~ www/fiveminutes.wiki ~ www/forum.wiki ~ www/fossil-v-git.wiki ~ www/fossil_prompt.sh ~ www/fossil_prompt.wiki ~ www/gitusers.md ~ www/globs.md ~ www/grep.md ~ www/hacker-howto.wiki ~ www/hashes.md ~ www/hashpolicy.wiki ~ www/history.md ~ www/image-format-vs-repo-size.ipynb ~ www/image-format-vs-repo-size.md ~ www/image-format-vs-repo-size.svg ~ www/index.wiki ~ www/javascript.md ~ www/json-api/_template.md ~ www/json-api/api-artifact.md ~ www/json-api/api-auth.md ~ www/json-api/api-branch.md ~ www/json-api/api-checkout.md ~ www/json-api/api-config.md ~ www/json-api/api-diff.md ~ www/json-api/api-dir.md ~ www/json-api/api-finfo.md ~ www/json-api/api-misc.md ~ www/json-api/api-query.md ~ www/json-api/api-stat.md ~ www/json-api/api-tag.md ~ www/json-api/api-ticket.md ~ www/json-api/api-timeline.md ~ www/json-api/api-user.md ~ www/json-api/api-version.md ~ www/json-api/api-wiki.md ~ www/json-api/conventions.md ~ www/json-api/hacking.md ~ www/json-api/index.md ~ www/json-api/intro.md ~ www/json-api/tips.md ~ www/loadmgmt.md ~ www/makefile.wiki ~ www/mdtest/test1.md ~ www/mirrorlimitations.md ~ www/mirrortogithub.md ~ www/mkindex.tcl ~ www/password.wiki ~ www/permutedindex.html ~ www/private.wiki ~ www/qandc.wiki ~ www/quickstart.wiki ~ www/rebase01.graphml ~ www/rebase01.svg ~ www/rebase02.graphml ~ www/rebase02.svg ~ www/rebase03.graphml ~ www/rebase03.svg ~ www/rebase04.graphml ~ www/rebase04.svg ~ www/rebase05.graphml ~ www/rebase05.svg ~ www/rebaseharm.md ~ www/selfcheck.wiki ~ www/selfhost.wiki - www/server.wiki ~ www/server/any/althttpd.md ~ www/server/any/cgi.md ~ www/server/any/inetd.md ~ www/server/any/none.md ~ www/server/any/scgi.md ~ www/server/any/stunnel.md ~ www/server/any/xinetd.md ~ www/server/debian/nginx.md ~ www/server/debian/service.md ~ www/server/index.html + www/server/index.html ~ www/server/macos/service.md ~ www/server/openbsd/httpd.md ~ www/server/whyuseaserver.wiki ~ www/server/windows/cgi-bin-perm.png ~ www/server/windows/cgi-exec-perm.png ~ www/server/windows/cgi-install-iis.png ~ www/server/windows/cgi-script-map.png ~ www/server/windows/cgi.md ~ www/server/windows/iis.md ~ www/server/windows/index.md ~ www/server/windows/none.md ~ www/server/windows/service.md ~ www/server/windows/stunnel.md ~ www/serverext.wiki ~ www/shunning.wiki ~ www/ssl.wiki ~ www/stats.wiki ~ www/sync.wiki ~ www/tech_overview.wiki ~ www/th1.md ~ www/tickets.wiki ~ www/tls-nginx.md ~ www/unvers.wiki ~ www/userlinks.wiki ~ www/webpage-ex.md ~ www/webui.wiki ~ www/whyusefossil.wiki ~ www/wikitheory.wiki
--- a/.editorconfig
+++ b/.editorconfig
@@ -0,0 +1,10 @@
1
+# EditorConfig (https://editorconfig.com) Configuration for Fossil
2
+#
3
+# Following https://fossil-scm.org/fossil/doc/trunk/www/style.wiki
4
+#
5
+
6
+# Defaults for all files
7
+[*]
8
+end_of_line = lf
9
+insert_final_newline = true
10
+indent_styl[{Makefile,Makefile.*
--- a/.editorconfig
+++ b/.editorconfig
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
--- a/.editorconfig
+++ b/.editorconfig
@@ -0,0 +1,10 @@
1 # EditorConfig (https://editorconfig.com) Configuration for Fossil
2 #
3 # Following https://fossil-scm.org/fossil/doc/trunk/www/style.wiki
4 #
5
6 # Defaults for all files
7 [*]
8 end_of_line = lf
9 insert_final_newline = true
10 indent_styl[{Makefile,Makefile.*
--- Makefile.classic
+++ Makefile.classic
@@ -99,7 +99,9 @@
9999
TCC.DragonFly += -DUSE_PREAD
100100
TCC.FreeBSD += -DUSE_PREAD
101101
TCC.NetBSD += -DUSE_PREAD
102102
TCC.OpenBSD += -DUSE_PREAD
103103
TCC += $(TCC.$(HOST_OS))
104
+
105
+APPNAME = fossil$(E)
104106
105107
include $(SRCDIR)/main.mk
106108
--- Makefile.classic
+++ Makefile.classic
@@ -99,7 +99,9 @@
99 TCC.DragonFly += -DUSE_PREAD
100 TCC.FreeBSD += -DUSE_PREAD
101 TCC.NetBSD += -DUSE_PREAD
102 TCC.OpenBSD += -DUSE_PREAD
103 TCC += $(TCC.$(HOST_OS))
 
 
104
105 include $(SRCDIR)/main.mk
106
--- Makefile.classic
+++ Makefile.classic
@@ -99,7 +99,9 @@
99 TCC.DragonFly += -DUSE_PREAD
100 TCC.FreeBSD += -DUSE_PREAD
101 TCC.NetBSD += -DUSE_PREAD
102 TCC.OpenBSD += -DUSE_PREAD
103 TCC += $(TCC.$(HOST_OS))
104
105 APPNAME = fossil$(E)
106
107 include $(SRCDIR)/main.mk
108
--- Makefile.in
+++ Makefile.in
@@ -46,19 +46,27 @@
4646
USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@
4747
USE_LINENOISE = @USE_LINENOISE@
4848
USE_MMAN_H = @USE_MMAN_H@
4949
USE_SEE = @USE_SEE@
5050
FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@
51
+APPNAME = fossil
52
+
53
+.PHONY: all tags
5154
5255
include $(SRCDIR)/main.mk
5356
5457
distclean: clean
5558
-rm -f autoconfig.h config.log Makefile
59
+ -rm -f cscope.out tags
5660
5761
reconfig:
5862
@AUTOREMAKE@
5963
64
+tags:
65
+ ctags -R @srcdir@/src
66
+ @COLLECT_CSCOPE_DATA@
67
+
6068
# Automatically reconfigure whenever an autosetup file or one of the
6169
# make source files change.
6270
#
6371
# The "touch" is necessary to avoid a make loop due to a new upstream
6472
# feature in autosetup (GH 0a71e3c3b7) which rewrites *.in outputs only
6573
--- Makefile.in
+++ Makefile.in
@@ -46,19 +46,27 @@
46 USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@
47 USE_LINENOISE = @USE_LINENOISE@
48 USE_MMAN_H = @USE_MMAN_H@
49 USE_SEE = @USE_SEE@
50 FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@
 
 
 
51
52 include $(SRCDIR)/main.mk
53
54 distclean: clean
55 -rm -f autoconfig.h config.log Makefile
 
56
57 reconfig:
58 @AUTOREMAKE@
59
 
 
 
 
60 # Automatically reconfigure whenever an autosetup file or one of the
61 # make source files change.
62 #
63 # The "touch" is necessary to avoid a make loop due to a new upstream
64 # feature in autosetup (GH 0a71e3c3b7) which rewrites *.in outputs only
65
--- Makefile.in
+++ Makefile.in
@@ -46,19 +46,27 @@
46 USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@
47 USE_LINENOISE = @USE_LINENOISE@
48 USE_MMAN_H = @USE_MMAN_H@
49 USE_SEE = @USE_SEE@
50 FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@
51 APPNAME = fossil
52
53 .PHONY: all tags
54
55 include $(SRCDIR)/main.mk
56
57 distclean: clean
58 -rm -f autoconfig.h config.log Makefile
59 -rm -f cscope.out tags
60
61 reconfig:
62 @AUTOREMAKE@
63
64 tags:
65 ctags -R @srcdir@/src
66 @COLLECT_CSCOPE_DATA@
67
68 # Automatically reconfigure whenever an autosetup file or one of the
69 # make source files change.
70 #
71 # The "touch" is necessary to avoid a make loop due to a new upstream
72 # feature in autosetup (GH 0a71e3c3b7) which rewrites *.in outputs only
73
+1 -1
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1
-2.10
1
+2.12
22
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1 2.10
2
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1 2.12
2
+59 -8
--- auto.def
+++ auto.def
@@ -9,10 +9,11 @@
99
with-zlib:path|auto|tree
1010
=> {Look for zlib in the given path, automatically, or in the source tree}
1111
with-exec-rel-paths=0
1212
=> {Enable relative paths for external diff/gdiff}
1313
with-legacy-mv-rm=1 => {Enable legacy behavior for mv/rm (skip checkout files)}
14
+ with-sanitizer: => {Build with C compiler's -fsanitize=LIST; e.g. address,enum,null,undefined}
1415
with-th1-docs=0 => {Enable TH1 for embedded documentation pages}
1516
with-th1-hooks=0 => {Enable TH1 hooks for commands and web pages}
1617
with-tcl:path => {Enable Tcl integration, with Tcl in the specified path}
1718
with-tcl-stubs=0 => {Enable Tcl integration via stubs library mechanism}
1819
with-tcl-private-stubs=0
@@ -32,10 +33,17 @@
3233
cc-check-types uint32_t uint16_t int16_t uint8_t
3334
}
3435
3536
# Use pread/pwrite system calls in place of seek + read/write if possible
3637
define USE_PREAD [cc-check-functions pread]
38
+
39
+# If we have cscope here, we'll use it in the "tags" target
40
+if {[cc-check-progs cscope]} {
41
+ define COLLECT_CSCOPE_DATA "cscope -bR -s$::autosetup(srcdir)/src"
42
+} else {
43
+ define COLLECT_CSCOPE_DATA ""
44
+}
3745
3846
# Find tclsh for the test suite.
3947
#
4048
# We can't use jimsh for this: the test suite uses features of Tcl that
4149
# Jim doesn't support, either statically or due to the way it's built by
@@ -116,11 +124,11 @@
116124
}
117125
return $found
118126
}
119127
120128
if {![opt-bool internal-sqlite]} {
121
- proc find_internal_sqlite {} {
129
+ proc find_system_sqlite {} {
122130
123131
# On some systems (slackware), libsqlite3 requires -ldl to link. So
124132
# search for the system SQLite once with -ldl, and once without. If
125133
# the library can only be found with $extralibs set to -ldl, then
126134
# the code below will append -ldl to LIBS.
@@ -131,14 +139,10 @@
131139
# if sqlite3_stmt_isexplain can be found as well. If we can find open() but
132140
# not stmt_isexplain(), then the system SQLite is too old to link against
133141
# fossil.
134142
#
135143
if {[check-function-in-lib sqlite3_open sqlite3 $extralibs]} {
136
- if {![check-function-in-lib sqlite3_stmt_isexplain sqlite3 $extralibs]} {
137
- user-error "system sqlite3 too old (require >= 3.28.0)"
138
- }
139
-
140144
# Success. Update symbols and return.
141145
#
142146
define USE_SYSTEM_SQLITE 1
143147
define-append LIBS -lsqlite3
144148
define-append LIBS $extralibs
@@ -146,11 +150,44 @@
146150
}
147151
}
148152
user-error "system sqlite3 not found"
149153
}
150154
151
- find_internal_sqlite
155
+ find_system_sqlite
156
+
157
+ proc test_system_sqlite {} {
158
+ # Check compatibility of the system SQLite library by running the sqlcompttest.c
159
+ # program in the source tree
160
+ #
161
+ set cmdline {}
162
+ lappend cmdline {*}[get-define CCACHE]
163
+ lappend cmdline {*}[get-define CC] {*}[get-define CFLAGS]
164
+ lappend cmdline $::autosetup(dir)/../src/sqlcompattest.c -o conftest__
165
+ lappend cmdline {*}[get-define LDFLAGS]
166
+ lappend cmdline {*}[get-define LIBS]
167
+ set ok 1
168
+ set err [catch {exec-with-stderr {*}$cmdline} result errinfo]
169
+ if {$err} {
170
+ configlog "Failed: [join $cmdline]"
171
+ if {[string length $result]>0} {configlog $result}
172
+ configlog "============"
173
+ set ok 0
174
+ } elseif {$::autosetup(debug)} {
175
+ configlog "Compiled OK: [join $cmdline]"
176
+ configlog "============"
177
+ }
178
+ if {!$ok} {
179
+ user-error "unable to compile SQLite compatibility test program"
180
+ }
181
+ set err [catch {exec-with-stderr ./conftest__} result errinfo]
182
+ if {$err} {
183
+ user-error $result
184
+ }
185
+ file delete ./conftest__
186
+ }
187
+ test_system_sqlite
188
+
152189
}
153190
154191
proc is_mingw {} {
155192
return [string match *mingw* [get-define host]]
156193
}
@@ -243,11 +280,11 @@
243280
244281
# Check for libraries that need to be sorted out early
245282
cc-check-function-in-lib iconv iconv
246283
247284
# Helper for OpenSSL checking
248
-proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto}}} {
285
+proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto -lpthread}}} {
249286
msg-checking "Checking for $msg..."
250287
set rc 0
251288
if {[is_mingw]} {
252289
lappend libs -lgdi32 -lwsock32 -lcrypt32
253290
}
@@ -321,11 +358,11 @@
321358
user-error "The OpenSSL in source tree directory does not exist"
322359
}
323360
set msg "ssl in $ssldir"
324361
set cflags "-I$ssldir/include"
325362
set ldflags "-L$ssldir"
326
- set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a"
363
+ set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread"
327364
set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs]
328365
} else {
329366
if {$ssldirs in {auto ""}} {
330367
catch {
331368
set cflags [exec pkg-config openssl --cflags-only-I]
@@ -560,10 +597,24 @@
560597
define FOSSIL_HAVE_FUSEFS 1
561598
define-append LIBS -lfuse
562599
msg-result "FuseFS support enabled"
563600
}
564601
}
602
+
603
+# Add -fsanitize compile and link options late: we don't want the C
604
+# checks above to run with those sanitizers enabled. It can not only
605
+# be pointless, it can actually break correct tests.
606
+set fsan [opt-val with-sanitizer]
607
+if {[string length $fsan]} {
608
+ define-append EXTRA_CFLAGS -fsanitize=$fsan
609
+ define-append EXTRA_LDFLAGS -fsanitize=$fsan
610
+ if {[string first "undefined" $fsan] != -1} {
611
+ # We need to link with libubsan if we're compiling under
612
+ # GCC with -fsanitize=undefined.
613
+ cc-check-function-in-lib __ubsan_handle_add_overflow ubsan
614
+ }
615
+}
565616
566617
# Finally, append -ldl to make sure it's the last in the list.
567618
# The library order matters in case of static linking.
568619
if {[check-function-in-lib dlopen dl]} {
569620
# Some platforms (*BSD) have the dl functions already in libc and no libdl.
570621
--- auto.def
+++ auto.def
@@ -9,10 +9,11 @@
9 with-zlib:path|auto|tree
10 => {Look for zlib in the given path, automatically, or in the source tree}
11 with-exec-rel-paths=0
12 => {Enable relative paths for external diff/gdiff}
13 with-legacy-mv-rm=1 => {Enable legacy behavior for mv/rm (skip checkout files)}
 
14 with-th1-docs=0 => {Enable TH1 for embedded documentation pages}
15 with-th1-hooks=0 => {Enable TH1 hooks for commands and web pages}
16 with-tcl:path => {Enable Tcl integration, with Tcl in the specified path}
17 with-tcl-stubs=0 => {Enable Tcl integration via stubs library mechanism}
18 with-tcl-private-stubs=0
@@ -32,10 +33,17 @@
32 cc-check-types uint32_t uint16_t int16_t uint8_t
33 }
34
35 # Use pread/pwrite system calls in place of seek + read/write if possible
36 define USE_PREAD [cc-check-functions pread]
 
 
 
 
 
 
 
37
38 # Find tclsh for the test suite.
39 #
40 # We can't use jimsh for this: the test suite uses features of Tcl that
41 # Jim doesn't support, either statically or due to the way it's built by
@@ -116,11 +124,11 @@
116 }
117 return $found
118 }
119
120 if {![opt-bool internal-sqlite]} {
121 proc find_internal_sqlite {} {
122
123 # On some systems (slackware), libsqlite3 requires -ldl to link. So
124 # search for the system SQLite once with -ldl, and once without. If
125 # the library can only be found with $extralibs set to -ldl, then
126 # the code below will append -ldl to LIBS.
@@ -131,14 +139,10 @@
131 # if sqlite3_stmt_isexplain can be found as well. If we can find open() but
132 # not stmt_isexplain(), then the system SQLite is too old to link against
133 # fossil.
134 #
135 if {[check-function-in-lib sqlite3_open sqlite3 $extralibs]} {
136 if {![check-function-in-lib sqlite3_stmt_isexplain sqlite3 $extralibs]} {
137 user-error "system sqlite3 too old (require >= 3.28.0)"
138 }
139
140 # Success. Update symbols and return.
141 #
142 define USE_SYSTEM_SQLITE 1
143 define-append LIBS -lsqlite3
144 define-append LIBS $extralibs
@@ -146,11 +150,44 @@
146 }
147 }
148 user-error "system sqlite3 not found"
149 }
150
151 find_internal_sqlite
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152 }
153
154 proc is_mingw {} {
155 return [string match *mingw* [get-define host]]
156 }
@@ -243,11 +280,11 @@
243
244 # Check for libraries that need to be sorted out early
245 cc-check-function-in-lib iconv iconv
246
247 # Helper for OpenSSL checking
248 proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto}}} {
249 msg-checking "Checking for $msg..."
250 set rc 0
251 if {[is_mingw]} {
252 lappend libs -lgdi32 -lwsock32 -lcrypt32
253 }
@@ -321,11 +358,11 @@
321 user-error "The OpenSSL in source tree directory does not exist"
322 }
323 set msg "ssl in $ssldir"
324 set cflags "-I$ssldir/include"
325 set ldflags "-L$ssldir"
326 set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a"
327 set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs]
328 } else {
329 if {$ssldirs in {auto ""}} {
330 catch {
331 set cflags [exec pkg-config openssl --cflags-only-I]
@@ -560,10 +597,24 @@
560 define FOSSIL_HAVE_FUSEFS 1
561 define-append LIBS -lfuse
562 msg-result "FuseFS support enabled"
563 }
564 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
566 # Finally, append -ldl to make sure it's the last in the list.
567 # The library order matters in case of static linking.
568 if {[check-function-in-lib dlopen dl]} {
569 # Some platforms (*BSD) have the dl functions already in libc and no libdl.
570
--- auto.def
+++ auto.def
@@ -9,10 +9,11 @@
9 with-zlib:path|auto|tree
10 => {Look for zlib in the given path, automatically, or in the source tree}
11 with-exec-rel-paths=0
12 => {Enable relative paths for external diff/gdiff}
13 with-legacy-mv-rm=1 => {Enable legacy behavior for mv/rm (skip checkout files)}
14 with-sanitizer: => {Build with C compiler's -fsanitize=LIST; e.g. address,enum,null,undefined}
15 with-th1-docs=0 => {Enable TH1 for embedded documentation pages}
16 with-th1-hooks=0 => {Enable TH1 hooks for commands and web pages}
17 with-tcl:path => {Enable Tcl integration, with Tcl in the specified path}
18 with-tcl-stubs=0 => {Enable Tcl integration via stubs library mechanism}
19 with-tcl-private-stubs=0
@@ -32,10 +33,17 @@
33 cc-check-types uint32_t uint16_t int16_t uint8_t
34 }
35
36 # Use pread/pwrite system calls in place of seek + read/write if possible
37 define USE_PREAD [cc-check-functions pread]
38
39 # If we have cscope here, we'll use it in the "tags" target
40 if {[cc-check-progs cscope]} {
41 define COLLECT_CSCOPE_DATA "cscope -bR -s$::autosetup(srcdir)/src"
42 } else {
43 define COLLECT_CSCOPE_DATA ""
44 }
45
46 # Find tclsh for the test suite.
47 #
48 # We can't use jimsh for this: the test suite uses features of Tcl that
49 # Jim doesn't support, either statically or due to the way it's built by
@@ -116,11 +124,11 @@
124 }
125 return $found
126 }
127
128 if {![opt-bool internal-sqlite]} {
129 proc find_system_sqlite {} {
130
131 # On some systems (slackware), libsqlite3 requires -ldl to link. So
132 # search for the system SQLite once with -ldl, and once without. If
133 # the library can only be found with $extralibs set to -ldl, then
134 # the code below will append -ldl to LIBS.
@@ -131,14 +139,10 @@
139 # if sqlite3_stmt_isexplain can be found as well. If we can find open() but
140 # not stmt_isexplain(), then the system SQLite is too old to link against
141 # fossil.
142 #
143 if {[check-function-in-lib sqlite3_open sqlite3 $extralibs]} {
 
 
 
 
144 # Success. Update symbols and return.
145 #
146 define USE_SYSTEM_SQLITE 1
147 define-append LIBS -lsqlite3
148 define-append LIBS $extralibs
@@ -146,11 +150,44 @@
150 }
151 }
152 user-error "system sqlite3 not found"
153 }
154
155 find_system_sqlite
156
157 proc test_system_sqlite {} {
158 # Check compatibility of the system SQLite library by running the sqlcompttest.c
159 # program in the source tree
160 #
161 set cmdline {}
162 lappend cmdline {*}[get-define CCACHE]
163 lappend cmdline {*}[get-define CC] {*}[get-define CFLAGS]
164 lappend cmdline $::autosetup(dir)/../src/sqlcompattest.c -o conftest__
165 lappend cmdline {*}[get-define LDFLAGS]
166 lappend cmdline {*}[get-define LIBS]
167 set ok 1
168 set err [catch {exec-with-stderr {*}$cmdline} result errinfo]
169 if {$err} {
170 configlog "Failed: [join $cmdline]"
171 if {[string length $result]>0} {configlog $result}
172 configlog "============"
173 set ok 0
174 } elseif {$::autosetup(debug)} {
175 configlog "Compiled OK: [join $cmdline]"
176 configlog "============"
177 }
178 if {!$ok} {
179 user-error "unable to compile SQLite compatibility test program"
180 }
181 set err [catch {exec-with-stderr ./conftest__} result errinfo]
182 if {$err} {
183 user-error $result
184 }
185 file delete ./conftest__
186 }
187 test_system_sqlite
188
189 }
190
191 proc is_mingw {} {
192 return [string match *mingw* [get-define host]]
193 }
@@ -243,11 +280,11 @@
280
281 # Check for libraries that need to be sorted out early
282 cc-check-function-in-lib iconv iconv
283
284 # Helper for OpenSSL checking
285 proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto -lpthread}}} {
286 msg-checking "Checking for $msg..."
287 set rc 0
288 if {[is_mingw]} {
289 lappend libs -lgdi32 -lwsock32 -lcrypt32
290 }
@@ -321,11 +358,11 @@
358 user-error "The OpenSSL in source tree directory does not exist"
359 }
360 set msg "ssl in $ssldir"
361 set cflags "-I$ssldir/include"
362 set ldflags "-L$ssldir"
363 set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread"
364 set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs]
365 } else {
366 if {$ssldirs in {auto ""}} {
367 catch {
368 set cflags [exec pkg-config openssl --cflags-only-I]
@@ -560,10 +597,24 @@
597 define FOSSIL_HAVE_FUSEFS 1
598 define-append LIBS -lfuse
599 msg-result "FuseFS support enabled"
600 }
601 }
602
603 # Add -fsanitize compile and link options late: we don't want the C
604 # checks above to run with those sanitizers enabled. It can not only
605 # be pointless, it can actually break correct tests.
606 set fsan [opt-val with-sanitizer]
607 if {[string length $fsan]} {
608 define-append EXTRA_CFLAGS -fsanitize=$fsan
609 define-append EXTRA_LDFLAGS -fsanitize=$fsan
610 if {[string first "undefined" $fsan] != -1} {
611 # We need to link with libubsan if we're compiling under
612 # GCC with -fsanitize=undefined.
613 cc-check-function-in-lib __ubsan_handle_add_overflow ubsan
614 }
615 }
616
617 # Finally, append -ldl to make sure it's the last in the list.
618 # The library order matters in case of static linking.
619 if {[check-function-in-lib dlopen dl]} {
620 # Some platforms (*BSD) have the dl functions already in libc and no libdl.
621
+17 -10
--- skins/README.md
+++ skins/README.md
@@ -1,11 +1,16 @@
11
Built-in Skins
22
==============
33
44
Each subdirectory under this folder describes a built-in "skin".
5
-There are four files in each subdirectory for the CSS, the "details"
6
-file, the footer, and the header for that skin.
5
+There are five key files in each subdirectory:
6
+
7
+ * `css.txt` → The CSS for the skin
8
+ * `details.txt` → Skin-specific settings
9
+ * `footer.txt` → Text of the Content Footer for each page
10
+ * `header.txt` → Text of the Content Header for each page
11
+ * `js.txt` → Javascript included in the Content Footer
712
813
To improve an existing built-in skin, simply edit the appropriate
914
files and recompile.
1015
1116
To add a new skin:
@@ -13,12 +18,12 @@
1318
1. Create a new subdirectory under skins/. (The new directory is
1419
called "skins/newskin" below but you should use a new original
1520
name, of course.)
1621
1722
2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
18
- skins/newskin/footer.txt and skins/newskin/header.txt.
19
- Be sure to "fossil add" these files.
23
+ skins/newskin/footer.txt, skins/newskin/header.txt, and
24
+ skins/newskin/js.txt. Be sure to "fossil add" these files.
2025
2126
3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
2227
step rebuilds the various makefiles so that they have dependencies
2328
on the skin files you just installed.
2429
@@ -25,21 +30,23 @@
2530
4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
2631
file so that it describes and references the "newskin" skin.
2732
2833
5. Type "make" to rebuild.
2934
35
+See the [custom skin documentation](../www/customskin.md) for more information.
36
+
3037
Development Hints
3138
-----------------
3239
3340
One way to develop a new skin is to copy the baseline files (css.txt,
34
-details.txt, footer.txt, and header.txt) into a working directory $WORKDIR
35
-then launch Fossil with a command-line option "--skin $WORKDIR". Example:
41
+details.txt, footer.txt, header.txt, and js.txt) into a working
42
+directory $WORKDIR then launch Fossil with a command-line option
43
+"--skin $WORKDIR". Example:
3644
3745
cp -r skins/default newskin
3846
fossil ui --skin ./newskin
3947
4048
When the argument to --skin contains one or more '/' characters, the
4149
appropriate skin files are read from disk from the directory specified.
42
-So after launching fossil as shown above, you can edit the newskin/css.txt,
43
-newskin/details.txt, newskin/footer.txt, and newskin/header.txt files using
44
-your favorite text editor, then press Reload on your browser to see
45
-immediate results.
50
+So after launching fossil as shown above, you can edit the newskin/*.txt
51
+files using your favorite text editor, then press Reload on your browser
52
+to see immediate results.
4653
--- skins/README.md
+++ skins/README.md
@@ -1,11 +1,16 @@
1 Built-in Skins
2 ==============
3
4 Each subdirectory under this folder describes a built-in "skin".
5 There are four files in each subdirectory for the CSS, the "details"
6 file, the footer, and the header for that skin.
 
 
 
 
 
7
8 To improve an existing built-in skin, simply edit the appropriate
9 files and recompile.
10
11 To add a new skin:
@@ -13,12 +18,12 @@
13 1. Create a new subdirectory under skins/. (The new directory is
14 called "skins/newskin" below but you should use a new original
15 name, of course.)
16
17 2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
18 skins/newskin/footer.txt and skins/newskin/header.txt.
19 Be sure to "fossil add" these files.
20
21 3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
22 step rebuilds the various makefiles so that they have dependencies
23 on the skin files you just installed.
24
@@ -25,21 +30,23 @@
25 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
26 file so that it describes and references the "newskin" skin.
27
28 5. Type "make" to rebuild.
29
 
 
30 Development Hints
31 -----------------
32
33 One way to develop a new skin is to copy the baseline files (css.txt,
34 details.txt, footer.txt, and header.txt) into a working directory $WORKDIR
35 then launch Fossil with a command-line option "--skin $WORKDIR". Example:
 
36
37 cp -r skins/default newskin
38 fossil ui --skin ./newskin
39
40 When the argument to --skin contains one or more '/' characters, the
41 appropriate skin files are read from disk from the directory specified.
42 So after launching fossil as shown above, you can edit the newskin/css.txt,
43 newskin/details.txt, newskin/footer.txt, and newskin/header.txt files using
44 your favorite text editor, then press Reload on your browser to see
45 immediate results.
46
--- skins/README.md
+++ skins/README.md
@@ -1,11 +1,16 @@
1 Built-in Skins
2 ==============
3
4 Each subdirectory under this folder describes a built-in "skin".
5 There are five key files in each subdirectory:
6
7 * `css.txt` → The CSS for the skin
8 * `details.txt` → Skin-specific settings
9 * `footer.txt` → Text of the Content Footer for each page
10 * `header.txt` → Text of the Content Header for each page
11 * `js.txt` → Javascript included in the Content Footer
12
13 To improve an existing built-in skin, simply edit the appropriate
14 files and recompile.
15
16 To add a new skin:
@@ -13,12 +18,12 @@
18 1. Create a new subdirectory under skins/. (The new directory is
19 called "skins/newskin" below but you should use a new original
20 name, of course.)
21
22 2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
23 skins/newskin/footer.txt, skins/newskin/header.txt, and
24 skins/newskin/js.txt. Be sure to "fossil add" these files.
25
26 3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
27 step rebuilds the various makefiles so that they have dependencies
28 on the skin files you just installed.
29
@@ -25,21 +30,23 @@
30 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
31 file so that it describes and references the "newskin" skin.
32
33 5. Type "make" to rebuild.
34
35 See the [custom skin documentation](../www/customskin.md) for more information.
36
37 Development Hints
38 -----------------
39
40 One way to develop a new skin is to copy the baseline files (css.txt,
41 details.txt, footer.txt, header.txt, and js.txt) into a working
42 directory $WORKDIR then launch Fossil with a command-line option
43 "--skin $WORKDIR". Example:
44
45 cp -r skins/default newskin
46 fossil ui --skin ./newskin
47
48 When the argument to --skin contains one or more '/' characters, the
49 appropriate skin files are read from disk from the directory specified.
50 So after launching fossil as shown above, you can edit the newskin/*.txt
51 files using your favorite text editor, then press Reload on your browser
52 to see immediate results.
 
53
--- skins/ardoise/css.txt
+++ skins/ardoise/css.txt
@@ -291,11 +291,11 @@
291291
box-sizing: border-box
292292
}
293293
.column,
294294
.columns {
295295
width: 100%;
296
- float: left;
296
+ /*float: left; can break README.md in /dir view*/
297297
box-sizing: border-box
298298
}
299299
@media (min-width:400px) {
300300
.container {
301301
width: 95%;
@@ -802,10 +802,16 @@
802802
background-color: #333
803803
}
804804
td.browser,
805805
td.tktDescLabel {
806806
vertical-align: top
807
+}
808
+td.tktTlOpen {
809
+ color: #ffa0a0;
810
+}
811
+td.tktTlClosed {
812
+ color: #555;
807813
}
808814
div.filetreeline {
809815
display: table;
810816
width: 100%;
811817
white-space: nowrap
@@ -947,10 +953,38 @@
947953
border-radius: 5px;
948954
border: solid #ff8000;
949955
vertical-align: top;
950956
text-align: left;
951957
background: #442800
958
+}
959
+span.timelineSelected {
960
+ border-radius: 5px;
961
+ border: solid #ff8000;
962
+ vertical-align: top;
963
+ text-align: left;
964
+ background: #442800
965
+}
966
+.timelineSelected {}
967
+.timelineSecondary {}
968
+.timelineSecondary > .timelineColumnarCell,
969
+.timelineSecondary > .timelineCompactCell,
970
+.timelineSecondary > .timelineDetailCell,
971
+.timelineSecondary > .timelineModernCell,
972
+.timelineSecondary > .timelineVerboseCell {
973
+ padding: .75em;
974
+ border-radius: 5px;
975
+ border: solid #0080ff;
976
+ vertical-align: top;
977
+ text-align: left;
978
+ background: #002844
979
+}
980
+span.timelineSecondary {
981
+ border-radius: 5px;
982
+ border: solid #0080ff;
983
+ vertical-align: top;
984
+ text-align: left;
985
+ background: #002844
952986
}
953987
.timelineCurrent > .timelineColumnarCell,
954988
.timelineCurrent > .timelineCompactCell,
955989
.timelineCurrent > .timelineDetailCell,
956990
.timelineCurrent > .timelineModernCell,
957991
--- skins/ardoise/css.txt
+++ skins/ardoise/css.txt
@@ -291,11 +291,11 @@
291 box-sizing: border-box
292 }
293 .column,
294 .columns {
295 width: 100%;
296 float: left;
297 box-sizing: border-box
298 }
299 @media (min-width:400px) {
300 .container {
301 width: 95%;
@@ -802,10 +802,16 @@
802 background-color: #333
803 }
804 td.browser,
805 td.tktDescLabel {
806 vertical-align: top
 
 
 
 
 
 
807 }
808 div.filetreeline {
809 display: table;
810 width: 100%;
811 white-space: nowrap
@@ -947,10 +953,38 @@
947 border-radius: 5px;
948 border: solid #ff8000;
949 vertical-align: top;
950 text-align: left;
951 background: #442800
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952 }
953 .timelineCurrent > .timelineColumnarCell,
954 .timelineCurrent > .timelineCompactCell,
955 .timelineCurrent > .timelineDetailCell,
956 .timelineCurrent > .timelineModernCell,
957
--- skins/ardoise/css.txt
+++ skins/ardoise/css.txt
@@ -291,11 +291,11 @@
291 box-sizing: border-box
292 }
293 .column,
294 .columns {
295 width: 100%;
296 /*float: left; can break README.md in /dir view*/
297 box-sizing: border-box
298 }
299 @media (min-width:400px) {
300 .container {
301 width: 95%;
@@ -802,10 +802,16 @@
802 background-color: #333
803 }
804 td.browser,
805 td.tktDescLabel {
806 vertical-align: top
807 }
808 td.tktTlOpen {
809 color: #ffa0a0;
810 }
811 td.tktTlClosed {
812 color: #555;
813 }
814 div.filetreeline {
815 display: table;
816 width: 100%;
817 white-space: nowrap
@@ -947,10 +953,38 @@
953 border-radius: 5px;
954 border: solid #ff8000;
955 vertical-align: top;
956 text-align: left;
957 background: #442800
958 }
959 span.timelineSelected {
960 border-radius: 5px;
961 border: solid #ff8000;
962 vertical-align: top;
963 text-align: left;
964 background: #442800
965 }
966 .timelineSelected {}
967 .timelineSecondary {}
968 .timelineSecondary > .timelineColumnarCell,
969 .timelineSecondary > .timelineCompactCell,
970 .timelineSecondary > .timelineDetailCell,
971 .timelineSecondary > .timelineModernCell,
972 .timelineSecondary > .timelineVerboseCell {
973 padding: .75em;
974 border-radius: 5px;
975 border: solid #0080ff;
976 vertical-align: top;
977 text-align: left;
978 background: #002844
979 }
980 span.timelineSecondary {
981 border-radius: 5px;
982 border: solid #0080ff;
983 vertical-align: top;
984 text-align: left;
985 background: #002844
986 }
987 .timelineCurrent > .timelineColumnarCell,
988 .timelineCurrent > .timelineCompactCell,
989 .timelineCurrent > .timelineDetailCell,
990 .timelineCurrent > .timelineModernCell,
991
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -1060,16 +1060,19 @@
10601060
background-color: #ffc;
10611061
border-bottom: 1px solid #ddd;
10621062
border-right: 1px solid #ddd;
10631063
}
10641064
1065
-tr.timelineSelected {
1065
+.timelineSelected {
10661066
border-left: 2px solid orange;
10671067
background-color: #ffffe8;
10681068
border-bottom: 1px solid #ddd;
10691069
border-right: 1px solid #ddd;
10701070
}
1071
+.timelineSecondary {
1072
+ background-color: #e8ffff;
1073
+}
10711074
10721075
tr.timelineCurrent td.timelineTableCell {
10731076
}
10741077
10751078
tr.timelineBottom td {
10761079
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -1060,16 +1060,19 @@
1060 background-color: #ffc;
1061 border-bottom: 1px solid #ddd;
1062 border-right: 1px solid #ddd;
1063 }
1064
1065 tr.timelineSelected {
1066 border-left: 2px solid orange;
1067 background-color: #ffffe8;
1068 border-bottom: 1px solid #ddd;
1069 border-right: 1px solid #ddd;
1070 }
 
 
 
1071
1072 tr.timelineCurrent td.timelineTableCell {
1073 }
1074
1075 tr.timelineBottom td {
1076
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -1060,16 +1060,19 @@
1060 background-color: #ffc;
1061 border-bottom: 1px solid #ddd;
1062 border-right: 1px solid #ddd;
1063 }
1064
1065 .timelineSelected {
1066 border-left: 2px solid orange;
1067 background-color: #ffffe8;
1068 border-bottom: 1px solid #ddd;
1069 border-right: 1px solid #ddd;
1070 }
1071 .timelineSecondary {
1072 background-color: #e8ffff;
1073 }
1074
1075 tr.timelineCurrent td.timelineTableCell {
1076 }
1077
1078 tr.timelineBottom td {
1079
--- skins/blitz/ticket.txt
+++ skins/blitz/ticket.txt
@@ -1,8 +1,8 @@
11
<h4>$<title></h4>
22
<table class="tktDsp">
3
-<tr><td class="tktDspLabel">Ticket&nbsp;UUID</td>
3
+<tr><td class="tktDspLabel">Ticket&nbsp;Hash</td>
44
<th1>
55
if {[info exists tkt_uuid]} {
66
if {[hascap s]} {
77
html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
88
html "($tkt_id)</td></tr>\n"
99
--- skins/blitz/ticket.txt
+++ skins/blitz/ticket.txt
@@ -1,8 +1,8 @@
1 <h4>$<title></h4>
2 <table class="tktDsp">
3 <tr><td class="tktDspLabel">Ticket&nbsp;UUID</td>
4 <th1>
5 if {[info exists tkt_uuid]} {
6 if {[hascap s]} {
7 html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
8 html "($tkt_id)</td></tr>\n"
9
--- skins/blitz/ticket.txt
+++ skins/blitz/ticket.txt
@@ -1,8 +1,8 @@
1 <h4>$<title></h4>
2 <table class="tktDsp">
3 <tr><td class="tktDspLabel">Ticket&nbsp;Hash</td>
4 <th1>
5 if {[info exists tkt_uuid]} {
6 if {[hascap s]} {
7 html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
8 html "($tkt_id)</td></tr>\n"
9
--- skins/blitz_no_logo/ticket.txt
+++ skins/blitz_no_logo/ticket.txt
@@ -1,8 +1,8 @@
11
<h4>$<title></h4>
22
<table class="tktDsp">
3
-<tr><td class="tktDspLabel">Ticket&nbsp;UUID</td>
3
+<tr><td class="tktDspLabel">Ticket&nbsp;Hash</td>
44
<th1>
55
if {[info exists tkt_uuid]} {
66
if {[hascap s]} {
77
html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
88
html "($tkt_id)</td></tr>\n"
99
--- skins/blitz_no_logo/ticket.txt
+++ skins/blitz_no_logo/ticket.txt
@@ -1,8 +1,8 @@
1 <h4>$<title></h4>
2 <table class="tktDsp">
3 <tr><td class="tktDspLabel">Ticket&nbsp;UUID</td>
4 <th1>
5 if {[info exists tkt_uuid]} {
6 if {[hascap s]} {
7 html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
8 html "($tkt_id)</td></tr>\n"
9
--- skins/blitz_no_logo/ticket.txt
+++ skins/blitz_no_logo/ticket.txt
@@ -1,8 +1,8 @@
1 <h4>$<title></h4>
2 <table class="tktDsp">
3 <tr><td class="tktDspLabel">Ticket&nbsp;Hash</td>
4 <th1>
5 if {[info exists tkt_uuid]} {
6 if {[hascap s]} {
7 html "<td class='tktDspValue' colspan='3'>$tkt_uuid "
8 html "($tkt_id)</td></tr>\n"
9
--- skins/bootstrap/header.txt
+++ skins/bootstrap/header.txt
@@ -2,13 +2,13 @@
22
<head>
33
<meta charset="utf-8">
44
<base href="$baseurl/$current_page" />
55
<title>$<project_name>: $<title></title>
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <meta http-equiv="Content-Security-Policy" content="default-src 'self' data:; script-src 'self' 'nonce-$<nonce>'; style-src 'self' 'unsafe-inline'"/>
7
+ <meta http-equiv="Content-Security-Policy" content="$default_csp"/>
88
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="$home/timeline.rss" />
9
- <link rel="stylesheet" href="$home/style.css?default" type="text/css" media="screen" />
9
+ <link rel="stylesheet" href="$stylesheet_url" type="text/css" media="screen" />
1010
<script nonce="$<nonce>">
1111
function gebi(x){
1212
if(/^#/.test(x)) x = x.substr(1);
1313
var e = document.getElementById(x);
1414
if(!e) throw new Error("Expecting element with ID "+x);
1515
--- skins/bootstrap/header.txt
+++ skins/bootstrap/header.txt
@@ -2,13 +2,13 @@
2 <head>
3 <meta charset="utf-8">
4 <base href="$baseurl/$current_page" />
5 <title>$<project_name>: $<title></title>
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <meta http-equiv="Content-Security-Policy" content="default-src 'self' data:; script-src 'self' 'nonce-$<nonce>'; style-src 'self' 'unsafe-inline'"/>
8 <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="$home/timeline.rss" />
9 <link rel="stylesheet" href="$home/style.css?default" type="text/css" media="screen" />
10 <script nonce="$<nonce>">
11 function gebi(x){
12 if(/^#/.test(x)) x = x.substr(1);
13 var e = document.getElementById(x);
14 if(!e) throw new Error("Expecting element with ID "+x);
15
--- skins/bootstrap/header.txt
+++ skins/bootstrap/header.txt
@@ -2,13 +2,13 @@
2 <head>
3 <meta charset="utf-8">
4 <base href="$baseurl/$current_page" />
5 <title>$<project_name>: $<title></title>
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <meta http-equiv="Content-Security-Policy" content="$default_csp"/>
8 <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="$home/timeline.rss" />
9 <link rel="stylesheet" href="$stylesheet_url" type="text/css" media="screen" />
10 <script nonce="$<nonce>">
11 function gebi(x){
12 if(/^#/.test(x)) x = x.substr(1);
13 var e = document.getElementById(x);
14 if(!e) throw new Error("Expecting element with ID "+x);
15
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145145
border-radius: 5px;
146146
}
147147
.content blockquote {
148148
padding: 0 15px;
149149
}
150
-div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote {
150
+div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote, div.forumTime blockquote, div.forumTimeline blockquote {
151151
background-color: rgba(65, 131, 196, 0.1);
152152
border-left: 3px solid #254769;
153153
padding: .1em 1em;
154154
}
155155
156156
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145 border-radius: 5px;
146 }
147 .content blockquote {
148 padding: 0 15px;
149 }
150 div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote {
151 background-color: rgba(65, 131, 196, 0.1);
152 border-left: 3px solid #254769;
153 padding: .1em 1em;
154 }
155
156
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145 border-radius: 5px;
146 }
147 .content blockquote {
148 padding: 0 15px;
149 }
150 div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote, div.forumTime blockquote, div.forumTimeline blockquote {
151 background-color: rgba(65, 131, 196, 0.1);
152 border-left: 3px solid #254769;
153 padding: .1em 1em;
154 }
155
156
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -1,10 +1,10 @@
11
<div class="header">
22
<div class="title"><h1>$<project_name></h1>$<title></div>
33
<div class="status"><th1>
44
if {[info exists login]} {
5
- html "$login — <a href='$home/login'>Logout</a>\n"
5
+ html "<a href='$home/login'>$login</a>\n"
66
} else {
77
html "<a href='$home/login'>Login</a>\n"
88
}
99
</th1></div>
1010
</div>
@@ -17,17 +17,18 @@
1717
html "<a href='$home$url' class='active $cls'>$name</a>\n"
1818
} else {
1919
html "<a href='$home$url' class='$cls'>$name</a>\n"
2020
}
2121
}
22
-html "<a id='hbbtn' href='#'>&#9776;</a>"
22
+html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>&#9776;</a>"
2323
menulink $index_page Home {}
2424
if {[anycap jor]} {
2525
menulink /timeline Timeline {}
2626
}
2727
if {[hascap oh]} {
28
- menulink /dir?ci=tip Files desktoponly
28
+ if {![info exists current_checkin]} {set current_checkin tip}
29
+ menulink /dir?ci=$current_checkin Files desktoponly
2930
}
3031
if {[hascap o]} {
3132
menulink /brlist Branches desktoponly
3233
menulink /taglist Tags wideonly
3334
}
3435
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -1,10 +1,10 @@
1 <div class="header">
2 <div class="title"><h1>$<project_name></h1>$<title></div>
3 <div class="status"><th1>
4 if {[info exists login]} {
5 html "$login — <a href='$home/login'>Logout</a>\n"
6 } else {
7 html "<a href='$home/login'>Login</a>\n"
8 }
9 </th1></div>
10 </div>
@@ -17,17 +17,18 @@
17 html "<a href='$home$url' class='active $cls'>$name</a>\n"
18 } else {
19 html "<a href='$home$url' class='$cls'>$name</a>\n"
20 }
21 }
22 html "<a id='hbbtn' href='#'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28 menulink /dir?ci=tip Files desktoponly
 
29 }
30 if {[hascap o]} {
31 menulink /brlist Branches desktoponly
32 menulink /taglist Tags wideonly
33 }
34
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -1,10 +1,10 @@
1 <div class="header">
2 <div class="title"><h1>$<project_name></h1>$<title></div>
3 <div class="status"><th1>
4 if {[info exists login]} {
5 html "<a href='$home/login'>$login</a>\n"
6 } else {
7 html "<a href='$home/login'>Login</a>\n"
8 }
9 </th1></div>
10 </div>
@@ -17,17 +17,18 @@
17 html "<a href='$home$url' class='active $cls'>$name</a>\n"
18 } else {
19 html "<a href='$home$url' class='$cls'>$name</a>\n"
20 }
21 }
22 html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28 if {![info exists current_checkin]} {set current_checkin tip}
29 menulink /dir?ci=$current_checkin Files desktoponly
30 }
31 if {[hascap o]} {
32 menulink /brlist Branches desktoponly
33 menulink /taglist Tags wideonly
34 }
35
--- skins/eagle/css.txt
+++ skins/eagle/css.txt
@@ -168,13 +168,16 @@
168168
font-family: "courier new";
169169
border-spacing: 0px 2px;
170170
// border-collapse: collapse;
171171
}
172172
173
-tr.timelineSelected {
173
+.timelineSelected {
174174
background-color: #7EA2D9;
175175
}
176
+.timelineSecondary {
177
+ background-color: #7EA27E;
178
+}
176179
177180
/* commit node */
178181
.tl-node {
179182
width: 10px;
180183
height: 10px;
@@ -285,10 +288,18 @@
285288
td.tktDspValue {
286289
text-align: left;
287290
vertical-align: top;
288291
background-color: #485D7B;
289292
}
293
+
294
+/* Ticket display on timelines */
295
+td.tktTlOpen {
296
+ color: #ffc0c0;
297
+}
298
+td.tktTlClose {
299
+ color: #c0c0c0;
300
+}
290301
291302
/* format for example table cells on the report edit page */
292303
td.rpteditex {
293304
border-width: thin;
294305
border-color: white;
295306
--- skins/eagle/css.txt
+++ skins/eagle/css.txt
@@ -168,13 +168,16 @@
168 font-family: "courier new";
169 border-spacing: 0px 2px;
170 // border-collapse: collapse;
171 }
172
173 tr.timelineSelected {
174 background-color: #7EA2D9;
175 }
 
 
 
176
177 /* commit node */
178 .tl-node {
179 width: 10px;
180 height: 10px;
@@ -285,10 +288,18 @@
285 td.tktDspValue {
286 text-align: left;
287 vertical-align: top;
288 background-color: #485D7B;
289 }
 
 
 
 
 
 
 
 
290
291 /* format for example table cells on the report edit page */
292 td.rpteditex {
293 border-width: thin;
294 border-color: white;
295
--- skins/eagle/css.txt
+++ skins/eagle/css.txt
@@ -168,13 +168,16 @@
168 font-family: "courier new";
169 border-spacing: 0px 2px;
170 // border-collapse: collapse;
171 }
172
173 .timelineSelected {
174 background-color: #7EA2D9;
175 }
176 .timelineSecondary {
177 background-color: #7EA27E;
178 }
179
180 /* commit node */
181 .tl-node {
182 width: 10px;
183 height: 10px;
@@ -285,10 +288,18 @@
288 td.tktDspValue {
289 text-align: left;
290 vertical-align: top;
291 background-color: #485D7B;
292 }
293
294 /* Ticket display on timelines */
295 td.tktTlOpen {
296 color: #ffc0c0;
297 }
298 td.tktTlClose {
299 color: #c0c0c0;
300 }
301
302 /* format for example table cells on the report edit page */
303 td.rpteditex {
304 border-width: thin;
305 border-color: white;
306
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -695,10 +695,15 @@
695695
td.tktDspValue {
696696
background-color: #111;
697697
text-align: left;
698698
vertical-align: top;
699699
}
700
+
701
+/* Tickets on timelines */
702
+td.tktTlOpen {
703
+ color: #ffa0a0;
704
+}
700705
701706
/* format for ticket error messages */
702707
span.tktError {
703708
color: #f00;
704709
font-weight: bold;
@@ -803,10 +808,12 @@
803808
.timelineTable .timelineSelected {
804809
background: #222;
805810
border: 0;
806811
box-shadow: none;
807812
}
813
+.timelineSelected {}
814
+.timelineSecondary {}
808815
809816
.timelineTable .timelineSelected .timelineTime {
810817
background: #333;
811818
border-radius: 1rem 0 0 1rem;
812819
box-shadow: 2px 2px 1px #000;
@@ -823,10 +830,17 @@
823830
.timelineTable .timelineSelected .timelineDetailCell {
824831
background: #333;
825832
border-radius: 0 1rem 1rem 0;
826833
box-shadow: 2px 2px 1px #000;
827834
}
835
+
836
+span.timelineSelected {
837
+ padding: 0 1em 0 1em;
838
+ border-radius: 1rem;
839
+ background: #333;
840
+ box-shadow: 2px 2px 1px #000;
841
+}
828842
829843
.timelineTable .timelineModernCell .timelineModernComment ,
830844
.timelineTable .timelineModernCell .timelineModernDetail ,
831845
.timelineTable .timelineCompactCell .timelineCompactComment ,
832846
.timelineTable .timelineCompactCell .timelineCompactDetail ,
833847
834848
ADDED src/accordion.js
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -695,10 +695,15 @@
695 td.tktDspValue {
696 background-color: #111;
697 text-align: left;
698 vertical-align: top;
699 }
 
 
 
 
 
700
701 /* format for ticket error messages */
702 span.tktError {
703 color: #f00;
704 font-weight: bold;
@@ -803,10 +808,12 @@
803 .timelineTable .timelineSelected {
804 background: #222;
805 border: 0;
806 box-shadow: none;
807 }
 
 
808
809 .timelineTable .timelineSelected .timelineTime {
810 background: #333;
811 border-radius: 1rem 0 0 1rem;
812 box-shadow: 2px 2px 1px #000;
@@ -823,10 +830,17 @@
823 .timelineTable .timelineSelected .timelineDetailCell {
824 background: #333;
825 border-radius: 0 1rem 1rem 0;
826 box-shadow: 2px 2px 1px #000;
827 }
 
 
 
 
 
 
 
828
829 .timelineTable .timelineModernCell .timelineModernComment ,
830 .timelineTable .timelineModernCell .timelineModernDetail ,
831 .timelineTable .timelineCompactCell .timelineCompactComment ,
832 .timelineTable .timelineCompactCell .timelineCompactDetail ,
833
834 DDED src/accordion.js
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -695,10 +695,15 @@
695 td.tktDspValue {
696 background-color: #111;
697 text-align: left;
698 vertical-align: top;
699 }
700
701 /* Tickets on timelines */
702 td.tktTlOpen {
703 color: #ffa0a0;
704 }
705
706 /* format for ticket error messages */
707 span.tktError {
708 color: #f00;
709 font-weight: bold;
@@ -803,10 +808,12 @@
808 .timelineTable .timelineSelected {
809 background: #222;
810 border: 0;
811 box-shadow: none;
812 }
813 .timelineSelected {}
814 .timelineSecondary {}
815
816 .timelineTable .timelineSelected .timelineTime {
817 background: #333;
818 border-radius: 1rem 0 0 1rem;
819 box-shadow: 2px 2px 1px #000;
@@ -823,10 +830,17 @@
830 .timelineTable .timelineSelected .timelineDetailCell {
831 background: #333;
832 border-radius: 0 1rem 1rem 0;
833 box-shadow: 2px 2px 1px #000;
834 }
835
836 span.timelineSelected {
837 padding: 0 1em 0 1em;
838 border-radius: 1rem;
839 background: #333;
840 box-shadow: 2px 2px 1px #000;
841 }
842
843 .timelineTable .timelineModernCell .timelineModernComment ,
844 .timelineTable .timelineModernCell .timelineModernDetail ,
845 .timelineTable .timelineCompactCell .timelineCompactComment ,
846 .timelineTable .timelineCompactCell .timelineCompactDetail ,
847
848 DDED src/accordion.js
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
1
+/* Attach appropriate javascript to each ".accordion" button so that
2
+** it expands and contracts when clicked.
3
+**ntracts when clicked.
4
+**
5
+** The uncompressed source code for the SV
6
+** wiki page "branch/accordion-experiments" window to a smaller
7
+** width, causing vertical growth.)
8
+**
9
+** Ano"+"/"+"ttach appropriate javascript to each ".accordion" button so that
10
+** it expands and contracts when clicked.
11
+**ntracts when clicked.
12
+**
13
+** The uncompressed source code for the SV
14
+** wiki page "branch/accordion-experiments" window to a smaller
15
+** width, causing vertical growth.)
16
+**
17
+** Another problem is that `scrollHeight' used to calculate the expanded height
18
+** while still in the contracted state may return values with small errors on
19
+** some browsers, especially for large elements, presumably due to omitting the
20
+** space required by the vertical scrollbar that may become necessary, causing
21
+** additional horizontal shrinking and consequently more vertical growth than
22
+** calculated. That's why se/* Attach appropriate javascript to each ".accordion" button so that
23
+** it expands and contracts when clicked.
24
+**ntracts when clicked.
25
+**
26
+** The uncompressed source code for the SV
27
+** wiki page "branch/accordion-experiments" window to a smvar p = a[i].nextElementSibli
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
1 /* Attach appropriate javascript to each ".accordion" button so that
2 ** it expands and contracts when clicked.
3 **ntracts when clicked.
4 **
5 ** The uncompressed source code for the SV
6 ** wiki page "branch/accordion-experiments" window to a smaller
7 ** width, causing vertical growth.)
8 **
9 ** Ano"+"/"+"ttach appropriate javascript to each ".accordion" button so that
10 ** it expands and contracts when clicked.
11 **ntracts when clicked.
12 **
13 ** The uncompressed source code for the SV
14 ** wiki page "branch/accordion-experiments" window to a smaller
15 ** width, causing vertical growth.)
16 **
17 ** Another problem is that `scrollHeight' used to calculate the expanded height
18 ** while still in the contracted state may return values with small errors on
19 ** some browsers, especially for large elements, presumably due to omitting the
20 ** space required by the vertical scrollbar that may become necessary, causing
21 ** additional horizontal shrinking and consequently more vertical growth than
22 ** calculated. That's why se/* Attach appropriate javascript to each ".accordion" button so that
23 ** it expands and contracts when clicked.
24 **ntracts when clicked.
25 **
26 ** The uncompressed source code for the SV
27 ** wiki page "branch/accordion-experiments" window to a smvar p = a[i].nextElementSibli
+163 -18
--- src/add.c
+++ src/add.c
@@ -240,10 +240,101 @@
240240
}
241241
db_finalize(&loop);
242242
blob_reset(&repoName);
243243
return nAdd;
244244
}
245
+
246
+/*
247
+** Resets the ADDED/DELETED state of a checkout, such that all
248
+** newly-added (but not yet committed) files are no longer added and
249
+** newly-removed (but not yet committed) files are no longer
250
+** removed. If bIsAdd is true, it operates on the "add" state, else it
251
+** operates on the "rm" state.
252
+**
253
+** If bDryRun is true it outputs what it would have done, but does not
254
+** actually do it. In this case it rolls back the transaction it
255
+** starts (so don't start a transaction before calling this).
256
+**
257
+** If bVerbose is true it outputs the name of each reset entry.
258
+**
259
+** This is intended to be called only in the context of the
260
+** add/rm/addremove commands, after a call to verify_all_options().
261
+**
262
+** Un-added files are not modified but any un-rm'd files which are
263
+** missing from the checkout are restored from the repo. un-rm'd files
264
+** which exist in the checkout are left as-is, rather than restoring
265
+** them using vfile_to_disk(), to avoid overwriting any local changes
266
+** made to those files.
267
+*/
268
+static void addremove_reset(int bIsAdd, int bDryRun, int bVerbose){
269
+ int nReset = 0; /* # of entries which get reset */
270
+ Stmt stmt; /* vfile loop query */
271
+
272
+ db_begin_transaction();
273
+ db_prepare(&stmt, "SELECT id, pathname FROM vfile "
274
+ "WHERE %s ORDER BY pathname",
275
+ bIsAdd==0 ? "deleted<>0" : "rid=0"/*safe-for-%s*/);
276
+ while( db_step(&stmt)==SQLITE_ROW ){
277
+ /* This loop exists only so we can restore the contents of un-rm'd
278
+ ** files and support verbose mode. All manipulation of vfile's
279
+ ** contents happens after the loop. For the ADD case in non-verbose
280
+ ** mode we "could" skip this loop entirely.
281
+ */
282
+ int const id = db_column_int(&stmt, 0);
283
+ char const * zPathname = db_column_text(&stmt, 1);
284
+ Blob relName = empty_blob;
285
+ if(bIsAdd==0 || bVerbose!=0){
286
+ /* Make filename relative... */
287
+ char *zFullName = mprintf("%s%s", g.zLocalRoot, zPathname);
288
+ file_relative_name(zFullName, &relName, 0);
289
+ fossil_free(zFullName);
290
+ }
291
+ if(bIsAdd==0){
292
+ /* Restore contents of missing un-rm'd files. We don't do this
293
+ ** unconditionally because we might cause data loss if a file
294
+ ** is modified, rm'd, then un-rm'd.
295
+ */
296
+ ++nReset;
297
+ if(!file_isfile_or_link(blob_str(&relName))){
298
+ if(bDryRun==0){
299
+ vfile_to_disk(0, id, 0, 0);
300
+ if(bVerbose){
301
+ fossil_print("Restored missing file: %b\n", &relName);
302
+ }
303
+ }else{
304
+ fossil_print("Dry-run: not restoring missing file: %b\n", &relName);
305
+ }
306
+ }
307
+ if(bVerbose){
308
+ fossil_print("Un-removed: %b\n", &relName);
309
+ }
310
+ }else{
311
+ /* un-add... */
312
+ ++nReset;
313
+ if(bVerbose){
314
+ fossil_print("Un-added: %b\n", &relName);
315
+ }
316
+ }
317
+ blob_reset(&relName);
318
+ }
319
+ db_finalize(&stmt);
320
+ if(nReset>0){
321
+ if(bIsAdd==0){
322
+ if(bDryRun==0){
323
+ db_exec_sql("UPDATE vfile SET deleted=0 WHERE deleted<>0");
324
+ }
325
+ fossil_print("Un-removed %d file(s).\n", nReset);
326
+ }else{
327
+ if(bDryRun==0){
328
+ db_exec_sql("DELETE FROM vfile WHERE rid=0");
329
+ }
330
+ fossil_print("Un-added %d file(s).\n", nReset);
331
+ }
332
+ }
333
+ db_end_transaction(bDryRun ? 1 : 0);
334
+}
335
+
245336
246337
/*
247338
** COMMAND: add
248339
**
249340
** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...?
@@ -269,17 +360,26 @@
269360
** be treated case sensitive or not. If the option is not given, the default
270361
** depends on the global setting, or the operating system default, if not set.
271362
**
272363
** Options:
273364
**
274
-** --case-sensitive <BOOL> Override the case-sensitive setting.
365
+** --case-sensitive BOOL Override the case-sensitive setting.
275366
** --dotfiles include files beginning with a dot (".")
276367
** -f|--force Add files without prompting
277
-** --ignore <CSG> Ignore unmanaged files matching patterns from
278
-** the comma separated list of glob patterns.
279
-** --clean <CSG> Also ignore files matching patterns from
280
-** the comma separated list of glob patterns.
368
+** --ignore CSG Ignore unmanaged files matching patterns from
369
+** the Comma Separated Glob (CSG) pattern list
370
+** --clean CSG Also ignore files matching patterns from
371
+** the Comma Separated Glob (CSG) list
372
+** --reset Reset the ADDED state of a checkout, such
373
+** that all newly-added (but not yet committed)
374
+** files are no longer added. No flags other
375
+** than --verbose and --dry-run may be used
376
+** with --reset.
377
+**
378
+** The following options are only valid with --reset:
379
+** -v|--verbose Outputs information about each --reset file.
380
+** -n|--dry-run Display instead of run actions.
281381
**
282382
** See also: addremove, rm
283383
*/
284384
void add_cmd(void){
285385
int i; /* Loop counter */
@@ -288,10 +388,19 @@
288388
const char *zCleanFlag; /* The --clean option or clean-glob setting */
289389
const char *zIgnoreFlag; /* The --ignore option or ignore-glob setting */
290390
Glob *pIgnore, *pClean; /* Ignore everything matching the glob patterns */
291391
unsigned scanFlags = 0; /* Flags passed to vfile_scan() */
292392
int forceFlag;
393
+
394
+ if(0!=find_option("reset",0,0)){
395
+ int const verboseFlag = find_option("verbose","v",0)!=0;
396
+ int const dryRunFlag = find_option("dry-run","n",0)!=0;
397
+ db_must_be_within_tree();
398
+ verify_all_options();
399
+ addremove_reset(1, dryRunFlag, verboseFlag);
400
+ return;
401
+ }
293402
294403
zCleanFlag = find_option("clean",0,1);
295404
zIgnoreFlag = find_option("ignore",0,1);
296405
forceFlag = find_option("force","f",0)!=0;
297406
if( find_option("dotfiles",0,0)!=0 ) scanFlags |= SCAN_ALL;
@@ -339,10 +448,11 @@
339448
Blob ans;
340449
char cReply;
341450
char *prompt = mprintf("file \"%s\" matches \"ignore-glob\". "
342451
"Add it (a=all/y/N)? ", zTreeName);
343452
prompt_user(prompt, &ans);
453
+ fossil_free(prompt);
344454
cReply = blob_str(&ans)[0];
345455
blob_reset(&ans);
346456
if( cReply=='a' || cReply=='A' ){
347457
forceFlag = 1;
348458
}else if( cReply!='y' && cReply!='Y' ){
@@ -441,22 +551,36 @@
441551
** --soft Skip removing files from the checkout.
442552
** This supersedes the --hard option.
443553
** --hard Remove files from the checkout.
444554
** --case-sensitive <BOOL> Override the case-sensitive setting.
445555
** -n|--dry-run If given, display instead of run actions.
556
+** --reset Reset the DELETED state of a checkout, such
557
+** that all newly-rm'd (but not yet committed)
558
+** files are no longer removed. No flags other
559
+** than --verbose or --dry-run may be used with
560
+** --reset.
561
+** --verbose|-v Outputs information about each --reset file.
562
+** Only usable with --reset.
446563
**
447564
** See also: addremove, add
448565
*/
449566
void delete_cmd(void){
450567
int i;
451568
int removeFiles;
452
- int dryRunFlag;
569
+ int dryRunFlag = find_option("dry-run","n",0)!=0;
453570
int softFlag;
454571
int hardFlag;
455572
Stmt loop;
456573
457
- dryRunFlag = find_option("dry-run","n",0)!=0;
574
+ if(0!=find_option("reset",0,0)){
575
+ int const verboseFlag = find_option("verbose","v",0)!=0;
576
+ db_must_be_within_tree();
577
+ verify_all_options();
578
+ addremove_reset(0, dryRunFlag, verboseFlag);
579
+ return;
580
+ }
581
+
458582
softFlag = find_option("soft",0,0)!=0;
459583
hardFlag = find_option("hard",0,0)!=0;
460584
461585
/* We should be done with options.. */
462586
verify_all_options();
@@ -615,25 +739,33 @@
615739
** anything.
616740
**
617741
** This command can be used to track third party software.
618742
**
619743
** Options:
620
-** --case-sensitive <BOOL> Override the case-sensitive setting.
744
+** --case-sensitive BOOL Override the case-sensitive setting.
621745
** --dotfiles Include files beginning with a dot (".")
622
-** --ignore <CSG> Ignore unmanaged files matching patterns from
623
-** the comma separated list of glob patterns.
624
-** --clean <CSG> Also ignore files matching patterns from
625
-** the comma separated list of glob patterns.
746
+** --ignore CSG Ignore unmanaged files matching patterns from
747
+** the Comma Separated Glob (CSG) list
748
+** --clean CSG Also ignore files matching patterns from
749
+** the Comma Separated Glob (CSG) list
626750
** -n|--dry-run If given, display instead of run actions.
751
+** --reset Reset the ADDED/DELETED state of a checkout,
752
+** such that all newly-added (but not yet committed)
753
+** files are no longer added and all newly-removed
754
+** (but not yet committed) files are no longer
755
+** removed. No flags other than --verbose and
756
+** --dry-run may be used with --reset.
757
+** --verbose|-v Outputs information about each --reset file.
758
+** Only usable with --reset.
627759
**
628760
** See also: add, rm
629761
*/
630762
void addremove_cmd(void){
631763
Blob path;
632
- const char *zCleanFlag = find_option("clean",0,1);
633
- const char *zIgnoreFlag = find_option("ignore",0,1);
634
- unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0;
764
+ const char *zCleanFlag;
765
+ const char *zIgnoreFlag;
766
+ unsigned scanFlags;
635767
int dryRunFlag = find_option("dry-run","n",0)!=0;
636768
int n;
637769
Stmt q;
638770
int vid;
639771
int nAdd = 0;
@@ -642,10 +774,23 @@
642774
643775
if( !dryRunFlag ){
644776
dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
645777
}
646778
779
+ if(0!=find_option("reset",0,0)){
780
+ int const verboseFlag = find_option("verbose","v",0)!=0;
781
+ db_must_be_within_tree();
782
+ verify_all_options();
783
+ addremove_reset(0, dryRunFlag, verboseFlag);
784
+ addremove_reset(1, dryRunFlag, verboseFlag);
785
+ return;
786
+ }
787
+
788
+ zCleanFlag = find_option("clean",0,1);
789
+ zIgnoreFlag = find_option("ignore",0,1);
790
+ scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0;
791
+
647792
/* We should be done with options.. */
648793
verify_all_options();
649794
650795
/* Fail if unprocessed arguments are present, in case user expect the
651796
** addremove command to accept a list of file or directory.
@@ -709,11 +854,10 @@
709854
/* show command summary */
710855
fossil_print("added %d files, deleted %d files\n", nAdd, nDelete);
711856
712857
db_end_transaction(dryRunFlag);
713858
}
714
-
715859
716860
/*
717861
** Rename a single file.
718862
**
719863
** The original name of the file is zOrig. The new filename is zNew.
@@ -935,11 +1079,11 @@
9351079
zOrig = blob_str(&orig);
9361080
nOrig = blob_size(&orig);
9371081
db_prepare(&q,
9381082
"SELECT pathname FROM vfile"
9391083
" WHERE vid=%d"
940
- " AND (pathname='%q' %s OR (pathname>'%q/' %s AND pathname<'%q0' %s))"
1084
+ " AND (pathname='%q' %s OR (pathname>'%q/' %s AND pathname<'%q0' %s))"
9411085
" ORDER BY 1",
9421086
vid, zOrig, filename_collation(), zOrig, filename_collation(),
9431087
zOrig, filename_collation()
9441088
);
9451089
while( db_step(&q)==SQLITE_ROW ){
@@ -946,11 +1090,11 @@
9461090
const char *zPath = db_column_text(&q, 0);
9471091
int nPath = db_column_bytes(&q, 0);
9481092
const char *zTail;
9491093
if( nPath==nOrig ){
9501094
zTail = file_tail(zPath);
951
- }else if( destType==1 ){
1095
+ }else if( origType!=0 && destType==1 ){
9521096
zTail = &zPath[nOrig-strlen(file_tail(zOrig))];
9531097
}else{
9541098
zTail = &zPath[nOrig+1];
9551099
}
9561100
db_multi_exec(
@@ -967,10 +1111,11 @@
9671111
const char *zTo = db_column_text(&q, 1);
9681112
mv_one_file(vid, zFrom, zTo, dryRunFlag);
9691113
if( moveFiles ) add_file_to_move(zFrom, zTo);
9701114
}
9711115
db_finalize(&q);
1116
+ undo_reset();
9721117
db_end_transaction(0);
9731118
if( moveFiles ) process_files_to_move(dryRunFlag);
9741119
}
9751120
9761121
/*
9771122
9781123
ADDED src/ajax.c
--- src/add.c
+++ src/add.c
@@ -240,10 +240,101 @@
240 }
241 db_finalize(&loop);
242 blob_reset(&repoName);
243 return nAdd;
244 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
246 /*
247 ** COMMAND: add
248 **
249 ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...?
@@ -269,17 +360,26 @@
269 ** be treated case sensitive or not. If the option is not given, the default
270 ** depends on the global setting, or the operating system default, if not set.
271 **
272 ** Options:
273 **
274 ** --case-sensitive <BOOL> Override the case-sensitive setting.
275 ** --dotfiles include files beginning with a dot (".")
276 ** -f|--force Add files without prompting
277 ** --ignore <CSG> Ignore unmanaged files matching patterns from
278 ** the comma separated list of glob patterns.
279 ** --clean <CSG> Also ignore files matching patterns from
280 ** the comma separated list of glob patterns.
 
 
 
 
 
 
 
 
 
281 **
282 ** See also: addremove, rm
283 */
284 void add_cmd(void){
285 int i; /* Loop counter */
@@ -288,10 +388,19 @@
288 const char *zCleanFlag; /* The --clean option or clean-glob setting */
289 const char *zIgnoreFlag; /* The --ignore option or ignore-glob setting */
290 Glob *pIgnore, *pClean; /* Ignore everything matching the glob patterns */
291 unsigned scanFlags = 0; /* Flags passed to vfile_scan() */
292 int forceFlag;
 
 
 
 
 
 
 
 
 
293
294 zCleanFlag = find_option("clean",0,1);
295 zIgnoreFlag = find_option("ignore",0,1);
296 forceFlag = find_option("force","f",0)!=0;
297 if( find_option("dotfiles",0,0)!=0 ) scanFlags |= SCAN_ALL;
@@ -339,10 +448,11 @@
339 Blob ans;
340 char cReply;
341 char *prompt = mprintf("file \"%s\" matches \"ignore-glob\". "
342 "Add it (a=all/y/N)? ", zTreeName);
343 prompt_user(prompt, &ans);
 
344 cReply = blob_str(&ans)[0];
345 blob_reset(&ans);
346 if( cReply=='a' || cReply=='A' ){
347 forceFlag = 1;
348 }else if( cReply!='y' && cReply!='Y' ){
@@ -441,22 +551,36 @@
441 ** --soft Skip removing files from the checkout.
442 ** This supersedes the --hard option.
443 ** --hard Remove files from the checkout.
444 ** --case-sensitive <BOOL> Override the case-sensitive setting.
445 ** -n|--dry-run If given, display instead of run actions.
 
 
 
 
 
 
 
446 **
447 ** See also: addremove, add
448 */
449 void delete_cmd(void){
450 int i;
451 int removeFiles;
452 int dryRunFlag;
453 int softFlag;
454 int hardFlag;
455 Stmt loop;
456
457 dryRunFlag = find_option("dry-run","n",0)!=0;
 
 
 
 
 
 
 
458 softFlag = find_option("soft",0,0)!=0;
459 hardFlag = find_option("hard",0,0)!=0;
460
461 /* We should be done with options.. */
462 verify_all_options();
@@ -615,25 +739,33 @@
615 ** anything.
616 **
617 ** This command can be used to track third party software.
618 **
619 ** Options:
620 ** --case-sensitive <BOOL> Override the case-sensitive setting.
621 ** --dotfiles Include files beginning with a dot (".")
622 ** --ignore <CSG> Ignore unmanaged files matching patterns from
623 ** the comma separated list of glob patterns.
624 ** --clean <CSG> Also ignore files matching patterns from
625 ** the comma separated list of glob patterns.
626 ** -n|--dry-run If given, display instead of run actions.
 
 
 
 
 
 
 
 
627 **
628 ** See also: add, rm
629 */
630 void addremove_cmd(void){
631 Blob path;
632 const char *zCleanFlag = find_option("clean",0,1);
633 const char *zIgnoreFlag = find_option("ignore",0,1);
634 unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0;
635 int dryRunFlag = find_option("dry-run","n",0)!=0;
636 int n;
637 Stmt q;
638 int vid;
639 int nAdd = 0;
@@ -642,10 +774,23 @@
642
643 if( !dryRunFlag ){
644 dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
645 }
646
 
 
 
 
 
 
 
 
 
 
 
 
 
647 /* We should be done with options.. */
648 verify_all_options();
649
650 /* Fail if unprocessed arguments are present, in case user expect the
651 ** addremove command to accept a list of file or directory.
@@ -709,11 +854,10 @@
709 /* show command summary */
710 fossil_print("added %d files, deleted %d files\n", nAdd, nDelete);
711
712 db_end_transaction(dryRunFlag);
713 }
714
715
716 /*
717 ** Rename a single file.
718 **
719 ** The original name of the file is zOrig. The new filename is zNew.
@@ -935,11 +1079,11 @@
935 zOrig = blob_str(&orig);
936 nOrig = blob_size(&orig);
937 db_prepare(&q,
938 "SELECT pathname FROM vfile"
939 " WHERE vid=%d"
940 " AND (pathname='%q' %s OR (pathname>'%q/' %s AND pathname<'%q0' %s))"
941 " ORDER BY 1",
942 vid, zOrig, filename_collation(), zOrig, filename_collation(),
943 zOrig, filename_collation()
944 );
945 while( db_step(&q)==SQLITE_ROW ){
@@ -946,11 +1090,11 @@
946 const char *zPath = db_column_text(&q, 0);
947 int nPath = db_column_bytes(&q, 0);
948 const char *zTail;
949 if( nPath==nOrig ){
950 zTail = file_tail(zPath);
951 }else if( destType==1 ){
952 zTail = &zPath[nOrig-strlen(file_tail(zOrig))];
953 }else{
954 zTail = &zPath[nOrig+1];
955 }
956 db_multi_exec(
@@ -967,10 +1111,11 @@
967 const char *zTo = db_column_text(&q, 1);
968 mv_one_file(vid, zFrom, zTo, dryRunFlag);
969 if( moveFiles ) add_file_to_move(zFrom, zTo);
970 }
971 db_finalize(&q);
 
972 db_end_transaction(0);
973 if( moveFiles ) process_files_to_move(dryRunFlag);
974 }
975
976 /*
977
978 DDED src/ajax.c
--- src/add.c
+++ src/add.c
@@ -240,10 +240,101 @@
240 }
241 db_finalize(&loop);
242 blob_reset(&repoName);
243 return nAdd;
244 }
245
246 /*
247 ** Resets the ADDED/DELETED state of a checkout, such that all
248 ** newly-added (but not yet committed) files are no longer added and
249 ** newly-removed (but not yet committed) files are no longer
250 ** removed. If bIsAdd is true, it operates on the "add" state, else it
251 ** operates on the "rm" state.
252 **
253 ** If bDryRun is true it outputs what it would have done, but does not
254 ** actually do it. In this case it rolls back the transaction it
255 ** starts (so don't start a transaction before calling this).
256 **
257 ** If bVerbose is true it outputs the name of each reset entry.
258 **
259 ** This is intended to be called only in the context of the
260 ** add/rm/addremove commands, after a call to verify_all_options().
261 **
262 ** Un-added files are not modified but any un-rm'd files which are
263 ** missing from the checkout are restored from the repo. un-rm'd files
264 ** which exist in the checkout are left as-is, rather than restoring
265 ** them using vfile_to_disk(), to avoid overwriting any local changes
266 ** made to those files.
267 */
268 static void addremove_reset(int bIsAdd, int bDryRun, int bVerbose){
269 int nReset = 0; /* # of entries which get reset */
270 Stmt stmt; /* vfile loop query */
271
272 db_begin_transaction();
273 db_prepare(&stmt, "SELECT id, pathname FROM vfile "
274 "WHERE %s ORDER BY pathname",
275 bIsAdd==0 ? "deleted<>0" : "rid=0"/*safe-for-%s*/);
276 while( db_step(&stmt)==SQLITE_ROW ){
277 /* This loop exists only so we can restore the contents of un-rm'd
278 ** files and support verbose mode. All manipulation of vfile's
279 ** contents happens after the loop. For the ADD case in non-verbose
280 ** mode we "could" skip this loop entirely.
281 */
282 int const id = db_column_int(&stmt, 0);
283 char const * zPathname = db_column_text(&stmt, 1);
284 Blob relName = empty_blob;
285 if(bIsAdd==0 || bVerbose!=0){
286 /* Make filename relative... */
287 char *zFullName = mprintf("%s%s", g.zLocalRoot, zPathname);
288 file_relative_name(zFullName, &relName, 0);
289 fossil_free(zFullName);
290 }
291 if(bIsAdd==0){
292 /* Restore contents of missing un-rm'd files. We don't do this
293 ** unconditionally because we might cause data loss if a file
294 ** is modified, rm'd, then un-rm'd.
295 */
296 ++nReset;
297 if(!file_isfile_or_link(blob_str(&relName))){
298 if(bDryRun==0){
299 vfile_to_disk(0, id, 0, 0);
300 if(bVerbose){
301 fossil_print("Restored missing file: %b\n", &relName);
302 }
303 }else{
304 fossil_print("Dry-run: not restoring missing file: %b\n", &relName);
305 }
306 }
307 if(bVerbose){
308 fossil_print("Un-removed: %b\n", &relName);
309 }
310 }else{
311 /* un-add... */
312 ++nReset;
313 if(bVerbose){
314 fossil_print("Un-added: %b\n", &relName);
315 }
316 }
317 blob_reset(&relName);
318 }
319 db_finalize(&stmt);
320 if(nReset>0){
321 if(bIsAdd==0){
322 if(bDryRun==0){
323 db_exec_sql("UPDATE vfile SET deleted=0 WHERE deleted<>0");
324 }
325 fossil_print("Un-removed %d file(s).\n", nReset);
326 }else{
327 if(bDryRun==0){
328 db_exec_sql("DELETE FROM vfile WHERE rid=0");
329 }
330 fossil_print("Un-added %d file(s).\n", nReset);
331 }
332 }
333 db_end_transaction(bDryRun ? 1 : 0);
334 }
335
336
337 /*
338 ** COMMAND: add
339 **
340 ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...?
@@ -269,17 +360,26 @@
360 ** be treated case sensitive or not. If the option is not given, the default
361 ** depends on the global setting, or the operating system default, if not set.
362 **
363 ** Options:
364 **
365 ** --case-sensitive BOOL Override the case-sensitive setting.
366 ** --dotfiles include files beginning with a dot (".")
367 ** -f|--force Add files without prompting
368 ** --ignore CSG Ignore unmanaged files matching patterns from
369 ** the Comma Separated Glob (CSG) pattern list
370 ** --clean CSG Also ignore files matching patterns from
371 ** the Comma Separated Glob (CSG) list
372 ** --reset Reset the ADDED state of a checkout, such
373 ** that all newly-added (but not yet committed)
374 ** files are no longer added. No flags other
375 ** than --verbose and --dry-run may be used
376 ** with --reset.
377 **
378 ** The following options are only valid with --reset:
379 ** -v|--verbose Outputs information about each --reset file.
380 ** -n|--dry-run Display instead of run actions.
381 **
382 ** See also: addremove, rm
383 */
384 void add_cmd(void){
385 int i; /* Loop counter */
@@ -288,10 +388,19 @@
388 const char *zCleanFlag; /* The --clean option or clean-glob setting */
389 const char *zIgnoreFlag; /* The --ignore option or ignore-glob setting */
390 Glob *pIgnore, *pClean; /* Ignore everything matching the glob patterns */
391 unsigned scanFlags = 0; /* Flags passed to vfile_scan() */
392 int forceFlag;
393
394 if(0!=find_option("reset",0,0)){
395 int const verboseFlag = find_option("verbose","v",0)!=0;
396 int const dryRunFlag = find_option("dry-run","n",0)!=0;
397 db_must_be_within_tree();
398 verify_all_options();
399 addremove_reset(1, dryRunFlag, verboseFlag);
400 return;
401 }
402
403 zCleanFlag = find_option("clean",0,1);
404 zIgnoreFlag = find_option("ignore",0,1);
405 forceFlag = find_option("force","f",0)!=0;
406 if( find_option("dotfiles",0,0)!=0 ) scanFlags |= SCAN_ALL;
@@ -339,10 +448,11 @@
448 Blob ans;
449 char cReply;
450 char *prompt = mprintf("file \"%s\" matches \"ignore-glob\". "
451 "Add it (a=all/y/N)? ", zTreeName);
452 prompt_user(prompt, &ans);
453 fossil_free(prompt);
454 cReply = blob_str(&ans)[0];
455 blob_reset(&ans);
456 if( cReply=='a' || cReply=='A' ){
457 forceFlag = 1;
458 }else if( cReply!='y' && cReply!='Y' ){
@@ -441,22 +551,36 @@
551 ** --soft Skip removing files from the checkout.
552 ** This supersedes the --hard option.
553 ** --hard Remove files from the checkout.
554 ** --case-sensitive <BOOL> Override the case-sensitive setting.
555 ** -n|--dry-run If given, display instead of run actions.
556 ** --reset Reset the DELETED state of a checkout, such
557 ** that all newly-rm'd (but not yet committed)
558 ** files are no longer removed. No flags other
559 ** than --verbose or --dry-run may be used with
560 ** --reset.
561 ** --verbose|-v Outputs information about each --reset file.
562 ** Only usable with --reset.
563 **
564 ** See also: addremove, add
565 */
566 void delete_cmd(void){
567 int i;
568 int removeFiles;
569 int dryRunFlag = find_option("dry-run","n",0)!=0;
570 int softFlag;
571 int hardFlag;
572 Stmt loop;
573
574 if(0!=find_option("reset",0,0)){
575 int const verboseFlag = find_option("verbose","v",0)!=0;
576 db_must_be_within_tree();
577 verify_all_options();
578 addremove_reset(0, dryRunFlag, verboseFlag);
579 return;
580 }
581
582 softFlag = find_option("soft",0,0)!=0;
583 hardFlag = find_option("hard",0,0)!=0;
584
585 /* We should be done with options.. */
586 verify_all_options();
@@ -615,25 +739,33 @@
739 ** anything.
740 **
741 ** This command can be used to track third party software.
742 **
743 ** Options:
744 ** --case-sensitive BOOL Override the case-sensitive setting.
745 ** --dotfiles Include files beginning with a dot (".")
746 ** --ignore CSG Ignore unmanaged files matching patterns from
747 ** the Comma Separated Glob (CSG) list
748 ** --clean CSG Also ignore files matching patterns from
749 ** the Comma Separated Glob (CSG) list
750 ** -n|--dry-run If given, display instead of run actions.
751 ** --reset Reset the ADDED/DELETED state of a checkout,
752 ** such that all newly-added (but not yet committed)
753 ** files are no longer added and all newly-removed
754 ** (but not yet committed) files are no longer
755 ** removed. No flags other than --verbose and
756 ** --dry-run may be used with --reset.
757 ** --verbose|-v Outputs information about each --reset file.
758 ** Only usable with --reset.
759 **
760 ** See also: add, rm
761 */
762 void addremove_cmd(void){
763 Blob path;
764 const char *zCleanFlag;
765 const char *zIgnoreFlag;
766 unsigned scanFlags;
767 int dryRunFlag = find_option("dry-run","n",0)!=0;
768 int n;
769 Stmt q;
770 int vid;
771 int nAdd = 0;
@@ -642,10 +774,23 @@
774
775 if( !dryRunFlag ){
776 dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
777 }
778
779 if(0!=find_option("reset",0,0)){
780 int const verboseFlag = find_option("verbose","v",0)!=0;
781 db_must_be_within_tree();
782 verify_all_options();
783 addremove_reset(0, dryRunFlag, verboseFlag);
784 addremove_reset(1, dryRunFlag, verboseFlag);
785 return;
786 }
787
788 zCleanFlag = find_option("clean",0,1);
789 zIgnoreFlag = find_option("ignore",0,1);
790 scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0;
791
792 /* We should be done with options.. */
793 verify_all_options();
794
795 /* Fail if unprocessed arguments are present, in case user expect the
796 ** addremove command to accept a list of file or directory.
@@ -709,11 +854,10 @@
854 /* show command summary */
855 fossil_print("added %d files, deleted %d files\n", nAdd, nDelete);
856
857 db_end_transaction(dryRunFlag);
858 }
 
859
860 /*
861 ** Rename a single file.
862 **
863 ** The original name of the file is zOrig. The new filename is zNew.
@@ -935,11 +1079,11 @@
1079 zOrig = blob_str(&orig);
1080 nOrig = blob_size(&orig);
1081 db_prepare(&q,
1082 "SELECT pathname FROM vfile"
1083 " WHERE vid=%d"
1084 " AND (pathname='%q' %s OR (pathname>'%q/' %s AND pathname<'%q0' %s))"
1085 " ORDER BY 1",
1086 vid, zOrig, filename_collation(), zOrig, filename_collation(),
1087 zOrig, filename_collation()
1088 );
1089 while( db_step(&q)==SQLITE_ROW ){
@@ -946,11 +1090,11 @@
1090 const char *zPath = db_column_text(&q, 0);
1091 int nPath = db_column_bytes(&q, 0);
1092 const char *zTail;
1093 if( nPath==nOrig ){
1094 zTail = file_tail(zPath);
1095 }else if( origType!=0 && destType==1 ){
1096 zTail = &zPath[nOrig-strlen(file_tail(zOrig))];
1097 }else{
1098 zTail = &zPath[nOrig+1];
1099 }
1100 db_multi_exec(
@@ -967,10 +1111,11 @@
1111 const char *zTo = db_column_text(&q, 1);
1112 mv_one_file(vid, zFrom, zTo, dryRunFlag);
1113 if( moveFiles ) add_file_to_move(zFrom, zTo);
1114 }
1115 db_finalize(&q);
1116 undo_reset();
1117 db_end_transaction(0);
1118 if( moveFiles ) process_files_to_move(dryRunFlag);
1119 }
1120
1121 /*
1122
1123 DDED src/ajax.c
+19
--- a/src/ajax.c
+++ b/src/ajax.c
@@ -0,0 +1,19 @@
1
+/*
2
+** Copyright (c) 2020 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 "FreeB11, 1
7
+}
8
+ 0, diffFlags if(DIFF_SIDEBYSIDE & diffFlags)else{
9
+ CX("wiki emit_script_tag(0,0);
10
+ CX("\n"emit_script_tag(1,0stylere; you can redistri/*
11
+** Copyright (c) 2020 D. Richard Hipp
12
+**
13
+** This program is free software; you can redistribute it and/or
14
+** modify it under the terms of the Simplified BSD License (also
15
+** known as the "2-Clause License" or "FreeB11, 1
16
+}
17
+ 0, diffFlags if(DIFF_SIDEBYSIDE & diffFlags)else{
18
+ CX("wiki emit_script_tag(0,0);
19
+ CX("\n"emit_script_tag(1,0stylestatic
--- a/src/ajax.c
+++ b/src/ajax.c
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/ajax.c
+++ b/src/ajax.c
@@ -0,0 +1,19 @@
1 /*
2 ** Copyright (c) 2020 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 "FreeB11, 1
7 }
8 0, diffFlags if(DIFF_SIDEBYSIDE & diffFlags)else{
9 CX("wiki emit_script_tag(0,0);
10 CX("\n"emit_script_tag(1,0stylere; you can redistri/*
11 ** Copyright (c) 2020 D. Richard Hipp
12 **
13 ** This program is free software; you can redistribute it and/or
14 ** modify it under the terms of the Simplified BSD License (also
15 ** known as the "2-Clause License" or "FreeB11, 1
16 }
17 0, diffFlags if(DIFF_SIDEBYSIDE & diffFlags)else{
18 CX("wiki emit_script_tag(0,0);
19 CX("\n"emit_script_tag(1,0stylestatic
+459 -199
--- src/alerts.c
+++ src/alerts.c
@@ -46,10 +46,11 @@
4646
@ --
4747
@ -- The ssub field is a string where each character indicates a particular
4848
@ -- type of event to subscribe to. Choices:
4949
@ -- a - Announcements
5050
@ -- c - Check-ins
51
+@ -- f - Forum posts
5152
@ -- t - Ticket changes
5253
@ -- w - Wiki changes
5354
@ -- Probably different codes will be added in the future. In the future
5455
@ -- we might also add a separate table that allows subscribing to email
5556
@ -- notifications for specific branches or tags or tickets.
@@ -110,15 +111,15 @@
110111
** if the email-send-method is something other than "off".
111112
*/
112113
void alert_schema(int bOnlyIfEnabled){
113114
if( !alert_tables_exist() ){
114115
if( bOnlyIfEnabled
115
- && fossil_strcmp(db_get("email-send-method","off"),"off")==0
116
+ && fossil_strcmp(db_get("email-send-method",0),"off")==0
116117
){
117118
return; /* Don't create table for disabled email */
118119
}
119
- db_multi_exec(zAlertInit/*works-like:""*/);
120
+ db_exec_sql(zAlertInit);
120121
alert_triggers_enable();
121122
}else if( !db_table_has_column("repository","pending_alert","sentMod") ){
122123
db_multi_exec(
123124
"ALTER TABLE repository.pending_alert"
124125
" ADD COLUMN sentMod BOOLEAN DEFAULT false;"
@@ -158,11 +159,11 @@
158159
/*
159160
** Return true if email alerts are active.
160161
*/
161162
int alert_enabled(void){
162163
if( !alert_tables_exist() ) return 0;
163
- if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
164
+ if( fossil_strcmp(db_get("email-send-method",0),"off")==0 ) return 0;
164165
return 1;
165166
}
166167
167168
/*
168169
** If the subscriber table does not exist, then paint an error message
@@ -183,11 +184,11 @@
183184
** is an administrator.
184185
*/
185186
void alert_submenu_common(void){
186187
if( g.perm.Admin ){
187188
if( fossil_strcmp(g.zPath,"subscribers") ){
188
- style_submenu_element("List Subscribers","%R/subscribers");
189
+ style_submenu_element("Subscribers","%R/subscribers");
189190
}
190191
if( fossil_strcmp(g.zPath,"subscribe") ){
191192
style_submenu_element("Add New Subscriber","%R/subscribe");
192193
}
193194
}
@@ -238,10 +239,17 @@
238239
@ This is URL used as the basename for hyperlinks included in
239240
@ email alert text. Omit the trailing "/".
240241
@ Suggested value: "%h(g.zBaseURL)"
241242
@ (Property: "email-url")</p>
242243
@ <hr>
244
+
245
+ entry_attribute("Administrator email address", 40, "email-admin",
246
+ "eadmin", "", 0);
247
+ @ <p>This is the email for the human administrator for the system.
248
+ @ Abuse and trouble reports and password reset requests are send here.
249
+ @ (Property: "email-admin")</p>
250
+ @ <hr>
243251
244252
entry_attribute("\"Return-Path\" email address", 20, "email-self",
245253
"eself", "", 0);
246254
@ <p><b>Required.</b>
247255
@ This is the email to which email notification bounces should be sent.
@@ -297,17 +305,10 @@
297305
@ append a colon and TCP port number (ex: smtp.example.com:587).
298306
@ The default TCP port number is 25.
299307
@ (Property: "email-send-relayhost")</p>
300308
@ <hr>
301309
302
- entry_attribute("Administrator email address", 40, "email-admin",
303
- "eadmin", "", 0);
304
- @ <p>This is the email for the human administrator for the system.
305
- @ Abuse and trouble reports are send here.
306
- @ (Property: "email-admin")</p>
307
- @ <hr>
308
-
309310
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
310311
@ </div></form>
311312
db_end_transaction(0);
312313
style_footer();
313314
}
@@ -482,11 +483,11 @@
482483
blob_init(&p->out, 0, 0);
483484
p->mFlags = mFlags;
484485
if( zAltDest ){
485486
p->zDest = zAltDest;
486487
}else{
487
- p->zDest = db_get("email-send-method","off");
488
+ p->zDest = db_get("email-send-method",0);
488489
}
489490
if( fossil_strcmp(p->zDest,"off")==0 ) return p;
490491
if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
491492
if( fossil_strcmp(p->zDest,"db")==0 ){
492493
char *zErr;
@@ -574,22 +575,18 @@
574575
}
575576
return 0;
576577
}
577578
578579
/*
579
-** Make a copy of the input string up to but not including the
580
-** first cTerm character.
581
-**
582
-** Verify that the string really that is to be copied really is a
583
-** valid email address. If it is not, then return NULL.
584
-**
585
-** This routine is more restrictive than necessary. It does not
586
-** allow comments, IP address, quoted strings, or certain uncommon
587
-** characters. The only non-alphanumerics allowed in the local
588
-** part are "_", "+", "-" and "+".
589
-*/
590
-char *email_copy_addr(const char *z, char cTerm ){
580
+** Determine whether or not the input string is a valid email address.
581
+** Only look at character up to but not including the first \000 or
582
+** the first cTerm character, whichever comes first.
583
+**
584
+** Return the length of the email addresss string in bytes if the email
585
+** address is valid. If the email address is misformed, return 0.
586
+*/
587
+int email_address_is_valid(const char *z, char cTerm){
591588
int i;
592589
int nAt = 0;
593590
int nDot = 0;
594591
char c;
595592
if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */
@@ -619,13 +616,28 @@
619616
}
620617
}
621618
if( c!=cTerm ) return 0; /* Missing terminator */
622619
if( nAt==0 ) return 0; /* No "@" found anywhere */
623620
if( nDot==0 ) return 0; /* No "." in the domain */
621
+ return i;
622
+}
624623
625
- /* If we reach this point, the email address is valid */
626
- return mprintf("%.*s", i, z);
624
+/*
625
+** Make a copy of the input string up to but not including the
626
+** first cTerm character.
627
+**
628
+** Verify that the string really that is to be copied really is a
629
+** valid email address. If it is not, then return NULL.
630
+**
631
+** This routine is more restrictive than necessary. It does not
632
+** allow comments, IP address, quoted strings, or certain uncommon
633
+** characters. The only non-alphanumerics allowed in the local
634
+** part are "_", "+", "-" and "+".
635
+*/
636
+char *email_copy_addr(const char *z, char cTerm ){
637
+ int i = email_address_is_valid(z, cTerm);
638
+ return i==0 ? 0 : mprintf("%.*s", i, z);
627639
}
628640
629641
/*
630642
** Scan the input string for a valid email address enclosed in <...>
631643
** If the string contains one or more email addresses, extract the first
@@ -659,10 +671,38 @@
659671
char *zOut = alert_find_emailaddr(zIn);
660672
if( zOut ){
661673
sqlite3_result_text(context, zOut, -1, fossil_free);
662674
}
663675
}
676
+
677
+/*
678
+** SQL function: display_name(X)
679
+**
680
+** If X is a string, search for a user name at the beginning of that
681
+** string. The user name must be followed by an email address. If
682
+** found, return the user name. If not found, return NULL.
683
+**
684
+** This routine is used to extract the display name from the USER.INFO
685
+** field.
686
+*/
687
+void alert_display_name_func(
688
+ sqlite3_context *context,
689
+ int argc,
690
+ sqlite3_value **argv
691
+){
692
+ const char *zIn = (const char*)sqlite3_value_text(argv[0]);
693
+ int i;
694
+ if( zIn==0 ) return;
695
+ while( fossil_isspace(zIn[0]) ) zIn++;
696
+ for(i=0; zIn[i] && zIn[i]!='<' && zIn[i]!='\n'; i++){}
697
+ if( zIn[i]=='<' ){
698
+ while( i>0 && fossil_isspace(zIn[i-1]) ){ i--; }
699
+ if( i>0 ){
700
+ sqlite3_result_text(context, zIn, i, SQLITE_TRANSIENT);
701
+ }
702
+ }
703
+}
664704
665705
/*
666706
** Return the hostname portion of an email address - the part following
667707
** the @
668708
*/
@@ -762,11 +802,11 @@
762802
** Date:
763803
** Message-Id:
764804
** Content-Type:
765805
** Content-Transfer-Encoding:
766806
** MIME-Version:
767
-** X-Fossil-From:
807
+** Sender:
768808
**
769809
** The caller maintains ownership of the input Blobs. This routine will
770810
** read the Blobs and send them onward to the email system, but it will
771811
** not free them.
772812
**
@@ -774,14 +814,14 @@
774814
** in the pHdr parameter.
775815
**
776816
** If the zFromName argument is not NULL, then it should be a human-readable
777817
** name or handle for the sender. In that case, "From:" becomes a made-up
778818
** 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
819
+** and an additional "Sender:" field is inserted with the email-self
820
+** address. Downstream software might use the Sender header to set
781821
** 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
822
+** then the "From:" is set to the email-self value and Sender is
783823
** omitted.
784824
*/
785825
void alert_send(
786826
AlertSender *p, /* Emailer context */
787827
Blob *pHdr, /* Email header (incomplete) */
@@ -794,26 +834,26 @@
794834
fossil_print("Sending email\n");
795835
}
796836
if( fossil_strcmp(p->zDest, "off")==0 ){
797837
return;
798838
}
839
+ blob_init(&all, 0, 0);
799840
if( fossil_strcmp(p->zDest, "blob")==0 ){
800841
pOut = &p->out;
801842
if( blob_size(pOut) ){
802843
blob_appendf(pOut, "%.72c\n", '=');
803844
}
804845
}else{
805
- blob_init(&all, 0, 0);
806846
pOut = &all;
807847
}
808848
blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
809849
if( p->zFrom==0 || p->zFrom[0]==0 ){
810850
return; /* email-self is not set. Error will be reported separately */
811851
}else if( zFromName ){
812852
blob_appendf(pOut, "From: %s <%s@%s>\r\n",
813853
zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
814
- blob_appendf(pOut, "X-Fossil-From: <%s>\r\n", p->zFrom);
854
+ blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
815855
}else{
816856
blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
817857
}
818858
blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
819859
if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
@@ -879,10 +919,26 @@
879919
fossil_print("%s", blob_str(&all));
880920
}
881921
blob_reset(&all);
882922
}
883923
924
+/*
925
+** SETTING: email-url width=40
926
+** This URL is used as the basename for hyperlinks included in email alert
927
+** text. Omit the trailing "/".
928
+*/
929
+/*
930
+** SETTING: email-admin width=40
931
+** This is the email address for the human administrator for the system.
932
+** Abuse and trouble reports and password reset requests are send here.
933
+*/
934
+/*
935
+** SETTING: email-subname width=16
936
+** This is a short name used to identifies the repository in the Subject:
937
+** line of email alerts. Traditionally this name is included in square
938
+** brackets. Examples: "[fossil-src]", "[sqlite-src]".
939
+*/
884940
/*
885941
** SETTING: email-send-method width=5 default=off
886942
** Determine the method used to send email. Allowed values are
887943
** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
888944
** means no email is ever sent. The "relay" value means emails are sent
@@ -1152,10 +1208,19 @@
11521208
int i, j, n;
11531209
char c;
11541210
11551211
*peErr = 0;
11561212
*pzErr = 0;
1213
+
1214
+ /* Verify the captcha first */
1215
+ if( needCaptcha ){
1216
+ if( !captcha_is_correct(1) ){
1217
+ *peErr = 2;
1218
+ *pzErr = mprintf("incorrect security code");
1219
+ return 0;
1220
+ }
1221
+ }
11571222
11581223
/* Check the validity of the email address.
11591224
**
11601225
** (1) Exactly one '@' character.
11611226
** (2) No other characters besides [a-zA-Z0-9._+-]
@@ -1163,11 +1228,15 @@
11631228
** The local part is currently more restrictive than RFC 5322 allows:
11641229
** https://stackoverflow.com/a/2049510/142454 We will expand this as
11651230
** necessary.
11661231
*/
11671232
zEAddr = P("e");
1168
- if( zEAddr==0 ) return 0;
1233
+ if( zEAddr==0 ){
1234
+ *peErr = 1;
1235
+ *pzErr = mprintf("required");
1236
+ return 0;
1237
+ }
11691238
for(i=j=n=0; (c = zEAddr[i])!=0; i++){
11701239
if( c=='@' ){
11711240
n = i;
11721241
j++;
11731242
continue;
@@ -1193,14 +1262,13 @@
11931262
*peErr = 1;
11941263
*pzErr = mprintf("email domain too short");
11951264
return 0;
11961265
}
11971266
1198
- /* Verify the captcha */
1199
- if( needCaptcha && !captcha_is_correct(1) ){
1200
- *peErr = 2;
1201
- *pzErr = mprintf("incorrect security code");
1267
+ if( authorized_subscription_email(zEAddr)==0 ){
1268
+ *peErr = 1;
1269
+ *pzErr = mprintf("not an authorized email address");
12021270
return 0;
12031271
}
12041272
12051273
/* Check to make sure the email address is available for reuse */
12061274
if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
@@ -1259,11 +1327,11 @@
12591327
** page. To allow anonymous passers-by to sign up for email
12601328
** notification, set Email-Alerts on user "nobody" or "anonymous".
12611329
*/
12621330
void subscribe_page(void){
12631331
int needCaptcha;
1264
- unsigned int uSeed;
1332
+ unsigned int uSeed = 0;
12651333
const char *zDecoded;
12661334
char *zCaptcha = 0;
12671335
char *zErr = 0;
12681336
int eErr = 0;
12691337
int di;
@@ -1291,10 +1359,14 @@
12911359
/* Everybody else jumps to the page to administer their own
12921360
** account only. */
12931361
cgi_redirectf("%R/alerts");
12941362
return;
12951363
}
1364
+ }
1365
+ if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){
1366
+ register_page();
1367
+ return;
12961368
}
12971369
alert_submenu_common();
12981370
needCaptcha = !login_is_individual();
12991371
if( P("submit")
13001372
&& cgi_csrf_safe(1)
@@ -1312,10 +1384,11 @@
13121384
if( PB("sa") ) ssub[nsub++] = 'a';
13131385
if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
13141386
if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
13151387
if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
13161388
if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1389
+ if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x';
13171390
ssub[nsub] = 0;
13181391
db_multi_exec(
13191392
"INSERT INTO subscriber(semail,suname,"
13201393
" sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
13211394
"VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
@@ -1332,11 +1405,15 @@
13321405
id);
13331406
if( !needCaptcha ){
13341407
/* The new subscription has been added on behalf of a logged-in user.
13351408
** No verification is required. Jump immediately to /alerts page.
13361409
*/
1337
- cgi_redirectf("%R/alerts/%s", zCode);
1410
+ if( g.perm.Admin ){
1411
+ cgi_redirectf("%R/alerts/%.32s", zCode);
1412
+ }else{
1413
+ cgi_redirectf("%R/alerts");
1414
+ }
13381415
return;
13391416
}else{
13401417
/* We need to send a verification email */
13411418
Blob hdr, body;
13421419
AlertSender *pSender = alert_sender_new(0,0);
@@ -1354,11 +1431,11 @@
13541431
@ <blockquote><pre>
13551432
@ %h(pSender->zErr)
13561433
@ </pre></blockquote>
13571434
}else{
13581435
@ <p>An email has been sent to "%h(zEAddr)". That email contains a
1359
- @ hyperlink that you must click on in order to activate your
1436
+ @ hyperlink that you must click to activate your
13601437
@ subscription.</p>
13611438
}
13621439
alert_sender_free(pSender);
13631440
style_footer();
13641441
}
@@ -1386,16 +1463,23 @@
13861463
if( eErr==1 ){
13871464
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
13881465
}
13891466
@ </tr>
13901467
if( needCaptcha ){
1391
- uSeed = captcha_seed();
1468
+ const char *zInit = "";
1469
+ if( P("captchaseed")!=0 && eErr!=2 ){
1470
+ uSeed = strtoul(P("captchaseed"),0,10);
1471
+ zInit = P("captcha");
1472
+ }else{
1473
+ uSeed = captcha_seed();
1474
+ }
13921475
zDecoded = captcha_decode(uSeed);
13931476
zCaptcha = captcha_render(zDecoded);
13941477
@ <tr>
13951478
@ <td class="form_label">Security Code:</td>
1396
- @ <td><input type="text" name="captcha" value="" size="30">
1479
+ @ <td><input type="text" name="captcha" value="%h(zInit)" size="30">
1480
+ captcha_speakit_button(uSeed, "Speak the code");
13971481
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
13981482
@ </tr>
13991483
if( eErr==2 ){
14001484
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
14011485
}
@@ -1421,10 +1505,12 @@
14211505
@ Check-ins</label><br>
14221506
}
14231507
if( g.perm.RdForum ){
14241508
@ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
14251509
@ Forum Posts</label><br>
1510
+ @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \
1511
+ @ Forum Edits</label><br>
14261512
}
14271513
if( g.perm.RdTkt ){
14281514
@ <label><input type="checkbox" name="st" %s(PCK("st"))> \
14291515
@ Ticket changes</label><br>
14301516
}
@@ -1461,11 +1547,11 @@
14611547
@ </table>
14621548
if( needCaptcha ){
14631549
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
14641550
@ %h(zCaptcha)
14651551
@ </pre>
1466
- @ Enter the 8 characters above in the "Security Code" box
1552
+ @ Enter the 8 characters above in the "Security Code" box<br/>
14671553
@ </td></tr></table></div>
14681554
}
14691555
@ </form>
14701556
fossil_free(zErr);
14711557
style_footer();
@@ -1474,21 +1560,20 @@
14741560
/*
14751561
** Either shutdown or completely delete a subscription entry given
14761562
** by the hex value zName. Then paint a webpage that explains that
14771563
** the entry has been removed.
14781564
*/
1479
-static void alert_unsubscribe(const char *zName){
1565
+static void alert_unsubscribe(int sid){
14801566
char *zEmail;
14811567
zEmail = db_text(0, "SELECT semail FROM subscriber"
1482
- " WHERE subscriberCode=hextoblob(%Q)", zName);
1568
+ " WHERE subscriberId=%d", sid);
14831569
if( zEmail==0 ){
14841570
style_header("Unsubscribe Fail");
14851571
@ <p>Unable to locate a subscriber with the requested key</p>
14861572
}else{
14871573
db_multi_exec(
1488
- "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1489
- zName
1574
+ "DELETE FROM subscriber WHERE subscriberId=%d", sid
14901575
);
14911576
style_header("Unsubscribed");
14921577
@ <p>The "%h(zEmail)" email address has been delisted.
14931578
@ All traces of that email address have been removed</p>
14941579
}
@@ -1499,108 +1584,149 @@
14991584
/*
15001585
** WEBPAGE: alerts
15011586
**
15021587
** Edit email alert and notification settings.
15031588
**
1504
-** The subscriber is identified in either of two ways:
1505
-**
1506
-** (1) The name= query parameter contains the subscriberCode.
1507
-**
1508
-** (2) The user is logged into an account other than "nobody" or
1509
-** "anonymous". In that case the notification settings
1510
-** associated with that account can be edited without needing
1511
-** to know the subscriber code.
1512
-*/
1513
-void alert_page(void){
1514
- const char *zName = P("name");
1515
- Stmt q;
1516
- int sa, sc, sf, st, sw;
1517
- int sdigest, sdonotcall, sverified;
1518
- const char *ssub;
1519
- const char *semail;
1520
- const char *smip;
1521
- const char *suname;
1522
- const char *mtime;
1523
- const char *sctime;
1524
- int eErr = 0;
1525
- char *zErr = 0;
1526
-
1527
- if( alert_webpages_disabled() ) return;
1528
- login_check_credentials();
1529
- if( !g.perm.EmailAlert ){
1530
- login_needed(g.anon.EmailAlert);
1531
- return;
1532
- }
1533
- if( zName==0 && login_is_individual() ){
1534
- zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
1535
- " WHERE suname=%Q", g.zLogin);
1536
- }
1537
- if( zName==0 || !validate16(zName, -1) ){
1538
- cgi_redirect("subscribe");
1539
- return;
1540
- }
1541
- alert_submenu_common();
1542
- if( P("submit")!=0 && cgi_csrf_safe(1) ){
1543
- int sdonotcall = PB("sdonotcall");
1544
- int sdigest = PB("sdigest");
1545
- char ssub[10];
1546
- int nsub = 0;
1547
- if( PB("sa") ) ssub[nsub++] = 'a';
1548
- if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1549
- if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1550
- if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1551
- if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1552
- ssub[nsub] = 0;
1553
- if( g.perm.Admin ){
1554
- const char *suname = PT("suname");
1555
- int sverified = PB("sverified");
1556
- if( suname && suname[0]==0 ) suname = 0;
1557
- db_multi_exec(
1558
- "UPDATE subscriber SET"
1559
- " sdonotcall=%d,"
1560
- " sdigest=%d,"
1561
- " ssub=%Q,"
1562
- " mtime=strftime('%%s','now'),"
1563
- " smip=%Q,"
1564
- " suname=%Q,"
1565
- " sverified=%d"
1566
- " WHERE subscriberCode=hextoblob(%Q)",
1567
- sdonotcall,
1568
- sdigest,
1569
- ssub,
1570
- g.zIpAddr,
1571
- suname,
1572
- sverified,
1573
- zName
1574
- );
1575
- }else{
1576
- db_multi_exec(
1577
- "UPDATE subscriber SET"
1578
- " sdonotcall=%d,"
1579
- " sdigest=%d,"
1580
- " ssub=%Q,"
1581
- " mtime=strftime('%%s','now'),"
1582
- " smip=%Q"
1583
- " WHERE subscriberCode=hextoblob(%Q)",
1584
- sdonotcall,
1585
- sdigest,
1586
- ssub,
1587
- g.zIpAddr,
1588
- zName
1589
- );
1590
- }
1589
+** The subscriber is identified in several ways:
1590
+**
1591
+** (1) The name= query parameter contains the complete subscriberCode.
1592
+** This only happens when the user receives a verification
1593
+** email and clicks on the link in the email. When a
1594
+** compilete subscriberCode is seen on the name= query parameter,
1595
+** that constitutes verification of the email address.
1596
+**
1597
+** (2) The sid= query parameter contains an integer subscriberId.
1598
+** This only works for the administrator. It allows the
1599
+** administrator to edit any subscription.
1600
+**
1601
+** (3) The user is logged into an account other than "nobody" or
1602
+** "anonymous". In that case the notification settings
1603
+** associated with that account can be edited without needing
1604
+** to know the subscriber code.
1605
+**
1606
+** (4) The name= query parameter contains a 32-digit prefix of
1607
+** subscriber code. (Subscriber codes are normally 64 hex digits
1608
+** in length.) This uniquely identifies the subscriber without
1609
+** revealing the complete subscriber code, and hence without
1610
+** verifying the email address.
1611
+*/
1612
+void alert_page(void){
1613
+ const char *zName = 0; /* Value of the name= query parameter */
1614
+ Stmt q; /* For querying the database */
1615
+ int sa, sc, sf, st, sw, sx; /* Types of notifications requested */
1616
+ int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */
1617
+ int isLogin; /* True if logged in as an individual */
1618
+ const char *ssub = 0; /* Subscription flags */
1619
+ const char *semail = 0; /* Email address */
1620
+ const char *smip; /* */
1621
+ const char *suname = 0; /* Corresponding user.login value */
1622
+ const char *mtime; /* */
1623
+ const char *sctime; /* Time subscription created */
1624
+ int eErr = 0; /* Type of error */
1625
+ char *zErr = 0; /* Error message text */
1626
+ int sid = 0; /* Subscriber ID */
1627
+ int nName; /* Length of zName in bytes */
1628
+ char *zHalfCode; /* prefix of subscriberCode */
1629
+
1630
+ db_begin_transaction();
1631
+ if( alert_webpages_disabled() ){
1632
+ db_commit_transaction();
1633
+ return;
1634
+ }
1635
+ login_check_credentials();
1636
+ if( !g.perm.EmailAlert ){
1637
+ db_commit_transaction();
1638
+ login_needed(g.anon.EmailAlert);
1639
+ /*NOTREACHED*/
1640
+ }
1641
+ isLogin = login_is_individual();
1642
+ zName = P("name");
1643
+ nName = zName ? (int)strlen(zName) : 0;
1644
+ if( g.perm.Admin && P("sid")!=0 ){
1645
+ sid = atoi(P("sid"));
1646
+ }
1647
+ if( sid==0 && nName>=32 ){
1648
+ sid = db_int(0,
1649
+ "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')"
1650
+ " THEN subscriberId ELSE 0 END"
1651
+ " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)"
1652
+ " LIMIT 1", zName, zName);
1653
+ }
1654
+ if( sid==0 && isLogin ){
1655
+ sid = db_int(0, "SELECT subscriberId FROM subscriber"
1656
+ " WHERE suname=%Q", g.zLogin);
1657
+ }
1658
+ if( sid==0 ){
1659
+ db_commit_transaction();
1660
+ cgi_redirect("subscribe");
1661
+ /*NOTREACHED*/
1662
+ }
1663
+ alert_submenu_common();
1664
+ if( P("submit")!=0 && cgi_csrf_safe(1) ){
1665
+ char newSsub[10];
1666
+ int nsub = 0;
1667
+ Blob update;
1668
+
1669
+ sdonotcall = PB("sdonotcall");
1670
+ sdigest = PB("sdigest");
1671
+ semail = P("semail");
1672
+ if( PB("sa") ) newSsub[nsub++] = 'a';
1673
+ if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c';
1674
+ if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f';
1675
+ if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't';
1676
+ if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w';
1677
+ if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x';
1678
+ newSsub[nsub] = 0;
1679
+ ssub = newSsub;
1680
+ blob_init(&update, "UPDATE subscriber SET", -1);
1681
+ blob_append_sql(&update,
1682
+ " sdonotcall=%d,"
1683
+ " sdigest=%d,"
1684
+ " ssub=%Q,"
1685
+ " mtime=strftime('%%s','now'),"
1686
+ " smip=%Q",
1687
+ sdonotcall,
1688
+ sdigest,
1689
+ ssub,
1690
+ g.zIpAddr
1691
+ );
1692
+ if( g.perm.Admin ){
1693
+ suname = PT("suname");
1694
+ sverified = PB("sverified");
1695
+ if( suname && suname[0]==0 ) suname = 0;
1696
+ blob_append_sql(&update,
1697
+ ", suname=%Q,"
1698
+ " sverified=%d",
1699
+ suname,
1700
+ sverified
1701
+ );
1702
+ }
1703
+ if( isLogin ){
1704
+ if( semail==0 || email_address_is_valid(semail,0)==0 ){
1705
+ eErr = 8;
1706
+ }
1707
+ blob_append_sql(&update, ", semail=%Q", semail);
1708
+ }
1709
+ blob_append_sql(&update," WHERE subscriberId=%d", sid);
1710
+ if( eErr==0 ){
1711
+ db_exec_sql(blob_str(&update));
1712
+ ssub = 0;
1713
+ }
1714
+ blob_reset(&update);
15911715
}
15921716
if( P("delete")!=0 && cgi_csrf_safe(1) ){
15931717
if( !PB("dodelete") ){
15941718
eErr = 9;
15951719
zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
15961720
" unsubscribe");
15971721
}else{
1598
- alert_unsubscribe(zName);
1599
- return;
1722
+ alert_unsubscribe(sid);
1723
+ db_commit_transaction();
1724
+ return;
16001725
}
16011726
}
1727
+ style_header("Update Subscription");
16021728
db_prepare(&q,
16031729
"SELECT"
16041730
" semail," /* 0 */
16051731
" sverified," /* 1 */
16061732
" sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
16071733
" sdigest," /* 3 */
16081734
" ssub," /* 4 */
16091735
" smip," /* 5 */
16101736
" suname," /* 6 */
16111737
" datetime(mtime,'unixepoch')," /* 7 */
1612
- " datetime(sctime,'unixepoch')" /* 8 */
1613
- " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
1738
+ " datetime(sctime,'unixepoch')," /* 8 */
1739
+ " hex(subscriberCode)" /* 9 */
1740
+ " FROM subscriber WHERE subscriberId=%d", sid);
16141741
if( db_step(&q)!=SQLITE_ROW ){
16151742
db_finalize(&q);
1743
+ db_commit_transaction();
16161744
cgi_redirect("subscribe");
1617
- return;
1618
- }
1619
- style_header("Update Subscription");
1620
- semail = db_column_text(&q, 0);
1621
- sverified = db_column_int(&q, 1);
1622
- sdonotcall = db_column_int(&q, 2);
1623
- sdigest = db_column_int(&q, 3);
1624
- ssub = db_column_text(&q, 4);
1745
+ /*NOTREACHED*/
1746
+ }
1747
+ if( ssub==0 ){
1748
+ semail = db_column_text(&q, 0);
1749
+ sdonotcall = db_column_int(&q, 2);
1750
+ sdigest = db_column_int(&q, 3);
1751
+ ssub = db_column_text(&q, 4);
1752
+ }
1753
+ if( suname==0 ){
1754
+ suname = db_column_text(&q, 6);
1755
+ sverified = db_column_int(&q, 1);
1756
+ }
16251757
sa = strchr(ssub,'a')!=0;
16261758
sc = strchr(ssub,'c')!=0;
16271759
sf = strchr(ssub,'f')!=0;
16281760
st = strchr(ssub,'t')!=0;
16291761
sw = strchr(ssub,'w')!=0;
1762
+ sx = strchr(ssub,'x')!=0;
16301763
smip = db_column_text(&q, 5);
1631
- suname = db_column_text(&q, 6);
16321764
mtime = db_column_text(&q, 7);
16331765
sctime = db_column_text(&q, 8);
16341766
if( !g.perm.Admin && !sverified ){
1635
- db_multi_exec(
1636
- "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1637
- zName);
1638
- @ <h1>Your email alert subscription has been verified!</h1>
1639
- @ <p>Use the form below to update your subscription information.</p>
1640
- @ <p>Hint: Bookmark this page so that you can more easily update
1641
- @ your subscription information in the future</p>
1767
+ if( nName==64 ){
1768
+ db_multi_exec(
1769
+ "UPDATE subscriber SET sverified=1"
1770
+ " WHERE subscriberCode=hextoblob(%Q)",
1771
+ zName);
1772
+ if( db_get_boolean("selfreg-verify",0) ){
1773
+ char *zNewCap = db_get("default-perms","u");
1774
+ db_multi_exec(
1775
+ "UPDATE user"
1776
+ " SET cap=%Q"
1777
+ " WHERE cap='7' AND login=("
1778
+ " SELECT suname FROM subscriber"
1779
+ " WHERE subscriberCode=hextoblob(%Q))",
1780
+ zNewCap, zName
1781
+ );
1782
+ login_set_capabilities(zNewCap, 0);
1783
+ }
1784
+ @ <h1>Your email alert subscription has been verified!</h1>
1785
+ @ <p>Use the form below to update your subscription information.</p>
1786
+ @ <p>Hint: Bookmark this page so that you can more easily update
1787
+ @ your subscription information in the future</p>
1788
+ }else{
1789
+ @ <h2>Your email address is unverified</h2>
1790
+ @ <p>You should have received an email message containing a link
1791
+ @ that you must visit to verify your account. No email notifications
1792
+ @ will be sent until your email address has been verified.</p>
1793
+ }
16421794
}else{
16431795
@ <p>Make changes to the email subscription shown below and
16441796
@ press "Submit".</p>
16451797
}
16461798
form_begin(0, "%R/alerts");
1647
- @ <input type="hidden" name="name" value="%h(zName)">
1799
+ zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))"
1800
+ " FROM subscriber WHERE subscriberId=%d", sid);
1801
+ @ <input type="hidden" name="name" value="%h(zHalfCode)">
16481802
@ <table class="subscribe">
16491803
@ <tr>
16501804
@ <td class="form_label">Email&nbsp;Address:</td>
1651
- @ <td>%h(semail)</td>
1805
+ if( isLogin ){
1806
+ @ <td><input type="text" name="semail" value="%h(semail)" size="30">\
1807
+ if( eErr==8 ){
1808
+ @ <span class='loginError'>&larr; not a valid email address!</span>
1809
+ }else if( g.perm.Admin ){
1810
+ @ &nbsp;&nbsp;<a href="%R/announce?to=%t(semail)">\
1811
+ @ (Send a message to %h(semail))</a>\
1812
+ }
1813
+ @ </td>
1814
+ }else{
1815
+ @ <td>%h(semail)</td>
1816
+ }
16521817
@ </tr>
16531818
if( g.perm.Admin ){
1819
+ int uid;
16541820
@ <tr>
16551821
@ <td class='form_label'>Created:</td>
16561822
@ <td>%h(sctime)</td>
16571823
@ </tr>
16581824
@ <tr>
@@ -1662,13 +1828,21 @@
16621828
@ <tr>
16631829
@ <td class='form_label'>IP Address:</td>
16641830
@ <td>%h(smip)</td>
16651831
@ </tr>
16661832
@ <tr>
1833
+ @ <td class='form_label'>Subscriber&nbsp;Code:</td>
1834
+ @ <td>%h(db_column_text(&q,9))</td>
1835
+ @ <tr>
16671836
@ <td class="form_label">User:</td>
16681837
@ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1669
- @ size="30"></td>
1838
+ @ size="30">\
1839
+ uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname);
1840
+ if( uid ){
1841
+ @ &nbsp;&nbsp;<a href='%R/setup_uedit?id=%d(uid)'>\
1842
+ @ (login info for %h(suname))</a>\
1843
+ }
16701844
@ </tr>
16711845
}
16721846
@ <tr>
16731847
@ <td class="form_label">Topics:</td>
16741848
@ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
16781852
@ Check-ins</label><br>
16791853
}
16801854
if( g.perm.RdForum ){
16811855
@ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
16821856
@ Forum Posts</label><br>
1857
+ @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\
1858
+ @ Forum Edits</label><br>
16831859
}
16841860
if( g.perm.RdTkt ){
16851861
@ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
16861862
@ Ticket changes</label><br>
16871863
}
@@ -1695,19 +1871,15 @@
16951871
@ <td><select size="1" name="sdigest">
16961872
@ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
16971873
@ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
16981874
@ </select></td>
16991875
@ </tr>
1700
-#if 0
1701
- @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1702
- @ Daily digest only</label><br>
1703
-#endif
17041876
if( g.perm.Admin ){
17051877
@ <tr>
17061878
@ <td class="form_label">Admin Options:</td><td>
17071879
@ <label><input type="checkbox" name="sdonotcall" \
1708
- @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1880
+ @ %s(sdonotcall?"checked":"")> Do not disturb</label><br>
17091881
@ <label><input type="checkbox" name="sverified" \
17101882
@ %s(sverified?"checked":"")>\
17111883
@ Verified</label></td></tr>
17121884
}
17131885
if( eErr==9 ){
@@ -1726,10 +1898,12 @@
17261898
@ </table>
17271899
@ </form>
17281900
fossil_free(zErr);
17291901
db_finalize(&q);
17301902
style_footer();
1903
+ db_commit_transaction();
1904
+ return;
17311905
}
17321906
17331907
/* This is the message that gets sent to describe how to change
17341908
** or modify a subscription
17351909
*/
@@ -1760,25 +1934,26 @@
17601934
*/
17611935
void unsubscribe_page(void){
17621936
const char *zName = P("name");
17631937
char *zErr = 0;
17641938
int eErr = 0;
1765
- unsigned int uSeed;
1939
+ unsigned int uSeed = 0;
17661940
const char *zDecoded;
17671941
char *zCaptcha = 0;
17681942
int dx;
17691943
int bSubmit;
17701944
const char *zEAddr;
17711945
char *zCode = 0;
1946
+ int sid = 0;
17721947
17731948
/* If a valid subscriber code is supplied, then unsubscribe immediately.
17741949
*/
17751950
if( zName
1776
- && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1777
- zName)
1951
+ && (sid = db_int(0, "SELECT subscriberId FROM subscriber"
1952
+ " WHERE subscriberCode=hextoblob(%Q)", zName))!=0
17781953
){
1779
- alert_unsubscribe(zName);
1954
+ alert_unsubscribe(sid);
17801955
return;
17811956
}
17821957
17831958
/* Logged in users are redirected to the /alerts page */
17841959
login_check_credentials();
@@ -1855,10 +2030,11 @@
18552030
zDecoded = captcha_decode(uSeed);
18562031
zCaptcha = captcha_render(zDecoded);
18572032
@ <tr>
18582033
@ <td class="form_label">Security Code:</td>
18592034
@ <td><input type="text" name="captcha" value="" size="30">
2035
+ captcha_speakit_button(uSeed, "Speak the code");
18602036
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
18612037
if( eErr==2 ){
18622038
@ <td><span class="loginError">&larr; %h(zErr)</span></td>
18632039
}
18642040
@ </tr>
@@ -1874,11 +2050,11 @@
18742050
@ </tr>
18752051
@ </table>
18762052
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
18772053
@ %h(zCaptcha)
18782054
@ </pre>
1879
- @ Enter the 8 characters above in the "Security Code" box
2055
+ @ Enter the 8 characters above in the "Security Code" box<br/>
18802056
@ </td></tr></table></div>
18812057
@ </form>
18822058
fossil_free(zErr);
18832059
style_footer();
18842060
}
@@ -1885,37 +2061,69 @@
18852061
18862062
/*
18872063
** WEBPAGE: subscribers
18882064
**
18892065
** This page, accessible to administrators only,
1890
-** shows a list of email notification email addresses.
2066
+** shows a list of subscriber email addresses.
18912067
** Clicking on an email takes one to the /alerts page
18922068
** for that email where the delivery settings can be
18932069
** modified.
18942070
*/
18952071
void subscriber_list_page(void){
18962072
Blob sql;
18972073
Stmt q;
18982074
sqlite3_int64 iNow;
2075
+ int nTotal;
2076
+ int nPending;
2077
+ int nDel = 0;
18992078
if( alert_webpages_disabled() ) return;
19002079
login_check_credentials();
19012080
if( !g.perm.Admin ){
19022081
login_needed(0);
19032082
return;
19042083
}
19052084
alert_submenu_common();
2085
+ style_submenu_element("Users","setup_ulist");
19062086
style_header("Subscriber List");
2087
+ nTotal = db_int(0, "SELECT count(*) FROM subscriber");
2088
+ nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified");
2089
+ if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){
2090
+ int nNewPending;
2091
+ db_multi_exec(
2092
+ "DELETE FROM subscriber"
2093
+ " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')"
2094
+ );
2095
+ nNewPending = db_int(0, "SELECT count(*) FROM subscriber"
2096
+ " WHERE NOT sverified");
2097
+ nDel = nPending - nNewPending;
2098
+ nPending = nNewPending;
2099
+ nTotal -= nDel;
2100
+ }
2101
+ if( nPending>0 ){
2102
+ @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1>
2103
+ if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber"
2104
+ " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')")
2105
+ ){
2106
+ style_submenu_element("Purge Pending","subscribers?purge");
2107
+ }
2108
+ }else{
2109
+ @ <h1>%,d(nTotal) Subscribers</h1>
2110
+ }
2111
+ if( nDel>0 ){
2112
+ @ <p>*** %d(nDel) pending subscriptions deleted ***</p>
2113
+ }
19072114
blob_init(&sql, 0, 0);
19082115
blob_append_sql(&sql,
1909
- "SELECT hex(subscriberCode)," /* 0 */
2116
+ "SELECT subscriberId," /* 0 */
19102117
" semail," /* 1 */
19112118
" ssub," /* 2 */
19122119
" suname," /* 3 */
19132120
" sverified," /* 4 */
19142121
" sdigest," /* 5 */
19152122
" mtime," /* 6 */
1916
- " date(sctime,'unixepoch')" /* 7 */
2123
+ " date(sctime,'unixepoch')," /* 7 */
2124
+ " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */
19172125
" FROM subscriber"
19182126
);
19192127
if( P("only")!=0 ){
19202128
blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
19212129
style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
19372145
@ </tr>
19382146
@ </thead><tbody>
19392147
while( db_step(&q)==SQLITE_ROW ){
19402148
sqlite3_int64 iMtime = db_column_int64(&q, 6);
19412149
double rAge = (iNow - iMtime)/86400.0;
2150
+ int uid = db_column_int(&q, 8);
2151
+ const char *zUname = db_column_text(&q, 3);
19422152
@ <tr>
1943
- @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
2153
+ @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\
19442154
@ %h(db_column_text(&q,1))</a></td>
19452155
@ <td>%h(db_column_text(&q,2))</td>
19462156
@ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1947
- @ <td>%h(db_column_text(&q,3))</td>
2157
+ if( uid ){
2158
+ @ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a>
2159
+ }else{
2160
+ @ <td>%h(zUname)</td>
2161
+ }
19482162
@ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
19492163
@ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
19502164
@ <td>%h(db_column_text(&q,7))</td>
19512165
@ </tr>
19522166
}
@@ -1958,13 +2172,21 @@
19582172
19592173
#if LOCAL_INTERFACE
19602174
/*
19612175
** A single event that might appear in an alert is recorded as an
19622176
** instance of the following object.
2177
+**
2178
+** type values:
2179
+**
2180
+** c A new check-in
2181
+** f An original forum post
2182
+** x An edit to a prior forum post
2183
+** t A new ticket or a change to an existing ticket
2184
+** w A change to a wiki page
19632185
*/
19642186
struct EmailEvent {
1965
- int type; /* 'c', 'f', 'm', 't', 'w' */
2187
+ int type; /* 'c', 'f', 't', 'w', 'x' */
19662188
int needMod; /* Pending moderator approval */
19672189
Blob hdr; /* Header content, for forum entries */
19682190
Blob txt; /* Text description to appear in an alert */
19692191
char *zFromName; /* Human name of the sender */
19702192
EmailEvent *pNext; /* Next in chronological order */
@@ -2003,11 +2225,14 @@
20032225
20042226
20052227
/* First do non-forum post events */
20062228
db_prepare(&q,
20072229
"SELECT"
2008
- " blob.uuid," /* 0 */
2230
+ " CASE WHEN event.type='t'"
2231
+ " THEN (SELECT substr(tagname,5) FROM tag"
2232
+ " WHERE tagid=event.tagid AND tagname LIKE 'tkt-%%')"
2233
+ " ELSE blob.uuid END," /* 0 */
20092234
" datetime(event.mtime)," /* 1 */
20102235
" coalesce(ecomment,comment)"
20112236
" || ' (user: ' || coalesce(euser,user,'?')"
20122237
" || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
20132238
" FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
@@ -2036,20 +2261,20 @@
20362261
p->needMod = db_column_int(&q, 4);
20372262
p->zFromName = 0;
20382263
p->pNext = 0;
20392264
switch( p->type ){
20402265
case 'c': zType = "Check-In"; break;
2041
- case 'f': zType = "Forum post"; break;
2266
+ /* case 'f': -- forum posts omitted from this loop. See below */
20422267
case 't': zType = "Ticket Change"; break;
20432268
case 'w': zType = "Wiki Edit"; break;
20442269
}
20452270
blob_init(&p->hdr, 0, 0);
20462271
blob_init(&p->txt, 0, 0);
20472272
blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
20482273
db_column_text(&q,1),
20492274
zType,
2050
- db_column_text(&q,2),
2275
+ db_column_text(&q, 2),
20512276
zUrl,
20522277
db_column_text(&q,0)
20532278
);
20542279
if( p->needMod ){
20552280
blob_appendf(&p->txt,
@@ -2075,17 +2300,28 @@
20752300
** is a normal email alert. Construct full-text forum post alerts
20762301
** using a format that enables them to be sent as separate emails.
20772302
*/
20782303
db_prepare(&q,
20792304
"SELECT"
2080
- " forumpost.fpid," /* 0 */
2081
- " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */
2082
- " datetime(event.mtime)," /* 2 */
2083
- " substr(comment,instr(comment,':')+2)," /* 3 */
2084
- " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */
2085
- " wantalert.needMod," /* 5 */
2086
- " coalesce(trim(substr(info,1,instr(info,'<')-1)),euser,user)" /* 6 */
2305
+ " forumpost.fpid," /* 0: fpid */
2306
+ " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1: hash */
2307
+ " datetime(event.mtime)," /* 2: date/time */
2308
+ " substr(comment,instr(comment,':')+2)," /* 3: comment */
2309
+ " (WITH thread(fpid,fprev) AS ("
2310
+ " SELECT fpid,fprev FROM forumpost AS tx"
2311
+ " WHERE tx.froot=forumpost.froot),"
2312
+ " basepid(fpid,bpid) AS ("
2313
+ " SELECT fpid, fpid FROM thread WHERE fprev IS NULL"
2314
+ " UNION ALL"
2315
+ " SELECT thread.fpid, basepid.bpid FROM basepid, thread"
2316
+ " WHERE basepid.fpid=thread.fprev)"
2317
+ " SELECT uuid FROM blob, basepid"
2318
+ " WHERE basepid.fpid=forumpost.firt"
2319
+ " AND blob.rid=basepid.bpid)," /* 4: in-reply-to */
2320
+ " wantalert.needMod," /* 5: moderated */
2321
+ " coalesce(display_name(info),euser,user)," /* 6: user */
2322
+ " forumpost.fprev IS NULL" /* 7: is an edit */
20872323
" FROM temp.wantalert, event, forumpost"
20882324
" LEFT JOIN user ON (login=coalesce(euser,user))"
20892325
" WHERE event.objid=substr(wantalert.eventId,2)+0"
20902326
" AND eventId GLOB 'f*'"
20912327
" AND forumpost.fpid=event.objid"
@@ -2101,11 +2337,11 @@
21012337
const char *z;
21022338
if( pPost==0 ) continue;
21032339
p = fossil_malloc( sizeof(EmailEvent) );
21042340
pLast->pNext = p;
21052341
pLast = p;
2106
- p->type = 'f';
2342
+ p->type = db_column_int(&q,7) ? 'f' : 'x';
21072343
p->needMod = db_column_int(&q, 5);
21082344
z = db_column_text(&q,6);
21092345
p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
21102346
p->pNext = 0;
21112347
blob_init(&p->hdr, 0, 0);
@@ -2433,13 +2669,13 @@
24332669
** if the recipient is a moderator for that type of event. Setup
24342670
** and Admin users always get notified. */
24352671
char xType = '*';
24362672
if( strpbrk(zCap,"as")==0 ){
24372673
switch( p->type ){
2438
- case 'f': xType = '5'; break;
2439
- case 't': xType = 'q'; break;
2440
- case 'w': xType = 'l'; break;
2674
+ case 'x': case 'f': xType = '5'; break;
2675
+ case 't': xType = 'q'; break;
2676
+ case 'w': xType = 'l'; break;
24412677
}
24422678
if( strchr(zCap,xType)==0 ) continue;
24432679
}
24442680
}else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
24452681
/* Setup and admin users can get any notification that does not
@@ -2447,14 +2683,14 @@
24472683
}else{
24482684
/* Other users only see the alert if they have sufficient
24492685
** privilege to view the event itself */
24502686
char xType = '*';
24512687
switch( p->type ){
2452
- case 'c': xType = 'o'; break;
2453
- case 'f': xType = '2'; break;
2454
- case 't': xType = 'r'; break;
2455
- case 'w': xType = 'j'; break;
2688
+ case 'c': xType = 'o'; break;
2689
+ case 'x': case 'f': xType = '2'; break;
2690
+ case 't': xType = 'r'; break;
2691
+ case 'w': xType = 'j'; break;
24562692
}
24572693
if( strchr(zCap,xType)==0 ) continue;
24582694
}
24592695
if( blob_size(&p->hdr)>0 ){
24602696
/* This alert should be sent as a separate email */
@@ -2591,10 +2827,11 @@
25912827
@ <table class="subscribe">
25922828
if( zCaptcha ){
25932829
@ <tr>
25942830
@ <td class="form_label">Security&nbsp;Code:</td>
25952831
@ <td><input type="text" name="captcha" value="" size="10">
2832
+ captcha_speakit_button(uSeed, "Speak the code");
25962833
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
25972834
@ </tr>
25982835
}
25992836
@ <tr>
26002837
@ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
@@ -2617,11 +2854,11 @@
26172854
@ </table>
26182855
if( zCaptcha ){
26192856
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
26202857
@ %h(zCaptcha)
26212858
@ </pre>
2622
- @ Enter the 8 characters above in the "Security Code" box
2859
+ @ Enter the 8 characters above in the "Security Code" box<br/>
26232860
@ </td></tr></table></div>
26242861
}
26252862
@ </form>
26262863
style_footer();
26272864
}
@@ -2635,10 +2872,11 @@
26352872
char *zErr;
26362873
const char *zTo = PT("to");
26372874
char *zSubject = PT("subject");
26382875
int bAll = PB("all");
26392876
int bAA = PB("aa");
2877
+ int bMods = PB("mods");
26402878
const char *zSub = db_get("email-subname", "[Fossil Repo]");
26412879
int bTest2 = fossil_strcmp(P("name"),"test2")==0;
26422880
Blob hdr, body;
26432881
blob_init(&body, 0, 0);
26442882
blob_init(&hdr, 0, 0);
@@ -2646,17 +2884,29 @@
26462884
pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
26472885
if( zTo[0] ){
26482886
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
26492887
alert_send(pSender, &hdr, &body, 0);
26502888
}
2651
- if( bAll || bAA ){
2889
+ if( bAll || bAA || bMods ){
26522890
Stmt q;
26532891
int nUsed = blob_size(&body);
26542892
const char *zURL = db_get("email-url",0);
2655
- db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2656
- " WHERE sverified AND NOT sdonotcall %s",
2657
- bAll ? "" : " AND ssub LIKE '%a%'");
2893
+ if( bAll ){
2894
+ db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2895
+ " WHERE sverified AND NOT sdonotcall");
2896
+ }else if( bAA ){
2897
+ db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2898
+ " WHERE sverified AND NOT sdonotcall"
2899
+ " AND ssub LIKE '%%a%%'");
2900
+ }else if( bMods ){
2901
+ db_prepare(&q,
2902
+ "SELECT semail, hex(subscriberCode)"
2903
+ " FROM subscriber, user "
2904
+ " WHERE sverified AND NOT sdonotcall"
2905
+ " AND suname=login"
2906
+ " AND fullcap(cap) GLOB '*5*'");
2907
+ }
26582908
while( db_step(&q)==SQLITE_ROW ){
26592909
const char *zCode = db_column_text(&q, 1);
26602910
zTo = db_column_text(&q, 0);
26612911
blob_truncate(&hdr, 0);
26622912
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
@@ -2713,11 +2963,12 @@
27132963
@ <p>The following error was reported by the system:
27142964
@ <blockquote><pre>
27152965
@ %h(zErr)
27162966
@ </pre></blockquote>
27172967
}else{
2718
- @ <p>The announcement has been sent.</p>
2968
+ @ <p>The announcement has been sent.
2969
+ @ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p>
27192970
}
27202971
style_footer();
27212972
return;
27222973
} else if( !alert_enabled() ){
27232974
style_header("Cannot Send Announcement");
@@ -2731,21 +2982,26 @@
27312982
@ <form method="POST">
27322983
@ <table class="subscribe">
27332984
if( g.perm.Admin ){
27342985
int aa = PB("aa");
27352986
int all = PB("all");
2987
+ int aMod = PB("mods");
27362988
const char *aack = aa ? "checked" : "";
27372989
const char *allck = all ? "checked" : "";
2990
+ const char *modck = aMod ? "checked" : "";
27382991
@ <tr>
27392992
@ <td class="form_label">To:</td>
27402993
@ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
27412994
@ <label><input type="checkbox" name="aa" %s(aack)> \
27422995
@ All "announcement" subscribers</label> \
27432996
@ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
27442997
@ <label><input type="checkbox" name="all" %s(allck)> \
27452998
@ All subscribers</label> \
2746
- @ <a href="%R/subscribers" target="_blank">(list)</a><br></td>
2999
+ @ <a href="%R/subscribers" target="_blank">(list)</a><br>
3000
+ @ <label><input type="checkbox" name="mods" %s(modck)> \
3001
+ @ All moderators</label> \
3002
+ @ <a href="%R/setup_ulist?with=5" target="_blank">(list)</a><br></td>
27473003
@ </tr>
27483004
}
27493005
@ <tr>
27503006
@ <td class="form_label">Subject:</td>
27513007
@ <td><input type="text" name="subject" value="%h(PT("subject"))"\
@@ -2756,11 +3012,15 @@
27563012
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
27573013
@ %h(PT("msg"))</textarea>
27583014
@ </tr>
27593015
@ <tr>
27603016
@ <td></td>
2761
- @ <td><input type="submit" name="submit" value="Send Message">
3017
+ if( fossil_strcmp(P("name"),"test2")==0 ){
3018
+ @ <td><input type="submit" name="submit" value="Dry Run">
3019
+ }else{
3020
+ @ <td><input type="submit" name="submit" value="Send Message">
3021
+ }
27623022
@ </tr>
27633023
@ </table>
27643024
@ </form>
27653025
style_footer();
27663026
}
27673027
--- src/alerts.c
+++ src/alerts.c
@@ -46,10 +46,11 @@
46 @ --
47 @ -- The ssub field is a string where each character indicates a particular
48 @ -- type of event to subscribe to. Choices:
49 @ -- a - Announcements
50 @ -- c - Check-ins
 
51 @ -- t - Ticket changes
52 @ -- w - Wiki changes
53 @ -- Probably different codes will be added in the future. In the future
54 @ -- we might also add a separate table that allows subscribing to email
55 @ -- notifications for specific branches or tags or tickets.
@@ -110,15 +111,15 @@
110 ** if the email-send-method is something other than "off".
111 */
112 void alert_schema(int bOnlyIfEnabled){
113 if( !alert_tables_exist() ){
114 if( bOnlyIfEnabled
115 && fossil_strcmp(db_get("email-send-method","off"),"off")==0
116 ){
117 return; /* Don't create table for disabled email */
118 }
119 db_multi_exec(zAlertInit/*works-like:""*/);
120 alert_triggers_enable();
121 }else if( !db_table_has_column("repository","pending_alert","sentMod") ){
122 db_multi_exec(
123 "ALTER TABLE repository.pending_alert"
124 " ADD COLUMN sentMod BOOLEAN DEFAULT false;"
@@ -158,11 +159,11 @@
158 /*
159 ** Return true if email alerts are active.
160 */
161 int alert_enabled(void){
162 if( !alert_tables_exist() ) return 0;
163 if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
164 return 1;
165 }
166
167 /*
168 ** If the subscriber table does not exist, then paint an error message
@@ -183,11 +184,11 @@
183 ** is an administrator.
184 */
185 void alert_submenu_common(void){
186 if( g.perm.Admin ){
187 if( fossil_strcmp(g.zPath,"subscribers") ){
188 style_submenu_element("List Subscribers","%R/subscribers");
189 }
190 if( fossil_strcmp(g.zPath,"subscribe") ){
191 style_submenu_element("Add New Subscriber","%R/subscribe");
192 }
193 }
@@ -238,10 +239,17 @@
238 @ This is URL used as the basename for hyperlinks included in
239 @ email alert text. Omit the trailing "/".
240 @ Suggested value: "%h(g.zBaseURL)"
241 @ (Property: "email-url")</p>
242 @ <hr>
 
 
 
 
 
 
 
243
244 entry_attribute("\"Return-Path\" email address", 20, "email-self",
245 "eself", "", 0);
246 @ <p><b>Required.</b>
247 @ This is the email to which email notification bounces should be sent.
@@ -297,17 +305,10 @@
297 @ append a colon and TCP port number (ex: smtp.example.com:587).
298 @ The default TCP port number is 25.
299 @ (Property: "email-send-relayhost")</p>
300 @ <hr>
301
302 entry_attribute("Administrator email address", 40, "email-admin",
303 "eadmin", "", 0);
304 @ <p>This is the email for the human administrator for the system.
305 @ Abuse and trouble reports are send here.
306 @ (Property: "email-admin")</p>
307 @ <hr>
308
309 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
310 @ </div></form>
311 db_end_transaction(0);
312 style_footer();
313 }
@@ -482,11 +483,11 @@
482 blob_init(&p->out, 0, 0);
483 p->mFlags = mFlags;
484 if( zAltDest ){
485 p->zDest = zAltDest;
486 }else{
487 p->zDest = db_get("email-send-method","off");
488 }
489 if( fossil_strcmp(p->zDest,"off")==0 ) return p;
490 if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
491 if( fossil_strcmp(p->zDest,"db")==0 ){
492 char *zErr;
@@ -574,22 +575,18 @@
574 }
575 return 0;
576 }
577
578 /*
579 ** Make a copy of the input string up to but not including the
580 ** first cTerm character.
581 **
582 ** Verify that the string really that is to be copied really is a
583 ** valid email address. If it is not, then return NULL.
584 **
585 ** This routine is more restrictive than necessary. It does not
586 ** allow comments, IP address, quoted strings, or certain uncommon
587 ** characters. The only non-alphanumerics allowed in the local
588 ** part are "_", "+", "-" and "+".
589 */
590 char *email_copy_addr(const char *z, char cTerm ){
591 int i;
592 int nAt = 0;
593 int nDot = 0;
594 char c;
595 if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */
@@ -619,13 +616,28 @@
619 }
620 }
621 if( c!=cTerm ) return 0; /* Missing terminator */
622 if( nAt==0 ) return 0; /* No "@" found anywhere */
623 if( nDot==0 ) return 0; /* No "." in the domain */
 
 
624
625 /* If we reach this point, the email address is valid */
626 return mprintf("%.*s", i, z);
 
 
 
 
 
 
 
 
 
 
 
 
 
627 }
628
629 /*
630 ** Scan the input string for a valid email address enclosed in <...>
631 ** If the string contains one or more email addresses, extract the first
@@ -659,10 +671,38 @@
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 */
@@ -762,11 +802,11 @@
762 ** Date:
763 ** Message-Id:
764 ** Content-Type:
765 ** Content-Transfer-Encoding:
766 ** MIME-Version:
767 ** X-Fossil-From:
768 **
769 ** The caller maintains ownership of the input Blobs. This routine will
770 ** read the Blobs and send them onward to the email system, but it will
771 ** not free them.
772 **
@@ -774,14 +814,14 @@
774 ** in the pHdr parameter.
775 **
776 ** If the zFromName argument is not NULL, then it should be a human-readable
777 ** name or handle for the sender. In that case, "From:" becomes a made-up
778 ** email address based on a hash of zFromName and the domain of email-self,
779 ** and an additional "X-Fossil-From:" field is inserted with the email-self
780 ** address. Downstream software might use the X-Fossil-From header to set
781 ** the envelope-from address of the email. If zFromName is a NULL pointer,
782 ** then the "From:" is set to the email-self value and X-Fossil-From is
783 ** omitted.
784 */
785 void alert_send(
786 AlertSender *p, /* Emailer context */
787 Blob *pHdr, /* Email header (incomplete) */
@@ -794,26 +834,26 @@
794 fossil_print("Sending email\n");
795 }
796 if( fossil_strcmp(p->zDest, "off")==0 ){
797 return;
798 }
 
799 if( fossil_strcmp(p->zDest, "blob")==0 ){
800 pOut = &p->out;
801 if( blob_size(pOut) ){
802 blob_appendf(pOut, "%.72c\n", '=');
803 }
804 }else{
805 blob_init(&all, 0, 0);
806 pOut = &all;
807 }
808 blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
809 if( p->zFrom==0 || p->zFrom[0]==0 ){
810 return; /* email-self is not set. Error will be reported separately */
811 }else if( zFromName ){
812 blob_appendf(pOut, "From: %s <%s@%s>\r\n",
813 zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
814 blob_appendf(pOut, "X-Fossil-From: <%s>\r\n", p->zFrom);
815 }else{
816 blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
817 }
818 blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
819 if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
@@ -879,10 +919,26 @@
879 fossil_print("%s", blob_str(&all));
880 }
881 blob_reset(&all);
882 }
883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884 /*
885 ** SETTING: email-send-method width=5 default=off
886 ** Determine the method used to send email. Allowed values are
887 ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
888 ** means no email is ever sent. The "relay" value means emails are sent
@@ -1152,10 +1208,19 @@
1152 int i, j, n;
1153 char c;
1154
1155 *peErr = 0;
1156 *pzErr = 0;
 
 
 
 
 
 
 
 
 
1157
1158 /* Check the validity of the email address.
1159 **
1160 ** (1) Exactly one '@' character.
1161 ** (2) No other characters besides [a-zA-Z0-9._+-]
@@ -1163,11 +1228,15 @@
1163 ** The local part is currently more restrictive than RFC 5322 allows:
1164 ** https://stackoverflow.com/a/2049510/142454 We will expand this as
1165 ** necessary.
1166 */
1167 zEAddr = P("e");
1168 if( zEAddr==0 ) return 0;
 
 
 
 
1169 for(i=j=n=0; (c = zEAddr[i])!=0; i++){
1170 if( c=='@' ){
1171 n = i;
1172 j++;
1173 continue;
@@ -1193,14 +1262,13 @@
1193 *peErr = 1;
1194 *pzErr = mprintf("email domain too short");
1195 return 0;
1196 }
1197
1198 /* Verify the captcha */
1199 if( needCaptcha && !captcha_is_correct(1) ){
1200 *peErr = 2;
1201 *pzErr = mprintf("incorrect security code");
1202 return 0;
1203 }
1204
1205 /* Check to make sure the email address is available for reuse */
1206 if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
@@ -1259,11 +1327,11 @@
1259 ** page. To allow anonymous passers-by to sign up for email
1260 ** notification, set Email-Alerts on user "nobody" or "anonymous".
1261 */
1262 void subscribe_page(void){
1263 int needCaptcha;
1264 unsigned int uSeed;
1265 const char *zDecoded;
1266 char *zCaptcha = 0;
1267 char *zErr = 0;
1268 int eErr = 0;
1269 int di;
@@ -1291,10 +1359,14 @@
1291 /* Everybody else jumps to the page to administer their own
1292 ** account only. */
1293 cgi_redirectf("%R/alerts");
1294 return;
1295 }
 
 
 
 
1296 }
1297 alert_submenu_common();
1298 needCaptcha = !login_is_individual();
1299 if( P("submit")
1300 && cgi_csrf_safe(1)
@@ -1312,10 +1384,11 @@
1312 if( PB("sa") ) ssub[nsub++] = 'a';
1313 if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1314 if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1315 if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1316 if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
 
1317 ssub[nsub] = 0;
1318 db_multi_exec(
1319 "INSERT INTO subscriber(semail,suname,"
1320 " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
1321 "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
@@ -1332,11 +1405,15 @@
1332 id);
1333 if( !needCaptcha ){
1334 /* The new subscription has been added on behalf of a logged-in user.
1335 ** No verification is required. Jump immediately to /alerts page.
1336 */
1337 cgi_redirectf("%R/alerts/%s", zCode);
 
 
 
 
1338 return;
1339 }else{
1340 /* We need to send a verification email */
1341 Blob hdr, body;
1342 AlertSender *pSender = alert_sender_new(0,0);
@@ -1354,11 +1431,11 @@
1354 @ <blockquote><pre>
1355 @ %h(pSender->zErr)
1356 @ </pre></blockquote>
1357 }else{
1358 @ <p>An email has been sent to "%h(zEAddr)". That email contains a
1359 @ hyperlink that you must click on in order to activate your
1360 @ subscription.</p>
1361 }
1362 alert_sender_free(pSender);
1363 style_footer();
1364 }
@@ -1386,16 +1463,23 @@
1386 if( eErr==1 ){
1387 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1388 }
1389 @ </tr>
1390 if( needCaptcha ){
1391 uSeed = captcha_seed();
 
 
 
 
 
 
1392 zDecoded = captcha_decode(uSeed);
1393 zCaptcha = captcha_render(zDecoded);
1394 @ <tr>
1395 @ <td class="form_label">Security Code:</td>
1396 @ <td><input type="text" name="captcha" value="" size="30">
 
1397 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1398 @ </tr>
1399 if( eErr==2 ){
1400 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1401 }
@@ -1421,10 +1505,12 @@
1421 @ Check-ins</label><br>
1422 }
1423 if( g.perm.RdForum ){
1424 @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
1425 @ Forum Posts</label><br>
 
 
1426 }
1427 if( g.perm.RdTkt ){
1428 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
1429 @ Ticket changes</label><br>
1430 }
@@ -1461,11 +1547,11 @@
1461 @ </table>
1462 if( needCaptcha ){
1463 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1464 @ %h(zCaptcha)
1465 @ </pre>
1466 @ Enter the 8 characters above in the "Security Code" box
1467 @ </td></tr></table></div>
1468 }
1469 @ </form>
1470 fossil_free(zErr);
1471 style_footer();
@@ -1474,21 +1560,20 @@
1474 /*
1475 ** Either shutdown or completely delete a subscription entry given
1476 ** by the hex value zName. Then paint a webpage that explains that
1477 ** the entry has been removed.
1478 */
1479 static void alert_unsubscribe(const char *zName){
1480 char *zEmail;
1481 zEmail = db_text(0, "SELECT semail FROM subscriber"
1482 " WHERE subscriberCode=hextoblob(%Q)", zName);
1483 if( zEmail==0 ){
1484 style_header("Unsubscribe Fail");
1485 @ <p>Unable to locate a subscriber with the requested key</p>
1486 }else{
1487 db_multi_exec(
1488 "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1489 zName
1490 );
1491 style_header("Unsubscribed");
1492 @ <p>The "%h(zEmail)" email address has been delisted.
1493 @ All traces of that email address have been removed</p>
1494 }
@@ -1499,108 +1584,149 @@
1499 /*
1500 ** WEBPAGE: alerts
1501 **
1502 ** Edit email alert and notification settings.
1503 **
1504 ** The subscriber is identified in either of two ways:
1505 **
1506 ** (1) The name= query parameter contains the subscriberCode.
1507 **
1508 ** (2) The user is logged into an account other than "nobody" or
1509 ** "anonymous". In that case the notification settings
1510 ** associated with that account can be edited without needing
1511 ** to know the subscriber code.
1512 */
1513 void alert_page(void){
1514 const char *zName = P("name");
1515 Stmt q;
1516 int sa, sc, sf, st, sw;
1517 int sdigest, sdonotcall, sverified;
1518 const char *ssub;
1519 const char *semail;
1520 const char *smip;
1521 const char *suname;
1522 const char *mtime;
1523 const char *sctime;
1524 int eErr = 0;
1525 char *zErr = 0;
1526
1527 if( alert_webpages_disabled() ) return;
1528 login_check_credentials();
1529 if( !g.perm.EmailAlert ){
1530 login_needed(g.anon.EmailAlert);
1531 return;
1532 }
1533 if( zName==0 && login_is_individual() ){
1534 zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
1535 " WHERE suname=%Q", g.zLogin);
1536 }
1537 if( zName==0 || !validate16(zName, -1) ){
1538 cgi_redirect("subscribe");
1539 return;
1540 }
1541 alert_submenu_common();
1542 if( P("submit")!=0 && cgi_csrf_safe(1) ){
1543 int sdonotcall = PB("sdonotcall");
1544 int sdigest = PB("sdigest");
1545 char ssub[10];
1546 int nsub = 0;
1547 if( PB("sa") ) ssub[nsub++] = 'a';
1548 if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1549 if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1550 if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1551 if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1552 ssub[nsub] = 0;
1553 if( g.perm.Admin ){
1554 const char *suname = PT("suname");
1555 int sverified = PB("sverified");
1556 if( suname && suname[0]==0 ) suname = 0;
1557 db_multi_exec(
1558 "UPDATE subscriber SET"
1559 " sdonotcall=%d,"
1560 " sdigest=%d,"
1561 " ssub=%Q,"
1562 " mtime=strftime('%%s','now'),"
1563 " smip=%Q,"
1564 " suname=%Q,"
1565 " sverified=%d"
1566 " WHERE subscriberCode=hextoblob(%Q)",
1567 sdonotcall,
1568 sdigest,
1569 ssub,
1570 g.zIpAddr,
1571 suname,
1572 sverified,
1573 zName
1574 );
1575 }else{
1576 db_multi_exec(
1577 "UPDATE subscriber SET"
1578 " sdonotcall=%d,"
1579 " sdigest=%d,"
1580 " ssub=%Q,"
1581 " mtime=strftime('%%s','now'),"
1582 " smip=%Q"
1583 " WHERE subscriberCode=hextoblob(%Q)",
1584 sdonotcall,
1585 sdigest,
1586 ssub,
1587 g.zIpAddr,
1588 zName
1589 );
1590 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1591 }
1592 if( P("delete")!=0 && cgi_csrf_safe(1) ){
1593 if( !PB("dodelete") ){
1594 eErr = 9;
1595 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
1596 " unsubscribe");
1597 }else{
1598 alert_unsubscribe(zName);
1599 return;
 
1600 }
1601 }
 
1602 db_prepare(&q,
1603 "SELECT"
1604 " semail," /* 0 */
1605 " sverified," /* 1 */
1606 " sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
1607 " sdigest," /* 3 */
1608 " ssub," /* 4 */
1609 " smip," /* 5 */
1610 " suname," /* 6 */
1611 " datetime(mtime,'unixepoch')," /* 7 */
1612 " datetime(sctime,'unixepoch')" /* 8 */
1613 " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
 
1614 if( db_step(&q)!=SQLITE_ROW ){
1615 db_finalize(&q);
 
1616 cgi_redirect("subscribe");
1617 return;
1618 }
1619 style_header("Update Subscription");
1620 semail = db_column_text(&q, 0);
1621 sverified = db_column_int(&q, 1);
1622 sdonotcall = db_column_int(&q, 2);
1623 sdigest = db_column_int(&q, 3);
1624 ssub = db_column_text(&q, 4);
 
 
 
 
1625 sa = strchr(ssub,'a')!=0;
1626 sc = strchr(ssub,'c')!=0;
1627 sf = strchr(ssub,'f')!=0;
1628 st = strchr(ssub,'t')!=0;
1629 sw = strchr(ssub,'w')!=0;
 
1630 smip = db_column_text(&q, 5);
1631 suname = db_column_text(&q, 6);
1632 mtime = db_column_text(&q, 7);
1633 sctime = db_column_text(&q, 8);
1634 if( !g.perm.Admin && !sverified ){
1635 db_multi_exec(
1636 "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1637 zName);
1638 @ <h1>Your email alert subscription has been verified!</h1>
1639 @ <p>Use the form below to update your subscription information.</p>
1640 @ <p>Hint: Bookmark this page so that you can more easily update
1641 @ your subscription information in the future</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1642 }else{
1643 @ <p>Make changes to the email subscription shown below and
1644 @ press "Submit".</p>
1645 }
1646 form_begin(0, "%R/alerts");
1647 @ <input type="hidden" name="name" value="%h(zName)">
 
 
1648 @ <table class="subscribe">
1649 @ <tr>
1650 @ <td class="form_label">Email&nbsp;Address:</td>
1651 @ <td>%h(semail)</td>
 
 
 
 
 
 
 
 
 
 
 
1652 @ </tr>
1653 if( g.perm.Admin ){
 
1654 @ <tr>
1655 @ <td class='form_label'>Created:</td>
1656 @ <td>%h(sctime)</td>
1657 @ </tr>
1658 @ <tr>
@@ -1662,13 +1828,21 @@
1662 @ <tr>
1663 @ <td class='form_label'>IP Address:</td>
1664 @ <td>%h(smip)</td>
1665 @ </tr>
1666 @ <tr>
 
 
 
1667 @ <td class="form_label">User:</td>
1668 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1669 @ size="30"></td>
 
 
 
 
 
1670 @ </tr>
1671 }
1672 @ <tr>
1673 @ <td class="form_label">Topics:</td>
1674 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
1678 @ Check-ins</label><br>
1679 }
1680 if( g.perm.RdForum ){
1681 @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
1682 @ Forum Posts</label><br>
 
 
1683 }
1684 if( g.perm.RdTkt ){
1685 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1686 @ Ticket changes</label><br>
1687 }
@@ -1695,19 +1871,15 @@
1695 @ <td><select size="1" name="sdigest">
1696 @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
1697 @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
1698 @ </select></td>
1699 @ </tr>
1700 #if 0
1701 @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1702 @ Daily digest only</label><br>
1703 #endif
1704 if( g.perm.Admin ){
1705 @ <tr>
1706 @ <td class="form_label">Admin Options:</td><td>
1707 @ <label><input type="checkbox" name="sdonotcall" \
1708 @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1709 @ <label><input type="checkbox" name="sverified" \
1710 @ %s(sverified?"checked":"")>\
1711 @ Verified</label></td></tr>
1712 }
1713 if( eErr==9 ){
@@ -1726,10 +1898,12 @@
1726 @ </table>
1727 @ </form>
1728 fossil_free(zErr);
1729 db_finalize(&q);
1730 style_footer();
 
 
1731 }
1732
1733 /* This is the message that gets sent to describe how to change
1734 ** or modify a subscription
1735 */
@@ -1760,25 +1934,26 @@
1760 */
1761 void unsubscribe_page(void){
1762 const char *zName = P("name");
1763 char *zErr = 0;
1764 int eErr = 0;
1765 unsigned int uSeed;
1766 const char *zDecoded;
1767 char *zCaptcha = 0;
1768 int dx;
1769 int bSubmit;
1770 const char *zEAddr;
1771 char *zCode = 0;
 
1772
1773 /* If a valid subscriber code is supplied, then unsubscribe immediately.
1774 */
1775 if( zName
1776 && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1777 zName)
1778 ){
1779 alert_unsubscribe(zName);
1780 return;
1781 }
1782
1783 /* Logged in users are redirected to the /alerts page */
1784 login_check_credentials();
@@ -1855,10 +2030,11 @@
1855 zDecoded = captcha_decode(uSeed);
1856 zCaptcha = captcha_render(zDecoded);
1857 @ <tr>
1858 @ <td class="form_label">Security Code:</td>
1859 @ <td><input type="text" name="captcha" value="" size="30">
 
1860 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1861 if( eErr==2 ){
1862 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1863 }
1864 @ </tr>
@@ -1874,11 +2050,11 @@
1874 @ </tr>
1875 @ </table>
1876 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1877 @ %h(zCaptcha)
1878 @ </pre>
1879 @ Enter the 8 characters above in the "Security Code" box
1880 @ </td></tr></table></div>
1881 @ </form>
1882 fossil_free(zErr);
1883 style_footer();
1884 }
@@ -1885,37 +2061,69 @@
1885
1886 /*
1887 ** WEBPAGE: subscribers
1888 **
1889 ** This page, accessible to administrators only,
1890 ** shows a list of email notification email addresses.
1891 ** Clicking on an email takes one to the /alerts page
1892 ** for that email where the delivery settings can be
1893 ** modified.
1894 */
1895 void subscriber_list_page(void){
1896 Blob sql;
1897 Stmt q;
1898 sqlite3_int64 iNow;
 
 
 
1899 if( alert_webpages_disabled() ) return;
1900 login_check_credentials();
1901 if( !g.perm.Admin ){
1902 login_needed(0);
1903 return;
1904 }
1905 alert_submenu_common();
 
1906 style_header("Subscriber List");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1907 blob_init(&sql, 0, 0);
1908 blob_append_sql(&sql,
1909 "SELECT hex(subscriberCode)," /* 0 */
1910 " semail," /* 1 */
1911 " ssub," /* 2 */
1912 " suname," /* 3 */
1913 " sverified," /* 4 */
1914 " sdigest," /* 5 */
1915 " mtime," /* 6 */
1916 " date(sctime,'unixepoch')" /* 7 */
 
1917 " FROM subscriber"
1918 );
1919 if( P("only")!=0 ){
1920 blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
1921 style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
1937 @ </tr>
1938 @ </thead><tbody>
1939 while( db_step(&q)==SQLITE_ROW ){
1940 sqlite3_int64 iMtime = db_column_int64(&q, 6);
1941 double rAge = (iNow - iMtime)/86400.0;
 
 
1942 @ <tr>
1943 @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
1944 @ %h(db_column_text(&q,1))</a></td>
1945 @ <td>%h(db_column_text(&q,2))</td>
1946 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1947 @ <td>%h(db_column_text(&q,3))</td>
 
 
 
 
1948 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
1949 @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
1950 @ <td>%h(db_column_text(&q,7))</td>
1951 @ </tr>
1952 }
@@ -1958,13 +2172,21 @@
1958
1959 #if LOCAL_INTERFACE
1960 /*
1961 ** A single event that might appear in an alert is recorded as an
1962 ** instance of the following object.
 
 
 
 
 
 
 
 
1963 */
1964 struct EmailEvent {
1965 int type; /* 'c', 'f', 'm', 't', 'w' */
1966 int needMod; /* Pending moderator approval */
1967 Blob hdr; /* Header content, for forum entries */
1968 Blob txt; /* Text description to appear in an alert */
1969 char *zFromName; /* Human name of the sender */
1970 EmailEvent *pNext; /* Next in chronological order */
@@ -2003,11 +2225,14 @@
2003
2004
2005 /* First do non-forum post events */
2006 db_prepare(&q,
2007 "SELECT"
2008 " blob.uuid," /* 0 */
 
 
 
2009 " datetime(event.mtime)," /* 1 */
2010 " coalesce(ecomment,comment)"
2011 " || ' (user: ' || coalesce(euser,user,'?')"
2012 " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
2013 " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
@@ -2036,20 +2261,20 @@
2036 p->needMod = db_column_int(&q, 4);
2037 p->zFromName = 0;
2038 p->pNext = 0;
2039 switch( p->type ){
2040 case 'c': zType = "Check-In"; break;
2041 case 'f': zType = "Forum post"; break;
2042 case 't': zType = "Ticket Change"; break;
2043 case 'w': zType = "Wiki Edit"; break;
2044 }
2045 blob_init(&p->hdr, 0, 0);
2046 blob_init(&p->txt, 0, 0);
2047 blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
2048 db_column_text(&q,1),
2049 zType,
2050 db_column_text(&q,2),
2051 zUrl,
2052 db_column_text(&q,0)
2053 );
2054 if( p->needMod ){
2055 blob_appendf(&p->txt,
@@ -2075,17 +2300,28 @@
2075 ** is a normal email alert. Construct full-text forum post alerts
2076 ** using a format that enables them to be sent as separate emails.
2077 */
2078 db_prepare(&q,
2079 "SELECT"
2080 " forumpost.fpid," /* 0 */
2081 " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */
2082 " datetime(event.mtime)," /* 2 */
2083 " substr(comment,instr(comment,':')+2)," /* 3 */
2084 " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */
2085 " wantalert.needMod," /* 5 */
2086 " coalesce(trim(substr(info,1,instr(info,'<')-1)),euser,user)" /* 6 */
 
 
 
 
 
 
 
 
 
 
 
2087 " FROM temp.wantalert, event, forumpost"
2088 " LEFT JOIN user ON (login=coalesce(euser,user))"
2089 " WHERE event.objid=substr(wantalert.eventId,2)+0"
2090 " AND eventId GLOB 'f*'"
2091 " AND forumpost.fpid=event.objid"
@@ -2101,11 +2337,11 @@
2101 const char *z;
2102 if( pPost==0 ) continue;
2103 p = fossil_malloc( sizeof(EmailEvent) );
2104 pLast->pNext = p;
2105 pLast = p;
2106 p->type = 'f';
2107 p->needMod = db_column_int(&q, 5);
2108 z = db_column_text(&q,6);
2109 p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
2110 p->pNext = 0;
2111 blob_init(&p->hdr, 0, 0);
@@ -2433,13 +2669,13 @@
2433 ** if the recipient is a moderator for that type of event. Setup
2434 ** and Admin users always get notified. */
2435 char xType = '*';
2436 if( strpbrk(zCap,"as")==0 ){
2437 switch( p->type ){
2438 case 'f': xType = '5'; break;
2439 case 't': xType = 'q'; break;
2440 case 'w': xType = 'l'; break;
2441 }
2442 if( strchr(zCap,xType)==0 ) continue;
2443 }
2444 }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
2445 /* Setup and admin users can get any notification that does not
@@ -2447,14 +2683,14 @@
2447 }else{
2448 /* Other users only see the alert if they have sufficient
2449 ** privilege to view the event itself */
2450 char xType = '*';
2451 switch( p->type ){
2452 case 'c': xType = 'o'; break;
2453 case 'f': xType = '2'; break;
2454 case 't': xType = 'r'; break;
2455 case 'w': xType = 'j'; break;
2456 }
2457 if( strchr(zCap,xType)==0 ) continue;
2458 }
2459 if( blob_size(&p->hdr)>0 ){
2460 /* This alert should be sent as a separate email */
@@ -2591,10 +2827,11 @@
2591 @ <table class="subscribe">
2592 if( zCaptcha ){
2593 @ <tr>
2594 @ <td class="form_label">Security&nbsp;Code:</td>
2595 @ <td><input type="text" name="captcha" value="" size="10">
 
2596 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
2597 @ </tr>
2598 }
2599 @ <tr>
2600 @ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
@@ -2617,11 +2854,11 @@
2617 @ </table>
2618 if( zCaptcha ){
2619 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2620 @ %h(zCaptcha)
2621 @ </pre>
2622 @ Enter the 8 characters above in the "Security Code" box
2623 @ </td></tr></table></div>
2624 }
2625 @ </form>
2626 style_footer();
2627 }
@@ -2635,10 +2872,11 @@
2635 char *zErr;
2636 const char *zTo = PT("to");
2637 char *zSubject = PT("subject");
2638 int bAll = PB("all");
2639 int bAA = PB("aa");
 
2640 const char *zSub = db_get("email-subname", "[Fossil Repo]");
2641 int bTest2 = fossil_strcmp(P("name"),"test2")==0;
2642 Blob hdr, body;
2643 blob_init(&body, 0, 0);
2644 blob_init(&hdr, 0, 0);
@@ -2646,17 +2884,29 @@
2646 pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
2647 if( zTo[0] ){
2648 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
2649 alert_send(pSender, &hdr, &body, 0);
2650 }
2651 if( bAll || bAA ){
2652 Stmt q;
2653 int nUsed = blob_size(&body);
2654 const char *zURL = db_get("email-url",0);
2655 db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2656 " WHERE sverified AND NOT sdonotcall %s",
2657 bAll ? "" : " AND ssub LIKE '%a%'");
 
 
 
 
 
 
 
 
 
 
 
 
2658 while( db_step(&q)==SQLITE_ROW ){
2659 const char *zCode = db_column_text(&q, 1);
2660 zTo = db_column_text(&q, 0);
2661 blob_truncate(&hdr, 0);
2662 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
@@ -2713,11 +2963,12 @@
2713 @ <p>The following error was reported by the system:
2714 @ <blockquote><pre>
2715 @ %h(zErr)
2716 @ </pre></blockquote>
2717 }else{
2718 @ <p>The announcement has been sent.</p>
 
2719 }
2720 style_footer();
2721 return;
2722 } else if( !alert_enabled() ){
2723 style_header("Cannot Send Announcement");
@@ -2731,21 +2982,26 @@
2731 @ <form method="POST">
2732 @ <table class="subscribe">
2733 if( g.perm.Admin ){
2734 int aa = PB("aa");
2735 int all = PB("all");
 
2736 const char *aack = aa ? "checked" : "";
2737 const char *allck = all ? "checked" : "";
 
2738 @ <tr>
2739 @ <td class="form_label">To:</td>
2740 @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
2741 @ <label><input type="checkbox" name="aa" %s(aack)> \
2742 @ All "announcement" subscribers</label> \
2743 @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
2744 @ <label><input type="checkbox" name="all" %s(allck)> \
2745 @ All subscribers</label> \
2746 @ <a href="%R/subscribers" target="_blank">(list)</a><br></td>
 
 
 
2747 @ </tr>
2748 }
2749 @ <tr>
2750 @ <td class="form_label">Subject:</td>
2751 @ <td><input type="text" name="subject" value="%h(PT("subject"))"\
@@ -2756,11 +3012,15 @@
2756 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
2757 @ %h(PT("msg"))</textarea>
2758 @ </tr>
2759 @ <tr>
2760 @ <td></td>
2761 @ <td><input type="submit" name="submit" value="Send Message">
 
 
 
 
2762 @ </tr>
2763 @ </table>
2764 @ </form>
2765 style_footer();
2766 }
2767
--- src/alerts.c
+++ src/alerts.c
@@ -46,10 +46,11 @@
46 @ --
47 @ -- The ssub field is a string where each character indicates a particular
48 @ -- type of event to subscribe to. Choices:
49 @ -- a - Announcements
50 @ -- c - Check-ins
51 @ -- f - Forum posts
52 @ -- t - Ticket changes
53 @ -- w - Wiki changes
54 @ -- Probably different codes will be added in the future. In the future
55 @ -- we might also add a separate table that allows subscribing to email
56 @ -- notifications for specific branches or tags or tickets.
@@ -110,15 +111,15 @@
111 ** if the email-send-method is something other than "off".
112 */
113 void alert_schema(int bOnlyIfEnabled){
114 if( !alert_tables_exist() ){
115 if( bOnlyIfEnabled
116 && fossil_strcmp(db_get("email-send-method",0),"off")==0
117 ){
118 return; /* Don't create table for disabled email */
119 }
120 db_exec_sql(zAlertInit);
121 alert_triggers_enable();
122 }else if( !db_table_has_column("repository","pending_alert","sentMod") ){
123 db_multi_exec(
124 "ALTER TABLE repository.pending_alert"
125 " ADD COLUMN sentMod BOOLEAN DEFAULT false;"
@@ -158,11 +159,11 @@
159 /*
160 ** Return true if email alerts are active.
161 */
162 int alert_enabled(void){
163 if( !alert_tables_exist() ) return 0;
164 if( fossil_strcmp(db_get("email-send-method",0),"off")==0 ) return 0;
165 return 1;
166 }
167
168 /*
169 ** If the subscriber table does not exist, then paint an error message
@@ -183,11 +184,11 @@
184 ** is an administrator.
185 */
186 void alert_submenu_common(void){
187 if( g.perm.Admin ){
188 if( fossil_strcmp(g.zPath,"subscribers") ){
189 style_submenu_element("Subscribers","%R/subscribers");
190 }
191 if( fossil_strcmp(g.zPath,"subscribe") ){
192 style_submenu_element("Add New Subscriber","%R/subscribe");
193 }
194 }
@@ -238,10 +239,17 @@
239 @ This is URL used as the basename for hyperlinks included in
240 @ email alert text. Omit the trailing "/".
241 @ Suggested value: "%h(g.zBaseURL)"
242 @ (Property: "email-url")</p>
243 @ <hr>
244
245 entry_attribute("Administrator email address", 40, "email-admin",
246 "eadmin", "", 0);
247 @ <p>This is the email for the human administrator for the system.
248 @ Abuse and trouble reports and password reset requests are send here.
249 @ (Property: "email-admin")</p>
250 @ <hr>
251
252 entry_attribute("\"Return-Path\" email address", 20, "email-self",
253 "eself", "", 0);
254 @ <p><b>Required.</b>
255 @ This is the email to which email notification bounces should be sent.
@@ -297,17 +305,10 @@
305 @ append a colon and TCP port number (ex: smtp.example.com:587).
306 @ The default TCP port number is 25.
307 @ (Property: "email-send-relayhost")</p>
308 @ <hr>
309
 
 
 
 
 
 
 
310 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
311 @ </div></form>
312 db_end_transaction(0);
313 style_footer();
314 }
@@ -482,11 +483,11 @@
483 blob_init(&p->out, 0, 0);
484 p->mFlags = mFlags;
485 if( zAltDest ){
486 p->zDest = zAltDest;
487 }else{
488 p->zDest = db_get("email-send-method",0);
489 }
490 if( fossil_strcmp(p->zDest,"off")==0 ) return p;
491 if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
492 if( fossil_strcmp(p->zDest,"db")==0 ){
493 char *zErr;
@@ -574,22 +575,18 @@
575 }
576 return 0;
577 }
578
579 /*
580 ** Determine whether or not the input string is a valid email address.
581 ** Only look at character up to but not including the first \000 or
582 ** the first cTerm character, whichever comes first.
583 **
584 ** Return the length of the email addresss string in bytes if the email
585 ** address is valid. If the email address is misformed, return 0.
586 */
587 int email_address_is_valid(const char *z, char cTerm){
 
 
 
 
588 int i;
589 int nAt = 0;
590 int nDot = 0;
591 char c;
592 if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */
@@ -619,13 +616,28 @@
616 }
617 }
618 if( c!=cTerm ) return 0; /* Missing terminator */
619 if( nAt==0 ) return 0; /* No "@" found anywhere */
620 if( nDot==0 ) return 0; /* No "." in the domain */
621 return i;
622 }
623
624 /*
625 ** Make a copy of the input string up to but not including the
626 ** first cTerm character.
627 **
628 ** Verify that the string really that is to be copied really is a
629 ** valid email address. If it is not, then return NULL.
630 **
631 ** This routine is more restrictive than necessary. It does not
632 ** allow comments, IP address, quoted strings, or certain uncommon
633 ** characters. The only non-alphanumerics allowed in the local
634 ** part are "_", "+", "-" and "+".
635 */
636 char *email_copy_addr(const char *z, char cTerm ){
637 int i = email_address_is_valid(z, cTerm);
638 return i==0 ? 0 : mprintf("%.*s", i, z);
639 }
640
641 /*
642 ** Scan the input string for a valid email address enclosed in <...>
643 ** If the string contains one or more email addresses, extract the first
@@ -659,10 +671,38 @@
671 char *zOut = alert_find_emailaddr(zIn);
672 if( zOut ){
673 sqlite3_result_text(context, zOut, -1, fossil_free);
674 }
675 }
676
677 /*
678 ** SQL function: display_name(X)
679 **
680 ** If X is a string, search for a user name at the beginning of that
681 ** string. The user name must be followed by an email address. If
682 ** found, return the user name. If not found, return NULL.
683 **
684 ** This routine is used to extract the display name from the USER.INFO
685 ** field.
686 */
687 void alert_display_name_func(
688 sqlite3_context *context,
689 int argc,
690 sqlite3_value **argv
691 ){
692 const char *zIn = (const char*)sqlite3_value_text(argv[0]);
693 int i;
694 if( zIn==0 ) return;
695 while( fossil_isspace(zIn[0]) ) zIn++;
696 for(i=0; zIn[i] && zIn[i]!='<' && zIn[i]!='\n'; i++){}
697 if( zIn[i]=='<' ){
698 while( i>0 && fossil_isspace(zIn[i-1]) ){ i--; }
699 if( i>0 ){
700 sqlite3_result_text(context, zIn, i, SQLITE_TRANSIENT);
701 }
702 }
703 }
704
705 /*
706 ** Return the hostname portion of an email address - the part following
707 ** the @
708 */
@@ -762,11 +802,11 @@
802 ** Date:
803 ** Message-Id:
804 ** Content-Type:
805 ** Content-Transfer-Encoding:
806 ** MIME-Version:
807 ** Sender:
808 **
809 ** The caller maintains ownership of the input Blobs. This routine will
810 ** read the Blobs and send them onward to the email system, but it will
811 ** not free them.
812 **
@@ -774,14 +814,14 @@
814 ** in the pHdr parameter.
815 **
816 ** If the zFromName argument is not NULL, then it should be a human-readable
817 ** name or handle for the sender. In that case, "From:" becomes a made-up
818 ** email address based on a hash of zFromName and the domain of email-self,
819 ** and an additional "Sender:" field is inserted with the email-self
820 ** address. Downstream software might use the Sender header to set
821 ** the envelope-from address of the email. If zFromName is a NULL pointer,
822 ** then the "From:" is set to the email-self value and Sender is
823 ** omitted.
824 */
825 void alert_send(
826 AlertSender *p, /* Emailer context */
827 Blob *pHdr, /* Email header (incomplete) */
@@ -794,26 +834,26 @@
834 fossil_print("Sending email\n");
835 }
836 if( fossil_strcmp(p->zDest, "off")==0 ){
837 return;
838 }
839 blob_init(&all, 0, 0);
840 if( fossil_strcmp(p->zDest, "blob")==0 ){
841 pOut = &p->out;
842 if( blob_size(pOut) ){
843 blob_appendf(pOut, "%.72c\n", '=');
844 }
845 }else{
 
846 pOut = &all;
847 }
848 blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
849 if( p->zFrom==0 || p->zFrom[0]==0 ){
850 return; /* email-self is not set. Error will be reported separately */
851 }else if( zFromName ){
852 blob_appendf(pOut, "From: %s <%s@%s>\r\n",
853 zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
854 blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
855 }else{
856 blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
857 }
858 blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
859 if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
@@ -879,10 +919,26 @@
919 fossil_print("%s", blob_str(&all));
920 }
921 blob_reset(&all);
922 }
923
924 /*
925 ** SETTING: email-url width=40
926 ** This URL is used as the basename for hyperlinks included in email alert
927 ** text. Omit the trailing "/".
928 */
929 /*
930 ** SETTING: email-admin width=40
931 ** This is the email address for the human administrator for the system.
932 ** Abuse and trouble reports and password reset requests are send here.
933 */
934 /*
935 ** SETTING: email-subname width=16
936 ** This is a short name used to identifies the repository in the Subject:
937 ** line of email alerts. Traditionally this name is included in square
938 ** brackets. Examples: "[fossil-src]", "[sqlite-src]".
939 */
940 /*
941 ** SETTING: email-send-method width=5 default=off
942 ** Determine the method used to send email. Allowed values are
943 ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
944 ** means no email is ever sent. The "relay" value means emails are sent
@@ -1152,10 +1208,19 @@
1208 int i, j, n;
1209 char c;
1210
1211 *peErr = 0;
1212 *pzErr = 0;
1213
1214 /* Verify the captcha first */
1215 if( needCaptcha ){
1216 if( !captcha_is_correct(1) ){
1217 *peErr = 2;
1218 *pzErr = mprintf("incorrect security code");
1219 return 0;
1220 }
1221 }
1222
1223 /* Check the validity of the email address.
1224 **
1225 ** (1) Exactly one '@' character.
1226 ** (2) No other characters besides [a-zA-Z0-9._+-]
@@ -1163,11 +1228,15 @@
1228 ** The local part is currently more restrictive than RFC 5322 allows:
1229 ** https://stackoverflow.com/a/2049510/142454 We will expand this as
1230 ** necessary.
1231 */
1232 zEAddr = P("e");
1233 if( zEAddr==0 ){
1234 *peErr = 1;
1235 *pzErr = mprintf("required");
1236 return 0;
1237 }
1238 for(i=j=n=0; (c = zEAddr[i])!=0; i++){
1239 if( c=='@' ){
1240 n = i;
1241 j++;
1242 continue;
@@ -1193,14 +1262,13 @@
1262 *peErr = 1;
1263 *pzErr = mprintf("email domain too short");
1264 return 0;
1265 }
1266
1267 if( authorized_subscription_email(zEAddr)==0 ){
1268 *peErr = 1;
1269 *pzErr = mprintf("not an authorized email address");
 
1270 return 0;
1271 }
1272
1273 /* Check to make sure the email address is available for reuse */
1274 if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
@@ -1259,11 +1327,11 @@
1327 ** page. To allow anonymous passers-by to sign up for email
1328 ** notification, set Email-Alerts on user "nobody" or "anonymous".
1329 */
1330 void subscribe_page(void){
1331 int needCaptcha;
1332 unsigned int uSeed = 0;
1333 const char *zDecoded;
1334 char *zCaptcha = 0;
1335 char *zErr = 0;
1336 int eErr = 0;
1337 int di;
@@ -1291,10 +1359,14 @@
1359 /* Everybody else jumps to the page to administer their own
1360 ** account only. */
1361 cgi_redirectf("%R/alerts");
1362 return;
1363 }
1364 }
1365 if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){
1366 register_page();
1367 return;
1368 }
1369 alert_submenu_common();
1370 needCaptcha = !login_is_individual();
1371 if( P("submit")
1372 && cgi_csrf_safe(1)
@@ -1312,10 +1384,11 @@
1384 if( PB("sa") ) ssub[nsub++] = 'a';
1385 if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1386 if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1387 if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1388 if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1389 if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x';
1390 ssub[nsub] = 0;
1391 db_multi_exec(
1392 "INSERT INTO subscriber(semail,suname,"
1393 " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
1394 "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
@@ -1332,11 +1405,15 @@
1405 id);
1406 if( !needCaptcha ){
1407 /* The new subscription has been added on behalf of a logged-in user.
1408 ** No verification is required. Jump immediately to /alerts page.
1409 */
1410 if( g.perm.Admin ){
1411 cgi_redirectf("%R/alerts/%.32s", zCode);
1412 }else{
1413 cgi_redirectf("%R/alerts");
1414 }
1415 return;
1416 }else{
1417 /* We need to send a verification email */
1418 Blob hdr, body;
1419 AlertSender *pSender = alert_sender_new(0,0);
@@ -1354,11 +1431,11 @@
1431 @ <blockquote><pre>
1432 @ %h(pSender->zErr)
1433 @ </pre></blockquote>
1434 }else{
1435 @ <p>An email has been sent to "%h(zEAddr)". That email contains a
1436 @ hyperlink that you must click to activate your
1437 @ subscription.</p>
1438 }
1439 alert_sender_free(pSender);
1440 style_footer();
1441 }
@@ -1386,16 +1463,23 @@
1463 if( eErr==1 ){
1464 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1465 }
1466 @ </tr>
1467 if( needCaptcha ){
1468 const char *zInit = "";
1469 if( P("captchaseed")!=0 && eErr!=2 ){
1470 uSeed = strtoul(P("captchaseed"),0,10);
1471 zInit = P("captcha");
1472 }else{
1473 uSeed = captcha_seed();
1474 }
1475 zDecoded = captcha_decode(uSeed);
1476 zCaptcha = captcha_render(zDecoded);
1477 @ <tr>
1478 @ <td class="form_label">Security Code:</td>
1479 @ <td><input type="text" name="captcha" value="%h(zInit)" size="30">
1480 captcha_speakit_button(uSeed, "Speak the code");
1481 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1482 @ </tr>
1483 if( eErr==2 ){
1484 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1485 }
@@ -1421,10 +1505,12 @@
1505 @ Check-ins</label><br>
1506 }
1507 if( g.perm.RdForum ){
1508 @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
1509 @ Forum Posts</label><br>
1510 @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \
1511 @ Forum Edits</label><br>
1512 }
1513 if( g.perm.RdTkt ){
1514 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
1515 @ Ticket changes</label><br>
1516 }
@@ -1461,11 +1547,11 @@
1547 @ </table>
1548 if( needCaptcha ){
1549 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1550 @ %h(zCaptcha)
1551 @ </pre>
1552 @ Enter the 8 characters above in the "Security Code" box<br/>
1553 @ </td></tr></table></div>
1554 }
1555 @ </form>
1556 fossil_free(zErr);
1557 style_footer();
@@ -1474,21 +1560,20 @@
1560 /*
1561 ** Either shutdown or completely delete a subscription entry given
1562 ** by the hex value zName. Then paint a webpage that explains that
1563 ** the entry has been removed.
1564 */
1565 static void alert_unsubscribe(int sid){
1566 char *zEmail;
1567 zEmail = db_text(0, "SELECT semail FROM subscriber"
1568 " WHERE subscriberId=%d", sid);
1569 if( zEmail==0 ){
1570 style_header("Unsubscribe Fail");
1571 @ <p>Unable to locate a subscriber with the requested key</p>
1572 }else{
1573 db_multi_exec(
1574 "DELETE FROM subscriber WHERE subscriberId=%d", sid
 
1575 );
1576 style_header("Unsubscribed");
1577 @ <p>The "%h(zEmail)" email address has been delisted.
1578 @ All traces of that email address have been removed</p>
1579 }
@@ -1499,108 +1584,149 @@
1584 /*
1585 ** WEBPAGE: alerts
1586 **
1587 ** Edit email alert and notification settings.
1588 **
1589 ** The subscriber is identified in several ways:
1590 **
1591 ** (1) The name= query parameter contains the complete subscriberCode.
1592 ** This only happens when the user receives a verification
1593 ** email and clicks on the link in the email. When a
1594 ** compilete subscriberCode is seen on the name= query parameter,
1595 ** that constitutes verification of the email address.
1596 **
1597 ** (2) The sid= query parameter contains an integer subscriberId.
1598 ** This only works for the administrator. It allows the
1599 ** administrator to edit any subscription.
1600 **
1601 ** (3) The user is logged into an account other than "nobody" or
1602 ** "anonymous". In that case the notification settings
1603 ** associated with that account can be edited without needing
1604 ** to know the subscriber code.
1605 **
1606 ** (4) The name= query parameter contains a 32-digit prefix of
1607 ** subscriber code. (Subscriber codes are normally 64 hex digits
1608 ** in length.) This uniquely identifies the subscriber without
1609 ** revealing the complete subscriber code, and hence without
1610 ** verifying the email address.
1611 */
1612 void alert_page(void){
1613 const char *zName = 0; /* Value of the name= query parameter */
1614 Stmt q; /* For querying the database */
1615 int sa, sc, sf, st, sw, sx; /* Types of notifications requested */
1616 int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */
1617 int isLogin; /* True if logged in as an individual */
1618 const char *ssub = 0; /* Subscription flags */
1619 const char *semail = 0; /* Email address */
1620 const char *smip; /* */
1621 const char *suname = 0; /* Corresponding user.login value */
1622 const char *mtime; /* */
1623 const char *sctime; /* Time subscription created */
1624 int eErr = 0; /* Type of error */
1625 char *zErr = 0; /* Error message text */
1626 int sid = 0; /* Subscriber ID */
1627 int nName; /* Length of zName in bytes */
1628 char *zHalfCode; /* prefix of subscriberCode */
1629
1630 db_begin_transaction();
1631 if( alert_webpages_disabled() ){
1632 db_commit_transaction();
1633 return;
1634 }
1635 login_check_credentials();
1636 if( !g.perm.EmailAlert ){
1637 db_commit_transaction();
1638 login_needed(g.anon.EmailAlert);
1639 /*NOTREACHED*/
1640 }
1641 isLogin = login_is_individual();
1642 zName = P("name");
1643 nName = zName ? (int)strlen(zName) : 0;
1644 if( g.perm.Admin && P("sid")!=0 ){
1645 sid = atoi(P("sid"));
1646 }
1647 if( sid==0 && nName>=32 ){
1648 sid = db_int(0,
1649 "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')"
1650 " THEN subscriberId ELSE 0 END"
1651 " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)"
1652 " LIMIT 1", zName, zName);
1653 }
1654 if( sid==0 && isLogin ){
1655 sid = db_int(0, "SELECT subscriberId FROM subscriber"
1656 " WHERE suname=%Q", g.zLogin);
1657 }
1658 if( sid==0 ){
1659 db_commit_transaction();
1660 cgi_redirect("subscribe");
1661 /*NOTREACHED*/
1662 }
1663 alert_submenu_common();
1664 if( P("submit")!=0 && cgi_csrf_safe(1) ){
1665 char newSsub[10];
1666 int nsub = 0;
1667 Blob update;
1668
1669 sdonotcall = PB("sdonotcall");
1670 sdigest = PB("sdigest");
1671 semail = P("semail");
1672 if( PB("sa") ) newSsub[nsub++] = 'a';
1673 if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c';
1674 if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f';
1675 if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't';
1676 if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w';
1677 if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x';
1678 newSsub[nsub] = 0;
1679 ssub = newSsub;
1680 blob_init(&update, "UPDATE subscriber SET", -1);
1681 blob_append_sql(&update,
1682 " sdonotcall=%d,"
1683 " sdigest=%d,"
1684 " ssub=%Q,"
1685 " mtime=strftime('%%s','now'),"
1686 " smip=%Q",
1687 sdonotcall,
1688 sdigest,
1689 ssub,
1690 g.zIpAddr
1691 );
1692 if( g.perm.Admin ){
1693 suname = PT("suname");
1694 sverified = PB("sverified");
1695 if( suname && suname[0]==0 ) suname = 0;
1696 blob_append_sql(&update,
1697 ", suname=%Q,"
1698 " sverified=%d",
1699 suname,
1700 sverified
1701 );
1702 }
1703 if( isLogin ){
1704 if( semail==0 || email_address_is_valid(semail,0)==0 ){
1705 eErr = 8;
1706 }
1707 blob_append_sql(&update, ", semail=%Q", semail);
1708 }
1709 blob_append_sql(&update," WHERE subscriberId=%d", sid);
1710 if( eErr==0 ){
1711 db_exec_sql(blob_str(&update));
1712 ssub = 0;
1713 }
1714 blob_reset(&update);
1715 }
1716 if( P("delete")!=0 && cgi_csrf_safe(1) ){
1717 if( !PB("dodelete") ){
1718 eErr = 9;
1719 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
1720 " unsubscribe");
1721 }else{
1722 alert_unsubscribe(sid);
1723 db_commit_transaction();
1724 return;
1725 }
1726 }
1727 style_header("Update Subscription");
1728 db_prepare(&q,
1729 "SELECT"
1730 " semail," /* 0 */
1731 " sverified," /* 1 */
1732 " sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
1733 " sdigest," /* 3 */
1734 " ssub," /* 4 */
1735 " smip," /* 5 */
1736 " suname," /* 6 */
1737 " datetime(mtime,'unixepoch')," /* 7 */
1738 " datetime(sctime,'unixepoch')," /* 8 */
1739 " hex(subscriberCode)" /* 9 */
1740 " FROM subscriber WHERE subscriberId=%d", sid);
1741 if( db_step(&q)!=SQLITE_ROW ){
1742 db_finalize(&q);
1743 db_commit_transaction();
1744 cgi_redirect("subscribe");
1745 /*NOTREACHED*/
1746 }
1747 if( ssub==0 ){
1748 semail = db_column_text(&q, 0);
1749 sdonotcall = db_column_int(&q, 2);
1750 sdigest = db_column_int(&q, 3);
1751 ssub = db_column_text(&q, 4);
1752 }
1753 if( suname==0 ){
1754 suname = db_column_text(&q, 6);
1755 sverified = db_column_int(&q, 1);
1756 }
1757 sa = strchr(ssub,'a')!=0;
1758 sc = strchr(ssub,'c')!=0;
1759 sf = strchr(ssub,'f')!=0;
1760 st = strchr(ssub,'t')!=0;
1761 sw = strchr(ssub,'w')!=0;
1762 sx = strchr(ssub,'x')!=0;
1763 smip = db_column_text(&q, 5);
 
1764 mtime = db_column_text(&q, 7);
1765 sctime = db_column_text(&q, 8);
1766 if( !g.perm.Admin && !sverified ){
1767 if( nName==64 ){
1768 db_multi_exec(
1769 "UPDATE subscriber SET sverified=1"
1770 " WHERE subscriberCode=hextoblob(%Q)",
1771 zName);
1772 if( db_get_boolean("selfreg-verify",0) ){
1773 char *zNewCap = db_get("default-perms","u");
1774 db_multi_exec(
1775 "UPDATE user"
1776 " SET cap=%Q"
1777 " WHERE cap='7' AND login=("
1778 " SELECT suname FROM subscriber"
1779 " WHERE subscriberCode=hextoblob(%Q))",
1780 zNewCap, zName
1781 );
1782 login_set_capabilities(zNewCap, 0);
1783 }
1784 @ <h1>Your email alert subscription has been verified!</h1>
1785 @ <p>Use the form below to update your subscription information.</p>
1786 @ <p>Hint: Bookmark this page so that you can more easily update
1787 @ your subscription information in the future</p>
1788 }else{
1789 @ <h2>Your email address is unverified</h2>
1790 @ <p>You should have received an email message containing a link
1791 @ that you must visit to verify your account. No email notifications
1792 @ will be sent until your email address has been verified.</p>
1793 }
1794 }else{
1795 @ <p>Make changes to the email subscription shown below and
1796 @ press "Submit".</p>
1797 }
1798 form_begin(0, "%R/alerts");
1799 zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))"
1800 " FROM subscriber WHERE subscriberId=%d", sid);
1801 @ <input type="hidden" name="name" value="%h(zHalfCode)">
1802 @ <table class="subscribe">
1803 @ <tr>
1804 @ <td class="form_label">Email&nbsp;Address:</td>
1805 if( isLogin ){
1806 @ <td><input type="text" name="semail" value="%h(semail)" size="30">\
1807 if( eErr==8 ){
1808 @ <span class='loginError'>&larr; not a valid email address!</span>
1809 }else if( g.perm.Admin ){
1810 @ &nbsp;&nbsp;<a href="%R/announce?to=%t(semail)">\
1811 @ (Send a message to %h(semail))</a>\
1812 }
1813 @ </td>
1814 }else{
1815 @ <td>%h(semail)</td>
1816 }
1817 @ </tr>
1818 if( g.perm.Admin ){
1819 int uid;
1820 @ <tr>
1821 @ <td class='form_label'>Created:</td>
1822 @ <td>%h(sctime)</td>
1823 @ </tr>
1824 @ <tr>
@@ -1662,13 +1828,21 @@
1828 @ <tr>
1829 @ <td class='form_label'>IP Address:</td>
1830 @ <td>%h(smip)</td>
1831 @ </tr>
1832 @ <tr>
1833 @ <td class='form_label'>Subscriber&nbsp;Code:</td>
1834 @ <td>%h(db_column_text(&q,9))</td>
1835 @ <tr>
1836 @ <td class="form_label">User:</td>
1837 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1838 @ size="30">\
1839 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname);
1840 if( uid ){
1841 @ &nbsp;&nbsp;<a href='%R/setup_uedit?id=%d(uid)'>\
1842 @ (login info for %h(suname))</a>\
1843 }
1844 @ </tr>
1845 }
1846 @ <tr>
1847 @ <td class="form_label">Topics:</td>
1848 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
1852 @ Check-ins</label><br>
1853 }
1854 if( g.perm.RdForum ){
1855 @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
1856 @ Forum Posts</label><br>
1857 @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\
1858 @ Forum Edits</label><br>
1859 }
1860 if( g.perm.RdTkt ){
1861 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1862 @ Ticket changes</label><br>
1863 }
@@ -1695,19 +1871,15 @@
1871 @ <td><select size="1" name="sdigest">
1872 @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
1873 @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
1874 @ </select></td>
1875 @ </tr>
 
 
 
 
1876 if( g.perm.Admin ){
1877 @ <tr>
1878 @ <td class="form_label">Admin Options:</td><td>
1879 @ <label><input type="checkbox" name="sdonotcall" \
1880 @ %s(sdonotcall?"checked":"")> Do not disturb</label><br>
1881 @ <label><input type="checkbox" name="sverified" \
1882 @ %s(sverified?"checked":"")>\
1883 @ Verified</label></td></tr>
1884 }
1885 if( eErr==9 ){
@@ -1726,10 +1898,12 @@
1898 @ </table>
1899 @ </form>
1900 fossil_free(zErr);
1901 db_finalize(&q);
1902 style_footer();
1903 db_commit_transaction();
1904 return;
1905 }
1906
1907 /* This is the message that gets sent to describe how to change
1908 ** or modify a subscription
1909 */
@@ -1760,25 +1934,26 @@
1934 */
1935 void unsubscribe_page(void){
1936 const char *zName = P("name");
1937 char *zErr = 0;
1938 int eErr = 0;
1939 unsigned int uSeed = 0;
1940 const char *zDecoded;
1941 char *zCaptcha = 0;
1942 int dx;
1943 int bSubmit;
1944 const char *zEAddr;
1945 char *zCode = 0;
1946 int sid = 0;
1947
1948 /* If a valid subscriber code is supplied, then unsubscribe immediately.
1949 */
1950 if( zName
1951 && (sid = db_int(0, "SELECT subscriberId FROM subscriber"
1952 " WHERE subscriberCode=hextoblob(%Q)", zName))!=0
1953 ){
1954 alert_unsubscribe(sid);
1955 return;
1956 }
1957
1958 /* Logged in users are redirected to the /alerts page */
1959 login_check_credentials();
@@ -1855,10 +2030,11 @@
2030 zDecoded = captcha_decode(uSeed);
2031 zCaptcha = captcha_render(zDecoded);
2032 @ <tr>
2033 @ <td class="form_label">Security Code:</td>
2034 @ <td><input type="text" name="captcha" value="" size="30">
2035 captcha_speakit_button(uSeed, "Speak the code");
2036 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
2037 if( eErr==2 ){
2038 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
2039 }
2040 @ </tr>
@@ -1874,11 +2050,11 @@
2050 @ </tr>
2051 @ </table>
2052 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2053 @ %h(zCaptcha)
2054 @ </pre>
2055 @ Enter the 8 characters above in the "Security Code" box<br/>
2056 @ </td></tr></table></div>
2057 @ </form>
2058 fossil_free(zErr);
2059 style_footer();
2060 }
@@ -1885,37 +2061,69 @@
2061
2062 /*
2063 ** WEBPAGE: subscribers
2064 **
2065 ** This page, accessible to administrators only,
2066 ** shows a list of subscriber email addresses.
2067 ** Clicking on an email takes one to the /alerts page
2068 ** for that email where the delivery settings can be
2069 ** modified.
2070 */
2071 void subscriber_list_page(void){
2072 Blob sql;
2073 Stmt q;
2074 sqlite3_int64 iNow;
2075 int nTotal;
2076 int nPending;
2077 int nDel = 0;
2078 if( alert_webpages_disabled() ) return;
2079 login_check_credentials();
2080 if( !g.perm.Admin ){
2081 login_needed(0);
2082 return;
2083 }
2084 alert_submenu_common();
2085 style_submenu_element("Users","setup_ulist");
2086 style_header("Subscriber List");
2087 nTotal = db_int(0, "SELECT count(*) FROM subscriber");
2088 nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified");
2089 if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){
2090 int nNewPending;
2091 db_multi_exec(
2092 "DELETE FROM subscriber"
2093 " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')"
2094 );
2095 nNewPending = db_int(0, "SELECT count(*) FROM subscriber"
2096 " WHERE NOT sverified");
2097 nDel = nPending - nNewPending;
2098 nPending = nNewPending;
2099 nTotal -= nDel;
2100 }
2101 if( nPending>0 ){
2102 @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1>
2103 if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber"
2104 " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')")
2105 ){
2106 style_submenu_element("Purge Pending","subscribers?purge");
2107 }
2108 }else{
2109 @ <h1>%,d(nTotal) Subscribers</h1>
2110 }
2111 if( nDel>0 ){
2112 @ <p>*** %d(nDel) pending subscriptions deleted ***</p>
2113 }
2114 blob_init(&sql, 0, 0);
2115 blob_append_sql(&sql,
2116 "SELECT subscriberId," /* 0 */
2117 " semail," /* 1 */
2118 " ssub," /* 2 */
2119 " suname," /* 3 */
2120 " sverified," /* 4 */
2121 " sdigest," /* 5 */
2122 " mtime," /* 6 */
2123 " date(sctime,'unixepoch')," /* 7 */
2124 " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */
2125 " FROM subscriber"
2126 );
2127 if( P("only")!=0 ){
2128 blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
2129 style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
2145 @ </tr>
2146 @ </thead><tbody>
2147 while( db_step(&q)==SQLITE_ROW ){
2148 sqlite3_int64 iMtime = db_column_int64(&q, 6);
2149 double rAge = (iNow - iMtime)/86400.0;
2150 int uid = db_column_int(&q, 8);
2151 const char *zUname = db_column_text(&q, 3);
2152 @ <tr>
2153 @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\
2154 @ %h(db_column_text(&q,1))</a></td>
2155 @ <td>%h(db_column_text(&q,2))</td>
2156 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
2157 if( uid ){
2158 @ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a>
2159 }else{
2160 @ <td>%h(zUname)</td>
2161 }
2162 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
2163 @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
2164 @ <td>%h(db_column_text(&q,7))</td>
2165 @ </tr>
2166 }
@@ -1958,13 +2172,21 @@
2172
2173 #if LOCAL_INTERFACE
2174 /*
2175 ** A single event that might appear in an alert is recorded as an
2176 ** instance of the following object.
2177 **
2178 ** type values:
2179 **
2180 ** c A new check-in
2181 ** f An original forum post
2182 ** x An edit to a prior forum post
2183 ** t A new ticket or a change to an existing ticket
2184 ** w A change to a wiki page
2185 */
2186 struct EmailEvent {
2187 int type; /* 'c', 'f', 't', 'w', 'x' */
2188 int needMod; /* Pending moderator approval */
2189 Blob hdr; /* Header content, for forum entries */
2190 Blob txt; /* Text description to appear in an alert */
2191 char *zFromName; /* Human name of the sender */
2192 EmailEvent *pNext; /* Next in chronological order */
@@ -2003,11 +2225,14 @@
2225
2226
2227 /* First do non-forum post events */
2228 db_prepare(&q,
2229 "SELECT"
2230 " CASE WHEN event.type='t'"
2231 " THEN (SELECT substr(tagname,5) FROM tag"
2232 " WHERE tagid=event.tagid AND tagname LIKE 'tkt-%%')"
2233 " ELSE blob.uuid END," /* 0 */
2234 " datetime(event.mtime)," /* 1 */
2235 " coalesce(ecomment,comment)"
2236 " || ' (user: ' || coalesce(euser,user,'?')"
2237 " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
2238 " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
@@ -2036,20 +2261,20 @@
2261 p->needMod = db_column_int(&q, 4);
2262 p->zFromName = 0;
2263 p->pNext = 0;
2264 switch( p->type ){
2265 case 'c': zType = "Check-In"; break;
2266 /* case 'f': -- forum posts omitted from this loop. See below */
2267 case 't': zType = "Ticket Change"; break;
2268 case 'w': zType = "Wiki Edit"; break;
2269 }
2270 blob_init(&p->hdr, 0, 0);
2271 blob_init(&p->txt, 0, 0);
2272 blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
2273 db_column_text(&q,1),
2274 zType,
2275 db_column_text(&q, 2),
2276 zUrl,
2277 db_column_text(&q,0)
2278 );
2279 if( p->needMod ){
2280 blob_appendf(&p->txt,
@@ -2075,17 +2300,28 @@
2300 ** is a normal email alert. Construct full-text forum post alerts
2301 ** using a format that enables them to be sent as separate emails.
2302 */
2303 db_prepare(&q,
2304 "SELECT"
2305 " forumpost.fpid," /* 0: fpid */
2306 " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1: hash */
2307 " datetime(event.mtime)," /* 2: date/time */
2308 " substr(comment,instr(comment,':')+2)," /* 3: comment */
2309 " (WITH thread(fpid,fprev) AS ("
2310 " SELECT fpid,fprev FROM forumpost AS tx"
2311 " WHERE tx.froot=forumpost.froot),"
2312 " basepid(fpid,bpid) AS ("
2313 " SELECT fpid, fpid FROM thread WHERE fprev IS NULL"
2314 " UNION ALL"
2315 " SELECT thread.fpid, basepid.bpid FROM basepid, thread"
2316 " WHERE basepid.fpid=thread.fprev)"
2317 " SELECT uuid FROM blob, basepid"
2318 " WHERE basepid.fpid=forumpost.firt"
2319 " AND blob.rid=basepid.bpid)," /* 4: in-reply-to */
2320 " wantalert.needMod," /* 5: moderated */
2321 " coalesce(display_name(info),euser,user)," /* 6: user */
2322 " forumpost.fprev IS NULL" /* 7: is an edit */
2323 " FROM temp.wantalert, event, forumpost"
2324 " LEFT JOIN user ON (login=coalesce(euser,user))"
2325 " WHERE event.objid=substr(wantalert.eventId,2)+0"
2326 " AND eventId GLOB 'f*'"
2327 " AND forumpost.fpid=event.objid"
@@ -2101,11 +2337,11 @@
2337 const char *z;
2338 if( pPost==0 ) continue;
2339 p = fossil_malloc( sizeof(EmailEvent) );
2340 pLast->pNext = p;
2341 pLast = p;
2342 p->type = db_column_int(&q,7) ? 'f' : 'x';
2343 p->needMod = db_column_int(&q, 5);
2344 z = db_column_text(&q,6);
2345 p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
2346 p->pNext = 0;
2347 blob_init(&p->hdr, 0, 0);
@@ -2433,13 +2669,13 @@
2669 ** if the recipient is a moderator for that type of event. Setup
2670 ** and Admin users always get notified. */
2671 char xType = '*';
2672 if( strpbrk(zCap,"as")==0 ){
2673 switch( p->type ){
2674 case 'x': case 'f': xType = '5'; break;
2675 case 't': xType = 'q'; break;
2676 case 'w': xType = 'l'; break;
2677 }
2678 if( strchr(zCap,xType)==0 ) continue;
2679 }
2680 }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
2681 /* Setup and admin users can get any notification that does not
@@ -2447,14 +2683,14 @@
2683 }else{
2684 /* Other users only see the alert if they have sufficient
2685 ** privilege to view the event itself */
2686 char xType = '*';
2687 switch( p->type ){
2688 case 'c': xType = 'o'; break;
2689 case 'x': case 'f': xType = '2'; break;
2690 case 't': xType = 'r'; break;
2691 case 'w': xType = 'j'; break;
2692 }
2693 if( strchr(zCap,xType)==0 ) continue;
2694 }
2695 if( blob_size(&p->hdr)>0 ){
2696 /* This alert should be sent as a separate email */
@@ -2591,10 +2827,11 @@
2827 @ <table class="subscribe">
2828 if( zCaptcha ){
2829 @ <tr>
2830 @ <td class="form_label">Security&nbsp;Code:</td>
2831 @ <td><input type="text" name="captcha" value="" size="10">
2832 captcha_speakit_button(uSeed, "Speak the code");
2833 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
2834 @ </tr>
2835 }
2836 @ <tr>
2837 @ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
@@ -2617,11 +2854,11 @@
2854 @ </table>
2855 if( zCaptcha ){
2856 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2857 @ %h(zCaptcha)
2858 @ </pre>
2859 @ Enter the 8 characters above in the "Security Code" box<br/>
2860 @ </td></tr></table></div>
2861 }
2862 @ </form>
2863 style_footer();
2864 }
@@ -2635,10 +2872,11 @@
2872 char *zErr;
2873 const char *zTo = PT("to");
2874 char *zSubject = PT("subject");
2875 int bAll = PB("all");
2876 int bAA = PB("aa");
2877 int bMods = PB("mods");
2878 const char *zSub = db_get("email-subname", "[Fossil Repo]");
2879 int bTest2 = fossil_strcmp(P("name"),"test2")==0;
2880 Blob hdr, body;
2881 blob_init(&body, 0, 0);
2882 blob_init(&hdr, 0, 0);
@@ -2646,17 +2884,29 @@
2884 pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
2885 if( zTo[0] ){
2886 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
2887 alert_send(pSender, &hdr, &body, 0);
2888 }
2889 if( bAll || bAA || bMods ){
2890 Stmt q;
2891 int nUsed = blob_size(&body);
2892 const char *zURL = db_get("email-url",0);
2893 if( bAll ){
2894 db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2895 " WHERE sverified AND NOT sdonotcall");
2896 }else if( bAA ){
2897 db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
2898 " WHERE sverified AND NOT sdonotcall"
2899 " AND ssub LIKE '%%a%%'");
2900 }else if( bMods ){
2901 db_prepare(&q,
2902 "SELECT semail, hex(subscriberCode)"
2903 " FROM subscriber, user "
2904 " WHERE sverified AND NOT sdonotcall"
2905 " AND suname=login"
2906 " AND fullcap(cap) GLOB '*5*'");
2907 }
2908 while( db_step(&q)==SQLITE_ROW ){
2909 const char *zCode = db_column_text(&q, 1);
2910 zTo = db_column_text(&q, 0);
2911 blob_truncate(&hdr, 0);
2912 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
@@ -2713,11 +2963,12 @@
2963 @ <p>The following error was reported by the system:
2964 @ <blockquote><pre>
2965 @ %h(zErr)
2966 @ </pre></blockquote>
2967 }else{
2968 @ <p>The announcement has been sent.
2969 @ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p>
2970 }
2971 style_footer();
2972 return;
2973 } else if( !alert_enabled() ){
2974 style_header("Cannot Send Announcement");
@@ -2731,21 +2982,26 @@
2982 @ <form method="POST">
2983 @ <table class="subscribe">
2984 if( g.perm.Admin ){
2985 int aa = PB("aa");
2986 int all = PB("all");
2987 int aMod = PB("mods");
2988 const char *aack = aa ? "checked" : "";
2989 const char *allck = all ? "checked" : "";
2990 const char *modck = aMod ? "checked" : "";
2991 @ <tr>
2992 @ <td class="form_label">To:</td>
2993 @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
2994 @ <label><input type="checkbox" name="aa" %s(aack)> \
2995 @ All "announcement" subscribers</label> \
2996 @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
2997 @ <label><input type="checkbox" name="all" %s(allck)> \
2998 @ All subscribers</label> \
2999 @ <a href="%R/subscribers" target="_blank">(list)</a><br>
3000 @ <label><input type="checkbox" name="mods" %s(modck)> \
3001 @ All moderators</label> \
3002 @ <a href="%R/setup_ulist?with=5" target="_blank">(list)</a><br></td>
3003 @ </tr>
3004 }
3005 @ <tr>
3006 @ <td class="form_label">Subject:</td>
3007 @ <td><input type="text" name="subject" value="%h(PT("subject"))"\
@@ -2756,11 +3012,15 @@
3012 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3013 @ %h(PT("msg"))</textarea>
3014 @ </tr>
3015 @ <tr>
3016 @ <td></td>
3017 if( fossil_strcmp(P("name"),"test2")==0 ){
3018 @ <td><input type="submit" name="submit" value="Dry Run">
3019 }else{
3020 @ <td><input type="submit" name="submit" value="Send Message">
3021 }
3022 @ </tr>
3023 @ </table>
3024 @ </form>
3025 style_footer();
3026 }
3027
+10 -39
--- src/allrepo.c
+++ src/allrepo.c
@@ -19,52 +19,26 @@
1919
*/
2020
#include "config.h"
2121
#include "allrepo.h"
2222
#include <assert.h>
2323
24
-/*
25
-** The input string is a filename. Return a new copy of this
26
-** filename if the filename requires quoting due to special characters
27
-** such as spaces in the name.
28
-**
29
-** If the filename cannot be safely quoted, return a NULL pointer.
30
-**
31
-** Space to hold the returned string is obtained from malloc. A new
32
-** string is returned even if no quoting is needed.
33
-*/
34
-static char *quoteFilename(const char *zFilename){
35
- int i, c;
36
- int needQuote = 0;
37
- for(i=0; (c = zFilename[i])!=0; i++){
38
- if( c=='"' ) return 0;
39
- if( fossil_isspace(c) ) needQuote = 1;
40
- if( c=='\\' && zFilename[i+1]==0 ) return 0;
41
- if( c=='$' ) return 0;
42
- }
43
- if( needQuote ){
44
- return mprintf("\"%s\"", zFilename);
45
- }else{
46
- return mprintf("%s", zFilename);
47
- }
48
-}
49
-
5024
/*
5125
** Build a string that contains all of the command-line options
5226
** specified as arguments. If the option name begins with "+" then
5327
** it takes an argument. Without the "+" it does not.
5428
*/
55
-static void collect_argument(Blob *pExtra, const char *zArg, const char *zShort){
29
+static void collect_argument(Blob *pExtra,const char *zArg,const char *zShort){
5630
const char *z = find_option(zArg, zShort, 0);
5731
if( z!=0 ){
5832
blob_appendf(pExtra, " %s", z);
5933
}
6034
}
6135
static void collect_argument_value(Blob *pExtra, const char *zArg){
6236
const char *zValue = find_option(zArg, 0, 1);
6337
if( zValue ){
6438
if( zValue[0] ){
65
- blob_appendf(pExtra, " --%s %s", zArg, zValue);
39
+ blob_appendf(pExtra, " --%s %$", zArg, zValue);
6640
}else{
6741
blob_appendf(pExtra, " --%s \"\"", zArg);
6842
}
6943
}
7044
}
@@ -128,15 +102,15 @@
128102
** supported by the rebuild command itself, if any are
129103
** present, are passed along verbatim. The --force and
130104
** --randomize options are not supported.
131105
**
132106
** sync Run a "sync" on all repositories. Only the --verbose
133
-** option is supported.
107
+** and --unversioned options are supported.
134108
**
135
-** setting Run the "setting", "set", or "unset" commands on all
136
-** set repositories. These command are particularly useful in
137
-** unset conjunction with the "max-loadavg" setting which cannot
109
+** set|unset Run the "setting", "set", or "unset" commands on all
110
+** repositories. These command are particularly useful in
111
+** conjunction with the "max-loadavg" setting which cannot
138112
** otherwise be set globally.
139113
**
140114
** server Run the "ui" or "server" commands on all repositories.
141115
** ui The root URI gives a listing of all repos.
142116
**
@@ -169,12 +143,10 @@
169143
void all_cmd(void){
170144
int n;
171145
Stmt q;
172146
const char *zCmd;
173147
char *zSyscmd;
174
- char *zFossil;
175
- char *zQFilename;
176148
Blob extra;
177149
int useCheckouts = 0;
178150
int quiet = 0;
179151
int dryRunFlag = 0;
180152
int showFile = find_option("showfile",0,0)!=0;
@@ -283,11 +255,13 @@
283255
}else if( strncmp(zCmd, "sync", n)==0 ){
284256
zCmd = "sync -autourl -R";
285257
collect_argument(&extra, "verbose","v");
286258
collect_argument(&extra, "unversioned","u");
287259
}else if( strncmp(zCmd, "test-integrity", n)==0 ){
260
+ collect_argument(&extra, "db-only", "d");
288261
collect_argument(&extra, "parse", 0);
262
+ collect_argument(&extra, "quick", "q");
289263
zCmd = "test-integrity";
290264
}else if( strncmp(zCmd, "test-orphans", n)==0 ){
291265
zCmd = "test-orphans -R";
292266
}else if( strncmp(zCmd, "test-missing", n)==0 ){
293267
zCmd = "test-missing -q -R";
@@ -368,11 +342,10 @@
368342
fossil_fatal("\"all\" subcommand should be one of: "
369343
"add cache changes clean dbstat extras fts-config ignore "
370344
"info list ls pull push rebuild server setting sync ui unset");
371345
}
372346
verify_all_options();
373
- zFossil = quoteFilename(g.nameOfExe);
374347
db_multi_exec("CREATE TEMP TABLE repolist(name,tag);");
375348
if( useCheckouts ){
376349
db_multi_exec(
377350
"INSERT INTO repolist "
378351
"SELECT DISTINCT substr(name, 7), name COLLATE nocase"
@@ -410,13 +383,12 @@
410383
continue;
411384
}else if( showFile ){
412385
fossil_print("%s: %s\n", useCheckouts ? "checkout" : "repository",
413386
zFilename);
414387
}
415
- zQFilename = quoteFilename(zFilename);
416
- zSyscmd = mprintf("%s %s %s%s",
417
- zFossil, zCmd, zQFilename, blob_str(&extra));
388
+ zSyscmd = mprintf("%$ %s %$%s",
389
+ g.nameOfExe, zCmd, zFilename, blob_str(&extra));
418390
if( showLabel ){
419391
int len = (int)strlen(zFilename);
420392
int nStar = 80 - (len + 15);
421393
if( nStar<2 ) nStar = 1;
422394
fossil_print("%.13c %s %.*c\n", '*', zFilename, nStar, '*');
@@ -426,11 +398,10 @@
426398
fossil_print("%s\n", zSyscmd);
427399
fflush(stdout);
428400
}
429401
rc = dryRunFlag ? 0 : fossil_system(zSyscmd);
430402
free(zSyscmd);
431
- free(zQFilename);
432403
if( stopOnError && rc ){
433404
break;
434405
}
435406
}
436407
db_finalize(&q);
437408
--- src/allrepo.c
+++ src/allrepo.c
@@ -19,52 +19,26 @@
19 */
20 #include "config.h"
21 #include "allrepo.h"
22 #include <assert.h>
23
24 /*
25 ** The input string is a filename. Return a new copy of this
26 ** filename if the filename requires quoting due to special characters
27 ** such as spaces in the name.
28 **
29 ** If the filename cannot be safely quoted, return a NULL pointer.
30 **
31 ** Space to hold the returned string is obtained from malloc. A new
32 ** string is returned even if no quoting is needed.
33 */
34 static char *quoteFilename(const char *zFilename){
35 int i, c;
36 int needQuote = 0;
37 for(i=0; (c = zFilename[i])!=0; i++){
38 if( c=='"' ) return 0;
39 if( fossil_isspace(c) ) needQuote = 1;
40 if( c=='\\' && zFilename[i+1]==0 ) return 0;
41 if( c=='$' ) return 0;
42 }
43 if( needQuote ){
44 return mprintf("\"%s\"", zFilename);
45 }else{
46 return mprintf("%s", zFilename);
47 }
48 }
49
50 /*
51 ** Build a string that contains all of the command-line options
52 ** specified as arguments. If the option name begins with "+" then
53 ** it takes an argument. Without the "+" it does not.
54 */
55 static void collect_argument(Blob *pExtra, const char *zArg, const char *zShort){
56 const char *z = find_option(zArg, zShort, 0);
57 if( z!=0 ){
58 blob_appendf(pExtra, " %s", z);
59 }
60 }
61 static void collect_argument_value(Blob *pExtra, const char *zArg){
62 const char *zValue = find_option(zArg, 0, 1);
63 if( zValue ){
64 if( zValue[0] ){
65 blob_appendf(pExtra, " --%s %s", zArg, zValue);
66 }else{
67 blob_appendf(pExtra, " --%s \"\"", zArg);
68 }
69 }
70 }
@@ -128,15 +102,15 @@
128 ** supported by the rebuild command itself, if any are
129 ** present, are passed along verbatim. The --force and
130 ** --randomize options are not supported.
131 **
132 ** sync Run a "sync" on all repositories. Only the --verbose
133 ** option is supported.
134 **
135 ** setting Run the "setting", "set", or "unset" commands on all
136 ** set repositories. These command are particularly useful in
137 ** unset conjunction with the "max-loadavg" setting which cannot
138 ** otherwise be set globally.
139 **
140 ** server Run the "ui" or "server" commands on all repositories.
141 ** ui The root URI gives a listing of all repos.
142 **
@@ -169,12 +143,10 @@
169 void all_cmd(void){
170 int n;
171 Stmt q;
172 const char *zCmd;
173 char *zSyscmd;
174 char *zFossil;
175 char *zQFilename;
176 Blob extra;
177 int useCheckouts = 0;
178 int quiet = 0;
179 int dryRunFlag = 0;
180 int showFile = find_option("showfile",0,0)!=0;
@@ -283,11 +255,13 @@
283 }else if( strncmp(zCmd, "sync", n)==0 ){
284 zCmd = "sync -autourl -R";
285 collect_argument(&extra, "verbose","v");
286 collect_argument(&extra, "unversioned","u");
287 }else if( strncmp(zCmd, "test-integrity", n)==0 ){
 
288 collect_argument(&extra, "parse", 0);
 
289 zCmd = "test-integrity";
290 }else if( strncmp(zCmd, "test-orphans", n)==0 ){
291 zCmd = "test-orphans -R";
292 }else if( strncmp(zCmd, "test-missing", n)==0 ){
293 zCmd = "test-missing -q -R";
@@ -368,11 +342,10 @@
368 fossil_fatal("\"all\" subcommand should be one of: "
369 "add cache changes clean dbstat extras fts-config ignore "
370 "info list ls pull push rebuild server setting sync ui unset");
371 }
372 verify_all_options();
373 zFossil = quoteFilename(g.nameOfExe);
374 db_multi_exec("CREATE TEMP TABLE repolist(name,tag);");
375 if( useCheckouts ){
376 db_multi_exec(
377 "INSERT INTO repolist "
378 "SELECT DISTINCT substr(name, 7), name COLLATE nocase"
@@ -410,13 +383,12 @@
410 continue;
411 }else if( showFile ){
412 fossil_print("%s: %s\n", useCheckouts ? "checkout" : "repository",
413 zFilename);
414 }
415 zQFilename = quoteFilename(zFilename);
416 zSyscmd = mprintf("%s %s %s%s",
417 zFossil, zCmd, zQFilename, blob_str(&extra));
418 if( showLabel ){
419 int len = (int)strlen(zFilename);
420 int nStar = 80 - (len + 15);
421 if( nStar<2 ) nStar = 1;
422 fossil_print("%.13c %s %.*c\n", '*', zFilename, nStar, '*');
@@ -426,11 +398,10 @@
426 fossil_print("%s\n", zSyscmd);
427 fflush(stdout);
428 }
429 rc = dryRunFlag ? 0 : fossil_system(zSyscmd);
430 free(zSyscmd);
431 free(zQFilename);
432 if( stopOnError && rc ){
433 break;
434 }
435 }
436 db_finalize(&q);
437
--- src/allrepo.c
+++ src/allrepo.c
@@ -19,52 +19,26 @@
19 */
20 #include "config.h"
21 #include "allrepo.h"
22 #include <assert.h>
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24 /*
25 ** Build a string that contains all of the command-line options
26 ** specified as arguments. If the option name begins with "+" then
27 ** it takes an argument. Without the "+" it does not.
28 */
29 static void collect_argument(Blob *pExtra,const char *zArg,const char *zShort){
30 const char *z = find_option(zArg, zShort, 0);
31 if( z!=0 ){
32 blob_appendf(pExtra, " %s", z);
33 }
34 }
35 static void collect_argument_value(Blob *pExtra, const char *zArg){
36 const char *zValue = find_option(zArg, 0, 1);
37 if( zValue ){
38 if( zValue[0] ){
39 blob_appendf(pExtra, " --%s %$", zArg, zValue);
40 }else{
41 blob_appendf(pExtra, " --%s \"\"", zArg);
42 }
43 }
44 }
@@ -128,15 +102,15 @@
102 ** supported by the rebuild command itself, if any are
103 ** present, are passed along verbatim. The --force and
104 ** --randomize options are not supported.
105 **
106 ** sync Run a "sync" on all repositories. Only the --verbose
107 ** and --unversioned options are supported.
108 **
109 ** set|unset Run the "setting", "set", or "unset" commands on all
110 ** repositories. These command are particularly useful in
111 ** conjunction with the "max-loadavg" setting which cannot
112 ** otherwise be set globally.
113 **
114 ** server Run the "ui" or "server" commands on all repositories.
115 ** ui The root URI gives a listing of all repos.
116 **
@@ -169,12 +143,10 @@
143 void all_cmd(void){
144 int n;
145 Stmt q;
146 const char *zCmd;
147 char *zSyscmd;
 
 
148 Blob extra;
149 int useCheckouts = 0;
150 int quiet = 0;
151 int dryRunFlag = 0;
152 int showFile = find_option("showfile",0,0)!=0;
@@ -283,11 +255,13 @@
255 }else if( strncmp(zCmd, "sync", n)==0 ){
256 zCmd = "sync -autourl -R";
257 collect_argument(&extra, "verbose","v");
258 collect_argument(&extra, "unversioned","u");
259 }else if( strncmp(zCmd, "test-integrity", n)==0 ){
260 collect_argument(&extra, "db-only", "d");
261 collect_argument(&extra, "parse", 0);
262 collect_argument(&extra, "quick", "q");
263 zCmd = "test-integrity";
264 }else if( strncmp(zCmd, "test-orphans", n)==0 ){
265 zCmd = "test-orphans -R";
266 }else if( strncmp(zCmd, "test-missing", n)==0 ){
267 zCmd = "test-missing -q -R";
@@ -368,11 +342,10 @@
342 fossil_fatal("\"all\" subcommand should be one of: "
343 "add cache changes clean dbstat extras fts-config ignore "
344 "info list ls pull push rebuild server setting sync ui unset");
345 }
346 verify_all_options();
 
347 db_multi_exec("CREATE TEMP TABLE repolist(name,tag);");
348 if( useCheckouts ){
349 db_multi_exec(
350 "INSERT INTO repolist "
351 "SELECT DISTINCT substr(name, 7), name COLLATE nocase"
@@ -410,13 +383,12 @@
383 continue;
384 }else if( showFile ){
385 fossil_print("%s: %s\n", useCheckouts ? "checkout" : "repository",
386 zFilename);
387 }
388 zSyscmd = mprintf("%$ %s %$%s",
389 g.nameOfExe, zCmd, zFilename, blob_str(&extra));
 
390 if( showLabel ){
391 int len = (int)strlen(zFilename);
392 int nStar = 80 - (len + 15);
393 if( nStar<2 ) nStar = 1;
394 fossil_print("%.13c %s %.*c\n", '*', zFilename, nStar, '*');
@@ -426,11 +398,10 @@
398 fossil_print("%s\n", zSyscmd);
399 fflush(stdout);
400 }
401 rc = dryRunFlag ? 0 : fossil_system(zSyscmd);
402 free(zSyscmd);
 
403 if( stopOnError && rc ){
404 break;
405 }
406 }
407 db_finalize(&q);
408
+33 -28
--- src/attach.c
+++ src/attach.c
@@ -23,19 +23,22 @@
2323
2424
/*
2525
** WEBPAGE: attachlist
2626
** List attachments.
2727
**
28
-** tkt=TICKETUUID
28
+** tkt=HASH
2929
** page=WIKIPAGE
30
+** technote=HASH
31
+**
32
+** At most one of technote=, tkt= or page= may be supplied.
33
+**
34
+** If none are given, all attachments are listed. If one is given, only
35
+** attachments for the designated technote, ticket or wiki page are shown.
3036
**
31
-** At most one of technote=, tkt= or page= are supplied.
32
-** If none is given, all attachments are listed. If one is given,
33
-** only attachments for the designated technote, ticket or wiki page
34
-** are shown. TECHNOTEUUID and TICKETUUID may be just a prefix of the
35
-** relevant technical note or ticket, in which case all attachments
36
-** of all technical notes or tickets with the prefix will be listed.
37
+** HASH may be just a prefix of the relevant technical note or ticket
38
+** artifact hash, in which case all attachments of all technical notes or
39
+** tickets with the prefix will be listed.
3740
*/
3841
void attachlist_page(void){
3942
const char *zPage = P("page");
4043
const char *zTkt = P("tkt");
4144
const char *zTechNote = P("technote");
@@ -152,15 +155,16 @@
152155
** WEBPAGE: attachdownload
153156
** WEBPAGE: attachimage
154157
** WEBPAGE: attachview
155158
**
156159
** Download or display an attachment.
160
+**
157161
** Query parameters:
158162
**
159
-** tkt=TICKETUUID
163
+** tkt=HASH
160164
** page=WIKIPAGE
161
-** technote=TECHNOTEUUID
165
+** technote=HASH
162166
** file=FILENAME
163167
** attachid=ID
164168
**
165169
*/
166170
void attachview_page(void){
@@ -250,11 +254,11 @@
250254
/*
251255
** Commit a new attachment into the repository
252256
*/
253257
void attach_commit(
254258
const char *zName, /* The filename of the attachment */
255
- const char *zTarget, /* The artifact uuid to attach to */
259
+ const char *zTarget, /* The artifact hash to attach to */
256260
const char *aContent, /* The content of the attachment */
257261
int szContent, /* The length of the attachment */
258262
int needModerator, /* Moderate the attachment? */
259263
const char *zComment /* The comment for the attachment */
260264
){
@@ -305,13 +309,13 @@
305309
306310
/*
307311
** WEBPAGE: attachadd
308312
** Add a new attachment.
309313
**
310
-** tkt=TICKETUUID
314
+** tkt=HASH
311315
** page=WIKIPAGE
312
-** technote=TECHNOTEUUID
316
+** technote=HASH
313317
** from=URL
314318
**
315319
*/
316320
void attachadd_page(void){
317321
const char *zPage = P("page");
@@ -420,14 +424,14 @@
420424
*/
421425
void ainfo_page(void){
422426
int rid; /* RID for the control artifact */
423427
int ridSrc; /* RID for the attached file */
424428
char *zDate; /* Date attached */
425
- const char *zUuid; /* UUID of the control artifact */
429
+ const char *zUuid; /* Hash of the control artifact */
426430
Manifest *pAttach; /* Parse of the control artifact */
427431
const char *zTarget; /* Wiki, ticket or tech note attached to */
428
- const char *zSrc; /* UUID of the attached file */
432
+ const char *zSrc; /* Hash of the attached file */
429433
const char *zName; /* Name of the attached file */
430434
const char *zDesc; /* Description of the attached file */
431435
const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
432436
const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
433437
const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
@@ -545,11 +549,11 @@
545549
cgi_redirectf("%R/wiki?name=%t", zWikiName);
546550
}
547551
return;
548552
}
549553
if( strcmp(zModAction,"approve")==0 ){
550
- moderation_approve(rid);
554
+ moderation_approve('a', rid);
551555
}
552556
}
553557
style_header("Attachment Details");
554558
style_submenu_element("Raw", "%R/artifact/%s", zUuid);
555559
if(fShowContent){
@@ -681,22 +685,23 @@
681685
/*
682686
** COMMAND: attachment*
683687
**
684688
** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS?
685689
**
686
-** Add an attachment to an existing wiki page or tech note.
687
-**
688
-** Options:
689
-** -t|--technote DATETIME Specifies the timestamp of
690
-** the technote to which the attachment
691
-** is to be made. The attachment will be
692
-** to the most recently modified tech note
693
-** with the specified timestamp.
694
-** -t|--technote TECHNOTE-ID Specifies the technote to be
695
-** updated by its technote id.
696
-**
697
-** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified.
690
+** Add an attachment to an existing wiki page or tech note.
691
+** Options:
692
+**
693
+** -t|--technote DATETIME Specifies the timestamp of
694
+** the technote to which the attachment
695
+** is to be made. The attachment will be
696
+** to the most recently modified tech note
697
+** with the specified timestamp.
698
+**
699
+** -t|--technote TECHNOTE-ID Specifies the technote to be
700
+** updated by its technote id.
701
+**
702
+** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified.
698703
**
699704
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
700705
** year-month-day form, it may be truncated, the "T" may be replaced by
701706
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
702707
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -763,11 +768,11 @@
763768
}
764769
blob_read_from_file(&content, zFile, ExtFILE);
765770
user_select();
766771
attach_commit(
767772
zFile, /* The filename of the attachment */
768
- zTarget, /* The artifact uuid to attach to */
773
+ zTarget, /* The artifact hash to attach to */
769774
blob_buffer(&content), /* The content of the attachment */
770775
blob_size(&content), /* The length of the attachment */
771776
0, /* No need to moderate the attachment */
772777
"" /* Empty attachment comment */
773778
);
774779
775780
ADDED src/backlink.c
--- src/attach.c
+++ src/attach.c
@@ -23,19 +23,22 @@
23
24 /*
25 ** WEBPAGE: attachlist
26 ** List attachments.
27 **
28 ** tkt=TICKETUUID
29 ** page=WIKIPAGE
 
 
 
 
 
 
30 **
31 ** At most one of technote=, tkt= or page= are supplied.
32 ** If none is given, all attachments are listed. If one is given,
33 ** only attachments for the designated technote, ticket or wiki page
34 ** are shown. TECHNOTEUUID and TICKETUUID may be just a prefix of the
35 ** relevant technical note or ticket, in which case all attachments
36 ** of all technical notes or tickets with the prefix will be listed.
37 */
38 void attachlist_page(void){
39 const char *zPage = P("page");
40 const char *zTkt = P("tkt");
41 const char *zTechNote = P("technote");
@@ -152,15 +155,16 @@
152 ** WEBPAGE: attachdownload
153 ** WEBPAGE: attachimage
154 ** WEBPAGE: attachview
155 **
156 ** Download or display an attachment.
 
157 ** Query parameters:
158 **
159 ** tkt=TICKETUUID
160 ** page=WIKIPAGE
161 ** technote=TECHNOTEUUID
162 ** file=FILENAME
163 ** attachid=ID
164 **
165 */
166 void attachview_page(void){
@@ -250,11 +254,11 @@
250 /*
251 ** Commit a new attachment into the repository
252 */
253 void attach_commit(
254 const char *zName, /* The filename of the attachment */
255 const char *zTarget, /* The artifact uuid to attach to */
256 const char *aContent, /* The content of the attachment */
257 int szContent, /* The length of the attachment */
258 int needModerator, /* Moderate the attachment? */
259 const char *zComment /* The comment for the attachment */
260 ){
@@ -305,13 +309,13 @@
305
306 /*
307 ** WEBPAGE: attachadd
308 ** Add a new attachment.
309 **
310 ** tkt=TICKETUUID
311 ** page=WIKIPAGE
312 ** technote=TECHNOTEUUID
313 ** from=URL
314 **
315 */
316 void attachadd_page(void){
317 const char *zPage = P("page");
@@ -420,14 +424,14 @@
420 */
421 void ainfo_page(void){
422 int rid; /* RID for the control artifact */
423 int ridSrc; /* RID for the attached file */
424 char *zDate; /* Date attached */
425 const char *zUuid; /* UUID of the control artifact */
426 Manifest *pAttach; /* Parse of the control artifact */
427 const char *zTarget; /* Wiki, ticket or tech note attached to */
428 const char *zSrc; /* UUID of the attached file */
429 const char *zName; /* Name of the attached file */
430 const char *zDesc; /* Description of the attached file */
431 const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
432 const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
433 const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
@@ -545,11 +549,11 @@
545 cgi_redirectf("%R/wiki?name=%t", zWikiName);
546 }
547 return;
548 }
549 if( strcmp(zModAction,"approve")==0 ){
550 moderation_approve(rid);
551 }
552 }
553 style_header("Attachment Details");
554 style_submenu_element("Raw", "%R/artifact/%s", zUuid);
555 if(fShowContent){
@@ -681,22 +685,23 @@
681 /*
682 ** COMMAND: attachment*
683 **
684 ** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS?
685 **
686 ** Add an attachment to an existing wiki page or tech note.
687 **
688 ** Options:
689 ** -t|--technote DATETIME Specifies the timestamp of
690 ** the technote to which the attachment
691 ** is to be made. The attachment will be
692 ** to the most recently modified tech note
693 ** with the specified timestamp.
694 ** -t|--technote TECHNOTE-ID Specifies the technote to be
695 ** updated by its technote id.
696 **
697 ** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified.
 
698 **
699 ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
700 ** year-month-day form, it may be truncated, the "T" may be replaced by
701 ** a space, and it may also name a timezone offset from UTC as "-HH:MM"
702 ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -763,11 +768,11 @@
763 }
764 blob_read_from_file(&content, zFile, ExtFILE);
765 user_select();
766 attach_commit(
767 zFile, /* The filename of the attachment */
768 zTarget, /* The artifact uuid to attach to */
769 blob_buffer(&content), /* The content of the attachment */
770 blob_size(&content), /* The length of the attachment */
771 0, /* No need to moderate the attachment */
772 "" /* Empty attachment comment */
773 );
774
775 DDED src/backlink.c
--- src/attach.c
+++ src/attach.c
@@ -23,19 +23,22 @@
23
24 /*
25 ** WEBPAGE: attachlist
26 ** List attachments.
27 **
28 ** tkt=HASH
29 ** page=WIKIPAGE
30 ** technote=HASH
31 **
32 ** At most one of technote=, tkt= or page= may be supplied.
33 **
34 ** If none are given, all attachments are listed. If one is given, only
35 ** attachments for the designated technote, ticket or wiki page are shown.
36 **
37 ** HASH may be just a prefix of the relevant technical note or ticket
38 ** artifact hash, in which case all attachments of all technical notes or
39 ** tickets with the prefix will be listed.
 
 
 
40 */
41 void attachlist_page(void){
42 const char *zPage = P("page");
43 const char *zTkt = P("tkt");
44 const char *zTechNote = P("technote");
@@ -152,15 +155,16 @@
155 ** WEBPAGE: attachdownload
156 ** WEBPAGE: attachimage
157 ** WEBPAGE: attachview
158 **
159 ** Download or display an attachment.
160 **
161 ** Query parameters:
162 **
163 ** tkt=HASH
164 ** page=WIKIPAGE
165 ** technote=HASH
166 ** file=FILENAME
167 ** attachid=ID
168 **
169 */
170 void attachview_page(void){
@@ -250,11 +254,11 @@
254 /*
255 ** Commit a new attachment into the repository
256 */
257 void attach_commit(
258 const char *zName, /* The filename of the attachment */
259 const char *zTarget, /* The artifact hash to attach to */
260 const char *aContent, /* The content of the attachment */
261 int szContent, /* The length of the attachment */
262 int needModerator, /* Moderate the attachment? */
263 const char *zComment /* The comment for the attachment */
264 ){
@@ -305,13 +309,13 @@
309
310 /*
311 ** WEBPAGE: attachadd
312 ** Add a new attachment.
313 **
314 ** tkt=HASH
315 ** page=WIKIPAGE
316 ** technote=HASH
317 ** from=URL
318 **
319 */
320 void attachadd_page(void){
321 const char *zPage = P("page");
@@ -420,14 +424,14 @@
424 */
425 void ainfo_page(void){
426 int rid; /* RID for the control artifact */
427 int ridSrc; /* RID for the attached file */
428 char *zDate; /* Date attached */
429 const char *zUuid; /* Hash of the control artifact */
430 Manifest *pAttach; /* Parse of the control artifact */
431 const char *zTarget; /* Wiki, ticket or tech note attached to */
432 const char *zSrc; /* Hash of the attached file */
433 const char *zName; /* Name of the attached file */
434 const char *zDesc; /* Description of the attached file */
435 const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
436 const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
437 const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
@@ -545,11 +549,11 @@
549 cgi_redirectf("%R/wiki?name=%t", zWikiName);
550 }
551 return;
552 }
553 if( strcmp(zModAction,"approve")==0 ){
554 moderation_approve('a', rid);
555 }
556 }
557 style_header("Attachment Details");
558 style_submenu_element("Raw", "%R/artifact/%s", zUuid);
559 if(fShowContent){
@@ -681,22 +685,23 @@
685 /*
686 ** COMMAND: attachment*
687 **
688 ** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS?
689 **
690 ** Add an attachment to an existing wiki page or tech note.
691 ** Options:
692 **
693 ** -t|--technote DATETIME Specifies the timestamp of
694 ** the technote to which the attachment
695 ** is to be made. The attachment will be
696 ** to the most recently modified tech note
697 ** with the specified timestamp.
698 **
699 ** -t|--technote TECHNOTE-ID Specifies the technote to be
700 ** updated by its technote id.
701 **
702 ** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified.
703 **
704 ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
705 ** year-month-day form, it may be truncated, the "T" may be replaced by
706 ** a space, and it may also name a timezone offset from UTC as "-HH:MM"
707 ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
@@ -763,11 +768,11 @@
768 }
769 blob_read_from_file(&content, zFile, ExtFILE);
770 user_select();
771 attach_commit(
772 zFile, /* The filename of the attachment */
773 zTarget, /* The artifact hash to attach to */
774 blob_buffer(&content), /* The content of the attachment */
775 blob_size(&content), /* The length of the attachment */
776 0, /* No need to moderate the attachment */
777 "" /* Empty attachment comment */
778 );
779
780 DDED src/backlink.c
--- a/src/backlink.c
+++ b/src/backlink.c
@@ -0,0 +1,6 @@
1
+/*
2
+** Copyright (c) 2020 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-Clau
--- a/src/backlink.c
+++ b/src/backlink.c
@@ -0,0 +1,6 @@
 
 
 
 
 
 
--- a/src/backlink.c
+++ b/src/backlink.c
@@ -0,0 +1,6 @@
1 /*
2 ** Copyright (c) 2020 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-Clau
+23 -2
--- src/backoffice.c
+++ src/backoffice.c
@@ -555,31 +555,40 @@
555555
**
556556
** OPTIONS:
557557
**
558558
** --debug Show what this command is doing.
559559
**
560
+** --min N When polling, invoke backoffice at least
561
+** once every N seconds even if the repository
562
+** never changes. 0 or negative means disable
563
+** this feature. Default: 3600 (once per hour).
564
+**
560565
** --nodelay Do not queue up or wait for a backoffice job
561566
** to complete. If no work is available or if
562567
** backoffice has run recently, return immediately.
563568
** The --nodelay option is implied if more than
564569
** one repository is listed on the command-line.
565570
**
566571
** --poll N Repeat backoffice calls for repositories that
567572
** change in appoximately N-second intervals.
568573
** N less than 1 turns polling off (the default).
574
+** Recommended polling interval: 60 seconds.
569575
**
570576
** --trace Enable debugging output on stderr
571577
*/
572578
void backoffice_command(void){
573579
int nPoll;
580
+ int nMin;
574581
const char *zPoll;
575582
int bDebug = 0;
576583
unsigned int nCmd = 0;
577584
if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1;
578585
if( find_option("nodelay",0,0)!=0 ) backofficeNoDelay = 1;
579586
zPoll = find_option("poll",0,1);
580587
nPoll = zPoll ? atoi(zPoll) : 0;
588
+ zPoll = find_option("min",0,1);
589
+ nMin = zPoll ? atoi(zPoll) : 3600;
581590
bDebug = find_option("debug",0,0)!=0;
582591
583592
/* Silently consume the -R or --repository flag, leaving behind its
584593
** argument. This is for legacy compatibility. Older versions of the
585594
** backoffice command only ran on a single repository that was specified
@@ -592,16 +601,25 @@
592601
** or we are polling. In either case, each backoffice should be run
593602
** using a separate sub-process */
594603
int i;
595604
time_t iNow = 0;
596605
time_t ix;
606
+ i64 *aLastRun = fossil_malloc( sizeof(i64)*g.argc );
607
+ memset(aLastRun, 0, sizeof(i64)*g.argc );
597608
while( 1 /* exit via "break;" */){
598609
time_t iNext = time(0);
599610
for(i=2; i<g.argc; i++){
600611
Blob cmd;
601
- if( !file_isfile(g.argv[i], ExtFILE) ) continue;
602
- if( iNow && iNow>file_mtime(g.argv[i],ExtFILE) ) continue;
612
+ if( !file_isfile(g.argv[i], ExtFILE) ){
613
+ continue; /* Repo no longer exists. Ignore it. */
614
+ }
615
+ if( iNow
616
+ && iNow>file_mtime(g.argv[i], ExtFILE)
617
+ && (nMin<=0 || aLastRun[i]+nMin>iNow)
618
+ ){
619
+ continue; /* Not yet time to run this one */
620
+ }
603621
blob_init(&cmd, 0, 0);
604622
blob_append_escaped_arg(&cmd, g.nameOfExe);
605623
blob_append(&cmd, " backoffice --nodelay", -1);
606624
if( g.fAnyTrace ){
607625
blob_append(&cmd, " --trace", -1);
@@ -610,10 +628,11 @@
610628
nCmd++;
611629
if( bDebug ){
612630
fossil_print("COMMAND[%u]: %s\n", nCmd, blob_str(&cmd));
613631
}
614632
fossil_system(blob_str(&cmd));
633
+ aLastRun[i] = iNext;
615634
blob_reset(&cmd);
616635
}
617636
if( nPoll<1 ) break;
618637
iNow = iNext;
619638
ix = time(0);
@@ -622,10 +641,12 @@
622641
if( bDebug )fossil_print("SLEEP: %lld\n", nMS);
623642
sqlite3_sleep((int)nMS);
624643
}
625644
}
626645
}else{
646
+ /* Not polling and only one repository named. Backoffice is run
647
+ ** once by this process, which then exits */
627648
if( g.argc==3 ){
628649
g.zRepositoryOption = g.argv[2];
629650
g.argc--;
630651
}
631652
db_find_and_open_repository(0,0);
632653
--- src/backoffice.c
+++ src/backoffice.c
@@ -555,31 +555,40 @@
555 **
556 ** OPTIONS:
557 **
558 ** --debug Show what this command is doing.
559 **
 
 
 
 
 
560 ** --nodelay Do not queue up or wait for a backoffice job
561 ** to complete. If no work is available or if
562 ** backoffice has run recently, return immediately.
563 ** The --nodelay option is implied if more than
564 ** one repository is listed on the command-line.
565 **
566 ** --poll N Repeat backoffice calls for repositories that
567 ** change in appoximately N-second intervals.
568 ** N less than 1 turns polling off (the default).
 
569 **
570 ** --trace Enable debugging output on stderr
571 */
572 void backoffice_command(void){
573 int nPoll;
 
574 const char *zPoll;
575 int bDebug = 0;
576 unsigned int nCmd = 0;
577 if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1;
578 if( find_option("nodelay",0,0)!=0 ) backofficeNoDelay = 1;
579 zPoll = find_option("poll",0,1);
580 nPoll = zPoll ? atoi(zPoll) : 0;
 
 
581 bDebug = find_option("debug",0,0)!=0;
582
583 /* Silently consume the -R or --repository flag, leaving behind its
584 ** argument. This is for legacy compatibility. Older versions of the
585 ** backoffice command only ran on a single repository that was specified
@@ -592,16 +601,25 @@
592 ** or we are polling. In either case, each backoffice should be run
593 ** using a separate sub-process */
594 int i;
595 time_t iNow = 0;
596 time_t ix;
 
 
597 while( 1 /* exit via "break;" */){
598 time_t iNext = time(0);
599 for(i=2; i<g.argc; i++){
600 Blob cmd;
601 if( !file_isfile(g.argv[i], ExtFILE) ) continue;
602 if( iNow && iNow>file_mtime(g.argv[i],ExtFILE) ) continue;
 
 
 
 
 
 
 
603 blob_init(&cmd, 0, 0);
604 blob_append_escaped_arg(&cmd, g.nameOfExe);
605 blob_append(&cmd, " backoffice --nodelay", -1);
606 if( g.fAnyTrace ){
607 blob_append(&cmd, " --trace", -1);
@@ -610,10 +628,11 @@
610 nCmd++;
611 if( bDebug ){
612 fossil_print("COMMAND[%u]: %s\n", nCmd, blob_str(&cmd));
613 }
614 fossil_system(blob_str(&cmd));
 
615 blob_reset(&cmd);
616 }
617 if( nPoll<1 ) break;
618 iNow = iNext;
619 ix = time(0);
@@ -622,10 +641,12 @@
622 if( bDebug )fossil_print("SLEEP: %lld\n", nMS);
623 sqlite3_sleep((int)nMS);
624 }
625 }
626 }else{
 
 
627 if( g.argc==3 ){
628 g.zRepositoryOption = g.argv[2];
629 g.argc--;
630 }
631 db_find_and_open_repository(0,0);
632
--- src/backoffice.c
+++ src/backoffice.c
@@ -555,31 +555,40 @@
555 **
556 ** OPTIONS:
557 **
558 ** --debug Show what this command is doing.
559 **
560 ** --min N When polling, invoke backoffice at least
561 ** once every N seconds even if the repository
562 ** never changes. 0 or negative means disable
563 ** this feature. Default: 3600 (once per hour).
564 **
565 ** --nodelay Do not queue up or wait for a backoffice job
566 ** to complete. If no work is available or if
567 ** backoffice has run recently, return immediately.
568 ** The --nodelay option is implied if more than
569 ** one repository is listed on the command-line.
570 **
571 ** --poll N Repeat backoffice calls for repositories that
572 ** change in appoximately N-second intervals.
573 ** N less than 1 turns polling off (the default).
574 ** Recommended polling interval: 60 seconds.
575 **
576 ** --trace Enable debugging output on stderr
577 */
578 void backoffice_command(void){
579 int nPoll;
580 int nMin;
581 const char *zPoll;
582 int bDebug = 0;
583 unsigned int nCmd = 0;
584 if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1;
585 if( find_option("nodelay",0,0)!=0 ) backofficeNoDelay = 1;
586 zPoll = find_option("poll",0,1);
587 nPoll = zPoll ? atoi(zPoll) : 0;
588 zPoll = find_option("min",0,1);
589 nMin = zPoll ? atoi(zPoll) : 3600;
590 bDebug = find_option("debug",0,0)!=0;
591
592 /* Silently consume the -R or --repository flag, leaving behind its
593 ** argument. This is for legacy compatibility. Older versions of the
594 ** backoffice command only ran on a single repository that was specified
@@ -592,16 +601,25 @@
601 ** or we are polling. In either case, each backoffice should be run
602 ** using a separate sub-process */
603 int i;
604 time_t iNow = 0;
605 time_t ix;
606 i64 *aLastRun = fossil_malloc( sizeof(i64)*g.argc );
607 memset(aLastRun, 0, sizeof(i64)*g.argc );
608 while( 1 /* exit via "break;" */){
609 time_t iNext = time(0);
610 for(i=2; i<g.argc; i++){
611 Blob cmd;
612 if( !file_isfile(g.argv[i], ExtFILE) ){
613 continue; /* Repo no longer exists. Ignore it. */
614 }
615 if( iNow
616 && iNow>file_mtime(g.argv[i], ExtFILE)
617 && (nMin<=0 || aLastRun[i]+nMin>iNow)
618 ){
619 continue; /* Not yet time to run this one */
620 }
621 blob_init(&cmd, 0, 0);
622 blob_append_escaped_arg(&cmd, g.nameOfExe);
623 blob_append(&cmd, " backoffice --nodelay", -1);
624 if( g.fAnyTrace ){
625 blob_append(&cmd, " --trace", -1);
@@ -610,10 +628,11 @@
628 nCmd++;
629 if( bDebug ){
630 fossil_print("COMMAND[%u]: %s\n", nCmd, blob_str(&cmd));
631 }
632 fossil_system(blob_str(&cmd));
633 aLastRun[i] = iNext;
634 blob_reset(&cmd);
635 }
636 if( nPoll<1 ) break;
637 iNow = iNext;
638 ix = time(0);
@@ -622,10 +641,12 @@
641 if( bDebug )fossil_print("SLEEP: %lld\n", nMS);
642 sqlite3_sleep((int)nMS);
643 }
644 }
645 }else{
646 /* Not polling and only one repository named. Backoffice is run
647 ** once by this process, which then exits */
648 if( g.argc==3 ){
649 g.zRepositoryOption = g.argv[2];
650 g.argc--;
651 }
652 db_find_and_open_repository(0,0);
653
+6
--- src/bag.c
+++ src/bag.c
@@ -48,10 +48,16 @@
4848
int cnt; /* Number of integers in the bag */
4949
int sz; /* Number of slots in a[] */
5050
int used; /* Number of used slots in a[] */
5151
int *a; /* Hash table of integers that are in the bag */
5252
};
53
+/*
54
+** An expression for statically initializing a Bag instance, to be
55
+** assigned to Bag instances at their declaration point. It has
56
+** the same effect as passing the Bag to bag_init().
57
+*/
58
+#define Bag_INIT {0,0,0,0}
5359
#endif
5460
5561
/*
5662
** Initialize a Bag structure
5763
*/
5864
--- src/bag.c
+++ src/bag.c
@@ -48,10 +48,16 @@
48 int cnt; /* Number of integers in the bag */
49 int sz; /* Number of slots in a[] */
50 int used; /* Number of used slots in a[] */
51 int *a; /* Hash table of integers that are in the bag */
52 };
 
 
 
 
 
 
53 #endif
54
55 /*
56 ** Initialize a Bag structure
57 */
58
--- src/bag.c
+++ src/bag.c
@@ -48,10 +48,16 @@
48 int cnt; /* Number of integers in the bag */
49 int sz; /* Number of slots in a[] */
50 int used; /* Number of used slots in a[] */
51 int *a; /* Hash table of integers that are in the bag */
52 };
53 /*
54 ** An expression for statically initializing a Bag instance, to be
55 ** assigned to Bag instances at their declaration point. It has
56 ** the same effect as passing the Bag to bag_init().
57 */
58 #define Bag_INIT {0,0,0,0}
59 #endif
60
61 /*
62 ** Initialize a Bag structure
63 */
64
+54 -55
--- src/bisect.c
+++ src/bisect.c
@@ -304,67 +304,66 @@
304304
**
305305
** Usage: %fossil bisect SUBCOMMAND ...
306306
**
307307
** Run various subcommands useful for searching for bugs.
308308
**
309
-** fossil bisect bad ?VERSION?
310
-**
311
-** Identify version VERSION as non-working. If VERSION is omitted,
312
-** the current checkout is marked as non-working.
313
-**
314
-** fossil bisect good ?VERSION?
315
-**
316
-** Identify version VERSION as working. If VERSION is omitted,
317
-** the current checkout is marked as working.
318
-**
319
-** fossil bisect log
320
-** fossil bisect chart
321
-**
322
-** Show a log of "good" and "bad" versions. "bisect log" shows the
323
-** events in the order that they were tested. "bisect chart" shows
324
-** them in order of check-in.
325
-**
326
-** fossil bisect next
327
-**
328
-** Update to the next version that is halfway between the working and
329
-** non-working versions.
330
-**
331
-** fossil bisect options ?NAME? ?VALUE?
332
-**
333
-** List all bisect options, or the value of a single option, or set the
334
-** value of a bisect option.
335
-**
336
-** fossil bisect reset
337
-**
338
-** Reinitialize a bisect session. This cancels prior bisect history
339
-** and allows a bisect session to start over from the beginning.
340
-**
341
-** fossil bisect vlist|ls|status ?-a|--all?
342
-**
343
-** List the versions in between "bad" and "good".
344
-**
345
-** fossil bisect ui
346
-**
347
-** Like "fossil ui" except start on a timeline that shows only the
348
-** check-ins that are part of the current bisect.
349
-**
350
-** fossil bisect undo
351
-**
352
-** Undo the most recent "good" or "bad" command.
309
+** > fossil bisect bad ?VERSION?
310
+**
311
+** Identify version VERSION as non-working. If VERSION is omitted,
312
+** the current checkout is marked as non-working.
313
+**
314
+** > fossil bisect good ?VERSION?
315
+**
316
+** Identify version VERSION as working. If VERSION is omitted,
317
+** the current checkout is marked as working.
318
+**
319
+** > fossil bisect log
320
+** > fossil bisect chart
321
+**
322
+** Show a log of "good" and "bad" versions. "bisect log" shows the
323
+** events in the order that they were tested. "bisect chart" shows
324
+** them in order of check-in.
325
+**
326
+** > fossil bisect next
327
+**
328
+** Update to the next version that is halfway between the working and
329
+** non-working versions.
330
+**
331
+** > fossil bisect options ?NAME? ?VALUE?
332
+**
333
+** List all bisect options, or the value of a single option, or set the
334
+** value of a bisect option.
335
+**
336
+** > fossil bisect reset
337
+**
338
+** Reinitialize a bisect session. This cancels prior bisect history
339
+** and allows a bisect session to start over from the beginning.
340
+**
341
+** > fossil bisect vlist|ls|status ?-a|--all?
342
+**
343
+** List the versions in between "bad" and "good".
344
+**
345
+** > fossil bisect ui
346
+**
347
+** Like "fossil ui" except start on a timeline that shows only the
348
+** check-ins that are part of the current bisect.
349
+**
350
+** > fossil bisect undo
351
+**
352
+** Undo the most recent "good" or "bad" command.
353353
**
354354
** Summary:
355
-**
356
-** fossil bisect bad ?VERSION?
357
-** fossil bisect good ?VERSION?
358
-** fossil bisect log
359
-** fossil bisect chart
360
-** fossil bisect next
361
-** fossil bisect options
362
-** fossil bisect reset
363
-** fossil bisect status
364
-** fossil bisect ui
365
-** fossil bisect undo
355
+** * fossil bisect bad ?VERSION?
356
+** * fossil bisect good ?VERSION?
357
+** * fossil bisect log
358
+** * fossil bisect chart
359
+** * fossil bisect next
360
+** * fossil bisect options
361
+** * fossil bisect reset
362
+** * fossil bisect status
363
+** * fossil bisect ui
364
+** * fossil bisect undo
366365
*/
367366
void bisect_cmd(void){
368367
int n;
369368
const char *zCmd;
370369
int foundCmd = 0;
371370
--- src/bisect.c
+++ src/bisect.c
@@ -304,67 +304,66 @@
304 **
305 ** Usage: %fossil bisect SUBCOMMAND ...
306 **
307 ** Run various subcommands useful for searching for bugs.
308 **
309 ** fossil bisect bad ?VERSION?
310 **
311 ** Identify version VERSION as non-working. If VERSION is omitted,
312 ** the current checkout is marked as non-working.
313 **
314 ** fossil bisect good ?VERSION?
315 **
316 ** Identify version VERSION as working. If VERSION is omitted,
317 ** the current checkout is marked as working.
318 **
319 ** fossil bisect log
320 ** fossil bisect chart
321 **
322 ** Show a log of "good" and "bad" versions. "bisect log" shows the
323 ** events in the order that they were tested. "bisect chart" shows
324 ** them in order of check-in.
325 **
326 ** fossil bisect next
327 **
328 ** Update to the next version that is halfway between the working and
329 ** non-working versions.
330 **
331 ** fossil bisect options ?NAME? ?VALUE?
332 **
333 ** List all bisect options, or the value of a single option, or set the
334 ** value of a bisect option.
335 **
336 ** fossil bisect reset
337 **
338 ** Reinitialize a bisect session. This cancels prior bisect history
339 ** and allows a bisect session to start over from the beginning.
340 **
341 ** fossil bisect vlist|ls|status ?-a|--all?
342 **
343 ** List the versions in between "bad" and "good".
344 **
345 ** fossil bisect ui
346 **
347 ** Like "fossil ui" except start on a timeline that shows only the
348 ** check-ins that are part of the current bisect.
349 **
350 ** fossil bisect undo
351 **
352 ** Undo the most recent "good" or "bad" command.
353 **
354 ** Summary:
355 **
356 ** fossil bisect bad ?VERSION?
357 ** fossil bisect good ?VERSION?
358 ** fossil bisect log
359 ** fossil bisect chart
360 ** fossil bisect next
361 ** fossil bisect options
362 ** fossil bisect reset
363 ** fossil bisect status
364 ** fossil bisect ui
365 ** fossil bisect undo
366 */
367 void bisect_cmd(void){
368 int n;
369 const char *zCmd;
370 int foundCmd = 0;
371
--- src/bisect.c
+++ src/bisect.c
@@ -304,67 +304,66 @@
304 **
305 ** Usage: %fossil bisect SUBCOMMAND ...
306 **
307 ** Run various subcommands useful for searching for bugs.
308 **
309 ** > fossil bisect bad ?VERSION?
310 **
311 ** Identify version VERSION as non-working. If VERSION is omitted,
312 ** the current checkout is marked as non-working.
313 **
314 ** > fossil bisect good ?VERSION?
315 **
316 ** Identify version VERSION as working. If VERSION is omitted,
317 ** the current checkout is marked as working.
318 **
319 ** > fossil bisect log
320 ** > fossil bisect chart
321 **
322 ** Show a log of "good" and "bad" versions. "bisect log" shows the
323 ** events in the order that they were tested. "bisect chart" shows
324 ** them in order of check-in.
325 **
326 ** > fossil bisect next
327 **
328 ** Update to the next version that is halfway between the working and
329 ** non-working versions.
330 **
331 ** > fossil bisect options ?NAME? ?VALUE?
332 **
333 ** List all bisect options, or the value of a single option, or set the
334 ** value of a bisect option.
335 **
336 ** > fossil bisect reset
337 **
338 ** Reinitialize a bisect session. This cancels prior bisect history
339 ** and allows a bisect session to start over from the beginning.
340 **
341 ** > fossil bisect vlist|ls|status ?-a|--all?
342 **
343 ** List the versions in between "bad" and "good".
344 **
345 ** > fossil bisect ui
346 **
347 ** Like "fossil ui" except start on a timeline that shows only the
348 ** check-ins that are part of the current bisect.
349 **
350 ** > fossil bisect undo
351 **
352 ** Undo the most recent "good" or "bad" command.
353 **
354 ** Summary:
355 ** * fossil bisect bad ?VERSION?
356 ** * fossil bisect good ?VERSION?
357 ** * fossil bisect log
358 ** * fossil bisect chart
359 ** * fossil bisect next
360 ** * fossil bisect options
361 ** * fossil bisect reset
362 ** * fossil bisect status
363 ** * fossil bisect ui
364 ** * fossil bisect undo
 
365 */
366 void bisect_cmd(void){
367 int n;
368 const char *zCmd;
369 int foundCmd = 0;
370
+113 -15
--- src/blob.c
+++ src/blob.c
@@ -277,12 +277,19 @@
277277
pBlob->xRealloc = blobReallocStatic;
278278
}
279279
280280
/*
281281
** Append text or data to the end of a blob.
282
+**
283
+** The blob_append_full() routine is a complete implementation.
284
+** The blob_append() routine only works for cases where nData>0 and
285
+** no resizing is required, and falls back to blob_append_full() if
286
+** either condition is not met, but runs faster in the common case
287
+** where all conditions are met. The use of blob_append() is
288
+** recommended, unless it is known in advance that nData<0.
282289
*/
283
-void blob_append(Blob *pBlob, const char *aData, int nData){
290
+void blob_append_full(Blob *pBlob, const char *aData, int nData){
284291
sqlite3_int64 nNew;
285292
assert( aData!=0 || nData==0 );
286293
blob_is_init(pBlob);
287294
if( nData<0 ) nData = strlen(aData);
288295
if( nData==0 ) return;
@@ -301,22 +308,40 @@
301308
}
302309
memcpy(&pBlob->aData[pBlob->nUsed], aData, nData);
303310
pBlob->nUsed += nData;
304311
pBlob->aData[pBlob->nUsed] = 0; /* Blobs are always nul-terminated */
305312
}
313
+void blob_append(Blob *pBlob, const char *aData, int nData){
314
+ sqlite3_int64 nUsed;
315
+ assert( aData!=0 || nData==0 );
316
+ /* blob_is_init(pBlob); // omitted for speed */
317
+ if( nData<=0 || pBlob->nUsed + nData >= pBlob->nAlloc ){
318
+ blob_append_full(pBlob, aData, nData);
319
+ return;
320
+ }
321
+ nUsed = pBlob->nUsed;
322
+ pBlob->nUsed += nData;
323
+ pBlob->aData[pBlob->nUsed] = 0;
324
+ memcpy(&pBlob->aData[nUsed], aData, nData);
325
+}
326
+
327
+/*
328
+** Append a string literal to a blob.
329
+*/
330
+#if INTERFACE
331
+#define blob_append_string(BLOB,STR) blob_append(BLOB,STR,sizeof(STR)-1)
332
+#endif
306333
307334
/*
308335
** Append a single character to the blob
309336
*/
310337
void blob_append_char(Blob *pBlob, char c){
311338
if( pBlob->nUsed+1 >= pBlob->nAlloc ){
312
- pBlob->xRealloc(pBlob, pBlob->nUsed + pBlob->nAlloc + 100);
313
- if( pBlob->nUsed + 1 >= pBlob->nAlloc ){
314
- blob_panic();
315
- }
339
+ blob_append_full(pBlob, &c, 1);
340
+ }else{
341
+ pBlob->aData[pBlob->nUsed++] = c;
316342
}
317
- pBlob->aData[pBlob->nUsed++] = c;
318343
}
319344
320345
/*
321346
** Copy a blob
322347
*/
@@ -333,11 +358,13 @@
333358
blob_is_init(p);
334359
if( p->nUsed==0 ){
335360
blob_append_char(p, 0); /* NOTE: Changes nUsed. */
336361
p->nUsed = 0;
337362
}
338
- if( p->aData[p->nUsed]!=0 ){
363
+ if( p->nUsed<p->nAlloc ){
364
+ p->aData[p->nUsed] = 0;
365
+ }else{
339366
blob_materialize(p);
340367
}
341368
return p->aData;
342369
}
343370
@@ -450,10 +477,29 @@
450477
void blob_resize(Blob *pBlob, unsigned int newSize){
451478
pBlob->xRealloc(pBlob, newSize+1);
452479
pBlob->nUsed = newSize;
453480
pBlob->aData[newSize] = 0;
454481
}
482
+
483
+/*
484
+** Ensures that the given blob has at least the given amount of memory
485
+** allocated to it. Does not modify pBlob->nUsed nor will it reduce
486
+** the currently-allocated amount of memory.
487
+**
488
+** For semantic compatibility with blob_append_full(), if newSize is
489
+** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If
490
+** it didn't, it would be possible to bypass that hard-coded limit via
491
+** this function.
492
+*/
493
+void blob_reserve(Blob *pBlob, unsigned int newSize){
494
+ if(newSize>=0x7fff0000 ){
495
+ blob_panic();
496
+ }else if(newSize>pBlob->nUsed){
497
+ pBlob->xRealloc(pBlob, newSize);
498
+ pBlob->aData[newSize] = 0;
499
+ }
500
+}
455501
456502
/*
457503
** Make sure a blob is nul-terminated and is not a pointer to unmanaged
458504
** space. Return a pointer to the data.
459505
*/
@@ -1140,11 +1186,10 @@
11401186
blob_reset(&b3);
11411187
}
11421188
fossil_print("ok\n");
11431189
}
11441190
1145
-#if defined(_WIN32) || defined(__CYGWIN__)
11461191
/*
11471192
** Convert every \n character in the given blob into \r\n.
11481193
*/
11491194
void blob_add_cr(Blob *p){
11501195
char *z = p->aData;
@@ -1164,11 +1209,10 @@
11641209
if( (z[--j] = z[--i]) =='\n' ){
11651210
z[--j] = '\r';
11661211
}
11671212
}
11681213
}
1169
-#endif
11701214
11711215
/*
11721216
** Remove every \r character from the given blob, replacing each one with
11731217
** a \n character if it was not already part of a \r\n pair.
11741218
*/
@@ -1257,35 +1301,88 @@
12571301
char c;
12581302
int needEscape = 0;
12591303
int n = blob_size(pBlob);
12601304
char *z = blob_buffer(pBlob);
12611305
#if defined(_WIN32)
1306
+ const char cDirSep = '\\'; /* Use \ as directory separator */
12621307
const char cQuote = '"'; /* Use "..." quoting on windows */
1308
+ const char cEscape = '^'; /* Use ^X escaping on windows */
12631309
#else
1310
+ const char cDirSep = '/'; /* Use / as directory separator */
12641311
const char cQuote = '\''; /* Use '...' quoting on unix */
1312
+ const char cEscape = '\\'; /* Use \X escaping on unix */
12651313
#endif
12661314
12671315
for(i=0; (c = zIn[i])!=0; i++){
12681316
if( c==cQuote || (unsigned char)c<' ' ||
1269
- c=='\\' || c==';' || c=='*' || c=='?' || c=='[' ){
1317
+ c==cEscape || c==';' || c=='*' || c=='?' || c=='[' ){
12701318
Blob bad;
12711319
blob_token(pBlob, &bad);
12721320
fossil_fatal("the [%s] argument to the \"%s\" command contains "
12731321
"a character (ascii 0x%02x) that is a security risk",
12741322
zIn, blob_str(&bad), c);
12751323
}
1276
- if( !needEscape && !fossil_isalnum(c) && c!='/' && c!='.' && c!='_' ){
1324
+ if( !needEscape && !fossil_isalnum(c) && c!=cDirSep && c!='.' && c!='_' ){
12771325
needEscape = 1;
12781326
}
12791327
}
12801328
if( n>0 && !fossil_isspace(z[n-1]) ){
12811329
blob_append_char(pBlob, ' ');
12821330
}
12831331
if( needEscape ) blob_append_char(pBlob, cQuote);
1284
- if( zIn[0]=='-' ) blob_append(pBlob, "./", 2);
1332
+ if( zIn[0]=='-' ){
1333
+ blob_append_char(pBlob, '.');
1334
+ blob_append_char(pBlob, cDirSep);
1335
+#if defined(_WIN32)
1336
+ }else if( zIn[0]=='/' ){
1337
+ blob_append_char(pBlob, '.');
1338
+#endif
1339
+ }
1340
+#if defined(_WIN32)
1341
+ if( needEscape ){
1342
+ for(i=0; (c = zIn[i])!=0; i++){
1343
+ if( c==cQuote ) blob_append_char(pBlob, cDirSep);
1344
+ blob_append_char(pBlob, c);
1345
+ }
1346
+ }else{
1347
+ blob_append(pBlob, zIn, -1);
1348
+ }
1349
+#else
12851350
blob_append(pBlob, zIn, -1);
1286
- if( needEscape ) blob_append_char(pBlob, cQuote);
1351
+#endif
1352
+ if( needEscape ){
1353
+#if defined(_WIN32)
1354
+ /* NOTE: Trailing backslash must be doubled before final double quote. */
1355
+ if( pBlob->aData[pBlob->nUsed-1]==cDirSep ){
1356
+ blob_append_char(pBlob, cDirSep);
1357
+ }
1358
+#endif
1359
+ blob_append_char(pBlob, cQuote);
1360
+ }
1361
+}
1362
+
1363
+/*
1364
+** COMMAND: test-escaped-arg
1365
+**
1366
+** Usage %fossil ARG ...
1367
+**
1368
+** Run each argment through blob_append_escaped_arg() and show the
1369
+** result. Append each argument to "fossil test-echo" and run that
1370
+** using fossil_system() to verify that it really does get escaped
1371
+** correctly.
1372
+*/
1373
+void test_escaped_arg__cmd(void){
1374
+ int i;
1375
+ Blob x;
1376
+ blob_init(&x, 0, 0);
1377
+ for(i=2; i<g.argc; i++){
1378
+ fossil_print("%3d [%s]: ", i, g.argv[i]);
1379
+ blob_appendf(&x, "fossil test-echo %$", g.argv[i]);
1380
+ fossil_print("%s\n", blob_str(&x));
1381
+ fossil_system(blob_str(&x));
1382
+ blob_reset(&x);
1383
+ }
12871384
}
12881385
12891386
/*
12901387
** A read(2)-like impl for the Blob class. Reads (copies) up to nLen
12911388
** bytes from pIn, starting at position pIn->iCursor, and copies them
@@ -1344,21 +1441,22 @@
13441441
}else if( starts_with_utf16_bom(pBlob, &bomSize, &bomReverse) ){
13451442
zUtf8 = blob_buffer(pBlob);
13461443
if( bomReverse ){
13471444
/* Found BOM, but with reversed bytes */
13481445
unsigned int i = blob_size(pBlob);
1349
- while( i>0 ){
1446
+ while( i>1 ){
13501447
/* swap bytes of unicode representation */
13511448
char zTemp = zUtf8[--i];
13521449
zUtf8[i] = zUtf8[i-1];
13531450
zUtf8[--i] = zTemp;
13541451
}
13551452
}
13561453
/* Make sure the blob contains two terminating 0-bytes */
1357
- blob_append_char(pBlob, 0);
1454
+ blob_append(pBlob, "\000\000", 3);
13581455
zUtf8 = blob_str(pBlob) + bomSize;
13591456
zUtf8 = fossil_unicode_to_utf8(zUtf8);
1457
+ blob_reset(pBlob);
13601458
blob_set_dynamic(pBlob, zUtf8);
13611459
}else if( useMbcs && invalid_utf8(pBlob) ){
13621460
#if defined(_WIN32) || defined(__CYGWIN__)
13631461
zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob));
13641462
blob_reset(pBlob);
13651463
--- src/blob.c
+++ src/blob.c
@@ -277,12 +277,19 @@
277 pBlob->xRealloc = blobReallocStatic;
278 }
279
280 /*
281 ** Append text or data to the end of a blob.
 
 
 
 
 
 
 
282 */
283 void blob_append(Blob *pBlob, const char *aData, int nData){
284 sqlite3_int64 nNew;
285 assert( aData!=0 || nData==0 );
286 blob_is_init(pBlob);
287 if( nData<0 ) nData = strlen(aData);
288 if( nData==0 ) return;
@@ -301,22 +308,40 @@
301 }
302 memcpy(&pBlob->aData[pBlob->nUsed], aData, nData);
303 pBlob->nUsed += nData;
304 pBlob->aData[pBlob->nUsed] = 0; /* Blobs are always nul-terminated */
305 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
307 /*
308 ** Append a single character to the blob
309 */
310 void blob_append_char(Blob *pBlob, char c){
311 if( pBlob->nUsed+1 >= pBlob->nAlloc ){
312 pBlob->xRealloc(pBlob, pBlob->nUsed + pBlob->nAlloc + 100);
313 if( pBlob->nUsed + 1 >= pBlob->nAlloc ){
314 blob_panic();
315 }
316 }
317 pBlob->aData[pBlob->nUsed++] = c;
318 }
319
320 /*
321 ** Copy a blob
322 */
@@ -333,11 +358,13 @@
333 blob_is_init(p);
334 if( p->nUsed==0 ){
335 blob_append_char(p, 0); /* NOTE: Changes nUsed. */
336 p->nUsed = 0;
337 }
338 if( p->aData[p->nUsed]!=0 ){
 
 
339 blob_materialize(p);
340 }
341 return p->aData;
342 }
343
@@ -450,10 +477,29 @@
450 void blob_resize(Blob *pBlob, unsigned int newSize){
451 pBlob->xRealloc(pBlob, newSize+1);
452 pBlob->nUsed = newSize;
453 pBlob->aData[newSize] = 0;
454 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
456 /*
457 ** Make sure a blob is nul-terminated and is not a pointer to unmanaged
458 ** space. Return a pointer to the data.
459 */
@@ -1140,11 +1186,10 @@
1140 blob_reset(&b3);
1141 }
1142 fossil_print("ok\n");
1143 }
1144
1145 #if defined(_WIN32) || defined(__CYGWIN__)
1146 /*
1147 ** Convert every \n character in the given blob into \r\n.
1148 */
1149 void blob_add_cr(Blob *p){
1150 char *z = p->aData;
@@ -1164,11 +1209,10 @@
1164 if( (z[--j] = z[--i]) =='\n' ){
1165 z[--j] = '\r';
1166 }
1167 }
1168 }
1169 #endif
1170
1171 /*
1172 ** Remove every \r character from the given blob, replacing each one with
1173 ** a \n character if it was not already part of a \r\n pair.
1174 */
@@ -1257,35 +1301,88 @@
1257 char c;
1258 int needEscape = 0;
1259 int n = blob_size(pBlob);
1260 char *z = blob_buffer(pBlob);
1261 #if defined(_WIN32)
 
1262 const char cQuote = '"'; /* Use "..." quoting on windows */
 
1263 #else
 
1264 const char cQuote = '\''; /* Use '...' quoting on unix */
 
1265 #endif
1266
1267 for(i=0; (c = zIn[i])!=0; i++){
1268 if( c==cQuote || (unsigned char)c<' ' ||
1269 c=='\\' || c==';' || c=='*' || c=='?' || c=='[' ){
1270 Blob bad;
1271 blob_token(pBlob, &bad);
1272 fossil_fatal("the [%s] argument to the \"%s\" command contains "
1273 "a character (ascii 0x%02x) that is a security risk",
1274 zIn, blob_str(&bad), c);
1275 }
1276 if( !needEscape && !fossil_isalnum(c) && c!='/' && c!='.' && c!='_' ){
1277 needEscape = 1;
1278 }
1279 }
1280 if( n>0 && !fossil_isspace(z[n-1]) ){
1281 blob_append_char(pBlob, ' ');
1282 }
1283 if( needEscape ) blob_append_char(pBlob, cQuote);
1284 if( zIn[0]=='-' ) blob_append(pBlob, "./", 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285 blob_append(pBlob, zIn, -1);
1286 if( needEscape ) blob_append_char(pBlob, cQuote);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1287 }
1288
1289 /*
1290 ** A read(2)-like impl for the Blob class. Reads (copies) up to nLen
1291 ** bytes from pIn, starting at position pIn->iCursor, and copies them
@@ -1344,21 +1441,22 @@
1344 }else if( starts_with_utf16_bom(pBlob, &bomSize, &bomReverse) ){
1345 zUtf8 = blob_buffer(pBlob);
1346 if( bomReverse ){
1347 /* Found BOM, but with reversed bytes */
1348 unsigned int i = blob_size(pBlob);
1349 while( i>0 ){
1350 /* swap bytes of unicode representation */
1351 char zTemp = zUtf8[--i];
1352 zUtf8[i] = zUtf8[i-1];
1353 zUtf8[--i] = zTemp;
1354 }
1355 }
1356 /* Make sure the blob contains two terminating 0-bytes */
1357 blob_append_char(pBlob, 0);
1358 zUtf8 = blob_str(pBlob) + bomSize;
1359 zUtf8 = fossil_unicode_to_utf8(zUtf8);
 
1360 blob_set_dynamic(pBlob, zUtf8);
1361 }else if( useMbcs && invalid_utf8(pBlob) ){
1362 #if defined(_WIN32) || defined(__CYGWIN__)
1363 zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob));
1364 blob_reset(pBlob);
1365
--- src/blob.c
+++ src/blob.c
@@ -277,12 +277,19 @@
277 pBlob->xRealloc = blobReallocStatic;
278 }
279
280 /*
281 ** Append text or data to the end of a blob.
282 **
283 ** The blob_append_full() routine is a complete implementation.
284 ** The blob_append() routine only works for cases where nData>0 and
285 ** no resizing is required, and falls back to blob_append_full() if
286 ** either condition is not met, but runs faster in the common case
287 ** where all conditions are met. The use of blob_append() is
288 ** recommended, unless it is known in advance that nData<0.
289 */
290 void blob_append_full(Blob *pBlob, const char *aData, int nData){
291 sqlite3_int64 nNew;
292 assert( aData!=0 || nData==0 );
293 blob_is_init(pBlob);
294 if( nData<0 ) nData = strlen(aData);
295 if( nData==0 ) return;
@@ -301,22 +308,40 @@
308 }
309 memcpy(&pBlob->aData[pBlob->nUsed], aData, nData);
310 pBlob->nUsed += nData;
311 pBlob->aData[pBlob->nUsed] = 0; /* Blobs are always nul-terminated */
312 }
313 void blob_append(Blob *pBlob, const char *aData, int nData){
314 sqlite3_int64 nUsed;
315 assert( aData!=0 || nData==0 );
316 /* blob_is_init(pBlob); // omitted for speed */
317 if( nData<=0 || pBlob->nUsed + nData >= pBlob->nAlloc ){
318 blob_append_full(pBlob, aData, nData);
319 return;
320 }
321 nUsed = pBlob->nUsed;
322 pBlob->nUsed += nData;
323 pBlob->aData[pBlob->nUsed] = 0;
324 memcpy(&pBlob->aData[nUsed], aData, nData);
325 }
326
327 /*
328 ** Append a string literal to a blob.
329 */
330 #if INTERFACE
331 #define blob_append_string(BLOB,STR) blob_append(BLOB,STR,sizeof(STR)-1)
332 #endif
333
334 /*
335 ** Append a single character to the blob
336 */
337 void blob_append_char(Blob *pBlob, char c){
338 if( pBlob->nUsed+1 >= pBlob->nAlloc ){
339 blob_append_full(pBlob, &c, 1);
340 }else{
341 pBlob->aData[pBlob->nUsed++] = c;
 
342 }
 
343 }
344
345 /*
346 ** Copy a blob
347 */
@@ -333,11 +358,13 @@
358 blob_is_init(p);
359 if( p->nUsed==0 ){
360 blob_append_char(p, 0); /* NOTE: Changes nUsed. */
361 p->nUsed = 0;
362 }
363 if( p->nUsed<p->nAlloc ){
364 p->aData[p->nUsed] = 0;
365 }else{
366 blob_materialize(p);
367 }
368 return p->aData;
369 }
370
@@ -450,10 +477,29 @@
477 void blob_resize(Blob *pBlob, unsigned int newSize){
478 pBlob->xRealloc(pBlob, newSize+1);
479 pBlob->nUsed = newSize;
480 pBlob->aData[newSize] = 0;
481 }
482
483 /*
484 ** Ensures that the given blob has at least the given amount of memory
485 ** allocated to it. Does not modify pBlob->nUsed nor will it reduce
486 ** the currently-allocated amount of memory.
487 **
488 ** For semantic compatibility with blob_append_full(), if newSize is
489 ** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If
490 ** it didn't, it would be possible to bypass that hard-coded limit via
491 ** this function.
492 */
493 void blob_reserve(Blob *pBlob, unsigned int newSize){
494 if(newSize>=0x7fff0000 ){
495 blob_panic();
496 }else if(newSize>pBlob->nUsed){
497 pBlob->xRealloc(pBlob, newSize);
498 pBlob->aData[newSize] = 0;
499 }
500 }
501
502 /*
503 ** Make sure a blob is nul-terminated and is not a pointer to unmanaged
504 ** space. Return a pointer to the data.
505 */
@@ -1140,11 +1186,10 @@
1186 blob_reset(&b3);
1187 }
1188 fossil_print("ok\n");
1189 }
1190
 
1191 /*
1192 ** Convert every \n character in the given blob into \r\n.
1193 */
1194 void blob_add_cr(Blob *p){
1195 char *z = p->aData;
@@ -1164,11 +1209,10 @@
1209 if( (z[--j] = z[--i]) =='\n' ){
1210 z[--j] = '\r';
1211 }
1212 }
1213 }
 
1214
1215 /*
1216 ** Remove every \r character from the given blob, replacing each one with
1217 ** a \n character if it was not already part of a \r\n pair.
1218 */
@@ -1257,35 +1301,88 @@
1301 char c;
1302 int needEscape = 0;
1303 int n = blob_size(pBlob);
1304 char *z = blob_buffer(pBlob);
1305 #if defined(_WIN32)
1306 const char cDirSep = '\\'; /* Use \ as directory separator */
1307 const char cQuote = '"'; /* Use "..." quoting on windows */
1308 const char cEscape = '^'; /* Use ^X escaping on windows */
1309 #else
1310 const char cDirSep = '/'; /* Use / as directory separator */
1311 const char cQuote = '\''; /* Use '...' quoting on unix */
1312 const char cEscape = '\\'; /* Use \X escaping on unix */
1313 #endif
1314
1315 for(i=0; (c = zIn[i])!=0; i++){
1316 if( c==cQuote || (unsigned char)c<' ' ||
1317 c==cEscape || c==';' || c=='*' || c=='?' || c=='[' ){
1318 Blob bad;
1319 blob_token(pBlob, &bad);
1320 fossil_fatal("the [%s] argument to the \"%s\" command contains "
1321 "a character (ascii 0x%02x) that is a security risk",
1322 zIn, blob_str(&bad), c);
1323 }
1324 if( !needEscape && !fossil_isalnum(c) && c!=cDirSep && c!='.' && c!='_' ){
1325 needEscape = 1;
1326 }
1327 }
1328 if( n>0 && !fossil_isspace(z[n-1]) ){
1329 blob_append_char(pBlob, ' ');
1330 }
1331 if( needEscape ) blob_append_char(pBlob, cQuote);
1332 if( zIn[0]=='-' ){
1333 blob_append_char(pBlob, '.');
1334 blob_append_char(pBlob, cDirSep);
1335 #if defined(_WIN32)
1336 }else if( zIn[0]=='/' ){
1337 blob_append_char(pBlob, '.');
1338 #endif
1339 }
1340 #if defined(_WIN32)
1341 if( needEscape ){
1342 for(i=0; (c = zIn[i])!=0; i++){
1343 if( c==cQuote ) blob_append_char(pBlob, cDirSep);
1344 blob_append_char(pBlob, c);
1345 }
1346 }else{
1347 blob_append(pBlob, zIn, -1);
1348 }
1349 #else
1350 blob_append(pBlob, zIn, -1);
1351 #endif
1352 if( needEscape ){
1353 #if defined(_WIN32)
1354 /* NOTE: Trailing backslash must be doubled before final double quote. */
1355 if( pBlob->aData[pBlob->nUsed-1]==cDirSep ){
1356 blob_append_char(pBlob, cDirSep);
1357 }
1358 #endif
1359 blob_append_char(pBlob, cQuote);
1360 }
1361 }
1362
1363 /*
1364 ** COMMAND: test-escaped-arg
1365 **
1366 ** Usage %fossil ARG ...
1367 **
1368 ** Run each argment through blob_append_escaped_arg() and show the
1369 ** result. Append each argument to "fossil test-echo" and run that
1370 ** using fossil_system() to verify that it really does get escaped
1371 ** correctly.
1372 */
1373 void test_escaped_arg__cmd(void){
1374 int i;
1375 Blob x;
1376 blob_init(&x, 0, 0);
1377 for(i=2; i<g.argc; i++){
1378 fossil_print("%3d [%s]: ", i, g.argv[i]);
1379 blob_appendf(&x, "fossil test-echo %$", g.argv[i]);
1380 fossil_print("%s\n", blob_str(&x));
1381 fossil_system(blob_str(&x));
1382 blob_reset(&x);
1383 }
1384 }
1385
1386 /*
1387 ** A read(2)-like impl for the Blob class. Reads (copies) up to nLen
1388 ** bytes from pIn, starting at position pIn->iCursor, and copies them
@@ -1344,21 +1441,22 @@
1441 }else if( starts_with_utf16_bom(pBlob, &bomSize, &bomReverse) ){
1442 zUtf8 = blob_buffer(pBlob);
1443 if( bomReverse ){
1444 /* Found BOM, but with reversed bytes */
1445 unsigned int i = blob_size(pBlob);
1446 while( i>1 ){
1447 /* swap bytes of unicode representation */
1448 char zTemp = zUtf8[--i];
1449 zUtf8[i] = zUtf8[i-1];
1450 zUtf8[--i] = zTemp;
1451 }
1452 }
1453 /* Make sure the blob contains two terminating 0-bytes */
1454 blob_append(pBlob, "\000\000", 3);
1455 zUtf8 = blob_str(pBlob) + bomSize;
1456 zUtf8 = fossil_unicode_to_utf8(zUtf8);
1457 blob_reset(pBlob);
1458 blob_set_dynamic(pBlob, zUtf8);
1459 }else if( useMbcs && invalid_utf8(pBlob) ){
1460 #if defined(_WIN32) || defined(__CYGWIN__)
1461 zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob));
1462 blob_reset(pBlob);
1463
+51 -10
--- src/branch.c
+++ src/branch.c
@@ -18,10 +18,51 @@
1818
** This file contains code used to create new branches within a repository.
1919
*/
2020
#include "config.h"
2121
#include "branch.h"
2222
#include <assert.h>
23
+
24
+/*
25
+** Return true if zBr is the branch name associated with check-in with
26
+** blob.uuid value of zUuid
27
+*/
28
+int branch_includes_uuid(const char *zBr, const char *zUuid){
29
+ return db_exists(
30
+ "SELECT 1 FROM tagxref, blob"
31
+ " WHERE blob.uuid=%Q AND tagxref.rid=blob.rid"
32
+ " AND tagxref.value=%Q AND tagxref.tagtype>0"
33
+ " AND tagxref.tagid=%d",
34
+ zUuid, zBr, TAG_BRANCH
35
+ );
36
+}
37
+
38
+/*
39
+** If RID refers to a check-in, return the name of the branch for that
40
+** check-in.
41
+**
42
+** Space to hold the returned value is obtained from fossil_malloc()
43
+** and should be freed by the caller.
44
+*/
45
+char *branch_of_rid(int rid){
46
+ char *zBr = 0;
47
+ static Stmt q;
48
+ db_static_prepare(&q,
49
+ "SELECT value FROM tagxref"
50
+ " WHERE rid=$rid AND tagid=%d"
51
+ " AND tagtype>0", TAG_BRANCH);
52
+ db_bind_int(&q, "$rid", rid);
53
+ if( db_step(&q)==SQLITE_ROW ){
54
+ zBr = fossil_strdup(db_column_text(&q,0));
55
+ }
56
+ db_reset(&q);
57
+ if( zBr==0 ){
58
+ static char *zMain = 0;
59
+ if( zMain==0 ) zMain = db_get("main-branch",0);
60
+ zBr = fossil_strdup(zMain);
61
+ }
62
+ return zBr;
63
+}
2364
2465
/*
2566
** fossil branch new NAME BASIS ?OPTIONS?
2667
** argv0 argv1 argv2 argv3 argv4
2768
*/
@@ -117,11 +158,10 @@
117158
blob_appendf(&branch, "T *bgcolor * %F\n", zColor);
118159
}
119160
blob_appendf(&branch, "T *branch * %F\n", zBranch);
120161
blob_appendf(&branch, "T *sym-%F *\n", zBranch);
121162
if( isPrivate ){
122
- blob_appendf(&branch, "T +private *\n");
123163
noSign = 1;
124164
}
125165
126166
/* Cancel all other symbolic tags */
127167
db_prepare(&q,
@@ -218,11 +258,11 @@
218258
@ GROUP BY 1;
219259
;
220260
221261
/* Call this routine to create the TEMP table */
222262
static void brlist_create_temp_table(void){
223
- db_multi_exec(createBrlistQuery/*works-like:""*/);
263
+ db_exec_sql(createBrlistQuery);
224264
}
225265
226266
227267
#if INTERFACE
228268
/*
@@ -307,27 +347,27 @@
307347
** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS?
308348
**
309349
** Run various subcommands to manage branches of the open repository or
310350
** of the repository identified by the -R or --repository option.
311351
**
312
-** fossil branch current
352
+** > fossil branch current
313353
**
314354
** Print the name of the branch for the current check-out
315355
**
316
-** fossil branch info BRANCH-NAME
356
+** > fossil branch info BRANCH-NAME
317357
**
318358
** Print information about a branch
319359
**
320
-** fossil branch list|ls ?OPTIONS?
360
+** > fossil branch list|ls ?OPTIONS?
321361
**
322362
** List all branches. Options:
323363
** -a|--all List all branches. Default show only open branches
324364
** -c|--closed List closed branches.
325365
** -r Reverse the sort order
326366
** -t Show recently changed branches first
327367
**
328
-** fossil branch new BRANCH-NAME BASIS ?OPTIONS?
368
+** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
329369
**
330370
** Create a new branch BRANCH-NAME off of check-in BASIS.
331371
** Supported options for this subcommand include:
332372
** --private branch is private (i.e., remains local)
333373
** --bgcolor COLOR use COLOR instead of automatic background
@@ -339,18 +379,19 @@
339379
** year-month-day form, it may be truncated, the "T" may be
340380
** replaced by a space, and it may also name a timezone offset
341381
** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
342382
** Either no timezone suffix or "Z" means UTC.
343383
**
344
-** Options:
384
+** Options valid for all subcommands:
385
+**
345386
** -R|--repository FILE Run commands on repository FILE
346387
**
347388
** Summary:
348389
** fossil branch current
349
-** fossil branch info BRANCHNAME
390
+** fossil branch info BRANCH-NAME
350391
** fossil branch [list|ls]
351
-** fossil branch new
392
+** fossil branch new BRANCH-NAME BASIS
352393
*/
353394
void branch_cmd(void){
354395
int n;
355396
const char *zCmd = "list";
356397
db_find_and_open_repository(0, 0);
@@ -661,9 +702,9 @@
661702
** many descenders to (off-screen) parents. */
662703
tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
663704
if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
664705
if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
665706
if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
666
- www_print_timeline(&q, tmFlags, 0, 0, 0, 0, brtimeline_extra);
707
+ www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, brtimeline_extra);
667708
db_finalize(&q);
668709
style_footer();
669710
}
670711
--- src/branch.c
+++ src/branch.c
@@ -18,10 +18,51 @@
18 ** This file contains code used to create new branches within a repository.
19 */
20 #include "config.h"
21 #include "branch.h"
22 #include <assert.h>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
24 /*
25 ** fossil branch new NAME BASIS ?OPTIONS?
26 ** argv0 argv1 argv2 argv3 argv4
27 */
@@ -117,11 +158,10 @@
117 blob_appendf(&branch, "T *bgcolor * %F\n", zColor);
118 }
119 blob_appendf(&branch, "T *branch * %F\n", zBranch);
120 blob_appendf(&branch, "T *sym-%F *\n", zBranch);
121 if( isPrivate ){
122 blob_appendf(&branch, "T +private *\n");
123 noSign = 1;
124 }
125
126 /* Cancel all other symbolic tags */
127 db_prepare(&q,
@@ -218,11 +258,11 @@
218 @ GROUP BY 1;
219 ;
220
221 /* Call this routine to create the TEMP table */
222 static void brlist_create_temp_table(void){
223 db_multi_exec(createBrlistQuery/*works-like:""*/);
224 }
225
226
227 #if INTERFACE
228 /*
@@ -307,27 +347,27 @@
307 ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS?
308 **
309 ** Run various subcommands to manage branches of the open repository or
310 ** of the repository identified by the -R or --repository option.
311 **
312 ** fossil branch current
313 **
314 ** Print the name of the branch for the current check-out
315 **
316 ** fossil branch info BRANCH-NAME
317 **
318 ** Print information about a branch
319 **
320 ** fossil branch list|ls ?OPTIONS?
321 **
322 ** List all branches. Options:
323 ** -a|--all List all branches. Default show only open branches
324 ** -c|--closed List closed branches.
325 ** -r Reverse the sort order
326 ** -t Show recently changed branches first
327 **
328 ** fossil branch new BRANCH-NAME BASIS ?OPTIONS?
329 **
330 ** Create a new branch BRANCH-NAME off of check-in BASIS.
331 ** Supported options for this subcommand include:
332 ** --private branch is private (i.e., remains local)
333 ** --bgcolor COLOR use COLOR instead of automatic background
@@ -339,18 +379,19 @@
339 ** year-month-day form, it may be truncated, the "T" may be
340 ** replaced by a space, and it may also name a timezone offset
341 ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
342 ** Either no timezone suffix or "Z" means UTC.
343 **
344 ** Options:
 
345 ** -R|--repository FILE Run commands on repository FILE
346 **
347 ** Summary:
348 ** fossil branch current
349 ** fossil branch info BRANCHNAME
350 ** fossil branch [list|ls]
351 ** fossil branch new
352 */
353 void branch_cmd(void){
354 int n;
355 const char *zCmd = "list";
356 db_find_and_open_repository(0, 0);
@@ -661,9 +702,9 @@
661 ** many descenders to (off-screen) parents. */
662 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
663 if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
664 if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
665 if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
666 www_print_timeline(&q, tmFlags, 0, 0, 0, 0, brtimeline_extra);
667 db_finalize(&q);
668 style_footer();
669 }
670
--- src/branch.c
+++ src/branch.c
@@ -18,10 +18,51 @@
18 ** This file contains code used to create new branches within a repository.
19 */
20 #include "config.h"
21 #include "branch.h"
22 #include <assert.h>
23
24 /*
25 ** Return true if zBr is the branch name associated with check-in with
26 ** blob.uuid value of zUuid
27 */
28 int branch_includes_uuid(const char *zBr, const char *zUuid){
29 return db_exists(
30 "SELECT 1 FROM tagxref, blob"
31 " WHERE blob.uuid=%Q AND tagxref.rid=blob.rid"
32 " AND tagxref.value=%Q AND tagxref.tagtype>0"
33 " AND tagxref.tagid=%d",
34 zUuid, zBr, TAG_BRANCH
35 );
36 }
37
38 /*
39 ** If RID refers to a check-in, return the name of the branch for that
40 ** check-in.
41 **
42 ** Space to hold the returned value is obtained from fossil_malloc()
43 ** and should be freed by the caller.
44 */
45 char *branch_of_rid(int rid){
46 char *zBr = 0;
47 static Stmt q;
48 db_static_prepare(&q,
49 "SELECT value FROM tagxref"
50 " WHERE rid=$rid AND tagid=%d"
51 " AND tagtype>0", TAG_BRANCH);
52 db_bind_int(&q, "$rid", rid);
53 if( db_step(&q)==SQLITE_ROW ){
54 zBr = fossil_strdup(db_column_text(&q,0));
55 }
56 db_reset(&q);
57 if( zBr==0 ){
58 static char *zMain = 0;
59 if( zMain==0 ) zMain = db_get("main-branch",0);
60 zBr = fossil_strdup(zMain);
61 }
62 return zBr;
63 }
64
65 /*
66 ** fossil branch new NAME BASIS ?OPTIONS?
67 ** argv0 argv1 argv2 argv3 argv4
68 */
@@ -117,11 +158,10 @@
158 blob_appendf(&branch, "T *bgcolor * %F\n", zColor);
159 }
160 blob_appendf(&branch, "T *branch * %F\n", zBranch);
161 blob_appendf(&branch, "T *sym-%F *\n", zBranch);
162 if( isPrivate ){
 
163 noSign = 1;
164 }
165
166 /* Cancel all other symbolic tags */
167 db_prepare(&q,
@@ -218,11 +258,11 @@
258 @ GROUP BY 1;
259 ;
260
261 /* Call this routine to create the TEMP table */
262 static void brlist_create_temp_table(void){
263 db_exec_sql(createBrlistQuery);
264 }
265
266
267 #if INTERFACE
268 /*
@@ -307,27 +347,27 @@
347 ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS?
348 **
349 ** Run various subcommands to manage branches of the open repository or
350 ** of the repository identified by the -R or --repository option.
351 **
352 ** > fossil branch current
353 **
354 ** Print the name of the branch for the current check-out
355 **
356 ** > fossil branch info BRANCH-NAME
357 **
358 ** Print information about a branch
359 **
360 ** > fossil branch list|ls ?OPTIONS?
361 **
362 ** List all branches. Options:
363 ** -a|--all List all branches. Default show only open branches
364 ** -c|--closed List closed branches.
365 ** -r Reverse the sort order
366 ** -t Show recently changed branches first
367 **
368 ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
369 **
370 ** Create a new branch BRANCH-NAME off of check-in BASIS.
371 ** Supported options for this subcommand include:
372 ** --private branch is private (i.e., remains local)
373 ** --bgcolor COLOR use COLOR instead of automatic background
@@ -339,18 +379,19 @@
379 ** year-month-day form, it may be truncated, the "T" may be
380 ** replaced by a space, and it may also name a timezone offset
381 ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
382 ** Either no timezone suffix or "Z" means UTC.
383 **
384 ** Options valid for all subcommands:
385 **
386 ** -R|--repository FILE Run commands on repository FILE
387 **
388 ** Summary:
389 ** fossil branch current
390 ** fossil branch info BRANCH-NAME
391 ** fossil branch [list|ls]
392 ** fossil branch new BRANCH-NAME BASIS
393 */
394 void branch_cmd(void){
395 int n;
396 const char *zCmd = "list";
397 db_find_and_open_repository(0, 0);
@@ -661,9 +702,9 @@
702 ** many descenders to (off-screen) parents. */
703 tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL;
704 if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH;
705 if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR;
706 if( PB("ubg")!=0 ) tmFlags |= TIMELINE_UCOLOR;
707 www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, brtimeline_extra);
708 db_finalize(&q);
709 style_footer();
710 }
711
+158 -79
--- src/browse.c
+++ src/browse.c
@@ -59,10 +59,18 @@
5959
zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]);
6060
sqlite3_result_text(context, zOut, i-n+1, sqlite3_free);
6161
}
6262
}
6363
64
+/*
65
+** Flag arguments for hyperlinked_path()
66
+*/
67
+#if INTERFACE
68
+# define LINKPATH_FINFO 0x0001 /* Link final term to /finfo */
69
+# define LINKPATH_FILE 0x0002 /* Link final term to /file */
70
+#endif
71
+
6472
/*
6573
** Given a pathname which is a relative path from the root of
6674
** the repository to a file or directory, compute a string which
6775
** is an HTML rendering of that path with hyperlinks on each
6876
** directory component of the path where the hyperlink redirects
@@ -76,29 +84,36 @@
7684
void hyperlinked_path(
7785
const char *zPath, /* Path to render */
7886
Blob *pOut, /* Write into this blob */
7987
const char *zCI, /* check-in name, or NULL */
8088
const char *zURI, /* "dir" or "tree" */
81
- const char *zREx /* Extra query parameters */
89
+ const char *zREx, /* Extra query parameters */
90
+ unsigned int mFlags /* Extra flags */
8291
){
8392
int i, j;
8493
char *zSep = "";
8594
8695
for(i=0; zPath[i]; i=j){
8796
for(j=i; zPath[j] && zPath[j]!='/'; j++){}
88
- if( zPath[j] && g.perm.Hyperlink ){
89
- if( zCI ){
90
- char *zLink = href("%R/%s?name=%#T%s&ci=%!S", zURI, j, zPath, zREx,zCI);
91
- blob_appendf(pOut, "%s%z%#h</a>",
92
- zSep, zLink, j-i, &zPath[i]);
93
- }else{
94
- char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
95
- blob_appendf(pOut, "%s%z%#h</a>",
96
- zSep, zLink, j-i, &zPath[i]);
97
- }
98
- }else{
99
- blob_appendf(pOut, "%s%#h", zSep, j-i, &zPath[i]);
97
+ if( zPath[j]==0 ){
98
+ if( mFlags & LINKPATH_FILE ){
99
+ zURI = "file";
100
+ }else if( mFlags & LINKPATH_FINFO ){
101
+ zURI = "finfo";
102
+ }else{
103
+ blob_appendf(pOut, "/%h", zPath+i);
104
+ break;
105
+ }
106
+ }
107
+ if( zCI ){
108
+ char *zLink = href("%R/%s?name=%#T%s&ci=%T", zURI, j, zPath, zREx,zCI);
109
+ blob_appendf(pOut, "%s%z%#h</a>",
110
+ zSep, zLink, j-i, &zPath[i]);
111
+ }else{
112
+ char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
113
+ blob_appendf(pOut, "%s%z%#h</a>",
114
+ zSep, zLink, j-i, &zPath[i]);
100115
}
101116
zSep = "/";
102117
while( zPath[j]=='/' ){ j++; }
103118
}
104119
}
@@ -116,38 +131,35 @@
116131
**
117132
** name=PATH Directory to display. Optional. Top-level if missing
118133
** ci=LABEL Show only files in this check-in. Optional.
119134
** type=TYPE TYPE=flat: use this display
120135
** TYPE=tree: use the /tree display instead
136
+** noreadme Do not attempt to display the README file.
121137
*/
122138
void page_dir(void){
123139
char *zD = fossil_strdup(P("name"));
124140
int nD = zD ? strlen(zD)+1 : 0;
125141
int mxLen;
126
- int n;
127142
char *zPrefix;
128143
Stmt q;
129144
const char *zCI = P("ci");
130145
int rid = 0;
131146
char *zUuid = 0;
132
- Blob dirname;
133147
Manifest *pM = 0;
134148
const char *zSubdirLink;
135149
int linkTrunk = 1;
136150
int linkTip = 1;
137151
HQuery sURI;
152
+ int isSymbolicCI = 0; /* ci= is symbolic name, not a hash prefix */
153
+ int isBranchCI = 0; /* True if ci= refers to a branch name */
154
+ char *zHeader = 0;
138155
156
+ if( zCI && strlen(zCI)==0 ){ zCI = 0; }
139157
if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; }
140158
login_check_credentials();
141159
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
142160
while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
143
- style_header("File List");
144
- style_adunit_config(ADUNIT_RIGHT_OK);
145
- sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
146
- pathelementFunc, 0, 0);
147
- url_initialize(&sURI, "dir");
148
- cgi_query_parameters_to_url(&sURI);
149161
150162
/* If the name= parameter is an empty string, make it a NULL pointer */
151163
if( zD && strlen(zD)==0 ){ zD = 0; }
152164
153165
/* If a specific check-in is requested, fetch and parse it. If the
@@ -159,53 +171,80 @@
159171
if( pM ){
160172
int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
161173
linkTrunk = trunkRid && rid != trunkRid;
162174
linkTip = rid != symbolic_name_to_rid("tip", "ci");
163175
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
176
+ isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
177
+ isBranchCI = branch_includes_uuid(zCI, zUuid);
178
+ Th_Store("current_checkin", zCI);
164179
}else{
165180
zCI = 0;
166181
}
167182
}
168183
184
+ assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
185
+ if( zD==0 ){
186
+ if( zCI ){
187
+ zHeader = mprintf("Top-level Files of %s", zCI);
188
+ }else{
189
+ zHeader = mprintf("All Top-level Files");
190
+ }
191
+ }else{
192
+ if( zCI ){
193
+ zHeader = mprintf("Files in %s/ of %s", zD, zCI);
194
+ }else{
195
+ zHeader = mprintf("All File in %s/", zD);
196
+ }
197
+ }
198
+ style_header("%s", zHeader);
199
+ fossil_free(zHeader);
200
+ style_adunit_config(ADUNIT_RIGHT_OK);
201
+ sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
202
+ pathelementFunc, 0, 0);
203
+ url_initialize(&sURI, "dir");
204
+ cgi_query_parameters_to_url(&sURI);
205
+
169206
/* Compute the title of the page */
170
- blob_zero(&dirname);
171207
if( zD ){
172
- blob_append(&dirname, "in directory ", -1);
173
- hyperlinked_path(zD, &dirname, zCI, "dir", "");
208
+ Blob dirname;
209
+ blob_init(&dirname, 0, 0);
210
+ hyperlinked_path(zD, &dirname, zCI, "dir", "", 0);
211
+ @ <h2>Files in directory %s(blob_str(&dirname)) \
212
+ blob_reset(&dirname);
174213
zPrefix = mprintf("%s/", zD);
175214
style_submenu_element("Top-Level", "%s",
176215
url_render(&sURI, "name", 0, 0, 0));
177216
}else{
178
- blob_append(&dirname, "in the top-level directory", -1);
217
+ @ <h2>Files in the top-level directory \
179218
zPrefix = "";
180219
}
220
+ if( zCI ){
221
+ if( fossil_strcmp(zCI,"tip")==0 ){
222
+ @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a></h2>
223
+ }else if( isBranchCI ){
224
+ @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a> \
225
+ @ of branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a></h2>
226
+ }else {
227
+ @ of check-in %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2>
228
+ }
229
+ zSubdirLink = mprintf("%R/dir?ci=%T&name=%T", zCI, zPrefix);
230
+ if( nD==0 ){
231
+ style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
232
+ }
233
+ }else{
234
+ @ in any check-in</h2>
235
+ zSubdirLink = mprintf("%R/dir?name=%T", zPrefix);
236
+ }
181237
if( linkTrunk ){
182238
style_submenu_element("Trunk", "%s",
183239
url_render(&sURI, "ci", "trunk", 0, 0));
184240
}
185241
if( linkTip ){
186242
style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
187243
}
188
- if( zCI ){
189
- @ <h2>Files of check-in [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>]
190
- @ %s(blob_str(&dirname))
191
- if( zD ){
192
- @ &nbsp;&nbsp;%z(href("%R/timeline?chng=%T/*", zD))[history]</a>
193
- }
194
- @ </h2>
195
- zSubdirLink = mprintf("%R/dir?ci=%!S&name=%T", zUuid, zPrefix);
196
- if( nD==0 ){
197
- style_submenu_element("File Ages", "%R/fileage?name=%!S", zUuid);
198
- }
199
- }else{
200
- @ <h2>The union of all files from all check-ins
201
- @ %s(blob_str(&dirname))
202
- if( zD ){
203
- @ &nbsp;&nbsp;%z(href("%R/timeline?chng=%T/*", zD))[history]</a>
204
- }
205
- @ </h2>
206
- zSubdirLink = mprintf("%R/dir?name=%T", zPrefix);
244
+ if( zD ){
245
+ style_submenu_element("History","%R/timeline?chng=%T/*", zD);
207246
}
208247
style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
209248
style_submenu_element("Tree-View", "%s",
210249
url_render(&sURI, "type", "tree", 0, 0));
211250
@@ -268,15 +307,14 @@
268307
269308
/* Generate a multi-column table listing the contents of zD[]
270309
** directory.
271310
*/
272311
mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
273
- n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/");
274312
if( mxLen<12 ) mxLen = 12;
275313
mxLen += (mxLen+9)/10;
276314
db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/");
277
- @ <div class="columns" style="columns: %d(mxLen)ex %d(n);">
315
+ @ <div class="columns files" style="columns: %d(mxLen)ex auto">
278316
@ <ul class="browser">
279317
while( db_step(&q)==SQLITE_ROW ){
280318
const char *zFN;
281319
zFN = db_column_text(&q, 0);
282320
if( zFN[0]=='/' ){
@@ -283,21 +321,28 @@
283321
zFN++;
284322
@ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
285323
}else{
286324
const char *zLink;
287325
if( zCI ){
288
- const char *zUuid = db_column_text(&q, 1);
289
- zLink = href("%R/artifact/%!S",zUuid);
326
+ zLink = href("%R/file?name=%T%T&ci=%T",zPrefix,zFN,zCI);
290327
}else{
291328
zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
292329
}
293330
@ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
294331
}
295332
}
296333
db_finalize(&q);
297334
manifest_destroy(pM);
298335
@ </ul></div>
336
+
337
+ /* If the "noreadme" query parameter is present, do not try to
338
+ ** show the content of the README file.
339
+ */
340
+ if( P("noreadme")!=0 ){
341
+ style_footer();
342
+ return;
343
+ }
299344
300345
/* If the directory contains a readme file, then display its content below
301346
** the list of files
302347
*/
303348
db_prepare(&q,
@@ -345,10 +390,11 @@
345390
@ </iframe>
346391
}else{
347392
Blob content;
348393
const char *zMime = mimetype_from_name(zName);
349394
content_get(rid, &content);
395
+ safe_html_context(DOCSRC_FILE);
350396
wiki_render_by_mimetype(&content, zMime);
351397
}
352398
}
353399
}
354400
db_finalize(&q);
@@ -605,11 +651,15 @@
605651
HQuery sURI; /* Hyperlink */
606652
int startExpanded; /* True to start out with the tree expanded */
607653
int showDirOnly; /* Show directories only. Omit files */
608654
int nDir = 0; /* Number of directories. Used for ID attributes */
609655
char *zProjectName = db_get("project-name", 0);
656
+ int isSymbolicCI = 0; /* ci= is a symbolic name, not a hash prefix */
657
+ int isBranchCI = 0; /* ci= refers to a branch name */
658
+ char *zHeader = 0;
610659
660
+ if( zCI && strlen(zCI)==0 ){ zCI = 0; }
611661
if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; }
612662
memset(&sTree, 0, sizeof(sTree));
613663
login_check_credentials();
614664
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
615665
while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
@@ -617,14 +667,12 @@
617667
pathelementFunc, 0, 0);
618668
url_initialize(&sURI, "tree");
619669
cgi_query_parameters_to_url(&sURI);
620670
if( PB("nofiles") ){
621671
showDirOnly = 1;
622
- style_header("Folder Hierarchy");
623672
}else{
624673
showDirOnly = 0;
625
- style_header("File Tree");
626674
}
627675
style_adunit_config(ADUNIT_RIGHT_OK);
628676
if( PB("expand") ){
629677
startExpanded = 1;
630678
}else{
@@ -653,37 +701,55 @@
653701
linkTip = rid != symbolic_name_to_rid("tip", "ci");
654702
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
655703
rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
656704
zNow = db_text("", "SELECT datetime(mtime,toLocal())"
657705
" FROM event WHERE objid=%d", rid);
706
+ isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
707
+ isBranchCI = branch_includes_uuid(zCI, zUuid);
708
+ Th_Store("current_checkin", zCI);
658709
}else{
659710
zCI = 0;
660711
}
661712
}
662713
if( zCI==0 ){
663714
rNow = db_double(0.0, "SELECT max(mtime) FROM event");
664715
zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
665716
}
717
+
718
+ assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
719
+ if( zD==0 ){
720
+ if( zCI ){
721
+ zHeader = mprintf("Top-level Files of %s", zCI);
722
+ }else{
723
+ zHeader = mprintf("All Top-level Files");
724
+ }
725
+ }else{
726
+ if( zCI ){
727
+ zHeader = mprintf("Files in %s/ of %s", zD, zCI);
728
+ }else{
729
+ zHeader = mprintf("All File in %s/", zD);
730
+ }
731
+ }
732
+ style_header("%s", zHeader);
733
+ fossil_free(zHeader);
666734
667735
/* Compute the title of the page */
668736
blob_zero(&dirname);
669737
if( zD ){
670738
blob_append(&dirname, "within directory ", -1);
671
- hyperlinked_path(zD, &dirname, zCI, "tree", zREx);
739
+ hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0);
672740
if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE);
673741
style_submenu_element("Top-Level", "%s",
674742
url_render(&sURI, "name", 0, 0, 0));
675
- }else{
676
- if( zRE ){
677
- blob_appendf(&dirname, "matching \"%s\"", zRE);
678
- }
743
+ }else if( zRE ){
744
+ blob_appendf(&dirname, "matching \"%s\"", zRE);
679745
}
680746
style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
681747
if( zCI ){
682748
style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
683749
if( nD==0 && !showDirOnly ){
684
- style_submenu_element("File Ages", "%R/fileage?name=%s", zUuid);
750
+ style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
685751
}
686752
}
687753
if( linkTrunk ){
688754
style_submenu_element("Trunk", "%s",
689755
url_render(&sURI, "ci", "trunk", 0, 0));
@@ -738,10 +804,11 @@
738804
tree_add_node(&sTree, zName, zUuid, mtime);
739805
nFile++;
740806
}
741807
db_finalize(&q);
742808
}
809
+ style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
743810
744811
if( showDirOnly ){
745812
for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
746813
if( p->pChild!=0 && p->nFullName>nD ) nFile++;
747814
}
@@ -748,18 +815,24 @@
748815
zObjType = "Folders";
749816
}else{
750817
zObjType = "Files";
751818
}
752819
753
- style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
754
-
755
- if( zCI ){
756
- @ <h2>%s(zObjType) from
757
- if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){
758
- @ "%h(zCI)"
759
- }
760
- @ [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))
820
+ if( zCI && strcmp(zCI,"tip")==0 ){
821
+ @ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a>
822
+ }else if( isBranchCI ){
823
+ @ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\
824
+ @ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a>
825
+ if( blob_size(&dirname) ){
826
+ @ and %s(blob_str(&dirname))</h2>
827
+ }
828
+ }else if( zCI ){
829
+ @ <h2>%s(zObjType) for check-in \
830
+ @ %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2>
831
+ if( blob_size(&dirname) ){
832
+ @ and %s(blob_str(&dirname))</h2>
833
+ }
761834
}else{
762835
int n = db_int(0, "SELECT count(*) FROM plink");
763836
@ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
764837
}
765838
if( useMtime ){
@@ -817,11 +890,11 @@
817890
nDir++;
818891
}else if( !showDirOnly ){
819892
const char *zFileClass = fileext_class(p->zName);
820893
char *zLink;
821894
if( zCI ){
822
- zLink = href("%R/artifact/%!S",p->zUuid);
895
+ zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI);
823896
}else{
824897
zLink = href("%R/finfo?name=%T",p->zFullName);
825898
}
826899
@ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
827900
@ %z(zLink)%h(p->zName)</a>
@@ -911,11 +984,11 @@
911984
** mtime on that check-in. If zGlob and *zGlob then only files matching
912985
** the given glob are computed.
913986
*/
914987
int compute_fileage(int vid, const char* zGlob){
915988
Stmt q;
916
- db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/);
989
+ db_exec_sql(zComputeFileAgeSetup);
917990
db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/);
918991
db_bind_int(&q, ":ckin", vid);
919992
db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*");
920993
db_exec(&q);
921994
db_finalize(&q);
@@ -994,10 +1067,11 @@
9941067
int rid;
9951068
const char *zName;
9961069
const char *zGlob;
9971070
const char *zUuid;
9981071
const char *zNow; /* Time of check-in */
1072
+ int isBranchCI; /* name= is a branch name */
9991073
int showId = PB("showid");
10001074
Stmt q1, q2;
10011075
double baseTime;
10021076
login_check_credentials();
10031077
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
@@ -1007,28 +1081,34 @@
10071081
rid = symbolic_name_to_rid(zName, "ci");
10081082
if( rid==0 ){
10091083
fossil_fatal("not a valid check-in: %s", zName);
10101084
}
10111085
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1086
+ isBranchCI = branch_includes_uuid(zName,zUuid);
10121087
baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
10131088
zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event"
10141089
" WHERE objid=%d", rid);
10151090
style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName);
10161091
style_header("File Ages");
10171092
zGlob = P("glob");
10181093
compute_fileage(rid,zGlob);
10191094
db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
10201095
1021
- @ <h1>Files in
1022
- @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)]</a>
1096
+ if( fossil_strcmp(zName,"tip")==0 ){
1097
+ @ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a>
1098
+ }else if( isBranchCI ){
1099
+ @ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a>
1100
+ @ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a>
1101
+ }else{
1102
+ @ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a>
1103
+ }
10231104
if( zGlob && zGlob[0] ){
10241105
@ that match "%h(zGlob)"
10251106
}
10261107
@ ordered by age</h1>
10271108
@
1028
- @ <p>File ages are expressed relative to the
1029
- @ %z(href("%R/ci/%!S",zUuid))[%S(zUuid)]</a> check-in time of
1109
+ @ <p>File ages are expressed relative to the check-in time of
10301110
@ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
10311111
@
10321112
@ <div class='fileage'><table>
10331113
@ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr>
10341114
db_prepare(&q1,
@@ -1043,14 +1123,13 @@
10431123
" AND blob.rid=event.objid\n"
10441124
" ORDER BY event.mtime DESC;",
10451125
TAG_BRANCH
10461126
);
10471127
db_prepare(&q2,
1048
- "SELECT blob.uuid, filename.name, fileage.fid\n"
1049
- " FROM fileage, blob, filename\n"
1128
+ "SELECT filename.name, fileage.fid\n"
1129
+ " FROM fileage, filename\n"
10501130
" WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid"
1051
- " AND blob.rid=fileage.fid;"
10521131
);
10531132
while( db_step(&q1)==SQLITE_ROW ){
10541133
double age = baseTime - db_column_double(&q1, 0);
10551134
int mid = db_column_int(&q1, 1);
10561135
const char *zUuid = db_column_text(&q1, 2);
@@ -1060,24 +1139,24 @@
10601139
char *zAge = human_readable_age(age);
10611140
@ <tr><td>%s(zAge)</td>
10621141
@ <td>
10631142
db_bind_int(&q2, ":mid", mid);
10641143
while( db_step(&q2)==SQLITE_ROW ){
1065
- const char *zFUuid = db_column_text(&q2,0);
1066
- const char *zFile = db_column_text(&q2,1);
1067
- int fid = db_column_int(&q2,2);
1144
+ const char *zFile = db_column_text(&q2,0);
1145
+ @ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \
10681146
if( showId ){
1069
- @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a> (%d(fid))<br />
1147
+ int fid = db_column_int(&q2,1);
1148
+ @ (%d(fid))<br />
10701149
}else{
1071
- @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br />
1150
+ @ </a><br />
10721151
}
10731152
}
10741153
db_reset(&q2);
10751154
@ </td>
10761155
@ <td>
10771156
@ %W(zComment)
1078
- @ (check-in:&nbsp;%z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>,
1157
+ @ (check-in:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
10791158
if( showId ){
10801159
@ id: %d(mid)
10811160
}
10821161
@ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
10831162
@ branch:&nbsp;\
10841163
--- src/browse.c
+++ src/browse.c
@@ -59,10 +59,18 @@
59 zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]);
60 sqlite3_result_text(context, zOut, i-n+1, sqlite3_free);
61 }
62 }
63
 
 
 
 
 
 
 
 
64 /*
65 ** Given a pathname which is a relative path from the root of
66 ** the repository to a file or directory, compute a string which
67 ** is an HTML rendering of that path with hyperlinks on each
68 ** directory component of the path where the hyperlink redirects
@@ -76,29 +84,36 @@
76 void hyperlinked_path(
77 const char *zPath, /* Path to render */
78 Blob *pOut, /* Write into this blob */
79 const char *zCI, /* check-in name, or NULL */
80 const char *zURI, /* "dir" or "tree" */
81 const char *zREx /* Extra query parameters */
 
82 ){
83 int i, j;
84 char *zSep = "";
85
86 for(i=0; zPath[i]; i=j){
87 for(j=i; zPath[j] && zPath[j]!='/'; j++){}
88 if( zPath[j] && g.perm.Hyperlink ){
89 if( zCI ){
90 char *zLink = href("%R/%s?name=%#T%s&ci=%!S", zURI, j, zPath, zREx,zCI);
91 blob_appendf(pOut, "%s%z%#h</a>",
92 zSep, zLink, j-i, &zPath[i]);
93 }else{
94 char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
95 blob_appendf(pOut, "%s%z%#h</a>",
96 zSep, zLink, j-i, &zPath[i]);
97 }
98 }else{
99 blob_appendf(pOut, "%s%#h", zSep, j-i, &zPath[i]);
 
 
 
 
 
 
100 }
101 zSep = "/";
102 while( zPath[j]=='/' ){ j++; }
103 }
104 }
@@ -116,38 +131,35 @@
116 **
117 ** name=PATH Directory to display. Optional. Top-level if missing
118 ** ci=LABEL Show only files in this check-in. Optional.
119 ** type=TYPE TYPE=flat: use this display
120 ** TYPE=tree: use the /tree display instead
 
121 */
122 void page_dir(void){
123 char *zD = fossil_strdup(P("name"));
124 int nD = zD ? strlen(zD)+1 : 0;
125 int mxLen;
126 int n;
127 char *zPrefix;
128 Stmt q;
129 const char *zCI = P("ci");
130 int rid = 0;
131 char *zUuid = 0;
132 Blob dirname;
133 Manifest *pM = 0;
134 const char *zSubdirLink;
135 int linkTrunk = 1;
136 int linkTip = 1;
137 HQuery sURI;
 
 
 
138
 
139 if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; }
140 login_check_credentials();
141 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
142 while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
143 style_header("File List");
144 style_adunit_config(ADUNIT_RIGHT_OK);
145 sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
146 pathelementFunc, 0, 0);
147 url_initialize(&sURI, "dir");
148 cgi_query_parameters_to_url(&sURI);
149
150 /* If the name= parameter is an empty string, make it a NULL pointer */
151 if( zD && strlen(zD)==0 ){ zD = 0; }
152
153 /* If a specific check-in is requested, fetch and parse it. If the
@@ -159,53 +171,80 @@
159 if( pM ){
160 int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
161 linkTrunk = trunkRid && rid != trunkRid;
162 linkTip = rid != symbolic_name_to_rid("tip", "ci");
163 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
 
 
 
164 }else{
165 zCI = 0;
166 }
167 }
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169 /* Compute the title of the page */
170 blob_zero(&dirname);
171 if( zD ){
172 blob_append(&dirname, "in directory ", -1);
173 hyperlinked_path(zD, &dirname, zCI, "dir", "");
 
 
 
174 zPrefix = mprintf("%s/", zD);
175 style_submenu_element("Top-Level", "%s",
176 url_render(&sURI, "name", 0, 0, 0));
177 }else{
178 blob_append(&dirname, "in the top-level directory", -1);
179 zPrefix = "";
180 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181 if( linkTrunk ){
182 style_submenu_element("Trunk", "%s",
183 url_render(&sURI, "ci", "trunk", 0, 0));
184 }
185 if( linkTip ){
186 style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
187 }
188 if( zCI ){
189 @ <h2>Files of check-in [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>]
190 @ %s(blob_str(&dirname))
191 if( zD ){
192 @ &nbsp;&nbsp;%z(href("%R/timeline?chng=%T/*", zD))[history]</a>
193 }
194 @ </h2>
195 zSubdirLink = mprintf("%R/dir?ci=%!S&name=%T", zUuid, zPrefix);
196 if( nD==0 ){
197 style_submenu_element("File Ages", "%R/fileage?name=%!S", zUuid);
198 }
199 }else{
200 @ <h2>The union of all files from all check-ins
201 @ %s(blob_str(&dirname))
202 if( zD ){
203 @ &nbsp;&nbsp;%z(href("%R/timeline?chng=%T/*", zD))[history]</a>
204 }
205 @ </h2>
206 zSubdirLink = mprintf("%R/dir?name=%T", zPrefix);
207 }
208 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
209 style_submenu_element("Tree-View", "%s",
210 url_render(&sURI, "type", "tree", 0, 0));
211
@@ -268,15 +307,14 @@
268
269 /* Generate a multi-column table listing the contents of zD[]
270 ** directory.
271 */
272 mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
273 n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/");
274 if( mxLen<12 ) mxLen = 12;
275 mxLen += (mxLen+9)/10;
276 db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/");
277 @ <div class="columns" style="columns: %d(mxLen)ex %d(n);">
278 @ <ul class="browser">
279 while( db_step(&q)==SQLITE_ROW ){
280 const char *zFN;
281 zFN = db_column_text(&q, 0);
282 if( zFN[0]=='/' ){
@@ -283,21 +321,28 @@
283 zFN++;
284 @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
285 }else{
286 const char *zLink;
287 if( zCI ){
288 const char *zUuid = db_column_text(&q, 1);
289 zLink = href("%R/artifact/%!S",zUuid);
290 }else{
291 zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
292 }
293 @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
294 }
295 }
296 db_finalize(&q);
297 manifest_destroy(pM);
298 @ </ul></div>
 
 
 
 
 
 
 
 
299
300 /* If the directory contains a readme file, then display its content below
301 ** the list of files
302 */
303 db_prepare(&q,
@@ -345,10 +390,11 @@
345 @ </iframe>
346 }else{
347 Blob content;
348 const char *zMime = mimetype_from_name(zName);
349 content_get(rid, &content);
 
350 wiki_render_by_mimetype(&content, zMime);
351 }
352 }
353 }
354 db_finalize(&q);
@@ -605,11 +651,15 @@
605 HQuery sURI; /* Hyperlink */
606 int startExpanded; /* True to start out with the tree expanded */
607 int showDirOnly; /* Show directories only. Omit files */
608 int nDir = 0; /* Number of directories. Used for ID attributes */
609 char *zProjectName = db_get("project-name", 0);
 
 
 
610
 
611 if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; }
612 memset(&sTree, 0, sizeof(sTree));
613 login_check_credentials();
614 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
615 while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
@@ -617,14 +667,12 @@
617 pathelementFunc, 0, 0);
618 url_initialize(&sURI, "tree");
619 cgi_query_parameters_to_url(&sURI);
620 if( PB("nofiles") ){
621 showDirOnly = 1;
622 style_header("Folder Hierarchy");
623 }else{
624 showDirOnly = 0;
625 style_header("File Tree");
626 }
627 style_adunit_config(ADUNIT_RIGHT_OK);
628 if( PB("expand") ){
629 startExpanded = 1;
630 }else{
@@ -653,37 +701,55 @@
653 linkTip = rid != symbolic_name_to_rid("tip", "ci");
654 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
655 rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
656 zNow = db_text("", "SELECT datetime(mtime,toLocal())"
657 " FROM event WHERE objid=%d", rid);
 
 
 
658 }else{
659 zCI = 0;
660 }
661 }
662 if( zCI==0 ){
663 rNow = db_double(0.0, "SELECT max(mtime) FROM event");
664 zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
665 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
667 /* Compute the title of the page */
668 blob_zero(&dirname);
669 if( zD ){
670 blob_append(&dirname, "within directory ", -1);
671 hyperlinked_path(zD, &dirname, zCI, "tree", zREx);
672 if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE);
673 style_submenu_element("Top-Level", "%s",
674 url_render(&sURI, "name", 0, 0, 0));
675 }else{
676 if( zRE ){
677 blob_appendf(&dirname, "matching \"%s\"", zRE);
678 }
679 }
680 style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
681 if( zCI ){
682 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
683 if( nD==0 && !showDirOnly ){
684 style_submenu_element("File Ages", "%R/fileage?name=%s", zUuid);
685 }
686 }
687 if( linkTrunk ){
688 style_submenu_element("Trunk", "%s",
689 url_render(&sURI, "ci", "trunk", 0, 0));
@@ -738,10 +804,11 @@
738 tree_add_node(&sTree, zName, zUuid, mtime);
739 nFile++;
740 }
741 db_finalize(&q);
742 }
 
743
744 if( showDirOnly ){
745 for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
746 if( p->pChild!=0 && p->nFullName>nD ) nFile++;
747 }
@@ -748,18 +815,24 @@
748 zObjType = "Folders";
749 }else{
750 zObjType = "Files";
751 }
752
753 style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
754
755 if( zCI ){
756 @ <h2>%s(zObjType) from
757 if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){
758 @ "%h(zCI)"
759 }
760 @ [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))
 
 
 
 
 
 
761 }else{
762 int n = db_int(0, "SELECT count(*) FROM plink");
763 @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
764 }
765 if( useMtime ){
@@ -817,11 +890,11 @@
817 nDir++;
818 }else if( !showDirOnly ){
819 const char *zFileClass = fileext_class(p->zName);
820 char *zLink;
821 if( zCI ){
822 zLink = href("%R/artifact/%!S",p->zUuid);
823 }else{
824 zLink = href("%R/finfo?name=%T",p->zFullName);
825 }
826 @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
827 @ %z(zLink)%h(p->zName)</a>
@@ -911,11 +984,11 @@
911 ** mtime on that check-in. If zGlob and *zGlob then only files matching
912 ** the given glob are computed.
913 */
914 int compute_fileage(int vid, const char* zGlob){
915 Stmt q;
916 db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/);
917 db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/);
918 db_bind_int(&q, ":ckin", vid);
919 db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*");
920 db_exec(&q);
921 db_finalize(&q);
@@ -994,10 +1067,11 @@
994 int rid;
995 const char *zName;
996 const char *zGlob;
997 const char *zUuid;
998 const char *zNow; /* Time of check-in */
 
999 int showId = PB("showid");
1000 Stmt q1, q2;
1001 double baseTime;
1002 login_check_credentials();
1003 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
@@ -1007,28 +1081,34 @@
1007 rid = symbolic_name_to_rid(zName, "ci");
1008 if( rid==0 ){
1009 fossil_fatal("not a valid check-in: %s", zName);
1010 }
1011 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
 
1012 baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
1013 zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event"
1014 " WHERE objid=%d", rid);
1015 style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName);
1016 style_header("File Ages");
1017 zGlob = P("glob");
1018 compute_fileage(rid,zGlob);
1019 db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
1020
1021 @ <h1>Files in
1022 @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)]</a>
 
 
 
 
 
 
1023 if( zGlob && zGlob[0] ){
1024 @ that match "%h(zGlob)"
1025 }
1026 @ ordered by age</h1>
1027 @
1028 @ <p>File ages are expressed relative to the
1029 @ %z(href("%R/ci/%!S",zUuid))[%S(zUuid)]</a> check-in time of
1030 @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
1031 @
1032 @ <div class='fileage'><table>
1033 @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr>
1034 db_prepare(&q1,
@@ -1043,14 +1123,13 @@
1043 " AND blob.rid=event.objid\n"
1044 " ORDER BY event.mtime DESC;",
1045 TAG_BRANCH
1046 );
1047 db_prepare(&q2,
1048 "SELECT blob.uuid, filename.name, fileage.fid\n"
1049 " FROM fileage, blob, filename\n"
1050 " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid"
1051 " AND blob.rid=fileage.fid;"
1052 );
1053 while( db_step(&q1)==SQLITE_ROW ){
1054 double age = baseTime - db_column_double(&q1, 0);
1055 int mid = db_column_int(&q1, 1);
1056 const char *zUuid = db_column_text(&q1, 2);
@@ -1060,24 +1139,24 @@
1060 char *zAge = human_readable_age(age);
1061 @ <tr><td>%s(zAge)</td>
1062 @ <td>
1063 db_bind_int(&q2, ":mid", mid);
1064 while( db_step(&q2)==SQLITE_ROW ){
1065 const char *zFUuid = db_column_text(&q2,0);
1066 const char *zFile = db_column_text(&q2,1);
1067 int fid = db_column_int(&q2,2);
1068 if( showId ){
1069 @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a> (%d(fid))<br />
 
1070 }else{
1071 @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br />
1072 }
1073 }
1074 db_reset(&q2);
1075 @ </td>
1076 @ <td>
1077 @ %W(zComment)
1078 @ (check-in:&nbsp;%z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>,
1079 if( showId ){
1080 @ id: %d(mid)
1081 }
1082 @ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1083 @ branch:&nbsp;\
1084
--- src/browse.c
+++ src/browse.c
@@ -59,10 +59,18 @@
59 zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]);
60 sqlite3_result_text(context, zOut, i-n+1, sqlite3_free);
61 }
62 }
63
64 /*
65 ** Flag arguments for hyperlinked_path()
66 */
67 #if INTERFACE
68 # define LINKPATH_FINFO 0x0001 /* Link final term to /finfo */
69 # define LINKPATH_FILE 0x0002 /* Link final term to /file */
70 #endif
71
72 /*
73 ** Given a pathname which is a relative path from the root of
74 ** the repository to a file or directory, compute a string which
75 ** is an HTML rendering of that path with hyperlinks on each
76 ** directory component of the path where the hyperlink redirects
@@ -76,29 +84,36 @@
84 void hyperlinked_path(
85 const char *zPath, /* Path to render */
86 Blob *pOut, /* Write into this blob */
87 const char *zCI, /* check-in name, or NULL */
88 const char *zURI, /* "dir" or "tree" */
89 const char *zREx, /* Extra query parameters */
90 unsigned int mFlags /* Extra flags */
91 ){
92 int i, j;
93 char *zSep = "";
94
95 for(i=0; zPath[i]; i=j){
96 for(j=i; zPath[j] && zPath[j]!='/'; j++){}
97 if( zPath[j]==0 ){
98 if( mFlags & LINKPATH_FILE ){
99 zURI = "file";
100 }else if( mFlags & LINKPATH_FINFO ){
101 zURI = "finfo";
102 }else{
103 blob_appendf(pOut, "/%h", zPath+i);
104 break;
105 }
106 }
107 if( zCI ){
108 char *zLink = href("%R/%s?name=%#T%s&ci=%T", zURI, j, zPath, zREx,zCI);
109 blob_appendf(pOut, "%s%z%#h</a>",
110 zSep, zLink, j-i, &zPath[i]);
111 }else{
112 char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
113 blob_appendf(pOut, "%s%z%#h</a>",
114 zSep, zLink, j-i, &zPath[i]);
115 }
116 zSep = "/";
117 while( zPath[j]=='/' ){ j++; }
118 }
119 }
@@ -116,38 +131,35 @@
131 **
132 ** name=PATH Directory to display. Optional. Top-level if missing
133 ** ci=LABEL Show only files in this check-in. Optional.
134 ** type=TYPE TYPE=flat: use this display
135 ** TYPE=tree: use the /tree display instead
136 ** noreadme Do not attempt to display the README file.
137 */
138 void page_dir(void){
139 char *zD = fossil_strdup(P("name"));
140 int nD = zD ? strlen(zD)+1 : 0;
141 int mxLen;
 
142 char *zPrefix;
143 Stmt q;
144 const char *zCI = P("ci");
145 int rid = 0;
146 char *zUuid = 0;
 
147 Manifest *pM = 0;
148 const char *zSubdirLink;
149 int linkTrunk = 1;
150 int linkTip = 1;
151 HQuery sURI;
152 int isSymbolicCI = 0; /* ci= is symbolic name, not a hash prefix */
153 int isBranchCI = 0; /* True if ci= refers to a branch name */
154 char *zHeader = 0;
155
156 if( zCI && strlen(zCI)==0 ){ zCI = 0; }
157 if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; }
158 login_check_credentials();
159 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
160 while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
 
 
 
 
 
 
161
162 /* If the name= parameter is an empty string, make it a NULL pointer */
163 if( zD && strlen(zD)==0 ){ zD = 0; }
164
165 /* If a specific check-in is requested, fetch and parse it. If the
@@ -159,53 +171,80 @@
171 if( pM ){
172 int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
173 linkTrunk = trunkRid && rid != trunkRid;
174 linkTip = rid != symbolic_name_to_rid("tip", "ci");
175 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
176 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
177 isBranchCI = branch_includes_uuid(zCI, zUuid);
178 Th_Store("current_checkin", zCI);
179 }else{
180 zCI = 0;
181 }
182 }
183
184 assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
185 if( zD==0 ){
186 if( zCI ){
187 zHeader = mprintf("Top-level Files of %s", zCI);
188 }else{
189 zHeader = mprintf("All Top-level Files");
190 }
191 }else{
192 if( zCI ){
193 zHeader = mprintf("Files in %s/ of %s", zD, zCI);
194 }else{
195 zHeader = mprintf("All File in %s/", zD);
196 }
197 }
198 style_header("%s", zHeader);
199 fossil_free(zHeader);
200 style_adunit_config(ADUNIT_RIGHT_OK);
201 sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
202 pathelementFunc, 0, 0);
203 url_initialize(&sURI, "dir");
204 cgi_query_parameters_to_url(&sURI);
205
206 /* Compute the title of the page */
 
207 if( zD ){
208 Blob dirname;
209 blob_init(&dirname, 0, 0);
210 hyperlinked_path(zD, &dirname, zCI, "dir", "", 0);
211 @ <h2>Files in directory %s(blob_str(&dirname)) \
212 blob_reset(&dirname);
213 zPrefix = mprintf("%s/", zD);
214 style_submenu_element("Top-Level", "%s",
215 url_render(&sURI, "name", 0, 0, 0));
216 }else{
217 @ <h2>Files in the top-level directory \
218 zPrefix = "";
219 }
220 if( zCI ){
221 if( fossil_strcmp(zCI,"tip")==0 ){
222 @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a></h2>
223 }else if( isBranchCI ){
224 @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a> \
225 @ of branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a></h2>
226 }else {
227 @ of check-in %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2>
228 }
229 zSubdirLink = mprintf("%R/dir?ci=%T&name=%T", zCI, zPrefix);
230 if( nD==0 ){
231 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
232 }
233 }else{
234 @ in any check-in</h2>
235 zSubdirLink = mprintf("%R/dir?name=%T", zPrefix);
236 }
237 if( linkTrunk ){
238 style_submenu_element("Trunk", "%s",
239 url_render(&sURI, "ci", "trunk", 0, 0));
240 }
241 if( linkTip ){
242 style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
243 }
244 if( zD ){
245 style_submenu_element("History","%R/timeline?chng=%T/*", zD);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246 }
247 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
248 style_submenu_element("Tree-View", "%s",
249 url_render(&sURI, "type", "tree", 0, 0));
250
@@ -268,15 +307,14 @@
307
308 /* Generate a multi-column table listing the contents of zD[]
309 ** directory.
310 */
311 mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
 
312 if( mxLen<12 ) mxLen = 12;
313 mxLen += (mxLen+9)/10;
314 db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/");
315 @ <div class="columns files" style="columns: %d(mxLen)ex auto">
316 @ <ul class="browser">
317 while( db_step(&q)==SQLITE_ROW ){
318 const char *zFN;
319 zFN = db_column_text(&q, 0);
320 if( zFN[0]=='/' ){
@@ -283,21 +321,28 @@
321 zFN++;
322 @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
323 }else{
324 const char *zLink;
325 if( zCI ){
326 zLink = href("%R/file?name=%T%T&ci=%T",zPrefix,zFN,zCI);
 
327 }else{
328 zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
329 }
330 @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
331 }
332 }
333 db_finalize(&q);
334 manifest_destroy(pM);
335 @ </ul></div>
336
337 /* If the "noreadme" query parameter is present, do not try to
338 ** show the content of the README file.
339 */
340 if( P("noreadme")!=0 ){
341 style_footer();
342 return;
343 }
344
345 /* If the directory contains a readme file, then display its content below
346 ** the list of files
347 */
348 db_prepare(&q,
@@ -345,10 +390,11 @@
390 @ </iframe>
391 }else{
392 Blob content;
393 const char *zMime = mimetype_from_name(zName);
394 content_get(rid, &content);
395 safe_html_context(DOCSRC_FILE);
396 wiki_render_by_mimetype(&content, zMime);
397 }
398 }
399 }
400 db_finalize(&q);
@@ -605,11 +651,15 @@
651 HQuery sURI; /* Hyperlink */
652 int startExpanded; /* True to start out with the tree expanded */
653 int showDirOnly; /* Show directories only. Omit files */
654 int nDir = 0; /* Number of directories. Used for ID attributes */
655 char *zProjectName = db_get("project-name", 0);
656 int isSymbolicCI = 0; /* ci= is a symbolic name, not a hash prefix */
657 int isBranchCI = 0; /* ci= refers to a branch name */
658 char *zHeader = 0;
659
660 if( zCI && strlen(zCI)==0 ){ zCI = 0; }
661 if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; }
662 memset(&sTree, 0, sizeof(sTree));
663 login_check_credentials();
664 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
665 while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
@@ -617,14 +667,12 @@
667 pathelementFunc, 0, 0);
668 url_initialize(&sURI, "tree");
669 cgi_query_parameters_to_url(&sURI);
670 if( PB("nofiles") ){
671 showDirOnly = 1;
 
672 }else{
673 showDirOnly = 0;
 
674 }
675 style_adunit_config(ADUNIT_RIGHT_OK);
676 if( PB("expand") ){
677 startExpanded = 1;
678 }else{
@@ -653,37 +701,55 @@
701 linkTip = rid != symbolic_name_to_rid("tip", "ci");
702 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
703 rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
704 zNow = db_text("", "SELECT datetime(mtime,toLocal())"
705 " FROM event WHERE objid=%d", rid);
706 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
707 isBranchCI = branch_includes_uuid(zCI, zUuid);
708 Th_Store("current_checkin", zCI);
709 }else{
710 zCI = 0;
711 }
712 }
713 if( zCI==0 ){
714 rNow = db_double(0.0, "SELECT max(mtime) FROM event");
715 zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
716 }
717
718 assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
719 if( zD==0 ){
720 if( zCI ){
721 zHeader = mprintf("Top-level Files of %s", zCI);
722 }else{
723 zHeader = mprintf("All Top-level Files");
724 }
725 }else{
726 if( zCI ){
727 zHeader = mprintf("Files in %s/ of %s", zD, zCI);
728 }else{
729 zHeader = mprintf("All File in %s/", zD);
730 }
731 }
732 style_header("%s", zHeader);
733 fossil_free(zHeader);
734
735 /* Compute the title of the page */
736 blob_zero(&dirname);
737 if( zD ){
738 blob_append(&dirname, "within directory ", -1);
739 hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0);
740 if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE);
741 style_submenu_element("Top-Level", "%s",
742 url_render(&sURI, "name", 0, 0, 0));
743 }else if( zRE ){
744 blob_appendf(&dirname, "matching \"%s\"", zRE);
 
 
745 }
746 style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
747 if( zCI ){
748 style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
749 if( nD==0 && !showDirOnly ){
750 style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
751 }
752 }
753 if( linkTrunk ){
754 style_submenu_element("Trunk", "%s",
755 url_render(&sURI, "ci", "trunk", 0, 0));
@@ -738,10 +804,11 @@
804 tree_add_node(&sTree, zName, zUuid, mtime);
805 nFile++;
806 }
807 db_finalize(&q);
808 }
809 style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
810
811 if( showDirOnly ){
812 for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
813 if( p->pChild!=0 && p->nFullName>nD ) nFile++;
814 }
@@ -748,18 +815,24 @@
815 zObjType = "Folders";
816 }else{
817 zObjType = "Files";
818 }
819
820 if( zCI && strcmp(zCI,"tip")==0 ){
821 @ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a>
822 }else if( isBranchCI ){
823 @ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\
824 @ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a>
825 if( blob_size(&dirname) ){
826 @ and %s(blob_str(&dirname))</h2>
827 }
828 }else if( zCI ){
829 @ <h2>%s(zObjType) for check-in \
830 @ %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2>
831 if( blob_size(&dirname) ){
832 @ and %s(blob_str(&dirname))</h2>
833 }
834 }else{
835 int n = db_int(0, "SELECT count(*) FROM plink");
836 @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
837 }
838 if( useMtime ){
@@ -817,11 +890,11 @@
890 nDir++;
891 }else if( !showDirOnly ){
892 const char *zFileClass = fileext_class(p->zName);
893 char *zLink;
894 if( zCI ){
895 zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI);
896 }else{
897 zLink = href("%R/finfo?name=%T",p->zFullName);
898 }
899 @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
900 @ %z(zLink)%h(p->zName)</a>
@@ -911,11 +984,11 @@
984 ** mtime on that check-in. If zGlob and *zGlob then only files matching
985 ** the given glob are computed.
986 */
987 int compute_fileage(int vid, const char* zGlob){
988 Stmt q;
989 db_exec_sql(zComputeFileAgeSetup);
990 db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/);
991 db_bind_int(&q, ":ckin", vid);
992 db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*");
993 db_exec(&q);
994 db_finalize(&q);
@@ -994,10 +1067,11 @@
1067 int rid;
1068 const char *zName;
1069 const char *zGlob;
1070 const char *zUuid;
1071 const char *zNow; /* Time of check-in */
1072 int isBranchCI; /* name= is a branch name */
1073 int showId = PB("showid");
1074 Stmt q1, q2;
1075 double baseTime;
1076 login_check_credentials();
1077 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
@@ -1007,28 +1081,34 @@
1081 rid = symbolic_name_to_rid(zName, "ci");
1082 if( rid==0 ){
1083 fossil_fatal("not a valid check-in: %s", zName);
1084 }
1085 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1086 isBranchCI = branch_includes_uuid(zName,zUuid);
1087 baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
1088 zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event"
1089 " WHERE objid=%d", rid);
1090 style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName);
1091 style_header("File Ages");
1092 zGlob = P("glob");
1093 compute_fileage(rid,zGlob);
1094 db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
1095
1096 if( fossil_strcmp(zName,"tip")==0 ){
1097 @ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a>
1098 }else if( isBranchCI ){
1099 @ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a>
1100 @ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a>
1101 }else{
1102 @ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a>
1103 }
1104 if( zGlob && zGlob[0] ){
1105 @ that match "%h(zGlob)"
1106 }
1107 @ ordered by age</h1>
1108 @
1109 @ <p>File ages are expressed relative to the check-in time of
 
1110 @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
1111 @
1112 @ <div class='fileage'><table>
1113 @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr>
1114 db_prepare(&q1,
@@ -1043,14 +1123,13 @@
1123 " AND blob.rid=event.objid\n"
1124 " ORDER BY event.mtime DESC;",
1125 TAG_BRANCH
1126 );
1127 db_prepare(&q2,
1128 "SELECT filename.name, fileage.fid\n"
1129 " FROM fileage, filename\n"
1130 " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid"
 
1131 );
1132 while( db_step(&q1)==SQLITE_ROW ){
1133 double age = baseTime - db_column_double(&q1, 0);
1134 int mid = db_column_int(&q1, 1);
1135 const char *zUuid = db_column_text(&q1, 2);
@@ -1060,24 +1139,24 @@
1139 char *zAge = human_readable_age(age);
1140 @ <tr><td>%s(zAge)</td>
1141 @ <td>
1142 db_bind_int(&q2, ":mid", mid);
1143 while( db_step(&q2)==SQLITE_ROW ){
1144 const char *zFile = db_column_text(&q2,0);
1145 @ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \
 
1146 if( showId ){
1147 int fid = db_column_int(&q2,1);
1148 @ (%d(fid))<br />
1149 }else{
1150 @ </a><br />
1151 }
1152 }
1153 db_reset(&q2);
1154 @ </td>
1155 @ <td>
1156 @ %W(zComment)
1157 @ (check-in:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
1158 if( showId ){
1159 @ id: %d(mid)
1160 }
1161 @ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1162 @ branch:&nbsp;\
1163
+10 -2
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
5555
return (char*)builtin_file(zFilename, 0);
5656
}
5757
5858
/*
5959
** COMMAND: test-builtin-list
60
+**
61
+** If -verbose is used, it outputs a line at the end
62
+** with the total item count and size.
6063
**
6164
** List the names and sizes of all built-in resources.
6265
*/
6366
void test_builtin_list(void){
64
- int i;
67
+ int i, size = 0;;
6568
for(i=0; i<count(aBuiltinFiles); i++){
66
- fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,aBuiltinFiles[i].nByte);
69
+ const int n = aBuiltinFiles[i].nByte;
70
+ fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n);
71
+ size += n;
72
+ }
73
+ if(find_option("verbose","v",0)!=0){
74
+ fossil_print("%d entries totaling %d bytes\n", i, size);
6775
}
6876
}
6977
7078
/*
7179
** WEBPAGE: test-builtin-files
7280
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
55 return (char*)builtin_file(zFilename, 0);
56 }
57
58 /*
59 ** COMMAND: test-builtin-list
 
 
 
60 **
61 ** List the names and sizes of all built-in resources.
62 */
63 void test_builtin_list(void){
64 int i;
65 for(i=0; i<count(aBuiltinFiles); i++){
66 fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,aBuiltinFiles[i].nByte);
 
 
 
 
 
67 }
68 }
69
70 /*
71 ** WEBPAGE: test-builtin-files
72
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
55 return (char*)builtin_file(zFilename, 0);
56 }
57
58 /*
59 ** COMMAND: test-builtin-list
60 **
61 ** If -verbose is used, it outputs a line at the end
62 ** with the total item count and size.
63 **
64 ** List the names and sizes of all built-in resources.
65 */
66 void test_builtin_list(void){
67 int i, size = 0;;
68 for(i=0; i<count(aBuiltinFiles); i++){
69 const int n = aBuiltinFiles[i].nByte;
70 fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n);
71 size += n;
72 }
73 if(find_option("verbose","v",0)!=0){
74 fossil_print("%d entries totaling %d bytes\n", i, size);
75 }
76 }
77
78 /*
79 ** WEBPAGE: test-builtin-files
80
+12 -12
--- src/bundle.c
+++ src/bundle.c
@@ -25,11 +25,11 @@
2525
** SQL code used to initialize the schema of a bundle.
2626
**
2727
** The bblob.delta field can be an integer, a text string, or NULL.
2828
** If an integer, then the corresponding blobid is the delta basis.
2929
** If a text string, then that string is a SHA1 hash for the delta
30
-** basis, which is presumably in the master repository. If NULL, then
30
+** basis, which is presumably in the main repository. If NULL, then
3131
** data contains content without delta compression.
3232
*/
3333
static const char zBundleInit[] =
3434
@ CREATE TABLE IF NOT EXISTS "%w".bconfig(
3535
@ bcname TEXT,
@@ -530,19 +530,19 @@
530530
blob_reset(&h1);
531531
bag_remove(&busy, blobid);
532532
db_finalize(&q);
533533
}
534534
535
-/* fossil bundle cat BUNDLE UUID...
535
+/* fossil bundle cat BUNDLE HASH...
536536
**
537537
** Write elements of a bundle on standard output
538538
*/
539539
static void bundle_cat_cmd(void){
540540
int i;
541541
Blob x;
542542
verify_all_options();
543
- if( g.argc<5 ) usage("cat BUNDLE UUID...");
543
+ if( g.argc<5 ) usage("cat BUNDLE HASH...");
544544
bundle_attach_file(g.argv[3], "b1", 1);
545545
blob_zero(&x);
546546
for(i=4; i<g.argc; i++){
547547
int blobid = db_int(0,"SELECT blobid FROM bblob WHERE uuid LIKE '%q%%'",
548548
g.argv[i]);
@@ -720,23 +720,23 @@
720720
/*
721721
** COMMAND: bundle
722722
**
723723
** Usage: %fossil bundle SUBCOMMAND ARGS...
724724
**
725
-** fossil bundle append BUNDLE FILE...
725
+** > fossil bundle append BUNDLE FILE...
726726
**
727727
** Add files named on the command line to BUNDLE. This subcommand has
728728
** little practical use and is mostly intended for testing.
729729
**
730
-** fossil bundle cat BUNDLE UUID...
730
+** > fossil bundle cat BUNDLE HASH...
731731
**
732732
** Extract one or more artifacts from the bundle and write them
733733
** consecutively on standard output. This subcommand was designed
734734
** for testing and introspection of bundles and is not something
735735
** commonly used.
736736
**
737
-** fossil bundle export BUNDLE ?OPTIONS?
737
+** > fossil bundle export BUNDLE ?OPTIONS?
738738
**
739739
** Generate a new bundle, in the file named BUNDLE, that contains a
740740
** subset of the check-ins in the repository (usually a single branch)
741741
** described by the --branch, --from, --to, and/or --checkin options,
742742
** at least one of which is required. If BUNDLE already exists, the
@@ -744,37 +744,37 @@
744744
**
745745
** --branch BRANCH Package all check-ins on BRANCH.
746746
** --from TAG1 --to TAG2 Package check-ins between TAG1 and TAG2.
747747
** --checkin TAG Package the single check-in TAG
748748
** --standalone Do no use delta-encoding against
749
-** artifacts not in the bundle
749
+** artifacts not in the bundle
750750
**
751
-** fossil bundle extend BUNDLE
751
+** > fossil bundle extend BUNDLE
752752
**
753753
** The BUNDLE must already exist. This subcommand adds to the bundle
754754
** any check-ins that are descendants of check-ins already in the bundle,
755755
** and any tags that apply to artifacts in the bundle.
756756
**
757
-** fossil bundle import BUNDLE ?--publish?
757
+** > fossil bundle import BUNDLE ?--publish?
758758
**
759759
** Import all content from BUNDLE into the repository. By default, the
760760
** imported files are private and will not sync. Use the --publish
761761
** option to make the import public.
762762
**
763
-** fossil bundle ls BUNDLE
763
+** > fossil bundle ls BUNDLE
764764
**
765765
** List the contents of BUNDLE on standard output
766766
**
767
-** fossil bundle purge BUNDLE
767
+** > fossil bundle purge BUNDLE
768768
**
769769
** Remove from the repository all files that are used exclusively
770770
** by check-ins in BUNDLE. This has the effect of undoing a
771771
** "fossil bundle import".
772772
**
773773
** SUMMARY:
774774
** fossil bundle append BUNDLE FILE... Add files to BUNDLE
775
-** fossil bundle cat BUNDLE UUID... Extract file from BUNDLE
775
+** fossil bundle cat BUNDLE HASH... Extract file from BUNDLE
776776
** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE
777777
** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include
778778
** --checkin TAG Use only check-in TAG
779779
** --standalone Omit dependencies
780780
** fossil bundle extend BUNDLE Update with newer content
781781
--- src/bundle.c
+++ src/bundle.c
@@ -25,11 +25,11 @@
25 ** SQL code used to initialize the schema of a bundle.
26 **
27 ** The bblob.delta field can be an integer, a text string, or NULL.
28 ** If an integer, then the corresponding blobid is the delta basis.
29 ** If a text string, then that string is a SHA1 hash for the delta
30 ** basis, which is presumably in the master repository. If NULL, then
31 ** data contains content without delta compression.
32 */
33 static const char zBundleInit[] =
34 @ CREATE TABLE IF NOT EXISTS "%w".bconfig(
35 @ bcname TEXT,
@@ -530,19 +530,19 @@
530 blob_reset(&h1);
531 bag_remove(&busy, blobid);
532 db_finalize(&q);
533 }
534
535 /* fossil bundle cat BUNDLE UUID...
536 **
537 ** Write elements of a bundle on standard output
538 */
539 static void bundle_cat_cmd(void){
540 int i;
541 Blob x;
542 verify_all_options();
543 if( g.argc<5 ) usage("cat BUNDLE UUID...");
544 bundle_attach_file(g.argv[3], "b1", 1);
545 blob_zero(&x);
546 for(i=4; i<g.argc; i++){
547 int blobid = db_int(0,"SELECT blobid FROM bblob WHERE uuid LIKE '%q%%'",
548 g.argv[i]);
@@ -720,23 +720,23 @@
720 /*
721 ** COMMAND: bundle
722 **
723 ** Usage: %fossil bundle SUBCOMMAND ARGS...
724 **
725 ** fossil bundle append BUNDLE FILE...
726 **
727 ** Add files named on the command line to BUNDLE. This subcommand has
728 ** little practical use and is mostly intended for testing.
729 **
730 ** fossil bundle cat BUNDLE UUID...
731 **
732 ** Extract one or more artifacts from the bundle and write them
733 ** consecutively on standard output. This subcommand was designed
734 ** for testing and introspection of bundles and is not something
735 ** commonly used.
736 **
737 ** fossil bundle export BUNDLE ?OPTIONS?
738 **
739 ** Generate a new bundle, in the file named BUNDLE, that contains a
740 ** subset of the check-ins in the repository (usually a single branch)
741 ** described by the --branch, --from, --to, and/or --checkin options,
742 ** at least one of which is required. If BUNDLE already exists, the
@@ -744,37 +744,37 @@
744 **
745 ** --branch BRANCH Package all check-ins on BRANCH.
746 ** --from TAG1 --to TAG2 Package check-ins between TAG1 and TAG2.
747 ** --checkin TAG Package the single check-in TAG
748 ** --standalone Do no use delta-encoding against
749 ** artifacts not in the bundle
750 **
751 ** fossil bundle extend BUNDLE
752 **
753 ** The BUNDLE must already exist. This subcommand adds to the bundle
754 ** any check-ins that are descendants of check-ins already in the bundle,
755 ** and any tags that apply to artifacts in the bundle.
756 **
757 ** fossil bundle import BUNDLE ?--publish?
758 **
759 ** Import all content from BUNDLE into the repository. By default, the
760 ** imported files are private and will not sync. Use the --publish
761 ** option to make the import public.
762 **
763 ** fossil bundle ls BUNDLE
764 **
765 ** List the contents of BUNDLE on standard output
766 **
767 ** fossil bundle purge BUNDLE
768 **
769 ** Remove from the repository all files that are used exclusively
770 ** by check-ins in BUNDLE. This has the effect of undoing a
771 ** "fossil bundle import".
772 **
773 ** SUMMARY:
774 ** fossil bundle append BUNDLE FILE... Add files to BUNDLE
775 ** fossil bundle cat BUNDLE UUID... Extract file from BUNDLE
776 ** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE
777 ** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include
778 ** --checkin TAG Use only check-in TAG
779 ** --standalone Omit dependencies
780 ** fossil bundle extend BUNDLE Update with newer content
781
--- src/bundle.c
+++ src/bundle.c
@@ -25,11 +25,11 @@
25 ** SQL code used to initialize the schema of a bundle.
26 **
27 ** The bblob.delta field can be an integer, a text string, or NULL.
28 ** If an integer, then the corresponding blobid is the delta basis.
29 ** If a text string, then that string is a SHA1 hash for the delta
30 ** basis, which is presumably in the main repository. If NULL, then
31 ** data contains content without delta compression.
32 */
33 static const char zBundleInit[] =
34 @ CREATE TABLE IF NOT EXISTS "%w".bconfig(
35 @ bcname TEXT,
@@ -530,19 +530,19 @@
530 blob_reset(&h1);
531 bag_remove(&busy, blobid);
532 db_finalize(&q);
533 }
534
535 /* fossil bundle cat BUNDLE HASH...
536 **
537 ** Write elements of a bundle on standard output
538 */
539 static void bundle_cat_cmd(void){
540 int i;
541 Blob x;
542 verify_all_options();
543 if( g.argc<5 ) usage("cat BUNDLE HASH...");
544 bundle_attach_file(g.argv[3], "b1", 1);
545 blob_zero(&x);
546 for(i=4; i<g.argc; i++){
547 int blobid = db_int(0,"SELECT blobid FROM bblob WHERE uuid LIKE '%q%%'",
548 g.argv[i]);
@@ -720,23 +720,23 @@
720 /*
721 ** COMMAND: bundle
722 **
723 ** Usage: %fossil bundle SUBCOMMAND ARGS...
724 **
725 ** > fossil bundle append BUNDLE FILE...
726 **
727 ** Add files named on the command line to BUNDLE. This subcommand has
728 ** little practical use and is mostly intended for testing.
729 **
730 ** > fossil bundle cat BUNDLE HASH...
731 **
732 ** Extract one or more artifacts from the bundle and write them
733 ** consecutively on standard output. This subcommand was designed
734 ** for testing and introspection of bundles and is not something
735 ** commonly used.
736 **
737 ** > fossil bundle export BUNDLE ?OPTIONS?
738 **
739 ** Generate a new bundle, in the file named BUNDLE, that contains a
740 ** subset of the check-ins in the repository (usually a single branch)
741 ** described by the --branch, --from, --to, and/or --checkin options,
742 ** at least one of which is required. If BUNDLE already exists, the
@@ -744,37 +744,37 @@
744 **
745 ** --branch BRANCH Package all check-ins on BRANCH.
746 ** --from TAG1 --to TAG2 Package check-ins between TAG1 and TAG2.
747 ** --checkin TAG Package the single check-in TAG
748 ** --standalone Do no use delta-encoding against
749 ** artifacts not in the bundle
750 **
751 ** > fossil bundle extend BUNDLE
752 **
753 ** The BUNDLE must already exist. This subcommand adds to the bundle
754 ** any check-ins that are descendants of check-ins already in the bundle,
755 ** and any tags that apply to artifacts in the bundle.
756 **
757 ** > fossil bundle import BUNDLE ?--publish?
758 **
759 ** Import all content from BUNDLE into the repository. By default, the
760 ** imported files are private and will not sync. Use the --publish
761 ** option to make the import public.
762 **
763 ** > fossil bundle ls BUNDLE
764 **
765 ** List the contents of BUNDLE on standard output
766 **
767 ** > fossil bundle purge BUNDLE
768 **
769 ** Remove from the repository all files that are used exclusively
770 ** by check-ins in BUNDLE. This has the effect of undoing a
771 ** "fossil bundle import".
772 **
773 ** SUMMARY:
774 ** fossil bundle append BUNDLE FILE... Add files to BUNDLE
775 ** fossil bundle cat BUNDLE HASH... Extract file from BUNDLE
776 ** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE
777 ** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include
778 ** --checkin TAG Use only check-in TAG
779 ** --standalone Omit dependencies
780 ** fossil bundle extend BUNDLE Update with newer content
781
+28 -12
--- src/capabilities.c
+++ src/capabilities.c
@@ -100,11 +100,11 @@
100100
void capability_expand(CapabilityString *pIn){
101101
static char *zNobody = 0;
102102
static char *zAnon = 0;
103103
static char *zReader = 0;
104104
static char *zDev = 0;
105
- static char *zAdmin = "bcdefghijklmnopqrtwxz234567AD";
105
+ static char *zAdmin = "bcdefghijklmnopqrtwz234567AD";
106106
int doneV = 0;
107107
108108
if( pIn==0 ){
109109
fossil_free(zNobody); zNobody = 0;
110110
fossil_free(zAnon); zAnon = 0;
@@ -239,15 +239,17 @@
239239
char *zOneLiner; /* One-line summary */
240240
} aCap[] = {
241241
{ 'a', CAPCLASS_SUPER, 0,
242242
"Admin", "Create and delete users" },
243243
{ 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
244
- "Attach", "Add attchments to wiki or tickets" },
244
+ "Attach", "Add attachments to wiki or tickets" },
245245
{ 'c', CAPCLASS_TKT, 0,
246246
"Append-Tkt", "Append to existing tickets" },
247
- { 'd', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
248
- "Delete", "Delete wiki or tickets" },
247
+ /*
248
+ ** d unused since fork from CVSTrac;
249
+ ** see https://fossil-scm.org/forum/forumpost/43c78f4bef
250
+ */
249251
{ 'e', CAPCLASS_DATA, 0,
250252
"View-PII", "View sensitive info such as email addresses" },
251253
{ 'f', CAPCLASS_WIKI, 0,
252254
"New-Wiki", "Create new wiki pages" },
253255
{ 'g', CAPCLASS_DATA, 0,
@@ -297,11 +299,11 @@
297299
{ '4', CAPCLASS_FORUM, 0,
298300
"Forum-Trusted", "Create forum messages that bypass moderation" },
299301
{ '5', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
300302
"Forum-Mod", "Moderator for forum messages" },
301303
{ '6', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
302
- "Forum-Admin", "Set or remove capability '4' from other users" },
304
+ "Forum-Admin", "Grant capability '4' to other users" },
303305
{ '7', CAPCLASS_ALERT, 0,
304306
"Alerts", "Sign up for email alerts" },
305307
{ 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
306308
"Announce", "Send announcements to all subscribers" },
307309
{ 'D', CAPCLASS_OTHER, 0,
@@ -359,25 +361,38 @@
359361
** Generate a "capability summary table" that shows the major capabilities
360362
** against the various user categories.
361363
*/
362364
void capability_summary(void){
363365
Stmt q;
366
+ CapabilityString *pCap;
367
+ char *zSelfCap;
368
+ char *zPubPages = db_get("public-pages",0);
369
+ int hasPubPages = zPubPages && zPubPages[0];
370
+
371
+ pCap = capability_add(0, db_get("default-perms","u"));
372
+ capability_expand(pCap);
373
+ zSelfCap = capability_string(pCap);
374
+ capability_free(pCap);
375
+
364376
db_prepare(&q,
365377
"WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
366378
"('developer',4))"
367
- " SELECT id, fullcap(user.cap),seq,1"
379
+ " SELECT id, CASE WHEN user.login='nobody' THEN user.cap"
380
+ " ELSE fullcap(user.cap) END,seq,1"
368381
" FROM t LEFT JOIN user ON t.id=user.login"
369382
" UNION ALL"
370
- " SELECT 'New User Default', fullcap(%Q), 10, 1"
383
+ " SELECT 'Public Pages', %Q, 100, %d"
384
+ " UNION ALL"
385
+ " SELECT 'New User Default', %Q, 110, 1"
371386
" UNION ALL"
372
- " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user"
387
+ " SELECT 'Regular User', fullcap(capunion(cap)), 200, count(*) FROM user"
373388
" WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)"
374389
" UNION ALL"
375
- " SELECT 'Adminstrator', fullcap(capunion(cap)), 30, count(*) FROM user"
390
+ " SELECT 'Adminstrator', fullcap(capunion(cap)), 300, count(*) FROM user"
376391
" WHERE cap GLOB '*[as]*'"
377392
" ORDER BY 3 ASC",
378
- db_get("default-perms","")
393
+ zSelfCap, hasPubPages, zSelfCap
379394
);
380395
@ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
381396
@ <tr><th>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
382397
@ <th>Unversioned Content</th></tr>
383398
while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
384399
const char *zId = db_column_text(&q, 0);
385400
const char *zCap = db_column_text(&q, 1);
386401
int n = db_column_int(&q, 3);
387402
int eType;
388403
static const char *const azType[] = { "off", "read", "write" };
389
- static const char *const azClass[] = { "capsumOff", "capsumRead", "capsumWrite" };
404
+ static const char *const azClass[] =
405
+ { "capsumOff", "capsumRead", "capsumWrite" };
390406
391407
if( n==0 ) continue;
392408
393409
/* Code */
394410
if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
416432
eType = 0;
417433
}
418434
@ <td class="%s(azClass[eType])">%s(azType[eType])</td>
419435
420436
/* Ticket */
421
- if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){
437
+ if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){
422438
eType = 2;
423439
}else if( sqlite3_strglob("*r*",zCap)==0 ){
424440
eType = 1;
425441
}else{
426442
eType = 0;
427443
--- src/capabilities.c
+++ src/capabilities.c
@@ -100,11 +100,11 @@
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 static char *zAdmin = "bcdefghijklmnopqrtwxz234567AD";
106 int doneV = 0;
107
108 if( pIn==0 ){
109 fossil_free(zNobody); zNobody = 0;
110 fossil_free(zAnon); zAnon = 0;
@@ -239,15 +239,17 @@
239 char *zOneLiner; /* One-line summary */
240 } aCap[] = {
241 { 'a', CAPCLASS_SUPER, 0,
242 "Admin", "Create and delete users" },
243 { 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
244 "Attach", "Add attchments to wiki or tickets" },
245 { 'c', CAPCLASS_TKT, 0,
246 "Append-Tkt", "Append to existing tickets" },
247 { 'd', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
248 "Delete", "Delete wiki or tickets" },
 
 
249 { 'e', CAPCLASS_DATA, 0,
250 "View-PII", "View sensitive info such as email addresses" },
251 { 'f', CAPCLASS_WIKI, 0,
252 "New-Wiki", "Create new wiki pages" },
253 { 'g', CAPCLASS_DATA, 0,
@@ -297,11 +299,11 @@
297 { '4', CAPCLASS_FORUM, 0,
298 "Forum-Trusted", "Create forum messages that bypass moderation" },
299 { '5', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
300 "Forum-Mod", "Moderator for forum messages" },
301 { '6', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
302 "Forum-Admin", "Set or remove capability '4' from other users" },
303 { '7', CAPCLASS_ALERT, 0,
304 "Alerts", "Sign up for email alerts" },
305 { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
306 "Announce", "Send announcements to all subscribers" },
307 { 'D', CAPCLASS_OTHER, 0,
@@ -359,25 +361,38 @@
359 ** Generate a "capability summary table" that shows the major capabilities
360 ** against the various user categories.
361 */
362 void capability_summary(void){
363 Stmt q;
 
 
 
 
 
 
 
 
 
 
364 db_prepare(&q,
365 "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
366 "('developer',4))"
367 " SELECT id, fullcap(user.cap),seq,1"
 
368 " FROM t LEFT JOIN user ON t.id=user.login"
369 " UNION ALL"
370 " SELECT 'New User Default', fullcap(%Q), 10, 1"
 
 
371 " UNION ALL"
372 " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user"
373 " WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)"
374 " UNION ALL"
375 " SELECT 'Adminstrator', fullcap(capunion(cap)), 30, count(*) FROM user"
376 " WHERE cap GLOB '*[as]*'"
377 " ORDER BY 3 ASC",
378 db_get("default-perms","")
379 );
380 @ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
381 @ <tr><th>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
382 @ <th>Unversioned Content</th></tr>
383 while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
384 const char *zId = db_column_text(&q, 0);
385 const char *zCap = db_column_text(&q, 1);
386 int n = db_column_int(&q, 3);
387 int eType;
388 static const char *const azType[] = { "off", "read", "write" };
389 static const char *const azClass[] = { "capsumOff", "capsumRead", "capsumWrite" };
 
390
391 if( n==0 ) continue;
392
393 /* Code */
394 if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
416 eType = 0;
417 }
418 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
419
420 /* Ticket */
421 if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){
422 eType = 2;
423 }else if( sqlite3_strglob("*r*",zCap)==0 ){
424 eType = 1;
425 }else{
426 eType = 0;
427
--- src/capabilities.c
+++ src/capabilities.c
@@ -100,11 +100,11 @@
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 static char *zAdmin = "bcdefghijklmnopqrtwz234567AD";
106 int doneV = 0;
107
108 if( pIn==0 ){
109 fossil_free(zNobody); zNobody = 0;
110 fossil_free(zAnon); zAnon = 0;
@@ -239,15 +239,17 @@
239 char *zOneLiner; /* One-line summary */
240 } aCap[] = {
241 { 'a', CAPCLASS_SUPER, 0,
242 "Admin", "Create and delete users" },
243 { 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
244 "Attach", "Add attachments to wiki or tickets" },
245 { 'c', CAPCLASS_TKT, 0,
246 "Append-Tkt", "Append to existing tickets" },
247 /*
248 ** d unused since fork from CVSTrac;
249 ** see https://fossil-scm.org/forum/forumpost/43c78f4bef
250 */
251 { 'e', CAPCLASS_DATA, 0,
252 "View-PII", "View sensitive info such as email addresses" },
253 { 'f', CAPCLASS_WIKI, 0,
254 "New-Wiki", "Create new wiki pages" },
255 { 'g', CAPCLASS_DATA, 0,
@@ -297,11 +299,11 @@
299 { '4', CAPCLASS_FORUM, 0,
300 "Forum-Trusted", "Create forum messages that bypass moderation" },
301 { '5', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
302 "Forum-Mod", "Moderator for forum messages" },
303 { '6', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
304 "Forum-Admin", "Grant capability '4' to other users" },
305 { '7', CAPCLASS_ALERT, 0,
306 "Alerts", "Sign up for email alerts" },
307 { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
308 "Announce", "Send announcements to all subscribers" },
309 { 'D', CAPCLASS_OTHER, 0,
@@ -359,25 +361,38 @@
361 ** Generate a "capability summary table" that shows the major capabilities
362 ** against the various user categories.
363 */
364 void capability_summary(void){
365 Stmt q;
366 CapabilityString *pCap;
367 char *zSelfCap;
368 char *zPubPages = db_get("public-pages",0);
369 int hasPubPages = zPubPages && zPubPages[0];
370
371 pCap = capability_add(0, db_get("default-perms","u"));
372 capability_expand(pCap);
373 zSelfCap = capability_string(pCap);
374 capability_free(pCap);
375
376 db_prepare(&q,
377 "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
378 "('developer',4))"
379 " SELECT id, CASE WHEN user.login='nobody' THEN user.cap"
380 " ELSE fullcap(user.cap) END,seq,1"
381 " FROM t LEFT JOIN user ON t.id=user.login"
382 " UNION ALL"
383 " SELECT 'Public Pages', %Q, 100, %d"
384 " UNION ALL"
385 " SELECT 'New User Default', %Q, 110, 1"
386 " UNION ALL"
387 " SELECT 'Regular User', fullcap(capunion(cap)), 200, count(*) FROM user"
388 " WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)"
389 " UNION ALL"
390 " SELECT 'Adminstrator', fullcap(capunion(cap)), 300, count(*) FROM user"
391 " WHERE cap GLOB '*[as]*'"
392 " ORDER BY 3 ASC",
393 zSelfCap, hasPubPages, zSelfCap
394 );
395 @ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
396 @ <tr><th>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
397 @ <th>Unversioned Content</th></tr>
398 while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
399 const char *zId = db_column_text(&q, 0);
400 const char *zCap = db_column_text(&q, 1);
401 int n = db_column_int(&q, 3);
402 int eType;
403 static const char *const azType[] = { "off", "read", "write" };
404 static const char *const azClass[] =
405 { "capsumOff", "capsumRead", "capsumWrite" };
406
407 if( n==0 ) continue;
408
409 /* Code */
410 if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
432 eType = 0;
433 }
434 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
435
436 /* Ticket */
437 if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){
438 eType = 2;
439 }else if( sqlite3_strglob("*r*",zCap)==0 ){
440 eType = 1;
441 }else{
442 eType = 0;
443
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548548
@ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549549
@ <input type="text" name="captcha" size=8 />
550550
if( showButton ){
551551
@ <input type="submit" value="Submit">
552552
}
553
+ @ <br/>\
554
+ captcha_speakit_button(uSeed, 0);
553555
@ </td></tr></table></div>
554556
}
557
+
558
+/*
559
+** Add a "Speak the captcha" button.
560
+*/
561
+void captcha_speakit_button(unsigned int uSeed, const char *zMsg){
562
+ if( zMsg==0 ) zMsg = "Speak the text";
563
+ @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \
564
+ @ id="speakthetext">
565
+ @ <script nonce="%h(style_nonce())">
566
+ @ document.getElementById("speakthetext").onclick = function(){
567
+ @ var audio = window.fossilAudioCaptcha \
568
+ @ || new Audio("%R/captcha-audio/%u(uSeed)");
569
+ @ window.fossilAudioCaptcha = audio;
570
+ @ audio.currentTime = 0;
571
+ @ audio.play();
572
+ @ }
573
+ @ </script>
574
+}
555575
556576
/*
557577
** WEBPAGE: test-captcha
558578
** Test the captcha-generator by rendering the value of the name= query
559579
** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
608628
captcha_generate(1);
609629
@ </form>
610630
style_footer();
611631
return 1;
612632
}
633
+
634
+/*
635
+** Generate a WAV file that reads aloud the hex digits given by
636
+** zHex.
637
+*/
638
+static void captcha_wav(const char *zHex, Blob *pOut){
639
+ int i;
640
+ const int szWavHdr = 44;
641
+ blob_init(pOut, 0, 0);
642
+ blob_resize(pOut, szWavHdr); /* Space for the WAV header */
643
+ pOut->nUsed = szWavHdr;
644
+ memset(pOut->aData, 0, szWavHdr);
645
+ if( zHex==0 || zHex[0]==0 ) zHex = "0";
646
+ for(i=0; zHex[i]; i++){
647
+ int v = hex_digit_value(zHex[i]);
648
+ int sz;
649
+ int nData;
650
+ const unsigned char *pData;
651
+ char zSoundName[50];
652
+ sqlite3_snprintf(sizeof(zSoundName),zSoundName,"sounds/%c.wav",
653
+ "0123456789abcdef"[v]);
654
+ /* Extra silence in between letters */
655
+ if( i>0 ){
656
+ int nQuiet = 3000;
657
+ blob_resize(pOut, pOut->nUsed+nQuiet);
658
+ memset(pOut->aData+pOut->nUsed-nQuiet, 0x80, nQuiet);
659
+ }
660
+ pData = builtin_file(zSoundName, &sz);
661
+ nData = sz - szWavHdr;
662
+ blob_resize(pOut, pOut->nUsed+nData);
663
+ memcpy(pOut->aData+pOut->nUsed-nData, pData+szWavHdr, nData);
664
+ if( zHex[i+1]==0 ){
665
+ int len = pOut->nUsed + 36;
666
+ memcpy(pOut->aData, pData, szWavHdr);
667
+ pOut->aData[4] = (char)(len&0xff);
668
+ pOut->aData[5] = (char)((len>>8)&0xff);
669
+ pOut->aData[6] = (char)((len>>16)&0xff);
670
+ pOut->aData[7] = (char)((len>>24)&0xff);
671
+ len = pOut->nUsed;
672
+ pOut->aData[40] = (char)(len&0xff);
673
+ pOut->aData[41] = (char)((len>>8)&0xff);
674
+ pOut->aData[42] = (char)((len>>16)&0xff);
675
+ pOut->aData[43] = (char)((len>>24)&0xff);
676
+ }
677
+ }
678
+}
679
+
680
+/*
681
+** WEBPAGE: /captcha-audio
682
+**
683
+** Return a WAV file that pronounces the digits of the captcha that
684
+** is determined by the seed given in the name= query parameter.
685
+*/
686
+void captcha_wav_page(void){
687
+ const char *zSeed = P("name");
688
+ const char *zDecode = captcha_decode((unsigned int)atoi(zSeed));
689
+ Blob audio;
690
+ captcha_wav(zDecode, &audio);
691
+ cgi_set_content_type("audio/wav");
692
+ cgi_set_content(&audio);
693
+}
694
+
695
+/*
696
+** WEBPAGE: /test-captcha-audio
697
+**
698
+** Return a WAV file that pronounces the hex digits of the name=
699
+** query parameter.
700
+*/
701
+void captcha_test_wav_page(void){
702
+ const char *zSeed = P("name");
703
+ Blob audio;
704
+ captcha_wav(zSeed, &audio);
705
+ cgi_set_content_type("audio/wav");
706
+ cgi_set_content(&audio);
707
+}
613708
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548 @ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549 @ <input type="text" name="captcha" size=8 />
550 if( showButton ){
551 @ <input type="submit" value="Submit">
552 }
 
 
553 @ </td></tr></table></div>
554 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
556 /*
557 ** WEBPAGE: test-captcha
558 ** Test the captcha-generator by rendering the value of the name= query
559 ** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
608 captcha_generate(1);
609 @ </form>
610 style_footer();
611 return 1;
612 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548 @ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549 @ <input type="text" name="captcha" size=8 />
550 if( showButton ){
551 @ <input type="submit" value="Submit">
552 }
553 @ <br/>\
554 captcha_speakit_button(uSeed, 0);
555 @ </td></tr></table></div>
556 }
557
558 /*
559 ** Add a "Speak the captcha" button.
560 */
561 void captcha_speakit_button(unsigned int uSeed, const char *zMsg){
562 if( zMsg==0 ) zMsg = "Speak the text";
563 @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \
564 @ id="speakthetext">
565 @ <script nonce="%h(style_nonce())">
566 @ document.getElementById("speakthetext").onclick = function(){
567 @ var audio = window.fossilAudioCaptcha \
568 @ || new Audio("%R/captcha-audio/%u(uSeed)");
569 @ window.fossilAudioCaptcha = audio;
570 @ audio.currentTime = 0;
571 @ audio.play();
572 @ }
573 @ </script>
574 }
575
576 /*
577 ** WEBPAGE: test-captcha
578 ** Test the captcha-generator by rendering the value of the name= query
579 ** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
628 captcha_generate(1);
629 @ </form>
630 style_footer();
631 return 1;
632 }
633
634 /*
635 ** Generate a WAV file that reads aloud the hex digits given by
636 ** zHex.
637 */
638 static void captcha_wav(const char *zHex, Blob *pOut){
639 int i;
640 const int szWavHdr = 44;
641 blob_init(pOut, 0, 0);
642 blob_resize(pOut, szWavHdr); /* Space for the WAV header */
643 pOut->nUsed = szWavHdr;
644 memset(pOut->aData, 0, szWavHdr);
645 if( zHex==0 || zHex[0]==0 ) zHex = "0";
646 for(i=0; zHex[i]; i++){
647 int v = hex_digit_value(zHex[i]);
648 int sz;
649 int nData;
650 const unsigned char *pData;
651 char zSoundName[50];
652 sqlite3_snprintf(sizeof(zSoundName),zSoundName,"sounds/%c.wav",
653 "0123456789abcdef"[v]);
654 /* Extra silence in between letters */
655 if( i>0 ){
656 int nQuiet = 3000;
657 blob_resize(pOut, pOut->nUsed+nQuiet);
658 memset(pOut->aData+pOut->nUsed-nQuiet, 0x80, nQuiet);
659 }
660 pData = builtin_file(zSoundName, &sz);
661 nData = sz - szWavHdr;
662 blob_resize(pOut, pOut->nUsed+nData);
663 memcpy(pOut->aData+pOut->nUsed-nData, pData+szWavHdr, nData);
664 if( zHex[i+1]==0 ){
665 int len = pOut->nUsed + 36;
666 memcpy(pOut->aData, pData, szWavHdr);
667 pOut->aData[4] = (char)(len&0xff);
668 pOut->aData[5] = (char)((len>>8)&0xff);
669 pOut->aData[6] = (char)((len>>16)&0xff);
670 pOut->aData[7] = (char)((len>>24)&0xff);
671 len = pOut->nUsed;
672 pOut->aData[40] = (char)(len&0xff);
673 pOut->aData[41] = (char)((len>>8)&0xff);
674 pOut->aData[42] = (char)((len>>16)&0xff);
675 pOut->aData[43] = (char)((len>>24)&0xff);
676 }
677 }
678 }
679
680 /*
681 ** WEBPAGE: /captcha-audio
682 **
683 ** Return a WAV file that pronounces the digits of the captcha that
684 ** is determined by the seed given in the name= query parameter.
685 */
686 void captcha_wav_page(void){
687 const char *zSeed = P("name");
688 const char *zDecode = captcha_decode((unsigned int)atoi(zSeed));
689 Blob audio;
690 captcha_wav(zDecode, &audio);
691 cgi_set_content_type("audio/wav");
692 cgi_set_content(&audio);
693 }
694
695 /*
696 ** WEBPAGE: /test-captcha-audio
697 **
698 ** Return a WAV file that pronounces the hex digits of the name=
699 ** query parameter.
700 */
701 void captcha_test_wav_page(void){
702 const char *zSeed = P("name");
703 Blob audio;
704 captcha_wav(zSeed, &audio);
705 cgi_set_content_type("audio/wav");
706 cgi_set_content(&audio);
707 }
708
+296 -70
--- src/cgi.c
+++ src/cgi.c
@@ -64,10 +64,21 @@
6464
#define PDT(x,y) cgi_parameter_trimmed((x),(y))
6565
#define PB(x) cgi_parameter_boolean(x)
6666
#define PCK(x) cgi_parameter_checked(x,1)
6767
#define PIF(x,y) cgi_parameter_checked(x,y)
6868
69
+/*
70
+** Shortcut for the cgi_printf() routine. Instead of using the
71
+**
72
+** @ ...
73
+**
74
+** notation provided by the translate.c utility, you can also
75
+** optionally use:
76
+**
77
+** CX(...)
78
+*/
79
+#define CX cgi_printf
6980
7081
/*
7182
** Destinations for output text.
7283
*/
7384
#define CGI_HEADER 0
@@ -144,10 +155,17 @@
144155
** Return a pointer to the CGI output blob.
145156
*/
146157
Blob *cgi_output_blob(void){
147158
return pContent;
148159
}
160
+
161
+/*
162
+** Return complete text of the output header
163
+*/
164
+const char *cgi_header(void){
165
+ return blob_str(&cgiContent[0]);
166
+}
149167
150168
/*
151169
** Combine the header and body of the CGI into a single string.
152170
*/
153171
static void cgi_combine_header_and_body(void){
@@ -167,14 +185,16 @@
167185
}
168186
169187
/*
170188
** Additional information used to form the HTTP reply
171189
*/
172
-static const char *zContentType = "text/html"; /* Content type of the reply */
173
-static const char *zReplyStatus = "OK"; /* Reply status description */
190
+static const char *zContentType = "text/html"; /* Content type of the reply */
191
+static const char *zReplyStatus = "OK"; /* Reply status description */
174192
static int iReplyStatus = 200; /* Reply status code */
175193
static Blob extraHeader = BLOB_INITIALIZER; /* Extra header text */
194
+static int rangeStart = 0; /* Start of Range: */
195
+static int rangeEnd = 0; /* End of Range: plus 1 */
176196
177197
/*
178198
** Set the reply content type
179199
*/
180200
void cgi_set_content_type(const char *zType){
@@ -202,10 +222,16 @@
202222
/*
203223
** Append text to the header of an HTTP reply
204224
*/
205225
void cgi_append_header(const char *zLine){
206226
blob_append(&extraHeader, zLine, -1);
227
+}
228
+void cgi_printf_header(const char *zLine, ...){
229
+ va_list ap;
230
+ va_start(ap, zLine);
231
+ blob_vappendf(&extraHeader, zLine, ap);
232
+ va_end(ap);
207233
}
208234
209235
/*
210236
** Set a cookie by queuing up the appropriate HTTP header output. If
211237
** !g.isHTTP, this is a no-op.
@@ -259,22 +285,30 @@
259285
iReplyStatus = 200;
260286
zReplyStatus = "OK";
261287
}
262288
263289
if( g.fullHttpReply ){
290
+ if( rangeEnd>0
291
+ && iReplyStatus==200
292
+ && fossil_strcmp(P("REQUEST_METHOD"),"GET")==0
293
+ ){
294
+ iReplyStatus = 206;
295
+ zReplyStatus = "Partial Content";
296
+ }
264297
fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus);
265298
fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0)));
266299
fprintf(g.httpOut, "Connection: close\r\n");
267300
fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n");
268301
}else{
302
+ assert( rangeEnd==0 );
269303
fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus);
270304
}
271305
if( g.isConst ){
272306
/* isConst means that the reply is guaranteed to be invariant, even
273307
** after configuration changes and/or Fossil binary recompiles. */
274308
fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n");
275
- }else if( etag_tag()!=0 ){
309
+ }else if( etag_tag()[0]!=0 ){
276310
fprintf(g.httpOut, "ETag: %s\r\n", etag_tag());
277311
fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage());
278312
}else{
279313
fprintf(g.httpOut, "Cache-control: no-cache\r\n");
280314
}
@@ -306,18 +340,18 @@
306340
*/
307341
308342
/* Content intended for logged in users should only be cached in
309343
** the browser, not some shared location.
310344
*/
311
- fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType);
312
- if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){
313
- cgi_combine_header_and_body();
314
- blob_compress(&cgiContent[0], &cgiContent[0]);
315
- }
316
-
317
- if( iReplyStatus != 304 ) {
318
- if( is_gzippable() ){
345
+ if( iReplyStatus!=304 ) {
346
+ fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType);
347
+ if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){
348
+ cgi_combine_header_and_body();
349
+ blob_compress(&cgiContent[0], &cgiContent[0]);
350
+ }
351
+
352
+ if( is_gzippable() && iReplyStatus!=206 ){
319353
int i;
320354
gzip_begin(0);
321355
for( i=0; i<2; i++ ){
322356
int size = blob_size(&cgiContent[i]);
323357
if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size);
@@ -326,28 +360,42 @@
326360
gzip_finish(&cgiContent[0]);
327361
fprintf(g.httpOut, "Content-Encoding: gzip\r\n");
328362
fprintf(g.httpOut, "Vary: Accept-Encoding\r\n");
329363
}
330364
total_size = blob_size(&cgiContent[0]) + blob_size(&cgiContent[1]);
365
+ if( iReplyStatus==206 ){
366
+ fprintf(g.httpOut, "Content-Range: bytes %d-%d/%d\r\n",
367
+ rangeStart, rangeEnd-1, total_size);
368
+ total_size = rangeEnd - rangeStart;
369
+ }
331370
fprintf(g.httpOut, "Content-Length: %d\r\n", total_size);
332371
}else{
333372
total_size = 0;
334373
}
335374
fprintf(g.httpOut, "\r\n");
336
- if( total_size>0 && iReplyStatus != 304
375
+ if( total_size>0
376
+ && iReplyStatus!=304
337377
&& fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0
338378
){
339379
int i, size;
340380
for(i=0; i<2; i++){
341381
size = blob_size(&cgiContent[i]);
342
- if( size>0 ){
343
- fwrite(blob_buffer(&cgiContent[i]), 1, size, g.httpOut);
382
+ if( size<=rangeStart ){
383
+ rangeStart -= size;
384
+ }else{
385
+ int n = size - rangeStart;
386
+ if( n>total_size ){
387
+ n = total_size;
388
+ }
389
+ fwrite(blob_buffer(&cgiContent[i])+rangeStart, 1, n, g.httpOut);
390
+ rangeStart = 0;
391
+ total_size -= n;
344392
}
345393
}
346394
}
347395
fflush(g.httpOut);
348
- CGIDEBUG(("DONE\n"));
396
+ CGIDEBUG(("-------- END cgi ---------\n"));
349397
350398
/* After the webpage has been sent, do any useful background
351399
** processing.
352400
*/
353401
g.cgiOutput = 2;
@@ -396,10 +444,31 @@
396444
va_list ap;
397445
va_start(ap, zFormat);
398446
cgi_redirect(vmprintf(zFormat, ap));
399447
va_end(ap);
400448
}
449
+
450
+/*
451
+** Add a "Content-disposition: attachment; filename=%s" header to the reply.
452
+*/
453
+void cgi_content_disposition_filename(const char *zFilename){
454
+ char *z;
455
+ int i, n;
456
+
457
+ /* 0123456789 123456789 123456789 123456789 123456*/
458
+ z = mprintf("Content-Disposition: attachment; filename=\"%s\";\r\n",
459
+ file_tail(zFilename));
460
+ n = (int)strlen(z);
461
+ for(i=43; i<n-4; i++){
462
+ char c = z[i];
463
+ if( fossil_isalnum(c) ) continue;
464
+ if( c=='.' || c=='-' || c=='/' ) continue;
465
+ z[i] = '_';
466
+ }
467
+ cgi_append_header(z);
468
+ fossil_free(z);
469
+}
401470
402471
/*
403472
** Return the URL for the caller. This is obtained from either the
404473
** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter.
405474
** If neither exist, return zDefault.
@@ -482,10 +551,31 @@
482551
}
483552
484553
/*
485554
** Add another query parameter or cookie to the parameter set.
486555
** zName is the name of the query parameter or cookie and zValue
556
+** is its fully decoded value. zName will be modified to be an
557
+** all lowercase string.
558
+**
559
+** zName and zValue are not copied and must not change or be
560
+** deallocated after this routine returns. This routine changes
561
+** all ASCII alphabetic characters in zName to lower case. The
562
+** caller must not change them back.
563
+*/
564
+void cgi_set_parameter_nocopy_tolower(
565
+ char *zName,
566
+ const char *zValue,
567
+ int isQP
568
+){
569
+ int i;
570
+ for(i=0; zName[i]; i++){ zName[i] = fossil_tolower(zName[i]); }
571
+ cgi_set_parameter_nocopy(zName, zValue, isQP);
572
+}
573
+
574
+/*
575
+** Add another query parameter or cookie to the parameter set.
576
+** zName is the name of the query parameter or cookie and zValue
487577
** is its fully decoded value.
488578
**
489579
** Copies are made of both the zName and zValue parameters.
490580
*/
491581
void cgi_set_parameter(const char *zName, const char *zValue){
@@ -516,10 +606,15 @@
516606
assert( aParamQP[i].isQP );
517607
return;
518608
}
519609
}
520610
cgi_set_parameter_nocopy(zName, zValue, 1);
611
+}
612
+void cgi_replace_query_parameter_tolower(char *zName, const char *zValue){
613
+ int i;
614
+ for(i=0; zName[i]; i++){ zName[i] = fossil_tolower(zName[i]); }
615
+ cgi_replace_query_parameter(zName, zValue);
521616
}
522617
523618
/*
524619
** Delete a parameter.
525620
*/
@@ -554,11 +649,10 @@
554649
** must be made of zValue.
555650
*/
556651
void cgi_setenv(const char *zName, const char *zValue){
557652
cgi_set_parameter_nocopy(zName, mprintf("%s",zValue), 0);
558653
}
559
-
560654
561655
/*
562656
** Add a list of query parameters or cookies to the parameter set.
563657
**
564658
** Each parameter is of the form NAME=VALUE. Both the NAME and the
@@ -608,12 +702,16 @@
608702
dehttpize(zValue);
609703
}else{
610704
if( *z ){ *z++ = 0; }
611705
zValue = "";
612706
}
613
- if( fossil_islower(zName[0]) && fossil_no_strange_characters(zName+1) ){
614
- cgi_set_parameter_nocopy(zName, zValue, isQP);
707
+ if( zName[0] && fossil_no_strange_characters(zName+1) ){
708
+ if( fossil_islower(zName[0]) ){
709
+ cgi_set_parameter_nocopy(zName, zValue, isQP);
710
+ }else if( fossil_isupper(zName[0]) ){
711
+ cgi_set_parameter_nocopy_tolower(zName, zValue, isQP);
712
+ }
615713
}
616714
#ifdef FOSSIL_ENABLE_JSON
617715
json_setenv( zName, cson_value_new_string(zValue,strlen(zValue)) );
618716
#endif /* FOSSIL_ENABLE_JSON */
619717
}
@@ -646,36 +744,36 @@
646744
return z;
647745
}
648746
649747
/*
650748
** The input *pz points to content that is terminated by a "\r\n"
651
-** followed by the boundry marker zBoundry. An extra "--" may or
652
-** may not be appended to the boundry marker. There are *pLen characters
749
+** followed by the boundary marker zBoundary. An extra "--" may or
750
+** may not be appended to the boundary marker. There are *pLen characters
653751
** in *pz.
654752
**
655753
** This routine adds a "\000" to the end of the content (overwriting
656754
** the "\r\n") and returns a pointer to the content. The *pz input
657
-** is adjusted to point to the first line following the boundry.
755
+** is adjusted to point to the first line following the boundary.
658756
** The length of the content is stored in *pnContent.
659757
*/
660758
static char *get_bounded_content(
661759
char **pz, /* Content taken from here */
662760
int *pLen, /* Number of bytes of data in (*pz)[] */
663
- char *zBoundry, /* Boundry text marking the end of content */
761
+ char *zBoundary, /* Boundary text marking the end of content */
664762
int *pnContent /* Write the size of the content here */
665763
){
666764
char *z = *pz;
667765
int len = *pLen;
668766
int i;
669
- int nBoundry = strlen(zBoundry);
767
+ int nBoundary = strlen(zBoundary);
670768
*pnContent = len;
671769
for(i=0; i<len; i++){
672
- if( z[i]=='\n' && strncmp(zBoundry, &z[i+1], nBoundry)==0 ){
770
+ if( z[i]=='\n' && strncmp(zBoundary, &z[i+1], nBoundary)==0 ){
673771
if( i>0 && z[i-1]=='\r' ) i--;
674772
z[i] = 0;
675773
*pnContent = i;
676
- i += nBoundry;
774
+ i += nBoundary;
677775
break;
678776
}
679777
}
680778
*pz = &z[i];
681779
get_line_from_string(pz, pLen);
@@ -740,27 +838,35 @@
740838
** table.
741839
*/
742840
static void process_multipart_form_data(char *z, int len){
743841
char *zLine;
744842
int nArg, i;
745
- char *zBoundry;
843
+ char *zBoundary;
746844
char *zValue;
747845
char *zName = 0;
748846
int showBytes = 0;
749847
char *azArg[50];
750848
751
- zBoundry = get_line_from_string(&z, &len);
752
- if( zBoundry==0 ) return;
849
+ zBoundary = get_line_from_string(&z, &len);
850
+ if( zBoundary==0 ) return;
753851
while( (zLine = get_line_from_string(&z, &len))!=0 ){
754852
if( zLine[0]==0 ){
755853
int nContent = 0;
756
- zValue = get_bounded_content(&z, &len, zBoundry, &nContent);
757
- if( zName && zValue && fossil_islower(zName[0]) ){
758
- cgi_set_parameter_nocopy(zName, zValue, 1);
759
- if( showBytes ){
760
- cgi_set_parameter_nocopy(mprintf("%s:bytes", zName),
761
- mprintf("%d",nContent), 1);
854
+ zValue = get_bounded_content(&z, &len, zBoundary, &nContent);
855
+ if( zName && zValue ){
856
+ if( fossil_islower(zName[0]) ){
857
+ cgi_set_parameter_nocopy(zName, zValue, 1);
858
+ if( showBytes ){
859
+ cgi_set_parameter_nocopy(mprintf("%s:bytes", zName),
860
+ mprintf("%d",nContent), 1);
861
+ }
862
+ }else if( fossil_isupper(zName[0]) ){
863
+ cgi_set_parameter_nocopy_tolower(zName, zValue, 1);
864
+ if( showBytes ){
865
+ cgi_set_parameter_nocopy_tolower(mprintf("%s:bytes", zName),
866
+ mprintf("%d",nContent), 1);
867
+ }
762868
}
763869
}
764870
zName = 0;
765871
showBytes = 0;
766872
}else{
@@ -772,18 +878,28 @@
772878
i++;
773879
}else if( c=='n' && sqlite3_strnicmp(azArg[i],"name=",n)==0 ){
774880
zName = azArg[++i];
775881
}else if( c=='f' && sqlite3_strnicmp(azArg[i],"filename=",n)==0 ){
776882
char *z = azArg[++i];
777
- if( zName && z && fossil_islower(zName[0]) ){
778
- cgi_set_parameter_nocopy(mprintf("%s:filename",zName), z, 1);
883
+ if( zName && z ){
884
+ if( fossil_islower(zName[0]) ){
885
+ cgi_set_parameter_nocopy(mprintf("%s:filename",zName), z, 1);
886
+ }else if( fossil_isupper(zName[0]) ){
887
+ cgi_set_parameter_nocopy_tolower(mprintf("%s:filename",zName),
888
+ z, 1);
889
+ }
779890
}
780891
showBytes = 1;
781892
}else if( c=='c' && sqlite3_strnicmp(azArg[i],"content-type:",n)==0 ){
782893
char *z = azArg[++i];
783
- if( zName && z && fossil_islower(zName[0]) ){
784
- cgi_set_parameter_nocopy(mprintf("%s:mimetype",zName), z, 1);
894
+ if( zName && z ){
895
+ if( fossil_islower(zName[0]) ){
896
+ cgi_set_parameter_nocopy(mprintf("%s:mimetype",zName), z, 1);
897
+ }else if( fossil_isupper(zName[0]) ){
898
+ cgi_set_parameter_nocopy_tolower(mprintf("%s:mimetype",zName),
899
+ z, 1);
900
+ }
785901
}
786902
}
787903
}
788904
}
789905
}
@@ -938,21 +1054,40 @@
9381054
** PATH_INFO when it is empty.
9391055
*/
9401056
void cgi_init(void){
9411057
char *z;
9421058
const char *zType;
1059
+ char *zSemi;
9431060
int len;
9441061
const char *zRequestUri = cgi_parameter("REQUEST_URI",0);
9451062
const char *zScriptName = cgi_parameter("SCRIPT_NAME",0);
9461063
const char *zPathInfo = cgi_parameter("PATH_INFO",0);
1064
+#ifdef _WIN32
1065
+ const char *zServerSoftware = cgi_parameter("SERVER_SOFTWARE",0);
1066
+#endif
9471067
9481068
#ifdef FOSSIL_ENABLE_JSON
949
- json_main_bootstrap();
1069
+ int noJson = P("no_json")!=0;
1070
+ if( noJson==0 ){ json_main_bootstrap(); }
9501071
#endif
9511072
g.isHTTP = 1;
9521073
cgi_destination(CGI_BODY);
9531074
if( zScriptName==0 ) malformed_request("missing SCRIPT_NAME");
1075
+#ifdef _WIN32
1076
+ /* The Microsoft IIS web server does not define REQUEST_URI, instead it uses
1077
+ ** PATH_INFO for virtually the same purpose. Define REQUEST_URI the same as
1078
+ ** PATH_INFO and redefine PATH_INFO with SCRIPT_NAME removed from the
1079
+ ** beginning. */
1080
+ if( zServerSoftware && strstr(zServerSoftware, "Microsoft-IIS") ){
1081
+ int i, j;
1082
+ cgi_set_parameter("REQUEST_URI", zPathInfo);
1083
+ for(i=0; zPathInfo[i]==zScriptName[i] && zPathInfo[i]; i++){}
1084
+ for(j=i; zPathInfo[j] && zPathInfo[j]!='?'; j++){}
1085
+ zPathInfo = mprintf("%.*s", j-i, zPathInfo+i);
1086
+ cgi_replace_parameter("PATH_INFO", zPathInfo);
1087
+ }
1088
+#endif
9541089
if( zRequestUri==0 ){
9551090
const char *z = zPathInfo;
9561091
if( zPathInfo==0 ){
9571092
malformed_request("missing PATH_INFO and/or REQUEST_URI");
9581093
}
@@ -962,13 +1097,27 @@
9621097
}
9631098
if( zPathInfo==0 ){
9641099
int i, j;
9651100
for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){}
9661101
for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){}
967
- cgi_set_parameter("PATH_INFO", mprintf("%.*s", j-i, zRequestUri+i));
1102
+ zPathInfo = mprintf("%.*s", j-i, zRequestUri+i);
1103
+ cgi_set_parameter("PATH_INFO", zPathInfo);
1104
+ }
1105
+#ifdef FOSSIL_ENABLE_JSON
1106
+ if(strncmp("/json",zPathInfo,5)==0
1107
+ && (zPathInfo[5]==0 || zPathInfo[5]=='/')){
1108
+ /* We need to change some following behaviour depending on whether
1109
+ ** we are operating in JSON mode or not. We cannot, however, be
1110
+ ** certain whether we should/need to be in JSON mode until the
1111
+ ** PATH_INFO is set up.
1112
+ */
1113
+ g.json.isJsonMode = 1;
1114
+ }else{
1115
+ assert(!g.json.isJsonMode &&
1116
+ "Internal misconfiguration of g.json.isJsonMode");
9681117
}
969
-
1118
+#endif
9701119
z = (char*)P("HTTP_COOKIE");
9711120
if( z ){
9721121
z = mprintf("%s",z);
9731122
add_param_list(z, ';');
9741123
}
@@ -983,37 +1132,33 @@
9831132
if( z ){
9841133
g.zIpAddr = mprintf("%s", z);
9851134
}
9861135
9871136
len = atoi(PD("CONTENT_LENGTH", "0"));
988
- g.zContentType = zType = P("CONTENT_TYPE");
1137
+ zType = P("CONTENT_TYPE");
1138
+ zSemi = zType ? strchr(zType, ';') : 0;
1139
+ if( zSemi ){
1140
+ g.zContentType = mprintf("%.*s", (int)(zSemi-zType), zType);
1141
+ zType = g.zContentType;
1142
+ }else{
1143
+ g.zContentType = zType;
1144
+ }
9891145
blob_zero(&g.cgiIn);
9901146
if( len>0 && zType ){
9911147
if( fossil_strcmp(zType, "application/x-fossil")==0 ){
9921148
blob_read_from_channel(&g.cgiIn, g.httpIn, len);
9931149
blob_uncompress(&g.cgiIn, &g.cgiIn);
9941150
}
9951151
#ifdef FOSSIL_ENABLE_JSON
996
- else if( fossil_strcmp(zType, "application/json")==0
997
- || fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/
998
- || fossil_strcmp(zType,"application/javascript")==0){
999
- g.json.isJsonMode = 1;
1152
+ else if( noJson==0 && g.json.isJsonMode!=0
1153
+ && json_can_consume_content_type(zType)!=0 ){
10001154
cgi_parse_POST_JSON(g.httpIn, (unsigned int)len);
1001
- /* FIXMEs:
1002
-
1003
- - See if fossil really needs g.cgiIn to be set for this purpose
1004
- (i don't think it does). If it does then fill g.cgiIn and
1005
- refactor to parse the JSON from there.
1006
-
1007
- - After parsing POST JSON, copy the "first layer" of keys/values
1008
- to cgi_setenv(), honoring the upper-case distinction used
1009
- in add_param_list(). However...
1010
-
1011
- - If we do that then we might get a disconnect in precedence of
1012
- GET/POST arguments. i prefer for GET entries to take precedence
1013
- over like-named POST entries, but in order for that to happen we
1014
- need to process QUERY_STRING _after_ reading the POST data.
1155
+ /*
1156
+ Potential TODOs:
1157
+
1158
+ 1) If parsing fails, immediately return an error response
1159
+ without dispatching the ostensibly-upcoming JSON API.
10151160
*/
10161161
cgi_set_content_type(json_guess_content_type());
10171162
}
10181163
#endif /* FOSSIL_ENABLE_JSON */
10191164
else{
@@ -1087,10 +1232,14 @@
10871232
}
10881233
j++;
10891234
}
10901235
nUsedQP = j;
10911236
}
1237
+
1238
+ /* Invoking with a NULL zName is just a way to cause the parameters
1239
+ ** to be sorted. So go ahead and bail out in that case */
1240
+ if( zName==0 || zName[0]==0 ) return 0;
10921241
10931242
/* Do a binary search for a matching query parameter */
10941243
lo = 0;
10951244
hi = nUsedQP-1;
10961245
while( lo<=hi ){
@@ -1106,15 +1255,16 @@
11061255
}
11071256
}
11081257
11091258
/* If no match is found and the name begins with an upper-case
11101259
** letter, then check to see if there is an environment variable
1111
- ** with the given name.
1260
+ ** with the given name. Handle environment variables with empty values
1261
+ ** the same as non-existent environment variables.
11121262
*/
1113
- if( zName && fossil_isupper(zName[0]) ){
1263
+ if( fossil_isupper(zName[0]) ){
11141264
const char *zValue = fossil_getenv(zName);
1115
- if( zValue ){
1265
+ if( zValue && zValue[0] ){
11161266
cgi_set_parameter_nocopy(zName, zValue, 0);
11171267
CGIDEBUG(("env-match [%s] = [%s]\n", zName, zValue));
11181268
return zValue;
11191269
}
11201270
}
@@ -1233,28 +1383,73 @@
12331383
va_end(ap);
12341384
return 1;
12351385
}
12361386
12371387
/*
1238
-** Print all query parameters on standard output. Format the
1239
-** parameters as HTML. This is used for testing and debugging.
1388
+** Load all relevant environment variables into the parameter buffer.
1389
+** Invoke this routine prior to calling cgi_print_all() in order to see
1390
+** the full CGI environment. This routine intended for debugging purposes
1391
+** only.
1392
+*/
1393
+void cgi_load_environment(void){
1394
+ /* The following is a list of environment variables that Fossil considers
1395
+ ** to be "relevant". */
1396
+ static const char *const azCgiVars[] = {
1397
+ "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", "SCGI",
1398
+ "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING",
1399
+ "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION",
1400
+ "HTTP_CONNECTION", "HTTP_HOST",
1401
+ "HTTP_IF_NONE_MATCH", "HTTP_IF_MODIFIED_SINCE",
1402
+ "HTTP_USER_AGENT", "HTTP_REFERER", "PATH_INFO", "PATH_TRANSLATED",
1403
+ "QUERY_STRING", "REMOTE_ADDR", "REMOTE_PORT",
1404
+ "REMOTE_USER", "REQUEST_METHOD",
1405
+ "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_PROTOCOL",
1406
+ "HOME", "FOSSIL_HOME", "USERNAME", "USER", "FOSSIL_USER",
1407
+ "SQLITE_TMPDIR", "TMPDIR",
1408
+ "TEMP", "TMP", "FOSSIL_VFS",
1409
+ "FOSSIL_FORCE_TICKET_MODERATION", "FOSSIL_FORCE_WIKI_MODERATION",
1410
+ "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS",
1411
+ "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST",
1412
+ };
1413
+ int i;
1414
+ for(i=0; i<count(azCgiVars); i++) (void)P(azCgiVars[i]);
1415
+}
1416
+
1417
+/*
1418
+** Print all query parameters on standard output.
1419
+** This is used for testing and debugging.
12401420
**
12411421
** Omit the values of the cookies unless showAll is true.
1422
+**
1423
+** The eDest parameter determines where the output is shown:
1424
+**
1425
+** eDest==0: Rendering as HTML into the CGI reply
1426
+** eDest==1: Written to stderr
1427
+** eDest==2: Written to cgi_debug
12421428
*/
1243
-void cgi_print_all(int showAll, int onConsole){
1429
+void cgi_print_all(int showAll, unsigned int eDest){
12441430
int i;
12451431
cgi_parameter("",""); /* Force the parameters into sorted order */
12461432
for(i=0; i<nUsedQP; i++){
12471433
const char *zName = aParamQP[i].zName;
12481434
if( !showAll ){
12491435
if( fossil_stricmp("HTTP_COOKIE",zName)==0 ) continue;
12501436
if( fossil_strnicmp("fossil-",zName,7)==0 ) continue;
12511437
}
1252
- if( onConsole ){
1253
- fossil_trace("%s = %s\n", zName, aParamQP[i].zValue);
1254
- }else{
1255
- cgi_printf("%h = %h <br />\n", zName, aParamQP[i].zValue);
1438
+ switch( eDest ){
1439
+ case 0: {
1440
+ cgi_printf("%h = %h <br />\n", zName, aParamQP[i].zValue);
1441
+ break;
1442
+ }
1443
+ case 1: {
1444
+ fossil_trace("%s = %s\n", zName, aParamQP[i].zValue);
1445
+ break;
1446
+ }
1447
+ case 2: {
1448
+ cgi_debug("%s = %s\n", zName, aParamQP[i].zValue);
1449
+ break;
1450
+ }
12561451
}
12571452
}
12581453
}
12591454
12601455
/*
@@ -1473,10 +1668,11 @@
14731668
}
14741669
if( zIpAddr ){
14751670
cgi_setenv("REMOTE_ADDR", zIpAddr);
14761671
g.zIpAddr = mprintf("%s", zIpAddr);
14771672
}
1673
+
14781674
14791675
/* Get all the optional fields that follow the first line.
14801676
*/
14811677
while( fgets(zLine,sizeof(zLine),g.httpIn) ){
14821678
char *zFieldName;
@@ -1519,10 +1715,17 @@
15191715
const char *zIpAddr = cgi_accept_forwarded_for(zVal);
15201716
if( zIpAddr!=0 ){
15211717
g.zIpAddr = mprintf("%s", zIpAddr);
15221718
cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr);
15231719
}
1720
+ }else if( fossil_strcmp(zFieldName,"range:")==0 ){
1721
+ int x1 = 0;
1722
+ int x2 = 0;
1723
+ if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){
1724
+ rangeStart = x1;
1725
+ rangeEnd = x2+1;
1726
+ }
15241727
}
15251728
}
15261729
cgi_init();
15271730
cgi_trace(0);
15281731
}
@@ -1542,10 +1745,13 @@
15421745
char *z, *zToken;
15431746
const char *zType = 0;
15441747
int i, content_length = 0;
15451748
char zLine[2000]; /* A single line of input. */
15461749
1750
+#ifdef FOSSIL_ENABLE_JSON
1751
+ if( nCycles==0 ){ json_main_bootstrap(); }
1752
+#endif
15471753
if( zIpAddr ){
15481754
if( nCycles==0 ){
15491755
cgi_setenv("REMOTE_ADDR", zIpAddr);
15501756
g.zIpAddr = mprintf("%s", zIpAddr);
15511757
}
@@ -1995,10 +2201,30 @@
19952201
return mprintf("%s, %d %s %02d %02d:%02d:%02d +0000",
19962202
azDays[pTm->tm_wday], pTm->tm_mday, azMonths[pTm->tm_mon],
19972203
pTm->tm_year+1900, pTm->tm_hour, pTm->tm_min, pTm->tm_sec);
19982204
}
19992205
}
2206