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.
Commit
923affb930a27bc7c20c54a0c2ac1904d95b9e13f39ee3652ae83540f413dea9
Parent
5b9a4c90594d8ea…
2 files changed
+62
-18
+10
+62
-18
| --- src/fossil.storage.js | ||
| +++ src/fossil.storage.js | ||
| @@ -17,50 +17,94 @@ | ||
| 17 | 17 | } |
| 18 | 18 | }; |
| 19 | 19 | |
| 20 | 20 | /** Internal storage impl for fossil.storage. */ |
| 21 | 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) | |
| 22 | + tryStorage(window.localStorage) | |
| 23 | + || tryStorage(window.sessionStorage) | |
| 28 | 24 | || tryStorage({ |
| 29 | 25 | // A basic dummy xyzStorage stand-in |
| 30 | - $:{}, | |
| 31 | - setItem: function(k,v){this.$[k]=v}, | |
| 26 | + $$$:{}, | |
| 27 | + setItem: function(k,v){this.$$$[k]=v}, | |
| 32 | 28 | getItem: function(k){ |
| 33 | - return this.$.hasOwnProperty(k) ? this.$[k] : undefined; | |
| 29 | + return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined; | |
| 34 | 30 | }, |
| 35 | - removeItem: function(k){delete this.$[k]}, | |
| 36 | - clear: function(){this.$={}} | |
| 31 | + removeItem: function(k){delete this.$$$[k]}, | |
| 32 | + clear: function(){this.$$$={}} | |
| 37 | 33 | }); |
| 38 | 34 | |
| 39 | 35 | /** |
| 40 | 36 | For the dummy storage we need to differentiate between |
| 41 | 37 | $storage and its real property storage for hasOwnProperty() |
| 42 | 38 | to work properly... |
| 43 | 39 | */ |
| 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 | + ); | |
| 45 | 86 | |
| 46 | 87 | /** |
| 47 | 88 | A proxy for localStorage or sessionStorage or a |
| 48 | 89 | page-instance-local proxy, if neither one is availble. |
| 49 | 90 | |
| 50 | 91 | Which exact storage implementation is uses is unspecified, and |
| 51 | 92 | apps must not rely on it. |
| 52 | 93 | */ |
| 53 | 94 | fossil.storage = { |
| 95 | + storageKeyPrefix: storageKeyPrefix, | |
| 54 | 96 | /** Sets the storage key k to value v, implicitly converting |
| 55 | 97 | it to a string. */ |
| 56 | - set: (k,v)=>$storage.setItem(k,v), | |
| 98 | + set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v), | |
| 57 | 99 | /** 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)), | |
| 59 | 101 | /** Returns the value for the given storage key, or |
| 60 | 102 | 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, | |
| 62 | 106 | /** Returns the JSON.parse()'d value of the given |
| 63 | 107 | storage key's value, or dflt is the key is not |
| 64 | 108 | found or JSON.parse() fails. */ |
| 65 | 109 | getJSON: function f(k,dflt){ |
| 66 | 110 | try { |
| @@ -69,23 +113,23 @@ | ||
| 69 | 113 | } |
| 70 | 114 | catch(e){return dflt} |
| 71 | 115 | }, |
| 72 | 116 | /** Returns true if the storage contains the given key, |
| 73 | 117 | else false. */ |
| 74 | - contains: (k)=>$storageHolder.hasOwnProperty(k), | |
| 118 | + contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), | |
| 75 | 119 | /** Removes the given key from the storage. Returns this. */ |
| 76 | 120 | remove: function(k){ |
| 77 | - $storage.removeItem(k); | |
| 121 | + $storage.removeItem(storageKeyPrefix+k); | |
| 78 | 122 | return this; |
| 79 | 123 | }, |
| 80 | 124 | /** Clears ALL keys from the storage. Returns this. */ |
| 81 | 125 | clear: function(){ |
| 82 | 126 | $storage.clear(); |
| 83 | 127 | return this; |
| 84 | 128 | }, |
| 85 | 129 | /** 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)), | |
| 87 | 131 | /** Returns true if this storage is transient (only available |
| 88 | 132 | until the page is reloaded), indicating that fileStorage |
| 89 | 133 | and sessionStorage are unavailable. */ |
| 90 | 134 | isTransient: ()=>$storageHolder!==$storage, |
| 91 | 135 | /** Returns a symbolic name for the current storage mechanism. */ |
| 92 | 136 |
| --- 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 @@ | ||
| 1436 | 1436 | ** 2) Emits the static fossil.bootstrap.js using builtin_request_js(). |
| 1437 | 1437 | */ |
| 1438 | 1438 | void style_emit_script_fossil_bootstrap(int addScriptTag){ |
| 1439 | 1439 | static int once = 0; |
| 1440 | 1440 | if(0==once++){ |
| 1441 | + char * zName; | |
| 1441 | 1442 | /* Set up the generic/app-agnostic parts of window.fossil |
| 1442 | 1443 | ** which require C-level state... */ |
| 1443 | 1444 | if(addScriptTag!=0){ |
| 1444 | 1445 | style_emit_script_tag(0,0); |
| 1445 | 1446 | } |
| @@ -1456,10 +1457,19 @@ | ||
| 1456 | 1457 | ** including a trailing slash. */ |
| 1457 | 1458 | "window.fossil.rootPath = %!j+'/';\n", |
| 1458 | 1459 | get_version(), g.zTop); |
| 1459 | 1460 | /* fossil.config = {...various config-level options...} */ |
| 1460 | 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); | |
| 1461 | 1471 | CX("/* Length of UUID hashes for display purposes. */"); |
| 1462 | 1472 | CX("hashDigits: %d, hashDigitsUrl: %d,\n", |
| 1463 | 1473 | hash_digits(0), hash_digits(1)); |
| 1464 | 1474 | CX("editStateMarkers: {" |
| 1465 | 1475 | "/*Symbolic markers to denote certain edit states.*/" |
| 1466 | 1476 |
| --- 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 |