Fossil SCM

Merge trunk

jan.nijtmans 2020-05-22 20:16 UTC multi-thread merge
Commit 1e4aaf7b2e754f8cd589f25ef1cabf5aaf69fb44a2440eb059d5463239599048
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
+2 -2
--- auto.def
+++ auto.def
@@ -273,11 +273,11 @@
273273
274274
# Check for libraries that need to be sorted out early
275275
cc-check-function-in-lib iconv iconv
276276
277277
# 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}}} {
279279
msg-checking "Checking for $msg..."
280280
set rc 0
281281
if {[is_mingw]} {
282282
lappend libs -lgdi32 -lwsock32 -lcrypt32
283283
}
@@ -351,11 +351,11 @@
351351
user-error "The OpenSSL in source tree directory does not exist"
352352
}
353353
set msg "ssl in $ssldir"
354354
set cflags "-I$ssldir/include"
355355
set ldflags "-L$ssldir"
356
- set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a"
356
+ set ssllibs "$ssldir/libssl.a $ssldir/libcrypto.a -lpthread"
357357
set found [check-for-openssl "ssl in source tree" "$cflags $ldflags" $ssllibs]
358358
} else {
359359
if {$ssldirs in {auto ""}} {
360360
catch {
361361
set cflags [exec pkg-config openssl --cflags-only-I]
362362
--- 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 @@
11
Built-in Skins
22
==============
33
44
Each subdirectory under this folder describes a built-in "skin".
5
-There are four files in each subdirectory for the CSS, the "details"
6
-file, the footer, and the header for that skin.
5
+There are five key files in each subdirectory:
6
+
7
+ * `css.txt` → The CSS for the skin
8
+ * `details.txt` → Skin-specific settings
9
+ * `footer.txt` → Text of the Content Footer for each page
10
+ * `header.txt` → Text of the Content Header for each page
11
+ * `js.txt` → Javascript included in the Content Footer
712
813
To improve an existing built-in skin, simply edit the appropriate
914
files and recompile.
1015
1116
To add a new skin:
@@ -13,12 +18,12 @@
1318
1. Create a new subdirectory under skins/. (The new directory is
1419
called "skins/newskin" below but you should use a new original
1520
name, of course.)
1621
1722
2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
18
- skins/newskin/footer.txt and skins/newskin/header.txt.
19
- Be sure to "fossil add" these files.
23
+ skins/newskin/footer.txt, skins/newskin/header.txt, and
24
+ skins/newskin/js.txt. Be sure to "fossil add" these files.
2025
2126
3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
2227
step rebuilds the various makefiles so that they have dependencies
2328
on the skin files you just installed.
2429
@@ -25,21 +30,23 @@
2530
4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
2631
file so that it describes and references the "newskin" skin.
2732
2833
5. Type "make" to rebuild.
2934
35
+See the [custom skin documentation](../www/customskin.md) for more information.
36
+
3037
Development Hints
3138
-----------------
3239
3340
One way to develop a new skin is to copy the baseline files (css.txt,
34
-details.txt, footer.txt, and header.txt) into a working directory $WORKDIR
35
-then launch Fossil with a command-line option "--skin $WORKDIR". Example:
41
+details.txt, footer.txt, header.txt, and js.txt) into a working
42
+directory $WORKDIR then launch Fossil with a command-line option
43
+"--skin $WORKDIR". Example:
3644
3745
cp -r skins/default newskin
3846
fossil ui --skin ./newskin
3947
4048
When the argument to --skin contains one or more '/' characters, the
4149
appropriate skin files are read from disk from the directory specified.
42
-So after launching fossil as shown above, you can edit the newskin/css.txt,
43
-newskin/details.txt, newskin/footer.txt, and newskin/header.txt files using
44
-your favorite text editor, then press Reload on your browser to see
45
-immediate results.
50
+So after launching fossil as shown above, you can edit the newskin/*.txt
51
+files using your favorite text editor, then press Reload on your browser
52
+to see immediate results.
4653
--- skins/README.md
+++ skins/README.md
@@ -1,11 +1,16 @@
1 Built-in Skins
2 ==============
3
4 Each subdirectory under this folder describes a built-in "skin".
5 There are four files in each subdirectory for the CSS, the "details"
6 file, the footer, and the header for that skin.
 
 
 
 
 
7
8 To improve an existing built-in skin, simply edit the appropriate
9 files and recompile.
10
11 To add a new skin:
@@ -13,12 +18,12 @@
13 1. Create a new subdirectory under skins/. (The new directory is
14 called "skins/newskin" below but you should use a new original
15 name, of course.)
16
17 2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
18 skins/newskin/footer.txt and skins/newskin/header.txt.
19 Be sure to "fossil add" these files.
20
21 3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
22 step rebuilds the various makefiles so that they have dependencies
23 on the skin files you just installed.
24
@@ -25,21 +30,23 @@
25 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
26 file so that it describes and references the "newskin" skin.
27
28 5. Type "make" to rebuild.
29
 
 
30 Development Hints
31 -----------------
32
33 One way to develop a new skin is to copy the baseline files (css.txt,
34 details.txt, footer.txt, and header.txt) into a working directory $WORKDIR
35 then launch Fossil with a command-line option "--skin $WORKDIR". Example:
 
36
37 cp -r skins/default newskin
38 fossil ui --skin ./newskin
39
40 When the argument to --skin contains one or more '/' characters, the
41 appropriate skin files are read from disk from the directory specified.
42 So after launching fossil as shown above, you can edit the newskin/css.txt,
43 newskin/details.txt, newskin/footer.txt, and newskin/header.txt files using
44 your favorite text editor, then press Reload on your browser to see
45 immediate results.
46
--- skins/README.md
+++ skins/README.md
@@ -1,11 +1,16 @@
1 Built-in Skins
2 ==============
3
4 Each subdirectory under this folder describes a built-in "skin".
5 There are five key files in each subdirectory:
6
7 * `css.txt` → The CSS for the skin
8 * `details.txt` → Skin-specific settings
9 * `footer.txt` → Text of the Content Footer for each page
10 * `header.txt` → Text of the Content Header for each page
11 * `js.txt` → Javascript included in the Content Footer
12
13 To improve an existing built-in skin, simply edit the appropriate
14 files and recompile.
15
16 To add a new skin:
@@ -13,12 +18,12 @@
18 1. Create a new subdirectory under skins/. (The new directory is
19 called "skins/newskin" below but you should use a new original
20 name, of course.)
21
22 2. Add files skins/newskin/css.txt, skins/newskin/details.txt,
23 skins/newskin/footer.txt, skins/newskin/header.txt, and
24 skins/newskin/js.txt. Be sure to "fossil add" these files.
25
26 3. Go to the src/ directory and rerun "tclsh makemake.tcl". This
27 step rebuilds the various makefiles so that they have dependencies
28 on the skin files you just installed.
29
@@ -25,21 +30,23 @@
30 4. Edit the BuiltinSkin[] array near the top of the src/skins.c source
31 file so that it describes and references the "newskin" skin.
32
33 5. Type "make" to rebuild.
34
35 See the [custom skin documentation](../www/customskin.md) for more information.
36
37 Development Hints
38 -----------------
39
40 One way to develop a new skin is to copy the baseline files (css.txt,
41 details.txt, footer.txt, header.txt, and js.txt) into a working
42 directory $WORKDIR then launch Fossil with a command-line option
43 "--skin $WORKDIR". Example:
44
45 cp -r skins/default newskin
46 fossil ui --skin ./newskin
47
48 When the argument to --skin contains one or more '/' characters, the
49 appropriate skin files are read from disk from the directory specified.
50 So after launching fossil as shown above, you can edit the newskin/*.txt
51 files using your favorite text editor, then press Reload on your browser
52 to see immediate results.
 
53
--- skins/ardoise/css.txt
+++ skins/ardoise/css.txt
@@ -291,11 +291,11 @@
291291
box-sizing: border-box
292292
}
293293
.column,
294294
.columns {
295295
width: 100%;
296
- float: left;
296
+ /*float: left; can break README.md in /dir view*/
297297
box-sizing: border-box
298298
}
299299
@media (min-width:400px) {
300300
.container {
301301
width: 95%;
302302
--- 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
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145145
border-radius: 5px;
146146
}
147147
.content blockquote {
148148
padding: 0 15px;
149149
}
150
-div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote {
150
+div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote, div.forumTime blockquote, div.forumTimeline blockquote {
151151
background-color: rgba(65, 131, 196, 0.1);
152152
border-left: 3px solid #254769;
153153
padding: .1em 1em;
154154
}
155155
156156
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145 border-radius: 5px;
146 }
147 .content blockquote {
148 padding: 0 15px;
149 }
150 div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote {
151 background-color: rgba(65, 131, 196, 0.1);
152 border-left: 3px solid #254769;
153 padding: .1em 1em;
154 }
155
156
--- skins/default/css.txt
+++ skins/default/css.txt
@@ -145,11 +145,11 @@
145 border-radius: 5px;
146 }
147 .content blockquote {
148 padding: 0 15px;
149 }
150 div.forumHierRoot blockquote, div.forumHier blockquote, div.forumEdit blockquote, div.forumTime blockquote, div.forumTimeline blockquote {
151 background-color: rgba(65, 131, 196, 0.1);
152 border-left: 3px solid #254769;
153 padding: .1em 1em;
154 }
155
156
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -1,10 +1,10 @@
11
<div class="header">
22
<div class="title"><h1>$<project_name></h1>$<title></div>
33
<div class="status"><th1>
44
if {[info exists login]} {
5
- html "$login — <a href='$home/login'>Logout</a>\n"
5
+ html "<a href='$home/login'>$login</a>\n"
66
} else {
77
html "<a href='$home/login'>Login</a>\n"
88
}
99
</th1></div>
1010
</div>
@@ -17,17 +17,18 @@
1717
html "<a href='$home$url' class='active $cls'>$name</a>\n"
1818
} else {
1919
html "<a href='$home$url' class='$cls'>$name</a>\n"
2020
}
2121
}
22
-html "<a id='hbbtn' href='/sitemap'>&#9776;</a>"
22
+html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>&#9776;</a>"
2323
menulink $index_page Home {}
2424
if {[anycap jor]} {
2525
menulink /timeline Timeline {}
2626
}
2727
if {[hascap oh]} {
28
- menulink /dir?ci=tip Files desktoponly
28
+ if {![info exists current_checkin]} {set current_checkin tip}
29
+ menulink /dir?ci=$current_checkin Files desktoponly
2930
}
3031
if {[hascap o]} {
3132
menulink /brlist Branches desktoponly
3233
menulink /taglist Tags wideonly
3334
}
3435
3536
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'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28 menulink /dir?ci=tip Files desktoponly
 
