From ceae5d9b68a2010f78615100dd565fbd22747712 Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Tue, 30 May 2017 19:38:29 +0100 Subject: [PATCH] Extracted sync code --- Gruntfile.js | 282 +- README.md | 154 +- example/index.html | 63 + fh-sync-js.d.ts | 8 +- libs/generated/crypto.js | 1387 +------ npm-shrinkwrap.json | 4 +- package.json | 41 +- src/index.js | 5 + src/modules/ajax.js | 408 --- src/sync-client.js | 1255 +++++++ test/browser/fh-sync-latest-require.js | 4641 ++++++++++++++++++++++++ test/browser/suite.js | 6 - test/tests/test_sync_manage.js | 7 +- test/tests/test_sync_offline.js | 8 +- test/tests/test_sync_online.js | 53 +- 15 files changed, 6002 insertions(+), 2320 deletions(-) create mode 100644 example/index.html create mode 100644 src/index.js delete mode 100644 src/modules/ajax.js create mode 100755 src/sync-client.js create mode 100644 test/browser/fh-sync-latest-require.js diff --git a/Gruntfile.js b/Gruntfile.js index 3e48b90..0ba459f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,16 +10,7 @@ module.exports = function(grunt) { pkg: pkg, meta: {}, jshint: { - all: ['src/modules/**/*.js', - '!src/modules/ajax.js', - 'src/appforms/src/core/*.js', - 'src/appforms/src/backbone/*.js', - '!src/appforms/src/core/000*.js', - '!src/appforms/src/core/060*.js', - '!src/appforms/src/core/999*.js', - '!src/appforms/src/backbone/000*.js', - '!src/appforms/src/backbone/001*.js', - '!src/appforms/src/backbone/999*.js'], + all: ['src/**/*.js'], options: { curly: true, eqeqeq: true, @@ -41,17 +32,6 @@ module.exports = function(grunt) { ], dest: "libs/generated/lawnchair.js" }, - lawnchair_titanium: { - src: [ - "libs/generated/lawnchair.js", - "libs/lawnchair/lawnchairTitanium.js", - ], - dest: "libs/generated/lawnchair.js" - }, - titanium_globals : { - src : ["src/modules/titanium/ti.js", "dist/feedhenry-titanium.js"], - dest : "dist/feedhenry-titanium.js" - }, crypto: { src:[ "libs/cryptojs/cryptojs-core.js", @@ -66,30 +46,6 @@ module.exports = function(grunt) { "libs/cryptojs/cryptojs-sha3.js" ], dest: "libs/generated/crypto.js" - }, - forms_core: { - "src": "src/appforms/src/core/*.js", - "dest": "libs/generated/appForms/appForms-core.js" - }, - forms_core_no_v2: { - "src": ["src/appforms/src/core/*.js", "!src/appforms/src/core/000-api-v2.js"], - "dest": "libs/generated/appForms/appForms-core-no-v2.js" - }, - forms_backbone: { - "src": ["src/appforms/src/backbone/*.js", "!src/appforms/src/backbone/000-closureStartRequireJS.js", "!src/appforms/src/backbone/999-closureEndRequireJS.js", "!src/appforms/src/backbone/templates.js"], - "dest": "dist/appForms-backbone.js" - }, - forms_backboneRequireJS: { - "src": ["src/appforms/src/backbone/*.js", "!src/appforms/src/backbone/000-closureStart.js", "!src/appforms/src/backbone/999-closureEnd.js", "!src/appforms/src/backbone/templates.js"], - "dest": "libs/generated/appForms/appForms-backboneRequireJS.js" - }, - forms_sdk :{ - "src": ["dist/feedhenry.js", "libs/generated/appForms/appForms-core.js"], - "dest": "dist/feedhenry-forms.js" - }, - forms_appFormsTest: { - "src": ["dist/feedhenry.js"], - "dest": "src/appforms/tests/feedhenry.js" } }, 'mocha_phantomjs': { @@ -124,76 +80,22 @@ module.exports = function(grunt) { // This browserify build be used by users of the module. It contains a // UMD (universal module definition) and can be used via an AMD module // loader like RequireJS or by simply placing a script tag in the page, - // which registers feedhenry as a global var (the module itself registers as $fh as well). + // which registers fhsync as a global var (the module itself registers as $fh.sync as well). dist:{ //shim is defined inside package.json - src:['src/feedhenry.js'], - dest: 'dist/feedhenry.js', + src:['src/index.js'], + dest: 'dist/fh-sync.js', options: { - standalone: 'feedhenry', - transform: [function(file){ - var data = ''; - - function write (buf) { data += buf } - function end () { - var t = data; - if(file.indexOf("constants.js") >= 0){ - var version = pkg.version; - console.log("found current version = " + version); - if(process.env.TRAVIS_BUILD_NUMBER){ - console.log("found BUILD_NUMBER in process.env " + process.env.TRAVIS_BUILD_NUMBER); - version = version + '-' + process.env.TRAVIS_BUILD_NUMBER; - } - console.log("Version to inject is " + version); - t = data.replace("BUILD_VERSION", version); - } - this.queue(t); - this.queue(null); - } - return through(write, end); - }] + standalone: 'fhsync' } }, - dist_titanium:{ - //shim is defined inside package.json - src:['src/feedhenry.js'], - dest: 'dist/feedhenry-titanium.js', - options: { - standalone: 'feedhenry', - transform: [function(file){ - var data = ''; - - function write (buf) { data += buf } - function end () { - var t = data; - if(file.indexOf("constants.js") >= 0){ - var version = pkg.version; - console.log("found current version = " + version); - if(process.env.TRAVIS_BUILD_NUMBER){ - console.log("found BUILD_NUMBER in process.env " + process.env.TRAVIS_BUILD_NUMBER); - version = version + '-' + process.env.TRAVIS_BUILD_NUMBER; - } - console.log("Version to inject is " + version); - t = data.replace("BUILD_VERSION", version); - } - this.queue(t); - this.queue(null); - } - return through(write, end); - }], - alias: ['./src/modules/titanium/cookies.js:./cookies', - './src/modules/titanium/appProps.js:./appProps', - './src/modules/titanium/appProps.js:./modules/appProps' - ] - } - }, - // This browserify build can be required by other browserify modules that + // This browserify build can be required by other browserify that // have been created with an --external parameter. require: { - src:['src/feedhenry.js'], - dest: 'test/browser/feedhenry-latest-require.js', + src:['src/index.js'], + dest: 'test/browser/fh-sync-latest-require.js', options: { - alias:['./src/feedhenry.js'] + alias:['./src/sync-client.js'] } }, // These are the browserified tests. We need to browserify the tests to be @@ -205,46 +107,11 @@ module.exports = function(grunt) { src: [ './test/browser/suite.js' ], dest: './test/browser/browserified_tests.js', options: { - external: [ './src/feedhenry.js' ], - ignore: ['../../src-cov/modules/ajax', '../../src-cov/modules/events', '../../src-cov/modules/queryMap', '../../src-cov/modules/sync-cli', '../../src-cov/feedhenry'], + external: [ './src/index.js' ], // Embed source map for tests debug: true } }, - require_cov: { - src:['src-cov/feedhenry.js'], - dest: 'test/browser/feedhenry-latest-require.js', - options: { - alias:['./src-cov/feedhenry.js'] - } - }, - test_cov: { - src: [ './test/browser/suite.js' ], - dest: './test/browser/browserified_tests.js', - options: { - external: [ './src-cov/feedhenry.js' ], - // Embed source map for tests - debug: true, - add: { - "LIB_COV": 1 - } - } - } - }, - replace: { - forms_templates: { - src: ["src/appforms/src/backbone/templates.js"], - dest: "src/appforms/src/backbone/040-view00Templates.js", - options: { - processTemplates: false - }, - replacements: [{ - from: '************TEMPLATES***************', // string replacement - to: function(){ - return grunt.file.read("src/appforms/src/backbone/040-view00Templates.html", {encoding: 'utf8'}).replace(/(\r\n|\n|\r)/gm,""); - } - }] - } }, watch: { browserify: { @@ -258,60 +125,12 @@ module.exports = function(grunt) { uglify: { dist: { "files": { - 'dist/feedhenry.min.js': ['dist/feedhenry.js'], - 'dist/feedhenry-forms.min.js': ['dist/feedhenry-forms.js'], - 'dist/feedhenry-titanium.min.js': ['dist/feedhenry-titanium.js'] + 'dist/fh-sync.min.js': ['dist/fh-sync.js'], } } }, - zip: { - project: { - router: function(filepath) { - grunt.log.writeln(filepath); - var filename = path.basename(filepath); - return 'feedhenry-js-sdk/' + filename; - }, - dest: 'dist/fh-starter-project-latest.zip', - src: ['src/index.html', 'src/fhconfig.json', 'dist/feedhenry.min.js'] - }, - sdk: { - router: function(filepath) { - grunt.log.writeln(filepath); - var filename = path.basename(filepath); - return 'feedhenry-js/' + filename; - }, - dest: 'dist/feedhenry-js.zip', - src:['dist/feedhenry.js', 'dist/feedhenry-forms.js', 'dist/feedhenry.min.js', 'dist/feedhenry-forms.min.js', 'dist/appForms-backbone.js'] - }, - titanium: { - router: function(filepath) { - grunt.log.writeln(filepath); - var filename = path.basename(filepath); - return 'feedhenry-titanium/' + filename; - }, - dest: 'dist/feedhenry-titanium.zip', - src:['dist/feedhenry-titanium.js', 'dist/feedhenry-titanium.min.js'] - } - }, - shell: { - jscov: { - //NOTE: install node-jscoverage first from here: https://github.com/visionmedia/node-jscoverage - command: 'jscoverage src/ src-cov/ --exclude=appforms', - options: { - stdout: true - } - }, - htmlcov: { - //NOTE: install jsoncov2htmlcov first from here: https://github.com/plasticine/json2htmlcov - command: 'json2htmlcov rep/coverage.json > rep/coverage.html', - options: { - stdout: true - } - } - } }); - grunt.loadNpmTasks('grunt-zip'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); @@ -323,86 +142,11 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-text-replace'); - var spawns = []; - grunt.registerTask('start-local-servers', function () { - var done = this.async(); - var spawn = require('child_process').spawn; - - var spawnTestCloudServer = function (port, script, cb) { - grunt.log.writeln('Spawning server on port ' + port + ' in cwd ' + __dirname + ' using file ' + __dirname + '/' + script); - var env = {}; - env.FH_PORT = port; - var server = spawn('node', [__dirname + './bin/' + script], { - cwd: __dirname, - env: env - }).on('exit', function (code) { - grunt.log.writeln('Exiting server on port ' + port + ' with exit code ' + code); - }); - server.stdout.on('data', function (data) { - grunt.log.writeln('Spawned Server port ' + port + ' stdout:' + data); - if(data.toString("utf8").indexOf("started") !== -1){ - cb(null, null); - } - }); - server.stderr.on('data', function (data) { - grunt.log.writeln('Spawned Server port ' + port + ' stderr:' + data); - if(data.toString("utf8").indexOf("Error:") !== -1){ - cb(data.toString("utf8"), null); - } - }); - grunt.log.writeln('Spawned server on port ' + port); - spawns.push(server); - }; - - var servers = [{port: 8100, file:"bin/appinit.js"}, {port: 8101, file:"bin/appcloud.js"}]; - async.map(servers, function(conf, cb){ - spawnTestCloudServer(conf.port, conf.file, cb); - }, function(err){ - if(err) { - grunt.log.writeln("Failed to start server. Error: " + err); - return done(false); - } - return done(); - }); - - }); - - var stopLocalServers = function(){ - spawns.forEach(function (server) { - grunt.log.writeln("Killing process " + server.pid); - server.kill(); - }); - } - - process.on('exit', function() { - console.log('killing spawned servers if there are any'); - stopLocalServers(); - }); - - grunt.registerTask('stop-local-servers', function(){ - stopLocalServers(); - }); - - //use this task for local development. Load example/index.html file in the browser after server started. - //can run grunt watch as well in another terminal to auto generate the combined js file - grunt.registerTask('local', ['start-local-servers', 'connect:server:keepalive']); - //run tests in phatomjs grunt.registerTask('test', ['jshint:all', 'browserify:dist', 'browserify:require', 'browserify:test', 'connect:server', 'mocha_phantomjs:test']); - grunt.registerTask('concat-forms-backbone', ['jshint', 'replace:forms_templates', 'concat:forms_backbone', 'concat:forms_backboneRequireJS']); - - grunt.registerTask('concat-core-sdk', ['jshint', 'concat:lawnchair', 'concat:crypto', 'browserify:dist', 'concat:forms_core', 'concat:forms_sdk','concat:forms_core_no_v2', 'concat-forms-backbone']); - - - grunt.registerTask('concat-titanium', ['concat:lawnchair', 'concat:lawnchair_titanium', 'concat:crypto']); - - // We need to ensure that the Titanium globals (definition of window, document, navigator) are at the very top of the file - grunt.registerTask('concat-titanium-globals', ['concat:titanium_globals']); - - grunt.registerTask('titanium', 'concat-titanium browserify:dist_titanium concat-titanium-globals'); + grunt.registerTask('concat-core-sdk', ['jshint', 'concat:lawnchair', 'concat:crypto', 'browserify:dist']); - grunt.registerTask('coverage', ['shell:jscov', 'browserify:require_cov', 'browserify:test_cov', 'connect:server', 'mocha_phantomjs:test_coverage', 'shell:htmlcov']); - grunt.registerTask('default', 'jshint concat-core-sdk concat:forms_appFormsTest test titanium uglify:dist zip'); + grunt.registerTask('default', ['jshint', 'concat-core-sdk', 'test','uglify:dist']); }; diff --git a/README.md b/README.md index a410971..a8b1595 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,8 @@ -FeedHenry JavaScript SDK +FeedHenry Sync Javascript client ======================== -[![Build Status](https://travis-ci.org/feedhenry/fh-js-sdk.svg?branch=use-travis-ci)](https://travis-ci.org/feedhenry/fh-js-sdk) - -The JavaScript SDK allows developers to integrate the FeedHenry Cloud into any web-based solution - desktop websites, mobile websites or a stand-alone JavaScript client. - -The API is provided in the $fh namespace and uses a common convention for most functions, which takes the format: - - $fh.doSomething(parameterObject, successFunction, failureFunction) - -Where parameterObject is an ordinary JavaScript object. The successFunction callback is called if the operation was successful and the failureFunction callback is called if the operation fails. All of these arguments are optional. If there is only one function, it is taken as the success function. - -The successFunction callback is called with one argument, a result object, which is again an ordinary JavaScript object. The failureFunction callback is called with two arguments: an error code string, and an object containing additional error properties (if any). - -Detailed documentation for the JavaScript SDK's API can be found here: http://docs.feedhenry.com/v3/api/app_api - -## Using with Titanium Applications -The FeedHenry Javascript SDK is built to work with Titanium applications. To get started, you need to first include the FeedHenry JS SDK, `feedhenry.js` in your Resources folder, at the root level. You also need to include a `fhconfig.js` file, which sets configuration properties for initializing the JS SDK. This file is a little different than normal, it should take the format of: - - module.exports = { - "appid":"yourAppIdHere", - "appkey":"yourAppKeyHere", - "connectiontag":"yourConnectionTagHere", - "host":"https://YourHost.feedhenry.com", - "projectid":"yourProjectIdHere" - }; - -You can then require the FeedHenry SDK from any JavaScript file in your Titanium project, and use it as normal: - - var $fh = require('feedhenry'); - $fh.act // ...FeedHenry Calls are now possible - -For a practical exampe, see the [FeedHenry Titanium example app](https://github.com/feedhenry-training/fh-titanium-example). - + +[Note] This repository it's currently in development for production version +please refer to fh-js-sdk npm module. ## Building @@ -67,24 +38,6 @@ grunt watch In another terminal window to auto generate the combined js sdk file. -### Testling - -For browser compatibility testing, we use [Testling](https://ci.testling.com/). The project page is here: https://ci.testling.com/feedhenry/fh-js-sdk. - -You can also run testling locally: - -``` -npm install -g testling -testling -``` - -If testling can not find any browser locally, you either need to add browser paths to your PATH environment variable, or use - -``` -testling -u -``` -and copy the url to a browser. - ### Build When finish developing and testing, run @@ -93,102 +46,3 @@ When finish developing and testing, run grunt ``` To generate the release builds. - -### Updating the AppForms Template App - -This process should be changed, however, documenting it here. - -Build the js-sdk as above. -clone the AppForms-Template repo - -``` -git clone git@github.com:feedhenry/AppForms-Template.git -``` - -switch to the feedhenry3 branch - -``` -git checkout feedhenry3 -``` - -copy the appForms-backbone file generated in the js-sdk to the AppForms-Template - -``` -cp fh-js-sdk/dist/appForms-backbone.js AppForms-Template/client/default/lib/appform-backbone.js -``` - -install the node modules in the AppForms-Template - -``` -cd AppForms-Template -npm install -``` - -and run grunt - -``` -grunt -``` - -clone the app forms-project-client template app - -``` -git clone git://github.com/feedhenry/appforms-project-client.git -``` - -copy the resulting lib.min.js to the appforms template - -``` -cp AppForms-Template/dist/client/default/lib.min.js appforms-project-client/www/lib.min.js -``` - -push the updated changes - -``` -git push origin master -``` - -## Releasing - -Our SDK is release on [npmjs](https://www.npmjs.com/package/fh-js-sdk). To do a release follow those steps: - -### Prepare release -* Update ```package.json```, ```npm-shrinkwrap.json```, ``` ``` file with the new version number. -* Update ```CHANGELOG.md``` with some JIRA link -* Do a PR, make sure Travis build passed. Merge. -* Tag the repository with the new version number: - -``` -git tag -s -a {VERSION} -m 'version {VERSION}' // e.g. {VERSION} format is '2.17.0' -``` - -* Push the new release tag on GitHub: - -``` -git push origin {TAG} -``` - -* [Travis build](.travis.yml#L12-L18) will generate a relase for you in release tab. - -### Publish to npmjs -* Login to npm as `feedhenry` or make sure your own login is part of the collaborator list in [fh-js-sdk in npmjs.org](https://www.npmjs.com/package/fh-js-sdk) - -``` -npm login -``` - -* Publish: - -``` -npm publish -``` - -### Generate API doc -The documentation is generated from `fh-js-sdk.d.ts`. -The generated doc is comitted in `doc/` folder and deployed to `gh-pages` so that feedhenry.org and portal documentation can pick it up from a published location. - -To publish doc, run the command: -``` -npm run doc -``` -Go to [gh-pages](https://github.com/feedhenry/fh-js-sdk/tree/gh-pages) and see recent commit for the publication. diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..0564fb1 --- /dev/null +++ b/example/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/fh-sync-js.d.ts b/fh-sync-js.d.ts index 1a0fa1a..b2b829d 100644 --- a/fh-sync-js.d.ts +++ b/fh-sync-js.d.ts @@ -48,7 +48,6 @@ declare module SyncClient { sync_active?: boolean; storage_strategy?: "html5-filesystem" | "dom" | "webkit-sqlite" | "indexed-db"; file_system_quota?: number; - has_custom_sync?: boolean; icloud_backup?: boolean; } @@ -84,7 +83,6 @@ declare module SyncClient { * @param {Boolean} [options.sync_active=true] - Is the background synchronization with the cloud currently active. If this is set to false, the synchronization loop will not start automatically. You need to call startSync to start the synchronization loop. * @param {String} [options.storage_strategy=html5_filesystem] - Storage strategy to use for the underlying client storage framework Lawnchair. Valid values include 'dom', 'html5-filesystem', 'webkit-sqlite', 'indexed-db'. Multiple values can be specified as an array and the first valid storage option will be used. If the app is running on Titanium, the only support value is 'titanium'. * @param {Number} [options.file_system_quota=52428800] - Amount of space to request from the HTML5 filesystem API when running in browser - * @param {Boolean} [options.has_custom_sync=null] - If the app has legacy custom cloud sync function (the app implemented the data CRUDL operations in main.js file in FH V2 apps), it should be set to true. If set to false, the default mbaas sync implementation will be used. When set to null or undefined, a check will be performed to determine which implementation to use. * @param {Boolean} [options.icloud_backup=false] - iOS only. If set to true, the file will be backed by iCloud. */ function init(options: SyncOptions); @@ -120,7 +118,6 @@ declare module SyncClient { * @param {Boolean} [options.sync_active=true] - Is the background synchronization with the cloud currently active. If this is set to false, the synchronization loop will not start automatically. You need to call startSync to start the synchronization loop. * @param {String} [options.storage_strategy=html5_filesystem] - Storage strategy to use for the underlying client storage framework Lawnchair. Valid values include 'dom', 'html5-filesystem', 'webkit-sqlite', 'indexed-db'. Multiple values can be specified as an array and the first valid storage option will be used. If the app is running on Titanium, the only support value is 'titanium'. * @param {Number} [options.file_system_quota=52428800] - Amount of space to request from the HTML5 filesystem API when running in browser - * @param {Boolean} [options.has_custom_sync=null] - If the app has legacy custom cloud sync function (the app implemented the data CRUDL operations in main.js file in FH V2 apps), it should be set to true. If set to false, the default mbaas sync implementation will be used. When set to null or undefined, a check will be performed to determine which implementation to use. * @param {Boolean} [options.icloud_backup=false] - iOS only. If set to true, the file will be backed by iCloud. * @param {Object} query_params * @param {Object} meta_data @@ -314,6 +311,11 @@ declare module SyncClient { * @param {Function} callback */ function clearCache(datasetId: string, callback?: () => void); + + /** + * Sets default cloud call handler for sync. Required to make any sync requests to the cloud + */ + function setCloudHandler(handler: (params: any, success: function, failure: function) ); } } diff --git a/libs/generated/crypto.js b/libs/generated/crypto.js index 2bdd598..4cd3e49 100644 --- a/libs/generated/crypto.js +++ b/libs/generated/crypto.js @@ -711,116 +711,6 @@ var CryptoJS = CryptoJS || (function (Math, undefined) { return C; }(Math)); -/* - CryptoJS v3.1.2 - enc-base64.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function () { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var WordArray = C_lib.WordArray; - var C_enc = C.enc; - - /** - * Base64 encoding strategy. - */ - var Base64 = C_enc.Base64 = { - /** - * Converts a word array to a Base64 string. - * - * @param {WordArray} wordArray The word array. - * - * @return {string} The Base64 string. - * - * @static - * - * @example - * - * var base64String = CryptoJS.enc.Base64.stringify(wordArray); - */ - stringify: function (wordArray) { - // Shortcuts - var words = wordArray.words; - var sigBytes = wordArray.sigBytes; - var map = this._map; - - // Clamp excess bits - wordArray.clamp(); - - // Convert - var base64Chars = []; - for (var i = 0; i < sigBytes; i += 3) { - var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; - var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; - var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; - - var triplet = (byte1 << 16) | (byte2 << 8) | byte3; - - for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { - base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); - } - } - - // Add padding - var paddingChar = map.charAt(64); - if (paddingChar) { - while (base64Chars.length % 4) { - base64Chars.push(paddingChar); - } - } - - return base64Chars.join(''); - }, - - /** - * Converts a Base64 string to a word array. - * - * @param {string} base64Str The Base64 string. - * - * @return {WordArray} The word array. - * - * @static - * - * @example - * - * var wordArray = CryptoJS.enc.Base64.parse(base64String); - */ - parse: function (base64Str) { - // Shortcuts - var base64StrLength = base64Str.length; - var map = this._map; - - // Ignore padding - var paddingChar = map.charAt(64); - if (paddingChar) { - var paddingIndex = base64Str.indexOf(paddingChar); - if (paddingIndex != -1) { - base64StrLength = paddingIndex; - } - } - - // Convert - var words = []; - var nBytes = 0; - for (var i = 0; i < base64StrLength; i++) { - if (i % 4) { - var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2); - var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2); - words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8); - nBytes++; - } - } - - return WordArray.create(words, nBytes); - }, - - _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' - }; -}()); /* CryptoJS v3.1.2 cipher-core @@ -1685,475 +1575,6 @@ CryptoJS.lib.Cipher || (function (undefined) { } }); }()); -/* - CryptoJS v3.1.2 - aes.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function () { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var BlockCipher = C_lib.BlockCipher; - var C_algo = C.algo; - - // Lookup tables - var SBOX = []; - var INV_SBOX = []; - var SUB_MIX_0 = []; - var SUB_MIX_1 = []; - var SUB_MIX_2 = []; - var SUB_MIX_3 = []; - var INV_SUB_MIX_0 = []; - var INV_SUB_MIX_1 = []; - var INV_SUB_MIX_2 = []; - var INV_SUB_MIX_3 = []; - - // Compute lookup tables - (function () { - // Compute double table - var d = []; - for (var i = 0; i < 256; i++) { - if (i < 128) { - d[i] = i << 1; - } else { - d[i] = (i << 1) ^ 0x11b; - } - } - - // Walk GF(2^8) - var x = 0; - var xi = 0; - for (var i = 0; i < 256; i++) { - // Compute sbox - var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4); - sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63; - SBOX[x] = sx; - INV_SBOX[sx] = x; - - // Compute multiplication - var x2 = d[x]; - var x4 = d[x2]; - var x8 = d[x4]; - - // Compute sub bytes, mix columns tables - var t = (d[sx] * 0x101) ^ (sx * 0x1010100); - SUB_MIX_0[x] = (t << 24) | (t >>> 8); - SUB_MIX_1[x] = (t << 16) | (t >>> 16); - SUB_MIX_2[x] = (t << 8) | (t >>> 24); - SUB_MIX_3[x] = t; - - // Compute inv sub bytes, inv mix columns tables - var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); - INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8); - INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16); - INV_SUB_MIX_2[sx] = (t << 8) | (t >>> 24); - INV_SUB_MIX_3[sx] = t; - - // Compute next counter - if (!x) { - x = xi = 1; - } else { - x = x2 ^ d[d[d[x8 ^ x2]]]; - xi ^= d[d[xi]]; - } - } - }()); - - // Precomputed Rcon lookup - var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; - - /** - * AES block cipher algorithm. - */ - var AES = C_algo.AES = BlockCipher.extend({ - _doReset: function () { - // Shortcuts - var key = this._key; - var keyWords = key.words; - var keySize = key.sigBytes / 4; - - // Compute number of rounds - var nRounds = this._nRounds = keySize + 6 - - // Compute number of key schedule rows - var ksRows = (nRounds + 1) * 4; - - // Compute key schedule - var keySchedule = this._keySchedule = []; - for (var ksRow = 0; ksRow < ksRows; ksRow++) { - if (ksRow < keySize) { - keySchedule[ksRow] = keyWords[ksRow]; - } else { - var t = keySchedule[ksRow - 1]; - - if (!(ksRow % keySize)) { - // Rot word - t = (t << 8) | (t >>> 24); - - // Sub word - t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; - - // Mix Rcon - t ^= RCON[(ksRow / keySize) | 0] << 24; - } else if (keySize > 6 && ksRow % keySize == 4) { - // Sub word - t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff]; - } - - keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t; - } - } - - // Compute inv key schedule - var invKeySchedule = this._invKeySchedule = []; - for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) { - var ksRow = ksRows - invKsRow; - - if (invKsRow % 4) { - var t = keySchedule[ksRow]; - } else { - var t = keySchedule[ksRow - 4]; - } - - if (invKsRow < 4 || ksRow <= 4) { - invKeySchedule[invKsRow] = t; - } else { - invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^ - INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]]; - } - } - }, - - encryptBlock: function (M, offset) { - this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX); - }, - - decryptBlock: function (M, offset) { - // Swap 2nd and 4th rows - var t = M[offset + 1]; - M[offset + 1] = M[offset + 3]; - M[offset + 3] = t; - - this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX); - - // Inv swap 2nd and 4th rows - var t = M[offset + 1]; - M[offset + 1] = M[offset + 3]; - M[offset + 3] = t; - }, - - _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) { - // Shortcut - var nRounds = this._nRounds; - - // Get input, add round key - var s0 = M[offset] ^ keySchedule[0]; - var s1 = M[offset + 1] ^ keySchedule[1]; - var s2 = M[offset + 2] ^ keySchedule[2]; - var s3 = M[offset + 3] ^ keySchedule[3]; - - // Key schedule row counter - var ksRow = 4; - - // Rounds - for (var round = 1; round < nRounds; round++) { - // Shift rows, sub bytes, mix columns, add round key - var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++]; - var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++]; - var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++]; - var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++]; - - // Update state - s0 = t0; - s1 = t1; - s2 = t2; - s3 = t3; - } - - // Shift rows, sub bytes, add round key - var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++]; - var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++]; - var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++]; - var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++]; - - // Set output - M[offset] = t0; - M[offset + 1] = t1; - M[offset + 2] = t2; - M[offset + 3] = t3; - }, - - keySize: 256/32 - }); - - /** - * Shortcut functions to the cipher's object interface. - * - * @example - * - * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); - * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); - */ - C.AES = BlockCipher._createHelper(AES); -}()); -/* - CryptoJS v3.1.2 - md5.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function (Math) { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var WordArray = C_lib.WordArray; - var Hasher = C_lib.Hasher; - var C_algo = C.algo; - - // Constants table - var T = []; - - // Compute constants - (function () { - for (var i = 0; i < 64; i++) { - T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0; - } - }()); - - /** - * MD5 hash algorithm. - */ - var MD5 = C_algo.MD5 = Hasher.extend({ - _doReset: function () { - this._hash = new WordArray.init([ - 0x67452301, 0xefcdab89, - 0x98badcfe, 0x10325476 - ]); - }, - - _doProcessBlock: function (M, offset) { - // Swap endian - for (var i = 0; i < 16; i++) { - // Shortcuts - var offset_i = offset + i; - var M_offset_i = M[offset_i]; - - M[offset_i] = ( - (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | - (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) - ); - } - - // Shortcuts - var H = this._hash.words; - - var M_offset_0 = M[offset + 0]; - var M_offset_1 = M[offset + 1]; - var M_offset_2 = M[offset + 2]; - var M_offset_3 = M[offset + 3]; - var M_offset_4 = M[offset + 4]; - var M_offset_5 = M[offset + 5]; - var M_offset_6 = M[offset + 6]; - var M_offset_7 = M[offset + 7]; - var M_offset_8 = M[offset + 8]; - var M_offset_9 = M[offset + 9]; - var M_offset_10 = M[offset + 10]; - var M_offset_11 = M[offset + 11]; - var M_offset_12 = M[offset + 12]; - var M_offset_13 = M[offset + 13]; - var M_offset_14 = M[offset + 14]; - var M_offset_15 = M[offset + 15]; - - // Working varialbes - var a = H[0]; - var b = H[1]; - var c = H[2]; - var d = H[3]; - - // Computation - a = FF(a, b, c, d, M_offset_0, 7, T[0]); - d = FF(d, a, b, c, M_offset_1, 12, T[1]); - c = FF(c, d, a, b, M_offset_2, 17, T[2]); - b = FF(b, c, d, a, M_offset_3, 22, T[3]); - a = FF(a, b, c, d, M_offset_4, 7, T[4]); - d = FF(d, a, b, c, M_offset_5, 12, T[5]); - c = FF(c, d, a, b, M_offset_6, 17, T[6]); - b = FF(b, c, d, a, M_offset_7, 22, T[7]); - a = FF(a, b, c, d, M_offset_8, 7, T[8]); - d = FF(d, a, b, c, M_offset_9, 12, T[9]); - c = FF(c, d, a, b, M_offset_10, 17, T[10]); - b = FF(b, c, d, a, M_offset_11, 22, T[11]); - a = FF(a, b, c, d, M_offset_12, 7, T[12]); - d = FF(d, a, b, c, M_offset_13, 12, T[13]); - c = FF(c, d, a, b, M_offset_14, 17, T[14]); - b = FF(b, c, d, a, M_offset_15, 22, T[15]); - - a = GG(a, b, c, d, M_offset_1, 5, T[16]); - d = GG(d, a, b, c, M_offset_6, 9, T[17]); - c = GG(c, d, a, b, M_offset_11, 14, T[18]); - b = GG(b, c, d, a, M_offset_0, 20, T[19]); - a = GG(a, b, c, d, M_offset_5, 5, T[20]); - d = GG(d, a, b, c, M_offset_10, 9, T[21]); - c = GG(c, d, a, b, M_offset_15, 14, T[22]); - b = GG(b, c, d, a, M_offset_4, 20, T[23]); - a = GG(a, b, c, d, M_offset_9, 5, T[24]); - d = GG(d, a, b, c, M_offset_14, 9, T[25]); - c = GG(c, d, a, b, M_offset_3, 14, T[26]); - b = GG(b, c, d, a, M_offset_8, 20, T[27]); - a = GG(a, b, c, d, M_offset_13, 5, T[28]); - d = GG(d, a, b, c, M_offset_2, 9, T[29]); - c = GG(c, d, a, b, M_offset_7, 14, T[30]); - b = GG(b, c, d, a, M_offset_12, 20, T[31]); - - a = HH(a, b, c, d, M_offset_5, 4, T[32]); - d = HH(d, a, b, c, M_offset_8, 11, T[33]); - c = HH(c, d, a, b, M_offset_11, 16, T[34]); - b = HH(b, c, d, a, M_offset_14, 23, T[35]); - a = HH(a, b, c, d, M_offset_1, 4, T[36]); - d = HH(d, a, b, c, M_offset_4, 11, T[37]); - c = HH(c, d, a, b, M_offset_7, 16, T[38]); - b = HH(b, c, d, a, M_offset_10, 23, T[39]); - a = HH(a, b, c, d, M_offset_13, 4, T[40]); - d = HH(d, a, b, c, M_offset_0, 11, T[41]); - c = HH(c, d, a, b, M_offset_3, 16, T[42]); - b = HH(b, c, d, a, M_offset_6, 23, T[43]); - a = HH(a, b, c, d, M_offset_9, 4, T[44]); - d = HH(d, a, b, c, M_offset_12, 11, T[45]); - c = HH(c, d, a, b, M_offset_15, 16, T[46]); - b = HH(b, c, d, a, M_offset_2, 23, T[47]); - - a = II(a, b, c, d, M_offset_0, 6, T[48]); - d = II(d, a, b, c, M_offset_7, 10, T[49]); - c = II(c, d, a, b, M_offset_14, 15, T[50]); - b = II(b, c, d, a, M_offset_5, 21, T[51]); - a = II(a, b, c, d, M_offset_12, 6, T[52]); - d = II(d, a, b, c, M_offset_3, 10, T[53]); - c = II(c, d, a, b, M_offset_10, 15, T[54]); - b = II(b, c, d, a, M_offset_1, 21, T[55]); - a = II(a, b, c, d, M_offset_8, 6, T[56]); - d = II(d, a, b, c, M_offset_15, 10, T[57]); - c = II(c, d, a, b, M_offset_6, 15, T[58]); - b = II(b, c, d, a, M_offset_13, 21, T[59]); - a = II(a, b, c, d, M_offset_4, 6, T[60]); - d = II(d, a, b, c, M_offset_11, 10, T[61]); - c = II(c, d, a, b, M_offset_2, 15, T[62]); - b = II(b, c, d, a, M_offset_9, 21, T[63]); - - // Intermediate hash value - H[0] = (H[0] + a) | 0; - H[1] = (H[1] + b) | 0; - H[2] = (H[2] + c) | 0; - H[3] = (H[3] + d) | 0; - }, - - _doFinalize: function () { - // Shortcuts - var data = this._data; - var dataWords = data.words; - - var nBitsTotal = this._nDataBytes * 8; - var nBitsLeft = data.sigBytes * 8; - - // Add padding - dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); - - var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); - var nBitsTotalL = nBitsTotal; - dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( - (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | - (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) - ); - dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( - (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | - (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) - ); - - data.sigBytes = (dataWords.length + 1) * 4; - - // Hash final blocks - this._process(); - - // Shortcuts - var hash = this._hash; - var H = hash.words; - - // Swap endian - for (var i = 0; i < 4; i++) { - // Shortcut - var H_i = H[i]; - - H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | - (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); - } - - // Return final computed hash - return hash; - }, - - clone: function () { - var clone = Hasher.clone.call(this); - clone._hash = this._hash.clone(); - - return clone; - } - }); - - function FF(a, b, c, d, x, s, t) { - var n = a + ((b & c) | (~b & d)) + x + t; - return ((n << s) | (n >>> (32 - s))) + b; - } - - function GG(a, b, c, d, x, s, t) { - var n = a + ((b & d) | (c & ~d)) + x + t; - return ((n << s) | (n >>> (32 - s))) + b; - } - - function HH(a, b, c, d, x, s, t) { - var n = a + (b ^ c ^ d) + x + t; - return ((n << s) | (n >>> (32 - s))) + b; - } - - function II(a, b, c, d, x, s, t) { - var n = a + (c ^ (b | ~d)) + x + t; - return ((n << s) | (n >>> (32 - s))) + b; - } - - /** - * Shortcut function to the hasher's object interface. - * - * @param {WordArray|string} message The message to hash. - * - * @return {WordArray} The hash. - * - * @static - * - * @example - * - * var hash = CryptoJS.MD5('message'); - * var hash = CryptoJS.MD5(wordArray); - */ - C.MD5 = Hasher._createHelper(MD5); - - /** - * Shortcut function to the HMAC's object interface. - * - * @param {WordArray|string} message The message to hash. - * @param {WordArray|string} key The secret key. - * - * @return {WordArray} The HMAC. - * - * @static - * - * @example - * - * var hmac = CryptoJS.HmacMD5(message, key); - */ - C.HmacMD5 = Hasher._createHmacHelper(MD5); -}(Math)); /* CryptoJS v3.1.2 sha1.js @@ -2581,810 +2002,4 @@ CryptoJS.lib.Cipher || (function (undefined) { return clone; } }); -}()); -/* - CryptoJS v3.1.2 - sha256.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function (Math) { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var WordArray = C_lib.WordArray; - var Hasher = C_lib.Hasher; - var C_algo = C.algo; - - // Initialization and round constants tables - var H = []; - var K = []; - - // Compute constants - (function () { - function isPrime(n) { - var sqrtN = Math.sqrt(n); - for (var factor = 2; factor <= sqrtN; factor++) { - if (!(n % factor)) { - return false; - } - } - - return true; - } - - function getFractionalBits(n) { - return ((n - (n | 0)) * 0x100000000) | 0; - } - - var n = 2; - var nPrime = 0; - while (nPrime < 64) { - if (isPrime(n)) { - if (nPrime < 8) { - H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); - } - K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); - - nPrime++; - } - - n++; - } - }()); - - // Reusable object - var W = []; - - /** - * SHA-256 hash algorithm. - */ - var SHA256 = C_algo.SHA256 = Hasher.extend({ - _doReset: function () { - this._hash = new WordArray.init(H.slice(0)); - }, - - _doProcessBlock: function (M, offset) { - // Shortcut - var H = this._hash.words; - - // Working variables - var a = H[0]; - var b = H[1]; - var c = H[2]; - var d = H[3]; - var e = H[4]; - var f = H[5]; - var g = H[6]; - var h = H[7]; - - // Computation - for (var i = 0; i < 64; i++) { - if (i < 16) { - W[i] = M[offset + i] | 0; - } else { - var gamma0x = W[i - 15]; - var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ - ((gamma0x << 14) | (gamma0x >>> 18)) ^ - (gamma0x >>> 3); - - var gamma1x = W[i - 2]; - var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ - ((gamma1x << 13) | (gamma1x >>> 19)) ^ - (gamma1x >>> 10); - - W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; - } - - var ch = (e & f) ^ (~e & g); - var maj = (a & b) ^ (a & c) ^ (b & c); - - var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); - var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); - - var t1 = h + sigma1 + ch + K[i] + W[i]; - var t2 = sigma0 + maj; - - h = g; - g = f; - f = e; - e = (d + t1) | 0; - d = c; - c = b; - b = a; - a = (t1 + t2) | 0; - } - - // Intermediate hash value - H[0] = (H[0] + a) | 0; - H[1] = (H[1] + b) | 0; - H[2] = (H[2] + c) | 0; - H[3] = (H[3] + d) | 0; - H[4] = (H[4] + e) | 0; - H[5] = (H[5] + f) | 0; - H[6] = (H[6] + g) | 0; - H[7] = (H[7] + h) | 0; - }, - - _doFinalize: function () { - // Shortcuts - var data = this._data; - var dataWords = data.words; - - var nBitsTotal = this._nDataBytes * 8; - var nBitsLeft = data.sigBytes * 8; - - // Add padding - dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); - dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); - dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; - data.sigBytes = dataWords.length * 4; - - // Hash final blocks - this._process(); - - // Return final computed hash - return this._hash; - }, - - clone: function () { - var clone = Hasher.clone.call(this); - clone._hash = this._hash.clone(); - - return clone; - } - }); - - /** - * Shortcut function to the hasher's object interface. - * - * @param {WordArray|string} message The message to hash. - * - * @return {WordArray} The hash. - * - * @static - * - * @example - * - * var hash = CryptoJS.SHA256('message'); - * var hash = CryptoJS.SHA256(wordArray); - */ - C.SHA256 = Hasher._createHelper(SHA256); - - /** - * Shortcut function to the HMAC's object interface. - * - * @param {WordArray|string} message The message to hash. - * @param {WordArray|string} key The secret key. - * - * @return {WordArray} The HMAC. - * - * @static - * - * @example - * - * var hmac = CryptoJS.HmacSHA256(message, key); - */ - C.HmacSHA256 = Hasher._createHmacHelper(SHA256); -}(Math)); -/* - CryptoJS v3.1.2 - sha512.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function () { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var Hasher = C_lib.Hasher; - var C_x64 = C.x64; - var X64Word = C_x64.Word; - var X64WordArray = C_x64.WordArray; - var C_algo = C.algo; - - function X64Word_create() { - return X64Word.create.apply(X64Word, arguments); - } - - // Constants - var K = [ - X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd), - X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc), - X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019), - X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118), - X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe), - X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2), - X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1), - X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694), - X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3), - X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65), - X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483), - X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5), - X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210), - X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4), - X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725), - X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70), - X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926), - X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df), - X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8), - X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b), - X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001), - X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30), - X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910), - X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8), - X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53), - X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8), - X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb), - X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3), - X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60), - X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec), - X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9), - X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b), - X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207), - X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178), - X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6), - X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b), - X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493), - X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c), - X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a), - X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817) - ]; - - // Reusable objects - var W = []; - (function () { - for (var i = 0; i < 80; i++) { - W[i] = X64Word_create(); - } - }()); - - /** - * SHA-512 hash algorithm. - */ - var SHA512 = C_algo.SHA512 = Hasher.extend({ - _doReset: function () { - this._hash = new X64WordArray.init([ - new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b), - new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1), - new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f), - new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179) - ]); - }, - - _doProcessBlock: function (M, offset) { - // Shortcuts - var H = this._hash.words; - - var H0 = H[0]; - var H1 = H[1]; - var H2 = H[2]; - var H3 = H[3]; - var H4 = H[4]; - var H5 = H[5]; - var H6 = H[6]; - var H7 = H[7]; - - var H0h = H0.high; - var H0l = H0.low; - var H1h = H1.high; - var H1l = H1.low; - var H2h = H2.high; - var H2l = H2.low; - var H3h = H3.high; - var H3l = H3.low; - var H4h = H4.high; - var H4l = H4.low; - var H5h = H5.high; - var H5l = H5.low; - var H6h = H6.high; - var H6l = H6.low; - var H7h = H7.high; - var H7l = H7.low; - - // Working variables - var ah = H0h; - var al = H0l; - var bh = H1h; - var bl = H1l; - var ch = H2h; - var cl = H2l; - var dh = H3h; - var dl = H3l; - var eh = H4h; - var el = H4l; - var fh = H5h; - var fl = H5l; - var gh = H6h; - var gl = H6l; - var hh = H7h; - var hl = H7l; - - // Rounds - for (var i = 0; i < 80; i++) { - // Shortcut - var Wi = W[i]; - - // Extend message - if (i < 16) { - var Wih = Wi.high = M[offset + i * 2] | 0; - var Wil = Wi.low = M[offset + i * 2 + 1] | 0; - } else { - // Gamma0 - var gamma0x = W[i - 15]; - var gamma0xh = gamma0x.high; - var gamma0xl = gamma0x.low; - var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7); - var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25)); - - // Gamma1 - var gamma1x = W[i - 2]; - var gamma1xh = gamma1x.high; - var gamma1xl = gamma1x.low; - var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6); - var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26)); - - // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16] - var Wi7 = W[i - 7]; - var Wi7h = Wi7.high; - var Wi7l = Wi7.low; - - var Wi16 = W[i - 16]; - var Wi16h = Wi16.high; - var Wi16l = Wi16.low; - - var Wil = gamma0l + Wi7l; - var Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0); - var Wil = Wil + gamma1l; - var Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0); - var Wil = Wil + Wi16l; - var Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0); - - Wi.high = Wih; - Wi.low = Wil; - } - - var chh = (eh & fh) ^ (~eh & gh); - var chl = (el & fl) ^ (~el & gl); - var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); - var majl = (al & bl) ^ (al & cl) ^ (bl & cl); - - var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); - var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); - var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9)); - var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9)); - - // t1 = h + sigma1 + ch + K[i] + W[i] - var Ki = K[i]; - var Kih = Ki.high; - var Kil = Ki.low; - - var t1l = hl + sigma1l; - var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); - var t1l = t1l + chl; - var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); - var t1l = t1l + Kil; - var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0); - var t1l = t1l + Wil; - var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0); - - // t2 = sigma0 + maj - var t2l = sigma0l + majl; - var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); - - // Update working variables - hh = gh; - hl = gl; - gh = fh; - gl = fl; - fh = eh; - fl = el; - el = (dl + t1l) | 0; - eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; - dh = ch; - dl = cl; - ch = bh; - cl = bl; - bh = ah; - bl = al; - al = (t1l + t2l) | 0; - ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; - } - - // Intermediate hash value - H0l = H0.low = (H0l + al); - H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0)); - H1l = H1.low = (H1l + bl); - H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0)); - H2l = H2.low = (H2l + cl); - H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0)); - H3l = H3.low = (H3l + dl); - H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0)); - H4l = H4.low = (H4l + el); - H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0)); - H5l = H5.low = (H5l + fl); - H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0)); - H6l = H6.low = (H6l + gl); - H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0)); - H7l = H7.low = (H7l + hl); - H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0)); - }, - - _doFinalize: function () { - // Shortcuts - var data = this._data; - var dataWords = data.words; - - var nBitsTotal = this._nDataBytes * 8; - var nBitsLeft = data.sigBytes * 8; - - // Add padding - dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); - dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000); - dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal; - data.sigBytes = dataWords.length * 4; - - // Hash final blocks - this._process(); - - // Convert hash to 32-bit word array before returning - var hash = this._hash.toX32(); - - // Return final computed hash - return hash; - }, - - clone: function () { - var clone = Hasher.clone.call(this); - clone._hash = this._hash.clone(); - - return clone; - }, - - blockSize: 1024/32 - }); - - /** - * Shortcut function to the hasher's object interface. - * - * @param {WordArray|string} message The message to hash. - * - * @return {WordArray} The hash. - * - * @static - * - * @example - * - * var hash = CryptoJS.SHA512('message'); - * var hash = CryptoJS.SHA512(wordArray); - */ - C.SHA512 = Hasher._createHelper(SHA512); - - /** - * Shortcut function to the HMAC's object interface. - * - * @param {WordArray|string} message The message to hash. - * @param {WordArray|string} key The secret key. - * - * @return {WordArray} The HMAC. - * - * @static - * - * @example - * - * var hmac = CryptoJS.HmacSHA512(message, key); - */ - C.HmacSHA512 = Hasher._createHmacHelper(SHA512); -}()); -/* - CryptoJS v3.1.2 - sha3.js - code.google.com/p/crypto-js - (c) 2009-2013 by Jeff Mott. All rights reserved. - code.google.com/p/crypto-js/wiki/License - */ -(function (Math) { - // Shortcuts - var C = CryptoJS; - var C_lib = C.lib; - var WordArray = C_lib.WordArray; - var Hasher = C_lib.Hasher; - var C_x64 = C.x64; - var X64Word = C_x64.Word; - var C_algo = C.algo; - - // Constants tables - var RHO_OFFSETS = []; - var PI_INDEXES = []; - var ROUND_CONSTANTS = []; - - // Compute Constants - (function () { - // Compute rho offset constants - var x = 1, y = 0; - for (var t = 0; t < 24; t++) { - RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64; - - var newX = y % 5; - var newY = (2 * x + 3 * y) % 5; - x = newX; - y = newY; - } - - // Compute pi index constants - for (var x = 0; x < 5; x++) { - for (var y = 0; y < 5; y++) { - PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5; - } - } - - // Compute round constants - var LFSR = 0x01; - for (var i = 0; i < 24; i++) { - var roundConstantMsw = 0; - var roundConstantLsw = 0; - - for (var j = 0; j < 7; j++) { - if (LFSR & 0x01) { - var bitPosition = (1 << j) - 1; - if (bitPosition < 32) { - roundConstantLsw ^= 1 << bitPosition; - } else /* if (bitPosition >= 32) */ { - roundConstantMsw ^= 1 << (bitPosition - 32); - } - } - - // Compute next LFSR - if (LFSR & 0x80) { - // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1 - LFSR = (LFSR << 1) ^ 0x71; - } else { - LFSR <<= 1; - } - } - - ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw); - } - }()); - - // Reusable objects for temporary values - var T = []; - (function () { - for (var i = 0; i < 25; i++) { - T[i] = X64Word.create(); - } - }()); - - /** - * SHA-3 hash algorithm. - */ - var SHA3 = C_algo.SHA3 = Hasher.extend({ - /** - * Configuration options. - * - * @property {number} outputLength - * The desired number of bits in the output hash. - * Only values permitted are: 224, 256, 384, 512. - * Default: 512 - */ - cfg: Hasher.cfg.extend({ - outputLength: 512 - }), - - _doReset: function () { - var state = this._state = [] - for (var i = 0; i < 25; i++) { - state[i] = new X64Word.init(); - } - - this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32; - }, - - _doProcessBlock: function (M, offset) { - // Shortcuts - var state = this._state; - var nBlockSizeLanes = this.blockSize / 2; - - // Absorb - for (var i = 0; i < nBlockSizeLanes; i++) { - // Shortcuts - var M2i = M[offset + 2 * i]; - var M2i1 = M[offset + 2 * i + 1]; - - // Swap endian - M2i = ( - (((M2i << 8) | (M2i >>> 24)) & 0x00ff00ff) | - (((M2i << 24) | (M2i >>> 8)) & 0xff00ff00) - ); - M2i1 = ( - (((M2i1 << 8) | (M2i1 >>> 24)) & 0x00ff00ff) | - (((M2i1 << 24) | (M2i1 >>> 8)) & 0xff00ff00) - ); - - // Absorb message into state - var lane = state[i]; - lane.high ^= M2i1; - lane.low ^= M2i; - } - - // Rounds - for (var round = 0; round < 24; round++) { - // Theta - for (var x = 0; x < 5; x++) { - // Mix column lanes - var tMsw = 0, tLsw = 0; - for (var y = 0; y < 5; y++) { - var lane = state[x + 5 * y]; - tMsw ^= lane.high; - tLsw ^= lane.low; - } - - // Temporary values - var Tx = T[x]; - Tx.high = tMsw; - Tx.low = tLsw; - } - for (var x = 0; x < 5; x++) { - // Shortcuts - var Tx4 = T[(x + 4) % 5]; - var Tx1 = T[(x + 1) % 5]; - var Tx1Msw = Tx1.high; - var Tx1Lsw = Tx1.low; - - // Mix surrounding columns - var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31)); - var tLsw = Tx4.low ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31)); - for (var y = 0; y < 5; y++) { - var lane = state[x + 5 * y]; - lane.high ^= tMsw; - lane.low ^= tLsw; - } - } - - // Rho Pi - for (var laneIndex = 1; laneIndex < 25; laneIndex++) { - // Shortcuts - var lane = state[laneIndex]; - var laneMsw = lane.high; - var laneLsw = lane.low; - var rhoOffset = RHO_OFFSETS[laneIndex]; - - // Rotate lanes - if (rhoOffset < 32) { - var tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset)); - var tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset)); - } else /* if (rhoOffset >= 32) */ { - var tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset)); - var tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset)); - } - - // Transpose lanes - var TPiLane = T[PI_INDEXES[laneIndex]]; - TPiLane.high = tMsw; - TPiLane.low = tLsw; - } - - // Rho pi at x = y = 0 - var T0 = T[0]; - var state0 = state[0]; - T0.high = state0.high; - T0.low = state0.low; - - // Chi - for (var x = 0; x < 5; x++) { - for (var y = 0; y < 5; y++) { - // Shortcuts - var laneIndex = x + 5 * y; - var lane = state[laneIndex]; - var TLane = T[laneIndex]; - var Tx1Lane = T[((x + 1) % 5) + 5 * y]; - var Tx2Lane = T[((x + 2) % 5) + 5 * y]; - - // Mix rows - lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high); - lane.low = TLane.low ^ (~Tx1Lane.low & Tx2Lane.low); - } - } - - // Iota - var lane = state[0]; - var roundConstant = ROUND_CONSTANTS[round]; - lane.high ^= roundConstant.high; - lane.low ^= roundConstant.low;; - } - }, - - _doFinalize: function () { - // Shortcuts - var data = this._data; - var dataWords = data.words; - var nBitsTotal = this._nDataBytes * 8; - var nBitsLeft = data.sigBytes * 8; - var blockSizeBits = this.blockSize * 32; - - // Add padding - dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32); - dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80; - data.sigBytes = dataWords.length * 4; - - // Hash final blocks - this._process(); - - // Shortcuts - var state = this._state; - var outputLengthBytes = this.cfg.outputLength / 8; - var outputLengthLanes = outputLengthBytes / 8; - - // Squeeze - var hashWords = []; - for (var i = 0; i < outputLengthLanes; i++) { - // Shortcuts - var lane = state[i]; - var laneMsw = lane.high; - var laneLsw = lane.low; - - // Swap endian - laneMsw = ( - (((laneMsw << 8) | (laneMsw >>> 24)) & 0x00ff00ff) | - (((laneMsw << 24) | (laneMsw >>> 8)) & 0xff00ff00) - ); - laneLsw = ( - (((laneLsw << 8) | (laneLsw >>> 24)) & 0x00ff00ff) | - (((laneLsw << 24) | (laneLsw >>> 8)) & 0xff00ff00) - ); - - // Squeeze state to retrieve hash - hashWords.push(laneLsw); - hashWords.push(laneMsw); - } - - // Return final computed hash - return new WordArray.init(hashWords, outputLengthBytes); - }, - - clone: function () { - var clone = Hasher.clone.call(this); - - var state = clone._state = this._state.slice(0); - for (var i = 0; i < 25; i++) { - state[i] = state[i].clone(); - } - - return clone; - } - }); - - /** - * Shortcut function to the hasher's object interface. - * - * @param {WordArray|string} message The message to hash. - * - * @return {WordArray} The hash. - * - * @static - * - * @example - * - * var hash = CryptoJS.SHA3('message'); - * var hash = CryptoJS.SHA3(wordArray); - */ - C.SHA3 = Hasher._createHelper(SHA3); - - /** - * Shortcut function to the HMAC's object interface. - * - * @param {WordArray|string} message The message to hash. - * @param {WordArray|string} key The secret key. - * - * @return {WordArray} The HMAC. - * - * @static - * - * @example - * - * var hmac = CryptoJS.HmacSHA3(message, key); - */ - C.HmacSHA3 = Hasher._createHmacHelper(SHA3); -}(Math)); +}()); \ No newline at end of file diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 519a3c5..64526c4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { - "name": "fh-js-sdk", - "version": "2.18.6", + "name": "fh-sync-js", + "version": "1.0.0", "dependencies": { "loglevel": { "version": "0.6.0", diff --git a/package.json b/package.json index c27684d..a2740be 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,9 @@ { "name": "fh-sync-js", - "version": "1.0", + "version": "1.0.0", "description": "Javascript client for fh-sync offline synchronization library", "main": "dist/fh-sync.js", "types": "./fh-js-sdk.d.ts", - "browser": { - "JSON": "./libs/json2.js" - }, "browserify-shim": { "JSON": { "exports": "JSON" @@ -24,43 +21,15 @@ "browserify-shim" ] }, - "testling": { - "harness": "mocha-bdd", - "scripts": [ - "test/browser/libs/sinon/sinon.js", - "test/browser/libs/sinon/sinon-ie.js" - ], - "files": "test/tests/*.js", - "browsers": [ - "ie/9..10", - "firefox/5.0", - "firefox/latest", - "chrome/7.0", - "chrome/latest", - "safari/5.0.5", - "safari/latest", - "opera/11", - "opera/next", - "iphone/6.0", - "ipad/6.0", - "android-browser/latest" - ] - }, "scripts": { - "test": "grunt test", - "prepublish": "grunt concat-core-sdk", - "doc:clean": "rimraf doc", - "doc:generate": "typedoc --includeDeclarations --excludeExternals --out doc ./fh-js-sdk.d.ts", - "doc:gh-pages": "touch doc/.nojekyll; git add doc/.*; git add doc/*; git commit -m 'Publishing to gh-pages'; git push origin :gh-pages --force ; git subtree push --prefix doc origin gh-pages", - "doc": "npm run doc:clean && npm run doc:generate && npm run doc:gh-pages" + "test": "grunt test" }, "repository": { "type": "git", - "url": "git://github.com/feedhenry/fh-js-sdk.git" + "url": "git://github.com/feedhenry/fh-sync-js.git" }, - "author": "", - "license": "Copyright (c) 2014 FeedHenry Ltd, All Rights Reserved.", - "gitHead": "251a1f88a39f59f2652552ae245893a833faee71", + "author": "Feedhenry Team", + "license": "Apache 2.0", "dependencies": { "type-of": "~2.0.1", "loglevel": "~0.6.0", diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6211bcd --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +var api_sync = require("./sync-client"); + +// Mounting into global fh namespace +var fh = window.$fh = window.$fh || {}; +fh.sync = api_sync; \ No newline at end of file diff --git a/src/modules/ajax.js b/src/modules/ajax.js deleted file mode 100644 index 5c2ffda..0000000 --- a/src/modules/ajax.js +++ /dev/null @@ -1,408 +0,0 @@ -//a shameless copy from https://github.com/ForbesLindesay/ajax/blob/master/index.js. -//it has the same methods and config options as jQuery/zeptojs but very light weight. see http://api.jquery.com/jQuery.ajax/ -//a few small changes are made for supporting IE 8 and other features: -//1. use getXhr function to replace the default XMLHttpRequest implementation for supporting IE8 -//2. Integrate with events emitter. So to subscribe ajax events, you can do $fh.on("ajaxStart", handler). See http://api.jquery.com/Ajax_Events/ for full list of events -//3. allow passing xhr factory method through options: e.g. $fh.ajax({xhr: function(){/*own implementation of xhr*/}}); -//4. Use fh_timeout value as the default timeout -//5. an extra option called "tryJSONP" to allow try the same call with JSONP if normal CORS failed - should only be used internally -//6. for jsonp, allow to specify the callback query param name using the "jsonp" option - -var eventsHandler = require("./events"); -var XDomainRequestWrapper = require("./XDomainRequestWrapper"); -var logger = require("./logger"); - -var type -try { - type = require('type-of') -} catch (ex) { - //hide from browserify - var r = require - type = r('type') -} - -var jsonpID = 0, - document = window.document, - key, - name, - rscript = /)<[^<]*)*<\/script>/gi, - scriptTypeRE = /^(?:text|application)\/javascript/i, - xmlTypeRE = /^(?:text|application)\/xml/i, - jsonType = 'application/json', - htmlType = 'text/html', - blankRE = /^\s*$/; - -var ajax = module.exports = function (options) { - var settings = extend({}, options || {}) - //keep backward compatibility - if(window && window.$fh && typeof window.$fh.fh_timeout === "number"){ - ajax.settings.timeout = window.$fh.fh_timeout; - } - - for (key in ajax.settings) - if (settings[key] === undefined) settings[key] = ajax.settings[key] - - ajaxStart(settings) - - if (!settings.crossDomain) { - settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && (RegExp.$1 != window.location.protocol || RegExp.$2 != window.location.host) - } - - var dataType = settings.dataType, - hasPlaceholder = /=\?/.test(settings.url) - if (dataType == 'jsonp' || hasPlaceholder) { - if (!hasPlaceholder) { - settings.url = appendQuery(settings.url, (settings.jsonp? settings.jsonp: '_callback') + '=?'); - } - return ajax.JSONP(settings) - } - - if (!settings.url) settings.url = window.location.toString() - serializeData(settings) - - var mime = settings.accepts[dataType], - baseHeaders = {}, - protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol, - xhr = settings.xhr(settings.crossDomain), - abortTimeout = null; - - if (!settings.crossDomain) baseHeaders['X-Requested-With'] = 'XMLHttpRequest' - if (mime) { - baseHeaders['Accept'] = mime - if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0] - xhr.overrideMimeType && xhr.overrideMimeType(mime) - } - if (settings.contentType || (settings.data && !settings.formdata && settings.type.toUpperCase() != 'GET')) - baseHeaders['Content-Type'] = (settings.contentType || 'application/x-www-form-urlencoded') - settings.headers = extend(baseHeaders, settings.headers || {}) - - if (typeof Titanium !== 'undefined') { - xhr.onerror = function(){ - if (!abortTimeout){ - return; - } - clearTimeout(abortTimeout); - ajaxError(null, 'error', xhr, settings); - }; - } - - xhr.onreadystatechange = function () { - - if (xhr.readyState == 4) { - clearTimeout(abortTimeout) - abortTimeout = undefined; - var result, error = false - if(settings.tryJSONP){ - //check if the request has fail. In some cases, we may want to try jsonp as well. Again, FH only... - if(xhr.status === 0 && settings.crossDomain && !xhr.isTimeout && protocol != 'file:'){ - logger.debug("retry ajax call with jsonp") - settings.type = "GET"; - settings.dataType = "jsonp"; - - if (settings.data) { - settings.data = "_jsonpdata=" + JSON.stringify( - require("./fhparams").addFHParams(JSON.parse(settings.data)) - ); - } else { - settings.data = "_jsonpdata=" + settings.data; - } - - return ajax(settings); - } - } - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) { - dataType = dataType || mimeToDataType(xhr.getResponseHeader('content-type')) - result = xhr.responseText - logger.debug("ajax response :: status = " + xhr.status + " :: body = " + result) - - try { - if (dataType == 'script')(1, eval)(result) - else if (dataType == 'xml') result = xhr.responseXML - else if (dataType == 'json') result = blankRE.test(result) ? null : JSON.parse(result) - } catch (e) { - error = e - } - - if (error) { - logger.debug("ajax error", error); - ajaxError(error, 'parsererror', xhr, settings) - } - else ajaxSuccess(result, xhr, settings) - } else { - ajaxError(null, 'error', xhr, settings) - } - } - } - - var async = 'async' in settings ? settings.async : true - logger.debug("ajax call settings", settings) - xhr.open(settings.type, settings.url, async) - - for (name in settings.headers) xhr.setRequestHeader(name, settings.headers[name]) - - if (ajaxBeforeSend(xhr, settings) === false) { - logger.debug("ajax call is aborted due to ajaxBeforeSend") - xhr.abort() - return false - } - - if (settings.timeout > 0) abortTimeout = setTimeout(function () { - logger.debug("ajax call timed out") - xhr.onreadystatechange = empty - xhr.abort() - xhr.isTimeout = true - ajaxError(null, 'timeout', xhr, settings) - }, settings.timeout) - - // avoid sending empty string (#319) - xhr.send(settings.data ? settings.data : null) - return xhr -} - - -// trigger a custom event and return true -function triggerAndReturn(context, eventName, data) { - eventsHandler.emit(eventName, data); - return true; -} - -// trigger an Ajax "global" event -function triggerGlobal(settings, context, eventName, data) { - if (settings.global) return triggerAndReturn(context || document, eventName, data) -} - -// Number of active Ajax requests -ajax.active = 0 - -function ajaxStart(settings) { - if (settings.global && ajax.active++ === 0) triggerGlobal(settings, null, 'ajaxStart') -} - -function ajaxStop(settings) { - if (settings.global && !(--ajax.active)) triggerGlobal(settings, null, 'ajaxStop') -} - -// triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable -function ajaxBeforeSend(xhr, settings) { - var context = settings.context - if (settings.beforeSend.call(context, xhr, settings) === false) - return false - - triggerGlobal(settings, context, 'ajaxSend', [xhr, settings]) -} - -function ajaxSuccess(data, xhr, settings) { - var context = settings.context, - status = 'success' - settings.success.call(context, data, status, xhr) - triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data]) - ajaxComplete(status, xhr, settings) -} -// type: "timeout", "error", "abort", "parsererror" -function ajaxError(error, type, xhr, settings) { - var context = settings.context - settings.error.call(context, xhr, type, error) - triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error]) - ajaxComplete(type, xhr, settings) -} -// status: "success", "notmodified", "error", "timeout", "abort", "parsererror" -function ajaxComplete(status, xhr, settings) { - var context = settings.context - settings.complete.call(context, xhr, status) - triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings]) - ajaxStop(settings) -} - -// Empty function, used as default callback -function empty() {} - -ajax.JSONP = function (options) { - if (!('type' in options)) return ajax(options) - - var callbackName = 'jsonp' + (++jsonpID), - script = document.createElement('script'), - abort = function () { - //todo: remove script - //$(script).remove() - if (callbackName in window) window[callbackName] = empty - ajaxComplete('abort', xhr, options) - }, - xhr = { - abort: abort - }, abortTimeout, - head = document.getElementsByTagName("head")[0] || document.documentElement - - if (options.error) script.onerror = function () { - xhr.abort() - options.error() - } - - window[callbackName] = function (data) { - clearTimeout(abortTimeout) - abortTimeout = undefined; - //todo: remove script - //$(script).remove() - delete window[callbackName] - ajaxSuccess(data, xhr, options) - } - - serializeData(options) - script.src = options.url.replace(/=\?/, '=' + callbackName) - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (see jQuery bugs #2709 and #4378). - head.insertBefore(script, head.firstChild); - - if (options.timeout > 0) abortTimeout = setTimeout(function () { - xhr.abort() - ajaxComplete('timeout', xhr, options) - }, options.timeout) - - return xhr -} - -function isIE(){ - var ie = false; - if(navigator.userAgent && navigator.userAgent.indexOf("MSIE") >=0 ){ - ie = true; - } - return ie; -} - -function getXhr(crossDomain){ - var xhr = null; - //always use XMLHttpRequest if available - if(window.XMLHttpRequest){ - xhr = new XMLHttpRequest(); - } - //for IE8 only. Need to make sure it's not used when running inside Cordova. - if(isIE() && (crossDomain === true) && typeof window.XDomainRequest !== "undefined" && typeof window.cordova === "undefined"){ - xhr = new XDomainRequestWrapper(new XDomainRequest()); - } - // For Titanium SDK - if (typeof Titanium !== 'undefined'){ - var params = {}; - if(ajax.settings && ajax.settings.timeout){ - params.timeout = ajax.settings.timeout; - } - xhr = Titanium.Network.createHTTPClient(params); - } - - return xhr; -} - -ajax.settings = { - // Default type of request - type: 'GET', - // Callback that is executed before request - beforeSend: empty, - // Callback that is executed if the request succeeds - success: empty, - // Callback that is executed the the server drops error - error: empty, - // Callback that is executed on request complete (both: error and success) - complete: empty, - // The context for the callbacks - context: null, - // Whether to trigger "global" Ajax events - global: true, - // Transport - xhr: getXhr, - // MIME types mapping - accepts: { - script: 'text/javascript, application/javascript', - json: jsonType, - xml: 'application/xml, text/xml', - html: htmlType, - text: 'text/plain' - }, - // Whether the request is to another domain - crossDomain: false -} - -function mimeToDataType(mime) { - return mime && (mime == htmlType ? 'html' : - mime == jsonType ? 'json' : - scriptTypeRE.test(mime) ? 'script' : - xmlTypeRE.test(mime) && 'xml') || 'text' -} - -function appendQuery(url, query) { - return (url + '&' + query).replace(/[&?]{1,2}/, '?') -} - -// serialize payload and append it to the URL for GET requests -function serializeData(options) { - if (type(options.data) === 'object') { - if(typeof options.data.append === "function"){ - //we are dealing with FormData, do not serialize - options.formdata = true; - } else { - options.data = param(options.data) - } - } - if (options.data && (!options.type || options.type.toUpperCase() == 'GET')) - options.url = appendQuery(options.url, options.data) -} - -ajax.get = function (url, success) { - return ajax({ - url: url, - success: success - }) -} - -ajax.post = function (url, data, success, dataType) { - if (type(data) === 'function') dataType = dataType || success, success = data, data = null - return ajax({ - type: 'POST', - url: url, - data: data, - success: success, - dataType: dataType - }) -} - -ajax.getJSON = function (url, success) { - return ajax({ - url: url, - success: success, - dataType: 'json' - }) -} - -var escape = encodeURIComponent; - -function serialize(params, obj, traditional, scope) { - var array = type(obj) === 'array'; - for (var key in obj) { - var value = obj[key]; - - if (scope) key = traditional ? scope : scope + '[' + (array ? '' : key) + ']' - // handle data in serializeArray() format - if (!scope && array) params.add(value.name, value.value) - // recurse into nested objects - else if (traditional ? (type(value) === 'array') : (type(value) === 'object')) - serialize(params, value, traditional, key) - else params.add(key, value) - } -} - -function param(obj, traditional) { - var params = [] - params.add = function (k, v) { - this.push(escape(k) + '=' + escape(v)) - } - serialize(params, obj, traditional) - return params.join('&').replace('%20', '+') -} - -function extend(target) { - var slice = Array.prototype.slice; - slice.call(arguments, 1).forEach(function (source) { - for (key in source) - if (source[key] !== undefined) - target[key] = source[key] - }) - return target -} diff --git a/src/sync-client.js b/src/sync-client.js new file mode 100755 index 0000000..7613b6d --- /dev/null +++ b/src/sync-client.js @@ -0,0 +1,1255 @@ +var CryptoJS = require("../libs/generated/crypto"); +var Lawnchair = require('../libs/generated/lawnchair'); + +var self = { + + // CONFIG + defaults: { + "sync_frequency": 10, + // How often to synchronise data with the cloud in seconds. + "auto_sync_local_updates": true, + // Should local chages be syned to the cloud immediately, or should they wait for the next sync interval + "notify_client_storage_failed": true, + // Should a notification event be triggered when loading/saving to client storage fails + "notify_sync_started": true, + // Should a notification event be triggered when a sync cycle with the server has been started + "notify_sync_complete": true, + // Should a notification event be triggered when a sync cycle with the server has been completed + "notify_offline_update": true, + // Should a notification event be triggered when an attempt was made to update a record while offline + "notify_collision_detected": true, + // Should a notification event be triggered when an update failed due to data collision + "notify_remote_update_failed": true, + // Should a notification event be triggered when an update failed for a reason other than data collision + "notify_local_update_applied": true, + // Should a notification event be triggered when an update was applied to the local data store + "notify_remote_update_applied": true, + // Should a notification event be triggered when an update was applied to the remote data store + "notify_delta_received": true, + // Should a notification event be triggered when a delta was received from the remote data store for the dataset + "notify_record_delta_received": true, + // Should a notification event be triggered when a delta was received from the remote data store for a record + "notify_sync_failed": true, + // Should a notification event be triggered when the sync loop failed to complete + "do_console_log": false, + // Should log statements be written to console.log + "crashed_count_wait" : 10, + // How many syncs should we check for updates on crashed in flight updates before we give up searching + "resend_crashed_updates" : true, + // If we have reached the crashed_count_wait limit, should we re-try sending the crashed in flight pending record + "sync_active" : true, + // Is the background sync with the cloud currently active + "storage_strategy" : "html5-filesystem", + // Storage strategy to use for Lawnchair - supported strategies are 'html5-filesystem' and 'dom' + "file_system_quota" : 50 * 1024 * 1204, + // Amount of space to request from the HTML5 filesystem API when running in browser + "icloud_backup" : false //ios only. If set to true, the file will be backed by icloud + }, + + notifications: { + "CLIENT_STORAGE_FAILED": "client_storage_failed", + // loading/saving to client storage failed + "SYNC_STARTED": "sync_started", + // A sync cycle with the server has been started + "SYNC_COMPLETE": "sync_complete", + // A sync cycle with the server has been completed + "OFFLINE_UPDATE": "offline_update", + // An attempt was made to update a record while offline + "COLLISION_DETECTED": "collision_detected", + //Update Failed due to data collision + "REMOTE_UPDATE_FAILED": "remote_update_failed", + // Update Failed for a reason other than data collision + "REMOTE_UPDATE_APPLIED": "remote_update_applied", + // An update was applied to the remote data store + "LOCAL_UPDATE_APPLIED": "local_update_applied", + // An update was applied to the local data store + "DELTA_RECEIVED": "delta_received", + // A delta was received from the remote data store for the dataset + "RECORD_DELTA_RECEIVED": "record_delta_received", + // A delta was received from the remote data store for the record + "SYNC_FAILED": "sync_failed" + // Sync loop failed to complete + }, + + datasets: {}, + + // Initialise config to default values; + config: undefined, + + //TODO: deprecate this + notify_callback: undefined, + + notify_callback_map : {}, + + init_is_called: false, + + //this is used to map the temp data uid (created on client) to the real uid (created in the cloud) + uid_map: {}, + + // PUBLIC FUNCTION IMPLEMENTATIONS + init: function(options) { + self.consoleLog('sync - init called'); + + self.config = JSON.parse(JSON.stringify(self.defaults)); + for (var i in options) { + self.config[i] = options[i]; + } + + //prevent multiple monitors from created if init is called multiple times + if(!self.init_is_called){ + self.init_is_called = true; + self.datasetMonitor(); + } + }, + + notify: function(datasetId, callback) { + if(arguments.length === 1 && typeof datasetId === 'function'){ + self.notify_callback = datasetId; + } else { + self.notify_callback_map[datasetId] = callback; + } + }, + + manage: function(dataset_id, opts, query_params, meta_data, cb) { + self.consoleLog('manage - START'); + + // Currently we do not enforce the rule that init() funciton should be called before manage(). + // We need this check to guard against self.config undefined + if (!self.config){ + self.config = JSON.parse(JSON.stringify(self.defaults)); + } + + var options = opts || {}; + + var doManage = function(dataset) { + self.consoleLog('doManage dataset :: initialised = ' + dataset.initialised + " :: " + dataset_id + ' :: ' + JSON.stringify(options)); + + var currentDatasetCfg = (dataset.config) ? dataset.config : self.config; + var datasetConfig = self.setOptions(currentDatasetCfg, options); + + dataset.query_params = query_params || dataset.query_params || {}; + dataset.meta_data = meta_data || dataset.meta_data || {}; + dataset.config = datasetConfig; + dataset.syncRunning = false; + dataset.syncPending = true; + dataset.initialised = true; + if(typeof dataset.meta === "undefined"){ + dataset.meta = {}; + } + + self.saveDataSet(dataset_id, function() { + + if( cb ) { + cb(); + } + }); + }; + + // Check if the dataset is already loaded + self.getDataSet(dataset_id, function(dataset) { + self.consoleLog('manage - dataset already loaded'); + doManage(dataset); + }, function(err) { + self.consoleLog('manage - dataset not loaded... trying to load'); + + // Not already loaded, try to load from local storage + self.loadDataSet(dataset_id, function(dataset) { + self.consoleLog('manage - dataset loaded from local storage'); + + // Loading from local storage worked + + // Fire the local update event to indicate that dataset was loaded from local storage + self.doNotify(dataset_id, null, self.notifications.LOCAL_UPDATE_APPLIED, "load"); + + // Put the dataet under the management of the sync service + doManage(dataset); + }, + function(err) { + // No dataset in memory or local storage - create a new one and put it in memory + self.consoleLog('manage - Creating new dataset for id ' + dataset_id); + var dataset = {}; + dataset.data = {}; + dataset.pending = {}; + dataset.meta = {}; + self.datasets[dataset_id] = dataset; + doManage(dataset); + }); + }); + }, + + /** + * Sets options for passed in config, if !config then options will be applied to default config. + * @param {Object} config - config to which options will be applied + * @param {Object} options - options to be applied to the config + */ + setOptions: function(config, options) { + // Make sure config is initialised + if( ! config ) { + config = JSON.parse(JSON.stringify(self.defaults)); + } + + + var datasetConfig = JSON.parse(JSON.stringify(config)); + var optionsIn = JSON.parse(JSON.stringify(options)); + for (var k in optionsIn) { + datasetConfig[k] = optionsIn[k]; + } + + return datasetConfig; + }, + + list: function(dataset_id, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + if (dataset && dataset.data) { + // Return a copy of the dataset so updates will not automatically make it back into the dataset + var res = JSON.parse(JSON.stringify(dataset.data)); + success(res); + } else { + if(failure) { + failure('no_data'); + } + } + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + }, + + getUID: function(oldOrNewUid){ + var uid = self.uid_map[oldOrNewUid]; + if(uid || uid === 0){ + return uid; + } else { + return oldOrNewUid; + } + }, + + create: function(dataset_id, data, success, failure) { + if(data == null){ + if(failure){ + return failure("null_data"); + } + } + self.addPendingObj(dataset_id, null, data, "create", success, failure); + }, + + read: function(dataset_id, uid, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + uid = self.getUID(uid); + var rec = dataset.data[uid]; + if (!rec) { + failure("unknown_uid"); + } else { + // Return a copy of the record so updates will not automatically make it back into the dataset + var res = JSON.parse(JSON.stringify(rec)); + success(res); + } + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + }, + + update: function(dataset_id, uid, data, success, failure) { + uid = self.getUID(uid); + self.addPendingObj(dataset_id, uid, data, "update", success, failure); + }, + + 'delete': function(dataset_id, uid, success, failure) { + uid = self.getUID(uid); + self.addPendingObj(dataset_id, uid, null, "delete", success, failure); + }, + + getPending: function(dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + var res; + if( dataset ) { + res = dataset.pending; + } + cb(res); + }, function(err, datatset_id) { + self.consoleLog(err); + }); + }, + + clearPending: function(dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + dataset.pending = {}; + self.saveDataSet(dataset_id, cb); + }); + }, + + listCollisions : function(dataset_id, success, failure){ + self.getDataSet(dataset_id, function(dataset) { + self.doCloudCall({ + "dataset_id": dataset_id, + "req": { + "fn": "listCollisions", + "meta_data" : dataset.meta_data + } + }, success, failure); + }, failure); + }, + + removeCollision: function(dataset_id, colissionHash, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + self.doCloudCall({ + "dataset_id" : dataset_id, + "req": { + "fn": "removeCollision", + "hash": colissionHash, + meta_data: dataset.meta_data + } + }, success, failure); + }); + }, + + + // PRIVATE FUNCTIONS + isOnline: function(callback) { + var online = true; + + // first, check if navigator.online is available + if(typeof navigator.onLine !== "undefined"){ + online = navigator.onLine; + } + + // second, check if Phonegap is available and has online info + if(online){ + //use phonegap to determin if the network is available + if(typeof navigator.network !== "undefined" && typeof navigator.network.connection !== "undefined"){ + var networkType = navigator.network.connection.type; + if(networkType === "none" || networkType === null) { + online = false; + } + } + } + + return callback(online); + }, + + doNotify: function(dataset_id, uid, code, message) { + + if( self.notify_callback || self.notify_callback_map[dataset_id]) { + var notifyFunc = self.notify_callback_map[dataset_id] || self.notify_callback; + if ( self.config['notify_' + code] ) { + var notification = { + "dataset_id" : dataset_id, + "uid" : uid, + "code" : code, + "message" : message + }; + // make sure user doesn't block + setTimeout(function () { + notifyFunc(notification); + }, 0); + } + } + }, + + getDataSet: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getQueryParams: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.query_params); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setQueryParams: function(dataset_id, queryParams, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.query_params = queryParams; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.query_params); + } + } else { + if ( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getMetaData: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.meta_data); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setMetaData: function(dataset_id, metaData, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.meta_data = metaData; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.meta_data); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getConfig: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.config); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setConfig: function(dataset_id, config, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + var fullConfig = self.setOptions(dataset.config, config); + dataset.config = fullConfig; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.config); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + stopSync: function(dataset_id, success, failure) { + self.setConfig(dataset_id, {"sync_active" : false}, function() { + if( success ) { + success(); + } + }, failure); + }, + + startSync: function(dataset_id, success, failure) { + self.setConfig(dataset_id, {"sync_active" : true}, function() { + if( success ) { + success(); + } + }, failure); + }, + + doSync: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.syncPending = true; + self.saveDataSet(dataset_id); + if( success ) { + success(); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + forceSync: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.syncForced = true; + self.saveDataSet(dataset_id); + if( success ) { + success(); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + sortObject : function(object) { + if (typeof object !== "object" || object === null) { + return object; + } + + var result = []; + + Object.keys(object).sort().forEach(function(key) { + result.push({ + key: key, + value: self.sortObject(object[key]) + }); + }); + + return result; + }, + + sortedStringify : function(obj) { + + var str = ''; + + try { + str = JSON.stringify(self.sortObject(obj)); + } catch (e) { + console.error('Error stringifying sorted object:' + e); + } + + return str; + }, + + generateHash: function(object) { + var hash = CryptoJS.SHA1(self.sortedStringify(object)); + return hash.toString(); + }, + + addPendingObj: function(dataset_id, uid, data, action, success, failure) { + self.isOnline(function (online) { + if (!online) { + self.doNotify(dataset_id, uid, self.notifications.OFFLINE_UPDATE, action); + } + }); + + function storePendingObject(obj) { + obj.hash = obj.hash || self.generateHash(obj); + + self.getDataSet(dataset_id, function(dataset) { + + dataset.pending[obj.hash] = obj; + + self.updateDatasetFromLocal(dataset, obj); + + if(self.config.auto_sync_local_updates) { + dataset.syncPending = true; + } + self.saveDataSet(dataset_id); + self.doNotify(dataset_id, uid, self.notifications.LOCAL_UPDATE_APPLIED, action); + + success(obj); + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + } + + var pendingObj = {}; + pendingObj.inFlight = false; + pendingObj.action = action; + pendingObj.post = JSON.parse(JSON.stringify(data)); + pendingObj.postHash = self.generateHash(pendingObj.post); + pendingObj.timestamp = new Date().getTime(); + if( "create" === action ) { + //this hash value will be returned later on when the cloud returns updates. We can then link the old uid + //with new uid + pendingObj.hash = self.generateHash(pendingObj); + pendingObj.uid = pendingObj.hash; + storePendingObject(pendingObj); + } else { + self.read(dataset_id, uid, function(rec) { + pendingObj.uid = uid; + pendingObj.pre = rec.data; + pendingObj.preHash = self.generateHash(rec.data); + storePendingObject(pendingObj); + }, function(code, msg) { + if(failure){ + failure(code, msg); + } + }); + } + }, + + syncLoop: function(dataset_id) { + self.getDataSet(dataset_id, function(dataSet) { + + // The sync loop is currently active + dataSet.syncPending = false; + dataSet.syncRunning = true; + dataSet.syncLoopStart = new Date().getTime(); + self.doNotify(dataset_id, null, self.notifications.SYNC_STARTED, null); + + self.isOnline(function(online) { + if (!online) { + self.syncComplete(dataset_id, "offline", self.notifications.SYNC_FAILED); + } else { + var syncLoopParams = {}; + syncLoopParams.fn = 'sync'; + syncLoopParams.dataset_id = dataset_id; + syncLoopParams.query_params = dataSet.query_params; + syncLoopParams.config = dataSet.config; + syncLoopParams.meta_data = dataSet.meta_data; + //var datasetHash = self.generateLocalDatasetHash(dataSet); + syncLoopParams.dataset_hash = dataSet.hash; + syncLoopParams.acknowledgements = dataSet.acknowledgements || []; + + var pending = dataSet.pending; + var pendingArray = []; + for(var i in pending ) { + // Mark the pending records we are about to submit as inflight and add them to the array for submission + // Don't re-add previous inFlight pending records who whave crashed - i.e. who's current state is unknown + // Don't add delayed records + if( !pending[i].inFlight && !pending[i].crashed && !pending[i].delayed) { + pending[i].inFlight = true; + pending[i].inFlightDate = new Date().getTime(); + pendingArray.push(pending[i]); + } + } + syncLoopParams.pending = pendingArray; + + if( pendingArray.length > 0 ) { + self.consoleLog('Starting sync loop - global hash = ' + dataSet.hash + ' :: params = ' + JSON.stringify(syncLoopParams, null, 2)); + } + self.doCloudCall({ + 'dataset_id': dataset_id, + 'req': syncLoopParams + }, function(res) { + var rec; + + function processUpdates(updates, notification, acknowledgements) { + if( updates ) { + for (var up in updates) { + rec = updates[up]; + acknowledgements.push(rec); + if( dataSet.pending[up] && dataSet.pending[up].inFlight) { + delete dataSet.pending[up]; + self.doNotify(dataset_id, rec.uid, notification, rec); + } + } + } + } + + // Check to see if any previously crashed inflight records can now be resolved + self.updateCrashedInFlightFromNewData(dataset_id, dataSet, res); + + //Check to see if any delayed pending records can now be set to ready + self.updateDelayedFromNewData(dataset_id, dataSet, res); + + //Check meta data as well to make sure it contains the correct info + self.updateMetaFromNewData(dataset_id, dataSet, res); + + + if (res.updates) { + var acknowledgements = []; + self.checkUidChanges(dataSet, res.updates.applied); + processUpdates(res.updates.applied, self.notifications.REMOTE_UPDATE_APPLIED, acknowledgements); + processUpdates(res.updates.failed, self.notifications.REMOTE_UPDATE_FAILED, acknowledgements); + processUpdates(res.updates.collisions, self.notifications.COLLISION_DETECTED, acknowledgements); + dataSet.acknowledgements = acknowledgements; + } + + if (res.hash && res.hash !== dataSet.hash) { + self.consoleLog("Local dataset stale - syncing records :: local hash= " + dataSet.hash + " - remoteHash=" + res.hash); + // Different hash value returned - Sync individual records + self.syncRecords(dataset_id); + } else { + self.consoleLog("Local dataset up to date"); + self.syncComplete(dataset_id, "online", self.notifications.SYNC_COMPLETE); + } + }, function(msg, err) { + // The AJAX call failed to complete succesfully, so the state of the current pending updates is unknown + // Mark them as "crashed". The next time a syncLoop completets successfully, we will review the crashed + // records to see if we can determine their current state. + self.markInFlightAsCrashed(dataSet); + self.consoleLog("syncLoop failed : msg=" + msg + " :: err = " + err); + self.syncComplete(dataset_id, msg, self.notifications.SYNC_FAILED); + }); + } + }); + }); + }, + + syncRecords: function(dataset_id) { + + self.getDataSet(dataset_id, function(dataSet) { + + var localDataSet = dataSet.data || {}; + + var clientRecs = {}; + for (var i in localDataSet) { + var uid = i; + var hash = localDataSet[i].hash; + clientRecs[uid] = hash; + } + + var syncRecParams = {}; + + syncRecParams.fn = 'syncRecords'; + syncRecParams.dataset_id = dataset_id; + syncRecParams.query_params = dataSet.query_params; + syncRecParams.clientRecs = clientRecs; + + self.consoleLog("syncRecParams :: " + JSON.stringify(syncRecParams)); + + self.doCloudCall({ + 'dataset_id': dataset_id, + 'req': syncRecParams + }, function(res) { + self.consoleLog('syncRecords Res before applying pending changes :: ' + JSON.stringify(res)); + self.applyPendingChangesToRecords(dataSet, res); + self.consoleLog('syncRecords Res after apply pending changes :: ' + JSON.stringify(res)); + + var i; + + if (res.create) { + for (i in res.create) { + localDataSet[i] = {"hash" : res.create[i].hash, "data" : res.create[i].data}; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "create"); + } + } + + if (res.update) { + for (i in res.update) { + localDataSet[i].hash = res.update[i].hash; + localDataSet[i].data = res.update[i].data; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "update"); + } + } + if (res['delete']) { + for (i in res['delete']) { + delete localDataSet[i]; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "delete"); + } + } + + self.doNotify(dataset_id, res.hash, self.notifications.DELTA_RECEIVED, 'partial dataset'); + + dataSet.data = localDataSet; + if(res.hash) { + dataSet.hash = res.hash; + } + self.syncComplete(dataset_id, "online", self.notifications.SYNC_COMPLETE); + }, function(msg, err) { + self.consoleLog("syncRecords failed : msg=" + msg + " :: err=" + err); + self.syncComplete(dataset_id, msg, self.notifications.SYNC_FAILED); + }); + }); + }, + + syncComplete: function(dataset_id, status, notification) { + + self.getDataSet(dataset_id, function(dataset) { + dataset.syncRunning = false; + dataset.syncLoopEnd = new Date().getTime(); + self.saveDataSet(dataset_id); + self.doNotify(dataset_id, dataset.hash, notification, status); + }); + }, + + applyPendingChangesToRecords: function(dataset, records){ + var pendings = dataset.pending; + for(var pendingUid in pendings){ + if(pendings.hasOwnProperty(pendingUid)){ + var pendingObj = pendings[pendingUid]; + var uid = pendingObj.uid; + //if the records contain any thing about the data records that are currently in pendings, + //it means there are local changes that haven't been applied to the cloud yet, + //so update the pre value of each pending record to relect the latest status from cloud + //and remove them from the response + if(records.create){ + var creates = records.create; + if(creates && creates[uid]){ + delete creates[uid]; + } + } + if(records.update){ + var updates = records.update; + if(updates && updates[uid]){ + delete updates[uid]; + } + } + if(records['delete']){ + var deletes = records['delete']; + if(deletes && deletes[uid]){ + delete deletes[uid]; + } + } + } + } + }, + + checkUidChanges: function(dataset, appliedUpdates){ + if(appliedUpdates){ + var new_uids = {}; + var changeUidsCount = 0; + for(var update in appliedUpdates){ + if(appliedUpdates.hasOwnProperty(update)){ + var applied_update = appliedUpdates[update]; + var action = applied_update.action; + if(action && action === 'create'){ + //we are receving the results of creations, at this point, we will have the old uid(the hash) and the real uid generated by the cloud + var newUid = applied_update.uid; + var oldUid = applied_update.hash; + changeUidsCount++; + //remember the mapping + self.uid_map[oldUid] = newUid; + new_uids[oldUid] = newUid; + //update the data uid in the dataset + var record = dataset.data[oldUid]; + if(record){ + dataset.data[newUid] = record; + delete dataset.data[oldUid]; + } + + //update the old uid in meta data + var metaData = dataset.meta[oldUid]; + if(metaData) { + dataset.meta[newUid] = metaData; + delete dataset.meta[oldUid]; + } + } + } + } + if(changeUidsCount > 0){ + //we need to check all existing pendingRecords and update their UIDs if they are still the old values + for(var pending in dataset.pending){ + if(dataset.pending.hasOwnProperty(pending)){ + var pendingObj = dataset.pending[pending]; + var pendingRecordUid = pendingObj.uid; + if(new_uids[pendingRecordUid]){ + pendingObj.uid = new_uids[pendingRecordUid]; + } + } + } + } + } + }, + + checkDatasets: function() { + for( var dataset_id in self.datasets ) { + if( self.datasets.hasOwnProperty(dataset_id) ) { + var dataset = self.datasets[dataset_id]; + if(dataset && !dataset.syncRunning && (dataset.config.sync_active || dataset.syncForced)) { + // Check to see if it is time for the sync loop to run again + var lastSyncStart = dataset.syncLoopStart; + var lastSyncCmp = dataset.syncLoopEnd; + if(dataset.syncForced){ + dataset.syncPending = true; + } else if( lastSyncStart == null ) { + self.consoleLog(dataset_id +' - Performing initial sync'); + // Dataset has never been synced before - do initial sync + dataset.syncPending = true; + } else if (lastSyncCmp != null) { + var timeSinceLastSync = new Date().getTime() - lastSyncCmp; + var syncFrequency = dataset.config.sync_frequency * 1000; + if( timeSinceLastSync > syncFrequency ) { + // Time between sync loops has passed - do another sync + dataset.syncPending = true; + } + } + + if( dataset.syncPending ) { + // Reset syncForced in case it was what caused the sync cycle to run. + dataset.syncForced = false; + + // If the dataset requres syncing, run the sync loop. This may be because the sync interval has passed + // or because the sync_frequency has been changed or because a change was made to the dataset and the + // immediate_sync flag set to true + self.syncLoop(dataset_id); + } + } + } + } + }, + + /** + * Sets cloud handler for sync responsible for making network requests: + * For example function(params, success, failure) + */ + setCloudHandler: function(cloudHandler){ + self.cloudHandler = cloudHandler; + }, + + doCloudCall: function(params, success, failure) { + if(self.cloudHandler && typeof self.cloudHandler === "function" ){ + self.cloudHandler(params, success, failure); + } else { + console.log("Missing cloud handler for sync. Please refer to documentation"); + } + }, + + datasetMonitor: function() { + self.checkDatasets(); + + // Re-execute datasetMonitor every 500ms so we keep invoking checkDatasets(); + setTimeout(function() { + self.datasetMonitor(); + }, 500); + }, + + getStorageAdapter: function(dataset_id, isSave, cb){ + var onFail = function(msg, err){ + var errMsg = (isSave?'save to': 'load from' ) + ' local storage failed msg: ' + msg + ' err: ' + err; + self.doNotify(dataset_id, null, self.notifications.CLIENT_STORAGE_FAILED, errMsg); + self.consoleLog(errMsg); + }; + Lawnchair({fail:onFail, adapter: self.config.storage_strategy, size:self.config.file_system_quota, backup: self.config.icloud_backup}, function(){ + return cb(null, this); + }); + }, + + saveDataSet: function (dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + self.getStorageAdapter(dataset_id, true, function(err, storage){ + storage.save({key:"dataset_" + dataset_id, val:dataset}, function(){ + //save success + if(cb) { + return cb(); + } + }); + }); + }); + }, + + loadDataSet: function (dataset_id, success, failure) { + self.getStorageAdapter(dataset_id, false, function(err, storage){ + storage.get( "dataset_" + dataset_id, function (data){ + if (data && data.val) { + var dataset = data.val; + if(typeof dataset === "string"){ + dataset = JSON.parse(dataset); + } + // Datasets should not be auto initialised when loaded - the mange function should be called for each dataset + // the user wants sync + dataset.initialised = false; + self.datasets[dataset_id] = dataset; // TODO: do we need to handle binary data? + self.consoleLog('load from local storage success for dataset_id :' + dataset_id); + if(success) { + return success(dataset); + } + } else { + // no data yet, probably first time. failure calback should handle this + if(failure) { + return failure(); + } + } + }); + }); + }, + + clearCache: function(dataset_id, cb){ + delete self.datasets[dataset_id]; + self.notify_callback_map[dataset_id] = null; + self.getStorageAdapter(dataset_id, true, function(err, storage){ + storage.remove("dataset_" + dataset_id, function(){ + self.consoleLog('local cache is cleared for dataset : ' + dataset_id); + if(cb){ + return cb(); + } + }); + }); + }, + + updateDatasetFromLocal: function(dataset, pendingRec) { + var pending = dataset.pending; + var previousPendingUid; + var previousPending; + + var uid = pendingRec.uid; + self.consoleLog('updating local dataset for uid ' + uid + ' - action = ' + pendingRec.action); + + dataset.meta[uid] = dataset.meta[uid] || {}; + + // Creating a new record + if( pendingRec.action === "create" ) { + if( dataset.data[uid] ) { + self.consoleLog('dataset already exists for uid in create :: ' + JSON.stringify(dataset.data[uid])); + + // We are trying to do a create using a uid which already exists + if (dataset.meta[uid].fromPending) { + // We are trying to create on top of an existing pending record + // Remove the previous pending record and use this one instead + previousPendingUid = dataset.meta[uid].pendingUid; + delete pending[previousPendingUid]; + } + } + dataset.data[uid] = {}; + } + + if( pendingRec.action === "update" ) { + if( dataset.data[uid] ) { + if (dataset.meta[uid].fromPending) { + self.consoleLog('updating an existing pending record for dataset :: ' + JSON.stringify(dataset.data[uid])); + // We are trying to update an existing pending record + previousPendingUid = dataset.meta[uid].pendingUid; + previousPending = pending[previousPendingUid]; + if(previousPending) { + if(!previousPending.inFlight){ + self.consoleLog('existing pre-flight pending record = ' + JSON.stringify(previousPending)); + // We are trying to perform an update on an existing pending record + // modify the original record to have the latest value and delete the pending update + previousPending.post = pendingRec.post; + previousPending.postHash = pendingRec.postHash; + delete pending[pendingRec.hash]; + // Update the pending record to have the hash of the previous record as this is what is now being + // maintained in the pending array & is what we want in the meta record + pendingRec.hash = previousPendingUid; + } else { + //we are performing changes to a pending record which is inFlight. Until the status of this pending record is resolved, + //we should not submit this pending record to the cloud. Mark it as delayed. + self.consoleLog('existing in-inflight pending record = ' + JSON.stringify(previousPending)); + pendingRec.delayed = true; + pendingRec.waiting = previousPending.hash; + } + } + } + } + } + + if( pendingRec.action === "delete" ) { + if( dataset.data[uid] ) { + if (dataset.meta[uid].fromPending) { + self.consoleLog('Deleting an existing pending record for dataset :: ' + JSON.stringify(dataset.data[uid])); + // We are trying to delete an existing pending record + previousPendingUid = dataset.meta[uid].pendingUid; + previousPending = pending[previousPendingUid]; + if( previousPending ) { + if(!previousPending.inFlight){ + self.consoleLog('existing pending record = ' + JSON.stringify(previousPending)); + if( previousPending.action === "create" ) { + // We are trying to perform a delete on an existing pending create + // These cancel each other out so remove them both + delete pending[pendingRec.hash]; + delete pending[previousPendingUid]; + } + if( previousPending.action === "update" ) { + // We are trying to perform a delete on an existing pending update + // Use the pre value from the pending update for the delete and + // get rid of the pending update + pendingRec.pre = previousPending.pre; + pendingRec.preHash = previousPending.preHash; + pendingRec.inFlight = false; + delete pending[previousPendingUid]; + } + } else { + self.consoleLog('existing in-inflight pending record = ' + JSON.stringify(previousPending)); + pendingRec.delayed = true; + pendingRec.waiting = previousPending.hash; + } + } + } + delete dataset.data[uid]; + } + } + + if( dataset.data[uid] ) { + dataset.data[uid].data = pendingRec.post; + dataset.data[uid].hash = pendingRec.postHash; + dataset.meta[uid].fromPending = true; + dataset.meta[uid].pendingUid = pendingRec.hash; + } + }, + + updateCrashedInFlightFromNewData: function(dataset_id, dataset, newData) { + var updateNotifications = { + applied: self.notifications.REMOTE_UPDATE_APPLIED, + failed: self.notifications.REMOTE_UPDATE_FAILED, + collisions: self.notifications.COLLISION_DETECTED + }; + + var pending = dataset.pending; + var resolvedCrashes = {}; + var pendingHash; + var pendingRec; + + + if( pending ) { + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight && pendingRec.crashed) { + self.consoleLog('updateCrashedInFlightFromNewData - Found crashed inFlight pending record uid=' + pendingRec.uid + ' :: hash=' + pendingRec.hash ); + if( newData && newData.updates && newData.updates.hashes) { + + // Check if the updates received contain any info about the crashed in flight update + var crashedUpdate = newData.updates.hashes[pendingHash]; + if( !crashedUpdate ) { + //TODO: review this - why we need to wait? + // No word on our crashed update - increment a counter to reflect another sync that did not give us + // any update on our crashed record. + if( pendingRec.crashedCount ) { + pendingRec.crashedCount++; + } + else { + pendingRec.crashedCount = 1; + } + } + } + else { + // No word on our crashed update - increment a counter to reflect another sync that did not give us + // any update on our crashed record. + if( pendingRec.crashedCount ) { + pendingRec.crashedCount++; + } + else { + pendingRec.crashedCount = 1; + } + } + } + } + } + + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight && pendingRec.crashed) { + if( pendingRec.crashedCount > dataset.config.crashed_count_wait ) { + self.consoleLog('updateCrashedInFlightFromNewData - Crashed inflight pending record has reached crashed_count_wait limit : ' + JSON.stringify(pendingRec)); + self.consoleLog('updateCrashedInFlightFromNewData - Retryig crashed inflight pending record'); + pendingRec.crashed = false; + pendingRec.inFlight = false; + } + } + } + } + } + }, + + updateDelayedFromNewData: function(dataset_id, dataset, newData){ + var pending = dataset.pending; + var pendingHash; + var pendingRec; + if(pending){ + for( pendingHash in pending ){ + if( pending.hasOwnProperty(pendingHash) ){ + pendingRec = pending[pendingHash]; + if( pendingRec.delayed && pendingRec.waiting ){ + self.consoleLog('updateDelayedFromNewData - Found delayed pending record uid=' + pendingRec.uid + ' :: hash=' + pendingRec.hash + ' :: waiting=' + pendingRec.waiting); + if( newData && newData.updates && newData.updates.hashes ){ + var waitingRec = newData.updates.hashes[pendingRec.waiting]; + if(waitingRec){ + self.consoleLog('updateDelayedFromNewData - Waiting pending record is resolved rec=' + JSON.stringify(waitingRec)); + pendingRec.delayed = false; + pendingRec.waiting = undefined; + } + } + } + } + } + } + }, + + updateMetaFromNewData: function(dataset_id, dataset, newData){ + var meta = dataset.meta; + if(meta && newData && newData.updates && newData.updates.hashes){ + for(var uid in meta){ + if(meta.hasOwnProperty(uid)){ + var metadata = meta[uid]; + var pendingHash = metadata.pendingUid; + self.consoleLog("updateMetaFromNewData - Found metadata with uid = " + uid + " :: pendingHash = " + pendingHash); + var pendingResolved = true; + + if(pendingHash){ + //we have current pending in meta data, see if it's resolved + pendingResolved = false; + var hashresolved = newData.updates.hashes[pendingHash]; + if(hashresolved){ + self.consoleLog("updateMetaFromNewData - Found pendingUid in meta data resolved - resolved = " + JSON.stringify(hashresolved)); + //the current pending is resolved in the cloud + metadata.pendingUid = undefined; + pendingResolved = true; + } + } + + if(pendingResolved){ + self.consoleLog("updateMetaFromNewData - both previous and current pendings are resolved for meta data with uid " + uid + ". Delete it."); + //all pendings are resolved, the entry can be removed from meta data + delete meta[uid]; + } + } + } + } + }, + + + markInFlightAsCrashed : function(dataset) { + var pending = dataset.pending; + var pendingHash; + var pendingRec; + + if( pending ) { + var crashedRecords = {}; + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight ) { + self.consoleLog('Marking in flight pending record as crashed : ' + pendingHash); + pendingRec.crashed = true; + crashedRecords[pendingRec.uid] = pendingRec; + } + } + } + } + }, + + consoleLog: function(msg) { + if( self.config.do_console_log ) { + console.log(msg); + } + } +}; + +(function() { + self.config = self.defaults; + //Initialse the sync service with default config + //self.init({}); +})(); + +module.exports = { + init: self.init, + manage: self.manage, + notify: self.notify, + doList: self.list, + getUID: self.getUID, + doCreate: self.create, + doRead: self.read, + doUpdate: self.update, + doDelete: self['delete'], + listCollisions: self.listCollisions, + removeCollision: self.removeCollision, + getPending : self.getPending, + clearPending : self.clearPending, + getDataset : self.getDataSet, + getQueryParams: self.getQueryParams, + setQueryParams: self.setQueryParams, + getMetaData: self.getMetaData, + setMetaData: self.setMetaData, + getConfig: self.getConfig, + setConfig: self.setConfig, + startSync: self.startSync, + stopSync: self.stopSync, + doSync: self.doSync, + forceSync: self.forceSync, + generateHash: self.generateHash, + loadDataSet: self.loadDataSet, + clearCache: self.clearCache, + setCloudHandler: self.setCloudHandler +}; diff --git a/test/browser/fh-sync-latest-require.js b/test/browser/fh-sync-latest-require.js new file mode 100644 index 0000000..e985594 --- /dev/null +++ b/test/browser/fh-sync-latest-require.js @@ -0,0 +1,4641 @@ +require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else if (thatWords.length > 0xffff) { + // Copy one word at a time + for (var i = 0; i < thatSigBytes; i += 4) { + thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; + } + } else { + // Copy all words at once + thisWords.push.apply(thisWords, thatWords); + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + for (var i = 0; i < nBytes; i += 4) { + words.push((Math.random() * 0x100000000) | 0); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + var processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; +}(Math)); +/* + CryptoJS v3.1.2 + cipher-core + code.google.com/p/crypto-js + (c) 2009-2013 by Jeff Mott. All rights reserved. + code.google.com/p/crypto-js/wiki/License + */ +/** + * Cipher core components. + */ +CryptoJS.lib.Cipher || (function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var WordArray = C_lib.WordArray; + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; + var C_enc = C.enc; + var Utf8 = C_enc.Utf8; + var Base64 = C_enc.Base64; + var C_algo = C.algo; + var EvpKDF = C_algo.EvpKDF; + + /** + * Abstract base cipher template. + * + * @property {number} keySize This cipher's key size. Default: 4 (128 bits) + * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits) + * @property {number} _ENC_XFORM_MODE A constant representing encryption mode. + * @property {number} _DEC_XFORM_MODE A constant representing decryption mode. + */ + var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + * + * @property {WordArray} iv The IV to use for this operation. + */ + cfg: Base.extend(), + + /** + * Creates this cipher in encryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray }); + */ + createEncryptor: function (key, cfg) { + return this.create(this._ENC_XFORM_MODE, key, cfg); + }, + + /** + * Creates this cipher in decryption mode. + * + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {Cipher} A cipher instance. + * + * @static + * + * @example + * + * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray }); + */ + createDecryptor: function (key, cfg) { + return this.create(this._DEC_XFORM_MODE, key, cfg); + }, + + /** + * Initializes a newly created cipher. + * + * @param {number} xformMode Either the encryption or decryption transormation mode constant. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @example + * + * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray }); + */ + init: function (xformMode, key, cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Store transform mode and key + this._xformMode = xformMode; + this._key = key; + + // Set initial values + this.reset(); + }, + + /** + * Resets this cipher to its initial state. + * + * @example + * + * cipher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-cipher logic + this._doReset(); + }, + + /** + * Adds data to be encrypted or decrypted. + * + * @param {WordArray|string} dataUpdate The data to encrypt or decrypt. + * + * @return {WordArray} The data after processing. + * + * @example + * + * var encrypted = cipher.process('data'); + * var encrypted = cipher.process(wordArray); + */ + process: function (dataUpdate) { + // Append + this._append(dataUpdate); + + // Process available blocks + return this._process(); + }, + + /** + * Finalizes the encryption or decryption process. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt. + * + * @return {WordArray} The data after final processing. + * + * @example + * + * var encrypted = cipher.finalize(); + * var encrypted = cipher.finalize('data'); + * var encrypted = cipher.finalize(wordArray); + */ + finalize: function (dataUpdate) { + // Final data update + if (dataUpdate) { + this._append(dataUpdate); + } + + // Perform concrete-cipher logic + var finalProcessedData = this._doFinalize(); + + return finalProcessedData; + }, + + keySize: 128/32, + + ivSize: 128/32, + + _ENC_XFORM_MODE: 1, + + _DEC_XFORM_MODE: 2, + + /** + * Creates shortcut functions to a cipher's object interface. + * + * @param {Cipher} cipher The cipher to create a helper for. + * + * @return {Object} An object with encrypt and decrypt shortcut functions. + * + * @static + * + * @example + * + * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES); + */ + _createHelper: (function () { + function selectCipherStrategy(key) { + if (typeof key == 'string') { + return PasswordBasedCipher; + } else { + return SerializableCipher; + } + } + + return function (cipher) { + return { + encrypt: function (message, key, cfg) { + return selectCipherStrategy(key).encrypt(cipher, message, key, cfg); + }, + + decrypt: function (ciphertext, key, cfg) { + return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg); + } + }; + }; + }()) + }); + + /** + * Abstract base stream cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits) + */ + var StreamCipher = C_lib.StreamCipher = Cipher.extend({ + _doFinalize: function () { + // Process partial blocks + var finalProcessedBlocks = this._process(!!'flush'); + + return finalProcessedBlocks; + }, + + blockSize: 1 + }); + + /** + * Mode namespace. + */ + var C_mode = C.mode = {}; + + /** + * Abstract base block cipher mode template. + */ + var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({ + /** + * Creates this mode for encryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor: function (cipher, iv) { + return this.Encryptor.create(cipher, iv); + }, + + /** + * Creates this mode for decryption. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @static + * + * @example + * + * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words); + */ + createDecryptor: function (cipher, iv) { + return this.Decryptor.create(cipher, iv); + }, + + /** + * Initializes a newly created mode. + * + * @param {Cipher} cipher A block cipher instance. + * @param {Array} iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words); + */ + init: function (cipher, iv) { + this._cipher = cipher; + this._iv = iv; + } + }); + + /** + * Cipher Block Chaining mode. + */ + var CBC = C_mode.CBC = (function () { + /** + * Abstract base CBC mode. + */ + var CBC = BlockCipherMode.extend(); + + /** + * CBC encryptor. + */ + CBC.Encryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // XOR and encrypt + xorBlock.call(this, words, offset, blockSize); + cipher.encryptBlock(words, offset); + + // Remember this block to use with next block + this._prevBlock = words.slice(offset, offset + blockSize); + } + }); + + /** + * CBC decryptor. + */ + CBC.Decryptor = CBC.extend({ + /** + * Processes the data block at offset. + * + * @param {Array} words The data words to operate on. + * @param {number} offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock: function (words, offset) { + // Shortcuts + var cipher = this._cipher; + var blockSize = cipher.blockSize; + + // Remember this block to use with next block + var thisBlock = words.slice(offset, offset + blockSize); + + // Decrypt and XOR + cipher.decryptBlock(words, offset); + xorBlock.call(this, words, offset, blockSize); + + // This block becomes the previous block + this._prevBlock = thisBlock; + } + }); + + function xorBlock(words, offset, blockSize) { + // Shortcut + var iv = this._iv; + + // Choose mixing block + if (iv) { + var block = iv; + + // Remove IV for subsequent blocks + this._iv = undefined; + } else { + var block = this._prevBlock; + } + + // XOR blocks + for (var i = 0; i < blockSize; i++) { + words[offset + i] ^= block[i]; + } + } + + return CBC; + }()); + + /** + * Padding namespace. + */ + var C_pad = C.pad = {}; + + /** + * PKCS #5/7 padding strategy. + */ + var Pkcs7 = C_pad.Pkcs7 = { + /** + * Pads data using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to pad. + * @param {number} blockSize The multiple that the data should be padded to. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.pad(wordArray, 4); + */ + pad: function (data, blockSize) { + // Shortcut + var blockSizeBytes = blockSize * 4; + + // Count padding bytes + var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes; + + // Create padding word + var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes; + + // Create padding + var paddingWords = []; + for (var i = 0; i < nPaddingBytes; i += 4) { + paddingWords.push(paddingWord); + } + var padding = WordArray.create(paddingWords, nPaddingBytes); + + // Add padding + data.concat(padding); + }, + + /** + * Unpads data that had been padded using the algorithm defined in PKCS #5/7. + * + * @param {WordArray} data The data to unpad. + * + * @static + * + * @example + * + * CryptoJS.pad.Pkcs7.unpad(wordArray); + */ + unpad: function (data) { + // Get number of padding bytes from last byte + var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff; + + // Remove padding + data.sigBytes -= nPaddingBytes; + } + }; + + /** + * Abstract base block cipher template. + * + * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits) + */ + var BlockCipher = C_lib.BlockCipher = Cipher.extend({ + /** + * Configuration options. + * + * @property {Mode} mode The block mode to use. Default: CBC + * @property {Padding} padding The padding strategy to use. Default: Pkcs7 + */ + cfg: Cipher.cfg.extend({ + mode: CBC, + padding: Pkcs7 + }), + + reset: function () { + // Reset cipher + Cipher.reset.call(this); + + // Shortcuts + var cfg = this.cfg; + var iv = cfg.iv; + var mode = cfg.mode; + + // Reset block mode + if (this._xformMode == this._ENC_XFORM_MODE) { + var modeCreator = mode.createEncryptor; + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + var modeCreator = mode.createDecryptor; + + // Keep at least one block in the buffer for unpadding + this._minBufferSize = 1; + } + this._mode = modeCreator.call(mode, this, iv && iv.words); + }, + + _doProcessBlock: function (words, offset) { + this._mode.processBlock(words, offset); + }, + + _doFinalize: function () { + // Shortcut + var padding = this.cfg.padding; + + // Finalize + if (this._xformMode == this._ENC_XFORM_MODE) { + // Pad data + padding.pad(this._data, this.blockSize); + + // Process final blocks + var finalProcessedBlocks = this._process(!!'flush'); + } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ { + // Process final blocks + var finalProcessedBlocks = this._process(!!'flush'); + + // Unpad data + padding.unpad(finalProcessedBlocks); + } + + return finalProcessedBlocks; + }, + + blockSize: 128/32 + }); + + /** + * A collection of cipher parameters. + * + * @property {WordArray} ciphertext The raw ciphertext. + * @property {WordArray} key The key to this ciphertext. + * @property {WordArray} iv The IV used in the ciphering operation. + * @property {WordArray} salt The salt used with a key derivation function. + * @property {Cipher} algorithm The cipher algorithm. + * @property {Mode} mode The block mode used in the ciphering operation. + * @property {Padding} padding The padding scheme used in the ciphering operation. + * @property {number} blockSize The block size of the cipher. + * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string. + */ + var CipherParams = C_lib.CipherParams = Base.extend({ + /** + * Initializes a newly created cipher params object. + * + * @param {Object} cipherParams An object with any of the possible cipher parameters. + * + * @example + * + * var cipherParams = CryptoJS.lib.CipherParams.create({ + * ciphertext: ciphertextWordArray, + * key: keyWordArray, + * iv: ivWordArray, + * salt: saltWordArray, + * algorithm: CryptoJS.algo.AES, + * mode: CryptoJS.mode.CBC, + * padding: CryptoJS.pad.PKCS7, + * blockSize: 4, + * formatter: CryptoJS.format.OpenSSL + * }); + */ + init: function (cipherParams) { + this.mixIn(cipherParams); + }, + + /** + * Converts this cipher params object to a string. + * + * @param {Format} formatter (Optional) The formatting strategy to use. + * + * @return {string} The stringified cipher params. + * + * @throws Error If neither the formatter nor the default formatter is set. + * + * @example + * + * var string = cipherParams + ''; + * var string = cipherParams.toString(); + * var string = cipherParams.toString(CryptoJS.format.OpenSSL); + */ + toString: function (formatter) { + return (formatter || this.formatter).stringify(this); + } + }); + + /** + * Format namespace. + */ + var C_format = C.format = {}; + + /** + * OpenSSL formatting strategy. + */ + var OpenSSLFormatter = C_format.OpenSSL = { + /** + * Converts a cipher params object to an OpenSSL-compatible string. + * + * @param {CipherParams} cipherParams The cipher params object. + * + * @return {string} The OpenSSL-compatible string. + * + * @static + * + * @example + * + * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams); + */ + stringify: function (cipherParams) { + // Shortcuts + var ciphertext = cipherParams.ciphertext; + var salt = cipherParams.salt; + + // Format + if (salt) { + var wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext); + } else { + var wordArray = ciphertext; + } + + return wordArray.toString(Base64); + }, + + /** + * Converts an OpenSSL-compatible string to a cipher params object. + * + * @param {string} openSSLStr The OpenSSL-compatible string. + * + * @return {CipherParams} The cipher params object. + * + * @static + * + * @example + * + * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString); + */ + parse: function (openSSLStr) { + // Parse base64 + var ciphertext = Base64.parse(openSSLStr); + + // Shortcut + var ciphertextWords = ciphertext.words; + + // Test for salt + if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) { + // Extract salt + var salt = WordArray.create(ciphertextWords.slice(2, 4)); + + // Remove salt from ciphertext + ciphertextWords.splice(0, 4); + ciphertext.sigBytes -= 16; + } + + return CipherParams.create({ ciphertext: ciphertext, salt: salt }); + } + }; + + /** + * A cipher wrapper that returns ciphertext as a serializable cipher params object. + */ + var SerializableCipher = C_lib.SerializableCipher = Base.extend({ + /** + * Configuration options. + * + * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL + */ + cfg: Base.extend({ + format: OpenSSLFormatter + }), + + /** + * Encrypts a message. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv }); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Encrypt + var encryptor = cipher.createEncryptor(key, cfg); + var ciphertext = encryptor.finalize(message); + + // Shortcut + var cipherCfg = encryptor.cfg; + + // Create and return serializable cipher params + return CipherParams.create({ + ciphertext: ciphertext, + key: key, + iv: cipherCfg.iv, + algorithm: cipher, + mode: cipherCfg.mode, + padding: cipherCfg.padding, + blockSize: cipher.blockSize, + formatter: cfg.format + }); + }, + + /** + * Decrypts serialized ciphertext. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {WordArray} key The key. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, key, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Decrypt + var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext); + + return plaintext; + }, + + /** + * Converts serialized ciphertext to CipherParams, + * else assumed CipherParams already and returns ciphertext unchanged. + * + * @param {CipherParams|string} ciphertext The ciphertext. + * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext. + * + * @return {CipherParams} The unserialized ciphertext. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format); + */ + _parse: function (ciphertext, format) { + if (typeof ciphertext == 'string') { + return format.parse(ciphertext, this); + } else { + return ciphertext; + } + } + }); + + /** + * Key derivation function namespace. + */ + var C_kdf = C.kdf = {}; + + /** + * OpenSSL key derivation function. + */ + var OpenSSLKdf = C_kdf.OpenSSL = { + /** + * Derives a key and IV from a password. + * + * @param {string} password The password to derive from. + * @param {number} keySize The size in words of the key to generate. + * @param {number} ivSize The size in words of the IV to generate. + * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly. + * + * @return {CipherParams} A cipher params object with the key, IV, and salt. + * + * @static + * + * @example + * + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32); + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt'); + */ + execute: function (password, keySize, ivSize, salt) { + // Generate random salt + if (!salt) { + salt = WordArray.random(64/8); + } + + // Derive key and IV + var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt); + + // Separate key and IV + var iv = WordArray.create(key.words.slice(keySize), ivSize * 4); + key.sigBytes = keySize * 4; + + // Return params + return CipherParams.create({ key: key, iv: iv, salt: salt }); + } + }; + + /** + * A serializable cipher wrapper that derives the key from a password, + * and returns ciphertext as a serializable cipher params object. + */ + var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({ + /** + * Configuration options. + * + * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL + */ + cfg: SerializableCipher.cfg.extend({ + kdf: OpenSSLKdf + }), + + /** + * Encrypts a message using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {WordArray|string} message The message to encrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {CipherParams} A cipher params object. + * + * @static + * + * @example + * + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password'); + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL }); + */ + encrypt: function (cipher, message, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Encrypt + var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg); + + // Mix in derived params + ciphertext.mixIn(derivedParams); + + return ciphertext; + }, + + /** + * Decrypts serialized ciphertext using a password. + * + * @param {Cipher} cipher The cipher algorithm to use. + * @param {CipherParams|string} ciphertext The ciphertext to decrypt. + * @param {string} password The password. + * @param {Object} cfg (Optional) The configuration options to use for this operation. + * + * @return {WordArray} The plaintext. + * + * @static + * + * @example + * + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL }); + */ + decrypt: function (cipher, ciphertext, password, cfg) { + // Apply config defaults + cfg = this.cfg.extend(cfg); + + // Convert string to CipherParams + ciphertext = this._parse(ciphertext, cfg.format); + + // Derive key and other params + var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt); + + // Add IV to config + cfg.iv = derivedParams.iv; + + // Decrypt + var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg); + + return plaintext; + } + }); +}()); +/* + CryptoJS v3.1.2 + sha1.js + code.google.com/p/crypto-js + (c) 2009-2013 by Jeff Mott. All rights reserved. + code.google.com/p/crypto-js/wiki/License + */ +(function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Reusable object + var W = []; + + /** + * SHA-1 hash algorithm. + */ + var SHA1 = C_algo.SHA1 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476, + 0xc3d2e1f0 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + + // Computation + for (var i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (n << 1) | (n >>> 31); + } + + var t = ((a << 5) | (a >>> 27)) + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5a827999; + } else if (i < 40) { + t += (b ^ c ^ d) + 0x6ed9eba1; + } else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; + } else /* if (i < 80) */ { + t += (b ^ c ^ d) - 0x359d3e2a; + } + + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + C.SHA1 = Hasher._createHelper(SHA1); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + C.HmacSHA1 = Hasher._createHmacHelper(SHA1); +}()); +/* + CryptoJS v3.1.2 + x64-core.js + code.google.com/p/crypto-js + (c) 2009-2013 by Jeff Mott. All rights reserved. + code.google.com/p/crypto-js/wiki/License + */ +(function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var X32WordArray = C_lib.WordArray; + + /** + * x64 namespace. + */ + var C_x64 = C.x64 = {}; + + /** + * A 64-bit word. + */ + var X64Word = C_x64.Word = Base.extend({ + /** + * Initializes a newly created 64-bit word. + * + * @param {number} high The high 32 bits. + * @param {number} low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + init: function (high, low) { + this.high = high; + this.low = low; + } + + /** + * Bitwise NOTs this word. + * + * @return {X64Word} A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + // not: function () { + // var high = ~this.high; + // var low = ~this.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ANDs this word with the passed word. + * + * @param {X64Word} word The x64-Word to AND with this word. + * + * @return {X64Word} A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + // and: function (word) { + // var high = this.high & word.high; + // var low = this.low & word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to OR with this word. + * + * @return {X64Word} A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + // or: function (word) { + // var high = this.high | word.high; + // var low = this.low | word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise XORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to XOR with this word. + * + * @return {X64Word} A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + // xor: function (word) { + // var high = this.high ^ word.high; + // var low = this.low ^ word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the left. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + // shiftL: function (n) { + // if (n < 32) { + // var high = (this.high << n) | (this.low >>> (32 - n)); + // var low = this.low << n; + // } else { + // var high = this.low << (n - 32); + // var low = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the right. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + // shiftR: function (n) { + // if (n < 32) { + // var low = (this.low >>> n) | (this.high << (32 - n)); + // var high = this.high >>> n; + // } else { + // var low = this.high >>> (n - 32); + // var high = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Rotates this word n bits to the left. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + // rotL: function (n) { + // return this.shiftL(n).or(this.shiftR(64 - n)); + // }, + + /** + * Rotates this word n bits to the right. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + // rotR: function (n) { + // return this.shiftR(n).or(this.shiftL(64 - n)); + // }, + + /** + * Adds this word with the passed word. + * + * @param {X64Word} word The x64-Word to add with this word. + * + * @return {X64Word} A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + // add: function (word) { + // var low = (this.low + word.low) | 0; + // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0; + // var high = (this.high + word.high + carry) | 0; + + // return X64Word.create(high, low); + // } + }); + + /** + * An array of 64-bit words. + * + * @property {Array} words The array of CryptoJS.x64.Word objects. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var X64WordArray = C_x64.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 8; + } + }, + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32: function () { + // Shortcuts + var x64Words = this.words; + var x64WordsLength = x64Words.length; + + // Convert + var x32Words = []; + for (var i = 0; i < x64WordsLength; i++) { + var x64Word = x64Words[i]; + x32Words.push(x64Word.high); + x32Words.push(x64Word.low); + } + + return X32WordArray.create(x32Words, this.sigBytes); + }, + + /** + * Creates a copy of this word array. + * + * @return {X64WordArray} The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + + // Clone "words" array + var words = clone.words = this.words.slice(0); + + // Clone each X64Word object + var wordsLength = words.length; + for (var i = 0; i < wordsLength; i++) { + words[i] = words[i].clone(); + } + + return clone; + } + }); +}()); +; browserify_shim__define__module__export__(typeof CryptoJS != "undefined" ? CryptoJS : window.CryptoJS); + +}).call(global, undefined, undefined, undefined, undefined, function defineExport(ex) { module.exports = ex; }); + +}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],2:[function(require,module,exports){ +(function (global){ +;__browserify_shim_require__=require;(function browserifyShim(module, exports, require, define, browserify_shim__define__module__export__) { +/** + * Lawnchair! + * --- + * clientside json store + * + */ +var Lawnchair = function (options, callback) { + // ensure Lawnchair was called as a constructor + if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback); + + // lawnchair requires json + if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.' + // options are optional; callback is not + if (arguments.length <= 2 && arguments.length > 0) { + callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1]; + options = (typeof arguments[0] === 'function') ? {} : arguments[0]; + } else { + throw 'Incorrect # of ctor args!' + } + // TODO perhaps allow for pub/sub instead? + if (typeof callback !== 'function') throw 'No callback was provided'; + + // default configuration + this.record = options.record || 'record' // default for records + this.name = options.name || 'records' // default name for underlying store + + // mixin first valid adapter + var adapter + // if the adapter is passed in we try to load that only + if (options.adapter) { + + // the argument passed should be an array of prefered adapters + // if it is not, we convert it + if(typeof(options.adapter) === 'string'){ + options.adapter = [options.adapter]; + } + + // iterates over the array of passed adapters + for(var j = 0, k = options.adapter.length; j < k; j++){ + + // itirates over the array of available adapters + for (var i = Lawnchair.adapters.length-1; i >= 0; i--) { + if (Lawnchair.adapters[i].adapter === options.adapter[j]) { + adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined; + if (adapter) break + } + } + if (adapter) break + } + + // otherwise find the first valid adapter for this env + } + else { + for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { + adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined + if (adapter) break + } + } + + // we have failed + if (!adapter) throw 'No valid adapter.' + + // yay! mixin the adapter + for (var j in adapter) + this[j] = adapter[j] + + // call init for each mixed in plugin + for (var i = 0, l = Lawnchair.plugins.length; i < l; i++) + Lawnchair.plugins[i].call(this) + + // init the adapter + this.init(options, callback) +} + +Lawnchair.adapters = [] + +/** + * queues an adapter for mixin + * === + * - ensures an adapter conforms to a specific interface + * + */ +Lawnchair.adapter = function (id, obj) { + // add the adapter id to the adapter obj + // ugly here for a cleaner dsl for implementing adapters + obj['adapter'] = id + // methods required to implement a lawnchair adapter + var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ') + , indexOf = this.prototype.indexOf + // mix in the adapter + for (var i in obj) { + if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i + } + // if we made it this far the adapter interface is valid + // insert the new adapter as the preferred adapter + Lawnchair.adapters.splice(0,0,obj) +} + +Lawnchair.plugins = [] + +/** + * generic shallow extension for plugins + * === + * - if an init method is found it registers it to be called when the lawnchair is inited + * - yes we could use hasOwnProp but nobody here is an asshole + */ +Lawnchair.plugin = function (obj) { + for (var i in obj) + i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i] +} + +/** + * helpers + * + */ +Lawnchair.prototype = { + + isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' }, + + /** + * this code exists for ie8... for more background see: + * http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream + */ + indexOf: function(ary, item, i, l) { + if (ary.indexOf) return ary.indexOf(item) + for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i + return -1 + }, + + // awesome shorthand callbacks as strings. this is shameless theft from dojo. + lambda: function (callback) { + return this.fn(this.record, callback) + }, + + // first stab at named parameters for terse callbacks; dojo: first != best // ;D + fn: function (name, callback) { + return typeof callback == 'string' ? new Function(name, callback) : callback + }, + + // returns a unique identifier (by way of Backbone.localStorage.js) + // TODO investigate smaller UUIDs to cut on storage cost + uuid: function () { + var S4 = function () { + return (((1+Math.random())*0x10000)|0).toString(16).substring(1); + } + return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); + }, + + // a classic iterator + each: function (callback) { + var cb = this.lambda(callback) + // iterate from chain + if (this.__results) { + for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i) + } + // otherwise iterate the entire collection + else { + this.all(function(r) { + for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i) + }) + } + return this + } +// -- +}; +// window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser +Lawnchair.adapter('window-name', (function() { + if (typeof window==='undefined') { + window = { top: { } }; // node/optimizer compatibility + } + + // edited from the original here by elsigh + // Some sites store JSON data in window.top.name, but some folks (twitter on iPad) + // put simple strings in there - we should make sure not to cause a SyntaxError. + var data = {} + try { + data = JSON.parse(window.top.name) + } catch (e) {} + + + return { + + valid: function () { + return typeof window.top.name != 'undefined' + }, + + init: function (options, callback) { + data[this.name] = data[this.name] || {index:[],store:{}} + this.index = data[this.name].index + this.store = data[this.name].store + this.fn(this.name, callback).call(this, this) + return this + }, + + keys: function (callback) { + this.fn('keys', callback).call(this, this.index) + return this + }, + + save: function (obj, cb) { + // data[key] = value + ''; // force to string + // window.top.name = JSON.stringify(data); + var key = obj.key || this.uuid() + this.exists(key, function(exists) { + if (!exists) { + if (obj.key) delete obj.key + this.index.push(key) + } + this.store[key] = obj + + try { + window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter + } catch(e) { + // restore index/store to previous value before JSON exception + if (!exists) { + this.index.pop(); + delete this.store[key]; + } + throw e; + } + + if (cb) { + obj.key = key + this.lambda(cb).call(this, obj) + } + }) + return this + }, + + batch: function (objs, cb) { + var r = [] + for (var i = 0, l = objs.length; i < l; i++) { + this.save(objs[i], function(record) { + r.push(record) + }) + } + if (cb) this.lambda(cb).call(this, r) + return this + }, + + get: function (keyOrArray, cb) { + var r; + if (this.isArray(keyOrArray)) { + r = [] + for (var i = 0, l = keyOrArray.length; i < l; i++) { + r.push(this.store[keyOrArray[i]]) + } + } else { + r = this.store[keyOrArray] + if (r) r.key = keyOrArray + } + if (cb) this.lambda(cb).call(this, r) + return this + }, + + exists: function (key, cb) { + this.lambda(cb).call(this, !!(this.store[key])) + return this + }, + + all: function (cb) { + var r = [] + for (var i = 0, l = this.index.length; i < l; i++) { + var obj = this.store[this.index[i]] + obj.key = this.index[i] + r.push(obj) + } + this.fn(this.name, cb).call(this, r) + return this + }, + + remove: function (keyOrArray, cb) { + var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] + for (var i = 0, l = del.length; i < l; i++) { + var key = del[i].key ? del[i].key : del[i] + var where = this.indexOf(this.index, key) + if (where < 0) continue /* key not present */ + delete this.store[key] + this.index.splice(where, 1) + } + window.top.name = JSON.stringify(data) + if (cb) this.lambda(cb).call(this) + return this + }, + + nuke: function (cb) { + this.store = data[this.name].store = {} + this.index = data[this.name].index = [] + window.top.name = JSON.stringify(data) + if (cb) this.lambda(cb).call(this) + return this + } + } +///// +})()) +/** + * dom storage adapter + * === + * - originally authored by Joseph Pecoraro + * + */ +// +// TODO does it make sense to be chainable all over the place? +// chainable: nuke, remove, all, get, save, all +// not chainable: valid, keys +// +Lawnchair.adapter('dom', (function() { + var storage = null; + try{ + storage = window.localStorage; + }catch(e){ + + } + // the indexer is an encapsulation of the helpers needed to keep an ordered index of the keys + var indexer = function(name) { + return { + // the key + key: name + '._index_', + // returns the index + all: function() { + var a = storage.getItem(this.key) + if (a) { + a = JSON.parse(a) + } + if (a === null) storage.setItem(this.key, JSON.stringify([])) // lazy init + return JSON.parse(storage.getItem(this.key)) + }, + // adds a key to the index + add: function (key) { + var a = this.all() + a.push(key) + storage.setItem(this.key, JSON.stringify(a)) + }, + // deletes a key from the index + del: function (key) { + var a = this.all(), r = [] + // FIXME this is crazy inefficient but I'm in a strata meeting and half concentrating + for (var i = 0, l = a.length; i < l; i++) { + if (a[i] != key) r.push(a[i]) + } + storage.setItem(this.key, JSON.stringify(r)) + }, + // returns index for a key + find: function (key) { + var a = this.all() + for (var i = 0, l = a.length; i < l; i++) { + if (key === a[i]) return i + } + return false + } + } + } + + // adapter api + return { + + // ensure we are in an env with localStorage + valid: function () { + return !!storage && function() { + // in mobile safari if safe browsing is enabled, window.storage + // is defined but setItem calls throw exceptions. + var success = true + var value = Math.random() + try { + storage.setItem(value, value) + } catch (e) { + success = false + } + storage.removeItem(value) + return success + }() + }, + + init: function (options, callback) { + this.indexer = indexer(this.name) + if (callback) this.fn(this.name, callback).call(this, this) + }, + + save: function (obj, callback) { + var key = obj.key ? this.name + '.' + obj.key : this.name + '.' + this.uuid() + // now we kil the key and use it in the store colleciton + delete obj.key; + storage.setItem(key, JSON.stringify(obj)) + // if the key is not in the index push it on + if (this.indexer.find(key) === false) this.indexer.add(key) + obj.key = key.slice(this.name.length + 1) + if (callback) { + this.lambda(callback).call(this, obj) + } + return this + }, + + batch: function (ary, callback) { + var saved = [] + // not particularily efficient but this is more for sqlite situations + for (var i = 0, l = ary.length; i < l; i++) { + this.save(ary[i], function(r){ + saved.push(r) + }) + } + if (callback) this.lambda(callback).call(this, saved) + return this + }, + + // accepts [options], callback + keys: function(callback) { + if (callback) { + var name = this.name + var indices = this.indexer.all(); + var keys = []; + //Checking for the support of map. + if(Array.prototype.map) { + keys = indices.map(function(r){ return r.replace(name + '.', '') }) + } else { + for (var key in indices) { + keys.push(key.replace(name + '.', '')); + } + } + this.fn('keys', callback).call(this, keys) + } + return this // TODO options for limit/offset, return promise + }, + + get: function (key, callback) { + if (this.isArray(key)) { + var r = [] + for (var i = 0, l = key.length; i < l; i++) { + var k = this.name + '.' + key[i] + var obj = storage.getItem(k) + if (obj) { + obj = JSON.parse(obj) + obj.key = key[i] + } + r.push(obj) + } + if (callback) this.lambda(callback).call(this, r) + } else { + var k = this.name + '.' + key + var obj = storage.getItem(k) + if (obj) { + obj = JSON.parse(obj) + obj.key = key + } + if (callback) this.lambda(callback).call(this, obj) + } + return this + }, + + exists: function (key, cb) { + var exists = this.indexer.find(this.name+'.'+key) === false ? false : true ; + this.lambda(cb).call(this, exists); + return this; + }, + // NOTE adapters cannot set this.__results but plugins do + // this probably should be reviewed + all: function (callback) { + var idx = this.indexer.all() + , r = [] + , o + , k + for (var i = 0, l = idx.length; i < l; i++) { + k = idx[i] //v + o = JSON.parse(storage.getItem(k)) + o.key = k.replace(this.name + '.', '') + r.push(o) + } + if (callback) this.fn(this.name, callback).call(this, r) + return this + }, + + remove: function (keyOrArray, callback) { + var self = this; + if (this.isArray(keyOrArray)) { + // batch remove + var i, done = keyOrArray.length; + var removeOne = function(i) { + self.remove(keyOrArray[i], function() { + if ((--done) > 0) { return; } + if (callback) { + self.lambda(callback).call(self); + } + }); + }; + for (i=0; i < keyOrArray.length; i++) + removeOne(i); + return this; + } + var key = this.name + '.' + + ((keyOrArray.key) ? keyOrArray.key : keyOrArray) + this.indexer.del(key) + storage.removeItem(key) + if (callback) this.lambda(callback).call(this) + return this + }, + + nuke: function (callback) { + this.all(function(r) { + for (var i = 0, l = r.length; i < l; i++) { + this.remove(r[i]); + } + if (callback) this.lambda(callback).call(this) + }) + return this + } + }})()); +Lawnchair.adapter('webkit-sqlite', (function() { + // private methods + var fail = function(e, i) { + if (console) { + console.log('error in sqlite adaptor!', e, i) + } + }, now = function() { + return new Date() + } // FIXME need to use better date fn + // not entirely sure if this is needed... + + // public methods + return { + + valid: function() { + return !!(window.openDatabase) + }, + + init: function(options, callback) { + var that = this, + cb = that.fn(that.name, callback), + create = "CREATE TABLE IF NOT EXISTS " + this.record + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)", + win = function() { + return cb.call(that, that); + } + // open a connection and create the db if it doesn't exist + //FEEDHENRY CHANGE TO ALLOW ERROR CALLBACK + if (options && 'function' === typeof options.fail) fail = options.fail + //END CHANGE + this.db = openDatabase(this.name, '1.0.0', this.name, 65536) + this.db.transaction(function(t) { + t.executeSql(create, [], win, fail) + }) + }, + + keys: function(callback) { + var cb = this.lambda(callback), + that = this, + keys = "SELECT id FROM " + this.record + " ORDER BY timestamp DESC" + + this.db.readTransaction(function(t) { + var win = function(xxx, results) { + if (results.rows.length == 0) { + cb.call(that, []) + } else { + var r = []; + for (var i = 0, l = results.rows.length; i < l; i++) { + r.push(results.rows.item(i).id); + } + cb.call(that, r) + } + } + t.executeSql(keys, [], win, fail) + }) + return this + }, + // you think thats air you're breathing now? + save: function(obj, callback, error) { + var that = this + objs = (this.isArray(obj) ? obj : [obj]).map(function(o) { + if (!o.key) { + o.key = that.uuid() + } + return o + }), + ins = "INSERT OR REPLACE INTO " + this.record + " (value, timestamp, id) VALUES (?,?,?)", + win = function() { + if (callback) { + that.lambda(callback).call(that, that.isArray(obj) ? objs : objs[0]) + } + }, error = error || function() {}, insvals = [], + ts = now() + + try { + for (var i = 0, l = objs.length; i < l; i++) { + insvals[i] = [JSON.stringify(objs[i]), ts, objs[i].key]; + } + } catch (e) { + fail(e) + throw e; + } + + that.db.transaction(function(t) { + for (var i = 0, l = objs.length; i < l; i++) + t.executeSql(ins, insvals[i]) + }, function(e, i) { + fail(e, i) + }, win) + + return this + }, + + + batch: function(objs, callback) { + return this.save(objs, callback) + }, + + get: function(keyOrArray, cb) { + var that = this, + sql = '', + args = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray]; + // batch selects support + sql = 'SELECT id, value FROM ' + this.record + " WHERE id IN (" + + args.map(function() { + return '?' + }).join(",") + ")" + // FIXME + // will always loop the results but cleans it up if not a batch return at the end.. + // in other words, this could be faster + var win = function(xxx, results) { + var o, r, lookup = {} + // map from results to keys + for (var i = 0, l = results.rows.length; i < l; i++) { + o = JSON.parse(results.rows.item(i).value) + o.key = results.rows.item(i).id + lookup[o.key] = o; + } + r = args.map(function(key) { + return lookup[key]; + }); + if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null + if (cb) that.lambda(cb).call(that, r) + } + this.db.readTransaction(function(t) { + t.executeSql(sql, args, win, fail) + }) + return this + }, + + exists: function(key, cb) { + var is = "SELECT * FROM " + this.record + " WHERE id = ?", + that = this, + win = function(xxx, results) { + if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) + } + this.db.readTransaction(function(t) { + t.executeSql(is, [key], win, fail) + }) + return this + }, + + all: function(callback) { + var that = this, + all = "SELECT * FROM " + this.record, + r = [], + cb = this.fn(this.name, callback) || undefined, + win = function(xxx, results) { + if (results.rows.length != 0) { + for (var i = 0, l = results.rows.length; i < l; i++) { + var obj = JSON.parse(results.rows.item(i).value) + obj.key = results.rows.item(i).id + r.push(obj) + } + } + if (cb) cb.call(that, r) + } + + this.db.readTransaction(function(t) { + t.executeSql(all, [], win, fail) + }) + return this + }, + + remove: function(keyOrArray, cb) { + var that = this, + args, sql = "DELETE FROM " + this.record + " WHERE id ", + win = function() { + if (cb) that.lambda(cb).call(that) + } + if (!this.isArray(keyOrArray)) { + sql += '= ?'; + args = [keyOrArray]; + } else { + args = keyOrArray; + sql += "IN (" + + args.map(function() { + return '?' + }).join(',') + + ")"; + } + args = args.map(function(obj) { + return obj.key ? obj.key : obj; + }); + + this.db.transaction(function(t) { + t.executeSql(sql, args, win, fail); + }); + + return this; + }, + + nuke: function(cb) { + var nuke = "DELETE FROM " + this.record, + that = this, + win = cb ? function() { + that.lambda(cb).call(that) + } : function() {} + this.db.transaction(function(t) { + t.executeSql(nuke, [], win, fail) + }) + return this + } + } +})()); +Lawnchair.adapter('indexed-db', (function(){ + + function fail(e, i) { + if(console) { console.log('error in indexed-db adapter!' + e.message, e, i); debugger;} + } ; + + function getIDB(){ + return window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.oIndexedDB || window.msIndexedDB; + }; + + + + return { + + valid: function() { return !!getIDB(); }, + + init:function(options, callback) { + this.idb = getIDB(); + this.waiting = []; + var request = this.idb.open(this.name, 2); + var self = this; + var cb = self.fn(self.name, callback); + var win = function(){ return cb.call(self, self); } + //FEEDHENRY CHANGE TO ALLOW ERROR CALLBACK + if(options && 'function' === typeof options.fail) fail = options.fail + //END CHANGE + request.onupgradeneeded = function(event){ + self.store = request.result.createObjectStore("teststore", { autoIncrement: true} ); + for (var i = 0; i < self.waiting.length; i++) { + self.waiting[i].call(self); + } + self.waiting = []; + win(); + } + + request.onsuccess = function(event) { + self.db = request.result; + + + if(self.db.version != "2.0") { + if(typeof self.db.setVersion == 'function'){ + + var setVrequest = self.db.setVersion("2.0"); + // onsuccess is the only place we can create Object Stores + setVrequest.onsuccess = function(e) { + self.store = self.db.createObjectStore("teststore", { autoIncrement: true} ); + for (var i = 0; i < self.waiting.length; i++) { + self.waiting[i].call(self); + } + self.waiting = []; + win(); + }; + setVrequest.onerror = function(e) { + // console.log("Failed to create objectstore " + e); + fail(e); + } + + } + } else { + self.store = {}; + for (var i = 0; i < self.waiting.length; i++) { + self.waiting[i].call(self); + } + self.waiting = []; + win(); + } + } + request.onerror = fail; + }, + + save:function(obj, callback) { + if(!this.store) { + this.waiting.push(function() { + this.save(obj, callback); + }); + return; + } + + var self = this; + var win = function (e) { if (callback) { obj.key = e.target.result; self.lambda(callback).call(self, obj) }}; + var accessType = "readwrite"; + var trans = this.db.transaction(["teststore"],accessType); + var store = trans.objectStore("teststore"); + var request = obj.key ? store.put(obj, obj.key) : store.put(obj); + + request.onsuccess = win; + request.onerror = fail; + + return this; + }, + + // FIXME this should be a batch insert / just getting the test to pass... + batch: function (objs, cb) { + + var results = [] + , done = false + , self = this + + var updateProgress = function(obj) { + results.push(obj) + done = results.length === objs.length + } + + var checkProgress = setInterval(function() { + if (done) { + if (cb) self.lambda(cb).call(self, results) + clearInterval(checkProgress) + } + }, 200) + + for (var i = 0, l = objs.length; i < l; i++) + this.save(objs[i], updateProgress) + + return this + }, + + + get:function(key, callback) { + if(!this.store || !this.db) { + this.waiting.push(function() { + this.get(key, callback); + }); + return; + } + + + var self = this; + var win = function (e) { if (callback) { self.lambda(callback).call(self, e.target.result) }}; + + + if (!this.isArray(key)){ + var req = this.db.transaction("teststore").objectStore("teststore").get(key); + + req.onsuccess = win; + req.onerror = function(event) { + //console.log("Failed to find " + key); + fail(event); + }; + + // FIXME: again the setInterval solution to async callbacks.. + } else { + + // note: these are hosted. + var results = [] + , done = false + , keys = key + + var updateProgress = function(obj) { + results.push(obj) + done = results.length === keys.length + } + + var checkProgress = setInterval(function() { + if (done) { + if (callback) self.lambda(callback).call(self, results) + clearInterval(checkProgress) + } + }, 200) + + for (var i = 0, l = keys.length; i < l; i++) + this.get(keys[i], updateProgress) + + } + + return this; + }, + + all:function(callback) { + if(!this.store) { + this.waiting.push(function() { + this.all(callback); + }); + return; + } + var cb = this.fn(this.name, callback) || undefined; + var self = this; + var objectStore = this.db.transaction("teststore").objectStore("teststore"); + var toReturn = []; + objectStore.openCursor().onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + toReturn.push(cursor.value); + cursor.continue(); + } + else { + if (cb) cb.call(self, toReturn); + } + }; + return this; + }, + + remove:function(keyOrObj, callback) { + if(!this.store) { + this.waiting.push(function() { + this.remove(keyOrObj, callback); + }); + return; + } + if (typeof keyOrObj == "object") { + keyOrObj = keyOrObj.key; + } + var self = this; + var win = function () { if (callback) self.lambda(callback).call(self) }; + + var request = this.db.transaction(["teststore"], "readwrite").objectStore("teststore").delete(keyOrObj); + request.onsuccess = win; + request.onerror = fail; + return this; + }, + + nuke:function(callback) { + if(!this.store) { + this.waiting.push(function() { + this.nuke(callback); + }); + return; + } + + var self = this + , win = callback ? function() { self.lambda(callback).call(self) } : function(){}; + + try { + this.db + .transaction(["teststore"], "readwrite") + .objectStore("teststore").clear().onsuccess = win; + + } catch(e) { + fail(); + } + return this; + } + + }; + +})()); +Lawnchair.adapter('html5-filesystem', (function(global){ + + var fail = function( e ) { + if ( console ) console.error(e, e.name); + }; + + var ls = function( reader, callback, entries ) { + var result = entries || []; + reader.readEntries(function( results ) { + if ( !results.length ) { + if ( callback ) callback( result.map(function(entry) { return entry.name; }) ); + } else { + ls( reader, callback, result.concat( Array.prototype.slice.call( results ) ) ); + } + }, fail ); + }; + + var filesystems = {}; + + var root = function( store, callback ) { + var directory = filesystems[store.name]; + if ( directory ) { + callback( directory ); + } else { + setTimeout(function() { + root( store, callback ); + }, 10 ); + } + }; + + var isPhoneGap = function() { + //http://stackoverflow.com/questions/10347539/detect-between-a-mobile-browser-or-a-phonegap-application + //may break. + var app = document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1; + if (app) { + return true; + } else { + return false; + } + } + + var createBlobOrString = function(contentstr) { + var retVal; + if (isPhoneGap()) { // phonegap filewriter works with strings, later versions also work with binary arrays, and if passed a blob will just convert to binary array anyway + retVal = contentstr; + } else { + var targetContentType = 'application/json'; + try { + retVal = new Blob( [contentstr], { type: targetContentType }); // Blob doesn't exist on all androids + } + catch (e){ + // TypeError old chrome and FF + var blobBuilder = window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder; + if (e.name == 'TypeError' && blobBuilder) { + var bb = new blobBuilder(); + bb.append([contentstr.buffer]); + retVal = bb.getBlob(targetContentType); + } else { + // We can't make a Blob, so just return the stringified content + retVal = contentstr; + } + } + } + return retVal; + } + + return { + // boolean; true if the adapter is valid for the current environment + valid: function() { + var fs = global.requestFileSystem || global.webkitRequestFileSystem || global.moz_requestFileSystem; + return !!fs; + }, + + // constructor call and callback. 'name' is the most common option + init: function( options, callback ) { + var me = this; + var error = function(e) { fail(e); if ( callback ) me.fn( me.name, callback ).call( me, me ); }; + var size = options.size || 100*1024*1024; + var name = this.name; + //disable file backup to icloud + me.backup = false; + if(typeof options.backup !== 'undefined'){ + me.backup = options.backup; + } + + function requestFileSystem(amount) { +// console.log('in requestFileSystem'); + var fs = global.requestFileSystem || global.webkitRequestFileSystem || global.moz_requestFileSystem; + var mode = window.PERSISTENT; + if(typeof LocalFileSystem !== "undefined" && typeof LocalFileSystem.PERSISTENT !== "undefined"){ + mode = LocalFileSystem.PERSISTENT; + } + fs(mode, amount, function(fs) { +// console.log('got FS ', fs); + fs.root.getDirectory( name, {create:true}, function( directory ) { +// console.log('got DIR ', directory); + filesystems[name] = directory; + if ( callback ) me.fn( me.name, callback ).call( me, me ); + }, function( e ) { +// console.log('error getting dir :: ', e); + error(e); + }); + }, function( e ) { +// console.log('error getting FS :: ', e); + error(e); + }); + }; + + // When in the browser we need to use the html5 file system rather than + // the one cordova supplies, but it needs to request a quota first. + if (typeof navigator.webkitPersistentStorage !== 'undefined') { + navigator.webkitPersistentStorage.requestQuota(size, requestFileSystem, function() { + logger.warn('User declined file storage'); + error('User declined file storage'); + }); + } else { + // Amount is 0 because we pretty much have free reign over the + // amount of storage we use on an android device. + requestFileSystem(0); + } + }, + + // returns all the keys in the store + keys: function( callback ) { + var me = this; + root( this, function( store ) { + ls( store.createReader(), function( entries ) { + if ( callback ) me.fn( 'keys', callback ).call( me, entries ); + }); + }); + return this; + }, + + // save an object + save: function( obj, callback ) { + var me = this; + var key = obj.key || this.uuid(); + obj.key = key; + var error = function(e) { fail(e); if ( callback ) me.lambda( callback ).call( me ); }; + root( this, function( store ) { + var writeContent = function(file, error){ + file.createWriter(function( writer ) { + writer.onerror = error; + writer.onwriteend = function() { + // Clear the onWriteEnd handler so the truncate does not call it and cause an infinite loop + this.onwriteend = null; + // Truncate the file at the end of the written contents. This ensures that if we are updating + // a file which was previously longer, we will not be left with old contents beyond the end of + // the current buffer. + this.truncate(this.position); + if ( callback ) me.lambda( callback ).call( me, obj ); + }; + var contentStr = JSON.stringify(obj); + + var writerContent = createBlobOrString(contentStr); + writer.write(writerContent); + }, error ); + } + store.getFile( key, {create:true}, function( file ) { + if(typeof file.setMetadata === 'function' && (me.backup === false || me.backup === 'false')){ + //set meta data on the file to make sure it won't be backed up by icloud + file.setMetadata(function(){ + writeContent(file, error); + }, function(){ + writeContent(file, error); + }, {'com.apple.MobileBackup': 1}); + } else { + writeContent(file, error); + } + }, error ); + }); + return this; + }, + + // batch save array of objs + batch: function( objs, callback ) { + var me = this; + var saved = []; + for ( var i = 0, il = objs.length; i < il; i++ ) { + me.save( objs[i], function( obj ) { + saved.push( obj ); + if ( saved.length === il && callback ) { + me.lambda( callback ).call( me, saved ); + } + }); + } + return this; + }, + + // retrieve obj (or array of objs) and apply callback to each + get: function( key /* or array */, callback ) { + var me = this; + if ( this.isArray( key ) ) { + var values = []; + for ( var i = 0, il = key.length; i < il; i++ ) { + me.get( key[i], function( result ) { + if ( result ) values.push( result ); + if ( values.length === il && callback ) { + me.lambda( callback ).call( me, values ); + } + }); + } + } else { + var error = function(e) { + fail( e ); + if ( callback ) { + me.lambda( callback ).call( me ); + } + }; + root( this, function( store ) { + store.getFile( key, {create:false}, function( entry ) { + entry.file(function( file ) { + var reader = new FileReader(); + + reader.onerror = error; + + reader.onload = function(e) { + var res = {}; + try { + res = JSON.parse( e.target.result); + res.key = key; + } catch (e) { + res = {key:key}; + } + if ( callback ) me.lambda( callback ).call( me, res ); + }; + + reader.readAsText( file ); + }, error ); + }, error ); + }); + } + return this; + }, + + // check if an obj exists in the collection + exists: function( key, callback ) { + var me = this; + root( this, function( store ) { + store.getFile( key, {create:false}, function() { + if ( callback ) me.lambda( callback ).call( me, true ); + }, function() { + if ( callback ) me.lambda( callback ).call( me, false ); + }); + }); + return this; + }, + + // returns all the objs to the callback as an array + all: function( callback ) { + var me = this; + if ( callback ) { + this.keys(function( keys ) { + if ( !keys.length ) { + me.fn( me.name, callback ).call( me, [] ); + } else { + me.get( keys, function( values ) { + me.fn( me.name, callback ).call( me, values ); + }); + } + }); + } + return this; + }, + + // remove a doc or collection of em + remove: function( key /* or object */, callback ) { + var me = this; + var error = function(e) { fail( e ); if ( callback ) me.lambda( callback ).call( me ); }; + root( this, function( store ) { + store.getFile( (typeof key === 'string' ? key : key.key ), {create:false}, function( file ) { + file.remove(function() { + if ( callback ) me.lambda( callback ).call( me ); + }, error ); + }, error ); + }); + return this; + }, + + // destroy everything + nuke: function( callback ) { + var me = this; + var count = 0; + this.keys(function( keys ) { + if ( !keys.length ) { + if ( callback ) me.lambda( callback ).call( me ); + } else { + for ( var i = 0, il = keys.length; i < il; i++ ) { + me.remove( keys[i], function() { + count++; + if ( count === il && callback ) { + me.lambda( callback ).call( me ); + } + }); + } + } + }); + return this; + } + }; +}(this))); + +Lawnchair.adapter('memory', (function(){ + + var data = {} + + return { + valid: function() { return true }, + + init: function (options, callback) { + data[this.name] = data[this.name] || {index:[],store:{}} + this.index = data[this.name].index + this.store = data[this.name].store + var cb = this.fn(this.name, callback) + if (cb) cb.call(this, this) + return this + }, + + keys: function (callback) { + this.fn('keys', callback).call(this, this.index) + return this + }, + + save: function(obj, cb) { + var key = obj.key || this.uuid() + + this.exists(key, function(exists) { + if (!exists) { + if (obj.key) delete obj.key + this.index.push(key) + } + + this.store[key] = obj + + if (cb) { + obj.key = key + this.lambda(cb).call(this, obj) + } + }) + + return this + }, + + batch: function (objs, cb) { + var r = [] + for (var i = 0, l = objs.length; i < l; i++) { + this.save(objs[i], function(record) { + r.push(record) + }) + } + if (cb) this.lambda(cb).call(this, r) + return this + }, + + get: function (keyOrArray, cb) { + var r; + if (this.isArray(keyOrArray)) { + r = [] + for (var i = 0, l = keyOrArray.length; i < l; i++) { + r.push(this.store[keyOrArray[i]]) + } + } else { + r = this.store[keyOrArray] + if (r) r.key = keyOrArray + } + if (cb) this.lambda(cb).call(this, r) + return this + }, + + exists: function (key, cb) { + this.lambda(cb).call(this, !!(this.store[key])) + return this + }, + + all: function (cb) { + var r = [] + for (var i = 0, l = this.index.length; i < l; i++) { + var obj = this.store[this.index[i]] + obj.key = this.index[i] + r.push(obj) + } + this.fn(this.name, cb).call(this, r) + return this + }, + + remove: function (keyOrArray, cb) { + var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray] + for (var i = 0, l = del.length; i < l; i++) { + var key = del[i].key ? del[i].key : del[i] + var where = this.indexOf(this.index, key) + if (where < 0) continue /* key not present */ + delete this.store[key] + this.index.splice(where, 1) + } + if (cb) this.lambda(cb).call(this) + return this + }, + + nuke: function (cb) { + this.store = data[this.name].store = {} + this.index = data[this.name].index = [] + if (cb) this.lambda(cb).call(this) + return this + } + } +///// +})()); +; browserify_shim__define__module__export__(typeof Lawnchair != "undefined" ? Lawnchair : window.Lawnchair); + +}).call(global, undefined, undefined, undefined, undefined, function defineExport(ex) { module.exports = ex; }); + +}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],3:[function(require,module,exports){ +var api_sync = require("./sync-client"); + +// Mounting into global fh namespace +var fh = window.$fh = window.$fh || {}; +fh.sync = api_sync; +},{"./sync-client":"93xVCx"}],"93xVCx":[function(require,module,exports){ +var CryptoJS = require("../libs/generated/crypto"); +var Lawnchair = require('../libs/generated/lawnchair'); + +var self = { + + // CONFIG + defaults: { + "sync_frequency": 10, + // How often to synchronise data with the cloud in seconds. + "auto_sync_local_updates": true, + // Should local chages be syned to the cloud immediately, or should they wait for the next sync interval + "notify_client_storage_failed": true, + // Should a notification event be triggered when loading/saving to client storage fails + "notify_sync_started": true, + // Should a notification event be triggered when a sync cycle with the server has been started + "notify_sync_complete": true, + // Should a notification event be triggered when a sync cycle with the server has been completed + "notify_offline_update": true, + // Should a notification event be triggered when an attempt was made to update a record while offline + "notify_collision_detected": true, + // Should a notification event be triggered when an update failed due to data collision + "notify_remote_update_failed": true, + // Should a notification event be triggered when an update failed for a reason other than data collision + "notify_local_update_applied": true, + // Should a notification event be triggered when an update was applied to the local data store + "notify_remote_update_applied": true, + // Should a notification event be triggered when an update was applied to the remote data store + "notify_delta_received": true, + // Should a notification event be triggered when a delta was received from the remote data store for the dataset + "notify_record_delta_received": true, + // Should a notification event be triggered when a delta was received from the remote data store for a record + "notify_sync_failed": true, + // Should a notification event be triggered when the sync loop failed to complete + "do_console_log": false, + // Should log statements be written to console.log + "crashed_count_wait" : 10, + // How many syncs should we check for updates on crashed in flight updates before we give up searching + "resend_crashed_updates" : true, + // If we have reached the crashed_count_wait limit, should we re-try sending the crashed in flight pending record + "sync_active" : true, + // Is the background sync with the cloud currently active + "storage_strategy" : "html5-filesystem", + // Storage strategy to use for Lawnchair - supported strategies are 'html5-filesystem' and 'dom' + "file_system_quota" : 50 * 1024 * 1204, + // Amount of space to request from the HTML5 filesystem API when running in browser + "icloud_backup" : false //ios only. If set to true, the file will be backed by icloud + }, + + notifications: { + "CLIENT_STORAGE_FAILED": "client_storage_failed", + // loading/saving to client storage failed + "SYNC_STARTED": "sync_started", + // A sync cycle with the server has been started + "SYNC_COMPLETE": "sync_complete", + // A sync cycle with the server has been completed + "OFFLINE_UPDATE": "offline_update", + // An attempt was made to update a record while offline + "COLLISION_DETECTED": "collision_detected", + //Update Failed due to data collision + "REMOTE_UPDATE_FAILED": "remote_update_failed", + // Update Failed for a reason other than data collision + "REMOTE_UPDATE_APPLIED": "remote_update_applied", + // An update was applied to the remote data store + "LOCAL_UPDATE_APPLIED": "local_update_applied", + // An update was applied to the local data store + "DELTA_RECEIVED": "delta_received", + // A delta was received from the remote data store for the dataset + "RECORD_DELTA_RECEIVED": "record_delta_received", + // A delta was received from the remote data store for the record + "SYNC_FAILED": "sync_failed" + // Sync loop failed to complete + }, + + datasets: {}, + + // Initialise config to default values; + config: undefined, + + //TODO: deprecate this + notify_callback: undefined, + + notify_callback_map : {}, + + init_is_called: false, + + //this is used to map the temp data uid (created on client) to the real uid (created in the cloud) + uid_map: {}, + + // PUBLIC FUNCTION IMPLEMENTATIONS + init: function(options) { + self.consoleLog('sync - init called'); + + self.config = JSON.parse(JSON.stringify(self.defaults)); + for (var i in options) { + self.config[i] = options[i]; + } + + //prevent multiple monitors from created if init is called multiple times + if(!self.init_is_called){ + self.init_is_called = true; + self.datasetMonitor(); + } + }, + + notify: function(datasetId, callback) { + if(arguments.length === 1 && typeof datasetId === 'function'){ + self.notify_callback = datasetId; + } else { + self.notify_callback_map[datasetId] = callback; + } + }, + + manage: function(dataset_id, opts, query_params, meta_data, cb) { + self.consoleLog('manage - START'); + + // Currently we do not enforce the rule that init() funciton should be called before manage(). + // We need this check to guard against self.config undefined + if (!self.config){ + self.config = JSON.parse(JSON.stringify(self.defaults)); + } + + var options = opts || {}; + + var doManage = function(dataset) { + self.consoleLog('doManage dataset :: initialised = ' + dataset.initialised + " :: " + dataset_id + ' :: ' + JSON.stringify(options)); + + var currentDatasetCfg = (dataset.config) ? dataset.config : self.config; + var datasetConfig = self.setOptions(currentDatasetCfg, options); + + dataset.query_params = query_params || dataset.query_params || {}; + dataset.meta_data = meta_data || dataset.meta_data || {}; + dataset.config = datasetConfig; + dataset.syncRunning = false; + dataset.syncPending = true; + dataset.initialised = true; + if(typeof dataset.meta === "undefined"){ + dataset.meta = {}; + } + + self.saveDataSet(dataset_id, function() { + + if( cb ) { + cb(); + } + }); + }; + + // Check if the dataset is already loaded + self.getDataSet(dataset_id, function(dataset) { + self.consoleLog('manage - dataset already loaded'); + doManage(dataset); + }, function(err) { + self.consoleLog('manage - dataset not loaded... trying to load'); + + // Not already loaded, try to load from local storage + self.loadDataSet(dataset_id, function(dataset) { + self.consoleLog('manage - dataset loaded from local storage'); + + // Loading from local storage worked + + // Fire the local update event to indicate that dataset was loaded from local storage + self.doNotify(dataset_id, null, self.notifications.LOCAL_UPDATE_APPLIED, "load"); + + // Put the dataet under the management of the sync service + doManage(dataset); + }, + function(err) { + // No dataset in memory or local storage - create a new one and put it in memory + self.consoleLog('manage - Creating new dataset for id ' + dataset_id); + var dataset = {}; + dataset.data = {}; + dataset.pending = {}; + dataset.meta = {}; + self.datasets[dataset_id] = dataset; + doManage(dataset); + }); + }); + }, + + /** + * Sets options for passed in config, if !config then options will be applied to default config. + * @param {Object} config - config to which options will be applied + * @param {Object} options - options to be applied to the config + */ + setOptions: function(config, options) { + // Make sure config is initialised + if( ! config ) { + config = JSON.parse(JSON.stringify(self.defaults)); + } + + + var datasetConfig = JSON.parse(JSON.stringify(config)); + var optionsIn = JSON.parse(JSON.stringify(options)); + for (var k in optionsIn) { + datasetConfig[k] = optionsIn[k]; + } + + return datasetConfig; + }, + + list: function(dataset_id, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + if (dataset && dataset.data) { + // Return a copy of the dataset so updates will not automatically make it back into the dataset + var res = JSON.parse(JSON.stringify(dataset.data)); + success(res); + } else { + if(failure) { + failure('no_data'); + } + } + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + }, + + getUID: function(oldOrNewUid){ + var uid = self.uid_map[oldOrNewUid]; + if(uid || uid === 0){ + return uid; + } else { + return oldOrNewUid; + } + }, + + create: function(dataset_id, data, success, failure) { + if(data == null){ + if(failure){ + return failure("null_data"); + } + } + self.addPendingObj(dataset_id, null, data, "create", success, failure); + }, + + read: function(dataset_id, uid, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + uid = self.getUID(uid); + var rec = dataset.data[uid]; + if (!rec) { + failure("unknown_uid"); + } else { + // Return a copy of the record so updates will not automatically make it back into the dataset + var res = JSON.parse(JSON.stringify(rec)); + success(res); + } + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + }, + + update: function(dataset_id, uid, data, success, failure) { + uid = self.getUID(uid); + self.addPendingObj(dataset_id, uid, data, "update", success, failure); + }, + + 'delete': function(dataset_id, uid, success, failure) { + uid = self.getUID(uid); + self.addPendingObj(dataset_id, uid, null, "delete", success, failure); + }, + + getPending: function(dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + var res; + if( dataset ) { + res = dataset.pending; + } + cb(res); + }, function(err, datatset_id) { + self.consoleLog(err); + }); + }, + + clearPending: function(dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + dataset.pending = {}; + self.saveDataSet(dataset_id, cb); + }); + }, + + listCollisions : function(dataset_id, success, failure){ + self.getDataSet(dataset_id, function(dataset) { + self.doCloudCall({ + "dataset_id": dataset_id, + "req": { + "fn": "listCollisions", + "meta_data" : dataset.meta_data + } + }, success, failure); + }, failure); + }, + + removeCollision: function(dataset_id, colissionHash, success, failure) { + self.getDataSet(dataset_id, function(dataset) { + self.doCloudCall({ + "dataset_id" : dataset_id, + "req": { + "fn": "removeCollision", + "hash": colissionHash, + meta_data: dataset.meta_data + } + }, success, failure); + }); + }, + + + // PRIVATE FUNCTIONS + isOnline: function(callback) { + var online = true; + + // first, check if navigator.online is available + if(typeof navigator.onLine !== "undefined"){ + online = navigator.onLine; + } + + // second, check if Phonegap is available and has online info + if(online){ + //use phonegap to determin if the network is available + if(typeof navigator.network !== "undefined" && typeof navigator.network.connection !== "undefined"){ + var networkType = navigator.network.connection.type; + if(networkType === "none" || networkType === null) { + online = false; + } + } + } + + return callback(online); + }, + + doNotify: function(dataset_id, uid, code, message) { + + if( self.notify_callback || self.notify_callback_map[dataset_id]) { + var notifyFunc = self.notify_callback_map[dataset_id] || self.notify_callback; + if ( self.config['notify_' + code] ) { + var notification = { + "dataset_id" : dataset_id, + "uid" : uid, + "code" : code, + "message" : message + }; + // make sure user doesn't block + setTimeout(function () { + notifyFunc(notification); + }, 0); + } + } + }, + + getDataSet: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getQueryParams: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.query_params); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setQueryParams: function(dataset_id, queryParams, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.query_params = queryParams; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.query_params); + } + } else { + if ( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getMetaData: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.meta_data); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setMetaData: function(dataset_id, metaData, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.meta_data = metaData; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.meta_data); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + getConfig: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + success(dataset.config); + } else { + if(failure){ + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + setConfig: function(dataset_id, config, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + var fullConfig = self.setOptions(dataset.config, config); + dataset.config = fullConfig; + self.saveDataSet(dataset_id); + if( success ) { + success(dataset.config); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + stopSync: function(dataset_id, success, failure) { + self.setConfig(dataset_id, {"sync_active" : false}, function() { + if( success ) { + success(); + } + }, failure); + }, + + startSync: function(dataset_id, success, failure) { + self.setConfig(dataset_id, {"sync_active" : true}, function() { + if( success ) { + success(); + } + }, failure); + }, + + doSync: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.syncPending = true; + self.saveDataSet(dataset_id); + if( success ) { + success(); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + forceSync: function(dataset_id, success, failure) { + var dataset = self.datasets[dataset_id]; + + if (dataset) { + dataset.syncForced = true; + self.saveDataSet(dataset_id); + if( success ) { + success(); + } + } else { + if( failure ) { + failure('unknown_dataset ' + dataset_id, dataset_id); + } + } + }, + + sortObject : function(object) { + if (typeof object !== "object" || object === null) { + return object; + } + + var result = []; + + Object.keys(object).sort().forEach(function(key) { + result.push({ + key: key, + value: self.sortObject(object[key]) + }); + }); + + return result; + }, + + sortedStringify : function(obj) { + + var str = ''; + + try { + str = JSON.stringify(self.sortObject(obj)); + } catch (e) { + console.error('Error stringifying sorted object:' + e); + } + + return str; + }, + + generateHash: function(object) { + var hash = CryptoJS.SHA1(self.sortedStringify(object)); + return hash.toString(); + }, + + addPendingObj: function(dataset_id, uid, data, action, success, failure) { + self.isOnline(function (online) { + if (!online) { + self.doNotify(dataset_id, uid, self.notifications.OFFLINE_UPDATE, action); + } + }); + + function storePendingObject(obj) { + obj.hash = obj.hash || self.generateHash(obj); + + self.getDataSet(dataset_id, function(dataset) { + + dataset.pending[obj.hash] = obj; + + self.updateDatasetFromLocal(dataset, obj); + + if(self.config.auto_sync_local_updates) { + dataset.syncPending = true; + } + self.saveDataSet(dataset_id); + self.doNotify(dataset_id, uid, self.notifications.LOCAL_UPDATE_APPLIED, action); + + success(obj); + }, function(code, msg) { + if(failure) { + failure(code, msg); + } + }); + } + + var pendingObj = {}; + pendingObj.inFlight = false; + pendingObj.action = action; + pendingObj.post = JSON.parse(JSON.stringify(data)); + pendingObj.postHash = self.generateHash(pendingObj.post); + pendingObj.timestamp = new Date().getTime(); + if( "create" === action ) { + //this hash value will be returned later on when the cloud returns updates. We can then link the old uid + //with new uid + pendingObj.hash = self.generateHash(pendingObj); + pendingObj.uid = pendingObj.hash; + storePendingObject(pendingObj); + } else { + self.read(dataset_id, uid, function(rec) { + pendingObj.uid = uid; + pendingObj.pre = rec.data; + pendingObj.preHash = self.generateHash(rec.data); + storePendingObject(pendingObj); + }, function(code, msg) { + if(failure){ + failure(code, msg); + } + }); + } + }, + + syncLoop: function(dataset_id) { + self.getDataSet(dataset_id, function(dataSet) { + + // The sync loop is currently active + dataSet.syncPending = false; + dataSet.syncRunning = true; + dataSet.syncLoopStart = new Date().getTime(); + self.doNotify(dataset_id, null, self.notifications.SYNC_STARTED, null); + + self.isOnline(function(online) { + if (!online) { + self.syncComplete(dataset_id, "offline", self.notifications.SYNC_FAILED); + } else { + var syncLoopParams = {}; + syncLoopParams.fn = 'sync'; + syncLoopParams.dataset_id = dataset_id; + syncLoopParams.query_params = dataSet.query_params; + syncLoopParams.config = dataSet.config; + syncLoopParams.meta_data = dataSet.meta_data; + //var datasetHash = self.generateLocalDatasetHash(dataSet); + syncLoopParams.dataset_hash = dataSet.hash; + syncLoopParams.acknowledgements = dataSet.acknowledgements || []; + + var pending = dataSet.pending; + var pendingArray = []; + for(var i in pending ) { + // Mark the pending records we are about to submit as inflight and add them to the array for submission + // Don't re-add previous inFlight pending records who whave crashed - i.e. who's current state is unknown + // Don't add delayed records + if( !pending[i].inFlight && !pending[i].crashed && !pending[i].delayed) { + pending[i].inFlight = true; + pending[i].inFlightDate = new Date().getTime(); + pendingArray.push(pending[i]); + } + } + syncLoopParams.pending = pendingArray; + + if( pendingArray.length > 0 ) { + self.consoleLog('Starting sync loop - global hash = ' + dataSet.hash + ' :: params = ' + JSON.stringify(syncLoopParams, null, 2)); + } + self.doCloudCall({ + 'dataset_id': dataset_id, + 'req': syncLoopParams + }, function(res) { + var rec; + + function processUpdates(updates, notification, acknowledgements) { + if( updates ) { + for (var up in updates) { + rec = updates[up]; + acknowledgements.push(rec); + if( dataSet.pending[up] && dataSet.pending[up].inFlight) { + delete dataSet.pending[up]; + self.doNotify(dataset_id, rec.uid, notification, rec); + } + } + } + } + + // Check to see if any previously crashed inflight records can now be resolved + self.updateCrashedInFlightFromNewData(dataset_id, dataSet, res); + + //Check to see if any delayed pending records can now be set to ready + self.updateDelayedFromNewData(dataset_id, dataSet, res); + + //Check meta data as well to make sure it contains the correct info + self.updateMetaFromNewData(dataset_id, dataSet, res); + + + if (res.updates) { + var acknowledgements = []; + self.checkUidChanges(dataSet, res.updates.applied); + processUpdates(res.updates.applied, self.notifications.REMOTE_UPDATE_APPLIED, acknowledgements); + processUpdates(res.updates.failed, self.notifications.REMOTE_UPDATE_FAILED, acknowledgements); + processUpdates(res.updates.collisions, self.notifications.COLLISION_DETECTED, acknowledgements); + dataSet.acknowledgements = acknowledgements; + } + + if (res.hash && res.hash !== dataSet.hash) { + self.consoleLog("Local dataset stale - syncing records :: local hash= " + dataSet.hash + " - remoteHash=" + res.hash); + // Different hash value returned - Sync individual records + self.syncRecords(dataset_id); + } else { + self.consoleLog("Local dataset up to date"); + self.syncComplete(dataset_id, "online", self.notifications.SYNC_COMPLETE); + } + }, function(msg, err) { + // The AJAX call failed to complete succesfully, so the state of the current pending updates is unknown + // Mark them as "crashed". The next time a syncLoop completets successfully, we will review the crashed + // records to see if we can determine their current state. + self.markInFlightAsCrashed(dataSet); + self.consoleLog("syncLoop failed : msg=" + msg + " :: err = " + err); + self.syncComplete(dataset_id, msg, self.notifications.SYNC_FAILED); + }); + } + }); + }); + }, + + syncRecords: function(dataset_id) { + + self.getDataSet(dataset_id, function(dataSet) { + + var localDataSet = dataSet.data || {}; + + var clientRecs = {}; + for (var i in localDataSet) { + var uid = i; + var hash = localDataSet[i].hash; + clientRecs[uid] = hash; + } + + var syncRecParams = {}; + + syncRecParams.fn = 'syncRecords'; + syncRecParams.dataset_id = dataset_id; + syncRecParams.query_params = dataSet.query_params; + syncRecParams.clientRecs = clientRecs; + + self.consoleLog("syncRecParams :: " + JSON.stringify(syncRecParams)); + + self.doCloudCall({ + 'dataset_id': dataset_id, + 'req': syncRecParams + }, function(res) { + self.consoleLog('syncRecords Res before applying pending changes :: ' + JSON.stringify(res)); + self.applyPendingChangesToRecords(dataSet, res); + self.consoleLog('syncRecords Res after apply pending changes :: ' + JSON.stringify(res)); + + var i; + + if (res.create) { + for (i in res.create) { + localDataSet[i] = {"hash" : res.create[i].hash, "data" : res.create[i].data}; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "create"); + } + } + + if (res.update) { + for (i in res.update) { + localDataSet[i].hash = res.update[i].hash; + localDataSet[i].data = res.update[i].data; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "update"); + } + } + if (res['delete']) { + for (i in res['delete']) { + delete localDataSet[i]; + self.doNotify(dataset_id, i, self.notifications.RECORD_DELTA_RECEIVED, "delete"); + } + } + + self.doNotify(dataset_id, res.hash, self.notifications.DELTA_RECEIVED, 'partial dataset'); + + dataSet.data = localDataSet; + if(res.hash) { + dataSet.hash = res.hash; + } + self.syncComplete(dataset_id, "online", self.notifications.SYNC_COMPLETE); + }, function(msg, err) { + self.consoleLog("syncRecords failed : msg=" + msg + " :: err=" + err); + self.syncComplete(dataset_id, msg, self.notifications.SYNC_FAILED); + }); + }); + }, + + syncComplete: function(dataset_id, status, notification) { + + self.getDataSet(dataset_id, function(dataset) { + dataset.syncRunning = false; + dataset.syncLoopEnd = new Date().getTime(); + self.saveDataSet(dataset_id); + self.doNotify(dataset_id, dataset.hash, notification, status); + }); + }, + + applyPendingChangesToRecords: function(dataset, records){ + var pendings = dataset.pending; + for(var pendingUid in pendings){ + if(pendings.hasOwnProperty(pendingUid)){ + var pendingObj = pendings[pendingUid]; + var uid = pendingObj.uid; + //if the records contain any thing about the data records that are currently in pendings, + //it means there are local changes that haven't been applied to the cloud yet, + //so update the pre value of each pending record to relect the latest status from cloud + //and remove them from the response + if(records.create){ + var creates = records.create; + if(creates && creates[uid]){ + delete creates[uid]; + } + } + if(records.update){ + var updates = records.update; + if(updates && updates[uid]){ + delete updates[uid]; + } + } + if(records['delete']){ + var deletes = records['delete']; + if(deletes && deletes[uid]){ + delete deletes[uid]; + } + } + } + } + }, + + checkUidChanges: function(dataset, appliedUpdates){ + if(appliedUpdates){ + var new_uids = {}; + var changeUidsCount = 0; + for(var update in appliedUpdates){ + if(appliedUpdates.hasOwnProperty(update)){ + var applied_update = appliedUpdates[update]; + var action = applied_update.action; + if(action && action === 'create'){ + //we are receving the results of creations, at this point, we will have the old uid(the hash) and the real uid generated by the cloud + var newUid = applied_update.uid; + var oldUid = applied_update.hash; + changeUidsCount++; + //remember the mapping + self.uid_map[oldUid] = newUid; + new_uids[oldUid] = newUid; + //update the data uid in the dataset + var record = dataset.data[oldUid]; + if(record){ + dataset.data[newUid] = record; + delete dataset.data[oldUid]; + } + + //update the old uid in meta data + var metaData = dataset.meta[oldUid]; + if(metaData) { + dataset.meta[newUid] = metaData; + delete dataset.meta[oldUid]; + } + } + } + } + if(changeUidsCount > 0){ + //we need to check all existing pendingRecords and update their UIDs if they are still the old values + for(var pending in dataset.pending){ + if(dataset.pending.hasOwnProperty(pending)){ + var pendingObj = dataset.pending[pending]; + var pendingRecordUid = pendingObj.uid; + if(new_uids[pendingRecordUid]){ + pendingObj.uid = new_uids[pendingRecordUid]; + } + } + } + } + } + }, + + checkDatasets: function() { + for( var dataset_id in self.datasets ) { + if( self.datasets.hasOwnProperty(dataset_id) ) { + var dataset = self.datasets[dataset_id]; + if(dataset && !dataset.syncRunning && (dataset.config.sync_active || dataset.syncForced)) { + // Check to see if it is time for the sync loop to run again + var lastSyncStart = dataset.syncLoopStart; + var lastSyncCmp = dataset.syncLoopEnd; + if(dataset.syncForced){ + dataset.syncPending = true; + } else if( lastSyncStart == null ) { + self.consoleLog(dataset_id +' - Performing initial sync'); + // Dataset has never been synced before - do initial sync + dataset.syncPending = true; + } else if (lastSyncCmp != null) { + var timeSinceLastSync = new Date().getTime() - lastSyncCmp; + var syncFrequency = dataset.config.sync_frequency * 1000; + if( timeSinceLastSync > syncFrequency ) { + // Time between sync loops has passed - do another sync + dataset.syncPending = true; + } + } + + if( dataset.syncPending ) { + // Reset syncForced in case it was what caused the sync cycle to run. + dataset.syncForced = false; + + // If the dataset requres syncing, run the sync loop. This may be because the sync interval has passed + // or because the sync_frequency has been changed or because a change was made to the dataset and the + // immediate_sync flag set to true + self.syncLoop(dataset_id); + } + } + } + } + }, + + /** + * Sets cloud handler for sync responsible for making network requests: + * For example function(params, success, failure) + */ + setCloudHandler: function(cloudHandler){ + self.cloudHandler = cloudHandler; + }, + + doCloudCall: function(params, success, failure) { + if(self.cloudHandler && typeof self.cloudHandler === "function" ){ + self.cloudHandler(params, success, failure); + } else { + console.log("Missing cloud handler for sync. Please refer to documentation"); + } + }, + + datasetMonitor: function() { + self.checkDatasets(); + + // Re-execute datasetMonitor every 500ms so we keep invoking checkDatasets(); + setTimeout(function() { + self.datasetMonitor(); + }, 500); + }, + + getStorageAdapter: function(dataset_id, isSave, cb){ + var onFail = function(msg, err){ + var errMsg = (isSave?'save to': 'load from' ) + ' local storage failed msg: ' + msg + ' err: ' + err; + self.doNotify(dataset_id, null, self.notifications.CLIENT_STORAGE_FAILED, errMsg); + self.consoleLog(errMsg); + }; + Lawnchair({fail:onFail, adapter: self.config.storage_strategy, size:self.config.file_system_quota, backup: self.config.icloud_backup}, function(){ + return cb(null, this); + }); + }, + + saveDataSet: function (dataset_id, cb) { + self.getDataSet(dataset_id, function(dataset) { + self.getStorageAdapter(dataset_id, true, function(err, storage){ + storage.save({key:"dataset_" + dataset_id, val:dataset}, function(){ + //save success + if(cb) { + return cb(); + } + }); + }); + }); + }, + + loadDataSet: function (dataset_id, success, failure) { + self.getStorageAdapter(dataset_id, false, function(err, storage){ + storage.get( "dataset_" + dataset_id, function (data){ + if (data && data.val) { + var dataset = data.val; + if(typeof dataset === "string"){ + dataset = JSON.parse(dataset); + } + // Datasets should not be auto initialised when loaded - the mange function should be called for each dataset + // the user wants sync + dataset.initialised = false; + self.datasets[dataset_id] = dataset; // TODO: do we need to handle binary data? + self.consoleLog('load from local storage success for dataset_id :' + dataset_id); + if(success) { + return success(dataset); + } + } else { + // no data yet, probably first time. failure calback should handle this + if(failure) { + return failure(); + } + } + }); + }); + }, + + clearCache: function(dataset_id, cb){ + delete self.datasets[dataset_id]; + self.notify_callback_map[dataset_id] = null; + self.getStorageAdapter(dataset_id, true, function(err, storage){ + storage.remove("dataset_" + dataset_id, function(){ + self.consoleLog('local cache is cleared for dataset : ' + dataset_id); + if(cb){ + return cb(); + } + }); + }); + }, + + updateDatasetFromLocal: function(dataset, pendingRec) { + var pending = dataset.pending; + var previousPendingUid; + var previousPending; + + var uid = pendingRec.uid; + self.consoleLog('updating local dataset for uid ' + uid + ' - action = ' + pendingRec.action); + + dataset.meta[uid] = dataset.meta[uid] || {}; + + // Creating a new record + if( pendingRec.action === "create" ) { + if( dataset.data[uid] ) { + self.consoleLog('dataset already exists for uid in create :: ' + JSON.stringify(dataset.data[uid])); + + // We are trying to do a create using a uid which already exists + if (dataset.meta[uid].fromPending) { + // We are trying to create on top of an existing pending record + // Remove the previous pending record and use this one instead + previousPendingUid = dataset.meta[uid].pendingUid; + delete pending[previousPendingUid]; + } + } + dataset.data[uid] = {}; + } + + if( pendingRec.action === "update" ) { + if( dataset.data[uid] ) { + if (dataset.meta[uid].fromPending) { + self.consoleLog('updating an existing pending record for dataset :: ' + JSON.stringify(dataset.data[uid])); + // We are trying to update an existing pending record + previousPendingUid = dataset.meta[uid].pendingUid; + previousPending = pending[previousPendingUid]; + if(previousPending) { + if(!previousPending.inFlight){ + self.consoleLog('existing pre-flight pending record = ' + JSON.stringify(previousPending)); + // We are trying to perform an update on an existing pending record + // modify the original record to have the latest value and delete the pending update + previousPending.post = pendingRec.post; + previousPending.postHash = pendingRec.postHash; + delete pending[pendingRec.hash]; + // Update the pending record to have the hash of the previous record as this is what is now being + // maintained in the pending array & is what we want in the meta record + pendingRec.hash = previousPendingUid; + } else { + //we are performing changes to a pending record which is inFlight. Until the status of this pending record is resolved, + //we should not submit this pending record to the cloud. Mark it as delayed. + self.consoleLog('existing in-inflight pending record = ' + JSON.stringify(previousPending)); + pendingRec.delayed = true; + pendingRec.waiting = previousPending.hash; + } + } + } + } + } + + if( pendingRec.action === "delete" ) { + if( dataset.data[uid] ) { + if (dataset.meta[uid].fromPending) { + self.consoleLog('Deleting an existing pending record for dataset :: ' + JSON.stringify(dataset.data[uid])); + // We are trying to delete an existing pending record + previousPendingUid = dataset.meta[uid].pendingUid; + previousPending = pending[previousPendingUid]; + if( previousPending ) { + if(!previousPending.inFlight){ + self.consoleLog('existing pending record = ' + JSON.stringify(previousPending)); + if( previousPending.action === "create" ) { + // We are trying to perform a delete on an existing pending create + // These cancel each other out so remove them both + delete pending[pendingRec.hash]; + delete pending[previousPendingUid]; + } + if( previousPending.action === "update" ) { + // We are trying to perform a delete on an existing pending update + // Use the pre value from the pending update for the delete and + // get rid of the pending update + pendingRec.pre = previousPending.pre; + pendingRec.preHash = previousPending.preHash; + pendingRec.inFlight = false; + delete pending[previousPendingUid]; + } + } else { + self.consoleLog('existing in-inflight pending record = ' + JSON.stringify(previousPending)); + pendingRec.delayed = true; + pendingRec.waiting = previousPending.hash; + } + } + } + delete dataset.data[uid]; + } + } + + if( dataset.data[uid] ) { + dataset.data[uid].data = pendingRec.post; + dataset.data[uid].hash = pendingRec.postHash; + dataset.meta[uid].fromPending = true; + dataset.meta[uid].pendingUid = pendingRec.hash; + } + }, + + updateCrashedInFlightFromNewData: function(dataset_id, dataset, newData) { + var updateNotifications = { + applied: self.notifications.REMOTE_UPDATE_APPLIED, + failed: self.notifications.REMOTE_UPDATE_FAILED, + collisions: self.notifications.COLLISION_DETECTED + }; + + var pending = dataset.pending; + var resolvedCrashes = {}; + var pendingHash; + var pendingRec; + + + if( pending ) { + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight && pendingRec.crashed) { + self.consoleLog('updateCrashedInFlightFromNewData - Found crashed inFlight pending record uid=' + pendingRec.uid + ' :: hash=' + pendingRec.hash ); + if( newData && newData.updates && newData.updates.hashes) { + + // Check if the updates received contain any info about the crashed in flight update + var crashedUpdate = newData.updates.hashes[pendingHash]; + if( !crashedUpdate ) { + //TODO: review this - why we need to wait? + // No word on our crashed update - increment a counter to reflect another sync that did not give us + // any update on our crashed record. + if( pendingRec.crashedCount ) { + pendingRec.crashedCount++; + } + else { + pendingRec.crashedCount = 1; + } + } + } + else { + // No word on our crashed update - increment a counter to reflect another sync that did not give us + // any update on our crashed record. + if( pendingRec.crashedCount ) { + pendingRec.crashedCount++; + } + else { + pendingRec.crashedCount = 1; + } + } + } + } + } + + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight && pendingRec.crashed) { + if( pendingRec.crashedCount > dataset.config.crashed_count_wait ) { + self.consoleLog('updateCrashedInFlightFromNewData - Crashed inflight pending record has reached crashed_count_wait limit : ' + JSON.stringify(pendingRec)); + self.consoleLog('updateCrashedInFlightFromNewData - Retryig crashed inflight pending record'); + pendingRec.crashed = false; + pendingRec.inFlight = false; + } + } + } + } + } + }, + + updateDelayedFromNewData: function(dataset_id, dataset, newData){ + var pending = dataset.pending; + var pendingHash; + var pendingRec; + if(pending){ + for( pendingHash in pending ){ + if( pending.hasOwnProperty(pendingHash) ){ + pendingRec = pending[pendingHash]; + if( pendingRec.delayed && pendingRec.waiting ){ + self.consoleLog('updateDelayedFromNewData - Found delayed pending record uid=' + pendingRec.uid + ' :: hash=' + pendingRec.hash + ' :: waiting=' + pendingRec.waiting); + if( newData && newData.updates && newData.updates.hashes ){ + var waitingRec = newData.updates.hashes[pendingRec.waiting]; + if(waitingRec){ + self.consoleLog('updateDelayedFromNewData - Waiting pending record is resolved rec=' + JSON.stringify(waitingRec)); + pendingRec.delayed = false; + pendingRec.waiting = undefined; + } + } + } + } + } + } + }, + + updateMetaFromNewData: function(dataset_id, dataset, newData){ + var meta = dataset.meta; + if(meta && newData && newData.updates && newData.updates.hashes){ + for(var uid in meta){ + if(meta.hasOwnProperty(uid)){ + var metadata = meta[uid]; + var pendingHash = metadata.pendingUid; + self.consoleLog("updateMetaFromNewData - Found metadata with uid = " + uid + " :: pendingHash = " + pendingHash); + var pendingResolved = true; + + if(pendingHash){ + //we have current pending in meta data, see if it's resolved + pendingResolved = false; + var hashresolved = newData.updates.hashes[pendingHash]; + if(hashresolved){ + self.consoleLog("updateMetaFromNewData - Found pendingUid in meta data resolved - resolved = " + JSON.stringify(hashresolved)); + //the current pending is resolved in the cloud + metadata.pendingUid = undefined; + pendingResolved = true; + } + } + + if(pendingResolved){ + self.consoleLog("updateMetaFromNewData - both previous and current pendings are resolved for meta data with uid " + uid + ". Delete it."); + //all pendings are resolved, the entry can be removed from meta data + delete meta[uid]; + } + } + } + } + }, + + + markInFlightAsCrashed : function(dataset) { + var pending = dataset.pending; + var pendingHash; + var pendingRec; + + if( pending ) { + var crashedRecords = {}; + for( pendingHash in pending ) { + if( pending.hasOwnProperty(pendingHash) ) { + pendingRec = pending[pendingHash]; + + if( pendingRec.inFlight ) { + self.consoleLog('Marking in flight pending record as crashed : ' + pendingHash); + pendingRec.crashed = true; + crashedRecords[pendingRec.uid] = pendingRec; + } + } + } + } + }, + + consoleLog: function(msg) { + if( self.config.do_console_log ) { + console.log(msg); + } + } +}; + +(function() { + self.config = self.defaults; + //Initialse the sync service with default config + //self.init({}); +})(); + +module.exports = { + init: self.init, + manage: self.manage, + notify: self.notify, + doList: self.list, + getUID: self.getUID, + doCreate: self.create, + doRead: self.read, + doUpdate: self.update, + doDelete: self['delete'], + listCollisions: self.listCollisions, + removeCollision: self.removeCollision, + getPending : self.getPending, + clearPending : self.clearPending, + getDataset : self.getDataSet, + getQueryParams: self.getQueryParams, + setQueryParams: self.setQueryParams, + getMetaData: self.getMetaData, + setMetaData: self.setMetaData, + getConfig: self.getConfig, + setConfig: self.setConfig, + startSync: self.startSync, + stopSync: self.stopSync, + doSync: self.doSync, + forceSync: self.forceSync, + generateHash: self.generateHash, + loadDataSet: self.loadDataSet, + clearCache: self.clearCache, + setCloudHandler: self.setCloudHandler +}; + +},{"../libs/generated/crypto":1,"../libs/generated/lawnchair":2}],"/Users/wtrocki/Projects/rcnext/fh-js-sdk/src/sync-client.js":[function(require,module,exports){ +module.exports=require('93xVCx'); +},{}]},{},[3]) \ No newline at end of file diff --git a/test/browser/suite.js b/test/browser/suite.js index 24252ea..a7c6d00 100644 --- a/test/browser/suite.js +++ b/test/browser/suite.js @@ -1,9 +1,3 @@ -require("../tests/test_ajax.js"); -require("../tests/test_sec.js"); -require("../tests/test_cloud_related.js"); -require("../tests/test_cloud_get.js"); -require("../tests/test_legacy_act.js"); require("../tests/test_sync_offline.js"); require("../tests/test_sync_online.js"); -require("../tests/test_push.js"); require("../tests/test_sync_manage.js"); \ No newline at end of file diff --git a/test/tests/test_sync_manage.js b/test/tests/test_sync_manage.js index 2fbab9f..814710f 100644 --- a/test/tests/test_sync_manage.js +++ b/test/tests/test_sync_manage.js @@ -1,11 +1,6 @@ var process = require("process"); -if (document && document.location) { - if (document.location.href.indexOf("coverage=1") > -1) { - process.env.LIB_COV = 1; - } -} -var syncClient = process.env.LIB_COV ? require("../../src-cov/modules/sync-cli") : require("../../src/modules/sync-cli"); +var syncClient = require("../../src/sync-client"); var chai = require('chai'); var expect = chai.expect; var sinonChai = require('sinon-chai'); diff --git a/test/tests/test_sync_offline.js b/test/tests/test_sync_offline.js index fb64783..36d44b8 100644 --- a/test/tests/test_sync_offline.js +++ b/test/tests/test_sync_offline.js @@ -1,10 +1,6 @@ var process = require("process"); -if(document && document.location){ - if(document.location.href.indexOf("coverage=1") > -1){ - process.env.LIB_COV = 1; - } -} -var syncClient = process.env.LIB_COV? require("../../src-cov/modules/sync-cli") : require("../../src/modules/sync-cli"); + +var syncClient = require("../../src/sync-client"); var chai = require('chai'); var expect = chai.expect; var sinonChai = require('sinon-chai'); diff --git a/test/tests/test_sync_online.js b/test/tests/test_sync_online.js index cdffc8d..1405583 100644 --- a/test/tests/test_sync_online.js +++ b/test/tests/test_sync_online.js @@ -1,11 +1,6 @@ var process = require("process"); -if(document && document.location){ - if(document.location.href.indexOf("coverage=1") > -1){ - process.env.LIB_COV = 1; - } -} -var syncClient = process.env.LIB_COV? require("../../src-cov/modules/sync-cli") : require("../../src/modules/sync-cli"); +var syncClient = require("../../src/sync-client"); var chai = require('chai'); var expect = chai.expect; var sinonChai = require('sinon-chai'); @@ -42,7 +37,7 @@ describe("test sync framework online with fake XMLHttpRequest", function(){ storage_strategy: ['memory'], crashed_count_wait: 0 }); - syncClient.manage(dataSetId, {"sync_active": false, "has_custom_sync": false}, {}, {}, done); + syncClient.manage(dataSetId, {"sync_active": false}, {}, {}, done); }); beforeEach(function(done){ @@ -56,7 +51,7 @@ describe("test sync framework online with fake XMLHttpRequest", function(){ - syncClient.manage(dataSetId, {"has_custom_sync": false}, {}, {}, function(){ + syncClient.manage(dataSetId, {}, {}, {}, function(){ syncClient.clearPending(dataSetId, function(){ done(); }); @@ -802,7 +797,7 @@ describe("test sync framework online with fake XMLHttpRequest", function(){ }); it("test updateCrashedInFlightFromNewData create", function(done){ - syncClient.setConfig(dataSetId, {crashed_count_wait: 10, has_custom_sync: false}, function(){ + syncClient.setConfig(dataSetId, {crashed_count_wait: 10}, function(){ var createRecord = {name:'item13'}; syncClient.doCreate(dataSetId, createRecord, function(){ onSync(function(){ @@ -959,7 +954,7 @@ describe("test sync framework online with fake XMLHttpRequest", function(){ }); it("test updateCrashedInFlightFromNewData resend", function(done){ - syncClient.setConfig(dataSetId, {"resend_crashed_updates": false, "crashed_count_wait": 0, "has_custom_sync": false}, function(){ + syncClient.setConfig(dataSetId, {"resend_crashed_updates": false, "crashed_count_wait": 0}, function(){ syncClient.doCreate(dataSetId, {name: "item16"}, function(){ onSync(function(){ var reqObj = requests[0]; @@ -1035,44 +1030,6 @@ describe("test sync framework online with fake XMLHttpRequest", function(){ done(); }); - - it("test checkHasCustomSync", function(done){ - - var resetCustomSync = function(cb){ - syncClient.manage(dataSetId, {has_custom_sync: null}, {}, {}, cb); - } - - resetCustomSync(function(){ - syncClient.checkHasCustomSync(dataSetId, function(){}); - expect(requests.length).to.equal(1); - var reqObj = requests[0]; - reqObj.respond(200, header, null); - - var dataset = syncClient.getDataset(dataSetId, function(dataset){ - expect(dataset.config.has_custom_sync).to.be.true; - - resetCustomSync(function(){ - syncClient.checkHasCustomSync(dataSetId, function(){}); - var reqObj1 = requests[1]; - reqObj1.respond(500, header, null); - expect(dataset.config.has_custom_sync).to.be.true; - - resetCustomSync(function(){ - syncClient.checkHasCustomSync(dataSetId, function(){}); - expect(requests.length).to.equal(3); - var reqObj2 = requests[2]; - reqObj2.respond(404, header, null); - - expect(dataset.config.has_custom_sync).to.be.false; - - done(); - }); - }); - }); - }); - - }); - it("test uid change", function(done){ syncClient.doCreate(dataSetId, {name: "item17"}, function(created){ var olduid = created.uid;