Fossil

Check-in [923affb930]
Login

Check-in [923affb930]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment: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.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 923affb930a27bc7c20c54a0c2ac1904d95b9e13f39ee3652ae83540f413dea9
User & Date: stephan 2020-08-18 20:46:36.631
References
2020-08-18
20:51
Merged in [923affb930a27b], which reinstates localStorage but sandboxes access to fossil.storage on a per-repo basis. ... (check-in: 21fbd4738c user: stephan tags: branch-2.12)
Context
2020-08-18
21:01
fossil.storage.clear() is now also sandboxed - no longer nuking all state for all repos on the same origin. ... (check-in: d2d8894bb2 user: stephan tags: trunk)
20:51
Merged in [923affb930a27b], which reinstates localStorage but sandboxes access to fossil.storage on a per-repo basis. ... (check-in: 21fbd4738c user: stephan tags: branch-2.12)
20:46
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. ... (check-in: 923affb930 user: stephan tags: trunk)
18:19
Disabled localStorage as a backend option for the fossil.storage JS API after it was painfully discovered that multiple repos on the same hoster actually share that storage, as opposed to it being achored at the repo. That API now uses sessionStorage, if available, before falling back to transient instance-local storage. ... (check-in: 5b9a4c9059 user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/fossil.storage.js.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44













































45
46
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
    }catch(e){
      return undefined;
    }
  };

  /** Internal storage impl for fossil.storage. */
  const $storage =
        /* We must not use localStorage on a multi-repo domain!
          See: https://fossil-scm.org/forum/forumpost/0e794dbb91

          tryStorage(window.localStorage)
          ||*/
        tryStorage(window.sessionStorage)
        || tryStorage({
          // A basic dummy xyzStorage stand-in
          $:{},
          setItem: function(k,v){this.$[k]=v},
          getItem: function(k){
            return this.$.hasOwnProperty(k) ? this.$[k] : undefined;
          },
          removeItem: function(k){delete this.$[k]},
          clear: function(){this.$={}}
        });

  /**
     For the dummy storage we need to differentiate between
     $storage and its real property storage for hasOwnProperty()
     to work properly...
  */
  const $storageHolder = $storage.hasOwnProperty('$') ? $storage.$ : $storage;














































  /**
     A proxy for localStorage or sessionStorage or a
     page-instance-local proxy, if neither one is availble.

     Which exact storage implementation is uses is unspecified, and
     apps must not rely on it.
  */
  fossil.storage = {

    /** Sets the storage key k to value v, implicitly converting
        it to a string. */
    set: (k,v)=>$storage.setItem(k,v),
    /** Sets storage key k to JSON.stringify(v). */
    setJSON: (k,v)=>$storage.setItem(k,JSON.stringify(v)),
    /** Returns the value for the given storage key, or
        dflt if the key is not found in the storage. */
    get: (k,dflt)=>$storageHolder.hasOwnProperty(k) ? $storage.getItem(k) : dflt,


    /** Returns the JSON.parse()'d value of the given
        storage key's value, or dflt is the key is not
        found or JSON.parse() fails. */
    getJSON: function f(k,dflt){
      try {
        const x = this.get(k,f);
        return x===f ? dflt : JSON.parse(x);
      }
      catch(e){return dflt}
    },
    /** Returns true if the storage contains the given key,
        else false. */
    contains: (k)=>$storageHolder.hasOwnProperty(k),
    /** Removes the given key from the storage. Returns this. */
    remove: function(k){
      $storage.removeItem(k);
      return this;
    },
    /** Clears ALL keys from the storage. Returns this. */
    clear: function(){
      $storage.clear();
      return this;
    },
    /** Returns an array of all keys currently in the storage. */
    keys: ()=>Object.keys($storageHolder),
    /** Returns true if this storage is transient (only available
        until the page is reloaded), indicating that fileStorage
        and sessionStorage are unavailable. */
    isTransient: ()=>$storageHolder!==$storage,
    /** Returns a symbolic name for the current storage mechanism. */
    storageImplName: function(){
      if($storage===window.localStorage) return 'localStorage';







<
<
<
|
<
|


|
|

|

|
|







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>









>


|

|


|
>
>












|


|








|







15
16
17
18
19
20
21



22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    }catch(e){
      return undefined;
    }
  };

  /** Internal storage impl for fossil.storage. */
  const $storage =



        tryStorage(window.localStorage)

        || tryStorage(window.sessionStorage)
        || tryStorage({
          // A basic dummy xyzStorage stand-in
          $$$:{},
          setItem: function(k,v){this.$$$[k]=v},
          getItem: function(k){
            return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
          },
          removeItem: function(k){delete this.$$$[k]},
          clear: function(){this.$$$={}}
        });

  /**
     For the dummy storage we need to differentiate between
     $storage and its real property storage for hasOwnProperty()
     to work properly...
  */
  const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;

  /**
     A prefix which gets internally applied to all fossil.storage
     property keys so that localStorage and sessionStorage across the
     same browser profile instance do not "leak" across multiple repos
     being hosted by the same origin server. Such polination is still
     there but, with this key prefix applied, it won't be immediately
     visible via the storage API.

     With this in place we can justify using localStorage instead of
     sessionStorage again.

     One implication, it was discovered after the release of 2.12, of
     using localStorage and sessionStorage, is that their scope (the
     same "origin" and client application/profile) allows multiple
     repos on the same origin to use the same storage. Thus a user
     editing a wiki in /repoA/wikiedit could then see those edits in
     /repoB/wikiedit. The data do not cross user- or browser
     boundaries, though, so it "might" arguably be called a bug. Even
     so, it was never intended for that to happen. Rather than lose
     localStorage access altogether, storageKeyPrefix was added so
     that we can sandbox that state for the various repos.

     See: https://fossil-scm.org/forum/forumpost/4afc4d34de

     Sidebar: it might seem odd to provide a key prefix and stick all
     properties in the topmost level of the storage object. We do that
     because adding a layer of object to sandbox each repo would mean
     (de)serializing that whole tree on every storage property change
     (and we update storage often during editing
     sessions). e.g. instead of storageObject.projectName.foo we have
     storageObject[storageKeyPrefix+'foo']. That's soley for
     efficiency's sake (in terms of battery life and
     environment-internal storage-level effort). Even so, it might
     (or might not) be useful to do that someday.
  */
  const storageKeyPrefix = (
    $storageHolder===$storage/*localStorage or sessionStorage*/
      ? (
        F.config.projectCode || F.config.projectName
          || F.config.shortProjectName || window.location.pathname
      )+'::' : (
        '' /* transient storage */
      )
  );

  /**
     A proxy for localStorage or sessionStorage or a
     page-instance-local proxy, if neither one is availble.

     Which exact storage implementation is uses is unspecified, and
     apps must not rely on it.
  */
  fossil.storage = {
    storageKeyPrefix: storageKeyPrefix,
    /** Sets the storage key k to value v, implicitly converting
        it to a string. */
    set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
    /** Sets storage key k to JSON.stringify(v). */
    setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
    /** Returns the value for the given storage key, or
        dflt if the key is not found in the storage. */
    get: (k,dflt)=>$storageHolder.hasOwnProperty(
      storageKeyPrefix+k
    ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
    /** Returns the JSON.parse()'d value of the given
        storage key's value, or dflt is the key is not
        found or JSON.parse() fails. */
    getJSON: function f(k,dflt){
      try {
        const x = this.get(k,f);
        return x===f ? dflt : JSON.parse(x);
      }
      catch(e){return dflt}
    },
    /** Returns true if the storage contains the given key,
        else false. */
    contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
    /** Removes the given key from the storage. Returns this. */
    remove: function(k){
      $storage.removeItem(storageKeyPrefix+k);
      return this;
    },
    /** Clears ALL keys from the storage. Returns this. */
    clear: function(){
      $storage.clear();
      return this;
    },
    /** Returns an array of all keys currently in the storage. */
    keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
    /** Returns true if this storage is transient (only available
        until the page is reloaded), indicating that fileStorage
        and sessionStorage are unavailable. */
    isTransient: ()=>$storageHolder!==$storage,
    /** Returns a symbolic name for the current storage mechanism. */
    storageImplName: function(){
      if($storage===window.localStorage) return 'localStorage';
Changes to src/style.c.
1434
1435
1436
1437
1438
1439
1440

1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460









1461
1462
1463
1464
1465
1466
1467
** it is assumed that the caller already opened a tag.
**
** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
*/
void style_emit_script_fossil_bootstrap(int addScriptTag){
  static int once = 0;
  if(0==once++){

    /* Set up the generic/app-agnostic parts of window.fossil
    ** which require C-level state... */
    if(addScriptTag!=0){
      style_emit_script_tag(0,0);
    }
    CX("(function(){\n");
    CX(/*MSIE NodeList.forEach polyfill, courtesy of Mozilla:
    https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
       */
       "if(window.NodeList && !NodeList.prototype.forEach){"
       "NodeList.prototype.forEach = Array.prototype.forEach;"
       "}\n");
    CX("if(!window.fossil) window.fossil={};\n"
       "window.fossil.version = %!j;\n"
    /* fossil.rootPath is the top-most CGI/server path,
    ** including a trailing slash. */
       "window.fossil.rootPath = %!j+'/';\n",
       get_version(), g.zTop);
    /* fossil.config = {...various config-level options...} */
    CX("window.fossil.config = {");









    CX("/* Length of UUID hashes for display purposes. */");
    CX("hashDigits: %d, hashDigitsUrl: %d,\n",
       hash_digits(0), hash_digits(1));
    CX("editStateMarkers: {"
       "/*Symbolic markers to denote certain edit states.*/"
       "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
    CX("confirmerButtonTicks: 3 "







>




















>
>
>
>
>
>
>
>
>







1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
** it is assumed that the caller already opened a tag.
**
** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
*/
void style_emit_script_fossil_bootstrap(int addScriptTag){
  static int once = 0;
  if(0==once++){
    char * zName;
    /* Set up the generic/app-agnostic parts of window.fossil
    ** which require C-level state... */
    if(addScriptTag!=0){
      style_emit_script_tag(0,0);
    }
    CX("(function(){\n");
    CX(/*MSIE NodeList.forEach polyfill, courtesy of Mozilla:
    https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
       */
       "if(window.NodeList && !NodeList.prototype.forEach){"
       "NodeList.prototype.forEach = Array.prototype.forEach;"
       "}\n");
    CX("if(!window.fossil) window.fossil={};\n"
       "window.fossil.version = %!j;\n"
    /* fossil.rootPath is the top-most CGI/server path,
    ** including a trailing slash. */
       "window.fossil.rootPath = %!j+'/';\n",
       get_version(), g.zTop);
    /* fossil.config = {...various config-level options...} */
    CX("window.fossil.config = {");
    zName = db_get("project-name", "");
    CX("projectName: %!j,\n", zName);
    fossil_free(zName);
    zName = db_get("short-project-name", "");
    CX("shortProjectName: %!j,\n", zName);
    fossil_free(zName);
    zName = db_get("project-code", "");
    CX("projectCode: %!j,\n", zName);
    fossil_free(zName);
    CX("/* Length of UUID hashes for display purposes. */");
    CX("hashDigits: %d, hashDigitsUrl: %d,\n",
       hash_digits(0), hash_digits(1));
    CX("editStateMarkers: {"
       "/*Symbolic markers to denote certain edit states.*/"
       "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
    CX("confirmerButtonTicks: 3 "