├── .gitignore ├── .travis.yml ├── .zuul.yml ├── LICENSE ├── README.md ├── bower.json ├── dist ├── client.js ├── client.min.js ├── hub.js └── hub.min.js ├── example ├── README.md ├── client1.html ├── client2.html └── hub.html ├── gulpfile.js ├── lib ├── client.js ├── hub.js └── index.js ├── media └── logo.png ├── package.json └── test ├── es6-promise.auto.min.js ├── getOnlyHub.html ├── hub.html ├── invalidOriginHub.html ├── server.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | coverage 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | node_modules 17 | 18 | .grunt 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2' 4 | script: 5 | - npm run saucetest 6 | env: 7 | global: 8 | - secure: bL3VTCycgkXYNtnehowvX3hIgOreNH4itXyw096kDtsu7CMnk5ubEzbnih76EOtDECc0k+HOAAikYEvGSxK+Am9EsFGEKPGs5r/KmLeH0oPEWc7bCkQFvrMr9WLAyuSv/zjBUo7ReYJ1h1TcMProwLWDEdHFFdrnxsuLYn2NAiw= 9 | - secure: WmrXIrRG9dq21tNHuER3pMPosv4KgoNhaLQHvol7kSLeWz3bWNR2z2oM6PYtj5ODN1MaoKyYknB9Z2XbpFB2rsgM6I0ECvGnIShVBGXn9u/O5zFt0xQItIJaYZZDauSCe+xSTIhcYXutaJQK1qPBiFg1U92aSNtpsVRNYkvs230= 10 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | server: "./test/server.js" 3 | concurrency: 1 4 | scripts: 5 | - "test/es6-promise.auto.min.js" 6 | - "lib/client.js" 7 | browsers: 8 | - name: chrome 9 | version: 34 10 | platform: Linux 11 | - name: chrome 12 | version: latest 13 | platform: Linux 14 | - name: firefox 15 | version: 29 16 | platform: Linux 17 | - name: firefox 18 | version: latest 19 | platform: Linux 20 | - name: safari 21 | version: 7 22 | - name: ie 23 | version: 8..latest 24 | - name: android 25 | version: latest 26 | - name: iphone 27 | version: 8.0 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cross-storage](https://github.com/zendesk/cross-storage/raw/master/media/logo.png) 2 | 3 | Cross domain local storage, with permissions. Enables multiple browser 4 | windows/tabs, across a variety of domains, to share a single localStorage. 5 | Features an API using ES6 promises. 6 | 7 | [![Build Status](https://travis-ci.org/zendesk/cross-storage.svg?branch=master)](https://travis-ci.org/zendesk/cross-storage) 8 | 9 | * [Overview](#overview) 10 | * [Installation](#installation) 11 | * [API](#api) 12 | * [CrossStorageHub.init(permissions)](#crossstoragehubinitpermissions) 13 | * [new CrossStorageClient(url, \[opts\])](#new-crossstorageclienturl-opts) 14 | * [CrossStorageClient.prototype.onConnect()](#crossstorageclientprototypeonconnect) 15 | * [CrossStorageClient.prototype.set(key, value)](#crossstorageclientprototypesetkey-value) 16 | * [CrossStorageClient.prototype.get(key1, \[key2\], \[...\])](#crossstorageclientprototypegetkey1-key2-) 17 | * [CrossStorageClient.prototype.del(key1, \[key2\], \[...\])](#crossstorageclientprototypedelkey1-key2-) 18 | * [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys) 19 | * [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear) 20 | * [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose) 21 | * [Compatibility](#compatibility) 22 | * [Compression](#compression) 23 | * [Building](#building) 24 | * [Tests](#tests) 25 | * [Copyright and license](#copyright-and-license) 26 | 27 | ## Overview 28 | 29 | The library is a convenient alternative to sharing a root domain cookie. 30 | Unlike cookies, your client-side data isn't limited to a few kilobytes - you 31 | get up to 2.49M chars. For a client-heavy application, you can potentially 32 | shave a few KB off your request headers by avoiding cookies. This is all thanks 33 | to LocalStorage, which is available in IE 8+, FF 3.5+, Chrome 4+, as well as a 34 | majority of mobile browsers. For a list of compatible browsers, refer to 35 | [caniuse](http://caniuse.com/#feat=namevalue-storage). 36 | 37 | How does it work? The library is divided into two types of components: hubs 38 | and clients. The hubs reside on a host of choice and interact directly with 39 | the LocalStorage API. The clients then load said hub over an embedded iframe 40 | and post messages, requesting data to be stored, retrieved, and deleted. This 41 | allows multiple clients to access and share the data located in a single store. 42 | 43 | Care should be made to limit the origins of the bidirectional communication. 44 | As such, when initializing the hub, an array of permissions objects is passed. 45 | Any messages from clients whose origin does not match the pattern are ignored, 46 | as well as those not within the allowed set of methods. The set of permissions 47 | are enforced thanks to the same-origin policy. However, keep in mind that any 48 | user has full control of their local storage data - it's still client data. 49 | This only restricts access on a per-domain or web app level. 50 | 51 | **Hub** 52 | 53 | ``` javascript 54 | // Config s.t. subdomains can get, but only the root domain can set and del 55 | CrossStorageHub.init([ 56 | {origin: /\.example.com$/, allow: ['get']}, 57 | {origin: /:\/\/(www\.)?example.com$/, allow: ['get', 'set', 'del']} 58 | ]); 59 | ``` 60 | 61 | Note the $ for matching the end of the string. The RegExps in the above example 62 | will match origins such as valid.example.com, but not 63 | invalid.example.com.malicious.com. 64 | 65 | **Client** 66 | 67 | ``` javascript 68 | var storage = new CrossStorageClient('https://store.example.com/hub.html'); 69 | 70 | storage.onConnect().then(function() { 71 | return storage.set('newKey', 'foobar'); 72 | }).then(function() { 73 | return storage.get('existingKey', 'newKey'); 74 | }).then(function(res) { 75 | console.log(res.length); // 2 76 | }).catch(function(err) { 77 | // Handle error 78 | }); 79 | ``` 80 | 81 | ## Installation 82 | 83 | The library can be installed via bower: 84 | 85 | ``` bash 86 | bower install cross-storage 87 | ``` 88 | 89 | Or using npm: 90 | 91 | ``` bash 92 | npm install cross-storage 93 | ``` 94 | 95 | along with browserify: 96 | 97 | ``` javascript 98 | var CrossStorageClient = require('cross-storage').CrossStorageClient; 99 | var CrossStorageHub = require('cross-storage').CrossStorageHub; 100 | ``` 101 | 102 | When serving the hub, you may want to set the CORS and CSP headers for your 103 | server depending on client/hub location. For example: 104 | 105 | ``` javascript 106 | { 107 | 'Access-Control-Allow-Origin': '*', 108 | 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 109 | 'Access-Control-Allow-Headers': 'X-Requested-With', 110 | 'Content-Security-Policy': "default-src 'unsafe-inline' *", 111 | 'X-Content-Security-Policy': "default-src 'unsafe-inline' *", 112 | 'X-WebKit-CSP': "default-src 'unsafe-inline' *", 113 | } 114 | ``` 115 | 116 | If using inline JS to create the hub, you'll need to specify `unsafe-inline` 117 | for the CSP headers. Otherwise, it can be left out if simply including the 118 | init code via another resource. 119 | 120 | ## API 121 | 122 | #### CrossStorageHub.init(permissions) 123 | 124 | Accepts an array of objects with two keys: origin and allow. The value 125 | of origin is expected to be a RegExp, and allow, an array of strings. 126 | The cross storage hub is then initialized to accept requests from any of 127 | the matching origins, allowing access to the associated lists of methods. 128 | Methods may include any of: get, set, del, getKeys and clear. A 'ready' 129 | message is sent to the parent window once complete. 130 | 131 | ``` javascript 132 | CrossStorageHub.init([ 133 | {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear']} 134 | ]); 135 | ``` 136 | 137 | #### new CrossStorageClient(url, [opts]) 138 | 139 | Constructs a new cross storage client given the url to a hub. By default, 140 | an iframe is created within the document body that points to the url. It 141 | also accepts an options object, which may include a timeout, frameId, and 142 | promise. The timeout, in milliseconds, is applied to each request and 143 | defaults to 5000ms. The options object may also include a frameId, 144 | identifying an existing frame on which to install its listeners. If the 145 | promise key is supplied the constructor for a Promise, that Promise library 146 | will be used instead of the default window.Promise. 147 | 148 | ``` javascript 149 | var storage = new CrossStorageClient('http://localhost:3000/hub.html'); 150 | 151 | var storage = new CrossStorageClient('http://localhost:3000/hub.html', { 152 | timeout: 5000, 153 | frameId: 'storageFrame' 154 | }); 155 | ``` 156 | 157 | #### CrossStorageClient.prototype.onConnect() 158 | 159 | Returns a promise that is fulfilled when a connection has been established 160 | with the cross storage hub. Its use is required to avoid sending any 161 | requests prior to initialization being complete. 162 | 163 | ``` javascript 164 | storage.onConnect().then(function() { 165 | // ready! 166 | }); 167 | ``` 168 | 169 | #### CrossStorageClient.prototype.set(key, value) 170 | 171 | Sets a key to the specified value. Returns a promise that is fulfilled on 172 | success, or rejected if any errors setting the key occurred, or the request 173 | timed out. 174 | 175 | ``` javascript 176 | storage.onConnect().then(function() { 177 | return storage.set('key', JSON.stringify({foo: 'bar'})); 178 | }); 179 | ``` 180 | 181 | #### CrossStorageClient.prototype.get(key1, [key2], [...]) 182 | 183 | Accepts one or more keys for which to retrieve their values. Returns a 184 | promise that is settled on hub response or timeout. On success, it is 185 | fulfilled with the value of the key if only passed a single argument. 186 | Otherwise it's resolved with an array of values. On failure, it is rejected 187 | with the corresponding error message. 188 | 189 | ``` javascript 190 | storage.onConnect().then(function() { 191 | return storage.get('key1'); 192 | }).then(function(res) { 193 | return storage.get('key1', 'key2', 'key3'); 194 | }).then(function(res) { 195 | // ... 196 | }); 197 | ``` 198 | 199 | #### CrossStorageClient.prototype.del(key1, [key2], [...]) 200 | 201 | Accepts one or more keys for deletion. Returns a promise that is settled on 202 | hub response or timeout. 203 | 204 | ``` javascript 205 | storage.onConnect().then(function() { 206 | return storage.del('key1', 'key2'); 207 | }); 208 | ``` 209 | 210 | #### CrossStorageClient.prototype.getKeys() 211 | 212 | Returns a promise that, when resolved, passes an array of keys currently 213 | in storage. 214 | 215 | ``` javascript 216 | storage.onConnect().then(function() { 217 | return storage.getKeys(); 218 | }).then(function(keys) { 219 | // ['key1', 'key2', ...] 220 | }); 221 | ``` 222 | 223 | #### CrossStorageClient.prototype.clear() 224 | 225 | Returns a promise that, when resolved, indicates that all localStorage 226 | data has been cleared. 227 | 228 | ``` javascript 229 | storage.onConnect().then(function() { 230 | return storage.clear(); 231 | }); 232 | ``` 233 | 234 | #### CrossStorageClient.prototype.close() 235 | 236 | Deletes the iframe and sets the connected state to false. The client can 237 | no longer be used after being invoked. 238 | 239 | ``` javascript 240 | storage.onConnect().then(function() { 241 | return storage.set('key1', 'key2'); 242 | }).catch(function(err) { 243 | // Handle error 244 | }).then(function() { 245 | storage.close(); 246 | }); 247 | ``` 248 | 249 | ## Compatibility 250 | 251 | For compatibility with older browsers, simply load a Promise polyfill such as 252 | [es6-promise](https://github.com/jakearchibald/es6-promise). 253 | 254 | You can also use RSVP or any other ES6 compliant promise library. Supports IE8 255 | and up using the above polyfill. A JSON polyfill is also required 256 | for IE8 in Compatibility View. Also note that `catch` is a reserved word in IE8, 257 | and so error handling with promises can be done as: 258 | 259 | ``` javascript 260 | storage.onConnect().then(function() { 261 | return storage.get('key1'); 262 | }).then(function(res) { 263 | // ... on success 264 | })['catch'](function(err) { 265 | // ... on error 266 | }); 267 | ``` 268 | 269 | **Breaking Changes** 270 | 271 | API breaking changes were introduced in both 0.6 and 1.0. Refer to 272 | [releases](https://github.com/zendesk/cross-storage/releases) for details. 273 | 274 | **Notes on Safari 7+ (OSX, iOS)** 275 | 276 | All cross-domain local storage access is disabled by default with Safari 7+. 277 | This is a result of the "Block cookies and other website data" privacy setting 278 | being set to "From third parties and advertisers". Any cross-storage client 279 | code will not crash, however, it will only have access to a sandboxed, isolated 280 | local storage instance. As such, none of the data previously set by other 281 | origins will be accessible. If an option, one could fall back to using root 282 | cookies for those user agents, or requesting the data from a server-side store. 283 | 284 | ## Compression 285 | 286 | Most localStorage-compatible browsers offer at least ~5Mb of storage. But keys 287 | and values are defined as DOMStrings, which are UTF-8 encoded using single 288 | 16-bit sequences. That means a string of ~2.5 million ASCII characters will use 289 | up ~5Mb, since they're 2 bytes per char. 290 | 291 | If you need to maximize your storage space, consider using 292 | [lz-string](https://github.com/pieroxy/lz-string/). For smaller strings, it's 293 | not uncommon to see a 50% reduction in size when compressed, which will bring 294 | you a lot closer to 5 million characters. At that point, you're only limited by 295 | the average compression rate of your strings. 296 | 297 | ## Building 298 | 299 | The minified, production JavaScript can be generated with gulp by running 300 | `gulp dist`. If not already on your system, gulp can be installed using 301 | `npm install -g gulp` 302 | 303 | ## Tests 304 | 305 | Tests can be ran locally using `npm test`. Tests are ran using Zuul, and 306 | the Travis CI build uses Sauce Labs for multi-browser testing as well. 307 | 308 | ## Copyright and license 309 | 310 | Copyright 2016 Zendesk 311 | 312 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 313 | this file except in compliance with the License. 314 | You may obtain a copy of the License at 315 | 316 | http://www.apache.org/licenses/LICENSE-2.0 317 | 318 | Unless required by applicable law or agreed to in writing, software distributed 319 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 320 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 321 | specific language governing permissions and limitations under the License. 322 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cross-storage", 3 | "version": "1.0.0", 4 | "description": "Cross domain local storage", 5 | "license": "Apache-2.0", 6 | "authors": [ 7 | { 8 | "name": "Daniel St. Jules", 9 | "email": "danielst.jules@gmail.com", 10 | "url": "http://danielstjules.com" 11 | } 12 | ], 13 | "keywords": [ 14 | "local", 15 | "storage", 16 | "cross", 17 | "domain" 18 | ], 19 | "ignore": [ 20 | ".*", 21 | "media", 22 | "gulpfile.js", 23 | "package.json", 24 | "README.md", 25 | "index.js", 26 | "lib", 27 | "example", 28 | "test" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /dist/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cross-storage - Cross domain local storage 3 | * 4 | * @version 1.0.0 5 | * @link https://github.com/zendesk/cross-storage 6 | * @author Daniel St. Jules 7 | * @copyright Zendesk 8 | * @license Apache-2.0 9 | */ 10 | 11 | ;(function(root) { 12 | /** 13 | * Constructs a new cross storage client given the url to a hub. By default, 14 | * an iframe is created within the document body that points to the url. It 15 | * also accepts an options object, which may include a timeout, frameId, and 16 | * promise. The timeout, in milliseconds, is applied to each request and 17 | * defaults to 5000ms. The options object may also include a frameId, 18 | * identifying an existing frame on which to install its listeners. If the 19 | * promise key is supplied the constructor for a Promise, that Promise library 20 | * will be used instead of the default window.Promise. 21 | * 22 | * @example 23 | * var storage = new CrossStorageClient('https://store.example.com/hub.html'); 24 | * 25 | * @example 26 | * var storage = new CrossStorageClient('https://store.example.com/hub.html', { 27 | * timeout: 5000, 28 | * frameId: 'storageFrame' 29 | * }); 30 | * 31 | * @constructor 32 | * 33 | * @param {string} url The url to a cross storage hub 34 | * @param {object} [opts] An optional object containing additional options, 35 | * including timeout, frameId, and promise 36 | * 37 | * @property {string} _id A UUID v4 id 38 | * @property {function} _promise The Promise object to use 39 | * @property {string} _frameId The id of the iFrame pointing to the hub url 40 | * @property {string} _origin The hub's origin 41 | * @property {object} _requests Mapping of request ids to callbacks 42 | * @property {bool} _connected Whether or not it has connected 43 | * @property {bool} _closed Whether or not the client has closed 44 | * @property {int} _count Number of requests sent 45 | * @property {function} _listener The listener added to the window 46 | * @property {Window} _hub The hub window 47 | */ 48 | function CrossStorageClient(url, opts) { 49 | opts = opts || {}; 50 | 51 | this._id = CrossStorageClient._generateUUID(); 52 | this._promise = opts.promise || Promise; 53 | this._frameId = opts.frameId || 'CrossStorageClient-' + this._id; 54 | this._origin = CrossStorageClient._getOrigin(url); 55 | this._requests = {}; 56 | this._connected = false; 57 | this._closed = false; 58 | this._count = 0; 59 | this._timeout = opts.timeout || 5000; 60 | this._listener = null; 61 | 62 | this._installListener(); 63 | 64 | var frame; 65 | if (opts.frameId) { 66 | frame = document.getElementById(opts.frameId); 67 | } 68 | 69 | // If using a passed iframe, poll the hub for a ready message 70 | if (frame) { 71 | this._poll(); 72 | } 73 | 74 | // Create the frame if not found or specified 75 | frame = frame || this._createFrame(url); 76 | this._hub = frame.contentWindow; 77 | } 78 | 79 | /** 80 | * The styles to be applied to the generated iFrame. Defines a set of properties 81 | * that hide the element by positioning it outside of the visible area, and 82 | * by modifying its display. 83 | * 84 | * @member {Object} 85 | */ 86 | CrossStorageClient.frameStyle = { 87 | display: 'none', 88 | position: 'absolute', 89 | top: '-999px', 90 | left: '-999px' 91 | }; 92 | 93 | /** 94 | * Returns the origin of an url, with cross browser support. Accommodates 95 | * the lack of location.origin in IE, as well as the discrepancies in the 96 | * inclusion of the port when using the default port for a protocol, e.g. 97 | * 443 over https. Defaults to the origin of window.location if passed a 98 | * relative path. 99 | * 100 | * @param {string} url The url to a cross storage hub 101 | * @returns {string} The origin of the url 102 | */ 103 | CrossStorageClient._getOrigin = function(url) { 104 | var uri, protocol, origin; 105 | 106 | uri = document.createElement('a'); 107 | uri.href = url; 108 | 109 | if (!uri.host) { 110 | uri = window.location; 111 | } 112 | 113 | if (!uri.protocol || uri.protocol === ':') { 114 | protocol = window.location.protocol; 115 | } else { 116 | protocol = uri.protocol; 117 | } 118 | 119 | origin = protocol + '//' + uri.host; 120 | origin = origin.replace(/:80$|:443$/, ''); 121 | 122 | return origin; 123 | }; 124 | 125 | /** 126 | * UUID v4 generation, taken from: http://stackoverflow.com/questions/ 127 | * 105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 128 | * 129 | * @returns {string} A UUID v4 string 130 | */ 131 | CrossStorageClient._generateUUID = function() { 132 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 133 | var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8); 134 | 135 | return v.toString(16); 136 | }); 137 | }; 138 | 139 | /** 140 | * Returns a promise that is fulfilled when a connection has been established 141 | * with the cross storage hub. Its use is required to avoid sending any 142 | * requests prior to initialization being complete. 143 | * 144 | * @returns {Promise} A promise that is resolved on connect 145 | */ 146 | CrossStorageClient.prototype.onConnect = function() { 147 | var client = this; 148 | 149 | if (this._connected) { 150 | return this._promise.resolve(); 151 | } else if (this._closed) { 152 | return this._promise.reject(new Error('CrossStorageClient has closed')); 153 | } 154 | 155 | // Queue connect requests for client re-use 156 | if (!this._requests.connect) { 157 | this._requests.connect = []; 158 | } 159 | 160 | return new this._promise(function(resolve, reject) { 161 | var timeout = setTimeout(function() { 162 | reject(new Error('CrossStorageClient could not connect')); 163 | }, client._timeout); 164 | 165 | client._requests.connect.push(function(err) { 166 | clearTimeout(timeout); 167 | if (err) return reject(err); 168 | 169 | resolve(); 170 | }); 171 | }); 172 | }; 173 | 174 | /** 175 | * Sets a key to the specified value. Returns a promise that is fulfilled on 176 | * success, or rejected if any errors setting the key occurred, or the request 177 | * timed out. 178 | * 179 | * @param {string} key The key to set 180 | * @param {*} value The value to assign 181 | * @returns {Promise} A promise that is settled on hub response or timeout 182 | */ 183 | CrossStorageClient.prototype.set = function(key, value) { 184 | return this._request('set', { 185 | key: key, 186 | value: value 187 | }); 188 | }; 189 | 190 | /** 191 | * Accepts one or more keys for which to retrieve their values. Returns a 192 | * promise that is settled on hub response or timeout. On success, it is 193 | * fulfilled with the value of the key if only passed a single argument. 194 | * Otherwise it's resolved with an array of values. On failure, it is rejected 195 | * with the corresponding error message. 196 | * 197 | * @param {...string} key The key to retrieve 198 | * @returns {Promise} A promise that is settled on hub response or timeout 199 | */ 200 | CrossStorageClient.prototype.get = function(key) { 201 | var args = Array.prototype.slice.call(arguments); 202 | 203 | return this._request('get', {keys: args}); 204 | }; 205 | 206 | /** 207 | * Accepts one or more keys for deletion. Returns a promise that is settled on 208 | * hub response or timeout. 209 | * 210 | * @param {...string} key The key to delete 211 | * @returns {Promise} A promise that is settled on hub response or timeout 212 | */ 213 | CrossStorageClient.prototype.del = function() { 214 | var args = Array.prototype.slice.call(arguments); 215 | 216 | return this._request('del', {keys: args}); 217 | }; 218 | 219 | /** 220 | * Returns a promise that, when resolved, indicates that all localStorage 221 | * data has been cleared. 222 | * 223 | * @returns {Promise} A promise that is settled on hub response or timeout 224 | */ 225 | CrossStorageClient.prototype.clear = function() { 226 | return this._request('clear'); 227 | }; 228 | 229 | /** 230 | * Returns a promise that, when resolved, passes an array of all keys 231 | * currently in storage. 232 | * 233 | * @returns {Promise} A promise that is settled on hub response or timeout 234 | */ 235 | CrossStorageClient.prototype.getKeys = function() { 236 | return this._request('getKeys'); 237 | }; 238 | 239 | /** 240 | * Deletes the iframe and sets the connected state to false. The client can 241 | * no longer be used after being invoked. 242 | */ 243 | CrossStorageClient.prototype.close = function() { 244 | var frame = document.getElementById(this._frameId); 245 | if (frame) { 246 | frame.parentNode.removeChild(frame); 247 | } 248 | 249 | // Support IE8 with detachEvent 250 | if (window.removeEventListener) { 251 | window.removeEventListener('message', this._listener, false); 252 | } else { 253 | window.detachEvent('onmessage', this._listener); 254 | } 255 | 256 | this._connected = false; 257 | this._closed = true; 258 | }; 259 | 260 | /** 261 | * Installs the necessary listener for the window message event. When a message 262 | * is received, the client's _connected status is changed to true, and the 263 | * onConnect promise is fulfilled. Given a response message, the callback 264 | * corresponding to its request is invoked. If response.error holds a truthy 265 | * value, the promise associated with the original request is rejected with 266 | * the error. Otherwise the promise is fulfilled and passed response.result. 267 | * 268 | * @private 269 | */ 270 | CrossStorageClient.prototype._installListener = function() { 271 | var client = this; 272 | 273 | this._listener = function(message) { 274 | var i, origin, error, response; 275 | 276 | // Ignore invalid messages or those after the client has closed 277 | if (client._closed || !message.data || typeof message.data !== 'string') { 278 | return; 279 | } 280 | 281 | // postMessage returns the string "null" as the origin for "file://" 282 | origin = (message.origin === 'null') ? 'file://' : message.origin; 283 | 284 | // Ignore messages not from the correct origin 285 | if (origin !== client._origin) return; 286 | 287 | // LocalStorage isn't available in the hub 288 | if (message.data === 'cross-storage:unavailable') { 289 | if (!client._closed) client.close(); 290 | if (!client._requests.connect) return; 291 | 292 | error = new Error('Closing client. Could not access localStorage in hub.'); 293 | for (i = 0; i < client._requests.connect.length; i++) { 294 | client._requests.connect[i](error); 295 | } 296 | 297 | return; 298 | } 299 | 300 | // Handle initial connection 301 | if (message.data.indexOf('cross-storage:') !== -1 && !client._connected) { 302 | client._connected = true; 303 | if (!client._requests.connect) return; 304 | 305 | for (i = 0; i < client._requests.connect.length; i++) { 306 | client._requests.connect[i](error); 307 | } 308 | delete client._requests.connect; 309 | } 310 | 311 | if (message.data === 'cross-storage:ready') return; 312 | 313 | // All other messages 314 | try { 315 | response = JSON.parse(message.data); 316 | } catch(e) { 317 | return; 318 | } 319 | 320 | if (!response.id) return; 321 | 322 | if (client._requests[response.id]) { 323 | client._requests[response.id](response.error, response.result); 324 | } 325 | }; 326 | 327 | // Support IE8 with attachEvent 328 | if (window.addEventListener) { 329 | window.addEventListener('message', this._listener, false); 330 | } else { 331 | window.attachEvent('onmessage', this._listener); 332 | } 333 | }; 334 | 335 | /** 336 | * Invoked when a frame id was passed to the client, rather than allowing 337 | * the client to create its own iframe. Polls the hub for a ready event to 338 | * establish a connected state. 339 | */ 340 | CrossStorageClient.prototype._poll = function() { 341 | var client, interval, targetOrigin; 342 | 343 | client = this; 344 | 345 | // postMessage requires that the target origin be set to "*" for "file://" 346 | targetOrigin = (client._origin === 'file://') ? '*' : client._origin; 347 | 348 | interval = setInterval(function() { 349 | if (client._connected) return clearInterval(interval); 350 | if (!client._hub) return; 351 | 352 | client._hub.postMessage('cross-storage:poll', targetOrigin); 353 | }, 1000); 354 | }; 355 | 356 | /** 357 | * Creates a new iFrame containing the hub. Applies the necessary styles to 358 | * hide the element from view, prior to adding it to the document body. 359 | * Returns the created element. 360 | * 361 | * @private 362 | * 363 | * @param {string} url The url to the hub 364 | * returns {HTMLIFrameElement} The iFrame element itself 365 | */ 366 | CrossStorageClient.prototype._createFrame = function(url) { 367 | var frame, key; 368 | 369 | frame = window.document.createElement('iframe'); 370 | frame.id = this._frameId; 371 | 372 | // Style the iframe 373 | for (key in CrossStorageClient.frameStyle) { 374 | if (CrossStorageClient.frameStyle.hasOwnProperty(key)) { 375 | frame.style[key] = CrossStorageClient.frameStyle[key]; 376 | } 377 | } 378 | 379 | window.document.body.appendChild(frame); 380 | frame.src = url; 381 | 382 | return frame; 383 | }; 384 | 385 | /** 386 | * Sends a message containing the given method and params to the hub. Stores 387 | * a callback in the _requests object for later invocation on message, or 388 | * deletion on timeout. Returns a promise that is settled in either instance. 389 | * 390 | * @private 391 | * 392 | * @param {string} method The method to invoke 393 | * @param {*} params The arguments to pass 394 | * @returns {Promise} A promise that is settled on hub response or timeout 395 | */ 396 | CrossStorageClient.prototype._request = function(method, params) { 397 | var req, client; 398 | 399 | if (this._closed) { 400 | return this._promise.reject(new Error('CrossStorageClient has closed')); 401 | } 402 | 403 | client = this; 404 | client._count++; 405 | 406 | req = { 407 | id: this._id + ':' + client._count, 408 | method: 'cross-storage:' + method, 409 | params: params 410 | }; 411 | 412 | return new this._promise(function(resolve, reject) { 413 | var timeout, originalToJSON, targetOrigin; 414 | 415 | // Timeout if a response isn't received after 4s 416 | timeout = setTimeout(function() { 417 | if (!client._requests[req.id]) return; 418 | 419 | delete client._requests[req.id]; 420 | reject(new Error('Timeout: could not perform ' + req.method)); 421 | }, client._timeout); 422 | 423 | // Add request callback 424 | client._requests[req.id] = function(err, result) { 425 | clearTimeout(timeout); 426 | delete client._requests[req.id]; 427 | if (err) return reject(new Error(err)); 428 | resolve(result); 429 | }; 430 | 431 | // In case we have a broken Array.prototype.toJSON, e.g. because of 432 | // old versions of prototype 433 | if (Array.prototype.toJSON) { 434 | originalToJSON = Array.prototype.toJSON; 435 | Array.prototype.toJSON = null; 436 | } 437 | 438 | // postMessage requires that the target origin be set to "*" for "file://" 439 | targetOrigin = (client._origin === 'file://') ? '*' : client._origin; 440 | 441 | // Send serialized message 442 | client._hub.postMessage(JSON.stringify(req), targetOrigin); 443 | 444 | // Restore original toJSON 445 | if (originalToJSON) { 446 | Array.prototype.toJSON = originalToJSON; 447 | } 448 | }); 449 | }; 450 | 451 | /** 452 | * Export for various environments. 453 | */ 454 | if (typeof module !== 'undefined' && module.exports) { 455 | module.exports = CrossStorageClient; 456 | } else if (typeof exports !== 'undefined') { 457 | exports.CrossStorageClient = CrossStorageClient; 458 | } else if (typeof define === 'function' && define.amd) { 459 | define([], function() { 460 | return CrossStorageClient; 461 | }); 462 | } else { 463 | root.CrossStorageClient = CrossStorageClient; 464 | } 465 | }(this)); 466 | -------------------------------------------------------------------------------- /dist/client.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cross-storage - Cross domain local storage 3 | * 4 | * @version 1.0.0 5 | * @link https://github.com/zendesk/cross-storage 6 | * @author Daniel St. Jules 7 | * @copyright Zendesk 8 | * @license Apache-2.0 9 | */ 10 | 11 | !function(e){function t(e,r){r=r||{},this._id=t._generateUUID(),this._promise=r.promise||Promise,this._frameId=r.frameId||"CrossStorageClient-"+this._id,this._origin=t._getOrigin(e),this._requests={},this._connected=!1,this._closed=!1,this._count=0,this._timeout=r.timeout||5e3,this._listener=null,this._installListener();var o;r.frameId&&(o=document.getElementById(r.frameId)),o&&this._poll(),o=o||this._createFrame(e),this._hub=o.contentWindow}t.frameStyle={display:"none",position:"absolute",top:"-999px",left:"-999px"},t._getOrigin=function(e){var t,r,o;return t=document.createElement("a"),t.href=e,t.host||(t=window.location),r=t.protocol&&":"!==t.protocol?t.protocol:window.location.protocol,o=r+"//"+t.host,o=o.replace(/:80$|:443$/,"")},t._generateUUID=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0,r="x"==e?t:3&t|8;return r.toString(16)})},t.prototype.onConnect=function(){var e=this;return this._connected?this._promise.resolve():this._closed?this._promise.reject(new Error("CrossStorageClient has closed")):(this._requests.connect||(this._requests.connect=[]),new this._promise(function(t,r){var o=setTimeout(function(){r(new Error("CrossStorageClient could not connect"))},e._timeout);e._requests.connect.push(function(e){return clearTimeout(o),e?r(e):(t(),void 0)})}))},t.prototype.set=function(e,t){return this._request("set",{key:e,value:t})},t.prototype.get=function(){var e=Array.prototype.slice.call(arguments);return this._request("get",{keys:e})},t.prototype.del=function(){var e=Array.prototype.slice.call(arguments);return this._request("del",{keys:e})},t.prototype.clear=function(){return this._request("clear")},t.prototype.getKeys=function(){return this._request("getKeys")},t.prototype.close=function(){var e=document.getElementById(this._frameId);e&&e.parentNode.removeChild(e),window.removeEventListener?window.removeEventListener("message",this._listener,!1):window.detachEvent("onmessage",this._listener),this._connected=!1,this._closed=!0},t.prototype._installListener=function(){var e=this;this._listener=function(t){var r,o,n,s;if(!e._closed&&t.data&&"string"==typeof t.data&&(o="null"===t.origin?"file://":t.origin,o===e._origin))if("cross-storage:unavailable"!==t.data){if(-1!==t.data.indexOf("cross-storage:")&&!e._connected){if(e._connected=!0,!e._requests.connect)return;for(r=0;r 7 | * @copyright Zendesk 8 | * @license Apache-2.0 9 | */ 10 | 11 | ;(function(root) { 12 | var CrossStorageHub = {}; 13 | 14 | /** 15 | * Accepts an array of objects with two keys: origin and allow. The value 16 | * of origin is expected to be a RegExp, and allow, an array of strings. 17 | * The cross storage hub is then initialized to accept requests from any of 18 | * the matching origins, allowing access to the associated lists of methods. 19 | * Methods may include any of: get, set, del, getKeys and clear. A 'ready' 20 | * message is sent to the parent window once complete. 21 | * 22 | * @example 23 | * // Subdomain can get, but only root domain can set and del 24 | * CrossStorageHub.init([ 25 | * {origin: /\.example.com$/, allow: ['get']}, 26 | * {origin: /:(www\.)?example.com$/, allow: ['get', 'set', 'del']} 27 | * ]); 28 | * 29 | * @param {array} permissions An array of objects with origin and allow 30 | */ 31 | CrossStorageHub.init = function(permissions) { 32 | var available = true; 33 | 34 | // Return if localStorage is unavailable, or third party 35 | // access is disabled 36 | try { 37 | if (!window.localStorage) available = false; 38 | } catch (e) { 39 | available = false; 40 | } 41 | 42 | if (!available) { 43 | try { 44 | return window.parent.postMessage('cross-storage:unavailable', '*'); 45 | } catch (e) { 46 | return; 47 | } 48 | } 49 | 50 | CrossStorageHub._permissions = permissions || []; 51 | CrossStorageHub._installListener(); 52 | window.parent.postMessage('cross-storage:ready', '*'); 53 | }; 54 | 55 | /** 56 | * Installs the necessary listener for the window message event. Accommodates 57 | * IE8 and up. 58 | * 59 | * @private 60 | */ 61 | CrossStorageHub._installListener = function() { 62 | var listener = CrossStorageHub._listener; 63 | if (window.addEventListener) { 64 | window.addEventListener('message', listener, false); 65 | } else { 66 | window.attachEvent('onmessage', listener); 67 | } 68 | }; 69 | 70 | /** 71 | * The message handler for all requests posted to the window. It ignores any 72 | * messages having an origin that does not match the originally supplied 73 | * pattern. Given a JSON object with one of get, set, del or getKeys as the 74 | * method, the function performs the requested action and returns its result. 75 | * 76 | * @param {MessageEvent} message A message to be processed 77 | */ 78 | CrossStorageHub._listener = function(message) { 79 | var origin, targetOrigin, request, method, error, result, response; 80 | 81 | // postMessage returns the string "null" as the origin for "file://" 82 | origin = (message.origin === 'null') ? 'file://' : message.origin; 83 | 84 | // Handle polling for a ready message 85 | if (message.data === 'cross-storage:poll') { 86 | return window.parent.postMessage('cross-storage:ready', message.origin); 87 | } 88 | 89 | // Ignore the ready message when viewing the hub directly 90 | if (message.data === 'cross-storage:ready') return; 91 | 92 | // Check whether message.data is a valid json 93 | try { 94 | request = JSON.parse(message.data); 95 | } catch (err) { 96 | return; 97 | } 98 | 99 | // Check whether request.method is a string 100 | if (!request || typeof request.method !== 'string') { 101 | return; 102 | } 103 | 104 | method = request.method.split('cross-storage:')[1]; 105 | 106 | if (!method) { 107 | return; 108 | } else if (!CrossStorageHub._permitted(origin, method)) { 109 | error = 'Invalid permissions for ' + method; 110 | } else { 111 | try { 112 | result = CrossStorageHub['_' + method](request.params); 113 | } catch (err) { 114 | error = err.message; 115 | } 116 | } 117 | 118 | response = JSON.stringify({ 119 | id: request.id, 120 | error: error, 121 | result: result 122 | }); 123 | 124 | // postMessage requires that the target origin be set to "*" for "file://" 125 | targetOrigin = (origin === 'file://') ? '*' : origin; 126 | 127 | window.parent.postMessage(response, targetOrigin); 128 | }; 129 | 130 | /** 131 | * Returns a boolean indicating whether or not the requested method is 132 | * permitted for the given origin. The argument passed to method is expected 133 | * to be one of 'get', 'set', 'del' or 'getKeys'. 134 | * 135 | * @param {string} origin The origin for which to determine permissions 136 | * @param {string} method Requested action 137 | * @returns {bool} Whether or not the request is permitted 138 | */ 139 | CrossStorageHub._permitted = function(origin, method) { 140 | var available, i, entry, match; 141 | 142 | available = ['get', 'set', 'del', 'clear', 'getKeys']; 143 | if (!CrossStorageHub._inArray(method, available)) { 144 | return false; 145 | } 146 | 147 | for (i = 0; i < CrossStorageHub._permissions.length; i++) { 148 | entry = CrossStorageHub._permissions[i]; 149 | if (!(entry.origin instanceof RegExp) || !(entry.allow instanceof Array)) { 150 | continue; 151 | } 152 | 153 | match = entry.origin.test(origin); 154 | if (match && CrossStorageHub._inArray(method, entry.allow)) { 155 | return true; 156 | } 157 | } 158 | 159 | return false; 160 | }; 161 | 162 | /** 163 | * Sets a key to the specified value. 164 | * 165 | * @param {object} params An object with key and value 166 | */ 167 | CrossStorageHub._set = function(params) { 168 | window.localStorage.setItem(params.key, params.value); 169 | }; 170 | 171 | /** 172 | * Accepts an object with an array of keys for which to retrieve their values. 173 | * Returns a single value if only one key was supplied, otherwise it returns 174 | * an array. Any keys not set result in a null element in the resulting array. 175 | * 176 | * @param {object} params An object with an array of keys 177 | * @returns {*|*[]} Either a single value, or an array 178 | */ 179 | CrossStorageHub._get = function(params) { 180 | var storage, result, i, value; 181 | 182 | storage = window.localStorage; 183 | result = []; 184 | 185 | for (i = 0; i < params.keys.length; i++) { 186 | try { 187 | value = storage.getItem(params.keys[i]); 188 | } catch (e) { 189 | value = null; 190 | } 191 | 192 | result.push(value); 193 | } 194 | 195 | return (result.length > 1) ? result : result[0]; 196 | }; 197 | 198 | /** 199 | * Deletes all keys specified in the array found at params.keys. 200 | * 201 | * @param {object} params An object with an array of keys 202 | */ 203 | CrossStorageHub._del = function(params) { 204 | for (var i = 0; i < params.keys.length; i++) { 205 | window.localStorage.removeItem(params.keys[i]); 206 | } 207 | }; 208 | 209 | /** 210 | * Clears localStorage. 211 | */ 212 | CrossStorageHub._clear = function() { 213 | window.localStorage.clear(); 214 | }; 215 | 216 | /** 217 | * Returns an array of all keys stored in localStorage. 218 | * 219 | * @returns {string[]} The array of keys 220 | */ 221 | CrossStorageHub._getKeys = function(params) { 222 | var i, length, keys; 223 | 224 | keys = []; 225 | length = window.localStorage.length; 226 | 227 | for (i = 0; i < length; i++) { 228 | keys.push(window.localStorage.key(i)); 229 | } 230 | 231 | return keys; 232 | }; 233 | 234 | /** 235 | * Returns whether or not a value is present in the array. Consists of an 236 | * alternative to extending the array prototype for indexOf, since it's 237 | * unavailable for IE8. 238 | * 239 | * @param {*} value The value to find 240 | * @parma {[]*} array The array in which to search 241 | * @returns {bool} Whether or not the value was found 242 | */ 243 | CrossStorageHub._inArray = function(value, array) { 244 | for (var i = 0; i < array.length; i++) { 245 | if (value === array[i]) return true; 246 | } 247 | 248 | return false; 249 | }; 250 | 251 | /** 252 | * A cross-browser version of Date.now compatible with IE8 that avoids 253 | * modifying the Date object. 254 | * 255 | * @return {int} The current timestamp in milliseconds 256 | */ 257 | CrossStorageHub._now = function() { 258 | if (typeof Date.now === 'function') { 259 | return Date.now(); 260 | } 261 | 262 | return new Date().getTime(); 263 | }; 264 | 265 | /** 266 | * Export for various environments. 267 | */ 268 | if (typeof module !== 'undefined' && module.exports) { 269 | module.exports = CrossStorageHub; 270 | } else if (typeof exports !== 'undefined') { 271 | exports.CrossStorageHub = CrossStorageHub; 272 | } else if (typeof define === 'function' && define.amd) { 273 | define([], function() { 274 | return CrossStorageHub; 275 | }); 276 | } else { 277 | root.CrossStorageHub = CrossStorageHub; 278 | } 279 | }(this)); 280 | -------------------------------------------------------------------------------- /dist/hub.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cross-storage - Cross domain local storage 3 | * 4 | * @version 1.0.0 5 | * @link https://github.com/zendesk/cross-storage 6 | * @author Daniel St. Jules 7 | * @copyright Zendesk 8 | * @license Apache-2.0 9 | */ 10 | 11 | !function(e){var t={};t.init=function(e){var r=!0;try{window.localStorage||(r=!1)}catch(n){r=!1}if(!r)try{return window.parent.postMessage("cross-storage:unavailable","*")}catch(n){return}t._permissions=e||[],t._installListener(),window.parent.postMessage("cross-storage:ready","*")},t._installListener=function(){var e=t._listener;window.addEventListener?window.addEventListener("message",e,!1):window.attachEvent("onmessage",e)},t._listener=function(e){var r,n,o,i,s,a,l;if(r="null"===e.origin?"file://":e.origin,"cross-storage:poll"===e.data)return window.parent.postMessage("cross-storage:ready",e.origin);if("cross-storage:ready"!==e.data){try{o=JSON.parse(e.data)}catch(c){return}if(o&&"string"==typeof o.method&&(i=o.method.split("cross-storage:")[1])){if(t._permitted(r,i))try{a=t["_"+i](o.params)}catch(c){s=c.message}else s="Invalid permissions for "+i;l=JSON.stringify({id:o.id,error:s,result:a}),n="file://"===r?"*":r,window.parent.postMessage(l,n)}}},t._permitted=function(e,r){var n,o,i,s;if(n=["get","set","del","clear","getKeys"],!t._inArray(r,n))return!1;for(o=0;o1?r:r[0]},t._del=function(e){for(var t=0;te;e++)r.push(window.localStorage.key(e));return r},t._inArray=function(e,t){for(var r=0;r 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 8 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/client2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/hub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var rimraf = require('gulp-rimraf'); 3 | var uglify = require('gulp-uglify'); 4 | var jshint = require('gulp-jshint'); 5 | var rename = require('gulp-rename'); 6 | var header = require('gulp-header'); 7 | 8 | var pkg = require('./package.json'); 9 | var banner = [ 10 | '/**', 11 | ' * <%= pkg.name %> - <%= pkg.description %>', 12 | ' *', 13 | ' * @version <%= pkg.version %>', 14 | ' * @link <%= pkg.homepage %>', 15 | ' * @author <%= pkg.author %>', 16 | ' * @copyright Zendesk', 17 | ' * @license <%= pkg.license %>', 18 | ' */\n\n' 19 | ].join('\n'); 20 | 21 | var paths = { 22 | scripts: ['./lib/client.js', './lib/hub.js'], 23 | dist: './dist/' 24 | }; 25 | 26 | gulp.task('clean', function() { 27 | gulp.src(paths.dist + '*', {read: false}) 28 | .pipe(rimraf()); 29 | }); 30 | 31 | gulp.task('copy', function() { 32 | gulp.src(paths.scripts) 33 | .pipe(header(banner, {pkg: pkg})) 34 | .pipe(gulp.dest(paths.dist)); 35 | }); 36 | 37 | gulp.task('minify', function() { 38 | gulp.src(paths.scripts) 39 | .pipe(uglify()) 40 | .pipe(header(banner, {pkg: pkg})) 41 | .pipe(rename(function(path) { 42 | path.basename += '.min'; 43 | })) 44 | .pipe(gulp.dest(paths.dist)); 45 | }); 46 | 47 | gulp.task('jshint', function() { 48 | gulp.src(paths.scripts) 49 | .pipe(jshint()) 50 | .pipe(jshint.reporter()); 51 | }); 52 | 53 | gulp.task('dist', ['clean', 'copy', 'minify']); 54 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | ;(function(root) { 2 | /** 3 | * Constructs a new cross storage client given the url to a hub. By default, 4 | * an iframe is created within the document body that points to the url. It 5 | * also accepts an options object, which may include a timeout, frameId, and 6 | * promise. The timeout, in milliseconds, is applied to each request and 7 | * defaults to 5000ms. The options object may also include a frameId, 8 | * identifying an existing frame on which to install its listeners. If the 9 | * promise key is supplied the constructor for a Promise, that Promise library 10 | * will be used instead of the default window.Promise. 11 | * 12 | * @example 13 | * var storage = new CrossStorageClient('https://store.example.com/hub.html'); 14 | * 15 | * @example 16 | * var storage = new CrossStorageClient('https://store.example.com/hub.html', { 17 | * timeout: 5000, 18 | * frameId: 'storageFrame' 19 | * }); 20 | * 21 | * @constructor 22 | * 23 | * @param {string} url The url to a cross storage hub 24 | * @param {object} [opts] An optional object containing additional options, 25 | * including timeout, frameId, and promise 26 | * 27 | * @property {string} _id A UUID v4 id 28 | * @property {function} _promise The Promise object to use 29 | * @property {string} _frameId The id of the iFrame pointing to the hub url 30 | * @property {string} _origin The hub's origin 31 | * @property {object} _requests Mapping of request ids to callbacks 32 | * @property {bool} _connected Whether or not it has connected 33 | * @property {bool} _closed Whether or not the client has closed 34 | * @property {int} _count Number of requests sent 35 | * @property {function} _listener The listener added to the window 36 | * @property {Window} _hub The hub window 37 | */ 38 | function CrossStorageClient(url, opts) { 39 | opts = opts || {}; 40 | 41 | this._id = CrossStorageClient._generateUUID(); 42 | this._promise = opts.promise || Promise; 43 | this._frameId = opts.frameId || 'CrossStorageClient-' + this._id; 44 | this._origin = CrossStorageClient._getOrigin(url); 45 | this._requests = {}; 46 | this._connected = false; 47 | this._closed = false; 48 | this._count = 0; 49 | this._timeout = opts.timeout || 5000; 50 | this._listener = null; 51 | 52 | this._installListener(); 53 | 54 | var frame; 55 | if (opts.frameId) { 56 | frame = document.getElementById(opts.frameId); 57 | } 58 | 59 | // If using a passed iframe, poll the hub for a ready message 60 | if (frame) { 61 | this._poll(); 62 | } 63 | 64 | // Create the frame if not found or specified 65 | frame = frame || this._createFrame(url); 66 | this._hub = frame.contentWindow; 67 | } 68 | 69 | /** 70 | * The styles to be applied to the generated iFrame. Defines a set of properties 71 | * that hide the element by positioning it outside of the visible area, and 72 | * by modifying its display. 73 | * 74 | * @member {Object} 75 | */ 76 | CrossStorageClient.frameStyle = { 77 | display: 'none', 78 | position: 'absolute', 79 | top: '-999px', 80 | left: '-999px' 81 | }; 82 | 83 | /** 84 | * Returns the origin of an url, with cross browser support. Accommodates 85 | * the lack of location.origin in IE, as well as the discrepancies in the 86 | * inclusion of the port when using the default port for a protocol, e.g. 87 | * 443 over https. Defaults to the origin of window.location if passed a 88 | * relative path. 89 | * 90 | * @param {string} url The url to a cross storage hub 91 | * @returns {string} The origin of the url 92 | */ 93 | CrossStorageClient._getOrigin = function(url) { 94 | var uri, protocol, origin; 95 | 96 | uri = document.createElement('a'); 97 | uri.href = url; 98 | 99 | if (!uri.host) { 100 | uri = window.location; 101 | } 102 | 103 | if (!uri.protocol || uri.protocol === ':') { 104 | protocol = window.location.protocol; 105 | } else { 106 | protocol = uri.protocol; 107 | } 108 | 109 | origin = protocol + '//' + uri.host; 110 | origin = origin.replace(/:80$|:443$/, ''); 111 | 112 | return origin; 113 | }; 114 | 115 | /** 116 | * UUID v4 generation, taken from: http://stackoverflow.com/questions/ 117 | * 105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 118 | * 119 | * @returns {string} A UUID v4 string 120 | */ 121 | CrossStorageClient._generateUUID = function() { 122 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 123 | var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8); 124 | 125 | return v.toString(16); 126 | }); 127 | }; 128 | 129 | /** 130 | * Returns a promise that is fulfilled when a connection has been established 131 | * with the cross storage hub. Its use is required to avoid sending any 132 | * requests prior to initialization being complete. 133 | * 134 | * @returns {Promise} A promise that is resolved on connect 135 | */ 136 | CrossStorageClient.prototype.onConnect = function() { 137 | var client = this; 138 | 139 | if (this._connected) { 140 | return this._promise.resolve(); 141 | } else if (this._closed) { 142 | return this._promise.reject(new Error('CrossStorageClient has closed')); 143 | } 144 | 145 | // Queue connect requests for client re-use 146 | if (!this._requests.connect) { 147 | this._requests.connect = []; 148 | } 149 | 150 | return new this._promise(function(resolve, reject) { 151 | var timeout = setTimeout(function() { 152 | reject(new Error('CrossStorageClient could not connect')); 153 | }, client._timeout); 154 | 155 | client._requests.connect.push(function(err) { 156 | clearTimeout(timeout); 157 | if (err) return reject(err); 158 | 159 | resolve(); 160 | }); 161 | }); 162 | }; 163 | 164 | /** 165 | * Sets a key to the specified value. Returns a promise that is fulfilled on 166 | * success, or rejected if any errors setting the key occurred, or the request 167 | * timed out. 168 | * 169 | * @param {string} key The key to set 170 | * @param {*} value The value to assign 171 | * @returns {Promise} A promise that is settled on hub response or timeout 172 | */ 173 | CrossStorageClient.prototype.set = function(key, value) { 174 | return this._request('set', { 175 | key: key, 176 | value: value 177 | }); 178 | }; 179 | 180 | /** 181 | * Accepts one or more keys for which to retrieve their values. Returns a 182 | * promise that is settled on hub response or timeout. On success, it is 183 | * fulfilled with the value of the key if only passed a single argument. 184 | * Otherwise it's resolved with an array of values. On failure, it is rejected 185 | * with the corresponding error message. 186 | * 187 | * @param {...string} key The key to retrieve 188 | * @returns {Promise} A promise that is settled on hub response or timeout 189 | */ 190 | CrossStorageClient.prototype.get = function(key) { 191 | var args = Array.prototype.slice.call(arguments); 192 | 193 | return this._request('get', {keys: args}); 194 | }; 195 | 196 | /** 197 | * Accepts one or more keys for deletion. Returns a promise that is settled on 198 | * hub response or timeout. 199 | * 200 | * @param {...string} key The key to delete 201 | * @returns {Promise} A promise that is settled on hub response or timeout 202 | */ 203 | CrossStorageClient.prototype.del = function() { 204 | var args = Array.prototype.slice.call(arguments); 205 | 206 | return this._request('del', {keys: args}); 207 | }; 208 | 209 | /** 210 | * Returns a promise that, when resolved, indicates that all localStorage 211 | * data has been cleared. 212 | * 213 | * @returns {Promise} A promise that is settled on hub response or timeout 214 | */ 215 | CrossStorageClient.prototype.clear = function() { 216 | return this._request('clear'); 217 | }; 218 | 219 | /** 220 | * Returns a promise that, when resolved, passes an array of all keys 221 | * currently in storage. 222 | * 223 | * @returns {Promise} A promise that is settled on hub response or timeout 224 | */ 225 | CrossStorageClient.prototype.getKeys = function() { 226 | return this._request('getKeys'); 227 | }; 228 | 229 | /** 230 | * Deletes the iframe and sets the connected state to false. The client can 231 | * no longer be used after being invoked. 232 | */ 233 | CrossStorageClient.prototype.close = function() { 234 | var frame = document.getElementById(this._frameId); 235 | if (frame) { 236 | frame.parentNode.removeChild(frame); 237 | } 238 | 239 | // Support IE8 with detachEvent 240 | if (window.removeEventListener) { 241 | window.removeEventListener('message', this._listener, false); 242 | } else { 243 | window.detachEvent('onmessage', this._listener); 244 | } 245 | 246 | this._connected = false; 247 | this._closed = true; 248 | }; 249 | 250 | /** 251 | * Installs the necessary listener for the window message event. When a message 252 | * is received, the client's _connected status is changed to true, and the 253 | * onConnect promise is fulfilled. Given a response message, the callback 254 | * corresponding to its request is invoked. If response.error holds a truthy 255 | * value, the promise associated with the original request is rejected with 256 | * the error. Otherwise the promise is fulfilled and passed response.result. 257 | * 258 | * @private 259 | */ 260 | CrossStorageClient.prototype._installListener = function() { 261 | var client = this; 262 | 263 | this._listener = function(message) { 264 | var i, origin, error, response; 265 | 266 | // Ignore invalid messages or those after the client has closed 267 | if (client._closed || !message.data || typeof message.data !== 'string') { 268 | return; 269 | } 270 | 271 | // postMessage returns the string "null" as the origin for "file://" 272 | origin = (message.origin === 'null') ? 'file://' : message.origin; 273 | 274 | // Ignore messages not from the correct origin 275 | if (origin !== client._origin) return; 276 | 277 | // LocalStorage isn't available in the hub 278 | if (message.data === 'cross-storage:unavailable') { 279 | if (!client._closed) client.close(); 280 | if (!client._requests.connect) return; 281 | 282 | error = new Error('Closing client. Could not access localStorage in hub.'); 283 | for (i = 0; i < client._requests.connect.length; i++) { 284 | client._requests.connect[i](error); 285 | } 286 | 287 | return; 288 | } 289 | 290 | // Handle initial connection 291 | if (message.data.indexOf('cross-storage:') !== -1 && !client._connected) { 292 | client._connected = true; 293 | if (!client._requests.connect) return; 294 | 295 | for (i = 0; i < client._requests.connect.length; i++) { 296 | client._requests.connect[i](error); 297 | } 298 | delete client._requests.connect; 299 | } 300 | 301 | if (message.data === 'cross-storage:ready') return; 302 | 303 | // All other messages 304 | try { 305 | response = JSON.parse(message.data); 306 | } catch(e) { 307 | return; 308 | } 309 | 310 | if (!response.id) return; 311 | 312 | if (client._requests[response.id]) { 313 | client._requests[response.id](response.error, response.result); 314 | } 315 | }; 316 | 317 | // Support IE8 with attachEvent 318 | if (window.addEventListener) { 319 | window.addEventListener('message', this._listener, false); 320 | } else { 321 | window.attachEvent('onmessage', this._listener); 322 | } 323 | }; 324 | 325 | /** 326 | * Invoked when a frame id was passed to the client, rather than allowing 327 | * the client to create its own iframe. Polls the hub for a ready event to 328 | * establish a connected state. 329 | */ 330 | CrossStorageClient.prototype._poll = function() { 331 | var client, interval, targetOrigin; 332 | 333 | client = this; 334 | 335 | // postMessage requires that the target origin be set to "*" for "file://" 336 | targetOrigin = (client._origin === 'file://') ? '*' : client._origin; 337 | 338 | interval = setInterval(function() { 339 | if (client._connected) return clearInterval(interval); 340 | if (!client._hub) return; 341 | 342 | client._hub.postMessage('cross-storage:poll', targetOrigin); 343 | }, 1000); 344 | }; 345 | 346 | /** 347 | * Creates a new iFrame containing the hub. Applies the necessary styles to 348 | * hide the element from view, prior to adding it to the document body. 349 | * Returns the created element. 350 | * 351 | * @private 352 | * 353 | * @param {string} url The url to the hub 354 | * returns {HTMLIFrameElement} The iFrame element itself 355 | */ 356 | CrossStorageClient.prototype._createFrame = function(url) { 357 | var frame, key; 358 | 359 | frame = window.document.createElement('iframe'); 360 | frame.id = this._frameId; 361 | 362 | // Style the iframe 363 | for (key in CrossStorageClient.frameStyle) { 364 | if (CrossStorageClient.frameStyle.hasOwnProperty(key)) { 365 | frame.style[key] = CrossStorageClient.frameStyle[key]; 366 | } 367 | } 368 | 369 | window.document.body.appendChild(frame); 370 | frame.src = url; 371 | 372 | return frame; 373 | }; 374 | 375 | /** 376 | * Sends a message containing the given method and params to the hub. Stores 377 | * a callback in the _requests object for later invocation on message, or 378 | * deletion on timeout. Returns a promise that is settled in either instance. 379 | * 380 | * @private 381 | * 382 | * @param {string} method The method to invoke 383 | * @param {*} params The arguments to pass 384 | * @returns {Promise} A promise that is settled on hub response or timeout 385 | */ 386 | CrossStorageClient.prototype._request = function(method, params) { 387 | var req, client; 388 | 389 | if (this._closed) { 390 | return this._promise.reject(new Error('CrossStorageClient has closed')); 391 | } 392 | 393 | client = this; 394 | client._count++; 395 | 396 | req = { 397 | id: this._id + ':' + client._count, 398 | method: 'cross-storage:' + method, 399 | params: params 400 | }; 401 | 402 | return new this._promise(function(resolve, reject) { 403 | var timeout, originalToJSON, targetOrigin; 404 | 405 | // Timeout if a response isn't received after 4s 406 | timeout = setTimeout(function() { 407 | if (!client._requests[req.id]) return; 408 | 409 | delete client._requests[req.id]; 410 | reject(new Error('Timeout: could not perform ' + req.method)); 411 | }, client._timeout); 412 | 413 | // Add request callback 414 | client._requests[req.id] = function(err, result) { 415 | clearTimeout(timeout); 416 | delete client._requests[req.id]; 417 | if (err) return reject(new Error(err)); 418 | resolve(result); 419 | }; 420 | 421 | // In case we have a broken Array.prototype.toJSON, e.g. because of 422 | // old versions of prototype 423 | if (Array.prototype.toJSON) { 424 | originalToJSON = Array.prototype.toJSON; 425 | Array.prototype.toJSON = null; 426 | } 427 | 428 | // postMessage requires that the target origin be set to "*" for "file://" 429 | targetOrigin = (client._origin === 'file://') ? '*' : client._origin; 430 | 431 | // Send serialized message 432 | client._hub.postMessage(JSON.stringify(req), targetOrigin); 433 | 434 | // Restore original toJSON 435 | if (originalToJSON) { 436 | Array.prototype.toJSON = originalToJSON; 437 | } 438 | }); 439 | }; 440 | 441 | /** 442 | * Export for various environments. 443 | */ 444 | if (typeof module !== 'undefined' && module.exports) { 445 | module.exports = CrossStorageClient; 446 | } else if (typeof exports !== 'undefined') { 447 | exports.CrossStorageClient = CrossStorageClient; 448 | } else if (typeof define === 'function' && define.amd) { 449 | define([], function() { 450 | return CrossStorageClient; 451 | }); 452 | } else { 453 | root.CrossStorageClient = CrossStorageClient; 454 | } 455 | }(this)); 456 | -------------------------------------------------------------------------------- /lib/hub.js: -------------------------------------------------------------------------------- 1 | ;(function(root) { 2 | var CrossStorageHub = {}; 3 | 4 | /** 5 | * Accepts an array of objects with two keys: origin and allow. The value 6 | * of origin is expected to be a RegExp, and allow, an array of strings. 7 | * The cross storage hub is then initialized to accept requests from any of 8 | * the matching origins, allowing access to the associated lists of methods. 9 | * Methods may include any of: get, set, del, getKeys and clear. A 'ready' 10 | * message is sent to the parent window once complete. 11 | * 12 | * @example 13 | * // Subdomain can get, but only root domain can set and del 14 | * CrossStorageHub.init([ 15 | * {origin: /\.example.com$/, allow: ['get']}, 16 | * {origin: /:(www\.)?example.com$/, allow: ['get', 'set', 'del']} 17 | * ]); 18 | * 19 | * @param {array} permissions An array of objects with origin and allow 20 | */ 21 | CrossStorageHub.init = function(permissions) { 22 | var available = true; 23 | 24 | // Return if localStorage is unavailable, or third party 25 | // access is disabled 26 | try { 27 | if (!window.localStorage) available = false; 28 | } catch (e) { 29 | available = false; 30 | } 31 | 32 | if (!available) { 33 | try { 34 | return window.parent.postMessage('cross-storage:unavailable', '*'); 35 | } catch (e) { 36 | return; 37 | } 38 | } 39 | 40 | CrossStorageHub._permissions = permissions || []; 41 | CrossStorageHub._installListener(); 42 | window.parent.postMessage('cross-storage:ready', '*'); 43 | }; 44 | 45 | /** 46 | * Installs the necessary listener for the window message event. Accommodates 47 | * IE8 and up. 48 | * 49 | * @private 50 | */ 51 | CrossStorageHub._installListener = function() { 52 | var listener = CrossStorageHub._listener; 53 | if (window.addEventListener) { 54 | window.addEventListener('message', listener, false); 55 | } else { 56 | window.attachEvent('onmessage', listener); 57 | } 58 | }; 59 | 60 | /** 61 | * The message handler for all requests posted to the window. It ignores any 62 | * messages having an origin that does not match the originally supplied 63 | * pattern. Given a JSON object with one of get, set, del or getKeys as the 64 | * method, the function performs the requested action and returns its result. 65 | * 66 | * @param {MessageEvent} message A message to be processed 67 | */ 68 | CrossStorageHub._listener = function(message) { 69 | var origin, targetOrigin, request, method, error, result, response; 70 | 71 | // postMessage returns the string "null" as the origin for "file://" 72 | origin = (message.origin === 'null') ? 'file://' : message.origin; 73 | 74 | // Handle polling for a ready message 75 | if (message.data === 'cross-storage:poll') { 76 | return window.parent.postMessage('cross-storage:ready', message.origin); 77 | } 78 | 79 | // Ignore the ready message when viewing the hub directly 80 | if (message.data === 'cross-storage:ready') return; 81 | 82 | // Check whether message.data is a valid json 83 | try { 84 | request = JSON.parse(message.data); 85 | } catch (err) { 86 | return; 87 | } 88 | 89 | // Check whether request.method is a string 90 | if (!request || typeof request.method !== 'string') { 91 | return; 92 | } 93 | 94 | method = request.method.split('cross-storage:')[1]; 95 | 96 | if (!method) { 97 | return; 98 | } else if (!CrossStorageHub._permitted(origin, method)) { 99 | error = 'Invalid permissions for ' + method; 100 | } else { 101 | try { 102 | result = CrossStorageHub['_' + method](request.params); 103 | } catch (err) { 104 | error = err.message; 105 | } 106 | } 107 | 108 | response = JSON.stringify({ 109 | id: request.id, 110 | error: error, 111 | result: result 112 | }); 113 | 114 | // postMessage requires that the target origin be set to "*" for "file://" 115 | targetOrigin = (origin === 'file://') ? '*' : origin; 116 | 117 | window.parent.postMessage(response, targetOrigin); 118 | }; 119 | 120 | /** 121 | * Returns a boolean indicating whether or not the requested method is 122 | * permitted for the given origin. The argument passed to method is expected 123 | * to be one of 'get', 'set', 'del' or 'getKeys'. 124 | * 125 | * @param {string} origin The origin for which to determine permissions 126 | * @param {string} method Requested action 127 | * @returns {bool} Whether or not the request is permitted 128 | */ 129 | CrossStorageHub._permitted = function(origin, method) { 130 | var available, i, entry, match; 131 | 132 | available = ['get', 'set', 'del', 'clear', 'getKeys']; 133 | if (!CrossStorageHub._inArray(method, available)) { 134 | return false; 135 | } 136 | 137 | for (i = 0; i < CrossStorageHub._permissions.length; i++) { 138 | entry = CrossStorageHub._permissions[i]; 139 | if (!(entry.origin instanceof RegExp) || !(entry.allow instanceof Array)) { 140 | continue; 141 | } 142 | 143 | match = entry.origin.test(origin); 144 | if (match && CrossStorageHub._inArray(method, entry.allow)) { 145 | return true; 146 | } 147 | } 148 | 149 | return false; 150 | }; 151 | 152 | /** 153 | * Sets a key to the specified value. 154 | * 155 | * @param {object} params An object with key and value 156 | */ 157 | CrossStorageHub._set = function(params) { 158 | window.localStorage.setItem(params.key, params.value); 159 | }; 160 | 161 | /** 162 | * Accepts an object with an array of keys for which to retrieve their values. 163 | * Returns a single value if only one key was supplied, otherwise it returns 164 | * an array. Any keys not set result in a null element in the resulting array. 165 | * 166 | * @param {object} params An object with an array of keys 167 | * @returns {*|*[]} Either a single value, or an array 168 | */ 169 | CrossStorageHub._get = function(params) { 170 | var storage, result, i, value; 171 | 172 | storage = window.localStorage; 173 | result = []; 174 | 175 | for (i = 0; i < params.keys.length; i++) { 176 | try { 177 | value = storage.getItem(params.keys[i]); 178 | } catch (e) { 179 | value = null; 180 | } 181 | 182 | result.push(value); 183 | } 184 | 185 | return (result.length > 1) ? result : result[0]; 186 | }; 187 | 188 | /** 189 | * Deletes all keys specified in the array found at params.keys. 190 | * 191 | * @param {object} params An object with an array of keys 192 | */ 193 | CrossStorageHub._del = function(params) { 194 | for (var i = 0; i < params.keys.length; i++) { 195 | window.localStorage.removeItem(params.keys[i]); 196 | } 197 | }; 198 | 199 | /** 200 | * Clears localStorage. 201 | */ 202 | CrossStorageHub._clear = function() { 203 | window.localStorage.clear(); 204 | }; 205 | 206 | /** 207 | * Returns an array of all keys stored in localStorage. 208 | * 209 | * @returns {string[]} The array of keys 210 | */ 211 | CrossStorageHub._getKeys = function(params) { 212 | var i, length, keys; 213 | 214 | keys = []; 215 | length = window.localStorage.length; 216 | 217 | for (i = 0; i < length; i++) { 218 | keys.push(window.localStorage.key(i)); 219 | } 220 | 221 | return keys; 222 | }; 223 | 224 | /** 225 | * Returns whether or not a value is present in the array. Consists of an 226 | * alternative to extending the array prototype for indexOf, since it's 227 | * unavailable for IE8. 228 | * 229 | * @param {*} value The value to find 230 | * @parma {[]*} array The array in which to search 231 | * @returns {bool} Whether or not the value was found 232 | */ 233 | CrossStorageHub._inArray = function(value, array) { 234 | for (var i = 0; i < array.length; i++) { 235 | if (value === array[i]) return true; 236 | } 237 | 238 | return false; 239 | }; 240 | 241 | /** 242 | * A cross-browser version of Date.now compatible with IE8 that avoids 243 | * modifying the Date object. 244 | * 245 | * @return {int} The current timestamp in milliseconds 246 | */ 247 | CrossStorageHub._now = function() { 248 | if (typeof Date.now === 'function') { 249 | return Date.now(); 250 | } 251 | 252 | return new Date().getTime(); 253 | }; 254 | 255 | /** 256 | * Export for various environments. 257 | */ 258 | if (typeof module !== 'undefined' && module.exports) { 259 | module.exports = CrossStorageHub; 260 | } else if (typeof exports !== 'undefined') { 261 | exports.CrossStorageHub = CrossStorageHub; 262 | } else if (typeof define === 'function' && define.amd) { 263 | define([], function() { 264 | return CrossStorageHub; 265 | }); 266 | } else { 267 | root.CrossStorageHub = CrossStorageHub; 268 | } 269 | }(this)); 270 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CrossStorageClient: require('./client.js'), 3 | CrossStorageHub: require('./hub.js') 4 | }; 5 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/cross-storage/9db1cef7f8903baad72236f392701c4b71a24eba/media/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cross-storage", 3 | "version": "1.0.0", 4 | "description": "Cross domain local storage", 5 | "keywords": [ 6 | "local", 7 | "storage", 8 | "cross", 9 | "domain" 10 | ], 11 | "main": "lib/index.js", 12 | "author": "Daniel St. Jules ", 13 | "license": "Apache-2.0", 14 | "homepage": "https://github.com/zendesk/cross-storage", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/zendesk/cross-storage.git" 18 | }, 19 | "devDependencies": { 20 | "connect": "^2.25.7", 21 | "expect.js": "^0.3.1", 22 | "gulp": "*", 23 | "gulp-jshint": "^1.8.4", 24 | "gulp-rename": "^1.2.0", 25 | "gulp-uglify": "^0.3.1", 26 | "gulp-header": "^1.1.1", 27 | "gulp-rimraf": "^0.1.0", 28 | "serve-static": "^1.5.3", 29 | "zuul": "~3.10.1" 30 | }, 31 | "scripts": { 32 | "test": "./node_modules/.bin/zuul --local 8080 --ui mocha-bdd -- test/test.js", 33 | "saucetest": "./node_modules/.bin/zuul --ui mocha-bdd -- test/test.js" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/es6-promise.auto.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){I=t}function r(t){J=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof H?function(){H(a)}:c()}function s(){var t=0,e=new V(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /test/hub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /test/invalidOriginHub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cross Storage Hub 4 | 5 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'); 2 | var serveStatic = require('serve-static'); 3 | 4 | connect().use(serveStatic(__dirname + '/..')).listen(process.env.ZUUL_PORT); 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | // Note: IE8 requires that catch be referenced as ['catch'] on a promise 3 | 4 | describe('CrossStorageClient', function() { 5 | // Mocha detects the frame id as being leaked in IE 6 | var userAgent = window.navigator.userAgent; 7 | var ieDetected = (userAgent.indexOf('MSIE ') !== false || 8 | !!navigator.userAgent.match(/Trident.*rv\:11\./)); 9 | 10 | if (global.mocha && ieDetected) { 11 | global.mocha.globals(['CrossStorageClient-*']); 12 | } 13 | 14 | var origin, url, storage; 15 | 16 | this.timeout(15000); 17 | origin = CrossStorageClient._getOrigin(window.location.href); 18 | url = origin + '/test/hub.html'; 19 | 20 | // Create initial client 21 | before(function(done) { 22 | var invoked = false; 23 | var next = function(msg) { 24 | if (msg.data !== 'cross-storage:ready' || invoked) return; 25 | invoked = true; 26 | done(); 27 | }; 28 | 29 | if (window.addEventListener) { 30 | window.addEventListener('message', next, false); 31 | } else { 32 | window.attachEvent('onmessage', next); 33 | } 34 | 35 | storage = new CrossStorageClient(url, {timeout: 10000}); 36 | }); 37 | 38 | // Cleanup old iframes 39 | afterEach(function() { 40 | var iframes = document.getElementsByTagName('iframe'); 41 | for (var i = 0; i < iframes.length; i++) { 42 | if (iframes[i].src !== url) { 43 | iframes[i].parentNode.removeChild(iframes[i]); 44 | } 45 | } 46 | }); 47 | 48 | var setGet = function(key, value) { 49 | return function() { 50 | return storage.set(key, value).then(function() { 51 | return storage.get(key); 52 | }); 53 | }; 54 | }; 55 | 56 | // Used to delete keys before each test 57 | var cleanup = function(fn) { 58 | storage.onConnect().then(function() { 59 | return storage.del('key1', 'key2'); 60 | }) 61 | .then(fn) 62 | ['catch'](fn); 63 | }; 64 | 65 | describe('Constructor', function() { 66 | it('parses the passed url and stores its origin', function() { 67 | expect(storage._origin).to.be(origin); 68 | }); 69 | 70 | it("uses window.location's origin if passed a relative path", function() { 71 | var storage, origin; 72 | storage = new CrossStorageClient('hub.html'); 73 | origin = window.location.protocol + '//' + window.location.host; 74 | origin = origin.replace(/:80$|:443$/, ''); 75 | 76 | expect(storage._origin).to.be(origin); 77 | }); 78 | 79 | it('sets _timeout to opts.timeout, if provided', function() { 80 | expect(storage._timeout).to.be(10000); 81 | }); 82 | 83 | it('sets its connected status to false', function() { 84 | var storage = new CrossStorageClient(url); 85 | expect(storage._connected).to.be(false); 86 | }); 87 | 88 | it('initializes _requests as an empty object', function() { 89 | var storage = new CrossStorageClient(url); 90 | expect(storage._requests).to.eql({}); 91 | }); 92 | 93 | it('creates a hidden iframe', function() { 94 | var frame = document.getElementsByTagName('iframe')[0]; 95 | expect(frame.style.display).to.be('none'); 96 | expect(frame.style.position).to.be('absolute'); 97 | expect(frame.style.top).to.be('-999px'); 98 | expect(frame.style.left).to.be('-999px'); 99 | }); 100 | 101 | it('sets the iframe src to the hub url', function() { 102 | var frame = document.getElementsByTagName('iframe')[0]; 103 | expect(frame.src).to.be(url); 104 | }); 105 | 106 | it('sets the frame id to _frameId', function() { 107 | var frame = document.getElementById(storage._frameId); 108 | expect(frame).not.to.be(null); 109 | }); 110 | 111 | it('stores the frame context window in _hub', function() { 112 | // constructor.name isn't cross browser, and the window function name 113 | // varies between browsers (WindowConstructor, Window, etc) 114 | expect(storage._hub).to.not.be(null); 115 | }); 116 | }); 117 | 118 | describe('onConnect', function() { 119 | beforeEach(function(done) { 120 | cleanup(done); 121 | }); 122 | 123 | it('returns a promise that is resolved when connected', function(done) { 124 | storage.onConnect().then(done); 125 | }); 126 | 127 | it('rejects if no connection could be established', function(done) { 128 | var storage = new CrossStorageClient('http://localhost:9999'); 129 | 130 | storage.onConnect()['catch'](function(err) { 131 | expect(err.message).to.be('CrossStorageClient could not connect'); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('can be used multiple times prior to connection', function(done) { 137 | var storage, count, incrOnConnect, i; 138 | 139 | storage = new CrossStorageClient(url); 140 | count = 0; 141 | incrOnConnect = function() { 142 | storage.onConnect().then(function() { 143 | count++; 144 | }); 145 | }; 146 | 147 | for (i = 0; i < 5; i++) { 148 | incrOnConnect(); 149 | } 150 | 151 | storage.onConnect().then(function() { 152 | expect(count).to.be(5); 153 | done(); 154 | }); 155 | }); 156 | }); 157 | 158 | describe('close', function() { 159 | var storage; 160 | 161 | before(function(done) { 162 | storage = new CrossStorageClient(url); 163 | storage.onConnect().then(function() { 164 | storage.close(); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('sets _connected to false', function() { 170 | expect(storage._connected).to.be(false); 171 | }); 172 | 173 | it('deletes the iframe', function() { 174 | var frame = document.getElementById(storage._frameId); 175 | expect(frame).to.be(null); 176 | }); 177 | }); 178 | 179 | it('fails to make any requests not within its permissions', function(done) { 180 | var url = origin + '/test/getOnlyHub.html'; 181 | var storage = new CrossStorageClient(url, {timeout: 50000}); 182 | 183 | storage.onConnect().then(function() { 184 | return storage.set('key1', 'new'); 185 | })['catch'](function(err) { 186 | expect(err.message).to.be('Invalid permissions for set'); 187 | done(); 188 | }); 189 | }); 190 | 191 | it('fails to make any requests if not of an allowed origin', function(done) { 192 | var url = origin + '/test/invalidOriginHub.html'; 193 | var storage = new CrossStorageClient(url, {timeout: 50000}); 194 | 195 | storage.onConnect().then(function() { 196 | return storage.set('key1', 'new'); 197 | })['catch'](function(err) { 198 | expect(err.message).to.be('Invalid permissions for set'); 199 | done(); 200 | }); 201 | }); 202 | 203 | it('fails to make any requests if the client has closed', function(done) { 204 | var storage = new CrossStorageClient(url, {timeout: 50000}); 205 | 206 | storage.onConnect().then(function() { 207 | storage.close(); 208 | return storage.set('key1', 'new'); 209 | })['catch'](function(err) { 210 | expect(err.message).to.be('CrossStorageClient has closed'); 211 | done(); 212 | }); 213 | }); 214 | 215 | describe('given sufficient permissions', function() { 216 | beforeEach(function(done) { 217 | cleanup(done); 218 | }); 219 | 220 | it('returns null when calling get on a non-existent key', function(done) { 221 | storage.onConnect().then(function() { 222 | return storage.get('key1'); 223 | }).then(function(res) { 224 | expect(res).to.be(null); 225 | done(); 226 | })['catch'](done); 227 | }); 228 | 229 | it('can set a key to the specified value', function(done) { 230 | var key = 'key1'; 231 | var value = 'foo'; 232 | 233 | storage.onConnect() 234 | .then(setGet(key, value)) 235 | .then(function(res) { 236 | expect(res).to.eql(value); 237 | done(); 238 | })['catch'](done); 239 | }); 240 | 241 | it('can set JSON objects as the value', function(done) { 242 | var key = 'key1'; 243 | var str = JSON.stringify({foo: 'bar'}); 244 | 245 | storage.onConnect() 246 | .then(setGet(key, str)) 247 | .then(function(res) { 248 | expect(res).to.eql(str); 249 | done(); 250 | })['catch'](done); 251 | }); 252 | 253 | it('can overwrite existing values', function(done) { 254 | var key = 'key1'; 255 | var value = 'new'; 256 | 257 | storage.onConnect().then(function() { 258 | return storage.set(key, 'old'); 259 | }) 260 | .then(setGet(key, value)) 261 | .then(function(res) { 262 | expect(res).to.eql(value); 263 | done(); 264 | })['catch'](done); 265 | }); 266 | 267 | it('returns an array of values if get is passed multiple keys', function(done) { 268 | var keys = ['key1', 'key2']; 269 | var values = ['foo', 'bar']; 270 | 271 | storage.onConnect() 272 | .then(setGet(keys[0], values[0])) 273 | .then(setGet(keys[1], values[1])) 274 | .then(function() { 275 | return storage.get(keys[0], keys[1]); 276 | }) 277 | .then(function(res) { 278 | expect(res).to.eql([values[0], values[1]]); 279 | done(); 280 | })['catch'](done); 281 | }); 282 | 283 | it('can delete multiple keys', function(done) { 284 | var keys = ['key1', 'key2']; 285 | var values = ['foo', 'bar']; 286 | 287 | storage.onConnect() 288 | .then(setGet(keys[0], values[0])) 289 | .then(setGet(keys[1], values[1])) 290 | .then(function() { 291 | return storage.del(keys[0], keys[1]); 292 | }).then(function() { 293 | return storage.get(keys[0], keys[1]); 294 | }) 295 | .then(function(res) { 296 | expect(res).to.eql([null, null]); 297 | done(); 298 | })['catch'](done); 299 | }); 300 | 301 | it('can clear all entries', function (done) { 302 | var keys = ['key1', 'key2']; 303 | var values = ['foo', 'bar']; 304 | 305 | storage.onConnect() 306 | .then(setGet(keys[0], values[0])) 307 | .then(setGet(keys[1], values[1])) 308 | .then(function() { 309 | return storage.clear(); 310 | }).then(function() { 311 | return storage.get(keys[0], keys[1]); 312 | }) 313 | .then(function(res) { 314 | expect(res).to.eql([null, null]); 315 | done(); 316 | })['catch'](done); 317 | }); 318 | 319 | it('can retrieve all keys using getKeys', function(done) { 320 | var keys = ['key1', 'key2']; 321 | var values = ['foo', 'bar']; 322 | 323 | storage.onConnect() 324 | .then(setGet(keys[0], values[0])) 325 | .then(setGet(keys[1], values[1])) 326 | .then(function() { 327 | return storage.getKeys(); 328 | }) 329 | .then(function(res) { 330 | // key order varies in some browsers 331 | expect(res).to.have.length(2); 332 | expect(res).to.contain(keys[0], keys[1]); 333 | done(); 334 | })['catch'](done); 335 | }); 336 | }); 337 | }); 338 | --------------------------------------------------------------------------------