Fossil SCM

Re-enabled localStorage for fossil.storage but enhanced it to sandbox the keys used by the apps on a per-repo basis, so there is no longer any (immediately visible) cross-repo polution. The underlying localStorage/sessionStorage is still shared per origin/browser profile instance, but fossil.storage clients will only see the state from their own repo.

stephan 2020-08-18 20:46 trunk
Commit 923affb930a27bc7c20c54a0c2ac1904d95b9e13f39ee3652ae83540f413dea9
2 files changed +62 -18 +10
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -17,50 +17,94 @@
1717
}
1818
};
1919
2020
/** Internal storage impl for fossil.storage. */
2121
const $storage =
22
- /* We must not use localStorage on a multi-repo domain!
23
- See: https://fossil-scm.org/forum/forumpost/0e794dbb91
24
-
25
- tryStorage(window.localStorage)
26
- ||*/
27
- tryStorage(window.sessionStorage)
22
+ tryStorage(window.localStorage)
23
+ || tryStorage(window.sessionStorage)
2824
|| tryStorage({
2925
// A basic dummy xyzStorage stand-in
30
- $:{},
31
- setItem: function(k,v){this.$[k]=v},
26
+ $$$:{},
27
+ setItem: function(k,v){this.$$$[k]=v},
3228
getItem: function(k){
33
- return this.$.hasOwnProperty(k) ? this.$[k] : undefined;
29
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
3430
},
35
- removeItem: function(k){delete this.$[k]},
36
- clear: function(){this.$={}}
31
+ removeItem: function(k){delete this.$$$[k]},
32
+ clear: function(){this.$$$={}}
3733
});
3834
3935
/**
4036
For the dummy storage we need to differentiate between
4137
$storage and its real property storage for hasOwnProperty()
4238
to work properly...
4339
*/
44
- const $storageHolder = $storage.hasOwnProperty('$') ? $storage.$ : $storage;
40
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
41
+
42
+ /**
43
+ A prefix which gets internally applied to all fossil.storage
44
+ property keys so that localStorage and sessionStorage across the
45
+ same browser profile instance do not "leak" across multiple repos
46
+ being hosted by the same origin server. Such polination is still
47
+ there but, with this key prefix applied, it won't be immediately
48
+ visible via the storage API.
49
+
50
+ With this in place we can justify using localStorage instead of
51
+ sessionStorage again.
52
+
53
+ One implication, it was discovered after the release of 2.12, of
54
+ using localStorage and sessionStorage, is that their scope (the
55
+ same "origin" and client application/profile) allows multiple
56
+ repos on the same origin to use the same storage. Thus a user
57
+ editing a wiki in /repoA/wikiedit could then see those edits in
58
+ /repoB/wikiedit. The data do not cross user- or browser
59
+ boundaries, though, so it "might" arguably be called a bug. Even
60
+ so, it was never intended for that to happen. Rather than lose
61
+ localStorage access altogether, storageKeyPrefix was added so
62
+ that we can sandbox that state for the various repos.
63
+
64
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
65
+
66
+ Sidebar: it might seem odd to provide a key prefix and stick all
67
+ properties in the topmost level of the storage object. We do that
68
+ because adding a layer of object to sandbox each repo would mean
69
+ (de)serializing that whole tree on every storage property change
70
+ (and we update storage often during editing
71
+ sessions). e.g. instead of storageObject.projectName.foo we have
72
+ storageObject[storageKeyPrefix+'foo']. That's soley for
73
+ efficiency's sake (in terms of battery life and
74
+ environment-internal storage-level effort). Even so, it might
75
+ (or might not) be useful to do that someday.
76
+ */
77
+ const storageKeyPrefix = (
78
+ $storageHolder===$storage/*localStorage or sessionStorage*/
79
+ ? (
80
+ F.config.projectCode || F.config.projectName
81
+ || F.config.shortProjectName || window.location.pathname
82
+ )+'::' : (
83
+ '' /* transient storage */
84
+ )
85
+ );
4586
4687
/**
4788
A proxy for localStorage or sessionStorage or a
4889
page-instance-local proxy, if neither one is availble.
4990
5091
Which exact storage implementation is uses is unspecified, and
5192
apps must not rely on it.
5293
*/
5394
fossil.storage = {
95
+ storageKeyPrefix: storageKeyPrefix,
5496
/** Sets the storage key k to value v, implicitly converting
5597
it to a string. */
56
- set: (k,v)=>$storage.setItem(k,v),
98
+ set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
5799
/** Sets storage key k to JSON.stringify(v). */
58
- setJSON: (k,v)=>$storage.setItem(k,JSON.stringify(v)),
100
+ setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
59101
/** Returns the value for the given storage key, or
60102
dflt if the key is not found in the storage. */
61
- get: (k,dflt)=>$storageHolder.hasOwnProperty(k) ? $storage.getItem(k) : dflt,
103
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
104
+ storageKeyPrefix+k
105
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
62106
/** Returns the JSON.parse()'d value of the given
63107
storage key's value, or dflt is the key is not
64108
found or JSON.parse() fails. */
65109
getJSON: function f(k,dflt){
66110
try {
@@ -69,23 +113,23 @@
69113
}
70114
catch(e){return dflt}
71115
},
72116
/** Returns true if the storage contains the given key,
73117
else false. */
74
- contains: (k)=>$storageHolder.hasOwnProperty(k),
118
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
75119
/** Removes the given key from the storage. Returns this. */
76120
remove: function(k){
77
- $storage.removeItem(k);
121
+ $storage.removeItem(storageKeyPrefix+k);
78122
return this;
79123
},
80124
/** Clears ALL keys from the storage. Returns this. */
81125
clear: function(){
82126
$storage.clear();
83127
return this;
84128
},
85129
/** Returns an array of all keys currently in the storage. */
86
- keys: ()=>Object.keys($storageHolder),
130
+ keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
87131
/** Returns true if this storage is transient (only available
88132
until the page is reloaded), indicating that fileStorage
89133
and sessionStorage are unavailable. */
90134
isTransient: ()=>$storageHolder!==$storage,
91135
/** Returns a symbolic name for the current storage mechanism. */
92136
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -17,50 +17,94 @@
17 }
18 };
19
20 /** Internal storage impl for fossil.storage. */
21 const $storage =
22 /* We must not use localStorage on a multi-repo domain!
23 See: https://fossil-scm.org/forum/forumpost/0e794dbb91
24
25 tryStorage(window.localStorage)
26 ||*/
27 tryStorage(window.sessionStorage)
28 || tryStorage({
29 // A basic dummy xyzStorage stand-in
30 $:{},
31 setItem: function(k,v){this.$[k]=v},
32 getItem: function(k){
33 return this.$.hasOwnProperty(k) ? this.$[k] : undefined;
34 },
35 removeItem: function(k){delete this.$[k]},
36 clear: function(){this.$={}}
37 });
38
39 /**
40 For the dummy storage we need to differentiate between
41 $storage and its real property storage for hasOwnProperty()
42 to work properly...
43 */
44 const $storageHolder = $storage.hasOwnProperty('$') ? $storage.$ : $storage;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
46 /**
47 A proxy for localStorage or sessionStorage or a
48 page-instance-local proxy, if neither one is availble.
49
50 Which exact storage implementation is uses is unspecified, and
51 apps must not rely on it.
52 */
53 fossil.storage = {
 
54 /** Sets the storage key k to value v, implicitly converting
55 it to a string. */
56 set: (k,v)=>$storage.setItem(k,v),
57 /** Sets storage key k to JSON.stringify(v). */
58 setJSON: (k,v)=>$storage.setItem(k,JSON.stringify(v)),
59 /** Returns the value for the given storage key, or
60 dflt if the key is not found in the storage. */
61 get: (k,dflt)=>$storageHolder.hasOwnProperty(k) ? $storage.getItem(k) : dflt,
 
 
62 /** Returns the JSON.parse()'d value of the given
63 storage key's value, or dflt is the key is not
64 found or JSON.parse() fails. */
65 getJSON: function f(k,dflt){
66 try {
@@ -69,23 +113,23 @@
69 }
70 catch(e){return dflt}
71 },
72 /** Returns true if the storage contains the given key,
73 else false. */
74 contains: (k)=>$storageHolder.hasOwnProperty(k),
75 /** Removes the given key from the storage. Returns this. */
76 remove: function(k){
77 $storage.removeItem(k);
78 return this;
79 },
80 /** Clears ALL keys from the storage. Returns this. */
81 clear: function(){
82 $storage.clear();
83 return this;
84 },
85 /** Returns an array of all keys currently in the storage. */
86 keys: ()=>Object.keys($storageHolder),
87 /** Returns true if this storage is transient (only available
88 until the page is reloaded), indicating that fileStorage
89 and sessionStorage are unavailable. */
90 isTransient: ()=>$storageHolder!==$storage,
91 /** Returns a symbolic name for the current storage mechanism. */
92
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -17,50 +17,94 @@
17 }
18 };
19
20 /** Internal storage impl for fossil.storage. */
21 const $storage =
22 tryStorage(window.localStorage)
23 || tryStorage(window.sessionStorage)
 
 
 
 
24 || tryStorage({
25 // A basic dummy xyzStorage stand-in
26 $$$:{},
27 setItem: function(k,v){this.$$$[k]=v},
28 getItem: function(k){
29 return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
30 },
31 removeItem: function(k){delete this.$$$[k]},
32 clear: function(){this.$$$={}}
33 });
34
35 /**
36 For the dummy storage we need to differentiate between
37 $storage and its real property storage for hasOwnProperty()
38 to work properly...
39 */
40 const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
41
42 /**
43 A prefix which gets internally applied to all fossil.storage
44 property keys so that localStorage and sessionStorage across the
45 same browser profile instance do not "leak" across multiple repos
46 being hosted by the same origin server. Such polination is still
47 there but, with this key prefix applied, it won't be immediately
48 visible via the storage API.
49
50 With this in place we can justify using localStorage instead of
51 sessionStorage again.
52
53 One implication, it was discovered after the release of 2.12, of
54 using localStorage and sessionStorage, is that their scope (the
55 same "origin" and client application/profile) allows multiple
56 repos on the same origin to use the same storage. Thus a user
57 editing a wiki in /repoA/wikiedit could then see those edits in
58 /repoB/wikiedit. The data do not cross user- or browser
59 boundaries, though, so it "might" arguably be called a bug. Even
60 so, it was never intended for that to happen. Rather than lose
61 localStorage access altogether, storageKeyPrefix was added so
62 that we can sandbox that state for the various repos.
63
64 See: https://fossil-scm.org/forum/forumpost/4afc4d34de
65
66 Sidebar: it might seem odd to provide a key prefix and stick all
67 properties in the topmost level of the storage object. We do that
68 because adding a layer of object to sandbox each repo would mean
69 (de)serializing that whole tree on every storage property change
70 (and we update storage often during editing
71 sessions). e.g. instead of storageObject.projectName.foo we have
72 storageObject[storageKeyPrefix+'foo']. That's soley for
73 efficiency's sake (in terms of battery life and
74 environment-internal storage-level effort). Even so, it might
75 (or might not) be useful to do that someday.
76 */
77 const storageKeyPrefix = (
78 $storageHolder===$storage/*localStorage or sessionStorage*/
79 ? (
80 F.config.projectCode || F.config.projectName
81 || F.config.shortProjectName || window.location.pathname
82 )+'::' : (
83 '' /* transient storage */
84 )
85 );
86
87 /**
88 A proxy for localStorage or sessionStorage or a
89 page-instance-local proxy, if neither one is availble.
90
91 Which exact storage implementation is uses is unspecified, and
92 apps must not rely on it.
93 */
94 fossil.storage = {
95 storageKeyPrefix: storageKeyPrefix,
96 /** Sets the storage key k to value v, implicitly converting
97 it to a string. */
98 set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
99 /** Sets storage key k to JSON.stringify(v). */
100 setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
101 /** Returns the value for the given storage key, or
102 dflt if the key is not found in the storage. */
103 get: (k,dflt)=>$storageHolder.hasOwnProperty(
104 storageKeyPrefix+k
105 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
106 /** Returns the JSON.parse()'d value of the given
107 storage key's value, or dflt is the key is not
108 found or JSON.parse() fails. */
109 getJSON: function f(k,dflt){
110 try {
@@ -69,23 +113,23 @@
113 }
114 catch(e){return dflt}
115 },
116 /** Returns true if the storage contains the given key,
117 else false. */
118 contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
119 /** Removes the given key from the storage. Returns this. */
120 remove: function(k){
121 $storage.removeItem(storageKeyPrefix+k);
122 return this;
123 },
124 /** Clears ALL keys from the storage. Returns this. */
125 clear: function(){
126 $storage.clear();
127 return this;
128 },
129 /** Returns an array of all keys currently in the storage. */
130 keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
131 /** Returns true if this storage is transient (only available
132 until the page is reloaded), indicating that fileStorage
133 and sessionStorage are unavailable. */
134 isTransient: ()=>$storageHolder!==$storage,
135 /** Returns a symbolic name for the current storage mechanism. */
136
+10
--- src/style.c
+++ src/style.c
@@ -1436,10 +1436,11 @@
14361436
** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
14371437
*/
14381438
void style_emit_script_fossil_bootstrap(int addScriptTag){
14391439
static int once = 0;
14401440
if(0==once++){
1441
+ char * zName;
14411442
/* Set up the generic/app-agnostic parts of window.fossil
14421443
** which require C-level state... */
14431444
if(addScriptTag!=0){
14441445
style_emit_script_tag(0,0);
14451446
}
@@ -1456,10 +1457,19 @@
14561457
** including a trailing slash. */
14571458
"window.fossil.rootPath = %!j+'/';\n",
14581459
get_version(), g.zTop);
14591460
/* fossil.config = {...various config-level options...} */
14601461
CX("window.fossil.config = {");
1462
+ zName = db_get("project-name", "");
1463
+ CX("projectName: %!j,\n", zName);
1464
+ fossil_free(zName);
1465
+ zName = db_get("short-project-name", "");
1466
+ CX("shortProjectName: %!j,\n", zName);
1467
+ fossil_free(zName);
1468
+ zName = db_get("project-code", "");
1469
+ CX("projectCode: %!j,\n", zName);
1470
+ fossil_free(zName);
14611471
CX("/* Length of UUID hashes for display purposes. */");
14621472
CX("hashDigits: %d, hashDigitsUrl: %d,\n",
14631473
hash_digits(0), hash_digits(1));
14641474
CX("editStateMarkers: {"
14651475
"/*Symbolic markers to denote certain edit states.*/"
14661476
--- src/style.c
+++ src/style.c
@@ -1436,10 +1436,11 @@
1436 ** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
1437 */
1438 void style_emit_script_fossil_bootstrap(int addScriptTag){
1439 static int once = 0;
1440 if(0==once++){
 
1441 /* Set up the generic/app-agnostic parts of window.fossil
1442 ** which require C-level state... */
1443 if(addScriptTag!=0){
1444 style_emit_script_tag(0,0);
1445 }
@@ -1456,10 +1457,19 @@
1456 ** including a trailing slash. */
1457 "window.fossil.rootPath = %!j+'/';\n",
1458 get_version(), g.zTop);
1459 /* fossil.config = {...various config-level options...} */
1460 CX("window.fossil.config = {");
 
 
 
 
 
 
 
 
 
1461 CX("/* Length of UUID hashes for display purposes. */");
1462 CX("hashDigits: %d, hashDigitsUrl: %d,\n",
1463 hash_digits(0), hash_digits(1));
1464 CX("editStateMarkers: {"
1465 "/*Symbolic markers to denote certain edit states.*/"
1466
--- src/style.c
+++ src/style.c
@@ -1436,10 +1436,11 @@
1436 ** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
1437 */
1438 void style_emit_script_fossil_bootstrap(int addScriptTag){
1439 static int once = 0;
1440 if(0==once++){
1441 char * zName;
1442 /* Set up the generic/app-agnostic parts of window.fossil
1443 ** which require C-level state... */
1444 if(addScriptTag!=0){
1445 style_emit_script_tag(0,0);
1446 }
@@ -1456,10 +1457,19 @@
1457 ** including a trailing slash. */
1458 "window.fossil.rootPath = %!j+'/';\n",
1459 get_version(), g.zTop);
1460 /* fossil.config = {...various config-level options...} */
1461 CX("window.fossil.config = {");
1462 zName = db_get("project-name", "");
1463 CX("projectName: %!j,\n", zName);
1464 fossil_free(zName);
1465 zName = db_get("short-project-name", "");
1466 CX("shortProjectName: %!j,\n", zName);
1467 fossil_free(zName);
1468 zName = db_get("project-code", "");
1469 CX("projectCode: %!j,\n", zName);
1470 fossil_free(zName);
1471 CX("/* Length of UUID hashes for display purposes. */");
1472 CX("hashDigits: %d, hashDigitsUrl: %d,\n",
1473 hash_digits(0), hash_digits(1));
1474 CX("editStateMarkers: {"
1475 "/*Symbolic markers to denote certain edit states.*/"
1476

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button