29 }
30 if {[hascap o]} {
31 menulink /brlist Branches desktoponly
32 menulink /taglist Tags wideonly
33 }
34
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'>&#9776;</a>"
23 menulink $index_page Home {}
24 if {[anycap jor]} {
25 menulink /timeline Timeline {}
26 }
27 if {[hascap oh]} {
28 if {![info exists current_checkin]} {set current_checkin tip}
29 menulink /dir?ci=$current_checkin Files desktoponly
30 }
31 if {[hascap o]} {
32 menulink /brlist Branches desktoponly
33 menulink /taglist Tags wideonly
34 }
35
36 DDED src/accordion.js
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
1
+/* Attach appropriate javascript to each ".accordion" button so that
2
+** it expands and contracts when clicked.
3
+**ntracts when clicked.
4
+**
5
+** The uncompressed source code for the SV
6
+** wiki page "branch/accordion-experiments" window to a smaller
7
+** width, causing vertical growth.)
8
+**
9
+** Ano"+"/"+"ttach appropriate javascript to each ".accordion" button so that
10
+** it expands and contracts when clicked.
11
+**ntracts when clicked.
12
+**
13
+** The uncompressed source code for the SV
14
+** wiki page "branch/accordion-experiments" window to a smaller
15
+** width, causing vertical growth.)
16
+**
17
+** Another problem is that `scrollHeight' used to calculate the expanded height
18
+** while still in the contracted state may return values with small errors on
19
+** some browsers, especially for large elements, presumably due to omitting the
20
+** space required by the vertical scrollbar that may become necessary, causing
21
+** additional horizontal shrinking and consequently more vertical growth than
22
+** calculated. That's why se/* Attach appropriate javascript to each ".accordion" button so that
23
+** it expands and contracts when clicked.
24
+**ntracts when clicked.
25
+**
26
+** The uncompressed source code for the SV
27
+** wiki page "branch/accordion-experiments" window to a smvar p = a[i].nextElementSibli
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/accordion.js
+++ b/src/accordion.js
@@ -0,0 +1,27 @@
1 /* Attach appropriate javascript to each ".accordion" button so that
2 ** it expands and contracts when clicked.
3 **ntracts when clicked.
4 **
5 ** The uncompressed source code for the SV
6 ** wiki page "branch/accordion-experiments" window to a smaller
7 ** width, causing vertical growth.)
8 **
9 ** Ano"+"/"+"ttach appropriate javascript to each ".accordion" button so that
10 ** it expands and contracts when clicked.
11 **ntracts when clicked.
12 **
13 ** The uncompressed source code for the SV
14 ** wiki page "branch/accordion-experiments" window to a smaller
15 ** width, causing vertical growth.)
16 **
17 ** Another problem is that `scrollHeight' used to calculate the expanded height
18 ** while still in the contracted state may return values with small errors on
19 ** some browsers, especially for large elements, presumably due to omitting the
20 ** space required by the vertical scrollbar that may become necessary, causing
21 ** additional horizontal shrinking and consequently more vertical growth than
22 ** calculated. That's why se/* Attach appropriate javascript to each ".accordion" button so that
23 ** it expands and contracts when clicked.
24 **ntracts when clicked.
25 **
26 ** The uncompressed source code for the SV
27 ** wiki page "branch/accordion-experiments" window to a smvar p = a[i].nextElementSibli
+452 -195
--- src/alerts.c
+++ src/alerts.c
@@ -46,10 +46,11 @@
4646
@ --
4747
@ -- The ssub field is a string where each character indicates a particular
4848
@ -- type of event to subscribe to. Choices:
4949
@ -- a - Announcements
5050
@ -- c - Check-ins
51
+@ -- f - Forum posts
5152
@ -- t - Ticket changes
5253
@ -- w - Wiki changes
5354
@ -- Probably different codes will be added in the future. In the future
5455
@ -- we might also add a separate table that allows subscribing to email
5556
@ -- notifications for specific branches or tags or tickets.
@@ -114,11 +115,11 @@
114115
if( bOnlyIfEnabled
115116
&& fossil_strcmp(db_get("email-send-method",0),"off")==0
116117
){
117118
return; /* Don't create table for disabled email */
118119
}
119
- db_multi_exec(zAlertInit/*works-like:""*/);
120
+ db_exec_sql(zAlertInit);
120121
alert_triggers_enable();
121122
}else if( !db_table_has_column("repository","pending_alert","sentMod") ){
122123
db_multi_exec(
123124
"ALTER TABLE repository.pending_alert"
124125
" ADD COLUMN sentMod BOOLEAN DEFAULT false;"
@@ -183,11 +184,11 @@
183184
** is an administrator.
184185
*/
185186
void alert_submenu_common(void){
186187
if( g.perm.Admin ){
187188
if( fossil_strcmp(g.zPath,"subscribers") ){
188
- style_submenu_element("List Subscribers","%R/subscribers");
189
+ style_submenu_element("Subscribers","%R/subscribers");
189190
}
190191
if( fossil_strcmp(g.zPath,"subscribe") ){
191192
style_submenu_element("Add New Subscriber","%R/subscribe");
192193
}
193194
}
@@ -238,10 +239,17 @@
238239
@ This is URL used as the basename for hyperlinks included in
239240
@ email alert text. Omit the trailing "/".
240241
@ Suggested value: "%h(g.zBaseURL)"
241242
@ (Property: "email-url")</p>
242243
@ <hr>
244
+
245
+ entry_attribute("Administrator email address", 40, "email-admin",
246
+ "eadmin", "", 0);
247
+ @ <p>This is the email for the human administrator for the system.
248
+ @ Abuse and trouble reports and password reset requests are send here.
249
+ @ (Property: "email-admin")</p>
250
+ @ <hr>
243251
244252
entry_attribute("\"Return-Path\" email address", 20, "email-self",
245253
"eself", "", 0);
246254
@ <p><b>Required.</b>
247255
@ This is the email to which email notification bounces should be sent.
@@ -297,17 +305,10 @@
297305
@ append a colon and TCP port number (ex: smtp.example.com:587).
298306
@ The default TCP port number is 25.
299307
@ (Property: "email-send-relayhost")</p>
300308
@ <hr>
301309
302
- entry_attribute("Administrator email address", 40, "email-admin",
303
- "eadmin", "", 0);
304
- @ <p>This is the email for the human administrator for the system.
305
- @ Abuse and trouble reports are send here.
306
- @ (Property: "email-admin")</p>
307
- @ <hr>
308
-
309310
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
310311
@ </div></form>
311312
db_end_transaction(0);
312313
style_footer();
313314
}
@@ -574,22 +575,18 @@
574575
}
575576
return 0;
576577
}
577578
578579
/*
579
-** Make a copy of the input string up to but not including the
580
-** first cTerm character.
581
-**
582
-** Verify that the string really that is to be copied really is a
583
-** valid email address. If it is not, then return NULL.
584
-**
585
-** This routine is more restrictive than necessary. It does not
586
-** allow comments, IP address, quoted strings, or certain uncommon
587
-** characters. The only non-alphanumerics allowed in the local
588
-** part are "_", "+", "-" and "+".
589
-*/
590
-char *email_copy_addr(const char *z, char cTerm ){
580
+** Determine whether or not the input string is a valid email address.
581
+** Only look at character up to but not including the first \000 or
582
+** the first cTerm character, whichever comes first.
583
+**
584
+** Return the length of the email addresss string in bytes if the email
585
+** address is valid. If the email address is misformed, return 0.
586
+*/
587
+int email_address_is_valid(const char *z, char cTerm){
591588
int i;
592589
int nAt = 0;
593590
int nDot = 0;
594591
char c;
595592
if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */
@@ -619,13 +616,28 @@
619616
}
620617
}
621618
if( c!=cTerm ) return 0; /* Missing terminator */
622619
if( nAt==0 ) return 0; /* No "@" found anywhere */
623620
if( nDot==0 ) return 0; /* No "." in the domain */
621
+ return i;
622
+}
624623
625
- /* If we reach this point, the email address is valid */
626
- return mprintf("%.*s", i, z);
624
+/*
625
+** Make a copy of the input string up to but not including the
626
+** first cTerm character.
627
+**
628
+** Verify that the string really that is to be copied really is a
629
+** valid email address. If it is not, then return NULL.
630
+**
631
+** This routine is more restrictive than necessary. It does not
632
+** allow comments, IP address, quoted strings, or certain uncommon
633
+** characters. The only non-alphanumerics allowed in the local
634
+** part are "_", "+", "-" and "+".
635
+*/
636
+char *email_copy_addr(const char *z, char cTerm ){
637
+ int i = email_address_is_valid(z, cTerm);
638
+ return i==0 ? 0 : mprintf("%.*s", i, z);
627639
}
628640
629641
/*
630642
** Scan the input string for a valid email address enclosed in <...>
631643
** If the string contains one or more email addresses, extract the first
@@ -659,10 +671,38 @@
659671
char *zOut = alert_find_emailaddr(zIn);
660672
if( zOut ){
661673
sqlite3_result_text(context, zOut, -1, fossil_free);
662674
}
663675
}
676
+
677
+/*
678
+** SQL function: display_name(X)
679
+**
680
+** If X is a string, search for a user name at the beginning of that
681
+** string. The user name must be followed by an email address. If
682
+** found, return the user name. If not found, return NULL.
683
+**
684
+** This routine is used to extract the display name from the USER.INFO
685
+** field.
686
+*/
687
+void alert_display_name_func(
688
+ sqlite3_context *context,
689
+ int argc,
690
+ sqlite3_value **argv
691
+){
692
+ const char *zIn = (const char*)sqlite3_value_text(argv[0]);
693
+ int i;
694
+ if( zIn==0 ) return;
695
+ while( fossil_isspace(zIn[0]) ) zIn++;
696
+ for(i=0; zIn[i] && zIn[i]!='<' && zIn[i]!='\n'; i++){}
697
+ if( zIn[i]=='<' ){
698
+ while( i>0 && fossil_isspace(zIn[i-1]) ){ i--; }
699
+ if( i>0 ){
700
+ sqlite3_result_text(context, zIn, i, SQLITE_TRANSIENT);
701
+ }
702
+ }
703
+}
664704
665705
/*
666706
** Return the hostname portion of an email address - the part following
667707
** the @
668708
*/
@@ -762,11 +802,11 @@
762802
** Date:
763803
** Message-Id:
764804
** Content-Type:
765805
** Content-Transfer-Encoding:
766806
** MIME-Version:
767
-** X-Fossil-From:
807
+** Sender:
768808
**
769809
** The caller maintains ownership of the input Blobs. This routine will
770810
** read the Blobs and send them onward to the email system, but it will
771811
** not free them.
772812
**
@@ -774,14 +814,14 @@
774814
** in the pHdr parameter.
775815
**
776816
** If the zFromName argument is not NULL, then it should be a human-readable
777817
** name or handle for the sender. In that case, "From:" becomes a made-up
778818
** email address based on a hash of zFromName and the domain of email-self,
779
-** and an additional "X-Fossil-From:" field is inserted with the email-self
780
-** address. Downstream software might use the X-Fossil-From header to set
819
+** and an additional "Sender:" field is inserted with the email-self
820
+** address. Downstream software might use the Sender header to set
781821
** the envelope-from address of the email. If zFromName is a NULL pointer,
782
-** then the "From:" is set to the email-self value and X-Fossil-From is
822
+** then the "From:" is set to the email-self value and Sender is
783823
** omitted.
784824
*/
785825
void alert_send(
786826
AlertSender *p, /* Emailer context */
787827
Blob *pHdr, /* Email header (incomplete) */
@@ -794,26 +834,26 @@
794834
fossil_print("Sending email\n");
795835
}
796836
if( fossil_strcmp(p->zDest, "off")==0 ){
797837
return;
798838
}
839
+ blob_init(&all, 0, 0);
799840
if( fossil_strcmp(p->zDest, "blob")==0 ){
800841
pOut = &p->out;
801842
if( blob_size(pOut) ){
802843
blob_appendf(pOut, "%.72c\n", '=');
803844
}
804845
}else{
805
- blob_init(&all, 0, 0);
806846
pOut = &all;
807847
}
808848
blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
809849
if( p->zFrom==0 || p->zFrom[0]==0 ){
810850
return; /* email-self is not set. Error will be reported separately */
811851
}else if( zFromName ){
812852
blob_appendf(pOut, "From: %s <%s@%s>\r\n",
813853
zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
814
- blob_appendf(pOut, "X-Fossil-From: <%s>\r\n", p->zFrom);
854
+ blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
815855
}else{
816856
blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
817857
}
818858
blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
819859
if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
@@ -879,10 +919,26 @@
879919
fossil_print("%s", blob_str(&all));
880920
}
881921
blob_reset(&all);
882922
}
883923
924
+/*
925
+** SETTING: email-url width=40
926
+** This URL is used as the basename for hyperlinks included in email alert
927
+** text. Omit the trailing "/".
928
+*/
929
+/*
930
+** SETTING: email-admin width=40
931
+** This is the email address for the human administrator for the system.
932
+** Abuse and trouble reports and password reset requests are send here.
933
+*/
934
+/*
935
+** SETTING: email-subname width=16
936
+** This is a short name used to identifies the repository in the Subject:
937
+** line of email alerts. Traditionally this name is included in square
938
+** brackets. Examples: "[fossil-src]", "[sqlite-src]".
939
+*/
884940
/*
885941
** SETTING: email-send-method width=5 default=off
886942
** Determine the method used to send email. Allowed values are
887943
** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
888944
** means no email is ever sent. The "relay" value means emails are sent
@@ -1152,10 +1208,19 @@
11521208
int i, j, n;
11531209
char c;
11541210
11551211
*peErr = 0;
11561212
*pzErr = 0;
1213
+
1214
+ /* Verify the captcha first */
1215
+ if( needCaptcha ){
1216
+ if( !captcha_is_correct(1) ){
1217
+ *peErr = 2;
1218
+ *pzErr = mprintf("incorrect security code");
1219
+ return 0;
1220
+ }
1221
+ }
11571222
11581223
/* Check the validity of the email address.
11591224
**
11601225
** (1) Exactly one '@' character.
11611226
** (2) No other characters besides [a-zA-Z0-9._+-]
@@ -1163,11 +1228,15 @@
11631228
** The local part is currently more restrictive than RFC 5322 allows:
11641229
** https://stackoverflow.com/a/2049510/142454 We will expand this as
11651230
** necessary.
11661231
*/
11671232
zEAddr = P("e");
1168
- if( zEAddr==0 ) return 0;
1233
+ if( zEAddr==0 ){
1234
+ *peErr = 1;
1235
+ *pzErr = mprintf("required");
1236
+ return 0;
1237
+ }
11691238
for(i=j=n=0; (c = zEAddr[i])!=0; i++){
11701239
if( c=='@' ){
11711240
n = i;
11721241
j++;
11731242
continue;
@@ -1193,14 +1262,13 @@
11931262
*peErr = 1;
11941263
*pzErr = mprintf("email domain too short");
11951264
return 0;
11961265
}
11971266
1198
- /* Verify the captcha */
1199
- if( needCaptcha && !captcha_is_correct(1) ){
1200
- *peErr = 2;
1201
- *pzErr = mprintf("incorrect security code");
1267
+ if( authorized_subscription_email(zEAddr)==0 ){
1268
+ *peErr = 1;
1269
+ *pzErr = mprintf("not an authorized email address");
12021270
return 0;
12031271
}
12041272
12051273
/* Check to make sure the email address is available for reuse */
12061274
if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
@@ -1259,11 +1327,11 @@
12591327
** page. To allow anonymous passers-by to sign up for email
12601328
** notification, set Email-Alerts on user "nobody" or "anonymous".
12611329
*/
12621330
void subscribe_page(void){
12631331
int needCaptcha;
1264
- unsigned int uSeed;
1332
+ unsigned int uSeed = 0;
12651333
const char *zDecoded;
12661334
char *zCaptcha = 0;
12671335
char *zErr = 0;
12681336
int eErr = 0;
12691337
int di;
@@ -1291,10 +1359,14 @@
12911359
/* Everybody else jumps to the page to administer their own
12921360
** account only. */
12931361
cgi_redirectf("%R/alerts");
12941362
return;
12951363
}
1364
+ }
1365
+ if( !g.perm.Admin && !db_get_boolean("anon-subscribe",1) ){
1366
+ register_page();
1367
+ return;
12961368
}
12971369
alert_submenu_common();
12981370
needCaptcha = !login_is_individual();
12991371
if( P("submit")
13001372
&& cgi_csrf_safe(1)
@@ -1312,10 +1384,11 @@
13121384
if( PB("sa") ) ssub[nsub++] = 'a';
13131385
if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
13141386
if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
13151387
if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
13161388
if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1389
+ if( g.perm.RdForum && PB("sx") ) ssub[nsub++] = 'x';
13171390
ssub[nsub] = 0;
13181391
db_multi_exec(
13191392
"INSERT INTO subscriber(semail,suname,"
13201393
" sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
13211394
"VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
@@ -1332,11 +1405,15 @@
13321405
id);
13331406
if( !needCaptcha ){
13341407
/* The new subscription has been added on behalf of a logged-in user.
13351408
** No verification is required. Jump immediately to /alerts page.
13361409
*/
1337
- cgi_redirectf("%R/alerts/%s", zCode);
1410
+ if( g.perm.Admin ){
1411
+ cgi_redirectf("%R/alerts/%.32s", zCode);
1412
+ }else{
1413
+ cgi_redirectf("%R/alerts");
1414
+ }
13381415
return;
13391416
}else{
13401417
/* We need to send a verification email */
13411418
Blob hdr, body;
13421419
AlertSender *pSender = alert_sender_new(0,0);
@@ -1354,11 +1431,11 @@
13541431
@ <blockquote><pre>
13551432
@ %h(pSender->zErr)
13561433
@ </pre></blockquote>
13571434
}else{
13581435
@ <p>An email has been sent to "%h(zEAddr)". That email contains a
1359
- @ hyperlink that you must click on in order to activate your
1436
+ @ hyperlink that you must click to activate your
13601437
@ subscription.</p>
13611438
}
13621439
alert_sender_free(pSender);
13631440
style_footer();
13641441
}
@@ -1386,16 +1463,23 @@
13861463
if( eErr==1 ){
13871464
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
13881465
}
13891466
@ </tr>
13901467
if( needCaptcha ){
1391
- uSeed = captcha_seed();
1468
+ const char *zInit = "";
1469
+ if( P("captchaseed")!=0 && eErr!=2 ){
1470
+ uSeed = strtoul(P("captchaseed"),0,10);
1471
+ zInit = P("captcha");
1472
+ }else{
1473
+ uSeed = captcha_seed();
1474
+ }
13921475
zDecoded = captcha_decode(uSeed);
13931476
zCaptcha = captcha_render(zDecoded);
13941477
@ <tr>
13951478
@ <td class="form_label">Security Code:</td>
1396
- @ <td><input type="text" name="captcha" value="" size="30">
1479
+ @ <td><input type="text" name="captcha" value="%h(zInit)" size="30">
1480
+ captcha_speakit_button(uSeed, "Speak the code");
13971481
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
13981482
@ </tr>
13991483
if( eErr==2 ){
14001484
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
14011485
}
@@ -1421,10 +1505,12 @@
14211505
@ Check-ins</label><br>
14221506
}
14231507
if( g.perm.RdForum ){
14241508
@ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
14251509
@ Forum Posts</label><br>
1510
+ @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \
1511
+ @ Forum Edits</label><br>
14261512
}
14271513
if( g.perm.RdTkt ){
14281514
@ <label><input type="checkbox" name="st" %s(PCK("st"))> \
14291515
@ Ticket changes</label><br>
14301516
}
@@ -1461,11 +1547,11 @@
14611547
@ </table>
14621548
if( needCaptcha ){
14631549
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
14641550
@ %h(zCaptcha)
14651551
@ </pre>
1466
- @ Enter the 8 characters above in the "Security Code" box
1552
+ @ Enter the 8 characters above in the "Security Code" box<br/>
14671553
@ </td></tr></table></div>
14681554
}
14691555
@ </form>
14701556
fossil_free(zErr);
14711557
style_footer();
@@ -1474,21 +1560,20 @@
14741560
/*
14751561
** Either shutdown or completely delete a subscription entry given
14761562
** by the hex value zName. Then paint a webpage that explains that
14771563
** the entry has been removed.
14781564
*/
1479
-static void alert_unsubscribe(const char *zName){
1565
+static void alert_unsubscribe(int sid){
14801566
char *zEmail;
14811567
zEmail = db_text(0, "SELECT semail FROM subscriber"
1482
- " WHERE subscriberCode=hextoblob(%Q)", zName);
1568
+ " WHERE subscriberId=%d", sid);
14831569
if( zEmail==0 ){
14841570
style_header("Unsubscribe Fail");
14851571
@ <p>Unable to locate a subscriber with the requested key</p>
14861572
}else{
14871573
db_multi_exec(
1488
- "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1489
- zName
1574
+ "DELETE FROM subscriber WHERE subscriberId=%d", sid
14901575
);
14911576
style_header("Unsubscribed");
14921577
@ <p>The "%h(zEmail)" email address has been delisted.
14931578
@ All traces of that email address have been removed</p>
14941579
}
@@ -1499,108 +1584,149 @@
14991584
/*
15001585
** WEBPAGE: alerts
15011586
**
15021587
** Edit email alert and notification settings.
15031588
**
1504
-** The subscriber is identified in either of two ways:
1505
-**
1506
-** (1) The name= query parameter contains the subscriberCode.
1507
-**
1508
-** (2) The user is logged into an account other than "nobody" or
1509
-** "anonymous". In that case the notification settings
1510
-** associated with that account can be edited without needing
1511
-** to know the subscriber code.
1512
-*/
1513
-void alert_page(void){
1514
- const char *zName = P("name");
1515
- Stmt q;
1516
- int sa, sc, sf, st, sw;
1517
- int sdigest, sdonotcall, sverified;
1518
- const char *ssub;
1519
- const char *semail;
1520
- const char *smip;
1521
- const char *suname;
1522
- const char *mtime;
1523
- const char *sctime;
1524
- int eErr = 0;
1525
- char *zErr = 0;
1526
-
1527
- if( alert_webpages_disabled() ) return;
1528
- login_check_credentials();
1529
- if( !g.perm.EmailAlert ){
1530
- login_needed(g.anon.EmailAlert);
1531
- return;
1532
- }
1533
- if( zName==0 && login_is_individual() ){
1534
- zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
1535
- " WHERE suname=%Q", g.zLogin);
1536
- }
1537
- if( zName==0 || !validate16(zName, -1) ){
1538
- cgi_redirect("subscribe");
1539
- return;
1540
- }
1541
- alert_submenu_common();
1542
- if( P("submit")!=0 && cgi_csrf_safe(1) ){
1543
- int sdonotcall = PB("sdonotcall");
1544
- int sdigest = PB("sdigest");
1545
- char ssub[10];
1546
- int nsub = 0;
1547
- if( PB("sa") ) ssub[nsub++] = 'a';
1548
- if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1549
- if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1550
- if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1551
- if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1552
- ssub[nsub] = 0;
1553
- if( g.perm.Admin ){
1554
- const char *suname = PT("suname");
1555
- int sverified = PB("sverified");
1556
- if( suname && suname[0]==0 ) suname = 0;
1557
- db_multi_exec(
1558
- "UPDATE subscriber SET"
1559
- " sdonotcall=%d,"
1560
- " sdigest=%d,"
1561
- " ssub=%Q,"
1562
- " mtime=strftime('%%s','now'),"
1563
- " smip=%Q,"
1564
- " suname=%Q,"
1565
- " sverified=%d"
1566
- " WHERE subscriberCode=hextoblob(%Q)",
1567
- sdonotcall,
1568
- sdigest,
1569
- ssub,
1570
- g.zIpAddr,
1571
- suname,
1572
- sverified,
1573
- zName
1574
- );
1575
- }else{
1576
- db_multi_exec(
1577
- "UPDATE subscriber SET"
1578
- " sdonotcall=%d,"
1579
- " sdigest=%d,"
1580
- " ssub=%Q,"
1581
- " mtime=strftime('%%s','now'),"
1582
- " smip=%Q"
1583
- " WHERE subscriberCode=hextoblob(%Q)",
1584
- sdonotcall,
1585
- sdigest,
1586
- ssub,
1587
- g.zIpAddr,
1588
- zName
1589
- );
1590
- }
1589
+** The subscriber is identified in several ways:
1590
+**
1591
+** (1) The name= query parameter contains the complete subscriberCode.
1592
+** This only happens when the user receives a verification
1593
+** email and clicks on the link in the email. When a
1594
+** compilete subscriberCode is seen on the name= query parameter,
1595
+** that constitutes verification of the email address.
1596
+**
1597
+** (2) The sid= query parameter contains an integer subscriberId.
1598
+** This only works for the administrator. It allows the
1599
+** administrator to edit any subscription.
1600
+**
1601
+** (3) The user is logged into an account other than "nobody" or
1602
+** "anonymous". In that case the notification settings
1603
+** associated with that account can be edited without needing
1604
+** to know the subscriber code.
1605
+**
1606
+** (4) The name= query parameter contains a 32-digit prefix of
1607
+** subscriber code. (Subscriber codes are normally 64 hex digits
1608
+** in length.) This uniquely identifies the subscriber without
1609
+** revealing the complete subscriber code, and hence without
1610
+** verifying the email address.
1611
+*/
1612
+void alert_page(void){
1613
+ const char *zName = 0; /* Value of the name= query parameter */
1614
+ Stmt q; /* For querying the database */
1615
+ int sa, sc, sf, st, sw, sx; /* Types of notifications requested */
1616
+ int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */
1617
+ int isLogin; /* True if logged in as an individual */
1618
+ const char *ssub = 0; /* Subscription flags */
1619
+ const char *semail = 0; /* Email address */
1620
+ const char *smip; /* */
1621
+ const char *suname = 0; /* Corresponding user.login value */
1622
+ const char *mtime; /* */
1623
+ const char *sctime; /* Time subscription created */
1624
+ int eErr = 0; /* Type of error */
1625
+ char *zErr = 0; /* Error message text */
1626
+ int sid = 0; /* Subscriber ID */
1627
+ int nName; /* Length of zName in bytes */
1628
+ char *zHalfCode; /* prefix of subscriberCode */
1629
+
1630
+ db_begin_transaction();
1631
+ if( alert_webpages_disabled() ){
1632
+ db_commit_transaction();
1633
+ return;
1634
+ }
1635
+ login_check_credentials();
1636
+ if( !g.perm.EmailAlert ){
1637
+ db_commit_transaction();
1638
+ login_needed(g.anon.EmailAlert);
1639
+ /*NOTREACHED*/
1640
+ }
1641
+ isLogin = login_is_individual();
1642
+ zName = P("name");
1643
+ nName = zName ? (int)strlen(zName) : 0;
1644
+ if( g.perm.Admin && P("sid")!=0 ){
1645
+ sid = atoi(P("sid"));
1646
+ }
1647
+ if( sid==0 && nName>=32 ){
1648
+ sid = db_int(0,
1649
+ "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')"
1650
+ " THEN subscriberId ELSE 0 END"
1651
+ " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)"
1652
+ " LIMIT 1", zName, zName);
1653
+ }
1654
+ if( sid==0 && isLogin ){
1655
+ sid = db_int(0, "SELECT subscriberId FROM subscriber"
1656
+ " WHERE suname=%Q", g.zLogin);
1657
+ }
1658
+ if( sid==0 ){
1659
+ db_commit_transaction();
1660
+ cgi_redirect("subscribe");
1661
+ /*NOTREACHED*/
1662
+ }
1663
+ alert_submenu_common();
1664
+ if( P("submit")!=0 && cgi_csrf_safe(1) ){
1665
+ char newSsub[10];
1666
+ int nsub = 0;
1667
+ Blob update;
1668
+
1669
+ sdonotcall = PB("sdonotcall");
1670
+ sdigest = PB("sdigest");
1671
+ semail = P("semail");
1672
+ if( PB("sa") ) newSsub[nsub++] = 'a';
1673
+ if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c';
1674
+ if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f';
1675
+ if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't';
1676
+ if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w';
1677
+ if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x';
1678
+ newSsub[nsub] = 0;
1679
+ ssub = newSsub;
1680
+ blob_init(&update, "UPDATE subscriber SET", -1);
1681
+ blob_append_sql(&update,
1682
+ " sdonotcall=%d,"
1683
+ " sdigest=%d,"
1684
+ " ssub=%Q,"
1685
+ " mtime=strftime('%%s','now'),"
1686
+ " smip=%Q",
1687
+ sdonotcall,
1688
+ sdigest,
1689
+ ssub,
1690
+ g.zIpAddr
1691
+ );
1692
+ if( g.perm.Admin ){
1693
+ suname = PT("suname");
1694
+ sverified = PB("sverified");
1695
+ if( suname && suname[0]==0 ) suname = 0;
1696
+ blob_append_sql(&update,
1697
+ ", suname=%Q,"
1698
+ " sverified=%d",
1699
+ suname,
1700
+ sverified
1701
+ );
1702
+ }
1703
+ if( isLogin ){
1704
+ if( semail==0 || email_address_is_valid(semail,0)==0 ){
1705
+ eErr = 8;
1706
+ }
1707
+ blob_append_sql(&update, ", semail=%Q", semail);
1708
+ }
1709
+ blob_append_sql(&update," WHERE subscriberId=%d", sid);
1710
+ if( eErr==0 ){
1711
+ db_exec_sql(blob_str(&update));
1712
+ ssub = 0;
1713
+ }
1714
+ blob_reset(&update);
15911715
}
15921716
if( P("delete")!=0 && cgi_csrf_safe(1) ){
15931717
if( !PB("dodelete") ){
15941718
eErr = 9;
15951719
zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
15961720
" unsubscribe");
15971721
}else{
1598
- alert_unsubscribe(zName);
1599
- return;
1722
+ alert_unsubscribe(sid);
1723
+ db_commit_transaction();
1724
+ return;
16001725
}
16011726
}
1727
+ style_header("Update Subscription");
16021728
db_prepare(&q,
16031729
"SELECT"
16041730
" semail," /* 0 */
16051731
" sverified," /* 1 */
16061732
" sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
16071733
" sdigest," /* 3 */
16081734
" ssub," /* 4 */
16091735
" smip," /* 5 */
16101736
" suname," /* 6 */
16111737
" datetime(mtime,'unixepoch')," /* 7 */
1612
- " datetime(sctime,'unixepoch')" /* 8 */
1613
- " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
1738
+ " datetime(sctime,'unixepoch')," /* 8 */
1739
+ " hex(subscriberCode)" /* 9 */
1740
+ " FROM subscriber WHERE subscriberId=%d", sid);
16141741
if( db_step(&q)!=SQLITE_ROW ){
16151742
db_finalize(&q);
1743
+ db_commit_transaction();
16161744
cgi_redirect("subscribe");
1617
- return;
1618
- }
1619
- style_header("Update Subscription");
1620
- semail = db_column_text(&q, 0);
1621
- sverified = db_column_int(&q, 1);
1622
- sdonotcall = db_column_int(&q, 2);
1623
- sdigest = db_column_int(&q, 3);
1624
- ssub = db_column_text(&q, 4);
1745
+ /*NOTREACHED*/
1746
+ }
1747
+ if( ssub==0 ){
1748
+ semail = db_column_text(&q, 0);
1749
+ sdonotcall = db_column_int(&q, 2);
1750
+ sdigest = db_column_int(&q, 3);
1751
+ ssub = db_column_text(&q, 4);
1752
+ }
1753
+ if( suname==0 ){
1754
+ suname = db_column_text(&q, 6);
1755
+ sverified = db_column_int(&q, 1);
1756
+ }
16251757
sa = strchr(ssub,'a')!=0;
16261758
sc = strchr(ssub,'c')!=0;
16271759
sf = strchr(ssub,'f')!=0;
16281760
st = strchr(ssub,'t')!=0;
16291761
sw = strchr(ssub,'w')!=0;
1762
+ sx = strchr(ssub,'x')!=0;
16301763
smip = db_column_text(&q, 5);
1631
- suname = db_column_text(&q, 6);
16321764
mtime = db_column_text(&q, 7);
16331765
sctime = db_column_text(&q, 8);
16341766
if( !g.perm.Admin && !sverified ){
1635
- db_multi_exec(
1636
- "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1637
- zName);
1638
- @ <h1>Your email alert subscription has been verified!</h1>
1639
- @ <p>Use the form below to update your subscription information.</p>
1640
- @ <p>Hint: Bookmark this page so that you can more easily update
1641
- @ your subscription information in the future</p>
1767
+ if( nName==64 ){
1768
+ db_multi_exec(
1769
+ "UPDATE subscriber SET sverified=1"
1770
+ " WHERE subscriberCode=hextoblob(%Q)",
1771
+ zName);
1772
+ if( db_get_boolean("selfreg-verify",0) ){
1773
+ char *zNewCap = db_get("default-perms","u");
1774
+ db_multi_exec(
1775
+ "UPDATE user"
1776
+ " SET cap=%Q"
1777
+ " WHERE cap='7' AND login=("
1778
+ " SELECT suname FROM subscriber"
1779
+ " WHERE subscriberCode=hextoblob(%Q))",
1780
+ zNewCap, zName
1781
+ );
1782
+ login_set_capabilities(zNewCap, 0);
1783
+ }
1784
+ @ <h1>Your email alert subscription has been verified!</h1>
1785
+ @ <p>Use the form below to update your subscription information.</p>
1786
+ @ <p>Hint: Bookmark this page so that you can more easily update
1787
+ @ your subscription information in the future</p>
1788
+ }else{
1789
+ @ <h2>Your email address is unverified</h2>
1790
+ @ <p>You should have received an email message containing a link
1791
+ @ that you must visit to verify your account. No email notifications
1792
+ @ will be sent until your email address has been verified.</p>
1793
+ }
16421794
}else{
16431795
@ <p>Make changes to the email subscription shown below and
16441796
@ press "Submit".</p>
16451797
}
16461798
form_begin(0, "%R/alerts");
1647
- @ <input type="hidden" name="name" value="%h(zName)">
1799
+ zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))"
1800
+ " FROM subscriber WHERE subscriberId=%d", sid);
1801
+ @ <input type="hidden" name="name" value="%h(zHalfCode)">
16481802
@ <table class="subscribe">
16491803
@ <tr>
16501804
@ <td class="form_label">Email&nbsp;Address:</td>
1651
- @ <td>%h(semail)</td>
1805
+ if( isLogin ){
1806
+ @ <td><input type="text" name="semail" value="%h(semail)" size="30">\
1807
+ if( eErr==8 ){
1808
+ @ <span class='loginError'>&larr; not a valid email address!</span>
1809
+ }else if( g.perm.Admin ){
1810
+ @ &nbsp;&nbsp;<a href="%R/announce?to=%t(semail)">\
1811
+ @ (Send a message to %h(semail))</a>\
1812
+ }
1813
+ @ </td>
1814
+ }else{
1815
+ @ <td>%h(semail)</td>
1816
+ }
16521817
@ </tr>
16531818
if( g.perm.Admin ){
1819
+ int uid;
16541820
@ <tr>
16551821
@ <td class='form_label'>Created:</td>
16561822
@ <td>%h(sctime)</td>
16571823
@ </tr>
16581824
@ <tr>
@@ -1662,13 +1828,21 @@
16621828
@ <tr>
16631829
@ <td class='form_label'>IP Address:</td>
16641830
@ <td>%h(smip)</td>
16651831
@ </tr>
16661832
@ <tr>
1833
+ @ <td class='form_label'>Subscriber&nbsp;Code:</td>
1834
+ @ <td>%h(db_column_text(&q,9))</td>
1835
+ @ <tr>
16671836
@ <td class="form_label">User:</td>
16681837
@ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1669
- @ size="30"></td>
1838
+ @ size="30">\
1839
+ uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname);
1840
+ if( uid ){
1841
+ @ &nbsp;&nbsp;<a href='%R/setup_uedit?id=%d(uid)'>\
1842
+ @ (login info for %h(suname))</a>\
1843
+ }
16701844
@ </tr>
16711845
}
16721846
@ <tr>
16731847
@ <td class="form_label">Topics:</td>
16741848
@ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
16781852
@ Check-ins</label><br>
16791853
}
16801854
if( g.perm.RdForum ){
16811855
@ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
16821856
@ Forum Posts</label><br>
1857
+ @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\
1858
+ @ Forum Edits</label><br>
16831859
}
16841860
if( g.perm.RdTkt ){
16851861
@ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
16861862
@ Ticket changes</label><br>
16871863
}
@@ -1695,19 +1871,15 @@
16951871
@ <td><select size="1" name="sdigest">
16961872
@ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
16971873
@ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
16981874
@ </select></td>
16991875
@ </tr>
1700
-#if 0
1701
- @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1702
- @ Daily digest only</label><br>
1703
-#endif
17041876
if( g.perm.Admin ){
17051877
@ <tr>
17061878
@ <td class="form_label">Admin Options:</td><td>
17071879
@ <label><input type="checkbox" name="sdonotcall" \
1708
- @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1880
+ @ %s(sdonotcall?"checked":"")> Do not disturb</label><br>
17091881
@ <label><input type="checkbox" name="sverified" \
17101882
@ %s(sverified?"checked":"")>\
17111883
@ Verified</label></td></tr>
17121884
}
17131885
if( eErr==9 ){
@@ -1726,10 +1898,12 @@
17261898
@ </table>
17271899
@ </form>
17281900
fossil_free(zErr);
17291901
db_finalize(&q);
17301902
style_footer();
1903
+ db_commit_transaction();
1904
+ return;
17311905
}
17321906
17331907
/* This is the message that gets sent to describe how to change
17341908
** or modify a subscription
17351909
*/
@@ -1760,25 +1934,26 @@
17601934
*/
17611935
void unsubscribe_page(void){
17621936
const char *zName = P("name");
17631937
char *zErr = 0;
17641938
int eErr = 0;
1765
- unsigned int uSeed;
1939
+ unsigned int uSeed = 0;
17661940
const char *zDecoded;
17671941
char *zCaptcha = 0;
17681942
int dx;
17691943
int bSubmit;
17701944
const char *zEAddr;
17711945
char *zCode = 0;
1946
+ int sid = 0;
17721947
17731948
/* If a valid subscriber code is supplied, then unsubscribe immediately.
17741949
*/
17751950
if( zName
1776
- && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1777
- zName)
1951
+ && (sid = db_int(0, "SELECT subscriberId FROM subscriber"
1952
+ " WHERE subscriberCode=hextoblob(%Q)", zName))!=0
17781953
){
1779
- alert_unsubscribe(zName);
1954
+ alert_unsubscribe(sid);
17801955
return;
17811956
}
17821957
17831958
/* Logged in users are redirected to the /alerts page */
17841959
login_check_credentials();
@@ -1855,10 +2030,11 @@
18552030
zDecoded = captcha_decode(uSeed);
18562031
zCaptcha = captcha_render(zDecoded);
18572032
@ <tr>
18582033
@ <td class="form_label">Security Code:</td>
18592034
@ <td><input type="text" name="captcha" value="" size="30">
2035
+ captcha_speakit_button(uSeed, "Speak the code");
18602036
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
18612037
if( eErr==2 ){
18622038
@ <td><span class="loginError">&larr; %h(zErr)</span></td>
18632039
}
18642040
@ </tr>
@@ -1874,11 +2050,11 @@
18742050
@ </tr>
18752051
@ </table>
18762052
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
18772053
@ %h(zCaptcha)
18782054
@ </pre>
1879
- @ Enter the 8 characters above in the "Security Code" box
2055
+ @ Enter the 8 characters above in the "Security Code" box<br/>
18802056
@ </td></tr></table></div>
18812057
@ </form>
18822058
fossil_free(zErr);
18832059
style_footer();
18842060
}
@@ -1885,37 +2061,69 @@
18852061
18862062
/*
18872063
** WEBPAGE: subscribers
18882064
**
18892065
** This page, accessible to administrators only,
1890
-** shows a list of email notification email addresses.
2066
+** shows a list of subscriber email addresses.
18912067
** Clicking on an email takes one to the /alerts page
18922068
** for that email where the delivery settings can be
18932069
** modified.
18942070
*/
18952071
void subscriber_list_page(void){
18962072
Blob sql;
18972073
Stmt q;
18982074
sqlite3_int64 iNow;
2075
+ int nTotal;
2076
+ int nPending;
2077
+ int nDel = 0;
18992078
if( alert_webpages_disabled() ) return;
19002079
login_check_credentials();
19012080
if( !g.perm.Admin ){
19022081
login_needed(0);
19032082
return;
19042083
}
19052084
alert_submenu_common();
2085
+ style_submenu_element("Users","setup_ulist");
19062086
style_header("Subscriber List");
2087
+ nTotal = db_int(0, "SELECT count(*) FROM subscriber");
2088
+ nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified");
2089
+ if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){
2090
+ int nNewPending;
2091
+ db_multi_exec(
2092
+ "DELETE FROM subscriber"
2093
+ " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')"
2094
+ );
2095
+ nNewPending = db_int(0, "SELECT count(*) FROM subscriber"
2096
+ " WHERE NOT sverified");
2097
+ nDel = nPending - nNewPending;
2098
+ nPending = nNewPending;
2099
+ nTotal -= nDel;
2100
+ }
2101
+ if( nPending>0 ){
2102
+ @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1>
2103
+ if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber"
2104
+ " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')")
2105
+ ){
2106
+ style_submenu_element("Purge Pending","subscribers?purge");
2107
+ }
2108
+ }else{
2109
+ @ <h1>%,d(nTotal) Subscribers</h1>
2110
+ }
2111
+ if( nDel>0 ){
2112
+ @ <p>*** %d(nDel) pending subscriptions deleted ***</p>
2113
+ }
19072114
blob_init(&sql, 0, 0);
19082115
blob_append_sql(&sql,
1909
- "SELECT hex(subscriberCode)," /* 0 */
2116
+ "SELECT subscriberId," /* 0 */
19102117
" semail," /* 1 */
19112118
" ssub," /* 2 */
19122119
" suname," /* 3 */
19132120
" sverified," /* 4 */
19142121
" sdigest," /* 5 */
19152122
" mtime," /* 6 */
1916
- " date(sctime,'unixepoch')" /* 7 */
2123
+ " date(sctime,'unixepoch')," /* 7 */
2124
+ " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */
19172125
" FROM subscriber"
19182126
);
19192127
if( P("only")!=0 ){
19202128
blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
19212129
style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
19372145
@ </tr>
19382146
@ </thead><tbody>
19392147
while( db_step(&q)==SQLITE_ROW ){
19402148
sqlite3_int64 iMtime = db_column_int64(&q, 6);
19412149
double rAge = (iNow - iMtime)/86400.0;
2150
+ int uid = db_column_int(&q, 8);
2151
+ const char *zUname = db_column_text(&q, 3);
19422152
@ <tr>
1943
- @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
2153
+ @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\
19442154
@ %h(db_column_text(&q,1))</a></td>
19452155
@ <td>%h(db_column_text(&q,2))</td>
19462156
@ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1947
- @ <td>%h(db_column_text(&q,3))</td>
2157
+ if( uid ){
2158
+ @ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a>
2159
+ }else{
2160
+ @ <td>%h(zUname)</td>
2161
+ }
19482162
@ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
19492163
@ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
19502164
@ <td>%h(db_column_text(&q,7))</td>
19512165
@ </tr>
19522166
}
@@ -1958,13 +2172,21 @@
19582172
19592173
#if LOCAL_INTERFACE
19602174
/*
19612175
** A single event that might appear in an alert is recorded as an
19622176
** instance of the following object.
2177
+**
2178
+** type values:
2179
+**
2180
+** c A new check-in
2181
+** f An original forum post
2182
+** x An edit to a prior forum post
2183
+** t A new ticket or a change to an existing ticket
2184
+** w A change to a wiki page
19632185
*/
19642186
struct EmailEvent {
1965
- int type; /* 'c', 'f', 'm', 't', 'w' */
2187
+ int type; /* 'c', 'f', 't', 'w', 'x' */
19662188
int needMod; /* Pending moderator approval */
19672189
Blob hdr; /* Header content, for forum entries */
19682190
Blob txt; /* Text description to appear in an alert */
19692191
char *zFromName; /* Human name of the sender */
19702192
EmailEvent *pNext; /* Next in chronological order */
@@ -2039,20 +2261,20 @@
20392261
p->needMod = db_column_int(&q, 4);
20402262
p->zFromName = 0;
20412263
p->pNext = 0;
20422264
switch( p->type ){
20432265
case 'c': zType = "Check-In"; break;
2044
- case 'f': zType = "Forum post"; break;
2266
+ /* case 'f': -- forum posts omitted from this loop. See below */
20452267
case 't': zType = "Ticket Change"; break;
20462268
case 'w': zType = "Wiki Edit"; break;
20472269
}
20482270
blob_init(&p->hdr, 0, 0);
20492271
blob_init(&p->txt, 0, 0);
20502272
blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
20512273
db_column_text(&q,1),
20522274
zType,
2053
- db_column_text(&q,2),
2275
+ db_column_text(&q, 2),
20542276
zUrl,
20552277
db_column_text(&q,0)
20562278
);
20572279
if( p->needMod ){
20582280
blob_appendf(&p->txt,
@@ -2078,17 +2300,28 @@
20782300
** is a normal email alert. Construct full-text forum post alerts
20792301
** using a format that enables them to be sent as separate emails.
20802302
*/
20812303
db_prepare(&q,
20822304
"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 */
20902323
" FROM temp.wantalert, event, forumpost"
20912324
" LEFT JOIN user ON (login=coalesce(euser,user))"
20922325
" WHERE event.objid=substr(wantalert.eventId,2)+0"
20932326
" AND eventId GLOB 'f*'"
20942327
" AND forumpost.fpid=event.objid"
@@ -2104,11 +2337,11 @@
21042337
const char *z;
21052338
if( pPost==0 ) continue;
21062339
p = fossil_malloc( sizeof(EmailEvent) );
21072340
pLast->pNext = p;
21082341
pLast = p;
2109
- p->type = 'f';
2342
+ p->type = db_column_int(&q,7) ? 'f' : 'x';
21102343
p->needMod = db_column_int(&q, 5);
21112344
z = db_column_text(&q,6);
21122345
p->zFromName = z && z[0] ? fossil_strdup(z) : 0;
21132346
p->pNext = 0;
21142347
blob_init(&p->hdr, 0, 0);
@@ -2436,13 +2669,13 @@
24362669
** if the recipient is a moderator for that type of event. Setup
24372670
** and Admin users always get notified. */
24382671
char xType = '*';
24392672
if( strpbrk(zCap,"as")==0 ){
24402673
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;
24442677
}
24452678
if( strchr(zCap,xType)==0 ) continue;
24462679
}
24472680
}else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){
24482681
/* Setup and admin users can get any notification that does not
@@ -2450,14 +2683,14 @@
24502683
}else{
24512684
/* Other users only see the alert if they have sufficient
24522685
** privilege to view the event itself */
24532686
char xType = '*';
24542687
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;
24592692
}
24602693
if( strchr(zCap,xType)==0 ) continue;
24612694
}
24622695
if( blob_size(&p->hdr)>0 ){
24632696
/* This alert should be sent as a separate email */
@@ -2594,10 +2827,11 @@
25942827
@ <table class="subscribe">
25952828
if( zCaptcha ){
25962829
@ <tr>
25972830
@ <td class="form_label">Security&nbsp;Code:</td>
25982831
@ <td><input type="text" name="captcha" value="" size="10">
2832
+ captcha_speakit_button(uSeed, "Speak the code");
25992833
@ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
26002834
@ </tr>
26012835
}
26022836
@ <tr>
26032837
@ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
@@ -2620,11 +2854,11 @@
26202854
@ </table>
26212855
if( zCaptcha ){
26222856
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
26232857
@ %h(zCaptcha)
26242858
@ </pre>
2625
- @ Enter the 8 characters above in the "Security Code" box
2859
+ @ Enter the 8 characters above in the "Security Code" box<br/>
26262860
@ </td></tr></table></div>
26272861
}
26282862
@ </form>
26292863
style_footer();
26302864
}
@@ -2638,10 +2872,11 @@
26382872
char *zErr;
26392873
const char *zTo = PT("to");
26402874
char *zSubject = PT("subject");
26412875
int bAll = PB("all");
26422876
int bAA = PB("aa");
2877
+ int bMods = PB("mods");
26432878
const char *zSub = db_get("email-subname", "[Fossil Repo]");
26442879
int bTest2 = fossil_strcmp(P("name"),"test2")==0;
26452880
Blob hdr, body;
26462881
blob_init(&body, 0, 0);
26472882
blob_init(&hdr, 0, 0);
@@ -2649,17 +2884,29 @@
26492884
pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
26502885
if( zTo[0] ){
26512886
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
26522887
alert_send(pSender, &hdr, &body, 0);
26532888
}
2654
- if( bAll || bAA ){
2889
+ if( bAll || bAA || bMods ){
26552890
Stmt q;
26562891
int nUsed = blob_size(&body);
26572892
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
+ }
26612908
while( db_step(&q)==SQLITE_ROW ){
26622909
const char *zCode = db_column_text(&q, 1);
26632910
zTo = db_column_text(&q, 0);
26642911
blob_truncate(&hdr, 0);
26652912
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
@@ -2716,11 +2963,12 @@
27162963
@ <p>The following error was reported by the system:
27172964
@ <blockquote><pre>
27182965
@ %h(zErr)
27192966
@ </pre></blockquote>
27202967
}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>
27222970
}
27232971
style_footer();
27242972
return;
27252973
} else if( !alert_enabled() ){
27262974
style_header("Cannot Send Announcement");
@@ -2734,21 +2982,26 @@
27342982
@ <form method="POST">
27352983
@ <table class="subscribe">
27362984
if( g.perm.Admin ){
27372985
int aa = PB("aa");
27382986
int all = PB("all");
2987
+ int aMod = PB("mods");
27392988
const char *aack = aa ? "checked" : "";
27402989
const char *allck = all ? "checked" : "";
2990
+ const char *modck = aMod ? "checked" : "";
27412991
@ <tr>
27422992
@ <td class="form_label">To:</td>
27432993
@ <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
27442994
@ <label><input type="checkbox" name="aa" %s(aack)> \
27452995
@ All "announcement" subscribers</label> \
27462996
@ <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
27472997
@ <label><input type="checkbox" name="all" %s(allck)> \
27482998
@ 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>
27503003
@ </tr>
27513004
}
27523005
@ <tr>
27533006
@ <td class="form_label">Subject:</td>
27543007
@ <td><input type="text" name="subject" value="%h(PT("subject"))"\
@@ -2759,11 +3012,15 @@
27593012
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
27603013
@ %h(PT("msg"))</textarea>
27613014
@ </tr>
27623015
@ <tr>
27633016
@ <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
+ }
27653022
@ </tr>
27663023
@ </table>
27673024
@ </form>
27683025
style_footer();
27693026
}
27703027
--- 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'>&uarr; %h(zErr)</span></td></tr>
1388 }
1389 @ </tr>
1390 if( needCaptcha ){
1391 uSeed = captcha_seed();
 
 
 
 
 
 
1392 zDecoded = captcha_decode(uSeed);
1393 zCaptcha = captcha_render(zDecoded);
1394 @ <tr>
1395 @ <td class="form_label">Security Code:</td>
1396 @ <td><input type="text" name="captcha" value="" size="30">
 
1397 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1398 @ </tr>
1399 if( eErr==2 ){
1400 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1401 }
@@ -1421,10 +1505,12 @@
1421 @ Check-ins</label><br>
1422 }
1423 if( g.perm.RdForum ){
1424 @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
1425 @ Forum Posts</label><br>
 
 
1426 }
1427 if( g.perm.RdTkt ){
1428 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
1429 @ Ticket changes</label><br>
1430 }
@@ -1461,11 +1547,11 @@
1461 @ </table>
1462 if( needCaptcha ){
1463 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1464 @ %h(zCaptcha)
1465 @ </pre>
1466 @ Enter the 8 characters above in the "Security Code" box
1467 @ </td></tr></table></div>
1468 }
1469 @ </form>
1470 fossil_free(zErr);
1471 style_footer();
@@ -1474,21 +1560,20 @@
1474 /*
1475 ** Either shutdown or completely delete a subscription entry given
1476 ** by the hex value zName. Then paint a webpage that explains that
1477 ** the entry has been removed.
1478 */
1479 static void alert_unsubscribe(const char *zName){
1480 char *zEmail;
1481 zEmail = db_text(0, "SELECT semail FROM subscriber"
1482 " WHERE subscriberCode=hextoblob(%Q)", zName);
1483 if( zEmail==0 ){
1484 style_header("Unsubscribe Fail");
1485 @ <p>Unable to locate a subscriber with the requested key</p>
1486 }else{
1487 db_multi_exec(
1488 "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1489 zName
1490 );
1491 style_header("Unsubscribed");
1492 @ <p>The "%h(zEmail)" email address has been delisted.
1493 @ All traces of that email address have been removed</p>
1494 }
@@ -1499,108 +1584,149 @@
1499 /*
1500 ** WEBPAGE: alerts
1501 **
1502 ** Edit email alert and notification settings.
1503 **
1504 ** The subscriber is identified in either of two ways:
1505 **
1506 ** (1) The name= query parameter contains the subscriberCode.
1507 **
1508 ** (2) The user is logged into an account other than "nobody" or
1509 ** "anonymous". In that case the notification settings
1510 ** associated with that account can be edited without needing
1511 ** to know the subscriber code.
1512 */
1513 void alert_page(void){
1514 const char *zName = P("name");
1515 Stmt q;
1516 int sa, sc, sf, st, sw;
1517 int sdigest, sdonotcall, sverified;
1518 const char *ssub;
1519 const char *semail;
1520 const char *smip;
1521 const char *suname;
1522 const char *mtime;
1523 const char *sctime;
1524 int eErr = 0;
1525 char *zErr = 0;
1526
1527 if( alert_webpages_disabled() ) return;
1528 login_check_credentials();
1529 if( !g.perm.EmailAlert ){
1530 login_needed(g.anon.EmailAlert);
1531 return;
1532 }
1533 if( zName==0 && login_is_individual() ){
1534 zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
1535 " WHERE suname=%Q", g.zLogin);
1536 }
1537 if( zName==0 || !validate16(zName, -1) ){
1538 cgi_redirect("subscribe");
1539 return;
1540 }
1541 alert_submenu_common();
1542 if( P("submit")!=0 && cgi_csrf_safe(1) ){
1543 int sdonotcall = PB("sdonotcall");
1544 int sdigest = PB("sdigest");
1545 char ssub[10];
1546 int nsub = 0;
1547 if( PB("sa") ) ssub[nsub++] = 'a';
1548 if( g.perm.Read && PB("sc") ) ssub[nsub++] = 'c';
1549 if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
1550 if( g.perm.RdTkt && PB("st") ) ssub[nsub++] = 't';
1551 if( g.perm.RdWiki && PB("sw") ) ssub[nsub++] = 'w';
1552 ssub[nsub] = 0;
1553 if( g.perm.Admin ){
1554 const char *suname = PT("suname");
1555 int sverified = PB("sverified");
1556 if( suname && suname[0]==0 ) suname = 0;
1557 db_multi_exec(
1558 "UPDATE subscriber SET"
1559 " sdonotcall=%d,"
1560 " sdigest=%d,"
1561 " ssub=%Q,"
1562 " mtime=strftime('%%s','now'),"
1563 " smip=%Q,"
1564 " suname=%Q,"
1565 " sverified=%d"
1566 " WHERE subscriberCode=hextoblob(%Q)",
1567 sdonotcall,
1568 sdigest,
1569 ssub,
1570 g.zIpAddr,
1571 suname,
1572 sverified,
1573 zName
1574 );
1575 }else{
1576 db_multi_exec(
1577 "UPDATE subscriber SET"
1578 " sdonotcall=%d,"
1579 " sdigest=%d,"
1580 " ssub=%Q,"
1581 " mtime=strftime('%%s','now'),"
1582 " smip=%Q"
1583 " WHERE subscriberCode=hextoblob(%Q)",
1584 sdonotcall,
1585 sdigest,
1586 ssub,
1587 g.zIpAddr,
1588 zName
1589 );
1590 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1591 }
1592 if( P("delete")!=0 && cgi_csrf_safe(1) ){
1593 if( !PB("dodelete") ){
1594 eErr = 9;
1595 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
1596 " unsubscribe");
1597 }else{
1598 alert_unsubscribe(zName);
1599 return;
 
1600 }
1601 }
 
1602 db_prepare(&q,
1603 "SELECT"
1604 " semail," /* 0 */
1605 " sverified," /* 1 */
1606 " sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
1607 " sdigest," /* 3 */
1608 " ssub," /* 4 */
1609 " smip," /* 5 */
1610 " suname," /* 6 */
1611 " datetime(mtime,'unixepoch')," /* 7 */
1612 " datetime(sctime,'unixepoch')" /* 8 */
1613 " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
 
1614 if( db_step(&q)!=SQLITE_ROW ){
1615 db_finalize(&q);
 
1616 cgi_redirect("subscribe");
1617 return;
1618 }
1619 style_header("Update Subscription");
1620 semail = db_column_text(&q, 0);
1621 sverified = db_column_int(&q, 1);
1622 sdonotcall = db_column_int(&q, 2);
1623 sdigest = db_column_int(&q, 3);
1624 ssub = db_column_text(&q, 4);
 
 
 
 
1625 sa = strchr(ssub,'a')!=0;
1626 sc = strchr(ssub,'c')!=0;
1627 sf = strchr(ssub,'f')!=0;
1628 st = strchr(ssub,'t')!=0;
1629 sw = strchr(ssub,'w')!=0;
 
1630 smip = db_column_text(&q, 5);
1631 suname = db_column_text(&q, 6);
1632 mtime = db_column_text(&q, 7);
1633 sctime = db_column_text(&q, 8);
1634 if( !g.perm.Admin && !sverified ){
1635 db_multi_exec(
1636 "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1637 zName);
1638 @ <h1>Your email alert subscription has been verified!</h1>
1639 @ <p>Use the form below to update your subscription information.</p>
1640 @ <p>Hint: Bookmark this page so that you can more easily update
1641 @ your subscription information in the future</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1642 }else{
1643 @ <p>Make changes to the email subscription shown below and
1644 @ press "Submit".</p>
1645 }
1646 form_begin(0, "%R/alerts");
1647 @ <input type="hidden" name="name" value="%h(zName)">
 
 
1648 @ <table class="subscribe">
1649 @ <tr>
1650 @ <td class="form_label">Email&nbsp;Address:</td>
1651 @ <td>%h(semail)</td>
 
 
 
 
 
 
 
 
 
 
 
1652 @ </tr>
1653 if( g.perm.Admin ){
 
1654 @ <tr>
1655 @ <td class='form_label'>Created:</td>
1656 @ <td>%h(sctime)</td>
1657 @ </tr>
1658 @ <tr>
@@ -1662,13 +1828,21 @@
1662 @ <tr>
1663 @ <td class='form_label'>IP Address:</td>
1664 @ <td>%h(smip)</td>
1665 @ </tr>
1666 @ <tr>
 
 
 
1667 @ <td class="form_label">User:</td>
1668 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1669 @ size="30"></td>
 
 
 
 
 
1670 @ </tr>
1671 }
1672 @ <tr>
1673 @ <td class="form_label">Topics:</td>
1674 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
1678 @ Check-ins</label><br>
1679 }
1680 if( g.perm.RdForum ){
1681 @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
1682 @ Forum Posts</label><br>
 
 
1683 }
1684 if( g.perm.RdTkt ){
1685 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1686 @ Ticket changes</label><br>
1687 }
@@ -1695,19 +1871,15 @@
1695 @ <td><select size="1" name="sdigest">
1696 @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
1697 @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
1698 @ </select></td>
1699 @ </tr>
1700 #if 0
1701 @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1702 @ Daily digest only</label><br>
1703 #endif
1704 if( g.perm.Admin ){
1705 @ <tr>
1706 @ <td class="form_label">Admin Options:</td><td>
1707 @ <label><input type="checkbox" name="sdonotcall" \
1708 @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1709 @ <label><input type="checkbox" name="sverified" \
1710 @ %s(sverified?"checked":"")>\
1711 @ Verified</label></td></tr>
1712 }
1713 if( eErr==9 ){
@@ -1726,10 +1898,12 @@
1726 @ </table>
1727 @ </form>
1728 fossil_free(zErr);
1729 db_finalize(&q);
1730 style_footer();
 
 
1731 }
1732
1733 /* This is the message that gets sent to describe how to change
1734 ** or modify a subscription
1735 */
@@ -1760,25 +1934,26 @@
1760 */
1761 void unsubscribe_page(void){
1762 const char *zName = P("name");
1763 char *zErr = 0;
1764 int eErr = 0;
1765 unsigned int uSeed;
1766 const char *zDecoded;
1767 char *zCaptcha = 0;
1768 int dx;
1769 int bSubmit;
1770 const char *zEAddr;
1771 char *zCode = 0;
 
1772
1773 /* If a valid subscriber code is supplied, then unsubscribe immediately.
1774 */
1775 if( zName
1776 && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1777 zName)
1778 ){
1779 alert_unsubscribe(zName);
1780 return;
1781 }
1782
1783 /* Logged in users are redirected to the /alerts page */
1784 login_check_credentials();
@@ -1855,10 +2030,11 @@
1855 zDecoded = captcha_decode(uSeed);
1856 zCaptcha = captcha_render(zDecoded);
1857 @ <tr>
1858 @ <td class="form_label">Security Code:</td>
1859 @ <td><input type="text" name="captcha" value="" size="30">
 
1860 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1861 if( eErr==2 ){
1862 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1863 }
1864 @ </tr>
@@ -1874,11 +2050,11 @@
1874 @ </tr>
1875 @ </table>
1876 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1877 @ %h(zCaptcha)
1878 @ </pre>
1879 @ Enter the 8 characters above in the "Security Code" box
1880 @ </td></tr></table></div>
1881 @ </form>
1882 fossil_free(zErr);
1883 style_footer();
1884 }
@@ -1885,37 +2061,69 @@
1885
1886 /*
1887 ** WEBPAGE: subscribers
1888 **
1889 ** This page, accessible to administrators only,
1890 ** shows a list of email notification email addresses.
1891 ** Clicking on an email takes one to the /alerts page
1892 ** for that email where the delivery settings can be
1893 ** modified.
1894 */
1895 void subscriber_list_page(void){
1896 Blob sql;
1897 Stmt q;
1898 sqlite3_int64 iNow;
 
 
 
1899 if( alert_webpages_disabled() ) return;
1900 login_check_credentials();
1901 if( !g.perm.Admin ){
1902 login_needed(0);
1903 return;
1904 }
1905 alert_submenu_common();
 
1906 style_header("Subscriber List");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1907 blob_init(&sql, 0, 0);
1908 blob_append_sql(&sql,
1909 "SELECT hex(subscriberCode)," /* 0 */
1910 " semail," /* 1 */
1911 " ssub," /* 2 */
1912 " suname," /* 3 */
1913 " sverified," /* 4 */
1914 " sdigest," /* 5 */
1915 " mtime," /* 6 */
1916 " date(sctime,'unixepoch')" /* 7 */
 
1917 " FROM subscriber"
1918 );
1919 if( P("only")!=0 ){
1920 blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
1921 style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
1937 @ </tr>
1938 @ </thead><tbody>
1939 while( db_step(&q)==SQLITE_ROW ){
1940 sqlite3_int64 iMtime = db_column_int64(&q, 6);
1941 double rAge = (iNow - iMtime)/86400.0;
 
 
1942 @ <tr>
1943 @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
1944 @ %h(db_column_text(&q,1))</a></td>
1945 @ <td>%h(db_column_text(&q,2))</td>
1946 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1947 @ <td>%h(db_column_text(&q,3))</td>
 
 
 
 
1948 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
1949 @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
1950 @ <td>%h(db_column_text(&q,7))</td>
1951 @ </tr>
1952 }
@@ -1958,13 +2172,21 @@
1958
1959 #if LOCAL_INTERFACE
1960 /*
1961 ** A single event that might appear in an alert is recorded as an
1962 ** instance of the following object.
 
 
 
 
 
 
 
 
1963 */
1964 struct EmailEvent {
1965 int type; /* 'c', 'f', 'm', 't', 'w' */
1966 int needMod; /* Pending moderator approval */
1967 Blob hdr; /* Header content, for forum entries */
1968 Blob txt; /* Text description to appear in an alert */
1969 char *zFromName; /* Human name of the sender */
1970 EmailEvent *pNext; /* Next in chronological order */
@@ -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&nbsp;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&nbsp;Email&nbsp;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'>&uarr; %h(zErr)</span></td></tr>
1465 }
1466 @ </tr>
1467 if( needCaptcha ){
1468 const char *zInit = "";
1469 if( P("captchaseed")!=0 && eErr!=2 ){
1470 uSeed = strtoul(P("captchaseed"),0,10);
1471 zInit = P("captcha");
1472 }else{
1473 uSeed = captcha_seed();
1474 }
1475 zDecoded = captcha_decode(uSeed);
1476 zCaptcha = captcha_render(zDecoded);
1477 @ <tr>
1478 @ <td class="form_label">Security Code:</td>
1479 @ <td><input type="text" name="captcha" value="%h(zInit)" size="30">
1480 captcha_speakit_button(uSeed, "Speak the code");
1481 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1482 @ </tr>
1483 if( eErr==2 ){
1484 @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1485 }
@@ -1421,10 +1505,12 @@
1505 @ Check-ins</label><br>
1506 }
1507 if( g.perm.RdForum ){
1508 @ <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
1509 @ Forum Posts</label><br>
1510 @ <label><input type="checkbox" name="sx" %s(PCK("sx"))> \
1511 @ Forum Edits</label><br>
1512 }
1513 if( g.perm.RdTkt ){
1514 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
1515 @ Ticket changes</label><br>
1516 }
@@ -1461,11 +1547,11 @@
1547 @ </table>
1548 if( needCaptcha ){
1549 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1550 @ %h(zCaptcha)
1551 @ </pre>
1552 @ Enter the 8 characters above in the "Security Code" box<br/>
1553 @ </td></tr></table></div>
1554 }
1555 @ </form>
1556 fossil_free(zErr);
1557 style_footer();
@@ -1474,21 +1560,20 @@
1560 /*
1561 ** Either shutdown or completely delete a subscription entry given
1562 ** by the hex value zName. Then paint a webpage that explains that
1563 ** the entry has been removed.
1564 */
1565 static void alert_unsubscribe(int sid){
1566 char *zEmail;
1567 zEmail = db_text(0, "SELECT semail FROM subscriber"
1568 " WHERE subscriberId=%d", sid);
1569 if( zEmail==0 ){
1570 style_header("Unsubscribe Fail");
1571 @ <p>Unable to locate a subscriber with the requested key</p>
1572 }else{
1573 db_multi_exec(
1574 "DELETE FROM subscriber WHERE subscriberId=%d", sid
 
1575 );
1576 style_header("Unsubscribed");
1577 @ <p>The "%h(zEmail)" email address has been delisted.
1578 @ All traces of that email address have been removed</p>
1579 }
@@ -1499,108 +1584,149 @@
1584 /*
1585 ** WEBPAGE: alerts
1586 **
1587 ** Edit email alert and notification settings.
1588 **
1589 ** The subscriber is identified in several ways:
1590 **
1591 ** (1) The name= query parameter contains the complete subscriberCode.
1592 ** This only happens when the user receives a verification
1593 ** email and clicks on the link in the email. When a
1594 ** compilete subscriberCode is seen on the name= query parameter,
1595 ** that constitutes verification of the email address.
1596 **
1597 ** (2) The sid= query parameter contains an integer subscriberId.
1598 ** This only works for the administrator. It allows the
1599 ** administrator to edit any subscription.
1600 **
1601 ** (3) The user is logged into an account other than "nobody" or
1602 ** "anonymous". In that case the notification settings
1603 ** associated with that account can be edited without needing
1604 ** to know the subscriber code.
1605 **
1606 ** (4) The name= query parameter contains a 32-digit prefix of
1607 ** subscriber code. (Subscriber codes are normally 64 hex digits
1608 ** in length.) This uniquely identifies the subscriber without
1609 ** revealing the complete subscriber code, and hence without
1610 ** verifying the email address.
1611 */
1612 void alert_page(void){
1613 const char *zName = 0; /* Value of the name= query parameter */
1614 Stmt q; /* For querying the database */
1615 int sa, sc, sf, st, sw, sx; /* Types of notifications requested */
1616 int sdigest = 0, sdonotcall = 0, sverified = 0; /* Other fields */
1617 int isLogin; /* True if logged in as an individual */
1618 const char *ssub = 0; /* Subscription flags */
1619 const char *semail = 0; /* Email address */
1620 const char *smip; /* */
1621 const char *suname = 0; /* Corresponding user.login value */
1622 const char *mtime; /* */
1623 const char *sctime; /* Time subscription created */
1624 int eErr = 0; /* Type of error */
1625 char *zErr = 0; /* Error message text */
1626 int sid = 0; /* Subscriber ID */
1627 int nName; /* Length of zName in bytes */
1628 char *zHalfCode; /* prefix of subscriberCode */
1629
1630 db_begin_transaction();
1631 if( alert_webpages_disabled() ){
1632 db_commit_transaction();
1633 return;
1634 }
1635 login_check_credentials();
1636 if( !g.perm.EmailAlert ){
1637 db_commit_transaction();
1638 login_needed(g.anon.EmailAlert);
1639 /*NOTREACHED*/
1640 }
1641 isLogin = login_is_individual();
1642 zName = P("name");
1643 nName = zName ? (int)strlen(zName) : 0;
1644 if( g.perm.Admin && P("sid")!=0 ){
1645 sid = atoi(P("sid"));
1646 }
1647 if( sid==0 && nName>=32 ){
1648 sid = db_int(0,
1649 "SELECT CASE WHEN hex(subscriberCode) LIKE (%Q||'%%')"
1650 " THEN subscriberId ELSE 0 END"
1651 " FROM subscriber WHERE subscriberCode>=hextoblob(%Q)"
1652 " LIMIT 1", zName, zName);
1653 }
1654 if( sid==0 && isLogin ){
1655 sid = db_int(0, "SELECT subscriberId FROM subscriber"
1656 " WHERE suname=%Q", g.zLogin);
1657 }
1658 if( sid==0 ){
1659 db_commit_transaction();
1660 cgi_redirect("subscribe");
1661 /*NOTREACHED*/
1662 }
1663 alert_submenu_common();
1664 if( P("submit")!=0 && cgi_csrf_safe(1) ){
1665 char newSsub[10];
1666 int nsub = 0;
1667 Blob update;
1668
1669 sdonotcall = PB("sdonotcall");
1670 sdigest = PB("sdigest");
1671 semail = P("semail");
1672 if( PB("sa") ) newSsub[nsub++] = 'a';
1673 if( g.perm.Read && PB("sc") ) newSsub[nsub++] = 'c';
1674 if( g.perm.RdForum && PB("sf") ) newSsub[nsub++] = 'f';
1675 if( g.perm.RdTkt && PB("st") ) newSsub[nsub++] = 't';
1676 if( g.perm.RdWiki && PB("sw") ) newSsub[nsub++] = 'w';
1677 if( g.perm.RdForum && PB("sx") ) newSsub[nsub++] = 'x';
1678 newSsub[nsub] = 0;
1679 ssub = newSsub;
1680 blob_init(&update, "UPDATE subscriber SET", -1);
1681 blob_append_sql(&update,
1682 " sdonotcall=%d,"
1683 " sdigest=%d,"
1684 " ssub=%Q,"
1685 " mtime=strftime('%%s','now'),"
1686 " smip=%Q",
1687 sdonotcall,
1688 sdigest,
1689 ssub,
1690 g.zIpAddr
1691 );
1692 if( g.perm.Admin ){
1693 suname = PT("suname");
1694 sverified = PB("sverified");
1695 if( suname && suname[0]==0 ) suname = 0;
1696 blob_append_sql(&update,
1697 ", suname=%Q,"
1698 " sverified=%d",
1699 suname,
1700 sverified
1701 );
1702 }
1703 if( isLogin ){
1704 if( semail==0 || email_address_is_valid(semail,0)==0 ){
1705 eErr = 8;
1706 }
1707 blob_append_sql(&update, ", semail=%Q", semail);
1708 }
1709 blob_append_sql(&update," WHERE subscriberId=%d", sid);
1710 if( eErr==0 ){
1711 db_exec_sql(blob_str(&update));
1712 ssub = 0;
1713 }
1714 blob_reset(&update);
1715 }
1716 if( P("delete")!=0 && cgi_csrf_safe(1) ){
1717 if( !PB("dodelete") ){
1718 eErr = 9;
1719 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" again to"
1720 " unsubscribe");
1721 }else{
1722 alert_unsubscribe(sid);
1723 db_commit_transaction();
1724 return;
1725 }
1726 }
1727 style_header("Update Subscription");
1728 db_prepare(&q,
1729 "SELECT"
1730 " semail," /* 0 */
1731 " sverified," /* 1 */
1732 " sdonotcall," /* 2 */
@@ -1607,52 +1733,92 @@
1733 " sdigest," /* 3 */
1734 " ssub," /* 4 */
1735 " smip," /* 5 */
1736 " suname," /* 6 */
1737 " datetime(mtime,'unixepoch')," /* 7 */
1738 " datetime(sctime,'unixepoch')," /* 8 */
1739 " hex(subscriberCode)" /* 9 */
1740 " FROM subscriber WHERE subscriberId=%d", sid);
1741 if( db_step(&q)!=SQLITE_ROW ){
1742 db_finalize(&q);
1743 db_commit_transaction();
1744 cgi_redirect("subscribe");
1745 /*NOTREACHED*/
1746 }
1747 if( ssub==0 ){
1748 semail = db_column_text(&q, 0);
1749 sdonotcall = db_column_int(&q, 2);
1750 sdigest = db_column_int(&q, 3);
1751 ssub = db_column_text(&q, 4);
1752 }
1753 if( suname==0 ){
1754 suname = db_column_text(&q, 6);
1755 sverified = db_column_int(&q, 1);
1756 }
1757 sa = strchr(ssub,'a')!=0;
1758 sc = strchr(ssub,'c')!=0;
1759 sf = strchr(ssub,'f')!=0;
1760 st = strchr(ssub,'t')!=0;
1761 sw = strchr(ssub,'w')!=0;
1762 sx = strchr(ssub,'x')!=0;
1763 smip = db_column_text(&q, 5);
 
1764 mtime = db_column_text(&q, 7);
1765 sctime = db_column_text(&q, 8);
1766 if( !g.perm.Admin && !sverified ){
1767 if( nName==64 ){
1768 db_multi_exec(
1769 "UPDATE subscriber SET sverified=1"
1770 " WHERE subscriberCode=hextoblob(%Q)",
1771 zName);
1772 if( db_get_boolean("selfreg-verify",0) ){
1773 char *zNewCap = db_get("default-perms","u");
1774 db_multi_exec(
1775 "UPDATE user"
1776 " SET cap=%Q"
1777 " WHERE cap='7' AND login=("
1778 " SELECT suname FROM subscriber"
1779 " WHERE subscriberCode=hextoblob(%Q))",
1780 zNewCap, zName
1781 );
1782 login_set_capabilities(zNewCap, 0);
1783 }
1784 @ <h1>Your email alert subscription has been verified!</h1>
1785 @ <p>Use the form below to update your subscription information.</p>
1786 @ <p>Hint: Bookmark this page so that you can more easily update
1787 @ your subscription information in the future</p>
1788 }else{
1789 @ <h2>Your email address is unverified</h2>
1790 @ <p>You should have received an email message containing a link
1791 @ that you must visit to verify your account. No email notifications
1792 @ will be sent until your email address has been verified.</p>
1793 }
1794 }else{
1795 @ <p>Make changes to the email subscription shown below and
1796 @ press "Submit".</p>
1797 }
1798 form_begin(0, "%R/alerts");
1799 zHalfCode = db_text("x","SELECT hex(substr(subscriberCode,1,16))"
1800 " FROM subscriber WHERE subscriberId=%d", sid);
1801 @ <input type="hidden" name="name" value="%h(zHalfCode)">
1802 @ <table class="subscribe">
1803 @ <tr>
1804 @ <td class="form_label">Email&nbsp;Address:</td>
1805 if( isLogin ){
1806 @ <td><input type="text" name="semail" value="%h(semail)" size="30">\
1807 if( eErr==8 ){
1808 @ <span class='loginError'>&larr; not a valid email address!</span>
1809 }else if( g.perm.Admin ){
1810 @ &nbsp;&nbsp;<a href="%R/announce?to=%t(semail)">\
1811 @ (Send a message to %h(semail))</a>\
1812 }
1813 @ </td>
1814 }else{
1815 @ <td>%h(semail)</td>
1816 }
1817 @ </tr>
1818 if( g.perm.Admin ){
1819 int uid;
1820 @ <tr>
1821 @ <td class='form_label'>Created:</td>
1822 @ <td>%h(sctime)</td>
1823 @ </tr>
1824 @ <tr>
@@ -1662,13 +1828,21 @@
1828 @ <tr>
1829 @ <td class='form_label'>IP Address:</td>
1830 @ <td>%h(smip)</td>
1831 @ </tr>
1832 @ <tr>
1833 @ <td class='form_label'>Subscriber&nbsp;Code:</td>
1834 @ <td>%h(db_column_text(&q,9))</td>
1835 @ <tr>
1836 @ <td class="form_label">User:</td>
1837 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1838 @ size="30">\
1839 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", suname);
1840 if( uid ){
1841 @ &nbsp;&nbsp;<a href='%R/setup_uedit?id=%d(uid)'>\
1842 @ (login info for %h(suname))</a>\
1843 }
1844 @ </tr>
1845 }
1846 @ <tr>
1847 @ <td class="form_label">Topics:</td>
1848 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
@@ -1678,10 +1852,12 @@
1852 @ Check-ins</label><br>
1853 }
1854 if( g.perm.RdForum ){
1855 @ <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
1856 @ Forum Posts</label><br>
1857 @ <label><input type="checkbox" name="sx" %s(sx?"checked":"")>\
1858 @ Forum Edits</label><br>
1859 }
1860 if( g.perm.RdTkt ){
1861 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1862 @ Ticket changes</label><br>
1863 }
@@ -1695,19 +1871,15 @@
1871 @ <td><select size="1" name="sdigest">
1872 @ <option value="0" %s(sdigest?"":"selected")>Individual Emails</option>
1873 @ <option value="1" %s(sdigest?"selected":"")>Daily Digest</option>
1874 @ </select></td>
1875 @ </tr>
 
 
 
 
1876 if( g.perm.Admin ){
1877 @ <tr>
1878 @ <td class="form_label">Admin Options:</td><td>
1879 @ <label><input type="checkbox" name="sdonotcall" \
1880 @ %s(sdonotcall?"checked":"")> Do not disturb</label><br>
1881 @ <label><input type="checkbox" name="sverified" \
1882 @ %s(sverified?"checked":"")>\
1883 @ Verified</label></td></tr>
1884 }
1885 if( eErr==9 ){
@@ -1726,10 +1898,12 @@
1898 @ </table>
1899 @ </form>
1900 fossil_free(zErr);
1901 db_finalize(&q);
1902 style_footer();
1903 db_commit_transaction();
1904 return;
1905 }
1906
1907 /* This is the message that gets sent to describe how to change
1908 ** or modify a subscription
1909 */
@@ -1760,25 +1934,26 @@
1934 */
1935 void unsubscribe_page(void){
1936 const char *zName = P("name");
1937 char *zErr = 0;
1938 int eErr = 0;
1939 unsigned int uSeed = 0;
1940 const char *zDecoded;
1941 char *zCaptcha = 0;
1942 int dx;
1943 int bSubmit;
1944 const char *zEAddr;
1945 char *zCode = 0;
1946 int sid = 0;
1947
1948 /* If a valid subscriber code is supplied, then unsubscribe immediately.
1949 */
1950 if( zName
1951 && (sid = db_int(0, "SELECT subscriberId FROM subscriber"
1952 " WHERE subscriberCode=hextoblob(%Q)", zName))!=0
1953 ){
1954 alert_unsubscribe(sid);
1955 return;
1956 }
1957
1958 /* Logged in users are redirected to the /alerts page */
1959 login_check_credentials();
@@ -1855,10 +2030,11 @@
2030 zDecoded = captcha_decode(uSeed);
2031 zCaptcha = captcha_render(zDecoded);
2032 @ <tr>
2033 @ <td class="form_label">Security Code:</td>
2034 @ <td><input type="text" name="captcha" value="" size="30">
2035 captcha_speakit_button(uSeed, "Speak the code");
2036 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
2037 if( eErr==2 ){
2038 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
2039 }
2040 @ </tr>
@@ -1874,11 +2050,11 @@
2050 @ </tr>
2051 @ </table>
2052 @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2053 @ %h(zCaptcha)
2054 @ </pre>
2055 @ Enter the 8 characters above in the "Security Code" box<br/>
2056 @ </td></tr></table></div>
2057 @ </form>
2058 fossil_free(zErr);
2059 style_footer();
2060 }
@@ -1885,37 +2061,69 @@
2061
2062 /*
2063 ** WEBPAGE: subscribers
2064 **
2065 ** This page, accessible to administrators only,
2066 ** shows a list of subscriber email addresses.
2067 ** Clicking on an email takes one to the /alerts page
2068 ** for that email where the delivery settings can be
2069 ** modified.
2070 */
2071 void subscriber_list_page(void){
2072 Blob sql;
2073 Stmt q;
2074 sqlite3_int64 iNow;
2075 int nTotal;
2076 int nPending;
2077 int nDel = 0;
2078 if( alert_webpages_disabled() ) return;
2079 login_check_credentials();
2080 if( !g.perm.Admin ){
2081 login_needed(0);
2082 return;
2083 }
2084 alert_submenu_common();
2085 style_submenu_element("Users","setup_ulist");
2086 style_header("Subscriber List");
2087 nTotal = db_int(0, "SELECT count(*) FROM subscriber");
2088 nPending = db_int(0, "SELECT count(*) FROM subscriber WHERE NOT sverified");
2089 if( nPending>0 && P("purge") && cgi_csrf_safe(0) ){
2090 int nNewPending;
2091 db_multi_exec(
2092 "DELETE FROM subscriber"
2093 " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')"
2094 );
2095 nNewPending = db_int(0, "SELECT count(*) FROM subscriber"
2096 " WHERE NOT sverified");
2097 nDel = nPending - nNewPending;
2098 nPending = nNewPending;
2099 nTotal -= nDel;
2100 }
2101 if( nPending>0 ){
2102 @ <h1>%,d(nTotal) Subscribers, %,d(nPending) Pending</h1>
2103 if( nDel==0 && 0<db_int(0,"SELECT count(*) FROM subscriber"
2104 " WHERE NOT sverified AND mtime<0+strftime('%%s','now','-1 day')")
2105 ){
2106 style_submenu_element("Purge Pending","subscribers?purge");
2107 }
2108 }else{
2109 @ <h1>%,d(nTotal) Subscribers</h1>
2110 }
2111 if( nDel>0 ){
2112 @ <p>*** %d(nDel) pending subscriptions deleted ***</p>
2113 }
2114 blob_init(&sql, 0, 0);
2115 blob_append_sql(&sql,
2116 "SELECT subscriberId," /* 0 */
2117 " semail," /* 1 */
2118 " ssub," /* 2 */
2119 " suname," /* 3 */
2120 " sverified," /* 4 */
2121 " sdigest," /* 5 */
2122 " mtime," /* 6 */
2123 " date(sctime,'unixepoch')," /* 7 */
2124 " (SELECT uid FROM user WHERE login=subscriber.suname)" /* 8 */
2125 " FROM subscriber"
2126 );
2127 if( P("only")!=0 ){
2128 blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
2129 style_submenu_element("Show All","%R/subscribers");
@@ -1937,16 +2145,22 @@
2145 @ </tr>
2146 @ </thead><tbody>
2147 while( db_step(&q)==SQLITE_ROW ){
2148 sqlite3_int64 iMtime = db_column_int64(&q, 6);
2149 double rAge = (iNow - iMtime)/86400.0;
2150 int uid = db_column_int(&q, 8);
2151 const char *zUname = db_column_text(&q, 3);
2152 @ <tr>
2153 @ <td><a href='%R/alerts?sid=%d(db_column_int(&q,0))'>\
2154 @ %h(db_column_text(&q,1))</a></td>
2155 @ <td>%h(db_column_text(&q,2))</td>
2156 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
2157 if( uid ){
2158 @ <td><a href='%R/setup_uedit?id=%d(uid)'>%h(zUname)</a>
2159 }else{
2160 @ <td>%h(zUname)</td>
2161 }
2162 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
2163 @ <td data-sortkey='%010llx(iMtime)'>%z(human_readable_age(rAge))</td>
2164 @ <td>%h(db_column_text(&q,7))</td>
2165 @ </tr>
2166 }
@@ -1958,13 +2172,21 @@
2172
2173 #if LOCAL_INTERFACE
2174 /*
2175 ** A single event that might appear in an alert is recorded as an
2176 ** instance of the following object.
2177 **
2178 ** type values:
2179 **
2180 ** c A new check-in
2181 ** f An original forum post
2182 ** x An edit to a prior forum post
2183 ** t A new ticket or a change to an existing ticket
2184 ** w A change to a wiki page
2185 */
2186 struct EmailEvent {
2187 int type; /* 'c', 'f', 't', 'w', 'x' */
2188 int needMod; /* Pending moderator approval */
2189 Blob hdr; /* Header content, for forum entries */
2190 Blob txt; /* Text description to appear in an alert */
2191 char *zFromName; /* Human name of the sender */
2192 EmailEvent *pNext; /* Next in chronological order */
@@ -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&nbsp;Code:</td>
2831 @ <td><input type="text" name="captcha" value="" size="10">
2832 captcha_speakit_button(uSeed, "Speak the code");
2833 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
2834 @ </tr>
2835 }
2836 @ <tr>
2837 @ <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
@@ -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 @@
128128
** supported by the rebuild command itself, if any are
129129
** present, are passed along verbatim. The --force and
130130
** --randomize options are not supported.
131131
**
132132
** sync Run a "sync" on all repositories. Only the --verbose
133
-** option is supported.
133
+** and --unversioned options are supported.
134134
**
135135
** setting Run the "setting", "set", or "unset" commands on all
136136
** set repositories. These command are particularly useful in
137137
** unset conjunction with the "max-loadavg" setting which cannot
138138
** otherwise be set globally.
@@ -283,11 +283,13 @@
283283
}else if( strncmp(zCmd, "sync", n)==0 ){
284284
zCmd = "sync -autourl -R";
285285
collect_argument(&extra, "verbose","v");
286286
collect_argument(&extra, "unversioned","u");
287287
}else if( strncmp(zCmd, "test-integrity", n)==0 ){
288
+ collect_argument(&extra, "db-only", "d");
288289
collect_argument(&extra, "parse", 0);
290
+ collect_argument(&extra, "quick", "q");
289291
zCmd = "test-integrity";
290292
}else if( strncmp(zCmd, "test-orphans", n)==0 ){
291293
zCmd = "test-orphans -R";
292294
}else if( strncmp(zCmd, "test-missing", n)==0 ){
293295
zCmd = "test-missing -q -R";
294296
--- 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 @@
545545
cgi_redirectf("%R/wiki?name=%t", zWikiName);
546546
}
547547
return;
548548
}
549549
if( strcmp(zModAction,"approve")==0 ){
550
- moderation_approve(rid);
550
+ moderation_approve('a', rid);
551551
}
552552
}
553553
style_header("Attachment Details");
554554
style_submenu_element("Raw", "%R/artifact/%s", zUuid);
555555
if(fShowContent){
556556
557557
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
--- 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 @@
1818
** This file contains code used to create new branches within a repository.
1919
*/
2020
#include "config.h"
2121
#include "branch.h"
2222
#include <assert.h>
23
+
24
+/*
25
+** Return true if zBr is the branch name associated with check-in with
26
+** blob.uuid value of zUuid
27
+*/
28
+int branch_includes_uuid(const char *zBr, const char *zUuid){
29
+ return db_exists(
30
+ "SELECT 1 FROM tagxref, blob"
31
+ " WHERE blob.uuid=%Q AND tagxref.rid=blob.rid"
32
+ " AND tagxref.value=%Q AND tagxref.tagtype>0"
33
+ " AND tagxref.tagid=%d",
34
+ zUuid, zBr, TAG_BRANCH
35
+ );
36
+}
2337
2438
/*
2539
** If RID refers to a check-in, return the name of the branch for that
2640
** check-in.
2741
**
@@ -144,11 +158,10 @@
144158
blob_appendf(&branch, "T *bgcolor * %F\n", zColor);
145159
}
146160
blob_appendf(&branch, "T *branch * %F\n", zBranch);
147161
blob_appendf(&branch, "T *sym-%F *\n", zBranch);
148162
if( isPrivate ){
149
- blob_appendf(&branch, "T +private *\n");
150163
noSign = 1;
151164
}
152165
153166
/* Cancel all other symbolic tags */
154167
db_prepare(&q,
@@ -245,11 +258,11 @@
245258
@ GROUP BY 1;
246259
;
247260
248261
/* Call this routine to create the TEMP table */
249262
static void brlist_create_temp_table(void){
250
- db_multi_exec(createBrlistQuery/*works-like:""*/);
263
+ db_exec_sql(createBrlistQuery);
251264
}
252265
253266
254267
#if INTERFACE
255268
/*
256269
--- 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 @@
5959
zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]);
6060
sqlite3_result_text(context, zOut, i-n+1, sqlite3_free);
6161
}
6262
}
6363
64
+/*
65
+** Flag arguments for hyperlinked_path()
66
+*/
67
+#if INTERFACE
68
+# define LINKPATH_FINFO 0x0001 /* Link final term to /finfo */
69
+# define LINKPATH_FILE 0x0002 /* Link final term to /file */
70
+#endif
71
+
6472
/*
6573
** Given a pathname which is a relative path from the root of
6674
** the repository to a file or directory, compute a string which
6775
** is an HTML rendering of that path with hyperlinks on each
6876
** directory component of the path where the hyperlink redirects
@@ -76,29 +84,36 @@
7684
void hyperlinked_path(
7785
const char *zPath, /* Path to render */
7886
Blob *pOut, /* Write into this blob */
7987
const char *zCI, /* check-in name, or NULL */
8088
const char *zURI, /* "dir" or "tree" */
81
- const char *zREx /* Extra query parameters */
89
+ const char *zREx, /* Extra query parameters */
90
+ unsigned int mFlags /* Extra flags */
8291
){
8392
int i, j;
8493
char *zSep = "";
8594
8695
for(i=0; zPath[i]; i=j){
8796
for(j=i; zPath[j] && zPath[j]!='/'; j++){}
88
- if( zPath[j] && g.perm.Hyperlink ){
89
- if( zCI ){
90
- char *zLink = href("%R/%s?name=%#T%s&ci=%!S", zURI, j, zPath, zREx,zCI);
91
- blob_appendf(pOut, "%s%z%#h</a>",
92
- zSep, zLink, j-i, &zPath[i]);
93
- }else{
94
- char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
95
- blob_appendf(pOut, "%s%z%#h</a>",
96
- zSep, zLink, j-i, &zPath[i]);
97
- }
98
- }else{
99
- blob_appendf(pOut, "%s%#h", zSep, j-i, &zPath[i]);
97
+ if( zPath[j]==0 ){
98
+ if( mFlags & LINKPATH_FILE ){
99
+ zURI = "file";
100
+ }else if( mFlags & LINKPATH_FINFO ){
101
+ zURI = "finfo";
102
+ }else{
103
+ blob_appendf(pOut, "/%h", zPath+i);
104
+ break;
105
+ }
106
+ }
107
+ if( zCI ){
108
+ char *zLink = href("%R/%s?name=%#T%s&ci=%T", zURI, j, zPath, zREx,zCI);
109
+ blob_appendf(pOut, "%s%z%#h</a>",
110
+ zSep, zLink, j-i, &zPath[i]);
111
+ }else{
112
+ char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
113
+ blob_appendf(pOut, "%s%z%#h</a>",
114
+ zSep, zLink, j-i, &zPath[i]);
100115
}
101116
zSep = "/";
102117
while( zPath[j]=='/' ){ j++; }
103118
}
104119
}
@@ -122,33 +137,29 @@
122137
*/
123138
void page_dir(void){
124139
char *zD = fossil_strdup(P("name"));
125140
int nD = zD ? strlen(zD)+1 : 0;
126141
int mxLen;
127
- int n;
128142
char *zPrefix;
129143
Stmt q;
130144
const char *zCI = P("ci");
131145
int rid = 0;
132146
char *zUuid = 0;
133
- Blob dirname;
134147
Manifest *pM = 0;
135148
const char *zSubdirLink;
136149
int linkTrunk = 1;
137150
int linkTip = 1;
138151
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;
139155
156
+ if( zCI && strlen(zCI)==0 ){ zCI = 0; }
140157
if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; }
141158
login_check_credentials();
142159
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
143160
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);
150161
151162
/* If the name= parameter is an empty string, make it a NULL pointer */
152163
if( zD && strlen(zD)==0 ){ zD = 0; }
153164
154165
/* If a specific check-in is requested, fetch and parse it. If the
@@ -160,53 +171,80 @@
160171
if( pM ){
161172
int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
162173
linkTrunk = trunkRid && rid != trunkRid;
163174
linkTip = rid != symbolic_name_to_rid("tip", "ci");
164175
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);
165179
}else{
166180
zCI = 0;
167181
}
168182
}
169183
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
+
170206
/* Compute the title of the page */
171
- blob_zero(&dirname);
172207
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);
175213
zPrefix = mprintf("%s/", zD);
176214
style_submenu_element("Top-Level", "%s",
177215
url_render(&sURI, "name", 0, 0, 0));
178216
}else{
179
- blob_append(&dirname, "in the top-level directory", -1);
217
+ @ <h2>Files in the top-level directory \
180218
zPrefix = "";
181219
}
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
+ }
182237
if( linkTrunk ){
183238
style_submenu_element("Trunk", "%s",
184239
url_render(&sURI, "ci", "trunk", 0, 0));
185240
}
186241
if( linkTip ){
187242
style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
188243
}
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
- @ &nbsp;&nbsp;%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
- @ &nbsp;&nbsp;%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);
208246
}
209247
style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
210248
style_submenu_element("Tree-View", "%s",
211249
url_render(&sURI, "type", "tree", 0, 0));
212250
@@ -269,15 +307,14 @@
269307
270308
/* Generate a multi-column table listing the contents of zD[]
271309
** directory.
272310
*/
273311
mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
274
- n = db_int(1,"SELECT count(*) FROM localfiles; /*scan*/");
275312
if( mxLen<12 ) mxLen = 12;
276313
mxLen += (mxLen+9)/10;
277314
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">
279316
@ <ul class="browser">
280317
while( db_step(&q)==SQLITE_ROW ){
281318
const char *zFN;
282319
zFN = db_column_text(&q, 0);
283320
if( zFN[0]=='/' ){
@@ -284,12 +321,11 @@
284321
zFN++;
285322
@ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
286323
}else{
287324
const char *zLink;
288325
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);
291327
}else{
292328
zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
293329
}
294330
@ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
295331
}
@@ -614,11 +650,15 @@
614650
HQuery sURI; /* Hyperlink */
615651
int startExpanded; /* True to start out with the tree expanded */
616652
int showDirOnly; /* Show directories only. Omit files */
617653
int nDir = 0; /* Number of directories. Used for ID attributes */
618654
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;
619658
659
+ if( zCI && strlen(zCI)==0 ){ zCI = 0; }
620660
if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; }
621661
memset(&sTree, 0, sizeof(sTree));
622662
login_check_credentials();
623663
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
624664
while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
@@ -626,14 +666,12 @@
626666
pathelementFunc, 0, 0);
627667
url_initialize(&sURI, "tree");
628668
cgi_query_parameters_to_url(&sURI);
629669
if( PB("nofiles") ){
630670
showDirOnly = 1;
631
- style_header("Folder Hierarchy");
632671
}else{
633672
showDirOnly = 0;
634
- style_header("File Tree");
635673
}
636674
style_adunit_config(ADUNIT_RIGHT_OK);
637675
if( PB("expand") ){
638676
startExpanded = 1;
639677
}else{
@@ -662,37 +700,55 @@
662700
linkTip = rid != symbolic_name_to_rid("tip", "ci");
663701
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
664702
rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
665703
zNow = db_text("", "SELECT datetime(mtime,toLocal())"
666704
" 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);
667708
}else{
668709
zCI = 0;
669710
}
670711
}
671712
if( zCI==0 ){
672713
rNow = db_double(0.0, "SELECT max(mtime) FROM event");
673714
zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
674715
}
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);
675733
676734
/* Compute the title of the page */
677735
blob_zero(&dirname);
678736
if( zD ){
679737
blob_append(&dirname, "within directory ", -1);
680
- hyperlinked_path(zD, &dirname, zCI, "tree", zREx);
738
+ hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0);
681739
if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE);
682740
style_submenu_element("Top-Level", "%s",
683741
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);
688744
}
689745
style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
690746
if( zCI ){
691747
style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
692748
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);
694750
}
695751
}
696752
if( linkTrunk ){
697753
style_submenu_element("Trunk", "%s",
698754
url_render(&sURI, "ci", "trunk", 0, 0));
@@ -747,10 +803,11 @@
747803
tree_add_node(&sTree, zName, zUuid, mtime);
748804
nFile++;
749805
}
750806
db_finalize(&q);
751807
}
808
+ style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
752809
753810
if( showDirOnly ){
754811
for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
755812
if( p->pChild!=0 && p->nFullName>nD ) nFile++;
756813
}
@@ -757,18 +814,24 @@
757814
zObjType = "Folders";
758815
}else{
759816
zObjType = "Files";
760817
}
761818
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
+ }
770833
}else{
771834
int n = db_int(0, "SELECT count(*) FROM plink");
772835
@ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
773836
}
774837
if( useMtime ){
@@ -826,11 +889,11 @@
826889
nDir++;
827890
}else if( !showDirOnly ){
828891
const char *zFileClass = fileext_class(p->zName);
829892
char *zLink;
830893
if( zCI ){
831
- zLink = href("%R/artifact/%!S",p->zUuid);
894
+ zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI);
832895
}else{
833896
zLink = href("%R/finfo?name=%T",p->zFullName);
834897
}
835898
@ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
836899
@ %z(zLink)%h(p->zName)</a>
@@ -920,11 +983,11 @@
920983
** mtime on that check-in. If zGlob and *zGlob then only files matching
921984
** the given glob are computed.
922985
*/
923986
int compute_fileage(int vid, const char* zGlob){
924987
Stmt q;
925
- db_multi_exec(zComputeFileAgeSetup /*works-like:"constant"*/);
988
+ db_exec_sql(zComputeFileAgeSetup);
926989
db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/);
927990
db_bind_int(&q, ":ckin", vid);
928991
db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*");
929992
db_exec(&q);
930993
db_finalize(&q);
@@ -1003,10 +1066,11 @@
10031066
int rid;
10041067
const char *zName;
10051068
const char *zGlob;
10061069
const char *zUuid;
10071070
const char *zNow; /* Time of check-in */
1071
+ int isBranchCI; /* name= is a branch name */
10081072
int showId = PB("showid");
10091073
Stmt q1, q2;
10101074
double baseTime;
10111075
login_check_credentials();
10121076
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
@@ -1016,28 +1080,34 @@
10161080
rid = symbolic_name_to_rid(zName, "ci");
10171081
if( rid==0 ){
10181082
fossil_fatal("not a valid check-in: %s", zName);
10191083
}
10201084
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1085
+ isBranchCI = branch_includes_uuid(zName,zUuid);
10211086
baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
10221087
zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event"
10231088
" WHERE objid=%d", rid);
10241089
style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName);
10251090
style_header("File Ages");
10261091
zGlob = P("glob");
10271092
compute_fileage(rid,zGlob);
10281093
db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
10291094
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
+ }
10321103
if( zGlob && zGlob[0] ){
10331104
@ that match "%h(zGlob)"
10341105
}
10351106
@ ordered by age</h1>
10361107
@
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
10391109
@ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
10401110
@
10411111
@ <div class='fileage'><table>
10421112
@ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr>
10431113
db_prepare(&q1,
@@ -1052,14 +1122,13 @@
10521122
" AND blob.rid=event.objid\n"
10531123
" ORDER BY event.mtime DESC;",
10541124
TAG_BRANCH
10551125
);
10561126
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"
10591129
" WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid"
1060
- " AND blob.rid=fileage.fid;"
10611130
);
10621131
while( db_step(&q1)==SQLITE_ROW ){
10631132
double age = baseTime - db_column_double(&q1, 0);
10641133
int mid = db_column_int(&q1, 1);
10651134
const char *zUuid = db_column_text(&q1, 2);
@@ -1069,24 +1138,24 @@
10691138
char *zAge = human_readable_age(age);
10701139
@ <tr><td>%s(zAge)</td>
10711140
@ <td>
10721141
db_bind_int(&q2, ":mid", mid);
10731142
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> \
10771145
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 />
10791148
}else{
1080
- @ %z(href("%R/artifact/%!S",zFUuid))%h(zFile)</a><br />
1149
+ @ </a><br />
10811150
}
10821151
}
10831152
db_reset(&q2);
10841153
@ </td>
10851154
@ <td>
10861155
@ %W(zComment)
1087
- @ (check-in:&nbsp;%z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>,
1156
+ @ (check-in:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
10881157
if( showId ){
10891158
@ id: %d(mid)
10901159
}
10911160
@ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
10921161
@ branch:&nbsp;\
10931162
--- 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 @ &nbsp;&nbsp;%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 @ &nbsp;&nbsp;%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:&nbsp;%z(href("%R/ci/%!S",zUuid))%S(zUuid)</a>,
1088 if( showId ){
1089 @ id: %d(mid)
1090 }
1091 @ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1092 @ branch:&nbsp;\
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:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
1157 if( showId ){
1158 @ id: %d(mid)
1159 }
1160 @ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1161 @ branch:&nbsp;\
1162
+10 -2
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
5555
return (char*)builtin_file(zFilename, 0);
5656
}
5757
5858
/*
5959
** COMMAND: test-builtin-list
60
+**
61
+** If -verbose is used, it outputs a line at the end
62
+** with the total item count and size.
6063
**
6164
** List the names and sizes of all built-in resources.
6265
*/
6366
void test_builtin_list(void){
64
- int i;
67
+ int i, size = 0;;
6568
for(i=0; i<count(aBuiltinFiles); i++){
66
- fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,aBuiltinFiles[i].nByte);
69
+ const int n = aBuiltinFiles[i].nByte;
70
+ fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n);
71
+ size += n;
72
+ }
73
+ if(find_option("verbose","v",0)!=0){
74
+ fossil_print("%d entries totaling %d bytes\n", i, size);
6775
}
6876
}
6977
7078
/*
7179
** WEBPAGE: test-builtin-files
7280
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
55 return (char*)builtin_file(zFilename, 0);
56 }
57
58 /*
59 ** COMMAND: test-builtin-list
 
 
 
60 **
61 ** List the names and sizes of all built-in resources.
62 */
63 void test_builtin_list(void){
64 int i;
65 for(i=0; i<count(aBuiltinFiles); i++){
66 fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,aBuiltinFiles[i].nByte);
 
 
 
 
 
67 }
68 }
69
70 /*
71 ** WEBPAGE: test-builtin-files
72
--- src/builtin.c
+++ src/builtin.c
@@ -55,17 +55,25 @@
55 return (char*)builtin_file(zFilename, 0);
56 }
57
58 /*
59 ** COMMAND: test-builtin-list
60 **
61 ** If -verbose is used, it outputs a line at the end
62 ** with the total item count and size.
63 **
64 ** List the names and sizes of all built-in resources.
65 */
66 void test_builtin_list(void){
67 int i, size = 0;;
68 for(i=0; i<count(aBuiltinFiles); i++){
69 const int n = aBuiltinFiles[i].nByte;
70 fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n);
71 size += n;
72 }
73 if(find_option("verbose","v",0)!=0){
74 fossil_print("%d entries totaling %d bytes\n", i, size);
75 }
76 }
77
78 /*
79 ** WEBPAGE: test-builtin-files
80
+26 -10
--- src/capabilities.c
+++ src/capabilities.c
@@ -239,15 +239,17 @@
239239
char *zOneLiner; /* One-line summary */
240240
} aCap[] = {
241241
{ 'a', CAPCLASS_SUPER, 0,
242242
"Admin", "Create and delete users" },
243243
{ 'b', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
244
- "Attach", "Add attchments to wiki or tickets" },
244
+ "Attach", "Add attachments to wiki or tickets" },
245245
{ 'c', CAPCLASS_TKT, 0,
246246
"Append-Tkt", "Append to existing tickets" },
247
- { 'd', CAPCLASS_WIKI|CAPCLASS_TKT, 0,
248
- "Delete", "Delete wiki or tickets" },
247
+ /*
248
+ ** d unused since fork from CVSTrac;
249
+ ** see https://fossil-scm.org/forum/forumpost/43c78f4bef
250
+ */
249251
{ 'e', CAPCLASS_DATA, 0,
250252
"View-PII", "View sensitive info such as email addresses" },
251253
{ 'f', CAPCLASS_WIKI, 0,
252254
"New-Wiki", "Create new wiki pages" },
253255
{ 'g', CAPCLASS_DATA, 0,
@@ -359,25 +361,38 @@
359361
** Generate a "capability summary table" that shows the major capabilities
360362
** against the various user categories.
361363
*/
362364
void capability_summary(void){
363365
Stmt q;
366
+ CapabilityString *pCap;
367
+ char *zSelfCap;
368
+ char *zPubPages = db_get("public-pages",0);
369
+ int hasPubPages = zPubPages && zPubPages[0];
370
+
371
+ pCap = capability_add(0, db_get("default-perms","u"));
372
+ capability_expand(pCap);
373
+ zSelfCap = capability_string(pCap);
374
+ capability_free(pCap);
375
+
364376
db_prepare(&q,
365377
"WITH t(id,seq) AS (VALUES('nobody',1),('anonymous',2),('reader',3),"
366378
"('developer',4))"
367
- " SELECT id, fullcap(user.cap),seq,1"
379
+ " SELECT id, CASE WHEN user.login='nobody' THEN user.cap"
380
+ " ELSE fullcap(user.cap) END,seq,1"
368381
" FROM t LEFT JOIN user ON t.id=user.login"
369382
" UNION ALL"
370
- " SELECT 'New User Default', fullcap(%Q), 10, 1"
383
+ " SELECT 'Public Pages', %Q, 100, %d"
384
+ " UNION ALL"
385
+ " SELECT 'New User Default', %Q, 110, 1"
371386
" UNION ALL"
372
- " SELECT 'Regular User', fullcap(capunion(cap)), 20, count(*) FROM user"
387
+ " SELECT 'Regular User', fullcap(capunion(cap)), 200, count(*) FROM user"
373388
" WHERE cap NOT GLOB '*[as]*' AND login NOT IN (SELECT id FROM t)"
374389
" UNION ALL"
375
- " SELECT 'Adminstrator', fullcap(capunion(cap)), 30, count(*) FROM user"
390
+ " SELECT 'Adminstrator', fullcap(capunion(cap)), 300, count(*) FROM user"
376391
" WHERE cap GLOB '*[as]*'"
377392
" ORDER BY 3 ASC",
378
- db_get("default-perms","")
393
+ zSelfCap, hasPubPages, zSelfCap
379394
);
380395
@ <table id='capabilitySummary' cellpadding="0" cellspacing="0" border="1">
381396
@ <tr><th>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
382397
@ <th>Unversioned Content</th></tr>
383398
while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
384399
const char *zId = db_column_text(&q, 0);
385400
const char *zCap = db_column_text(&q, 1);
386401
int n = db_column_int(&q, 3);
387402
int eType;
388403
static const char *const azType[] = { "off", "read", "write" };
389
- static const char *const azClass[] = { "capsumOff", "capsumRead", "capsumWrite" };
404
+ static const char *const azClass[] =
405
+ { "capsumOff", "capsumRead", "capsumWrite" };
390406
391407
if( n==0 ) continue;
392408
393409
/* Code */
394410
if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
416432
eType = 0;
417433
}
418434
@ <td class="%s(azClass[eType])">%s(azType[eType])</td>
419435
420436
/* Ticket */
421
- if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){
437
+ if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){
422438
eType = 2;
423439
}else if( sqlite3_strglob("*r*",zCap)==0 ){
424440
eType = 1;
425441
}else{
426442
eType = 0;
427443
--- src/capabilities.c
+++ src/capabilities.c
@@ -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>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
382 @ <th>Unversioned Content</th></tr>
383 while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
384 const char *zId = db_column_text(&q, 0);
385 const char *zCap = db_column_text(&q, 1);
386 int n = db_column_int(&q, 3);
387 int eType;
388 static const char *const azType[] = { "off", "read", "write" };
389 static const char *const azClass[] = { "capsumOff", "capsumRead", "capsumWrite" };
 
390
391 if( n==0 ) continue;
392
393 /* Code */
394 if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
416 eType = 0;
417 }
418 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
419
420 /* Ticket */
421 if( sqlite3_strglob("*[ascdnqtw]*",zCap)==0 ){
422 eType = 2;
423 }else if( sqlite3_strglob("*r*",zCap)==0 ){
424 eType = 1;
425 }else{
426 eType = 0;
427
--- src/capabilities.c
+++ src/capabilities.c
@@ -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>&nbsp;<th>Code<th>Forum<th>Tickets<th>Wiki\
397 @ <th>Unversioned Content</th></tr>
398 while( db_step(&q)==SQLITE_ROW ){
@@ -384,11 +399,12 @@
399 const char *zId = db_column_text(&q, 0);
400 const char *zCap = db_column_text(&q, 1);
401 int n = db_column_int(&q, 3);
402 int eType;
403 static const char *const azType[] = { "off", "read", "write" };
404 static const char *const azClass[] =
405 { "capsumOff", "capsumRead", "capsumWrite" };
406
407 if( n==0 ) continue;
408
409 /* Code */
410 if( db_column_int(&q,2)<10 ){
@@ -416,11 +432,11 @@
432 eType = 0;
433 }
434 @ <td class="%s(azClass[eType])">%s(azType[eType])</td>
435
436 /* Ticket */
437 if( sqlite3_strglob("*[ascnqtw]*",zCap)==0 ){
438 eType = 2;
439 }else if( sqlite3_strglob("*r*",zCap)==0 ){
440 eType = 1;
441 }else{
442 eType = 0;
443
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548548
@ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549549
@ <input type="text" name="captcha" size=8 />
550550
if( showButton ){
551551
@ <input type="submit" value="Submit">
552552
}
553
+ @ <br/>\
554
+ captcha_speakit_button(uSeed, 0);
553555
@ </td></tr></table></div>
554556
}
557
+
558
+/*
559
+** Add a "Speak the captcha" button.
560
+*/
561
+void captcha_speakit_button(unsigned int uSeed, const char *zMsg){
562
+ if( zMsg==0 ) zMsg = "Speak the text";
563
+ @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \
564
+ @ id="speakthetext">
565
+ @ <script nonce="%h(style_nonce())">
566
+ @ document.getElementById("speakthetext").onclick = function(){
567
+ @ var audio = window.fossilAudioCaptcha \
568
+ @ || new Audio("%R/captcha-audio/%u(uSeed)");
569
+ @ window.fossilAudioCaptcha = audio;
570
+ @ audio.currentTime = 0;
571
+ @ audio.play();
572
+ @ }
573
+ @ </script>
574
+}
555575
556576
/*
557577
** WEBPAGE: test-captcha
558578
** Test the captcha-generator by rendering the value of the name= query
559579
** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
608628
captcha_generate(1);
609629
@ </form>
610630
style_footer();
611631
return 1;
612632
}
633
+
634
+/*
635
+** Generate a WAV file that reads aloud the hex digits given by
636
+** zHex.
637
+*/
638
+static void captcha_wav(const char *zHex, Blob *pOut){
639
+ int i;
640
+ const int szWavHdr = 44;
641
+ blob_init(pOut, 0, 0);
642
+ blob_resize(pOut, szWavHdr); /* Space for the WAV header */
643
+ pOut->nUsed = szWavHdr;
644
+ memset(pOut->aData, 0, szWavHdr);
645
+ if( zHex==0 || zHex[0]==0 ) zHex = "0";
646
+ for(i=0; zHex[i]; i++){
647
+ int v = hex_digit_value(zHex[i]);
648
+ int sz;
649
+ int nData;
650
+ const unsigned char *pData;
651
+ char zSoundName[50];
652
+ sqlite3_snprintf(sizeof(zSoundName),zSoundName,"sounds/%c.wav",
653
+ "0123456789abcdef"[v]);
654
+ /* Extra silence in between letters */
655
+ if( i>0 ){
656
+ int nQuiet = 3000;
657
+ blob_resize(pOut, pOut->nUsed+nQuiet);
658
+ memset(pOut->aData+pOut->nUsed-nQuiet, 0x80, nQuiet);
659
+ }
660
+ pData = builtin_file(zSoundName, &sz);
661
+ nData = sz - szWavHdr;
662
+ blob_resize(pOut, pOut->nUsed+nData);
663
+ memcpy(pOut->aData+pOut->nUsed-nData, pData+szWavHdr, nData);
664
+ if( zHex[i+1]==0 ){
665
+ int len = pOut->nUsed + 36;
666
+ memcpy(pOut->aData, pData, szWavHdr);
667
+ pOut->aData[4] = (char)(len&0xff);
668
+ pOut->aData[5] = (char)((len>>8)&0xff);
669
+ pOut->aData[6] = (char)((len>>16)&0xff);
670
+ pOut->aData[7] = (char)((len>>24)&0xff);
671
+ len = pOut->nUsed;
672
+ pOut->aData[40] = (char)(len&0xff);
673
+ pOut->aData[41] = (char)((len>>8)&0xff);
674
+ pOut->aData[42] = (char)((len>>16)&0xff);
675
+ pOut->aData[43] = (char)((len>>24)&0xff);
676
+ }
677
+ }
678
+}
679
+
680
+/*
681
+** WEBPAGE: /captcha-audio
682
+**
683
+** Return a WAV file that pronounces the digits of the captcha that
684
+** is determined by the seed given in the name= query parameter.
685
+*/
686
+void captcha_wav_page(void){
687
+ const char *zSeed = P("name");
688
+ const char *zDecode = captcha_decode((unsigned int)atoi(zSeed));
689
+ Blob audio;
690
+ captcha_wav(zDecode, &audio);
691
+ cgi_set_content_type("audio/wav");
692
+ cgi_set_content(&audio);
693
+}
694
+
695
+/*
696
+** WEBPAGE: /test-captcha-audio
697
+**
698
+** Return a WAV file that pronounces the hex digits of the name=
699
+** query parameter.
700
+*/
701
+void captcha_test_wav_page(void){
702
+ const char *zSeed = P("name");
703
+ Blob audio;
704
+ captcha_wav(zSeed, &audio);
705
+ cgi_set_content_type("audio/wav");
706
+ cgi_set_content(&audio);
707
+}
613708
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548 @ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549 @ <input type="text" name="captcha" size=8 />
550 if( showButton ){
551 @ <input type="submit" value="Submit">
552 }
 
 
553 @ </td></tr></table></div>
554 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
556 /*
557 ** WEBPAGE: test-captcha
558 ** Test the captcha-generator by rendering the value of the name= query
559 ** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
608 captcha_generate(1);
609 @ </form>
610 style_footer();
611 return 1;
612 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
--- src/captcha.c
+++ src/captcha.c
@@ -548,12 +548,32 @@
548 @ <input type="hidden" name="captchaseed" value="%u(uSeed)" />
549 @ <input type="text" name="captcha" size=8 />
550 if( showButton ){
551 @ <input type="submit" value="Submit">
552 }
553 @ <br/>\
554 captcha_speakit_button(uSeed, 0);
555 @ </td></tr></table></div>
556 }
557
558 /*
559 ** Add a "Speak the captcha" button.
560 */
561 void captcha_speakit_button(unsigned int uSeed, const char *zMsg){
562 if( zMsg==0 ) zMsg = "Speak the text";
563 @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \
564 @ id="speakthetext">
565 @ <script nonce="%h(style_nonce())">
566 @ document.getElementById("speakthetext").onclick = function(){
567 @ var audio = window.fossilAudioCaptcha \
568 @ || new Audio("%R/captcha-audio/%u(uSeed)");
569 @ window.fossilAudioCaptcha = audio;
570 @ audio.currentTime = 0;
571 @ audio.play();
572 @ }
573 @ </script>
574 }
575
576 /*
577 ** WEBPAGE: test-captcha
578 ** Test the captcha-generator by rendering the value of the name= query
579 ** parameter using ascii-art. If name= is omitted, show a random 16-digit
@@ -608,5 +628,80 @@
628 captcha_generate(1);
629 @ </form>
630 style_footer();
631 return 1;
632 }
633
634 /*
635 ** Generate a WAV file that reads aloud the hex digits given by
636 ** zHex.
637 */
638 static void captcha_wav(const char *zHex, Blob *pOut){
639 int i;
640 const int szWavHdr = 44;
641 blob_init(pOut, 0, 0);
642 blob_resize(pOut, szWavHdr); /* Space for the WAV header */
643 pOut->nUsed = szWavHdr;
644 memset(pOut->aData, 0, szWavHdr);
645 if( zHex==0 || zHex[0]==0 ) zHex = "0";
646 for(i=0; zHex[i]; i++){
647 int v = hex_digit_value(zHex[i]);
648 int sz;
649 int nData;
650 const unsigned char *pData;
651 char zSoundName[50];
652 sqlite3_snprintf(sizeof(zSoundName),zSoundName,"sounds/%c.wav",
653 "0123456789abcdef"[v]);
654 /* Extra silence in between letters */
655 if( i>0 ){
656 int nQuiet = 3000;
657 blob_resize(pOut, pOut->nUsed+nQuiet);
658 memset(pOut->aData+pOut->nUsed-nQuiet, 0x80, nQuiet);
659 }
660 pData = builtin_file(zSoundName, &sz);
661 nData = sz - szWavHdr;
662 blob_resize(pOut, pOut->nUsed+nData);
663 memcpy(pOut->aData+pOut->nUsed-nData, pData+szWavHdr, nData);
664 if( zHex[i+1]==0 ){
665 int len = pOut->nUsed + 36;
666 memcpy(pOut->aData, pData, szWavHdr);
667 pOut->aData[4] = (char)(len&0xff);
668 pOut->aData[5] = (char)((len>>8)&0xff);
669 pOut->aData[6] = (char)((len>>16)&0xff);
670 pOut->aData[7] = (char)((len>>24)&0xff);
671 len = pOut->nUsed;
672 pOut->aData[40] = (char)(len&0xff);
673 pOut->aData[41] = (char)((len>>8)&0xff);
674 pOut->aData[42] = (char)((len>>16)&0xff);
675 pOut->aData[43] = (char)((len>>24)&0xff);
676 }
677 }
678 }
679
680 /*
681 ** WEBPAGE: /captcha-audio
682 **
683 ** Return a WAV file that pronounces the digits of the captcha that
684 ** is determined by the seed given in the name= query parameter.
685 */
686 void captcha_wav_page(void){
687 const char *zSeed = P("name");
688 const char *zDecode = captcha_decode((unsigned int)atoi(zSeed));
689 Blob audio;
690 captcha_wav(zDecode, &audio);
691 cgi_set_content_type("audio/wav");
692 cgi_set_content(&audio);
693 }
694
695 /*
696 ** WEBPAGE: /test-captcha-audio
697 **
698 ** Return a WAV file that pronounces the hex digits of the name=
699 ** query parameter.
700 */
701 void captcha_test_wav_page(void){
702 const char *zSeed = P("name");
703 Blob audio;
704 captcha_wav(zSeed, &audio);
705 cgi_set_content_type("audio/wav");
706 cgi_set_content(&audio);
707 }
708
+107 -45
--- src/cgi.c
+++ src/cgi.c
@@ -174,14 +174,16 @@
174174
}
175175
176176
/*
177177
** Additional information used to form the HTTP reply
178178
*/
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 */
181181
static int iReplyStatus = 200; /* Reply status code */
182182
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 */
183185
184186
/*
185187
** Set the reply content type
186188
*/
187189
void cgi_set_content_type(const char *zType){
@@ -209,10 +211,16 @@
209211
/*
210212
** Append text to the header of an HTTP reply
211213
*/
212214
void cgi_append_header(const char *zLine){
213215
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);
214222
}
215223
216224
/*
217225
** Set a cookie by queuing up the appropriate HTTP header output. If
218226
** !g.isHTTP, this is a no-op.
@@ -266,22 +274,30 @@
266274
iReplyStatus = 200;
267275
zReplyStatus = "OK";
268276
}
269277
270278
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
+ }
271286
fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus);
272287
fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0)));
273288
fprintf(g.httpOut, "Connection: close\r\n");
274289
fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n");
275290
}else{
291
+ assert( rangeEnd==0 );
276292
fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus);
277293
}
278294
if( g.isConst ){
279295
/* isConst means that the reply is guaranteed to be invariant, even
280296
** after configuration changes and/or Fossil binary recompiles. */
281297
fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n");
282
- }else if( etag_tag()!=0 ){
298
+ }else if( etag_tag()[0]!=0 ){
283299
fprintf(g.httpOut, "ETag: %s\r\n", etag_tag());
284300
fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage());
285301
}else{
286302
fprintf(g.httpOut, "Cache-control: no-cache\r\n");
287303
}
@@ -313,18 +329,18 @@
313329
*/
314330
315331
/* Content intended for logged in users should only be cached in
316332
** the browser, not some shared location.
317333
*/
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 ){
326342
int i;
327343
gzip_begin(0);
328344
for( i=0; i<2; i++ ){
329345
int size = blob_size(&cgiContent[i]);
330346
if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size);
@@ -333,23 +349,37 @@
333349
gzip_finish(&cgiContent[0]);
334350
fprintf(g.httpOut, "Content-Encoding: gzip\r\n");
335351
fprintf(g.httpOut, "Vary: Accept-Encoding\r\n");
336352
}
337353
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
+ }
338359
fprintf(g.httpOut, "Content-Length: %d\r\n", total_size);
339360
}else{
340361
total_size = 0;
341362
}
342363
fprintf(g.httpOut, "\r\n");
343
- if( total_size>0 && iReplyStatus != 304
364
+ if( total_size>0
365
+ && iReplyStatus!=304
344366
&& fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0
345367
){
346368
int i, size;
347369
for(i=0; i<2; i++){
348370
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;
351381
}
352382
}
353383
}
354384
fflush(g.httpOut);
355385
CGIDEBUG(("-------- END cgi ---------\n"));
@@ -403,10 +433,31 @@
403433
va_list ap;
404434
va_start(ap, zFormat);
405435
cgi_redirect(vmprintf(zFormat, ap));
406436
va_end(ap);
407437
}
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
+}
408459
409460
/*
410461
** Return the URL for the caller. This is obtained from either the
411462
** referer CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter.
412463
** If neither exist, return zDefault.
@@ -682,36 +733,36 @@
682733
return z;
683734
}
684735
685736
/*
686737
** 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
689740
** in *pz.
690741
**
691742
** This routine adds a "\000" to the end of the content (overwriting
692743
** 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.
694745
** The length of the content is stored in *pnContent.
695746
*/
696747
static char *get_bounded_content(
697748
char **pz, /* Content taken from here */
698749
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 */
700751
int *pnContent /* Write the size of the content here */
701752
){
702753
char *z = *pz;
703754
int len = *pLen;
704755
int i;
705
- int nBoundry = strlen(zBoundry);
756
+ int nBoundary = strlen(zBoundary);
706757
*pnContent = len;
707758
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 ){
709760
if( i>0 && z[i-1]=='\r' ) i--;
710761
z[i] = 0;
711762
*pnContent = i;
712
- i += nBoundry;
763
+ i += nBoundary;
713764
break;
714765
}
715766
}
716767
*pz = &z[i];
717768
get_line_from_string(pz, pLen);
@@ -776,22 +827,22 @@
776827
** table.
777828
*/
778829
static void process_multipart_form_data(char *z, int len){
779830
char *zLine;
780831
int nArg, i;
781
- char *zBoundry;
832
+ char *zBoundary;
782833
char *zValue;
783834
char *zName = 0;
784835
int showBytes = 0;
785836
char *azArg[50];
786837
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;
789840
while( (zLine = get_line_from_string(&z, &len))!=0 ){
790841
if( zLine[0]==0 ){
791842
int nContent = 0;
792
- zValue = get_bounded_content(&z, &len, zBoundry, &nContent);
843
+ zValue = get_bounded_content(&z, &len, zBoundary, &nContent);
793844
if( zName && zValue ){
794845
if( fossil_islower(zName[0]) ){
795846
cgi_set_parameter_nocopy(zName, zValue, 1);
796847
if( showBytes ){
797848
cgi_set_parameter_nocopy(mprintf("%s:bytes", zName),
@@ -1035,13 +1086,27 @@
10351086
}
10361087
if( zPathInfo==0 ){
10371088
int i, j;
10381089
for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){}
10391090
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");
10411106
}
1042
-
1107
+#endif
10431108
z = (char*)P("HTTP_COOKIE");
10441109
if( z ){
10451110
z = mprintf("%s",z);
10461111
add_param_list(z, ';');
10471112
}
@@ -1071,29 +1136,18 @@
10711136
if( fossil_strcmp(zType, "application/x-fossil")==0 ){
10721137
blob_read_from_channel(&g.cgiIn, g.httpIn, len);
10731138
blob_uncompress(&g.cgiIn, &g.cgiIn);
10741139
}
10751140
#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 ){
10801143
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.
10951149
*/
10961150
cgi_set_content_type(json_guess_content_type());
10971151
}
10981152
#endif /* FOSSIL_ENABLE_JSON */
10991153
else{
@@ -1603,10 +1657,11 @@
16031657
}
16041658
if( zIpAddr ){
16051659
cgi_setenv("REMOTE_ADDR", zIpAddr);
16061660
g.zIpAddr = mprintf("%s", zIpAddr);
16071661
}
1662
+
16081663
16091664
/* Get all the optional fields that follow the first line.
16101665
*/
16111666
while( fgets(zLine,sizeof(zLine),g.httpIn) ){
16121667
char *zFieldName;
@@ -1648,10 +1703,17 @@
16481703
}else if( fossil_strcmp(zFieldName,"x-forwarded-for:")==0 ){
16491704
const char *zIpAddr = cgi_accept_forwarded_for(zVal);
16501705
if( zIpAddr!=0 ){
16511706
g.zIpAddr = mprintf("%s", zIpAddr);
16521707
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;
16531715
}
16541716
}
16551717
}
16561718
cgi_init();
16571719
cgi_trace(0);
16581720
--- 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 @@
16731673
while( db_step(&q)==SQLITE_ROW ){
16741674
const char *zIntegrateUuid = db_column_text(&q, 0);
16751675
int rid = db_column_int(&q, 1);
16761676
if( is_a_leaf(rid) && !db_exists("SELECT 1 FROM tagxref "
16771677
" 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
16781686
blob_appendf(pOut, "T +closed %s\n", zIntegrateUuid);
16791687
}
16801688
}
16811689
db_finalize(&q);
16821690
@@ -2040,10 +2048,12 @@
20402048
const char *zComment; /* Check-in comment */
20412049
Stmt q; /* Various queries */
20422050
char *zUuid; /* UUID of the new check-in */
20432051
int useHash = 0; /* True to verify file status using hashing */
20442052
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 */
20452055
int isAMerge = 0; /* True if checking in a merge */
20462056
int noWarningFlag = 0; /* True if skipping all warnings */
20472057
int noPrompt = 0; /* True if skipping all prompts */
20482058
int forceFlag = 0; /* Undocumented: Disables all checks */
20492059
int forceDelta = 0; /* Force a delta-manifest */
@@ -2077,14 +2087,20 @@
20772087
memset(&sCiInfo, 0, sizeof(sCiInfo));
20782088
url_proxy_options();
20792089
/* --sha1sum is an undocumented alias for --hash for backwards compatiblity */
20802090
useHash = find_option("hash",0,0)!=0 || find_option("sha1sum",0,0)!=0;
20812091
noSign = find_option("nosign",0,0)!=0;
2092
+ privateFlag = find_option("private",0,0)!=0;
20822093
forceDelta = find_option("delta",0,0)!=0;
20832094
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
+ }
20862102
}
20872103
dryRunFlag = find_option("dry-run","n",0)!=0;
20882104
if( !dryRunFlag ){
20892105
dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
20902106
}
@@ -2109,17 +2125,10 @@
21092125
sizeof(char*)*(nTag+2));
21102126
sCiInfo.azTag[nTag++] = zTag;
21112127
sCiInfo.azTag[nTag] = 0;
21122128
}
21132129
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
- }
21212130
sCiInfo.zDateOvrd = find_option("date-override",0,1);
21222131
sCiInfo.zUserOvrd = find_option("user-override",0,1);
21232132
db_must_be_within_tree();
21242133
noSign = db_get_boolean("omitsign", 0)|noSign;
21252134
if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
@@ -2129,15 +2138,25 @@
21292138
21302139
/* Get the ID of the parent manifest artifact */
21312140
vid = db_lget_int("checkout", 0);
21322141
if( vid==0 ){
21332142
useCksum = 1;
2134
- if( sCiInfo.zBranch==0 ) {
2143
+ if( privateFlag==0 && sCiInfo.zBranch==0 ) {
21352144
sCiInfo.zBranch=db_get("main-branch", 0);
21362145
}
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";
21392158
}
21402159
21412160
/* Do not allow the creation of a new branch using an existing open
21422161
** branch name unless the --force flag is used */
21432162
if( sCiInfo.zBranch!=0
@@ -2160,11 +2179,14 @@
21602179
** manifest) can continue to use this repository, do not create a new
21612180
** delta-manifest unless this repository already contains one or more
21622181
** delta-manifests, or unless the delta-manifest is explicitly requested
21632182
** by the --delta option.
21642183
*/
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
+ ){
21662188
forceBaseline = 1;
21672189
}
21682190
21692191
/*
21702192
** Autosync if autosync is enabled and this is not a private check-in.
@@ -2634,10 +2656,11 @@
26342656
/* Commit */
26352657
db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'");
26362658
db_multi_exec("PRAGMA repository.application_id=252006673;");
26372659
db_multi_exec("PRAGMA localdb.application_id=252006674;");
26382660
if( dryRunFlag ){
2661
+ leaf_ambiguity_warning(nvid,nvid);
26392662
db_end_transaction(1);
26402663
exit(1);
26412664
}
26422665
db_end_transaction(0);
26432666
@@ -2656,7 +2679,9 @@
26562679
int nTries = db_get_int("autosync-tries",1);
26572680
autosync_loop(syncFlags, nTries, 0);
26582681
}
26592682
if( count_nonbranch_children(vid)>1 ){
26602683
fossil_print("**** warning: a fork has occurred *****\n");
2684
+ }else{
2685
+ leaf_ambiguity_warning(nvid,nvid);
26612686
}
26622687
}
26632688
--- 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 @@
204204
);
205205
url_enable_proxy(0);
206206
clone_ssh_db_set_options();
207207
url_get_password_if_needed();
208208
g.xlinkClusterOnly = 1;
209
- nErr = client_sync(syncFlags,CONFIGSET_ALL,0);
209
+ nErr = client_sync(syncFlags,CONFIGSET_ALL,0,0);
210210
g.xlinkClusterOnly = 0;
211211
verify_cancel();
212212
db_end_transaction(0);
213213
db_close(1);
214214
if( nErr ){
215215
--- 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 @@
1919
** text on a TTY.
2020
*/
2121
#include "config.h"
2222
#include "comformat.h"
2323
#include <assert.h>
24
-#ifdef _WIN32
25
-# include <windows.h>
26
-#else
27
-# include <termios.h>
28
-# include <sys/ioctl.h>
29
-#endif
3024
3125
#if INTERFACE
3226
#define COMMENT_PRINT_NONE ((u32)0x00000000) /* No flags = non-legacy. */
3327
#define COMMENT_PRINT_LEGACY ((u32)0x00000001) /* Use legacy algorithm. */
3428
#define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */
@@ -67,35 +61,27 @@
6761
*/
6862
static int comment_set_maxchars(
6963
int indent,
7064
int *pMaxChars
7165
){
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
+ }
9783
}
9884
9985
/*
10086
** This function checks the current line being printed against the original
10187
** comment text. Upon matching, it updates the provided character and line
10288
--- 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 @@
9696
{ "logo-image", CONFIGSET_SKIN },
9797
{ "background-mimetype", CONFIGSET_SKIN },
9898
{ "background-image", CONFIGSET_SKIN },
9999
{ "timeline-block-markup", CONFIGSET_SKIN },
100100
{ "timeline-date-format", CONFIGSET_SKIN },
101
+ { "timeline-default-style", CONFIGSET_SKIN },
101102
{ "timeline-dwelltime", CONFIGSET_SKIN },
102103
{ "timeline-closetime", CONFIGSET_SKIN },
103104
{ "timeline-max-comment", CONFIGSET_SKIN },
104105
{ "timeline-plaintext", CONFIGSET_SKIN },
105106
{ "timeline-truncate-at-blank", CONFIGSET_SKIN },
107
+ { "timeline-tslink-info", CONFIGSET_SKIN },
106108
{ "timeline-utc", CONFIGSET_SKIN },
107109
{ "adunit", CONFIGSET_SKIN },
108110
{ "adunit-omit-if-admin", CONFIGSET_SKIN },
109111
{ "adunit-omit-if-user", CONFIGSET_SKIN },
112
+ { "default-csp", CONFIGSET_SKIN },
110113
{ "sitemap-docidx", CONFIGSET_SKIN },
111114
{ "sitemap-download", CONFIGSET_SKIN },
112115
{ "sitemap-license", CONFIGSET_SKIN },
113116
{ "sitemap-contact", CONFIGSET_SKIN },
114117
@@ -144,10 +147,11 @@
144147
{ "parent-project-code", CONFIGSET_PROJ },
145148
{ "parent-project-name", CONFIGSET_PROJ },
146149
{ "hash-policy", CONFIGSET_PROJ },
147150
{ "comment-format", CONFIGSET_PROJ },
148151
{ "mimetypes", CONFIGSET_PROJ },
152
+ { "forbid-delta-manifests", CONFIGSET_PROJ },
149153
150154
#ifdef FOSSIL_ENABLE_LEGACY_MV_RM
151155
{ "mv-rm-files", CONFIGSET_PROJ },
152156
#endif
153157
@@ -814,15 +818,15 @@
814818
if( g.url.protocol==0 ) fossil_fatal("no server URL specified");
815819
user_select();
816820
url_enable_proxy("via proxy: ");
817821
if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE;
818822
if( strncmp(zMethod, "push", n)==0 ){
819
- client_sync(0,0,(unsigned)mask);
823
+ client_sync(0,0,(unsigned)mask,0);
820824
}else if( strncmp(zMethod, "pull", n)==0 ){
821
- client_sync(0,(unsigned)mask,0);
825
+ client_sync(0,(unsigned)mask,0,0);
822826
}else{
823
- client_sync(0,(unsigned)mask,(unsigned)mask);
827
+ client_sync(0,(unsigned)mask,(unsigned)mask,0);
824828
}
825829
}else
826830
if( strncmp(zMethod, "reset", n)==0 ){
827831
int mask, i;
828832
char *zBackup;
829833
--- 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
--- src/content.c
+++ src/content.c
@@ -777,10 +777,22 @@
777777
"DELETE FROM private WHERE rid=:rid"
778778
);
779779
db_bind_int(&s1, ":rid", rid);
780780
db_exec(&s1);
781781
}
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
+}
782794
783795
/*
784796
** Try to change the storage of rid so that it is a delta from one
785797
** of the artifacts given in aSrc[0]..aSrc[nSrc-1]. The aSrc[*] that
786798
** gives the smallest delta is choosen.
@@ -932,13 +944,19 @@
932944
** Verify that all content can be extracted from the BLOB table correctly.
933945
** If the BLOB table is correct, then the repository can always be
934946
** successfully reconstructed using "fossil rebuild".
935947
**
936948
** Options:
949
+**
950
+** -d|--db-only Run "PRAGMA integrity_check" on the database only.
951
+** No other validation is performed.
937952
**
938953
** --parse Parse all manifests, wikis, tickets, events, and
939954
** so forth, reporting any errors found.
955
+**
956
+** -q|--quick Run "PRAGMA quick_check" on the database only.
957
+** No other validation is performed.
940958
*/
941959
void test_integrity(void){
942960
Stmt q;
943961
Blob content;
944962
int n1 = 0;
@@ -946,11 +964,25 @@
946964
int nErr = 0;
947965
int total;
948966
int nCA = 0;
949967
int anCA[10];
950968
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;
951971
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
+ }
952984
memset(anCA, 0, sizeof(anCA));
953985
954986
/* Make sure no public artifact is a delta from a private artifact */
955987
db_prepare(&q,
956988
"SELECT "
957989
--- 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 @@
126126
for(i=0; i<cookies.nParam && strcmp(zPName,cookies.aParam[i].zPName); i++){}
127127
if( zQVal==0 && (flags & COOKIE_READ)!=0 && i<cookies.nParam ){
128128
cgi_set_parameter_nocopy(zQP, cookies.aParam[i].zPValue, 1);
129129
return;
130130
}
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
+ }
132135
if( (flags & COOKIE_WRITE)!=0
133136
&& i<COOKIE_NPARAM
134137
&& (i==cookies.nParam || strcmp(zQVal, cookies.aParam[i].zPValue))
135138
){
136139
if( i==cookies.nParam ){
137140
--- 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
+194 -80
--- src/db.c
+++ src/db.c
@@ -18,11 +18,12 @@
1818
** Code for interfacing to the various databases.
1919
**
2020
** There are three separate database files that fossil interacts
2121
** with:
2222
**
23
-** (1) The "user" database in ~/.fossil
23
+** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db
24
+** or in %LOCALAPPDATA%/_fossil
2425
**
2526
** (2) The "repository" database
2627
**
2728
** (3) A local checkout database named "_FOSSIL_" or ".fslckout"
2829
** and located at the root of the local copy of the source tree.
@@ -653,23 +654,17 @@
653654
blob_reset(&sql);
654655
return rc;
655656
}
656657
657658
/*
658
-** Execute multiple SQL statements.
659
+** Execute multiple SQL statements. The input text is executed
660
+** directly without any formatting.
659661
*/
660
-int db_multi_exec(const char *zSql, ...){
661
- Blob sql;
662
+int db_exec_sql(const char *z){
662663
int rc = SQLITE_OK;
663
- va_list ap;
664
- const char *z, *zEnd;
665664
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;
671666
while( rc==SQLITE_OK && z[0] ){
672667
pStmt = 0;
673668
rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd);
674669
if( rc ){
675670
db_err("%s: {%s}", sqlite3_errmsg(g.db), z);
@@ -679,10 +674,26 @@
679674
rc = sqlite3_finalize(pStmt);
680675
if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z);
681676
}
682677
z = zEnd;
683678
}
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));
684695
blob_reset(&sql);
685696
return rc;
686697
}
687698
688699
/*
@@ -1031,10 +1042,12 @@
10311042
0, capability_union_step, capability_union_finalize);
10321043
sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0,
10331044
capability_fullcap, 0, 0);
10341045
sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0,
10351046
alert_find_emailaddr_func, 0, 0);
1047
+ sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0,
1048
+ alert_display_name_func, 0, 0);
10361049
}
10371050
10381051
#if USE_SEE
10391052
/*
10401053
** This is a pointer to the saved database encryption key string.
@@ -1258,11 +1271,11 @@
12581271
db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0
12591272
);
12601273
sqlite3_create_function(
12611274
db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0
12621275
);
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);
12641277
db_add_aux_functions(db);
12651278
re_add_sql_func(db); /* The REGEXP operator */
12661279
foci_register(db); /* The "files_of_checkin" virtual table */
12671280
sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0);
12681281
#if USE_SYSTEM_SQLITE+0==1
@@ -1289,17 +1302,17 @@
12891302
blob_init(&key, 0, 0);
12901303
db_maybe_obtain_encryption_key(zDbName, &key);
12911304
if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){
12921305
char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q",
12931306
zDbName, zLabel, blob_str(&key));
1294
- db_multi_exec(zCmd /*works-like:""*/);
1307
+ db_exec_sql(zCmd);
12951308
fossil_secure_zero(zCmd, strlen(zCmd));
12961309
sqlite3_free(zCmd);
12971310
}else{
12981311
char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''",
12991312
zDbName, zLabel);
1300
- db_multi_exec(zCmd /*works-like:""*/);
1313
+ db_exec_sql(zCmd);
13011314
sqlite3_free(zCmd);
13021315
#if USE_SEE
13031316
if( blob_size(&key)>0 ){
13041317
sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1);
13051318
}
@@ -1360,11 +1373,11 @@
13601373
db_attach(zDbName, zLabel);
13611374
}
13621375
}
13631376
13641377
/*
1365
-** Close the per-user database file in ~/.fossil
1378
+** Close the per-user configuration database file
13661379
*/
13671380
void db_close_config(){
13681381
int iSlot = db_database_slot("configdb");
13691382
if( iSlot>0 ){
13701383
db_detach("configdb");
@@ -1384,31 +1397,30 @@
13841397
fossil_free(g.zConfigDbName);
13851398
g.zConfigDbName = 0;
13861399
}
13871400
13881401
/*
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.
13911404
**
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().
13991406
*/
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
+ */
14091420
#if defined(_WIN32) || defined(__CYGWIN__)
1421
+ zHome = fossil_getenv("FOSSIL_HOME");
14101422
if( zHome==0 ){
14111423
zHome = fossil_getenv("LOCALAPPDATA");
14121424
if( zHome==0 ){
14131425
zHome = fossil_getenv("APPDATA");
14141426
if( zHome==0 ){
@@ -1419,40 +1431,98 @@
14191431
if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath);
14201432
}
14211433
}
14221434
}
14231435
}
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);
14541524
}
14551525
db_init_database(zDbName, zConfigSchema, (char*)0);
14561526
}
14571527
if( file_access(zDbName, W_OK) ){
14581528
if( isOptional ) return 0;
@@ -1769,13 +1839,13 @@
17691839
"UPDATE vfile"
17701840
" SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)"
17711841
" WHERE mrid!=rid;"
17721842
);
17731843
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(
17771847
"INSERT OR IGNORE INTO vmerge(id,merge,mhash)"
17781848
" SELECT id, merge, blob.uuid FROM old_vmerge, blob"
17791849
" WHERE old_vmerge.merge=blob.rid;"
17801850
"DROP TABLE old_vmerge;"
17811851
);
@@ -2081,11 +2151,11 @@
20812151
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20822152
" VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');"
20832153
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20842154
" VALUES('nobody','','gjorz','Nobody');"
20852155
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
2086
- " VALUES('developer','','dei','Dev');"
2156
+ " VALUES('developer','','ei','Dev');"
20872157
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20882158
" VALUES('reader','','kptw','Reader');"
20892159
);
20902160
}
20912161
}
@@ -2305,11 +2375,11 @@
23052375
** SQL functions for debugging.
23062376
**
23072377
** The print() function writes its arguments on stdout, but only
23082378
** if the -sqlprint command-line option is turned on.
23092379
*/
2310
-LOCAL void db_sql_print(
2380
+void db_sql_print(
23112381
sqlite3_context *context,
23122382
int argc,
23132383
sqlite3_value **argv
23142384
){
23152385
int i;
@@ -2318,19 +2388,40 @@
23182388
char c = i==argc-1 ? '\n' : ' ';
23192389
fossil_print("%s%c", sqlite3_value_text(argv[i]), c);
23202390
}
23212391
}
23222392
}
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){
23242398
sqlite3_stmt *pStmt = (sqlite3_stmt*)pP;
23252399
char *zSql;
23262400
int n;
23272401
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
+ }
23282411
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
+ }
23292420
zSql = sqlite3_expanded_sql(pStmt);
23302421
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);
23322423
sqlite3_free(zSql);
23332424
return 0;
23342425
}
23352426
23362427
/*
@@ -2492,16 +2583,16 @@
24922583
return 0;
24932584
}
24942585
24952586
/*
24962587
** 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.
24982589
** Be sure to swap them back after doing the operation.
24992590
**
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.
25032594
*/
25042595
void db_swap_connections(void){
25052596
/*
25062597
** When swapping the main database connection with the config database
25072598
** connection, the config database connection must be open (not simply
@@ -3493,10 +3584,14 @@
34933584
/*
34943585
** SETTING: pgp-command width=40
34953586
** Command used to clear-sign manifests at check-in.
34963587
** Default value is "gpg --clearsign -o"
34973588
*/
3589
+/*
3590
+** SETTING: forbid-delta-manifests boolean default=off
3591
+** If enabled, new delta manifests are prohibited.
3592
+*/
34983593
/*
34993594
** SETTING: proxy width=32 default=off
35003595
** URL of the HTTP proxy. If undefined or "off" then
35013596
** the "http_proxy" environment variable is consulted.
35023597
** If the http_proxy environment variable is undefined
@@ -3627,10 +3722,30 @@
36273722
** SETTING: th1-uri-regexp width=40 block-text
36283723
** Specify which URI's are allowed in HTTP requests from
36293724
** TH1 scripts. If empty, no HTTP requests are allowed
36303725
** whatsoever.
36313726
*/
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
+*/
36323747
/*
36333748
** SETTING: uv-sync boolean default=off
36343749
** If true, automatically send unversioned files as part
36353750
** of a "fossil clone" or "fossil sync" command. The
36363751
** default is false, in which case the -u option is
@@ -3695,14 +3810,13 @@
36953810
** The "unset" command clears a setting.
36963811
**
36973812
** Settings can have both a "local" repository-only value and "global" value
36983813
** that applies to all repositories. The local values are stored in the
36993814
** "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.
37043818
**
37053819
** Options:
37063820
** --global set or unset the given property globally instead of
37073821
** setting or unsetting it for the open repository only.
37083822
**
@@ -3833,12 +3947,12 @@
38333947
** COMMAND: test-without-rowid
38343948
**
38353949
** Usage: %fossil test-without-rowid FILENAME...
38363950
**
38373951
** 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.
38403954
**
38413955
** The purpose of this command is for testing the WITHOUT ROWID capabilities
38423956
** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil.
38433957
**
38443958
** Options:
38453959
--- 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
+194 -80
--- src/db.c
+++ src/db.c
@@ -18,11 +18,12 @@
1818
** Code for interfacing to the various databases.
1919
**
2020
** There are three separate database files that fossil interacts
2121
** with:
2222
**
23
-** (1) The "user" database in ~/.fossil
23
+** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db
24
+** or in %LOCALAPPDATA%/_fossil
2425
**
2526
** (2) The "repository" database
2627
**
2728
** (3) A local checkout database named "_FOSSIL_" or ".fslckout"
2829
** and located at the root of the local copy of the source tree.
@@ -653,23 +654,17 @@
653654
blob_reset(&sql);
654655
return rc;
655656
}
656657
657658
/*
658
-** Execute multiple SQL statements.
659
+** Execute multiple SQL statements. The input text is executed
660
+** directly without any formatting.
659661
*/
660
-int db_multi_exec(const char *zSql, ...){
661
- Blob sql;
662
+int db_exec_sql(const char *z){
662663
int rc = SQLITE_OK;
663
- va_list ap;
664
- const char *z, *zEnd;
665664
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;
671666
while( rc==SQLITE_OK && z[0] ){
672667
pStmt = 0;
673668
rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd);
674669
if( rc ){
675670
db_err("%s: {%s}", sqlite3_errmsg(g.db), z);
@@ -679,10 +674,26 @@
679674
rc = sqlite3_finalize(pStmt);
680675
if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z);
681676
}
682677
z = zEnd;
683678
}
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));
684695
blob_reset(&sql);
685696
return rc;
686697
}
687698
688699
/*
@@ -1031,10 +1042,12 @@
10311042
0, capability_union_step, capability_union_finalize);
10321043
sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0,
10331044
capability_fullcap, 0, 0);
10341045
sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0,
10351046
alert_find_emailaddr_func, 0, 0);
1047
+ sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0,
1048
+ alert_display_name_func, 0, 0);
10361049
}
10371050
10381051
#if USE_SEE
10391052
/*
10401053
** This is a pointer to the saved database encryption key string.
@@ -1258,11 +1271,11 @@
12581271
db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0
12591272
);
12601273
sqlite3_create_function(
12611274
db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0
12621275
);
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);
12641277
db_add_aux_functions(db);
12651278
re_add_sql_func(db); /* The REGEXP operator */
12661279
foci_register(db); /* The "files_of_checkin" virtual table */
12671280
sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0);
12681281
#if USE_SYSTEM_SQLITE+0==1
@@ -1289,17 +1302,17 @@
12891302
blob_init(&key, 0, 0);
12901303
db_maybe_obtain_encryption_key(zDbName, &key);
12911304
if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){
12921305
char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q",
12931306
zDbName, zLabel, blob_str(&key));
1294
- db_multi_exec(zCmd /*works-like:""*/);
1307
+ db_exec_sql(zCmd);
12951308
fossil_secure_zero(zCmd, strlen(zCmd));
12961309
sqlite3_free(zCmd);
12971310
}else{
12981311
char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''",
12991312
zDbName, zLabel);
1300
- db_multi_exec(zCmd /*works-like:""*/);
1313
+ db_exec_sql(zCmd);
13011314
sqlite3_free(zCmd);
13021315
#if USE_SEE
13031316
if( blob_size(&key)>0 ){
13041317
sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1);
13051318
}
@@ -1360,11 +1373,11 @@
13601373
db_attach(zDbName, zLabel);
13611374
}
13621375
}
13631376
13641377
/*
1365
-** Close the per-user database file in ~/.fossil
1378
+** Close the per-user configuration database file
13661379
*/
13671380
void db_close_config(){
13681381
int iSlot = db_database_slot("configdb");
13691382
if( iSlot>0 ){
13701383
db_detach("configdb");
@@ -1384,31 +1397,30 @@
13841397
fossil_free(g.zConfigDbName);
13851398
g.zConfigDbName = 0;
13861399
}
13871400
13881401
/*
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.
13911404
**
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().
13991406
*/
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
+ */
14091420
#if defined(_WIN32) || defined(__CYGWIN__)
1421
+ zHome = fossil_getenv("FOSSIL_HOME");
14101422
if( zHome==0 ){
14111423
zHome = fossil_getenv("LOCALAPPDATA");
14121424
if( zHome==0 ){
14131425
zHome = fossil_getenv("APPDATA");
14141426
if( zHome==0 ){
@@ -1419,40 +1431,98 @@
14191431
if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath);
14201432
}
14211433
}
14221434
}
14231435
}
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);
14541524
}
14551525
db_init_database(zDbName, zConfigSchema, (char*)0);
14561526
}
14571527
if( file_access(zDbName, W_OK) ){
14581528
if( isOptional ) return 0;
@@ -1769,13 +1839,13 @@
17691839
"UPDATE vfile"
17701840
" SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)"
17711841
" WHERE mrid!=rid;"
17721842
);
17731843
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(
17771847
"INSERT OR IGNORE INTO vmerge(id,merge,mhash)"
17781848
" SELECT id, merge, blob.uuid FROM old_vmerge, blob"
17791849
" WHERE old_vmerge.merge=blob.rid;"
17801850
"DROP TABLE old_vmerge;"
17811851
);
@@ -2081,11 +2151,11 @@
20812151
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20822152
" VALUES('anonymous',hex(randomblob(8)),'hmnc','Anon');"
20832153
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20842154
" VALUES('nobody','','gjorz','Nobody');"
20852155
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
2086
- " VALUES('developer','','dei','Dev');"
2156
+ " VALUES('developer','','ei','Dev');"
20872157
"INSERT OR IGNORE INTO user(login,pw,cap,info)"
20882158
" VALUES('reader','','kptw','Reader');"
20892159
);
20902160
}
20912161
}
@@ -2305,11 +2375,11 @@
23052375
** SQL functions for debugging.
23062376
**
23072377
** The print() function writes its arguments on stdout, but only
23082378
** if the -sqlprint command-line option is turned on.
23092379
*/
2310
-LOCAL void db_sql_print(
2380
+void db_sql_print(
23112381
sqlite3_context *context,
23122382
int argc,
23132383
sqlite3_value **argv
23142384
){
23152385
int i;
@@ -2318,19 +2388,40 @@
23182388
char c = i==argc-1 ? '\n' : ' ';
23192389
fossil_print("%s%c", sqlite3_value_text(argv[i]), c);
23202390
}
23212391
}
23222392
}
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){
23242398
sqlite3_stmt *pStmt = (sqlite3_stmt*)pP;
23252399
char *zSql;
23262400
int n;
23272401
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
+ }
23282411
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
+ }
23292420
zSql = sqlite3_expanded_sql(pStmt);
23302421
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);
23322423
sqlite3_free(zSql);
23332424
return 0;
23342425
}
23352426
23362427
/*
@@ -2492,16 +2583,16 @@
24922583
return 0;
24932584
}
24942585
24952586
/*
24962587
** 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.
24982589
** Be sure to swap them back after doing the operation.
24992590
**
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.
25032594
*/
25042595
void db_swap_connections(void){
25052596
/*
25062597
** When swapping the main database connection with the config database
25072598
** connection, the config database connection must be open (not simply
@@ -3493,10 +3584,14 @@
34933584
/*
34943585
** SETTING: pgp-command width=40
34953586
** Command used to clear-sign manifests at check-in.
34963587
** Default value is "gpg --clearsign -o"
34973588
*/
3589
+/*
3590
+** SETTING: forbid-delta-manifests boolean default=off
3591
+** If enabled, new delta manifests are prohibited.
3592
+*/
34983593
/*
34993594
** SETTING: proxy width=32 default=off
35003595
** URL of the HTTP proxy. If undefined or "off" then
35013596
** the "http_proxy" environment variable is consulted.
35023597
** If the http_proxy environment variable is undefined
@@ -3627,10 +3722,30 @@
36273722
** SETTING: th1-uri-regexp width=40 block-text
36283723
** Specify which URI's are allowed in HTTP requests from
36293724
** TH1 scripts. If empty, no HTTP requests are allowed
36303725
** whatsoever.
36313726
*/
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
+*/
36323747
/*
36333748
** SETTING: uv-sync boolean default=off
36343749
** If true, automatically send unversioned files as part
36353750
** of a "fossil clone" or "fossil sync" command. The
36363751
** default is false, in which case the -u option is
@@ -3695,14 +3810,13 @@
36953810
** The "unset" command clears a setting.
36963811
**
36973812
** Settings can have both a "local" repository-only value and "global" value
36983813
** that applies to all repositories. The local values are stored in the
36993814
** "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.
37043818
**
37053819
** Options:
37063820
** --global set or unset the given property globally instead of
37073821
** setting or unsetting it for the open repository only.
37083822
**
@@ -3833,12 +3947,12 @@
38333947
** COMMAND: test-without-rowid
38343948
**
38353949
** Usage: %fossil test-without-rowid FILENAME...
38363950
**
38373951
** 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.
38403954
**
38413955
** The purpose of this command is for testing the WITHOUT ROWID capabilities
38423956
** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil.
38433957
**
38443958
** Options:
38453959
--- 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
--- src/default_css.txt
+++ src/default_css.txt
@@ -741,15 +741,32 @@
741741
div.forumEdit {
742742
border: 1px solid black;
743743
padding-left: 1ex;
744744
padding-right: 1ex;
745745
}
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
+}
746759
div.forumHier, div.forumTime {
747760
border: 1px solid black;
748761
padding-left: 1ex;
749762
padding-right: 1ex;
750763
margin-top: 1ex;
764
+}
765
+div.forumPostBody {
766
+ max-height: 40em;
767
+ overflow: auto;
751768
}
752769
div.forumSel {
753770
background-color: #cef;
754771
}
755772
div.forumObs {
@@ -799,5 +816,55 @@
799816
margin-right: 0;
800817
}
801818
.nobr {
802819
white-space: nowrap;
803820
}
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
+}
804871
--- 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 @@
22
** Copyright (c) 2007 D. Richard Hipp
33
**
44
** This program is free software; you can redistribute it and/or
55
** modify it under the terms of the Simplified BSD License (also
66
** known as the "2-Clause License" or "FreeBSD License".)
7
-
7
+**
88
** This program is distributed in the hope that it will be useful,
99
** but without any warranty; without even the implied warranty of
1010
** merchantability or fitness for a particular purpose.
1111
**
1212
** Author contact information:
@@ -60,13 +60,13 @@
6060
6161
#define DIFF_WHITESPACE_ONLY \
6262
"whitespace changes only\n"
6363
6464
/*
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)
6666
*/
67
-#define LENGTH_MASK_SZ 13
67
+#define LENGTH_MASK_SZ 15
6868
#define LENGTH_MASK ((1<<LENGTH_MASK_SZ)-1)
6969
7070
#endif /* INTERFACE */
7171
7272
/*
@@ -76,20 +76,20 @@
7676
** of the line. If any line is longer than LENGTH_MASK characters,
7777
** the file is considered binary.
7878
*/
7979
typedef struct DLine DLine;
8080
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 */
8383
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) */
8686
8787
/* an array of DLine elements serves two purposes. The fields
8888
** above are one per line of input text. But each entry is also
8989
** 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) */
9191
};
9292
9393
/*
9494
** Length of a dline
9595
*/
@@ -115,11 +115,11 @@
115115
int nEditAlloc; /* Space allocated for aEdit[] */
116116
DLine *aFrom; /* File on left side of the diff */
117117
int nFrom; /* Number of lines in aFrom[] */
118118
DLine *aTo; /* File on right side of the diff */
119119
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 */
121121
};
122122
123123
/*
124124
** Count the number of lines in the input string. Include the last line
125125
** in the count even if it lacks the \n terminator. If an empty string
@@ -164,11 +164,11 @@
164164
int n,
165165
int *pnLine,
166166
u64 diffFlags
167167
){
168168
int nLine, i, k, nn, s, x;
169
- unsigned int h, h2;
169
+ u64 h, h2;
170170
DLine *a;
171171
const char *zNL;
172172
173173
if( count_lines(z, n, &nLine)==0 ){
174174
return 0;
@@ -205,23 +205,27 @@
205205
for(h=0, x=s; x<k; x++){
206206
char c = z[x];
207207
if( fossil_isspace(c) ){
208208
++numws;
209209
}else{
210
- h += c;
211
- h *= 0x9e3779b1;
210
+ h = (h^c)*9000000000000000041LL;
212211
}
213212
}
214213
k -= numws;
215214
}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;
219220
}
221
+ m = 0;
222
+ memcpy(&m, z+x, k-k2);
223
+ h ^= m;
220224
}
221225
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);
223227
h2 = h % nLine;
224228
a[i].iNext = a[h2].iHash;
225229
a[h2].iHash = i+1;
226230
z += nn+1; n -= nn+1;
227231
i++;
@@ -232,33 +236,34 @@
232236
*pnLine = nLine;
233237
return a;
234238
}
235239
236240
/*
237
-** Return true if two DLine elements are identical.
241
+** Return zero if two DLine elements are identical.
238242
*/
239243
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);
241246
}
242247
243248
/*
244
-** Return true if two DLine elements are identical, ignoring
249
+** Return zero if two DLine elements are identical, ignoring
245250
** all whitespace. The indent field of pA/pB already points
246251
** to the first non-space character in the string.
247252
*/
248253
249254
static int same_dline_ignore_allws(const DLine *pA, const DLine *pB){
250255
int a = pA->indent, b = pB->indent;
251256
if( pA->h==pB->h ){
252257
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;
254259
while( a<pA->n && fossil_isspace(pA->z[a])) ++a;
255260
while( b<pB->n && fossil_isspace(pB->z[b])) ++b;
256261
}
257
- return pA->n-a == pB->n-b;
262
+ return pA->n-a != pB->n-b;
258263
}
259
- return 0;
264
+ return 1;
260265
}
261266
262267
/*
263268
** Return true if the regular expression *pRe matches any of the
264269
** N dlines
@@ -1444,16 +1449,16 @@
14441449
int iSXb = iS1; /* Best match so far */
14451450
int iSYb = iS2; /* Best match so far */
14461451
14471452
for(i=iS1; i<iE1-mxLength; i++){
14481453
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]) ){
14511456
continue;
14521457
}
14531458
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 ){
14551460
k++;
14561461
}
14571462
if( k>mxLength ){
14581463
iSXb = i;
14591464
iSYb = j;
@@ -1515,11 +1520,11 @@
15151520
mid = (iE1 + iS1)/2;
15161521
for(i=iS1; i<iE1; i++){
15171522
int limit = 0;
15181523
j = p->aTo[p->aFrom[i].h % p->nTo].iHash;
15191524
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]))
15211526
){
15221527
if( limit++ > 10 ){
15231528
j = 0;
15241529
break;
15251530
}
@@ -1532,19 +1537,19 @@
15321537
iSX = i;
15331538
iSY = j-1;
15341539
pA = &p->aFrom[iSX-1];
15351540
pB = &p->aTo[iSY-1];
15361541
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--){}
15381543
iSX -= k;
15391544
iSY -= k;
15401545
iEX = i+1;
15411546
iEY = j;
15421547
pA = &p->aFrom[iEX];
15431548
pB = &p->aTo[iEY];
15441549
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++){}
15461551
iEX += k;
15471552
iEY += k;
15481553
skew = (iSX-iS1) - (iSY-iS2);
15491554
if( skew<0 ) skew = -skew;
15501555
dist = (iSX+iEX)/2 - mid;
@@ -1681,16 +1686,16 @@
16811686
int mnE, iS, iE1, iE2;
16821687
16831688
/* Carve off the common header and footer */
16841689
iE1 = p->nFrom;
16851690
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 ){
16871692
iE1--;
16881693
iE2--;
16891694
}
16901695
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++){}
16921697
16931698
/* do the difference */
16941699
if( iS>0 ){
16951700
appendTriple(p, iS, 0, 0);
16961701
}
@@ -1755,11 +1760,11 @@
17551760
17561761
/* Shift insertions toward the beginning of the file */
17571762
while( cpy>0 && del==0 && ins>0 ){
17581763
DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of insert */
17591764
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;
17611766
if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break;
17621767
lnFrom--;
17631768
lnTo--;
17641769
p->aEdit[r]--;
17651770
p->aEdit[r+3]++;
@@ -1768,11 +1773,11 @@
17681773
17691774
/* Shift insertions toward the end of the file */
17701775
while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){
17711776
DLine *pTop = &p->aTo[lnTo]; /* First line inserted */
17721777
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;
17741779
if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break;
17751780
lnFrom++;
17761781
lnTo++;
17771782
p->aEdit[r]++;
17781783
p->aEdit[r+3]--;
@@ -1781,11 +1786,11 @@
17811786
17821787
/* Shift deletions toward the beginning of the file */
17831788
while( cpy>0 && del>0 && ins==0 ){
17841789
DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of delete */
17851790
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;
17871792
if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break;
17881793
lnFrom--;
17891794
lnTo--;
17901795
p->aEdit[r]--;
17911796
p->aEdit[r+3]++;
@@ -1794,11 +1799,11 @@
17941799
17951800
/* Shift deletions toward the end of the file */
17961801
while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){
17971802
DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */
17981803
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;
18001805
if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break;
18011806
lnFrom++;
18021807
lnTo++;
18031808
p->aEdit[r]++;
18041809
p->aEdit[r+3]--;
@@ -1821,14 +1826,39 @@
18211826
}
18221827
18231828
/*
18241829
** Extract the width of columns for side-by-side diff. Supply an
18251830
** 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
18261837
*/
18271838
int diff_width(u64 diffFlags){
18281839
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
+ }
18301860
return w;
18311861
}
18321862
18331863
/*
18341864
** Append the error message to pOut.
@@ -1875,13 +1905,13 @@
18751905
blob_to_utf8_no_bom(pB_Blob, 0);
18761906
18771907
/* Prepare the input files */
18781908
memset(&c, 0, sizeof(c));
18791909
if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){
1880
- c.same_fn = same_dline_ignore_allws;
1910
+ c.xDiffer = same_dline_ignore_allws;
18811911
}else{
1882
- c.same_fn = same_dline;
1912
+ c.xDiffer = same_dline;
18831913
}
18841914
c.aFrom = break_into_lines(blob_str(pA_Blob), blob_size(pA_Blob),
18851915
&c.nFrom, diffFlags);
18861916
c.aTo = break_into_lines(blob_str(pB_Blob), blob_size(pB_Blob),
18871917
&c.nTo, diffFlags);
@@ -2107,13 +2137,13 @@
21072137
static int annotation_start(Annotator *p, Blob *pInput, u64 diffFlags){
21082138
int i;
21092139
21102140
memset(p, 0, sizeof(*p));
21112141
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;
21132143
}else{
2114
- p->c.same_fn = same_dline;
2144
+ p->c.xDiffer = same_dline;
21152145
}
21162146
p->c.aTo = break_into_lines(blob_str(pInput), blob_size(pInput),&p->c.nTo,
21172147
diffFlags);
21182148
if( p->c.aTo==0 ){
21192149
return 1;
21202150
--- 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 @@
427427
**
428428
** Show all help text on a single page. Useful for proof-reading.
429429
*/
430430
void test_all_help_page(void){
431431
int i;
432
+ Blob buf;
433
+ blob_init(&buf,0,0);
432434
style_header("All Help Text");
435
+ @ <dl>
433436
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
+ }
434465
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>
437468
help_to_html(aCommand[i].zHelp, cgi_output_blob());
438
- @ </blockquote>
469
+ @ </dd>
439470
}
471
+ @ </dl>
472
+ blob_reset(&buf);
440473
style_footer();
441474
}
442475
443476
static void multi_column_list(const char **azWord, int nWord){
444477
int i, j, len;
445478
--- 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 @@
685685
** Transfer content to the output. During the transfer, when text of
686686
** the following form is seen:
687687
**
688688
** href="$ROOT/..."
689689
** action="$ROOT/..."
690
-** href=".../doc/$SELF/..."
690
+** href=".../doc/$CURRENT/..."
691691
**
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
693693
** version number of the /doc/ document currently being displayed (if any).
694694
** Allow ' in place of " and any case for href or action.
695695
**
696696
** Efforts are made to limit this translation to cases where the text is
697697
** fully contained with an HTML markup element.
@@ -712,20 +712,20 @@
712712
blob_append(cgi_output_blob(), &z[base], i-base);
713713
blob_appendf(cgi_output_blob(), "%R");
714714
base = i+5;
715715
}else
716716
if( z[i]=='$'
717
- && strncmp(&z[i-5],"/doc/$SELF/", 11)==0
717
+ && strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0
718718
&& isWithinHref(z,i-5)
719719
&& isWithinHtmlMarkup(z, i-5)
720720
&& strncmp(g.zPath, "doc/",4)==0
721721
){
722722
int j;
723
- for(j=4; g.zPath[j] && g.zPath[j]!='/'; j++){}
723
+ for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){}
724724
blob_append(cgi_output_blob(), &z[base], i-base);
725725
blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4);
726
- base = i+5;
726
+ base = i+8;
727727
}
728728
}
729729
blob_append(cgi_output_blob(), &z[base], i-base);
730730
}
731731
@@ -796,10 +796,11 @@
796796
if( !raw ){
797797
style_footer();
798798
}
799799
#endif
800800
}else{
801
+ fossil_free(style_csp(1));
801802
cgi_set_content_type(zMime);
802803
cgi_set_content(pBody);
803804
}
804805
}
805806
@@ -809,11 +810,11 @@
809810
** WEBPAGE: doc
810811
** URL: /uv/FILE
811812
** URL: /doc/CHECKIN/FILE
812813
**
813814
** 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
815816
** check-in on that branch) or one of various magic words:
816817
**
817818
** "tip" means the most recent check-in
818819
**
819820
** "ckout" means the current check-out, if the server is run from
@@ -823,11 +824,12 @@
823824
** regardless of what branch it occurs on.
824825
**
825826
** FILE is the name of a file to delivered up as a webpage. FILE is relative
826827
** to the root of the source tree of the repository. The FILE must
827828
** 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.
829831
**
830832
** The "ckout" CHECKIN is intended for development - to provide a mechanism
831833
** for looking at what a file will look like using the /doc webpage after
832834
** it gets checked in.
833835
**
@@ -930,22 +932,26 @@
930932
goto doc_not_found;
931933
}
932934
}
933935
if( isUV ){
934936
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;
947953
}
948954
}else if( fossil_strcmp(zCheckin,"ckout")==0 ){
949955
/* Read from the local checkout */
950956
char *zFullpath;
951957
db_must_be_within_tree();
@@ -1122,10 +1128,34 @@
11221128
}
11231129
cgi_set_content_type(zMime);
11241130
cgi_set_content(&bgimg);
11251131
}
11261132
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
+}
11271157
11281158
/*
11291159
** WEBPAGE: docsrch
11301160
**
11311161
** Search for documents that match a user-supplied full-text search pattern.
11321162
--- 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 **