Fossil SCM
Merge trunk
Commit
69b450c1e028c077db3030487fb6ab4e3bae5937d24c0c79cac9c3b5ee77711e
Parent
fc5f88d6d9f12e7…
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
+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.* |
| --- 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.* |
+2
| --- Makefile.classic | ||
| +++ Makefile.classic | ||
| @@ -99,7 +99,9 @@ | ||
| 99 | 99 | TCC.DragonFly += -DUSE_PREAD |
| 100 | 100 | TCC.FreeBSD += -DUSE_PREAD |
| 101 | 101 | TCC.NetBSD += -DUSE_PREAD |
| 102 | 102 | TCC.OpenBSD += -DUSE_PREAD |
| 103 | 103 | TCC += $(TCC.$(HOST_OS)) |
| 104 | + | |
| 105 | +APPNAME = fossil$(E) | |
| 104 | 106 | |
| 105 | 107 | include $(SRCDIR)/main.mk |
| 106 | 108 |
| --- 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 |
+8
| --- Makefile.in | ||
| +++ Makefile.in | ||
| @@ -46,19 +46,27 @@ | ||
| 46 | 46 | USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@ |
| 47 | 47 | USE_LINENOISE = @USE_LINENOISE@ |
| 48 | 48 | USE_MMAN_H = @USE_MMAN_H@ |
| 49 | 49 | USE_SEE = @USE_SEE@ |
| 50 | 50 | FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@ |
| 51 | +APPNAME = fossil | |
| 52 | + | |
| 53 | +.PHONY: all tags | |
| 51 | 54 | |
| 52 | 55 | include $(SRCDIR)/main.mk |
| 53 | 56 | |
| 54 | 57 | distclean: clean |
| 55 | 58 | -rm -f autoconfig.h config.log Makefile |
| 59 | + -rm -f cscope.out tags | |
| 56 | 60 | |
| 57 | 61 | reconfig: |
| 58 | 62 | @AUTOREMAKE@ |
| 59 | 63 | |
| 64 | +tags: | |
| 65 | + ctags -R @srcdir@/src | |
| 66 | + @COLLECT_CSCOPE_DATA@ | |
| 67 | + | |
| 60 | 68 | # Automatically reconfigure whenever an autosetup file or one of the |
| 61 | 69 | # make source files change. |
| 62 | 70 | # |
| 63 | 71 | # The "touch" is necessary to avoid a make loop due to a new upstream |
| 64 | 72 | # feature in autosetup (GH 0a71e3c3b7) which rewrites *.in outputs only |
| 65 | 73 |
| --- 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 |
M
VERSION
+1
-1
| --- VERSION | ||
| +++ VERSION | ||
| @@ -1,1 +1,1 @@ | ||
| 1 | -2.10 | |
| 1 | +2.12 | |
| 2 | 2 |
| --- VERSION | |
| +++ VERSION | |
| @@ -1,1 +1,1 @@ | |
| 1 | 2.10 |
| 2 |
| --- VERSION | |
| +++ VERSION | |
| @@ -1,1 +1,1 @@ | |
| 1 | 2.12 |
| 2 |
M
auto.def
+59
-8
| --- auto.def | ||
| +++ auto.def | ||
| @@ -9,10 +9,11 @@ | ||
| 9 | 9 | with-zlib:path|auto|tree |
| 10 | 10 | => {Look for zlib in the given path, automatically, or in the source tree} |
| 11 | 11 | with-exec-rel-paths=0 |
| 12 | 12 | => {Enable relative paths for external diff/gdiff} |
| 13 | 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} | |
| 14 | 15 | with-th1-docs=0 => {Enable TH1 for embedded documentation pages} |
| 15 | 16 | with-th1-hooks=0 => {Enable TH1 hooks for commands and web pages} |
| 16 | 17 | with-tcl:path => {Enable Tcl integration, with Tcl in the specified path} |
| 17 | 18 | with-tcl-stubs=0 => {Enable Tcl integration via stubs library mechanism} |
| 18 | 19 | with-tcl-private-stubs=0 |
| @@ -32,10 +33,17 @@ | ||
| 32 | 33 | cc-check-types uint32_t uint16_t int16_t uint8_t |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | # Use pread/pwrite system calls in place of seek + read/write if possible |
| 36 | 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 | +} | |
| 37 | 45 | |
| 38 | 46 | # Find tclsh for the test suite. |
| 39 | 47 | # |
| 40 | 48 | # We can't use jimsh for this: the test suite uses features of Tcl that |
| 41 | 49 | # Jim doesn't support, either statically or due to the way it's built by |
| @@ -116,11 +124,11 @@ | ||
| 116 | 124 | } |
| 117 | 125 | return $found |
| 118 | 126 | } |
| 119 | 127 | |
| 120 | 128 | if {![opt-bool internal-sqlite]} { |
| 121 | - proc find_internal_sqlite {} { | |
| 129 | + proc find_system_sqlite {} { | |
| 122 | 130 | |
| 123 | 131 | # On some systems (slackware), libsqlite3 requires -ldl to link. So |
| 124 | 132 | # search for the system SQLite once with -ldl, and once without. If |
| 125 | 133 | # the library can only be found with $extralibs set to -ldl, then |
| 126 | 134 | # the code below will append -ldl to LIBS. |
| @@ -131,14 +139,10 @@ | ||
| 131 | 139 | # if sqlite3_stmt_isexplain can be found as well. If we can find open() but |
| 132 | 140 | # not stmt_isexplain(), then the system SQLite is too old to link against |
| 133 | 141 | # fossil. |
| 134 | 142 | # |
| 135 | 143 | 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 | 144 | # Success. Update symbols and return. |
| 141 | 145 | # |
| 142 | 146 | define USE_SYSTEM_SQLITE 1 |
| 143 | 147 | define-append LIBS -lsqlite3 |
| 144 | 148 | define-append LIBS $extralibs |
| @@ -146,11 +150,44 @@ | ||
| 146 | 150 | } |
| 147 | 151 | } |
| 148 | 152 | user-error "system sqlite3 not found" |
| 149 | 153 | } |
| 150 | 154 | |
| 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 | + | |
| 152 | 189 | } |
| 153 | 190 | |
| 154 | 191 | proc is_mingw {} { |
| 155 | 192 | return [string match *mingw* [get-define host]] |
| 156 | 193 | } |
| @@ -243,11 +280,11 @@ | ||
| 243 | 280 | |
| 244 | 281 | # Check for libraries that need to be sorted out early |
| 245 | 282 | cc-check-function-in-lib iconv iconv |
| 246 | 283 | |
| 247 | 284 | # 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}}} { | |
| 249 | 286 | msg-checking "Checking for $msg..." |
| 250 | 287 | set rc 0 |
| 251 | 288 | if {[is_mingw]} { |
| 252 | 289 | lappend libs -lgdi32 -lwsock32 -lcrypt32 |
| 253 | 290 | } |
| @@ -321,11 +358,11 @@ | ||
| 321 | 358 | user-error "The OpenSSL in source tree directory does not exist" |
| 322 | 359 | } |
| 323 | 360 | set msg "ssl in $ssldir" |
| 324 | 361 | set cflags "-I$ssldir/include" |
| 325 | 362 | set ldflags "-L$ssldir" |
| 326 | - set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a" | |
| 363 | + set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread" | |
| 327 | 364 | set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs] |
| 328 | 365 | } else { |
| 329 | 366 | if {$ssldirs in {auto ""}} { |
| 330 | 367 | catch { |
| 331 | 368 | set cflags [exec pkg-config openssl --cflags-only-I] |
| @@ -560,10 +597,24 @@ | ||
| 560 | 597 | define FOSSIL_HAVE_FUSEFS 1 |
| 561 | 598 | define-append LIBS -lfuse |
| 562 | 599 | msg-result "FuseFS support enabled" |
| 563 | 600 | } |
| 564 | 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 | +} | |
| 565 | 616 | |
| 566 | 617 | # Finally, append -ldl to make sure it's the last in the list. |
| 567 | 618 | # The library order matters in case of static linking. |
| 568 | 619 | if {[check-function-in-lib dlopen dl]} { |
| 569 | 620 | # Some platforms (*BSD) have the dl functions already in libc and no libdl. |
| 570 | 621 |
| --- 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 @@ | ||
| 1 | 1 | Built-in Skins |
| 2 | 2 | ============== |
| 3 | 3 | |
| 4 | 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. | |
| 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 | |
| 7 | 12 | |
| 8 | 13 | To improve an existing built-in skin, simply edit the appropriate |
| 9 | 14 | files and recompile. |
| 10 | 15 | |
| 11 | 16 | To add a new skin: |
| @@ -13,12 +18,12 @@ | ||
| 13 | 18 | 1. Create a new subdirectory under skins/. (The new directory is |
| 14 | 19 | called "skins/newskin" below but you should use a new original |
| 15 | 20 | name, of course.) |
| 16 | 21 | |
| 17 | 22 | 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. | |
| 20 | 25 | |
| 21 | 26 | 3. Go to the src/ directory and rerun "tclsh makemake.tcl". This |
| 22 | 27 | step rebuilds the various makefiles so that they have dependencies |
| 23 | 28 | on the skin files you just installed. |
| 24 | 29 | |
| @@ -25,21 +30,23 @@ | ||
| 25 | 30 | 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source |
| 26 | 31 | file so that it describes and references the "newskin" skin. |
| 27 | 32 | |
| 28 | 33 | 5. Type "make" to rebuild. |
| 29 | 34 | |
| 35 | +See the [custom skin documentation](../www/customskin.md) for more information. | |
| 36 | + | |
| 30 | 37 | Development Hints |
| 31 | 38 | ----------------- |
| 32 | 39 | |
| 33 | 40 | 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: | |
| 36 | 44 | |
| 37 | 45 | cp -r skins/default newskin |
| 38 | 46 | fossil ui --skin ./newskin |
| 39 | 47 | |
| 40 | 48 | When the argument to --skin contains one or more '/' characters, the |
| 41 | 49 | 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. | |
| 46 | 53 |
| --- 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 |
+35
-1
| --- skins/ardoise/css.txt | ||
| +++ skins/ardoise/css.txt | ||
| @@ -291,11 +291,11 @@ | ||
| 291 | 291 | box-sizing: border-box |
| 292 | 292 | } |
| 293 | 293 | .column, |
| 294 | 294 | .columns { |
| 295 | 295 | width: 100%; |
| 296 | - float: left; | |
| 296 | + /*float: left; can break README.md in /dir view*/ | |
| 297 | 297 | box-sizing: border-box |
| 298 | 298 | } |
| 299 | 299 | @media (min-width:400px) { |
| 300 | 300 | .container { |
| 301 | 301 | width: 95%; |
| @@ -802,10 +802,16 @@ | ||
| 802 | 802 | background-color: #333 |
| 803 | 803 | } |
| 804 | 804 | td.browser, |
| 805 | 805 | td.tktDescLabel { |
| 806 | 806 | vertical-align: top |
| 807 | +} | |
| 808 | +td.tktTlOpen { | |
| 809 | + color: #ffa0a0; | |
| 810 | +} | |
| 811 | +td.tktTlClosed { | |
| 812 | + color: #555; | |
| 807 | 813 | } |
| 808 | 814 | div.filetreeline { |
| 809 | 815 | display: table; |
| 810 | 816 | width: 100%; |
| 811 | 817 | white-space: nowrap |
| @@ -947,10 +953,38 @@ | ||
| 947 | 953 | border-radius: 5px; |
| 948 | 954 | border: solid #ff8000; |
| 949 | 955 | vertical-align: top; |
| 950 | 956 | text-align: left; |
| 951 | 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 | |
| 952 | 986 | } |
| 953 | 987 | .timelineCurrent > .timelineColumnarCell, |
| 954 | 988 | .timelineCurrent > .timelineCompactCell, |
| 955 | 989 | .timelineCurrent > .timelineDetailCell, |
| 956 | 990 | .timelineCurrent > .timelineModernCell, |
| 957 | 991 |
| --- 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 |
+4
-1
| --- skins/blitz/css.txt | ||
| +++ skins/blitz/css.txt | ||
| @@ -1060,16 +1060,19 @@ | ||
| 1060 | 1060 | background-color: #ffc; |
| 1061 | 1061 | border-bottom: 1px solid #ddd; |
| 1062 | 1062 | border-right: 1px solid #ddd; |
| 1063 | 1063 | } |
| 1064 | 1064 | |
| 1065 | -tr.timelineSelected { | |
| 1065 | +.timelineSelected { | |
| 1066 | 1066 | border-left: 2px solid orange; |
| 1067 | 1067 | background-color: #ffffe8; |
| 1068 | 1068 | border-bottom: 1px solid #ddd; |
| 1069 | 1069 | border-right: 1px solid #ddd; |
| 1070 | 1070 | } |
| 1071 | +.timelineSecondary { | |
| 1072 | + background-color: #e8ffff; | |
| 1073 | +} | |
| 1071 | 1074 | |
| 1072 | 1075 | tr.timelineCurrent td.timelineTableCell { |
| 1073 | 1076 | } |
| 1074 | 1077 | |
| 1075 | 1078 | tr.timelineBottom td { |
| 1076 | 1079 |
| --- 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 |
+1
-1
| --- skins/blitz/ticket.txt | ||
| +++ skins/blitz/ticket.txt | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | <h4>$<title></h4> |
| 2 | 2 | <table class="tktDsp"> |
| 3 | -<tr><td class="tktDspLabel">Ticket UUID</td> | |
| 3 | +<tr><td class="tktDspLabel">Ticket Hash</td> | |
| 4 | 4 | <th1> |
| 5 | 5 | if {[info exists tkt_uuid]} { |
| 6 | 6 | if {[hascap s]} { |
| 7 | 7 | html "<td class='tktDspValue' colspan='3'>$tkt_uuid " |
| 8 | 8 | html "($tkt_id)</td></tr>\n" |
| 9 | 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 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 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 |
+1
-1
| --- skins/blitz_no_logo/ticket.txt | ||
| +++ skins/blitz_no_logo/ticket.txt | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | <h4>$<title></h4> |
| 2 | 2 | <table class="tktDsp"> |
| 3 | -<tr><td class="tktDspLabel">Ticket UUID</td> | |
| 3 | +<tr><td class="tktDspLabel">Ticket Hash</td> | |
| 4 | 4 | <th1> |
| 5 | 5 | if {[info exists tkt_uuid]} { |
| 6 | 6 | if {[hascap s]} { |
| 7 | 7 | html "<td class='tktDspValue' colspan='3'>$tkt_uuid " |
| 8 | 8 | html "($tkt_id)</td></tr>\n" |
| 9 | 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 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 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 |
+2
-2
| --- skins/bootstrap/header.txt | ||
| +++ skins/bootstrap/header.txt | ||
| @@ -2,13 +2,13 @@ | ||
| 2 | 2 | <head> |
| 3 | 3 | <meta charset="utf-8"> |
| 4 | 4 | <base href="$baseurl/$current_page" /> |
| 5 | 5 | <title>$<project_name>: $<title></title> |
| 6 | 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'"/> | |
| 7 | + <meta http-equiv="Content-Security-Policy" content="$default_csp"/> | |
| 8 | 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" /> | |
| 9 | + <link rel="stylesheet" href="$stylesheet_url" type="text/css" media="screen" /> | |
| 10 | 10 | <script nonce="$<nonce>"> |
| 11 | 11 | function gebi(x){ |
| 12 | 12 | if(/^#/.test(x)) x = x.substr(1); |
| 13 | 13 | var e = document.getElementById(x); |
| 14 | 14 | if(!e) throw new Error("Expecting element with ID "+x); |
| 15 | 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-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 |
+1
-1
| --- skins/default/css.txt | ||
| +++ skins/default/css.txt | ||
| @@ -145,11 +145,11 @@ | ||
| 145 | 145 | border-radius: 5px; |
| 146 | 146 | } |
| 147 | 147 | .content blockquote { |
| 148 | 148 | padding: 0 15px; |
| 149 | 149 | } |
| 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 { | |
| 151 | 151 | background-color: rgba(65, 131, 196, 0.1); |
| 152 | 152 | border-left: 3px solid #254769; |
| 153 | 153 | padding: .1em 1em; |
| 154 | 154 | } |
| 155 | 155 | |
| 156 | 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 { |
| 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 |
+4
-3
| --- skins/default/header.txt | ||
| +++ skins/default/header.txt | ||
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | <div class="header"> |
| 2 | 2 | <div class="title"><h1>$<project_name></h1>$<title></div> |
| 3 | 3 | <div class="status"><th1> |
| 4 | 4 | if {[info exists login]} { |
| 5 | - html "$login — <a href='$home/login'>Logout</a>\n" | |
| 5 | + html "<a href='$home/login'>$login</a>\n" | |
| 6 | 6 | } else { |
| 7 | 7 | html "<a href='$home/login'>Login</a>\n" |
| 8 | 8 | } |
| 9 | 9 | </th1></div> |
| 10 | 10 | </div> |
| @@ -17,17 +17,18 @@ | ||
| 17 | 17 | html "<a href='$home$url' class='active $cls'>$name</a>\n" |
| 18 | 18 | } else { |
| 19 | 19 | html "<a href='$home$url' class='$cls'>$name</a>\n" |
| 20 | 20 | } |
| 21 | 21 | } |
| 22 | -html "<a id='hbbtn' href='#'>☰</a>" | |
| 22 | +html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>☰</a>" | |
| 23 | 23 | menulink $index_page Home {} |
| 24 | 24 | if {[anycap jor]} { |
| 25 | 25 | menulink /timeline Timeline {} |
| 26 | 26 | } |
| 27 | 27 | 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 | |
| 29 | 30 | } |
| 30 | 31 | if {[hascap o]} { |
| 31 | 32 | menulink /brlist Branches desktoponly |
| 32 | 33 | menulink /taglist Tags wideonly |
| 33 | 34 | } |
| 34 | 35 |
| --- 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='#'>☰</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'>☰</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 |
+12
-1
| --- skins/eagle/css.txt | ||
| +++ skins/eagle/css.txt | ||
| @@ -168,13 +168,16 @@ | ||
| 168 | 168 | font-family: "courier new"; |
| 169 | 169 | border-spacing: 0px 2px; |
| 170 | 170 | // border-collapse: collapse; |
| 171 | 171 | } |
| 172 | 172 | |
| 173 | -tr.timelineSelected { | |
| 173 | +.timelineSelected { | |
| 174 | 174 | background-color: #7EA2D9; |
| 175 | 175 | } |
| 176 | +.timelineSecondary { | |
| 177 | + background-color: #7EA27E; | |
| 178 | +} | |
| 176 | 179 | |
| 177 | 180 | /* commit node */ |
| 178 | 181 | .tl-node { |
| 179 | 182 | width: 10px; |
| 180 | 183 | height: 10px; |
| @@ -285,10 +288,18 @@ | ||
| 285 | 288 | td.tktDspValue { |
| 286 | 289 | text-align: left; |
| 287 | 290 | vertical-align: top; |
| 288 | 291 | background-color: #485D7B; |
| 289 | 292 | } |
| 293 | + | |
| 294 | +/* Ticket display on timelines */ | |
| 295 | +td.tktTlOpen { | |
| 296 | + color: #ffc0c0; | |
| 297 | +} | |
| 298 | +td.tktTlClose { | |
| 299 | + color: #c0c0c0; | |
| 300 | +} | |
| 290 | 301 | |
| 291 | 302 | /* format for example table cells on the report edit page */ |
| 292 | 303 | td.rpteditex { |
| 293 | 304 | border-width: thin; |
| 294 | 305 | border-color: white; |
| 295 | 306 |
| --- 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 |
+14
| --- skins/xekri/css.txt | ||
| +++ skins/xekri/css.txt | ||
| @@ -695,10 +695,15 @@ | ||
| 695 | 695 | td.tktDspValue { |
| 696 | 696 | background-color: #111; |
| 697 | 697 | text-align: left; |
| 698 | 698 | vertical-align: top; |
| 699 | 699 | } |
| 700 | + | |
| 701 | +/* Tickets on timelines */ | |
| 702 | +td.tktTlOpen { | |
| 703 | + color: #ffa0a0; | |
| 704 | +} | |
| 700 | 705 | |
| 701 | 706 | /* format for ticket error messages */ |
| 702 | 707 | span.tktError { |
| 703 | 708 | color: #f00; |
| 704 | 709 | font-weight: bold; |
| @@ -803,10 +808,12 @@ | ||
| 803 | 808 | .timelineTable .timelineSelected { |
| 804 | 809 | background: #222; |
| 805 | 810 | border: 0; |
| 806 | 811 | box-shadow: none; |
| 807 | 812 | } |
| 813 | +.timelineSelected {} | |
| 814 | +.timelineSecondary {} | |
| 808 | 815 | |
| 809 | 816 | .timelineTable .timelineSelected .timelineTime { |
| 810 | 817 | background: #333; |
| 811 | 818 | border-radius: 1rem 0 0 1rem; |
| 812 | 819 | box-shadow: 2px 2px 1px #000; |
| @@ -823,10 +830,17 @@ | ||
| 823 | 830 | .timelineTable .timelineSelected .timelineDetailCell { |
| 824 | 831 | background: #333; |
| 825 | 832 | border-radius: 0 1rem 1rem 0; |
| 826 | 833 | box-shadow: 2px 2px 1px #000; |
| 827 | 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 | +} | |
| 828 | 842 | |
| 829 | 843 | .timelineTable .timelineModernCell .timelineModernComment , |
| 830 | 844 | .timelineTable .timelineModernCell .timelineModernDetail , |
| 831 | 845 | .timelineTable .timelineCompactCell .timelineCompactComment , |
| 832 | 846 | .timelineTable .timelineCompactCell .timelineCompactDetail , |
| 833 | 847 | |
| 834 | 848 | 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 |
+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 |
| --- 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 @@ | ||
| 240 | 240 | } |
| 241 | 241 | db_finalize(&loop); |
| 242 | 242 | blob_reset(&repoName); |
| 243 | 243 | return nAdd; |
| 244 | 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 | + | |
| 245 | 336 | |
| 246 | 337 | /* |
| 247 | 338 | ** COMMAND: add |
| 248 | 339 | ** |
| 249 | 340 | ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...? |
| @@ -269,17 +360,26 @@ | ||
| 269 | 360 | ** be treated case sensitive or not. If the option is not given, the default |
| 270 | 361 | ** depends on the global setting, or the operating system default, if not set. |
| 271 | 362 | ** |
| 272 | 363 | ** Options: |
| 273 | 364 | ** |
| 274 | -** --case-sensitive <BOOL> Override the case-sensitive setting. | |
| 365 | +** --case-sensitive BOOL Override the case-sensitive setting. | |
| 275 | 366 | ** --dotfiles include files beginning with a dot (".") |
| 276 | 367 | ** -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. | |
| 281 | 381 | ** |
| 282 | 382 | ** See also: addremove, rm |
| 283 | 383 | */ |
| 284 | 384 | void add_cmd(void){ |
| 285 | 385 | int i; /* Loop counter */ |
| @@ -288,10 +388,19 @@ | ||
| 288 | 388 | const char *zCleanFlag; /* The --clean option or clean-glob setting */ |
| 289 | 389 | const char *zIgnoreFlag; /* The --ignore option or ignore-glob setting */ |
| 290 | 390 | Glob *pIgnore, *pClean; /* Ignore everything matching the glob patterns */ |
| 291 | 391 | unsigned scanFlags = 0; /* Flags passed to vfile_scan() */ |
| 292 | 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 | + } | |
| 293 | 402 | |
| 294 | 403 | zCleanFlag = find_option("clean",0,1); |
| 295 | 404 | zIgnoreFlag = find_option("ignore",0,1); |
| 296 | 405 | forceFlag = find_option("force","f",0)!=0; |
| 297 | 406 | if( find_option("dotfiles",0,0)!=0 ) scanFlags |= SCAN_ALL; |
| @@ -339,10 +448,11 @@ | ||
| 339 | 448 | Blob ans; |
| 340 | 449 | char cReply; |
| 341 | 450 | char *prompt = mprintf("file \"%s\" matches \"ignore-glob\". " |
| 342 | 451 | "Add it (a=all/y/N)? ", zTreeName); |
| 343 | 452 | prompt_user(prompt, &ans); |
| 453 | + fossil_free(prompt); | |
| 344 | 454 | cReply = blob_str(&ans)[0]; |
| 345 | 455 | blob_reset(&ans); |
| 346 | 456 | if( cReply=='a' || cReply=='A' ){ |
| 347 | 457 | forceFlag = 1; |
| 348 | 458 | }else if( cReply!='y' && cReply!='Y' ){ |
| @@ -441,22 +551,36 @@ | ||
| 441 | 551 | ** --soft Skip removing files from the checkout. |
| 442 | 552 | ** This supersedes the --hard option. |
| 443 | 553 | ** --hard Remove files from the checkout. |
| 444 | 554 | ** --case-sensitive <BOOL> Override the case-sensitive setting. |
| 445 | 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. | |
| 446 | 563 | ** |
| 447 | 564 | ** See also: addremove, add |
| 448 | 565 | */ |
| 449 | 566 | void delete_cmd(void){ |
| 450 | 567 | int i; |
| 451 | 568 | int removeFiles; |
| 452 | - int dryRunFlag; | |
| 569 | + int dryRunFlag = find_option("dry-run","n",0)!=0; | |
| 453 | 570 | int softFlag; |
| 454 | 571 | int hardFlag; |
| 455 | 572 | Stmt loop; |
| 456 | 573 | |
| 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 | + | |
| 458 | 582 | softFlag = find_option("soft",0,0)!=0; |
| 459 | 583 | hardFlag = find_option("hard",0,0)!=0; |
| 460 | 584 | |
| 461 | 585 | /* We should be done with options.. */ |
| 462 | 586 | verify_all_options(); |
| @@ -615,25 +739,33 @@ | ||
| 615 | 739 | ** anything. |
| 616 | 740 | ** |
| 617 | 741 | ** This command can be used to track third party software. |
| 618 | 742 | ** |
| 619 | 743 | ** Options: |
| 620 | -** --case-sensitive <BOOL> Override the case-sensitive setting. | |
| 744 | +** --case-sensitive BOOL Override the case-sensitive setting. | |
| 621 | 745 | ** --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 | |
| 626 | 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. | |
| 627 | 759 | ** |
| 628 | 760 | ** See also: add, rm |
| 629 | 761 | */ |
| 630 | 762 | void addremove_cmd(void){ |
| 631 | 763 | 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; | |
| 635 | 767 | int dryRunFlag = find_option("dry-run","n",0)!=0; |
| 636 | 768 | int n; |
| 637 | 769 | Stmt q; |
| 638 | 770 | int vid; |
| 639 | 771 | int nAdd = 0; |
| @@ -642,10 +774,23 @@ | ||
| 642 | 774 | |
| 643 | 775 | if( !dryRunFlag ){ |
| 644 | 776 | dryRunFlag = find_option("test",0,0)!=0; /* deprecated */ |
| 645 | 777 | } |
| 646 | 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 | + | |
| 647 | 792 | /* We should be done with options.. */ |
| 648 | 793 | verify_all_options(); |
| 649 | 794 | |
| 650 | 795 | /* Fail if unprocessed arguments are present, in case user expect the |
| 651 | 796 | ** addremove command to accept a list of file or directory. |
| @@ -709,11 +854,10 @@ | ||
| 709 | 854 | /* show command summary */ |
| 710 | 855 | fossil_print("added %d files, deleted %d files\n", nAdd, nDelete); |
| 711 | 856 | |
| 712 | 857 | db_end_transaction(dryRunFlag); |
| 713 | 858 | } |
| 714 | - | |
| 715 | 859 | |
| 716 | 860 | /* |
| 717 | 861 | ** Rename a single file. |
| 718 | 862 | ** |
| 719 | 863 | ** The original name of the file is zOrig. The new filename is zNew. |
| @@ -935,11 +1079,11 @@ | ||
| 935 | 1079 | zOrig = blob_str(&orig); |
| 936 | 1080 | nOrig = blob_size(&orig); |
| 937 | 1081 | db_prepare(&q, |
| 938 | 1082 | "SELECT pathname FROM vfile" |
| 939 | 1083 | " 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))" | |
| 941 | 1085 | " ORDER BY 1", |
| 942 | 1086 | vid, zOrig, filename_collation(), zOrig, filename_collation(), |
| 943 | 1087 | zOrig, filename_collation() |
| 944 | 1088 | ); |
| 945 | 1089 | while( db_step(&q)==SQLITE_ROW ){ |
| @@ -946,11 +1090,11 @@ | ||
| 946 | 1090 | const char *zPath = db_column_text(&q, 0); |
| 947 | 1091 | int nPath = db_column_bytes(&q, 0); |
| 948 | 1092 | const char *zTail; |
| 949 | 1093 | if( nPath==nOrig ){ |
| 950 | 1094 | zTail = file_tail(zPath); |
| 951 | - }else if( destType==1 ){ | |
| 1095 | + }else if( origType!=0 && destType==1 ){ | |
| 952 | 1096 | zTail = &zPath[nOrig-strlen(file_tail(zOrig))]; |
| 953 | 1097 | }else{ |
| 954 | 1098 | zTail = &zPath[nOrig+1]; |
| 955 | 1099 | } |
| 956 | 1100 | db_multi_exec( |
| @@ -967,10 +1111,11 @@ | ||
| 967 | 1111 | const char *zTo = db_column_text(&q, 1); |
| 968 | 1112 | mv_one_file(vid, zFrom, zTo, dryRunFlag); |
| 969 | 1113 | if( moveFiles ) add_file_to_move(zFrom, zTo); |
| 970 | 1114 | } |
| 971 | 1115 | db_finalize(&q); |
| 1116 | + undo_reset(); | |
| 972 | 1117 | db_end_transaction(0); |
| 973 | 1118 | if( moveFiles ) process_files_to_move(dryRunFlag); |
| 974 | 1119 | } |
| 975 | 1120 | |
| 976 | 1121 | /* |
| 977 | 1122 | |
| 978 | 1123 | 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 @@ | ||
| 46 | 46 | @ -- |
| 47 | 47 | @ -- The ssub field is a string where each character indicates a particular |
| 48 | 48 | @ -- type of event to subscribe to. Choices: |
| 49 | 49 | @ -- a - Announcements |
| 50 | 50 | @ -- c - Check-ins |
| 51 | +@ -- f - Forum posts | |
| 51 | 52 | @ -- t - Ticket changes |
| 52 | 53 | @ -- w - Wiki changes |
| 53 | 54 | @ -- Probably different codes will be added in the future. In the future |
| 54 | 55 | @ -- we might also add a separate table that allows subscribing to email |
| 55 | 56 | @ -- notifications for specific branches or tags or tickets. |
| @@ -110,15 +111,15 @@ | ||
| 110 | 111 | ** if the email-send-method is something other than "off". |
| 111 | 112 | */ |
| 112 | 113 | void alert_schema(int bOnlyIfEnabled){ |
| 113 | 114 | if( !alert_tables_exist() ){ |
| 114 | 115 | if( bOnlyIfEnabled |
| 115 | - && fossil_strcmp(db_get("email-send-method","off"),"off")==0 | |
| 116 | + && fossil_strcmp(db_get("email-send-method",0),"off")==0 | |
| 116 | 117 | ){ |
| 117 | 118 | return; /* Don't create table for disabled email */ |
| 118 | 119 | } |
| 119 | - db_multi_exec(zAlertInit/*works-like:""*/); | |
| 120 | + db_exec_sql(zAlertInit); | |
| 120 | 121 | alert_triggers_enable(); |
| 121 | 122 | }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ |
| 122 | 123 | db_multi_exec( |
| 123 | 124 | "ALTER TABLE repository.pending_alert" |
| 124 | 125 | " ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
| @@ -158,11 +159,11 @@ | ||
| 158 | 159 | /* |
| 159 | 160 | ** Return true if email alerts are active. |
| 160 | 161 | */ |
| 161 | 162 | int alert_enabled(void){ |
| 162 | 163 | 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; | |
| 164 | 165 | return 1; |
| 165 | 166 | } |
| 166 | 167 | |
| 167 | 168 | /* |
| 168 | 169 | ** If the subscriber table does not exist, then paint an error message |
| @@ -183,11 +184,11 @@ | ||
| 183 | 184 | ** is an administrator. |
| 184 | 185 | */ |
| 185 | 186 | void alert_submenu_common(void){ |
| 186 | 187 | if( g.perm.Admin ){ |
| 187 | 188 | if( fossil_strcmp(g.zPath,"subscribers") ){ |
| 188 | - style_submenu_element("List Subscribers","%R/subscribers"); | |
| 189 | + style_submenu_element("Subscribers","%R/subscribers"); | |
| 189 | 190 | } |
| 190 | 191 | if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 191 | 192 | style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 192 | 193 | } |
| 193 | 194 | } |
| @@ -238,10 +239,17 @@ | ||
| 238 | 239 | @ This is URL used as the basename for hyperlinks included in |
| 239 | 240 | @ email alert text. Omit the trailing "/". |
| 240 | 241 | @ Suggested value: "%h(g.zBaseURL)" |
| 241 | 242 | @ (Property: "email-url")</p> |
| 242 | 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> | |
| 243 | 251 | |
| 244 | 252 | entry_attribute("\"Return-Path\" email address", 20, "email-self", |
| 245 | 253 | "eself", "", 0); |
| 246 | 254 | @ <p><b>Required.</b> |
| 247 | 255 | @ This is the email to which email notification bounces should be sent. |
| @@ -297,17 +305,10 @@ | ||
| 297 | 305 | @ append a colon and TCP port number (ex: smtp.example.com:587). |
| 298 | 306 | @ The default TCP port number is 25. |
| 299 | 307 | @ (Property: "email-send-relayhost")</p> |
| 300 | 308 | @ <hr> |
| 301 | 309 | |
| 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 | 310 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 310 | 311 | @ </div></form> |
| 311 | 312 | db_end_transaction(0); |
| 312 | 313 | style_footer(); |
| 313 | 314 | } |
| @@ -482,11 +483,11 @@ | ||
| 482 | 483 | blob_init(&p->out, 0, 0); |
| 483 | 484 | p->mFlags = mFlags; |
| 484 | 485 | if( zAltDest ){ |
| 485 | 486 | p->zDest = zAltDest; |
| 486 | 487 | }else{ |
| 487 | - p->zDest = db_get("email-send-method","off"); | |
| 488 | + p->zDest = db_get("email-send-method",0); | |
| 488 | 489 | } |
| 489 | 490 | if( fossil_strcmp(p->zDest,"off")==0 ) return p; |
| 490 | 491 | if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p; |
| 491 | 492 | if( fossil_strcmp(p->zDest,"db")==0 ){ |
| 492 | 493 | char *zErr; |
| @@ -574,22 +575,18 @@ | ||
| 574 | 575 | } |
| 575 | 576 | return 0; |
| 576 | 577 | } |
| 577 | 578 | |
| 578 | 579 | /* |
| 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){ | |
| 591 | 588 | int i; |
| 592 | 589 | int nAt = 0; |
| 593 | 590 | int nDot = 0; |
| 594 | 591 | char c; |
| 595 | 592 | if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ |
| @@ -619,13 +616,28 @@ | ||
| 619 | 616 | } |
| 620 | 617 | } |
| 621 | 618 | if( c!=cTerm ) return 0; /* Missing terminator */ |
| 622 | 619 | if( nAt==0 ) return 0; /* No "@" found anywhere */ |
| 623 | 620 | if( nDot==0 ) return 0; /* No "." in the domain */ |
| 621 | + return i; | |
| 622 | +} | |
| 624 | 623 | |
| 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); | |
| 627 | 639 | } |
| 628 | 640 | |
| 629 | 641 | /* |
| 630 | 642 | ** Scan the input string for a valid email address enclosed in <...> |
| 631 | 643 | ** If the string contains one or more email addresses, extract the first |
| @@ -659,10 +671,38 @@ | ||
| 659 | 671 | char *zOut = alert_find_emailaddr(zIn); |
| 660 | 672 | if( zOut ){ |
| 661 | 673 | sqlite3_result_text(context, zOut, -1, fossil_free); |
| 662 | 674 | } |
| 663 | 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 | +} | |
| 664 | 704 | |
| 665 | 705 | /* |
| 666 | 706 | ** Return the hostname portion of an email address - the part following |
| 667 | 707 | ** the @ |
| 668 | 708 | */ |
| @@ -762,11 +802,11 @@ | ||
| 762 | 802 | ** Date: |
| 763 | 803 | ** Message-Id: |
| 764 | 804 | ** Content-Type: |
| 765 | 805 | ** Content-Transfer-Encoding: |
| 766 | 806 | ** MIME-Version: |
| 767 | -** X-Fossil-From: | |
| 807 | +** Sender: | |
| 768 | 808 | ** |
| 769 | 809 | ** The caller maintains ownership of the input Blobs. This routine will |
| 770 | 810 | ** read the Blobs and send them onward to the email system, but it will |
| 771 | 811 | ** not free them. |
| 772 | 812 | ** |
| @@ -774,14 +814,14 @@ | ||
| 774 | 814 | ** in the pHdr parameter. |
| 775 | 815 | ** |
| 776 | 816 | ** If the zFromName argument is not NULL, then it should be a human-readable |
| 777 | 817 | ** name or handle for the sender. In that case, "From:" becomes a made-up |
| 778 | 818 | ** 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 | |
| 781 | 821 | ** 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 | |
| 783 | 823 | ** omitted. |
| 784 | 824 | */ |
| 785 | 825 | void alert_send( |
| 786 | 826 | AlertSender *p, /* Emailer context */ |
| 787 | 827 | Blob *pHdr, /* Email header (incomplete) */ |
| @@ -794,26 +834,26 @@ | ||
| 794 | 834 | fossil_print("Sending email\n"); |
| 795 | 835 | } |
| 796 | 836 | if( fossil_strcmp(p->zDest, "off")==0 ){ |
| 797 | 837 | return; |
| 798 | 838 | } |
| 839 | + blob_init(&all, 0, 0); | |
| 799 | 840 | if( fossil_strcmp(p->zDest, "blob")==0 ){ |
| 800 | 841 | pOut = &p->out; |
| 801 | 842 | if( blob_size(pOut) ){ |
| 802 | 843 | blob_appendf(pOut, "%.72c\n", '='); |
| 803 | 844 | } |
| 804 | 845 | }else{ |
| 805 | - blob_init(&all, 0, 0); | |
| 806 | 846 | pOut = &all; |
| 807 | 847 | } |
| 808 | 848 | blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); |
| 809 | 849 | if( p->zFrom==0 || p->zFrom[0]==0 ){ |
| 810 | 850 | return; /* email-self is not set. Error will be reported separately */ |
| 811 | 851 | }else if( zFromName ){ |
| 812 | 852 | blob_appendf(pOut, "From: %s <%s@%s>\r\n", |
| 813 | 853 | 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); | |
| 815 | 855 | }else{ |
| 816 | 856 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 817 | 857 | } |
| 818 | 858 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 819 | 859 | if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
| @@ -879,10 +919,26 @@ | ||
| 879 | 919 | fossil_print("%s", blob_str(&all)); |
| 880 | 920 | } |
| 881 | 921 | blob_reset(&all); |
| 882 | 922 | } |
| 883 | 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 | +*/ | |
| 884 | 940 | /* |
| 885 | 941 | ** SETTING: email-send-method width=5 default=off |
| 886 | 942 | ** Determine the method used to send email. Allowed values are |
| 887 | 943 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 888 | 944 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -1152,10 +1208,19 @@ | ||
| 1152 | 1208 | int i, j, n; |
| 1153 | 1209 | char c; |
| 1154 | 1210 | |
| 1155 | 1211 | *peErr = 0; |
| 1156 | 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 | + } | |
| 1157 | 1222 | |
| 1158 | 1223 | /* Check the validity of the email address. |
| 1159 | 1224 | ** |
| 1160 | 1225 | ** (1) Exactly one '@' character. |
| 1161 | 1226 | ** (2) No other characters besides [a-zA-Z0-9._+-] |
| @@ -1163,11 +1228,15 @@ | ||
| 1163 | 1228 | ** The local part is currently more restrictive than RFC 5322 allows: |
| 1164 | 1229 | ** https://stackoverflow.com/a/2049510/142454 We will expand this as |
| 1165 | 1230 | ** necessary. |
| 1166 | 1231 | */ |
| 1167 | 1232 | zEAddr = P("e"); |
| 1168 | - if( zEAddr==0 ) return 0; | |
| 1233 | + if( zEAddr==0 ){ | |
| 1234 | + *peErr = 1; | |
| 1235 | + *pzErr = mprintf("required"); | |
| 1236 | + return 0; | |
| 1237 | + } | |
| 1169 | 1238 | for(i=j=n=0; (c = zEAddr[i])!=0; i++){ |
| 1170 | 1239 | if( c=='@' ){ |
| 1171 | 1240 | n = i; |
| 1172 | 1241 | j++; |
| 1173 | 1242 | continue; |
| @@ -1193,14 +1262,13 @@ | ||
| 1193 | 1262 | *peErr = 1; |
| 1194 | 1263 | *pzErr = mprintf("email domain too short"); |
| 1195 | 1264 | return 0; |
| 1196 | 1265 | } |
| 1197 | 1266 | |
| 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"); | |
| 1202 | 1270 | return 0; |
| 1203 | 1271 | } |
| 1204 | 1272 | |
| 1205 | 1273 | /* Check to make sure the email address is available for reuse */ |
| 1206 | 1274 | if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){ |
| @@ -1259,11 +1327,11 @@ | ||
| 1259 | 1327 | ** page. To allow anonymous passers-by to sign up for email |
| 1260 | 1328 | ** notification, set Email-Alerts on user "nobody" or "anonymous". |
| 1261 | 1329 | */ |
| 1262 | 1330 | void subscribe_page(void){ |
| 1263 | 1331 | int needCaptcha; |
| 1264 | - unsigned int uSeed; | |
| 1332 | + unsigned int uSeed = 0; | |
| 1265 | 1333 | const char *zDecoded; |
| 1266 | 1334 | char *zCaptcha = 0; |
| 1267 | 1335 | char *zErr = 0; |
| 1268 | 1336 | int eErr = 0; |
| 1269 | 1337 | int di; |
| @@ -1291,10 +1359,14 @@ | ||
| 1291 | 1359 | /* Everybody else jumps to the page to administer their own |
| 1292 | 1360 | ** account only. */ |
| 1293 | 1361 | cgi_redirectf("%R/alerts"); |
| 1294 | 1362 | return; |
| 1295 | 1363 | } |
| 1364 | + } | |
| 1365 | + if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){ | |
| 1366 | + register_page(); | |
| 1367 | + return; | |
| 1296 | 1368 | } |
| 1297 | 1369 | alert_submenu_common(); |
| 1298 | 1370 | needCaptcha = !login_is_individual(); |
| 1299 | 1371 | if( P("submit") |
| 1300 | 1372 | && cgi_csrf_safe(1) |
| @@ -1312,10 +1384,11 @@ | ||
| 1312 | 1384 | if( PB("sa") ) ssub[nsub++] = 'a'; |
| 1313 | 1385 | if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c'; |
| 1314 | 1386 | if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f'; |
| 1315 | 1387 | if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't'; |
| 1316 | 1388 | if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w'; |
| 1389 | + if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x'; | |
| 1317 | 1390 | ssub[nsub] = 0; |
| 1318 | 1391 | db_multi_exec( |
| 1319 | 1392 | "INSERT INTO subscriber(semail,suname," |
| 1320 | 1393 | " sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)" |
| 1321 | 1394 | "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)", |
| @@ -1332,11 +1405,15 @@ | ||
| 1332 | 1405 | id); |
| 1333 | 1406 | if( !needCaptcha ){ |
| 1334 | 1407 | /* The new subscription has been added on behalf of a logged-in user. |
| 1335 | 1408 | ** No verification is required. Jump immediately to /alerts page. |
| 1336 | 1409 | */ |
| 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 | + } | |
| 1338 | 1415 | return; |
| 1339 | 1416 | }else{ |
| 1340 | 1417 | /* We need to send a verification email */ |
| 1341 | 1418 | Blob hdr, body; |
| 1342 | 1419 | AlertSender *pSender = alert_sender_new(0,0); |
| @@ -1354,11 +1431,11 @@ | ||
| 1354 | 1431 | @ <blockquote><pre> |
| 1355 | 1432 | @ %h(pSender->zErr) |
| 1356 | 1433 | @ </pre></blockquote> |
| 1357 | 1434 | }else{ |
| 1358 | 1435 | @ <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 | |
| 1360 | 1437 | @ subscription.</p> |
| 1361 | 1438 | } |
| 1362 | 1439 | alert_sender_free(pSender); |
| 1363 | 1440 | style_footer(); |
| 1364 | 1441 | } |
| @@ -1386,16 +1463,23 @@ | ||
| 1386 | 1463 | if( eErr==1 ){ |
| 1387 | 1464 | @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
| 1388 | 1465 | } |
| 1389 | 1466 | @ </tr> |
| 1390 | 1467 | 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 | + } | |
| 1392 | 1475 | zDecoded = captcha_decode(uSeed); |
| 1393 | 1476 | zCaptcha = captcha_render(zDecoded); |
| 1394 | 1477 | @ <tr> |
| 1395 | 1478 | @ <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"); | |
| 1397 | 1481 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 1398 | 1482 | @ </tr> |
| 1399 | 1483 | if( eErr==2 ){ |
| 1400 | 1484 | @ <tr><td><td><span class='loginError'>↑ %h(zErr)</span></td></tr> |
| 1401 | 1485 | } |
| @@ -1421,10 +1505,12 @@ | ||
| 1421 | 1505 | @ Check-ins</label><br> |
| 1422 | 1506 | } |
| 1423 | 1507 | if( g.perm.RdForum ){ |
| 1424 | 1508 | @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \ |
| 1425 | 1509 | @ Forum Posts</label><br> |
| 1510 | + @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \ | |
| 1511 | + @ Forum Edits</label><br> | |
| 1426 | 1512 | } |
| 1427 | 1513 | if( g.perm.RdTkt ){ |
| 1428 | 1514 | @ <label><input type="checkbox" name="st" %s(PCK("st"))> \ |
| 1429 | 1515 | @ Ticket changes</label><br> |
| 1430 | 1516 | } |
| @@ -1461,11 +1547,11 @@ | ||
| 1461 | 1547 | @ </table> |
| 1462 | 1548 | if( needCaptcha ){ |
| 1463 | 1549 | @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 1464 | 1550 | @ %h(zCaptcha) |
| 1465 | 1551 | @ </pre> |
| 1466 | - @ Enter the 8 characters above in the "Security Code" box | |
| 1552 | + @ Enter the 8 characters above in the "Security Code" box<br/> | |
| 1467 | 1553 | @ </td></tr></table></div> |
| 1468 | 1554 | } |
| 1469 | 1555 | @ </form> |
| 1470 | 1556 | fossil_free(zErr); |
| 1471 | 1557 | style_footer(); |
| @@ -1474,21 +1560,20 @@ | ||
| 1474 | 1560 | /* |
| 1475 | 1561 | ** Either shutdown or completely delete a subscription entry given |
| 1476 | 1562 | ** by the hex value zName. Then paint a webpage that explains that |
| 1477 | 1563 | ** the entry has been removed. |
| 1478 | 1564 | */ |
| 1479 | -static void alert_unsubscribe(const char *zName){ | |
| 1565 | +static void alert_unsubscribe(int sid){ | |
| 1480 | 1566 | char *zEmail; |
| 1481 | 1567 | zEmail = db_text(0, "SELECT semail FROM subscriber" |
| 1482 | - " WHERE subscriberCode=hextoblob(%Q)", zName); | |
| 1568 | + " WHERE subscriberId=%d", sid); | |
| 1483 | 1569 | if( zEmail==0 ){ |
| 1484 | 1570 | style_header("Unsubscribe Fail"); |
| 1485 | 1571 | @ <p>Unable to locate a subscriber with the requested key</p> |
| 1486 | 1572 | }else{ |
| 1487 | 1573 | db_multi_exec( |
| 1488 | - "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)", | |
| 1489 | - zName | |
| 1574 | + "DELETE FROM subscriber WHERE subscriberId=%d", sid | |
| 1490 | 1575 | ); |
| 1491 | 1576 | style_header("Unsubscribed"); |
| 1492 | 1577 | @ <p>The "%h(zEmail)" email address has been delisted. |
| 1493 | 1578 | @ All traces of that email address have been removed</p> |
| 1494 | 1579 | } |
| @@ -1499,108 +1584,149 @@ | ||
| 1499 | 1584 | /* |
| 1500 | 1585 | ** WEBPAGE: alerts |
| 1501 | 1586 | ** |
| 1502 | 1587 | ** Edit email alert and notification settings. |
| 1503 | 1588 | ** |
| 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); | |
| 1591 | 1715 | } |
| 1592 | 1716 | if( P("delete")!=0 && cgi_csrf_safe(1) ){ |
| 1593 | 1717 | if( !PB("dodelete") ){ |
| 1594 | 1718 | eErr = 9; |
| 1595 | 1719 | zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to" |
| 1596 | 1720 | " unsubscribe"); |
| 1597 | 1721 | }else{ |
| 1598 | - alert_unsubscribe(zName); | |
| 1599 | - return; | |
| 1722 | + alert_unsubscribe(sid); | |
| 1723 | + db_commit_transaction(); | |
| 1724 | + return; | |
| 1600 | 1725 | } |
| 1601 | 1726 | } |
| 1727 | + style_header("Update Subscription"); | |
| 1602 | 1728 | db_prepare(&q, |
| 1603 | 1729 | "SELECT" |
| 1604 | 1730 | " semail," /* 0 */ |
| 1605 | 1731 | " sverified," /* 1 */ |
| 1606 | 1732 | " sdonotcall," /* 2 */ |
| @@ -1607,52 +1733,92 @@ | ||
| 1607 | 1733 | " sdigest," /* 3 */ |
| 1608 | 1734 | " ssub," /* 4 */ |
| 1609 | 1735 | " smip," /* 5 */ |
| 1610 | 1736 | " suname," /* 6 */ |
| 1611 | 1737 | " 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); | |
| 1614 | 1741 | if( db_step(&q)!=SQLITE_ROW ){ |
| 1615 | 1742 | db_finalize(&q); |
| 1743 | + db_commit_transaction(); | |
| 1616 | 1744 | 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 | + } | |
| 1625 | 1757 | sa = strchr(ssub,'a')!=0; |
| 1626 | 1758 | sc = strchr(ssub,'c')!=0; |
| 1627 | 1759 | sf = strchr(ssub,'f')!=0; |
| 1628 | 1760 | st = strchr(ssub,'t')!=0; |
| 1629 | 1761 | sw = strchr(ssub,'w')!=0; |
| 1762 | + sx = strchr(ssub,'x')!=0; | |
| 1630 | 1763 | smip = db_column_text(&q, 5); |
| 1631 | - suname = db_column_text(&q, 6); | |
| 1632 | 1764 | mtime = db_column_text(&q, 7); |
| 1633 | 1765 | sctime = db_column_text(&q, 8); |
| 1634 | 1766 | 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 | + } | |
| 1642 | 1794 | }else{ |
| 1643 | 1795 | @ <p>Make changes to the email subscription shown below and |
| 1644 | 1796 | @ press "Submit".</p> |
| 1645 | 1797 | } |
| 1646 | 1798 | 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)"> | |
| 1648 | 1802 | @ <table class="subscribe"> |
| 1649 | 1803 | @ <tr> |
| 1650 | 1804 | @ <td class="form_label">Email 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'>← not a valid email address!</span> | |
| 1809 | + }else if( g.perm.Admin ){ | |
| 1810 | + @ <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 | + } | |
| 1652 | 1817 | @ </tr> |
| 1653 | 1818 | if( g.perm.Admin ){ |
| 1819 | + int uid; | |
| 1654 | 1820 | @ <tr> |
| 1655 | 1821 | @ <td class='form_label'>Created:</td> |
| 1656 | 1822 | @ <td>%h(sctime)</td> |
| 1657 | 1823 | @ </tr> |
| 1658 | 1824 | @ <tr> |
| @@ -1662,13 +1828,21 @@ | ||
| 1662 | 1828 | @ <tr> |
| 1663 | 1829 | @ <td class='form_label'>IP Address:</td> |
| 1664 | 1830 | @ <td>%h(smip)</td> |
| 1665 | 1831 | @ </tr> |
| 1666 | 1832 | @ <tr> |
| 1833 | + @ <td class='form_label'>Subscriber Code:</td> | |
| 1834 | + @ <td>%h(db_column_text(&q,9))</td> | |
| 1835 | + @ <tr> | |
| 1667 | 1836 | @ <td class="form_label">User:</td> |
| 1668 | 1837 | @ <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 | + @ <a href='%R/setup_uedit?id=%d(uid)'>\ | |
| 1842 | + @ (login info for %h(suname))</a>\ | |
| 1843 | + } | |
| 1670 | 1844 | @ </tr> |
| 1671 | 1845 | } |
| 1672 | 1846 | @ <tr> |
| 1673 | 1847 | @ <td class="form_label">Topics:</td> |
| 1674 | 1848 | @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\ |
| @@ -1678,10 +1852,12 @@ | ||
| 1678 | 1852 | @ Check-ins</label><br> |
| 1679 | 1853 | } |
| 1680 | 1854 | if( g.perm.RdForum ){ |
| 1681 | 1855 | @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\ |
| 1682 | 1856 | @ Forum Posts</label><br> |
| 1857 | + @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\ | |
| 1858 | + @ Forum Edits</label><br> | |
| 1683 | 1859 | } |
| 1684 | 1860 | if( g.perm.RdTkt ){ |
| 1685 | 1861 | @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\ |
| 1686 | 1862 | @ Ticket changes</label><br> |
| 1687 | 1863 | } |
| @@ -1695,19 +1871,15 @@ | ||
| 1695 | 1871 | @ <td><select size="1" name="sdigest"> |
| 1696 | 1872 | @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option> |
| 1697 | 1873 | @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option> |
| 1698 | 1874 | @ </select></td> |
| 1699 | 1875 | @ </tr> |
| 1700 | -#if 0 | |
| 1701 | - @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\ | |
| 1702 | - @ Daily digest only</label><br> | |
| 1703 | -#endif | |
| 1704 | 1876 | if( g.perm.Admin ){ |
| 1705 | 1877 | @ <tr> |
| 1706 | 1878 | @ <td class="form_label">Admin Options:</td><td> |
| 1707 | 1879 | @ <label><input type="checkbox" name="sdonotcall" \ |
| 1708 | - @ %s(sdonotcall?"checked":"")> Do not call</label><br> | |
| 1880 | + @ %s(sdonotcall?"checked":"")> Do not disturb</label><br> | |
| 1709 | 1881 | @ <label><input type="checkbox" name="sverified" \ |
| 1710 | 1882 | @ %s(sverified?"checked":"")>\ |
| 1711 | 1883 | @ Verified</label></td></tr> |
| 1712 | 1884 | } |
| 1713 | 1885 | if( eErr==9 ){ |
| @@ -1726,10 +1898,12 @@ | ||
| 1726 | 1898 | @ </table> |
| 1727 | 1899 | @ </form> |
| 1728 | 1900 | fossil_free(zErr); |
| 1729 | 1901 | db_finalize(&q); |
| 1730 | 1902 | style_footer(); |
| 1903 | + db_commit_transaction(); | |
| 1904 | + return; | |
| 1731 | 1905 | } |
| 1732 | 1906 | |
| 1733 | 1907 | /* This is the message that gets sent to describe how to change |
| 1734 | 1908 | ** or modify a subscription |
| 1735 | 1909 | */ |
| @@ -1760,25 +1934,26 @@ | ||
| 1760 | 1934 | */ |
| 1761 | 1935 | void unsubscribe_page(void){ |
| 1762 | 1936 | const char *zName = P("name"); |
| 1763 | 1937 | char *zErr = 0; |
| 1764 | 1938 | int eErr = 0; |
| 1765 | - unsigned int uSeed; | |
| 1939 | + unsigned int uSeed = 0; | |
| 1766 | 1940 | const char *zDecoded; |
| 1767 | 1941 | char *zCaptcha = 0; |
| 1768 | 1942 | int dx; |
| 1769 | 1943 | int bSubmit; |
| 1770 | 1944 | const char *zEAddr; |
| 1771 | 1945 | char *zCode = 0; |
| 1946 | + int sid = 0; | |
| 1772 | 1947 | |
| 1773 | 1948 | /* If a valid subscriber code is supplied, then unsubscribe immediately. |
| 1774 | 1949 | */ |
| 1775 | 1950 | 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 | |
| 1778 | 1953 | ){ |
| 1779 | - alert_unsubscribe(zName); | |
| 1954 | + alert_unsubscribe(sid); | |
| 1780 | 1955 | return; |
| 1781 | 1956 | } |
| 1782 | 1957 | |
| 1783 | 1958 | /* Logged in users are redirected to the /alerts page */ |
| 1784 | 1959 | login_check_credentials(); |
| @@ -1855,10 +2030,11 @@ | ||
| 1855 | 2030 | zDecoded = captcha_decode(uSeed); |
| 1856 | 2031 | zCaptcha = captcha_render(zDecoded); |
| 1857 | 2032 | @ <tr> |
| 1858 | 2033 | @ <td class="form_label">Security Code:</td> |
| 1859 | 2034 | @ <td><input type="text" name="captcha" value="" size="30"> |
| 2035 | + captcha_speakit_button(uSeed, "Speak the code"); | |
| 1860 | 2036 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 1861 | 2037 | if( eErr==2 ){ |
| 1862 | 2038 | @ <td><span class="loginError">← %h(zErr)</span></td> |
| 1863 | 2039 | } |
| 1864 | 2040 | @ </tr> |
| @@ -1874,11 +2050,11 @@ | ||
| 1874 | 2050 | @ </tr> |
| 1875 | 2051 | @ </table> |
| 1876 | 2052 | @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 1877 | 2053 | @ %h(zCaptcha) |
| 1878 | 2054 | @ </pre> |
| 1879 | - @ Enter the 8 characters above in the "Security Code" box | |
| 2055 | + @ Enter the 8 characters above in the "Security Code" box<br/> | |
| 1880 | 2056 | @ </td></tr></table></div> |
| 1881 | 2057 | @ </form> |
| 1882 | 2058 | fossil_free(zErr); |
| 1883 | 2059 | style_footer(); |
| 1884 | 2060 | } |
| @@ -1885,37 +2061,69 @@ | ||
| 1885 | 2061 | |
| 1886 | 2062 | /* |
| 1887 | 2063 | ** WEBPAGE: subscribers |
| 1888 | 2064 | ** |
| 1889 | 2065 | ** This page, accessible to administrators only, |
| 1890 | -** shows a list of email notification email addresses. | |
| 2066 | +** shows a list of subscriber email addresses. | |
| 1891 | 2067 | ** Clicking on an email takes one to the /alerts page |
| 1892 | 2068 | ** for that email where the delivery settings can be |
| 1893 | 2069 | ** modified. |
| 1894 | 2070 | */ |
| 1895 | 2071 | void subscriber_list_page(void){ |
| 1896 | 2072 | Blob sql; |
| 1897 | 2073 | Stmt q; |
| 1898 | 2074 | sqlite3_int64 iNow; |
| 2075 | + int nTotal; | |
| 2076 | + int nPending; | |
| 2077 | + int nDel = 0; | |
| 1899 | 2078 | if( alert_webpages_disabled() ) return; |
| 1900 | 2079 | login_check_credentials(); |
| 1901 | 2080 | if( !g.perm.Admin ){ |
| 1902 | 2081 | login_needed(0); |
| 1903 | 2082 | return; |
| 1904 | 2083 | } |
| 1905 | 2084 | alert_submenu_common(); |
| 2085 | + style_submenu_element("Users","setup_ulist"); | |
| 1906 | 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 | + } | |
| 1907 | 2114 | blob_init(&sql, 0, 0); |
| 1908 | 2115 | blob_append_sql(&sql, |
| 1909 | - "SELECT hex(subscriberCode)," /* 0 */ | |
| 2116 | + "SELECT subscriberId," /* 0 */ | |
| 1910 | 2117 | " semail," /* 1 */ |
| 1911 | 2118 | " ssub," /* 2 */ |
| 1912 | 2119 | " suname," /* 3 */ |
| 1913 | 2120 | " sverified," /* 4 */ |
| 1914 | 2121 | " sdigest," /* 5 */ |
| 1915 | 2122 | " mtime," /* 6 */ |
| 1916 | - " date(sctime,'unixepoch')" /* 7 */ | |
| 2123 | + " date(sctime,'unixepoch')," /* 7 */ | |
| 2124 | + " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */ | |
| 1917 | 2125 | " FROM subscriber" |
| 1918 | 2126 | ); |
| 1919 | 2127 | if( P("only")!=0 ){ |
| 1920 | 2128 | blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only")); |
| 1921 | 2129 | style_submenu_element("Show All","%R/subscribers"); |
| @@ -1937,16 +2145,22 @@ | ||
| 1937 | 2145 | @ </tr> |
| 1938 | 2146 | @ </thead><tbody> |
| 1939 | 2147 | while( db_step(&q)==SQLITE_ROW ){ |
| 1940 | 2148 | sqlite3_int64 iMtime = db_column_int64(&q, 6); |
| 1941 | 2149 | double rAge = (iNow - iMtime)/86400.0; |
| 2150 | + int uid = db_column_int(&q, 8); | |
| 2151 | + const char *zUname = db_column_text(&q, 3); | |
| 1942 | 2152 | @ <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))'>\ | |
| 1944 | 2154 | @ %h(db_column_text(&q,1))</a></td> |
| 1945 | 2155 | @ <td>%h(db_column_text(&q,2))</td> |
| 1946 | 2156 | @ <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 | + } | |
| 1948 | 2162 | @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td> |
| 1949 | 2163 | @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td> |
| 1950 | 2164 | @ <td>%h(db_column_text(&q,7))</td> |
| 1951 | 2165 | @ </tr> |
| 1952 | 2166 | } |
| @@ -1958,13 +2172,21 @@ | ||
| 1958 | 2172 | |
| 1959 | 2173 | #if LOCAL_INTERFACE |
| 1960 | 2174 | /* |
| 1961 | 2175 | ** A single event that might appear in an alert is recorded as an |
| 1962 | 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 | |
| 1963 | 2185 | */ |
| 1964 | 2186 | struct EmailEvent { |
| 1965 | - int type; /* 'c', 'f', 'm', 't', 'w' */ | |
| 2187 | + int type; /* 'c', 'f', 't', 'w', 'x' */ | |
| 1966 | 2188 | int needMod; /* Pending moderator approval */ |
| 1967 | 2189 | Blob hdr; /* Header content, for forum entries */ |
| 1968 | 2190 | Blob txt; /* Text description to appear in an alert */ |
| 1969 | 2191 | char *zFromName; /* Human name of the sender */ |
| 1970 | 2192 | EmailEvent *pNext; /* Next in chronological order */ |
| @@ -2003,11 +2225,14 @@ | ||
| 2003 | 2225 | |
| 2004 | 2226 | |
| 2005 | 2227 | /* First do non-forum post events */ |
| 2006 | 2228 | db_prepare(&q, |
| 2007 | 2229 | "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 */ | |
| 2009 | 2234 | " datetime(event.mtime)," /* 1 */ |
| 2010 | 2235 | " coalesce(ecomment,comment)" |
| 2011 | 2236 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 2012 | 2237 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 2013 | 2238 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| @@ -2036,20 +2261,20 @@ | ||
| 2036 | 2261 | p->needMod = db_column_int(&q, 4); |
| 2037 | 2262 | p->zFromName = 0; |
| 2038 | 2263 | p->pNext = 0; |
| 2039 | 2264 | switch( p->type ){ |
| 2040 | 2265 | case 'c': zType = "Check-In"; break; |
| 2041 | - case 'f': zType = "Forum post"; break; | |
| 2266 | + /* case 'f': -- forum posts omitted from this loop. See below */ | |
| 2042 | 2267 | case 't': zType = "Ticket Change"; break; |
| 2043 | 2268 | case 'w': zType = "Wiki Edit"; break; |
| 2044 | 2269 | } |
| 2045 | 2270 | blob_init(&p->hdr, 0, 0); |
| 2046 | 2271 | blob_init(&p->txt, 0, 0); |
| 2047 | 2272 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 2048 | 2273 | db_column_text(&q,1), |
| 2049 | 2274 | zType, |
| 2050 | - db_column_text(&q,2), | |
| 2275 | + db_column_text(&q, 2), | |
| 2051 | 2276 | zUrl, |
| 2052 | 2277 | db_column_text(&q,0) |
| 2053 | 2278 | ); |
| 2054 | 2279 | if( p->needMod ){ |
| 2055 | 2280 | blob_appendf(&p->txt, |
| @@ -2075,17 +2300,28 @@ | ||
| 2075 | 2300 | ** is a normal email alert. Construct full-text forum post alerts |
| 2076 | 2301 | ** using a format that enables them to be sent as separate emails. |
| 2077 | 2302 | */ |
| 2078 | 2303 | db_prepare(&q, |
| 2079 | 2304 | "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 */ | |
| 2087 | 2323 | " FROM temp.wantalert, event, forumpost" |
| 2088 | 2324 | " LEFT JOIN user ON (login=coalesce(euser,user))" |
| 2089 | 2325 | " WHERE event.objid=substr(wantalert.eventId,2)+0" |
| 2090 | 2326 | " AND eventId GLOB 'f*'" |
| 2091 | 2327 | " AND forumpost.fpid=event.objid" |
| @@ -2101,11 +2337,11 @@ | ||
| 2101 | 2337 | const char *z; |
| 2102 | 2338 | if( pPost==0 ) continue; |
| 2103 | 2339 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 2104 | 2340 | pLast->pNext = p; |
| 2105 | 2341 | pLast = p; |
| 2106 | - p->type = 'f'; | |
| 2342 | + p->type = db_column_int(&q,7) ? 'f' : 'x'; | |
| 2107 | 2343 | p->needMod = db_column_int(&q, 5); |
| 2108 | 2344 | z = db_column_text(&q,6); |
| 2109 | 2345 | p->zFromName = z && z[0] ? fossil_strdup(z) : 0; |
| 2110 | 2346 | p->pNext = 0; |
| 2111 | 2347 | blob_init(&p->hdr, 0, 0); |
| @@ -2433,13 +2669,13 @@ | ||
| 2433 | 2669 | ** if the recipient is a moderator for that type of event. Setup |
| 2434 | 2670 | ** and Admin users always get notified. */ |
| 2435 | 2671 | char xType = '*'; |
| 2436 | 2672 | if( strpbrk(zCap,"as")==0 ){ |
| 2437 | 2673 | 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; | |
| 2441 | 2677 | } |
| 2442 | 2678 | if( strchr(zCap,xType)==0 ) continue; |
| 2443 | 2679 | } |
| 2444 | 2680 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2445 | 2681 | /* Setup and admin users can get any notification that does not |
| @@ -2447,14 +2683,14 @@ | ||
| 2447 | 2683 | }else{ |
| 2448 | 2684 | /* Other users only see the alert if they have sufficient |
| 2449 | 2685 | ** privilege to view the event itself */ |
| 2450 | 2686 | char xType = '*'; |
| 2451 | 2687 | 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; | |
| 2456 | 2692 | } |
| 2457 | 2693 | if( strchr(zCap,xType)==0 ) continue; |
| 2458 | 2694 | } |
| 2459 | 2695 | if( blob_size(&p->hdr)>0 ){ |
| 2460 | 2696 | /* This alert should be sent as a separate email */ |
| @@ -2591,10 +2827,11 @@ | ||
| 2591 | 2827 | @ <table class="subscribe"> |
| 2592 | 2828 | if( zCaptcha ){ |
| 2593 | 2829 | @ <tr> |
| 2594 | 2830 | @ <td class="form_label">Security Code:</td> |
| 2595 | 2831 | @ <td><input type="text" name="captcha" value="" size="10"> |
| 2832 | + captcha_speakit_button(uSeed, "Speak the code"); | |
| 2596 | 2833 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 2597 | 2834 | @ </tr> |
| 2598 | 2835 | } |
| 2599 | 2836 | @ <tr> |
| 2600 | 2837 | @ <td class="form_label">Your Email Address:</td> |
| @@ -2617,11 +2854,11 @@ | ||
| 2617 | 2854 | @ </table> |
| 2618 | 2855 | if( zCaptcha ){ |
| 2619 | 2856 | @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 2620 | 2857 | @ %h(zCaptcha) |
| 2621 | 2858 | @ </pre> |
| 2622 | - @ Enter the 8 characters above in the "Security Code" box | |
| 2859 | + @ Enter the 8 characters above in the "Security Code" box<br/> | |
| 2623 | 2860 | @ </td></tr></table></div> |
| 2624 | 2861 | } |
| 2625 | 2862 | @ </form> |
| 2626 | 2863 | style_footer(); |
| 2627 | 2864 | } |
| @@ -2635,10 +2872,11 @@ | ||
| 2635 | 2872 | char *zErr; |
| 2636 | 2873 | const char *zTo = PT("to"); |
| 2637 | 2874 | char *zSubject = PT("subject"); |
| 2638 | 2875 | int bAll = PB("all"); |
| 2639 | 2876 | int bAA = PB("aa"); |
| 2877 | + int bMods = PB("mods"); | |
| 2640 | 2878 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 2641 | 2879 | int bTest2 = fossil_strcmp(P("name"),"test2")==0; |
| 2642 | 2880 | Blob hdr, body; |
| 2643 | 2881 | blob_init(&body, 0, 0); |
| 2644 | 2882 | blob_init(&hdr, 0, 0); |
| @@ -2646,17 +2884,29 @@ | ||
| 2646 | 2884 | pSender = alert_sender_new(bTest2 ? "blob" : 0, 0); |
| 2647 | 2885 | if( zTo[0] ){ |
| 2648 | 2886 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 2649 | 2887 | alert_send(pSender, &hdr, &body, 0); |
| 2650 | 2888 | } |
| 2651 | - if( bAll || bAA ){ | |
| 2889 | + if( bAll || bAA || bMods ){ | |
| 2652 | 2890 | Stmt q; |
| 2653 | 2891 | int nUsed = blob_size(&body); |
| 2654 | 2892 | 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 | + } | |
| 2658 | 2908 | while( db_step(&q)==SQLITE_ROW ){ |
| 2659 | 2909 | const char *zCode = db_column_text(&q, 1); |
| 2660 | 2910 | zTo = db_column_text(&q, 0); |
| 2661 | 2911 | blob_truncate(&hdr, 0); |
| 2662 | 2912 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| @@ -2713,11 +2963,12 @@ | ||
| 2713 | 2963 | @ <p>The following error was reported by the system: |
| 2714 | 2964 | @ <blockquote><pre> |
| 2715 | 2965 | @ %h(zErr) |
| 2716 | 2966 | @ </pre></blockquote> |
| 2717 | 2967 | }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> | |
| 2719 | 2970 | } |
| 2720 | 2971 | style_footer(); |
| 2721 | 2972 | return; |
| 2722 | 2973 | } else if( !alert_enabled() ){ |
| 2723 | 2974 | style_header("Cannot Send Announcement"); |
| @@ -2731,21 +2982,26 @@ | ||
| 2731 | 2982 | @ <form method="POST"> |
| 2732 | 2983 | @ <table class="subscribe"> |
| 2733 | 2984 | if( g.perm.Admin ){ |
| 2734 | 2985 | int aa = PB("aa"); |
| 2735 | 2986 | int all = PB("all"); |
| 2987 | + int aMod = PB("mods"); | |
| 2736 | 2988 | const char *aack = aa ? "checked" : ""; |
| 2737 | 2989 | const char *allck = all ? "checked" : ""; |
| 2990 | + const char *modck = aMod ? "checked" : ""; | |
| 2738 | 2991 | @ <tr> |
| 2739 | 2992 | @ <td class="form_label">To:</td> |
| 2740 | 2993 | @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br> |
| 2741 | 2994 | @ <label><input type="checkbox" name="aa" %s(aack)> \ |
| 2742 | 2995 | @ All "announcement" subscribers</label> \ |
| 2743 | 2996 | @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br> |
| 2744 | 2997 | @ <label><input type="checkbox" name="all" %s(allck)> \ |
| 2745 | 2998 | @ 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> | |
| 2747 | 3003 | @ </tr> |
| 2748 | 3004 | } |
| 2749 | 3005 | @ <tr> |
| 2750 | 3006 | @ <td class="form_label">Subject:</td> |
| 2751 | 3007 | @ <td><input type="text" name="subject" value="%h(PT("subject"))"\ |
| @@ -2756,11 +3012,15 @@ | ||
| 2756 | 3012 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 2757 | 3013 | @ %h(PT("msg"))</textarea> |
| 2758 | 3014 | @ </tr> |
| 2759 | 3015 | @ <tr> |
| 2760 | 3016 | @ <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 | + } | |
| 2762 | 3022 | @ </tr> |
| 2763 | 3023 | @ </table> |
| 2764 | 3024 | @ </form> |
| 2765 | 3025 | style_footer(); |
| 2766 | 3026 | } |
| 2767 | 3027 |
| --- 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'>↑ %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'>↑ %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 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">← %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 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 Email 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'>↑ %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'>↑ %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 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'>← not a valid email address!</span> |
| 1809 | }else if( g.perm.Admin ){ |
| 1810 | @ <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 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 | @ <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">← %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 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 Email 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 @@ | ||
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include "allrepo.h" |
| 22 | 22 | #include <assert.h> |
| 23 | 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 | 24 | /* |
| 51 | 25 | ** Build a string that contains all of the command-line options |
| 52 | 26 | ** specified as arguments. If the option name begins with "+" then |
| 53 | 27 | ** it takes an argument. Without the "+" it does not. |
| 54 | 28 | */ |
| 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){ | |
| 56 | 30 | const char *z = find_option(zArg, zShort, 0); |
| 57 | 31 | if( z!=0 ){ |
| 58 | 32 | blob_appendf(pExtra, " %s", z); |
| 59 | 33 | } |
| 60 | 34 | } |
| 61 | 35 | static void collect_argument_value(Blob *pExtra, const char *zArg){ |
| 62 | 36 | const char *zValue = find_option(zArg, 0, 1); |
| 63 | 37 | if( zValue ){ |
| 64 | 38 | if( zValue[0] ){ |
| 65 | - blob_appendf(pExtra, " --%s %s", zArg, zValue); | |
| 39 | + blob_appendf(pExtra, " --%s %$", zArg, zValue); | |
| 66 | 40 | }else{ |
| 67 | 41 | blob_appendf(pExtra, " --%s \"\"", zArg); |
| 68 | 42 | } |
| 69 | 43 | } |
| 70 | 44 | } |
| @@ -128,15 +102,15 @@ | ||
| 128 | 102 | ** supported by the rebuild command itself, if any are |
| 129 | 103 | ** present, are passed along verbatim. The --force and |
| 130 | 104 | ** --randomize options are not supported. |
| 131 | 105 | ** |
| 132 | 106 | ** sync Run a "sync" on all repositories. Only the --verbose |
| 133 | -** option is supported. | |
| 107 | +** and --unversioned options are supported. | |
| 134 | 108 | ** |
| 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 | |
| 138 | 112 | ** otherwise be set globally. |
| 139 | 113 | ** |
| 140 | 114 | ** server Run the "ui" or "server" commands on all repositories. |
| 141 | 115 | ** ui The root URI gives a listing of all repos. |
| 142 | 116 | ** |
| @@ -169,12 +143,10 @@ | ||
| 169 | 143 | void all_cmd(void){ |
| 170 | 144 | int n; |
| 171 | 145 | Stmt q; |
| 172 | 146 | const char *zCmd; |
| 173 | 147 | char *zSyscmd; |
| 174 | - char *zFossil; | |
| 175 | - char *zQFilename; | |
| 176 | 148 | Blob extra; |
| 177 | 149 | int useCheckouts = 0; |
| 178 | 150 | int quiet = 0; |
| 179 | 151 | int dryRunFlag = 0; |
| 180 | 152 | int showFile = find_option("showfile",0,0)!=0; |
| @@ -283,11 +255,13 @@ | ||
| 283 | 255 | }else if( strncmp(zCmd, "sync", n)==0 ){ |
| 284 | 256 | zCmd = "sync -autourl -R"; |
| 285 | 257 | collect_argument(&extra, "verbose","v"); |
| 286 | 258 | collect_argument(&extra, "unversioned","u"); |
| 287 | 259 | }else if( strncmp(zCmd, "test-integrity", n)==0 ){ |
| 260 | + collect_argument(&extra, "db-only", "d"); | |
| 288 | 261 | collect_argument(&extra, "parse", 0); |
| 262 | + collect_argument(&extra, "quick", "q"); | |
| 289 | 263 | zCmd = "test-integrity"; |
| 290 | 264 | }else if( strncmp(zCmd, "test-orphans", n)==0 ){ |
| 291 | 265 | zCmd = "test-orphans -R"; |
| 292 | 266 | }else if( strncmp(zCmd, "test-missing", n)==0 ){ |
| 293 | 267 | zCmd = "test-missing -q -R"; |
| @@ -368,11 +342,10 @@ | ||
| 368 | 342 | fossil_fatal("\"all\" subcommand should be one of: " |
| 369 | 343 | "add cache changes clean dbstat extras fts-config ignore " |
| 370 | 344 | "info list ls pull push rebuild server setting sync ui unset"); |
| 371 | 345 | } |
| 372 | 346 | verify_all_options(); |
| 373 | - zFossil = quoteFilename(g.nameOfExe); | |
| 374 | 347 | db_multi_exec("CREATE TEMP TABLE repolist(name,tag);"); |
| 375 | 348 | if( useCheckouts ){ |
| 376 | 349 | db_multi_exec( |
| 377 | 350 | "INSERT INTO repolist " |
| 378 | 351 | "SELECT DISTINCT substr(name, 7), name COLLATE nocase" |
| @@ -410,13 +383,12 @@ | ||
| 410 | 383 | continue; |
| 411 | 384 | }else if( showFile ){ |
| 412 | 385 | fossil_print("%s: %s\n", useCheckouts ? "checkout" : "repository", |
| 413 | 386 | zFilename); |
| 414 | 387 | } |
| 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)); | |
| 418 | 390 | if( showLabel ){ |
| 419 | 391 | int len = (int)strlen(zFilename); |
| 420 | 392 | int nStar = 80 - (len + 15); |
| 421 | 393 | if( nStar<2 ) nStar = 1; |
| 422 | 394 | fossil_print("%.13c %s %.*c\n", '*', zFilename, nStar, '*'); |
| @@ -426,11 +398,10 @@ | ||
| 426 | 398 | fossil_print("%s\n", zSyscmd); |
| 427 | 399 | fflush(stdout); |
| 428 | 400 | } |
| 429 | 401 | rc = dryRunFlag ? 0 : fossil_system(zSyscmd); |
| 430 | 402 | free(zSyscmd); |
| 431 | - free(zQFilename); | |
| 432 | 403 | if( stopOnError && rc ){ |
| 433 | 404 | break; |
| 434 | 405 | } |
| 435 | 406 | } |
| 436 | 407 | db_finalize(&q); |
| 437 | 408 |
| --- 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 @@ | ||
| 23 | 23 | |
| 24 | 24 | /* |
| 25 | 25 | ** WEBPAGE: attachlist |
| 26 | 26 | ** List attachments. |
| 27 | 27 | ** |
| 28 | -** tkt=TICKETUUID | |
| 28 | +** tkt=HASH | |
| 29 | 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. | |
| 30 | 36 | ** |
| 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. | |
| 37 | 40 | */ |
| 38 | 41 | void attachlist_page(void){ |
| 39 | 42 | const char *zPage = P("page"); |
| 40 | 43 | const char *zTkt = P("tkt"); |
| 41 | 44 | const char *zTechNote = P("technote"); |
| @@ -152,15 +155,16 @@ | ||
| 152 | 155 | ** WEBPAGE: attachdownload |
| 153 | 156 | ** WEBPAGE: attachimage |
| 154 | 157 | ** WEBPAGE: attachview |
| 155 | 158 | ** |
| 156 | 159 | ** Download or display an attachment. |
| 160 | +** | |
| 157 | 161 | ** Query parameters: |
| 158 | 162 | ** |
| 159 | -** tkt=TICKETUUID | |
| 163 | +** tkt=HASH | |
| 160 | 164 | ** page=WIKIPAGE |
| 161 | -** technote=TECHNOTEUUID | |
| 165 | +** technote=HASH | |
| 162 | 166 | ** file=FILENAME |
| 163 | 167 | ** attachid=ID |
| 164 | 168 | ** |
| 165 | 169 | */ |
| 166 | 170 | void attachview_page(void){ |
| @@ -250,11 +254,11 @@ | ||
| 250 | 254 | /* |
| 251 | 255 | ** Commit a new attachment into the repository |
| 252 | 256 | */ |
| 253 | 257 | void attach_commit( |
| 254 | 258 | 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 */ | |
| 256 | 260 | const char *aContent, /* The content of the attachment */ |
| 257 | 261 | int szContent, /* The length of the attachment */ |
| 258 | 262 | int needModerator, /* Moderate the attachment? */ |
| 259 | 263 | const char *zComment /* The comment for the attachment */ |
| 260 | 264 | ){ |
| @@ -305,13 +309,13 @@ | ||
| 305 | 309 | |
| 306 | 310 | /* |
| 307 | 311 | ** WEBPAGE: attachadd |
| 308 | 312 | ** Add a new attachment. |
| 309 | 313 | ** |
| 310 | -** tkt=TICKETUUID | |
| 314 | +** tkt=HASH | |
| 311 | 315 | ** page=WIKIPAGE |
| 312 | -** technote=TECHNOTEUUID | |
| 316 | +** technote=HASH | |
| 313 | 317 | ** from=URL |
| 314 | 318 | ** |
| 315 | 319 | */ |
| 316 | 320 | void attachadd_page(void){ |
| 317 | 321 | const char *zPage = P("page"); |
| @@ -420,14 +424,14 @@ | ||
| 420 | 424 | */ |
| 421 | 425 | void ainfo_page(void){ |
| 422 | 426 | int rid; /* RID for the control artifact */ |
| 423 | 427 | int ridSrc; /* RID for the attached file */ |
| 424 | 428 | char *zDate; /* Date attached */ |
| 425 | - const char *zUuid; /* UUID of the control artifact */ | |
| 429 | + const char *zUuid; /* Hash of the control artifact */ | |
| 426 | 430 | Manifest *pAttach; /* Parse of the control artifact */ |
| 427 | 431 | 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 */ | |
| 429 | 433 | const char *zName; /* Name of the attached file */ |
| 430 | 434 | const char *zDesc; /* Description of the attached file */ |
| 431 | 435 | const char *zWikiName = 0; /* Wiki page name when attached to Wiki */ |
| 432 | 436 | const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */ |
| 433 | 437 | const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */ |
| @@ -545,11 +549,11 @@ | ||
| 545 | 549 | cgi_redirectf("%R/wiki?name=%t", zWikiName); |
| 546 | 550 | } |
| 547 | 551 | return; |
| 548 | 552 | } |
| 549 | 553 | if( strcmp(zModAction,"approve")==0 ){ |
| 550 | - moderation_approve(rid); | |
| 554 | + moderation_approve('a', rid); | |
| 551 | 555 | } |
| 552 | 556 | } |
| 553 | 557 | style_header("Attachment Details"); |
| 554 | 558 | style_submenu_element("Raw", "%R/artifact/%s", zUuid); |
| 555 | 559 | if(fShowContent){ |
| @@ -681,22 +685,23 @@ | ||
| 681 | 685 | /* |
| 682 | 686 | ** COMMAND: attachment* |
| 683 | 687 | ** |
| 684 | 688 | ** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS? |
| 685 | 689 | ** |
| 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. | |
| 698 | 703 | ** |
| 699 | 704 | ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 700 | 705 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 701 | 706 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 702 | 707 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| @@ -763,11 +768,11 @@ | ||
| 763 | 768 | } |
| 764 | 769 | blob_read_from_file(&content, zFile, ExtFILE); |
| 765 | 770 | user_select(); |
| 766 | 771 | attach_commit( |
| 767 | 772 | zFile, /* The filename of the attachment */ |
| 768 | - zTarget, /* The artifact uuid to attach to */ | |
| 773 | + zTarget, /* The artifact hash to attach to */ | |
| 769 | 774 | blob_buffer(&content), /* The content of the attachment */ |
| 770 | 775 | blob_size(&content), /* The length of the attachment */ |
| 771 | 776 | 0, /* No need to moderate the attachment */ |
| 772 | 777 | "" /* Empty attachment comment */ |
| 773 | 778 | ); |
| 774 | 779 | |
| 775 | 780 | 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 |
+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 |
| --- 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 @@ | ||
| 555 | 555 | ** |
| 556 | 556 | ** OPTIONS: |
| 557 | 557 | ** |
| 558 | 558 | ** --debug Show what this command is doing. |
| 559 | 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 | +** | |
| 560 | 565 | ** --nodelay Do not queue up or wait for a backoffice job |
| 561 | 566 | ** to complete. If no work is available or if |
| 562 | 567 | ** backoffice has run recently, return immediately. |
| 563 | 568 | ** The --nodelay option is implied if more than |
| 564 | 569 | ** one repository is listed on the command-line. |
| 565 | 570 | ** |
| 566 | 571 | ** --poll N Repeat backoffice calls for repositories that |
| 567 | 572 | ** change in appoximately N-second intervals. |
| 568 | 573 | ** N less than 1 turns polling off (the default). |
| 574 | +** Recommended polling interval: 60 seconds. | |
| 569 | 575 | ** |
| 570 | 576 | ** --trace Enable debugging output on stderr |
| 571 | 577 | */ |
| 572 | 578 | void backoffice_command(void){ |
| 573 | 579 | int nPoll; |
| 580 | + int nMin; | |
| 574 | 581 | const char *zPoll; |
| 575 | 582 | int bDebug = 0; |
| 576 | 583 | unsigned int nCmd = 0; |
| 577 | 584 | if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1; |
| 578 | 585 | if( find_option("nodelay",0,0)!=0 ) backofficeNoDelay = 1; |
| 579 | 586 | zPoll = find_option("poll",0,1); |
| 580 | 587 | nPoll = zPoll ? atoi(zPoll) : 0; |
| 588 | + zPoll = find_option("min",0,1); | |
| 589 | + nMin = zPoll ? atoi(zPoll) : 3600; | |
| 581 | 590 | bDebug = find_option("debug",0,0)!=0; |
| 582 | 591 | |
| 583 | 592 | /* Silently consume the -R or --repository flag, leaving behind its |
| 584 | 593 | ** argument. This is for legacy compatibility. Older versions of the |
| 585 | 594 | ** backoffice command only ran on a single repository that was specified |
| @@ -592,16 +601,25 @@ | ||
| 592 | 601 | ** or we are polling. In either case, each backoffice should be run |
| 593 | 602 | ** using a separate sub-process */ |
| 594 | 603 | int i; |
| 595 | 604 | time_t iNow = 0; |
| 596 | 605 | time_t ix; |
| 606 | + i64 *aLastRun = fossil_malloc( sizeof(i64)*g.argc ); | |
| 607 | + memset(aLastRun, 0, sizeof(i64)*g.argc ); | |
| 597 | 608 | while( 1 /* exit via "break;" */){ |
| 598 | 609 | time_t iNext = time(0); |
| 599 | 610 | for(i=2; i<g.argc; i++){ |
| 600 | 611 | 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 | + } | |
| 603 | 621 | blob_init(&cmd, 0, 0); |
| 604 | 622 | blob_append_escaped_arg(&cmd, g.nameOfExe); |
| 605 | 623 | blob_append(&cmd, " backoffice --nodelay", -1); |
| 606 | 624 | if( g.fAnyTrace ){ |
| 607 | 625 | blob_append(&cmd, " --trace", -1); |
| @@ -610,10 +628,11 @@ | ||
| 610 | 628 | nCmd++; |
| 611 | 629 | if( bDebug ){ |
| 612 | 630 | fossil_print("COMMAND[%u]: %s\n", nCmd, blob_str(&cmd)); |
| 613 | 631 | } |
| 614 | 632 | fossil_system(blob_str(&cmd)); |
| 633 | + aLastRun[i] = iNext; | |
| 615 | 634 | blob_reset(&cmd); |
| 616 | 635 | } |
| 617 | 636 | if( nPoll<1 ) break; |
| 618 | 637 | iNow = iNext; |
| 619 | 638 | ix = time(0); |
| @@ -622,10 +641,12 @@ | ||
| 622 | 641 | if( bDebug )fossil_print("SLEEP: %lld\n", nMS); |
| 623 | 642 | sqlite3_sleep((int)nMS); |
| 624 | 643 | } |
| 625 | 644 | } |
| 626 | 645 | }else{ |
| 646 | + /* Not polling and only one repository named. Backoffice is run | |
| 647 | + ** once by this process, which then exits */ | |
| 627 | 648 | if( g.argc==3 ){ |
| 628 | 649 | g.zRepositoryOption = g.argv[2]; |
| 629 | 650 | g.argc--; |
| 630 | 651 | } |
| 631 | 652 | db_find_and_open_repository(0,0); |
| 632 | 653 |
| --- 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 @@ | ||
| 48 | 48 | int cnt; /* Number of integers in the bag */ |
| 49 | 49 | int sz; /* Number of slots in a[] */ |
| 50 | 50 | int used; /* Number of used slots in a[] */ |
| 51 | 51 | int *a; /* Hash table of integers that are in the bag */ |
| 52 | 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} | |
| 53 | 59 | #endif |
| 54 | 60 | |
| 55 | 61 | /* |
| 56 | 62 | ** Initialize a Bag structure |
| 57 | 63 | */ |
| 58 | 64 |
| --- 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 @@ | ||
| 304 | 304 | ** |
| 305 | 305 | ** Usage: %fossil bisect SUBCOMMAND ... |
| 306 | 306 | ** |
| 307 | 307 | ** Run various subcommands useful for searching for bugs. |
| 308 | 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. | |
| 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 | 353 | ** |
| 354 | 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 | |
| 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 | |
| 366 | 365 | */ |
| 367 | 366 | void bisect_cmd(void){ |
| 368 | 367 | int n; |
| 369 | 368 | const char *zCmd; |
| 370 | 369 | int foundCmd = 0; |
| 371 | 370 |
| --- 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 @@ | ||
| 277 | 277 | pBlob->xRealloc = blobReallocStatic; |
| 278 | 278 | } |
| 279 | 279 | |
| 280 | 280 | /* |
| 281 | 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. | |
| 282 | 289 | */ |
| 283 | -void blob_append(Blob *pBlob, const char *aData, int nData){ | |
| 290 | +void blob_append_full(Blob *pBlob, const char *aData, int nData){ | |
| 284 | 291 | sqlite3_int64 nNew; |
| 285 | 292 | assert( aData!=0 || nData==0 ); |
| 286 | 293 | blob_is_init(pBlob); |
| 287 | 294 | if( nData<0 ) nData = strlen(aData); |
| 288 | 295 | if( nData==0 ) return; |
| @@ -301,22 +308,40 @@ | ||
| 301 | 308 | } |
| 302 | 309 | memcpy(&pBlob->aData[pBlob->nUsed], aData, nData); |
| 303 | 310 | pBlob->nUsed += nData; |
| 304 | 311 | pBlob->aData[pBlob->nUsed] = 0; /* Blobs are always nul-terminated */ |
| 305 | 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 | |
| 306 | 333 | |
| 307 | 334 | /* |
| 308 | 335 | ** Append a single character to the blob |
| 309 | 336 | */ |
| 310 | 337 | void blob_append_char(Blob *pBlob, char c){ |
| 311 | 338 | 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; | |
| 316 | 342 | } |
| 317 | - pBlob->aData[pBlob->nUsed++] = c; | |
| 318 | 343 | } |
| 319 | 344 | |
| 320 | 345 | /* |
| 321 | 346 | ** Copy a blob |
| 322 | 347 | */ |
| @@ -333,11 +358,13 @@ | ||
| 333 | 358 | blob_is_init(p); |
| 334 | 359 | if( p->nUsed==0 ){ |
| 335 | 360 | blob_append_char(p, 0); /* NOTE: Changes nUsed. */ |
| 336 | 361 | p->nUsed = 0; |
| 337 | 362 | } |
| 338 | - if( p->aData[p->nUsed]!=0 ){ | |
| 363 | + if( p->nUsed<p->nAlloc ){ | |
| 364 | + p->aData[p->nUsed] = 0; | |
| 365 | + }else{ | |
| 339 | 366 | blob_materialize(p); |
| 340 | 367 | } |
| 341 | 368 | return p->aData; |
| 342 | 369 | } |
| 343 | 370 | |
| @@ -450,10 +477,29 @@ | ||
| 450 | 477 | void blob_resize(Blob *pBlob, unsigned int newSize){ |
| 451 | 478 | pBlob->xRealloc(pBlob, newSize+1); |
| 452 | 479 | pBlob->nUsed = newSize; |
| 453 | 480 | pBlob->aData[newSize] = 0; |
| 454 | 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 | +} | |
| 455 | 501 | |
| 456 | 502 | /* |
| 457 | 503 | ** Make sure a blob is nul-terminated and is not a pointer to unmanaged |
| 458 | 504 | ** space. Return a pointer to the data. |
| 459 | 505 | */ |
| @@ -1140,11 +1186,10 @@ | ||
| 1140 | 1186 | blob_reset(&b3); |
| 1141 | 1187 | } |
| 1142 | 1188 | fossil_print("ok\n"); |
| 1143 | 1189 | } |
| 1144 | 1190 | |
| 1145 | -#if defined(_WIN32) || defined(__CYGWIN__) | |
| 1146 | 1191 | /* |
| 1147 | 1192 | ** Convert every \n character in the given blob into \r\n. |
| 1148 | 1193 | */ |
| 1149 | 1194 | void blob_add_cr(Blob *p){ |
| 1150 | 1195 | char *z = p->aData; |
| @@ -1164,11 +1209,10 @@ | ||
| 1164 | 1209 | if( (z[--j] = z[--i]) =='\n' ){ |
| 1165 | 1210 | z[--j] = '\r'; |
| 1166 | 1211 | } |
| 1167 | 1212 | } |
| 1168 | 1213 | } |
| 1169 | -#endif | |
| 1170 | 1214 | |
| 1171 | 1215 | /* |
| 1172 | 1216 | ** Remove every \r character from the given blob, replacing each one with |
| 1173 | 1217 | ** a \n character if it was not already part of a \r\n pair. |
| 1174 | 1218 | */ |
| @@ -1257,35 +1301,88 @@ | ||
| 1257 | 1301 | char c; |
| 1258 | 1302 | int needEscape = 0; |
| 1259 | 1303 | int n = blob_size(pBlob); |
| 1260 | 1304 | char *z = blob_buffer(pBlob); |
| 1261 | 1305 | #if defined(_WIN32) |
| 1306 | + const char cDirSep = '\\'; /* Use \ as directory separator */ | |
| 1262 | 1307 | const char cQuote = '"'; /* Use "..." quoting on windows */ |
| 1308 | + const char cEscape = '^'; /* Use ^X escaping on windows */ | |
| 1263 | 1309 | #else |
| 1310 | + const char cDirSep = '/'; /* Use / as directory separator */ | |
| 1264 | 1311 | const char cQuote = '\''; /* Use '...' quoting on unix */ |
| 1312 | + const char cEscape = '\\'; /* Use \X escaping on unix */ | |
| 1265 | 1313 | #endif |
| 1266 | 1314 | |
| 1267 | 1315 | for(i=0; (c = zIn[i])!=0; i++){ |
| 1268 | 1316 | if( c==cQuote || (unsigned char)c<' ' || |
| 1269 | - c=='\\' || c==';' || c=='*' || c=='?' || c=='[' ){ | |
| 1317 | + c==cEscape || c==';' || c=='*' || c=='?' || c=='[' ){ | |
| 1270 | 1318 | Blob bad; |
| 1271 | 1319 | blob_token(pBlob, &bad); |
| 1272 | 1320 | fossil_fatal("the [%s] argument to the \"%s\" command contains " |
| 1273 | 1321 | "a character (ascii 0x%02x) that is a security risk", |
| 1274 | 1322 | zIn, blob_str(&bad), c); |
| 1275 | 1323 | } |
| 1276 | - if( !needEscape && !fossil_isalnum(c) && c!='/' && c!='.' && c!='_' ){ | |
| 1324 | + if( !needEscape && !fossil_isalnum(c) && c!=cDirSep && c!='.' && c!='_' ){ | |
| 1277 | 1325 | needEscape = 1; |
| 1278 | 1326 | } |
| 1279 | 1327 | } |
| 1280 | 1328 | if( n>0 && !fossil_isspace(z[n-1]) ){ |
| 1281 | 1329 | blob_append_char(pBlob, ' '); |
| 1282 | 1330 | } |
| 1283 | 1331 | 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 | |
| 1285 | 1350 | 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 | + } | |
| 1287 | 1384 | } |
| 1288 | 1385 | |
| 1289 | 1386 | /* |
| 1290 | 1387 | ** A read(2)-like impl for the Blob class. Reads (copies) up to nLen |
| 1291 | 1388 | ** bytes from pIn, starting at position pIn->iCursor, and copies them |
| @@ -1344,21 +1441,22 @@ | ||
| 1344 | 1441 | }else if( starts_with_utf16_bom(pBlob, &bomSize, &bomReverse) ){ |
| 1345 | 1442 | zUtf8 = blob_buffer(pBlob); |
| 1346 | 1443 | if( bomReverse ){ |
| 1347 | 1444 | /* Found BOM, but with reversed bytes */ |
| 1348 | 1445 | unsigned int i = blob_size(pBlob); |
| 1349 | - while( i>0 ){ | |
| 1446 | + while( i>1 ){ | |
| 1350 | 1447 | /* swap bytes of unicode representation */ |
| 1351 | 1448 | char zTemp = zUtf8[--i]; |
| 1352 | 1449 | zUtf8[i] = zUtf8[i-1]; |
| 1353 | 1450 | zUtf8[--i] = zTemp; |
| 1354 | 1451 | } |
| 1355 | 1452 | } |
| 1356 | 1453 | /* Make sure the blob contains two terminating 0-bytes */ |
| 1357 | - blob_append_char(pBlob, 0); | |
| 1454 | + blob_append(pBlob, "\000\000", 3); | |
| 1358 | 1455 | zUtf8 = blob_str(pBlob) + bomSize; |
| 1359 | 1456 | zUtf8 = fossil_unicode_to_utf8(zUtf8); |
| 1457 | + blob_reset(pBlob); | |
| 1360 | 1458 | blob_set_dynamic(pBlob, zUtf8); |
| 1361 | 1459 | }else if( useMbcs && invalid_utf8(pBlob) ){ |
| 1362 | 1460 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1363 | 1461 | zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob)); |
| 1364 | 1462 | blob_reset(pBlob); |
| 1365 | 1463 |
| --- 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 @@ | ||
| 18 | 18 | ** This file contains code used to create new branches within a repository. |
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include "branch.h" |
| 22 | 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 | +} | |
| 23 | 64 | |
| 24 | 65 | /* |
| 25 | 66 | ** fossil branch new NAME BASIS ?OPTIONS? |
| 26 | 67 | ** argv0 argv1 argv2 argv3 argv4 |
| 27 | 68 | */ |
| @@ -117,11 +158,10 @@ | ||
| 117 | 158 | blob_appendf(&branch, "T *bgcolor * %F\n", zColor); |
| 118 | 159 | } |
| 119 | 160 | blob_appendf(&branch, "T *branch * %F\n", zBranch); |
| 120 | 161 | blob_appendf(&branch, "T *sym-%F *\n", zBranch); |
| 121 | 162 | if( isPrivate ){ |
| 122 | - blob_appendf(&branch, "T +private *\n"); | |
| 123 | 163 | noSign = 1; |
| 124 | 164 | } |
| 125 | 165 | |
| 126 | 166 | /* Cancel all other symbolic tags */ |
| 127 | 167 | db_prepare(&q, |
| @@ -218,11 +258,11 @@ | ||
| 218 | 258 | @ GROUP BY 1; |
| 219 | 259 | ; |
| 220 | 260 | |
| 221 | 261 | /* Call this routine to create the TEMP table */ |
| 222 | 262 | static void brlist_create_temp_table(void){ |
| 223 | - db_multi_exec(createBrlistQuery/*works-like:""*/); | |
| 263 | + db_exec_sql(createBrlistQuery); | |
| 224 | 264 | } |
| 225 | 265 | |
| 226 | 266 | |
| 227 | 267 | #if INTERFACE |
| 228 | 268 | /* |
| @@ -307,27 +347,27 @@ | ||
| 307 | 347 | ** Usage: %fossil branch SUBCOMMAND ... ?OPTIONS? |
| 308 | 348 | ** |
| 309 | 349 | ** Run various subcommands to manage branches of the open repository or |
| 310 | 350 | ** of the repository identified by the -R or --repository option. |
| 311 | 351 | ** |
| 312 | -** fossil branch current | |
| 352 | +** > fossil branch current | |
| 313 | 353 | ** |
| 314 | 354 | ** Print the name of the branch for the current check-out |
| 315 | 355 | ** |
| 316 | -** fossil branch info BRANCH-NAME | |
| 356 | +** > fossil branch info BRANCH-NAME | |
| 317 | 357 | ** |
| 318 | 358 | ** Print information about a branch |
| 319 | 359 | ** |
| 320 | -** fossil branch list|ls ?OPTIONS? | |
| 360 | +** > fossil branch list|ls ?OPTIONS? | |
| 321 | 361 | ** |
| 322 | 362 | ** List all branches. Options: |
| 323 | 363 | ** -a|--all List all branches. Default show only open branches |
| 324 | 364 | ** -c|--closed List closed branches. |
| 325 | 365 | ** -r Reverse the sort order |
| 326 | 366 | ** -t Show recently changed branches first |
| 327 | 367 | ** |
| 328 | -** fossil branch new BRANCH-NAME BASIS ?OPTIONS? | |
| 368 | +** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? | |
| 329 | 369 | ** |
| 330 | 370 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 331 | 371 | ** Supported options for this subcommand include: |
| 332 | 372 | ** --private branch is private (i.e., remains local) |
| 333 | 373 | ** --bgcolor COLOR use COLOR instead of automatic background |
| @@ -339,18 +379,19 @@ | ||
| 339 | 379 | ** year-month-day form, it may be truncated, the "T" may be |
| 340 | 380 | ** replaced by a space, and it may also name a timezone offset |
| 341 | 381 | ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward). |
| 342 | 382 | ** Either no timezone suffix or "Z" means UTC. |
| 343 | 383 | ** |
| 344 | -** Options: | |
| 384 | +** Options valid for all subcommands: | |
| 385 | +** | |
| 345 | 386 | ** -R|--repository FILE Run commands on repository FILE |
| 346 | 387 | ** |
| 347 | 388 | ** Summary: |
| 348 | 389 | ** fossil branch current |
| 349 | -** fossil branch info BRANCHNAME | |
| 390 | +** fossil branch info BRANCH-NAME | |
| 350 | 391 | ** fossil branch [list|ls] |
| 351 | -** fossil branch new | |
| 392 | +** fossil branch new BRANCH-NAME BASIS | |
| 352 | 393 | */ |
| 353 | 394 | void branch_cmd(void){ |
| 354 | 395 | int n; |
| 355 | 396 | const char *zCmd = "list"; |
| 356 | 397 | db_find_and_open_repository(0, 0); |
| @@ -661,9 +702,9 @@ | ||
| 661 | 702 | ** many descenders to (off-screen) parents. */ |
| 662 | 703 | tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL; |
| 663 | 704 | if( PB("ng")==0 ) tmFlags |= TIMELINE_GRAPH; |
| 664 | 705 | if( PB("brbg")!=0 ) tmFlags |= TIMELINE_BRCOLOR; |
| 665 | 706 | 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); | |
| 667 | 708 | db_finalize(&q); |
| 668 | 709 | style_footer(); |
| 669 | 710 | } |
| 670 | 711 |
| --- 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 @@ | ||
| 59 | 59 | zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]); |
| 60 | 60 | sqlite3_result_text(context, zOut, i-n+1, sqlite3_free); |
| 61 | 61 | } |
| 62 | 62 | } |
| 63 | 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 | + | |
| 64 | 72 | /* |
| 65 | 73 | ** Given a pathname which is a relative path from the root of |
| 66 | 74 | ** the repository to a file or directory, compute a string which |
| 67 | 75 | ** is an HTML rendering of that path with hyperlinks on each |
| 68 | 76 | ** directory component of the path where the hyperlink redirects |
| @@ -76,29 +84,36 @@ | ||
| 76 | 84 | void hyperlinked_path( |
| 77 | 85 | const char *zPath, /* Path to render */ |
| 78 | 86 | Blob *pOut, /* Write into this blob */ |
| 79 | 87 | const char *zCI, /* check-in name, or NULL */ |
| 80 | 88 | 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 */ | |
| 82 | 91 | ){ |
| 83 | 92 | int i, j; |
| 84 | 93 | char *zSep = ""; |
| 85 | 94 | |
| 86 | 95 | for(i=0; zPath[i]; i=j){ |
| 87 | 96 | 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]); | |
| 100 | 115 | } |
| 101 | 116 | zSep = "/"; |
| 102 | 117 | while( zPath[j]=='/' ){ j++; } |
| 103 | 118 | } |
| 104 | 119 | } |
| @@ -116,38 +131,35 @@ | ||
| 116 | 131 | ** |
| 117 | 132 | ** name=PATH Directory to display. Optional. Top-level if missing |
| 118 | 133 | ** ci=LABEL Show only files in this check-in. Optional. |
| 119 | 134 | ** type=TYPE TYPE=flat: use this display |
| 120 | 135 | ** TYPE=tree: use the /tree display instead |
| 136 | +** noreadme Do not attempt to display the README file. | |
| 121 | 137 | */ |
| 122 | 138 | void page_dir(void){ |
| 123 | 139 | char *zD = fossil_strdup(P("name")); |
| 124 | 140 | int nD = zD ? strlen(zD)+1 : 0; |
| 125 | 141 | int mxLen; |
| 126 | - int n; | |
| 127 | 142 | char *zPrefix; |
| 128 | 143 | Stmt q; |
| 129 | 144 | const char *zCI = P("ci"); |
| 130 | 145 | int rid = 0; |
| 131 | 146 | char *zUuid = 0; |
| 132 | - Blob dirname; | |
| 133 | 147 | Manifest *pM = 0; |
| 134 | 148 | const char *zSubdirLink; |
| 135 | 149 | int linkTrunk = 1; |
| 136 | 150 | int linkTip = 1; |
| 137 | 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; | |
| 138 | 155 | |
| 156 | + if( zCI && strlen(zCI)==0 ){ zCI = 0; } | |
| 139 | 157 | if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; } |
| 140 | 158 | login_check_credentials(); |
| 141 | 159 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 142 | 160 | 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 | 161 | |
| 150 | 162 | /* If the name= parameter is an empty string, make it a NULL pointer */ |
| 151 | 163 | if( zD && strlen(zD)==0 ){ zD = 0; } |
| 152 | 164 | |
| 153 | 165 | /* If a specific check-in is requested, fetch and parse it. If the |
| @@ -159,53 +171,80 @@ | ||
| 159 | 171 | if( pM ){ |
| 160 | 172 | int trunkRid = symbolic_name_to_rid("tag:trunk", "ci"); |
| 161 | 173 | linkTrunk = trunkRid && rid != trunkRid; |
| 162 | 174 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 163 | 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); | |
| 164 | 179 | }else{ |
| 165 | 180 | zCI = 0; |
| 166 | 181 | } |
| 167 | 182 | } |
| 168 | 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 | + | |
| 169 | 206 | /* Compute the title of the page */ |
| 170 | - blob_zero(&dirname); | |
| 171 | 207 | 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); | |
| 174 | 213 | zPrefix = mprintf("%s/", zD); |
| 175 | 214 | style_submenu_element("Top-Level", "%s", |
| 176 | 215 | url_render(&sURI, "name", 0, 0, 0)); |
| 177 | 216 | }else{ |
| 178 | - blob_append(&dirname, "in the top-level directory", -1); | |
| 217 | + @ <h2>Files in the top-level directory \ | |
| 179 | 218 | zPrefix = ""; |
| 180 | 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 | + } | |
| 181 | 237 | if( linkTrunk ){ |
| 182 | 238 | style_submenu_element("Trunk", "%s", |
| 183 | 239 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| 184 | 240 | } |
| 185 | 241 | if( linkTip ){ |
| 186 | 242 | style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0)); |
| 187 | 243 | } |
| 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 | - @ %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 | - @ %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); | |
| 207 | 246 | } |
| 208 | 247 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 209 | 248 | style_submenu_element("Tree-View", "%s", |
| 210 | 249 | url_render(&sURI, "type", "tree", 0, 0)); |
| 211 | 250 | |
| @@ -268,15 +307,14 @@ | ||
| 268 | 307 | |
| 269 | 308 | /* Generate a multi-column table listing the contents of zD[] |
| 270 | 309 | ** directory. |
| 271 | 310 | */ |
| 272 | 311 | mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/"); |
| 273 | - n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/"); | |
| 274 | 312 | if( mxLen<12 ) mxLen = 12; |
| 275 | 313 | mxLen += (mxLen+9)/10; |
| 276 | 314 | 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"> | |
| 278 | 316 | @ <ul class="browser"> |
| 279 | 317 | while( db_step(&q)==SQLITE_ROW ){ |
| 280 | 318 | const char *zFN; |
| 281 | 319 | zFN = db_column_text(&q, 0); |
| 282 | 320 | if( zFN[0]=='/' ){ |
| @@ -283,21 +321,28 @@ | ||
| 283 | 321 | zFN++; |
| 284 | 322 | @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li> |
| 285 | 323 | }else{ |
| 286 | 324 | const char *zLink; |
| 287 | 325 | 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); | |
| 290 | 327 | }else{ |
| 291 | 328 | zLink = href("%R/finfo?name=%T%T",zPrefix,zFN); |
| 292 | 329 | } |
| 293 | 330 | @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li> |
| 294 | 331 | } |
| 295 | 332 | } |
| 296 | 333 | db_finalize(&q); |
| 297 | 334 | manifest_destroy(pM); |
| 298 | 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 | + } | |
| 299 | 344 | |
| 300 | 345 | /* If the directory contains a readme file, then display its content below |
| 301 | 346 | ** the list of files |
| 302 | 347 | */ |
| 303 | 348 | db_prepare(&q, |
| @@ -345,10 +390,11 @@ | ||
| 345 | 390 | @ </iframe> |
| 346 | 391 | }else{ |
| 347 | 392 | Blob content; |
| 348 | 393 | const char *zMime = mimetype_from_name(zName); |
| 349 | 394 | content_get(rid, &content); |
| 395 | + safe_html_context(DOCSRC_FILE); | |
| 350 | 396 | wiki_render_by_mimetype(&content, zMime); |
| 351 | 397 | } |
| 352 | 398 | } |
| 353 | 399 | } |
| 354 | 400 | db_finalize(&q); |
| @@ -605,11 +651,15 @@ | ||
| 605 | 651 | HQuery sURI; /* Hyperlink */ |
| 606 | 652 | int startExpanded; /* True to start out with the tree expanded */ |
| 607 | 653 | int showDirOnly; /* Show directories only. Omit files */ |
| 608 | 654 | int nDir = 0; /* Number of directories. Used for ID attributes */ |
| 609 | 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; | |
| 610 | 659 | |
| 660 | + if( zCI && strlen(zCI)==0 ){ zCI = 0; } | |
| 611 | 661 | if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } |
| 612 | 662 | memset(&sTree, 0, sizeof(sTree)); |
| 613 | 663 | login_check_credentials(); |
| 614 | 664 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 615 | 665 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| @@ -617,14 +667,12 @@ | ||
| 617 | 667 | pathelementFunc, 0, 0); |
| 618 | 668 | url_initialize(&sURI, "tree"); |
| 619 | 669 | cgi_query_parameters_to_url(&sURI); |
| 620 | 670 | if( PB("nofiles") ){ |
| 621 | 671 | showDirOnly = 1; |
| 622 | - style_header("Folder Hierarchy"); | |
| 623 | 672 | }else{ |
| 624 | 673 | showDirOnly = 0; |
| 625 | - style_header("File Tree"); | |
| 626 | 674 | } |
| 627 | 675 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 628 | 676 | if( PB("expand") ){ |
| 629 | 677 | startExpanded = 1; |
| 630 | 678 | }else{ |
| @@ -653,37 +701,55 @@ | ||
| 653 | 701 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 654 | 702 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 655 | 703 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 656 | 704 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 657 | 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); | |
| 658 | 709 | }else{ |
| 659 | 710 | zCI = 0; |
| 660 | 711 | } |
| 661 | 712 | } |
| 662 | 713 | if( zCI==0 ){ |
| 663 | 714 | rNow = db_double(0.0, "SELECT max(mtime) FROM event"); |
| 664 | 715 | zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event"); |
| 665 | 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); | |
| 666 | 734 | |
| 667 | 735 | /* Compute the title of the page */ |
| 668 | 736 | blob_zero(&dirname); |
| 669 | 737 | if( zD ){ |
| 670 | 738 | blob_append(&dirname, "within directory ", -1); |
| 671 | - hyperlinked_path(zD, &dirname, zCI, "tree", zREx); | |
| 739 | + hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0); | |
| 672 | 740 | if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE); |
| 673 | 741 | style_submenu_element("Top-Level", "%s", |
| 674 | 742 | 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); | |
| 679 | 745 | } |
| 680 | 746 | style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0); |
| 681 | 747 | if( zCI ){ |
| 682 | 748 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 683 | 749 | 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); | |
| 685 | 751 | } |
| 686 | 752 | } |
| 687 | 753 | if( linkTrunk ){ |
| 688 | 754 | style_submenu_element("Trunk", "%s", |
| 689 | 755 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| @@ -738,10 +804,11 @@ | ||
| 738 | 804 | tree_add_node(&sTree, zName, zUuid, mtime); |
| 739 | 805 | nFile++; |
| 740 | 806 | } |
| 741 | 807 | db_finalize(&q); |
| 742 | 808 | } |
| 809 | + style_submenu_checkbox("nofiles", "Folders Only", 0, 0); | |
| 743 | 810 | |
| 744 | 811 | if( showDirOnly ){ |
| 745 | 812 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 746 | 813 | if( p->pChild!=0 && p->nFullName>nD ) nFile++; |
| 747 | 814 | } |
| @@ -748,18 +815,24 @@ | ||
| 748 | 815 | zObjType = "Folders"; |
| 749 | 816 | }else{ |
| 750 | 817 | zObjType = "Files"; |
| 751 | 818 | } |
| 752 | 819 | |
| 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 | + } | |
| 761 | 834 | }else{ |
| 762 | 835 | int n = db_int(0, "SELECT count(*) FROM plink"); |
| 763 | 836 | @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) |
| 764 | 837 | } |
| 765 | 838 | if( useMtime ){ |
| @@ -817,11 +890,11 @@ | ||
| 817 | 890 | nDir++; |
| 818 | 891 | }else if( !showDirOnly ){ |
| 819 | 892 | const char *zFileClass = fileext_class(p->zName); |
| 820 | 893 | char *zLink; |
| 821 | 894 | if( zCI ){ |
| 822 | - zLink = href("%R/artifact/%!S",p->zUuid); | |
| 895 | + zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI); | |
| 823 | 896 | }else{ |
| 824 | 897 | zLink = href("%R/finfo?name=%T",p->zFullName); |
| 825 | 898 | } |
| 826 | 899 | @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline"> |
| 827 | 900 | @ %z(zLink)%h(p->zName)</a> |
| @@ -911,11 +984,11 @@ | ||
| 911 | 984 | ** mtime on that check-in. If zGlob and *zGlob then only files matching |
| 912 | 985 | ** the given glob are computed. |
| 913 | 986 | */ |
| 914 | 987 | int compute_fileage(int vid, const char* zGlob){ |
| 915 | 988 | Stmt q; |
| 916 | - db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/); | |
| 989 | + db_exec_sql(zComputeFileAgeSetup); | |
| 917 | 990 | db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/); |
| 918 | 991 | db_bind_int(&q, ":ckin", vid); |
| 919 | 992 | db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*"); |
| 920 | 993 | db_exec(&q); |
| 921 | 994 | db_finalize(&q); |
| @@ -994,10 +1067,11 @@ | ||
| 994 | 1067 | int rid; |
| 995 | 1068 | const char *zName; |
| 996 | 1069 | const char *zGlob; |
| 997 | 1070 | const char *zUuid; |
| 998 | 1071 | const char *zNow; /* Time of check-in */ |
| 1072 | + int isBranchCI; /* name= is a branch name */ | |
| 999 | 1073 | int showId = PB("showid"); |
| 1000 | 1074 | Stmt q1, q2; |
| 1001 | 1075 | double baseTime; |
| 1002 | 1076 | login_check_credentials(); |
| 1003 | 1077 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| @@ -1007,28 +1081,34 @@ | ||
| 1007 | 1081 | rid = symbolic_name_to_rid(zName, "ci"); |
| 1008 | 1082 | if( rid==0 ){ |
| 1009 | 1083 | fossil_fatal("not a valid check-in: %s", zName); |
| 1010 | 1084 | } |
| 1011 | 1085 | zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1086 | + isBranchCI = branch_includes_uuid(zName,zUuid); | |
| 1012 | 1087 | baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); |
| 1013 | 1088 | zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event" |
| 1014 | 1089 | " WHERE objid=%d", rid); |
| 1015 | 1090 | style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName); |
| 1016 | 1091 | style_header("File Ages"); |
| 1017 | 1092 | zGlob = P("glob"); |
| 1018 | 1093 | compute_fileage(rid,zGlob); |
| 1019 | 1094 | db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); |
| 1020 | 1095 | |
| 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 | + } | |
| 1023 | 1104 | if( zGlob && zGlob[0] ){ |
| 1024 | 1105 | @ that match "%h(zGlob)" |
| 1025 | 1106 | } |
| 1026 | 1107 | @ ordered by age</h1> |
| 1027 | 1108 | @ |
| 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 | |
| 1030 | 1110 | @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
| 1031 | 1111 | @ |
| 1032 | 1112 | @ <div class='fileage'><table> |
| 1033 | 1113 | @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr> |
| 1034 | 1114 | db_prepare(&q1, |
| @@ -1043,14 +1123,13 @@ | ||
| 1043 | 1123 | " AND blob.rid=event.objid\n" |
| 1044 | 1124 | " ORDER BY event.mtime DESC;", |
| 1045 | 1125 | TAG_BRANCH |
| 1046 | 1126 | ); |
| 1047 | 1127 | 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" | |
| 1050 | 1130 | " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid" |
| 1051 | - " AND blob.rid=fileage.fid;" | |
| 1052 | 1131 | ); |
| 1053 | 1132 | while( db_step(&q1)==SQLITE_ROW ){ |
| 1054 | 1133 | double age = baseTime - db_column_double(&q1, 0); |
| 1055 | 1134 | int mid = db_column_int(&q1, 1); |
| 1056 | 1135 | const char *zUuid = db_column_text(&q1, 2); |
| @@ -1060,24 +1139,24 @@ | ||
| 1060 | 1139 | char *zAge = human_readable_age(age); |
| 1061 | 1140 | @ <tr><td>%s(zAge)</td> |
| 1062 | 1141 | @ <td> |
| 1063 | 1142 | db_bind_int(&q2, ":mid", mid); |
| 1064 | 1143 | 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> \ | |
| 1068 | 1146 | 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 /> | |
| 1070 | 1149 | }else{ |
| 1071 | - @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br /> | |
| 1150 | + @ </a><br /> | |
| 1072 | 1151 | } |
| 1073 | 1152 | } |
| 1074 | 1153 | db_reset(&q2); |
| 1075 | 1154 | @ </td> |
| 1076 | 1155 | @ <td> |
| 1077 | 1156 | @ %W(zComment) |
| 1078 | - @ (check-in: %z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>, | |
| 1157 | + @ (check-in: %z(href("%R/info/%!S",zUuid))%S(zUuid)</a>, | |
| 1079 | 1158 | if( showId ){ |
| 1080 | 1159 | @ id: %d(mid) |
| 1081 | 1160 | } |
| 1082 | 1161 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1083 | 1162 | @ branch: \ |
| 1084 | 1163 |
| --- 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 | @ %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 | @ %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: %z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>, |
| 1079 | if( showId ){ |
| 1080 | @ id: %d(mid) |
| 1081 | } |
| 1082 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1083 | @ branch: \ |
| 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: %z(href("%R/info/%!S",zUuid))%S(zUuid)</a>, |
| 1158 | if( showId ){ |
| 1159 | @ id: %d(mid) |
| 1160 | } |
| 1161 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1162 | @ branch: \ |
| 1163 |
+10
-2
| --- src/builtin.c | ||
| +++ src/builtin.c | ||
| @@ -55,17 +55,25 @@ | ||
| 55 | 55 | return (char*)builtin_file(zFilename, 0); |
| 56 | 56 | } |
| 57 | 57 | |
| 58 | 58 | /* |
| 59 | 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. | |
| 60 | 63 | ** |
| 61 | 64 | ** List the names and sizes of all built-in resources. |
| 62 | 65 | */ |
| 63 | 66 | void test_builtin_list(void){ |
| 64 | - int i; | |
| 67 | + int i, size = 0;; | |
| 65 | 68 | 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); | |
| 67 | 75 | } |
| 68 | 76 | } |
| 69 | 77 | |
| 70 | 78 | /* |
| 71 | 79 | ** WEBPAGE: test-builtin-files |
| 72 | 80 |
| --- 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 @@ | ||
| 25 | 25 | ** SQL code used to initialize the schema of a bundle. |
| 26 | 26 | ** |
| 27 | 27 | ** The bblob.delta field can be an integer, a text string, or NULL. |
| 28 | 28 | ** If an integer, then the corresponding blobid is the delta basis. |
| 29 | 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 | |
| 30 | +** basis, which is presumably in the main repository. If NULL, then | |
| 31 | 31 | ** data contains content without delta compression. |
| 32 | 32 | */ |
| 33 | 33 | static const char zBundleInit[] = |
| 34 | 34 | @ CREATE TABLE IF NOT EXISTS "%w".bconfig( |
| 35 | 35 | @ bcname TEXT, |
| @@ -530,19 +530,19 @@ | ||
| 530 | 530 | blob_reset(&h1); |
| 531 | 531 | bag_remove(&busy, blobid); |
| 532 | 532 | db_finalize(&q); |
| 533 | 533 | } |
| 534 | 534 | |
| 535 | -/* fossil bundle cat BUNDLE UUID... | |
| 535 | +/* fossil bundle cat BUNDLE HASH... | |
| 536 | 536 | ** |
| 537 | 537 | ** Write elements of a bundle on standard output |
| 538 | 538 | */ |
| 539 | 539 | static void bundle_cat_cmd(void){ |
| 540 | 540 | int i; |
| 541 | 541 | Blob x; |
| 542 | 542 | verify_all_options(); |
| 543 | - if( g.argc<5 ) usage("cat BUNDLE UUID..."); | |
| 543 | + if( g.argc<5 ) usage("cat BUNDLE HASH..."); | |
| 544 | 544 | bundle_attach_file(g.argv[3], "b1", 1); |
| 545 | 545 | blob_zero(&x); |
| 546 | 546 | for(i=4; i<g.argc; i++){ |
| 547 | 547 | int blobid = db_int(0,"SELECT blobid FROM bblob WHERE uuid LIKE '%q%%'", |
| 548 | 548 | g.argv[i]); |
| @@ -720,23 +720,23 @@ | ||
| 720 | 720 | /* |
| 721 | 721 | ** COMMAND: bundle |
| 722 | 722 | ** |
| 723 | 723 | ** Usage: %fossil bundle SUBCOMMAND ARGS... |
| 724 | 724 | ** |
| 725 | -** fossil bundle append BUNDLE FILE... | |
| 725 | +** > fossil bundle append BUNDLE FILE... | |
| 726 | 726 | ** |
| 727 | 727 | ** Add files named on the command line to BUNDLE. This subcommand has |
| 728 | 728 | ** little practical use and is mostly intended for testing. |
| 729 | 729 | ** |
| 730 | -** fossil bundle cat BUNDLE UUID... | |
| 730 | +** > fossil bundle cat BUNDLE HASH... | |
| 731 | 731 | ** |
| 732 | 732 | ** Extract one or more artifacts from the bundle and write them |
| 733 | 733 | ** consecutively on standard output. This subcommand was designed |
| 734 | 734 | ** for testing and introspection of bundles and is not something |
| 735 | 735 | ** commonly used. |
| 736 | 736 | ** |
| 737 | -** fossil bundle export BUNDLE ?OPTIONS? | |
| 737 | +** > fossil bundle export BUNDLE ?OPTIONS? | |
| 738 | 738 | ** |
| 739 | 739 | ** Generate a new bundle, in the file named BUNDLE, that contains a |
| 740 | 740 | ** subset of the check-ins in the repository (usually a single branch) |
| 741 | 741 | ** described by the --branch, --from, --to, and/or --checkin options, |
| 742 | 742 | ** at least one of which is required. If BUNDLE already exists, the |
| @@ -744,37 +744,37 @@ | ||
| 744 | 744 | ** |
| 745 | 745 | ** --branch BRANCH Package all check-ins on BRANCH. |
| 746 | 746 | ** --from TAG1 --to TAG2 Package check-ins between TAG1 and TAG2. |
| 747 | 747 | ** --checkin TAG Package the single check-in TAG |
| 748 | 748 | ** --standalone Do no use delta-encoding against |
| 749 | -** artifacts not in the bundle | |
| 749 | +** artifacts not in the bundle | |
| 750 | 750 | ** |
| 751 | -** fossil bundle extend BUNDLE | |
| 751 | +** > fossil bundle extend BUNDLE | |
| 752 | 752 | ** |
| 753 | 753 | ** The BUNDLE must already exist. This subcommand adds to the bundle |
| 754 | 754 | ** any check-ins that are descendants of check-ins already in the bundle, |
| 755 | 755 | ** and any tags that apply to artifacts in the bundle. |
| 756 | 756 | ** |
| 757 | -** fossil bundle import BUNDLE ?--publish? | |
| 757 | +** > fossil bundle import BUNDLE ?--publish? | |
| 758 | 758 | ** |
| 759 | 759 | ** Import all content from BUNDLE into the repository. By default, the |
| 760 | 760 | ** imported files are private and will not sync. Use the --publish |
| 761 | 761 | ** option to make the import public. |
| 762 | 762 | ** |
| 763 | -** fossil bundle ls BUNDLE | |
| 763 | +** > fossil bundle ls BUNDLE | |
| 764 | 764 | ** |
| 765 | 765 | ** List the contents of BUNDLE on standard output |
| 766 | 766 | ** |
| 767 | -** fossil bundle purge BUNDLE | |
| 767 | +** > fossil bundle purge BUNDLE | |
| 768 | 768 | ** |
| 769 | 769 | ** Remove from the repository all files that are used exclusively |
| 770 | 770 | ** by check-ins in BUNDLE. This has the effect of undoing a |
| 771 | 771 | ** "fossil bundle import". |
| 772 | 772 | ** |
| 773 | 773 | ** SUMMARY: |
| 774 | 774 | ** 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 | |
| 776 | 776 | ** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE |
| 777 | 777 | ** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include |
| 778 | 778 | ** --checkin TAG Use only check-in TAG |
| 779 | 779 | ** --standalone Omit dependencies |
| 780 | 780 | ** fossil bundle extend BUNDLE Update with newer content |
| 781 | 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 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 @@ | ||
| 100 | 100 | void capability_expand(CapabilityString *pIn){ |
| 101 | 101 | static char *zNobody = 0; |
| 102 | 102 | static char *zAnon = 0; |
| 103 | 103 | static char *zReader = 0; |
| 104 | 104 | static char *zDev = 0; |
| 105 | - static char *zAdmin = "bcdefghijklmnopqrtwxz234567AD"; | |
| 105 | + static char *zAdmin = "bcdefghijklmnopqrtwz234567AD"; | |
| 106 | 106 | int doneV = 0; |
| 107 | 107 | |
| 108 | 108 | if( pIn==0 ){ |
| 109 | 109 | fossil_free(zNobody); zNobody = 0; |
| 110 | 110 | fossil_free(zAnon); zAnon = 0; |
| @@ -239,15 +239,17 @@ | ||
| 239 | 239 | char *zOneLiner; /* One-line summary */ |
| 240 | 240 | } aCap[] = { |
| 241 | 241 | { 'a', CAPCLASS_SUPER, 0, |
| 242 | 242 | "Admin", "Create and delete users" }, |
| 243 | 243 | { 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0, |
| 244 | - "Attach", "Add attchments to wiki or tickets" }, | |
| 244 | + "Attach", "Add attachments to wiki or tickets" }, | |
| 245 | 245 | { 'c', CAPCLASS_TKT, 0, |
| 246 | 246 | "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 | + */ | |
| 249 | 251 | { 'e', CAPCLASS_DATA, 0, |
| 250 | 252 | "View-PII", "View sensitive info such as email addresses" }, |
| 251 | 253 | { 'f', CAPCLASS_WIKI, 0, |
| 252 | 254 | "New-Wiki", "Create new wiki pages" }, |
| 253 | 255 | { 'g', CAPCLASS_DATA, 0, |
| @@ -297,11 +299,11 @@ | ||
| 297 | 299 | { '4', CAPCLASS_FORUM, 0, |
| 298 | 300 | "Forum-Trusted", "Create forum messages that bypass moderation" }, |
| 299 | 301 | { '5', CAPCLASS_FORUM|CAPCLASS_SUPER, 0, |
| 300 | 302 | "Forum-Mod", "Moderator for forum messages" }, |
| 301 | 303 | { '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" }, | |
| 303 | 305 | { '7', CAPCLASS_ALERT, 0, |
| 304 | 306 | "Alerts", "Sign up for email alerts" }, |
| 305 | 307 | { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0, |
| 306 | 308 | "Announce", "Send announcements to all subscribers" }, |
| 307 | 309 | { 'D', CAPCLASS_OTHER, 0, |
| @@ -359,25 +361,38 @@ | ||
| 359 | 361 | ** Generate a "capability summary table" that shows the major capabilities |
| 360 | 362 | ** against the various user categories. |
| 361 | 363 | */ |
| 362 | 364 | void capability_summary(void){ |
| 363 | 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 | + | |
| 364 | 376 | db_prepare(&q, |
| 365 | 377 | "WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3)," |
| 366 | 378 | "('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" | |
| 368 | 381 | " FROM t LEFT JOIN user ON t.id=user.login" |
| 369 | 382 | " 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" | |
| 371 | 386 | " UNION ALL" |
| 372 | - " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user" | |
| 387 | + " SELECT 'Regular User', fullcap(capunion(cap)), 200, count(*) FROM user" | |
| 373 | 388 | " WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)" |
| 374 | 389 | " UNION ALL" |
| 375 | - " SELECT 'Adminstrator', fullcap(capunion(cap)), 30, count(*) FROM user" | |
| 390 | + " SELECT 'Adminstrator', fullcap(capunion(cap)), 300, count(*) FROM user" | |
| 376 | 391 | " WHERE cap GLOB '*[as]*'" |
| 377 | 392 | " ORDER BY 3 ASC", |
| 378 | - db_get("default-perms","") | |
| 393 | + zSelfCap, hasPubPages, zSelfCap | |
| 379 | 394 | ); |
| 380 | 395 | @ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1"> |
| 381 | 396 | @ <tr><th> <th>Code<th>Forum<th>Tickets<th>Wiki\ |
| 382 | 397 | @ <th>Unversioned Content</th></tr> |
| 383 | 398 | while( db_step(&q)==SQLITE_ROW ){ |
| @@ -384,11 +399,12 @@ | ||
| 384 | 399 | const char *zId = db_column_text(&q, 0); |
| 385 | 400 | const char *zCap = db_column_text(&q, 1); |
| 386 | 401 | int n = db_column_int(&q, 3); |
| 387 | 402 | int eType; |
| 388 | 403 | 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" }; | |
| 390 | 406 | |
| 391 | 407 | if( n==0 ) continue; |
| 392 | 408 | |
| 393 | 409 | /* Code */ |
| 394 | 410 | if( db_column_int(&q,2)<10 ){ |
| @@ -416,11 +432,11 @@ | ||
| 416 | 432 | eType = 0; |
| 417 | 433 | } |
| 418 | 434 | @ <td class="%s(azClass[eType])">%s(azType[eType])</td> |
| 419 | 435 | |
| 420 | 436 | /* Ticket */ |
| 421 | - if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){ | |
| 437 | + if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){ | |
| 422 | 438 | eType = 2; |
| 423 | 439 | }else if( sqlite3_strglob("*r*",zCap)==0 ){ |
| 424 | 440 | eType = 1; |
| 425 | 441 | }else{ |
| 426 | 442 | eType = 0; |
| 427 | 443 |
| --- 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> <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> <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 |
+95
| --- src/captcha.c | ||
| +++ src/captcha.c | ||
| @@ -548,12 +548,32 @@ | ||
| 548 | 548 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)" /> |
| 549 | 549 | @ <input type="text" name="captcha" size=8 /> |
| 550 | 550 | if( showButton ){ |
| 551 | 551 | @ <input type="submit" value="Submit"> |
| 552 | 552 | } |
| 553 | + @ <br/>\ | |
| 554 | + captcha_speakit_button(uSeed, 0); | |
| 553 | 555 | @ </td></tr></table></div> |
| 554 | 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 | +} | |
| 555 | 575 | |
| 556 | 576 | /* |
| 557 | 577 | ** WEBPAGE: test-captcha |
| 558 | 578 | ** Test the captcha-generator by rendering the value of the name= query |
| 559 | 579 | ** parameter using ascii-art. If name= is omitted, show a random 16-digit |
| @@ -608,5 +628,80 @@ | ||
| 608 | 628 | captcha_generate(1); |
| 609 | 629 | @ </form> |
| 610 | 630 | style_footer(); |
| 611 | 631 | return 1; |
| 612 | 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 | +} | |
| 613 | 708 |
| --- 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 @@ | ||
| 64 | 64 | #define PDT(x,y) cgi_parameter_trimmed((x),(y)) |
| 65 | 65 | #define PB(x) cgi_parameter_boolean(x) |
| 66 | 66 | #define PCK(x) cgi_parameter_checked(x,1) |
| 67 | 67 | #define PIF(x,y) cgi_parameter_checked(x,y) |
| 68 | 68 | |
| 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 | |
| 69 | 80 | |
| 70 | 81 | /* |
| 71 | 82 | ** Destinations for output text. |
| 72 | 83 | */ |
| 73 | 84 | #define CGI_HEADER 0 |
| @@ -144,10 +155,17 @@ | ||
| 144 | 155 | ** Return a pointer to the CGI output blob. |
| 145 | 156 | */ |
| 146 | 157 | Blob *cgi_output_blob(void){ |
| 147 | 158 | return pContent; |
| 148 | 159 | } |
| 160 | + | |
| 161 | +/* | |
| 162 | +** Return complete text of the output header | |
| 163 | +*/ | |
| 164 | +const char *cgi_header(void){ | |
| 165 | + return blob_str(&cgiContent[0]); | |
| 166 | +} | |
| 149 | 167 | |
| 150 | 168 | /* |
| 151 | 169 | ** Combine the header and body of the CGI into a single string. |
| 152 | 170 | */ |
| 153 | 171 | static void cgi_combine_header_and_body(void){ |
| @@ -167,14 +185,16 @@ | ||
| 167 | 185 | } |
| 168 | 186 | |
| 169 | 187 | /* |
| 170 | 188 | ** Additional information used to form the HTTP reply |
| 171 | 189 | */ |
| 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 */ | |
| 174 | 192 | static int iReplyStatus = 200; /* Reply status code */ |
| 175 | 193 | 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 */ | |
| 176 | 196 | |
| 177 | 197 | /* |
| 178 | 198 | ** Set the reply content type |
| 179 | 199 | */ |
| 180 | 200 | void cgi_set_content_type(const char *zType){ |
| @@ -202,10 +222,16 @@ | ||
| 202 | 222 | /* |
| 203 | 223 | ** Append text to the header of an HTTP reply |
| 204 | 224 | */ |
| 205 | 225 | void cgi_append_header(const char *zLine){ |
| 206 | 226 | 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); | |
| 207 | 233 | } |
| 208 | 234 | |
| 209 | 235 | /* |
| 210 | 236 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 211 | 237 | ** !g.isHTTP, this is a no-op. |
| @@ -259,22 +285,30 @@ | ||
| 259 | 285 | iReplyStatus = 200; |
| 260 | 286 | zReplyStatus = "OK"; |
| 261 | 287 | } |
| 262 | 288 | |
| 263 | 289 | 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 | + } | |
| 264 | 297 | fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); |
| 265 | 298 | fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); |
| 266 | 299 | fprintf(g.httpOut, "Connection: close\r\n"); |
| 267 | 300 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 268 | 301 | }else{ |
| 302 | + assert( rangeEnd==0 ); | |
| 269 | 303 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 270 | 304 | } |
| 271 | 305 | if( g.isConst ){ |
| 272 | 306 | /* isConst means that the reply is guaranteed to be invariant, even |
| 273 | 307 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 274 | 308 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 275 | - }else if( etag_tag()!=0 ){ | |
| 309 | + }else if( etag_tag()[0]!=0 ){ | |
| 276 | 310 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 277 | 311 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 278 | 312 | }else{ |
| 279 | 313 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 280 | 314 | } |
| @@ -306,18 +340,18 @@ | ||
| 306 | 340 | */ |
| 307 | 341 | |
| 308 | 342 | /* Content intended for logged in users should only be cached in |
| 309 | 343 | ** the browser, not some shared location. |
| 310 | 344 | */ |
| 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 ){ | |
| 319 | 353 | int i; |
| 320 | 354 | gzip_begin(0); |
| 321 | 355 | for( i=0; i<2; i++ ){ |
| 322 | 356 | int size = blob_size(&cgiContent[i]); |
| 323 | 357 | if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size); |
| @@ -326,28 +360,42 @@ | ||
| 326 | 360 | gzip_finish(&cgiContent[0]); |
| 327 | 361 | fprintf(g.httpOut, "Content-Encoding: gzip\r\n"); |
| 328 | 362 | fprintf(g.httpOut, "Vary: Accept-Encoding\r\n"); |
| 329 | 363 | } |
| 330 | 364 | 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 | + } | |
| 331 | 370 | fprintf(g.httpOut, "Content-Length: %d\r\n", total_size); |
| 332 | 371 | }else{ |
| 333 | 372 | total_size = 0; |
| 334 | 373 | } |
| 335 | 374 | fprintf(g.httpOut, "\r\n"); |
| 336 | - if( total_size>0 && iReplyStatus != 304 | |
| 375 | + if( total_size>0 | |
| 376 | + && iReplyStatus!=304 | |
| 337 | 377 | && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 |
| 338 | 378 | ){ |
| 339 | 379 | int i, size; |
| 340 | 380 | for(i=0; i<2; i++){ |
| 341 | 381 | 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; | |
| 344 | 392 | } |
| 345 | 393 | } |
| 346 | 394 | } |
| 347 | 395 | fflush(g.httpOut); |
| 348 | - CGIDEBUG(("DONE\n")); | |
| 396 | + CGIDEBUG(("-------- END cgi ---------\n")); | |
| 349 | 397 | |
| 350 | 398 | /* After the webpage has been sent, do any useful background |
| 351 | 399 | ** processing. |
| 352 | 400 | */ |
| 353 | 401 | g.cgiOutput = 2; |
| @@ -396,10 +444,31 @@ | ||
| 396 | 444 | va_list ap; |
| 397 | 445 | va_start(ap, zFormat); |
| 398 | 446 | cgi_redirect(vmprintf(zFormat, ap)); |
| 399 | 447 | va_end(ap); |
| 400 | 448 | } |
| 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 | +} | |
| 401 | 470 | |
| 402 | 471 | /* |
| 403 | 472 | ** Return the URL for the caller. This is obtained from either the |
| 404 | 473 | ** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter. |
| 405 | 474 | ** If neither exist, return zDefault. |
| @@ -482,10 +551,31 @@ | ||
| 482 | 551 | } |
| 483 | 552 | |
| 484 | 553 | /* |
| 485 | 554 | ** Add another query parameter or cookie to the parameter set. |
| 486 | 555 | ** 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 | |
| 487 | 577 | ** is its fully decoded value. |
| 488 | 578 | ** |
| 489 | 579 | ** Copies are made of both the zName and zValue parameters. |
| 490 | 580 | */ |
| 491 | 581 | void cgi_set_parameter(const char *zName, const char *zValue){ |
| @@ -516,10 +606,15 @@ | ||
| 516 | 606 | assert( aParamQP[i].isQP ); |
| 517 | 607 | return; |
| 518 | 608 | } |
| 519 | 609 | } |
| 520 | 610 | 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); | |
| 521 | 616 | } |
| 522 | 617 | |
| 523 | 618 | /* |
| 524 | 619 | ** Delete a parameter. |
| 525 | 620 | */ |
| @@ -554,11 +649,10 @@ | ||
| 554 | 649 | ** must be made of zValue. |
| 555 | 650 | */ |
| 556 | 651 | void cgi_setenv(const char *zName, const char *zValue){ |
| 557 | 652 | cgi_set_parameter_nocopy(zName, mprintf("%s",zValue), 0); |
| 558 | 653 | } |
| 559 | - | |
| 560 | 654 | |
| 561 | 655 | /* |
| 562 | 656 | ** Add a list of query parameters or cookies to the parameter set. |
| 563 | 657 | ** |
| 564 | 658 | ** Each parameter is of the form NAME=VALUE. Both the NAME and the |
| @@ -608,12 +702,16 @@ | ||
| 608 | 702 | dehttpize(zValue); |
| 609 | 703 | }else{ |
| 610 | 704 | if( *z ){ *z++ = 0; } |
| 611 | 705 | zValue = ""; |
| 612 | 706 | } |
| 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 | + } | |
| 615 | 713 | } |
| 616 | 714 | #ifdef FOSSIL_ENABLE_JSON |
| 617 | 715 | json_setenv( zName, cson_value_new_string(zValue,strlen(zValue)) ); |
| 618 | 716 | #endif /* FOSSIL_ENABLE_JSON */ |
| 619 | 717 | } |
| @@ -646,36 +744,36 @@ | ||
| 646 | 744 | return z; |
| 647 | 745 | } |
| 648 | 746 | |
| 649 | 747 | /* |
| 650 | 748 | ** 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 | |
| 653 | 751 | ** in *pz. |
| 654 | 752 | ** |
| 655 | 753 | ** This routine adds a "\000" to the end of the content (overwriting |
| 656 | 754 | ** 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. | |
| 658 | 756 | ** The length of the content is stored in *pnContent. |
| 659 | 757 | */ |
| 660 | 758 | static char *get_bounded_content( |
| 661 | 759 | char **pz, /* Content taken from here */ |
| 662 | 760 | 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 */ | |
| 664 | 762 | int *pnContent /* Write the size of the content here */ |
| 665 | 763 | ){ |
| 666 | 764 | char *z = *pz; |
| 667 | 765 | int len = *pLen; |
| 668 | 766 | int i; |
| 669 | - int nBoundry = strlen(zBoundry); | |
| 767 | + int nBoundary = strlen(zBoundary); | |
| 670 | 768 | *pnContent = len; |
| 671 | 769 | 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 ){ | |
| 673 | 771 | if( i>0 && z[i-1]=='\r' ) i--; |
| 674 | 772 | z[i] = 0; |
| 675 | 773 | *pnContent = i; |
| 676 | - i += nBoundry; | |
| 774 | + i += nBoundary; | |
| 677 | 775 | break; |
| 678 | 776 | } |
| 679 | 777 | } |
| 680 | 778 | *pz = &z[i]; |
| 681 | 779 | get_line_from_string(pz, pLen); |
| @@ -740,27 +838,35 @@ | ||
| 740 | 838 | ** table. |
| 741 | 839 | */ |
| 742 | 840 | static void process_multipart_form_data(char *z, int len){ |
| 743 | 841 | char *zLine; |
| 744 | 842 | int nArg, i; |
| 745 | - char *zBoundry; | |
| 843 | + char *zBoundary; | |
| 746 | 844 | char *zValue; |
| 747 | 845 | char *zName = 0; |
| 748 | 846 | int showBytes = 0; |
| 749 | 847 | char *azArg[50]; |
| 750 | 848 | |
| 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; | |
| 753 | 851 | while( (zLine = get_line_from_string(&z, &len))!=0 ){ |
| 754 | 852 | if( zLine[0]==0 ){ |
| 755 | 853 | 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 | + } | |
| 762 | 868 | } |
| 763 | 869 | } |
| 764 | 870 | zName = 0; |
| 765 | 871 | showBytes = 0; |
| 766 | 872 | }else{ |
| @@ -772,18 +878,28 @@ | ||
| 772 | 878 | i++; |
| 773 | 879 | }else if( c=='n' && sqlite3_strnicmp(azArg[i],"name=",n)==0 ){ |
| 774 | 880 | zName = azArg[++i]; |
| 775 | 881 | }else if( c=='f' && sqlite3_strnicmp(azArg[i],"filename=",n)==0 ){ |
| 776 | 882 | 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 | + } | |
| 779 | 890 | } |
| 780 | 891 | showBytes = 1; |
| 781 | 892 | }else if( c=='c' && sqlite3_strnicmp(azArg[i],"content-type:",n)==0 ){ |
| 782 | 893 | 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 | + } | |
| 785 | 901 | } |
| 786 | 902 | } |
| 787 | 903 | } |
| 788 | 904 | } |
| 789 | 905 | } |
| @@ -938,21 +1054,40 @@ | ||
| 938 | 1054 | ** PATH_INFO when it is empty. |
| 939 | 1055 | */ |
| 940 | 1056 | void cgi_init(void){ |
| 941 | 1057 | char *z; |
| 942 | 1058 | const char *zType; |
| 1059 | + char *zSemi; | |
| 943 | 1060 | int len; |
| 944 | 1061 | const char *zRequestUri = cgi_parameter("REQUEST_URI",0); |
| 945 | 1062 | const char *zScriptName = cgi_parameter("SCRIPT_NAME",0); |
| 946 | 1063 | const char *zPathInfo = cgi_parameter("PATH_INFO",0); |
| 1064 | +#ifdef _WIN32 | |
| 1065 | + const char *zServerSoftware = cgi_parameter("SERVER_SOFTWARE",0); | |
| 1066 | +#endif | |
| 947 | 1067 | |
| 948 | 1068 | #ifdef FOSSIL_ENABLE_JSON |
| 949 | - json_main_bootstrap(); | |
| 1069 | + int noJson = P("no_json")!=0; | |
| 1070 | + if( noJson==0 ){ json_main_bootstrap(); } | |
| 950 | 1071 | #endif |
| 951 | 1072 | g.isHTTP = 1; |
| 952 | 1073 | cgi_destination(CGI_BODY); |
| 953 | 1074 | 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 | |
| 954 | 1089 | if( zRequestUri==0 ){ |
| 955 | 1090 | const char *z = zPathInfo; |
| 956 | 1091 | if( zPathInfo==0 ){ |
| 957 | 1092 | malformed_request("missing PATH_INFO and/or REQUEST_URI"); |
| 958 | 1093 | } |
| @@ -962,13 +1097,27 @@ | ||
| 962 | 1097 | } |
| 963 | 1098 | if( zPathInfo==0 ){ |
| 964 | 1099 | int i, j; |
| 965 | 1100 | for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){} |
| 966 | 1101 | 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"); | |
| 968 | 1117 | } |
| 969 | - | |
| 1118 | +#endif | |
| 970 | 1119 | z = (char*)P("HTTP_COOKIE"); |
| 971 | 1120 | if( z ){ |
| 972 | 1121 | z = mprintf("%s",z); |
| 973 | 1122 | add_param_list(z, ';'); |
| 974 | 1123 | } |
| @@ -983,37 +1132,33 @@ | ||
| 983 | 1132 | if( z ){ |
| 984 | 1133 | g.zIpAddr = mprintf("%s", z); |
| 985 | 1134 | } |
| 986 | 1135 | |
| 987 | 1136 | 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 | + } | |
| 989 | 1145 | blob_zero(&g.cgiIn); |
| 990 | 1146 | if( len>0 && zType ){ |
| 991 | 1147 | if( fossil_strcmp(zType, "application/x-fossil")==0 ){ |
| 992 | 1148 | blob_read_from_channel(&g.cgiIn, g.httpIn, len); |
| 993 | 1149 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 994 | 1150 | } |
| 995 | 1151 | #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 ){ | |
| 1000 | 1154 | 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. | |
| 1015 | 1160 | */ |
| 1016 | 1161 | cgi_set_content_type(json_guess_content_type()); |
| 1017 | 1162 | } |
| 1018 | 1163 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1019 | 1164 | else{ |
| @@ -1087,10 +1232,14 @@ | ||
| 1087 | 1232 | } |
| 1088 | 1233 | j++; |
| 1089 | 1234 | } |
| 1090 | 1235 | nUsedQP = j; |
| 1091 | 1236 | } |
| 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; | |
| 1092 | 1241 | |
| 1093 | 1242 | /* Do a binary search for a matching query parameter */ |
| 1094 | 1243 | lo = 0; |
| 1095 | 1244 | hi = nUsedQP-1; |
| 1096 | 1245 | while( lo<=hi ){ |
| @@ -1106,15 +1255,16 @@ | ||
| 1106 | 1255 | } |
| 1107 | 1256 | } |
| 1108 | 1257 | |
| 1109 | 1258 | /* If no match is found and the name begins with an upper-case |
| 1110 | 1259 | ** 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. | |
| 1112 | 1262 | */ |
| 1113 | - if( zName && fossil_isupper(zName[0]) ){ | |
| 1263 | + if( fossil_isupper(zName[0]) ){ | |
| 1114 | 1264 | const char *zValue = fossil_getenv(zName); |
| 1115 | - if( zValue ){ | |
| 1265 | + if( zValue && zValue[0] ){ | |
| 1116 | 1266 | cgi_set_parameter_nocopy(zName, zValue, 0); |
| 1117 | 1267 | CGIDEBUG(("env-match [%s] = [%s]\n", zName, zValue)); |
| 1118 | 1268 | return zValue; |
| 1119 | 1269 | } |
| 1120 | 1270 | } |
| @@ -1233,28 +1383,73 @@ | ||
| 1233 | 1383 | va_end(ap); |
| 1234 | 1384 | return 1; |
| 1235 | 1385 | } |
| 1236 | 1386 | |
| 1237 | 1387 | /* |
| 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. | |
| 1240 | 1420 | ** |
| 1241 | 1421 | ** 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 | |
| 1242 | 1428 | */ |
| 1243 | -void cgi_print_all(int showAll, int onConsole){ | |
| 1429 | +void cgi_print_all(int showAll, unsigned int eDest){ | |
| 1244 | 1430 | int i; |
| 1245 | 1431 | cgi_parameter("",""); /* Force the parameters into sorted order */ |
| 1246 | 1432 | for(i=0; i<nUsedQP; i++){ |
| 1247 | 1433 | const char *zName = aParamQP[i].zName; |
| 1248 | 1434 | if( !showAll ){ |
| 1249 | 1435 | if( fossil_stricmp("HTTP_COOKIE",zName)==0 ) continue; |
| 1250 | 1436 | if( fossil_strnicmp("fossil-",zName,7)==0 ) continue; |
| 1251 | 1437 | } |
| 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 | + } | |
| 1256 | 1451 | } |
| 1257 | 1452 | } |
| 1258 | 1453 | } |
| 1259 | 1454 | |
| 1260 | 1455 | /* |
| @@ -1473,10 +1668,11 @@ | ||
| 1473 | 1668 | } |
| 1474 | 1669 | if( zIpAddr ){ |
| 1475 | 1670 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 1476 | 1671 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1477 | 1672 | } |
| 1673 | + | |
| 1478 | 1674 | |
| 1479 | 1675 | /* Get all the optional fields that follow the first line. |
| 1480 | 1676 | */ |
| 1481 | 1677 | while( fgets(zLine,sizeof(zLine),g.httpIn) ){ |
| 1482 | 1678 | char *zFieldName; |
| @@ -1519,10 +1715,17 @@ | ||
| 1519 | 1715 | const char *zIpAddr = cgi_accept_forwarded_for(zVal); |
| 1520 | 1716 | if( zIpAddr!=0 ){ |
| 1521 | 1717 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1522 | 1718 | cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr); |
| 1523 | 1719 | } |
| 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 | + } | |
| 1524 | 1727 | } |
| 1525 | 1728 | } |
| 1526 | 1729 | cgi_init(); |
| 1527 | 1730 | cgi_trace(0); |
| 1528 | 1731 | } |
| @@ -1542,10 +1745,13 @@ | ||
| 1542 | 1745 | char *z, *zToken; |
| 1543 | 1746 | const char *zType = 0; |
| 1544 | 1747 | int i, content_length = 0; |
| 1545 | 1748 | char zLine[2000]; /* A single line of input. */ |
| 1546 | 1749 | |
| 1750 | +#ifdef FOSSIL_ENABLE_JSON | |
| 1751 | + if( nCycles==0 ){ json_main_bootstrap(); } | |
| 1752 | +#endif | |
| 1547 | 1753 | if( zIpAddr ){ |
| 1548 | 1754 | if( nCycles==0 ){ |
| 1549 | 1755 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 1550 | 1756 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1551 | 1757 | } |
| @@ -1995,10 +2201,30 @@ | ||
| 1995 | 2201 | return mprintf("%s, %d %s %02d %02d:%02d:%02d +0000", |
| 1996 | 2202 | azDays[pTm->tm_wday], pTm->tm_mday, azMonths[pTm->tm_mon], |
| 1997 | 2203 | pTm->tm_year+1900, pTm->tm_hour, pTm->tm_min, pTm->tm_sec); |
| 1998 | 2204 | } |
| 1999 | 2205 | } |
| 2206 |