Fossil SCM
Merge trunk
Commit
1e4aaf7b2e754f8cd589f25ef1cabf5aaf69fb44a2440eb059d5463239599048
Parent
9dbea18c7128a44…
187 files changed
+2
-2
+17
-10
+1
-1
+1
-1
+4
-3
+27
+452
-195
+3
-1
+1
-1
+6
+15
-2
+148
-79
+10
-2
+26
-10
+95
+107
-45
+38
-13
+1
-1
+17
-31
+7
-3
+32
+4
-1
+194
-80
+194
-80
+67
+69
-39
+36
-3
+49
-19
+43
-6
+4
+111
-7
+15
-11
+380
-62
+8
-1
+3
-1
+1
-1
+237
-122
+5
-2
+260
-225
+62
-33
+10
-1
+2
-5
+7
-8
+2
-1
+151
-115
+64
-23
+64
-23
+41
+5
-2
+63
-14
+15
+17
-9
+57
-3
+3
-2
+282
-47
+7
-3
+84
-14
+2
-1
+3
-2
+19
-1
+89
-20
+114
-73
+99
-49
+54
-17
+526
-158
+2
-2
+62
-2
+15
+20
+20
+2955
-1818
+163
-79
+1
-1
+9
-9
+75
-7
+18
-24
+1
-1
+1
-1
+9
+1
-1
+120
-25
+87
-33
+2
-2
+52
-49
+77
-20
+9
-1
+18
-14
+53
+15
+70
-34
+3
-21
+166
-61
+1
-1
+1
-1
+18
+2
+1
-1
+72
+16
-4
+42
-1
+44
-1
+38
-1
+11
-2
+2
-2
+1
-1
+54
-1
+5
-5
+11
-3
+2
-2
+80
-6
+5
-6
+35
+214
-83
+2
-2
+87
-54
+32
-44
+3
-1
+38
-26
+143
+199
-134
+5
-6
+3
-3
+48
-30
+72
+21
-73
+22
+159
+8
+86
+45
+36
+46
+53
+53
+92
+22
+59
+8
+136
+147
+142
+32
+301
+616
+10
+38
+208
+102
+2
-2
+7
-7
+38
-16
+8
-3
+18
-3
+15
-2
+4
-4
+12
-10
+91
-67
+1
-1
+5
-3
+26
-13
+7
+60
-17
+3
-3
+1
+6
-3
+3
-2
~
auto.def
~
skins/README.md
~
skins/ardoise/css.txt
~
skins/default/css.txt
~
skins/default/header.txt
~
src/accordion.js
~
src/alerts.c
~
src/allrepo.c
~
src/attach.c
~
src/backlink.c
~
src/branch.c
~
src/browse.c
~
src/builtin.c
~
src/capabilities.c
~
src/captcha.c
~
src/cgi.c
~
src/checkin.c
~
src/clone.c
~
src/comformat.c
~
src/configure.c
~
src/content.c
~
src/cookies.c
~
src/db.c
~
src/db.c
~
src/default_css.txt
~
src/diff.c
~
src/dispatch.c
~
src/doc.c
~
src/etag.c
~
src/export.c
~
src/file.c
~
src/finfo.c
~
src/forum.c
~
src/fshell.c
~
src/graph.c
~
src/http.c
~
src/http_ssl.c
~
src/import.c
~
src/info.c
~
src/json.c
~
src/json_artifact.c
~
src/json_branch.c
~
src/json_login.c
~
src/json_timeline.c
~
src/login.c
~
src/main.c
~
src/main.c
~
src/main.mk
~
src/makemake.tcl
~
src/manifest.c
~
src/merge.c
~
src/merge3.c
~
src/mkversion.c
~
src/moderate.c
~
src/name.c
~
src/printf.c
~
src/rebuild.c
~
src/repolist.c
~
src/rss.c
~
src/schema.c
~
src/search.c
~
src/security_audit.c
~
src/setup.c
~
src/setupuser.c
~
src/shell.c
~
src/shun.c
~
src/skins.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/sqlcmd.c
~
src/sqlite3.c
~
src/sqlite3.h
~
src/stash.c
~
src/stat.c
~
src/style.c
~
src/sync.c
~
src/tag.c
~
src/tar.c
~
src/terminal.c
~
src/th_main.c
~
src/timeline.c
~
src/tkt.c
~
src/undo.c
~
src/unicode.c
~
src/unversioned.c
~
src/update.c
~
src/url.c
~
src/utf8.c
~
src/util.c
~
src/wiki.c
~
src/wikiformat.c
~
src/xfer.c
~
src/xfersetup.c
~
src/zip.c
~
test/csp1.html
~
test/subdir with spaces/filename with spaces.txt
~
test/tester.tcl
~
tools/email-monitor.tcl
~
tools/encode_math.sh
~
tools/fossil-autocomplete.bash
~
tools/fossil-diff-log
~
tools/fossil-stress.tcl
~
tools/fossil_chat.tcl
~
win/Makefile.dmc
~
win/Makefile.mingw
~
win/Makefile.mingw.mistachkin
~
win/Makefile.msc
~
www/adding_code.wiki
~
www/alerts.md
~
www/antibot.wiki
~
www/build.wiki
~
www/caps/index.md
~
www/caps/ref.html
~
www/cgi.wiki
~
www/changes.wiki
~
www/checkin_names.wiki
~
www/css-tricks.md
~
www/customskin.md
~
www/defcsp.md
~
www/embeddeddoc.wiki
~
www/env-opts.md
~
www/faq.wiki
~
www/fossil-v-git.wiki
~
www/gitusers.md
~
www/globs.md
~
www/grep.md
~
www/hacker-howto.wiki
~
www/hashpolicy.wiki
~
www/history.md
~
www/index.wiki
~
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/mdtest/test1.md
~
www/mirrorlimitations.md
~
www/mkindex.tcl
~
www/permutedindex.html
~
www/private.wiki
~
www/qandc.wiki
~
www/quickstart.wiki
~
www/rebaseharm.md
~
www/server/whyuseaserver.wiki
~
www/serverext.wiki
~
www/ssl.wiki
~
www/sync.wiki
~
www/tech_overview.wiki
~
www/th1.md
~
www/userlinks.wiki
~
www/whyusefossil.wiki
~
www/wikitheory.wiki
M
auto.def
+2
-2
| --- auto.def | ||
| +++ auto.def | ||
| @@ -273,11 +273,11 @@ | ||
| 273 | 273 | |
| 274 | 274 | # Check for libraries that need to be sorted out early |
| 275 | 275 | cc-check-function-in-lib iconv iconv |
| 276 | 276 | |
| 277 | 277 | # Helper for OpenSSL checking |
| 278 | -proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto}}} { | |
| 278 | +proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto -lpthread}}} { | |
| 279 | 279 | msg-checking "Checking for $msg..." |
| 280 | 280 | set rc 0 |
| 281 | 281 | if {[is_mingw]} { |
| 282 | 282 | lappend libs -lgdi32 -lwsock32 -lcrypt32 |
| 283 | 283 | } |
| @@ -351,11 +351,11 @@ | ||
| 351 | 351 | user-error "The OpenSSL in source tree directory does not exist" |
| 352 | 352 | } |
| 353 | 353 | set msg "ssl in $ssldir" |
| 354 | 354 | set cflags "-I$ssldir/include" |
| 355 | 355 | set ldflags "-L$ssldir" |
| 356 | - set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a" | |
| 356 | + set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread" | |
| 357 | 357 | set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs] |
| 358 | 358 | } else { |
| 359 | 359 | if {$ssldirs in {auto ""}} { |
| 360 | 360 | catch { |
| 361 | 361 | set cflags [exec pkg-config openssl --cflags-only-I] |
| 362 | 362 |
| --- auto.def | |
| +++ auto.def | |
| @@ -273,11 +273,11 @@ | |
| 273 | |
| 274 | # Check for libraries that need to be sorted out early |
| 275 | cc-check-function-in-lib iconv iconv |
| 276 | |
| 277 | # Helper for OpenSSL checking |
| 278 | proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto}}} { |
| 279 | msg-checking "Checking for $msg..." |
| 280 | set rc 0 |
| 281 | if {[is_mingw]} { |
| 282 | lappend libs -lgdi32 -lwsock32 -lcrypt32 |
| 283 | } |
| @@ -351,11 +351,11 @@ | |
| 351 | user-error "The OpenSSL in source tree directory does not exist" |
| 352 | } |
| 353 | set msg "ssl in $ssldir" |
| 354 | set cflags "-I$ssldir/include" |
| 355 | set ldflags "-L$ssldir" |
| 356 | set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a" |
| 357 | set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs] |
| 358 | } else { |
| 359 | if {$ssldirs in {auto ""}} { |
| 360 | catch { |
| 361 | set cflags [exec pkg-config openssl --cflags-only-I] |
| 362 |
| --- auto.def | |
| +++ auto.def | |
| @@ -273,11 +273,11 @@ | |
| 273 | |
| 274 | # Check for libraries that need to be sorted out early |
| 275 | cc-check-function-in-lib iconv iconv |
| 276 | |
| 277 | # Helper for OpenSSL checking |
| 278 | proc check-for-openssl {msg {cflags {}} {libs {-lssl -lcrypto -lpthread}}} { |
| 279 | msg-checking "Checking for $msg..." |
| 280 | set rc 0 |
| 281 | if {[is_mingw]} { |
| 282 | lappend libs -lgdi32 -lwsock32 -lcrypt32 |
| 283 | } |
| @@ -351,11 +351,11 @@ | |
| 351 | user-error "The OpenSSL in source tree directory does not exist" |
| 352 | } |
| 353 | set msg "ssl in $ssldir" |
| 354 | set cflags "-I$ssldir/include" |
| 355 | set ldflags "-L$ssldir" |
| 356 | set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread" |
| 357 | set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs] |
| 358 | } else { |
| 359 | if {$ssldirs in {auto ""}} { |
| 360 | catch { |
| 361 | set cflags [exec pkg-config openssl --cflags-only-I] |
| 362 |
+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 |
+1
-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%; |
| 302 | 302 |
| --- 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%; |
| 302 |
| --- 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%; |
| 302 |
+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='/sitemap'>☰</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 | |
| 35 | 36 | ADDED src/accordion.js |
| --- 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='/sitemap'>☰</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 | |
| 35 | DDED src/accordion.js |
| --- 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 | |
| 36 | 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 |
+452
-195
| --- 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. |
| @@ -114,11 +115,11 @@ | ||
| 114 | 115 | if( bOnlyIfEnabled |
| 115 | 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;" |
| @@ -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 | } |
| @@ -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 */ |
| @@ -2039,20 +2261,20 @@ | ||
| 2039 | 2261 | p->needMod = db_column_int(&q, 4); |
| 2040 | 2262 | p->zFromName = 0; |
| 2041 | 2263 | p->pNext = 0; |
| 2042 | 2264 | switch( p->type ){ |
| 2043 | 2265 | case 'c': zType = "Check-In"; break; |
| 2044 | - case 'f': zType = "Forum post"; break; | |
| 2266 | + /* case 'f': -- forum posts omitted from this loop. See below */ | |
| 2045 | 2267 | case 't': zType = "Ticket Change"; break; |
| 2046 | 2268 | case 'w': zType = "Wiki Edit"; break; |
| 2047 | 2269 | } |
| 2048 | 2270 | blob_init(&p->hdr, 0, 0); |
| 2049 | 2271 | blob_init(&p->txt, 0, 0); |
| 2050 | 2272 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 2051 | 2273 | db_column_text(&q,1), |
| 2052 | 2274 | zType, |
| 2053 | - db_column_text(&q,2), | |
| 2275 | + db_column_text(&q, 2), | |
| 2054 | 2276 | zUrl, |
| 2055 | 2277 | db_column_text(&q,0) |
| 2056 | 2278 | ); |
| 2057 | 2279 | if( p->needMod ){ |
| 2058 | 2280 | blob_appendf(&p->txt, |
| @@ -2078,17 +2300,28 @@ | ||
| 2078 | 2300 | ** is a normal email alert. Construct full-text forum post alerts |
| 2079 | 2301 | ** using a format that enables them to be sent as separate emails. |
| 2080 | 2302 | */ |
| 2081 | 2303 | db_prepare(&q, |
| 2082 | 2304 | "SELECT" |
| 2083 | - " forumpost.fpid," /* 0 */ | |
| 2084 | - " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */ | |
| 2085 | - " datetime(event.mtime)," /* 2 */ | |
| 2086 | - " substr(comment,instr(comment,':')+2)," /* 3 */ | |
| 2087 | - " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */ | |
| 2088 | - " wantalert.needMod," /* 5 */ | |
| 2089 | - " 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 */ | |
| 2090 | 2323 | " FROM temp.wantalert, event, forumpost" |
| 2091 | 2324 | " LEFT JOIN user ON (login=coalesce(euser,user))" |
| 2092 | 2325 | " WHERE event.objid=substr(wantalert.eventId,2)+0" |
| 2093 | 2326 | " AND eventId GLOB 'f*'" |
| 2094 | 2327 | " AND forumpost.fpid=event.objid" |
| @@ -2104,11 +2337,11 @@ | ||
| 2104 | 2337 | const char *z; |
| 2105 | 2338 | if( pPost==0 ) continue; |
| 2106 | 2339 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 2107 | 2340 | pLast->pNext = p; |
| 2108 | 2341 | pLast = p; |
| 2109 | - p->type = 'f'; | |
| 2342 | + p->type = db_column_int(&q,7) ? 'f' : 'x'; | |
| 2110 | 2343 | p->needMod = db_column_int(&q, 5); |
| 2111 | 2344 | z = db_column_text(&q,6); |
| 2112 | 2345 | p->zFromName = z && z[0] ? fossil_strdup(z) : 0; |
| 2113 | 2346 | p->pNext = 0; |
| 2114 | 2347 | blob_init(&p->hdr, 0, 0); |
| @@ -2436,13 +2669,13 @@ | ||
| 2436 | 2669 | ** if the recipient is a moderator for that type of event. Setup |
| 2437 | 2670 | ** and Admin users always get notified. */ |
| 2438 | 2671 | char xType = '*'; |
| 2439 | 2672 | if( strpbrk(zCap,"as")==0 ){ |
| 2440 | 2673 | switch( p->type ){ |
| 2441 | - case 'f': xType = '5'; break; | |
| 2442 | - case 't': xType = 'q'; break; | |
| 2443 | - 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; | |
| 2444 | 2677 | } |
| 2445 | 2678 | if( strchr(zCap,xType)==0 ) continue; |
| 2446 | 2679 | } |
| 2447 | 2680 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2448 | 2681 | /* Setup and admin users can get any notification that does not |
| @@ -2450,14 +2683,14 @@ | ||
| 2450 | 2683 | }else{ |
| 2451 | 2684 | /* Other users only see the alert if they have sufficient |
| 2452 | 2685 | ** privilege to view the event itself */ |
| 2453 | 2686 | char xType = '*'; |
| 2454 | 2687 | switch( p->type ){ |
| 2455 | - case 'c': xType = 'o'; break; | |
| 2456 | - case 'f': xType = '2'; break; | |
| 2457 | - case 't': xType = 'r'; break; | |
| 2458 | - 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; | |
| 2459 | 2692 | } |
| 2460 | 2693 | if( strchr(zCap,xType)==0 ) continue; |
| 2461 | 2694 | } |
| 2462 | 2695 | if( blob_size(&p->hdr)>0 ){ |
| 2463 | 2696 | /* This alert should be sent as a separate email */ |
| @@ -2594,10 +2827,11 @@ | ||
| 2594 | 2827 | @ <table class="subscribe"> |
| 2595 | 2828 | if( zCaptcha ){ |
| 2596 | 2829 | @ <tr> |
| 2597 | 2830 | @ <td class="form_label">Security Code:</td> |
| 2598 | 2831 | @ <td><input type="text" name="captcha" value="" size="10"> |
| 2832 | + captcha_speakit_button(uSeed, "Speak the code"); | |
| 2599 | 2833 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 2600 | 2834 | @ </tr> |
| 2601 | 2835 | } |
| 2602 | 2836 | @ <tr> |
| 2603 | 2837 | @ <td class="form_label">Your Email Address:</td> |
| @@ -2620,11 +2854,11 @@ | ||
| 2620 | 2854 | @ </table> |
| 2621 | 2855 | if( zCaptcha ){ |
| 2622 | 2856 | @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 2623 | 2857 | @ %h(zCaptcha) |
| 2624 | 2858 | @ </pre> |
| 2625 | - @ Enter the 8 characters above in the "Security Code" box | |
| 2859 | + @ Enter the 8 characters above in the "Security Code" box<br/> | |
| 2626 | 2860 | @ </td></tr></table></div> |
| 2627 | 2861 | } |
| 2628 | 2862 | @ </form> |
| 2629 | 2863 | style_footer(); |
| 2630 | 2864 | } |
| @@ -2638,10 +2872,11 @@ | ||
| 2638 | 2872 | char *zErr; |
| 2639 | 2873 | const char *zTo = PT("to"); |
| 2640 | 2874 | char *zSubject = PT("subject"); |
| 2641 | 2875 | int bAll = PB("all"); |
| 2642 | 2876 | int bAA = PB("aa"); |
| 2877 | + int bMods = PB("mods"); | |
| 2643 | 2878 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 2644 | 2879 | int bTest2 = fossil_strcmp(P("name"),"test2")==0; |
| 2645 | 2880 | Blob hdr, body; |
| 2646 | 2881 | blob_init(&body, 0, 0); |
| 2647 | 2882 | blob_init(&hdr, 0, 0); |
| @@ -2649,17 +2884,29 @@ | ||
| 2649 | 2884 | pSender = alert_sender_new(bTest2 ? "blob" : 0, 0); |
| 2650 | 2885 | if( zTo[0] ){ |
| 2651 | 2886 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 2652 | 2887 | alert_send(pSender, &hdr, &body, 0); |
| 2653 | 2888 | } |
| 2654 | - if( bAll || bAA ){ | |
| 2889 | + if( bAll || bAA || bMods ){ | |
| 2655 | 2890 | Stmt q; |
| 2656 | 2891 | int nUsed = blob_size(&body); |
| 2657 | 2892 | const char *zURL = db_get("email-url",0); |
| 2658 | - db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber " | |
| 2659 | - " WHERE sverified AND NOT sdonotcall %s", | |
| 2660 | - 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 | + } | |
| 2661 | 2908 | while( db_step(&q)==SQLITE_ROW ){ |
| 2662 | 2909 | const char *zCode = db_column_text(&q, 1); |
| 2663 | 2910 | zTo = db_column_text(&q, 0); |
| 2664 | 2911 | blob_truncate(&hdr, 0); |
| 2665 | 2912 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| @@ -2716,11 +2963,12 @@ | ||
| 2716 | 2963 | @ <p>The following error was reported by the system: |
| 2717 | 2964 | @ <blockquote><pre> |
| 2718 | 2965 | @ %h(zErr) |
| 2719 | 2966 | @ </pre></blockquote> |
| 2720 | 2967 | }else{ |
| 2721 | - @ <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> | |
| 2722 | 2970 | } |
| 2723 | 2971 | style_footer(); |
| 2724 | 2972 | return; |
| 2725 | 2973 | } else if( !alert_enabled() ){ |
| 2726 | 2974 | style_header("Cannot Send Announcement"); |
| @@ -2734,21 +2982,26 @@ | ||
| 2734 | 2982 | @ <form method="POST"> |
| 2735 | 2983 | @ <table class="subscribe"> |
| 2736 | 2984 | if( g.perm.Admin ){ |
| 2737 | 2985 | int aa = PB("aa"); |
| 2738 | 2986 | int all = PB("all"); |
| 2987 | + int aMod = PB("mods"); | |
| 2739 | 2988 | const char *aack = aa ? "checked" : ""; |
| 2740 | 2989 | const char *allck = all ? "checked" : ""; |
| 2990 | + const char *modck = aMod ? "checked" : ""; | |
| 2741 | 2991 | @ <tr> |
| 2742 | 2992 | @ <td class="form_label">To:</td> |
| 2743 | 2993 | @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br> |
| 2744 | 2994 | @ <label><input type="checkbox" name="aa" %s(aack)> \ |
| 2745 | 2995 | @ All "announcement" subscribers</label> \ |
| 2746 | 2996 | @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br> |
| 2747 | 2997 | @ <label><input type="checkbox" name="all" %s(allck)> \ |
| 2748 | 2998 | @ All subscribers</label> \ |
| 2749 | - @ <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> | |
| 2750 | 3003 | @ </tr> |
| 2751 | 3004 | } |
| 2752 | 3005 | @ <tr> |
| 2753 | 3006 | @ <td class="form_label">Subject:</td> |
| 2754 | 3007 | @ <td><input type="text" name="subject" value="%h(PT("subject"))"\ |
| @@ -2759,11 +3012,15 @@ | ||
| 2759 | 3012 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 2760 | 3013 | @ %h(PT("msg"))</textarea> |
| 2761 | 3014 | @ </tr> |
| 2762 | 3015 | @ <tr> |
| 2763 | 3016 | @ <td></td> |
| 2764 | - @ <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 | + } | |
| 2765 | 3022 | @ </tr> |
| 2766 | 3023 | @ </table> |
| 2767 | 3024 | @ </form> |
| 2768 | 3025 | style_footer(); |
| 2769 | 3026 | } |
| 2770 | 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. |
| @@ -114,11 +115,11 @@ | |
| 114 | if( bOnlyIfEnabled |
| 115 | && fossil_strcmp(db_get("email-send-method",0),"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;" |
| @@ -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 | } |
| @@ -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 */ |
| @@ -2039,20 +2261,20 @@ | |
| 2039 | p->needMod = db_column_int(&q, 4); |
| 2040 | p->zFromName = 0; |
| 2041 | p->pNext = 0; |
| 2042 | switch( p->type ){ |
| 2043 | case 'c': zType = "Check-In"; break; |
| 2044 | case 'f': zType = "Forum post"; break; |
| 2045 | case 't': zType = "Ticket Change"; break; |
| 2046 | case 'w': zType = "Wiki Edit"; break; |
| 2047 | } |
| 2048 | blob_init(&p->hdr, 0, 0); |
| 2049 | blob_init(&p->txt, 0, 0); |
| 2050 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 2051 | db_column_text(&q,1), |
| 2052 | zType, |
| 2053 | db_column_text(&q,2), |
| 2054 | zUrl, |
| 2055 | db_column_text(&q,0) |
| 2056 | ); |
| 2057 | if( p->needMod ){ |
| 2058 | blob_appendf(&p->txt, |
| @@ -2078,17 +2300,28 @@ | |
| 2078 | ** is a normal email alert. Construct full-text forum post alerts |
| 2079 | ** using a format that enables them to be sent as separate emails. |
| 2080 | */ |
| 2081 | db_prepare(&q, |
| 2082 | "SELECT" |
| 2083 | " forumpost.fpid," /* 0 */ |
| 2084 | " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */ |
| 2085 | " datetime(event.mtime)," /* 2 */ |
| 2086 | " substr(comment,instr(comment,':')+2)," /* 3 */ |
| 2087 | " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */ |
| 2088 | " wantalert.needMod," /* 5 */ |
| 2089 | " coalesce(trim(substr(info,1,instr(info,'<')-1)),euser,user)" /* 6 */ |
| 2090 | " FROM temp.wantalert, event, forumpost" |
| 2091 | " LEFT JOIN user ON (login=coalesce(euser,user))" |
| 2092 | " WHERE event.objid=substr(wantalert.eventId,2)+0" |
| 2093 | " AND eventId GLOB 'f*'" |
| 2094 | " AND forumpost.fpid=event.objid" |
| @@ -2104,11 +2337,11 @@ | |
| 2104 | const char *z; |
| 2105 | if( pPost==0 ) continue; |
| 2106 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 2107 | pLast->pNext = p; |
| 2108 | pLast = p; |
| 2109 | p->type = 'f'; |
| 2110 | p->needMod = db_column_int(&q, 5); |
| 2111 | z = db_column_text(&q,6); |
| 2112 | p->zFromName = z && z[0] ? fossil_strdup(z) : 0; |
| 2113 | p->pNext = 0; |
| 2114 | blob_init(&p->hdr, 0, 0); |
| @@ -2436,13 +2669,13 @@ | |
| 2436 | ** if the recipient is a moderator for that type of event. Setup |
| 2437 | ** and Admin users always get notified. */ |
| 2438 | char xType = '*'; |
| 2439 | if( strpbrk(zCap,"as")==0 ){ |
| 2440 | switch( p->type ){ |
| 2441 | case 'f': xType = '5'; break; |
| 2442 | case 't': xType = 'q'; break; |
| 2443 | case 'w': xType = 'l'; break; |
| 2444 | } |
| 2445 | if( strchr(zCap,xType)==0 ) continue; |
| 2446 | } |
| 2447 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2448 | /* Setup and admin users can get any notification that does not |
| @@ -2450,14 +2683,14 @@ | |
| 2450 | }else{ |
| 2451 | /* Other users only see the alert if they have sufficient |
| 2452 | ** privilege to view the event itself */ |
| 2453 | char xType = '*'; |
| 2454 | switch( p->type ){ |
| 2455 | case 'c': xType = 'o'; break; |
| 2456 | case 'f': xType = '2'; break; |
| 2457 | case 't': xType = 'r'; break; |
| 2458 | case 'w': xType = 'j'; break; |
| 2459 | } |
| 2460 | if( strchr(zCap,xType)==0 ) continue; |
| 2461 | } |
| 2462 | if( blob_size(&p->hdr)>0 ){ |
| 2463 | /* This alert should be sent as a separate email */ |
| @@ -2594,10 +2827,11 @@ | |
| 2594 | @ <table class="subscribe"> |
| 2595 | if( zCaptcha ){ |
| 2596 | @ <tr> |
| 2597 | @ <td class="form_label">Security Code:</td> |
| 2598 | @ <td><input type="text" name="captcha" value="" size="10"> |
| 2599 | @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td> |
| 2600 | @ </tr> |
| 2601 | } |
| 2602 | @ <tr> |
| 2603 | @ <td class="form_label">Your Email Address:</td> |
| @@ -2620,11 +2854,11 @@ | |
| 2620 | @ </table> |
| 2621 | if( zCaptcha ){ |
| 2622 | @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha"> |
| 2623 | @ %h(zCaptcha) |
| 2624 | @ </pre> |
| 2625 | @ Enter the 8 characters above in the "Security Code" box |
| 2626 | @ </td></tr></table></div> |
| 2627 | } |
| 2628 | @ </form> |
| 2629 | style_footer(); |
| 2630 | } |
| @@ -2638,10 +2872,11 @@ | |
| 2638 | char *zErr; |
| 2639 | const char *zTo = PT("to"); |
| 2640 | char *zSubject = PT("subject"); |
| 2641 | int bAll = PB("all"); |
| 2642 | int bAA = PB("aa"); |
| 2643 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 2644 | int bTest2 = fossil_strcmp(P("name"),"test2")==0; |
| 2645 | Blob hdr, body; |
| 2646 | blob_init(&body, 0, 0); |
| 2647 | blob_init(&hdr, 0, 0); |
| @@ -2649,17 +2884,29 @@ | |
| 2649 | pSender = alert_sender_new(bTest2 ? "blob" : 0, 0); |
| 2650 | if( zTo[0] ){ |
| 2651 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 2652 | alert_send(pSender, &hdr, &body, 0); |
| 2653 | } |
| 2654 | if( bAll || bAA ){ |
| 2655 | Stmt q; |
| 2656 | int nUsed = blob_size(&body); |
| 2657 | const char *zURL = db_get("email-url",0); |
| 2658 | db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber " |
| 2659 | " WHERE sverified AND NOT sdonotcall %s", |
| 2660 | bAll ? "" : " AND ssub LIKE '%a%'"); |
| 2661 | while( db_step(&q)==SQLITE_ROW ){ |
| 2662 | const char *zCode = db_column_text(&q, 1); |
| 2663 | zTo = db_column_text(&q, 0); |
| 2664 | blob_truncate(&hdr, 0); |
| 2665 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| @@ -2716,11 +2963,12 @@ | |
| 2716 | @ <p>The following error was reported by the system: |
| 2717 | @ <blockquote><pre> |
| 2718 | @ %h(zErr) |
| 2719 | @ </pre></blockquote> |
| 2720 | }else{ |
| 2721 | @ <p>The announcement has been sent.</p> |
| 2722 | } |
| 2723 | style_footer(); |
| 2724 | return; |
| 2725 | } else if( !alert_enabled() ){ |
| 2726 | style_header("Cannot Send Announcement"); |
| @@ -2734,21 +2982,26 @@ | |
| 2734 | @ <form method="POST"> |
| 2735 | @ <table class="subscribe"> |
| 2736 | if( g.perm.Admin ){ |
| 2737 | int aa = PB("aa"); |
| 2738 | int all = PB("all"); |
| 2739 | const char *aack = aa ? "checked" : ""; |
| 2740 | const char *allck = all ? "checked" : ""; |
| 2741 | @ <tr> |
| 2742 | @ <td class="form_label">To:</td> |
| 2743 | @ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br> |
| 2744 | @ <label><input type="checkbox" name="aa" %s(aack)> \ |
| 2745 | @ All "announcement" subscribers</label> \ |
| 2746 | @ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br> |
| 2747 | @ <label><input type="checkbox" name="all" %s(allck)> \ |
| 2748 | @ All subscribers</label> \ |
| 2749 | @ <a href="%R/subscribers" target="_blank">(list)</a><br></td> |
| 2750 | @ </tr> |
| 2751 | } |
| 2752 | @ <tr> |
| 2753 | @ <td class="form_label">Subject:</td> |
| 2754 | @ <td><input type="text" name="subject" value="%h(PT("subject"))"\ |
| @@ -2759,11 +3012,15 @@ | |
| 2759 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 2760 | @ %h(PT("msg"))</textarea> |
| 2761 | @ </tr> |
| 2762 | @ <tr> |
| 2763 | @ <td></td> |
| 2764 | @ <td><input type="submit" name="submit" value="Send Message"> |
| 2765 | @ </tr> |
| 2766 | @ </table> |
| 2767 | @ </form> |
| 2768 | style_footer(); |
| 2769 | } |
| 2770 |
| --- 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. |
| @@ -114,11 +115,11 @@ | |
| 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;" |
| @@ -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 | } |
| @@ -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 */ |
| @@ -2039,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, |
| @@ -2078,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" |
| @@ -2104,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); |
| @@ -2436,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 |
| @@ -2450,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 */ |
| @@ -2594,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> |
| @@ -2620,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 | } |
| @@ -2638,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); |
| @@ -2649,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); |
| @@ -2716,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"); |
| @@ -2734,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"))"\ |
| @@ -2759,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 |
+3
-1
| --- src/allrepo.c | ||
| +++ src/allrepo.c | ||
| @@ -128,11 +128,11 @@ | ||
| 128 | 128 | ** supported by the rebuild command itself, if any are |
| 129 | 129 | ** present, are passed along verbatim. The --force and |
| 130 | 130 | ** --randomize options are not supported. |
| 131 | 131 | ** |
| 132 | 132 | ** sync Run a "sync" on all repositories. Only the --verbose |
| 133 | -** option is supported. | |
| 133 | +** and --unversioned options are supported. | |
| 134 | 134 | ** |
| 135 | 135 | ** setting Run the "setting", "set", or "unset" commands on all |
| 136 | 136 | ** set repositories. These command are particularly useful in |
| 137 | 137 | ** unset conjunction with the "max-loadavg" setting which cannot |
| 138 | 138 | ** otherwise be set globally. |
| @@ -283,11 +283,13 @@ | ||
| 283 | 283 | }else if( strncmp(zCmd, "sync", n)==0 ){ |
| 284 | 284 | zCmd = "sync -autourl -R"; |
| 285 | 285 | collect_argument(&extra, "verbose","v"); |
| 286 | 286 | collect_argument(&extra, "unversioned","u"); |
| 287 | 287 | }else if( strncmp(zCmd, "test-integrity", n)==0 ){ |
| 288 | + collect_argument(&extra, "db-only", "d"); | |
| 288 | 289 | collect_argument(&extra, "parse", 0); |
| 290 | + collect_argument(&extra, "quick", "q"); | |
| 289 | 291 | zCmd = "test-integrity"; |
| 290 | 292 | }else if( strncmp(zCmd, "test-orphans", n)==0 ){ |
| 291 | 293 | zCmd = "test-orphans -R"; |
| 292 | 294 | }else if( strncmp(zCmd, "test-missing", n)==0 ){ |
| 293 | 295 | zCmd = "test-missing -q -R"; |
| 294 | 296 |
| --- src/allrepo.c | |
| +++ src/allrepo.c | |
| @@ -128,11 +128,11 @@ | |
| 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. |
| @@ -283,11 +283,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"; |
| 294 |
| --- src/allrepo.c | |
| +++ src/allrepo.c | |
| @@ -128,11 +128,11 @@ | |
| 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 | ** and --unversioned options are 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. |
| @@ -283,11 +283,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, "db-only", "d"); |
| 289 | collect_argument(&extra, "parse", 0); |
| 290 | collect_argument(&extra, "quick", "q"); |
| 291 | zCmd = "test-integrity"; |
| 292 | }else if( strncmp(zCmd, "test-orphans", n)==0 ){ |
| 293 | zCmd = "test-orphans -R"; |
| 294 | }else if( strncmp(zCmd, "test-missing", n)==0 ){ |
| 295 | zCmd = "test-missing -q -R"; |
| 296 |
+1
-1
| --- src/attach.c | ||
| +++ src/attach.c | ||
| @@ -545,11 +545,11 @@ | ||
| 545 | 545 | cgi_redirectf("%R/wiki?name=%t", zWikiName); |
| 546 | 546 | } |
| 547 | 547 | return; |
| 548 | 548 | } |
| 549 | 549 | if( strcmp(zModAction,"approve")==0 ){ |
| 550 | - moderation_approve(rid); | |
| 550 | + moderation_approve('a', rid); | |
| 551 | 551 | } |
| 552 | 552 | } |
| 553 | 553 | style_header("Attachment Details"); |
| 554 | 554 | style_submenu_element("Raw", "%R/artifact/%s", zUuid); |
| 555 | 555 | if(fShowContent){ |
| 556 | 556 | |
| 557 | 557 | ADDED src/backlink.c |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -545,11 +545,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){ |
| 556 | |
| 557 | DDED src/backlink.c |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -545,11 +545,11 @@ | |
| 545 | cgi_redirectf("%R/wiki?name=%t", zWikiName); |
| 546 | } |
| 547 | return; |
| 548 | } |
| 549 | if( strcmp(zModAction,"approve")==0 ){ |
| 550 | moderation_approve('a', rid); |
| 551 | } |
| 552 | } |
| 553 | style_header("Attachment Details"); |
| 554 | style_submenu_element("Raw", "%R/artifact/%s", zUuid); |
| 555 | if(fShowContent){ |
| 556 | |
| 557 | 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 |
+15
-2
| --- src/branch.c | ||
| +++ src/branch.c | ||
| @@ -18,10 +18,24 @@ | ||
| 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 | +} | |
| 23 | 37 | |
| 24 | 38 | /* |
| 25 | 39 | ** If RID refers to a check-in, return the name of the branch for that |
| 26 | 40 | ** check-in. |
| 27 | 41 | ** |
| @@ -144,11 +158,10 @@ | ||
| 144 | 158 | blob_appendf(&branch, "T *bgcolor * %F\n", zColor); |
| 145 | 159 | } |
| 146 | 160 | blob_appendf(&branch, "T *branch * %F\n", zBranch); |
| 147 | 161 | blob_appendf(&branch, "T *sym-%F *\n", zBranch); |
| 148 | 162 | if( isPrivate ){ |
| 149 | - blob_appendf(&branch, "T +private *\n"); | |
| 150 | 163 | noSign = 1; |
| 151 | 164 | } |
| 152 | 165 | |
| 153 | 166 | /* Cancel all other symbolic tags */ |
| 154 | 167 | db_prepare(&q, |
| @@ -245,11 +258,11 @@ | ||
| 245 | 258 | @ GROUP BY 1; |
| 246 | 259 | ; |
| 247 | 260 | |
| 248 | 261 | /* Call this routine to create the TEMP table */ |
| 249 | 262 | static void brlist_create_temp_table(void){ |
| 250 | - db_multi_exec(createBrlistQuery/*works-like:""*/); | |
| 263 | + db_exec_sql(createBrlistQuery); | |
| 251 | 264 | } |
| 252 | 265 | |
| 253 | 266 | |
| 254 | 267 | #if INTERFACE |
| 255 | 268 | /* |
| 256 | 269 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -18,10 +18,24 @@ | |
| 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 | ** If RID refers to a check-in, return the name of the branch for that |
| 26 | ** check-in. |
| 27 | ** |
| @@ -144,11 +158,10 @@ | |
| 144 | blob_appendf(&branch, "T *bgcolor * %F\n", zColor); |
| 145 | } |
| 146 | blob_appendf(&branch, "T *branch * %F\n", zBranch); |
| 147 | blob_appendf(&branch, "T *sym-%F *\n", zBranch); |
| 148 | if( isPrivate ){ |
| 149 | blob_appendf(&branch, "T +private *\n"); |
| 150 | noSign = 1; |
| 151 | } |
| 152 | |
| 153 | /* Cancel all other symbolic tags */ |
| 154 | db_prepare(&q, |
| @@ -245,11 +258,11 @@ | |
| 245 | @ GROUP BY 1; |
| 246 | ; |
| 247 | |
| 248 | /* Call this routine to create the TEMP table */ |
| 249 | static void brlist_create_temp_table(void){ |
| 250 | db_multi_exec(createBrlistQuery/*works-like:""*/); |
| 251 | } |
| 252 | |
| 253 | |
| 254 | #if INTERFACE |
| 255 | /* |
| 256 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -18,10 +18,24 @@ | |
| 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 | ** |
| @@ -144,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, |
| @@ -245,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 | /* |
| 269 |
+148
-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 | } |
| @@ -122,33 +137,29 @@ | ||
| 122 | 137 | */ |
| 123 | 138 | void page_dir(void){ |
| 124 | 139 | char *zD = fossil_strdup(P("name")); |
| 125 | 140 | int nD = zD ? strlen(zD)+1 : 0; |
| 126 | 141 | int mxLen; |
| 127 | - int n; | |
| 128 | 142 | char *zPrefix; |
| 129 | 143 | Stmt q; |
| 130 | 144 | const char *zCI = P("ci"); |
| 131 | 145 | int rid = 0; |
| 132 | 146 | char *zUuid = 0; |
| 133 | - Blob dirname; | |
| 134 | 147 | Manifest *pM = 0; |
| 135 | 148 | const char *zSubdirLink; |
| 136 | 149 | int linkTrunk = 1; |
| 137 | 150 | int linkTip = 1; |
| 138 | 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; | |
| 139 | 155 | |
| 156 | + if( zCI && strlen(zCI)==0 ){ zCI = 0; } | |
| 140 | 157 | if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; } |
| 141 | 158 | login_check_credentials(); |
| 142 | 159 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 143 | 160 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| 144 | - style_header("File List"); | |
| 145 | - style_adunit_config(ADUNIT_RIGHT_OK); | |
| 146 | - sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, | |
| 147 | - pathelementFunc, 0, 0); | |
| 148 | - url_initialize(&sURI, "dir"); | |
| 149 | - cgi_query_parameters_to_url(&sURI); | |
| 150 | 161 | |
| 151 | 162 | /* If the name= parameter is an empty string, make it a NULL pointer */ |
| 152 | 163 | if( zD && strlen(zD)==0 ){ zD = 0; } |
| 153 | 164 | |
| 154 | 165 | /* If a specific check-in is requested, fetch and parse it. If the |
| @@ -160,53 +171,80 @@ | ||
| 160 | 171 | if( pM ){ |
| 161 | 172 | int trunkRid = symbolic_name_to_rid("tag:trunk", "ci"); |
| 162 | 173 | linkTrunk = trunkRid && rid != trunkRid; |
| 163 | 174 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 164 | 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); | |
| 165 | 179 | }else{ |
| 166 | 180 | zCI = 0; |
| 167 | 181 | } |
| 168 | 182 | } |
| 169 | 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 | + | |
| 170 | 206 | /* Compute the title of the page */ |
| 171 | - blob_zero(&dirname); | |
| 172 | 207 | if( zD ){ |
| 173 | - blob_append(&dirname, "in directory ", -1); | |
| 174 | - 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); | |
| 175 | 213 | zPrefix = mprintf("%s/", zD); |
| 176 | 214 | style_submenu_element("Top-Level", "%s", |
| 177 | 215 | url_render(&sURI, "name", 0, 0, 0)); |
| 178 | 216 | }else{ |
| 179 | - blob_append(&dirname, "in the top-level directory", -1); | |
| 217 | + @ <h2>Files in the top-level directory \ | |
| 180 | 218 | zPrefix = ""; |
| 181 | 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 | + } | |
| 182 | 237 | if( linkTrunk ){ |
| 183 | 238 | style_submenu_element("Trunk", "%s", |
| 184 | 239 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| 185 | 240 | } |
| 186 | 241 | if( linkTip ){ |
| 187 | 242 | style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0)); |
| 188 | 243 | } |
| 189 | - if( zCI ){ | |
| 190 | - @ <h2>Files of check-in [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] | |
| 191 | - @ %s(blob_str(&dirname)) | |
| 192 | - if( zD ){ | |
| 193 | - @ %z(href("%R/timeline?chng=%T/*", zD))[history]</a> | |
| 194 | - } | |
| 195 | - @ </h2> | |
| 196 | - zSubdirLink = mprintf("%R/dir?ci=%!S&name=%T", zUuid, zPrefix); | |
| 197 | - if( nD==0 ){ | |
| 198 | - style_submenu_element("File Ages", "%R/fileage?name=%!S", zUuid); | |
| 199 | - } | |
| 200 | - }else{ | |
| 201 | - @ <h2>The union of all files from all check-ins | |
| 202 | - @ %s(blob_str(&dirname)) | |
| 203 | - if( zD ){ | |
| 204 | - @ %z(href("%R/timeline?chng=%T/*", zD))[history]</a> | |
| 205 | - } | |
| 206 | - @ </h2> | |
| 207 | - zSubdirLink = mprintf("%R/dir?name=%T", zPrefix); | |
| 244 | + if( zD ){ | |
| 245 | + style_submenu_element("History","%R/timeline?chng=%T/*", zD); | |
| 208 | 246 | } |
| 209 | 247 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 210 | 248 | style_submenu_element("Tree-View", "%s", |
| 211 | 249 | url_render(&sURI, "type", "tree", 0, 0)); |
| 212 | 250 | |
| @@ -269,15 +307,14 @@ | ||
| 269 | 307 | |
| 270 | 308 | /* Generate a multi-column table listing the contents of zD[] |
| 271 | 309 | ** directory. |
| 272 | 310 | */ |
| 273 | 311 | mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/"); |
| 274 | - n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/"); | |
| 275 | 312 | if( mxLen<12 ) mxLen = 12; |
| 276 | 313 | mxLen += (mxLen+9)/10; |
| 277 | 314 | db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/"); |
| 278 | - @ <div class="columns" style="columns: %d(mxLen)ex %d(n);"> | |
| 315 | + @ <div class="columns files" style="columns: %d(mxLen)ex auto"> | |
| 279 | 316 | @ <ul class="browser"> |
| 280 | 317 | while( db_step(&q)==SQLITE_ROW ){ |
| 281 | 318 | const char *zFN; |
| 282 | 319 | zFN = db_column_text(&q, 0); |
| 283 | 320 | if( zFN[0]=='/' ){ |
| @@ -284,12 +321,11 @@ | ||
| 284 | 321 | zFN++; |
| 285 | 322 | @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li> |
| 286 | 323 | }else{ |
| 287 | 324 | const char *zLink; |
| 288 | 325 | if( zCI ){ |
| 289 | - const char *zUuid = db_column_text(&q, 1); | |
| 290 | - zLink = href("%R/artifact/%!S",zUuid); | |
| 326 | + zLink = href("%R/file?name=%T%T&ci=%T",zPrefix,zFN,zCI); | |
| 291 | 327 | }else{ |
| 292 | 328 | zLink = href("%R/finfo?name=%T%T",zPrefix,zFN); |
| 293 | 329 | } |
| 294 | 330 | @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li> |
| 295 | 331 | } |
| @@ -614,11 +650,15 @@ | ||
| 614 | 650 | HQuery sURI; /* Hyperlink */ |
| 615 | 651 | int startExpanded; /* True to start out with the tree expanded */ |
| 616 | 652 | int showDirOnly; /* Show directories only. Omit files */ |
| 617 | 653 | int nDir = 0; /* Number of directories. Used for ID attributes */ |
| 618 | 654 | char *zProjectName = db_get("project-name", 0); |
| 655 | + int isSymbolicCI = 0; /* ci= is a symbolic name, not a hash prefix */ | |
| 656 | + int isBranchCI = 0; /* ci= refers to a branch name */ | |
| 657 | + char *zHeader = 0; | |
| 619 | 658 | |
| 659 | + if( zCI && strlen(zCI)==0 ){ zCI = 0; } | |
| 620 | 660 | if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } |
| 621 | 661 | memset(&sTree, 0, sizeof(sTree)); |
| 622 | 662 | login_check_credentials(); |
| 623 | 663 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 624 | 664 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| @@ -626,14 +666,12 @@ | ||
| 626 | 666 | pathelementFunc, 0, 0); |
| 627 | 667 | url_initialize(&sURI, "tree"); |
| 628 | 668 | cgi_query_parameters_to_url(&sURI); |
| 629 | 669 | if( PB("nofiles") ){ |
| 630 | 670 | showDirOnly = 1; |
| 631 | - style_header("Folder Hierarchy"); | |
| 632 | 671 | }else{ |
| 633 | 672 | showDirOnly = 0; |
| 634 | - style_header("File Tree"); | |
| 635 | 673 | } |
| 636 | 674 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 637 | 675 | if( PB("expand") ){ |
| 638 | 676 | startExpanded = 1; |
| 639 | 677 | }else{ |
| @@ -662,37 +700,55 @@ | ||
| 662 | 700 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 663 | 701 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 664 | 702 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 665 | 703 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 666 | 704 | " FROM event WHERE objid=%d", rid); |
| 705 | + isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); | |
| 706 | + isBranchCI = branch_includes_uuid(zCI, zUuid); | |
| 707 | + Th_Store("current_checkin", zCI); | |
| 667 | 708 | }else{ |
| 668 | 709 | zCI = 0; |
| 669 | 710 | } |
| 670 | 711 | } |
| 671 | 712 | if( zCI==0 ){ |
| 672 | 713 | rNow = db_double(0.0, "SELECT max(mtime) FROM event"); |
| 673 | 714 | zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event"); |
| 674 | 715 | } |
| 716 | + | |
| 717 | + assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) ); | |
| 718 | + if( zD==0 ){ | |
| 719 | + if( zCI ){ | |
| 720 | + zHeader = mprintf("Top-level Files of %s", zCI); | |
| 721 | + }else{ | |
| 722 | + zHeader = mprintf("All Top-level Files"); | |
| 723 | + } | |
| 724 | + }else{ | |
| 725 | + if( zCI ){ | |
| 726 | + zHeader = mprintf("Files in %s/ of %s", zD, zCI); | |
| 727 | + }else{ | |
| 728 | + zHeader = mprintf("All File in %s/", zD); | |
| 729 | + } | |
| 730 | + } | |
| 731 | + style_header("%s", zHeader); | |
| 732 | + fossil_free(zHeader); | |
| 675 | 733 | |
| 676 | 734 | /* Compute the title of the page */ |
| 677 | 735 | blob_zero(&dirname); |
| 678 | 736 | if( zD ){ |
| 679 | 737 | blob_append(&dirname, "within directory ", -1); |
| 680 | - hyperlinked_path(zD, &dirname, zCI, "tree", zREx); | |
| 738 | + hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0); | |
| 681 | 739 | if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE); |
| 682 | 740 | style_submenu_element("Top-Level", "%s", |
| 683 | 741 | url_render(&sURI, "name", 0, 0, 0)); |
| 684 | - }else{ | |
| 685 | - if( zRE ){ | |
| 686 | - blob_appendf(&dirname, "matching \"%s\"", zRE); | |
| 687 | - } | |
| 742 | + }else if( zRE ){ | |
| 743 | + blob_appendf(&dirname, "matching \"%s\"", zRE); | |
| 688 | 744 | } |
| 689 | 745 | style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0); |
| 690 | 746 | if( zCI ){ |
| 691 | 747 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 692 | 748 | if( nD==0 && !showDirOnly ){ |
| 693 | - style_submenu_element("File Ages", "%R/fileage?name=%s", zUuid); | |
| 749 | + style_submenu_element("File Ages", "%R/fileage?name=%T", zCI); | |
| 694 | 750 | } |
| 695 | 751 | } |
| 696 | 752 | if( linkTrunk ){ |
| 697 | 753 | style_submenu_element("Trunk", "%s", |
| 698 | 754 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| @@ -747,10 +803,11 @@ | ||
| 747 | 803 | tree_add_node(&sTree, zName, zUuid, mtime); |
| 748 | 804 | nFile++; |
| 749 | 805 | } |
| 750 | 806 | db_finalize(&q); |
| 751 | 807 | } |
| 808 | + style_submenu_checkbox("nofiles", "Folders Only", 0, 0); | |
| 752 | 809 | |
| 753 | 810 | if( showDirOnly ){ |
| 754 | 811 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 755 | 812 | if( p->pChild!=0 && p->nFullName>nD ) nFile++; |
| 756 | 813 | } |
| @@ -757,18 +814,24 @@ | ||
| 757 | 814 | zObjType = "Folders"; |
| 758 | 815 | }else{ |
| 759 | 816 | zObjType = "Files"; |
| 760 | 817 | } |
| 761 | 818 | |
| 762 | - style_submenu_checkbox("nofiles", "Folders Only", 0, 0); | |
| 763 | - | |
| 764 | - if( zCI ){ | |
| 765 | - @ <h2>%s(zObjType) from | |
| 766 | - if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){ | |
| 767 | - @ "%h(zCI)" | |
| 768 | - } | |
| 769 | - @ [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname)) | |
| 819 | + if( zCI && strcmp(zCI,"tip")==0 ){ | |
| 820 | + @ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a> | |
| 821 | + }else if( isBranchCI ){ | |
| 822 | + @ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\ | |
| 823 | + @ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a> | |
| 824 | + if( blob_size(&dirname) ){ | |
| 825 | + @ and %s(blob_str(&dirname))</h2> | |
| 826 | + } | |
| 827 | + }else if( zCI ){ | |
| 828 | + @ <h2>%s(zObjType) for check-in \ | |
| 829 | + @ %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2> | |
| 830 | + if( blob_size(&dirname) ){ | |
| 831 | + @ and %s(blob_str(&dirname))</h2> | |
| 832 | + } | |
| 770 | 833 | }else{ |
| 771 | 834 | int n = db_int(0, "SELECT count(*) FROM plink"); |
| 772 | 835 | @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) |
| 773 | 836 | } |
| 774 | 837 | if( useMtime ){ |
| @@ -826,11 +889,11 @@ | ||
| 826 | 889 | nDir++; |
| 827 | 890 | }else if( !showDirOnly ){ |
| 828 | 891 | const char *zFileClass = fileext_class(p->zName); |
| 829 | 892 | char *zLink; |
| 830 | 893 | if( zCI ){ |
| 831 | - zLink = href("%R/artifact/%!S",p->zUuid); | |
| 894 | + zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI); | |
| 832 | 895 | }else{ |
| 833 | 896 | zLink = href("%R/finfo?name=%T",p->zFullName); |
| 834 | 897 | } |
| 835 | 898 | @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline"> |
| 836 | 899 | @ %z(zLink)%h(p->zName)</a> |
| @@ -920,11 +983,11 @@ | ||
| 920 | 983 | ** mtime on that check-in. If zGlob and *zGlob then only files matching |
| 921 | 984 | ** the given glob are computed. |
| 922 | 985 | */ |
| 923 | 986 | int compute_fileage(int vid, const char* zGlob){ |
| 924 | 987 | Stmt q; |
| 925 | - db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/); | |
| 988 | + db_exec_sql(zComputeFileAgeSetup); | |
| 926 | 989 | db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/); |
| 927 | 990 | db_bind_int(&q, ":ckin", vid); |
| 928 | 991 | db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*"); |
| 929 | 992 | db_exec(&q); |
| 930 | 993 | db_finalize(&q); |
| @@ -1003,10 +1066,11 @@ | ||
| 1003 | 1066 | int rid; |
| 1004 | 1067 | const char *zName; |
| 1005 | 1068 | const char *zGlob; |
| 1006 | 1069 | const char *zUuid; |
| 1007 | 1070 | const char *zNow; /* Time of check-in */ |
| 1071 | + int isBranchCI; /* name= is a branch name */ | |
| 1008 | 1072 | int showId = PB("showid"); |
| 1009 | 1073 | Stmt q1, q2; |
| 1010 | 1074 | double baseTime; |
| 1011 | 1075 | login_check_credentials(); |
| 1012 | 1076 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| @@ -1016,28 +1080,34 @@ | ||
| 1016 | 1080 | rid = symbolic_name_to_rid(zName, "ci"); |
| 1017 | 1081 | if( rid==0 ){ |
| 1018 | 1082 | fossil_fatal("not a valid check-in: %s", zName); |
| 1019 | 1083 | } |
| 1020 | 1084 | zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1085 | + isBranchCI = branch_includes_uuid(zName,zUuid); | |
| 1021 | 1086 | baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); |
| 1022 | 1087 | zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event" |
| 1023 | 1088 | " WHERE objid=%d", rid); |
| 1024 | 1089 | style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName); |
| 1025 | 1090 | style_header("File Ages"); |
| 1026 | 1091 | zGlob = P("glob"); |
| 1027 | 1092 | compute_fileage(rid,zGlob); |
| 1028 | 1093 | db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); |
| 1029 | 1094 | |
| 1030 | - @ <h1>Files in | |
| 1031 | - @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)]</a> | |
| 1095 | + if( fossil_strcmp(zName,"tip")==0 ){ | |
| 1096 | + @ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a> | |
| 1097 | + }else if( isBranchCI ){ | |
| 1098 | + @ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a> | |
| 1099 | + @ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a> | |
| 1100 | + }else{ | |
| 1101 | + @ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a> | |
| 1102 | + } | |
| 1032 | 1103 | if( zGlob && zGlob[0] ){ |
| 1033 | 1104 | @ that match "%h(zGlob)" |
| 1034 | 1105 | } |
| 1035 | 1106 | @ ordered by age</h1> |
| 1036 | 1107 | @ |
| 1037 | - @ <p>File ages are expressed relative to the | |
| 1038 | - @ %z(href("%R/ci/%!S",zUuid))[%S(zUuid)]</a> check-in time of | |
| 1108 | + @ <p>File ages are expressed relative to the check-in time of | |
| 1039 | 1109 | @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
| 1040 | 1110 | @ |
| 1041 | 1111 | @ <div class='fileage'><table> |
| 1042 | 1112 | @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr> |
| 1043 | 1113 | db_prepare(&q1, |
| @@ -1052,14 +1122,13 @@ | ||
| 1052 | 1122 | " AND blob.rid=event.objid\n" |
| 1053 | 1123 | " ORDER BY event.mtime DESC;", |
| 1054 | 1124 | TAG_BRANCH |
| 1055 | 1125 | ); |
| 1056 | 1126 | db_prepare(&q2, |
| 1057 | - "SELECT blob.uuid, filename.name, fileage.fid\n" | |
| 1058 | - " FROM fileage, blob, filename\n" | |
| 1127 | + "SELECT filename.name, fileage.fid\n" | |
| 1128 | + " FROM fileage, filename\n" | |
| 1059 | 1129 | " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid" |
| 1060 | - " AND blob.rid=fileage.fid;" | |
| 1061 | 1130 | ); |
| 1062 | 1131 | while( db_step(&q1)==SQLITE_ROW ){ |
| 1063 | 1132 | double age = baseTime - db_column_double(&q1, 0); |
| 1064 | 1133 | int mid = db_column_int(&q1, 1); |
| 1065 | 1134 | const char *zUuid = db_column_text(&q1, 2); |
| @@ -1069,24 +1138,24 @@ | ||
| 1069 | 1138 | char *zAge = human_readable_age(age); |
| 1070 | 1139 | @ <tr><td>%s(zAge)</td> |
| 1071 | 1140 | @ <td> |
| 1072 | 1141 | db_bind_int(&q2, ":mid", mid); |
| 1073 | 1142 | while( db_step(&q2)==SQLITE_ROW ){ |
| 1074 | - const char *zFUuid = db_column_text(&q2,0); | |
| 1075 | - const char *zFile = db_column_text(&q2,1); | |
| 1076 | - int fid = db_column_int(&q2,2); | |
| 1143 | + const char *zFile = db_column_text(&q2,0); | |
| 1144 | + @ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \ | |
| 1077 | 1145 | if( showId ){ |
| 1078 | - @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a> (%d(fid))<br /> | |
| 1146 | + int fid = db_column_int(&q2,1); | |
| 1147 | + @ (%d(fid))<br /> | |
| 1079 | 1148 | }else{ |
| 1080 | - @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br /> | |
| 1149 | + @ </a><br /> | |
| 1081 | 1150 | } |
| 1082 | 1151 | } |
| 1083 | 1152 | db_reset(&q2); |
| 1084 | 1153 | @ </td> |
| 1085 | 1154 | @ <td> |
| 1086 | 1155 | @ %W(zComment) |
| 1087 | - @ (check-in: %z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>, | |
| 1156 | + @ (check-in: %z(href("%R/info/%!S",zUuid))%S(zUuid)</a>, | |
| 1088 | 1157 | if( showId ){ |
| 1089 | 1158 | @ id: %d(mid) |
| 1090 | 1159 | } |
| 1091 | 1160 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1092 | 1161 | @ branch: \ |
| 1093 | 1162 |
| --- 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 | } |
| @@ -122,33 +137,29 @@ | |
| 122 | */ |
| 123 | void page_dir(void){ |
| 124 | char *zD = fossil_strdup(P("name")); |
| 125 | int nD = zD ? strlen(zD)+1 : 0; |
| 126 | int mxLen; |
| 127 | int n; |
| 128 | char *zPrefix; |
| 129 | Stmt q; |
| 130 | const char *zCI = P("ci"); |
| 131 | int rid = 0; |
| 132 | char *zUuid = 0; |
| 133 | Blob dirname; |
| 134 | Manifest *pM = 0; |
| 135 | const char *zSubdirLink; |
| 136 | int linkTrunk = 1; |
| 137 | int linkTip = 1; |
| 138 | HQuery sURI; |
| 139 | |
| 140 | if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; } |
| 141 | login_check_credentials(); |
| 142 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 143 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| 144 | style_header("File List"); |
| 145 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 146 | sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, |
| 147 | pathelementFunc, 0, 0); |
| 148 | url_initialize(&sURI, "dir"); |
| 149 | cgi_query_parameters_to_url(&sURI); |
| 150 | |
| 151 | /* If the name= parameter is an empty string, make it a NULL pointer */ |
| 152 | if( zD && strlen(zD)==0 ){ zD = 0; } |
| 153 | |
| 154 | /* If a specific check-in is requested, fetch and parse it. If the |
| @@ -160,53 +171,80 @@ | |
| 160 | if( pM ){ |
| 161 | int trunkRid = symbolic_name_to_rid("tag:trunk", "ci"); |
| 162 | linkTrunk = trunkRid && rid != trunkRid; |
| 163 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 164 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 165 | }else{ |
| 166 | zCI = 0; |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | /* Compute the title of the page */ |
| 171 | blob_zero(&dirname); |
| 172 | if( zD ){ |
| 173 | blob_append(&dirname, "in directory ", -1); |
| 174 | hyperlinked_path(zD, &dirname, zCI, "dir", ""); |
| 175 | zPrefix = mprintf("%s/", zD); |
| 176 | style_submenu_element("Top-Level", "%s", |
| 177 | url_render(&sURI, "name", 0, 0, 0)); |
| 178 | }else{ |
| 179 | blob_append(&dirname, "in the top-level directory", -1); |
| 180 | zPrefix = ""; |
| 181 | } |
| 182 | if( linkTrunk ){ |
| 183 | style_submenu_element("Trunk", "%s", |
| 184 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| 185 | } |
| 186 | if( linkTip ){ |
| 187 | style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0)); |
| 188 | } |
| 189 | if( zCI ){ |
| 190 | @ <h2>Files of check-in [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] |
| 191 | @ %s(blob_str(&dirname)) |
| 192 | if( zD ){ |
| 193 | @ %z(href("%R/timeline?chng=%T/*", zD))[history]</a> |
| 194 | } |
| 195 | @ </h2> |
| 196 | zSubdirLink = mprintf("%R/dir?ci=%!S&name=%T", zUuid, zPrefix); |
| 197 | if( nD==0 ){ |
| 198 | style_submenu_element("File Ages", "%R/fileage?name=%!S", zUuid); |
| 199 | } |
| 200 | }else{ |
| 201 | @ <h2>The union of all files from all check-ins |
| 202 | @ %s(blob_str(&dirname)) |
| 203 | if( zD ){ |
| 204 | @ %z(href("%R/timeline?chng=%T/*", zD))[history]</a> |
| 205 | } |
| 206 | @ </h2> |
| 207 | zSubdirLink = mprintf("%R/dir?name=%T", zPrefix); |
| 208 | } |
| 209 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 210 | style_submenu_element("Tree-View", "%s", |
| 211 | url_render(&sURI, "type", "tree", 0, 0)); |
| 212 | |
| @@ -269,15 +307,14 @@ | |
| 269 | |
| 270 | /* Generate a multi-column table listing the contents of zD[] |
| 271 | ** directory. |
| 272 | */ |
| 273 | mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/"); |
| 274 | n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/"); |
| 275 | if( mxLen<12 ) mxLen = 12; |
| 276 | mxLen += (mxLen+9)/10; |
| 277 | db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/"); |
| 278 | @ <div class="columns" style="columns: %d(mxLen)ex %d(n);"> |
| 279 | @ <ul class="browser"> |
| 280 | while( db_step(&q)==SQLITE_ROW ){ |
| 281 | const char *zFN; |
| 282 | zFN = db_column_text(&q, 0); |
| 283 | if( zFN[0]=='/' ){ |
| @@ -284,12 +321,11 @@ | |
| 284 | zFN++; |
| 285 | @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li> |
| 286 | }else{ |
| 287 | const char *zLink; |
| 288 | if( zCI ){ |
| 289 | const char *zUuid = db_column_text(&q, 1); |
| 290 | zLink = href("%R/artifact/%!S",zUuid); |
| 291 | }else{ |
| 292 | zLink = href("%R/finfo?name=%T%T",zPrefix,zFN); |
| 293 | } |
| 294 | @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li> |
| 295 | } |
| @@ -614,11 +650,15 @@ | |
| 614 | HQuery sURI; /* Hyperlink */ |
| 615 | int startExpanded; /* True to start out with the tree expanded */ |
| 616 | int showDirOnly; /* Show directories only. Omit files */ |
| 617 | int nDir = 0; /* Number of directories. Used for ID attributes */ |
| 618 | char *zProjectName = db_get("project-name", 0); |
| 619 | |
| 620 | if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } |
| 621 | memset(&sTree, 0, sizeof(sTree)); |
| 622 | login_check_credentials(); |
| 623 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 624 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| @@ -626,14 +666,12 @@ | |
| 626 | pathelementFunc, 0, 0); |
| 627 | url_initialize(&sURI, "tree"); |
| 628 | cgi_query_parameters_to_url(&sURI); |
| 629 | if( PB("nofiles") ){ |
| 630 | showDirOnly = 1; |
| 631 | style_header("Folder Hierarchy"); |
| 632 | }else{ |
| 633 | showDirOnly = 0; |
| 634 | style_header("File Tree"); |
| 635 | } |
| 636 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 637 | if( PB("expand") ){ |
| 638 | startExpanded = 1; |
| 639 | }else{ |
| @@ -662,37 +700,55 @@ | |
| 662 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 663 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 664 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 665 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 666 | " FROM event WHERE objid=%d", rid); |
| 667 | }else{ |
| 668 | zCI = 0; |
| 669 | } |
| 670 | } |
| 671 | if( zCI==0 ){ |
| 672 | rNow = db_double(0.0, "SELECT max(mtime) FROM event"); |
| 673 | zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event"); |
| 674 | } |
| 675 | |
| 676 | /* Compute the title of the page */ |
| 677 | blob_zero(&dirname); |
| 678 | if( zD ){ |
| 679 | blob_append(&dirname, "within directory ", -1); |
| 680 | hyperlinked_path(zD, &dirname, zCI, "tree", zREx); |
| 681 | if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE); |
| 682 | style_submenu_element("Top-Level", "%s", |
| 683 | url_render(&sURI, "name", 0, 0, 0)); |
| 684 | }else{ |
| 685 | if( zRE ){ |
| 686 | blob_appendf(&dirname, "matching \"%s\"", zRE); |
| 687 | } |
| 688 | } |
| 689 | style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0); |
| 690 | if( zCI ){ |
| 691 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 692 | if( nD==0 && !showDirOnly ){ |
| 693 | style_submenu_element("File Ages", "%R/fileage?name=%s", zUuid); |
| 694 | } |
| 695 | } |
| 696 | if( linkTrunk ){ |
| 697 | style_submenu_element("Trunk", "%s", |
| 698 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| @@ -747,10 +803,11 @@ | |
| 747 | tree_add_node(&sTree, zName, zUuid, mtime); |
| 748 | nFile++; |
| 749 | } |
| 750 | db_finalize(&q); |
| 751 | } |
| 752 | |
| 753 | if( showDirOnly ){ |
| 754 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 755 | if( p->pChild!=0 && p->nFullName>nD ) nFile++; |
| 756 | } |
| @@ -757,18 +814,24 @@ | |
| 757 | zObjType = "Folders"; |
| 758 | }else{ |
| 759 | zObjType = "Files"; |
| 760 | } |
| 761 | |
| 762 | style_submenu_checkbox("nofiles", "Folders Only", 0, 0); |
| 763 | |
| 764 | if( zCI ){ |
| 765 | @ <h2>%s(zObjType) from |
| 766 | if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){ |
| 767 | @ "%h(zCI)" |
| 768 | } |
| 769 | @ [%z(href("vinfo?name=%!S",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname)) |
| 770 | }else{ |
| 771 | int n = db_int(0, "SELECT count(*) FROM plink"); |
| 772 | @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) |
| 773 | } |
| 774 | if( useMtime ){ |
| @@ -826,11 +889,11 @@ | |
| 826 | nDir++; |
| 827 | }else if( !showDirOnly ){ |
| 828 | const char *zFileClass = fileext_class(p->zName); |
| 829 | char *zLink; |
| 830 | if( zCI ){ |
| 831 | zLink = href("%R/artifact/%!S",p->zUuid); |
| 832 | }else{ |
| 833 | zLink = href("%R/finfo?name=%T",p->zFullName); |
| 834 | } |
| 835 | @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline"> |
| 836 | @ %z(zLink)%h(p->zName)</a> |
| @@ -920,11 +983,11 @@ | |
| 920 | ** mtime on that check-in. If zGlob and *zGlob then only files matching |
| 921 | ** the given glob are computed. |
| 922 | */ |
| 923 | int compute_fileage(int vid, const char* zGlob){ |
| 924 | Stmt q; |
| 925 | db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/); |
| 926 | db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/); |
| 927 | db_bind_int(&q, ":ckin", vid); |
| 928 | db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*"); |
| 929 | db_exec(&q); |
| 930 | db_finalize(&q); |
| @@ -1003,10 +1066,11 @@ | |
| 1003 | int rid; |
| 1004 | const char *zName; |
| 1005 | const char *zGlob; |
| 1006 | const char *zUuid; |
| 1007 | const char *zNow; /* Time of check-in */ |
| 1008 | int showId = PB("showid"); |
| 1009 | Stmt q1, q2; |
| 1010 | double baseTime; |
| 1011 | login_check_credentials(); |
| 1012 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| @@ -1016,28 +1080,34 @@ | |
| 1016 | rid = symbolic_name_to_rid(zName, "ci"); |
| 1017 | if( rid==0 ){ |
| 1018 | fossil_fatal("not a valid check-in: %s", zName); |
| 1019 | } |
| 1020 | zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1021 | baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); |
| 1022 | zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event" |
| 1023 | " WHERE objid=%d", rid); |
| 1024 | style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName); |
| 1025 | style_header("File Ages"); |
| 1026 | zGlob = P("glob"); |
| 1027 | compute_fileage(rid,zGlob); |
| 1028 | db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); |
| 1029 | |
| 1030 | @ <h1>Files in |
| 1031 | @ %z(href("%R/info/%!S",zUuid))[%S(zUuid)]</a> |
| 1032 | if( zGlob && zGlob[0] ){ |
| 1033 | @ that match "%h(zGlob)" |
| 1034 | } |
| 1035 | @ ordered by age</h1> |
| 1036 | @ |
| 1037 | @ <p>File ages are expressed relative to the |
| 1038 | @ %z(href("%R/ci/%!S",zUuid))[%S(zUuid)]</a> check-in time of |
| 1039 | @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
| 1040 | @ |
| 1041 | @ <div class='fileage'><table> |
| 1042 | @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr> |
| 1043 | db_prepare(&q1, |
| @@ -1052,14 +1122,13 @@ | |
| 1052 | " AND blob.rid=event.objid\n" |
| 1053 | " ORDER BY event.mtime DESC;", |
| 1054 | TAG_BRANCH |
| 1055 | ); |
| 1056 | db_prepare(&q2, |
| 1057 | "SELECT blob.uuid, filename.name, fileage.fid\n" |
| 1058 | " FROM fileage, blob, filename\n" |
| 1059 | " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid" |
| 1060 | " AND blob.rid=fileage.fid;" |
| 1061 | ); |
| 1062 | while( db_step(&q1)==SQLITE_ROW ){ |
| 1063 | double age = baseTime - db_column_double(&q1, 0); |
| 1064 | int mid = db_column_int(&q1, 1); |
| 1065 | const char *zUuid = db_column_text(&q1, 2); |
| @@ -1069,24 +1138,24 @@ | |
| 1069 | char *zAge = human_readable_age(age); |
| 1070 | @ <tr><td>%s(zAge)</td> |
| 1071 | @ <td> |
| 1072 | db_bind_int(&q2, ":mid", mid); |
| 1073 | while( db_step(&q2)==SQLITE_ROW ){ |
| 1074 | const char *zFUuid = db_column_text(&q2,0); |
| 1075 | const char *zFile = db_column_text(&q2,1); |
| 1076 | int fid = db_column_int(&q2,2); |
| 1077 | if( showId ){ |
| 1078 | @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a> (%d(fid))<br /> |
| 1079 | }else{ |
| 1080 | @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br /> |
| 1081 | } |
| 1082 | } |
| 1083 | db_reset(&q2); |
| 1084 | @ </td> |
| 1085 | @ <td> |
| 1086 | @ %W(zComment) |
| 1087 | @ (check-in: %z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>, |
| 1088 | if( showId ){ |
| 1089 | @ id: %d(mid) |
| 1090 | } |
| 1091 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1092 | @ branch: \ |
| 1093 |
| --- 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 | } |
| @@ -122,33 +137,29 @@ | |
| 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 |
| @@ -160,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 | |
| @@ -269,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]=='/' ){ |
| @@ -284,12 +321,11 @@ | |
| 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 | } |
| @@ -614,11 +650,15 @@ | |
| 650 | HQuery sURI; /* Hyperlink */ |
| 651 | int startExpanded; /* True to start out with the tree expanded */ |
| 652 | int showDirOnly; /* Show directories only. Omit files */ |
| 653 | int nDir = 0; /* Number of directories. Used for ID attributes */ |
| 654 | char *zProjectName = db_get("project-name", 0); |
| 655 | int isSymbolicCI = 0; /* ci= is a symbolic name, not a hash prefix */ |
| 656 | int isBranchCI = 0; /* ci= refers to a branch name */ |
| 657 | char *zHeader = 0; |
| 658 | |
| 659 | if( zCI && strlen(zCI)==0 ){ zCI = 0; } |
| 660 | if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } |
| 661 | memset(&sTree, 0, sizeof(sTree)); |
| 662 | login_check_credentials(); |
| 663 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 664 | while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
| @@ -626,14 +666,12 @@ | |
| 666 | pathelementFunc, 0, 0); |
| 667 | url_initialize(&sURI, "tree"); |
| 668 | cgi_query_parameters_to_url(&sURI); |
| 669 | if( PB("nofiles") ){ |
| 670 | showDirOnly = 1; |
| 671 | }else{ |
| 672 | showDirOnly = 0; |
| 673 | } |
| 674 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 675 | if( PB("expand") ){ |
| 676 | startExpanded = 1; |
| 677 | }else{ |
| @@ -662,37 +700,55 @@ | |
| 700 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 701 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 702 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 703 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 704 | " FROM event WHERE objid=%d", rid); |
| 705 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); |
| 706 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 707 | Th_Store("current_checkin", zCI); |
| 708 | }else{ |
| 709 | zCI = 0; |
| 710 | } |
| 711 | } |
| 712 | if( zCI==0 ){ |
| 713 | rNow = db_double(0.0, "SELECT max(mtime) FROM event"); |
| 714 | zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event"); |
| 715 | } |
| 716 | |
| 717 | assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) ); |
| 718 | if( zD==0 ){ |
| 719 | if( zCI ){ |
| 720 | zHeader = mprintf("Top-level Files of %s", zCI); |
| 721 | }else{ |
| 722 | zHeader = mprintf("All Top-level Files"); |
| 723 | } |
| 724 | }else{ |
| 725 | if( zCI ){ |
| 726 | zHeader = mprintf("Files in %s/ of %s", zD, zCI); |
| 727 | }else{ |
| 728 | zHeader = mprintf("All File in %s/", zD); |
| 729 | } |
| 730 | } |
| 731 | style_header("%s", zHeader); |
| 732 | fossil_free(zHeader); |
| 733 | |
| 734 | /* Compute the title of the page */ |
| 735 | blob_zero(&dirname); |
| 736 | if( zD ){ |
| 737 | blob_append(&dirname, "within directory ", -1); |
| 738 | hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0); |
| 739 | if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE); |
| 740 | style_submenu_element("Top-Level", "%s", |
| 741 | url_render(&sURI, "name", 0, 0, 0)); |
| 742 | }else if( zRE ){ |
| 743 | blob_appendf(&dirname, "matching \"%s\"", zRE); |
| 744 | } |
| 745 | style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0); |
| 746 | if( zCI ){ |
| 747 | style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0)); |
| 748 | if( nD==0 && !showDirOnly ){ |
| 749 | style_submenu_element("File Ages", "%R/fileage?name=%T", zCI); |
| 750 | } |
| 751 | } |
| 752 | if( linkTrunk ){ |
| 753 | style_submenu_element("Trunk", "%s", |
| 754 | url_render(&sURI, "ci", "trunk", 0, 0)); |
| @@ -747,10 +803,11 @@ | |
| 803 | tree_add_node(&sTree, zName, zUuid, mtime); |
| 804 | nFile++; |
| 805 | } |
| 806 | db_finalize(&q); |
| 807 | } |
| 808 | style_submenu_checkbox("nofiles", "Folders Only", 0, 0); |
| 809 | |
| 810 | if( showDirOnly ){ |
| 811 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 812 | if( p->pChild!=0 && p->nFullName>nD ) nFile++; |
| 813 | } |
| @@ -757,18 +814,24 @@ | |
| 814 | zObjType = "Folders"; |
| 815 | }else{ |
| 816 | zObjType = "Files"; |
| 817 | } |
| 818 | |
| 819 | if( zCI && strcmp(zCI,"tip")==0 ){ |
| 820 | @ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a> |
| 821 | }else if( isBranchCI ){ |
| 822 | @ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\ |
| 823 | @ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a> |
| 824 | if( blob_size(&dirname) ){ |
| 825 | @ and %s(blob_str(&dirname))</h2> |
| 826 | } |
| 827 | }else if( zCI ){ |
| 828 | @ <h2>%s(zObjType) for check-in \ |
| 829 | @ %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2> |
| 830 | if( blob_size(&dirname) ){ |
| 831 | @ and %s(blob_str(&dirname))</h2> |
| 832 | } |
| 833 | }else{ |
| 834 | int n = db_int(0, "SELECT count(*) FROM plink"); |
| 835 | @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) |
| 836 | } |
| 837 | if( useMtime ){ |
| @@ -826,11 +889,11 @@ | |
| 889 | nDir++; |
| 890 | }else if( !showDirOnly ){ |
| 891 | const char *zFileClass = fileext_class(p->zName); |
| 892 | char *zLink; |
| 893 | if( zCI ){ |
| 894 | zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI); |
| 895 | }else{ |
| 896 | zLink = href("%R/finfo?name=%T",p->zFullName); |
| 897 | } |
| 898 | @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline"> |
| 899 | @ %z(zLink)%h(p->zName)</a> |
| @@ -920,11 +983,11 @@ | |
| 983 | ** mtime on that check-in. If zGlob and *zGlob then only files matching |
| 984 | ** the given glob are computed. |
| 985 | */ |
| 986 | int compute_fileage(int vid, const char* zGlob){ |
| 987 | Stmt q; |
| 988 | db_exec_sql(zComputeFileAgeSetup); |
| 989 | db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/); |
| 990 | db_bind_int(&q, ":ckin", vid); |
| 991 | db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*"); |
| 992 | db_exec(&q); |
| 993 | db_finalize(&q); |
| @@ -1003,10 +1066,11 @@ | |
| 1066 | int rid; |
| 1067 | const char *zName; |
| 1068 | const char *zGlob; |
| 1069 | const char *zUuid; |
| 1070 | const char *zNow; /* Time of check-in */ |
| 1071 | int isBranchCI; /* name= is a branch name */ |
| 1072 | int showId = PB("showid"); |
| 1073 | Stmt q1, q2; |
| 1074 | double baseTime; |
| 1075 | login_check_credentials(); |
| 1076 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| @@ -1016,28 +1080,34 @@ | |
| 1080 | rid = symbolic_name_to_rid(zName, "ci"); |
| 1081 | if( rid==0 ){ |
| 1082 | fossil_fatal("not a valid check-in: %s", zName); |
| 1083 | } |
| 1084 | zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1085 | isBranchCI = branch_includes_uuid(zName,zUuid); |
| 1086 | baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); |
| 1087 | zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event" |
| 1088 | " WHERE objid=%d", rid); |
| 1089 | style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName); |
| 1090 | style_header("File Ages"); |
| 1091 | zGlob = P("glob"); |
| 1092 | compute_fileage(rid,zGlob); |
| 1093 | db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); |
| 1094 | |
| 1095 | if( fossil_strcmp(zName,"tip")==0 ){ |
| 1096 | @ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a> |
| 1097 | }else if( isBranchCI ){ |
| 1098 | @ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a> |
| 1099 | @ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a> |
| 1100 | }else{ |
| 1101 | @ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a> |
| 1102 | } |
| 1103 | if( zGlob && zGlob[0] ){ |
| 1104 | @ that match "%h(zGlob)" |
| 1105 | } |
| 1106 | @ ordered by age</h1> |
| 1107 | @ |
| 1108 | @ <p>File ages are expressed relative to the check-in time of |
| 1109 | @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
| 1110 | @ |
| 1111 | @ <div class='fileage'><table> |
| 1112 | @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr> |
| 1113 | db_prepare(&q1, |
| @@ -1052,14 +1122,13 @@ | |
| 1122 | " AND blob.rid=event.objid\n" |
| 1123 | " ORDER BY event.mtime DESC;", |
| 1124 | TAG_BRANCH |
| 1125 | ); |
| 1126 | db_prepare(&q2, |
| 1127 | "SELECT filename.name, fileage.fid\n" |
| 1128 | " FROM fileage, filename\n" |
| 1129 | " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid" |
| 1130 | ); |
| 1131 | while( db_step(&q1)==SQLITE_ROW ){ |
| 1132 | double age = baseTime - db_column_double(&q1, 0); |
| 1133 | int mid = db_column_int(&q1, 1); |
| 1134 | const char *zUuid = db_column_text(&q1, 2); |
| @@ -1069,24 +1138,24 @@ | |
| 1138 | char *zAge = human_readable_age(age); |
| 1139 | @ <tr><td>%s(zAge)</td> |
| 1140 | @ <td> |
| 1141 | db_bind_int(&q2, ":mid", mid); |
| 1142 | while( db_step(&q2)==SQLITE_ROW ){ |
| 1143 | const char *zFile = db_column_text(&q2,0); |
| 1144 | @ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \ |
| 1145 | if( showId ){ |
| 1146 | int fid = db_column_int(&q2,1); |
| 1147 | @ (%d(fid))<br /> |
| 1148 | }else{ |
| 1149 | @ </a><br /> |
| 1150 | } |
| 1151 | } |
| 1152 | db_reset(&q2); |
| 1153 | @ </td> |
| 1154 | @ <td> |
| 1155 | @ %W(zComment) |
| 1156 | @ (check-in: %z(href("%R/info/%!S",zUuid))%S(zUuid)</a>, |
| 1157 | if( showId ){ |
| 1158 | @ id: %d(mid) |
| 1159 | } |
| 1160 | @ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
| 1161 | @ branch: \ |
| 1162 |
+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 |
+26
-10
| --- src/capabilities.c | ||
| +++ src/capabilities.c | ||
| @@ -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, |
| @@ -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 | |
| @@ -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, |
| @@ -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 | |
| @@ -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, |
| @@ -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 |
+107
-45
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -174,14 +174,16 @@ | ||
| 174 | 174 | } |
| 175 | 175 | |
| 176 | 176 | /* |
| 177 | 177 | ** Additional information used to form the HTTP reply |
| 178 | 178 | */ |
| 179 | -static const char *zContentType = "text/html"; /* Content type of the reply */ | |
| 180 | -static const char *zReplyStatus = "OK"; /* Reply status description */ | |
| 179 | +static const char *zContentType = "text/html"; /* Content type of the reply */ | |
| 180 | +static const char *zReplyStatus = "OK"; /* Reply status description */ | |
| 181 | 181 | static int iReplyStatus = 200; /* Reply status code */ |
| 182 | 182 | static Blob extraHeader = BLOB_INITIALIZER; /* Extra header text */ |
| 183 | +static int rangeStart = 0; /* Start of Range: */ | |
| 184 | +static int rangeEnd = 0; /* End of Range: plus 1 */ | |
| 183 | 185 | |
| 184 | 186 | /* |
| 185 | 187 | ** Set the reply content type |
| 186 | 188 | */ |
| 187 | 189 | void cgi_set_content_type(const char *zType){ |
| @@ -209,10 +211,16 @@ | ||
| 209 | 211 | /* |
| 210 | 212 | ** Append text to the header of an HTTP reply |
| 211 | 213 | */ |
| 212 | 214 | void cgi_append_header(const char *zLine){ |
| 213 | 215 | blob_append(&extraHeader, zLine, -1); |
| 216 | +} | |
| 217 | +void cgi_printf_header(const char *zLine, ...){ | |
| 218 | + va_list ap; | |
| 219 | + va_start(ap, zLine); | |
| 220 | + blob_vappendf(&extraHeader, zLine, ap); | |
| 221 | + va_end(ap); | |
| 214 | 222 | } |
| 215 | 223 | |
| 216 | 224 | /* |
| 217 | 225 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 218 | 226 | ** !g.isHTTP, this is a no-op. |
| @@ -266,22 +274,30 @@ | ||
| 266 | 274 | iReplyStatus = 200; |
| 267 | 275 | zReplyStatus = "OK"; |
| 268 | 276 | } |
| 269 | 277 | |
| 270 | 278 | if( g.fullHttpReply ){ |
| 279 | + if( rangeEnd>0 | |
| 280 | + && iReplyStatus==200 | |
| 281 | + && fossil_strcmp(P("REQUEST_METHOD"),"GET")==0 | |
| 282 | + ){ | |
| 283 | + iReplyStatus = 206; | |
| 284 | + zReplyStatus = "Partial Content"; | |
| 285 | + } | |
| 271 | 286 | fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); |
| 272 | 287 | fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); |
| 273 | 288 | fprintf(g.httpOut, "Connection: close\r\n"); |
| 274 | 289 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 275 | 290 | }else{ |
| 291 | + assert( rangeEnd==0 ); | |
| 276 | 292 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 277 | 293 | } |
| 278 | 294 | if( g.isConst ){ |
| 279 | 295 | /* isConst means that the reply is guaranteed to be invariant, even |
| 280 | 296 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 281 | 297 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 282 | - }else if( etag_tag()!=0 ){ | |
| 298 | + }else if( etag_tag()[0]!=0 ){ | |
| 283 | 299 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 284 | 300 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 285 | 301 | }else{ |
| 286 | 302 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 287 | 303 | } |
| @@ -313,18 +329,18 @@ | ||
| 313 | 329 | */ |
| 314 | 330 | |
| 315 | 331 | /* Content intended for logged in users should only be cached in |
| 316 | 332 | ** the browser, not some shared location. |
| 317 | 333 | */ |
| 318 | - fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType); | |
| 319 | - if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){ | |
| 320 | - cgi_combine_header_and_body(); | |
| 321 | - blob_compress(&cgiContent[0], &cgiContent[0]); | |
| 322 | - } | |
| 323 | - | |
| 324 | - if( iReplyStatus != 304 ) { | |
| 325 | - if( is_gzippable() ){ | |
| 334 | + if( iReplyStatus!=304 ) { | |
| 335 | + fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType); | |
| 336 | + if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){ | |
| 337 | + cgi_combine_header_and_body(); | |
| 338 | + blob_compress(&cgiContent[0], &cgiContent[0]); | |
| 339 | + } | |
| 340 | + | |
| 341 | + if( is_gzippable() && iReplyStatus!=206 ){ | |
| 326 | 342 | int i; |
| 327 | 343 | gzip_begin(0); |
| 328 | 344 | for( i=0; i<2; i++ ){ |
| 329 | 345 | int size = blob_size(&cgiContent[i]); |
| 330 | 346 | if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size); |
| @@ -333,23 +349,37 @@ | ||
| 333 | 349 | gzip_finish(&cgiContent[0]); |
| 334 | 350 | fprintf(g.httpOut, "Content-Encoding: gzip\r\n"); |
| 335 | 351 | fprintf(g.httpOut, "Vary: Accept-Encoding\r\n"); |
| 336 | 352 | } |
| 337 | 353 | total_size = blob_size(&cgiContent[0]) + blob_size(&cgiContent[1]); |
| 354 | + if( iReplyStatus==206 ){ | |
| 355 | + fprintf(g.httpOut, "Content-Range: bytes %d-%d/%d\r\n", | |
| 356 | + rangeStart, rangeEnd-1, total_size); | |
| 357 | + total_size = rangeEnd - rangeStart; | |
| 358 | + } | |
| 338 | 359 | fprintf(g.httpOut, "Content-Length: %d\r\n", total_size); |
| 339 | 360 | }else{ |
| 340 | 361 | total_size = 0; |
| 341 | 362 | } |
| 342 | 363 | fprintf(g.httpOut, "\r\n"); |
| 343 | - if( total_size>0 && iReplyStatus != 304 | |
| 364 | + if( total_size>0 | |
| 365 | + && iReplyStatus!=304 | |
| 344 | 366 | && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 |
| 345 | 367 | ){ |
| 346 | 368 | int i, size; |
| 347 | 369 | for(i=0; i<2; i++){ |
| 348 | 370 | size = blob_size(&cgiContent[i]); |
| 349 | - if( size>0 ){ | |
| 350 | - fwrite(blob_buffer(&cgiContent[i]), 1, size, g.httpOut); | |
| 371 | + if( size<=rangeStart ){ | |
| 372 | + rangeStart -= size; | |
| 373 | + }else{ | |
| 374 | + int n = size - rangeStart; | |
| 375 | + if( n>total_size ){ | |
| 376 | + n = total_size; | |
| 377 | + } | |
| 378 | + fwrite(blob_buffer(&cgiContent[i])+rangeStart, 1, n, g.httpOut); | |
| 379 | + rangeStart = 0; | |
| 380 | + total_size -= n; | |
| 351 | 381 | } |
| 352 | 382 | } |
| 353 | 383 | } |
| 354 | 384 | fflush(g.httpOut); |
| 355 | 385 | CGIDEBUG(("-------- END cgi ---------\n")); |
| @@ -403,10 +433,31 @@ | ||
| 403 | 433 | va_list ap; |
| 404 | 434 | va_start(ap, zFormat); |
| 405 | 435 | cgi_redirect(vmprintf(zFormat, ap)); |
| 406 | 436 | va_end(ap); |
| 407 | 437 | } |
| 438 | + | |
| 439 | +/* | |
| 440 | +** Add a "Content-disposition: attachment; filename=%s" header to the reply. | |
| 441 | +*/ | |
| 442 | +void cgi_content_disposition_filename(const char *zFilename){ | |
| 443 | + char *z; | |
| 444 | + int i, n; | |
| 445 | + | |
| 446 | + /* 0123456789 123456789 123456789 123456789 123456*/ | |
| 447 | + z = mprintf("Content-Disposition: attachment; filename=\"%s\";\r\n", | |
| 448 | + file_tail(zFilename)); | |
| 449 | + n = (int)strlen(z); | |
| 450 | + for(i=43; i<n-4; i++){ | |
| 451 | + char c = z[i]; | |
| 452 | + if( fossil_isalnum(c) ) continue; | |
| 453 | + if( c=='.' || c=='-' || c=='/' ) continue; | |
| 454 | + z[i] = '_'; | |
| 455 | + } | |
| 456 | + cgi_append_header(z); | |
| 457 | + fossil_free(z); | |
| 458 | +} | |
| 408 | 459 | |
| 409 | 460 | /* |
| 410 | 461 | ** Return the URL for the caller. This is obtained from either the |
| 411 | 462 | ** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter. |
| 412 | 463 | ** If neither exist, return zDefault. |
| @@ -682,36 +733,36 @@ | ||
| 682 | 733 | return z; |
| 683 | 734 | } |
| 684 | 735 | |
| 685 | 736 | /* |
| 686 | 737 | ** The input *pz points to content that is terminated by a "\r\n" |
| 687 | -** followed by the boundry marker zBoundry. An extra "--" may or | |
| 688 | -** may not be appended to the boundry marker. There are *pLen characters | |
| 738 | +** followed by the boundary marker zBoundary. An extra "--" may or | |
| 739 | +** may not be appended to the boundary marker. There are *pLen characters | |
| 689 | 740 | ** in *pz. |
| 690 | 741 | ** |
| 691 | 742 | ** This routine adds a "\000" to the end of the content (overwriting |
| 692 | 743 | ** the "\r\n") and returns a pointer to the content. The *pz input |
| 693 | -** is adjusted to point to the first line following the boundry. | |
| 744 | +** is adjusted to point to the first line following the boundary. | |
| 694 | 745 | ** The length of the content is stored in *pnContent. |
| 695 | 746 | */ |
| 696 | 747 | static char *get_bounded_content( |
| 697 | 748 | char **pz, /* Content taken from here */ |
| 698 | 749 | int *pLen, /* Number of bytes of data in (*pz)[] */ |
| 699 | - char *zBoundry, /* Boundry text marking the end of content */ | |
| 750 | + char *zBoundary, /* Boundary text marking the end of content */ | |
| 700 | 751 | int *pnContent /* Write the size of the content here */ |
| 701 | 752 | ){ |
| 702 | 753 | char *z = *pz; |
| 703 | 754 | int len = *pLen; |
| 704 | 755 | int i; |
| 705 | - int nBoundry = strlen(zBoundry); | |
| 756 | + int nBoundary = strlen(zBoundary); | |
| 706 | 757 | *pnContent = len; |
| 707 | 758 | for(i=0; i<len; i++){ |
| 708 | - if( z[i]=='\n' && strncmp(zBoundry, &z[i+1], nBoundry)==0 ){ | |
| 759 | + if( z[i]=='\n' && strncmp(zBoundary, &z[i+1], nBoundary)==0 ){ | |
| 709 | 760 | if( i>0 && z[i-1]=='\r' ) i--; |
| 710 | 761 | z[i] = 0; |
| 711 | 762 | *pnContent = i; |
| 712 | - i += nBoundry; | |
| 763 | + i += nBoundary; | |
| 713 | 764 | break; |
| 714 | 765 | } |
| 715 | 766 | } |
| 716 | 767 | *pz = &z[i]; |
| 717 | 768 | get_line_from_string(pz, pLen); |
| @@ -776,22 +827,22 @@ | ||
| 776 | 827 | ** table. |
| 777 | 828 | */ |
| 778 | 829 | static void process_multipart_form_data(char *z, int len){ |
| 779 | 830 | char *zLine; |
| 780 | 831 | int nArg, i; |
| 781 | - char *zBoundry; | |
| 832 | + char *zBoundary; | |
| 782 | 833 | char *zValue; |
| 783 | 834 | char *zName = 0; |
| 784 | 835 | int showBytes = 0; |
| 785 | 836 | char *azArg[50]; |
| 786 | 837 | |
| 787 | - zBoundry = get_line_from_string(&z, &len); | |
| 788 | - if( zBoundry==0 ) return; | |
| 838 | + zBoundary = get_line_from_string(&z, &len); | |
| 839 | + if( zBoundary==0 ) return; | |
| 789 | 840 | while( (zLine = get_line_from_string(&z, &len))!=0 ){ |
| 790 | 841 | if( zLine[0]==0 ){ |
| 791 | 842 | int nContent = 0; |
| 792 | - zValue = get_bounded_content(&z, &len, zBoundry, &nContent); | |
| 843 | + zValue = get_bounded_content(&z, &len, zBoundary, &nContent); | |
| 793 | 844 | if( zName && zValue ){ |
| 794 | 845 | if( fossil_islower(zName[0]) ){ |
| 795 | 846 | cgi_set_parameter_nocopy(zName, zValue, 1); |
| 796 | 847 | if( showBytes ){ |
| 797 | 848 | cgi_set_parameter_nocopy(mprintf("%s:bytes", zName), |
| @@ -1035,13 +1086,27 @@ | ||
| 1035 | 1086 | } |
| 1036 | 1087 | if( zPathInfo==0 ){ |
| 1037 | 1088 | int i, j; |
| 1038 | 1089 | for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){} |
| 1039 | 1090 | for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){} |
| 1040 | - cgi_set_parameter("PATH_INFO", mprintf("%.*s", j-i, zRequestUri+i)); | |
| 1091 | + zPathInfo = mprintf("%.*s", j-i, zRequestUri+i); | |
| 1092 | + cgi_set_parameter("PATH_INFO", zPathInfo); | |
| 1093 | + } | |
| 1094 | +#ifdef FOSSIL_ENABLE_JSON | |
| 1095 | + if(strncmp("/json",zPathInfo,5)==0 | |
| 1096 | + && (zPathInfo[5]==0 || zPathInfo[5]=='/')){ | |
| 1097 | + /* We need to change some following behaviour depending on whether | |
| 1098 | + ** we are operating in JSON mode or not. We cannot, however, be | |
| 1099 | + ** certain whether we should/need to be in JSON mode until the | |
| 1100 | + ** PATH_INFO is set up. | |
| 1101 | + */ | |
| 1102 | + g.json.isJsonMode = 1; | |
| 1103 | + }else{ | |
| 1104 | + assert(!g.json.isJsonMode && | |
| 1105 | + "Internal misconfiguration of g.json.isJsonMode"); | |
| 1041 | 1106 | } |
| 1042 | - | |
| 1107 | +#endif | |
| 1043 | 1108 | z = (char*)P("HTTP_COOKIE"); |
| 1044 | 1109 | if( z ){ |
| 1045 | 1110 | z = mprintf("%s",z); |
| 1046 | 1111 | add_param_list(z, ';'); |
| 1047 | 1112 | } |
| @@ -1071,29 +1136,18 @@ | ||
| 1071 | 1136 | if( fossil_strcmp(zType, "application/x-fossil")==0 ){ |
| 1072 | 1137 | blob_read_from_channel(&g.cgiIn, g.httpIn, len); |
| 1073 | 1138 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1074 | 1139 | } |
| 1075 | 1140 | #ifdef FOSSIL_ENABLE_JSON |
| 1076 | - else if( noJson==0 && (fossil_strcmp(zType, "application/json")==0 | |
| 1077 | - || fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/ | |
| 1078 | - || fossil_strcmp(zType,"application/javascript")==0) ){ | |
| 1079 | - g.json.isJsonMode = 1; | |
| 1141 | + else if( noJson==0 && g.json.isJsonMode!=0 | |
| 1142 | + && json_can_consume_content_type(zType)!=0 ){ | |
| 1080 | 1143 | cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); |
| 1081 | - /* FIXMEs: | |
| 1082 | - | |
| 1083 | - - See if fossil really needs g.cgiIn to be set for this purpose | |
| 1084 | - (i don't think it does). If it does then fill g.cgiIn and | |
| 1085 | - refactor to parse the JSON from there. | |
| 1086 | - | |
| 1087 | - - After parsing POST JSON, copy the "first layer" of keys/values | |
| 1088 | - to cgi_setenv(), honoring the upper-case distinction used | |
| 1089 | - in add_param_list(). However... | |
| 1090 | - | |
| 1091 | - - If we do that then we might get a disconnect in precedence of | |
| 1092 | - GET/POST arguments. i prefer for GET entries to take precedence | |
| 1093 | - over like-named POST entries, but in order for that to happen we | |
| 1094 | - need to process QUERY_STRING _after_ reading the POST data. | |
| 1144 | + /* | |
| 1145 | + Potential TODOs: | |
| 1146 | + | |
| 1147 | + 1) If parsing fails, immediately return an error response | |
| 1148 | + without dispatching the ostensibly-upcoming JSON API. | |
| 1095 | 1149 | */ |
| 1096 | 1150 | cgi_set_content_type(json_guess_content_type()); |
| 1097 | 1151 | } |
| 1098 | 1152 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1099 | 1153 | else{ |
| @@ -1603,10 +1657,11 @@ | ||
| 1603 | 1657 | } |
| 1604 | 1658 | if( zIpAddr ){ |
| 1605 | 1659 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 1606 | 1660 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1607 | 1661 | } |
| 1662 | + | |
| 1608 | 1663 | |
| 1609 | 1664 | /* Get all the optional fields that follow the first line. |
| 1610 | 1665 | */ |
| 1611 | 1666 | while( fgets(zLine,sizeof(zLine),g.httpIn) ){ |
| 1612 | 1667 | char *zFieldName; |
| @@ -1648,10 +1703,17 @@ | ||
| 1648 | 1703 | }else if( fossil_strcmp(zFieldName,"x-forwarded-for:")==0 ){ |
| 1649 | 1704 | const char *zIpAddr = cgi_accept_forwarded_for(zVal); |
| 1650 | 1705 | if( zIpAddr!=0 ){ |
| 1651 | 1706 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1652 | 1707 | cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr); |
| 1708 | + } | |
| 1709 | + }else if( fossil_strcmp(zFieldName,"range:")==0 ){ | |
| 1710 | + int x1 = 0; | |
| 1711 | + int x2 = 0; | |
| 1712 | + if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){ | |
| 1713 | + rangeStart = x1; | |
| 1714 | + rangeEnd = x2+1; | |
| 1653 | 1715 | } |
| 1654 | 1716 | } |
| 1655 | 1717 | } |
| 1656 | 1718 | cgi_init(); |
| 1657 | 1719 | cgi_trace(0); |
| 1658 | 1720 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -174,14 +174,16 @@ | |
| 174 | } |
| 175 | |
| 176 | /* |
| 177 | ** Additional information used to form the HTTP reply |
| 178 | */ |
| 179 | static const char *zContentType = "text/html"; /* Content type of the reply */ |
| 180 | static const char *zReplyStatus = "OK"; /* Reply status description */ |
| 181 | static int iReplyStatus = 200; /* Reply status code */ |
| 182 | static Blob extraHeader = BLOB_INITIALIZER; /* Extra header text */ |
| 183 | |
| 184 | /* |
| 185 | ** Set the reply content type |
| 186 | */ |
| 187 | void cgi_set_content_type(const char *zType){ |
| @@ -209,10 +211,16 @@ | |
| 209 | /* |
| 210 | ** Append text to the header of an HTTP reply |
| 211 | */ |
| 212 | void cgi_append_header(const char *zLine){ |
| 213 | blob_append(&extraHeader, zLine, -1); |
| 214 | } |
| 215 | |
| 216 | /* |
| 217 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 218 | ** !g.isHTTP, this is a no-op. |
| @@ -266,22 +274,30 @@ | |
| 266 | iReplyStatus = 200; |
| 267 | zReplyStatus = "OK"; |
| 268 | } |
| 269 | |
| 270 | if( g.fullHttpReply ){ |
| 271 | fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); |
| 272 | fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); |
| 273 | fprintf(g.httpOut, "Connection: close\r\n"); |
| 274 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 275 | }else{ |
| 276 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 277 | } |
| 278 | if( g.isConst ){ |
| 279 | /* isConst means that the reply is guaranteed to be invariant, even |
| 280 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 281 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 282 | }else if( etag_tag()!=0 ){ |
| 283 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 284 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 285 | }else{ |
| 286 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 287 | } |
| @@ -313,18 +329,18 @@ | |
| 313 | */ |
| 314 | |
| 315 | /* Content intended for logged in users should only be cached in |
| 316 | ** the browser, not some shared location. |
| 317 | */ |
| 318 | fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType); |
| 319 | if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){ |
| 320 | cgi_combine_header_and_body(); |
| 321 | blob_compress(&cgiContent[0], &cgiContent[0]); |
| 322 | } |
| 323 | |
| 324 | if( iReplyStatus != 304 ) { |
| 325 | if( is_gzippable() ){ |
| 326 | int i; |
| 327 | gzip_begin(0); |
| 328 | for( i=0; i<2; i++ ){ |
| 329 | int size = blob_size(&cgiContent[i]); |
| 330 | if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size); |
| @@ -333,23 +349,37 @@ | |
| 333 | gzip_finish(&cgiContent[0]); |
| 334 | fprintf(g.httpOut, "Content-Encoding: gzip\r\n"); |
| 335 | fprintf(g.httpOut, "Vary: Accept-Encoding\r\n"); |
| 336 | } |
| 337 | total_size = blob_size(&cgiContent[0]) + blob_size(&cgiContent[1]); |
| 338 | fprintf(g.httpOut, "Content-Length: %d\r\n", total_size); |
| 339 | }else{ |
| 340 | total_size = 0; |
| 341 | } |
| 342 | fprintf(g.httpOut, "\r\n"); |
| 343 | if( total_size>0 && iReplyStatus != 304 |
| 344 | && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 |
| 345 | ){ |
| 346 | int i, size; |
| 347 | for(i=0; i<2; i++){ |
| 348 | size = blob_size(&cgiContent[i]); |
| 349 | if( size>0 ){ |
| 350 | fwrite(blob_buffer(&cgiContent[i]), 1, size, g.httpOut); |
| 351 | } |
| 352 | } |
| 353 | } |
| 354 | fflush(g.httpOut); |
| 355 | CGIDEBUG(("-------- END cgi ---------\n")); |
| @@ -403,10 +433,31 @@ | |
| 403 | va_list ap; |
| 404 | va_start(ap, zFormat); |
| 405 | cgi_redirect(vmprintf(zFormat, ap)); |
| 406 | va_end(ap); |
| 407 | } |
| 408 | |
| 409 | /* |
| 410 | ** Return the URL for the caller. This is obtained from either the |
| 411 | ** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter. |
| 412 | ** If neither exist, return zDefault. |
| @@ -682,36 +733,36 @@ | |
| 682 | return z; |
| 683 | } |
| 684 | |
| 685 | /* |
| 686 | ** The input *pz points to content that is terminated by a "\r\n" |
| 687 | ** followed by the boundry marker zBoundry. An extra "--" may or |
| 688 | ** may not be appended to the boundry marker. There are *pLen characters |
| 689 | ** in *pz. |
| 690 | ** |
| 691 | ** This routine adds a "\000" to the end of the content (overwriting |
| 692 | ** the "\r\n") and returns a pointer to the content. The *pz input |
| 693 | ** is adjusted to point to the first line following the boundry. |
| 694 | ** The length of the content is stored in *pnContent. |
| 695 | */ |
| 696 | static char *get_bounded_content( |
| 697 | char **pz, /* Content taken from here */ |
| 698 | int *pLen, /* Number of bytes of data in (*pz)[] */ |
| 699 | char *zBoundry, /* Boundry text marking the end of content */ |
| 700 | int *pnContent /* Write the size of the content here */ |
| 701 | ){ |
| 702 | char *z = *pz; |
| 703 | int len = *pLen; |
| 704 | int i; |
| 705 | int nBoundry = strlen(zBoundry); |
| 706 | *pnContent = len; |
| 707 | for(i=0; i<len; i++){ |
| 708 | if( z[i]=='\n' && strncmp(zBoundry, &z[i+1], nBoundry)==0 ){ |
| 709 | if( i>0 && z[i-1]=='\r' ) i--; |
| 710 | z[i] = 0; |
| 711 | *pnContent = i; |
| 712 | i += nBoundry; |
| 713 | break; |
| 714 | } |
| 715 | } |
| 716 | *pz = &z[i]; |
| 717 | get_line_from_string(pz, pLen); |
| @@ -776,22 +827,22 @@ | |
| 776 | ** table. |
| 777 | */ |
| 778 | static void process_multipart_form_data(char *z, int len){ |
| 779 | char *zLine; |
| 780 | int nArg, i; |
| 781 | char *zBoundry; |
| 782 | char *zValue; |
| 783 | char *zName = 0; |
| 784 | int showBytes = 0; |
| 785 | char *azArg[50]; |
| 786 | |
| 787 | zBoundry = get_line_from_string(&z, &len); |
| 788 | if( zBoundry==0 ) return; |
| 789 | while( (zLine = get_line_from_string(&z, &len))!=0 ){ |
| 790 | if( zLine[0]==0 ){ |
| 791 | int nContent = 0; |
| 792 | zValue = get_bounded_content(&z, &len, zBoundry, &nContent); |
| 793 | if( zName && zValue ){ |
| 794 | if( fossil_islower(zName[0]) ){ |
| 795 | cgi_set_parameter_nocopy(zName, zValue, 1); |
| 796 | if( showBytes ){ |
| 797 | cgi_set_parameter_nocopy(mprintf("%s:bytes", zName), |
| @@ -1035,13 +1086,27 @@ | |
| 1035 | } |
| 1036 | if( zPathInfo==0 ){ |
| 1037 | int i, j; |
| 1038 | for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){} |
| 1039 | for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){} |
| 1040 | cgi_set_parameter("PATH_INFO", mprintf("%.*s", j-i, zRequestUri+i)); |
| 1041 | } |
| 1042 | |
| 1043 | z = (char*)P("HTTP_COOKIE"); |
| 1044 | if( z ){ |
| 1045 | z = mprintf("%s",z); |
| 1046 | add_param_list(z, ';'); |
| 1047 | } |
| @@ -1071,29 +1136,18 @@ | |
| 1071 | if( fossil_strcmp(zType, "application/x-fossil")==0 ){ |
| 1072 | blob_read_from_channel(&g.cgiIn, g.httpIn, len); |
| 1073 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1074 | } |
| 1075 | #ifdef FOSSIL_ENABLE_JSON |
| 1076 | else if( noJson==0 && (fossil_strcmp(zType, "application/json")==0 |
| 1077 | || fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/ |
| 1078 | || fossil_strcmp(zType,"application/javascript")==0) ){ |
| 1079 | g.json.isJsonMode = 1; |
| 1080 | cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); |
| 1081 | /* FIXMEs: |
| 1082 | |
| 1083 | - See if fossil really needs g.cgiIn to be set for this purpose |
| 1084 | (i don't think it does). If it does then fill g.cgiIn and |
| 1085 | refactor to parse the JSON from there. |
| 1086 | |
| 1087 | - After parsing POST JSON, copy the "first layer" of keys/values |
| 1088 | to cgi_setenv(), honoring the upper-case distinction used |
| 1089 | in add_param_list(). However... |
| 1090 | |
| 1091 | - If we do that then we might get a disconnect in precedence of |
| 1092 | GET/POST arguments. i prefer for GET entries to take precedence |
| 1093 | over like-named POST entries, but in order for that to happen we |
| 1094 | need to process QUERY_STRING _after_ reading the POST data. |
| 1095 | */ |
| 1096 | cgi_set_content_type(json_guess_content_type()); |
| 1097 | } |
| 1098 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1099 | else{ |
| @@ -1603,10 +1657,11 @@ | |
| 1603 | } |
| 1604 | if( zIpAddr ){ |
| 1605 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 1606 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1607 | } |
| 1608 | |
| 1609 | /* Get all the optional fields that follow the first line. |
| 1610 | */ |
| 1611 | while( fgets(zLine,sizeof(zLine),g.httpIn) ){ |
| 1612 | char *zFieldName; |
| @@ -1648,10 +1703,17 @@ | |
| 1648 | }else if( fossil_strcmp(zFieldName,"x-forwarded-for:")==0 ){ |
| 1649 | const char *zIpAddr = cgi_accept_forwarded_for(zVal); |
| 1650 | if( zIpAddr!=0 ){ |
| 1651 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1652 | cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr); |
| 1653 | } |
| 1654 | } |
| 1655 | } |
| 1656 | cgi_init(); |
| 1657 | cgi_trace(0); |
| 1658 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -174,14 +174,16 @@ | |
| 174 | } |
| 175 | |
| 176 | /* |
| 177 | ** Additional information used to form the HTTP reply |
| 178 | */ |
| 179 | static const char *zContentType = "text/html"; /* Content type of the reply */ |
| 180 | static const char *zReplyStatus = "OK"; /* Reply status description */ |
| 181 | static int iReplyStatus = 200; /* Reply status code */ |
| 182 | static Blob extraHeader = BLOB_INITIALIZER; /* Extra header text */ |
| 183 | static int rangeStart = 0; /* Start of Range: */ |
| 184 | static int rangeEnd = 0; /* End of Range: plus 1 */ |
| 185 | |
| 186 | /* |
| 187 | ** Set the reply content type |
| 188 | */ |
| 189 | void cgi_set_content_type(const char *zType){ |
| @@ -209,10 +211,16 @@ | |
| 211 | /* |
| 212 | ** Append text to the header of an HTTP reply |
| 213 | */ |
| 214 | void cgi_append_header(const char *zLine){ |
| 215 | blob_append(&extraHeader, zLine, -1); |
| 216 | } |
| 217 | void cgi_printf_header(const char *zLine, ...){ |
| 218 | va_list ap; |
| 219 | va_start(ap, zLine); |
| 220 | blob_vappendf(&extraHeader, zLine, ap); |
| 221 | va_end(ap); |
| 222 | } |
| 223 | |
| 224 | /* |
| 225 | ** Set a cookie by queuing up the appropriate HTTP header output. If |
| 226 | ** !g.isHTTP, this is a no-op. |
| @@ -266,22 +274,30 @@ | |
| 274 | iReplyStatus = 200; |
| 275 | zReplyStatus = "OK"; |
| 276 | } |
| 277 | |
| 278 | if( g.fullHttpReply ){ |
| 279 | if( rangeEnd>0 |
| 280 | && iReplyStatus==200 |
| 281 | && fossil_strcmp(P("REQUEST_METHOD"),"GET")==0 |
| 282 | ){ |
| 283 | iReplyStatus = 206; |
| 284 | zReplyStatus = "Partial Content"; |
| 285 | } |
| 286 | fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); |
| 287 | fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); |
| 288 | fprintf(g.httpOut, "Connection: close\r\n"); |
| 289 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 290 | }else{ |
| 291 | assert( rangeEnd==0 ); |
| 292 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 293 | } |
| 294 | if( g.isConst ){ |
| 295 | /* isConst means that the reply is guaranteed to be invariant, even |
| 296 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 297 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 298 | }else if( etag_tag()[0]!=0 ){ |
| 299 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 300 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 301 | }else{ |
| 302 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 303 | } |
| @@ -313,18 +329,18 @@ | |
| 329 | */ |
| 330 | |
| 331 | /* Content intended for logged in users should only be cached in |
| 332 | ** the browser, not some shared location. |
| 333 | */ |
| 334 | if( iReplyStatus!=304 ) { |
| 335 | fprintf(g.httpOut, "Content-Type: %s; charset=utf-8\r\n", zContentType); |
| 336 | if( fossil_strcmp(zContentType,"application/x-fossil")==0 ){ |
| 337 | cgi_combine_header_and_body(); |
| 338 | blob_compress(&cgiContent[0], &cgiContent[0]); |
| 339 | } |
| 340 | |
| 341 | if( is_gzippable() && iReplyStatus!=206 ){ |
| 342 | int i; |
| 343 | gzip_begin(0); |
| 344 | for( i=0; i<2; i++ ){ |
| 345 | int size = blob_size(&cgiContent[i]); |
| 346 | if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size); |
| @@ -333,23 +349,37 @@ | |
| 349 | gzip_finish(&cgiContent[0]); |
| 350 | fprintf(g.httpOut, "Content-Encoding: gzip\r\n"); |
| 351 | fprintf(g.httpOut, "Vary: Accept-Encoding\r\n"); |
| 352 | } |
| 353 | total_size = blob_size(&cgiContent[0]) + blob_size(&cgiContent[1]); |
| 354 | if( iReplyStatus==206 ){ |
| 355 | fprintf(g.httpOut, "Content-Range: bytes %d-%d/%d\r\n", |
| 356 | rangeStart, rangeEnd-1, total_size); |
| 357 | total_size = rangeEnd - rangeStart; |
| 358 | } |
| 359 | fprintf(g.httpOut, "Content-Length: %d\r\n", total_size); |
| 360 | }else{ |
| 361 | total_size = 0; |
| 362 | } |
| 363 | fprintf(g.httpOut, "\r\n"); |
| 364 | if( total_size>0 |
| 365 | && iReplyStatus!=304 |
| 366 | && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 |
| 367 | ){ |
| 368 | int i, size; |
| 369 | for(i=0; i<2; i++){ |
| 370 | size = blob_size(&cgiContent[i]); |
| 371 | if( size<=rangeStart ){ |
| 372 | rangeStart -= size; |
| 373 | }else{ |
| 374 | int n = size - rangeStart; |
| 375 | if( n>total_size ){ |
| 376 | n = total_size; |
| 377 | } |
| 378 | fwrite(blob_buffer(&cgiContent[i])+rangeStart, 1, n, g.httpOut); |
| 379 | rangeStart = 0; |
| 380 | total_size -= n; |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | fflush(g.httpOut); |
| 385 | CGIDEBUG(("-------- END cgi ---------\n")); |
| @@ -403,10 +433,31 @@ | |
| 433 | va_list ap; |
| 434 | va_start(ap, zFormat); |
| 435 | cgi_redirect(vmprintf(zFormat, ap)); |
| 436 | va_end(ap); |
| 437 | } |
| 438 | |
| 439 | /* |
| 440 | ** Add a "Content-disposition: attachment; filename=%s" header to the reply. |
| 441 | */ |
| 442 | void cgi_content_disposition_filename(const char *zFilename){ |
| 443 | char *z; |
| 444 | int i, n; |
| 445 | |
| 446 | /* 0123456789 123456789 123456789 123456789 123456*/ |
| 447 | z = mprintf("Content-Disposition: attachment; filename=\"%s\";\r\n", |
| 448 | file_tail(zFilename)); |
| 449 | n = (int)strlen(z); |
| 450 | for(i=43; i<n-4; i++){ |
| 451 | char c = z[i]; |
| 452 | if( fossil_isalnum(c) ) continue; |
| 453 | if( c=='.' || c=='-' || c=='/' ) continue; |
| 454 | z[i] = '_'; |
| 455 | } |
| 456 | cgi_append_header(z); |
| 457 | fossil_free(z); |
| 458 | } |
| 459 | |
| 460 | /* |
| 461 | ** Return the URL for the caller. This is obtained from either the |
| 462 | ** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter. |
| 463 | ** If neither exist, return zDefault. |
| @@ -682,36 +733,36 @@ | |
| 733 | return z; |
| 734 | } |
| 735 | |
| 736 | /* |
| 737 | ** The input *pz points to content that is terminated by a "\r\n" |
| 738 | ** followed by the boundary marker zBoundary. An extra "--" may or |
| 739 | ** may not be appended to the boundary marker. There are *pLen characters |
| 740 | ** in *pz. |
| 741 | ** |
| 742 | ** This routine adds a "\000" to the end of the content (overwriting |
| 743 | ** the "\r\n") and returns a pointer to the content. The *pz input |
| 744 | ** is adjusted to point to the first line following the boundary. |
| 745 | ** The length of the content is stored in *pnContent. |
| 746 | */ |
| 747 | static char *get_bounded_content( |
| 748 | char **pz, /* Content taken from here */ |
| 749 | int *pLen, /* Number of bytes of data in (*pz)[] */ |
| 750 | char *zBoundary, /* Boundary text marking the end of content */ |
| 751 | int *pnContent /* Write the size of the content here */ |
| 752 | ){ |
| 753 | char *z = *pz; |
| 754 | int len = *pLen; |
| 755 | int i; |
| 756 | int nBoundary = strlen(zBoundary); |
| 757 | *pnContent = len; |
| 758 | for(i=0; i<len; i++){ |
| 759 | if( z[i]=='\n' && strncmp(zBoundary, &z[i+1], nBoundary)==0 ){ |
| 760 | if( i>0 && z[i-1]=='\r' ) i--; |
| 761 | z[i] = 0; |
| 762 | *pnContent = i; |
| 763 | i += nBoundary; |
| 764 | break; |
| 765 | } |
| 766 | } |
| 767 | *pz = &z[i]; |
| 768 | get_line_from_string(pz, pLen); |
| @@ -776,22 +827,22 @@ | |
| 827 | ** table. |
| 828 | */ |
| 829 | static void process_multipart_form_data(char *z, int len){ |
| 830 | char *zLine; |
| 831 | int nArg, i; |
| 832 | char *zBoundary; |
| 833 | char *zValue; |
| 834 | char *zName = 0; |
| 835 | int showBytes = 0; |
| 836 | char *azArg[50]; |
| 837 | |
| 838 | zBoundary = get_line_from_string(&z, &len); |
| 839 | if( zBoundary==0 ) return; |
| 840 | while( (zLine = get_line_from_string(&z, &len))!=0 ){ |
| 841 | if( zLine[0]==0 ){ |
| 842 | int nContent = 0; |
| 843 | zValue = get_bounded_content(&z, &len, zBoundary, &nContent); |
| 844 | if( zName && zValue ){ |
| 845 | if( fossil_islower(zName[0]) ){ |
| 846 | cgi_set_parameter_nocopy(zName, zValue, 1); |
| 847 | if( showBytes ){ |
| 848 | cgi_set_parameter_nocopy(mprintf("%s:bytes", zName), |
| @@ -1035,13 +1086,27 @@ | |
| 1086 | } |
| 1087 | if( zPathInfo==0 ){ |
| 1088 | int i, j; |
| 1089 | for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){} |
| 1090 | for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){} |
| 1091 | zPathInfo = mprintf("%.*s", j-i, zRequestUri+i); |
| 1092 | cgi_set_parameter("PATH_INFO", zPathInfo); |
| 1093 | } |
| 1094 | #ifdef FOSSIL_ENABLE_JSON |
| 1095 | if(strncmp("/json",zPathInfo,5)==0 |
| 1096 | && (zPathInfo[5]==0 || zPathInfo[5]=='/')){ |
| 1097 | /* We need to change some following behaviour depending on whether |
| 1098 | ** we are operating in JSON mode or not. We cannot, however, be |
| 1099 | ** certain whether we should/need to be in JSON mode until the |
| 1100 | ** PATH_INFO is set up. |
| 1101 | */ |
| 1102 | g.json.isJsonMode = 1; |
| 1103 | }else{ |
| 1104 | assert(!g.json.isJsonMode && |
| 1105 | "Internal misconfiguration of g.json.isJsonMode"); |
| 1106 | } |
| 1107 | #endif |
| 1108 | z = (char*)P("HTTP_COOKIE"); |
| 1109 | if( z ){ |
| 1110 | z = mprintf("%s",z); |
| 1111 | add_param_list(z, ';'); |
| 1112 | } |
| @@ -1071,29 +1136,18 @@ | |
| 1136 | if( fossil_strcmp(zType, "application/x-fossil")==0 ){ |
| 1137 | blob_read_from_channel(&g.cgiIn, g.httpIn, len); |
| 1138 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1139 | } |
| 1140 | #ifdef FOSSIL_ENABLE_JSON |
| 1141 | else if( noJson==0 && g.json.isJsonMode!=0 |
| 1142 | && json_can_consume_content_type(zType)!=0 ){ |
| 1143 | cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); |
| 1144 | /* |
| 1145 | Potential TODOs: |
| 1146 | |
| 1147 | 1) If parsing fails, immediately return an error response |
| 1148 | without dispatching the ostensibly-upcoming JSON API. |
| 1149 | */ |
| 1150 | cgi_set_content_type(json_guess_content_type()); |
| 1151 | } |
| 1152 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1153 | else{ |
| @@ -1603,10 +1657,11 @@ | |
| 1657 | } |
| 1658 | if( zIpAddr ){ |
| 1659 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 1660 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1661 | } |
| 1662 | |
| 1663 | |
| 1664 | /* Get all the optional fields that follow the first line. |
| 1665 | */ |
| 1666 | while( fgets(zLine,sizeof(zLine),g.httpIn) ){ |
| 1667 | char *zFieldName; |
| @@ -1648,10 +1703,17 @@ | |
| 1703 | }else if( fossil_strcmp(zFieldName,"x-forwarded-for:")==0 ){ |
| 1704 | const char *zIpAddr = cgi_accept_forwarded_for(zVal); |
| 1705 | if( zIpAddr!=0 ){ |
| 1706 | g.zIpAddr = mprintf("%s", zIpAddr); |
| 1707 | cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr); |
| 1708 | } |
| 1709 | }else if( fossil_strcmp(zFieldName,"range:")==0 ){ |
| 1710 | int x1 = 0; |
| 1711 | int x2 = 0; |
| 1712 | if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){ |
| 1713 | rangeStart = x1; |
| 1714 | rangeEnd = x2+1; |
| 1715 | } |
| 1716 | } |
| 1717 | } |
| 1718 | cgi_init(); |
| 1719 | cgi_trace(0); |
| 1720 |
+38
-13
| --- src/checkin.c | ||
| +++ src/checkin.c | ||
| @@ -1673,10 +1673,18 @@ | ||
| 1673 | 1673 | while( db_step(&q)==SQLITE_ROW ){ |
| 1674 | 1674 | const char *zIntegrateUuid = db_column_text(&q, 0); |
| 1675 | 1675 | int rid = db_column_int(&q, 1); |
| 1676 | 1676 | if( is_a_leaf(rid) && !db_exists("SELECT 1 FROM tagxref " |
| 1677 | 1677 | " WHERE tagid=%d AND rid=%d AND tagtype>0", TAG_CLOSED, rid)){ |
| 1678 | +#if 0 | |
| 1679 | + /* Make sure the check-in manifest of the resulting merge child does not | |
| 1680 | + ** include a +close tag referring to the leaf check-in on a private | |
| 1681 | + ** branch, so as not to generate a missing artifact reference on | |
| 1682 | + ** repository clones without that private branch. The merge command | |
| 1683 | + ** should have dropped the --integrate option, at this point. */ | |
| 1684 | + assert( !content_is_private(rid) ); | |
| 1685 | +#endif | |
| 1678 | 1686 | blob_appendf(pOut, "T +closed %s\n", zIntegrateUuid); |
| 1679 | 1687 | } |
| 1680 | 1688 | } |
| 1681 | 1689 | db_finalize(&q); |
| 1682 | 1690 | |
| @@ -2040,10 +2048,12 @@ | ||
| 2040 | 2048 | const char *zComment; /* Check-in comment */ |
| 2041 | 2049 | Stmt q; /* Various queries */ |
| 2042 | 2050 | char *zUuid; /* UUID of the new check-in */ |
| 2043 | 2051 | int useHash = 0; /* True to verify file status using hashing */ |
| 2044 | 2052 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2053 | + int privateFlag = 0; /* True if the --private option is present */ | |
| 2054 | + int privateParent = 0; /* True if the parent check-in is private */ | |
| 2045 | 2055 | int isAMerge = 0; /* True if checking in a merge */ |
| 2046 | 2056 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2047 | 2057 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2048 | 2058 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2049 | 2059 | int forceDelta = 0; /* Force a delta-manifest */ |
| @@ -2077,14 +2087,20 @@ | ||
| 2077 | 2087 | memset(&sCiInfo, 0, sizeof(sCiInfo)); |
| 2078 | 2088 | url_proxy_options(); |
| 2079 | 2089 | /* --sha1sum is an undocumented alias for --hash for backwards compatiblity */ |
| 2080 | 2090 | useHash = find_option("hash",0,0)!=0 || find_option("sha1sum",0,0)!=0; |
| 2081 | 2091 | noSign = find_option("nosign",0,0)!=0; |
| 2092 | + privateFlag = find_option("private",0,0)!=0; | |
| 2082 | 2093 | forceDelta = find_option("delta",0,0)!=0; |
| 2083 | 2094 | forceBaseline = find_option("baseline",0,0)!=0; |
| 2084 | - if( forceDelta && forceBaseline ){ | |
| 2085 | - fossil_fatal("cannot use --delta and --baseline together"); | |
| 2095 | + if( forceDelta ){ | |
| 2096 | + if( forceBaseline ){ | |
| 2097 | + fossil_fatal("cannot use --delta and --baseline together"); | |
| 2098 | + } | |
| 2099 | + if( db_get_boolean("forbid-delta-manifests",0) ){ | |
| 2100 | + fossil_fatal("delta manifests are prohibited in this repository"); | |
| 2101 | + } | |
| 2086 | 2102 | } |
| 2087 | 2103 | dryRunFlag = find_option("dry-run","n",0)!=0; |
| 2088 | 2104 | if( !dryRunFlag ){ |
| 2089 | 2105 | dryRunFlag = find_option("test",0,0)!=0; /* deprecated */ |
| 2090 | 2106 | } |
| @@ -2109,17 +2125,10 @@ | ||
| 2109 | 2125 | sizeof(char*)*(nTag+2)); |
| 2110 | 2126 | sCiInfo.azTag[nTag++] = zTag; |
| 2111 | 2127 | sCiInfo.azTag[nTag] = 0; |
| 2112 | 2128 | } |
| 2113 | 2129 | zComFile = find_option("message-file", "M", 1); |
| 2114 | - if( find_option("private",0,0) ){ | |
| 2115 | - g.markPrivate = 1; | |
| 2116 | - if( sCiInfo.zBranch==0 ) sCiInfo.zBranch = "private"; | |
| 2117 | - if( sCiInfo.zBrClr==0 && sCiInfo.zColor==0 ){ | |
| 2118 | - sCiInfo.zBrClr = "#fec084"; /* Orange */ | |
| 2119 | - } | |
| 2120 | - } | |
| 2121 | 2130 | sCiInfo.zDateOvrd = find_option("date-override",0,1); |
| 2122 | 2131 | sCiInfo.zUserOvrd = find_option("user-override",0,1); |
| 2123 | 2132 | db_must_be_within_tree(); |
| 2124 | 2133 | noSign = db_get_boolean("omitsign", 0)|noSign; |
| 2125 | 2134 | if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; } |
| @@ -2129,15 +2138,25 @@ | ||
| 2129 | 2138 | |
| 2130 | 2139 | /* Get the ID of the parent manifest artifact */ |
| 2131 | 2140 | vid = db_lget_int("checkout", 0); |
| 2132 | 2141 | if( vid==0 ){ |
| 2133 | 2142 | useCksum = 1; |
| 2134 | - if( sCiInfo.zBranch==0 ) { | |
| 2143 | + if( privateFlag==0 && sCiInfo.zBranch==0 ) { | |
| 2135 | 2144 | sCiInfo.zBranch=db_get("main-branch", 0); |
| 2136 | 2145 | } |
| 2137 | - }else if( content_is_private(vid) ){ | |
| 2138 | - g.markPrivate = 1; | |
| 2146 | + }else{ | |
| 2147 | + privateParent = content_is_private(vid); | |
| 2148 | + } | |
| 2149 | + | |
| 2150 | + /* Track the "private" status */ | |
| 2151 | + g.markPrivate = privateFlag || privateParent; | |
| 2152 | + if( privateFlag && !privateParent ){ | |
| 2153 | + /* Apply default branch name ("private") and color ("orange") if not | |
| 2154 | + ** specified otherwise on the command-line, and if the parent is not | |
| 2155 | + ** already private. */ | |
| 2156 | + if( sCiInfo.zBranch==0 ) sCiInfo.zBranch = "private"; | |
| 2157 | + if( sCiInfo.zBrClr==0 && sCiInfo.zColor==0 ) sCiInfo.zBrClr = "#fec084"; | |
| 2139 | 2158 | } |
| 2140 | 2159 | |
| 2141 | 2160 | /* Do not allow the creation of a new branch using an existing open |
| 2142 | 2161 | ** branch name unless the --force flag is used */ |
| 2143 | 2162 | if( sCiInfo.zBranch!=0 |
| @@ -2160,11 +2179,14 @@ | ||
| 2160 | 2179 | ** manifest) can continue to use this repository, do not create a new |
| 2161 | 2180 | ** delta-manifest unless this repository already contains one or more |
| 2162 | 2181 | ** delta-manifests, or unless the delta-manifest is explicitly requested |
| 2163 | 2182 | ** by the --delta option. |
| 2164 | 2183 | */ |
| 2165 | - if( !forceDelta && !db_get_boolean("seen-delta-manifest",0) ){ | |
| 2184 | + if( !forceDelta | |
| 2185 | + && !db_get_boolean("seen-delta-manifest",0) | |
| 2186 | + && !db_get_boolean("forbid-delta-manifests",0) | |
| 2187 | + ){ | |
| 2166 | 2188 | forceBaseline = 1; |
| 2167 | 2189 | } |
| 2168 | 2190 | |
| 2169 | 2191 | /* |
| 2170 | 2192 | ** Autosync if autosync is enabled and this is not a private check-in. |
| @@ -2634,10 +2656,11 @@ | ||
| 2634 | 2656 | /* Commit */ |
| 2635 | 2657 | db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'"); |
| 2636 | 2658 | db_multi_exec("PRAGMA repository.application_id=252006673;"); |
| 2637 | 2659 | db_multi_exec("PRAGMA localdb.application_id=252006674;"); |
| 2638 | 2660 | if( dryRunFlag ){ |
| 2661 | + leaf_ambiguity_warning(nvid,nvid); | |
| 2639 | 2662 | db_end_transaction(1); |
| 2640 | 2663 | exit(1); |
| 2641 | 2664 | } |
| 2642 | 2665 | db_end_transaction(0); |
| 2643 | 2666 | |
| @@ -2656,7 +2679,9 @@ | ||
| 2656 | 2679 | int nTries = db_get_int("autosync-tries",1); |
| 2657 | 2680 | autosync_loop(syncFlags, nTries, 0); |
| 2658 | 2681 | } |
| 2659 | 2682 | if( count_nonbranch_children(vid)>1 ){ |
| 2660 | 2683 | fossil_print("**** warning: a fork has occurred *****\n"); |
| 2684 | + }else{ | |
| 2685 | + leaf_ambiguity_warning(nvid,nvid); | |
| 2661 | 2686 | } |
| 2662 | 2687 | } |
| 2663 | 2688 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -1673,10 +1673,18 @@ | |
| 1673 | while( db_step(&q)==SQLITE_ROW ){ |
| 1674 | const char *zIntegrateUuid = db_column_text(&q, 0); |
| 1675 | int rid = db_column_int(&q, 1); |
| 1676 | if( is_a_leaf(rid) && !db_exists("SELECT 1 FROM tagxref " |
| 1677 | " WHERE tagid=%d AND rid=%d AND tagtype>0", TAG_CLOSED, rid)){ |
| 1678 | blob_appendf(pOut, "T +closed %s\n", zIntegrateUuid); |
| 1679 | } |
| 1680 | } |
| 1681 | db_finalize(&q); |
| 1682 | |
| @@ -2040,10 +2048,12 @@ | |
| 2040 | const char *zComment; /* Check-in comment */ |
| 2041 | Stmt q; /* Various queries */ |
| 2042 | char *zUuid; /* UUID of the new check-in */ |
| 2043 | int useHash = 0; /* True to verify file status using hashing */ |
| 2044 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2045 | int isAMerge = 0; /* True if checking in a merge */ |
| 2046 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2047 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2048 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2049 | int forceDelta = 0; /* Force a delta-manifest */ |
| @@ -2077,14 +2087,20 @@ | |
| 2077 | memset(&sCiInfo, 0, sizeof(sCiInfo)); |
| 2078 | url_proxy_options(); |
| 2079 | /* --sha1sum is an undocumented alias for --hash for backwards compatiblity */ |
| 2080 | useHash = find_option("hash",0,0)!=0 || find_option("sha1sum",0,0)!=0; |
| 2081 | noSign = find_option("nosign",0,0)!=0; |
| 2082 | forceDelta = find_option("delta",0,0)!=0; |
| 2083 | forceBaseline = find_option("baseline",0,0)!=0; |
| 2084 | if( forceDelta && forceBaseline ){ |
| 2085 | fossil_fatal("cannot use --delta and --baseline together"); |
| 2086 | } |
| 2087 | dryRunFlag = find_option("dry-run","n",0)!=0; |
| 2088 | if( !dryRunFlag ){ |
| 2089 | dryRunFlag = find_option("test",0,0)!=0; /* deprecated */ |
| 2090 | } |
| @@ -2109,17 +2125,10 @@ | |
| 2109 | sizeof(char*)*(nTag+2)); |
| 2110 | sCiInfo.azTag[nTag++] = zTag; |
| 2111 | sCiInfo.azTag[nTag] = 0; |
| 2112 | } |
| 2113 | zComFile = find_option("message-file", "M", 1); |
| 2114 | if( find_option("private",0,0) ){ |
| 2115 | g.markPrivate = 1; |
| 2116 | if( sCiInfo.zBranch==0 ) sCiInfo.zBranch = "private"; |
| 2117 | if( sCiInfo.zBrClr==0 && sCiInfo.zColor==0 ){ |
| 2118 | sCiInfo.zBrClr = "#fec084"; /* Orange */ |
| 2119 | } |
| 2120 | } |
| 2121 | sCiInfo.zDateOvrd = find_option("date-override",0,1); |
| 2122 | sCiInfo.zUserOvrd = find_option("user-override",0,1); |
| 2123 | db_must_be_within_tree(); |
| 2124 | noSign = db_get_boolean("omitsign", 0)|noSign; |
| 2125 | if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; } |
| @@ -2129,15 +2138,25 @@ | |
| 2129 | |
| 2130 | /* Get the ID of the parent manifest artifact */ |
| 2131 | vid = db_lget_int("checkout", 0); |
| 2132 | if( vid==0 ){ |
| 2133 | useCksum = 1; |
| 2134 | if( sCiInfo.zBranch==0 ) { |
| 2135 | sCiInfo.zBranch=db_get("main-branch", 0); |
| 2136 | } |
| 2137 | }else if( content_is_private(vid) ){ |
| 2138 | g.markPrivate = 1; |
| 2139 | } |
| 2140 | |
| 2141 | /* Do not allow the creation of a new branch using an existing open |
| 2142 | ** branch name unless the --force flag is used */ |
| 2143 | if( sCiInfo.zBranch!=0 |
| @@ -2160,11 +2179,14 @@ | |
| 2160 | ** manifest) can continue to use this repository, do not create a new |
| 2161 | ** delta-manifest unless this repository already contains one or more |
| 2162 | ** delta-manifests, or unless the delta-manifest is explicitly requested |
| 2163 | ** by the --delta option. |
| 2164 | */ |
| 2165 | if( !forceDelta && !db_get_boolean("seen-delta-manifest",0) ){ |
| 2166 | forceBaseline = 1; |
| 2167 | } |
| 2168 | |
| 2169 | /* |
| 2170 | ** Autosync if autosync is enabled and this is not a private check-in. |
| @@ -2634,10 +2656,11 @@ | |
| 2634 | /* Commit */ |
| 2635 | db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'"); |
| 2636 | db_multi_exec("PRAGMA repository.application_id=252006673;"); |
| 2637 | db_multi_exec("PRAGMA localdb.application_id=252006674;"); |
| 2638 | if( dryRunFlag ){ |
| 2639 | db_end_transaction(1); |
| 2640 | exit(1); |
| 2641 | } |
| 2642 | db_end_transaction(0); |
| 2643 | |
| @@ -2656,7 +2679,9 @@ | |
| 2656 | int nTries = db_get_int("autosync-tries",1); |
| 2657 | autosync_loop(syncFlags, nTries, 0); |
| 2658 | } |
| 2659 | if( count_nonbranch_children(vid)>1 ){ |
| 2660 | fossil_print("**** warning: a fork has occurred *****\n"); |
| 2661 | } |
| 2662 | } |
| 2663 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -1673,10 +1673,18 @@ | |
| 1673 | while( db_step(&q)==SQLITE_ROW ){ |
| 1674 | const char *zIntegrateUuid = db_column_text(&q, 0); |
| 1675 | int rid = db_column_int(&q, 1); |
| 1676 | if( is_a_leaf(rid) && !db_exists("SELECT 1 FROM tagxref " |
| 1677 | " WHERE tagid=%d AND rid=%d AND tagtype>0", TAG_CLOSED, rid)){ |
| 1678 | #if 0 |
| 1679 | /* Make sure the check-in manifest of the resulting merge child does not |
| 1680 | ** include a +close tag referring to the leaf check-in on a private |
| 1681 | ** branch, so as not to generate a missing artifact reference on |
| 1682 | ** repository clones without that private branch. The merge command |
| 1683 | ** should have dropped the --integrate option, at this point. */ |
| 1684 | assert( !content_is_private(rid) ); |
| 1685 | #endif |
| 1686 | blob_appendf(pOut, "T +closed %s\n", zIntegrateUuid); |
| 1687 | } |
| 1688 | } |
| 1689 | db_finalize(&q); |
| 1690 | |
| @@ -2040,10 +2048,12 @@ | |
| 2048 | const char *zComment; /* Check-in comment */ |
| 2049 | Stmt q; /* Various queries */ |
| 2050 | char *zUuid; /* UUID of the new check-in */ |
| 2051 | int useHash = 0; /* True to verify file status using hashing */ |
| 2052 | int noSign = 0; /* True to omit signing the manifest using GPG */ |
| 2053 | int privateFlag = 0; /* True if the --private option is present */ |
| 2054 | int privateParent = 0; /* True if the parent check-in is private */ |
| 2055 | int isAMerge = 0; /* True if checking in a merge */ |
| 2056 | int noWarningFlag = 0; /* True if skipping all warnings */ |
| 2057 | int noPrompt = 0; /* True if skipping all prompts */ |
| 2058 | int forceFlag = 0; /* Undocumented: Disables all checks */ |
| 2059 | int forceDelta = 0; /* Force a delta-manifest */ |
| @@ -2077,14 +2087,20 @@ | |
| 2087 | memset(&sCiInfo, 0, sizeof(sCiInfo)); |
| 2088 | url_proxy_options(); |
| 2089 | /* --sha1sum is an undocumented alias for --hash for backwards compatiblity */ |
| 2090 | useHash = find_option("hash",0,0)!=0 || find_option("sha1sum",0,0)!=0; |
| 2091 | noSign = find_option("nosign",0,0)!=0; |
| 2092 | privateFlag = find_option("private",0,0)!=0; |
| 2093 | forceDelta = find_option("delta",0,0)!=0; |
| 2094 | forceBaseline = find_option("baseline",0,0)!=0; |
| 2095 | if( forceDelta ){ |
| 2096 | if( forceBaseline ){ |
| 2097 | fossil_fatal("cannot use --delta and --baseline together"); |
| 2098 | } |
| 2099 | if( db_get_boolean("forbid-delta-manifests",0) ){ |
| 2100 | fossil_fatal("delta manifests are prohibited in this repository"); |
| 2101 | } |
| 2102 | } |
| 2103 | dryRunFlag = find_option("dry-run","n",0)!=0; |
| 2104 | if( !dryRunFlag ){ |
| 2105 | dryRunFlag = find_option("test",0,0)!=0; /* deprecated */ |
| 2106 | } |
| @@ -2109,17 +2125,10 @@ | |
| 2125 | sizeof(char*)*(nTag+2)); |
| 2126 | sCiInfo.azTag[nTag++] = zTag; |
| 2127 | sCiInfo.azTag[nTag] = 0; |
| 2128 | } |
| 2129 | zComFile = find_option("message-file", "M", 1); |
| 2130 | sCiInfo.zDateOvrd = find_option("date-override",0,1); |
| 2131 | sCiInfo.zUserOvrd = find_option("user-override",0,1); |
| 2132 | db_must_be_within_tree(); |
| 2133 | noSign = db_get_boolean("omitsign", 0)|noSign; |
| 2134 | if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; } |
| @@ -2129,15 +2138,25 @@ | |
| 2138 | |
| 2139 | /* Get the ID of the parent manifest artifact */ |
| 2140 | vid = db_lget_int("checkout", 0); |
| 2141 | if( vid==0 ){ |
| 2142 | useCksum = 1; |
| 2143 | if( privateFlag==0 && sCiInfo.zBranch==0 ) { |
| 2144 | sCiInfo.zBranch=db_get("main-branch", 0); |
| 2145 | } |
| 2146 | }else{ |
| 2147 | privateParent = content_is_private(vid); |
| 2148 | } |
| 2149 | |
| 2150 | /* Track the "private" status */ |
| 2151 | g.markPrivate = privateFlag || privateParent; |
| 2152 | if( privateFlag && !privateParent ){ |
| 2153 | /* Apply default branch name ("private") and color ("orange") if not |
| 2154 | ** specified otherwise on the command-line, and if the parent is not |
| 2155 | ** already private. */ |
| 2156 | if( sCiInfo.zBranch==0 ) sCiInfo.zBranch = "private"; |
| 2157 | if( sCiInfo.zBrClr==0 && sCiInfo.zColor==0 ) sCiInfo.zBrClr = "#fec084"; |
| 2158 | } |
| 2159 | |
| 2160 | /* Do not allow the creation of a new branch using an existing open |
| 2161 | ** branch name unless the --force flag is used */ |
| 2162 | if( sCiInfo.zBranch!=0 |
| @@ -2160,11 +2179,14 @@ | |
| 2179 | ** manifest) can continue to use this repository, do not create a new |
| 2180 | ** delta-manifest unless this repository already contains one or more |
| 2181 | ** delta-manifests, or unless the delta-manifest is explicitly requested |
| 2182 | ** by the --delta option. |
| 2183 | */ |
| 2184 | if( !forceDelta |
| 2185 | && !db_get_boolean("seen-delta-manifest",0) |
| 2186 | && !db_get_boolean("forbid-delta-manifests",0) |
| 2187 | ){ |
| 2188 | forceBaseline = 1; |
| 2189 | } |
| 2190 | |
| 2191 | /* |
| 2192 | ** Autosync if autosync is enabled and this is not a private check-in. |
| @@ -2634,10 +2656,11 @@ | |
| 2656 | /* Commit */ |
| 2657 | db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'"); |
| 2658 | db_multi_exec("PRAGMA repository.application_id=252006673;"); |
| 2659 | db_multi_exec("PRAGMA localdb.application_id=252006674;"); |
| 2660 | if( dryRunFlag ){ |
| 2661 | leaf_ambiguity_warning(nvid,nvid); |
| 2662 | db_end_transaction(1); |
| 2663 | exit(1); |
| 2664 | } |
| 2665 | db_end_transaction(0); |
| 2666 | |
| @@ -2656,7 +2679,9 @@ | |
| 2679 | int nTries = db_get_int("autosync-tries",1); |
| 2680 | autosync_loop(syncFlags, nTries, 0); |
| 2681 | } |
| 2682 | if( count_nonbranch_children(vid)>1 ){ |
| 2683 | fossil_print("**** warning: a fork has occurred *****\n"); |
| 2684 | }else{ |
| 2685 | leaf_ambiguity_warning(nvid,nvid); |
| 2686 | } |
| 2687 | } |
| 2688 |
+1
-1
| --- src/clone.c | ||
| +++ src/clone.c | ||
| @@ -204,11 +204,11 @@ | ||
| 204 | 204 | ); |
| 205 | 205 | url_enable_proxy(0); |
| 206 | 206 | clone_ssh_db_set_options(); |
| 207 | 207 | url_get_password_if_needed(); |
| 208 | 208 | g.xlinkClusterOnly = 1; |
| 209 | - nErr = client_sync(syncFlags,CONFIGSET_ALL,0); | |
| 209 | + nErr = client_sync(syncFlags,CONFIGSET_ALL,0,0); | |
| 210 | 210 | g.xlinkClusterOnly = 0; |
| 211 | 211 | verify_cancel(); |
| 212 | 212 | db_end_transaction(0); |
| 213 | 213 | db_close(1); |
| 214 | 214 | if( nErr ){ |
| 215 | 215 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -204,11 +204,11 @@ | |
| 204 | ); |
| 205 | url_enable_proxy(0); |
| 206 | clone_ssh_db_set_options(); |
| 207 | url_get_password_if_needed(); |
| 208 | g.xlinkClusterOnly = 1; |
| 209 | nErr = client_sync(syncFlags,CONFIGSET_ALL,0); |
| 210 | g.xlinkClusterOnly = 0; |
| 211 | verify_cancel(); |
| 212 | db_end_transaction(0); |
| 213 | db_close(1); |
| 214 | if( nErr ){ |
| 215 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -204,11 +204,11 @@ | |
| 204 | ); |
| 205 | url_enable_proxy(0); |
| 206 | clone_ssh_db_set_options(); |
| 207 | url_get_password_if_needed(); |
| 208 | g.xlinkClusterOnly = 1; |
| 209 | nErr = client_sync(syncFlags,CONFIGSET_ALL,0,0); |
| 210 | g.xlinkClusterOnly = 0; |
| 211 | verify_cancel(); |
| 212 | db_end_transaction(0); |
| 213 | db_close(1); |
| 214 | if( nErr ){ |
| 215 |
+17
-31
| --- src/comformat.c | ||
| +++ src/comformat.c | ||
| @@ -19,16 +19,10 @@ | ||
| 19 | 19 | ** text on a TTY. |
| 20 | 20 | */ |
| 21 | 21 | #include "config.h" |
| 22 | 22 | #include "comformat.h" |
| 23 | 23 | #include <assert.h> |
| 24 | -#ifdef _WIN32 | |
| 25 | -# include <windows.h> | |
| 26 | -#else | |
| 27 | -# include <termios.h> | |
| 28 | -# include <sys/ioctl.h> | |
| 29 | -#endif | |
| 30 | 24 | |
| 31 | 25 | #if INTERFACE |
| 32 | 26 | #define COMMENT_PRINT_NONE ((u32)0x00000000) /* No flags = non-legacy. */ |
| 33 | 27 | #define COMMENT_PRINT_LEGACY ((u32)0x00000001) /* Use legacy algorithm. */ |
| 34 | 28 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| @@ -67,35 +61,27 @@ | ||
| 67 | 61 | */ |
| 68 | 62 | static int comment_set_maxchars( |
| 69 | 63 | int indent, |
| 70 | 64 | int *pMaxChars |
| 71 | 65 | ){ |
| 72 | -#if defined(_WIN32) | |
| 73 | - CONSOLE_SCREEN_BUFFER_INFO csbi; | |
| 74 | - memset(&csbi, 0, sizeof(CONSOLE_SCREEN_BUFFER_INFO)); | |
| 75 | - if( GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) ){ | |
| 76 | - *pMaxChars = csbi.srWindow.Right - csbi.srWindow.Left - indent; | |
| 77 | - return 1; | |
| 78 | - } | |
| 79 | - return 0; | |
| 80 | -#elif defined(TIOCGWINSZ) | |
| 81 | - struct winsize w; | |
| 82 | - memset(&w, 0, sizeof(struct winsize)); | |
| 83 | - if( ioctl(0, TIOCGWINSZ, &w)!=-1 ){ | |
| 84 | - *pMaxChars = w.ws_col - indent; | |
| 85 | - return 1; | |
| 86 | - } | |
| 87 | - return 0; | |
| 88 | -#else | |
| 89 | - /* | |
| 90 | - ** Fallback to using more-or-less the "legacy semantics" of hard-coding | |
| 91 | - ** the maximum line length to a value reasonable for the vast majority | |
| 92 | - ** of supported systems. | |
| 93 | - */ | |
| 94 | - *pMaxChars = COMMENT_LEGACY_LINE_LENGTH - indent; | |
| 95 | - return -1; | |
| 96 | -#endif | |
| 66 | + struct TerminalSize ts; | |
| 67 | + if ( !terminal_get_size(&ts) ){ | |
| 68 | + return 0; | |
| 69 | + } | |
| 70 | + | |
| 71 | + if( ts.nColumns ){ | |
| 72 | + *pMaxChars = ts.nColumns - indent; | |
| 73 | + return 1; | |
| 74 | + }else{ | |
| 75 | + /* | |
| 76 | + ** Fallback to using more-or-less the "legacy semantics" of hard-coding | |
| 77 | + ** the maximum line length to a value reasonable for the vast majority | |
| 78 | + ** of supported systems. | |
| 79 | + */ | |
| 80 | + *pMaxChars = COMMENT_LEGACY_LINE_LENGTH - indent; | |
| 81 | + return -1; | |
| 82 | + } | |
| 97 | 83 | } |
| 98 | 84 | |
| 99 | 85 | /* |
| 100 | 86 | ** This function checks the current line being printed against the original |
| 101 | 87 | ** comment text. Upon matching, it updates the provided character and line |
| 102 | 88 |
| --- src/comformat.c | |
| +++ src/comformat.c | |
| @@ -19,16 +19,10 @@ | |
| 19 | ** text on a TTY. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "comformat.h" |
| 23 | #include <assert.h> |
| 24 | #ifdef _WIN32 |
| 25 | # include <windows.h> |
| 26 | #else |
| 27 | # include <termios.h> |
| 28 | # include <sys/ioctl.h> |
| 29 | #endif |
| 30 | |
| 31 | #if INTERFACE |
| 32 | #define COMMENT_PRINT_NONE ((u32)0x00000000) /* No flags = non-legacy. */ |
| 33 | #define COMMENT_PRINT_LEGACY ((u32)0x00000001) /* Use legacy algorithm. */ |
| 34 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| @@ -67,35 +61,27 @@ | |
| 67 | */ |
| 68 | static int comment_set_maxchars( |
| 69 | int indent, |
| 70 | int *pMaxChars |
| 71 | ){ |
| 72 | #if defined(_WIN32) |
| 73 | CONSOLE_SCREEN_BUFFER_INFO csbi; |
| 74 | memset(&csbi, 0, sizeof(CONSOLE_SCREEN_BUFFER_INFO)); |
| 75 | if( GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) ){ |
| 76 | *pMaxChars = csbi.srWindow.Right - csbi.srWindow.Left - indent; |
| 77 | return 1; |
| 78 | } |
| 79 | return 0; |
| 80 | #elif defined(TIOCGWINSZ) |
| 81 | struct winsize w; |
| 82 | memset(&w, 0, sizeof(struct winsize)); |
| 83 | if( ioctl(0, TIOCGWINSZ, &w)!=-1 ){ |
| 84 | *pMaxChars = w.ws_col - indent; |
| 85 | return 1; |
| 86 | } |
| 87 | return 0; |
| 88 | #else |
| 89 | /* |
| 90 | ** Fallback to using more-or-less the "legacy semantics" of hard-coding |
| 91 | ** the maximum line length to a value reasonable for the vast majority |
| 92 | ** of supported systems. |
| 93 | */ |
| 94 | *pMaxChars = COMMENT_LEGACY_LINE_LENGTH - indent; |
| 95 | return -1; |
| 96 | #endif |
| 97 | } |
| 98 | |
| 99 | /* |
| 100 | ** This function checks the current line being printed against the original |
| 101 | ** comment text. Upon matching, it updates the provided character and line |
| 102 |
| --- src/comformat.c | |
| +++ src/comformat.c | |
| @@ -19,16 +19,10 @@ | |
| 19 | ** text on a TTY. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "comformat.h" |
| 23 | #include <assert.h> |
| 24 | |
| 25 | #if INTERFACE |
| 26 | #define COMMENT_PRINT_NONE ((u32)0x00000000) /* No flags = non-legacy. */ |
| 27 | #define COMMENT_PRINT_LEGACY ((u32)0x00000001) /* Use legacy algorithm. */ |
| 28 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| @@ -67,35 +61,27 @@ | |
| 61 | */ |
| 62 | static int comment_set_maxchars( |
| 63 | int indent, |
| 64 | int *pMaxChars |
| 65 | ){ |
| 66 | struct TerminalSize ts; |
| 67 | if ( !terminal_get_size(&ts) ){ |
| 68 | return 0; |
| 69 | } |
| 70 | |
| 71 | if( ts.nColumns ){ |
| 72 | *pMaxChars = ts.nColumns - indent; |
| 73 | return 1; |
| 74 | }else{ |
| 75 | /* |
| 76 | ** Fallback to using more-or-less the "legacy semantics" of hard-coding |
| 77 | ** the maximum line length to a value reasonable for the vast majority |
| 78 | ** of supported systems. |
| 79 | */ |
| 80 | *pMaxChars = COMMENT_LEGACY_LINE_LENGTH - indent; |
| 81 | return -1; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /* |
| 86 | ** This function checks the current line being printed against the original |
| 87 | ** comment text. Upon matching, it updates the provided character and line |
| 88 |
+7
-3
| --- src/configure.c | ||
| +++ src/configure.c | ||
| @@ -96,19 +96,22 @@ | ||
| 96 | 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | 99 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 100 | 100 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 101 | + { "timeline-default-style", CONFIGSET_SKIN }, | |
| 101 | 102 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 102 | 103 | { "timeline-closetime", CONFIGSET_SKIN }, |
| 103 | 104 | { "timeline-max-comment", CONFIGSET_SKIN }, |
| 104 | 105 | { "timeline-plaintext", CONFIGSET_SKIN }, |
| 105 | 106 | { "timeline-truncate-at-blank", CONFIGSET_SKIN }, |
| 107 | + { "timeline-tslink-info", CONFIGSET_SKIN }, | |
| 106 | 108 | { "timeline-utc", CONFIGSET_SKIN }, |
| 107 | 109 | { "adunit", CONFIGSET_SKIN }, |
| 108 | 110 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 109 | 111 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 112 | + { "default-csp", CONFIGSET_SKIN }, | |
| 110 | 113 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 111 | 114 | { "sitemap-download", CONFIGSET_SKIN }, |
| 112 | 115 | { "sitemap-license", CONFIGSET_SKIN }, |
| 113 | 116 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 114 | 117 | |
| @@ -144,10 +147,11 @@ | ||
| 144 | 147 | { "parent-project-code", CONFIGSET_PROJ }, |
| 145 | 148 | { "parent-project-name", CONFIGSET_PROJ }, |
| 146 | 149 | { "hash-policy", CONFIGSET_PROJ }, |
| 147 | 150 | { "comment-format", CONFIGSET_PROJ }, |
| 148 | 151 | { "mimetypes", CONFIGSET_PROJ }, |
| 152 | + { "forbid-delta-manifests", CONFIGSET_PROJ }, | |
| 149 | 153 | |
| 150 | 154 | #ifdef FOSSIL_ENABLE_LEGACY_MV_RM |
| 151 | 155 | { "mv-rm-files", CONFIGSET_PROJ }, |
| 152 | 156 | #endif |
| 153 | 157 | |
| @@ -814,15 +818,15 @@ | ||
| 814 | 818 | if( g.url.protocol==0 ) fossil_fatal("no server URL specified"); |
| 815 | 819 | user_select(); |
| 816 | 820 | url_enable_proxy("via proxy: "); |
| 817 | 821 | if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE; |
| 818 | 822 | if( strncmp(zMethod, "push", n)==0 ){ |
| 819 | - client_sync(0,0,(unsigned)mask); | |
| 823 | + client_sync(0,0,(unsigned)mask,0); | |
| 820 | 824 | }else if( strncmp(zMethod, "pull", n)==0 ){ |
| 821 | - client_sync(0,(unsigned)mask,0); | |
| 825 | + client_sync(0,(unsigned)mask,0,0); | |
| 822 | 826 | }else{ |
| 823 | - client_sync(0,(unsigned)mask,(unsigned)mask); | |
| 827 | + client_sync(0,(unsigned)mask,(unsigned)mask,0); | |
| 824 | 828 | } |
| 825 | 829 | }else |
| 826 | 830 | if( strncmp(zMethod, "reset", n)==0 ){ |
| 827 | 831 | int mask, i; |
| 828 | 832 | char *zBackup; |
| 829 | 833 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -96,19 +96,22 @@ | |
| 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 100 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 101 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 102 | { "timeline-closetime", CONFIGSET_SKIN }, |
| 103 | { "timeline-max-comment", CONFIGSET_SKIN }, |
| 104 | { "timeline-plaintext", CONFIGSET_SKIN }, |
| 105 | { "timeline-truncate-at-blank", CONFIGSET_SKIN }, |
| 106 | { "timeline-utc", CONFIGSET_SKIN }, |
| 107 | { "adunit", CONFIGSET_SKIN }, |
| 108 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 109 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 110 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 111 | { "sitemap-download", CONFIGSET_SKIN }, |
| 112 | { "sitemap-license", CONFIGSET_SKIN }, |
| 113 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 114 | |
| @@ -144,10 +147,11 @@ | |
| 144 | { "parent-project-code", CONFIGSET_PROJ }, |
| 145 | { "parent-project-name", CONFIGSET_PROJ }, |
| 146 | { "hash-policy", CONFIGSET_PROJ }, |
| 147 | { "comment-format", CONFIGSET_PROJ }, |
| 148 | { "mimetypes", CONFIGSET_PROJ }, |
| 149 | |
| 150 | #ifdef FOSSIL_ENABLE_LEGACY_MV_RM |
| 151 | { "mv-rm-files", CONFIGSET_PROJ }, |
| 152 | #endif |
| 153 | |
| @@ -814,15 +818,15 @@ | |
| 814 | if( g.url.protocol==0 ) fossil_fatal("no server URL specified"); |
| 815 | user_select(); |
| 816 | url_enable_proxy("via proxy: "); |
| 817 | if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE; |
| 818 | if( strncmp(zMethod, "push", n)==0 ){ |
| 819 | client_sync(0,0,(unsigned)mask); |
| 820 | }else if( strncmp(zMethod, "pull", n)==0 ){ |
| 821 | client_sync(0,(unsigned)mask,0); |
| 822 | }else{ |
| 823 | client_sync(0,(unsigned)mask,(unsigned)mask); |
| 824 | } |
| 825 | }else |
| 826 | if( strncmp(zMethod, "reset", n)==0 ){ |
| 827 | int mask, i; |
| 828 | char *zBackup; |
| 829 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -96,19 +96,22 @@ | |
| 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 100 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 101 | { "timeline-default-style", CONFIGSET_SKIN }, |
| 102 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 103 | { "timeline-closetime", CONFIGSET_SKIN }, |
| 104 | { "timeline-max-comment", CONFIGSET_SKIN }, |
| 105 | { "timeline-plaintext", CONFIGSET_SKIN }, |
| 106 | { "timeline-truncate-at-blank", CONFIGSET_SKIN }, |
| 107 | { "timeline-tslink-info", CONFIGSET_SKIN }, |
| 108 | { "timeline-utc", CONFIGSET_SKIN }, |
| 109 | { "adunit", CONFIGSET_SKIN }, |
| 110 | { "adunit-omit-if-admin", CONFIGSET_SKIN }, |
| 111 | { "adunit-omit-if-user", CONFIGSET_SKIN }, |
| 112 | { "default-csp", CONFIGSET_SKIN }, |
| 113 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 114 | { "sitemap-download", CONFIGSET_SKIN }, |
| 115 | { "sitemap-license", CONFIGSET_SKIN }, |
| 116 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 117 | |
| @@ -144,10 +147,11 @@ | |
| 147 | { "parent-project-code", CONFIGSET_PROJ }, |
| 148 | { "parent-project-name", CONFIGSET_PROJ }, |
| 149 | { "hash-policy", CONFIGSET_PROJ }, |
| 150 | { "comment-format", CONFIGSET_PROJ }, |
| 151 | { "mimetypes", CONFIGSET_PROJ }, |
| 152 | { "forbid-delta-manifests", CONFIGSET_PROJ }, |
| 153 | |
| 154 | #ifdef FOSSIL_ENABLE_LEGACY_MV_RM |
| 155 | { "mv-rm-files", CONFIGSET_PROJ }, |
| 156 | #endif |
| 157 | |
| @@ -814,15 +818,15 @@ | |
| 818 | if( g.url.protocol==0 ) fossil_fatal("no server URL specified"); |
| 819 | user_select(); |
| 820 | url_enable_proxy("via proxy: "); |
| 821 | if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE; |
| 822 | if( strncmp(zMethod, "push", n)==0 ){ |
| 823 | client_sync(0,0,(unsigned)mask,0); |
| 824 | }else if( strncmp(zMethod, "pull", n)==0 ){ |
| 825 | client_sync(0,(unsigned)mask,0,0); |
| 826 | }else{ |
| 827 | client_sync(0,(unsigned)mask,(unsigned)mask,0); |
| 828 | } |
| 829 | }else |
| 830 | if( strncmp(zMethod, "reset", n)==0 ){ |
| 831 | int mask, i; |
| 832 | char *zBackup; |
| 833 |
+32
| --- src/content.c | ||
| +++ src/content.c | ||
| @@ -777,10 +777,22 @@ | ||
| 777 | 777 | "DELETE FROM private WHERE rid=:rid" |
| 778 | 778 | ); |
| 779 | 779 | db_bind_int(&s1, ":rid", rid); |
| 780 | 780 | db_exec(&s1); |
| 781 | 781 | } |
| 782 | + | |
| 783 | +/* | |
| 784 | +** Make sure an artifact is private | |
| 785 | +*/ | |
| 786 | +void content_make_private(int rid){ | |
| 787 | + static Stmt s1; | |
| 788 | + db_static_prepare(&s1, | |
| 789 | + "INSERT OR IGNORE INTO private(rid) VALUES(:rid)" | |
| 790 | + ); | |
| 791 | + db_bind_int(&s1, ":rid", rid); | |
| 792 | + db_exec(&s1); | |
| 793 | +} | |
| 782 | 794 | |
| 783 | 795 | /* |
| 784 | 796 | ** Try to change the storage of rid so that it is a delta from one |
| 785 | 797 | ** of the artifacts given in aSrc[0]..aSrc[nSrc-1]. The aSrc[*] that |
| 786 | 798 | ** gives the smallest delta is choosen. |
| @@ -932,13 +944,19 @@ | ||
| 932 | 944 | ** Verify that all content can be extracted from the BLOB table correctly. |
| 933 | 945 | ** If the BLOB table is correct, then the repository can always be |
| 934 | 946 | ** successfully reconstructed using "fossil rebuild". |
| 935 | 947 | ** |
| 936 | 948 | ** Options: |
| 949 | +** | |
| 950 | +** -d|--db-only Run "PRAGMA integrity_check" on the database only. | |
| 951 | +** No other validation is performed. | |
| 937 | 952 | ** |
| 938 | 953 | ** --parse Parse all manifests, wikis, tickets, events, and |
| 939 | 954 | ** so forth, reporting any errors found. |
| 955 | +** | |
| 956 | +** -q|--quick Run "PRAGMA quick_check" on the database only. | |
| 957 | +** No other validation is performed. | |
| 940 | 958 | */ |
| 941 | 959 | void test_integrity(void){ |
| 942 | 960 | Stmt q; |
| 943 | 961 | Blob content; |
| 944 | 962 | int n1 = 0; |
| @@ -946,11 +964,25 @@ | ||
| 946 | 964 | int nErr = 0; |
| 947 | 965 | int total; |
| 948 | 966 | int nCA = 0; |
| 949 | 967 | int anCA[10]; |
| 950 | 968 | int bParse = find_option("parse",0,0)!=0; |
| 969 | + int bDbOnly = find_option("db-only","d",0)!=0; | |
| 970 | + int bQuick = find_option("quick","q",0)!=0; | |
| 951 | 971 | db_find_and_open_repository(OPEN_ANY_SCHEMA, 2); |
| 972 | + if( bDbOnly || bQuick ){ | |
| 973 | + const char *zType = bQuick ? "quick" : "integrity"; | |
| 974 | + char *zRes; | |
| 975 | + zRes = db_text(0,"PRAGMA repository.%s_check", zType/*safe-for-%s*/); | |
| 976 | + if( fossil_strcmp(zRes,"ok")!=0 ){ | |
| 977 | + fossil_print("%s_check failed!\n", zType); | |
| 978 | + exit(1); | |
| 979 | + }else{ | |
| 980 | + fossil_print("ok\n"); | |
| 981 | + } | |
| 982 | + return; | |
| 983 | + } | |
| 952 | 984 | memset(anCA, 0, sizeof(anCA)); |
| 953 | 985 | |
| 954 | 986 | /* Make sure no public artifact is a delta from a private artifact */ |
| 955 | 987 | db_prepare(&q, |
| 956 | 988 | "SELECT " |
| 957 | 989 |
| --- src/content.c | |
| +++ src/content.c | |
| @@ -777,10 +777,22 @@ | |
| 777 | "DELETE FROM private WHERE rid=:rid" |
| 778 | ); |
| 779 | db_bind_int(&s1, ":rid", rid); |
| 780 | db_exec(&s1); |
| 781 | } |
| 782 | |
| 783 | /* |
| 784 | ** Try to change the storage of rid so that it is a delta from one |
| 785 | ** of the artifacts given in aSrc[0]..aSrc[nSrc-1]. The aSrc[*] that |
| 786 | ** gives the smallest delta is choosen. |
| @@ -932,13 +944,19 @@ | |
| 932 | ** Verify that all content can be extracted from the BLOB table correctly. |
| 933 | ** If the BLOB table is correct, then the repository can always be |
| 934 | ** successfully reconstructed using "fossil rebuild". |
| 935 | ** |
| 936 | ** Options: |
| 937 | ** |
| 938 | ** --parse Parse all manifests, wikis, tickets, events, and |
| 939 | ** so forth, reporting any errors found. |
| 940 | */ |
| 941 | void test_integrity(void){ |
| 942 | Stmt q; |
| 943 | Blob content; |
| 944 | int n1 = 0; |
| @@ -946,11 +964,25 @@ | |
| 946 | int nErr = 0; |
| 947 | int total; |
| 948 | int nCA = 0; |
| 949 | int anCA[10]; |
| 950 | int bParse = find_option("parse",0,0)!=0; |
| 951 | db_find_and_open_repository(OPEN_ANY_SCHEMA, 2); |
| 952 | memset(anCA, 0, sizeof(anCA)); |
| 953 | |
| 954 | /* Make sure no public artifact is a delta from a private artifact */ |
| 955 | db_prepare(&q, |
| 956 | "SELECT " |
| 957 |
| --- src/content.c | |
| +++ src/content.c | |
| @@ -777,10 +777,22 @@ | |
| 777 | "DELETE FROM private WHERE rid=:rid" |
| 778 | ); |
| 779 | db_bind_int(&s1, ":rid", rid); |
| 780 | db_exec(&s1); |
| 781 | } |
| 782 | |
| 783 | /* |
| 784 | ** Make sure an artifact is private |
| 785 | */ |
| 786 | void content_make_private(int rid){ |
| 787 | static Stmt s1; |
| 788 | db_static_prepare(&s1, |
| 789 | "INSERT OR IGNORE INTO private(rid) VALUES(:rid)" |
| 790 | ); |
| 791 | db_bind_int(&s1, ":rid", rid); |
| 792 | db_exec(&s1); |
| 793 | } |
| 794 | |
| 795 | /* |
| 796 | ** Try to change the storage of rid so that it is a delta from one |
| 797 | ** of the artifacts given in aSrc[0]..aSrc[nSrc-1]. The aSrc[*] that |
| 798 | ** gives the smallest delta is choosen. |
| @@ -932,13 +944,19 @@ | |
| 944 | ** Verify that all content can be extracted from the BLOB table correctly. |
| 945 | ** If the BLOB table is correct, then the repository can always be |
| 946 | ** successfully reconstructed using "fossil rebuild". |
| 947 | ** |
| 948 | ** Options: |
| 949 | ** |
| 950 | ** -d|--db-only Run "PRAGMA integrity_check" on the database only. |
| 951 | ** No other validation is performed. |
| 952 | ** |
| 953 | ** --parse Parse all manifests, wikis, tickets, events, and |
| 954 | ** so forth, reporting any errors found. |
| 955 | ** |
| 956 | ** -q|--quick Run "PRAGMA quick_check" on the database only. |
| 957 | ** No other validation is performed. |
| 958 | */ |
| 959 | void test_integrity(void){ |
| 960 | Stmt q; |
| 961 | Blob content; |
| 962 | int n1 = 0; |
| @@ -946,11 +964,25 @@ | |
| 964 | int nErr = 0; |
| 965 | int total; |
| 966 | int nCA = 0; |
| 967 | int anCA[10]; |
| 968 | int bParse = find_option("parse",0,0)!=0; |
| 969 | int bDbOnly = find_option("db-only","d",0)!=0; |
| 970 | int bQuick = find_option("quick","q",0)!=0; |
| 971 | db_find_and_open_repository(OPEN_ANY_SCHEMA, 2); |
| 972 | if( bDbOnly || bQuick ){ |
| 973 | const char *zType = bQuick ? "quick" : "integrity"; |
| 974 | char *zRes; |
| 975 | zRes = db_text(0,"PRAGMA repository.%s_check", zType/*safe-for-%s*/); |
| 976 | if( fossil_strcmp(zRes,"ok")!=0 ){ |
| 977 | fossil_print("%s_check failed!\n", zType); |
| 978 | exit(1); |
| 979 | }else{ |
| 980 | fossil_print("ok\n"); |
| 981 | } |
| 982 | return; |
| 983 | } |
| 984 | memset(anCA, 0, sizeof(anCA)); |
| 985 | |
| 986 | /* Make sure no public artifact is a delta from a private artifact */ |
| 987 | db_prepare(&q, |
| 988 | "SELECT " |
| 989 |
+4
-1
| --- src/cookies.c | ||
| +++ src/cookies.c | ||
| @@ -126,11 +126,14 @@ | ||
| 126 | 126 | for(i=0; i<cookies.nParam && strcmp(zPName,cookies.aParam[i].zPName); i++){} |
| 127 | 127 | if( zQVal==0 && (flags & COOKIE_READ)!=0 && i<cookies.nParam ){ |
| 128 | 128 | cgi_set_parameter_nocopy(zQP, cookies.aParam[i].zPValue, 1); |
| 129 | 129 | return; |
| 130 | 130 | } |
| 131 | - if( zQVal==0 ) zQVal = zDflt; | |
| 131 | + if( zQVal==0 ){ | |
| 132 | + zQVal = zDflt; | |
| 133 | + if( flags & COOKIE_WRITE ) cgi_set_parameter_nocopy(zQP, zQVal, 1); | |
| 134 | + } | |
| 132 | 135 | if( (flags & COOKIE_WRITE)!=0 |
| 133 | 136 | && i<COOKIE_NPARAM |
| 134 | 137 | && (i==cookies.nParam || strcmp(zQVal, cookies.aParam[i].zPValue)) |
| 135 | 138 | ){ |
| 136 | 139 | if( i==cookies.nParam ){ |
| 137 | 140 |
| --- src/cookies.c | |
| +++ src/cookies.c | |
| @@ -126,11 +126,14 @@ | |
| 126 | for(i=0; i<cookies.nParam && strcmp(zPName,cookies.aParam[i].zPName); i++){} |
| 127 | if( zQVal==0 && (flags & COOKIE_READ)!=0 && i<cookies.nParam ){ |
| 128 | cgi_set_parameter_nocopy(zQP, cookies.aParam[i].zPValue, 1); |
| 129 | return; |
| 130 | } |
| 131 | if( zQVal==0 ) zQVal = zDflt; |
| 132 | if( (flags & COOKIE_WRITE)!=0 |
| 133 | && i<COOKIE_NPARAM |
| 134 | && (i==cookies.nParam || strcmp(zQVal, cookies.aParam[i].zPValue)) |
| 135 | ){ |
| 136 | if( i==cookies.nParam ){ |
| 137 |
| --- src/cookies.c | |
| +++ src/cookies.c | |
| @@ -126,11 +126,14 @@ | |
| 126 | for(i=0; i<cookies.nParam && strcmp(zPName,cookies.aParam[i].zPName); i++){} |
| 127 | if( zQVal==0 && (flags & COOKIE_READ)!=0 && i<cookies.nParam ){ |
| 128 | cgi_set_parameter_nocopy(zQP, cookies.aParam[i].zPValue, 1); |
| 129 | return; |
| 130 | } |
| 131 | if( zQVal==0 ){ |
| 132 | zQVal = zDflt; |
| 133 | if( flags & COOKIE_WRITE ) cgi_set_parameter_nocopy(zQP, zQVal, 1); |
| 134 | } |
| 135 | if( (flags & COOKIE_WRITE)!=0 |
| 136 | && i<COOKIE_NPARAM |
| 137 | && (i==cookies.nParam || strcmp(zQVal, cookies.aParam[i].zPValue)) |
| 138 | ){ |
| 139 | if( i==cookies.nParam ){ |
| 140 |
M
src/db.c
+194
-80
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -18,11 +18,12 @@ | ||
| 18 | 18 | ** Code for interfacing to the various databases. |
| 19 | 19 | ** |
| 20 | 20 | ** There are three separate database files that fossil interacts |
| 21 | 21 | ** with: |
| 22 | 22 | ** |
| 23 | -** (1) The "user" database in ~/.fossil | |
| 23 | +** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db | |
| 24 | +** or in %LOCALAPPDATA%/_fossil | |
| 24 | 25 | ** |
| 25 | 26 | ** (2) The "repository" database |
| 26 | 27 | ** |
| 27 | 28 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 28 | 29 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | ||
| 653 | 654 | blob_reset(&sql); |
| 654 | 655 | return rc; |
| 655 | 656 | } |
| 656 | 657 | |
| 657 | 658 | /* |
| 658 | -** Execute multiple SQL statements. | |
| 659 | +** Execute multiple SQL statements. The input text is executed | |
| 660 | +** directly without any formatting. | |
| 659 | 661 | */ |
| 660 | -int db_multi_exec(const char *zSql, ...){ | |
| 661 | - Blob sql; | |
| 662 | +int db_exec_sql(const char *z){ | |
| 662 | 663 | int rc = SQLITE_OK; |
| 663 | - va_list ap; | |
| 664 | - const char *z, *zEnd; | |
| 665 | 664 | sqlite3_stmt *pStmt; |
| 666 | - blob_init(&sql, 0, 0); | |
| 667 | - va_start(ap, zSql); | |
| 668 | - blob_vappendf(&sql, zSql, ap); | |
| 669 | - va_end(ap); | |
| 670 | - z = blob_str(&sql); | |
| 665 | + const char *zEnd; | |
| 671 | 666 | while( rc==SQLITE_OK && z[0] ){ |
| 672 | 667 | pStmt = 0; |
| 673 | 668 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 674 | 669 | if( rc ){ |
| 675 | 670 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | ||
| 679 | 674 | rc = sqlite3_finalize(pStmt); |
| 680 | 675 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 681 | 676 | } |
| 682 | 677 | z = zEnd; |
| 683 | 678 | } |
| 679 | + return rc; | |
| 680 | +} | |
| 681 | + | |
| 682 | +/* | |
| 683 | +** Execute multiple SQL statements using printf-style formatting. | |
| 684 | +*/ | |
| 685 | +int db_multi_exec(const char *zSql, ...){ | |
| 686 | + Blob sql; | |
| 687 | + int rc; | |
| 688 | + va_list ap; | |
| 689 | + | |
| 690 | + blob_init(&sql, 0, 0); | |
| 691 | + va_start(ap, zSql); | |
| 692 | + blob_vappendf(&sql, zSql, ap); | |
| 693 | + va_end(ap); | |
| 694 | + rc = db_exec_sql(blob_str(&sql)); | |
| 684 | 695 | blob_reset(&sql); |
| 685 | 696 | return rc; |
| 686 | 697 | } |
| 687 | 698 | |
| 688 | 699 | /* |
| @@ -1031,10 +1042,12 @@ | ||
| 1031 | 1042 | 0, capability_union_step, capability_union_finalize); |
| 1032 | 1043 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1033 | 1044 | capability_fullcap, 0, 0); |
| 1034 | 1045 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1035 | 1046 | alert_find_emailaddr_func, 0, 0); |
| 1047 | + sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, | |
| 1048 | + alert_display_name_func, 0, 0); | |
| 1036 | 1049 | } |
| 1037 | 1050 | |
| 1038 | 1051 | #if USE_SEE |
| 1039 | 1052 | /* |
| 1040 | 1053 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | ||
| 1258 | 1271 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1259 | 1272 | ); |
| 1260 | 1273 | sqlite3_create_function( |
| 1261 | 1274 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1262 | 1275 | ); |
| 1263 | - if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_STMT, db_sql_trace, 0); | |
| 1276 | + if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); | |
| 1264 | 1277 | db_add_aux_functions(db); |
| 1265 | 1278 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1266 | 1279 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1267 | 1280 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1268 | 1281 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | ||
| 1289 | 1302 | blob_init(&key, 0, 0); |
| 1290 | 1303 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1291 | 1304 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1292 | 1305 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1293 | 1306 | zDbName, zLabel, blob_str(&key)); |
| 1294 | - db_multi_exec(zCmd /*works-like:""*/); | |
| 1307 | + db_exec_sql(zCmd); | |
| 1295 | 1308 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1296 | 1309 | sqlite3_free(zCmd); |
| 1297 | 1310 | }else{ |
| 1298 | 1311 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1299 | 1312 | zDbName, zLabel); |
| 1300 | - db_multi_exec(zCmd /*works-like:""*/); | |
| 1313 | + db_exec_sql(zCmd); | |
| 1301 | 1314 | sqlite3_free(zCmd); |
| 1302 | 1315 | #if USE_SEE |
| 1303 | 1316 | if( blob_size(&key)>0 ){ |
| 1304 | 1317 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1305 | 1318 | } |
| @@ -1360,11 +1373,11 @@ | ||
| 1360 | 1373 | db_attach(zDbName, zLabel); |
| 1361 | 1374 | } |
| 1362 | 1375 | } |
| 1363 | 1376 | |
| 1364 | 1377 | /* |
| 1365 | -** Close the per-user database file in ~/.fossil | |
| 1378 | +** Close the per-user configuration database file | |
| 1366 | 1379 | */ |
| 1367 | 1380 | void db_close_config(){ |
| 1368 | 1381 | int iSlot = db_database_slot("configdb"); |
| 1369 | 1382 | if( iSlot>0 ){ |
| 1370 | 1383 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | ||
| 1384 | 1397 | fossil_free(g.zConfigDbName); |
| 1385 | 1398 | g.zConfigDbName = 0; |
| 1386 | 1399 | } |
| 1387 | 1400 | |
| 1388 | 1401 | /* |
| 1389 | -** Open the user database in "~/.fossil". Create the database anew if | |
| 1390 | -** it does not already exist. | |
| 1402 | +** Compute the name of the configuration database. If unable to find the | |
| 1403 | +** database, return 0 if isOptional is true, or panic if isOptional is false. | |
| 1391 | 1404 | ** |
| 1392 | -** If the useAttach flag is 0 (the usual case) then the user database is | |
| 1393 | -** opened on a separate database connection g.dbConfig. This prevents | |
| 1394 | -** the ~/.fossil database from becoming locked on long check-in or sync | |
| 1395 | -** operations which hold an exclusive transaction. In a few cases, though, | |
| 1396 | -** it is convenient for the ~/.fossil to be attached to the main database | |
| 1397 | -** connection so that we can join between the various databases. In that | |
| 1398 | -** case, invoke this routine with useAttach as 1. | |
| 1405 | +** Space to hold the result comes from fossil_malloc(). | |
| 1399 | 1406 | */ |
| 1400 | -int db_open_config(int useAttach, int isOptional){ | |
| 1401 | - char *zDbName; | |
| 1402 | - char *zHome; | |
| 1403 | - if( g.zConfigDbName ){ | |
| 1404 | - int alreadyAttached = db_database_slot("configdb")>0; | |
| 1405 | - if( useAttach==alreadyAttached ) return 1; /* Already open. */ | |
| 1406 | - db_close_config(); | |
| 1407 | - } | |
| 1408 | - zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1407 | +static char *db_configdb_name(int isOptional){ | |
| 1408 | + char *zHome; /* Home directory */ | |
| 1409 | + char *zDbName; /* Name of the database file */ | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + /* On Windows, look for these directories, in order: | |
| 1413 | + ** | |
| 1414 | + ** FOSSIL_HOME | |
| 1415 | + ** LOCALAPPDATA | |
| 1416 | + ** APPDATA | |
| 1417 | + ** USERPROFILE | |
| 1418 | + ** HOMEDRIVE HOMEPATH | |
| 1419 | + */ | |
| 1409 | 1420 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1421 | + zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1410 | 1422 | if( zHome==0 ){ |
| 1411 | 1423 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1412 | 1424 | if( zHome==0 ){ |
| 1413 | 1425 | zHome = fossil_getenv("APPDATA"); |
| 1414 | 1426 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | ||
| 1419 | 1431 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1420 | 1432 | } |
| 1421 | 1433 | } |
| 1422 | 1434 | } |
| 1423 | 1435 | } |
| 1424 | - if( zHome==0 ){ | |
| 1425 | - if( isOptional ) return 0; | |
| 1426 | - fossil_panic("cannot locate home directory - please set the " | |
| 1427 | - "FOSSIL_HOME, LOCALAPPDATA, APPDATA, USERPROFILE, " | |
| 1428 | - "or HOMEDRIVE / HOMEPATH environment variables"); | |
| 1429 | - } | |
| 1430 | -#else | |
| 1431 | - if( zHome==0 ){ | |
| 1432 | - zHome = fossil_getenv("HOME"); | |
| 1433 | - } | |
| 1434 | - if( zHome==0 ){ | |
| 1435 | - if( isOptional ) return 0; | |
| 1436 | - fossil_panic("cannot locate home directory - please set the " | |
| 1437 | - "FOSSIL_HOME or HOME environment variables"); | |
| 1438 | - } | |
| 1439 | -#endif | |
| 1440 | - if( file_isdir(zHome, ExtFILE)!=1 ){ | |
| 1441 | - if( isOptional ) return 0; | |
| 1442 | - fossil_panic("invalid home directory: %s", zHome); | |
| 1443 | - } | |
| 1444 | -#if defined(_WIN32) || defined(__CYGWIN__) | |
| 1445 | - /* . filenames give some window systems problems and many apps problems */ | |
| 1446 | - zDbName = mprintf("%//_fossil", zHome); | |
| 1447 | -#else | |
| 1448 | - zDbName = mprintf("%s/.fossil", zHome); | |
| 1449 | -#endif | |
| 1450 | - if( file_size(zDbName, ExtFILE)<1024*3 ){ | |
| 1451 | - if( file_access(zHome, W_OK) ){ | |
| 1452 | - if( isOptional ) return 0; | |
| 1453 | - fossil_panic("home directory %s must be writeable", zHome); | |
| 1436 | + zDbName = mprintf("%//_fossil", zHome); | |
| 1437 | + fossil_free(zHome); | |
| 1438 | + return zDbName; | |
| 1439 | + | |
| 1440 | +#else /* if unix */ | |
| 1441 | + char *zXdgHome; | |
| 1442 | + | |
| 1443 | + /* For unix. a 5-step algorithm is used. | |
| 1444 | + ** See ../www/tech_overview.wiki for discussion. | |
| 1445 | + ** | |
| 1446 | + ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil | |
| 1447 | + */ | |
| 1448 | + zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1449 | + if( zHome!=0 ) return mprintf("%s/.fossil", zHome); | |
| 1450 | + | |
| 1451 | + /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil | |
| 1452 | + */ | |
| 1453 | + zHome = fossil_getenv("HOME"); | |
| 1454 | + if( zHome ){ | |
| 1455 | + zDbName = mprintf("%s/.fossil", zHome); | |
| 1456 | + if( file_size(zDbName, ExtFILE)>1024*3 ){ | |
| 1457 | + return zDbName; | |
| 1458 | + } | |
| 1459 | + fossil_free(zDbName); | |
| 1460 | + } | |
| 1461 | + | |
| 1462 | + /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db | |
| 1463 | + */ | |
| 1464 | + zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); | |
| 1465 | + if( zXdgHome!=0 ){ | |
| 1466 | + return mprintf("%s/fossil.db", zXdgHome); | |
| 1467 | + } | |
| 1468 | + | |
| 1469 | + /* The HOME variable is required in order to continue. | |
| 1470 | + */ | |
| 1471 | + if( zHome==0 ){ | |
| 1472 | + if( isOptional ) return 0; | |
| 1473 | + fossil_panic("cannot locate home directory - please set one of the " | |
| 1474 | + "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " | |
| 1475 | + "variables"); | |
| 1476 | + } | |
| 1477 | + | |
| 1478 | + /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db | |
| 1479 | + */ | |
| 1480 | + zXdgHome = mprintf("%s/.config", zHome); | |
| 1481 | + if( file_isdir(zXdgHome, ExtFILE)==1 ){ | |
| 1482 | + fossil_free(zXdgHome); | |
| 1483 | + return mprintf("%s/.config/fossil.db", zHome); | |
| 1484 | + } | |
| 1485 | + | |
| 1486 | + /* Step 5: Otherwise -> $HOME/.fossil | |
| 1487 | + */ | |
| 1488 | + return mprintf("%s/.fossil", zHome); | |
| 1489 | +#endif /* unix */ | |
| 1490 | +} | |
| 1491 | + | |
| 1492 | +/* | |
| 1493 | +** Open the configuration database. Create the database anew if | |
| 1494 | +** it does not already exist. | |
| 1495 | +** | |
| 1496 | +** If the useAttach flag is 0 (the usual case) then the configuration | |
| 1497 | +** database is opened on a separate database connection g.dbConfig. | |
| 1498 | +** This prevents the database from becoming locked on long check-in or sync | |
| 1499 | +** operations which hold an exclusive transaction. In a few cases, though, | |
| 1500 | +** it is convenient for the database to be attached to the main database | |
| 1501 | +** connection so that we can join between the various databases. In that | |
| 1502 | +** case, invoke this routine with useAttach as 1. | |
| 1503 | +*/ | |
| 1504 | +int db_open_config(int useAttach, int isOptional){ | |
| 1505 | + char *zDbName; | |
| 1506 | + if( g.zConfigDbName ){ | |
| 1507 | + int alreadyAttached = db_database_slot("configdb")>0; | |
| 1508 | + if( useAttach==alreadyAttached ) return 1; /* Already open. */ | |
| 1509 | + db_close_config(); | |
| 1510 | + } | |
| 1511 | + zDbName = db_configdb_name(isOptional); | |
| 1512 | + if( zDbName==0 ) return 0; | |
| 1513 | + if( file_size(zDbName, ExtFILE)<1024*3 ){ | |
| 1514 | + char *zHome = file_dirname(zDbName); | |
| 1515 | + int rc; | |
| 1516 | + if( file_isdir(zHome, ExtFILE)==0 ){ | |
| 1517 | + file_mkdir(zHome, ExtFILE, 0); | |
| 1518 | + } | |
| 1519 | + rc = file_access(zHome, W_OK); | |
| 1520 | + fossil_free(zHome); | |
| 1521 | + if( rc ){ | |
| 1522 | + if( isOptional ) return 0; | |
| 1523 | + fossil_panic("home directory \"%s\" must be writeable", zHome); | |
| 1454 | 1524 | } |
| 1455 | 1525 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1456 | 1526 | } |
| 1457 | 1527 | if( file_access(zDbName, W_OK) ){ |
| 1458 | 1528 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | ||
| 1769 | 1839 | "UPDATE vfile" |
| 1770 | 1840 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1771 | 1841 | " WHERE mrid!=rid;" |
| 1772 | 1842 | ); |
| 1773 | 1843 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1774 | - db_multi_exec("ALTER TABLE vmerge RENAME TO old_vmerge;"); | |
| 1775 | - db_multi_exec(zLocalSchemaVmerge /*works-like:""*/); | |
| 1776 | - db_multi_exec( | |
| 1844 | + db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); | |
| 1845 | + db_exec_sql(zLocalSchemaVmerge); | |
| 1846 | + db_exec_sql( | |
| 1777 | 1847 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1778 | 1848 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1779 | 1849 | " WHERE old_vmerge.merge=blob.rid;" |
| 1780 | 1850 | "DROP TABLE old_vmerge;" |
| 1781 | 1851 | ); |
| @@ -2081,11 +2151,11 @@ | ||
| 2081 | 2151 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2082 | 2152 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2083 | 2153 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2084 | 2154 | " VALUES('nobody','','gjorz','Nobody');" |
| 2085 | 2155 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2086 | - " VALUES('developer','','dei','Dev');" | |
| 2156 | + " VALUES('developer','','ei','Dev');" | |
| 2087 | 2157 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2088 | 2158 | " VALUES('reader','','kptw','Reader');" |
| 2089 | 2159 | ); |
| 2090 | 2160 | } |
| 2091 | 2161 | } |
| @@ -2305,11 +2375,11 @@ | ||
| 2305 | 2375 | ** SQL functions for debugging. |
| 2306 | 2376 | ** |
| 2307 | 2377 | ** The print() function writes its arguments on stdout, but only |
| 2308 | 2378 | ** if the -sqlprint command-line option is turned on. |
| 2309 | 2379 | */ |
| 2310 | -LOCAL void db_sql_print( | |
| 2380 | +void db_sql_print( | |
| 2311 | 2381 | sqlite3_context *context, |
| 2312 | 2382 | int argc, |
| 2313 | 2383 | sqlite3_value **argv |
| 2314 | 2384 | ){ |
| 2315 | 2385 | int i; |
| @@ -2318,19 +2388,40 @@ | ||
| 2318 | 2388 | char c = i==argc-1 ? '\n' : ' '; |
| 2319 | 2389 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2320 | 2390 | } |
| 2321 | 2391 | } |
| 2322 | 2392 | } |
| 2323 | -LOCAL int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ | |
| 2393 | + | |
| 2394 | +/* | |
| 2395 | +** Callback for sqlite3_trace_v2(); | |
| 2396 | +*/ | |
| 2397 | +int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ | |
| 2324 | 2398 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2325 | 2399 | char *zSql; |
| 2326 | 2400 | int n; |
| 2327 | 2401 | const char *zArg = (const char*)pX; |
| 2402 | + char zEnd[40]; | |
| 2403 | + if( m & SQLITE_TRACE_CLOSE ){ | |
| 2404 | + /* If we are tracking closes, that means we want to clean up static | |
| 2405 | + ** prepared statements. */ | |
| 2406 | + while( db.pAllStmt ){ | |
| 2407 | + db_finalize(db.pAllStmt); | |
| 2408 | + } | |
| 2409 | + return 0; | |
| 2410 | + } | |
| 2328 | 2411 | if( zArg[0]=='-' ) return 0; |
| 2412 | + if( m & SQLITE_TRACE_PROFILE ){ | |
| 2413 | + sqlite3_int64 nNano = *(sqlite3_int64*)pX; | |
| 2414 | + double rMillisec = 0.000001 * nNano; | |
| 2415 | + sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms */\n", rMillisec); | |
| 2416 | + }else{ | |
| 2417 | + zEnd[0] = '\n'; | |
| 2418 | + zEnd[1] = 0; | |
| 2419 | + } | |
| 2329 | 2420 | zSql = sqlite3_expanded_sql(pStmt); |
| 2330 | 2421 | n = (int)strlen(zSql); |
| 2331 | - fossil_trace("%s%s\n", zSql, (n>0 && zSql[n-1]==';') ? "" : ";"); | |
| 2422 | + fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); | |
| 2332 | 2423 | sqlite3_free(zSql); |
| 2333 | 2424 | return 0; |
| 2334 | 2425 | } |
| 2335 | 2426 | |
| 2336 | 2427 | /* |
| @@ -2492,16 +2583,16 @@ | ||
| 2492 | 2583 | return 0; |
| 2493 | 2584 | } |
| 2494 | 2585 | |
| 2495 | 2586 | /* |
| 2496 | 2587 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2497 | -** work on the ~/.fossil database instead of on the repository database. | |
| 2588 | +** work on the configuration database instead of on the repository database. | |
| 2498 | 2589 | ** Be sure to swap them back after doing the operation. |
| 2499 | 2590 | ** |
| 2500 | -** If the ~/.fossil database has already been opened as the main database or | |
| 2501 | -** is attached to the main database, no connection swaps are required so this | |
| 2502 | -** routine is a no-op. | |
| 2591 | +** If the configuration database has already been opened as the main database | |
| 2592 | +** or is attached to the main database, no connection swaps are required so | |
| 2593 | +** this routine is a no-op. | |
| 2503 | 2594 | */ |
| 2504 | 2595 | void db_swap_connections(void){ |
| 2505 | 2596 | /* |
| 2506 | 2597 | ** When swapping the main database connection with the config database |
| 2507 | 2598 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | ||
| 3493 | 3584 | /* |
| 3494 | 3585 | ** SETTING: pgp-command width=40 |
| 3495 | 3586 | ** Command used to clear-sign manifests at check-in. |
| 3496 | 3587 | ** Default value is "gpg --clearsign -o" |
| 3497 | 3588 | */ |
| 3589 | +/* | |
| 3590 | +** SETTING: forbid-delta-manifests boolean default=off | |
| 3591 | +** If enabled, new delta manifests are prohibited. | |
| 3592 | +*/ | |
| 3498 | 3593 | /* |
| 3499 | 3594 | ** SETTING: proxy width=32 default=off |
| 3500 | 3595 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3501 | 3596 | ** the "http_proxy" environment variable is consulted. |
| 3502 | 3597 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | ||
| 3627 | 3722 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3628 | 3723 | ** Specify which URI's are allowed in HTTP requests from |
| 3629 | 3724 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3630 | 3725 | ** whatsoever. |
| 3631 | 3726 | */ |
| 3727 | +/* | |
| 3728 | +** SETTING: default-csp width=40 block-text | |
| 3729 | +** | |
| 3730 | +** The text of the Content Security Policy that is included | |
| 3731 | +** in the Content-Security-Policy: header field of the HTTP | |
| 3732 | +** reply and in the default HTML <head> section that is added when the | |
| 3733 | +** skin header does not specify a <head> section. The text "$nonce" | |
| 3734 | +** is replaced by the random nonce that is created for each web page. | |
| 3735 | +** | |
| 3736 | +** If this setting is an empty string or is omitted, then | |
| 3737 | +** the following default Content Security Policy is used: | |
| 3738 | +** | |
| 3739 | +** default-src 'self' data:; | |
| 3740 | +** script-src 'self' 'nonce-$nonce'; | |
| 3741 | +** style-src 'self' 'unsafe-inline'; | |
| 3742 | +** | |
| 3743 | +** The default CSP is recommended. The main reason to change | |
| 3744 | +** this setting would be to add CDNs from which it is safe to | |
| 3745 | +** load additional content. | |
| 3746 | +*/ | |
| 3632 | 3747 | /* |
| 3633 | 3748 | ** SETTING: uv-sync boolean default=off |
| 3634 | 3749 | ** If true, automatically send unversioned files as part |
| 3635 | 3750 | ** of a "fossil clone" or "fossil sync" command. The |
| 3636 | 3751 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | ||
| 3695 | 3810 | ** The "unset" command clears a setting. |
| 3696 | 3811 | ** |
| 3697 | 3812 | ** Settings can have both a "local" repository-only value and "global" value |
| 3698 | 3813 | ** that applies to all repositories. The local values are stored in the |
| 3699 | 3814 | ** "config" table of the repository and the global values are stored in the |
| 3700 | -** $HOME/.fossil file on unix or in the %LOCALAPPDATA%/_fossil file on Windows. | |
| 3701 | -** If both a local and a global value exists for a setting, the local value | |
| 3702 | -** takes precedence. This command normally operates on the local settings. | |
| 3703 | -** Use the --global option to change global settings. | |
| 3815 | +** configuration database. If both a local and a global value exists for a | |
| 3816 | +** setting, the local value takes precedence. This command normally operates | |
| 3817 | +** on the local settings. Use the --global option to change global settings. | |
| 3704 | 3818 | ** |
| 3705 | 3819 | ** Options: |
| 3706 | 3820 | ** --global set or unset the given property globally instead of |
| 3707 | 3821 | ** setting or unsetting it for the open repository only. |
| 3708 | 3822 | ** |
| @@ -3833,12 +3947,12 @@ | ||
| 3833 | 3947 | ** COMMAND: test-without-rowid |
| 3834 | 3948 | ** |
| 3835 | 3949 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3836 | 3950 | ** |
| 3837 | 3951 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3838 | -** optimization. FILENAME can also be the ~/.fossil file or a local | |
| 3839 | -** .fslckout or _FOSSIL_ file. | |
| 3952 | +** optimization. FILENAME can also be the configuration database file | |
| 3953 | +** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. | |
| 3840 | 3954 | ** |
| 3841 | 3955 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3842 | 3956 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3843 | 3957 | ** |
| 3844 | 3958 | ** Options: |
| 3845 | 3959 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -18,11 +18,12 @@ | |
| 18 | ** Code for interfacing to the various databases. |
| 19 | ** |
| 20 | ** There are three separate database files that fossil interacts |
| 21 | ** with: |
| 22 | ** |
| 23 | ** (1) The "user" database in ~/.fossil |
| 24 | ** |
| 25 | ** (2) The "repository" database |
| 26 | ** |
| 27 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 28 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | |
| 653 | blob_reset(&sql); |
| 654 | return rc; |
| 655 | } |
| 656 | |
| 657 | /* |
| 658 | ** Execute multiple SQL statements. |
| 659 | */ |
| 660 | int db_multi_exec(const char *zSql, ...){ |
| 661 | Blob sql; |
| 662 | int rc = SQLITE_OK; |
| 663 | va_list ap; |
| 664 | const char *z, *zEnd; |
| 665 | sqlite3_stmt *pStmt; |
| 666 | blob_init(&sql, 0, 0); |
| 667 | va_start(ap, zSql); |
| 668 | blob_vappendf(&sql, zSql, ap); |
| 669 | va_end(ap); |
| 670 | z = blob_str(&sql); |
| 671 | while( rc==SQLITE_OK && z[0] ){ |
| 672 | pStmt = 0; |
| 673 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 674 | if( rc ){ |
| 675 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | |
| 679 | rc = sqlite3_finalize(pStmt); |
| 680 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 681 | } |
| 682 | z = zEnd; |
| 683 | } |
| 684 | blob_reset(&sql); |
| 685 | return rc; |
| 686 | } |
| 687 | |
| 688 | /* |
| @@ -1031,10 +1042,12 @@ | |
| 1031 | 0, capability_union_step, capability_union_finalize); |
| 1032 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1033 | capability_fullcap, 0, 0); |
| 1034 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1035 | alert_find_emailaddr_func, 0, 0); |
| 1036 | } |
| 1037 | |
| 1038 | #if USE_SEE |
| 1039 | /* |
| 1040 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | |
| 1258 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1259 | ); |
| 1260 | sqlite3_create_function( |
| 1261 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1262 | ); |
| 1263 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_STMT, db_sql_trace, 0); |
| 1264 | db_add_aux_functions(db); |
| 1265 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1266 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1267 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1268 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | |
| 1289 | blob_init(&key, 0, 0); |
| 1290 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1291 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1292 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1293 | zDbName, zLabel, blob_str(&key)); |
| 1294 | db_multi_exec(zCmd /*works-like:""*/); |
| 1295 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1296 | sqlite3_free(zCmd); |
| 1297 | }else{ |
| 1298 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1299 | zDbName, zLabel); |
| 1300 | db_multi_exec(zCmd /*works-like:""*/); |
| 1301 | sqlite3_free(zCmd); |
| 1302 | #if USE_SEE |
| 1303 | if( blob_size(&key)>0 ){ |
| 1304 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1305 | } |
| @@ -1360,11 +1373,11 @@ | |
| 1360 | db_attach(zDbName, zLabel); |
| 1361 | } |
| 1362 | } |
| 1363 | |
| 1364 | /* |
| 1365 | ** Close the per-user database file in ~/.fossil |
| 1366 | */ |
| 1367 | void db_close_config(){ |
| 1368 | int iSlot = db_database_slot("configdb"); |
| 1369 | if( iSlot>0 ){ |
| 1370 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | |
| 1384 | fossil_free(g.zConfigDbName); |
| 1385 | g.zConfigDbName = 0; |
| 1386 | } |
| 1387 | |
| 1388 | /* |
| 1389 | ** Open the user database in "~/.fossil". Create the database anew if |
| 1390 | ** it does not already exist. |
| 1391 | ** |
| 1392 | ** If the useAttach flag is 0 (the usual case) then the user database is |
| 1393 | ** opened on a separate database connection g.dbConfig. This prevents |
| 1394 | ** the ~/.fossil database from becoming locked on long check-in or sync |
| 1395 | ** operations which hold an exclusive transaction. In a few cases, though, |
| 1396 | ** it is convenient for the ~/.fossil to be attached to the main database |
| 1397 | ** connection so that we can join between the various databases. In that |
| 1398 | ** case, invoke this routine with useAttach as 1. |
| 1399 | */ |
| 1400 | int db_open_config(int useAttach, int isOptional){ |
| 1401 | char *zDbName; |
| 1402 | char *zHome; |
| 1403 | if( g.zConfigDbName ){ |
| 1404 | int alreadyAttached = db_database_slot("configdb")>0; |
| 1405 | if( useAttach==alreadyAttached ) return 1; /* Already open. */ |
| 1406 | db_close_config(); |
| 1407 | } |
| 1408 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1409 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1410 | if( zHome==0 ){ |
| 1411 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1412 | if( zHome==0 ){ |
| 1413 | zHome = fossil_getenv("APPDATA"); |
| 1414 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | |
| 1419 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1420 | } |
| 1421 | } |
| 1422 | } |
| 1423 | } |
| 1424 | if( zHome==0 ){ |
| 1425 | if( isOptional ) return 0; |
| 1426 | fossil_panic("cannot locate home directory - please set the " |
| 1427 | "FOSSIL_HOME, LOCALAPPDATA, APPDATA, USERPROFILE, " |
| 1428 | "or HOMEDRIVE / HOMEPATH environment variables"); |
| 1429 | } |
| 1430 | #else |
| 1431 | if( zHome==0 ){ |
| 1432 | zHome = fossil_getenv("HOME"); |
| 1433 | } |
| 1434 | if( zHome==0 ){ |
| 1435 | if( isOptional ) return 0; |
| 1436 | fossil_panic("cannot locate home directory - please set the " |
| 1437 | "FOSSIL_HOME or HOME environment variables"); |
| 1438 | } |
| 1439 | #endif |
| 1440 | if( file_isdir(zHome, ExtFILE)!=1 ){ |
| 1441 | if( isOptional ) return 0; |
| 1442 | fossil_panic("invalid home directory: %s", zHome); |
| 1443 | } |
| 1444 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1445 | /* . filenames give some window systems problems and many apps problems */ |
| 1446 | zDbName = mprintf("%//_fossil", zHome); |
| 1447 | #else |
| 1448 | zDbName = mprintf("%s/.fossil", zHome); |
| 1449 | #endif |
| 1450 | if( file_size(zDbName, ExtFILE)<1024*3 ){ |
| 1451 | if( file_access(zHome, W_OK) ){ |
| 1452 | if( isOptional ) return 0; |
| 1453 | fossil_panic("home directory %s must be writeable", zHome); |
| 1454 | } |
| 1455 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1456 | } |
| 1457 | if( file_access(zDbName, W_OK) ){ |
| 1458 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | |
| 1769 | "UPDATE vfile" |
| 1770 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1771 | " WHERE mrid!=rid;" |
| 1772 | ); |
| 1773 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1774 | db_multi_exec("ALTER TABLE vmerge RENAME TO old_vmerge;"); |
| 1775 | db_multi_exec(zLocalSchemaVmerge /*works-like:""*/); |
| 1776 | db_multi_exec( |
| 1777 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1778 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1779 | " WHERE old_vmerge.merge=blob.rid;" |
| 1780 | "DROP TABLE old_vmerge;" |
| 1781 | ); |
| @@ -2081,11 +2151,11 @@ | |
| 2081 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2082 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2083 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2084 | " VALUES('nobody','','gjorz','Nobody');" |
| 2085 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2086 | " VALUES('developer','','dei','Dev');" |
| 2087 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2088 | " VALUES('reader','','kptw','Reader');" |
| 2089 | ); |
| 2090 | } |
| 2091 | } |
| @@ -2305,11 +2375,11 @@ | |
| 2305 | ** SQL functions for debugging. |
| 2306 | ** |
| 2307 | ** The print() function writes its arguments on stdout, but only |
| 2308 | ** if the -sqlprint command-line option is turned on. |
| 2309 | */ |
| 2310 | LOCAL void db_sql_print( |
| 2311 | sqlite3_context *context, |
| 2312 | int argc, |
| 2313 | sqlite3_value **argv |
| 2314 | ){ |
| 2315 | int i; |
| @@ -2318,19 +2388,40 @@ | |
| 2318 | char c = i==argc-1 ? '\n' : ' '; |
| 2319 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2320 | } |
| 2321 | } |
| 2322 | } |
| 2323 | LOCAL int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ |
| 2324 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2325 | char *zSql; |
| 2326 | int n; |
| 2327 | const char *zArg = (const char*)pX; |
| 2328 | if( zArg[0]=='-' ) return 0; |
| 2329 | zSql = sqlite3_expanded_sql(pStmt); |
| 2330 | n = (int)strlen(zSql); |
| 2331 | fossil_trace("%s%s\n", zSql, (n>0 && zSql[n-1]==';') ? "" : ";"); |
| 2332 | sqlite3_free(zSql); |
| 2333 | return 0; |
| 2334 | } |
| 2335 | |
| 2336 | /* |
| @@ -2492,16 +2583,16 @@ | |
| 2492 | return 0; |
| 2493 | } |
| 2494 | |
| 2495 | /* |
| 2496 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2497 | ** work on the ~/.fossil database instead of on the repository database. |
| 2498 | ** Be sure to swap them back after doing the operation. |
| 2499 | ** |
| 2500 | ** If the ~/.fossil database has already been opened as the main database or |
| 2501 | ** is attached to the main database, no connection swaps are required so this |
| 2502 | ** routine is a no-op. |
| 2503 | */ |
| 2504 | void db_swap_connections(void){ |
| 2505 | /* |
| 2506 | ** When swapping the main database connection with the config database |
| 2507 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | |
| 3493 | /* |
| 3494 | ** SETTING: pgp-command width=40 |
| 3495 | ** Command used to clear-sign manifests at check-in. |
| 3496 | ** Default value is "gpg --clearsign -o" |
| 3497 | */ |
| 3498 | /* |
| 3499 | ** SETTING: proxy width=32 default=off |
| 3500 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3501 | ** the "http_proxy" environment variable is consulted. |
| 3502 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | |
| 3627 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3628 | ** Specify which URI's are allowed in HTTP requests from |
| 3629 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3630 | ** whatsoever. |
| 3631 | */ |
| 3632 | /* |
| 3633 | ** SETTING: uv-sync boolean default=off |
| 3634 | ** If true, automatically send unversioned files as part |
| 3635 | ** of a "fossil clone" or "fossil sync" command. The |
| 3636 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | |
| 3695 | ** The "unset" command clears a setting. |
| 3696 | ** |
| 3697 | ** Settings can have both a "local" repository-only value and "global" value |
| 3698 | ** that applies to all repositories. The local values are stored in the |
| 3699 | ** "config" table of the repository and the global values are stored in the |
| 3700 | ** $HOME/.fossil file on unix or in the %LOCALAPPDATA%/_fossil file on Windows. |
| 3701 | ** If both a local and a global value exists for a setting, the local value |
| 3702 | ** takes precedence. This command normally operates on the local settings. |
| 3703 | ** Use the --global option to change global settings. |
| 3704 | ** |
| 3705 | ** Options: |
| 3706 | ** --global set or unset the given property globally instead of |
| 3707 | ** setting or unsetting it for the open repository only. |
| 3708 | ** |
| @@ -3833,12 +3947,12 @@ | |
| 3833 | ** COMMAND: test-without-rowid |
| 3834 | ** |
| 3835 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3836 | ** |
| 3837 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3838 | ** optimization. FILENAME can also be the ~/.fossil file or a local |
| 3839 | ** .fslckout or _FOSSIL_ file. |
| 3840 | ** |
| 3841 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3842 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3843 | ** |
| 3844 | ** Options: |
| 3845 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -18,11 +18,12 @@ | |
| 18 | ** Code for interfacing to the various databases. |
| 19 | ** |
| 20 | ** There are three separate database files that fossil interacts |
| 21 | ** with: |
| 22 | ** |
| 23 | ** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db |
| 24 | ** or in %LOCALAPPDATA%/_fossil |
| 25 | ** |
| 26 | ** (2) The "repository" database |
| 27 | ** |
| 28 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 29 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | |
| 654 | blob_reset(&sql); |
| 655 | return rc; |
| 656 | } |
| 657 | |
| 658 | /* |
| 659 | ** Execute multiple SQL statements. The input text is executed |
| 660 | ** directly without any formatting. |
| 661 | */ |
| 662 | int db_exec_sql(const char *z){ |
| 663 | int rc = SQLITE_OK; |
| 664 | sqlite3_stmt *pStmt; |
| 665 | const char *zEnd; |
| 666 | while( rc==SQLITE_OK && z[0] ){ |
| 667 | pStmt = 0; |
| 668 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 669 | if( rc ){ |
| 670 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | |
| 674 | rc = sqlite3_finalize(pStmt); |
| 675 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 676 | } |
| 677 | z = zEnd; |
| 678 | } |
| 679 | return rc; |
| 680 | } |
| 681 | |
| 682 | /* |
| 683 | ** Execute multiple SQL statements using printf-style formatting. |
| 684 | */ |
| 685 | int db_multi_exec(const char *zSql, ...){ |
| 686 | Blob sql; |
| 687 | int rc; |
| 688 | va_list ap; |
| 689 | |
| 690 | blob_init(&sql, 0, 0); |
| 691 | va_start(ap, zSql); |
| 692 | blob_vappendf(&sql, zSql, ap); |
| 693 | va_end(ap); |
| 694 | rc = db_exec_sql(blob_str(&sql)); |
| 695 | blob_reset(&sql); |
| 696 | return rc; |
| 697 | } |
| 698 | |
| 699 | /* |
| @@ -1031,10 +1042,12 @@ | |
| 1042 | 0, capability_union_step, capability_union_finalize); |
| 1043 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1044 | capability_fullcap, 0, 0); |
| 1045 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1046 | alert_find_emailaddr_func, 0, 0); |
| 1047 | sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, |
| 1048 | alert_display_name_func, 0, 0); |
| 1049 | } |
| 1050 | |
| 1051 | #if USE_SEE |
| 1052 | /* |
| 1053 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | |
| 1271 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1272 | ); |
| 1273 | sqlite3_create_function( |
| 1274 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1275 | ); |
| 1276 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); |
| 1277 | db_add_aux_functions(db); |
| 1278 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1279 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1280 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1281 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | |
| 1302 | blob_init(&key, 0, 0); |
| 1303 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1304 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1305 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1306 | zDbName, zLabel, blob_str(&key)); |
| 1307 | db_exec_sql(zCmd); |
| 1308 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1309 | sqlite3_free(zCmd); |
| 1310 | }else{ |
| 1311 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1312 | zDbName, zLabel); |
| 1313 | db_exec_sql(zCmd); |
| 1314 | sqlite3_free(zCmd); |
| 1315 | #if USE_SEE |
| 1316 | if( blob_size(&key)>0 ){ |
| 1317 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1318 | } |
| @@ -1360,11 +1373,11 @@ | |
| 1373 | db_attach(zDbName, zLabel); |
| 1374 | } |
| 1375 | } |
| 1376 | |
| 1377 | /* |
| 1378 | ** Close the per-user configuration database file |
| 1379 | */ |
| 1380 | void db_close_config(){ |
| 1381 | int iSlot = db_database_slot("configdb"); |
| 1382 | if( iSlot>0 ){ |
| 1383 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | |
| 1397 | fossil_free(g.zConfigDbName); |
| 1398 | g.zConfigDbName = 0; |
| 1399 | } |
| 1400 | |
| 1401 | /* |
| 1402 | ** Compute the name of the configuration database. If unable to find the |
| 1403 | ** database, return 0 if isOptional is true, or panic if isOptional is false. |
| 1404 | ** |
| 1405 | ** Space to hold the result comes from fossil_malloc(). |
| 1406 | */ |
| 1407 | static char *db_configdb_name(int isOptional){ |
| 1408 | char *zHome; /* Home directory */ |
| 1409 | char *zDbName; /* Name of the database file */ |
| 1410 | |
| 1411 | |
| 1412 | /* On Windows, look for these directories, in order: |
| 1413 | ** |
| 1414 | ** FOSSIL_HOME |
| 1415 | ** LOCALAPPDATA |
| 1416 | ** APPDATA |
| 1417 | ** USERPROFILE |
| 1418 | ** HOMEDRIVE HOMEPATH |
| 1419 | */ |
| 1420 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1421 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1422 | if( zHome==0 ){ |
| 1423 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1424 | if( zHome==0 ){ |
| 1425 | zHome = fossil_getenv("APPDATA"); |
| 1426 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | |
| 1431 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1432 | } |
| 1433 | } |
| 1434 | } |
| 1435 | } |
| 1436 | zDbName = mprintf("%//_fossil", zHome); |
| 1437 | fossil_free(zHome); |
| 1438 | return zDbName; |
| 1439 | |
| 1440 | #else /* if unix */ |
| 1441 | char *zXdgHome; |
| 1442 | |
| 1443 | /* For unix. a 5-step algorithm is used. |
| 1444 | ** See ../www/tech_overview.wiki for discussion. |
| 1445 | ** |
| 1446 | ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil |
| 1447 | */ |
| 1448 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1449 | if( zHome!=0 ) return mprintf("%s/.fossil", zHome); |
| 1450 | |
| 1451 | /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil |
| 1452 | */ |
| 1453 | zHome = fossil_getenv("HOME"); |
| 1454 | if( zHome ){ |
| 1455 | zDbName = mprintf("%s/.fossil", zHome); |
| 1456 | if( file_size(zDbName, ExtFILE)>1024*3 ){ |
| 1457 | return zDbName; |
| 1458 | } |
| 1459 | fossil_free(zDbName); |
| 1460 | } |
| 1461 | |
| 1462 | /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db |
| 1463 | */ |
| 1464 | zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); |
| 1465 | if( zXdgHome!=0 ){ |
| 1466 | return mprintf("%s/fossil.db", zXdgHome); |
| 1467 | } |
| 1468 | |
| 1469 | /* The HOME variable is required in order to continue. |
| 1470 | */ |
| 1471 | if( zHome==0 ){ |
| 1472 | if( isOptional ) return 0; |
| 1473 | fossil_panic("cannot locate home directory - please set one of the " |
| 1474 | "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " |
| 1475 | "variables"); |
| 1476 | } |
| 1477 | |
| 1478 | /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db |
| 1479 | */ |
| 1480 | zXdgHome = mprintf("%s/.config", zHome); |
| 1481 | if( file_isdir(zXdgHome, ExtFILE)==1 ){ |
| 1482 | fossil_free(zXdgHome); |
| 1483 | return mprintf("%s/.config/fossil.db", zHome); |
| 1484 | } |
| 1485 | |
| 1486 | /* Step 5: Otherwise -> $HOME/.fossil |
| 1487 | */ |
| 1488 | return mprintf("%s/.fossil", zHome); |
| 1489 | #endif /* unix */ |
| 1490 | } |
| 1491 | |
| 1492 | /* |
| 1493 | ** Open the configuration database. Create the database anew if |
| 1494 | ** it does not already exist. |
| 1495 | ** |
| 1496 | ** If the useAttach flag is 0 (the usual case) then the configuration |
| 1497 | ** database is opened on a separate database connection g.dbConfig. |
| 1498 | ** This prevents the database from becoming locked on long check-in or sync |
| 1499 | ** operations which hold an exclusive transaction. In a few cases, though, |
| 1500 | ** it is convenient for the database to be attached to the main database |
| 1501 | ** connection so that we can join between the various databases. In that |
| 1502 | ** case, invoke this routine with useAttach as 1. |
| 1503 | */ |
| 1504 | int db_open_config(int useAttach, int isOptional){ |
| 1505 | char *zDbName; |
| 1506 | if( g.zConfigDbName ){ |
| 1507 | int alreadyAttached = db_database_slot("configdb")>0; |
| 1508 | if( useAttach==alreadyAttached ) return 1; /* Already open. */ |
| 1509 | db_close_config(); |
| 1510 | } |
| 1511 | zDbName = db_configdb_name(isOptional); |
| 1512 | if( zDbName==0 ) return 0; |
| 1513 | if( file_size(zDbName, ExtFILE)<1024*3 ){ |
| 1514 | char *zHome = file_dirname(zDbName); |
| 1515 | int rc; |
| 1516 | if( file_isdir(zHome, ExtFILE)==0 ){ |
| 1517 | file_mkdir(zHome, ExtFILE, 0); |
| 1518 | } |
| 1519 | rc = file_access(zHome, W_OK); |
| 1520 | fossil_free(zHome); |
| 1521 | if( rc ){ |
| 1522 | if( isOptional ) return 0; |
| 1523 | fossil_panic("home directory \"%s\" must be writeable", zHome); |
| 1524 | } |
| 1525 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1526 | } |
| 1527 | if( file_access(zDbName, W_OK) ){ |
| 1528 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | |
| 1839 | "UPDATE vfile" |
| 1840 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1841 | " WHERE mrid!=rid;" |
| 1842 | ); |
| 1843 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1844 | db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); |
| 1845 | db_exec_sql(zLocalSchemaVmerge); |
| 1846 | db_exec_sql( |
| 1847 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1848 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1849 | " WHERE old_vmerge.merge=blob.rid;" |
| 1850 | "DROP TABLE old_vmerge;" |
| 1851 | ); |
| @@ -2081,11 +2151,11 @@ | |
| 2151 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2152 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2153 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2154 | " VALUES('nobody','','gjorz','Nobody');" |
| 2155 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2156 | " VALUES('developer','','ei','Dev');" |
| 2157 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2158 | " VALUES('reader','','kptw','Reader');" |
| 2159 | ); |
| 2160 | } |
| 2161 | } |
| @@ -2305,11 +2375,11 @@ | |
| 2375 | ** SQL functions for debugging. |
| 2376 | ** |
| 2377 | ** The print() function writes its arguments on stdout, but only |
| 2378 | ** if the -sqlprint command-line option is turned on. |
| 2379 | */ |
| 2380 | void db_sql_print( |
| 2381 | sqlite3_context *context, |
| 2382 | int argc, |
| 2383 | sqlite3_value **argv |
| 2384 | ){ |
| 2385 | int i; |
| @@ -2318,19 +2388,40 @@ | |
| 2388 | char c = i==argc-1 ? '\n' : ' '; |
| 2389 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2390 | } |
| 2391 | } |
| 2392 | } |
| 2393 | |
| 2394 | /* |
| 2395 | ** Callback for sqlite3_trace_v2(); |
| 2396 | */ |
| 2397 | int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ |
| 2398 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2399 | char *zSql; |
| 2400 | int n; |
| 2401 | const char *zArg = (const char*)pX; |
| 2402 | char zEnd[40]; |
| 2403 | if( m & SQLITE_TRACE_CLOSE ){ |
| 2404 | /* If we are tracking closes, that means we want to clean up static |
| 2405 | ** prepared statements. */ |
| 2406 | while( db.pAllStmt ){ |
| 2407 | db_finalize(db.pAllStmt); |
| 2408 | } |
| 2409 | return 0; |
| 2410 | } |
| 2411 | if( zArg[0]=='-' ) return 0; |
| 2412 | if( m & SQLITE_TRACE_PROFILE ){ |
| 2413 | sqlite3_int64 nNano = *(sqlite3_int64*)pX; |
| 2414 | double rMillisec = 0.000001 * nNano; |
| 2415 | sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms */\n", rMillisec); |
| 2416 | }else{ |
| 2417 | zEnd[0] = '\n'; |
| 2418 | zEnd[1] = 0; |
| 2419 | } |
| 2420 | zSql = sqlite3_expanded_sql(pStmt); |
| 2421 | n = (int)strlen(zSql); |
| 2422 | fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); |
| 2423 | sqlite3_free(zSql); |
| 2424 | return 0; |
| 2425 | } |
| 2426 | |
| 2427 | /* |
| @@ -2492,16 +2583,16 @@ | |
| 2583 | return 0; |
| 2584 | } |
| 2585 | |
| 2586 | /* |
| 2587 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2588 | ** work on the configuration database instead of on the repository database. |
| 2589 | ** Be sure to swap them back after doing the operation. |
| 2590 | ** |
| 2591 | ** If the configuration database has already been opened as the main database |
| 2592 | ** or is attached to the main database, no connection swaps are required so |
| 2593 | ** this routine is a no-op. |
| 2594 | */ |
| 2595 | void db_swap_connections(void){ |
| 2596 | /* |
| 2597 | ** When swapping the main database connection with the config database |
| 2598 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | |
| 3584 | /* |
| 3585 | ** SETTING: pgp-command width=40 |
| 3586 | ** Command used to clear-sign manifests at check-in. |
| 3587 | ** Default value is "gpg --clearsign -o" |
| 3588 | */ |
| 3589 | /* |
| 3590 | ** SETTING: forbid-delta-manifests boolean default=off |
| 3591 | ** If enabled, new delta manifests are prohibited. |
| 3592 | */ |
| 3593 | /* |
| 3594 | ** SETTING: proxy width=32 default=off |
| 3595 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3596 | ** the "http_proxy" environment variable is consulted. |
| 3597 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | |
| 3722 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3723 | ** Specify which URI's are allowed in HTTP requests from |
| 3724 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3725 | ** whatsoever. |
| 3726 | */ |
| 3727 | /* |
| 3728 | ** SETTING: default-csp width=40 block-text |
| 3729 | ** |
| 3730 | ** The text of the Content Security Policy that is included |
| 3731 | ** in the Content-Security-Policy: header field of the HTTP |
| 3732 | ** reply and in the default HTML <head> section that is added when the |
| 3733 | ** skin header does not specify a <head> section. The text "$nonce" |
| 3734 | ** is replaced by the random nonce that is created for each web page. |
| 3735 | ** |
| 3736 | ** If this setting is an empty string or is omitted, then |
| 3737 | ** the following default Content Security Policy is used: |
| 3738 | ** |
| 3739 | ** default-src 'self' data:; |
| 3740 | ** script-src 'self' 'nonce-$nonce'; |
| 3741 | ** style-src 'self' 'unsafe-inline'; |
| 3742 | ** |
| 3743 | ** The default CSP is recommended. The main reason to change |
| 3744 | ** this setting would be to add CDNs from which it is safe to |
| 3745 | ** load additional content. |
| 3746 | */ |
| 3747 | /* |
| 3748 | ** SETTING: uv-sync boolean default=off |
| 3749 | ** If true, automatically send unversioned files as part |
| 3750 | ** of a "fossil clone" or "fossil sync" command. The |
| 3751 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | |
| 3810 | ** The "unset" command clears a setting. |
| 3811 | ** |
| 3812 | ** Settings can have both a "local" repository-only value and "global" value |
| 3813 | ** that applies to all repositories. The local values are stored in the |
| 3814 | ** "config" table of the repository and the global values are stored in the |
| 3815 | ** configuration database. If both a local and a global value exists for a |
| 3816 | ** setting, the local value takes precedence. This command normally operates |
| 3817 | ** on the local settings. Use the --global option to change global settings. |
| 3818 | ** |
| 3819 | ** Options: |
| 3820 | ** --global set or unset the given property globally instead of |
| 3821 | ** setting or unsetting it for the open repository only. |
| 3822 | ** |
| @@ -3833,12 +3947,12 @@ | |
| 3947 | ** COMMAND: test-without-rowid |
| 3948 | ** |
| 3949 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3950 | ** |
| 3951 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3952 | ** optimization. FILENAME can also be the configuration database file |
| 3953 | ** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. |
| 3954 | ** |
| 3955 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3956 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3957 | ** |
| 3958 | ** Options: |
| 3959 |
M
src/db.c
+194
-80
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -18,11 +18,12 @@ | ||
| 18 | 18 | ** Code for interfacing to the various databases. |
| 19 | 19 | ** |
| 20 | 20 | ** There are three separate database files that fossil interacts |
| 21 | 21 | ** with: |
| 22 | 22 | ** |
| 23 | -** (1) The "user" database in ~/.fossil | |
| 23 | +** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db | |
| 24 | +** or in %LOCALAPPDATA%/_fossil | |
| 24 | 25 | ** |
| 25 | 26 | ** (2) The "repository" database |
| 26 | 27 | ** |
| 27 | 28 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 28 | 29 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | ||
| 653 | 654 | blob_reset(&sql); |
| 654 | 655 | return rc; |
| 655 | 656 | } |
| 656 | 657 | |
| 657 | 658 | /* |
| 658 | -** Execute multiple SQL statements. | |
| 659 | +** Execute multiple SQL statements. The input text is executed | |
| 660 | +** directly without any formatting. | |
| 659 | 661 | */ |
| 660 | -int db_multi_exec(const char *zSql, ...){ | |
| 661 | - Blob sql; | |
| 662 | +int db_exec_sql(const char *z){ | |
| 662 | 663 | int rc = SQLITE_OK; |
| 663 | - va_list ap; | |
| 664 | - const char *z, *zEnd; | |
| 665 | 664 | sqlite3_stmt *pStmt; |
| 666 | - blob_init(&sql, 0, 0); | |
| 667 | - va_start(ap, zSql); | |
| 668 | - blob_vappendf(&sql, zSql, ap); | |
| 669 | - va_end(ap); | |
| 670 | - z = blob_str(&sql); | |
| 665 | + const char *zEnd; | |
| 671 | 666 | while( rc==SQLITE_OK && z[0] ){ |
| 672 | 667 | pStmt = 0; |
| 673 | 668 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 674 | 669 | if( rc ){ |
| 675 | 670 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | ||
| 679 | 674 | rc = sqlite3_finalize(pStmt); |
| 680 | 675 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 681 | 676 | } |
| 682 | 677 | z = zEnd; |
| 683 | 678 | } |
| 679 | + return rc; | |
| 680 | +} | |
| 681 | + | |
| 682 | +/* | |
| 683 | +** Execute multiple SQL statements using printf-style formatting. | |
| 684 | +*/ | |
| 685 | +int db_multi_exec(const char *zSql, ...){ | |
| 686 | + Blob sql; | |
| 687 | + int rc; | |
| 688 | + va_list ap; | |
| 689 | + | |
| 690 | + blob_init(&sql, 0, 0); | |
| 691 | + va_start(ap, zSql); | |
| 692 | + blob_vappendf(&sql, zSql, ap); | |
| 693 | + va_end(ap); | |
| 694 | + rc = db_exec_sql(blob_str(&sql)); | |
| 684 | 695 | blob_reset(&sql); |
| 685 | 696 | return rc; |
| 686 | 697 | } |
| 687 | 698 | |
| 688 | 699 | /* |
| @@ -1031,10 +1042,12 @@ | ||
| 1031 | 1042 | 0, capability_union_step, capability_union_finalize); |
| 1032 | 1043 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1033 | 1044 | capability_fullcap, 0, 0); |
| 1034 | 1045 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1035 | 1046 | alert_find_emailaddr_func, 0, 0); |
| 1047 | + sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, | |
| 1048 | + alert_display_name_func, 0, 0); | |
| 1036 | 1049 | } |
| 1037 | 1050 | |
| 1038 | 1051 | #if USE_SEE |
| 1039 | 1052 | /* |
| 1040 | 1053 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | ||
| 1258 | 1271 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1259 | 1272 | ); |
| 1260 | 1273 | sqlite3_create_function( |
| 1261 | 1274 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1262 | 1275 | ); |
| 1263 | - if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_STMT, db_sql_trace, 0); | |
| 1276 | + if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); | |
| 1264 | 1277 | db_add_aux_functions(db); |
| 1265 | 1278 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1266 | 1279 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1267 | 1280 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1268 | 1281 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | ||
| 1289 | 1302 | blob_init(&key, 0, 0); |
| 1290 | 1303 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1291 | 1304 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1292 | 1305 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1293 | 1306 | zDbName, zLabel, blob_str(&key)); |
| 1294 | - db_multi_exec(zCmd /*works-like:""*/); | |
| 1307 | + db_exec_sql(zCmd); | |
| 1295 | 1308 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1296 | 1309 | sqlite3_free(zCmd); |
| 1297 | 1310 | }else{ |
| 1298 | 1311 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1299 | 1312 | zDbName, zLabel); |
| 1300 | - db_multi_exec(zCmd /*works-like:""*/); | |
| 1313 | + db_exec_sql(zCmd); | |
| 1301 | 1314 | sqlite3_free(zCmd); |
| 1302 | 1315 | #if USE_SEE |
| 1303 | 1316 | if( blob_size(&key)>0 ){ |
| 1304 | 1317 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1305 | 1318 | } |
| @@ -1360,11 +1373,11 @@ | ||
| 1360 | 1373 | db_attach(zDbName, zLabel); |
| 1361 | 1374 | } |
| 1362 | 1375 | } |
| 1363 | 1376 | |
| 1364 | 1377 | /* |
| 1365 | -** Close the per-user database file in ~/.fossil | |
| 1378 | +** Close the per-user configuration database file | |
| 1366 | 1379 | */ |
| 1367 | 1380 | void db_close_config(){ |
| 1368 | 1381 | int iSlot = db_database_slot("configdb"); |
| 1369 | 1382 | if( iSlot>0 ){ |
| 1370 | 1383 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | ||
| 1384 | 1397 | fossil_free(g.zConfigDbName); |
| 1385 | 1398 | g.zConfigDbName = 0; |
| 1386 | 1399 | } |
| 1387 | 1400 | |
| 1388 | 1401 | /* |
| 1389 | -** Open the user database in "~/.fossil". Create the database anew if | |
| 1390 | -** it does not already exist. | |
| 1402 | +** Compute the name of the configuration database. If unable to find the | |
| 1403 | +** database, return 0 if isOptional is true, or panic if isOptional is false. | |
| 1391 | 1404 | ** |
| 1392 | -** If the useAttach flag is 0 (the usual case) then the user database is | |
| 1393 | -** opened on a separate database connection g.dbConfig. This prevents | |
| 1394 | -** the ~/.fossil database from becoming locked on long check-in or sync | |
| 1395 | -** operations which hold an exclusive transaction. In a few cases, though, | |
| 1396 | -** it is convenient for the ~/.fossil to be attached to the main database | |
| 1397 | -** connection so that we can join between the various databases. In that | |
| 1398 | -** case, invoke this routine with useAttach as 1. | |
| 1405 | +** Space to hold the result comes from fossil_malloc(). | |
| 1399 | 1406 | */ |
| 1400 | -int db_open_config(int useAttach, int isOptional){ | |
| 1401 | - char *zDbName; | |
| 1402 | - char *zHome; | |
| 1403 | - if( g.zConfigDbName ){ | |
| 1404 | - int alreadyAttached = db_database_slot("configdb")>0; | |
| 1405 | - if( useAttach==alreadyAttached ) return 1; /* Already open. */ | |
| 1406 | - db_close_config(); | |
| 1407 | - } | |
| 1408 | - zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1407 | +static char *db_configdb_name(int isOptional){ | |
| 1408 | + char *zHome; /* Home directory */ | |
| 1409 | + char *zDbName; /* Name of the database file */ | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + /* On Windows, look for these directories, in order: | |
| 1413 | + ** | |
| 1414 | + ** FOSSIL_HOME | |
| 1415 | + ** LOCALAPPDATA | |
| 1416 | + ** APPDATA | |
| 1417 | + ** USERPROFILE | |
| 1418 | + ** HOMEDRIVE HOMEPATH | |
| 1419 | + */ | |
| 1409 | 1420 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1421 | + zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1410 | 1422 | if( zHome==0 ){ |
| 1411 | 1423 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1412 | 1424 | if( zHome==0 ){ |
| 1413 | 1425 | zHome = fossil_getenv("APPDATA"); |
| 1414 | 1426 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | ||
| 1419 | 1431 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1420 | 1432 | } |
| 1421 | 1433 | } |
| 1422 | 1434 | } |
| 1423 | 1435 | } |
| 1424 | - if( zHome==0 ){ | |
| 1425 | - if( isOptional ) return 0; | |
| 1426 | - fossil_panic("cannot locate home directory - please set the " | |
| 1427 | - "FOSSIL_HOME, LOCALAPPDATA, APPDATA, USERPROFILE, " | |
| 1428 | - "or HOMEDRIVE / HOMEPATH environment variables"); | |
| 1429 | - } | |
| 1430 | -#else | |
| 1431 | - if( zHome==0 ){ | |
| 1432 | - zHome = fossil_getenv("HOME"); | |
| 1433 | - } | |
| 1434 | - if( zHome==0 ){ | |
| 1435 | - if( isOptional ) return 0; | |
| 1436 | - fossil_panic("cannot locate home directory - please set the " | |
| 1437 | - "FOSSIL_HOME or HOME environment variables"); | |
| 1438 | - } | |
| 1439 | -#endif | |
| 1440 | - if( file_isdir(zHome, ExtFILE)!=1 ){ | |
| 1441 | - if( isOptional ) return 0; | |
| 1442 | - fossil_panic("invalid home directory: %s", zHome); | |
| 1443 | - } | |
| 1444 | -#if defined(_WIN32) || defined(__CYGWIN__) | |
| 1445 | - /* . filenames give some window systems problems and many apps problems */ | |
| 1446 | - zDbName = mprintf("%//_fossil", zHome); | |
| 1447 | -#else | |
| 1448 | - zDbName = mprintf("%s/.fossil", zHome); | |
| 1449 | -#endif | |
| 1450 | - if( file_size(zDbName, ExtFILE)<1024*3 ){ | |
| 1451 | - if( file_access(zHome, W_OK) ){ | |
| 1452 | - if( isOptional ) return 0; | |
| 1453 | - fossil_panic("home directory %s must be writeable", zHome); | |
| 1436 | + zDbName = mprintf("%//_fossil", zHome); | |
| 1437 | + fossil_free(zHome); | |
| 1438 | + return zDbName; | |
| 1439 | + | |
| 1440 | +#else /* if unix */ | |
| 1441 | + char *zXdgHome; | |
| 1442 | + | |
| 1443 | + /* For unix. a 5-step algorithm is used. | |
| 1444 | + ** See ../www/tech_overview.wiki for discussion. | |
| 1445 | + ** | |
| 1446 | + ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil | |
| 1447 | + */ | |
| 1448 | + zHome = fossil_getenv("FOSSIL_HOME"); | |
| 1449 | + if( zHome!=0 ) return mprintf("%s/.fossil", zHome); | |
| 1450 | + | |
| 1451 | + /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil | |
| 1452 | + */ | |
| 1453 | + zHome = fossil_getenv("HOME"); | |
| 1454 | + if( zHome ){ | |
| 1455 | + zDbName = mprintf("%s/.fossil", zHome); | |
| 1456 | + if( file_size(zDbName, ExtFILE)>1024*3 ){ | |
| 1457 | + return zDbName; | |
| 1458 | + } | |
| 1459 | + fossil_free(zDbName); | |
| 1460 | + } | |
| 1461 | + | |
| 1462 | + /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db | |
| 1463 | + */ | |
| 1464 | + zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); | |
| 1465 | + if( zXdgHome!=0 ){ | |
| 1466 | + return mprintf("%s/fossil.db", zXdgHome); | |
| 1467 | + } | |
| 1468 | + | |
| 1469 | + /* The HOME variable is required in order to continue. | |
| 1470 | + */ | |
| 1471 | + if( zHome==0 ){ | |
| 1472 | + if( isOptional ) return 0; | |
| 1473 | + fossil_panic("cannot locate home directory - please set one of the " | |
| 1474 | + "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " | |
| 1475 | + "variables"); | |
| 1476 | + } | |
| 1477 | + | |
| 1478 | + /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db | |
| 1479 | + */ | |
| 1480 | + zXdgHome = mprintf("%s/.config", zHome); | |
| 1481 | + if( file_isdir(zXdgHome, ExtFILE)==1 ){ | |
| 1482 | + fossil_free(zXdgHome); | |
| 1483 | + return mprintf("%s/.config/fossil.db", zHome); | |
| 1484 | + } | |
| 1485 | + | |
| 1486 | + /* Step 5: Otherwise -> $HOME/.fossil | |
| 1487 | + */ | |
| 1488 | + return mprintf("%s/.fossil", zHome); | |
| 1489 | +#endif /* unix */ | |
| 1490 | +} | |
| 1491 | + | |
| 1492 | +/* | |
| 1493 | +** Open the configuration database. Create the database anew if | |
| 1494 | +** it does not already exist. | |
| 1495 | +** | |
| 1496 | +** If the useAttach flag is 0 (the usual case) then the configuration | |
| 1497 | +** database is opened on a separate database connection g.dbConfig. | |
| 1498 | +** This prevents the database from becoming locked on long check-in or sync | |
| 1499 | +** operations which hold an exclusive transaction. In a few cases, though, | |
| 1500 | +** it is convenient for the database to be attached to the main database | |
| 1501 | +** connection so that we can join between the various databases. In that | |
| 1502 | +** case, invoke this routine with useAttach as 1. | |
| 1503 | +*/ | |
| 1504 | +int db_open_config(int useAttach, int isOptional){ | |
| 1505 | + char *zDbName; | |
| 1506 | + if( g.zConfigDbName ){ | |
| 1507 | + int alreadyAttached = db_database_slot("configdb")>0; | |
| 1508 | + if( useAttach==alreadyAttached ) return 1; /* Already open. */ | |
| 1509 | + db_close_config(); | |
| 1510 | + } | |
| 1511 | + zDbName = db_configdb_name(isOptional); | |
| 1512 | + if( zDbName==0 ) return 0; | |
| 1513 | + if( file_size(zDbName, ExtFILE)<1024*3 ){ | |
| 1514 | + char *zHome = file_dirname(zDbName); | |
| 1515 | + int rc; | |
| 1516 | + if( file_isdir(zHome, ExtFILE)==0 ){ | |
| 1517 | + file_mkdir(zHome, ExtFILE, 0); | |
| 1518 | + } | |
| 1519 | + rc = file_access(zHome, W_OK); | |
| 1520 | + fossil_free(zHome); | |
| 1521 | + if( rc ){ | |
| 1522 | + if( isOptional ) return 0; | |
| 1523 | + fossil_panic("home directory \"%s\" must be writeable", zHome); | |
| 1454 | 1524 | } |
| 1455 | 1525 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1456 | 1526 | } |
| 1457 | 1527 | if( file_access(zDbName, W_OK) ){ |
| 1458 | 1528 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | ||
| 1769 | 1839 | "UPDATE vfile" |
| 1770 | 1840 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1771 | 1841 | " WHERE mrid!=rid;" |
| 1772 | 1842 | ); |
| 1773 | 1843 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1774 | - db_multi_exec("ALTER TABLE vmerge RENAME TO old_vmerge;"); | |
| 1775 | - db_multi_exec(zLocalSchemaVmerge /*works-like:""*/); | |
| 1776 | - db_multi_exec( | |
| 1844 | + db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); | |
| 1845 | + db_exec_sql(zLocalSchemaVmerge); | |
| 1846 | + db_exec_sql( | |
| 1777 | 1847 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1778 | 1848 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1779 | 1849 | " WHERE old_vmerge.merge=blob.rid;" |
| 1780 | 1850 | "DROP TABLE old_vmerge;" |
| 1781 | 1851 | ); |
| @@ -2081,11 +2151,11 @@ | ||
| 2081 | 2151 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2082 | 2152 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2083 | 2153 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2084 | 2154 | " VALUES('nobody','','gjorz','Nobody');" |
| 2085 | 2155 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2086 | - " VALUES('developer','','dei','Dev');" | |
| 2156 | + " VALUES('developer','','ei','Dev');" | |
| 2087 | 2157 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2088 | 2158 | " VALUES('reader','','kptw','Reader');" |
| 2089 | 2159 | ); |
| 2090 | 2160 | } |
| 2091 | 2161 | } |
| @@ -2305,11 +2375,11 @@ | ||
| 2305 | 2375 | ** SQL functions for debugging. |
| 2306 | 2376 | ** |
| 2307 | 2377 | ** The print() function writes its arguments on stdout, but only |
| 2308 | 2378 | ** if the -sqlprint command-line option is turned on. |
| 2309 | 2379 | */ |
| 2310 | -LOCAL void db_sql_print( | |
| 2380 | +void db_sql_print( | |
| 2311 | 2381 | sqlite3_context *context, |
| 2312 | 2382 | int argc, |
| 2313 | 2383 | sqlite3_value **argv |
| 2314 | 2384 | ){ |
| 2315 | 2385 | int i; |
| @@ -2318,19 +2388,40 @@ | ||
| 2318 | 2388 | char c = i==argc-1 ? '\n' : ' '; |
| 2319 | 2389 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2320 | 2390 | } |
| 2321 | 2391 | } |
| 2322 | 2392 | } |
| 2323 | -LOCAL int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ | |
| 2393 | + | |
| 2394 | +/* | |
| 2395 | +** Callback for sqlite3_trace_v2(); | |
| 2396 | +*/ | |
| 2397 | +int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ | |
| 2324 | 2398 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2325 | 2399 | char *zSql; |
| 2326 | 2400 | int n; |
| 2327 | 2401 | const char *zArg = (const char*)pX; |
| 2402 | + char zEnd[40]; | |
| 2403 | + if( m & SQLITE_TRACE_CLOSE ){ | |
| 2404 | + /* If we are tracking closes, that means we want to clean up static | |
| 2405 | + ** prepared statements. */ | |
| 2406 | + while( db.pAllStmt ){ | |
| 2407 | + db_finalize(db.pAllStmt); | |
| 2408 | + } | |
| 2409 | + return 0; | |
| 2410 | + } | |
| 2328 | 2411 | if( zArg[0]=='-' ) return 0; |
| 2412 | + if( m & SQLITE_TRACE_PROFILE ){ | |
| 2413 | + sqlite3_int64 nNano = *(sqlite3_int64*)pX; | |
| 2414 | + double rMillisec = 0.000001 * nNano; | |
| 2415 | + sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms */\n", rMillisec); | |
| 2416 | + }else{ | |
| 2417 | + zEnd[0] = '\n'; | |
| 2418 | + zEnd[1] = 0; | |
| 2419 | + } | |
| 2329 | 2420 | zSql = sqlite3_expanded_sql(pStmt); |
| 2330 | 2421 | n = (int)strlen(zSql); |
| 2331 | - fossil_trace("%s%s\n", zSql, (n>0 && zSql[n-1]==';') ? "" : ";"); | |
| 2422 | + fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); | |
| 2332 | 2423 | sqlite3_free(zSql); |
| 2333 | 2424 | return 0; |
| 2334 | 2425 | } |
| 2335 | 2426 | |
| 2336 | 2427 | /* |
| @@ -2492,16 +2583,16 @@ | ||
| 2492 | 2583 | return 0; |
| 2493 | 2584 | } |
| 2494 | 2585 | |
| 2495 | 2586 | /* |
| 2496 | 2587 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2497 | -** work on the ~/.fossil database instead of on the repository database. | |
| 2588 | +** work on the configuration database instead of on the repository database. | |
| 2498 | 2589 | ** Be sure to swap them back after doing the operation. |
| 2499 | 2590 | ** |
| 2500 | -** If the ~/.fossil database has already been opened as the main database or | |
| 2501 | -** is attached to the main database, no connection swaps are required so this | |
| 2502 | -** routine is a no-op. | |
| 2591 | +** If the configuration database has already been opened as the main database | |
| 2592 | +** or is attached to the main database, no connection swaps are required so | |
| 2593 | +** this routine is a no-op. | |
| 2503 | 2594 | */ |
| 2504 | 2595 | void db_swap_connections(void){ |
| 2505 | 2596 | /* |
| 2506 | 2597 | ** When swapping the main database connection with the config database |
| 2507 | 2598 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | ||
| 3493 | 3584 | /* |
| 3494 | 3585 | ** SETTING: pgp-command width=40 |
| 3495 | 3586 | ** Command used to clear-sign manifests at check-in. |
| 3496 | 3587 | ** Default value is "gpg --clearsign -o" |
| 3497 | 3588 | */ |
| 3589 | +/* | |
| 3590 | +** SETTING: forbid-delta-manifests boolean default=off | |
| 3591 | +** If enabled, new delta manifests are prohibited. | |
| 3592 | +*/ | |
| 3498 | 3593 | /* |
| 3499 | 3594 | ** SETTING: proxy width=32 default=off |
| 3500 | 3595 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3501 | 3596 | ** the "http_proxy" environment variable is consulted. |
| 3502 | 3597 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | ||
| 3627 | 3722 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3628 | 3723 | ** Specify which URI's are allowed in HTTP requests from |
| 3629 | 3724 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3630 | 3725 | ** whatsoever. |
| 3631 | 3726 | */ |
| 3727 | +/* | |
| 3728 | +** SETTING: default-csp width=40 block-text | |
| 3729 | +** | |
| 3730 | +** The text of the Content Security Policy that is included | |
| 3731 | +** in the Content-Security-Policy: header field of the HTTP | |
| 3732 | +** reply and in the default HTML <head> section that is added when the | |
| 3733 | +** skin header does not specify a <head> section. The text "$nonce" | |
| 3734 | +** is replaced by the random nonce that is created for each web page. | |
| 3735 | +** | |
| 3736 | +** If this setting is an empty string or is omitted, then | |
| 3737 | +** the following default Content Security Policy is used: | |
| 3738 | +** | |
| 3739 | +** default-src 'self' data:; | |
| 3740 | +** script-src 'self' 'nonce-$nonce'; | |
| 3741 | +** style-src 'self' 'unsafe-inline'; | |
| 3742 | +** | |
| 3743 | +** The default CSP is recommended. The main reason to change | |
| 3744 | +** this setting would be to add CDNs from which it is safe to | |
| 3745 | +** load additional content. | |
| 3746 | +*/ | |
| 3632 | 3747 | /* |
| 3633 | 3748 | ** SETTING: uv-sync boolean default=off |
| 3634 | 3749 | ** If true, automatically send unversioned files as part |
| 3635 | 3750 | ** of a "fossil clone" or "fossil sync" command. The |
| 3636 | 3751 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | ||
| 3695 | 3810 | ** The "unset" command clears a setting. |
| 3696 | 3811 | ** |
| 3697 | 3812 | ** Settings can have both a "local" repository-only value and "global" value |
| 3698 | 3813 | ** that applies to all repositories. The local values are stored in the |
| 3699 | 3814 | ** "config" table of the repository and the global values are stored in the |
| 3700 | -** $HOME/.fossil file on unix or in the %LOCALAPPDATA%/_fossil file on Windows. | |
| 3701 | -** If both a local and a global value exists for a setting, the local value | |
| 3702 | -** takes precedence. This command normally operates on the local settings. | |
| 3703 | -** Use the --global option to change global settings. | |
| 3815 | +** configuration database. If both a local and a global value exists for a | |
| 3816 | +** setting, the local value takes precedence. This command normally operates | |
| 3817 | +** on the local settings. Use the --global option to change global settings. | |
| 3704 | 3818 | ** |
| 3705 | 3819 | ** Options: |
| 3706 | 3820 | ** --global set or unset the given property globally instead of |
| 3707 | 3821 | ** setting or unsetting it for the open repository only. |
| 3708 | 3822 | ** |
| @@ -3833,12 +3947,12 @@ | ||
| 3833 | 3947 | ** COMMAND: test-without-rowid |
| 3834 | 3948 | ** |
| 3835 | 3949 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3836 | 3950 | ** |
| 3837 | 3951 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3838 | -** optimization. FILENAME can also be the ~/.fossil file or a local | |
| 3839 | -** .fslckout or _FOSSIL_ file. | |
| 3952 | +** optimization. FILENAME can also be the configuration database file | |
| 3953 | +** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. | |
| 3840 | 3954 | ** |
| 3841 | 3955 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3842 | 3956 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3843 | 3957 | ** |
| 3844 | 3958 | ** Options: |
| 3845 | 3959 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -18,11 +18,12 @@ | |
| 18 | ** Code for interfacing to the various databases. |
| 19 | ** |
| 20 | ** There are three separate database files that fossil interacts |
| 21 | ** with: |
| 22 | ** |
| 23 | ** (1) The "user" database in ~/.fossil |
| 24 | ** |
| 25 | ** (2) The "repository" database |
| 26 | ** |
| 27 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 28 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | |
| 653 | blob_reset(&sql); |
| 654 | return rc; |
| 655 | } |
| 656 | |
| 657 | /* |
| 658 | ** Execute multiple SQL statements. |
| 659 | */ |
| 660 | int db_multi_exec(const char *zSql, ...){ |
| 661 | Blob sql; |
| 662 | int rc = SQLITE_OK; |
| 663 | va_list ap; |
| 664 | const char *z, *zEnd; |
| 665 | sqlite3_stmt *pStmt; |
| 666 | blob_init(&sql, 0, 0); |
| 667 | va_start(ap, zSql); |
| 668 | blob_vappendf(&sql, zSql, ap); |
| 669 | va_end(ap); |
| 670 | z = blob_str(&sql); |
| 671 | while( rc==SQLITE_OK && z[0] ){ |
| 672 | pStmt = 0; |
| 673 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 674 | if( rc ){ |
| 675 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | |
| 679 | rc = sqlite3_finalize(pStmt); |
| 680 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 681 | } |
| 682 | z = zEnd; |
| 683 | } |
| 684 | blob_reset(&sql); |
| 685 | return rc; |
| 686 | } |
| 687 | |
| 688 | /* |
| @@ -1031,10 +1042,12 @@ | |
| 1031 | 0, capability_union_step, capability_union_finalize); |
| 1032 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1033 | capability_fullcap, 0, 0); |
| 1034 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1035 | alert_find_emailaddr_func, 0, 0); |
| 1036 | } |
| 1037 | |
| 1038 | #if USE_SEE |
| 1039 | /* |
| 1040 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | |
| 1258 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1259 | ); |
| 1260 | sqlite3_create_function( |
| 1261 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1262 | ); |
| 1263 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_STMT, db_sql_trace, 0); |
| 1264 | db_add_aux_functions(db); |
| 1265 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1266 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1267 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1268 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | |
| 1289 | blob_init(&key, 0, 0); |
| 1290 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1291 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1292 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1293 | zDbName, zLabel, blob_str(&key)); |
| 1294 | db_multi_exec(zCmd /*works-like:""*/); |
| 1295 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1296 | sqlite3_free(zCmd); |
| 1297 | }else{ |
| 1298 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1299 | zDbName, zLabel); |
| 1300 | db_multi_exec(zCmd /*works-like:""*/); |
| 1301 | sqlite3_free(zCmd); |
| 1302 | #if USE_SEE |
| 1303 | if( blob_size(&key)>0 ){ |
| 1304 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1305 | } |
| @@ -1360,11 +1373,11 @@ | |
| 1360 | db_attach(zDbName, zLabel); |
| 1361 | } |
| 1362 | } |
| 1363 | |
| 1364 | /* |
| 1365 | ** Close the per-user database file in ~/.fossil |
| 1366 | */ |
| 1367 | void db_close_config(){ |
| 1368 | int iSlot = db_database_slot("configdb"); |
| 1369 | if( iSlot>0 ){ |
| 1370 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | |
| 1384 | fossil_free(g.zConfigDbName); |
| 1385 | g.zConfigDbName = 0; |
| 1386 | } |
| 1387 | |
| 1388 | /* |
| 1389 | ** Open the user database in "~/.fossil". Create the database anew if |
| 1390 | ** it does not already exist. |
| 1391 | ** |
| 1392 | ** If the useAttach flag is 0 (the usual case) then the user database is |
| 1393 | ** opened on a separate database connection g.dbConfig. This prevents |
| 1394 | ** the ~/.fossil database from becoming locked on long check-in or sync |
| 1395 | ** operations which hold an exclusive transaction. In a few cases, though, |
| 1396 | ** it is convenient for the ~/.fossil to be attached to the main database |
| 1397 | ** connection so that we can join between the various databases. In that |
| 1398 | ** case, invoke this routine with useAttach as 1. |
| 1399 | */ |
| 1400 | int db_open_config(int useAttach, int isOptional){ |
| 1401 | char *zDbName; |
| 1402 | char *zHome; |
| 1403 | if( g.zConfigDbName ){ |
| 1404 | int alreadyAttached = db_database_slot("configdb")>0; |
| 1405 | if( useAttach==alreadyAttached ) return 1; /* Already open. */ |
| 1406 | db_close_config(); |
| 1407 | } |
| 1408 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1409 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1410 | if( zHome==0 ){ |
| 1411 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1412 | if( zHome==0 ){ |
| 1413 | zHome = fossil_getenv("APPDATA"); |
| 1414 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | |
| 1419 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1420 | } |
| 1421 | } |
| 1422 | } |
| 1423 | } |
| 1424 | if( zHome==0 ){ |
| 1425 | if( isOptional ) return 0; |
| 1426 | fossil_panic("cannot locate home directory - please set the " |
| 1427 | "FOSSIL_HOME, LOCALAPPDATA, APPDATA, USERPROFILE, " |
| 1428 | "or HOMEDRIVE / HOMEPATH environment variables"); |
| 1429 | } |
| 1430 | #else |
| 1431 | if( zHome==0 ){ |
| 1432 | zHome = fossil_getenv("HOME"); |
| 1433 | } |
| 1434 | if( zHome==0 ){ |
| 1435 | if( isOptional ) return 0; |
| 1436 | fossil_panic("cannot locate home directory - please set the " |
| 1437 | "FOSSIL_HOME or HOME environment variables"); |
| 1438 | } |
| 1439 | #endif |
| 1440 | if( file_isdir(zHome, ExtFILE)!=1 ){ |
| 1441 | if( isOptional ) return 0; |
| 1442 | fossil_panic("invalid home directory: %s", zHome); |
| 1443 | } |
| 1444 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1445 | /* . filenames give some window systems problems and many apps problems */ |
| 1446 | zDbName = mprintf("%//_fossil", zHome); |
| 1447 | #else |
| 1448 | zDbName = mprintf("%s/.fossil", zHome); |
| 1449 | #endif |
| 1450 | if( file_size(zDbName, ExtFILE)<1024*3 ){ |
| 1451 | if( file_access(zHome, W_OK) ){ |
| 1452 | if( isOptional ) return 0; |
| 1453 | fossil_panic("home directory %s must be writeable", zHome); |
| 1454 | } |
| 1455 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1456 | } |
| 1457 | if( file_access(zDbName, W_OK) ){ |
| 1458 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | |
| 1769 | "UPDATE vfile" |
| 1770 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1771 | " WHERE mrid!=rid;" |
| 1772 | ); |
| 1773 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1774 | db_multi_exec("ALTER TABLE vmerge RENAME TO old_vmerge;"); |
| 1775 | db_multi_exec(zLocalSchemaVmerge /*works-like:""*/); |
| 1776 | db_multi_exec( |
| 1777 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1778 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1779 | " WHERE old_vmerge.merge=blob.rid;" |
| 1780 | "DROP TABLE old_vmerge;" |
| 1781 | ); |
| @@ -2081,11 +2151,11 @@ | |
| 2081 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2082 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2083 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2084 | " VALUES('nobody','','gjorz','Nobody');" |
| 2085 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2086 | " VALUES('developer','','dei','Dev');" |
| 2087 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2088 | " VALUES('reader','','kptw','Reader');" |
| 2089 | ); |
| 2090 | } |
| 2091 | } |
| @@ -2305,11 +2375,11 @@ | |
| 2305 | ** SQL functions for debugging. |
| 2306 | ** |
| 2307 | ** The print() function writes its arguments on stdout, but only |
| 2308 | ** if the -sqlprint command-line option is turned on. |
| 2309 | */ |
| 2310 | LOCAL void db_sql_print( |
| 2311 | sqlite3_context *context, |
| 2312 | int argc, |
| 2313 | sqlite3_value **argv |
| 2314 | ){ |
| 2315 | int i; |
| @@ -2318,19 +2388,40 @@ | |
| 2318 | char c = i==argc-1 ? '\n' : ' '; |
| 2319 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2320 | } |
| 2321 | } |
| 2322 | } |
| 2323 | LOCAL int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ |
| 2324 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2325 | char *zSql; |
| 2326 | int n; |
| 2327 | const char *zArg = (const char*)pX; |
| 2328 | if( zArg[0]=='-' ) return 0; |
| 2329 | zSql = sqlite3_expanded_sql(pStmt); |
| 2330 | n = (int)strlen(zSql); |
| 2331 | fossil_trace("%s%s\n", zSql, (n>0 && zSql[n-1]==';') ? "" : ";"); |
| 2332 | sqlite3_free(zSql); |
| 2333 | return 0; |
| 2334 | } |
| 2335 | |
| 2336 | /* |
| @@ -2492,16 +2583,16 @@ | |
| 2492 | return 0; |
| 2493 | } |
| 2494 | |
| 2495 | /* |
| 2496 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2497 | ** work on the ~/.fossil database instead of on the repository database. |
| 2498 | ** Be sure to swap them back after doing the operation. |
| 2499 | ** |
| 2500 | ** If the ~/.fossil database has already been opened as the main database or |
| 2501 | ** is attached to the main database, no connection swaps are required so this |
| 2502 | ** routine is a no-op. |
| 2503 | */ |
| 2504 | void db_swap_connections(void){ |
| 2505 | /* |
| 2506 | ** When swapping the main database connection with the config database |
| 2507 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | |
| 3493 | /* |
| 3494 | ** SETTING: pgp-command width=40 |
| 3495 | ** Command used to clear-sign manifests at check-in. |
| 3496 | ** Default value is "gpg --clearsign -o" |
| 3497 | */ |
| 3498 | /* |
| 3499 | ** SETTING: proxy width=32 default=off |
| 3500 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3501 | ** the "http_proxy" environment variable is consulted. |
| 3502 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | |
| 3627 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3628 | ** Specify which URI's are allowed in HTTP requests from |
| 3629 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3630 | ** whatsoever. |
| 3631 | */ |
| 3632 | /* |
| 3633 | ** SETTING: uv-sync boolean default=off |
| 3634 | ** If true, automatically send unversioned files as part |
| 3635 | ** of a "fossil clone" or "fossil sync" command. The |
| 3636 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | |
| 3695 | ** The "unset" command clears a setting. |
| 3696 | ** |
| 3697 | ** Settings can have both a "local" repository-only value and "global" value |
| 3698 | ** that applies to all repositories. The local values are stored in the |
| 3699 | ** "config" table of the repository and the global values are stored in the |
| 3700 | ** $HOME/.fossil file on unix or in the %LOCALAPPDATA%/_fossil file on Windows. |
| 3701 | ** If both a local and a global value exists for a setting, the local value |
| 3702 | ** takes precedence. This command normally operates on the local settings. |
| 3703 | ** Use the --global option to change global settings. |
| 3704 | ** |
| 3705 | ** Options: |
| 3706 | ** --global set or unset the given property globally instead of |
| 3707 | ** setting or unsetting it for the open repository only. |
| 3708 | ** |
| @@ -3833,12 +3947,12 @@ | |
| 3833 | ** COMMAND: test-without-rowid |
| 3834 | ** |
| 3835 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3836 | ** |
| 3837 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3838 | ** optimization. FILENAME can also be the ~/.fossil file or a local |
| 3839 | ** .fslckout or _FOSSIL_ file. |
| 3840 | ** |
| 3841 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3842 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3843 | ** |
| 3844 | ** Options: |
| 3845 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -18,11 +18,12 @@ | |
| 18 | ** Code for interfacing to the various databases. |
| 19 | ** |
| 20 | ** There are three separate database files that fossil interacts |
| 21 | ** with: |
| 22 | ** |
| 23 | ** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db |
| 24 | ** or in %LOCALAPPDATA%/_fossil |
| 25 | ** |
| 26 | ** (2) The "repository" database |
| 27 | ** |
| 28 | ** (3) A local checkout database named "_FOSSIL_" or ".fslckout" |
| 29 | ** and located at the root of the local copy of the source tree. |
| @@ -653,23 +654,17 @@ | |
| 654 | blob_reset(&sql); |
| 655 | return rc; |
| 656 | } |
| 657 | |
| 658 | /* |
| 659 | ** Execute multiple SQL statements. The input text is executed |
| 660 | ** directly without any formatting. |
| 661 | */ |
| 662 | int db_exec_sql(const char *z){ |
| 663 | int rc = SQLITE_OK; |
| 664 | sqlite3_stmt *pStmt; |
| 665 | const char *zEnd; |
| 666 | while( rc==SQLITE_OK && z[0] ){ |
| 667 | pStmt = 0; |
| 668 | rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); |
| 669 | if( rc ){ |
| 670 | db_err("%s: {%s}", sqlite3_errmsg(g.db), z); |
| @@ -679,10 +674,26 @@ | |
| 674 | rc = sqlite3_finalize(pStmt); |
| 675 | if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); |
| 676 | } |
| 677 | z = zEnd; |
| 678 | } |
| 679 | return rc; |
| 680 | } |
| 681 | |
| 682 | /* |
| 683 | ** Execute multiple SQL statements using printf-style formatting. |
| 684 | */ |
| 685 | int db_multi_exec(const char *zSql, ...){ |
| 686 | Blob sql; |
| 687 | int rc; |
| 688 | va_list ap; |
| 689 | |
| 690 | blob_init(&sql, 0, 0); |
| 691 | va_start(ap, zSql); |
| 692 | blob_vappendf(&sql, zSql, ap); |
| 693 | va_end(ap); |
| 694 | rc = db_exec_sql(blob_str(&sql)); |
| 695 | blob_reset(&sql); |
| 696 | return rc; |
| 697 | } |
| 698 | |
| 699 | /* |
| @@ -1031,10 +1042,12 @@ | |
| 1042 | 0, capability_union_step, capability_union_finalize); |
| 1043 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1044 | capability_fullcap, 0, 0); |
| 1045 | sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, |
| 1046 | alert_find_emailaddr_func, 0, 0); |
| 1047 | sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, |
| 1048 | alert_display_name_func, 0, 0); |
| 1049 | } |
| 1050 | |
| 1051 | #if USE_SEE |
| 1052 | /* |
| 1053 | ** This is a pointer to the saved database encryption key string. |
| @@ -1258,11 +1271,11 @@ | |
| 1271 | db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1272 | ); |
| 1273 | sqlite3_create_function( |
| 1274 | db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 |
| 1275 | ); |
| 1276 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); |
| 1277 | db_add_aux_functions(db); |
| 1278 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1279 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1280 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1281 | #if USE_SYSTEM_SQLITE+0==1 |
| @@ -1289,17 +1302,17 @@ | |
| 1302 | blob_init(&key, 0, 0); |
| 1303 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1304 | if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ |
| 1305 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1306 | zDbName, zLabel, blob_str(&key)); |
| 1307 | db_exec_sql(zCmd); |
| 1308 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1309 | sqlite3_free(zCmd); |
| 1310 | }else{ |
| 1311 | char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", |
| 1312 | zDbName, zLabel); |
| 1313 | db_exec_sql(zCmd); |
| 1314 | sqlite3_free(zCmd); |
| 1315 | #if USE_SEE |
| 1316 | if( blob_size(&key)>0 ){ |
| 1317 | sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); |
| 1318 | } |
| @@ -1360,11 +1373,11 @@ | |
| 1373 | db_attach(zDbName, zLabel); |
| 1374 | } |
| 1375 | } |
| 1376 | |
| 1377 | /* |
| 1378 | ** Close the per-user configuration database file |
| 1379 | */ |
| 1380 | void db_close_config(){ |
| 1381 | int iSlot = db_database_slot("configdb"); |
| 1382 | if( iSlot>0 ){ |
| 1383 | db_detach("configdb"); |
| @@ -1384,31 +1397,30 @@ | |
| 1397 | fossil_free(g.zConfigDbName); |
| 1398 | g.zConfigDbName = 0; |
| 1399 | } |
| 1400 | |
| 1401 | /* |
| 1402 | ** Compute the name of the configuration database. If unable to find the |
| 1403 | ** database, return 0 if isOptional is true, or panic if isOptional is false. |
| 1404 | ** |
| 1405 | ** Space to hold the result comes from fossil_malloc(). |
| 1406 | */ |
| 1407 | static char *db_configdb_name(int isOptional){ |
| 1408 | char *zHome; /* Home directory */ |
| 1409 | char *zDbName; /* Name of the database file */ |
| 1410 | |
| 1411 | |
| 1412 | /* On Windows, look for these directories, in order: |
| 1413 | ** |
| 1414 | ** FOSSIL_HOME |
| 1415 | ** LOCALAPPDATA |
| 1416 | ** APPDATA |
| 1417 | ** USERPROFILE |
| 1418 | ** HOMEDRIVE HOMEPATH |
| 1419 | */ |
| 1420 | #if defined(_WIN32) || defined(__CYGWIN__) |
| 1421 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1422 | if( zHome==0 ){ |
| 1423 | zHome = fossil_getenv("LOCALAPPDATA"); |
| 1424 | if( zHome==0 ){ |
| 1425 | zHome = fossil_getenv("APPDATA"); |
| 1426 | if( zHome==0 ){ |
| @@ -1419,40 +1431,98 @@ | |
| 1431 | if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); |
| 1432 | } |
| 1433 | } |
| 1434 | } |
| 1435 | } |
| 1436 | zDbName = mprintf("%//_fossil", zHome); |
| 1437 | fossil_free(zHome); |
| 1438 | return zDbName; |
| 1439 | |
| 1440 | #else /* if unix */ |
| 1441 | char *zXdgHome; |
| 1442 | |
| 1443 | /* For unix. a 5-step algorithm is used. |
| 1444 | ** See ../www/tech_overview.wiki for discussion. |
| 1445 | ** |
| 1446 | ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil |
| 1447 | */ |
| 1448 | zHome = fossil_getenv("FOSSIL_HOME"); |
| 1449 | if( zHome!=0 ) return mprintf("%s/.fossil", zHome); |
| 1450 | |
| 1451 | /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil |
| 1452 | */ |
| 1453 | zHome = fossil_getenv("HOME"); |
| 1454 | if( zHome ){ |
| 1455 | zDbName = mprintf("%s/.fossil", zHome); |
| 1456 | if( file_size(zDbName, ExtFILE)>1024*3 ){ |
| 1457 | return zDbName; |
| 1458 | } |
| 1459 | fossil_free(zDbName); |
| 1460 | } |
| 1461 | |
| 1462 | /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db |
| 1463 | */ |
| 1464 | zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); |
| 1465 | if( zXdgHome!=0 ){ |
| 1466 | return mprintf("%s/fossil.db", zXdgHome); |
| 1467 | } |
| 1468 | |
| 1469 | /* The HOME variable is required in order to continue. |
| 1470 | */ |
| 1471 | if( zHome==0 ){ |
| 1472 | if( isOptional ) return 0; |
| 1473 | fossil_panic("cannot locate home directory - please set one of the " |
| 1474 | "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " |
| 1475 | "variables"); |
| 1476 | } |
| 1477 | |
| 1478 | /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db |
| 1479 | */ |
| 1480 | zXdgHome = mprintf("%s/.config", zHome); |
| 1481 | if( file_isdir(zXdgHome, ExtFILE)==1 ){ |
| 1482 | fossil_free(zXdgHome); |
| 1483 | return mprintf("%s/.config/fossil.db", zHome); |
| 1484 | } |
| 1485 | |
| 1486 | /* Step 5: Otherwise -> $HOME/.fossil |
| 1487 | */ |
| 1488 | return mprintf("%s/.fossil", zHome); |
| 1489 | #endif /* unix */ |
| 1490 | } |
| 1491 | |
| 1492 | /* |
| 1493 | ** Open the configuration database. Create the database anew if |
| 1494 | ** it does not already exist. |
| 1495 | ** |
| 1496 | ** If the useAttach flag is 0 (the usual case) then the configuration |
| 1497 | ** database is opened on a separate database connection g.dbConfig. |
| 1498 | ** This prevents the database from becoming locked on long check-in or sync |
| 1499 | ** operations which hold an exclusive transaction. In a few cases, though, |
| 1500 | ** it is convenient for the database to be attached to the main database |
| 1501 | ** connection so that we can join between the various databases. In that |
| 1502 | ** case, invoke this routine with useAttach as 1. |
| 1503 | */ |
| 1504 | int db_open_config(int useAttach, int isOptional){ |
| 1505 | char *zDbName; |
| 1506 | if( g.zConfigDbName ){ |
| 1507 | int alreadyAttached = db_database_slot("configdb")>0; |
| 1508 | if( useAttach==alreadyAttached ) return 1; /* Already open. */ |
| 1509 | db_close_config(); |
| 1510 | } |
| 1511 | zDbName = db_configdb_name(isOptional); |
| 1512 | if( zDbName==0 ) return 0; |
| 1513 | if( file_size(zDbName, ExtFILE)<1024*3 ){ |
| 1514 | char *zHome = file_dirname(zDbName); |
| 1515 | int rc; |
| 1516 | if( file_isdir(zHome, ExtFILE)==0 ){ |
| 1517 | file_mkdir(zHome, ExtFILE, 0); |
| 1518 | } |
| 1519 | rc = file_access(zHome, W_OK); |
| 1520 | fossil_free(zHome); |
| 1521 | if( rc ){ |
| 1522 | if( isOptional ) return 0; |
| 1523 | fossil_panic("home directory \"%s\" must be writeable", zHome); |
| 1524 | } |
| 1525 | db_init_database(zDbName, zConfigSchema, (char*)0); |
| 1526 | } |
| 1527 | if( file_access(zDbName, W_OK) ){ |
| 1528 | if( isOptional ) return 0; |
| @@ -1769,13 +1839,13 @@ | |
| 1839 | "UPDATE vfile" |
| 1840 | " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" |
| 1841 | " WHERE mrid!=rid;" |
| 1842 | ); |
| 1843 | if( !db_table_has_column("localdb", "vmerge", "mhash") ){ |
| 1844 | db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); |
| 1845 | db_exec_sql(zLocalSchemaVmerge); |
| 1846 | db_exec_sql( |
| 1847 | "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" |
| 1848 | " SELECT id, merge, blob.uuid FROM old_vmerge, blob" |
| 1849 | " WHERE old_vmerge.merge=blob.rid;" |
| 1850 | "DROP TABLE old_vmerge;" |
| 1851 | ); |
| @@ -2081,11 +2151,11 @@ | |
| 2151 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2152 | " VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');" |
| 2153 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2154 | " VALUES('nobody','','gjorz','Nobody');" |
| 2155 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2156 | " VALUES('developer','','ei','Dev');" |
| 2157 | "INSERT OR IGNORE INTO user(login,pw,cap,info)" |
| 2158 | " VALUES('reader','','kptw','Reader');" |
| 2159 | ); |
| 2160 | } |
| 2161 | } |
| @@ -2305,11 +2375,11 @@ | |
| 2375 | ** SQL functions for debugging. |
| 2376 | ** |
| 2377 | ** The print() function writes its arguments on stdout, but only |
| 2378 | ** if the -sqlprint command-line option is turned on. |
| 2379 | */ |
| 2380 | void db_sql_print( |
| 2381 | sqlite3_context *context, |
| 2382 | int argc, |
| 2383 | sqlite3_value **argv |
| 2384 | ){ |
| 2385 | int i; |
| @@ -2318,19 +2388,40 @@ | |
| 2388 | char c = i==argc-1 ? '\n' : ' '; |
| 2389 | fossil_print("%s%c", sqlite3_value_text(argv[i]), c); |
| 2390 | } |
| 2391 | } |
| 2392 | } |
| 2393 | |
| 2394 | /* |
| 2395 | ** Callback for sqlite3_trace_v2(); |
| 2396 | */ |
| 2397 | int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ |
| 2398 | sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; |
| 2399 | char *zSql; |
| 2400 | int n; |
| 2401 | const char *zArg = (const char*)pX; |
| 2402 | char zEnd[40]; |
| 2403 | if( m & SQLITE_TRACE_CLOSE ){ |
| 2404 | /* If we are tracking closes, that means we want to clean up static |
| 2405 | ** prepared statements. */ |
| 2406 | while( db.pAllStmt ){ |
| 2407 | db_finalize(db.pAllStmt); |
| 2408 | } |
| 2409 | return 0; |
| 2410 | } |
| 2411 | if( zArg[0]=='-' ) return 0; |
| 2412 | if( m & SQLITE_TRACE_PROFILE ){ |
| 2413 | sqlite3_int64 nNano = *(sqlite3_int64*)pX; |
| 2414 | double rMillisec = 0.000001 * nNano; |
| 2415 | sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms */\n", rMillisec); |
| 2416 | }else{ |
| 2417 | zEnd[0] = '\n'; |
| 2418 | zEnd[1] = 0; |
| 2419 | } |
| 2420 | zSql = sqlite3_expanded_sql(pStmt); |
| 2421 | n = (int)strlen(zSql); |
| 2422 | fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); |
| 2423 | sqlite3_free(zSql); |
| 2424 | return 0; |
| 2425 | } |
| 2426 | |
| 2427 | /* |
| @@ -2492,16 +2583,16 @@ | |
| 2583 | return 0; |
| 2584 | } |
| 2585 | |
| 2586 | /* |
| 2587 | ** Swap the g.db and g.dbConfig connections so that the various db_* routines |
| 2588 | ** work on the configuration database instead of on the repository database. |
| 2589 | ** Be sure to swap them back after doing the operation. |
| 2590 | ** |
| 2591 | ** If the configuration database has already been opened as the main database |
| 2592 | ** or is attached to the main database, no connection swaps are required so |
| 2593 | ** this routine is a no-op. |
| 2594 | */ |
| 2595 | void db_swap_connections(void){ |
| 2596 | /* |
| 2597 | ** When swapping the main database connection with the config database |
| 2598 | ** connection, the config database connection must be open (not simply |
| @@ -3493,10 +3584,14 @@ | |
| 3584 | /* |
| 3585 | ** SETTING: pgp-command width=40 |
| 3586 | ** Command used to clear-sign manifests at check-in. |
| 3587 | ** Default value is "gpg --clearsign -o" |
| 3588 | */ |
| 3589 | /* |
| 3590 | ** SETTING: forbid-delta-manifests boolean default=off |
| 3591 | ** If enabled, new delta manifests are prohibited. |
| 3592 | */ |
| 3593 | /* |
| 3594 | ** SETTING: proxy width=32 default=off |
| 3595 | ** URL of the HTTP proxy. If undefined or "off" then |
| 3596 | ** the "http_proxy" environment variable is consulted. |
| 3597 | ** If the http_proxy environment variable is undefined |
| @@ -3627,10 +3722,30 @@ | |
| 3722 | ** SETTING: th1-uri-regexp width=40 block-text |
| 3723 | ** Specify which URI's are allowed in HTTP requests from |
| 3724 | ** TH1 scripts. If empty, no HTTP requests are allowed |
| 3725 | ** whatsoever. |
| 3726 | */ |
| 3727 | /* |
| 3728 | ** SETTING: default-csp width=40 block-text |
| 3729 | ** |
| 3730 | ** The text of the Content Security Policy that is included |
| 3731 | ** in the Content-Security-Policy: header field of the HTTP |
| 3732 | ** reply and in the default HTML <head> section that is added when the |
| 3733 | ** skin header does not specify a <head> section. The text "$nonce" |
| 3734 | ** is replaced by the random nonce that is created for each web page. |
| 3735 | ** |
| 3736 | ** If this setting is an empty string or is omitted, then |
| 3737 | ** the following default Content Security Policy is used: |
| 3738 | ** |
| 3739 | ** default-src 'self' data:; |
| 3740 | ** script-src 'self' 'nonce-$nonce'; |
| 3741 | ** style-src 'self' 'unsafe-inline'; |
| 3742 | ** |
| 3743 | ** The default CSP is recommended. The main reason to change |
| 3744 | ** this setting would be to add CDNs from which it is safe to |
| 3745 | ** load additional content. |
| 3746 | */ |
| 3747 | /* |
| 3748 | ** SETTING: uv-sync boolean default=off |
| 3749 | ** If true, automatically send unversioned files as part |
| 3750 | ** of a "fossil clone" or "fossil sync" command. The |
| 3751 | ** default is false, in which case the -u option is |
| @@ -3695,14 +3810,13 @@ | |
| 3810 | ** The "unset" command clears a setting. |
| 3811 | ** |
| 3812 | ** Settings can have both a "local" repository-only value and "global" value |
| 3813 | ** that applies to all repositories. The local values are stored in the |
| 3814 | ** "config" table of the repository and the global values are stored in the |
| 3815 | ** configuration database. If both a local and a global value exists for a |
| 3816 | ** setting, the local value takes precedence. This command normally operates |
| 3817 | ** on the local settings. Use the --global option to change global settings. |
| 3818 | ** |
| 3819 | ** Options: |
| 3820 | ** --global set or unset the given property globally instead of |
| 3821 | ** setting or unsetting it for the open repository only. |
| 3822 | ** |
| @@ -3833,12 +3947,12 @@ | |
| 3947 | ** COMMAND: test-without-rowid |
| 3948 | ** |
| 3949 | ** Usage: %fossil test-without-rowid FILENAME... |
| 3950 | ** |
| 3951 | ** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID |
| 3952 | ** optimization. FILENAME can also be the configuration database file |
| 3953 | ** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. |
| 3954 | ** |
| 3955 | ** The purpose of this command is for testing the WITHOUT ROWID capabilities |
| 3956 | ** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. |
| 3957 | ** |
| 3958 | ** Options: |
| 3959 |
+67
| --- src/default_css.txt | ||
| +++ src/default_css.txt | ||
| @@ -741,15 +741,32 @@ | ||
| 741 | 741 | div.forumEdit { |
| 742 | 742 | border: 1px solid black; |
| 743 | 743 | padding-left: 1ex; |
| 744 | 744 | padding-right: 1ex; |
| 745 | 745 | } |
| 746 | +div.forumTimeline { | |
| 747 | + border: 1px solid black; | |
| 748 | + padding-left: 1ex; | |
| 749 | + padding-right: 1ex; | |
| 750 | + max-width: 50em; | |
| 751 | + overflow: auto; | |
| 752 | +} | |
| 753 | +div.forumTimeline code { | |
| 754 | + white-space: pre-wrap; | |
| 755 | +} | |
| 756 | +div.markdown code { | |
| 757 | + white-space: pre-wrap; | |
| 758 | +} | |
| 746 | 759 | div.forumHier, div.forumTime { |
| 747 | 760 | border: 1px solid black; |
| 748 | 761 | padding-left: 1ex; |
| 749 | 762 | padding-right: 1ex; |
| 750 | 763 | margin-top: 1ex; |
| 764 | +} | |
| 765 | +div.forumPostBody { | |
| 766 | + max-height: 40em; | |
| 767 | + overflow: auto; | |
| 751 | 768 | } |
| 752 | 769 | div.forumSel { |
| 753 | 770 | background-color: #cef; |
| 754 | 771 | } |
| 755 | 772 | div.forumObs { |
| @@ -799,5 +816,55 @@ | ||
| 799 | 816 | margin-right: 0; |
| 800 | 817 | } |
| 801 | 818 | .nobr { |
| 802 | 819 | white-space: nowrap; |
| 803 | 820 | } |
| 821 | +.accordion { | |
| 822 | + cursor: pointer; | |
| 823 | +} | |
| 824 | +.accordion_btn { | |
| 825 | + display: inline-block; | |
| 826 | + width: 16px; | |
| 827 | + height: 16px; | |
| 828 | + margin-right: .5em; | |
| 829 | + vertical-align: middle; | |
| 830 | +} | |
| 831 | +// Note: the order of the next 3 entries should be | |
| 832 | +// maintained for the hierarchical cascade to work. | |
| 833 | +.accordion > .accordion_btn_plus { | |
| 834 | + display: none; | |
| 835 | +} | |
| 836 | +.accordion_closed > .accordion_btn_minus { | |
| 837 | + display: none; | |
| 838 | +} | |
| 839 | +.accordion_closed > .accordion_btn_plus { | |
| 840 | + display: inline-block; | |
| 841 | +} | |
| 842 | +.accordion_panel { | |
| 843 | + overflow: hidden; | |
| 844 | + transition: max-height 0.25s ease-out; | |
| 845 | +} | |
| 846 | +#setup_skinedit_css_defaults { | |
| 847 | + max-width: 98%; | |
| 848 | + font-family: monospace; | |
| 849 | +// These are for the UL-based implementation: | |
| 850 | + column-width: auto; | |
| 851 | + column-count: 2; | |
| 852 | + padding-top: 1em; | |
| 853 | +} | |
| 854 | +// These are for the alternate table-based skinedit CSS list: | |
| 855 | +// #setup_skinedit_css_defaults > tbody > tr > td { | |
| 856 | +// font-family: monospace; | |
| 857 | +// white-space: pre-wrap; | |
| 858 | +// border: 1px solid black; | |
| 859 | +// vertical-align: top; | |
| 860 | +// } | |
| 861 | +// #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div { | |
| 862 | +// max-width: 30em; | |
| 863 | +// overflow: auto; | |
| 864 | +// } | |
| 865 | +input { | |
| 866 | + max-width: 95%; | |
| 867 | +} | |
| 868 | +textarea { | |
| 869 | + max-width: 95%; | |
| 870 | +} | |
| 804 | 871 |
| --- src/default_css.txt | |
| +++ src/default_css.txt | |
| @@ -741,15 +741,32 @@ | |
| 741 | div.forumEdit { |
| 742 | border: 1px solid black; |
| 743 | padding-left: 1ex; |
| 744 | padding-right: 1ex; |
| 745 | } |
| 746 | div.forumHier, div.forumTime { |
| 747 | border: 1px solid black; |
| 748 | padding-left: 1ex; |
| 749 | padding-right: 1ex; |
| 750 | margin-top: 1ex; |
| 751 | } |
| 752 | div.forumSel { |
| 753 | background-color: #cef; |
| 754 | } |
| 755 | div.forumObs { |
| @@ -799,5 +816,55 @@ | |
| 799 | margin-right: 0; |
| 800 | } |
| 801 | .nobr { |
| 802 | white-space: nowrap; |
| 803 | } |
| 804 |
| --- src/default_css.txt | |
| +++ src/default_css.txt | |
| @@ -741,15 +741,32 @@ | |
| 741 | div.forumEdit { |
| 742 | border: 1px solid black; |
| 743 | padding-left: 1ex; |
| 744 | padding-right: 1ex; |
| 745 | } |
| 746 | div.forumTimeline { |
| 747 | border: 1px solid black; |
| 748 | padding-left: 1ex; |
| 749 | padding-right: 1ex; |
| 750 | max-width: 50em; |
| 751 | overflow: auto; |
| 752 | } |
| 753 | div.forumTimeline code { |
| 754 | white-space: pre-wrap; |
| 755 | } |
| 756 | div.markdown code { |
| 757 | white-space: pre-wrap; |
| 758 | } |
| 759 | div.forumHier, div.forumTime { |
| 760 | border: 1px solid black; |
| 761 | padding-left: 1ex; |
| 762 | padding-right: 1ex; |
| 763 | margin-top: 1ex; |
| 764 | } |
| 765 | div.forumPostBody { |
| 766 | max-height: 40em; |
| 767 | overflow: auto; |
| 768 | } |
| 769 | div.forumSel { |
| 770 | background-color: #cef; |
| 771 | } |
| 772 | div.forumObs { |
| @@ -799,5 +816,55 @@ | |
| 816 | margin-right: 0; |
| 817 | } |
| 818 | .nobr { |
| 819 | white-space: nowrap; |
| 820 | } |
| 821 | .accordion { |
| 822 | cursor: pointer; |
| 823 | } |
| 824 | .accordion_btn { |
| 825 | display: inline-block; |
| 826 | width: 16px; |
| 827 | height: 16px; |
| 828 | margin-right: .5em; |
| 829 | vertical-align: middle; |
| 830 | } |
| 831 | // Note: the order of the next 3 entries should be |
| 832 | // maintained for the hierarchical cascade to work. |
| 833 | .accordion > .accordion_btn_plus { |
| 834 | display: none; |
| 835 | } |
| 836 | .accordion_closed > .accordion_btn_minus { |
| 837 | display: none; |
| 838 | } |
| 839 | .accordion_closed > .accordion_btn_plus { |
| 840 | display: inline-block; |
| 841 | } |
| 842 | .accordion_panel { |
| 843 | overflow: hidden; |
| 844 | transition: max-height 0.25s ease-out; |
| 845 | } |
| 846 | #setup_skinedit_css_defaults { |
| 847 | max-width: 98%; |
| 848 | font-family: monospace; |
| 849 | // These are for the UL-based implementation: |
| 850 | column-width: auto; |
| 851 | column-count: 2; |
| 852 | padding-top: 1em; |
| 853 | } |
| 854 | // These are for the alternate table-based skinedit CSS list: |
| 855 | // #setup_skinedit_css_defaults > tbody > tr > td { |
| 856 | // font-family: monospace; |
| 857 | // white-space: pre-wrap; |
| 858 | // border: 1px solid black; |
| 859 | // vertical-align: top; |
| 860 | // } |
| 861 | // #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div { |
| 862 | // max-width: 30em; |
| 863 | // overflow: auto; |
| 864 | // } |
| 865 | input { |
| 866 | max-width: 95%; |
| 867 | } |
| 868 | textarea { |
| 869 | max-width: 95%; |
| 870 | } |
| 871 |
+69
-39
| --- src/diff.c | ||
| +++ src/diff.c | ||
| @@ -2,11 +2,11 @@ | ||
| 2 | 2 | ** Copyright (c) 2007 D. Richard Hipp |
| 3 | 3 | ** |
| 4 | 4 | ** This program is free software; you can redistribute it and/or |
| 5 | 5 | ** modify it under the terms of the Simplified BSD License (also |
| 6 | 6 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 7 | - | |
| 7 | +** | |
| 8 | 8 | ** This program is distributed in the hope that it will be useful, |
| 9 | 9 | ** but without any warranty; without even the implied warranty of |
| 10 | 10 | ** merchantability or fitness for a particular purpose. |
| 11 | 11 | ** |
| 12 | 12 | ** Author contact information: |
| @@ -60,13 +60,13 @@ | ||
| 60 | 60 | |
| 61 | 61 | #define DIFF_WHITESPACE_ONLY \ |
| 62 | 62 | "whitespace changes only\n" |
| 63 | 63 | |
| 64 | 64 | /* |
| 65 | -** Maximum length of a line in a text file, in bytes. (2**13 = 8192 bytes) | |
| 65 | +** Maximum length of a line in a text file, in bytes. (2**15 = 32768 bytes) | |
| 66 | 66 | */ |
| 67 | -#define LENGTH_MASK_SZ 13 | |
| 67 | +#define LENGTH_MASK_SZ 15 | |
| 68 | 68 | #define LENGTH_MASK ((1<<LENGTH_MASK_SZ)-1) |
| 69 | 69 | |
| 70 | 70 | #endif /* INTERFACE */ |
| 71 | 71 | |
| 72 | 72 | /* |
| @@ -76,20 +76,20 @@ | ||
| 76 | 76 | ** of the line. If any line is longer than LENGTH_MASK characters, |
| 77 | 77 | ** the file is considered binary. |
| 78 | 78 | */ |
| 79 | 79 | typedef struct DLine DLine; |
| 80 | 80 | struct DLine { |
| 81 | - const char *z; /* The text of the line */ | |
| 82 | - unsigned int h; /* Hash of the line */ | |
| 81 | + const char *z; /* The text of the line */ | |
| 82 | + u64 h; /* Hash of the line */ | |
| 83 | 83 | unsigned short indent; /* Indent of the line. Only !=0 with -w/-Z option */ |
| 84 | - unsigned short n; /* number of bytes */ | |
| 85 | - unsigned int iNext; /* 1+(Index of next line with same the same hash) */ | |
| 84 | + unsigned short n; /* number of bytes */ | |
| 85 | + unsigned int iNext; /* 1+(Index of next line with same the same hash) */ | |
| 86 | 86 | |
| 87 | 87 | /* an array of DLine elements serves two purposes. The fields |
| 88 | 88 | ** above are one per line of input text. But each entry is also |
| 89 | 89 | ** a bucket in a hash table, as follows: */ |
| 90 | - unsigned int iHash; /* 1+(first entry in the hash chain) */ | |
| 90 | + unsigned int iHash; /* 1+(first entry in the hash chain) */ | |
| 91 | 91 | }; |
| 92 | 92 | |
| 93 | 93 | /* |
| 94 | 94 | ** Length of a dline |
| 95 | 95 | */ |
| @@ -115,11 +115,11 @@ | ||
| 115 | 115 | int nEditAlloc; /* Space allocated for aEdit[] */ |
| 116 | 116 | DLine *aFrom; /* File on left side of the diff */ |
| 117 | 117 | int nFrom; /* Number of lines in aFrom[] */ |
| 118 | 118 | DLine *aTo; /* File on right side of the diff */ |
| 119 | 119 | int nTo; /* Number of lines in aTo[] */ |
| 120 | - int (*same_fn)(const DLine*,const DLine*); /* comparison function */ | |
| 120 | + int (*xDiffer)(const DLine*,const DLine*); /* comparison function */ | |
| 121 | 121 | }; |
| 122 | 122 | |
| 123 | 123 | /* |
| 124 | 124 | ** Count the number of lines in the input string. Include the last line |
| 125 | 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| @@ -164,11 +164,11 @@ | ||
| 164 | 164 | int n, |
| 165 | 165 | int *pnLine, |
| 166 | 166 | u64 diffFlags |
| 167 | 167 | ){ |
| 168 | 168 | int nLine, i, k, nn, s, x; |
| 169 | - unsigned int h, h2; | |
| 169 | + u64 h, h2; | |
| 170 | 170 | DLine *a; |
| 171 | 171 | const char *zNL; |
| 172 | 172 | |
| 173 | 173 | if( count_lines(z, n, &nLine)==0 ){ |
| 174 | 174 | return 0; |
| @@ -205,23 +205,27 @@ | ||
| 205 | 205 | for(h=0, x=s; x<k; x++){ |
| 206 | 206 | char c = z[x]; |
| 207 | 207 | if( fossil_isspace(c) ){ |
| 208 | 208 | ++numws; |
| 209 | 209 | }else{ |
| 210 | - h += c; | |
| 211 | - h *= 0x9e3779b1; | |
| 210 | + h = (h^c)*9000000000000000041LL; | |
| 212 | 211 | } |
| 213 | 212 | } |
| 214 | 213 | k -= numws; |
| 215 | 214 | }else{ |
| 216 | - for(h=0, x=s; x<k; x++){ | |
| 217 | - h += z[x]; | |
| 218 | - h *= 0x9e3779b1; | |
| 215 | + int k2 = k & ~0x7; | |
| 216 | + u64 m; | |
| 217 | + for(h=0, x=s; x<k2; x += 8){ | |
| 218 | + memcpy(&m, z+x, 8); | |
| 219 | + h = (h^m)*9000000000000000041LL; | |
| 219 | 220 | } |
| 221 | + m = 0; | |
| 222 | + memcpy(&m, z+x, k-k2); | |
| 223 | + h ^= m; | |
| 220 | 224 | } |
| 221 | 225 | a[i].indent = s; |
| 222 | - a[i].h = h = (h<<LENGTH_MASK_SZ) | (k-s); | |
| 226 | + a[i].h = h = ((h%281474976710597LL)<<LENGTH_MASK_SZ) | (k-s); | |
| 223 | 227 | h2 = h % nLine; |
| 224 | 228 | a[i].iNext = a[h2].iHash; |
| 225 | 229 | a[h2].iHash = i+1; |
| 226 | 230 | z += nn+1; n -= nn+1; |
| 227 | 231 | i++; |
| @@ -232,33 +236,34 @@ | ||
| 232 | 236 | *pnLine = nLine; |
| 233 | 237 | return a; |
| 234 | 238 | } |
| 235 | 239 | |
| 236 | 240 | /* |
| 237 | -** Return true if two DLine elements are identical. | |
| 241 | +** Return zero if two DLine elements are identical. | |
| 238 | 242 | */ |
| 239 | 243 | static int same_dline(const DLine *pA, const DLine *pB){ |
| 240 | - return pA->h==pB->h && memcmp(pA->z,pB->z, pA->h&LENGTH_MASK)==0; | |
| 244 | + if( pA->h!=pB->h ) return 1; | |
| 245 | + return memcmp(pA->z,pB->z, pA->h&LENGTH_MASK); | |
| 241 | 246 | } |
| 242 | 247 | |
| 243 | 248 | /* |
| 244 | -** Return true if two DLine elements are identical, ignoring | |
| 249 | +** Return zero if two DLine elements are identical, ignoring | |
| 245 | 250 | ** all whitespace. The indent field of pA/pB already points |
| 246 | 251 | ** to the first non-space character in the string. |
| 247 | 252 | */ |
| 248 | 253 | |
| 249 | 254 | static int same_dline_ignore_allws(const DLine *pA, const DLine *pB){ |
| 250 | 255 | int a = pA->indent, b = pB->indent; |
| 251 | 256 | if( pA->h==pB->h ){ |
| 252 | 257 | while( a<pA->n || b<pB->n ){ |
| 253 | - if( a<pA->n && b<pB->n && pA->z[a++] != pB->z[b++] ) return 0; | |
| 258 | + if( a<pA->n && b<pB->n && pA->z[a++] != pB->z[b++] ) return 1; | |
| 254 | 259 | while( a<pA->n && fossil_isspace(pA->z[a])) ++a; |
| 255 | 260 | while( b<pB->n && fossil_isspace(pB->z[b])) ++b; |
| 256 | 261 | } |
| 257 | - return pA->n-a == pB->n-b; | |
| 262 | + return pA->n-a != pB->n-b; | |
| 258 | 263 | } |
| 259 | - return 0; | |
| 264 | + return 1; | |
| 260 | 265 | } |
| 261 | 266 | |
| 262 | 267 | /* |
| 263 | 268 | ** Return true if the regular expression *pRe matches any of the |
| 264 | 269 | ** N dlines |
| @@ -1444,16 +1449,16 @@ | ||
| 1444 | 1449 | int iSXb = iS1; /* Best match so far */ |
| 1445 | 1450 | int iSYb = iS2; /* Best match so far */ |
| 1446 | 1451 | |
| 1447 | 1452 | for(i=iS1; i<iE1-mxLength; i++){ |
| 1448 | 1453 | for(j=iS2; j<iE2-mxLength; j++){ |
| 1449 | - if( !p->same_fn(&p->aFrom[i], &p->aTo[j]) ) continue; | |
| 1450 | - if( mxLength && !p->same_fn(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){ | |
| 1454 | + if( p->xDiffer(&p->aFrom[i], &p->aTo[j]) ) continue; | |
| 1455 | + if( mxLength && p->xDiffer(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){ | |
| 1451 | 1456 | continue; |
| 1452 | 1457 | } |
| 1453 | 1458 | k = 1; |
| 1454 | - while( i+k<iE1 && j+k<iE2 && p->same_fn(&p->aFrom[i+k],&p->aTo[j+k]) ){ | |
| 1459 | + while( i+k<iE1 && j+k<iE2 && p->xDiffer(&p->aFrom[i+k],&p->aTo[j+k])==0 ){ | |
| 1455 | 1460 | k++; |
| 1456 | 1461 | } |
| 1457 | 1462 | if( k>mxLength ){ |
| 1458 | 1463 | iSXb = i; |
| 1459 | 1464 | iSYb = j; |
| @@ -1515,11 +1520,11 @@ | ||
| 1515 | 1520 | mid = (iE1 + iS1)/2; |
| 1516 | 1521 | for(i=iS1; i<iE1; i++){ |
| 1517 | 1522 | int limit = 0; |
| 1518 | 1523 | j = p->aTo[p->aFrom[i].h % p->nTo].iHash; |
| 1519 | 1524 | while( j>0 |
| 1520 | - && (j-1<iS2 || j>=iE2 || !p->same_fn(&p->aFrom[i], &p->aTo[j-1])) | |
| 1525 | + && (j-1<iS2 || j>=iE2 || p->xDiffer(&p->aFrom[i], &p->aTo[j-1])) | |
| 1521 | 1526 | ){ |
| 1522 | 1527 | if( limit++ > 10 ){ |
| 1523 | 1528 | j = 0; |
| 1524 | 1529 | break; |
| 1525 | 1530 | } |
| @@ -1532,19 +1537,19 @@ | ||
| 1532 | 1537 | iSX = i; |
| 1533 | 1538 | iSY = j-1; |
| 1534 | 1539 | pA = &p->aFrom[iSX-1]; |
| 1535 | 1540 | pB = &p->aTo[iSY-1]; |
| 1536 | 1541 | n = minInt(iSX-iS1, iSY-iS2); |
| 1537 | - for(k=0; k<n && p->same_fn(pA,pB); k++, pA--, pB--){} | |
| 1542 | + for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA--, pB--){} | |
| 1538 | 1543 | iSX -= k; |
| 1539 | 1544 | iSY -= k; |
| 1540 | 1545 | iEX = i+1; |
| 1541 | 1546 | iEY = j; |
| 1542 | 1547 | pA = &p->aFrom[iEX]; |
| 1543 | 1548 | pB = &p->aTo[iEY]; |
| 1544 | 1549 | n = minInt(iE1-iEX, iE2-iEY); |
| 1545 | - for(k=0; k<n && p->same_fn(pA,pB); k++, pA++, pB++){} | |
| 1550 | + for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA++, pB++){} | |
| 1546 | 1551 | iEX += k; |
| 1547 | 1552 | iEY += k; |
| 1548 | 1553 | skew = (iSX-iS1) - (iSY-iS2); |
| 1549 | 1554 | if( skew<0 ) skew = -skew; |
| 1550 | 1555 | dist = (iSX+iEX)/2 - mid; |
| @@ -1681,16 +1686,16 @@ | ||
| 1681 | 1686 | int mnE, iS, iE1, iE2; |
| 1682 | 1687 | |
| 1683 | 1688 | /* Carve off the common header and footer */ |
| 1684 | 1689 | iE1 = p->nFrom; |
| 1685 | 1690 | iE2 = p->nTo; |
| 1686 | - while( iE1>0 && iE2>0 && p->same_fn(&p->aFrom[iE1-1], &p->aTo[iE2-1]) ){ | |
| 1691 | + while( iE1>0 && iE2>0 && p->xDiffer(&p->aFrom[iE1-1], &p->aTo[iE2-1])==0 ){ | |
| 1687 | 1692 | iE1--; |
| 1688 | 1693 | iE2--; |
| 1689 | 1694 | } |
| 1690 | 1695 | mnE = iE1<iE2 ? iE1 : iE2; |
| 1691 | - for(iS=0; iS<mnE && p->same_fn(&p->aFrom[iS],&p->aTo[iS]); iS++){} | |
| 1696 | + for(iS=0; iS<mnE && p->xDiffer(&p->aFrom[iS],&p->aTo[iS])==0; iS++){} | |
| 1692 | 1697 | |
| 1693 | 1698 | /* do the difference */ |
| 1694 | 1699 | if( iS>0 ){ |
| 1695 | 1700 | appendTriple(p, iS, 0, 0); |
| 1696 | 1701 | } |
| @@ -1755,11 +1760,11 @@ | ||
| 1755 | 1760 | |
| 1756 | 1761 | /* Shift insertions toward the beginning of the file */ |
| 1757 | 1762 | while( cpy>0 && del==0 && ins>0 ){ |
| 1758 | 1763 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of insert */ |
| 1759 | 1764 | DLine *pBtm = &p->aTo[lnTo+ins-1]; /* Last line inserted */ |
| 1760 | - if( p->same_fn(pTop, pBtm)==0 ) break; | |
| 1765 | + if( p->xDiffer(pTop, pBtm) ) break; | |
| 1761 | 1766 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1762 | 1767 | lnFrom--; |
| 1763 | 1768 | lnTo--; |
| 1764 | 1769 | p->aEdit[r]--; |
| 1765 | 1770 | p->aEdit[r+3]++; |
| @@ -1768,11 +1773,11 @@ | ||
| 1768 | 1773 | |
| 1769 | 1774 | /* Shift insertions toward the end of the file */ |
| 1770 | 1775 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){ |
| 1771 | 1776 | DLine *pTop = &p->aTo[lnTo]; /* First line inserted */ |
| 1772 | 1777 | DLine *pBtm = &p->aTo[lnTo+ins]; /* First line past end of insert */ |
| 1773 | - if( p->same_fn(pTop, pBtm)==0 ) break; | |
| 1778 | + if( p->xDiffer(pTop, pBtm) ) break; | |
| 1774 | 1779 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break; |
| 1775 | 1780 | lnFrom++; |
| 1776 | 1781 | lnTo++; |
| 1777 | 1782 | p->aEdit[r]++; |
| 1778 | 1783 | p->aEdit[r+3]--; |
| @@ -1781,11 +1786,11 @@ | ||
| 1781 | 1786 | |
| 1782 | 1787 | /* Shift deletions toward the beginning of the file */ |
| 1783 | 1788 | while( cpy>0 && del>0 && ins==0 ){ |
| 1784 | 1789 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of delete */ |
| 1785 | 1790 | DLine *pBtm = &p->aFrom[lnFrom+del-1]; /* Last line deleted */ |
| 1786 | - if( p->same_fn(pTop, pBtm)==0 ) break; | |
| 1791 | + if( p->xDiffer(pTop, pBtm) ) break; | |
| 1787 | 1792 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1788 | 1793 | lnFrom--; |
| 1789 | 1794 | lnTo--; |
| 1790 | 1795 | p->aEdit[r]--; |
| 1791 | 1796 | p->aEdit[r+3]++; |
| @@ -1794,11 +1799,11 @@ | ||
| 1794 | 1799 | |
| 1795 | 1800 | /* Shift deletions toward the end of the file */ |
| 1796 | 1801 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){ |
| 1797 | 1802 | DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */ |
| 1798 | 1803 | DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */ |
| 1799 | - if( p->same_fn(pTop, pBtm)==0 ) break; | |
| 1804 | + if( p->xDiffer(pTop, pBtm) ) break; | |
| 1800 | 1805 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break; |
| 1801 | 1806 | lnFrom++; |
| 1802 | 1807 | lnTo++; |
| 1803 | 1808 | p->aEdit[r]++; |
| 1804 | 1809 | p->aEdit[r+3]--; |
| @@ -1821,14 +1826,39 @@ | ||
| 1821 | 1826 | } |
| 1822 | 1827 | |
| 1823 | 1828 | /* |
| 1824 | 1829 | ** Extract the width of columns for side-by-side diff. Supply an |
| 1825 | 1830 | ** appropriate default if no width is given. |
| 1831 | +** | |
| 1832 | +** Calculate the default automatically, based on terminal's current width: | |
| 1833 | +** term-width = 2*diff-col + diff-marker + 1 | |
| 1834 | +** diff-col = lineno + lmargin + text-width + rmargin | |
| 1835 | +** | |
| 1836 | +** text-width = (term-width - diff-marker - 1)/2 - lineno - lmargin - rmargin | |
| 1826 | 1837 | */ |
| 1827 | 1838 | int diff_width(u64 diffFlags){ |
| 1828 | 1839 | int w = (diffFlags & DIFF_WIDTH_MASK)/(DIFF_CONTEXT_MASK+1); |
| 1829 | - if( w==0 ) w = 80; | |
| 1840 | + if( w==0 ){ | |
| 1841 | + static struct { | |
| 1842 | + unsigned int lineno, lmargin, text, rmargin, marker; | |
| 1843 | + } sbsW = { 5, 2, 0, 0, 3 }; | |
| 1844 | + const unsigned int wMin = 24, wMax = 132; | |
| 1845 | + unsigned int tw = terminal_get_width(80); | |
| 1846 | + unsigned int twMin = | |
| 1847 | + (wMin + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; | |
| 1848 | + unsigned int twMax = | |
| 1849 | + (wMax + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; | |
| 1850 | + | |
| 1851 | + if( tw<twMin ){ | |
| 1852 | + tw = twMin; | |
| 1853 | + }else if( tw>twMax ){ | |
| 1854 | + tw = twMax; | |
| 1855 | + } | |
| 1856 | + sbsW.text = | |
| 1857 | + (tw - sbsW.marker - 1)/2 - sbsW.lineno - sbsW.lmargin - sbsW.rmargin; | |
| 1858 | + w = sbsW.text; | |
| 1859 | + } | |
| 1830 | 1860 | return w; |
| 1831 | 1861 | } |
| 1832 | 1862 | |
| 1833 | 1863 | /* |
| 1834 | 1864 | ** Append the error message to pOut. |
| @@ -1875,13 +1905,13 @@ | ||
| 1875 | 1905 | blob_to_utf8_no_bom(pB_Blob, 0); |
| 1876 | 1906 | |
| 1877 | 1907 | /* Prepare the input files */ |
| 1878 | 1908 | memset(&c, 0, sizeof(c)); |
| 1879 | 1909 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 1880 | - c.same_fn = same_dline_ignore_allws; | |
| 1910 | + c.xDiffer = same_dline_ignore_allws; | |
| 1881 | 1911 | }else{ |
| 1882 | - c.same_fn = same_dline; | |
| 1912 | + c.xDiffer = same_dline; | |
| 1883 | 1913 | } |
| 1884 | 1914 | c.aFrom = break_into_lines(blob_str(pA_Blob), blob_size(pA_Blob), |
| 1885 | 1915 | &c.nFrom, diffFlags); |
| 1886 | 1916 | c.aTo = break_into_lines(blob_str(pB_Blob), blob_size(pB_Blob), |
| 1887 | 1917 | &c.nTo, diffFlags); |
| @@ -2107,13 +2137,13 @@ | ||
| 2107 | 2137 | static int annotation_start(Annotator *p, Blob *pInput, u64 diffFlags){ |
| 2108 | 2138 | int i; |
| 2109 | 2139 | |
| 2110 | 2140 | memset(p, 0, sizeof(*p)); |
| 2111 | 2141 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 2112 | - p->c.same_fn = same_dline_ignore_allws; | |
| 2142 | + p->c.xDiffer = same_dline_ignore_allws; | |
| 2113 | 2143 | }else{ |
| 2114 | - p->c.same_fn = same_dline; | |
| 2144 | + p->c.xDiffer = same_dline; | |
| 2115 | 2145 | } |
| 2116 | 2146 | p->c.aTo = break_into_lines(blob_str(pInput), blob_size(pInput),&p->c.nTo, |
| 2117 | 2147 | diffFlags); |
| 2118 | 2148 | if( p->c.aTo==0 ){ |
| 2119 | 2149 | return 1; |
| 2120 | 2150 |
| --- src/diff.c | |
| +++ src/diff.c | |
| @@ -2,11 +2,11 @@ | |
| 2 | ** Copyright (c) 2007 D. Richard Hipp |
| 3 | ** |
| 4 | ** This program is free software; you can redistribute it and/or |
| 5 | ** modify it under the terms of the Simplified BSD License (also |
| 6 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 7 | |
| 8 | ** This program is distributed in the hope that it will be useful, |
| 9 | ** but without any warranty; without even the implied warranty of |
| 10 | ** merchantability or fitness for a particular purpose. |
| 11 | ** |
| 12 | ** Author contact information: |
| @@ -60,13 +60,13 @@ | |
| 60 | |
| 61 | #define DIFF_WHITESPACE_ONLY \ |
| 62 | "whitespace changes only\n" |
| 63 | |
| 64 | /* |
| 65 | ** Maximum length of a line in a text file, in bytes. (2**13 = 8192 bytes) |
| 66 | */ |
| 67 | #define LENGTH_MASK_SZ 13 |
| 68 | #define LENGTH_MASK ((1<<LENGTH_MASK_SZ)-1) |
| 69 | |
| 70 | #endif /* INTERFACE */ |
| 71 | |
| 72 | /* |
| @@ -76,20 +76,20 @@ | |
| 76 | ** of the line. If any line is longer than LENGTH_MASK characters, |
| 77 | ** the file is considered binary. |
| 78 | */ |
| 79 | typedef struct DLine DLine; |
| 80 | struct DLine { |
| 81 | const char *z; /* The text of the line */ |
| 82 | unsigned int h; /* Hash of the line */ |
| 83 | unsigned short indent; /* Indent of the line. Only !=0 with -w/-Z option */ |
| 84 | unsigned short n; /* number of bytes */ |
| 85 | unsigned int iNext; /* 1+(Index of next line with same the same hash) */ |
| 86 | |
| 87 | /* an array of DLine elements serves two purposes. The fields |
| 88 | ** above are one per line of input text. But each entry is also |
| 89 | ** a bucket in a hash table, as follows: */ |
| 90 | unsigned int iHash; /* 1+(first entry in the hash chain) */ |
| 91 | }; |
| 92 | |
| 93 | /* |
| 94 | ** Length of a dline |
| 95 | */ |
| @@ -115,11 +115,11 @@ | |
| 115 | int nEditAlloc; /* Space allocated for aEdit[] */ |
| 116 | DLine *aFrom; /* File on left side of the diff */ |
| 117 | int nFrom; /* Number of lines in aFrom[] */ |
| 118 | DLine *aTo; /* File on right side of the diff */ |
| 119 | int nTo; /* Number of lines in aTo[] */ |
| 120 | int (*same_fn)(const DLine*,const DLine*); /* comparison function */ |
| 121 | }; |
| 122 | |
| 123 | /* |
| 124 | ** Count the number of lines in the input string. Include the last line |
| 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| @@ -164,11 +164,11 @@ | |
| 164 | int n, |
| 165 | int *pnLine, |
| 166 | u64 diffFlags |
| 167 | ){ |
| 168 | int nLine, i, k, nn, s, x; |
| 169 | unsigned int h, h2; |
| 170 | DLine *a; |
| 171 | const char *zNL; |
| 172 | |
| 173 | if( count_lines(z, n, &nLine)==0 ){ |
| 174 | return 0; |
| @@ -205,23 +205,27 @@ | |
| 205 | for(h=0, x=s; x<k; x++){ |
| 206 | char c = z[x]; |
| 207 | if( fossil_isspace(c) ){ |
| 208 | ++numws; |
| 209 | }else{ |
| 210 | h += c; |
| 211 | h *= 0x9e3779b1; |
| 212 | } |
| 213 | } |
| 214 | k -= numws; |
| 215 | }else{ |
| 216 | for(h=0, x=s; x<k; x++){ |
| 217 | h += z[x]; |
| 218 | h *= 0x9e3779b1; |
| 219 | } |
| 220 | } |
| 221 | a[i].indent = s; |
| 222 | a[i].h = h = (h<<LENGTH_MASK_SZ) | (k-s); |
| 223 | h2 = h % nLine; |
| 224 | a[i].iNext = a[h2].iHash; |
| 225 | a[h2].iHash = i+1; |
| 226 | z += nn+1; n -= nn+1; |
| 227 | i++; |
| @@ -232,33 +236,34 @@ | |
| 232 | *pnLine = nLine; |
| 233 | return a; |
| 234 | } |
| 235 | |
| 236 | /* |
| 237 | ** Return true if two DLine elements are identical. |
| 238 | */ |
| 239 | static int same_dline(const DLine *pA, const DLine *pB){ |
| 240 | return pA->h==pB->h && memcmp(pA->z,pB->z, pA->h&LENGTH_MASK)==0; |
| 241 | } |
| 242 | |
| 243 | /* |
| 244 | ** Return true if two DLine elements are identical, ignoring |
| 245 | ** all whitespace. The indent field of pA/pB already points |
| 246 | ** to the first non-space character in the string. |
| 247 | */ |
| 248 | |
| 249 | static int same_dline_ignore_allws(const DLine *pA, const DLine *pB){ |
| 250 | int a = pA->indent, b = pB->indent; |
| 251 | if( pA->h==pB->h ){ |
| 252 | while( a<pA->n || b<pB->n ){ |
| 253 | if( a<pA->n && b<pB->n && pA->z[a++] != pB->z[b++] ) return 0; |
| 254 | while( a<pA->n && fossil_isspace(pA->z[a])) ++a; |
| 255 | while( b<pB->n && fossil_isspace(pB->z[b])) ++b; |
| 256 | } |
| 257 | return pA->n-a == pB->n-b; |
| 258 | } |
| 259 | return 0; |
| 260 | } |
| 261 | |
| 262 | /* |
| 263 | ** Return true if the regular expression *pRe matches any of the |
| 264 | ** N dlines |
| @@ -1444,16 +1449,16 @@ | |
| 1444 | int iSXb = iS1; /* Best match so far */ |
| 1445 | int iSYb = iS2; /* Best match so far */ |
| 1446 | |
| 1447 | for(i=iS1; i<iE1-mxLength; i++){ |
| 1448 | for(j=iS2; j<iE2-mxLength; j++){ |
| 1449 | if( !p->same_fn(&p->aFrom[i], &p->aTo[j]) ) continue; |
| 1450 | if( mxLength && !p->same_fn(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){ |
| 1451 | continue; |
| 1452 | } |
| 1453 | k = 1; |
| 1454 | while( i+k<iE1 && j+k<iE2 && p->same_fn(&p->aFrom[i+k],&p->aTo[j+k]) ){ |
| 1455 | k++; |
| 1456 | } |
| 1457 | if( k>mxLength ){ |
| 1458 | iSXb = i; |
| 1459 | iSYb = j; |
| @@ -1515,11 +1520,11 @@ | |
| 1515 | mid = (iE1 + iS1)/2; |
| 1516 | for(i=iS1; i<iE1; i++){ |
| 1517 | int limit = 0; |
| 1518 | j = p->aTo[p->aFrom[i].h % p->nTo].iHash; |
| 1519 | while( j>0 |
| 1520 | && (j-1<iS2 || j>=iE2 || !p->same_fn(&p->aFrom[i], &p->aTo[j-1])) |
| 1521 | ){ |
| 1522 | if( limit++ > 10 ){ |
| 1523 | j = 0; |
| 1524 | break; |
| 1525 | } |
| @@ -1532,19 +1537,19 @@ | |
| 1532 | iSX = i; |
| 1533 | iSY = j-1; |
| 1534 | pA = &p->aFrom[iSX-1]; |
| 1535 | pB = &p->aTo[iSY-1]; |
| 1536 | n = minInt(iSX-iS1, iSY-iS2); |
| 1537 | for(k=0; k<n && p->same_fn(pA,pB); k++, pA--, pB--){} |
| 1538 | iSX -= k; |
| 1539 | iSY -= k; |
| 1540 | iEX = i+1; |
| 1541 | iEY = j; |
| 1542 | pA = &p->aFrom[iEX]; |
| 1543 | pB = &p->aTo[iEY]; |
| 1544 | n = minInt(iE1-iEX, iE2-iEY); |
| 1545 | for(k=0; k<n && p->same_fn(pA,pB); k++, pA++, pB++){} |
| 1546 | iEX += k; |
| 1547 | iEY += k; |
| 1548 | skew = (iSX-iS1) - (iSY-iS2); |
| 1549 | if( skew<0 ) skew = -skew; |
| 1550 | dist = (iSX+iEX)/2 - mid; |
| @@ -1681,16 +1686,16 @@ | |
| 1681 | int mnE, iS, iE1, iE2; |
| 1682 | |
| 1683 | /* Carve off the common header and footer */ |
| 1684 | iE1 = p->nFrom; |
| 1685 | iE2 = p->nTo; |
| 1686 | while( iE1>0 && iE2>0 && p->same_fn(&p->aFrom[iE1-1], &p->aTo[iE2-1]) ){ |
| 1687 | iE1--; |
| 1688 | iE2--; |
| 1689 | } |
| 1690 | mnE = iE1<iE2 ? iE1 : iE2; |
| 1691 | for(iS=0; iS<mnE && p->same_fn(&p->aFrom[iS],&p->aTo[iS]); iS++){} |
| 1692 | |
| 1693 | /* do the difference */ |
| 1694 | if( iS>0 ){ |
| 1695 | appendTriple(p, iS, 0, 0); |
| 1696 | } |
| @@ -1755,11 +1760,11 @@ | |
| 1755 | |
| 1756 | /* Shift insertions toward the beginning of the file */ |
| 1757 | while( cpy>0 && del==0 && ins>0 ){ |
| 1758 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of insert */ |
| 1759 | DLine *pBtm = &p->aTo[lnTo+ins-1]; /* Last line inserted */ |
| 1760 | if( p->same_fn(pTop, pBtm)==0 ) break; |
| 1761 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1762 | lnFrom--; |
| 1763 | lnTo--; |
| 1764 | p->aEdit[r]--; |
| 1765 | p->aEdit[r+3]++; |
| @@ -1768,11 +1773,11 @@ | |
| 1768 | |
| 1769 | /* Shift insertions toward the end of the file */ |
| 1770 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){ |
| 1771 | DLine *pTop = &p->aTo[lnTo]; /* First line inserted */ |
| 1772 | DLine *pBtm = &p->aTo[lnTo+ins]; /* First line past end of insert */ |
| 1773 | if( p->same_fn(pTop, pBtm)==0 ) break; |
| 1774 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break; |
| 1775 | lnFrom++; |
| 1776 | lnTo++; |
| 1777 | p->aEdit[r]++; |
| 1778 | p->aEdit[r+3]--; |
| @@ -1781,11 +1786,11 @@ | |
| 1781 | |
| 1782 | /* Shift deletions toward the beginning of the file */ |
| 1783 | while( cpy>0 && del>0 && ins==0 ){ |
| 1784 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of delete */ |
| 1785 | DLine *pBtm = &p->aFrom[lnFrom+del-1]; /* Last line deleted */ |
| 1786 | if( p->same_fn(pTop, pBtm)==0 ) break; |
| 1787 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1788 | lnFrom--; |
| 1789 | lnTo--; |
| 1790 | p->aEdit[r]--; |
| 1791 | p->aEdit[r+3]++; |
| @@ -1794,11 +1799,11 @@ | |
| 1794 | |
| 1795 | /* Shift deletions toward the end of the file */ |
| 1796 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){ |
| 1797 | DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */ |
| 1798 | DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */ |
| 1799 | if( p->same_fn(pTop, pBtm)==0 ) break; |
| 1800 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break; |
| 1801 | lnFrom++; |
| 1802 | lnTo++; |
| 1803 | p->aEdit[r]++; |
| 1804 | p->aEdit[r+3]--; |
| @@ -1821,14 +1826,39 @@ | |
| 1821 | } |
| 1822 | |
| 1823 | /* |
| 1824 | ** Extract the width of columns for side-by-side diff. Supply an |
| 1825 | ** appropriate default if no width is given. |
| 1826 | */ |
| 1827 | int diff_width(u64 diffFlags){ |
| 1828 | int w = (diffFlags & DIFF_WIDTH_MASK)/(DIFF_CONTEXT_MASK+1); |
| 1829 | if( w==0 ) w = 80; |
| 1830 | return w; |
| 1831 | } |
| 1832 | |
| 1833 | /* |
| 1834 | ** Append the error message to pOut. |
| @@ -1875,13 +1905,13 @@ | |
| 1875 | blob_to_utf8_no_bom(pB_Blob, 0); |
| 1876 | |
| 1877 | /* Prepare the input files */ |
| 1878 | memset(&c, 0, sizeof(c)); |
| 1879 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 1880 | c.same_fn = same_dline_ignore_allws; |
| 1881 | }else{ |
| 1882 | c.same_fn = same_dline; |
| 1883 | } |
| 1884 | c.aFrom = break_into_lines(blob_str(pA_Blob), blob_size(pA_Blob), |
| 1885 | &c.nFrom, diffFlags); |
| 1886 | c.aTo = break_into_lines(blob_str(pB_Blob), blob_size(pB_Blob), |
| 1887 | &c.nTo, diffFlags); |
| @@ -2107,13 +2137,13 @@ | |
| 2107 | static int annotation_start(Annotator *p, Blob *pInput, u64 diffFlags){ |
| 2108 | int i; |
| 2109 | |
| 2110 | memset(p, 0, sizeof(*p)); |
| 2111 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 2112 | p->c.same_fn = same_dline_ignore_allws; |
| 2113 | }else{ |
| 2114 | p->c.same_fn = same_dline; |
| 2115 | } |
| 2116 | p->c.aTo = break_into_lines(blob_str(pInput), blob_size(pInput),&p->c.nTo, |
| 2117 | diffFlags); |
| 2118 | if( p->c.aTo==0 ){ |
| 2119 | return 1; |
| 2120 |
| --- src/diff.c | |
| +++ src/diff.c | |
| @@ -2,11 +2,11 @@ | |
| 2 | ** Copyright (c) 2007 D. Richard Hipp |
| 3 | ** |
| 4 | ** This program is free software; you can redistribute it and/or |
| 5 | ** modify it under the terms of the Simplified BSD License (also |
| 6 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 7 | ** |
| 8 | ** This program is distributed in the hope that it will be useful, |
| 9 | ** but without any warranty; without even the implied warranty of |
| 10 | ** merchantability or fitness for a particular purpose. |
| 11 | ** |
| 12 | ** Author contact information: |
| @@ -60,13 +60,13 @@ | |
| 60 | |
| 61 | #define DIFF_WHITESPACE_ONLY \ |
| 62 | "whitespace changes only\n" |
| 63 | |
| 64 | /* |
| 65 | ** Maximum length of a line in a text file, in bytes. (2**15 = 32768 bytes) |
| 66 | */ |
| 67 | #define LENGTH_MASK_SZ 15 |
| 68 | #define LENGTH_MASK ((1<<LENGTH_MASK_SZ)-1) |
| 69 | |
| 70 | #endif /* INTERFACE */ |
| 71 | |
| 72 | /* |
| @@ -76,20 +76,20 @@ | |
| 76 | ** of the line. If any line is longer than LENGTH_MASK characters, |
| 77 | ** the file is considered binary. |
| 78 | */ |
| 79 | typedef struct DLine DLine; |
| 80 | struct DLine { |
| 81 | const char *z; /* The text of the line */ |
| 82 | u64 h; /* Hash of the line */ |
| 83 | unsigned short indent; /* Indent of the line. Only !=0 with -w/-Z option */ |
| 84 | unsigned short n; /* number of bytes */ |
| 85 | unsigned int iNext; /* 1+(Index of next line with same the same hash) */ |
| 86 | |
| 87 | /* an array of DLine elements serves two purposes. The fields |
| 88 | ** above are one per line of input text. But each entry is also |
| 89 | ** a bucket in a hash table, as follows: */ |
| 90 | unsigned int iHash; /* 1+(first entry in the hash chain) */ |
| 91 | }; |
| 92 | |
| 93 | /* |
| 94 | ** Length of a dline |
| 95 | */ |
| @@ -115,11 +115,11 @@ | |
| 115 | int nEditAlloc; /* Space allocated for aEdit[] */ |
| 116 | DLine *aFrom; /* File on left side of the diff */ |
| 117 | int nFrom; /* Number of lines in aFrom[] */ |
| 118 | DLine *aTo; /* File on right side of the diff */ |
| 119 | int nTo; /* Number of lines in aTo[] */ |
| 120 | int (*xDiffer)(const DLine*,const DLine*); /* comparison function */ |
| 121 | }; |
| 122 | |
| 123 | /* |
| 124 | ** Count the number of lines in the input string. Include the last line |
| 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| @@ -164,11 +164,11 @@ | |
| 164 | int n, |
| 165 | int *pnLine, |
| 166 | u64 diffFlags |
| 167 | ){ |
| 168 | int nLine, i, k, nn, s, x; |
| 169 | u64 h, h2; |
| 170 | DLine *a; |
| 171 | const char *zNL; |
| 172 | |
| 173 | if( count_lines(z, n, &nLine)==0 ){ |
| 174 | return 0; |
| @@ -205,23 +205,27 @@ | |
| 205 | for(h=0, x=s; x<k; x++){ |
| 206 | char c = z[x]; |
| 207 | if( fossil_isspace(c) ){ |
| 208 | ++numws; |
| 209 | }else{ |
| 210 | h = (h^c)*9000000000000000041LL; |
| 211 | } |
| 212 | } |
| 213 | k -= numws; |
| 214 | }else{ |
| 215 | int k2 = k & ~0x7; |
| 216 | u64 m; |
| 217 | for(h=0, x=s; x<k2; x += 8){ |
| 218 | memcpy(&m, z+x, 8); |
| 219 | h = (h^m)*9000000000000000041LL; |
| 220 | } |
| 221 | m = 0; |
| 222 | memcpy(&m, z+x, k-k2); |
| 223 | h ^= m; |
| 224 | } |
| 225 | a[i].indent = s; |
| 226 | a[i].h = h = ((h%281474976710597LL)<<LENGTH_MASK_SZ) | (k-s); |
| 227 | h2 = h % nLine; |
| 228 | a[i].iNext = a[h2].iHash; |
| 229 | a[h2].iHash = i+1; |
| 230 | z += nn+1; n -= nn+1; |
| 231 | i++; |
| @@ -232,33 +236,34 @@ | |
| 236 | *pnLine = nLine; |
| 237 | return a; |
| 238 | } |
| 239 | |
| 240 | /* |
| 241 | ** Return zero if two DLine elements are identical. |
| 242 | */ |
| 243 | static int same_dline(const DLine *pA, const DLine *pB){ |
| 244 | if( pA->h!=pB->h ) return 1; |
| 245 | return memcmp(pA->z,pB->z, pA->h&LENGTH_MASK); |
| 246 | } |
| 247 | |
| 248 | /* |
| 249 | ** Return zero if two DLine elements are identical, ignoring |
| 250 | ** all whitespace. The indent field of pA/pB already points |
| 251 | ** to the first non-space character in the string. |
| 252 | */ |
| 253 | |
| 254 | static int same_dline_ignore_allws(const DLine *pA, const DLine *pB){ |
| 255 | int a = pA->indent, b = pB->indent; |
| 256 | if( pA->h==pB->h ){ |
| 257 | while( a<pA->n || b<pB->n ){ |
| 258 | if( a<pA->n && b<pB->n && pA->z[a++] != pB->z[b++] ) return 1; |
| 259 | while( a<pA->n && fossil_isspace(pA->z[a])) ++a; |
| 260 | while( b<pB->n && fossil_isspace(pB->z[b])) ++b; |
| 261 | } |
| 262 | return pA->n-a != pB->n-b; |
| 263 | } |
| 264 | return 1; |
| 265 | } |
| 266 | |
| 267 | /* |
| 268 | ** Return true if the regular expression *pRe matches any of the |
| 269 | ** N dlines |
| @@ -1444,16 +1449,16 @@ | |
| 1449 | int iSXb = iS1; /* Best match so far */ |
| 1450 | int iSYb = iS2; /* Best match so far */ |
| 1451 | |
| 1452 | for(i=iS1; i<iE1-mxLength; i++){ |
| 1453 | for(j=iS2; j<iE2-mxLength; j++){ |
| 1454 | if( p->xDiffer(&p->aFrom[i], &p->aTo[j]) ) continue; |
| 1455 | if( mxLength && p->xDiffer(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){ |
| 1456 | continue; |
| 1457 | } |
| 1458 | k = 1; |
| 1459 | while( i+k<iE1 && j+k<iE2 && p->xDiffer(&p->aFrom[i+k],&p->aTo[j+k])==0 ){ |
| 1460 | k++; |
| 1461 | } |
| 1462 | if( k>mxLength ){ |
| 1463 | iSXb = i; |
| 1464 | iSYb = j; |
| @@ -1515,11 +1520,11 @@ | |
| 1520 | mid = (iE1 + iS1)/2; |
| 1521 | for(i=iS1; i<iE1; i++){ |
| 1522 | int limit = 0; |
| 1523 | j = p->aTo[p->aFrom[i].h % p->nTo].iHash; |
| 1524 | while( j>0 |
| 1525 | && (j-1<iS2 || j>=iE2 || p->xDiffer(&p->aFrom[i], &p->aTo[j-1])) |
| 1526 | ){ |
| 1527 | if( limit++ > 10 ){ |
| 1528 | j = 0; |
| 1529 | break; |
| 1530 | } |
| @@ -1532,19 +1537,19 @@ | |
| 1537 | iSX = i; |
| 1538 | iSY = j-1; |
| 1539 | pA = &p->aFrom[iSX-1]; |
| 1540 | pB = &p->aTo[iSY-1]; |
| 1541 | n = minInt(iSX-iS1, iSY-iS2); |
| 1542 | for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA--, pB--){} |
| 1543 | iSX -= k; |
| 1544 | iSY -= k; |
| 1545 | iEX = i+1; |
| 1546 | iEY = j; |
| 1547 | pA = &p->aFrom[iEX]; |
| 1548 | pB = &p->aTo[iEY]; |
| 1549 | n = minInt(iE1-iEX, iE2-iEY); |
| 1550 | for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA++, pB++){} |
| 1551 | iEX += k; |
| 1552 | iEY += k; |
| 1553 | skew = (iSX-iS1) - (iSY-iS2); |
| 1554 | if( skew<0 ) skew = -skew; |
| 1555 | dist = (iSX+iEX)/2 - mid; |
| @@ -1681,16 +1686,16 @@ | |
| 1686 | int mnE, iS, iE1, iE2; |
| 1687 | |
| 1688 | /* Carve off the common header and footer */ |
| 1689 | iE1 = p->nFrom; |
| 1690 | iE2 = p->nTo; |
| 1691 | while( iE1>0 && iE2>0 && p->xDiffer(&p->aFrom[iE1-1], &p->aTo[iE2-1])==0 ){ |
| 1692 | iE1--; |
| 1693 | iE2--; |
| 1694 | } |
| 1695 | mnE = iE1<iE2 ? iE1 : iE2; |
| 1696 | for(iS=0; iS<mnE && p->xDiffer(&p->aFrom[iS],&p->aTo[iS])==0; iS++){} |
| 1697 | |
| 1698 | /* do the difference */ |
| 1699 | if( iS>0 ){ |
| 1700 | appendTriple(p, iS, 0, 0); |
| 1701 | } |
| @@ -1755,11 +1760,11 @@ | |
| 1760 | |
| 1761 | /* Shift insertions toward the beginning of the file */ |
| 1762 | while( cpy>0 && del==0 && ins>0 ){ |
| 1763 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of insert */ |
| 1764 | DLine *pBtm = &p->aTo[lnTo+ins-1]; /* Last line inserted */ |
| 1765 | if( p->xDiffer(pTop, pBtm) ) break; |
| 1766 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1767 | lnFrom--; |
| 1768 | lnTo--; |
| 1769 | p->aEdit[r]--; |
| 1770 | p->aEdit[r+3]++; |
| @@ -1768,11 +1773,11 @@ | |
| 1773 | |
| 1774 | /* Shift insertions toward the end of the file */ |
| 1775 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){ |
| 1776 | DLine *pTop = &p->aTo[lnTo]; /* First line inserted */ |
| 1777 | DLine *pBtm = &p->aTo[lnTo+ins]; /* First line past end of insert */ |
| 1778 | if( p->xDiffer(pTop, pBtm) ) break; |
| 1779 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break; |
| 1780 | lnFrom++; |
| 1781 | lnTo++; |
| 1782 | p->aEdit[r]++; |
| 1783 | p->aEdit[r+3]--; |
| @@ -1781,11 +1786,11 @@ | |
| 1786 | |
| 1787 | /* Shift deletions toward the beginning of the file */ |
| 1788 | while( cpy>0 && del>0 && ins==0 ){ |
| 1789 | DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of delete */ |
| 1790 | DLine *pBtm = &p->aFrom[lnFrom+del-1]; /* Last line deleted */ |
| 1791 | if( p->xDiffer(pTop, pBtm) ) break; |
| 1792 | if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; |
| 1793 | lnFrom--; |
| 1794 | lnTo--; |
| 1795 | p->aEdit[r]--; |
| 1796 | p->aEdit[r+3]++; |
| @@ -1794,11 +1799,11 @@ | |
| 1799 | |
| 1800 | /* Shift deletions toward the end of the file */ |
| 1801 | while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){ |
| 1802 | DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */ |
| 1803 | DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */ |
| 1804 | if( p->xDiffer(pTop, pBtm) ) break; |
| 1805 | if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break; |
| 1806 | lnFrom++; |
| 1807 | lnTo++; |
| 1808 | p->aEdit[r]++; |
| 1809 | p->aEdit[r+3]--; |
| @@ -1821,14 +1826,39 @@ | |
| 1826 | } |
| 1827 | |
| 1828 | /* |
| 1829 | ** Extract the width of columns for side-by-side diff. Supply an |
| 1830 | ** appropriate default if no width is given. |
| 1831 | ** |
| 1832 | ** Calculate the default automatically, based on terminal's current width: |
| 1833 | ** term-width = 2*diff-col + diff-marker + 1 |
| 1834 | ** diff-col = lineno + lmargin + text-width + rmargin |
| 1835 | ** |
| 1836 | ** text-width = (term-width - diff-marker - 1)/2 - lineno - lmargin - rmargin |
| 1837 | */ |
| 1838 | int diff_width(u64 diffFlags){ |
| 1839 | int w = (diffFlags & DIFF_WIDTH_MASK)/(DIFF_CONTEXT_MASK+1); |
| 1840 | if( w==0 ){ |
| 1841 | static struct { |
| 1842 | unsigned int lineno, lmargin, text, rmargin, marker; |
| 1843 | } sbsW = { 5, 2, 0, 0, 3 }; |
| 1844 | const unsigned int wMin = 24, wMax = 132; |
| 1845 | unsigned int tw = terminal_get_width(80); |
| 1846 | unsigned int twMin = |
| 1847 | (wMin + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; |
| 1848 | unsigned int twMax = |
| 1849 | (wMax + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; |
| 1850 | |
| 1851 | if( tw<twMin ){ |
| 1852 | tw = twMin; |
| 1853 | }else if( tw>twMax ){ |
| 1854 | tw = twMax; |
| 1855 | } |
| 1856 | sbsW.text = |
| 1857 | (tw - sbsW.marker - 1)/2 - sbsW.lineno - sbsW.lmargin - sbsW.rmargin; |
| 1858 | w = sbsW.text; |
| 1859 | } |
| 1860 | return w; |
| 1861 | } |
| 1862 | |
| 1863 | /* |
| 1864 | ** Append the error message to pOut. |
| @@ -1875,13 +1905,13 @@ | |
| 1905 | blob_to_utf8_no_bom(pB_Blob, 0); |
| 1906 | |
| 1907 | /* Prepare the input files */ |
| 1908 | memset(&c, 0, sizeof(c)); |
| 1909 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 1910 | c.xDiffer = same_dline_ignore_allws; |
| 1911 | }else{ |
| 1912 | c.xDiffer = same_dline; |
| 1913 | } |
| 1914 | c.aFrom = break_into_lines(blob_str(pA_Blob), blob_size(pA_Blob), |
| 1915 | &c.nFrom, diffFlags); |
| 1916 | c.aTo = break_into_lines(blob_str(pB_Blob), blob_size(pB_Blob), |
| 1917 | &c.nTo, diffFlags); |
| @@ -2107,13 +2137,13 @@ | |
| 2137 | static int annotation_start(Annotator *p, Blob *pInput, u64 diffFlags){ |
| 2138 | int i; |
| 2139 | |
| 2140 | memset(p, 0, sizeof(*p)); |
| 2141 | if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ |
| 2142 | p->c.xDiffer = same_dline_ignore_allws; |
| 2143 | }else{ |
| 2144 | p->c.xDiffer = same_dline; |
| 2145 | } |
| 2146 | p->c.aTo = break_into_lines(blob_str(pInput), blob_size(pInput),&p->c.nTo, |
| 2147 | diffFlags); |
| 2148 | if( p->c.aTo==0 ){ |
| 2149 | return 1; |
| 2150 |
+36
-3
| --- src/dispatch.c | ||
| +++ src/dispatch.c | ||
| @@ -427,18 +427,51 @@ | ||
| 427 | 427 | ** |
| 428 | 428 | ** Show all help text on a single page. Useful for proof-reading. |
| 429 | 429 | */ |
| 430 | 430 | void test_all_help_page(void){ |
| 431 | 431 | int i; |
| 432 | + Blob buf; | |
| 433 | + blob_init(&buf,0,0); | |
| 432 | 434 | style_header("All Help Text"); |
| 435 | + @ <dl> | |
| 433 | 436 | for(i=0; i<MX_COMMAND; i++){ |
| 437 | + const char *zDesc; | |
| 438 | + unsigned int e = aCommand[i].eCmdFlags; | |
| 439 | + if( e & CMDFLAG_1ST_TIER ){ | |
| 440 | + zDesc = "1st tier command"; | |
| 441 | + }else if( e & CMDFLAG_2ND_TIER ){ | |
| 442 | + zDesc = "2nd tier command"; | |
| 443 | + }else if( e & CMDFLAG_TEST ){ | |
| 444 | + zDesc = "test command"; | |
| 445 | + }else if( e & CMDFLAG_WEBPAGE ){ | |
| 446 | + if( e & CMDFLAG_RAWCONTENT ){ | |
| 447 | + zDesc = "raw-content web page"; | |
| 448 | + }else{ | |
| 449 | + zDesc = "web page"; | |
| 450 | + } | |
| 451 | + }else{ | |
| 452 | + blob_reset(&buf); | |
| 453 | + if( e & CMDFLAG_VERSIONABLE ){ | |
| 454 | + blob_appendf(&buf, "versionable "); | |
| 455 | + } | |
| 456 | + if( e & CMDFLAG_BLOCKTEXT ){ | |
| 457 | + blob_appendf(&buf, "block-text "); | |
| 458 | + } | |
| 459 | + if( e & CMDFLAG_BOOLEAN ){ | |
| 460 | + blob_appendf(&buf, "boolean "); | |
| 461 | + } | |
| 462 | + blob_appendf(&buf,"setting"); | |
| 463 | + zDesc = blob_str(&buf); | |
| 464 | + } | |
| 434 | 465 | if( memcmp(aCommand[i].zName, "test", 4)==0 ) continue; |
| 435 | - @ <h2>%s(aCommand[i].zName):</h2> | |
| 436 | - @ <blockquote> | |
| 466 | + @ <dt><big><b>%s(aCommand[i].zName)</b></big> (%s(zDesc))</dt> | |
| 467 | + @ <dd> | |
| 437 | 468 | help_to_html(aCommand[i].zHelp, cgi_output_blob()); |
| 438 | - @ </blockquote> | |
| 469 | + @ </dd> | |
| 439 | 470 | } |
| 471 | + @ </dl> | |
| 472 | + blob_reset(&buf); | |
| 440 | 473 | style_footer(); |
| 441 | 474 | } |
| 442 | 475 | |
| 443 | 476 | static void multi_column_list(const char **azWord, int nWord){ |
| 444 | 477 | int i, j, len; |
| 445 | 478 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -427,18 +427,51 @@ | |
| 427 | ** |
| 428 | ** Show all help text on a single page. Useful for proof-reading. |
| 429 | */ |
| 430 | void test_all_help_page(void){ |
| 431 | int i; |
| 432 | style_header("All Help Text"); |
| 433 | for(i=0; i<MX_COMMAND; i++){ |
| 434 | if( memcmp(aCommand[i].zName, "test", 4)==0 ) continue; |
| 435 | @ <h2>%s(aCommand[i].zName):</h2> |
| 436 | @ <blockquote> |
| 437 | help_to_html(aCommand[i].zHelp, cgi_output_blob()); |
| 438 | @ </blockquote> |
| 439 | } |
| 440 | style_footer(); |
| 441 | } |
| 442 | |
| 443 | static void multi_column_list(const char **azWord, int nWord){ |
| 444 | int i, j, len; |
| 445 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -427,18 +427,51 @@ | |
| 427 | ** |
| 428 | ** Show all help text on a single page. Useful for proof-reading. |
| 429 | */ |
| 430 | void test_all_help_page(void){ |
| 431 | int i; |
| 432 | Blob buf; |
| 433 | blob_init(&buf,0,0); |
| 434 | style_header("All Help Text"); |
| 435 | @ <dl> |
| 436 | for(i=0; i<MX_COMMAND; i++){ |
| 437 | const char *zDesc; |
| 438 | unsigned int e = aCommand[i].eCmdFlags; |
| 439 | if( e & CMDFLAG_1ST_TIER ){ |
| 440 | zDesc = "1st tier command"; |
| 441 | }else if( e & CMDFLAG_2ND_TIER ){ |
| 442 | zDesc = "2nd tier command"; |
| 443 | }else if( e & CMDFLAG_TEST ){ |
| 444 | zDesc = "test command"; |
| 445 | }else if( e & CMDFLAG_WEBPAGE ){ |
| 446 | if( e & CMDFLAG_RAWCONTENT ){ |
| 447 | zDesc = "raw-content web page"; |
| 448 | }else{ |
| 449 | zDesc = "web page"; |
| 450 | } |
| 451 | }else{ |
| 452 | blob_reset(&buf); |
| 453 | if( e & CMDFLAG_VERSIONABLE ){ |
| 454 | blob_appendf(&buf, "versionable "); |
| 455 | } |
| 456 | if( e & CMDFLAG_BLOCKTEXT ){ |
| 457 | blob_appendf(&buf, "block-text "); |
| 458 | } |
| 459 | if( e & CMDFLAG_BOOLEAN ){ |
| 460 | blob_appendf(&buf, "boolean "); |
| 461 | } |
| 462 | blob_appendf(&buf,"setting"); |
| 463 | zDesc = blob_str(&buf); |
| 464 | } |
| 465 | if( memcmp(aCommand[i].zName, "test", 4)==0 ) continue; |
| 466 | @ <dt><big><b>%s(aCommand[i].zName)</b></big> (%s(zDesc))</dt> |
| 467 | @ <dd> |
| 468 | help_to_html(aCommand[i].zHelp, cgi_output_blob()); |
| 469 | @ </dd> |
| 470 | } |
| 471 | @ </dl> |
| 472 | blob_reset(&buf); |
| 473 | style_footer(); |
| 474 | } |
| 475 | |
| 476 | static void multi_column_list(const char **azWord, int nWord){ |
| 477 | int i, j, len; |
| 478 |
+49
-19
| --- src/doc.c | ||
| +++ src/doc.c | ||
| @@ -685,13 +685,13 @@ | ||
| 685 | 685 | ** Transfer content to the output. During the transfer, when text of |
| 686 | 686 | ** the following form is seen: |
| 687 | 687 | ** |
| 688 | 688 | ** href="$ROOT/..." |
| 689 | 689 | ** action="$ROOT/..." |
| 690 | -** href=".../doc/$SELF/..." | |
| 690 | +** href=".../doc/$CURRENT/..." | |
| 691 | 691 | ** |
| 692 | -** Convert $ROOT to the root URI of the repository, and $SELF to the | |
| 692 | +** Convert $ROOT to the root URI of the repository, and $CURRENT to the | |
| 693 | 693 | ** version number of the /doc/ document currently being displayed (if any). |
| 694 | 694 | ** Allow ' in place of " and any case for href or action. |
| 695 | 695 | ** |
| 696 | 696 | ** Efforts are made to limit this translation to cases where the text is |
| 697 | 697 | ** fully contained with an HTML markup element. |
| @@ -712,20 +712,20 @@ | ||
| 712 | 712 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 713 | 713 | blob_appendf(cgi_output_blob(), "%R"); |
| 714 | 714 | base = i+5; |
| 715 | 715 | }else |
| 716 | 716 | if( z[i]=='$' |
| 717 | - && strncmp(&z[i-5],"/doc/$SELF/", 11)==0 | |
| 717 | + && strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0 | |
| 718 | 718 | && isWithinHref(z,i-5) |
| 719 | 719 | && isWithinHtmlMarkup(z, i-5) |
| 720 | 720 | && strncmp(g.zPath, "doc/",4)==0 |
| 721 | 721 | ){ |
| 722 | 722 | int j; |
| 723 | - for(j=4; g.zPath[j] && g.zPath[j]!='/'; j++){} | |
| 723 | + for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){} | |
| 724 | 724 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 725 | 725 | blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); |
| 726 | - base = i+5; | |
| 726 | + base = i+8; | |
| 727 | 727 | } |
| 728 | 728 | } |
| 729 | 729 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 730 | 730 | } |
| 731 | 731 | |
| @@ -796,10 +796,11 @@ | ||
| 796 | 796 | if( !raw ){ |
| 797 | 797 | style_footer(); |
| 798 | 798 | } |
| 799 | 799 | #endif |
| 800 | 800 | }else{ |
| 801 | + fossil_free(style_csp(1)); | |
| 801 | 802 | cgi_set_content_type(zMime); |
| 802 | 803 | cgi_set_content(pBody); |
| 803 | 804 | } |
| 804 | 805 | } |
| 805 | 806 | |
| @@ -809,11 +810,11 @@ | ||
| 809 | 810 | ** WEBPAGE: doc |
| 810 | 811 | ** URL: /uv/FILE |
| 811 | 812 | ** URL: /doc/CHECKIN/FILE |
| 812 | 813 | ** |
| 813 | 814 | ** CHECKIN can be either tag or hash prefix or timestamp identifying a |
| 814 | -** particular check, or the name of a branch (meaning the most recent | |
| 815 | +** particular check-in, or the name of a branch (meaning the most recent | |
| 815 | 816 | ** check-in on that branch) or one of various magic words: |
| 816 | 817 | ** |
| 817 | 818 | ** "tip" means the most recent check-in |
| 818 | 819 | ** |
| 819 | 820 | ** "ckout" means the current check-out, if the server is run from |
| @@ -823,11 +824,12 @@ | ||
| 823 | 824 | ** regardless of what branch it occurs on. |
| 824 | 825 | ** |
| 825 | 826 | ** FILE is the name of a file to delivered up as a webpage. FILE is relative |
| 826 | 827 | ** to the root of the source tree of the repository. The FILE must |
| 827 | 828 | ** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read |
| 828 | -** directly from disk and need not be a managed file. | |
| 829 | +** directly from disk and need not be a managed file. For /uv, FILE | |
| 830 | +** can also be the hash of the unversioned file. | |
| 829 | 831 | ** |
| 830 | 832 | ** The "ckout" CHECKIN is intended for development - to provide a mechanism |
| 831 | 833 | ** for looking at what a file will look like using the /doc webpage after |
| 832 | 834 | ** it gets checked in. |
| 833 | 835 | ** |
| @@ -930,22 +932,26 @@ | ||
| 930 | 932 | goto doc_not_found; |
| 931 | 933 | } |
| 932 | 934 | } |
| 933 | 935 | if( isUV ){ |
| 934 | 936 | if( db_table_exists("repository","unversioned") ){ |
| 935 | - Stmt q; | |
| 936 | - db_prepare(&q, "SELECT hash, mtime FROM unversioned" | |
| 937 | - " WHERE name=%Q", zName); | |
| 938 | - if( db_step(&q)==SQLITE_ROW ){ | |
| 939 | - etag_check(ETAG_HASH, db_column_text(&q,0)); | |
| 940 | - etag_last_modified(db_column_int64(&q,1)); | |
| 941 | - } | |
| 942 | - db_finalize(&q); | |
| 943 | - if( unversioned_content(zName, &filebody)==0 ){ | |
| 944 | - rid = 1; | |
| 945 | - zDfltTitle = zName; | |
| 946 | - } | |
| 937 | + rid = unversioned_content(zName, &filebody); | |
| 938 | + if( rid==1 ){ | |
| 939 | + Stmt q; | |
| 940 | + db_prepare(&q, "SELECT hash, mtime FROM unversioned" | |
| 941 | + " WHERE name=%Q", zName); | |
| 942 | + if( db_step(&q)==SQLITE_ROW ){ | |
| 943 | + etag_check(ETAG_HASH, db_column_text(&q,0)); | |
| 944 | + etag_last_modified(db_column_int64(&q,1)); | |
| 945 | + } | |
| 946 | + db_finalize(&q); | |
| 947 | + }else if( rid==2 ){ | |
| 948 | + zName = db_text(zName, | |
| 949 | + "SELECT name FROM unversioned WHERE hash=%Q", zName); | |
| 950 | + g.isConst = 1; | |
| 951 | + } | |
| 952 | + zDfltTitle = zName; | |
| 947 | 953 | } |
| 948 | 954 | }else if( fossil_strcmp(zCheckin,"ckout")==0 ){ |
| 949 | 955 | /* Read from the local checkout */ |
| 950 | 956 | char *zFullpath; |
| 951 | 957 | db_must_be_within_tree(); |
| @@ -1122,10 +1128,34 @@ | ||
| 1122 | 1128 | } |
| 1123 | 1129 | cgi_set_content_type(zMime); |
| 1124 | 1130 | cgi_set_content(&bgimg); |
| 1125 | 1131 | } |
| 1126 | 1132 | |
| 1133 | + | |
| 1134 | +/* | |
| 1135 | +** WEBPAGE: favicon.ico | |
| 1136 | +** | |
| 1137 | +** Return the default favicon.ico image. The returned image is for the | |
| 1138 | +** Fossil lizard icon. | |
| 1139 | +** | |
| 1140 | +** The intended use case here is to supply a favicon for the "fossil ui" | |
| 1141 | +** command. For a permanent website, the recommended process is for | |
| 1142 | +** the admin to set up a project-specific favicon and reference that | |
| 1143 | +** icon in the HTML header using a line like: | |
| 1144 | +** | |
| 1145 | +** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> | |
| 1146 | +** | |
| 1147 | +*/ | |
| 1148 | +void favicon_page(void){ | |
| 1149 | + Blob favicon; | |
| 1150 | + | |
| 1151 | + etag_check(ETAG_CONFIG, 0); | |
| 1152 | + blob_zero(&favicon); | |
| 1153 | + blob_init(&favicon, (char*)aLogo, sizeof(aLogo)); | |
| 1154 | + cgi_set_content_type("image/gif"); | |
| 1155 | + cgi_set_content(&favicon); | |
| 1156 | +} | |
| 1127 | 1157 | |
| 1128 | 1158 | /* |
| 1129 | 1159 | ** WEBPAGE: docsrch |
| 1130 | 1160 | ** |
| 1131 | 1161 | ** Search for documents that match a user-supplied full-text search pattern. |
| 1132 | 1162 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -685,13 +685,13 @@ | |
| 685 | ** Transfer content to the output. During the transfer, when text of |
| 686 | ** the following form is seen: |
| 687 | ** |
| 688 | ** href="$ROOT/..." |
| 689 | ** action="$ROOT/..." |
| 690 | ** href=".../doc/$SELF/..." |
| 691 | ** |
| 692 | ** Convert $ROOT to the root URI of the repository, and $SELF to the |
| 693 | ** version number of the /doc/ document currently being displayed (if any). |
| 694 | ** Allow ' in place of " and any case for href or action. |
| 695 | ** |
| 696 | ** Efforts are made to limit this translation to cases where the text is |
| 697 | ** fully contained with an HTML markup element. |
| @@ -712,20 +712,20 @@ | |
| 712 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 713 | blob_appendf(cgi_output_blob(), "%R"); |
| 714 | base = i+5; |
| 715 | }else |
| 716 | if( z[i]=='$' |
| 717 | && strncmp(&z[i-5],"/doc/$SELF/", 11)==0 |
| 718 | && isWithinHref(z,i-5) |
| 719 | && isWithinHtmlMarkup(z, i-5) |
| 720 | && strncmp(g.zPath, "doc/",4)==0 |
| 721 | ){ |
| 722 | int j; |
| 723 | for(j=4; g.zPath[j] && g.zPath[j]!='/'; j++){} |
| 724 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 725 | blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); |
| 726 | base = i+5; |
| 727 | } |
| 728 | } |
| 729 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 730 | } |
| 731 | |
| @@ -796,10 +796,11 @@ | |
| 796 | if( !raw ){ |
| 797 | style_footer(); |
| 798 | } |
| 799 | #endif |
| 800 | }else{ |
| 801 | cgi_set_content_type(zMime); |
| 802 | cgi_set_content(pBody); |
| 803 | } |
| 804 | } |
| 805 | |
| @@ -809,11 +810,11 @@ | |
| 809 | ** WEBPAGE: doc |
| 810 | ** URL: /uv/FILE |
| 811 | ** URL: /doc/CHECKIN/FILE |
| 812 | ** |
| 813 | ** CHECKIN can be either tag or hash prefix or timestamp identifying a |
| 814 | ** particular check, or the name of a branch (meaning the most recent |
| 815 | ** check-in on that branch) or one of various magic words: |
| 816 | ** |
| 817 | ** "tip" means the most recent check-in |
| 818 | ** |
| 819 | ** "ckout" means the current check-out, if the server is run from |
| @@ -823,11 +824,12 @@ | |
| 823 | ** regardless of what branch it occurs on. |
| 824 | ** |
| 825 | ** FILE is the name of a file to delivered up as a webpage. FILE is relative |
| 826 | ** to the root of the source tree of the repository. The FILE must |
| 827 | ** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read |
| 828 | ** directly from disk and need not be a managed file. |
| 829 | ** |
| 830 | ** The "ckout" CHECKIN is intended for development - to provide a mechanism |
| 831 | ** for looking at what a file will look like using the /doc webpage after |
| 832 | ** it gets checked in. |
| 833 | ** |
| @@ -930,22 +932,26 @@ | |
| 930 | goto doc_not_found; |
| 931 | } |
| 932 | } |
| 933 | if( isUV ){ |
| 934 | if( db_table_exists("repository","unversioned") ){ |
| 935 | Stmt q; |
| 936 | db_prepare(&q, "SELECT hash, mtime FROM unversioned" |
| 937 | " WHERE name=%Q", zName); |
| 938 | if( db_step(&q)==SQLITE_ROW ){ |
| 939 | etag_check(ETAG_HASH, db_column_text(&q,0)); |
| 940 | etag_last_modified(db_column_int64(&q,1)); |
| 941 | } |
| 942 | db_finalize(&q); |
| 943 | if( unversioned_content(zName, &filebody)==0 ){ |
| 944 | rid = 1; |
| 945 | zDfltTitle = zName; |
| 946 | } |
| 947 | } |
| 948 | }else if( fossil_strcmp(zCheckin,"ckout")==0 ){ |
| 949 | /* Read from the local checkout */ |
| 950 | char *zFullpath; |
| 951 | db_must_be_within_tree(); |
| @@ -1122,10 +1128,34 @@ | |
| 1122 | } |
| 1123 | cgi_set_content_type(zMime); |
| 1124 | cgi_set_content(&bgimg); |
| 1125 | } |
| 1126 | |
| 1127 | |
| 1128 | /* |
| 1129 | ** WEBPAGE: docsrch |
| 1130 | ** |
| 1131 | ** Search for documents that match a user-supplied full-text search pattern. |
| 1132 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -685,13 +685,13 @@ | |
| 685 | ** Transfer content to the output. During the transfer, when text of |
| 686 | ** the following form is seen: |
| 687 | ** |
| 688 | ** href="$ROOT/..." |
| 689 | ** action="$ROOT/..." |
| 690 | ** href=".../doc/$CURRENT/..." |
| 691 | ** |
| 692 | ** Convert $ROOT to the root URI of the repository, and $CURRENT to the |
| 693 | ** version number of the /doc/ document currently being displayed (if any). |
| 694 | ** Allow ' in place of " and any case for href or action. |
| 695 | ** |
| 696 | ** Efforts are made to limit this translation to cases where the text is |
| 697 | ** fully contained with an HTML markup element. |
| @@ -712,20 +712,20 @@ | |
| 712 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 713 | blob_appendf(cgi_output_blob(), "%R"); |
| 714 | base = i+5; |
| 715 | }else |
| 716 | if( z[i]=='$' |
| 717 | && strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0 |
| 718 | && isWithinHref(z,i-5) |
| 719 | && isWithinHtmlMarkup(z, i-5) |
| 720 | && strncmp(g.zPath, "doc/",4)==0 |
| 721 | ){ |
| 722 | int j; |
| 723 | for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){} |
| 724 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 725 | blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); |
| 726 | base = i+8; |
| 727 | } |
| 728 | } |
| 729 | blob_append(cgi_output_blob(), &z[base], i-base); |
| 730 | } |
| 731 | |
| @@ -796,10 +796,11 @@ | |
| 796 | if( !raw ){ |
| 797 | style_footer(); |
| 798 | } |
| 799 | #endif |
| 800 | }else{ |
| 801 | fossil_free(style_csp(1)); |
| 802 | cgi_set_content_type(zMime); |
| 803 | cgi_set_content(pBody); |
| 804 | } |
| 805 | } |
| 806 | |
| @@ -809,11 +810,11 @@ | |
| 810 | ** WEBPAGE: doc |
| 811 | ** URL: /uv/FILE |
| 812 | ** URL: /doc/CHECKIN/FILE |
| 813 | ** |
| 814 | ** CHECKIN can be either tag or hash prefix or timestamp identifying a |
| 815 | ** particular check-in, or the name of a branch (meaning the most recent |
| 816 | ** check-in on that branch) or one of various magic words: |
| 817 | ** |
| 818 | ** "tip" means the most recent check-in |
| 819 | ** |
| 820 | ** "ckout" means the current check-out, if the server is run from |
| @@ -823,11 +824,12 @@ | |
| 824 | ** regardless of what branch it occurs on. |
| 825 | ** |
| 826 | ** FILE is the name of a file to delivered up as a webpage. FILE is relative |
| 827 | ** to the root of the source tree of the repository. The FILE must |
| 828 | ** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read |
| 829 | ** directly from disk and need not be a managed file. For /uv, FILE |
| 830 | ** can also be the hash of the unversioned file. |
| 831 | ** |
| 832 | ** The "ckout" CHECKIN is intended for development - to provide a mechanism |
| 833 | ** for looking at what a file will look like using the /doc webpage after |
| 834 | ** it gets checked in. |
| 835 | ** |
| @@ -930,22 +932,26 @@ | |
| 932 | goto doc_not_found; |
| 933 | } |
| 934 | } |
| 935 | if( isUV ){ |
| 936 | if( db_table_exists("repository","unversioned") ){ |
| 937 | rid = unversioned_content(zName, &filebody); |
| 938 | if( rid==1 ){ |
| 939 | Stmt q; |
| 940 | db_prepare(&q, "SELECT hash, mtime FROM unversioned" |
| 941 | " WHERE name=%Q", zName); |
| 942 | if( db_step(&q)==SQLITE_ROW ){ |
| 943 | etag_check(ETAG_HASH, db_column_text(&q,0)); |
| 944 | etag_last_modified(db_column_int64(&q,1)); |
| 945 | } |
| 946 | db_finalize(&q); |
| 947 | }else if( rid==2 ){ |
| 948 | zName = db_text(zName, |
| 949 | "SELECT name FROM unversioned WHERE hash=%Q", zName); |
| 950 | g.isConst = 1; |
| 951 | } |
| 952 | zDfltTitle = zName; |
| 953 | } |
| 954 | }else if( fossil_strcmp(zCheckin,"ckout")==0 ){ |
| 955 | /* Read from the local checkout */ |
| 956 | char *zFullpath; |
| 957 | db_must_be_within_tree(); |
| @@ -1122,10 +1128,34 @@ | |
| 1128 | } |
| 1129 | cgi_set_content_type(zMime); |
| 1130 | cgi_set_content(&bgimg); |
| 1131 | } |
| 1132 | |
| 1133 | |
| 1134 | /* |
| 1135 | ** WEBPAGE: favicon.ico |
| 1136 | ** |
| 1137 | ** Return the default favicon.ico image. The returned image is for the |
| 1138 | ** Fossil lizard icon. |
| 1139 | ** |
| 1140 | ** The intended use case here is to supply a favicon for the "fossil ui" |
| 1141 | ** command. For a permanent website, the recommended process is for |
| 1142 | ** the admin to set up a project-specific favicon and reference that |
| 1143 | ** icon in the HTML header using a line like: |
| 1144 | ** |