| | @@ -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 | |