diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..15dbf3325
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,14 @@
+.git
+node_modules
+.vagrant
+
+npm-debug.log
+*.bak
+
+browserify
+report.tap
+coverage
+.nyc_output
+instrumented
+reports
+.idea/
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..e91bbb7a5
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,6 @@
+/examples/browserChannelClient/node_modules/**
+
+/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/node_modules/**
+/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/public/lib/**
+/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/webTests/lib/**
+/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/node_modules/**
diff --git a/.eslintrc-md.json b/.eslintrc-md.json
new file mode 100644
index 000000000..28f4e3896
--- /dev/null
+++ b/.eslintrc-md.json
@@ -0,0 +1,15 @@
+{
+ "extends": "eslint-config-fluid",
+ "useEslintrc": false,
+ "env": {
+ "browser": true
+ },
+ "rules": {
+ "no-undef": "off",
+ "strict": "off",
+ "no-unused-vars": "off"
+ },
+ "plugins": [
+ "markdown"
+ ]
+}
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..0ffbac405
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "extends": "eslint-config-fluid",
+ "env": {
+ "node": true
+ }
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..a66295bef
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Github thinks our long .js files are generated and hides them from diffs in PRs. Disable that behavior.
+*.js linguist-generated=false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..6e6aa36a9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: gpii-universal tests
+
+on: [push, pull_request]
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ env:
+ HEADLESS: true
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: '12.x'
+
+ - name: Run npm install
+ run: npm install
+
+ - name: Pass linter
+ run: $(npm bin)/grunt lint
+
+ - name: Run node tests
+ run: npm run test:node
+
+ - name: Run browser tests
+ run: npm run test:browser
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..ff4878197
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+/node_modules/
+/build/
+/instrumented/
+
+/examples/browserChannelClient/node_modules/
+
+/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/node_modules/
+
+npm-debug.log
+package-lock.json
+*.bak
+
+Vagrantfile.local
+.vagrant/
+
+report.tap
+coverage
+.nyc_output
+reports
+.idea/
+browserify
diff --git a/.nycrc b/.nycrc
new file mode 100644
index 000000000..147cb57c0
--- /dev/null
+++ b/.nycrc
@@ -0,0 +1,132 @@
+{
+ "include": [
+ "**/*.js",
+ "gpii/node_modules/**/*.js"
+ ],
+ "exclude": [
+ "!**/gpii/node_modules/accessRequester/index.js",
+ "!**/gpii/node_modules/accessRequester/src/accessRequester.js",
+ "!**/gpii/node_modules/couchConnector/index.js",
+ "!**/gpii/node_modules/couchConnector/src/js/couchConnector.js",
+ "!**/gpii/node_modules/deviceReporter/index.js",
+ "!**/gpii/node_modules/deviceReporter/src/DeviceReporter.js",
+ "!**/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js",
+ "!**/gpii/node_modules/eventLog/index.js",
+ "!**/gpii/node_modules/eventLog/src/eventLog.js",
+ "!**/gpii/node_modules/eventLog/src/installID.js",
+ "!**/gpii/node_modules/eventLog/src/metrics.js",
+ "!**/gpii/node_modules/flatMatchMaker/index.js",
+ "!**/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js",
+ "!**/gpii/node_modules/flowManager/index.js",
+ "!**/gpii/node_modules/flowManager/src/BrowserChannel.js",
+ "!**/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js",
+ "!**/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js",
+ "!**/gpii/node_modules/flowManager/src/FlowManager.js",
+ "!**/gpii/node_modules/flowManager/src/HealthGetHandler.js",
+ "!**/gpii/node_modules/flowManager/src/MatchMaking.js",
+ "!**/gpii/node_modules/flowManager/src/PrefsServerDataSource.js",
+ "!**/gpii/node_modules/flowManager/src/PSPChannel.js",
+ "!**/gpii/node_modules/flowManager/src/ReadyGetHandler.js",
+ "!**/gpii/node_modules/flowManager/src/SessionAware.js",
+ "!**/gpii/node_modules/flowManager/src/SettingsDataSource.js",
+ "!**/gpii/node_modules/flowManager/src/SettingsGetHandler.js",
+ "!**/gpii/node_modules/flowManager/src/SettingsPutHandler.js",
+ "!**/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js",
+ "!**/gpii/node_modules/flowManager/src/SystemUtils.js",
+ "!**/gpii/node_modules/flowManager/src/UntrustedFlowManager.js",
+ "!**/gpii/node_modules/flowManager/src/UserLogonHandlers.js",
+ "!**/gpii/node_modules/gpii-db-operation/index.js",
+ "!**/gpii/node_modules/gpii-db-operation/src/DataStore.js",
+ "!**/gpii/node_modules/gpii-db-operation/src/DbConst.js",
+ "!**/gpii/node_modules/gpii-db-operation/src/DbDataStore.js",
+ "!**/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js",
+ "!**/gpii/node_modules/gpii-db-operation/src/DbUtils.js",
+ "!**/gpii/node_modules/gpii-ini-file/index.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/index.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthGrantFinder.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthorizationService.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthServer.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/ClientService.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/CodeGenerator.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/index.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Const.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Express.js",
+ "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js",
+ "!**/gpii/node_modules/gpii-oauth2/index.js",
+ "!**/gpii/node_modules/gpii-user-errors/index.js",
+ "!**/gpii/node_modules/gpii-user-errors/src/UserErrors.js",
+ "!**/gpii/node_modules/journal/index.js",
+ "!**/gpii/node_modules/journal/src/Journal.js",
+ "!**/gpii/node_modules/journal/src/JournalIdParser.js",
+ "!**/gpii/node_modules/journal/src/SettingsDir.js",
+ "!**/gpii/node_modules/lifecycleActions/index.js",
+ "!**/gpii/node_modules/lifecycleActions/src/LifecycleActions.js",
+ "!**/gpii/node_modules/lifecycleManager/index.js",
+ "!**/gpii/node_modules/lifecycleManager/src/LifecycleManager.js",
+ "!**/gpii/node_modules/lifecycleManager/src/LifecycleManagerSession.js",
+ "!**/gpii/node_modules/lifecycleManager/src/PrivateMatchMaker.js",
+ "!**/gpii/node_modules/lifecycleManager/src/Resolvers.js",
+ "!**/gpii/node_modules/lifecycleManager/src/UserLogonRequest.js",
+ "!**/gpii/node_modules/lifecycleManager/src/UserLogonStateChange.js",
+ "!**/gpii/node_modules/matchMakerFramework/index.js",
+ "!**/gpii/node_modules/matchMakerFramework/src/MatchMakerFramework.js",
+ "!**/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js",
+ "!**/gpii/node_modules/ontologyHandler/index.js",
+ "!**/gpii/node_modules/ontologyHandler/src/ontologyHandler.js",
+ "!**/gpii/node_modules/ontologyHandler/src/ontologyHandlerUtilities.js",
+ "!**/gpii/node_modules/preferencesServer/index.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesGetHandler.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesPostHandler.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesPutHandler.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesServer.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesServerConst.js",
+ "!**/gpii/node_modules/preferencesServer/src/preferencesService.js",
+ "!**/gpii/node_modules/preferencesServer/src/readyGetHandler.js",
+ "!**/gpii/node_modules/processReporter/index.js",
+ "!**/gpii/node_modules/processReporter/src/ProcessReporter.js",
+ "!**/gpii/node_modules/processReporter/src/processesBridge.js",
+ "!**/gpii/node_modules/settingsHandlers/index.js",
+ "!**/gpii/node_modules/settingsHandlers/src/ExplodingSettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/INISettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/JSONSettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/LaunchHandlers.js",
+ "!**/gpii/node_modules/settingsHandlers/src/NoSettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/WebSocketsSettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/XMLSettingsHandler.js",
+ "!**/gpii/node_modules/settingsHandlers/src/settingsHandlerUtilities.js",
+ "!**/gpii/node_modules/singleInstance/index.js",
+ "!**/gpii/node_modules/singleInstance/src/singleInstance.js",
+ "!**/gpii/node_modules/testing/index.js",
+ "!**/gpii/node_modules/testing/src/Acceptance.js",
+ "!**/gpii/node_modules/testing/src/BrowserIncludes.js",
+ "!**/gpii/node_modules/testing/src/CloudBased.js",
+ "!**/gpii/node_modules/testing/src/CloudBasedOAuth2TestsUtils.js",
+ "!**/gpii/node_modules/testing/src/Fixtures.js",
+ "!**/gpii/node_modules/testing/src/Integration.js",
+ "!**/gpii/node_modules/testing/src/Mocks.js",
+ "!**/gpii/node_modules/testing/src/NockUtils.js",
+ "!**/gpii/node_modules/testing/src/RunTestDefs.js",
+ "!**/gpii/node_modules/testing/src/TestUtils.js",
+ "!**/gpii/node_modules/transformer/index.js",
+ "!**/gpii/node_modules/transformer/src/js/Transformer.js",
+ "!**/gpii/node_modules/userListeners/index.js",
+ "!**/gpii/node_modules/userListeners/src/listeners.js",
+ "!**/gpii/node_modules/userListeners/src/pcsc.js",
+ "!**/gpii/node_modules/userListeners/src/usb.js",
+ "testData",
+ "tests",
+ "reports",
+ "coverage",
+ "instrumented",
+ ".nyc_output",
+ "**/browserify/**",
+ "scripts",
+ "examples",
+ "gpii.js",
+ "Gruntfile.js"
+ ],
+ "reporter": "none",
+ "report-dir": "reports",
+ "temp-directory": "coverage",
+ "clean": false
+}
diff --git a/.project b/.project
deleted file mode 100644
index 3d9434dfe..000000000
--- a/.project
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- gpii-universal
-
-
-
-
-
-
-
-
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..e128c8417
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,17 @@
+FROM node:8-alpine
+
+WORKDIR /app
+COPY . /app
+ARG gitFullRev
+
+RUN apk add --no-cache --virtual build-dependencies python make git g++ && \
+ apk add --no-cache curl jq && \
+ chown -R node:node . && \
+ su node -c 'npm install' && \
+ npm cache clean --force && \
+ apk del build-dependencies && \
+ echo "{\"sha256\": \"$gitFullRev\"}" > gpii-revision.json
+
+USER node
+
+CMD ["npm","start"]
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 000000000..b2b815d18
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,31 @@
+/*!
+GPII Universal project Gruntfile
+
+Copyright 2014 RTF-US
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+The research leading to these results has received funding from the European Union's
+Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+"use strict";
+module.exports = function (grunt) {
+ grunt.initConfig({
+ lintAll: {
+ sources: {
+ md: [ "./*.md","./documentation/*.md", "./examples/**/*.md", "./gpii/**/*.md"],
+ js: ["!./browserify/**/*.js", "./gpii/**/*.js", "./tests/**/*.js", "./examples/**/*.js", "*.js"],
+ json: ["./gpii/**/*.json", "./tests/**/*.json", "./testData/**/*.json", "./*.json"],
+ json5: ["./gpii/**/*.json5", "./tests/**/*.json5", "./testData/**/*.json5", "./*.json5"],
+ other: ["./.*"]
+ }
+ }
+ });
+
+ grunt.loadNpmTasks("gpii-grunt-lint-all");
+ grunt.registerTask("lint", "Perform all standard lint checks.", ["lint-all"]);
+};
diff --git a/LICENSE.txt b/LICENSE.txt
index 338c9fbd8..051ad3955 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,16 +1,23 @@
-Copyright (c) 2012, OCAD University
+All modules are Copyright 2014 Raising the Floor - International except
+where noted otherwise in the code itself, or if the modules reside in a
+separate directory, they may contain explicit declarations of copyright
+in both the LICENSE file in the directory in which they reside and in the
+code itself. No external contributions are allowed under licenses which are
+fundamentally incompatible with the BSD or Apache licenses that the GPII is
+distributed under.
+
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of OCAD University nor the names of its contributors may
- be used to endorse or promote products derived from this software without
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of OCAD University nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
@@ -22,4 +29,7 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The research leading to these results has received funding from
+the European Union's Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016.
\ No newline at end of file
diff --git a/README.md b/README.md
index d19427b17..62d628bba 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,329 @@
-GPII Universal
-==============
+# GPII Universal
-Cross platform, core components of the GPII personalization infrastructure.
+The package contains cross-platform core components of the GPII personalization infrastructure. This repository should
+not be used directly, but in conjunction with one of the top-level GPII architecture-specific repositories.
+Additional documentation is available [on our wiki](http://wiki.gpii.net/w/Architecture).
-Installation
-------------
+## Installation
-Run the following command in your newly checked out universal repository. This
-will pull all dependencies that are required by universal.
+In production, this repository should be installed using one of the top-level GPII architecture-specific repositories:
- npm install
+* [windows](https://github.com/GPII/windows)
+* [linux](https://github.com/GPII/linux)
+* [android](https://github.com/GPII/android)
-Usage
------
+For development purposes this repository can also be cloned directly.
-To use any of the gpii components or funcionality in node, use the
+## Quick Start
+
+To verify the basic installation of GPII universal, you can start the core framework with
+
+`npm start`
+
+If all is well, you will see a message like
+
+ 01:51:07.289: Kettle Server 2us8uqry-22 is listening on port 8081
+
+Note that this installation will not include any OS-specific features, but can be used to verify system function with
+basic preference sets which only start solutions which require filesystem-based configuration (XML, JSON or .INI files).
+
+## Environment Variables
+
+Through the use of Kettle
+[resolvers](https://github.com/fluid-project/kettle/blob/master/docs/ConfigsAndApplications.md#referring-to-external-data-via-resolvers),
+some pre-defined configuration files offer the ability to read environment variables to change commonly used settings.
+
+### Preferences Server
+
+The Preferences Server with the `gpii.config.cloudBased.flowManager.production` configuration uses the following
+variables:
+
+* `GPII_PREFERENCESSERVER_LISTEN_PORT`: TCP port to listen on (default: `8081`)
+* `GPII_DATASOURCE_HOSTNAME`: The host name of CouchDB (default: `http://localhost`)
+* `GPII_DATASOURCE_PORT`: The port of CouchDB (default: `5984`)
+
+Example:
+
+```snippet
+GPII_PREFERENCESSERVER_LISTEN_PORT=9081 \
+GPII_DATASOURCE_HOSTNAME=https://localhost \
+GPII_DATASOURCE_PORT=5984 \
+NODE_ENV=gpii.config.cloudBased.flowManager.production \
+npm start
+```
+
+### Flow Manager
+
+The Flow Manager with the `gpii.config.cloudBased.flowManager.production` configuration uses the following variables:
+
+* `GPII_FLOWMANAGER_LISTEN_PORT`: TCP port to listen on (default: 8081)
+* `GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL`: The preferences server URL used by the cloud based flow manager to
+ communicate with the preferences server
+ preferences (default: `http://localhost:8081`)
+* `GPII_DATASOURCE_HOSTNAME`: The host name of CouchDB (default: `http://localhost`)
+* `GPII_DATASOURCE_PORT`: The port of CouchDB (default: 5984)
+* `GPII_CLOUD_URL`: The URL to GPII Cloud (default: `http://localhost:8084`). Used by an untrusted local flow manager
+ to communicate with GPII Cloud.
+
+#### Example:
+
+```snippet
+GPII_FLOWMANAGER_LISTEN_PORT=9091 \
+GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL=http://localhost:8081 \
+GPII_DATASOURCE_HOSTNAME=https://localhost \
+GPII_DATASOURCE_PORT=5984 \
+NODE_ENV=gpii.config.cloudBased.flowManager.production \
+npm start
+```
+
+## Recovering From System Corruption Using the Journal
+
+Either when operating the live GPII system or running test cases, you may end up corrupting your desktop settings in the
+case there is a system crash. In this case, you can navigate to
+
+ http://localhost:8081/journal/journals.html
+
+to browse a set of journal recovery snapshots. Clicking on the first link on this page will restore your system
+to the state it was in prior to the most recent GPII login.
+
+## Testing
+
+There are currently 3 different sets of tests:
+
+* Tests that run in the browser
+* Tests that run in node.js
+* Production tests, that run in node.js, but which interact with external services.
+
+The `npm test` command will run the browser and Node based tests. Please note, the node tests may behave oddly if you
+have set the `NODE_ENV` variable. The last two types of tests require you to have either Docker or Vagrant installed
+and available on your path, or else have a local instance of CouchDB listening on port 25984.
+If you are using Vagrant, you'll need to set the `GPII_TEST_COUCH_USE_VAGRANT` environment
+variable to `true`, or if you wish to bind tests against a local CouchDB on port 25984, you can
+set the `GPII_TEST_COUCH_USE_EXTERNAL` environment variable to `true`. See [the documentation for the
+gpii-couchdb-test-harness package](https://github.com/GPII/gpii-couchdb-test-harness) for more details.
+
+### Convert Preferences Data
+
+GPII has two sets of source preferences JSON5 data files, located at `%gpii-universal/testData/preferences` and
+`%gpii-universal/tests/data/preferences/`. These are converted and used in various configurations.
+
+The preferences files in `%gpii-universaluniversal/testData/preferences/` are converted into both `snapset` and
+`user` preferences:
+
+* `%gpii-universal/build/dbData/snapset/gpiiKeys.json`
+* `%gpii-universal/build/dbData/snapset/prefsSafes.json`
+* `%gpii-universal/build/dbData/user/gpiiKeys.json`
+* `%gpii-universal/build/dbData/user/prefsSafes.json`
+
+The above `snapset` preferences safes and GPII keys are:
+
+1. loaded into production and staging CouchDB instances in cloud environments.
+2. loaded into a local CouchDB instance when GPII runs locally, regardless of which configuration is used.
+
+The above `user` preferences are loaded into the local CouchDB for running GPII integration tests.
+
+The preferences in `%gpii-universal/tests/data/preferences/` are converted into `user` preferences:
+
+* `%gpii-universal/build/tests/dbData/user/gpiiKeys.json`
+* `%gpii-universal/build/tests/dbData/user/prefsSafes.json`
+
+These `user` preferences and the above `snapset` preferences are used with CouchDB when GPII runs in a development configuration.
+
+When any preferences file in either one of the two source directories (`%gpii-universaluniversal/testData/preferences/`
+or `%gpii-universal/tests/data/preferences/`) are modified, running `npm run postinstall` will generate
+gpiiKeys.json and prefsSafes.json files, whose contents are structured for loading into CouchDB.
+This step is needed for any preferences modifications that are to be applied to GPII.
+
+### Running browser tests
+
+You can run the browser tests in this package with a range of browsers using [Testem](https://github.com/testem/testem),
+and the configuration file `tests/testem.js`, which will ensure that code coverage information is collected. From the
+root of the `universal` folder, run the following command:
+
+`npm run test:browser`
+
+Please note, when running the browser tests locally using this command, the tests will fail on some browsers
+(notably Firefox, Safari, and Opera) unless the browser has focus. You can manually click the browser when it launches
+to give it focus. You should not give another window focus until the tests complete. This is not a problem when
+running the browser tests in Vagrant (see below). It's also not a problem when running the tests in Chrome, which you
+can do using a command like:
+
+`npm run test:browser -- --launch Chrome`
+
+You can also run (and debug) the tests manually from the root of the repository using a command like:
+
+`node node_modules/testem/testem.js --file tests/testem.js`
+
+The required test fixtures and Testem will start, and Testem will provide a URL you can open in a browser.
+
+If you would like to debug individual tests or view the test summary in a browser without using Testem, you can:
+
+1. Host the working directory, for example, using a command like the following from the root of the repository:
+ `python -m SimpleHTTPServer 4103`
+2. Open the "rollup" file `tests/all-tests.html` that runs all tests in a browser. Continuing the above example, you
+ would load the URL `http://localhost:4103/tests/web/html/all-tests.html`.
+
+Please note, when hosting the tests yourself, you may see multiple 404 errors related to the `/testem.js` and
+`/coverage/client/coverageSender.js` files which are provided by Testem and the `gpii.testem` harness respectively.
+These 404 errors will not cause any tests to fail and can be safely ignored.
+
+### Running node-based tests
+
+From the root of the `universal` folder, run the following command:
+
+`npm run test:node`
+
+Please note, the node tests may behave oddly if you have set the `NODE_ENV` variable.
+
+### Running tests using a VM
+
+A VM can be automatically created using tools provided by the
+[Prosperity4All Quality Infrastructure](https://github.com/GPII/qi-development-environments/). Please ensure the
+[requirements](https://github.com/GPII/qi-development-environments/#requirements) have been met. The `vagrant up`
+command can then be used to provision a new VM.
+
+Within the VM, the application will be installed in the directory specified by the `nodejs_app_install_dir` variable,
+which is defined in `provisioning/vagrant-vars.yml` configuration file in this repository. By default the installation
+ directory is set to `/home/vagrant/sync/universal`.
+
+Following the provisioning phase, tests can be run in the VM directly from the host system, without the need to log
+into the VM or interact with its console.
+
+From your project's top-level directory (where the `Vagrantfile` and `package.json` files reside), run:
+
+* Node and browser tests: `npm run test:vagrant`
+* Node tests only: `npm run test:vagrantNode`
+* browser tests only: `npm run test:vagrantBrowser`
+* production tests: `npm run test:vagrantProduction`
+
+The `test:vagrantProduction` target uses the `vagrantCloudBasedContainers.sh` script to spin up docker container-based
+GPII components inside the VM.
+
+**WARNING:** There is also a `test:productionConfig` npm script but do not run it
+directly. It is used to execute the production tests *within* the docker container-based GPII components
+created by `vagrantCloudBasedContainers.sh`. It is executed as the last step of
+`vagrantCloudBasedContainers.sh`.
+
+You can also run `vagrant ssh` to connect to the VM (or open the VirtualBox console and interface with the desktop
+environment) and run the tests manually if you wish.
+
+## Usage
+
+To use any of the gpii components or functionality in Node.js, use the
following statements to get access to fluid and/or gpii objects.
- var fluid = require("universal");
+```javascript
+var fluid = require("infusion"),
gpii = fluid.registerNamespace("gpii");
- // Now you should have access to both fluid
- // and gpii namespace.
-Testing
--------
+fluid.require("%gpii-universal");
+// Now you will have access to both fluid and gpii namespaces.
+```
+
+### Running node-based production tests
+
+The purpose of these tests are to test production config setups of the system. This involves using the cloud based flow
+manager when fetching or updating user settings, so there are extended requirements for these tests.
+
+These tests are a supplement to the `all-tests.js` (and hence not part of that test suite) and should be run separately
+when testing the system and having the below requirements available.
+
+The tests are run in one of two ways, using the following commands:
+
+1. From a host machine running a guest VM: `npm run test:vagrantProduction`
+2. From within the VM: `./scripts/vagrantCloudBasedContainers.sh`
+
+### Coverage Reporting
+
+The preferred way to consistently generate a code coverage report is to use Vagrant as described above. When you start
+a VM using `vagrant up` and run `npm run test:vagrant`, the full test suite will run in the VM, and at then end of
+the run:
+
+1. A summary of the report will be displayed to the console.
+2. A coverage report will be saved to the `reports` directory (open the `index.html` file in that directory in a browser
+ to see the full details).
+
+The report displays the code coverage for:
+
+1. Code reached from the tests run directly in Node.
+2. Code reached within a browser when running the tests in Testem.
+3. Server fixtures exercised by browser requests when running the Testem tests.
+
+A code coverage report will also be generated when you run the `npm test` command on your local machine, but you will
+need to ensure that browsers receive focus when they are launched (see above).
+
+The `npm test` command has [two additional associated scripts](https://docs.npmjs.com/misc/scripts). The `pretest`
+script runs before the command defined for the `test` script. The `posttest` script runs after. In our case
+we use a `pretest` script to clean up previous coverage data before we run the tests, and a `posttest` script to
+compile the actual report. Both the `pretest` and `posttest` steps must run in order to generate the report. If you
+only care about the test results, you should not need to run either of these scripts.
+
+## Docker Containers
+
+The provided Dockerfile can be used to run GPII Universal directly.
+
+### Build image
+
+#### Method 1: Via the universal VM
+
+Start the universal VM by running `vagrant up` in the universal root directory.
+
+Inside the universal VM, run:
+
+```snippet
+cd sync/universal
+./scripts/vagrantCloudBasedContainers.sh
+docker images
+```
+
+Running `docker images` will show the universal docker image named `vagrant-universal` has been built.
+
+Running `./scripts/vagrantCloudBasedContainers.sh`:
+
+1. Starts the preferences server in the production mode on the port 9081 inside the VM. To test it, open a browser and
+ access the URL: `http://localhost:9081/preferences/carla`. The preferences for `carla` should be returned.
+2. Starts the flow manager in the production mode on the port 9082 inside the VM. To test it, open a terminal and run:
+
+ ```snippet
+ curl -H "Content-Type: application/x-www-form-urlencoded" -X POST
+ -d "username=li&password=dummy&client_id=pilot-computer&client_secret=pilot-computer-secret&grant_type=password"
+ -X POST http://localhost:9082/access_token
+ ```
+
+ An access token should be returned. An example:
+
+ ``` snippet
+ {
+ "access_token": "19199532c97ed2e0f0d360df6679f058",
+ "expiresIn":3600,
+ "token_type":"Bearer"
+ }
+ ```
+
+3. The CouchDB data can be accessed via the URL: `http://localhost:5984/_utils/`
+
+**Note**:
+
+1. All 3 ports are forwarded from the VM to the host machine. Running the same test URLs on the host machine should
+ return the same result.
+2. In the case that you would like to start these 3 servers without rebuilding the universal docker image, run the
+ script with `--no-rebuild` option: `./scripts/vagrantCloudBasedContainers.sh --no-rebuild`
+
+#### Method 2: On a host machine that has docker installed
+
+To build a Docker image simply run: `docker build -t my-universal .`
+
+The following command can be used to build an image locally as long as it is run relative to the repository's
+`Dockerfile`:
+
+`docker build --rm -t gpii/universal:$(git rev-parse --short HEAD) .`
-There are currently 2 different sets of tests: the ones that run in the browser
-and the ones that can run in node.
+That will use the Git repository's current abbreviated commit hash as a
+[Docker tag](https://docs.docker.com/reference/commandline/cli/#tag). If you would like to download the latest public
+Universal image you can use this command:
-Running browser tests:
-open `{universal}/tests/web/html/all-tests.html` in your preferred browser:
+`docker pull gpii/universal`
-Running node tests:
+Or use the following command to download a particular image identified using a Git commit hash:
- node tests/all-tests.js
\ No newline at end of file
+`docker pull gpii/universal:`
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 000000000..c3a0967e0
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,87 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+require 'yaml'
+
+vars = YAML.load_file("provisioning/vars.yml")
+vagrant_vars = YAML.load_file("provisioning/vagrant-vars.yml")
+
+app_name = vars["nodejs_app_name"]
+
+app_directory = vagrant_vars["nodejs_app_install_dir"]
+
+app_start_script = vars["nodejs_app_start_script"]
+
+# Prepare the port forwarding for the preferences server
+# Check for the existence of the 'VM_PREFERENCES_SERVER_PORT' environment variable. If it
+# doesn't exist but 'nodejs_preferences_server_port' is defined in vars.yml, then use that
+# port. The default port for the preferences server is 9081.
+host_preferences_server_port = ENV["VM_PREFERENCES_SERVER_PORT"] || vars["nodejs_preferences_server_port"] || 9081
+guest_preferences_server_port = vars["nodejs_preferences_server_port"] || 9081
+
+# Prepare the port forwarding for the preferences server
+host_flow_manager_port = ENV["VM_FLOW_MANAGER_PORT"] || vars["nodejs_flow_manager_port"] || 9082
+guest_flow_manager_port = vars["nodejs_flow_manager_port"] || 9082
+
+# Prepare the port forwarding for CouchDB
+host_couchdb_port = ENV["VM_FLOW_MANAGER_PORT"] || vars["nodejs_couchdb_port"] || 9082
+guest_couchdb_port = vars["nodejs_couchdb_port"] || 9082
+
+# By default this VM will use 2 processor cores and 2GB of RAM. The 'VM_CPUS' and
+# "VM_RAM" environment variables can be used to change that behaviour.
+cpus = ENV["VM_CPUS"] || 2
+ram = ENV["VM_RAM"] || 2048
+
+Vagrant.configure(2) do |config|
+
+ config.vm.box = "inclusivedesign/fedora31"
+
+ # Your working directory will be synced to /home/vagrant/sync in the VM.
+ config.vm.synced_folder ".", "#{app_directory}"
+
+ # Mounts node_modules in /var/tmp to work around issues in the VirtualBox shared folders
+ config.vm.provision "shell", run: "always", inline: <<-SHELL
+ sudo mkdir -p /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules
+ sudo chown vagrant:vagrant -R /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules
+ sudo mount -o bind /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules
+ SHELL
+
+ # List additional directories to sync to the VM in your "Vagrantfile.local" file
+ # using the following format:
+ # config.vm.synced_folder "../path/on/your/host/os/your-project", "/home/vagrant/sync/your-project"
+
+ if File.exist? "Vagrantfile.local"
+ instance_eval File.read("Vagrantfile.local"), "Vagrantfile.local"
+ end
+
+ # Port forwarding takes place here. The 'guest' port is used inside the VM
+ # whereas the 'host' port is used by your host operating system.
+ config.vm.network "forwarded_port", guest: guest_preferences_server_port, host: host_preferences_server_port, protocol: "tcp",
+ auto_correct: true
+ config.vm.network "forwarded_port", guest: guest_flow_manager_port, host: host_flow_manager_port, protocol: "tcp",
+ auto_correct: true
+ config.vm.network "forwarded_port", guest: guest_couchdb_port, host: host_couchdb_port, protocol: "tcp",
+ auto_correct: true
+
+ # Port 19531 is needed so logs can be viewed using systemd-journal-gateway
+ #config.vm.network "forwarded_port", guest: 19531, host: 19531, protocol: "tcp",
+ # auto_correct: true
+
+ config.vm.hostname = app_name
+
+ config.vm.provider :virtualbox do |vm|
+ vm.customize ["modifyvm", :id, "--memory", ram]
+ vm.customize ["modifyvm", :id, "--cpus", cpus]
+ vm.customize ["modifyvm", :id, "--vram", "256"]
+ vm.customize ["modifyvm", :id, "--accelerate3d", "off"]
+ vm.customize ["modifyvm", :id, "--audio", "null", "--audiocontroller", "ac97"]
+ vm.customize ["modifyvm", :id, "--ioapic", "on"]
+ vm.customize ["setextradata", "global", "GUI/SuppressMessages", "all"]
+ end
+
+ config.vm.provision "shell", inline: <<-SHELL
+ sudo ansible-galaxy install -fr #{app_directory}/provisioning/requirements.yml
+ sudo UNIVERSAL_VARS_FILE=vagrant-vars.yml PYTHONUNBUFFERED=1 ansible-playbook #{app_directory}/provisioning/playbook.yml --tags="install,configure" --inventory="localhost ansible_connection=local,"
+ SHELL
+
+end
diff --git a/documentation/Apptology.md b/documentation/Apptology.md
new file mode 100644
index 000000000..4dd98aa0a
--- /dev/null
+++ b/documentation/Apptology.md
@@ -0,0 +1,24 @@
+# The Apptology
+
+To allow [the flat matchmaker](./FlatMatchMaker.md) to distinguish what type of application a solution is (and hence
+avoid launching two of the same types of applications), the Apptology was introduced.
+
+The apptology is an ontology that expresses what type of application a solution is (or what principal roles a solution
+is to fullfil), such as "screenReader", "magnifier", etc. There is nothing in the apptology preventing a solution from
+being of multiple types (e.g. both a screenreader and magnifier).
+
+The file that defines the mapping between the application type and common terms is located at
+[flat-apptology.json5](../testData/ontologies/mappings/flat-apptology.json5).
+
+Note that while the apptology can be used to describe any type or capability of a solution, it should only be used to
+express mutually exclusive application types. So, for example, it only makes sense that a single screenreader is running
+on the system at any given time, whereas it is ok for multiple text-editors to be running on the system at the same
+time. The reason for this is that the flat matchmaker uses the apptology to decide which applications to launch on the
+system, or more specifically, when two solutions are conflicting in their capabilities/roles and only one should be
+launched. A more detailed description of the Flat Matchmaker flow can be found here: [Flat Matchmaker
+Documentation](FlatMatchMaker.md).
+
+There are several known and serious issues with the apptology and its current usage, amongst others that it doesn't
+express how to turn on and off certain features of a solution, which could affect its role (or solution type) - such as
+turning off the voice in a combined screenreader/magnifier. The issues are described in this JIRA:
+[GPII-1998](https://issues.gpii.net/browse/GPII-1998)
diff --git a/documentation/AuthGrantFinder.md b/documentation/AuthGrantFinder.md
new file mode 100644
index 000000000..8606d7f4b
--- /dev/null
+++ b/documentation/AuthGrantFinder.md
@@ -0,0 +1,30 @@
+# Authorization Grant Finder
+
+The authorization grant finder provides API that allows users to use access tokens to retrieve their corresponding
+authorization information. The access tokens can be requested via [Resource Owner GPII key
+Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant).
+
+## getGrantForAccessToken(accessToken)
+
+* **description**: Retrieve the authorization information of an access token.
+* **parameters:**
+ * accessToken: String. A string representing an authorization issued to the client.
+* **return:** A promise object. Once resolved, this object contains the authorization information for the access token.
+ Return `undefined` when the access token is not found.
+ * An example of the returned object:
+
+```JSON
+{
+ "accessToken": "the_input_accessToken",
+ "gpiiKey": "bob_gpii_key",
+ "allowSettingsGet": true,
+ "allowSettingsPut": true,
+ "allowedPrefsToWrite": [
+ "http://registry.gpii.net/common/language",
+ "http://registry.gpii.net/common/DPIScale",
+ "http://registry.gpii.net/common/highContrast/enabled",
+ "http://registry.gpii.net/common/selfVoicing/enabled",
+ "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings"
+ ]
+}
+```
diff --git a/documentation/AuthServer.md b/documentation/AuthServer.md
new file mode 100644
index 000000000..db3274e85
--- /dev/null
+++ b/documentation/AuthServer.md
@@ -0,0 +1,51 @@
+# Authorization Server:
+
+The authorization server adds access control to the cloud-based flow manger to secure access to user preferences. We are
+implementing the authorization server using the [OAuth 2.0 Authorization Framework](http://oauth.net/2/). See [GPII
+OAuth 2 Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide) for the implementation of GPII supported OAuth 2.0 grant
+types.
+
+## Supported GPII Clients
+
+The authorization server authorizes **GPII App installations**
+
+* **Custom OAuth2 grant**: [Resource Owner GPII Key
+ Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant).
+* **Capabilities**: GPII App installations can retrieve untrusted settings from GPII Cloud.
+* Local Flow Manger within the GPII App installation interacts with GPII Cloud-Based Flow Manager to:
+ * Retrieve user settings
+ * Update user preferences
+
+## APIs
+
+### Request an access token (POST /access_token)
+
+* **description**: Returns an access token. Requesting an access token is the first step of using Resource Owner GPII
+ Key Grant. This access token is required by HTTP requests that retrieve or update user settings.
+* **route:** `/access_token`
+* **body:**
+ * `grant_type`: Must be "password"
+ * `client_id`: The OAuth2 client ID.
+ * `client_secret`: The OAuth2 client secret.
+ * `username`: A GPII key.
+ * `password`: Any string. Note that the Resource Owner GPII Key Grant is customized upon [the OAuth 2.0 Resource
+ Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) provided by [the oauth2orize
+ library](https://github.com/jaredhanson/oauth2orize). The value of the `password` field can NOT be left empty due
+ to the requirement of this library.
+
+An example of the POST body:
+
+```snippet
+grant_type=password&client_id=pilot-computer&client_secret=pilot-computer-secret&username=li&password=dummy
+```
+
+* **method:** `POST`
+* **return:** An object, containing the access token and its lifetime in second. For example:
+
+```snippet
+{
+ "access_token": {String},
+ "expiresIn":3600,
+ "token_type":"Bearer"
+}
+```
diff --git a/documentation/AuthorizationService.md b/documentation/AuthorizationService.md
new file mode 100644
index 000000000..4709486c5
--- /dev/null
+++ b/documentation/AuthorizationService.md
@@ -0,0 +1,80 @@
+# Authorization Service
+
+The authorization service provides API that allows to add and retrieve authorization data for GPII App installations.
+GPII App Installations are authorized using [Resource Owner GPII key
+Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant).
+
+* [getInfoByAccessToken(accessToken)](#getinfobyaccesstokenaccesstoken)
+* [grantGpiiAppInstallationAuthorization(gpiiKey, clientId,
+ clientCredentialId)](#grantgpiiappinstallationauthorizationgpiikey-clientid-clientcredentialid)
+
+## getInfoByAccessToken(accessToken)
+
+* **description**: Get the authorization information that is associated with the access token.
+* **parameters:**
+ * accessToken: String. A string representing an authorization issued to the client.
+* **return: (one of the below)**
+ * An object. For GPII app installation clients, the object contains: the access token that matches the given
+ parameter, the client and authorization data. An example:
+ ```json
+ {
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "clientCredential": {
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "oauth2ClientSecret": "client_secret_ajc_bakersfield",
+ "allowedIPBlocks": null,
+ "allowedPrefsToWrite": null,
+ "isCreateGpiiKeyAllowed": false,
+ "isCreatePrefsSafeAllowed": false,
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null,
+ "id": "clientCredential-1"
+ },
+ "authorization": {
+ "type": "gpiiAppInstallationAuthorization",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "gpiiKey": "chrome_high_contrast",
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-05-29T17:54:00.000Z",
+ "timestampRevoked": null,
+ "timestampExpires": "3020-05-30T17:54:00.000Z",
+ "id": "gpiiAppInstallationAuthorization-1"
+ }
+ }
+ ```
+ * `undefined`. `undefined` is returned in any of these cases:
+ * The authorization that matches the given access token is not found;
+ * The authorization has been revoked.
+
+## grantGpiiAppInstallationAuthorization(gpiiKey, clientId, clientCredentialId)
+
+* **description**: Grant an authorization to a GPII app installation. The authorization allows a GPII app installation
+ to access user preferences associated with the given GPII key. A new authorization will always be generated and
+ returned regardless whether the previous authorization assigned for the same gpiiKey + clientId combination has
+ expired.
+* **parameters:**
+ * gpiiKey: String. A GPII key that associates with user preferences.
+ * clientId: String. A system generated unique string that identifies the client.
+ * clientCredentialId: String. A system generated unique string that identifies the valid client credential of this
+ client with clientId.
+* **return:** Object. Contains an access token and the number of seconds that the access token will expire. For example:
+
+```json
+{
+ "accessToken": "8ea3457bf283db5d34ea5a4079fa36b2",
+ "expiresIn": 3600
+}
+```
+
+Return an object that contains the error message and HTTP status code if,
+
+* The GPII key is not found, or,
+* The client is not a GPII App Installation Client.
diff --git a/documentation/BrowserChannel.md b/documentation/BrowserChannel.md
new file mode 100644
index 000000000..34a1f8588
--- /dev/null
+++ b/documentation/BrowserChannel.md
@@ -0,0 +1,122 @@
+# The browserChannel and the WebSockets settings handler
+
+This document describes how the __Browser Channel__ and the __WebSockets__ settings handler work.
+
+This feature consists on:
+
+* A route in the Flow Manager that serves as the entry point for clients: `/browserChannel`
+* The components behind this route are the _gpii.flowManager.browserChannel.handler_
+ and the _gpii.settingsHandlers.webSockets.component_
+
+## The browser channel
+
+This handler processes every request to `ws://localhost:8081/browserChannel` and is responsible for:
+
+* Processing every request and determining whether a client is allowed or not to connect
+* Registering and removing the clients as they connect or disconnect.
+* Processing modifications of settings that are caused by other aspects of the system, e.g. a new user logs in.
+
+The browser channel handler supports the following request messages and sends the
+associated responses. When an error occurs, the handler sends an response and
+closes the web sockets connection.
+
+* A client sends a connection request. In this example, the client is UIO+:
+ * request: `{type: "connect", solutionId: "net.gpii.uioPlus"}`
+ * response: `{type: "connectionSucceeded, "payload": {initial settings values for the solutionId}}`
+* Client sends a request to change settings values:
+ * request: `{type: "changeSettings", "payload": {settings values to change}}`
+ * response: `{type: "changeSettingsReceived", "payload": {settings values after changing}}`
+* Some other component of the system changes a setting relevant to connected clients:
+ * response: `{type: "onChangeSettings", "payload:" {settings values after changing}}`
+* Error response when connecting with an unknown solution:
+ * response: `{isError: true, message: "Rejecting a connection request from _solutionId_.
+ The solution id was not found in the solutions registry"}`
+* Error response when trying to connect more than once:
+ * response: `{isError: true, message: "Connection already established - cannot send a second connect message"}`
+
+## The WebSockets settings handler
+
+This settings handler follows the standard settings handler API and exposes both the .get and .set methods to the rest
+of the system. The settings handler is an instance of `gpii.settingsHandler.webSockets.component`, which can be found
+in _gpii/node_modules/settingsHandlers/src/WebSocketsComponent.js_.
+
+This component stores the information about clients and keeps a list of settings for every solution that makes use of
+this settings handler. Also, this component notifies connected clients whenever their settings change.
+
+## Usage
+
+This small and documented client illustrates the workflow.
+
+```javascript
+var Ws = require("ws");
+
+// The client starts the communication
+
+var socket = new Ws("ws://localhost:8081/browserChannel");
+
+// When the connection is done, the client tells to the flow manager its id
+
+socket.on("open", function () {
+ console.log("## Socket connected");
+ socket.send(JSON.stringify({
+ type: "connect",
+ payload: {
+ solutionId: "net.gpii.uioPlus"
+ }
+ }));
+});
+
+socket.on("message", function (data) {
+ console.log("## Received the following message: " + data);
+ var message = JSON.parse(data);
+ // Right after sending the id to the flow manager, the server will return back
+ // the current settings in the system (if any)
+ if (message.type === "connectionSucceeded") {
+ console.log("## Got initial settings ", message.payload, " on connection");
+ }
+ // By listening to this message type, the client will be notified when the system has
+ // new settings to be applied on the client side
+ else if (message.type === "onSettingsChanged") {
+ console.log("## Got changed settings ", message.payload);
+ }
+});
+```
+
+The workflow between the client and server can be summarised as follows:
+
+* After connecting to the flow manager, the client sends a socket message to the channel, which is a payload containing
+ the *id* of the client, in this instance `net.gpii.uioPlus`.
+* The client will be registered if the solution's id can be found of the solutions registry, otherwise, the registration
+ will be rejected and the system will emit en error, and the client will disconnect.
+* The client can request changes to its settings by sending a _changeSettings_ message type. If successful, the client
+ is sent a _changeSettingsReceived_ message type.
+* When a _connectionSucceeded_, _changeSettingsReceived_, or an _onSettingsChanged_ signal is sent to the client, the
+ current available settings for the client are sent as well, e.g.:
+ ```json
+ {
+ "characterSpace":1,
+ "clickToSelectEnabled":false,
+ "contrastTheme":"wb",
+ "fontSize":1.1,
+ "inputsLargerEnabled":false,
+ "lineSpace":1,
+ "selectionTheme":"default",
+ "selfVoicingEnabled":false,
+ "simplifiedUiEnabled":false,
+ "syllabificationEnabled":false,
+ "tableOfContentsEnabled":false,
+ "wordSpace":1
+ }
+ ```
+* When a client disconnects, it is removed from the list of registered clients
+
+## Running the sample client
+
+An example client is avaiable at [../examples/browserChannelClient](../examples/browserChannelClient). To try it out, first
+start the GPII test configuration from the root of universal with
+
+ npm start
+
+Then start the client from [../examples/browserChannelClient](../examples/browserChannelClient) with
+
+ node browserChannelClient.js
diff --git a/documentation/CloudBasedFlow.md b/documentation/CloudBasedFlow.md
new file mode 100644
index 000000000..4d3c5756a
--- /dev/null
+++ b/documentation/CloudBasedFlow.md
@@ -0,0 +1,48 @@
+# Cloud Based Flowmanager Flow
+
+This page describes the flow when GPII is run in cloud based flowmanager mode. This mainly involves two files:
+
+* `CloudBasedFlowManager.js` - This will be referred to as `Cloud Based FlowManager` in the below
+* `MatchMaking.js` - referred to in the below as `FlowManagerRequests`.
+
+## Overview and APIs
+
+The Cloud Based FlowManager can be considered as a standalone, cloud-only configuration of the GPII. This is relevant
+for applications that are built to support this, and running on a system without the core GPII installed. So for
+example, a simple javaphone that has a built-in support for GPII, but is not running the core architecture. This can
+send a GPII key and platform information to the Cloud Based FlowManager, and get instructions back on how it should be
+set up.
+
+The endpoint for this service is at `:gpiiKey/settings/:device` (where `:gpiiKey` is the GPII key to log in and
+`:device` is information about the current platform.)
+
+## Detailed walkthrough of flow:
+
+The process of retrieving application lifecycle instructions in the Cloud Based FlowManager mode is as follows:
+
+1. GET request is sent to the `:gpiiKey/settings/:device` URL where `:gpiiKey` is the GPII key to log in and `:device`
+ is a device reporter payload, like: `{"OS":{"id":"web"},"solutions":[{"id":"org.chrome.cloud4chrome"}]}`. It is
+ handled by the `onSettings` (Cloud Based FlowManager) function which ensures the device payload is valid and fires
+ two events: `onGpiiKey` and `onDeviceContext`
+2. `onGpiiKey` event has two listeners:
+ * `getPreferences` (FlowManagerRequests) which fetches the preferences and fires the `onPreferences` event when the
+ preferences are fetched.
+ * `setGpiiKey` (FlowManagerRequests) which sets the gpiiKey property in the handler
+3. `onDeviceContext` event has one listener:
+ * `getSolutions` (FlowManagerRequests) which fetches the solutions registry and filters it based on the device
+ reporter info. The `onSolutions` event is fired with the result. The `onReadyToMatch` event is listening to the
+ three events described above: `onDeviceContext`, `onPreferences` and `onSolutions`. When these three events have
+ been fired, the `onReadyToMatch` event will be triggered.
+4. This event signal that all the resources has been fetched and the matchmaking related portion of the workflow starts.
+ This is done via the `processMatch` pseudo event, which is listening to the `onReadyToMatch` event and triggered from
+ it. The `processMatch` event kickes off a set of functions fired sequentially as dictated by the
+ `flowManager.processMatch.priorities`. As with everything else in this flow, the sequence and ordering of the steps
+ in the matchmaking process can be modified by the config/setup being used, and this document wont dive into the
+ details of this flow except for some general observatiosn:
+ * Generally some prioritized steps: `preProcess`, `matchMakerDispatcher`, `updateActivePrefsSetName` and `transform`
+ will be run (in that order).
+ * The end result of this process is a description of the configuration to be applied to system, described per
+ application. The event `onMatchDone` signals that we have the lifecycle instructions ready. For more details on the
+ MatchMaker frameworks internal workings, see: [MatchMaker Framework Documentation](MatchMakerFramework.md)
+5. `onMatchDone` is being listened to by the `matchToSettings` function (Cloud Based FlowManager) which takes and puts
+ the lifecycle instructions in the response to the requesting application.
diff --git a/documentation/Configs.md b/documentation/Configs.md
new file mode 100644
index 000000000..0223ccf5b
--- /dev/null
+++ b/documentation/Configs.md
@@ -0,0 +1,86 @@
+# System Launch and Configs
+
+## Overview
+
+The system can generally run in three different modes:
+
+* **Locally Installed GPII**: The most common mode is as a locally installed system. This is what end users would have
+ running on their system, and is able to allow a user to log in, auto-configure the system, launch applications, etc.
+* **Cloud-based Flowmanager**: This is a mode which is supposed to be run in the cloud. It does not allow a user to log
+ in, and does no configuration (i.e. has no lifecycle manager). Instead, on request, it is able to serve a user's
+ preferences in a format required by one or more applications. This is useful for applications that run without a
+ locally installed GPII (for example, web applications).
+* **Custom Configs**: In addition to running the system as a local install or with a cloud-based flowmanager, it is also
+ possible to run the system in other configurations. Examples are: Preferences server only, Solution Registry only,
+ Flat MatchMaker only. These are generally useful for production mode where you might want individual (remote) servers
+ set up for each of the different services used by the system.
+
+Note that for both the locally installed system and cloud-based flowmanager mode, depending on the exact configuration
+used, components such as the solutions registry, preferences server, etc., can be set up to run both locally or
+remotely. A description of how to run the system in these modes and of the configs is given below.
+
+## System Launch
+
+### Locally Installed
+
+Since running the system in local install mode is expected to configure the machine and launch applications on login, it
+is dependent on the platform. For this reason, running the system locally is done from the platform-specific folders -
+that is windows, linux or android.
+
+If you have installed GPII in the folder :
+
+* In Microsoft Windows, go to \windows
+* In GNU/Linux and other Unix-like systems, go to /linux
+* In Android, go to /android
+
+Then run the command: `node gpii.js`
+
+By default this will start up the system with everything running locally, using the
+`gpii.config.development.manualTesting.json5` configuration file of universal repository (see the `universal/gpii/configs`
+folder). If you would like to run the system using a different configuration, say `dev.remote.prefs` run the following:
+
+* in Microsoft Windows: `SET NODE_ENV=dev.remote.prefs`
+* in GNU/Linux and other Unix-like systems: `export NODE_ENV=dev.remote.prefs`
+
+Followed by the `node gpii.js` command.
+
+### Cloud-Based Flowmanager
+
+Since the Cloud-Based Flowmanager does not require any platform-specific bindings (it only returns a modified set of
+settings - but does not configure anything), you can run it from universal. Given that you have installed GPII in the
+folder , go to: `/node_modules/universal`.
+
+Change the NODE_ENV environment variable to the cloudbased config file:
+
+* in Microsoft Windows: `SET NODE_ENV=gpii.config.cloudBased.development.manualTesting`
+* in GNU/Linux and other Unix-like systems: `export NODE_ENV=gpii.config.cloudBased.development.manualTesting`
+
+Then start up the server by running the following command from the universal folder:
+
+`node gpii.js`
+
+This will set up the system to run in cloud-based mode, but with everything running on the same (local) device.
+
+## Special Configs
+
+### Hide Key-in, Key-out Endpoints
+
+**Config file**: [``%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5)
+
+**Purpose**: This config is often used when starting the system in the production environment. For security reason,
+it stops exposing these key-in and key-out http endpoints: /login, /logout, /proximityTriggered.
+
+### Reset the Computer to Default Settings When the System Starts
+
+**Config file**: [``%flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5)
+
+**Purpose**: This config resets the computer to the default settings at the system startup. See [Reset Computer](ResetComputer.md)
+for the reset API and how to use it.
+
+### Fetch Default Settings from a Remote URL
+
+**Config file**: [``%flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5)
+
+**Purpose**: This config fetches default settings from a remote URL. The default URL is set to
+`https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5`
+in this config. See [Reset Computer](ResetComputer.md) for what are default settings.
diff --git a/documentation/CouchConnector.md b/documentation/CouchConnector.md
new file mode 100644
index 000000000..188913897
--- /dev/null
+++ b/documentation/CouchConnector.md
@@ -0,0 +1,49 @@
+# Couch Connector:
+
+When GPII runs using a development configuration, the couchConnector starts up a test instance of CouchDB and
+provisions it with sample data. This "Couch Connector" consists of:
+
+* An instance of CouchDB provided by the [gpii-couchdb-test-harness](https://github.com/GPII/gpii-couchdb-test-harness)
+ package, which runs in either Docker or a combination of Vagrant and Docker depending on your local setup. This is
+ configured to listen on port `25984`.
+* An express server that provides a `/reset-couch` API to restore the initial data set. By default, it listens on port
+ `8060`.
+
+Note that when GPII runs using a production configuration, the authorization server expects to work with a local CouchDB
+instance. The couchConnector is not needed or started for those configurations.
+
+## APIs
+
+The Docker/Vagrant test harness runs an instance of CouchDB, which provides its own [well-documented
+API](http://docs.couchdb.org/en/stable/api/index.html).
+
+In addition, there is a small REST API that can be used to reload the data in the CouchDB instance, which provides a
+single endpoint.
+
+### GET /reset-couch
+
+Reprovisions the CouchDB instance with the initial data set, listening on port 8060 by default.
+
+On success returns a 200 status code and a message like:
+
+`Success: CouchDB has been reprovisioned with the initial data set.`
+
+## `gpii.couchConnector`
+
+The Infusion component responsible for provisioning and resetting a CouchDB instance on request.
+
+### Component Options
+
+| Option | Type | Description | Default |
+| ----------------- | ---------- | ----------- | ------- |
+| `resetServerPort` | Integer | Required. The port on which the express server for restoring the initial data set will run. | 8060 |
+| `databases` | Object | [optional] The list of databases to provision. See [the gpii-couchdb-test-harness documentation for more details](https://github.com/the-t-in-rtf/gpii-couchdb-test-harness/blob/GPII-3531/docs/harness.md#the-databases-option). | None |
+
+### Supported Events
+
+| Event | Description | Parameters | Parameters Description |
+| ----------------------------- | ----------- | ---------- | ---------------------- |
+| `onCouchStartupComplete` | Fires when the CouchDB instance has completed startup and is ready to handle requests. | None | |
+| `onExpressResetServerStarted` | Fires when the express server used to resetting the data set is ready. | None | |
+| `onReady` | Fires when both CouchDB and the express "reset" server are ready. | None | |
+| `onCouchProvisioningComplete` | Fires once the CouchDB instance has been reprovisioned, typically in response to a "reset" request. | None | |
diff --git a/documentation/DataLoader.md b/documentation/DataLoader.md
new file mode 100644
index 000000000..bb7b23a5e
--- /dev/null
+++ b/documentation/DataLoader.md
@@ -0,0 +1,92 @@
+# CouchDB Data Loader
+
+(`scripts/deleteAndLoadSnapsets.sh`)
+
+This script is used to setup CouchDB database and is executed as a Kubernetes batch Job every time a new version of the
+universal image is deployed to the cluster (also when cluster is initially created).
+
+It does the following:
+
+1. Converts the preferences in universal into `snapset` Prefs Safes and their associated GPII Keys,
+2. Optionally deletes the existing database,
+3. Creates a CouchDB database if none exists,
+4. Updates the database with respect to its `_design/views` document, as required,
+5. Deletes the `snapset` Prefs Safes and their associated GPII Keys, if any, currently in the database,
+6. Loads the latest snapsets and associated keys created at step 1. into the database.
+
+Steps 4, 5, and 6 are handled by, and documented further in [`scripts/deleteAndLoadSnapsets.js`](https://github.com/GPII/universal/blob/master/scripts/deleteAndLoadSnapsets.js#L11).
+
+## Environment Variables
+
+With the exception of `GPII_COUCHDB_URL`, the following environment variables have default values defined within
+`scripts/deleteAndLoadSnapsets.sh`. The database, `GPII_COUCHDB_URL`, must be set outside of the script. Developers
+can set these variables as needed for testing and experimentation.
+
+The use of environment variables for data directories is also useful if you want to mount the database data using a Docker
+volume and point the data loader at it.
+
+WARNING: setting `GPII_CLEAR_INDEX` to `true` will erase all the contents of the database. Use with caution, and with
+your own database for development. In a staging or production environment, these variables are set appropriately for
+those contexts; in particular `GPII_CLEAR_INDEX` will not be set.
+
+- `GPII_COUCHDB_URL`: URL of the CouchDB database. (required)
+- `GPII_CLEAR_INDEX`: If set to `true`, the database at `$GPII_COUCHDB_URL` will be deleted and replaced with an empty
+ database. (optional)
+- `GPII_STATIC_DATA_DIR`: The directory where the static data to be loaded into CouchDB resides. (optional)
+- `GPII_PREFERENCES_DATA_DIR`: The directory containing the "raw" preferences that are converted into `snapset` Prefs
+ Safes and their associated GPII Keys (step 1 above). (optional)
+- `GPII_SNAPSET_DATA_DIR`: The directory where the data built from the conversion step reside. (optional)
+- `GPII_APP_DIR`: The main directory, typically `universal`. (optional)
+
+Note that since [the docker doesn't support the environment variable type of
+array](https://github.com/moby/moby/issues/20169), separate environment variables are used for inputting data
+directories instead of one array that holds these directories.
+
+## Running
+
+### Example using containers
+
+```bash
+$ docker run -d -p 5984:5984 --name couchdb couchdb
+$ docker run --rm --link couchdb -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \
+ -e GPII_CLEAR_INDEX=true vagrant-universal scripts/deleteAndLoadSnapsets.sh
+$ docker run -d -p 8081:8081 --name preferences --link couchdb \
+ -e NODE_ENV=gpii.config.preferencesServer.standalone.production \
+ -e PREFERENCESSERVER_LISTEN_PORT=8081 -e DATASOURCE_HOSTNAME=http://couchdb \
+ -e DATASOURCE_PORT=5984 vagrant-universal
+```
+
+Below are two versions of loading couchdb data from a different location (e.g.
+/home/vagrant/sync/universal/testData/dbData for static data directory and /home/vagrant/sync/universal/build/dbData for
+build data directory). The first version has the optional `GPII_CLEAR_INDEX` set to true to erase and reset the
+database prior to other database changes:
+
+```bash
+$ docker run --name dataloader --link couchdb \
+ -v /home/vagrant/sync/universal/testData/dbData:/static_data -e GPII_STATIC_DATA_DIR=/static_data \
+ -v /home/vagrant/sync/universal/build/dbData:/build_data -e GPII_SNAPSET_DATA_DIR=/build_data \
+ -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \
+ -e GPII_CLEAR_INDEX=true vagrant-universal scripts/deleteAndLoadSnapsets.sh
+```
+
+The second version does not set `GPII_CLEAR_INDEX` such that any existing database is left intact prior to subsequent
+changes to it (e.g., deleting the snapsets):
+
+```bash
+$ docker run --name dataloader --link couchdb \
+ -v /home/vagrant/sync/universal/testData/dbData:/static_data -e GPII_STATIC_DATA_DIR=/static_data \
+ -v /home/vagrant/sync/universal/build/dbData:/build_data -e GPII_SNAPSET_DATA_DIR=/build_data \
+ -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \
+ vagrant-universal scripts/deleteAndLoadSnapsets.sh
+```
+
+### Example using a CouchDB installation
+
+You can provision a CouchDB installation using your local clone of `universal` for testing and development as well.
+
+```bash
+GPII_COUCHDB_URL="http://localhost:5984/gpii" GPII_APP_DIR=$(pwd) bash -c ./scripts/deleteAndLoadSnapsets.sh
+```
+
+In the above example we've created a `gpii` database in our local CouchDB database, and are working in the top level
+`universal` directory.
diff --git a/documentation/DataModel.md b/documentation/DataModel.md
new file mode 100644
index 000000000..455ea3bc8
--- /dev/null
+++ b/documentation/DataModel.md
@@ -0,0 +1,6 @@
+# GPII Data Model
+
+GPII uses CouchDB to store data in JSON documents. The two GPII components that read and write data from the data
+storage are the Preferences Server and the Authorization Server.
+
+The details of the GPII data model can be found [here](https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences).
diff --git a/documentation/FlatMatchMaker.md b/documentation/FlatMatchMaker.md
new file mode 100644
index 000000000..9e8271aeb
--- /dev/null
+++ b/documentation/FlatMatchMaker.md
@@ -0,0 +1,23 @@
+# Flat Matchmaker
+
+## Motivation and Description
+
+This is a very simple reference matchmaker written by the architecture team when building the initial architecture. It
+was created to ensure that the framework was able to support the various aspects of the matchmaking process, to make
+available some potentially more globally useful functionality to the matchmaker teams, have a base matchmaker that could
+be used while the development of the more sophisticated matchmakers was being done.
+
+## Selection of solutions to configure and launch
+
+This involves four steps:
+
+1. Finding the leaves of all the (common term) preferences - 'leaves' meaning all the keys of the preference set that
+ holds a primitive as it's value.
+2. Finding all the capabilities of the solutions on the machine. The capabilities are taken from two sources, both part
+ of the solutions registry entry of each solution:
+ * Those explicitly expressed in the solution entry's `capabilitities` block
+ * The input-paths, as calculated from the solution entry's `capabilitiesTransformations` block.
+3. Knowing what preferences the user has (in common terms) and what capabilities each solution has, the flat matchmaker
+ now simply selects those solutions that have a capability matching a user preference.
+4. Any solution present on the device for which the user has application-specific preferences is added.
+5. Apply [Apptology](Apptology.md) to select only one solution from each solution type.
diff --git a/documentation/FlowManager.md b/documentation/FlowManager.md
new file mode 100644
index 000000000..6b65e7bef
--- /dev/null
+++ b/documentation/FlowManager.md
@@ -0,0 +1,295 @@
+# Flow Manager
+
+The flow manager is the central point of coordination in the system for managing flow between different architecture
+components. For example, it coordinates the steps involved during logging in, which require retrieving preferences,
+solutions, device data, etc. and passing these to the [MatchMaker Framework](MatchMakerFramework.md). Following those
+steps, the payload is sent to the [LifecycleManager](LifecycleManager.md).
+
+## Important flows
+
+Depending on what the usage of the system is, the flows will be different. For example user login, user log off, and
+retrieving settings from the system in "cloud based flowmanager" mode are all different. Each "flow" is managed in a
+different file, with the common events, functions, etc., located in `FlowManager.js` and `MatchMaking.js`. The
+different kinds of flows are:
+
+* **User Login** (`UserLogonHandlers.js`) - the flow for a user keying in to the system. The flow is described in
+ details in the [loginAndLogoutFlow](LoginAndLogoutFlow.md) document
+* **User Logout** (`UserLogonHandlers.js`) - the flow for a user keying out of the system
+* **User Logon State Change** (`UserLogonHandlers.js`) - the flow for changing a user's logon state
+* **Retrieve Settings** (`CloudBasedFlowManager.js`) - used to retrieve the settings when the system is running in
+ cloud-based mode. See [CloudBasedFlow](CloudBasedFlow.md) for more details
+* **Update Preferences** (`CloudBasedFlowManager.js`) - used to update the preferences when the system is running in
+ cloud-based mode. See [CloudBasedFlow](CloudBasedFlow.md) for more details
+
+## Reserved GPII Keys
+
+### noUser
+
+The reserved GPII key "noUser" is automatically keyed into the system when there is not an actual key keyed in. This includes:
+
+* When GPII starts
+* Once an actual GPII key is keyed out
+
+The present of "noUser" key allows users to continue to change settings via QSS (Quick Strip Set) when no actual GPII
+key is keyed into the system.
+
+### reset
+
+The reserved GPII key "reset" is used by [the flow manager login API](ResetComputer.md#reset-via-http-request) to
+reset the computer.
+
+See [Reset Computer Documentation](ResetComputer.md) for more details about the reset workflow.
+
+Note that a separate logout of "reset" is not necessary. The final condition of using the "reset" key is to have the
+"noUser" key log back in the system.
+
+### restore
+
+The reserved GPII key "restore" is used by the journal API to restore a specific journal. The API is:
+
+GET /journal/restore/:journalId
+
+Note that a separate logout of "restore" is not necessary. The final condition of using the "reset" key is to have the
+"noUser" key log back in the system.
+
+### readSetting
+
+The reserved GPII key "readSetting" is used by the PSPChannel read API to read a preference value.
+
+Note that "readSetting" GPII key does not log into the system at any time. It's only used to construct an initial
+payload structure to start a matchMaking process.
+
+## APIs on Local Flow Manager
+
+### User Logon state change (GET /user/:gpiiKey/proximityTriggered)
+
+* **description**: Change the logon state for the user with the given `:gpiiKey`. Note that there is a debounce
+ functionality implemented following these rules: any RFID actions is ignored for if a login/logout for
+ is in progress OR if the last login/logout process for finished less than 1.5 seconds ago. For
+ more details for rules on keying and out, see [LoginAndLogoutFlow](LoginAndLogoutFlow.md)
+* **route:** `/user/:gpiiKey/proximityTriggered` where `:gpiiKey` should be the GPII key of the user for which to change
+ the logon state
+* **return:** Message on success or failure of the login/logout
+
+### User Login (GET /user/:gpiiKey/login)
+
+* **description**: Log in a user to the system
+* **route:** `/user/:gpiiKey/login` where :gpiiKey should be the GPII key of the user
+* **method:** `GET`
+* **return:** Message saying that user successfully logged into the system or an error message.
+
+### User Logout (GET /user/:gpiiKey/logout)
+
+* **description**: Log out a user of the system
+* **route:** `/user/:gpiiKey/logout` where `:gpiiKey` should be the GPII key of the user
+* **method:** `GET`
+* **return:** Message saying that user successfully logged out of the system or an error message.
+
+## APIs on Cloud Based Flow Manager
+
+### Check the readiness of Cloud Based Flow Manager (GET /ready)
+
+* **description**: Check whether Cloud Based Flow Manager is ready to handle requests.
+ * When Cloud Based Flow Manager and Preferences Server are running together as one server, the readiness endpoint
+ checks the database connection.
+ * When Cloud Based Flow Manager and Preferences Server are running as separate servers, the readiness endpoint
+ checks the communication with Preferences Server.
+* **route:** `/ready`
+* **method:** `GET`
+* **return:** Return http status code 200 when Cloud Based Flow Manager is ready to handle requests. Otherwise, return
+ http status code 404.
+
+### Check the liveness of Cloud Based Flow Manager (GET /health)
+
+* **description**: Check whether Cloud Based Flow Manager itself is running. A running Cloud Based Flow Manager may or may
+ not be ready to handle requests because the liveness endpoint does not check communications between Cloud Based
+ Flow Manager with other modules such as Preferences Server and the database.
+* **route:** `/health`
+* **method:** `GET`
+* **return:** Return http status code 200 when Cloud Based Flow Manager itself is running. Otherwise, return http status
+ code 500.
+
+### Retrieve the revision of the Cloud Based Flow Manager (GET /revision)
+
+* **description**: Serve the full `SHA256` of the revision of the source code repository used by this deployment of the
+ cloud based components of the GPII.
+* **route:** `/revision`
+* **method:** `GET`
+* **return:** A JSON document containing the revision:
+
+```json
+{
+ "sha256": "2602bdf868aec49993d8780feec42d4e9f995e21"
+}
+```
+
+### Get an access token (POST /access_token)
+
+* **description**: Access tokens are credentials used to protect user preferences. An access token represents an
+ authorization issued to a GPII application. It needs to be provided at retrieving or updating user preferences.
+ An access token will not be granted in these cases:
+ * The OAuth2 client associates with an allowed IP range and the ip of the incoming request doesn't belong to this
+ range.
+ * The OAuth2 client requests access to a nonexistent GPII key but this client doesn't have privilege to create new
+ GPII keys or preferences safes.
+* **route:** `/access_token` with these parameters in the `POST` body using the `application/x-www-form-urlencoded` Content-Type.
+ * `grant_type`: must be set to "password".
+ * `client_id`: the OAuth2 client id.
+ * `client_secret`: the OAuth2 client_secret. Confidential shared secret, used to verify the identity of the OAuth2
+ client
+ * `username`: the GPII key.
+ * `password`: any string.
+* **method:** `POST`
+* **return:** A JSON document with an access token:
+
+```json5
+{
+ "access_token": "carla",
+ "expiresIn": 3600,
+ "token_type": "Bearer"
+}
+```
+
+### Get lifecycle instructions from Cloud Based Flow Manager (GET /:gpiiKey/settings/:device)
+
+* **description**: Get settings in the ontology of preferences from the cloud based flow manager. These settings are
+ untransformed lifecycle instructions. See [an example of the return payload of this endpoint.](https://github.com/GPII/gpii-payloads/blob/master/CloudBasedFlowManagerUntrustedSettings.md#user-content-return-payload)
+* **route:** `/:gpiiKey/settings/:device` where:
+ * `:gpiiKey` should be the GPII key of the user for which the settings are requested.
+ * `:device` should be a device reporter payload - for example:
+ `{"OS":{"id":"linux"},"solutions":[{"id":"org.gnome.desktop.a11y.magnifier"}]}` would retrieve the settings for the
+ solution with ID `org.gnome.desktop.a11y.magnifier` which is a solution for `linux`.
+* **header:** Authorization: Bearer < access_token >
+ * `access_token` The access token can be first requested via /access_token endpoint. It represents the authorization
+ that grants a GPII app to access settings associated with a GPII key. Refer to [GPII OAuth2
+ Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant) about the detail steps.
+* **method:** `GET`
+* **return:** An object, containing the GPII key and solution registry entries. Each block in the solution registry
+ entries contains the relevant lifecycle instructions in a format understandable by the solution. For example:
+
+```json5
+{
+ "gpiiKey": "carla",
+ "solutionsRegistryEntries": {
+ "org.nvda-project": {
+ "name": "NVDA Screen Reader",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "configs": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini",
+ "allowNumberSignComments": true,
+ "allowSubSections": true
+ },
+ // ...
+ }
+ },
+ "configure": [
+ "settings.configs"
+ ],
+ "restore": [
+ "settings.configs"
+ ],
+ "start": [
+ {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\""
+ }
+ ],
+ "stop": [
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda_service.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda.exe"
+ }
+ ],
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.registryKeyExists",
+ "hKey": "HKEY_LOCAL_MACHINE",
+ "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe",
+ "subPath": "",
+ "dataType": "REG_SZ"
+ }
+ ],
+ "isRunning": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "nvda"
+ }
+ ]
+ }
+ // ...
+ },
+ "matchMakerOutput": {
+ "inferredConfiguration": {
+ "gpii-default": {
+ "applications": {}
+ },
+ "turn-down-light": {
+ "applications": {}
+ }
+ }
+ }
+}
+```
+
+### Update preferences on Cloud Based Flow Manager (PUT /:gpiiKey/settings)
+
+* **description**: Call the preferences server API to update user preferences, or to create a GPII key and its
+ associated preferences safe if the GPII key does not exist but the OAuth2 client has privilege to create new GPII
+ keys and preferences safes. In the case of update existing preferences, the preferences server API merges the
+ incoming preferences with the existing user preferences and update the merged preferences on the cloud based flow
+ manager.
+* **route:** `/:gpiiKey/settings` where:
+ * `:gpiiKey` should be the GPII key of the user for which the preferences are updated.
+* **header:** Authorization: Bearer < access_token >
+ * `access_token` The access token can be first requested via /access_token endpoint. It represents the authorization
+ that grants a GPII app to update settings associated with a GPII key. Refer to [GPII OAuth2
+ Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant) about the detail steps.
+* **method:** `PUT`
+* **request body:** An object, containing a subset of to-be-updated preferences. For example:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/onScreenKeyboard/enabled": true,
+ "http://registry.gpii.net/common/initDelay": 120,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850
+ }
+ }
+ }
+}
+```
+
+* **return:** An object, containing the GPII key and a status message. For example:
+
+```json
+{
+ "gpiiKey": "carla",
+ "message": "Successfully updated."
+}
+```
+
+The returned payload when the request is rejected:
+
+```json
+{
+ "isError": true,
+ "message": "Unauthorized"
+}
+```
diff --git a/documentation/LifecycleManager.md b/documentation/LifecycleManager.md
new file mode 100644
index 000000000..9957b5a93
--- /dev/null
+++ b/documentation/LifecycleManager.md
@@ -0,0 +1,121 @@
+# Lifecycle Manager
+
+The Lifecycle Manager is responsible for actually performing user login, logout, retrieving the active GPII key
+requests. It also configs the users system via setting handlers and launch handlers.
+
+The Lifecycle Manager is the only component in the system that keeps state. This is done in the "session" member of
+the Lifecycle Manager component, and tracks what changes have been done to the system, what the original configuration
+of the system was and which user is currently logged in.
+
+## Lifecycle Manager Model
+
+This section lists a few important model items that the Lifecycle Manager keeps track of.
+
+### Current User Logon
+
+The model path `currentUserLogon` holds information about the last actual user logon state for the proximityTriggered
+logon request to verify its [debounce rule](FlowManager.md#user-logon-state-change-get-usergpiikeyproximitytriggered).
+Note that this model item does not keep track of the logon of "noUser" key.
+
+This model structure has the following format:
+
+```snippet
+{
+ currentUserLogon: {
+ type: , // "login" or "logout"
+ gpiiKey: , // A GPII key`
+ timeStamp: // Timestamp that the logon occurs
+ }
+}
+```
+
+### Logon Change
+
+The model path `logonChange` holds information about the most recent login/logout action including the "noUser" key.
+
+This model structure has the following format:
+
+```snippet
+{
+ logonChange: {
+ type: , // "login" or "logout"
+ inProgress: // Whether the handling of the logon change is in progress
+ gpiiKey: , // A GPII key`
+ timeStamp: // Timestamp that the logon occurs
+ }
+}
+```
+
+## Lifecycle Manager Maintained Queues
+
+### User Logon Request Queue
+
+The Lifecycle Manager user logon request queue is used to hold all requests for user logging in and logging out.
+The queue is handled sequentially to avoid racing issues (e.g. the next request will wait until the previous request
+finishes before the processing).
+
+The logging in and logging out items in the queue should have the following format:
+
+```snippet
+{
+ gpiiKey: , // A GPII key
+ logonState: // "login" or "logout"
+}
+```
+
+### Action Queue
+
+The Lifecycle Manager action queue is used to hold the high-level action that needs to happen: starting the login process,
+starting logout process, starting the update process. Since the steps of these processes are asynchronous, the action
+queue was implemented to avoid racing issues between these processes (e.g. if a logout attempt is started before login
+is complete, etc).
+
+Each item in the action queue should have the following format:
+
+```snippet
+{
+ invokerName: ,
+ func: ,
+ arg:
+}
+```
+
+The variable `` is an unresolved (i.e. string) single-argument function that returns a
+promise. The promise should be resolved when the function is complete (including side-effects). `` is the
+argument to pass to the function. `` is the name of the invoker that was called on the Lifecycle Manager for
+triggering the adding of the item to the action queue.
+
+The action queue is run sequentially, and an item is considered "done" once the promise returned by its function is resolved.
+
+## Main Functions of Lifecycle Manager:
+
+### Handle User Logon Requests
+
+The lifecycle manager provides three invokers to handle three type of user logon requests: "login", "logout" and ["proximityTriggered"]((FlowManager.md#user-logon-state-change-get-usergpiikeyproximitytriggered)):
+
+* `performLogin`: Handle login requests
+* `performLogout`: Handle logout requests
+* `performProximityTriggered`: Handle proximityTriggered requests
+
+These invokers perform checks and add corresponding login and logout requests to the user logon request queue. Each
+request in the queue triggers the creation of a dynamic user logon request component that instantiates the corresponding
+request handler component:
+
+* `"gpii.lifecycleManager.loginRequest"`: handle login requests
+* `"gpii.lifecycleManager.logoutRequest"`: handle logout requests
+
+The dynamic request component will be automatically destroyed once the handling of the request completes.
+
+### Start, Stop and Update Processes
+
+The lifecycle manager has three invokers that are generally the ones that will be called from the general system, namely
+"start", "stop" and "update". These invokers are manually created, so are not obvious to spot on the component defaults
+block, but they are the ones to be used (instead of the processStart, processStop and processUpdate). They will add the
+relevant task to the Lifecycle Managers action queue:
+
+* `start`: Should be called when keying in (configuring the system)
+* `stop`: Should be called when keying out (restoring the system)
+* `update`: Should be called when changing the settings of an already configured system (updating the applied settings)
+
+Note that these are unrelated to, and should not be confused with the "start", "stop" and "update" directives from
+solutions registry entries.
diff --git a/documentation/LoginAndLogoutFlow.md b/documentation/LoginAndLogoutFlow.md
new file mode 100644
index 000000000..2f33db549
--- /dev/null
+++ b/documentation/LoginAndLogoutFlow.md
@@ -0,0 +1,84 @@
+# User login and Logout flow
+
+This document describes the flow on a locally installed GPII system when the user logs in and logs out. First an
+overview of the endpoints and related semantics are given and this will be followed by a more in-depth technical
+walkthrough of the flow.
+
+## Overview and APIs
+
+We support 3 different logon related URLs, namely:
+
+* `/user/:gpiiKey/proximityTriggered` - change the logon state of the given GPII key (i.e. log in or out)
+ * **Debounce rule**: any RFID actions is ignored for if a login/logout for is in progress OR
+ if the last login/logout process for finished less than 1.5 seconds ago
+ * If no user is logged in and debounce doesn't apply, log in
+ * If is logged in and debounce doesn't apply, log out
+ * If another user is already logged in or in the process of logging in or out, log that user out and log in
+
+* `/user/:gpiiKey/login` - log the GPII key in
+ * If no user is logged in, will be logged in
+ * If another user is logged in, nothing will happen and an error is returned
+* `/user/:gpiiKey/logout` - log the GPII key out
+ * If no user is logged in, nothing happens
+ * if user myGpiiKey is logged in, he will be logged out
+ * If another user is logged in, nothing will happen and an error is returned
+
+In general, the the `proximityTriggered` URL should be used by proximity devices. This will take the appropriate action
+depending on whether the user is logged into the system or not.
+
+The reason for preferring the `/proximityTriggered` URL over the `/logout` and `/login` URL, is that we support multiple
+user listeners and means of logging in and out of the system, so unless the service calling these URLs does a lot of
+work checking for currently logged in users, e.g. a `/logout` call when no user (or some other user) is logged in
+becomes meaningless. The implementation allows for these kinds of bogus calls to login and logout, simply resulting in
+an error response to the request.
+
+The reason for continuing to support the specific /login and /logout, instead of only supporting the
+`/proximityTriggered` URL is that they make sense for some user listeners, for example when using the the USB listener,
+one wouldn't expect to log a user out when inserting the USB, or log a user in when pulling the USB out. So for that, it
+seems to make sense that it attempts to log in (i.e. call `/login`) or log out (i.e. call `/logout` when
+inserting/removing the USB, respectively
+
+## Technical description
+
+The core part of the flow is defined in these files:
+
+* `UserLogonHandlers.js` contains the handling of the logon related endpoints kicks off the related process. It
+ contains individual handlers for the `login`, `logout` and `proximityTriggered` URLs. These handlers add user logon
+ request to Lifecycle Manager user logon request queue. Lifecycle Manager then processes the queue in sequence to
+ perform actual logging in, loggin out and retrieving the current logged in GPII key.
+* `UserLogonStateChange.js` contains the functionality for the actual logging in, logging out and retrieving the active
+ GPII key.
+* `MatchMaking.js` describes the remaining part of the flow (e.g. fetching resources and preferences, matchmaking,
+ etc.).
+
+The user login process is as follows:
+
+1. a GET request is sent to either `/user/:gpiiKey/login` or `/user/:gpiiKey/proximityTriggered`. This is retrieved by
+ the relevant handler in `UserLogonHandlers`. The handler adds a relevant login or logout request to Lifecycle
+ Manager user logon request queue, which trigger the actual logging in or logging out. If it is found that the GPII
+ key needs to be logged in, the `onGpiiKey` event is fired (via the
+ `gpii.lifecycleManager.userLogonHandling.loginUser` function)
+2. the `onGpiiKey` event has three listeners:
+ 1. UserLogonStateChange's `getDeviceContext`, which fetches the device reporter data. When this has been fetched an
+ `onDeviceContext` event is fired.
+ 2. `getPreferences` (FlowManagerRequests), which fetches the preferences and fires the `onPreferences` event when the
+ preferences are fetched.
+ 3. `setGpiiKey` (FlowManagerRequests) which sets the gpiiKey property in the handler
+3. the `onDeviceContext` event has one listener:
+ 1. `getSolutions` (FlowManagerRequests), which fetches the solutions registry and filters it based on the device
+ reporter info. The `onSolutions` event is fired with the result.
+4. The `onReadyToMatch` event is listening to the three events described above: `onDeviceContext`, `onPreferences` and
+ `onSolutions`. When these three events have been fired, the `onReadyToMatch` event will be triggered.
+5. This event signal that all the resources has been fetched and the matchmaking related portion of the workflow starts.
+ This is done via the `processMatch` pseudo event, which is listening to the `onReadyToMatch` event and triggered from
+ it. The `processMatch` event kickes off a set of functions fired sequentially as dictated by the
+ `flowManager.processMatch.priorities`. As with everything else in this flow, the sequence and ordering of the steps
+ in the matchmaking process can be modified by the config/setup being used, and this document won't dive into the
+ details of this flow except for some general observation:
+ 1. Generally some prioritized steps: `preProcess`, `matchMakerDispatcher`, `updateActivePrefsSetName` and `transform`
+ will be run (in that order).
+ 2. The end result of this process is a description of the configuration to be applied to system, described per
+ application. The even `onMatchDone` signals that we have the lifecycle instructions ready. For more details on the
+ MatchMaker frameworks internal workings, see: [MatchMaker Framework Documentation](MatchMakerFramework.md)
+6. `onMatchDone` is being listened to by the `startLifecycle` (UserLogonStateChange), which applies the settings to the
+ system via the functionality in the LifecycleManager.
diff --git a/documentation/MatchMakerFramework.md b/documentation/MatchMakerFramework.md
new file mode 100644
index 000000000..e35e24f3e
--- /dev/null
+++ b/documentation/MatchMakerFramework.md
@@ -0,0 +1,114 @@
+# Matchmaker Framework
+
+The matchmaker framework is a component and a set of utitilities that can run either locally or remotely depending on
+the configuration. Together with the actual matchmaker, it is responsible for deciding how the users device should be
+configured based on their preferences, the specified set of preferences therein, and the solution registry data. More
+specifically, the matchmaker framework has the following responsibility:
+
+* Doing the preprocessing - that is, preparing and augmenting the input payload for the specific matchmaker
+* Making the decision of which matchmaker to call, and then call that matchmaker (for example the
+ [Flat MatchMaker](FlatMatchMaker.md))
+* Doing the post-processing - that is, taking the return payload from the matchmakers, parse it and pass it on in the
+ login (or other) flow.
+* Generally providing utilities that can be used by matchmaker implementations.
+
+## Configuration
+
+The matchmaker framework itself is not a Kettle app, but it expect at least matchmaker implementation to be available on
+a URL. The available matchmaker implementation should be provided in the options block under a `matchMakers` directive.
+Here, matchmaker implementation can be listed by some identifier (key) and an object with a `{ url: "SOME URL" }` object
+each. For example the options of having two matchmakers available, where the default one will be on port 8081 will be
+done like:
+
+```json
+{
+ "type": "my.matchmakerframework.config",
+ "options": {
+ "distributeOptions": {
+ "untrusted.development.matchMakers": {
+ "record": {
+ "default": {
+ "url": "http://localhost:8081"
+ },
+ "otherMM": {
+ "url": "http://localhost:8084"
+ }
+ },
+ "target": "{that flowManager}.options.matchMakers"
+ }
+ }
+ }
+}
+
+```
+
+## Detailed Description:
+
+### Main steps of the matchMaker frameworks matching process
+
+1. The matchmaking process is triggered by the `processMatch` event of the FlowManager. The actual matchmaking flow is
+ dictated by a combination of the priorities in `gpii.flowmanager.processmatch.priorities`, and the `processMatch`
+ listeners (which are different depending on whether the system runs in trusted or untrusted mode)
+* Preprocess:
+ * Triggered when the initial data has been collected (preferences, named preferences set, solution registry entries, etc)
+ * TODO: _Full solutions registry is currently being used when inferring common terms. This is because the users
+ preference set might contain e.g. Linux app preferences, even if the user is logging into a windows box. We then
+ need the linux solutions registry entries to get information (if available) about how to transform the linux app
+ preferences into common terms (that in turn can be translated into app settings for windows)._
+* matchMakerDispatcher:
+ * Sends the matchmaker input data to the actual matchmaker. _While there is functionality in place for allowing
+ multiple matchmakers, the system is currently hardcoded to use the “default” one, which is the Flat matchmaker in
+ all the default configs_.
+ * A promise is returned, which will contain the original matchmaker input along with the response from the matchmaker
+ in a block keyed by `matchMakerOutput`.
+ * The return payload is built in: `gpii.matchMakerFramework.utils.buildReturnPayload`
+
+### Important utilities and concepts in the MatchMaker Framework
+
+**The Flat Matchmaker**: While this is not part of the matchmaker framework, it is important to mention the Flat
+MatchMaker, which is an (the only) implementation of a matchmaker, which uses most of the utilities described in this
+document. It is described in details here:
+[https://github.com/GPII/universal/blob/master/documentation/FlatMatchMaker.md](./FlatMatchMaker.md)
+
+**Solution Types**: To ensure that several conflicting solutions (e.g. two screenreaders) are not launched, "Types" can
+be calculated for each solution, based on the capabilities they have (and in the future, on their explicitly stated
+types). The inference of solution type based on capabilities is based on the "apptology" tranformation, from the "flat"
+format into a solution type ontology. In the matchmaking process, each time a solution is 'accepted', any other
+conflicting solutions (i.e. of the same type) will be rejected. Adding solution type information can be done via the
+`gpii.matchMakerFramework.utils.addSolutionTypeInformation` function.
+
+The apptology is described in more details here: [Apptology.md](Apptology.md).
+
+**gpii.matchmakerframework.utils.disposeSolution:** In many ways the “Heart” of the matchmaking process. It is
+responsible for adding priorities, augmenting the solution and preference data and finally passing it to some
+disposition (matchmaker) strategy that is passed as a parameter. In more details the following happens:
+
+1. The solutions are run through `gpii.matchMakerFramework.utils.expandSolutions` augmenting them to, besides the
+ original solution registry data, a skeleton of the solutions capabilities is built is built - i.e. a hierarchical
+ object matching the solutions capabilities.
+2. Next solution priorities are added to each solution according to application settings via
+ `gpii.matchMakerFramework.utils.expandSolutions`. That is, if a user has any application settings the solution is
+ considered to have a priority of 512.
+3. Following this, explicit priorities of the users preference set is added to each solution. That is, if a user has
+ application priorities in their metadata, this is added. These values will always be above 1024, and hence take priority
+ (overwrite) the implicit priorities from the step above.
+4. A leaves structure of the preferences is calculated in the `gpii.matchMakerFramework.utils.computeLeaves` function.
+ This is a structure with a key for each preference in an EL-path format and a value of true. This, in conjunction with
+ the solutions skeleton is useful for looking up matches via fluids' getPath functionality.
+5. Once all this data has been prepared, the information is sent to the strategy (e.g. flat MatchMakers algorithm)
+6. Finally a disposition is assigned to each solution based on the output of the strategy.
+
+**Preference filtering:** When using a the untrusted flowmanager, security dictates that no "irrelevant" or unnused
+preferences of settings should be sent to the local device. Since the users preference set does need to be sent to the
+local device for use in the PSP, a filtering needs to happen, to ensure that the only settings reaching the local
+device are those that have been used to configure the device. This filtering can be done using the
+`gpii.matchMakerFramework.utils.filterPreferencesForSolution` function.
+
+**matchMakerDispatcher:** Sends the matchmaker input data to the actual matchmaker. While there is functionality in
+place for allowing multiple matchmakers, the system is currently hardcoded to use the “default” one, which is the
+Flat matchmaker.
+
+A promise is returned, which will contain the original matchmaker input along with the matchmaker response in a block
+keyed by matchMakerOutput
+
+The dispatching happens in the function `gpii.matchMakerFramework.matchMakerDispatcher`.
diff --git a/documentation/PSPChannel.md b/documentation/PSPChannel.md
new file mode 100644
index 000000000..659eee0ed
--- /dev/null
+++ b/documentation/PSPChannel.md
@@ -0,0 +1,333 @@
+# PSP Channel
+
+PSPChannel is used to establish persistent WebSocket connection between PSPChannel server and clients. Once the
+connection is established, the PSPChannel client can request various actions by transmitting the specified data to the
+pspChannel server. These actions include: apply preferences, switch to a different preference set, save changed
+preferences and read a preference value. Examples of how to construct PSPChannel connections and send/receive data
+from PSPChannel server can be found at
+[examples/pspChannelClient](../examples/pspChannelClient).
+
+## PSPChannel WebSocket APIs
+
+### Open up a PSPChannel WebSocket Connection
+
+Assume GPII is running on the port `localhost:8081`:
+
+```js
+var WS = require("ws");
+var socket = new WS("ws://localhost:8081/pspChannel");
+```
+
+Add socket listeners to process the communicated data with the server:
+
+```js
+socket.on("open", function () {
+ // Indicate the PSPChannel WebSocket Connection has been established
+});
+
+socket.on("message", function (data) {
+ // Process messages received from the server
+});
+
+socket.on("error", function (error) {
+ // Error handler
+});
+```
+
+Once the PSPChannel Connection is established, the server will send the first message to the client indicating
+the current lifecycle manager session state. An example of a typical first message that shows "noUser" is keyed
+in the system and no preferences have been read or applied:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "noUser",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {},
+ "preferences": {}
+ }
+ }
+}
+```
+
+### Close a PSPChannel WebSocket Connection
+
+```js
+socket.close();
+```
+
+### Send Data to PSPChannel Server
+
+The PSPChannel client can request various actions by transmitting the specified data to the server.
+
+```js
+socket.send(data);
+```
+
+* Parameters:
+
+ **data**: {Object}. The data to send to the server. It contains two paths: `type` and `value`.
+
+ **data.type**: {String}. It can be one of these values: `modelChanged`, `pullModel`.
+
+ _`modelChanged`_ typed messages are used to apply preferences, switch to a different preference set and save changed preferences.
+
+ _`pullModel`_ typed messages are used to read preference values.
+
+ **data.value**: {Object}. The actual value for performing actions. The detailed value structure is described below:
+
+#### Apply a Preference
+
+* The data transmitted from the client to the server:
+
+An example of changing the volume:
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/volume": {
+ "value": 0.5
+ }
+ }
+ }
+}
+```
+
+ An example of changing a nested keyed preference:
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "settingControls": {
+ "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon": {
+ "value": "StandardSet"
+ }
+ }
+ }
+}
+```
+
+* The server response:
+
+When preferences are applied, the server will send back a `modelChanged` message. An example:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "noUser",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/volume": {
+ "value": 0.5,
+ "schema": {
+ "title": "Volume",
+ "description": "General volume of the operating system",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "liveness": "live"
+ }
+ },
+ "preferences": {
+ "contexts": {
+ "gpii-default": {}
+ }
+ }
+ }
+ }
+}
+```
+
+#### Read a Preference
+
+* The data transmitted from the client to the server:
+1. An example of reading the magnification value:
+
+```json
+{
+ "type": "pullModel",
+ "value": {
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/volume": {
+ "value": 1
+ }
+ }
+ }
+}
+```
+
+The value at the path `value.settingControls.{preference}.value` can be any valid value constrained by this setting's
+schema defined in its solution registry entry. This value is required for sending the requested preference through the
+matchmaking process but it doesn't have any effect on the system.
+
+* The server response:
+
+The server will send back the current preference value. This value doesn't have any connection with the value in the
+incoming request. The typed messages from the server vary in different use cases:
+
+1. If the preference value is changed from the last time when it was read or applied, the server will send back
+ `modelChanged` and `preferenceReadSuccess` typed messages;
+2. If the preference value stays unchanged from the last time when it was read or applied, the server will only send
+ back the `preferenceReadSuccess` typed message;
+3. If GPII is not able to read the preference value, the server will send back the `preferenceReadFail` typed message.
+
+An example of a `modelChanged` typed message:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "noUser",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/volume": {
+ "value": 0.5,
+ "schema": {
+ "title": "Volume",
+ "description": "General volume of the operating system",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "liveness": "live"
+ }
+ },
+ "preferences": {
+ "contexts": {
+ "gpii-default": {}
+ }
+ }
+ }
+ }
+}
+```
+
+An example of a `preferenceReadSuccess` typed message:
+
+```json
+{
+ "type": "preferenceReadSuccess",
+ "payload": {}
+}
+```
+
+An example of a `preferenceReadFail` typed message:
+
+```json
+{
+ "type": "preferenceReadFail",
+ "payload": {}
+}
+```
+
+#### Switch to a different preference set
+
+* The data transmitted from the client to the server:
+
+An example of switching to a preference set named "bright":
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "activePrefsSetName": "bright"
+ }
+}
+```
+
+The value at the path `value.activePrefsSetName` specifies the name of the preference set to switch to.
+
+* The server response:
+
+Once the switch completes, the server will send back a `modelChanged` typed message with session state with the new
+preference set.
+
+An example of a `modelChanged` typed message:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "context1",
+ "activePrefsSetName": "bright",
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/magnification": {
+ "schema": {
+ "title": "Magnification",
+ "description": "Level of magnification",
+ "type": "number",
+ "default": 1,
+ "minimum": 1,
+ "multipleOf": 0.1
+ },
+ "liveness": "live",
+ "value": 2
+ }
+ },
+ "preferences": {
+ "name": "Multiple Contexts",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ },
+ "bright": {
+ "name": "bright"
+ },
+ "noise": {
+ "name": "noise"
+ },
+ "brightandnoise": {
+ "name": "bright and noise"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+#### Save changed preferences
+
+This request is to save all changed preferences into the database.
+
+* The data transmitted from the client to the server:
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "saveButtonClickCount": 1
+ }
+}
+```
+
+The integer at the path `value.saveButtonClickCount` should be incremented at each new request.
+
+* The server response:
+
+When the save completes, the server will send back a `preferencesApplied` typed message. An example:
+
+```json
+{
+ "type": "preferencesApplied",
+ "payload": {}
+}
+```
diff --git a/documentation/PreferencesServer.md b/documentation/PreferencesServer.md
new file mode 100644
index 000000000..00a94a8a1
--- /dev/null
+++ b/documentation/PreferencesServer.md
@@ -0,0 +1,346 @@
+# Preferences Server:
+
+The preferences server currently preferences APIs. The URLS are as follows:
+
+* **URL: `/preferences`**: The main URL to be used for the preferences server. Any new development should be using this
+ URL.
+
+## Description
+
+The Preferences Server is a server meant to be the public-facing, REST-based interface for consumers of preferences. Its
+main purpose is to be able to present a filtered view of the user preferences based on the requirements of the consumer,
+i.e. the preferences shown in a specific ontology, filtered according to a specified set of preferences.
+
+The bulk of work of the Preferences Server is done in other components of the GPII system, leaving the Preferences
+Server with the task of acting on the parameters supplied to the REST calls, calling the relevant functions in other
+components accordingly, etc.
+
+The main types of filtering provided by the Preferences Server are the following:
+
+* **?view=:view** - the ontology the preferences is given/required in. For each REST API call, there is the option of
+ providing desired 'view' or 'ontology' of the preferences. If none is given the _'flat'_ view is defaulted to. The
+ Preferences Server draws on the ontologyHandler component to take care of the bulk of this filtering, merging, etc.,
+ of the prefs and metadata sections.
+
+## APIs
+
+### GET /ready
+
+Check whether Preferences Server is ready to handle requests. The readiness endpoint checks the database connection.
+
+It returns http status code 200 when Preferences Server is ready to handle requests. Otherwise, returns http status code
+404.
+
+### GET /health
+
+Check whether Preferences Server itself is running. A running Preferences Server may or may not be ready to handle requests
+because the liveness endpoint does not check the connection between Preferences Server and the database.
+
+It returns http status code 200 when Preferences Server itself is running. Otherwise, returns http status code 500.
+
+### GET /preferences/:gpiiKey[?view=:view]
+
+Retrieves the preference sets for the GPII key (`:gpiiKey`). The optional `view` parameter is to retrieve the
+preferences in a different view (ontology). If no `view` is specified the 'flat' ontology will be defaulted to.
+
+#### Example of GET request with no view provided
+
+Example of a basic GET request to the Preferences Server (given that the Preferences Server is located on
+preferences.gpii.net):
+
+`http://preferences.gpii.net/preferences/sammy`
+
+Return payload:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/onScreenKeyboard/enabled": true,
+ "http://registry.gpii.net/common/initDelay": 120,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850,
+ "http://registry.gpii.net/common/mouseEmulation/enabled": true,
+ "http://registry.gpii.net/common/unknown": true,
+ "http://registry.gpii.net/applications/org.alsa-project": {
+ "masterVolume": 14
+ }
+ }
+ }
+ }
+}
+```
+
+#### Example of GET request with view parameter
+
+An example of a GET request (given that the Preferences Server is located on preferences.gpii.net):
+
+`http://preferences.gpii.net/preferences/myGpiiKey?view=ISO24751`
+
+Return payload:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "control": {
+ "onscreenKeyboard": true,
+ "mouseEmulation": {
+ "cursorSpeed": 0.85,
+ "-provisional-initDelay": 120,
+ "-provisional-mouseEmulation/enabled": true
+ }
+ },
+ "applications": {
+ "org.alsa-project": {
+ "parameters": {
+ "masterVolume": 14
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### POST /preferences/[?view=:view]
+
+This is used to post new preferences to the Preferences Server. A new GPII key will automatically be generated and
+returned in the payload along with the saved preferences.
+
+As with GET, this takes an optional `view` parameter denoting the ontology of the provided settings. If no `view` is
+provided, the preferences will be stored and interpreted as being in the `flat` format.
+
+#### Example POST query
+
+Below is an example of a post query to the following url (given that a Preferences Server is available at
+preferences.gpii.net):
+
+`http://preferences.gpii.net/preferences/?view=flat`
+
+The body of the POST should contain the preferences to be stored. They should be in the format specified by `view` or,
+if no `view` is provided, in the flat format.
+
+Example POST body:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/onScreenKeyboard/enabled": true,
+ "http://registry.gpii.net/common/initDelay": 120,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850,
+ "http://registry.gpii.net/common/mouseEmulation/enabled": true,
+ "http://registry.gpii.net/common/unknown": true,
+ "http://registry.gpii.net/applications/org.alsa-project": {
+ "masterVolume": 14
+ }
+ }
+ }
+ }
+}
+```
+
+The return payload will contain the stored preferences (keyed by `preferences`) and the newly generated GPII key
+(keyed by `gpiiKey`) under which the preference set is stored.
+
+Given that the above preferences was stored with the GPII key `123e4567-e89b-12d3-a456-426655440000`, the return
+payload would be:
+
+```json
+{
+ "gpiiKey": "123e4567-e89b-12d3-a456-426655440000",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/onScreenKeyboard/enabled": true,
+ "http://registry.gpii.net/common/initDelay": 120,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850,
+ "http://registry.gpii.net/common/mouseEmulation/enabled": true,
+ "http://registry.gpii.net/common/unknown": true,
+ "http://registry.gpii.net/applications/org.alsa-project": {
+ "masterVolume": 14
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### PUT /preferences/:gpiiKey[?merge=:mergeview=:view]
+
+This is used to create a new GPII key with its preferences, or update preferences for an existing GPII key to the
+Preferences Server. At the update, if no preferences safe is associated with the provided GPII key, a new safe will
+be automatically created.
+
+As with GET and PUT, this takes an optional `view` parameter denoting the ontology of the settings provided in the
+payload of the request body. If no `view` is provided, the preferences will be stored and interpreted as being in the
+`flat` format.
+
+As for the update, this takes an optional boolean `merge` parameter denoting whether the incoming preferences should be
+merged with the existing preferences. If `merge` is `true`, the incoming preferences will be merged with the existing
+ones. Otherwise, the incoming preferences will override the existing ones. If no `merge` is provided, the default value
+will be `false`.
+
+When preferences are PUT to the preferences server, all the preferences in that view will be overwritten. In other
+words, if I have preferences A, B and C already existing in my preference set (in a given view), and a put request is
+made containing only settings B and D, the resulting preference set will contain only settings B and D.
+
+The preferences are allowed to be stored in different ontologies. A (transformable) user preference will only be stored
+once in the preference set. This also means that if a put request is make containing a preference A1, which already
+exists in the preference set, but in a different ontology (lets call this same setting in a different ontology Ax), the
+preference A1 will be stored in the provided ontology, while Ax will be removed from the preference set. An example will
+be given below to help make this clearer.
+
+#### Example PUT query
+
+Below is an example of a put query to the following url (given that a Preferences Server is available at
+preferences.gpii.net):
+
+`http://preferences.gpii.net/preferences/myGpiiKey?merge=true&view=flat`
+
+Note that here, we store preferences to the GPII key 'myKey'. The body of the PUT should contain the preferences to be
+stored. They should be in the format specified by `view` or in the flat format if no `view` is provided.
+
+putBody:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/fontSize": 18,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850
+ }
+ }
+ }
+}
+```
+
+The return payload would then be the following:
+
+```json
+{
+ "gpiiKey": "myKey",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/fontSize": 18,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850
+ }
+ }
+ }
+ }
+}
+```
+
+If we look at what's going on at the preferences server level, imagine that the original preference set looked like this
+before the PUT request:
+
+```json
+{
+ "flat": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/initDelay": 120,
+ "http://registry.gpii.net/common/cursorSpeed": 0.850
+ }
+ }
+ }
+ },
+ "ISO24751": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "display": {
+ "screenEnhancement": {
+ "fontSize": 24
+ }
+ },
+ "control": {
+ "onscreenKeyboard": true
+ }
+ }
+ }
+ }
+ },
+ "bogus": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "foo": "bar"
+ }
+ }
+ }
+ }
+}
+```
+
+After the PUT request, it would be changed to:
+
+```json
+{
+ "flat": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/fontSize": 18,
+ "http://registry.gpii.net/common/cursorSpeed": 0.2
+ }
+ }
+ }
+ },
+ "ISO24751": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "control": {
+ "onscreenKeyboard": true
+ }
+ }
+ }
+ }
+ },
+ "bogus": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "foo": "bar"
+ }
+ }
+ }
+ }
+}
+```
+
+There are two important things to note here:
+
+1. As the PUT request was made in the flat format, all the existing preferences in the flat format were replaced by the
+ ones in the body of the PUT request. This means that a setting like _cursorAcceleration_ is no longer present in the
+ preference set.
+2. In the example, we consider the ISO-24751 setting "display.screenEnhancement.fontSize" to be ontologically equivalent
+ to `http://registry.gpii.net/common/fontSize`. Since we do not allow the same setting to be present multiple times in
+ the NP set, the fontSize has been stored in the flat ontology and removed from the ISO24751 block.
+
+## Other relevant documentation:
+
+* [The Preferences Server Framework](PreferencesServerFramework.md)
diff --git a/documentation/PreferencesServerFramework.md b/documentation/PreferencesServerFramework.md
new file mode 100644
index 000000000..1d313c8de
--- /dev/null
+++ b/documentation/PreferencesServerFramework.md
@@ -0,0 +1,57 @@
+# Preferences Server Framework
+
+## Introduction
+
+As the name suggests, the purpose of the preferences server framework is to server the user needs and preferences. Only
+the preferences that are relevant and permitted to the consumer are provided, and they can be provided in the ontology
+desired by the consumer, given that an ontology (or 'view') has been provided for this.
+
+Currently, the preferences server Framework consist of two components:
+
+* The Ontology Handler (see below)
+* [The Preferences Server](PreferencesServer.md)
+
+_The Ontology Handler_ is the component containing the functionality for translating between one ontology (or 'view')
+and another. Certain consumers prefer viewing the preferences in one view more suitable for them (i.e. ISO-24751) and
+the Ontology Handler is able to translate these settings (given that an ontology transformation has been provided) from
+one view to another.
+
+_The Preferences Server_ is the public facing component of the preferences server framework. It provides APIs, allowing
+requests for preference sets in some desired view (via API parameters), that only gives the consumer access to the
+relevant settings. It is responsible for (via the ontology handler) to translate the settings to the desired view,
+ensuring that no duplicate preferences are stored, etc. For a full description of the Preferences Server, see: [The
+Preferences Server](PreferencesServer.md).
+
+## Motivation and rules
+
+We have identified a set of requirements to a new Preferences Framework and Ontology Handler implementations, and the
+components using it:
+
+* **Each ontology should have a (ontology-global) unique ID** for the framework to be able to identify it
+* **preference sets should consist of containers keyed by ontology IDs** - as opposed to before, where it's guessed by
+ the pattern of the preferences what ontology is used, the preference sets should explicitly declare what ontology
+ they're in.
+* **A single preference safe can contain multiple ontologies.**
+* **A single (transformable) value will only be present once in the preference safe**
+* **Ontology, preference safes and security gateway should always be set up together** - seeing how we would now have
+ the preferences keyed by ontology, any instance of the preferences server would need to be deployed along with a
+ ontology server, as well as a little front-end allowing the transformation of the preferences into a desired ontology.
+ This will allow anyone who needs to read the preferences to get them in a format they understand. This has some
+ implications:
+* **Any consumer of preferences is expected to declare what format it wants them in** (or absense thereof implies the
+ use of the 'flat' ontology) - this includes the MMs and preference editors
+
+## Ontology Handler:
+
+Component with the ability of performing various operations on preference sets. It is NOT implemented as a REST API
+service, as it is expected to be present on all any instance running the Preferences Server. In other words, either the
+ontology handling is expected to be done based on parameters given when talking to the [Preferences
+Server](PreferencesServer.md) or by using the public functions of the OntologyHandler directly.
+
+### Ontologies:
+
+The ontology transform specs are stored in the `testData/ontologies` folder of the `universal` repository. The filename
+of a transformation spec file should describe what transformation that file contains. More specifically, a filename for
+ontology transformations should have the following format: -.json where should be replaced with the
+name of the ontology that is expected as the input model, and should be the name of the ontology that will be
+output from the transformation.
diff --git a/documentation/ProductionSetupInstructions.md b/documentation/ProductionSetupInstructions.md
new file mode 100644
index 000000000..71015f737
--- /dev/null
+++ b/documentation/ProductionSetupInstructions.md
@@ -0,0 +1,134 @@
+# Production Setup Instructions
+
+## Introduction
+
+The *Cloud Based Flow Manager* can be deployed in a VM using production configurations. Once the VM is running, the
+following services will be available in the VM:
+
+* *Preferences Server* listening on port 9081
+* *Cloud Based Flow Manager* listening on port 9082
+* *CouchDB* as the backend database listening on port 5984
+
+**Note:**
+
+* The *Preferences Server* is a Node.js process
+* The *Cloud Based Flow Manager* is a separate Node.js process
+* The above mentioned ports will be forwarded from the VM to your host machine allowing access to deployed services
+* Before starting the VM please make sure the ports in question are not being used on your host machine
+* All the Vagrant commands listed below should be run in the universal root directory.
+
+## Requirements
+
+In order to start the VM, make sure [all these requirements for setting up Quality Infrastructure
+Environments](https://github.com/GPII/qi-development-environments/blob/master/README.md#requirements) are satisfied.
+
+## Start the VM
+
+To start a VM, run the following command in the universal root directory:
+
+`vagrant up`
+
+## Test the VM
+
+### Test the Preferences Server
+
+Visiting the following link in a browser:
+
+`http://localhost:9081/preferences/carla`
+
+should return:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/applications/com.texthelp.readWriteGold": {
+ "ApplicationSettings.AppBar.Width.$t": 788,
+ "ApplicationSettings.AppBar.ShowText.$t": true,
+ "ApplicationSettings.AppBar.optToolbarShowText.$t": true,
+ "ApplicationSettings.AppBar.LargeIcons.$t": true,
+ "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true,
+ "ApplicationSettings.Speech.optSAPI5Speed.$t": 50,
+ "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false
+ },
+ "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": {
+ "show-cross-hairs": true,
+ "lens-mode": false,
+ "mag-factor": 2,
+ "mouse-tracking": "proportional",
+ "screen-position": "right-half",
+ "scroll-at-edges": true
+ },
+ "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": {
+ "Magnification": 200,
+ "ZoomIncrement": 50,
+ "Invert": 0,
+ "FollowMouse": 1,
+ "FollowFocus": 1,
+ "FollowCaret": 1,
+ "MagnificationMode": 1
+ },
+ "http://registry.gpii.net/common/fontSize": 24,
+ "http://registry.gpii.net/common/foregroundColor": "white",
+ "http://registry.gpii.net/common/backgroundColor": "black",
+ "http://registry.gpii.net/common/fontFaceFontName": ["Comic Sans"],
+ "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif",
+ "http://registry.gpii.net/common/magnification": 2,
+ "http://registry.gpii.net/common/tracking": ["mouse"],
+ "http://registry.gpii.net/common/invertColours": true,
+ "http://registry.gpii.net/common/adaptationPreference": [{
+ "adaptationType": "caption",
+ "language": "en"
+ }, {}],
+ "http://registry.gpii.net/common/tableOfContents": false
+ }
+ }
+ },
+ "name": "Carla"
+}
+```
+
+### Test the Cloud Based Flow Manager
+
+Open a terminal and copy, paste this command:
+
+```snippet
+curl -X POST -d "grant_type=password&client_id=pilot-computer&client_secret=pilot-computer-secret&username=li&password=dummy" http://localhost:9082/access_token
+```
+
+should return:
+
+```snippet
+{
+ "access_token": {String},
+ "expiresIn":3600,
+ "token_type":"Bearer"
+}
+```
+
+### Test CouchDB
+
+Opening this link in a browser:
+
+`http://localhost:5984/_utils/`
+
+should show the [CouchDB Web GUI Administration Panel](http://docs.couchdb.org/en/1.6.1/intro/futon.html). The following
+database should be present:
+
+* `gpii`: Contains all data for GPII Server that runs for the Preferences Server and the Cloud Based Flow Manager,
+ including GPII keys, prefs safes, GPII App installation clients, GPII App installation client authorizations and
+ client credentials
+
+## Stop the VM
+
+To stop the VM, run the following command:
+
+`vagrant halt`
+
+## Delete the VM
+
+Once you no longer need the VM, you can reclaim storage resources using the following command:
+
+`vagrant destroy -f`
diff --git a/documentation/README.md b/documentation/README.md
new file mode 100644
index 000000000..6909109df
--- /dev/null
+++ b/documentation/README.md
@@ -0,0 +1,22 @@
+# Documentation Overview
+
+* [System Startup and configs](Configs.md)
+* [Production Setup of Cloudbased FM and Preferences server](ProductionSetupInstructions.md)
+* [Flow Manager](FlowManager.md)
+ * [Cloud Based Flow Description](CloudBasedFlow.md)
+ * [Login and Logout Flow Description](loginAndLogoutFlow.md)
+* [Preferences Server Framework](PreferencesServerFramework.md)
+ * [Preferences Server](PreferencesServer.md)
+* [Data Model for Preferences and OAuth Data](DataModel.md)
+ * [Couch Connector](CouchConnector.md)
+ * [Data Loader](DataLoader.md)
+* [MatchMakerFramework](MatchMakerFramework.md)
+ * [Flat MatchMaker](FlatMatchMaker.md)
+ * [Apptology](Apptology.md)
+* [Lifecycle Manager](LifecycleManager.md)
+* [Transformer](Transformer.md)
+* [BrowserChannel and WebSockets settings handler](BrowserChannel.md)
+* [Auth Server](AuthServer.md)
+ * [Auth Grant Finder](AuthGrantFinder.md)
+ * [Authorization Service](AuthorizationService.md)
+* [Solutions Registry Format](SolutionsRegistryFormat.md)
diff --git a/documentation/ResetComputer.md b/documentation/ResetComputer.md
new file mode 100644
index 000000000..6adf9f253
--- /dev/null
+++ b/documentation/ResetComputer.md
@@ -0,0 +1,55 @@
+# Reset Computer
+
+GPII provides [reset APIs](FlowManager.md#reset) to reset the computer to default settings when needed. The reset workflow
+is described below.
+
+## Define the Default Settings File
+
+The default settings JSON5 file should be created at `testData/defaultSettings/defaultSettings.json5` that is in a
+format of a standard preferences set. An example of its content for starting gnome magnifier on Linux:
+
+```json
+{
+ "contexts": {
+ "gpii-default": {
+ "preferences": {
+ "http://registry.gpii.net/common/magnification/enabled": true
+ }
+ }
+ }
+}
+```
+
+## Reset on System Start
+
+The local flow manager provides a boolean option "resetAtStart" that serves as a flag indicating whether the computer
+should be reset on system start. This flag is set to `false` by default. Setting it to `true` will reset the computer
+on system start. This flag is set to true in GPII configs that run the local flow manager in the production mode.
+
+When GPII starts, `defaultSettings.json5` is automatically copied from `testData/defaultSettings` directory to the GPII
+settings directory if it hasn't been copied. GPII reads the default settings from `defaultSettings.json5` located
+at the GPII settings directory. Users are invited to edit the default settings file at the settings directory instead
+of at `testData/defaultSettings` directory to keep the code base clean and consistent.
+
+To make it easier to create windows installers, `testData/defaultSettings/defaultSettings.win32.json5` is pre-created. It
+should be renamed (or copied) to `testData/defaultSettings/defaultSettings.json5` before building windows installers.
+
+Note:
+
+* When `defaultSettings.json5` in the settings directory is wanted to be re-copied from `testData/defaultSettings` directory,
+ removing `defaultSettings.json5` from the settings directory will trigger the recopy automatically next time when
+ GPII starts.
+
+* The actual location of the settings directory can be found at the beginning of the log output when GPII starts
+
+## Reset via HTTP Request
+
+Once the default settings file is ready, the reset can be initiated by sending a HTTP request to the login API provided
+by the local flow manager. This API is:
+
+* GET /user/reset/login
+
+The reset actions performed by the local flow manager are:
+
+1. If there's a GPII key currently keyed in, key it out;
+2. If default settings are defined, apply them to reset the computer.
diff --git a/documentation/SolutionsRegistryFormat.md b/documentation/SolutionsRegistryFormat.md
new file mode 100644
index 000000000..9d0a9f89a
--- /dev/null
+++ b/documentation/SolutionsRegistryFormat.md
@@ -0,0 +1,403 @@
+# Solutions Registry format
+
+## Overall format:
+
+Each entry in the solution registry should have a unique ID (`Solution.id` in the below example), as well as a name
+(`name`), and a description of which context it requires to run (`context`). Besides these, information can be provided
+describing different potential aspects of its lifecycle. This can for example be information about how to start and stop
+the solution, detect whether it is running, set its settings, etc. These will all be described in the below. The overall
+structure and allowed keys in a solution description can be seen here.
+
+```snippet
+"Solution.id": {
+ "name": "My Solution"
+ "contexts": { ... },
+ "settingsHandlers": { ... },
+ "launchHandlers": { ... },
+ "capabilities": [ .. ],
+ "configure": [ .. ],
+ "restore": [ .. ],
+ "update": [ .. ],
+ "start": [ .. ],
+ "stop": [ .. ],
+ "isRunning": [ ..],
+ "isInstalled": [ .. ],
+
+ // Not yet supported.
+ "install": [ ... ],
+ "uninstall": [ ... ],
+ "makeConfigurable": [ ... ],
+ "isConfigurable": [ ... ]
+}
+```
+
+## contexts
+
+The `contexts` block describes what the required context is for the solution to run. Currently only one type of context
+is supported, namely `OS`. The context block is **mandatory**.
+
+**Example Context**:
+
+```snippet
+"contexts": {
+ "OS": [
+ {
+ "id": "win32"
+ }
+ ]
+}
+```
+
+## settingsHandlers
+
+The `settingsHandlers` block is unique and one of the most important blocks in the solutions registry entry. It consists
+of zero or more settingsHandler entries, each keyed by an arbitrary name (that is unique within this solutions
+settingsHandlers block). Inside each settingsHandler entry, the properties for that settingsHandler is provided. The
+entries in the settingsHandlers block can be referred to from the lifecycle blocks of the solutions registry entry. The
+settingsHandlers block is mandatory, but can be empty.
+
+**Example settingsHandlers block**:
+
+```snippet
+"settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "liveness": "manualRestart",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini",
+ "allowNumberSignComments": true,
+ "allowSubSections": true
+ },
+ "supportedSettings": : {
+ "speech.espeak.pitch": { ... metadata for setting... },
+ "presentation.reportHelpBalloons": { ... metadata for setting ... },
+ "speech.nonTransformableSetting": { ... metadata for setting ... }
+ },
+ "capabilitiesTransformations": {
+ "speech.espeak.pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 100
+ }
+ },
+ "presentation.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages"
+ }
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "speech.espeak.pitch",
+ "factor": 0.01
+ }
+ }
+ },
+ "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation.reportHelpBalloons"
+ }
+ },
+ "otherconf": {
+ "type": "gpii.settingsHandlers.XMLHandler",
+ "liveness: "liveRestart",
+ "options": {
+ "filename": "${{environment}.HOME}\\mySettings.ini"
+ },
+ "supportedSettings": {
+ "appsettingA": { ... appsettingA metadata ... },
+ "appsettingB": { ... appsettingB metadata ... },
+ ...
+ "appsettingZ": { ... appsettingZ metadata ... }
+ }
+ }
+}
+```
+
+An important thing to notice here is that this solution example has two settingsHandlers - one XMLHandler which has been
+given a reference `myconf` and an INIHandler referred to as `otherconf`.
+
+Each settingsHandler block can contain the following information:
+
+* **type (mandatory):** the type of settingshandler
+* **liveness (mandatory):** Describes the update behavior of this solution: `"live"` means that the settings can be
+ applied live without needing to restart the solution. `"liveRestart"` means that the a restart is required but
+ considered low-impact enough for e.g. the PSP to trigger this automatically but not on a framerate of e.g. dragging a
+ slider. `"manualRestart"` means that a change in settings requires a restart and that the restart of the solution is
+ considered high-impact and slow. `"OSRestart"` means that a restart of the operating system required.
+* **options:** Any options that should be passed to the settingsHandler. This is specific to the type of settingshandler
+ specified in the "type" block.
+* **capabilitiesTransformations**: Transformations from common terms to application specific settings can be defined
+ here. These will enable the framework to automatically translate common terms from a user's preference set into
+ application settings. Any common terms listed here, will automatically be added to the `capabilities` of the solution.
+* **inverseCapabilitiesTransformations**: This block describes transformations from application settings to common
+ terms. If this block is present, the transformations specified will be used by the framework to deduce common terms
+ based on any application specific settings in the users preference set. If this key is not present, the framework will
+ attempt to do the inversion itself, based on the `capabilitiesTransformations`. If this block is present, but empty,
+ the system will make no attempt to automatically invert the `capabilitiesTransformations`.
+* **supportedSettings (mandatory when multiple settingsHandlers)**: This block is used to determine which application
+ specific settings are relevant to the settingshandler, and also serves as location for providing metadata (such as
+ default values, data type, validation information, etc) about the setting. Currently no relevant metadata is
+ supported, so the empty object (`{}`) should be used as value. If a solution only has a single settingsHandler block,
+ all the settings will be passed to that handler by default. But in case there are multiple settingsHandlers, the
+ system needs some way of determining which settings to apply to which handler. The `supportedSettings` directive is
+ used for this:
+ * If a `supportedSettings` option is supplied, only those settings listed there will be applied to the settingsHandler
+ * If a solution registry entry has multiple settings handlers, the `supportedSettings` entry is mandatory for each settingshandler.
+
+## launchHandlers:
+
+The `launchHandlers` are very similar to the `settingsHandlers` block in both form, functionality and implementation,
+but have a different area of responsibility. As the name suggests, rather than being responsible for modifying settings,
+they are responsible for the 'launch' state of the application. That is, they are responsible for any actions related to
+stopping or starting the application, and detecting whether it is running.
+
+There are two main difference from settingshandlers: (1) internally, launch handlers only have one setting (`running`)
+which can be true or false depending on the (desired) state of solution and (2) launch handlers do not get their
+settings from the users NP directly, rather they get the value for `running` from the matchmaker. This is because the
+decision of which applications to run/stop/update on login depends on what is available on the system and what the
+matchmaker decides best works for the user.
+
+On a technical level, launch handlers work exactly as settings handlers, in that they have two methods `get` and `set`
+for getting and setting the current run-state of and application, respectively. They ignore all "settings" passed in the
+payload, except for the `running` setting, which should be a boolean value. An implementation of a launch handler should
+support 3 actions: reading the current run-state of an application (i.e. the `get` call), starting an application (i.e.
+when `set` is called with a `true` value) and stopping an application (i.e. when `set` is called with a `false` value).
+
+**Example launchHandlers block**:
+
+```snippet
+"launchHandlers:" {
+ "launcher": {
+ "type": "gpii.launchHandlers.flexibleHandler",
+ "options": {
+ "setTrue": [
+ {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_CURRENT_USER\\Software\\Texthelp\\Read&Write11\\InstallPath}\\ReadAndWrite.exe\""
+ }
+ ],
+ "setFalse": [
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "ReadAndWrite.exe"
+ }
+ ],
+ "getState": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "ReadAndWrite"
+ }
+ ]
+ }
+ }
+}
+```
+
+As can be seen, the structure of the `launchHandlers` block is very similar to the `settingsHandlers` block. It supports
+any number of launchHandler entries, keyed by some reference string (in this case "launcher", but it could be anything)
+that can be used in the lifecycle blocks to reference it.
+
+Each launch handler will have a `type` entry, describing its type, as well as an `options` block. The content of the
+`options` block will depend on the launch handler.
+
+## Capabilities
+
+While most of the users preferences for a certain application or group of applications are handled in the various
+`settingsHandlers` entries, there are some preferences that can affect the application in other ways than in its
+configuration. These are `enabled` terms, such as `http://registry.gpii.net/common/magnification/enabled`, which can
+have a special meaning. If a setting like this should affect whether the solution should be started at all, it should be
+listed in the solutions capabilities block. If a preference like this should just affect a certain feature/setting of
+the application, it should _not_ be listed in the capabilities.
+
+For clarity, lets take two different solutions:
+
+1. `Solution A` is a combined screen reader and magnifier. It supports a `magOff` setting which will turn the
+ magnification feature off, but still read what is on the screen. In this case, it would have
+ `http://registry.gpii.net/common/magnification/enabled` in one of its settingsHandlers' capabilitiesTransformation
+ block, where it would be transformed into an appropriate `magOff` value. This will affect whether the magnification
+ feature is enabled, but otherwise allow the application to run normally. It should _not_ have the
+ `http://registry\\.gpii\\.net/common/magnification/enabled` listed in its capabilities, since a value of `false`
+ would mean that the application would not be started at all - meaning that the screenreader features would not be
+ usable either.
+2. `Solution B` is a magnifier application. If the user does not want magnification enabled, there is no sense in having
+ this application running. In this case, it _should_ have the
+ `http://registry\\.gpii\\.net/common/magnification/enabled` listed in its capabilities. This effect is that if the
+ user have a preference for this term, it would affect whether the `Solution B` is launched or not. Note that
+ `Solution B` would still be configured according to the users preferences (just not launched) on user login, in case
+ the user manually starts it later.
+
+**Example capabilities block**:
+
+```snippet
+"capabilities": [
+ "http://registry\\.gpii\\.net/common/magnification/enabled"
+]
+```
+
+## Lifecycle Blocks: configure, restore, start, stop, update and isRunning
+
+Lifecycle blocks describe what should happen when the system needs to configure, start, update, etc., an application.
+Neither of these blocks are mandatory as the system will infer their content in case they are not specified.
+
+### configure and restore
+
+These blocks describe how to configure and restore a solution, that is:
+
+* `configure`: Configure the solution with the users setting (e.g. on login)
+* `restore`: Restore the settings of the system from before the user logged in
+
+Each of these lifecycle blocks allow the same content - which is an array with entries that are either references to
+settingsHandlers blocks or customized lifecycle blocks. To reference a settingsHandler block, the keyword
+`settings.` is used, where `` should be replaced with the name of a settingsHandler block. The
+meaning of referencing a settingsHandler is telling the system that the users preference set will be applied to that
+solution via the referenced settingshandler. Alternative to referencings setting and restoring settings, arbitrary
+lifecycle actions are allowed - the syntax for this is an object that contains at least a `type` key for the function to
+call and any further key/value pairs that are needed by the type.
+
+If the `configure` and/or `restore` blocks are omitted from a solution entry, they will be inferred as containing
+references to all the solutions settingshandlers (if any).
+
+**Example blocks**:
+
+```snippet
+"configure": [
+ "settings.myconf"
+],
+"restore": [
+ "settings.myconf"
+]
+```
+
+### start, stop and isRunning
+
+These blocks all have to do with the run-state of a solution. Their meanings are the following:
+
+* `start`: Launch/start the solution
+* `stop`: Stop/kill the solution
+* `isRunning`: Detect whether the application is currently running
+
+Similar to the configuration related blocks, each of these lifecycle blocks allow the same content - which is an array
+with entries that are either references to launcHandler blocks or customized lifecycle blocks. To reference a
+launchHandler block, the keyword `launchers.` is used, where `` should be replaced with the name
+of a `launchHandler` block. Internally, when referencing a launchHandler, different things will happen depending on
+which lifecycle block the reference is from. A reference from `start` or `stop` will call the launch handlers `.set`
+method with a `running` value of `true` or `false`, respectively. This should have the effect of starting or stopping
+the process. In case of a reference from `isRunning`, a call will be made to the launch handlers `.set` method.
+Alternative to referencing launch handler blocks, arbitrary lifecycle actions are allowed - the syntax for this is an
+object that contains at least a `type` key for the function to call.
+
+None of these blocks are mandatory. If one is omitted from the solution registry entry, it will be inferred as
+containing references to all launchHandlers specified for that solution (if any).
+
+**Example blocks**:
+
+```snippet
+"start": [
+ "launchers.myLauncher"
+],
+"stop": [
+ "launchers.myLauncher"
+],
+"isRunning": [
+ "launchers.myLauncher",
+ {
+ "type": "gpii.runCheckers.myCustomChecker",
+ "command": "applicationChecker.exe /n myApplication"
+ }
+]
+```
+
+### update
+
+The `update` block works very similarly to the lifecycle blocks. It describes what should happen when the configuration
+needs to be updated (e.g. due to preferences set changes, PSP adjustments, etc).
+
+The format of the `update` block allows for the same entries as the other lifecycle blocks - that is: arbitrary
+lifecycle action blocks and references to `settings.` and `launchers.`. Unlike for the other
+lifecycle blocks, the `update` block furthermore allows references to the `start`, `stop` and `configure` blocks. This
+is done by putting a string with the name of that block. When the system encounters one of these references, the entries
+of that block will be run.
+
+**Example block**:
+
+```snippet
+"configure": [
+ "settings.myconf"
+],
+"update": [
+ "configure",
+ {
+ "type": "gpii.launch.exec",
+ "command": "my_application --refresh"
+ }
+]
+```
+
+In the above example, the process of updating the application settings would consists of running the contents of the
+`configure` block (that is `"settings.myconf"`), followed by a custom lifecycle actions.
+
+If the `update` block is omitted, it will be inferred by the system. What the inferred content will be depends on the
+solutions' liveness. If any of the settingsHandlers have a `liveness` value of less than "live", the inferred content
+will be `[ "stop", "configure", "start" ]`, i.e. a cycle of stopping, configuring and starting the application. If all
+settingsHandlers are "live", that means that it supports settings being updated live and a value of `[ "configure" ]` is
+inferred.
+
+### isInstalled:
+
+This directive is used to detect whether a solution is installed. If any of these blocks evaluate to `true` (implicit
+**OR**), the application is considered to be installed.
+
+**Example Entry**:
+
+```snippet
+"isInstalled": [
+ {
+ "type": "gpii.reporter.fileExists",
+ "fileName": "${{registry}.HKEY_CURRENT_USER\\Software\\Texthelp\\Read&Write10\\InstallPath}\\ReadAndWrite.exe"
+ }, // IMPLICIT OR BETWEEN THESE BLOCKS
+ {
+ "type": "gpii.packageKit.find",
+ "name": "orca"
+ }
+]
+```
+
+*****
+
+### UNIMPLEMENTED BLOCKS
+
+There are several advanced options that we're not planning to implement in the short term, but which will make the
+implementation of things like the ORCA settings handler much less horrible.
+
+#### isConfigurable
+
+This is run before configuration to ensure that the application is actually ready to be configured. This is relevant for
+applications where e.g. a configuration file needs to be present, a tutorial needs to be run on the first launch, etc.
+
+**Example Entry**:
+
+```snippet
+"isConfigurable": [{
+ "type": "gpii.reporter.fileExists",
+ "path": "${{environment}.XDG_DATA_HOME}/orca/user-settings.conf""
+}]
+```
+
+#### makeConfigurable
+
+Is the actions that need to be taken to make the application configurable (such as running a wizard, creating a default
+configuration file, adding a new system user, etc).
+
+**Example Entry**:
+
+```snippet
+"makeConfigurable": [{
+ "launch" // A special key meaning "start it, wait until isConfigurable is met, and then stop it"
+}]
+```
+
+#### install:
+
+Used for describing the steps required for installing the application
+
+#### uninstall:
+
+Used for describing the steps required for uninstalling the application (i.e. completely removing it from the system)
diff --git a/documentation/Transformer.md b/documentation/Transformer.md
new file mode 100644
index 000000000..fd696a12e
--- /dev/null
+++ b/documentation/Transformer.md
@@ -0,0 +1,23 @@
+# Transformer
+
+## Description
+
+The transformer component of the core framework is a supportive component for the fluid.infusions transformation
+capabilities. It is responsible for supporting transformations in the core framework that aren't directly achievable
+using the Infusion transformation framework.
+
+The responsibilities worth highlighting for this component are:
+
+## Translation from matchmaker output to lifecycle manager input
+
+One of the functionalities of the transformer is to translate from the matchmaker output into the format
+required by the lifecycle manager. For this the function `gpii.transformer.toLifecycleFormat` should be used. See
+detailed documentation in code comments.
+
+## Regular transformation functions
+
+The transformer also contains useful transformation functions that do not qualify to become part of the general Infusion
+framework but are useful for GPII. These are:
+
+* `gpii.transformer.quantize` as described here:
+ [http://wiki.gpii.net/w/Architecture_-_Available_transformation_functions#Mapping_a_continuous_range_into_discrete_values_.28gpii.transformer.quantize.29](http://wiki.gpii.net/w/Architecture_-_Available_transformation_functions#Mapping_a_continuous_range_into_discrete_values_.28gpii.transformer.quantize.29)
diff --git a/examples/browserChannelClient/README.md b/examples/browserChannelClient/README.md
new file mode 100644
index 000000000..195b3110a
--- /dev/null
+++ b/examples/browserChannelClient/README.md
@@ -0,0 +1,11 @@
+# Browser Channel Client
+
+This directory contains a tiny sample client for the browserChannel WebSockets connection
+as described in [../documentation/BrowserChannel.md](../documentation/BrowserChannel.md).
+
+After running
+
+ npm install
+
+in this directory, follow the instructions there to start up the CloudBased FlowManager
+and connect to it with this client.
diff --git a/examples/browserChannelClient/browserChannelClient.js b/examples/browserChannelClient/browserChannelClient.js
new file mode 100644
index 000000000..097220269
--- /dev/null
+++ b/examples/browserChannelClient/browserChannelClient.js
@@ -0,0 +1,71 @@
+/*!
+ * Test client for BrowserChannel WebSockets
+ *
+ * Copyright 2016 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+"use strict";
+
+var ws = require("ws");
+
+// The client starts the communication
+
+var socket = new ws("ws://localhost:8081/browserChannel"); // eslint-disable-line new-cap
+
+var changeSetting = false;
+
+// When the connection is done, the client tells to the flow manager its id
+
+socket.on("open", function () {
+ console.log("## browserChannelClient: Socket connected");
+ socket.send(JSON.stringify({
+ type: "connect",
+ payload: {
+ solutionId: "net.gpii.uioPlus" // must match the solution id in (every) solutions registry
+ }
+ }));
+});
+
+socket.on("message", function (data) {
+ console.log("## browserChannelClient: Received the following message: " + data);
+ var message = JSON.parse(data);
+ // Right after sending the id to the flow manager, the server will return back
+ // the current settings in the system (if any)
+ if (message.type === "connectionSucceeded") {
+ changeSetting = true;
+ console.log("## browserChannelClient: Got initial settings ", message.payload, " on connection");
+ }
+ // By listening to this message type, the client will be notified when the system has
+ // new settings to be applied on the client side
+ else if (message.type === "onSettingsChanged") {
+ console.log("## browserChannelClient: Got changed settings ", message.payload);
+ }
+ // Log acknowledgement that the "changeSettings" message was sent
+ else if (message.type === "changeSettingsReceived") {
+ console.log("## browserChannelClient: ChangeSettings was successfully sent ", message.payload);
+ }
+
+ // Change two settings, and be done.
+ if (changeSetting) {
+ changeSetting = false;
+ socket.send(JSON.stringify({
+ type: "changeSettings",
+ payload: {
+ settings: {
+ characterSpace: 1,
+ clickToSelectEnabled: false,
+ contrastTheme: "default"
+ }
+ }
+ }));
+ }
+});
diff --git a/examples/browserChannelClient/package.json b/examples/browserChannelClient/package.json
new file mode 100644
index 000000000..fc41fafb0
--- /dev/null
+++ b/examples/browserChannelClient/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "gpii-browser-channel-client",
+ "version": "0.1.0",
+ "dependencies": {
+ "ws": "6.2.1"
+ }
+}
diff --git a/examples/deviceReporter/README.txt b/examples/deviceReporter/README.txt
new file mode 100644
index 000000000..e846ec293
--- /dev/null
+++ b/examples/deviceReporter/README.txt
@@ -0,0 +1,14 @@
+examples/deviceReporter folder
+=================================
+
+This folder contains examples of mock deviceReporter files - like the ones used for running
+the system in development mode.
+
+The content of this folder are:
+* android_installedSolutions.json: example device reporter file for the android OS
+* linux_installedSolutions.json: example device reporter file for the linux OS (gnome 3)
+* win32_installedSolutions.json: example device reporter file for Windows 7
+* osx_installedSolutions.json: example device reporter file for OSX (darwin)
+
+NOTE that these entries are only meant as examples and not exhaustive lists of solutions available
+on each platform.
\ No newline at end of file
diff --git a/testData/deviceReporter/android_installedSolutions.json b/examples/deviceReporter/android_installedSolutions.json
similarity index 51%
rename from testData/deviceReporter/android_installedSolutions.json
rename to examples/deviceReporter/android_installedSolutions.json
index c6c69cf79..b0d908134 100644
--- a/testData/deviceReporter/android_installedSolutions.json
+++ b/examples/deviceReporter/android_installedSolutions.json
@@ -12,6 +12,18 @@
},
{
"id": "com.android.freespeech"
+ },
+ {
+ "id": "com.android.persistentConfiguration"
+ },
+ {
+ "id": "com.android.audioManager"
+ },
+ {
+ "id": "com.android.settings.system"
+ },
+ {
+ "id": "com.android.settings.secure"
}
]
}
diff --git a/testData/deviceReporter/linux_installedSolutions.json b/examples/deviceReporter/linux_installedSolutions.json
similarity index 66%
rename from testData/deviceReporter/linux_installedSolutions.json
rename to examples/deviceReporter/linux_installedSolutions.json
index 6378dcb8c..6ccb07650 100644
--- a/testData/deviceReporter/linux_installedSolutions.json
+++ b/examples/deviceReporter/linux_installedSolutions.json
@@ -7,77 +7,65 @@
{
"id": "org.gnome.desktop.interface"
},
-
+
{
"id": "org.gnome.nautilus"
},
-
+
{
"id": "org.gnome.desktop.a11y.keyboard"
},
-
+
{
- "id": "org.gnome.desktop.a11y.caribou-keyboard"
+ "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard"
},
-
+
{
"id": "org.gnome.orca"
},
-
+
{
"id": "org.gnome.desktop.a11y.magnifier"
},
-
+
{
- "id": "com.microsoft.windows.magnifier"
+ "id": "org.gnome.shell.overrides"
},
-
+
{
- "id": "com.microsoft.windows.onscreenKeyboard"
+ "id": "org.gnome.desktop.wm.preferences"
},
-
+
{
- "id": "nvda.screenReader"
+ "id": "com.microsoft.windows.magnifier"
},
-
+
{
- "id": "fluid.uiOptions.windows"
+ "id": "com.microsoft.windows.onscreenKeyboard"
},
-
+
{
- "id": "fluid.uiOptions.linux"
+ "id": "nvda.screenReader"
},
-
+
{
"id": "org.gnome.desktop.interface"
},
-
+
{
"id": "org.gnome.nautilus"
},
-
- {
- "id": "trace.easyOne.communicator.windows"
- },
-
+
{
"id": "trace.easyOne.communicator.linux"
},
-
- {
- "id": "trace.easyOne.sudan.windows"
- },
-
+
{
"id": "trace.easyOne.sudan.linux"
},
-
- {
- "id": "webinsight.webAnywhere.windows"
- },
-
+
{
"id": "webinsight.webAnywhere.linux"
}
]
-}
\ No newline at end of file
+}
diff --git a/examples/deviceReporter/osx_installedSolutions.json b/examples/deviceReporter/osx_installedSolutions.json
new file mode 100644
index 000000000..c0395c6e4
--- /dev/null
+++ b/examples/deviceReporter/osx_installedSolutions.json
@@ -0,0 +1,8 @@
+{
+ "OS": {
+ "id": "darwin",
+ "version": "11.3.0"
+ },
+ "solutions": [
+ ]
+}
diff --git a/examples/deviceReporter/win32_installedSolutions.json b/examples/deviceReporter/win32_installedSolutions.json
new file mode 100644
index 000000000..d59147aad
--- /dev/null
+++ b/examples/deviceReporter/win32_installedSolutions.json
@@ -0,0 +1,47 @@
+{
+ "OS": {
+ "id": "win32",
+ "version": "5.0.0"
+ },
+ "solutions": [
+ {
+ "id": "com.microsoft.windows.magnifier"
+ },
+
+ {
+ "id": "com.microsoft.windows.onscreenKeyboard"
+ },
+
+ {
+ "id": "org.nvda-project"
+ },
+
+ {
+ "id": "trace.easyOne.communicator.windows"
+ },
+
+ {
+ "id": "trace.easyOne.sudan.windows"
+ },
+
+ {
+ "id": "webinsight.webAnywhere.windows"
+ },
+
+ {
+ "id": "com.texthelp.readWriteGold"
+ },
+
+ {
+ "id": "com.microsoft.windows.highContrast"
+ },
+
+ {
+ "id": "com.microsoft.windows.mouseTracking"
+ },
+
+ {
+ "id": "com.microsoft.windows.cursors"
+ }
+ ]
+}
diff --git a/examples/pspChannelClient/README.md b/examples/pspChannelClient/README.md
new file mode 100644
index 000000000..ab84241d0
--- /dev/null
+++ b/examples/pspChannelClient/README.md
@@ -0,0 +1,162 @@
+# PSP Example client
+
+This directory contains a tiny sample client for the pspChannel WebSockets connection.
+
+## Starting up the GPII and trying out the PSP channel example client.
+
+It can be tested by firing up any configuration of the GPII which includes a local FlowManager. Particularly
+suitable are the mock configurations in %gpii-universal/gpii/configs/mocks - for example you can run
+
+ `node gpii.js gpii/configs/mocks gpii.config.development.manualTesting.mock.windows`
+
+or
+
+ `node gpii.js gpii/configs/mocks gpii.config.untrusted.development.manualTesting.mock.windows`
+
+from the root of this repository. This will start up the servers in trusted or untrusted mode, respectively.
+
+After that, you can fire up the client at any time during the lifetime of the FlowManager by typing
+
+ `node pspChannelClient.js`
+
+from this directory in another shell.
+
+Before or after that, you can experiment with logging in and out of the GPII using endpoints such as
+
+ `http://localhost:8081/user/snapset_1a/login`
+
+and
+
+ `http://localhost:8081/user/snapset_1a/logout`
+
+Note that the preference sets used with the above configurations should be in the testData/preferences folder of this
+repository.
+
+## Example payloads received by the PSP
+
+Here are some sample payloads collected from this client during such testing.
+
+Firstly, connecting the client when no user is keyed into the system produces the payload
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "noUser",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {},
+ "preferences": {}
+ }
+ }
+}
+```
+
+After logging in `snapset_1a`, the client receives the following update:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "snapset_1a",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/DPIScale": {
+ "value": 1.25,
+ "schema": {
+ "title": "DPI Scale",
+ "description": "DPI scale factor on default monitor",
+ "type": "number",
+ "min": 1,
+ "max": 2,
+ "divisibleBy": 0.25
+ }
+ },
+ "http://registry\\.gpii\\.net/common/cursorSize": {
+ "value": 1,
+ "schema": {
+ "title": "Cursor Size",
+ "description": "Cursor size",
+ "type": "number",
+ "min": 0,
+ "max": 1,
+ "divisibleBy": 0.1
+ }
+ }
+ },
+ "preferences": {
+ "name": "Larger 125%",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+After logging out `snapset_1a`, the client receives the following update:
+
+```json
+{
+ "type": "modelChanged",
+ "payload": {
+ "path": [],
+ "value": null,
+ "type": "DELETE"
+ }
+}
+```
+
+When the PSP issues an update to the preferences (see below), a message will be sent to the PSP once the settings change
+has been applied to the system:
+
+```json
+{
+ "type": "preferencesApplied"
+}
+```
+
+## Example payloads sent by the PSP
+
+The PSP has can send two different payloads to the GPII core architecture. Namely a change in the set of preferences
+to use or a change in a setting.
+
+Changing the preferences set via the psp is done with the following payload:
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "activePrefsSetName": "bright"
+ }
+}
+```
+
+This will change the preferences set to that named by the key "bright". To change the set to something else, simply
+exchange "bright" with the name of the desired preferences set.
+
+The PSP API also supports changing preferences (one at a time). This is done via the following payload:
+
+```json
+{
+ "type": "modelChanged",
+ "value": {
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/magnification": {
+ "value": 4
+ }
+ }
+ }
+}
+```
+
+Where the value for the "preferences" key is the setting that should change, and the value for the "value" is the new
+value the setting should take. So in the above example the common term magnification is set to 4.
diff --git a/examples/pspChannelClient/package.json b/examples/pspChannelClient/package.json
new file mode 100644
index 000000000..08f307d26
--- /dev/null
+++ b/examples/pspChannelClient/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "gpii-psp-channel-client",
+ "version": "0.1.0",
+ "dependencies": {
+ "ws": "6.2.1"
+ }
+}
diff --git a/examples/pspChannelClient/pspChannelClientApplyPrefs.js b/examples/pspChannelClient/pspChannelClientApplyPrefs.js
new file mode 100644
index 000000000..246a2be09
--- /dev/null
+++ b/examples/pspChannelClient/pspChannelClientApplyPrefs.js
@@ -0,0 +1,55 @@
+/*!
+ * Test client for PSPChannel WebSockets
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+"use strict";
+
+var ws = require("ws");
+
+// The client starts the communication
+var socket = new ws("ws://localhost:8081/pspChannel"); // eslint-disable-line new-cap
+
+// When the connection is done, the server will send the initial data of the current session if any
+socket.on("open", function () {
+ console.log("## pspChannelClientApplyPrefs: Socket connected");
+});
+
+socket.on("message", function (data) {
+ var message = JSON.parse(data);
+ console.log("## pspChannelClientApplyPrefs: Received the following message: " + JSON.stringify(message, null, 4));
+
+ if (message.type === "preferencesApplied") {
+ console.log("## pspChannelClientApplyPrefs: Preferences have been applied");
+ socket.close();
+ return;
+ } else {
+ console.log("## pspChannelClientApplyPrefs: Message type '" + message.type + "' not 'preferencesApplied'");
+ }
+
+ console.log("## pspChannelClientApplyPrefs: Sending 'modelChanged' request");
+ socket.send(JSON.stringify(
+ {
+ "type": "modelChanged",
+ value: {
+ settingControls: {
+ "http://registry\\.gpii\\.net/common/DPIScale": {
+ value: 1.75
+ }
+ }
+ }
+ })
+ );
+});
diff --git a/examples/pspChannelClient/pspChannelClientReadPrefs.js b/examples/pspChannelClient/pspChannelClientReadPrefs.js
new file mode 100644
index 000000000..da7988d63
--- /dev/null
+++ b/examples/pspChannelClient/pspChannelClientReadPrefs.js
@@ -0,0 +1,64 @@
+/*!
+ * Test client for PSPChannel WebSockets
+ *
+ * Copyright 2019 OCAD University
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+"use strict";
+
+var ws = require("ws");
+
+// The client starts the communication
+var socket = new ws("ws://localhost:8081/pspChannel"); // eslint-disable-line new-cap
+var readRequestCount = 0;
+
+// When the connection is done, the server will send the initial data of the current session if any
+socket.on("open", function () {
+ console.log("## pspChannelClientReadPrefs: Socket connected");
+});
+
+socket.on("message", function (data) {
+ var message = JSON.parse(data);
+ console.log("## pspChannelClientReadPrefs: Received the following message: " + JSON.stringify(message, null, 4));
+
+ if (message.type === "preferenceReadSuccess") {
+ console.log("## pspChannelClientReadPrefs: Preference has been read");
+ socket.close();
+ return;
+ } else if (message.type === "preferenceReadFail") {
+ console.log("## pspChannelClientReadPrefs: Preference cannot be read");
+ socket.close();
+ return;
+ } else {
+ console.log("## pspChannelClientReadPrefs: Message type '" + message.type + "' not reading success/failure");
+ }
+
+ if (readRequestCount === 0) {
+ readRequestCount++;
+ // Only send the read request once
+ console.log("## pspChannelClientReadPrefs: Sending 'pullModel' request");
+ socket.send(JSON.stringify(
+ {
+ "type": "pullModel",
+ value: {
+ settingControls: {
+ "http://registry\\.gpii\\.net/common/magnification": {
+ value: 1
+ }
+ }
+ }
+ })
+ );
+ }
+});
diff --git a/gpii.js b/gpii.js
new file mode 100644
index 000000000..6777af152
--- /dev/null
+++ b/gpii.js
@@ -0,0 +1,32 @@
+/*!
+GPII Universal Personalization Framework Node.js Bootstrap
+
+Copyright 2012 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+The research leading to these results has received funding from the European Union's
+Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+// GPII-3394: This is temporary code enabling Google Stackdriver tracing https://cloud.google.com/trace/
+// If this requirement expands, this code will be moved into its own module, otherwise it will be removed
+
+if (process.env.GPII_ENABLE_STACKDRIVER_TRACE === "true") {
+ console.log("Enabling @google-cloud/trace-agent");
+ require("@google-cloud/trace-agent").start();
+}
+
+// A simple bootstrap file which allows a configuration of the GPII to be
+// started from the command line from universal
+
+var fluid = require("./index.js"),
+ gpii = fluid.registerNamespace("gpii");
+
+gpii.start();
diff --git a/gpii/configs/README.md b/gpii/configs/README.md
new file mode 100644
index 000000000..7d174f96c
--- /dev/null
+++ b/gpii/configs/README.md
@@ -0,0 +1,50 @@
+# GPII Configs
+
+This directory and its subdirectories contain variants of the configs that are used to:
+
+1. Start GPII application in development or production modes;
+2. Write integration and acceptance tests.
+
+The "production" configurations are intended for use in production deployments. The "development" configurations are
+used in the acceptance tests in this package. The "manualTesting" configurations provided in this directory are intended
+for use in manual QA, troubleshooting, demos, et cetera.
+
+## The Couchdb Test Harness
+
+The "development" and "manualTesting" configurations in this package make use of the gpii-couchdb-test-harness package,
+which ensures that a CouchDB instance is available and provisioned with the required data if no data is found. The
+caseHolder used in with most "development" configurations has sequence steps that start the harness and wait for CouchDB
+to be ready. The "manualTesting" configurations provided in this directory immediately start the CouchDB harness (see
+below) on startup, and are intended for use in manual QA, troubleshooting, demos, et cetera.
+
+For all configurations that use the CouchDB test harness, you can control how Couch is started/stopped using an
+environment variable:
+
+* (nothing, the default): Start a Docker container running CouchDB.
+* `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, a Vagrant VM will be created that is running Docker and CouchDB.
+* `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, an external instance of CouchDB running on port 25984 will be
+ used.
+
+For more information, see [the documentation for the gpii-couchdb-test-harness
+package](https://github.com/GPII/gpii-couchdb-test-harness).
+
+If a database already exists, the "development" configurations will not replace the data. To reset to the default data,
+stop the GPII, delete the existing database or container and start the GPII.
+
+You can delete the `gpii` database in your CouchDB instance either by:
+
+* Making a call to the [CouchDB REST endpoint that deletes a
+ database](https://docs.couchdb.org/en/stable/api/database/common.html#delete--db), i.e. `DELETE
+ http://localhost:25984/gpii`
+* Deleting the database from the built-in[Fauxton administrative
+ interface](https://docs.couchdb.org/en/stable/fauxton/index.html) in Couch, i.e. `http://localhost:25984/_utils`
+
+Either method will work regardless of how you're running CouchDB. You also have the option to stop the GPII, remove the
+VM/container, and then start the GPII. To remove the VM/container:
+
+* When running in Docker (the default):
+ * Find the ID of the container using a command like `docker ps -a`
+ * Remove the container using a command like `docker rm -rf {CONTAINER_ID}`
+* When running in Vagrant, destroy the VM using commands like:
+ * `cd node_modules/gpii-couchdb-test-harness/src/test`
+ * `vagrant destroy -f`
diff --git a/gpii/configs/cloudBased.json b/gpii/configs/cloudBased.json
deleted file mode 100644
index 93116cfff..000000000
--- a/gpii/configs/cloudBased.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "typeName": "cloudBased.development",
- "includes": [
- "../node_modules/matchMaker/configs/development.json",
- "../node_modules/ontologyServer/configs/development.json",
- "../node_modules/lifecycleManager/configs/development.json",
- "../node_modules/solutionsRegistry/configs/development.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json b/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json
deleted file mode 100644
index a3c2f956f..000000000
--- a/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "typeName": "flowManager.preferencesServer.solutionsRegistry.deviceReporter.matchMaker.ontologyServer.lifecycleManagerServer.development",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": true,
- "port": 8081
- }
- }
- }
- },
- "includes": [
- "../node_modules/deviceReporter/configs/development.json",
- "../node_modules/flowManager/configs/development.json",
- "../node_modules/matchMaker/configs/development.json",
- "../node_modules/ontologyServer/configs/development.json",
- "../node_modules/lifecycleManager/configs/development.json",
- "../node_modules/preferencesServer/configs/development.json",
- "../node_modules/solutionsRegistry/configs/development.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json b/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json
deleted file mode 100644
index 46f066660..000000000
--- a/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "typeName": "flowManager.preferencesServer.solutionsRegistry.deviceReporter.matchMaker.ontologyServer.lifecycleManagerServer.production",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": false,
- "port": 8081
- }
- }
- }
- },
- "includes": [
- "../node_modules/deviceReporter/configs/production.json",
- "../node_modules/flowManager/configs/production.json",
- "../node_modules/matchMaker/configs/production.json",
- "../node_modules/ontologyServer/configs/development.json",
- "../node_modules/lifecycleManager/configs/production.json",
- "../node_modules/solutionsRegistry/configs/development.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5 b/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5
new file mode 100644
index 000000000..ae3225452
--- /dev/null
+++ b/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5
@@ -0,0 +1,21 @@
+/**
+ * This configuration runs a GPII cloud including a cloud based flow manager and a preferences server, locally
+ * on port 8081. It also uses a CouchDB Docker test harness for persistence, which is available at port 25984.
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** gpii-couchdb-test-harness
+ */
+{
+ "type": "gpii.config.cloudBased.development.manualTesting",
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.cloudBased.development.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.cloudBased.flowManager.production.json5 b/gpii/configs/gpii.config.cloudBased.flowManager.production.json5
new file mode 100644
index 000000000..585cf14a7
--- /dev/null
+++ b/gpii/configs/gpii.config.cloudBased.flowManager.production.json5
@@ -0,0 +1,32 @@
+/**
+ * This configuration runs a cloud based flow manager locally on port 8082. It expects:
+ * 1. couchDB to be running on the localhost using the default port 5984.
+ * 2. Preferences Server running as a standalone server using `gpii.config.preferencesServer.standalone.production`.
+ *
+ * * The following components are running:
+ * ** Cloud Bsed Flow Manager
+ * ** Flat MatchMaker
+ */
+{
+ "type": "gpii.config.cloudBased.flowManager.production",
+ "options": {
+ "gradeNames": "gpii.withCloudLogging", // Apply cloud logging for GPII-3310
+ "mainServerPort": 8082,
+ "dbServerPort": 5984,
+ "dataSourceConfigProductionEnv": {
+ "baseUrl": "@expand:kettle.resolvers.env(GPII_DATASOURCE_HOSTNAME)",
+ "port": "@expand:kettle.resolvers.env(GPII_DATASOURCE_PORT)"
+ },
+ "distributeOptions": {
+ "flowManager.production.dataStore": {
+ "source": "{that}.options.dataSourceConfigProductionEnv",
+ "target": "{that flowManager oauth2DataStore}.options.dataSourceConfig",
+ "priority": "after:flowManager.base.dataStore"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%flowManager/configs/gpii.flowManager.config.cloud.base.json5",
+ "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.development.manualTesting.json5 b/gpii/configs/gpii.config.development.manualTesting.json5
new file mode 100644
index 000000000..a79742900
--- /dev/null
+++ b/gpii/configs/gpii.config.development.manualTesting.json5
@@ -0,0 +1,23 @@
+/**
+ * This configuration runs the entire system locally on port 8081, including a local flow manager, a cloud based
+ * flow manager, and a preferences server. It also uses a CouchDB Docker test harness for persistence, which is
+ * available at port 25984.
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based FlowManager
+ * ** Local FlowManager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** gpii-couchdb-test-harness
+ */
+{
+ "type": "gpii.config.development.manualTesting",
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5 b/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5
new file mode 100644
index 000000000..5a687684b
--- /dev/null
+++ b/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5
@@ -0,0 +1,20 @@
+/**
+ * This configuration runs a standalone preferences server locally on port 8081. It also starts and loads test data
+ * into a CouchDB instance listening on port 25984 for data persistence.
+ *
+ * * The following components are running:
+ * ** Preferences Server
+ * ** gpii-couchdb-test-harness
+ *
+ * Note: this config is created for testing PPT locally. Currently it's not used by any other configs and tests.
+ */
+{
+ "type": "gpii.config.preferencesServer.standalone.development.manualTesting",
+ "options": {
+ "dbServerPort": 25984
+ },
+ "mergeConfigs": [
+ "%preferencesServer/configs/gpii.preferencesServer.config.base.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.preferencesServer.standalone.production.json5 b/gpii/configs/gpii.config.preferencesServer.standalone.production.json5
new file mode 100644
index 000000000..3beb440ee
--- /dev/null
+++ b/gpii/configs/gpii.config.preferencesServer.standalone.production.json5
@@ -0,0 +1,28 @@
+/**
+ * This configuration runs a standalone Preferences Server on port 8081. It doesn't start any backend database
+ * for data persistence, but expect couchDB to be running separately on the localhost using the default port 5984.
+ *
+ * * The following components are running on the local machine:
+ * ** Preferences Server
+ */
+{
+ "type": "gpii.config.preferencesServer.standalone.production",
+ "options": {
+ "gradeNames": "gpii.withCloudLogging", // Apply cloud logging for GPII-3310
+ "dbServerPort": 5984,
+ "dataSourceConfigProductionEnv": {
+ "baseUrl": "@expand:kettle.resolvers.env(GPII_DATASOURCE_HOSTNAME)",
+ "port": "@expand:kettle.resolvers.env(GPII_DATASOURCE_PORT)"
+ },
+ "distributeOptions": {
+ "preferencesServer.dataStore.production": {
+ "source": "{that}.options.dataSourceConfigProductionEnv",
+ "target": "{that preferencesServer dataStore}.options.dataSourceConfig",
+ "priority": "after:preferencesServer.base.dataStore"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%preferencesServer/configs/gpii.preferencesServer.config.base.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5
new file mode 100644
index 000000000..964d27279
--- /dev/null
+++ b/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5
@@ -0,0 +1,26 @@
+/**
+ * This configuration runs the entire system locally under the untrusted Flow Manager architecture.
+ * It starts these servers:
+ * 1. Untrusted Local Flow Manager on port 8081
+ * 2. Cloud Based Flow Manager and Preferences Server on port 8084
+ * 3. CouchDB running in Docker, available on port 25984
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based Flow Manager
+ * ** Dynamic Device Reporter
+ * ** Untrusted Local Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** gpii-couchdb-test-harness
+ */
+{
+ "type": "gpii.config.untrusted.development.dynamicDR.manualTesting",
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.untrusted.development.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.development.manualTesting.json5
new file mode 100644
index 000000000..f272b555b
--- /dev/null
+++ b/gpii/configs/gpii.config.untrusted.development.manualTesting.json5
@@ -0,0 +1,26 @@
+/**
+ * This configuration runs the entire system locally under the untrusted Flow Manager architecture.
+ * It starts these servers:
+ * 1. Untrusted Local Flow Manager on port 8081
+ * 2. Cloud Based Flow Manager and Preferences Server on port 8084
+ * 3. CouchDB on port 25984
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based Flow Manager
+ * ** Untrusted Local Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** gpii-couchdb-test-harness
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** Static Device Reporter (reading from file)
+ */
+{
+ "type": "gpii.config.untrusted.development.manualTesting",
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5 b/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5
new file mode 100644
index 000000000..c9077f6a7
--- /dev/null
+++ b/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5
@@ -0,0 +1,15 @@
+/**
+ * This configuration is the production version of `gpii.config.untrusted.development.dynamicDR`. It runs
+ * the local Untrusted Flow Manager locally and supresses the 'login', 'logout' and 'proximityTriggered' HTTP endpoints.
+ *
+ * * The following components are running on the local machine:
+ * ** Untrusted Local Flow Manager
+ * ** Dynamic Device Reporter
+ */
+{
+ "type": "gpii.config.untrusted.production.dynamicDR",
+ "mergeConfigs": [
+ "%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5"
+ ]
+}
diff --git a/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5
new file mode 100644
index 000000000..ef2aa7e3d
--- /dev/null
+++ b/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5
@@ -0,0 +1,29 @@
+/**
+ * This configuration is the production version of `gpii.config.untrusted.development.dynamicDR.manualTesting`.
+ * It runs the entire system locally under the untrusted Flow Manager architecture and supresses
+ * the 'login', 'logout' and 'proximityTriggered' HTTP endpoints.
+ *
+ * It starts these servers:
+ * 1. Untrusted Local Flow Manager on port 8081
+ * 2. Cloud Based Flow Manager and Preferences Server on port 8084
+ * 3. CouchDB on port 25984
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based Flow Manager
+ * ** Dynamic Device Reporter
+ * ** Untrusted Local Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** gpii-couchdb-test-harness
+ */
+{
+ "type": "gpii.config.untrusted.production.dynamicDR.manualTesting",
+ "mergeConfigs": [
+ "%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5",
+ "%gpii-universal/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5"
+ ]
+}
diff --git a/gpii/configs/mocks/README.md b/gpii/configs/mocks/README.md
new file mode 100644
index 000000000..7b31f2f9e
--- /dev/null
+++ b/gpii/configs/mocks/README.md
@@ -0,0 +1,5 @@
+# Mock Configs
+
+This directory contains variants of the configs in the outer directory, with the settings handler definitions replaced
+by mocks. This makes them suitable for testing from the bare universal repository, without the risk of failures caused
+by missing platform-specific settings handlers, or any corruption to the outer machine's state.
diff --git a/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5 b/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5
new file mode 100644
index 000000000..6eafcd237
--- /dev/null
+++ b/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5
@@ -0,0 +1,21 @@
+/**
+ * A variety of gpii.config.development.local.json5 which supplies mock implementations of all settings handlers
+ * supported on the Windows platform.
+ *
+ * This configuration runs the entire system, in the "trusted local FlowManager" configuration, locally and in development mode.
+ * This configuration is used for testing PSP channel, which means that the fetch of solutions is sourced from the filesystem,
+ * and the preferences are fetched from a CouchDB instance that is created if needed and provisioned at the start of
+ * each test sequence.
+ */
+{
+ "type": "gpii.config.development.local.mock.windows",
+ "options": {
+ "gradeNames": "gpii.test.integration.mockHolder.windows"
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.development.local"
+ ],
+ "require": [
+ "%gpii-universal/gpii/node_modules/testing/src/Mocks.js"
+ ]
+}
diff --git a/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5 b/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5
new file mode 100644
index 000000000..14603888d
--- /dev/null
+++ b/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5
@@ -0,0 +1,19 @@
+/**
+ * A variety of gpii.config.development.manualTesting which supplies mock implementations of all settings handlers
+ * supported on the Windows platform.
+ *
+ * This configuration runs the entire system, in the "trusted local FlowManager" configuration, locally and in development mode.
+ * This means that fetches of solutions, preferences, etc. are sourced from the filesystem rather than from CouchDB URLs.
+ */
+{
+ "type": "gpii.config.development.manualTesting.mock.windows",
+ "options": {
+ "gradeNames": "gpii.test.integration.mockHolder.windows"
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/gpii.config.development.manualTesting"
+ ],
+ "require": [
+ "%gpii-universal/gpii/node_modules/testing/src/Mocks.js"
+ ]
+}
diff --git a/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5 b/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5
new file mode 100644
index 000000000..ea842f6b8
--- /dev/null
+++ b/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5
@@ -0,0 +1,55 @@
+/**
+ * A variant of gpii.config.untrusted.development which includes mock definitions for all builtin settings handlers
+ * supported on the Windows platform. This allows any preference set to be logged in from a bare universal repo without
+ * crashing the system.
+ *
+ * This configuration runs:
+ * * The local flow manager on port 8081
+ * * The cloud on port 8084
+ *
+ * This mean that:
+ * * The keys, preferences and authentication data is stored locally in a CouchDB instance.
+ * * The following components are running on the local machine:
+ * ** flowManager
+ * ** preferencesServer
+ * ** OntologyHandler
+ * ** solutionsRegistry (reading registry from file)
+ * ** flatMatchMaker
+ * ** MatchMakerFramework
+ * ** CouchDB
+ *
+ * Note that this config incorrectly derives from the deviceReporter development config. This is because of a dependency error
+ * in the system design where the cloudBased flowManager still attempts to query the platform deviceReporter rather than
+ * recognising that it is on platform "web" - e.g. within BrowserChannel.js
+ */
+
+{
+ "type": "gpii.config.untrusted.development.manualTesting.mock.windows",
+ "options": {
+ "gradeNames": ["kettle.multiConfig.config"],
+ "configs": {
+ "localConfig": {
+ "configName": "gpii.config.untrusted.development.mock.windows",
+ "configPath": "%gpii-universal/gpii/configs/mocks"
+ },
+ "cloudBasedConfig": {
+ "configName": "gpii.config.cloudBased.development",
+ "configPath": "%gpii-universal/gpii/configs/shared"
+ }
+ },
+ "distributeOptions": {
+ "untrusted.development.port": {
+ "record": 8084,
+ "target": "{that cloudBasedConfig}.options.mainServerPort"
+ },
+ "untrusted.development.prefs": {
+ "record": "http://localhost:8084",
+ "target": "{that cloudBasedConfig flowManager prefsServerDataSource}.options.prefsServerURL",
+ "priority": "after:flowManager.prefsServerDataSource.default"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5"
+ ]
+}
diff --git a/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5 b/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5
new file mode 100644
index 000000000..7257c2713
--- /dev/null
+++ b/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5
@@ -0,0 +1,16 @@
+/**
+ * Variant of gpii.config.untrusted.development.json5 with the local (untrusted) flowManager's settings handlers
+ * replaced by mocks emulating the Windows platform.
+ */
+{
+ "type": "gpii.config.untrusted.development.mock.windows",
+ "options": {
+ "gradeNames": "gpii.test.integration.mockHolder.windows"
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5"
+ ],
+ "require": [
+ "%gpii-universal/gpii/node_modules/testing/src/Mocks.js"
+ ]
+}
diff --git a/gpii/configs/ps.sr.development.json b/gpii/configs/ps.sr.development.json
deleted file mode 100644
index a56fa12a6..000000000
--- a/gpii/configs/ps.sr.development.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "typeName": "preferencesServer.solutionsRegistry.development",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": true,
- "port": 8081
- }
- }
- }
- },
- "includes": [
- "../node_modules/preferencesServer/configs/development.json",
- "../node_modules/solutionsRegistry/configs/development.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/configs/ps.sr.production.json b/gpii/configs/ps.sr.production.json
deleted file mode 100644
index b54a66ee8..000000000
--- a/gpii/configs/ps.sr.production.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "typeName": "preferencesServer.solutionsRegistry.production",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": false,
- "port": 8081
- }
- }
- }
- },
- "includes": [
- "../node_modules/preferencesServer/configs/production.json",
- "../node_modules/solutionsRegistry/configs/production.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/configs/shared/README.md b/gpii/configs/shared/README.md
new file mode 100644
index 000000000..cb4e0dc0c
--- /dev/null
+++ b/gpii/configs/shared/README.md
@@ -0,0 +1,5 @@
+# "Shared" Configurations
+
+The configuration files in this directory are used in acceptance tests, manual testing, and production. In almost all
+circumstances, you will want to use a configuration in the parent directory that merges the appropriate configuration
+files rather than using these files directory in tests or manual testing.
diff --git a/gpii/configs/shared/gpii.config.cloudBased.development.json5 b/gpii/configs/shared/gpii.config.cloudBased.development.json5
new file mode 100644
index 000000000..c3ff09d27
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.cloudBased.development.json5
@@ -0,0 +1,27 @@
+/**
+ * This configuration acts as a base for `gpii.config.cloudBased.development.manualTesting`.
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ *
+ * Notes on testing:
+ * This setup is used by the acceptance tests in universal testing the cloudBased applications.
+ * Each individual acceptance test may override some elements of this config, e.g. the folder
+ * to be used for reading prefs and solutionsRegistry).
+ * For more details, see the txt files describing the configs in the acceptance tests folder.
+ *
+ */
+{
+ "type": "gpii.config.cloudBased.development",
+ "mergeConfigs": [
+ "%flowManager/configs/gpii.flowManager.config.cloud.base.json5",
+ "%preferencesServer/configs/gpii.preferencesServer.config.base.json5",
+ "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.couch.development.base.json5 b/gpii/configs/shared/gpii.config.couch.development.base.json5
new file mode 100644
index 000000000..916a8c7ad
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.couch.development.base.json5
@@ -0,0 +1,43 @@
+/*
+
+ This configuration is common to both the tests and "development" configs, and adds a local CouchDB instance
+ provided by the gpii-couchdb-test-harness package. Note, you can control how Couch is started/stopped using an
+ environment variable:
+
+ * (nothing, the default): Start a Docker container running CouchDB.
+ * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, a Vagrant VM will be created that is running Docker and CouchDB.
+ * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, an external instance of CouchDB running on port 25984 will be
+ used.
+
+ For more information, see the documentation for the gpii-couchdb-test-harness package:
+
+ https://github.com/GPII/gpii-couchdb-test-harness
+
+ See "gpii.test.couchEnvironment" grade in gpii/node_modules/testing/src/Fixtures.js for the CouchDB data loader
+ used for running acceptance and integration tests.
+
+*/
+{
+ "type": "gpii.config.couch.development.base",
+ "options": {
+ "components": {
+ "harness": {
+ "type": "gpii.test.couchdb.harness",
+ "options": {
+ "databases": {
+ "gpii": {
+ "data": [
+ "%gpii-universal/testData/dbData/clientCredentials.json",
+ "%gpii-universal/testData/dbData/gpiiAppInstallationClients.json",
+ "%gpii-universal/build/dbData/snapset/gpiiKeys.json",
+ "%gpii-universal/build/dbData/snapset/prefsSafes.json",
+ "%gpii-universal/testData/dbData/views.json"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "require": ["gpii-couchdb-test-harness", "journal"]
+}
diff --git a/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5 b/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5
new file mode 100644
index 000000000..ef9ff4f64
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5
@@ -0,0 +1,33 @@
+/*
+
+ A configuration that starts a Couch instance if needed on startup, and which always clean up the data if the
+ database already exists. Note, you can control how Couch is started/stopped using an environment variable:
+
+ * (nothing, the default): Start a Docker container running CouchDB.
+ * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, create a Vagrant VM that runs Docker and CouchDB.
+ * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, connect to an external instance of CouchDB running on port 25984.
+
+ For more information, see the documentation for the gpii-couchdb-test-harness package:
+
+ https://github.com/GPII/gpii-couchdb-test-harness
+
+*/
+{
+ "type": "gpii.config.couch.development.startWithCleanData",
+ "options": {
+ "components": {
+ "harness": {
+ "options": {
+ "listeners": {
+ "onCreate.startHarness": {
+ func: "{that}.startup"
+ }
+ }
+ }
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5 b/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5
new file mode 100644
index 000000000..4044fb54c
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5
@@ -0,0 +1,36 @@
+/*
+
+ A configuration that starts a Couch instance if needed on startup, but which does not clean up the data if the
+ database already exists. Note, you can control how Couch is started/stopped using an environment variable:
+
+ * (nothing, the default): Start a Docker container running CouchDB.
+ * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, create a Vagrant VM that runs Docker and CouchDB.
+ * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, connect to an external instance of CouchDB running on port 25984.
+
+ For more information, see the documentation for the gpii-couchdb-test-harness package:
+
+ https://github.com/GPII/gpii-couchdb-test-harness
+
+*/
+{
+ "type": "gpii.config.couch.development.startWithoutCleanData",
+ "options": {
+ "components": {
+ "harness": {
+ "type": "gpii.test.couchdb.harness.persistent",
+ "options": {
+ "cleanDbs": false,
+ // Start the harness on startup.
+ "listeners": {
+ "onCreate.startHarness": {
+ func: "{that}.startup"
+ }
+ }
+ }
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.development.base.local.json5 b/gpii/configs/shared/gpii.config.development.base.local.json5
new file mode 100644
index 000000000..b02f6036e
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.development.base.local.json5
@@ -0,0 +1,34 @@
+/**
+ * This configuration acts as a base for `gpii.config.development.local` and `gpii.config.development.dynamicDR.local`.
+ * It includes configs to locate the local flow manager, the cloud based flow manager, the preferences server and
+ * matchmaker all locally. The extra work required is to arrange to cross-inject the ontology handler between the
+ * matchmaker and preferences server.
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based Flow Manager
+ * ** Local Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ */
+{
+ "type": "gpii.config.development.base.local",
+ "options": {
+ "distributeOptions": {
+ "development.all.ontologyHandler": {
+ "record": "{ontologyHandler}",
+ "target": "{that preferencesServer}.options.components.ontologyHandler"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%flowManager/configs/gpii.flowManager.config.cloud.base.json5",
+ "%flowManager/configs/gpii.flowManager.config.local.base.json5",
+ "%preferencesServer/configs/gpii.preferencesServer.config.base.json5",
+ "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5 b/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5
new file mode 100644
index 000000000..db38156d6
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5
@@ -0,0 +1,32 @@
+/**
+ * This configuration runs the entire system locally with a dynamic deviceReporter on port 8081. It expects couchDB
+ * to be running separately on localhost for data persistence.
+ *
+ * * The following components are running:
+ * ** Dynamic Device Reporter
+ * ** Flow Manager
+ * ** Preferences Server
+ * ** Ontology Handler
+ * ** Solutions Registry (reading registry from file)
+ * ** MatchMaker Framework
+ * ** Flat MatchMaker
+ * ** Lifecycle Manager
+ *
+ * Notes on testing:
+ * This setup is used as a basis for the platform specific acceptance tests that make use of a dynamic device reporter.
+ */
+{
+ "type": "gpii.config.development.dynamicDR.local",
+ "options": {
+ "distributeOptions": {
+ "gpii.config.deviceReporter.flowManager": {
+ "record": "{flowManager}.solutionsRegistryDataSource",
+ "target": "{that gpii.deviceReporter.live}.options.components.solutionsRegistryDataSource"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.development.base.local.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.development.local.json5 b/gpii/configs/shared/gpii.config.development.local.json5
new file mode 100644
index 000000000..c5de331d6
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.development.local.json5
@@ -0,0 +1,29 @@
+/**
+ * This configuration acts as a base for `gpii.config.development.manualTesting` and many configurations used by
+ * integration tests. It runs the entire system including the local flow manager, the cloud based flow manager,
+ * the preferences server and matchmaker all locally. It doesn't start any backend database for data persistence.
+ *
+ * * The following components are running:
+ * ** Auth Server
+ * ** Flat MatchMaker
+ * ** Cloud Based Flow Manager
+ * ** Lifecycle Manager
+ * ** Local Flow Manager
+ * ** MatchMaker Framework
+ * ** Ontology Handler
+ * ** Preferences Server
+ * ** Solutions Registry (reading registry from file)
+ * ** Static Device Reporter (reading from file)
+ *
+ * Notes on testing:
+ * This setup is used as a basis for the platform specific acceptance tests. Since the acceptance tests might use slightly different paths for the tests (i.e. a special folder for reading prefs and solutionsRegistry),
+ * what is actually being used for testing is a slight variation of this config. For more details, see the txt files describing the configs in the acceptance tests folder of both universal and the platform specific repositories.
+ *
+ */
+{
+ "type": "gpii.config.development.local",
+ "mergeConfigs": [
+ "%deviceReporter/configs/gpii.deviceReporter.config.static.json5",
+ "%gpii-universal/gpii/configs/shared/gpii.config.development.base.local.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5 b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5
new file mode 100644
index 000000000..c6c56449d
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5
@@ -0,0 +1,24 @@
+/**
+ * This configuration acts as a base for `gpii.config.untrusted.development.local`. It runs the
+ * local Untrusted Flow Manager locally.
+ *
+ * * The following components are running on the local machine:
+ * ** Untrusted Local Flow Manager
+ * ** Dynamic Device Reporter
+ */
+{
+ "type": "gpii.config.untrusted.development.dynamicDR",
+ "options": {
+ "distributeOptions": {
+ "gpii.config.deviceReporter.flowManager": {
+ "record": "{flowManager}.solutionsRegistryDataSource",
+ "target": "{that gpii.deviceReporter.live}.options.components.solutionsRegistryDataSource"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5",
+ "%flowManager/configs/gpii.flowManager.config.untrusted.base.json5",
+ "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5 b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5
new file mode 100644
index 000000000..26af42527
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5
@@ -0,0 +1,34 @@
+/**
+ * This configuration acts as a base for `gpii.config.untrusted.development.manualTesting`.
+ *
+ * It uses a multi-server Kettle configuration to run the Untrusted Flow Manager and
+ * a local version of the Cloud Based Flow Manager within the same Kettle process, but on different ports:
+ * It starts these servers:
+ * 1. Untrusted Local Flow Manager on port 8081
+ * 2. Cloud with both Cloud Based Flow Manager and Preferences Server on port 8084
+ *
+ * * The following components are running on the local machine:
+ * ** Untrusted Local Flow Manager
+ * ** Dynamic Device Reporter
+ *
+ * * The following components are running on the cloud:
+ * *** Auth Server
+ * *** Flat MatchMaker
+ * *** Cloud Based Flow Manager
+ * *** Preferences Server
+ * *** Solutions Registry
+ */
+{
+ "type": "gpii.config.untrusted.development.dynamicDR.local",
+ "options": {
+ "configs": {
+ "localConfig": {
+ "configName": "gpii.config.untrusted.development.dynamicDR",
+ "configPath": "%gpii-universal/gpii/configs/shared"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.untrusted.development.json5 b/gpii/configs/shared/gpii.config.untrusted.development.json5
new file mode 100644
index 000000000..82b0ce5af
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.untrusted.development.json5
@@ -0,0 +1,16 @@
+/**
+ * This configuration acts as a base for `gpii.config.untrusted.development.local`. It runs the
+ * local Untrusted Flow Manager locally.
+ *
+ * * The following components are running on the local machine:
+ * ** Untrusted Local Flow Manager
+ * ** Static Device Reporter (reading from file)
+ */
+{
+ "type": "gpii.config.untrusted.development",
+ "mergeConfigs": [
+ "%deviceReporter/configs/gpii.deviceReporter.config.static.json5",
+ "%flowManager/configs/gpii.flowManager.config.untrusted.base.json5",
+ "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5"
+ ]
+}
diff --git a/gpii/configs/shared/gpii.config.untrusted.development.local.json5 b/gpii/configs/shared/gpii.config.untrusted.development.local.json5
new file mode 100644
index 000000000..e4bb8fdbd
--- /dev/null
+++ b/gpii/configs/shared/gpii.config.untrusted.development.local.json5
@@ -0,0 +1,47 @@
+/**
+ * This configuration acts as a base for `gpii.config.untrusted.development.manualTesting`.
+ *
+ * It uses a multi-server Kettle configuration to run the Untrusted Flow Manager and
+ * a local version of the Cloud Based Flow Manager within the same Kettle process, but on different ports:
+ * It starts these servers:
+ * 1. Untrusted Local Flow Manager on port 8081
+ * 2. Cloud with both Cloud Based Flow Manager and Preferences Server on port 8084
+ *
+ * * The following components are running on the local machine:
+ * ** Untrusted Local Flow Manager
+ * ** Static Device Reporter (reading from file)
+ *
+ * * The following components are running on the cloud:
+ * *** Auth Server
+ * *** Flat MatchMaker
+ * *** Cloud Based Flow Manager
+ * *** Preferences Server
+ * *** Solutions Registry
+ */
+{
+ "type": "gpii.config.untrusted.development.local",
+ "options": {
+ "gradeNames": ["kettle.multiConfig.config"],
+ "configs": {
+ "localConfig": {
+ "configName": "gpii.config.untrusted.development",
+ "configPath": "%gpii-universal/gpii/configs/shared"
+ },
+ "cloudBasedConfig": {
+ "configName": "gpii.config.cloudBased.development",
+ "configPath": "%gpii-universal/gpii/configs/shared"
+ }
+ },
+ "distributeOptions": {
+ "untrusted.development.port": {
+ "record": 8084,
+ "target": "{that cloudBasedConfig}.options.mainServerPort"
+ },
+ "untrusted.development.prefs": {
+ "record": "http://localhost:8084",
+ "target": "{that cloudBasedConfig flowManager prefsServerDataSource}.options.prefsServerURL",
+ "priority": "after:flowManager.prefsServerDataSource.default"
+ }
+ }
+ }
+}
diff --git a/gpii/index.js b/gpii/index.js
deleted file mode 100644
index 0b2e53735..000000000
--- a/gpii/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-var fluid = require("infusion");
-
-fluid.require("kettle", require);
-fluid.require("matchMaker", require);
-fluid.require("transformer", require);
-fluid.require("deviceReporter", require);
-fluid.require("lifecycleManager", require);
-fluid.require("lifecycleActions", require);
-fluid.require("flowManager", require);
-fluid.require("settingsHandlers", require);
-fluid.require("preferencesServer", require);
-fluid.require("ontologyServer", require);
-
-module.exports = fluid;
diff --git a/gpii/node_modules/accessRequester/index.js b/gpii/node_modules/accessRequester/index.js
new file mode 100644
index 000000000..4870c8496
--- /dev/null
+++ b/gpii/node_modules/accessRequester/index.js
@@ -0,0 +1,7 @@
+"use strict";
+
+var fluid = require("infusion");
+
+fluid.module.register("accessRequester", __dirname, require);
+
+require("./src/accessRequester.js");
diff --git a/gpii/node_modules/accessRequester/package.json b/gpii/node_modules/accessRequester/package.json
new file mode 100644
index 000000000..d7d6d3517
--- /dev/null
+++ b/gpii/node_modules/accessRequester/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "accessRequester",
+ "description": "Used by GPII app installations to request OAuth2 access tokens from GPII Cloud",
+ "version": "0.1.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/accessRequester/src/accessRequester.js b/gpii/node_modules/accessRequester/src/accessRequester.js
new file mode 100644
index 000000000..8e4103461
--- /dev/null
+++ b/gpii/node_modules/accessRequester/src/accessRequester.js
@@ -0,0 +1,93 @@
+/*!
+GPII Access Requester
+
+Copyright 2017 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = fluid || require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ $ = fluid.registerNamespace("jQuery");
+
+/*
+ * Access Requester provides getAccessToken(gpiiKey) API that returns a promise object whose resolved value is the access token.
+ * When this API is called, the access requester does:
+ * 1. Retrieves client credentials;
+ * 2. Communicate with GPII Cloud end point via [resource owner GPII key grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant);
+ * 3. Retrieves and returns a promise object whose resolved value contains the access token responded by the GPII Cloud /access_token endpoint;
+ *
+ * Access Requester requires these input options:
+ * @url {String} The GPII cloud http end point to request access tokens granted by OAuth2 resource owner GPII key grant.
+ * such as https://flowmanager.gpii.net/access_token
+ * @clientCredentialDataSourceGrade {String or Array of Strings} The grade name of the implementation of "clientCredentialDataSource" subcomponent
+ * that provides a get() API that returns a promise object whose resolved value is the client credential.
+ */
+fluid.defaults("gpii.accessRequester", {
+ gradeNames: ["fluid.component"],
+
+ // These options must be provided by integrators
+ url: "/access_token", // Must be provided by integrators. The API endpoint provided by the authorization server to request access tokens via Resource Owner GPII key Grant
+ clientCredentialDataSourceGrade: null, // Must be provided by integrators
+ // End of integrators provided options
+
+ distributeOptions: {
+ source: "{that}.options.clientCredentialDataSourceGrade",
+ target: "{that > clientCredentialDataSource}.options.gradeNames"
+ },
+ components: {
+ clientCredentialDataSource: {
+ type: "fluid.component"
+ },
+ accessTokenDataSource: {
+ type: "kettle.dataSource.URL",
+ options: {
+ url: "{accessRequester}.options.url",
+ writable: true,
+ writeMethod: "POST",
+ dataSourceModel: {
+ grant_type: "password",
+ password: "dummy"
+ }
+ }
+ }
+ },
+ invokers: {
+ getAccessToken: {
+ funcName: "gpii.accessRequester.getAccessToken",
+ args: ["{that}.clientCredentialDataSource", "{that}.accessTokenDataSource", "{arguments}.0"]
+ // gpiiKey
+ }
+ }
+});
+
+gpii.accessRequester.getAccessToken = function (clientCredentialDataSource, accessTokenDataSource, gpiiKey) {
+ var promiseTogo = fluid.promise();
+ var clientCredentialPromise = clientCredentialDataSource.get();
+ clientCredentialPromise.then(function (clientCredential) {
+ var accessTokenRequestParams = $.extend({}, accessTokenDataSource.options.dataSourceModel, clientCredential, {
+ username: gpiiKey
+ });
+ var accessTokenPromise = accessTokenDataSource.set(null, accessTokenRequestParams);
+ fluid.promise.follow(accessTokenPromise, promiseTogo);
+ }, function (err) {
+ promiseTogo.reject(err);
+ });
+ return promiseTogo;
+};
+
+/***********************************
+ Client Credential File Data Source
+ ***********************************/
+
+// The client credential is read from a file
+fluid.defaults("gpii.accessRequester.clientCredentialDataSource.file", {
+ gradeNames: ["kettle.dataSource.file.moduleTerms"],
+ path: "%gpii-universal/testData/clientCredentials/pilot.json"
+});
diff --git a/gpii/node_modules/accessRequester/test/AccessRequesterTests.js b/gpii/node_modules/accessRequester/test/AccessRequesterTests.js
new file mode 100644
index 000000000..33af81498
--- /dev/null
+++ b/gpii/node_modules/accessRequester/test/AccessRequesterTests.js
@@ -0,0 +1,218 @@
+/*!
+GPII Access Requester
+
+Copyright 2017 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ kettle = require("kettle"),
+ nock = require("nock");
+
+require("accessRequester");
+
+fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js");
+
+kettle.loadTestingSupport();
+
+fluid.registerNamespace("gpii.tests.accessRequester");
+
+gpii.tests.accessRequester.hostname = "http://gpii.net";
+gpii.tests.accessRequester.path = "/access_token";
+gpii.tests.accessRequester.gpiiKey = "gpii-key-test";
+
+// Set up mock ajax responses
+gpii.tests.accessRequester.setUpNock = function (config) {
+ var cloudMock = nock(gpii.tests.accessRequester.hostname);
+
+ // log nock matches
+ cloudMock.log(console.log);
+
+ // mock POST requests to "/access_token"
+ cloudMock.post(gpii.tests.accessRequester.path, config.request)
+ .reply(config.status, config.response);
+};
+
+// The customized accessRequester component for tests
+fluid.defaults("gpii.tests.accessRequester", {
+ gradeNames: ["gpii.accessRequester"],
+ gpiiKey: gpii.tests.accessRequester.gpiiKey,
+ url: gpii.tests.accessRequester.hostname + gpii.tests.accessRequester.path,
+ clientCredentialDataSourceGrade: "gpii.accessRequester.clientCredentialDataSource.file"
+});
+
+// The base testEnvironment grade to be inherited by all tests
+fluid.defaults("gpii.tests.accessRequesterTests", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ clientCredentialFilePath: null, // supplied by individual tests
+ testCaseHolderGrade: null, // supplied by individual tests
+ distributeOptions: {
+ clientCredentialFilePath: {
+ source: "{that}.options.clientCredentialFilePath",
+ target: "{that clientCredentialDataSource}.options.path"
+ },
+ testCaseHolderGrade: {
+ source: "{that}.options.testCaseHolderGrade",
+ target: "{that > testCaseHolder}.type"
+ }
+ },
+ components: {
+ accessRequester: {
+ type: "gpii.tests.accessRequester"
+ },
+ testCaseHolder: {
+ type: "fluid.test.testCaseHolder"
+ }
+ }
+});
+
+// 1. A successful workflow
+gpii.tests.accessRequester.success = {
+ clientCredentialFilePath: "%gpii-universal/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json",
+ nockConfig: {
+ request: {
+ "grant_type": "password",
+ "password": "dummy",
+ "client_id": "pilot-computer",
+ "client_secret": "pilot-computer-secret",
+ "username": gpii.tests.accessRequester.gpiiKey
+ },
+ status: 200,
+ response: {
+ access_token: "access-token-test-in-accessRequester",
+ expiresIn: 3600,
+ token_type: "Bearer"
+ }
+ }
+};
+
+fluid.defaults("gpii.tests.accessRequester.testCaseHolder.success", {
+ gradeNames: "fluid.test.testCaseHolder",
+ modules: [{
+ name: "The access requester module tests - a successful workflow",
+ expect: 1,
+ tests: [{
+ name: "An access token is received",
+ sequence: [{
+ task: "{accessRequester}.getAccessToken",
+ args: [gpii.tests.accessRequester.gpiiKey],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The response is expected with an access token", gpii.tests.accessRequester.success.nockConfig.response, "{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.accessRequesterTests.success", {
+ gradeNames: ["gpii.tests.accessRequesterTests", "gpii.test.testWithNock"],
+ clientCredentialFilePath: gpii.tests.accessRequester.success.clientCredentialFilePath,
+ testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.success",
+ invokers: {
+ setUpNock: {
+ funcName: "gpii.tests.accessRequester.setUpNock",
+ args: gpii.tests.accessRequester.success.nockConfig
+ }
+ }
+});
+
+// 2. Error occurs due to the wrong client credentials
+gpii.tests.accessRequester.error_wrongClientCredential = {
+ clientCredentialFilePath: "%gpii-universal/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json",
+ nockConfig: {
+ url: gpii.tests.accessRequester.path,
+ type: "post",
+ request: {
+ "grant_type": "password",
+ "password": "dummy",
+ "client_id": "wrong-client-id",
+ "client_secret": "wrong-client-secret",
+ "username": gpii.tests.accessRequester.gpiiKey
+ },
+ status: 401,
+ response: {
+ isError: true,
+ message: "Unauthorized"
+ }
+ },
+ expected: {
+ statusCode: 401,
+ isError: true
+ }
+};
+
+fluid.defaults("gpii.tests.accessRequester.testCaseHolder.error.wrongClientCredential", {
+ gradeNames: "fluid.test.testCaseHolder",
+ modules: [{
+ name: "The access requester module tests - a failed workflow due to the wrong client credential",
+ expect: 1,
+ tests: [{
+ name: "The request for an access token is rejected due to the wrong client credential",
+ sequence: [{
+ task: "{accessRequester}.getAccessToken",
+ args: [gpii.tests.accessRequester.gpiiKey],
+ reject: "jqUnit.assertLeftHand",
+ rejectArgs: ["The error is received", gpii.tests.accessRequester.error_wrongClientCredential.expected, "{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.accessRequesterTests.error.wrongClientCredential", {
+ gradeNames: ["gpii.tests.accessRequesterTests", "gpii.test.testWithNock"],
+ clientCredentialFilePath: gpii.tests.accessRequester.error_wrongClientCredential.clientCredentialFilePath,
+ testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.error.wrongClientCredential",
+ accessRequesterGrade: "gpii.tests.accessRequester.configNock",
+ invokers: {
+ setUpNock: {
+ funcName: "gpii.tests.accessRequester.setUpNock",
+ args: gpii.tests.accessRequester.error_wrongClientCredential.nockConfig
+ }
+ }
+});
+
+// 3. Error occurs due to the missing client credential file
+gpii.tests.accessRequester.error_missingClientCredentialFile = {
+ clientCredentialFilePath: "non-existing.json",
+ expected: {
+ statusCode: 404,
+ isError: true
+ }
+};
+
+fluid.defaults("gpii.tests.accessRequester.testCaseHolder.error.missingClientCredentialFile", {
+ gradeNames: "fluid.test.testCaseHolder",
+ modules: [{
+ name: "The access requester module tests - a failed workflow due to the missing client credential file",
+ expect: 1,
+ tests: [{
+ name: "The request for an access token is rejected due to the missing client credential file",
+ sequence: [{
+ task: "{accessRequester}.getAccessToken",
+ args: [gpii.tests.accessRequester.gpiiKey],
+ reject: "jqUnit.assertLeftHand",
+ rejectArgs: ["The response is expected with an access token", gpii.tests.accessRequester.error_missingClientCredentialFile.expected, "{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.accessRequesterTests.error.missingClientCredentialFile", {
+ gradeNames: ["gpii.tests.accessRequesterTests"],
+ clientCredentialFilePath: gpii.tests.accessRequester.error_missingClientCredentialFile.clientCredentialFilePath,
+ testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.error.missingClientCredentialFile"
+});
+
+// Run all tests
+fluid.test.runTests([
+ "gpii.tests.accessRequesterTests.success",
+ "gpii.tests.accessRequesterTests.error.wrongClientCredential",
+ "gpii.tests.accessRequesterTests.error.missingClientCredentialFile"
+]);
diff --git a/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json b/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json
new file mode 100644
index 000000000..c8a964a69
--- /dev/null
+++ b/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json
@@ -0,0 +1,4 @@
+{
+ "client_id": "pilot-computer",
+ "client_secret": "pilot-computer-secret"
+}
diff --git a/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json b/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json
new file mode 100644
index 000000000..00aafe7ba
--- /dev/null
+++ b/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json
@@ -0,0 +1,4 @@
+{
+ "client_id": "wrong-client-id",
+ "client_secret": "wrong-client-secret"
+}
diff --git a/gpii/node_modules/deviceReporter/README.md b/gpii/node_modules/deviceReporter/README.md
index 68a2e55ea..3b6dc5b2f 100644
--- a/gpii/node_modules/deviceReporter/README.md
+++ b/gpii/node_modules/deviceReporter/README.md
@@ -1,26 +1,25 @@
-Device Reporter
-===
+# Device Reporter
A server app built to be deployed with node.js server that returns device information as well as the list of installed solutions.
-### Device Reporter API
+## Device Reporter API
The app currently supports the following url:
{urlToDeviceReporterServer}/device // GET ONLY
-### Dependencies
+## Dependencies
[infusion](https://github.com/fluid-project/infusion) framework.
-### Installation
+## Installation
Run the following command in your newly checked out universal repository. This
will pull all dependencies that are required by universal.
npm install
-### Run
+## Run
To run device reporter app simply type:
@@ -33,4 +32,4 @@ For example:
node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/
node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/deviceReporter/configs/
- NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/
\ No newline at end of file
+ NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/
diff --git a/gpii/node_modules/deviceReporter/configs/base.json b/gpii/node_modules/deviceReporter/configs/base.json
deleted file mode 100644
index 2395d3d9e..000000000
--- a/gpii/node_modules/deviceReporter/configs/base.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "modules": [
- "deviceReporter"
- ]
-}
diff --git a/gpii/node_modules/deviceReporter/configs/development.json b/gpii/node_modules/deviceReporter/configs/development.json
deleted file mode 100644
index 46144eea0..000000000
--- a/gpii/node_modules/deviceReporter/configs/development.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "typeName": "deviceReporter.development",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": true,
- "components": {
- "deviceReporter": {
- "type": "gpii.deviceReporter",
- "options": {
- "gradeNames": ["gpii.deviceReporter.dev"],
- "installedSolutionsUrl": "file://%root/../../../testData/deviceReporter/installedSolutions.json"
- }
- }
- }
- }
- }
- }
- },
- "includes": [
- "./base.json"
- ]
-}
diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5
new file mode 100644
index 000000000..66944fde2
--- /dev/null
+++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5
@@ -0,0 +1,19 @@
+{
+ "type": "gpii.deviceReporter.config.base",
+ "options": {
+ "gradeNames": ["fluid.component"],
+ "components": {
+ "server": {
+ "type": "kettle.server",
+ "options": {
+ "components": {
+ "deviceReporter": {
+ "type": "gpii.deviceReporter"
+ }
+ }
+ }
+ }
+ }
+ },
+ "require": "deviceReporter"
+}
diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5
new file mode 100644
index 000000000..caae7d5b2
--- /dev/null
+++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5
@@ -0,0 +1,17 @@
+/**
+ * Running the device reporter in production mode
+ *
+ * **WARNING: This config this is untrusted and most likely broken**
+ */
+{
+ "type": "gpii.deviceReporter.config.dynamic",
+ "options": {
+ "distributeOptions": {
+ "deviceReporterType": {
+ "record": "gpii.deviceReporter.live",
+ "target": "{that deviceReporter}.type"
+ }
+ }
+ },
+ "mergeConfigs": "gpii.deviceReporter.config.base.json5"
+}
diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5
new file mode 100644
index 000000000..6f1a54d98
--- /dev/null
+++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5
@@ -0,0 +1,20 @@
+/**
+ * This configuration runs the device reporter in development mode, meaning that the list of
+ * installed solutions is read from a file in the filesystem.
+ */
+{
+ "type": "gpii.deviceReporter.config.static",
+ "options": {
+ "distributeOptions": {
+ "development.installedSolutionsPath": {
+ "record": "%gpii-universal/testData/deviceReporter/installedSolutions.json",
+ "target": "{that deviceReporter installedSolutionsDataSource}.options.path"
+ },
+ "deviceReporterType": {
+ "record": "gpii.deviceReporter.static",
+ "target": "{that gpii.deviceReporter}.options.gradeNames"
+ }
+ }
+ },
+ "mergeConfigs": "gpii.deviceReporter.config.base.json5"
+}
diff --git a/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js b/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js
deleted file mode 100644
index 20c0170a7..000000000
--- a/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*!
-Kettle Module Loader.
-
-Copyright 2012-2013 OCAD University
-
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
-
-You may obtain a copy of the License at
-https://github.com/gpii/kettle/LICENSE.txt
-*/
-
-// The purpose of this file is to be copied to a location for which the
-// require function is needed. It allows to find node modules relative
-// to that location that are otherwise non-resolvable.
-module.exports = require;
\ No newline at end of file
diff --git a/gpii/node_modules/deviceReporter/configs/production.json b/gpii/node_modules/deviceReporter/configs/production.json
deleted file mode 100644
index ffc3ac22c..000000000
--- a/gpii/node_modules/deviceReporter/configs/production.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "typeName": "deviceReporter.production",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": false,
- "components": {
- "deviceReporter": {
- "type": "gpii.deviceReporter",
- "options": {
- "gradeNames": ["gpii.deviceReporter.dev"],
- "installedSolutionsUrl": "file://%root/../../../testData/deviceReporter/installedSolutions.json"
- }
- }
- }
- }
- }
- }
- },
- "includes": [
- "./base.json"
- ]
-}
diff --git a/gpii/node_modules/deviceReporter/index.js b/gpii/node_modules/deviceReporter/index.js
index b2daafab2..521ce7c90 100644
--- a/gpii/node_modules/deviceReporter/index.js
+++ b/gpii/node_modules/deviceReporter/index.js
@@ -1,5 +1,7 @@
+"use strict";
+
var fluid = require("infusion");
-var loader = fluid.getLoader(__dirname);
+fluid.module.register("deviceReporter", __dirname, require);
-loader.require("./src/DeviceReporter.js");
\ No newline at end of file
+require("./src/DeviceReporter.js");
diff --git a/gpii/node_modules/deviceReporter/package.json b/gpii/node_modules/deviceReporter/package.json
index 34775752f..f87581ca1 100644
--- a/gpii/node_modules/deviceReporter/package.json
+++ b/gpii/node_modules/deviceReporter/package.json
@@ -1,19 +1,13 @@
{
- "name": "deviceReporter",
- "description": "The Device Reporter module provides information about the platform and its installed software in order to guide the choice to a solution fitting the user's profile",
- "version": "0.1.0",
- "author": "GPII",
- "bugs": "http://wiki.gpii.net/index.php/Main_Page",
- "homepage": "http://gpii.net/",
- "dependencies": {},
- "licenses": [
- {
- "type": "BSD-3-Clause",
- "url": "http://www.opensource.org/licenses/BSD-3-Clause"
- }
- ],
- "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"],
- "repository": "git://github.com/GPII/universal.git",
- "main": "./index.js",
- "engines": { "node" : ">=0.1.9" }
-}
\ No newline at end of file
+ "name": "deviceReporter",
+ "description": "The Device Reporter module provides information about the platform and its installed software in order to guide the choice to a solution fitting the user's profile",
+ "version": "0.3.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/deviceReporter/src/DeviceGet.js b/gpii/node_modules/deviceReporter/src/DeviceGet.js
deleted file mode 100644
index 9fbfbf91b..000000000
--- a/gpii/node_modules/deviceReporter/src/DeviceGet.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * GPII Preferences Server GET Handler
- *
- * Copyright 2012 OCAD University
- *
- * Licensed under the New BSD license. You may not use this file except in
- * compliance with this License.
- *
- * You may obtain a copy of the License at
- * https://github.com/gpii/universal/LICENSE.txt
- */
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- os = require("os"),
- gpii = fluid.registerNamespace("gpii"),
- kettle = fluid.require("kettle", require);
-
- fluid.defaults("kettle.requests.request.handler.deviceGet", {
- gradeNames: ["fluid.littleComponent", "autoInit"],
- invokers: {
- handle: {
- funcName: "gpii.handleDeviceGet",
- args: ["{requestProxy}", "{deviceReporter}.installedSolutionsDataSource"]
- }
- }
- });
-
- gpii.handleDeviceGet = function (requestProxy, installedSolutionsDataSource) {
- installedSolutionsDataSource.get(null, function onSuccess(solutions) {
- requestProxy.events.onSuccess.fire({
- solutions: solutions,
- OS: {
- //TODO: need to report more details - windowmanager, etc.
- id: os.platform(),
- // TODO: Need a better strategy - Node semver fails horribly
- // in the face of the benign underscore (eg. x86_64).
- version: os.release().replace("_", "-")
- }
- });
- });
- };
-
-})();
diff --git a/gpii/node_modules/deviceReporter/src/DeviceReporter.js b/gpii/node_modules/deviceReporter/src/DeviceReporter.js
index 4abc9ae85..c4fe7d9d2 100644
--- a/gpii/node_modules/deviceReporter/src/DeviceReporter.js
+++ b/gpii/node_modules/deviceReporter/src/DeviceReporter.js
@@ -1,54 +1,169 @@
/*!
-Device Reporter
+ * Device Reporter
+ *
+ * Copyright 2012 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
-Copyright 2012 Raising the Floor - International
+"use strict";
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
+var fluid = require("infusion"),
+ os = require("os"),
+ gpii = fluid.registerNamespace("gpii");
-You may obtain a copy of the License at
-https://github.com/gpii/universal/LICENSE.txt
-*/
+require("kettle");
+require("./DeviceReporterUtilities.js");
-(function () {
+fluid.defaults("gpii.deviceReporter", {
+ gradeNames: ["fluid.component"],
+ components: {
+ platformReporter: {
+ type: "gpii.platformReporter.native"
+ }
+ },
+ invokers: {
+ get: "fluid.notImplemented",
+ getFinalPayload: {
+ funcName: "gpii.deviceReporter.getFinalPayload",
+ args: ["{arguments}.0", "{platformReporter}"]
+ // solutions
+ }
+ }
+});
- "use strict";
+gpii.deviceReporter.getFinalPayload = function (installedSolutions, platformReporter) {
+ return {
+ solutions: installedSolutions,
+ OS: platformReporter.reportPlatform()
+ };
+};
- var fluid = require("infusion"),
- path = require("path"),
- gpii = fluid.registerNamespace("gpii");
+// Static Device Reporter
+fluid.defaults("gpii.deviceReporter.static", {
+ gradeNames: ["gpii.deviceReporter"],
+ components: {
+ installedSolutionsDataSource: {
+ type: "kettle.dataSource.file",
+ options: {
+ gradeNames: "kettle.dataSource.file.moduleTerms"
+ }
+ }
+ },
+ invokers: {
+ get: {
+ funcName: "gpii.deviceReporter.static.get",
+ args: ["{deviceReporter}"]
+ }
+ }
+});
- fluid.require("kettle", require);
- fluid.require("./DeviceGet.js", require);
+gpii.deviceReporter["static"].get = function (deviceReporter) {
+ var promiseTogo = fluid.promise();
+ var solutionsPromise = deviceReporter.installedSolutionsDataSource.get();
+ solutionsPromise.then(function (solutions) {
+ if (!fluid.isDestroyed(deviceReporter)) { // Fix failure for GPII-1227 tests which can receive this response after the deviceReporter is gone
+ promiseTogo.resolve(deviceReporter.getFinalPayload(solutions)); // TODO: Remove such checks once FLUID-5790 is resolved
+ }
+ }, function (err) {
+ var error = fluid.extend(err, {
+ statusCode: 500,
+ message: "Failed to read deviceReporter source: " + err.message,
+ isError: true
+ });
+ promiseTogo.reject(error);
+ });
+ return promiseTogo;
+};
- fluid.defaults("gpii.deviceReporter", {
- gradeNames: ["kettle.app", "autoInit"],
- handlers: {
- deviceGet: {
- route: "/device",
- type: "get"
- }
+// Dynamic Device Reporter
+// TODO: Dynamic Device Reporter has *NO* unit tests
+
+fluid.defaults("gpii.deviceReporter.live.nameResolver", {
+ gradeNames: ["fluid.component"],
+ invokers: {
+ resolveName: {
+ funcName: "fluid.identity"
+ }
+ }
+});
+
+fluid.defaults("gpii.deviceReporter.live", {
+ gradeNames: ["gpii.deviceReporter"],
+ components: {
+ // currently in "gpii.config.development.dynamicDR.local" as {flowManager}.solutionsRegistryDataSource
+ // solutionsRegistryDataSource: // should be supplied by integrator
+ nameResolver: {
+ type: "gpii.deviceReporter.live.nameResolver"
+ }
+ },
+ invokers: {
+ get: {
+ funcName: "gpii.deviceReporter.live.get",
+ args: ["{deviceReporter}", "{solutionsRegistryDataSource}"]
},
- installedSolutionsUrl: "",
- root: path.join(__dirname, ".."),
- components: {
- installedSolutionsDataSource: {
- type: "kettle.dataSource.URL",
- options: {
- gradeNames: ["kettle.dataSource.simpleCallbackWrapper"],
- url: "{gpii.deviceReporter}.options.installedSolutionsUrl"
+ filterByInstalledSolutions: {
+ funcName: "gpii.deviceReporter.filterByInstalledSolutions",
+ args: [ "{arguments}.0", "{deviceReporter}" ]
+ }
+ }
+});
+
+gpii.deviceReporter.live.get = function (deviceReporter, solutionsRegistryDataSource) {
+ var promiseTogo = fluid.promise();
+ var solutionsPromise = solutionsRegistryDataSource.get({
+ os: deviceReporter.platformReporter.reportPlatform().id
+ });
+
+ solutionsPromise.then(function (entries) {
+ var filteredSolutions = deviceReporter.filterByInstalledSolutions(entries);
+ promiseTogo.resolve(deviceReporter.getFinalPayload(filteredSolutions));
+ }, promiseTogo.reject);
+
+ return promiseTogo;
+};
+
+gpii.deviceReporter.filterByInstalledSolutions = function (entries, deviceReporter) {
+ var installedSolutions = [];
+ fluid.each(entries, function (entry, entryId) {
+ if (!installedSolutions.some(function (s) { return s.id === entryId; })) {
+ var foundEntryId = fluid.find(entry.isInstalled, function (installedSolutionsReporter) {
+ var resolvedName = deviceReporter.nameResolver.resolveName(installedSolutionsReporter.type, "deviceReporter");
+ if (fluid.invokeGradedFunction(resolvedName, installedSolutionsReporter)) {
+ return entryId;
}
+ }, null);
+ if (foundEntryId !== null) {
+ installedSolutions.push({ "id": foundEntryId });
}
}
});
+ return installedSolutions;
+};
- fluid.defaults("gpii.deviceReporter.dev", {
- gradeNames: ["autoInit", "fluid.littleComponent"],
- distributeOptions: {
- source: "{that}.options.urlExpanderGradeNames",
- target: "{that urlExpander}.options.gradeNames"
- },
- urlExpanderGradeNames: ["kettle.urlExpander.development"]
- });
+fluid.defaults("gpii.platformReporter.native", {
+ gradeNames: ["fluid.component"],
+ invokers: {
+ reportPlatform: {
+ funcName: "gpii.platformReporter.native.reportPlatform"
+ }
+ }
+});
-})();
+gpii.platformReporter["native"].reportPlatform = function () { // "native" is a reserved word
+ return {
+ // TODO: need to report more details - windowmanager, etc.
+ id: os.platform(),
+ // TODO: Need a better strategy - Node semver fails horribly
+ // in the face of the benign underscore (eg. x86_64).
+ version: os.release().replace("_", "-")
+ };
+};
diff --git a/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js b/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js
new file mode 100644
index 000000000..b7b276f3f
--- /dev/null
+++ b/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js
@@ -0,0 +1,37 @@
+/**
+ * GPII Device Reporter Utilities.
+ *
+ * Copyright 2015 Emergya
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.deviceReporter");
+
+/* Marker function for use in isInstalled sections of Solutions Registry
+ * to identify in a meaningful way that the solution is always installed or
+ * available to the system.
+ * This is meant to be used by any OS built-in solution that needs to report
+ * to the dynamic device reporter that they are always installed.
+ */
+fluid.defaults("gpii.deviceReporter.alwaysInstalled", {
+ gradeNames: "fluid.function",
+ argumentMap: {}
+});
+
+gpii.deviceReporter.alwaysInstalled = function () {
+ return true;
+};
diff --git a/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js b/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js
new file mode 100644
index 000000000..511a397e5
--- /dev/null
+++ b/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js
@@ -0,0 +1,111 @@
+/*!
+ * Static Device Reporter Tests
+ *
+ * Copyright 2019 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ jqUnit = fluid.registerNamespace("jqUnit"),
+ kettle = require("kettle");
+
+kettle.loadTestingSupport();
+
+require("../src/DeviceReporter.js");
+
+fluid.registerNamespace("gpii.tests.deviceReporter");
+
+// The base testEnvironment grade to be inherited by all tests
+fluid.defaults("gpii.tests.deviceReporterTestEnv", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ installedSolutionsPath: null, // supplied by individual tests
+ testCaseHolderGrade: null, // supplied by individual tests
+ distributeOptions: {
+ installedSolutionsPath: {
+ source: "{that}.options.installedSolutionsPath",
+ target: "{that deviceReporter installedSolutionsDataSource}.options.path"
+ },
+ testCaseHolderGrade: {
+ source: "{that}.options.testCaseHolderGrade",
+ target: "{that > testCaseHolder}.type"
+ }
+ },
+ components: {
+ deviceReporter: {
+ type: "gpii.deviceReporter.static"
+ },
+ testCaseHolder: {
+ type: "fluid.test.testCaseHolder"
+ }
+ }
+});
+
+// Successful test cases
+fluid.defaults("gpii.tests.deviceReporter.testCaseHolder.success", {
+ gradeNames: "fluid.test.testCaseHolder",
+ modules: [{
+ name: "The device reporter test - a successful workflow",
+ expect: 2,
+ tests: [{
+ name: "The payload returned by the device reporter is correct",
+ sequence: [{
+ task: "{deviceReporter}.get",
+ resolve: "gpii.tests.deviceReporter.verifyPayload",
+ resolveArgs: ["{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.deviceReporter.success", {
+ gradeNames: ["gpii.tests.deviceReporterTestEnv"],
+ installedSolutionsPath: "%gpii-universal/testData/deviceReporter/installedSolutions.json",
+ testCaseHolderGrade: "gpii.tests.deviceReporter.testCaseHolder.success"
+});
+
+gpii.tests.deviceReporter.verifyPayload = function (response) {
+ jqUnit.assertNotUndefined("OS id has been received", response.OS.id);
+ jqUnit.assertTrue("Solutions have been received", response.solutions.length > 0);
+};
+
+// Error test cases
+fluid.defaults("gpii.tests.deviceReporter.testCaseHolder.error", {
+ gradeNames: "fluid.test.testCaseHolder",
+ modules: [{
+ name: "Device Reporter fails on corrupt JSON file",
+ expect: 2,
+ tests: [{
+ name: "The payload returned by the device reporter is correct",
+ sequence: [{
+ task: "{deviceReporter}.get",
+ reject: "gpii.tests.deviceReporter.assertError",
+ rejectArgs: ["{arguments}.0", ["Failed to read deviceReporter", "Parse error"]]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.deviceReporter.error", {
+ gradeNames: ["gpii.tests.deviceReporterTestEnv"],
+ installedSolutionsPath: "%gpii-universal/tests/data/faultyDeviceReport.jsonx",
+ testCaseHolderGrade: "gpii.tests.deviceReporter.testCaseHolder.error"
+});
+
+gpii.tests.deviceReporter.assertError = function (response, expectedErrorMsg) {
+ fluid.each(expectedErrorMsg, function (msg) {
+ jqUnit.assertTrue("The error message is expected", response.message.includes(msg));
+ });
+};
+
+fluid.test.runTests([
+ "gpii.tests.deviceReporter.success",
+ "gpii.tests.deviceReporter.error"
+]);
diff --git a/gpii/node_modules/eventLog/README.md b/gpii/node_modules/eventLog/README.md
new file mode 100644
index 000000000..2456699c7
--- /dev/null
+++ b/gpii/node_modules/eventLog/README.md
@@ -0,0 +1,164 @@
+# GPII Event Log
+
+Produces logs that can be read by a backend server for analysis and auditing. The log is in JSON format, and is sent
+to an external log server via tcp.
+
+Rather than transforming the fluid.log output, this module was produced so the debug log is still usable by humans and
+it serves as a way of opting-in to sending only useful (and non-private) information.
+
+## Log Destination
+
+GPII sends the log data to the Filebeat service on the localhost, via tcp. This can be overridden by setting the
+`logDestination` option of the `gpii.eventLog` grade, or the environment variable `GPII_EVENT_LOG`, to either the path
+to a file or a tcp URI (`tcp://127.0.0.1:51481`).
+
+If GPII is unable to connect to the log server, then the log data is buffered in memory until the connection can be
+made.
+
+### Filebeat
+
+Filebeat is usually automatically installed with GPII. The Filebeat configuration and installer module is in
+[github.com/stegru/gpii-metrics/.../filebeat-installer](https://github.com/stegru/gpii-metrics/tree/master/filebeat-installer)
+
+## Logged Events
+
+This module records GPII related events, listed below. For the additional events, related to the OS, see
+[gpii-windows/gpii/node_modules/windowsMetrics/README.md](https://github.com/GPII/windows/blob/master/gpii/node_modules/windowsMetrics/README.md).
+
+All logged events contain the following data:
+
+```json5
+{
+ "module": "gpii", // The area of GPII that caused this event
+ "event": "start", // The event name
+ "data": {
+ // Additional fields specific to the event
+ },
+ "installID": "hK/Dvo1GG8", // Unique identifier of the installation.
+ "sequence": 60689584, // Incrementing number, unique per installation.
+ "timestamp": "2018-10-31T06:45:21.971-07:00", // When the event occurred
+ "version": "0.4.0", // The version of this module (or the windows metrics, if higher).
+
+ // The following are optional, depending on the current state:
+ "sessionID": "4acr8sks-4069", // unique session identifier
+ "subSessionID": "4acr8sks-4069-5", // unique sub-session identifier
+ "logon": "in", // "in" or "out" if currently logging in/out - "in-after"/"out-after" 10 seconds after log in/out.
+ // for any other time, this field does not exist.
+ "gpiiKey": "d90eda4e-d1cd-415b-9605-dd9ce8be6359", // gpiiKey currently in use,
+ // from the UI:
+ "app": "active", // The active window belongs to Morphic.
+ "focus": "openUSB", // The button that currently has focus (combine with "app" to determine if it has system focus)
+ "hover": "launch-documorph", // The button that the mouse is currently hovering over.
+ "widget-hover": "openUSB", // The QSS pop-up that the mouse is over.
+ "field-hover": "ejectUsbButton", // The field within the pop-up that the mouse is over.
+ "field-focus": "regular-contrast", // The field within the pop-up that has key focus.
+}
+```
+
+### Start and stop
+
+When GPII starts
+
+```json5
+{
+ "module": "gpii",
+ "event": "start",
+}
+```
+
+```json5
+{
+ "module": "gpii",
+ "event": "stop",
+}
+```
+
+### Session start and end
+
+When a user session starts.
+
+```json5
+{
+ "module": "lifecycle",
+ "event": "SessionStart"
+}
+```
+
+```json5
+{
+ "module": "lifecycle",
+ "event": "SessionStop",
+ "data": {
+ "duration": 481 // number of seconds the session lasted.
+ }
+}
+```
+
+### Solutions
+
+When a solution has been applied or failed.
+
+```json5
+{
+ "module": "metrics",
+ "event": "solution-applied",
+ "data": {
+ "solutionID": "com.microsoft.windows.screenDPI"
+ }
+}
+```
+
+```json5
+{
+ "module": "metrics",
+ "event": "solution-failed",
+ "data": {
+ "solutionID": "com.microsoft.windows.screenResolution"
+ }
+}
+```
+
+### Preference
+
+A preference has been set
+
+```json5
+{
+ "module": "metrics",
+ "event": "preference",
+ "data": {
+ "name": "http://registry.gpii.net/common/highContrastTheme",
+ "value": "black-white"
+ }
+}
+```
+
+### Errors
+
+Two types of errors are recorded - those raised by `fluid.fail`, and uncaught exceptions.
+
+```json5
+{
+ "event": "Error.Fail",
+ "module": "GPII",
+ "data": {
+ "error": {
+ "message": "Received an invalid screen resolution: [object Object]",
+ "stack": "..."
+ }
+ }
+}
+```
+
+```json5
+{
+ "event": "Error.Exception",
+ "module": "GPII",
+ "data": {
+ "error": {
+ "message": "Cannot read property 'inferredConfiguration' of undefined",
+ "stack": "..."
+ }
+ }
+}
+```
diff --git a/gpii/node_modules/eventLog/index.js b/gpii/node_modules/eventLog/index.js
new file mode 100644
index 000000000..62c464543
--- /dev/null
+++ b/gpii/node_modules/eventLog/index.js
@@ -0,0 +1,27 @@
+/*
+ * Event logging.
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+
+fluid.module.register("eventLog", __dirname, require);
+
+require("./src/installID.js");
+require("./src/eventLog.js");
+require("./src/metrics.js");
diff --git a/gpii/node_modules/eventLog/package.json b/gpii/node_modules/eventLog/package.json
new file mode 100644
index 000000000..b8c2270ce
--- /dev/null
+++ b/gpii/node_modules/eventLog/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "eventLog",
+ "description": "Event logging.",
+ "version": "0.5.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/eventLog/src/eventLog.js b/gpii/node_modules/eventLog/src/eventLog.js
new file mode 100644
index 000000000..8b1a59f51
--- /dev/null
+++ b/gpii/node_modules/eventLog/src/eventLog.js
@@ -0,0 +1,439 @@
+/*
+ * Event logging.
+ * Handles logging of events that are deemed interesting enough to be recorded and sent to a log server for analysis.
+ * The log produced here is in JSON and sent to a different output than the usual fluid.log.
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+var fs = require("fs"),
+ moment = require("moment"),
+ net = require("net");
+
+var gpii = fluid.registerNamespace("gpii"),
+ $ = fluid.registerNamespace("jQuery");
+
+fluid.registerNamespace("gpii.eventLog");
+
+fluid.defaults("gpii.eventLog", {
+ gradeNames: ["fluid.component", "fluid.contextAware"],
+ components: {
+ installID: {
+ type: "gpii.installID"
+ },
+ settingsDir: {
+ type: "gpii.settingsDir"
+ }
+ },
+ invokers: {
+ logEvent: {
+ funcName: "gpii.eventLog.log", // moduleName, event, data, level
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"]
+ },
+ logError: "gpii.eventLog.logError",
+ getGpiiSettingsDir: "{settingsDir}.getGpiiSettingsDir",
+ getVersion: "gpii.eventLog.getVersion",
+ setState: {
+ funcName: "gpii.eventLog.setState",
+ args: ["{eventLog}", "{arguments}.0", "{arguments}.1"] // name, value
+ }
+ },
+ members: {
+ sequence: 0,
+ // Buffered log lines while there's not log server connection
+ logBuffer: [],
+ logLevel: fluid.logLevel.INFO,
+ // Maximum number of lines to buffer.
+ maxBufferlength: 0xfff,
+ // A TCP socket to send the log entries to (filebeat service). Taken from options.logDestination, if it's a url.
+ logServer: {
+ host: null,
+ port: null
+ },
+ // A file to write the log entries to. Taken from options.logDestination, if it's a local file.
+ logPath: null,
+ // Data to include in every log entry.
+ eventData: {
+ // The installation ID
+ installID: "@expand:{that}.installID.getInstallID()",
+ version: "@expand:{that}.getVersion()",
+ sessionID: undefined,
+ gpiiKey: undefined
+ },
+ // The start times for events which require durations to be recorded.
+ eventTimes: {}
+ },
+ listeners: {
+ "onCreate.logFile": {
+ func: "gpii.eventLog.initLogDestination",
+ args: ["{that}", "{that}.options.logDestination"]
+ },
+ "onCreate.log": {
+ func: "gpii.eventLog.logStartStop",
+ args: ["{that}", "start"]
+ },
+ "onDestroy.log": {
+ func: "gpii.eventLog.logStartStop",
+ args: ["{that}", "stop"]
+ }
+ },
+ // File path, or tcp://host:port
+ logDestination: null
+});
+
+/**
+ * A log event
+ * @typedef {Object} LogEvent
+ * @property {String} module The area of GPII the event is from.
+ * @property {String} event The name of the event.
+ * @property {String} data [optional] Extra information about the event.
+ * @property {Object} level The severity of the event (from fluid.logLevelsSpec, default: fluid.INFO).
+ * @property {String} version The eventLog module version. (automatically added)
+ * @property {String} timestamp Current time of the event. (automatically added)
+ * @property {String} sequence Ordered unique identifier to the event. (automatically added)
+ */
+
+/**
+ * Returns the actual date and time, as a string, in ISO 8601 format with the localtime + offset from UTC.
+ * eg: '2018-07-13T13:03:03.863+01:00'
+ * @return {String} The current date, in ISO-8601 format with localtime and UTC offset.
+ */
+gpii.eventLog.getTimestamp = function () {
+ return moment().toISOString(true);
+};
+
+/**
+ * Gets the version of the logger. This is logged so the log processing can adjust for any updates to this module.
+ *
+ * @return {String} The version of this module, from package.json.
+ */
+gpii.eventLog.getVersion = function () {
+ var packageJson = fluid.require("%eventLog/package.json");
+ return packageJson.version;
+};
+
+/**
+ * Logs the start or stop of GPII, without duplication.
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {String} state - "start" or "stop".
+ */
+gpii.eventLog.logStartStop = function (that, state) {
+ if (state !== gpii.eventLog.logStartStop.currentState) {
+ gpii.eventLog.logStartStop.currentState = state;
+ if (state === "start") {
+ // Start the event numbering based on the current timestamp to ensure it's unique (per instance).
+ that.sequence = Date.now() - 15e11;
+ }
+ that.logEvent("gpii", state);
+ }
+};
+
+/**
+ * Creates an object for the log. Everything in this object is what will be logged, and the "time" field will be added
+ * later when it is actually logged.
+ *
+ * @param {String} moduleName - The part of GPII causing this event.
+ * @param {String} event - Name of the event.
+ * @param {Any} [data] - [optional] Event specific data.
+ * @param {Object} level -[optional] Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE].
+ * @return {LogEvent} The log object.
+ */
+gpii.eventLog.createLogObject = function (moduleName, event, data, level) {
+ var eventObject = {
+ module: moduleName || "GPII",
+ event: event,
+ level: level
+ };
+
+ var hasValue = (data !== null && data !== undefined);
+ if (hasValue && fluid.isPlainObject(data)) {
+ hasValue = !$.isEmptyObject(data);
+ }
+
+ if (hasValue) {
+ eventObject.data = fluid.copy(data);
+ // Replace any components in the data object with their id.
+ fluid.each(eventObject.data, function (value, key) {
+ if (fluid.isComponent(value)) {
+ eventObject.data[key] = value.id;
+ }
+ });
+ }
+ return eventObject;
+};
+
+/**
+ * Logs an event.
+ *
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {String} moduleName - The part of GPII causing this event.
+ * @param {String} event - The event name.
+ * @param {Object} [data] - [optional] Event specific data. Can (shallowly) contain components, in which case just the ID is
+ * logged.
+ * @param {Object} [level] - [optional] Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE].
+ */
+gpii.eventLog.log = function (that, moduleName, event, data, level) {
+ var eventObject = gpii.eventLog.createLogObject(moduleName, event, data, level);
+ gpii.eventLog.writeLog(that, level, eventObject);
+};
+
+/**
+ * Logs an error.
+ *
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {String} moduleName - The part of GPII causing this error.
+ * @param {String} errType - Type of error.
+ * @param {Object} err - The error.
+ * @param {Object} [level] - [optional] Level of the log. default: fluid.logLevel.FAIL.
+ */
+gpii.eventLog.logError = function (that, moduleName, errType, err, level) {
+ if (!level) {
+ level = fluid.logLevel.FAIL;
+ }
+
+ var data = {};
+ if (err instanceof Error) {
+ // Error doesn't serialise
+ data.error = {};
+ fluid.each(Object.getOwnPropertyNames(err), function (source) {
+ // Ensure the first character of the field is lowercase - "Message" vs "message" was causing a duplicate
+ // field during the analysis.
+ var dest = source.charAt(0).toLowerCase() + source.slice(1);
+ data.error[dest] = err[source];
+ });
+ } else if (fluid.isPlainObject(err, true)) {
+ data.error = Object.assign({}, err);
+ } else {
+ data.error = {
+ message: err
+ };
+ }
+
+ if (!data.error.stack) {
+ data.error.stack = new Error().stack;
+ }
+
+ var eventObject = gpii.eventLog.createLogObject(moduleName, "Error." + errType, data);
+ gpii.eventLog.writeLog(that, fluid.logLevel.FAIL, eventObject);
+};
+
+// Log fluid.fail.
+fluid.failureEvent.addListener(function (args) {
+ var err = Array.isArray(args) ? args.join(" ") : args;
+ gpii.eventLog.gotError(err, "Fail");
+}, "gpii-eventLog", "before:fail");
+
+/**
+ * Logs an error caught by onUncaughtException or failureEvent.
+ *
+ * @param {String} err - The error.
+ * @param {String} errType - The error type (defaults to "Exception").
+ */
+gpii.eventLog.gotError = function (err, errType) {
+ var eventLog = fluid.queryIoCSelector(fluid.rootComponent, "gpii.eventLog");
+ if (eventLog.length > 0) {
+ fluid.each(eventLog, function (inst) {
+ inst.logError(inst, null, errType || "Exception", err);
+ });
+ }
+};
+
+// Log uncaught exceptions.
+fluid.onUncaughtException.addListener(gpii.eventLog.gotError, "gpii-eventLog");
+
+/**
+ * Parses the log path (which can be overridden by the GPII_EVENT_LOG environment variable), and sets either the
+ * logServer or logPath member.
+ *
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {String} logDestination - Where the log is sent (a local file path, or a tcp socket in the format of
+ * `tcp://:`).
+ */
+gpii.eventLog.initLogDestination = function (that, logDestination) {
+ logDestination = process.env.GPII_EVENT_LOG || logDestination;
+
+ var match = logDestination && /tcp:\/*([^:]+):([0-9]+)/.exec(logDestination);
+ if (match) {
+ that.logServer.host = match[1];
+ that.logServer.port = match[2];
+ } else {
+ that.logPath = logDestination;
+ }
+
+ var dest = that.logServer.host ? ("tcp://" + that.logServer.host + ":" + that.logServer.port) : that.logPath;
+ fluid.log(fluid.logLevel.IMPORTANT, "Writing event log to " + dest);
+};
+
+/**
+ * Writes an event to the log file.
+ *
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {Object} level - Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE].
+ * @param {LogEvent} event - The object. This will be modified to what has been sent to the log, adding the installID and
+ * timestamp fields.
+ */
+gpii.eventLog.writeLog = function (that, level, event) {
+ var eventLevel = gpii.eventLog.checkLevel(level);
+ event.level = eventLevel.value;
+
+ gpii.eventLog.recordDuration(that, event);
+
+ Object.assign(event, that.eventData);
+ event.timestamp = gpii.eventLog.getTimestamp();
+ event.sequence = that.sequence++;
+
+ if (eventLevel.priority <= that.logLevel.priority) {
+ var logLine = JSON.stringify(event) + "\n";
+
+ // Logging to both the server and file is allowed (gpii-windows tests depend on this feature)
+ if (that.logServer.host && that.logServer.port) {
+ gpii.eventLog.writeLogTcp(that, logLine);
+ }
+ if (that.logPath) {
+ fs.appendFileSync(that.logPath, logLine);
+ }
+ }
+};
+
+/**
+ * Handles the timing of a pair of duration events.
+ *
+ * Some events can be paired into having a duration. For example, tooltip-shown and tooltip-hidden. The timestamp for
+ * the start event is recorded, so when the end event is seen the duration is calculated and added to the event data.
+ *
+ * @param {Component} that The gpii.eventLog instance.
+ * @param {LogEvent} event The event.
+ */
+gpii.eventLog.recordDuration = function (that, event) {
+ var endEvent = that.options.durationEvents[event.event];
+ if (endEvent) {
+ // This is the start of a pair of duration events.
+ if (!that.eventTimes[endEvent]) {
+ that.eventTimes[endEvent] = [];
+ }
+ that.eventTimes[endEvent].push(process.hrtime());
+ } else {
+ var startTimes = that.eventTimes[event.event];
+ var startTime = startTimes && startTimes.pop();
+ if (startTime) {
+ // This is the ending of a pair of duration events.
+ if (!event.data) {
+ event.data = {};
+ }
+ var memberName = event.data.hasOwnProperty("duration") ? "duration_auto" : "duration";
+ event.data[memberName] = process.hrtime(startTime)[0];
+ }
+ }
+};
+
+/**
+ * Sends a log line to the configured log server.
+ *
+ * @param {Component} that - The gpii.eventLog instance.
+ * @param {String} logLine - The log line, including the newline.
+ */
+gpii.eventLog.writeLogTcp = function (that, logLine) {
+ if (!that.logSocket) {
+ gpii.eventLog.connectLog(that);
+ }
+
+ if (that.logSocket.connecting) {
+ // Buffer the output until the connection is made. Socket.write already buffers, however if the connection
+ // fails the data will be lost.
+ if (that.logBuffer.length > that.maxBufferlength) {
+ fluid.log("Dropping metrics data - exceeded initial buffer size of " + that.maxBufferlength);
+ } else {
+ that.logBuffer.push(logLine);
+ }
+ } else {
+ that.logSocket.write(logLine);
+ }
+
+};
+
+/**
+ * Connect to a TCP port which receives the log data.
+ * @param {Component} that - The gpii.eventLog instance.
+ */
+gpii.eventLog.connectLog = function (that) {
+ if (that.logSocket) {
+ fluid.fail("Already connecting to log server");
+ }
+
+ // Connect to the server.
+ that.logSocket = new net.Socket();
+ that.logSocket.connect(that.logServer.port, that.logServer.host, function () {
+ gpii.eventLog.connectLog.lastError = null;
+ fluid.log("Connected to log server");
+ // Send the initial data.
+ if (that.logBuffer.length > 0) {
+ that.logSocket.write(that.logBuffer.join(""));
+ that.logBuffer = [];
+ }
+ });
+
+ that.logSocket.on("close", function () {
+ if (that.logSocket) {
+ that.logSocket.destroy();
+ that.logSocket = null;
+ }
+ });
+
+ that.logSocket.on("error", function (err) {
+ if (gpii.eventLog.connectLog.lastError !== err.code) {
+ fluid.log("Log server socket error:", err);
+ if (that.logSocket) {
+ that.logSocket.destroy();
+ }
+ // Don't repeat the same error again.
+ gpii.eventLog.connectLog.lastError = err.code;
+ }
+ });
+};
+
+/**
+ * Ensure that the loglevel has a valid value. The levels are defined in the fluid.logLevelsSpec
+ * Sets INFO as default loglevel
+ *
+ * @param {Object} level - Level to check, can be a string that represents the value or a property of fluid.logLevel.
+ * @return {Object} A valid fluid.logLevel, with INFO as default.
+ */
+gpii.eventLog.checkLevel = function (level) {
+ var togo;
+ if (typeof level === "string" && level in fluid.logLevelsSpec) {
+ togo = fluid.logLevel[level];
+ } else {
+ togo = fluid.isLogLevel(level) && level;
+ }
+ return togo || fluid.logLevel.INFO;
+};
+
+/**
+ * Specifies a field which gets logged with every subsequent event.
+ *
+ * @param {Component} that The gpii.eventLog instance.
+ * @param {String} name The name of the field.
+ * @param {Object} value The field value. undefined or null will remove the field.
+ */
+gpii.eventLog.setState = function (that, name, value) {
+ if (value === undefined || value === null) {
+ delete that.eventData[name];
+ } else {
+ that.eventData[name] = value;
+ }
+};
diff --git a/gpii/node_modules/eventLog/src/installID.js b/gpii/node_modules/eventLog/src/installID.js
new file mode 100644
index 000000000..356bf604c
--- /dev/null
+++ b/gpii/node_modules/eventLog/src/installID.js
@@ -0,0 +1,98 @@
+/*
+ * Retrieves an "installation ID", which is something that uniquely identifies a particular machine.
+ *
+ * The installation ID is a string that's based on the OS specific "machine ID".
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ fs = require("fs"),
+ crypto = require("crypto");
+
+fluid.defaults("gpii.installID", {
+ gradeNames: ["fluid.component", "fluid.contextAware"],
+ contextAwareness: {
+ platform: {
+ checks: {
+ windows: {
+ contextValue: "{gpii.contexts.windows}",
+ gradeNames: "gpii.installID.windows"
+ },
+ linux: {
+ contextValue: "{gpii.contexts.linux}",
+ gradeNames: "gpii.installID.standard"
+ }
+ }
+ }
+ },
+ invokers: {
+ getInstallID: "gpii.installID.get({that}.getMachineID)",
+ getMachineID: "fluid.identity"
+ }
+});
+
+fluid.defaults("gpii.installID.standard", {
+ invokers: {
+ getMachineID: "gpii.installID.machineID.standard"
+ }
+});
+
+/**
+ * Gets the installation ID, using the given machine ID.
+ *
+ * @param {Function} getMachineID - The OS specific function to get the machine ID.
+ * @return {String} The installation ID.
+ */
+gpii.installID.get = function (getMachineID) {
+ var machineID = getMachineID();
+ var installID;
+
+ if (machineID) {
+ installID = crypto.createHash("sha1").update(machineID).digest("base64").substr(0, 10);
+ fluid.log(fluid.logLevel.IMPORTANT, "Installation ID: ", installID);
+ } else {
+ fluid.log(fluid.logLevel.WARN, "Unable to get installation ID");
+ installID = "none";
+ }
+
+ return installID;
+};
+
+fluid.registerNamespace("gpii.installID.machineID");
+
+/**
+ * Retrieve the machine ID from /etc/machine-id.
+ *
+ * @return {String} The machine ID.
+ */
+gpii.installID.machineID.standard = function () {
+ var togo;
+ var files = [ "/etc/machine-id", "/var/lib/dbus/machine-id" ];
+
+ for (var f in files) {
+ try {
+ togo = fs.readFileSync(files[f], "utf8");
+ break;
+ } catch (e) {
+ // Continue.
+ }
+ }
+
+ return togo;
+};
diff --git a/gpii/node_modules/eventLog/src/metrics.js b/gpii/node_modules/eventLog/src/metrics.js
new file mode 100644
index 000000000..e2dc8e1ab
--- /dev/null
+++ b/gpii/node_modules/eventLog/src/metrics.js
@@ -0,0 +1,336 @@
+/*
+ * Metrics logging.
+ * This acts upon events that are deemed interesting enough to be recorded and sent to a log server for analysis.
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+
+var gpii = fluid.registerNamespace("gpii");
+
+fluid.defaults("gpii.metrics", {
+ gradeNames: ["fluid.modelComponent", "fluid.contextAware"],
+ contextAwareness: {
+ platform: {
+ checks: {
+ test: {
+ contextValue: "{gpii.contexts.test}",
+ gradeNames: "gpii.metrics.test"
+ },
+ windows: {
+ contextValue: "{gpii.contexts.windows}",
+ gradeNames: "gpii.windowsMetrics"
+ }
+ }
+ }
+ },
+ invokers: {
+ logMetric: {
+ func: "{eventLog}.logEvent",
+ args: ["metrics", "{arguments}.0", "{arguments}.1"] // event, data
+ },
+ startSubSession: {
+ funcName: "gpii.metrics.startSubSession",
+ args: ["{that}", "{eventLog}"]
+ },
+ stopSubSession: {
+ funcName: "gpii.metrics.stopSubSession",
+ args: ["{that}", "{eventLog}"]
+ }
+ },
+ members: {
+ sessionSolutions: {},
+ // Incrementing sub-session id.
+ subSessionIncrementer: 0
+ },
+ events: {
+ "onStartMetrics": null,
+ "onStopMetrics": null,
+ // The user has become active (called on the first input after onInactive)
+ "onActive": null, // args: duration-inactive
+ // The user has become inactive (no input was received after {that}.config.input.inactiveTime)
+ "onInactive": null // args: {sleep:true}
+ },
+ listeners: {
+ "onStartMetrics.log": {
+ funcName: "fluid.log",
+ args: "Metrics started"
+ },
+ "onActive.metrics": {
+ func: "{that}.logMetric",
+ args: ["inactive-stop"]
+ },
+ "onActive.subsession": {
+ func: "{that}.startSubSession"
+ },
+ "onInactive.metrics": {
+ func: "{that}.logMetric",
+ args: ["inactive-begin", "{arguments}.0"]
+ },
+ "onInactive.subsession": {
+ func: "{that}.stopSubSession"
+ }
+ },
+ durationEvents: {
+ "start": "stop",
+ "inactive-begin": "inactive-stop",
+ "SessionStart": "SessionStop",
+ "subsession-begin": "subsession-end"
+ },
+ siteConfig: {}
+});
+
+// Mixin grade for gpii.metrics to log lifecycle manager things
+fluid.defaults("gpii.metrics.lifecycle", {
+ modelListeners: {
+ "{lifecycleManager}.model.logonChange": {
+ funcName: "gpii.metrics.logonStateChanged",
+ args: ["{that}", "{eventLog}", "{lifecycleManager}", "{change}.oldValue", "{change}.value"]
+ }
+ },
+ invokers: {
+ sessionStopped: {
+ funcName: "gpii.metrics.sessionStopped",
+ args: [ "{that}", "{eventLog}"]
+ }
+ },
+ listeners: {
+ "{lifecycleManager}.events.onCreate": {
+ namespace: "trackPrefsSetChange",
+ listener: "gpii.metrics.trackPrefsSetChange",
+ args: ["{that}", "{lifecycleManager}"]
+ },
+ "{lifecycleManager}.events.onSessionStart": [
+ {
+ namespace: "metrics.session",
+ funcName: "gpii.metrics.sessionStarted",
+ args: ["{that}", "{eventLog}", "{arguments}.1"]
+ }, {
+
+ func: "{that}.events.onStartMetrics"
+ }],
+ "{lifecycleManager}.events.onSessionStop": [{
+ namespace: "metrics.session",
+ func: "{that}.sessionStopped",
+ args: ["{that}", "{eventLog}", "{arguments}.1.id"]
+ }, {
+ "func": "{that}.events.onStopMetrics",
+ "priority": "before:eventLog"
+ }],
+ "{lifecycleManager}.events.onSessionSnapshotUpdate": {
+ namespace: "metrics",
+ funcName: "gpii.metrics.snapshotUpdate",
+ args: ["{that}", "{arguments}.2"]
+ },
+ "onDestroy.session": {
+ func: "{that}.sessionStopped"
+ }
+ }
+});
+
+fluid.defaults("gpii.metrics.standalone", {
+ listeners: {
+ "onCreate": "{that}.events.onStartMetrics.fire",
+ "onDestroy": "{that}.events.onStopMetrics.fire"
+ }
+});
+
+/**
+ * Attached as the lifecycleManager.onCreate event.
+ *
+ * Adds a model change listener to the user session's model to listen for changes to the preferences.
+ *
+ * @param {Component} that - The gpii.metrics instance.
+ * @param {Component} lifecycleManager - The lifecycleManager instance.
+ */
+gpii.metrics.trackPrefsSetChange = function (that, lifecycleManager) {
+ var userSession = lifecycleManager.getSession();
+ userSession.applier.modelChanged.addListener({
+ path: "preferences.contexts",
+ excludeSource: "SessionCleanup"
+ }, function (newValue, oldValue) {
+ var current = newValue && newValue[userSession.model.activePrefsSetName];
+ current = current && current.preferences;
+ var previous = oldValue && oldValue[userSession.model.activePrefsSetName];
+ previous = previous && previous.preferences;
+
+ gpii.metrics.preferenceChanged(that, current, previous);
+ });
+};
+
+/**
+ * Log the solutions as they're applied.
+ *
+ * @param {Component} that - The gpii.metrics instance.
+ * @param {Object} originalSettings - The original settings (only interested in the keys).
+ */
+gpii.metrics.snapshotUpdate = function (that, originalSettings) {
+ var ids = fluid.keys(originalSettings);
+ if (!that.sessionSolutions) {
+ that.sessionSolutions = {};
+ }
+ // Log the solution IDs that haven't been logged.
+ fluid.each(ids, function (id) {
+ if (!that.sessionSolutions[id]) {
+ that.logMetric("solution-applied", {
+ solutionID: id
+ });
+ that.sessionSolutions[id] = true;
+ }
+ });
+};
+
+/**
+ * Called when a preference has changed. (a change in the preferences field of the session's model.)
+ *
+ * @param {Comment} that - The gpii.metrics instance.
+ * @param {Object} current - The current preferences map for the active preferences set.
+ * @param {Object} previous - The previous preferences map for the active preferences set.
+ */
+gpii.metrics.preferenceChanged = function (that, current, previous) {
+ var diff = { changeMap: {}, changes: 0, unchanged: 0};
+ var same = fluid.model.diff(previous, current, diff);
+ if (!same) {
+ var changedPreferences;
+ if (fluid.isPlainObject(diff.changeMap)) {
+ changedPreferences = {};
+ fluid.each(diff.changeMap, function (value, key) {
+ if (value === "ADD") {
+ changedPreferences[key] = current[key];
+ }
+ });
+ } else if (diff.changeMap === "ADD") {
+ // Everything is new
+ changedPreferences = current;
+ }
+
+ fluid.each(changedPreferences, function (value, name) {
+ that.logMetric("preference", {
+ name: name,
+ newValue: value.toString()
+ });
+ });
+ }
+};
+
+/**
+ * Updates the logged gpiiKey
+ * @param {Component} that - The gpii.metrics instance.
+ * @param {Component} eventLog - The gpii.eventLog instance.
+ * @param {String} gpiiKey - The gpiiKey used for this session.
+ */
+gpii.metrics.sessionStarted = function (that, eventLog) {
+ eventLog.eventData.sessionID = fluid.allocateGuid();
+ eventLog.logEvent("lifecycle", "SessionStart");
+ that.startSubSession();
+};
+
+/**
+ * Removes the logged solution IDs for the session, and the current gpii key.
+ * @param {Component} that - The gpii.metrics instance.
+ * @param {Component} eventLog - The gpii.eventLog instance.
+ * @param {String} sessionID - Session ID.
+ */
+gpii.metrics.sessionStopped = function (that, eventLog) {
+ if (eventLog.eventData.sessionID) {
+ eventLog.logEvent("lifecycle", "SessionStop");
+ delete eventLog.eventData.sessionID;
+ that.sessionSolutions = {};
+ that.startSubSession();
+ }
+};
+
+/**
+ * A model state listener for {lifecycleManager}.model.logonChange.
+ * When the login state changes from "login" to "logout", log the solutions that did not get applied for that session.
+ *
+ * @param {Component} that - The gpii.metrics instance.
+ * @param {Component} eventLog - The eventLog instance.
+ * @param {Component} lifecycleManager - The lifecycleManager instance.
+ * @param {Object} oldValue - The old value.
+ * @param {Object} newValue - The new value.
+ */
+gpii.metrics.logonStateChanged = function (that, eventLog, lifecycleManager, oldValue, newValue) {
+ if (newValue.type === "login") {
+ if (newValue.inProgress) {
+ // Start marking all events with the gpiiKey.
+ if (newValue.gpiiKey === "noUser") {
+ eventLog.eventData.gpiiKeyBefore = eventLog.eventData.gpiiKey;
+ delete eventLog.eventData.gpiiKey;
+ } else {
+ eventLog.eventData.gpiiKey = newValue.gpiiKey;
+ }
+ // So metrics knows the next set of events is due to someone keying in
+ eventLog.eventData.logon = "in";
+ } else {
+ // Login is complete, so all further events are unrelated to key-in. There's a 15 second grace period for
+ // events that may occur shortly after, however.
+ eventLog.eventData.logon = "in-after";
+ setTimeout(function () {
+ delete eventLog.eventData.gpiiKeyBefore;
+ delete eventLog.eventData.logon;
+ }, 15000);
+ }
+ } else if (newValue.type === "logout" && !newValue.inProgress) {
+ // Stop marking events with the gpiiKey when they've completely logged out
+ delete eventLog.eventData.gpiiKey;
+
+ if (newValue.inProgress) {
+ eventLog.eventData.gpiiKeyBefore = eventLog.eventData.gpiiKey;
+ eventLog.eventData.logon = "out";
+ } else {
+ eventLog.eventData.logon = "out-after";
+ }
+ setTimeout(function () {
+ delete eventLog.eventData.gpiiKeyBefore;
+ delete eventLog.eventData.logon;
+ }, 15000);
+ }
+
+ if (oldValue && oldValue.type === "login" && (newValue.type === "logout" || !newValue.inProgress)) {
+ var session = lifecycleManager.getSession(oldValue.gpiiKey);
+ // The reason for this null checker is that the lifecyclaManager's "session" may be null after
+ // having logged on with a bogus token which terminated the request in error before the
+ // "session" could be created.
+ if (session && session.model.currentSettings) {
+ // Log the solution IDs that hadn't been applied.
+ var expectedSolutions = fluid.keys(session.model.activeConfiguration.lifecycleInstructions);
+ fluid.each(expectedSolutions, function (id) {
+ if (!session.model.currentSettings[id]) {
+ that.logMetric("solution-failed", {
+ solutionID: id
+ });
+ }
+ });
+ }
+ }
+};
+
+gpii.metrics.startSubSession = function (that, eventLog) {
+ eventLog.eventData.subSessionID = eventLog.eventData.sessionID + "-" + that.subSessionIncrementer++;
+ that.logMetric("subsession-begin", { subSessionID: eventLog.eventData.subSessionID});
+};
+
+gpii.metrics.stopSubSession = function (that, eventLog) {
+ if (eventLog.eventData.subSessionID) {
+ that.logMetric("subsession-end", {
+ subSessionID: eventLog.eventData.subSessionID
+ });
+ }
+ delete eventLog.eventData.subSessionID;
+};
diff --git a/gpii/node_modules/eventLog/test/EventLogTests.js b/gpii/node_modules/eventLog/test/EventLogTests.js
new file mode 100644
index 000000000..b3bb1038a
--- /dev/null
+++ b/gpii/node_modules/eventLog/test/EventLogTests.js
@@ -0,0 +1,865 @@
+/*
+ * eventLog Tests
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ fs = require("fs"),
+ os = require("os"),
+ net = require("net"),
+ readline = require("readline");
+
+var jqUnit = fluid.require("node-jqunit");
+var gpii = fluid.registerNamespace("gpii");
+fluid.registerNamespace("gpii.tests.eventLog");
+
+require("../index.js");
+
+require("../../lifecycleManager/");
+
+var teardowns = [];
+
+jqUnit.module("gpii.tests.eventLog", {
+ teardown: function () {
+ while (teardowns.length) {
+ teardowns.pop()();
+ }
+ }
+});
+
+fluid.defaults("gpii.installID.test", {
+ invokers: {
+ getMachineID: "gpii.tests.eventLog.getMachineID"
+ }
+});
+
+gpii.tests.eventLog.getMachineID = function () {
+ return "TEST-MACHINE-ID";
+};
+
+/**
+ * Creates a testable gpii.eventLog component.
+ *
+ * @param {String} logFile - The log file.
+ * @return {Component} An instance of gpii.eventLog component.
+ */
+gpii.tests.eventLog.createEventLogComponent = function (logFile) {
+ var eventLog = gpii.eventLog({
+ gradeNames: ["gpii.lifecycleManager", "gpii.metrics"],
+ distributeOptions: {
+ installID: {
+ record: "gpii.installID.test",
+ target: "{that installID}.options.gradeNames"
+ }
+ },
+ durationEvents: {
+ "test-start1": "test-end1",
+ "test-start2": "test-end2",
+ "test-start3": "test-end3",
+ "test-start4": "test-end4"
+ },
+ members: {
+ eventData: {
+ installID: gpii.tests.eventLog.installID,
+ sessionID: gpii.tests.eventLog.sessionID
+ }
+ },
+ logDestination: logFile
+ });
+ teardowns.push(eventLog.destroy);
+ return eventLog;
+};
+
+gpii.tests.eventLog.installID = "installation-id";
+gpii.tests.eventLog.timestamp = "2000-00-00T00:00:00.000Z";
+gpii.tests.eventLog.version = require("../package.json").version;
+gpii.tests.eventLog.sessionID = "session-id";
+
+gpii.tests.eventLog.testDefs = fluid.freezeRecursive(fluid.transform([
+ {
+ // generated by the component
+ testData: null,
+ expected: {
+ "level": "INFO",
+ "module": "gpii",
+ "event": "start"
+ }
+ },
+ {
+ // data object
+ testData: {
+ func: "log",
+ args: ["{that}", "module0", "event0", {"data0": "value0"}]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module0",
+ "event": "event0",
+ "data": {"data0": "value0"}
+ }
+ },
+ {
+ // no data
+ testData: {
+ func: "log",
+ args: ["{that}", "module1", "event1"]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module1",
+ "event": "event1"
+ }
+ },
+ {
+ // data string
+ testData: {
+ func: "log",
+ args: ["{that}", "module2", "event2", "value2"]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module2",
+ "event": "event2",
+ "data": "value2"
+ }
+ },
+ {
+ // data number
+ testData: {
+ func: "log",
+ args: ["{that}", "module3", "event3", 1234]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module3",
+ "event": "event3",
+ "data": 1234
+ }
+ },
+ {
+ // empty data
+ testData: {
+ func: "log",
+ args: ["{that}", "module4", "event4", {}]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module4",
+ "event": "event4"
+ }
+ },
+ {
+ // null data
+ testData: {
+ func: "log",
+ args: ["{that}", "module5", "event5", null]
+ },
+ expected: {
+ "level": "INFO",
+ "module": "module5",
+ "event": "event5"
+ }
+ },
+ {
+ // test log level
+ testData: {
+ func: "log",
+ args: ["{that}", "module14", "event14", {"data0": "value0"}, fluid.logLevel.WARN]
+ },
+ expected: {
+ "level": "WARN",
+ "module": "module14",
+ "event": "event14",
+ "data": {"data0": "value0"}
+ }
+ },
+ {
+ // test log level (dropped)
+ testData: {
+ func: "log",
+ args: ["{that}", "module14b", "event14b", {"data0": "value0"}, fluid.logLevel.TRACE]
+ },
+ expected: null
+ },
+ {
+ // logError
+ testData: {
+ func: "logError",
+ args: ["{that}", "module15", "event15", {"error15": "value15"}]
+ },
+ expected: {
+ "level": "FAIL",
+ "module": "module15",
+ "event": "Error.event15",
+ "data": {
+ "error": {"error15": "value15", "stack": ""}
+ }
+ }
+ },
+ // Duration events
+ {
+ // start
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1", {"value": "yes"}]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO",
+ "data": {
+ "value": "yes"
+ }
+ }
+ },
+ {
+ // end with data
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1", {"value": "yes"}]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "value": "yes",
+ "duration": 2
+ }
+ }
+ },
+ {
+ // start with no data
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO"
+ }
+ },
+ {
+ // end with no data
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ // multiple pairs stacked
+ {
+ // start 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO"
+ }
+ },
+ {
+ // start 2
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start2" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start2",
+ "level": "INFO"
+ }
+ },
+ {
+ // end 2
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end2" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end2",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ {
+ // end 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ // multiple pairs intersected
+ {
+ // start 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO"
+ }
+ },
+ {
+ // start 2
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start2" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start2",
+ "level": "INFO"
+ }
+ },
+ {
+ // end 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ {
+ // end 2
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end2" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end2",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ // same pairs stacked
+ {
+ // start 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO"
+ }
+ },
+ {
+ // start 1 (again)
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start1",
+ "level": "INFO"
+ }
+ },
+ {
+ // end 1
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ {
+ // end 1 (again)
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end1" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end1",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ // End without start
+ {
+ // end 3 (no start)
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end3" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end3",
+ "level": "INFO"
+ }
+ },
+ {
+ // start 3 (prove the test previous is correct)
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start3" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start3",
+ "level": "INFO"
+ }
+ },
+ {
+ // end 3
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end3" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end3",
+ "level": "INFO",
+ "data": {
+ "duration": 2
+ }
+ }
+ },
+ // Already has duration data
+ {
+ // start 4
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-start3" ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-start3",
+ "level": "INFO"
+ }
+ },
+ {
+ // end 3
+ testData: {
+ func: "log",
+ args: ["{that}", "module", "test-end3", {"duration": "hello"} ]
+ },
+ expected: {
+ "module": "module",
+ "event": "test-end3",
+ "level": "INFO",
+ "data": {
+ "duration": "hello",
+ "duration_auto": 2
+ }
+ }
+ }
+], function (item) {
+ fluid.each(fluid.makeArray(item.expected), function (expected) {
+ expected.timestamp = gpii.tests.eventLog.timestamp;
+ expected.installID = gpii.tests.eventLog.installID;
+ expected.version = gpii.tests.eventLog.version;
+ expected.sessionID = gpii.tests.eventLog.sessionID;
+ });
+ return item;
+}));
+
+/**
+ * Sets up the logging so it's predictable and can be examined easily.
+ *
+ * @return {String} The log file.
+ */
+gpii.tests.eventLog.prepareLogFile = function () {
+ var logFile = os.tmpdir() + "/gpii-test-eventLog-" + Math.random().toString(36);
+ teardowns.push(function () {
+ fs.unlinkSync(logFile);
+ });
+
+ // Mock getTimestamp function
+ var oldGetTimestamp = gpii.eventLog.getTimestamp;
+ teardowns.push(function () {
+ // Restore getTimestamp.
+ gpii.eventLog.getTimestamp = oldGetTimestamp;
+ });
+ gpii.eventLog.getTimestamp = function () {
+ return gpii.tests.eventLog.timestamp;
+ };
+
+ return logFile;
+};
+
+/**
+ * Checks a log line.
+ *
+ * @param {String} line - The log lone.
+ * @param {Object} expected - What it should parse into.
+ */
+gpii.tests.eventLog.checkLogLine = function (line, expected) {
+ jqUnit.expect(5);
+ jqUnit.assertTrue("Line should look like a JSON object", !!line.match(/^{.*}$/));
+ var obj = JSON.parse(line);
+ jqUnit.assertEquals("JSON should be an object", "object", typeof(obj));
+
+ if (obj.data) {
+ // The stack property of an error is unpredictable. If it's found, just make sure it's a string.
+ if (obj.data.error && obj.data.error.stack) {
+ jqUnit.expect(1);
+ jqUnit.assertEquals("data.error.stack property should a string", "string", typeof(obj.data.error.stack));
+ obj.data.error.stack = "";
+ }
+ }
+
+ // The sequence is unpredictable - just make sure it exists, and is larger than the last one.
+ var seq = obj.sequence || null;
+ jqUnit.assertFalse("Log line must have a numeric sequence field.", isNaN(seq));
+
+ var prev = gpii.tests.eventLog.checkLogLine.lastSequence || 0;
+ jqUnit.assertTrue("log sequence should be greater than the previous", seq > prev);
+ gpii.tests.eventLog.checkLogLine.lastSequence = seq;
+ delete obj.sequence;
+
+
+ jqUnit.assertDeepEq("Parsed log should match expected", expected, obj);
+};
+
+/**
+ * Checks an entire log file.
+ *
+ * @param {String} logFile - The log file.
+ * @param {Object[]} expected - Array of objects expected for each line.
+ * @return {Promise} A promise that resolves when the file is read.
+ */
+gpii.tests.eventLog.checkLogFile = function (logFile, expected) {
+ var promise = fluid.promise();
+ var reader = readline.createInterface({
+ input: fs.createReadStream(logFile)
+ });
+
+ // Inspect each line of the log, making sure each one parses into the expected object.
+ var lineNumber = 0;
+ reader.on("line", function (line) {
+ console.log("Log line", lineNumber, line);
+
+ if (lineNumber >= expected.length) {
+ jqUnit.fail("Log file should not contain more data.");
+ promise.reject();
+ reader.close();
+ return;
+ }
+ gpii.tests.eventLog.checkLogLine(line, expected[lineNumber]);
+ lineNumber++;
+ });
+
+ reader.on("close", function () {
+ if (!promise.disposition) {
+ promise.resolve();
+ }
+ });
+
+ return promise;
+};
+
+jqUnit.asyncTest("Uncaught exception test", function () {
+ jqUnit.expect(1);
+
+ var logFile = gpii.tests.eventLog.prepareLogFile();
+ gpii.tests.eventLog.createEventLogComponent(logFile);
+ // Clear the log file
+ try {
+ fs.unlinkSync(logFile);
+ } catch (e) {
+ // ignore
+ }
+
+ jqUnit.assertNotUndefined("Uncaught exception listener added",
+ fluid.onUncaughtException.listeners["gpii-eventLog"]);
+
+ var errorText = "eventLog uncaught exception logging test";
+ var expected = [
+ {
+ "installID": gpii.tests.eventLog.installID,
+ "timestamp": gpii.tests.eventLog.timestamp,
+ "version": gpii.tests.eventLog.version,
+ "sessionID": gpii.tests.eventLog.sessionID,
+ "level": "FAIL",
+ "module": "GPII",
+ "event": "Error.Exception",
+ "data": {
+ "error": {
+ "testError": true,
+ "message": errorText,
+ "stack": ""
+ }
+ }
+ }
+ ];
+
+ // Disable jqUnit and fluid's handlers, to allow the test to continue when the exception is thrown.
+ fluid.failureEvent.addListener(fluid.identity, "jqUnit", "before:fail");
+ fluid.onUncaughtException.addListener(fluid.identity, "fail", "first");
+
+ var promise = fluid.promise();
+ process.nextTick(function () {
+ try {
+ throw {
+ testError: true,
+ message: errorText
+ };
+ } finally {
+ // Resolve after the next tick - so the uncaught exception handler can be called.
+ setTimeout(promise.resolve, 1);
+ }
+ });
+
+ promise.then(function () {
+ fluid.failureEvent.removeListener("jqUnit");
+ fluid.onUncaughtException.removeListener("fail");
+
+ gpii.tests.eventLog.checkLogFile(logFile, expected)
+ .then(jqUnit.start);
+ });
+});
+
+jqUnit.asyncTest("fluid.fail test", function () {
+ var logFile = gpii.tests.eventLog.prepareLogFile();
+ gpii.tests.eventLog.createEventLogComponent(logFile);
+ // Clear the log file
+ fs.unlinkSync(logFile);
+
+ var errorText = "eventLog failure logging test";
+ var expected = [
+ {
+ "installID": gpii.tests.eventLog.installID,
+ "timestamp": gpii.tests.eventLog.timestamp,
+ "version": gpii.tests.eventLog.version,
+ "sessionID": gpii.tests.eventLog.sessionID,
+ "level": "FAIL",
+ "module": "GPII",
+ "event": "Error.Fail",
+ "data": {
+ "error": {
+ "message": errorText,
+ "stack": ""
+ }
+ }
+ }
+ ];
+
+ // Test fluid.fail gets logged.
+ jqUnit.expectFrameworkDiagnostic("testing fluid.fail gets logged", function () {
+ fluid.fail(errorText);
+ }, [errorText]);
+
+ gpii.tests.eventLog.checkLogFile(logFile, expected)
+ .then(jqUnit.start);
+});
+
+jqUnit.asyncTest("eventLog tests #1", function () {
+ var tests = gpii.tests.eventLog.testDefs;
+ var logFile = gpii.tests.eventLog.prepareLogFile();
+ var expected = [];
+
+ var that = gpii.tests.eventLog.createEventLogComponent(logFile);
+
+ // mock hrtime so it return something expected
+ var hrtime = process.hrtime;
+ process.hrtime = function (arg) {
+ return arg ? [2, 0] : [3, 0];
+ };
+
+
+ // Log the test data
+ for (var n = 0; n < tests.length; n++) {
+ var test = tests[n];
+ var calls = fluid.makeArray(test.testData);
+ while (calls.length) {
+ var call = calls.shift();
+ var args = fluid.transform(call.args, function (c) {
+ return (c === "{that}") ? that : c;
+ });
+ gpii.eventLog[call.func].apply(null, args);
+ }
+
+ expected.push.apply(expected, fluid.makeArray(test.expected));
+ }
+
+ process.hrtime = hrtime;
+
+ gpii.tests.eventLog.checkLogFile(logFile, expected)
+ .then(jqUnit.start);
+});
+
+/**
+ * Creates a TCP server, calling a callback for every line received.
+ * @param {Number} port The TCP port to listen on.
+ * @param {Function} gotLine The function to call for each line.
+ * @return {net.Server} The TCP server.
+ */
+gpii.tests.eventLog.createLogServer = function (port, gotLine) {
+ var buffer = "";
+ var logServer = net.createServer();
+
+ logServer.listen(port, "127.0.0.1");
+
+ logServer.on("listening", function () {
+ fluid.log("[test] log server listening");
+ });
+ logServer.on("connection", function (socket) {
+ fluid.log("[test] log server connection");
+ socket.setEncoding("utf8");
+
+ socket.on("data", function (chunk) {
+ buffer += chunk;
+ if (buffer.indexOf("\n") >= 0) {
+ var lines = buffer.split("\n");
+ buffer = lines.pop();
+ fluid.each(lines, gotLine);
+ }
+ });
+ });
+ logServer.on("close", function () {
+ });
+ logServer.on("error", function (err) {
+ jqUnit.fail(err);
+ });
+
+ return logServer;
+};
+
+/**
+ * Resolve after a timeout
+ * @param {Number} timeout The millisecond timeout.
+ * @return {Promise} Resolves after the timeout.
+ */
+gpii.tests.eventLog.promiseTimeout = function (timeout) {
+ var promise = fluid.promise();
+ setTimeout(promise.resolve, timeout);
+ return promise;
+};
+
+/**
+ * Tests writing logs to a log server.
+ */
+jqUnit.asyncTest("eventLog tests - writeLogTcp", function () {
+
+ var port = 50000 + Math.floor(Math.random() * 5000);
+ var logFile = "tcp://127.0.0.1:" + port;
+
+ var that = gpii.tests.eventLog.createEventLogComponent(logFile);
+
+ var linesWrote = [];
+ var lineCount = 0;
+ var stopTest = false;
+
+ var firstLine = true;
+ // Check each line as it comes in.
+ var gotLine = function (line) {
+ if (firstLine) {
+ // Ignore the first line - it's the gpii.start event
+ firstLine = false;
+ } else {
+ // All lines written are expected to be received.
+ var expected = linesWrote.shift();
+ jqUnit.assertEquals("Line received must match the one sent in sequence", expected, line);
+ }
+ };
+
+ // Continuously write log lines.
+ var writeLine = function () {
+ if (!stopTest) {
+ var line = "message" + lineCount++;
+ linesWrote.push(line);
+ gpii.eventLog.writeLogTcp(that, line + "\n");
+ setTimeout(writeLine, 100);
+ }
+ };
+
+ // Start writing logs before the server is started. Log lines should be buffered until the connection is made.
+ writeLine();
+
+ // Start the log server, resolving some time after a client connection is received
+ var startServer = function (state) {
+ fluid.log("[test] Starting log server");
+ state.logServer = gpii.tests.eventLog.createLogServer(port, gotLine);
+ var p = fluid.promise();
+ state.logServer.on("connection", function (socket) {
+ state.logServer.close();
+ state.socket = socket;
+ setTimeout(p.resolve, 500);
+ });
+ return p;
+ };
+
+ var actions = [
+ gpii.tests.eventLog.promiseTimeout(500),
+ // Start the log server.
+ startServer,
+ // Stop the log server. Outgoing lines should be buffered until reconnected.
+ function (state) {
+ fluid.log("[test] Stopping log server");
+ state.socket.end();
+ return gpii.tests.eventLog.promiseTimeout(700);
+ },
+ // Restart the log server. eventLog should reconnect.
+ startServer,
+ // End the test
+ function (state) {
+ stopTest = true;
+ state.socket.end();
+ jqUnit.expect(lineCount);
+ return linesWrote.length > 0 && gpii.tests.eventLog.promiseTimeout(500);
+ }
+ ];
+
+ fluid.promise.sequence(actions, {}).then(jqUnit.start);
+});
diff --git a/gpii/node_modules/eventLog/test/all-tests.js b/gpii/node_modules/eventLog/test/all-tests.js
new file mode 100644
index 000000000..8f276e0ce
--- /dev/null
+++ b/gpii/node_modules/eventLog/test/all-tests.js
@@ -0,0 +1,22 @@
+/*
+ * Event log and metrics tests.
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+require("./EventLogTests.js");
+require("./metricsTests.js");
diff --git a/gpii/node_modules/eventLog/test/metricsTests.js b/gpii/node_modules/eventLog/test/metricsTests.js
new file mode 100644
index 000000000..b118d94dd
--- /dev/null
+++ b/gpii/node_modules/eventLog/test/metricsTests.js
@@ -0,0 +1,213 @@
+/*
+ * Metrics Tests
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+
+var jqUnit = fluid.require("node-jqunit");
+var gpii = fluid.registerNamespace("gpii");
+fluid.registerNamespace("gpii.tests.metrics");
+
+require("../../lifecycleManager/");
+
+require("../index.js");
+
+var teardowns = [];
+
+jqUnit.module("gpii.tests.metrics", {
+ teardown: function () {
+ while (teardowns.length) {
+ teardowns.pop()();
+ }
+ }
+});
+
+// Tests ensure preferenceChanged logs added and changed preferences.
+gpii.tests.metrics.preferenceChangedTestData = fluid.freezeRecursive({
+ "unchanged 1/1": {
+ previous: {
+ pref1: "value1"
+ },
+ current: {
+ pref1: "value1"
+ },
+ expect: []
+ },
+ "unchanged 2/2": {
+ previous: {
+ pref1: "value1",
+ pref2: "value2"
+ },
+ current: {
+ pref1: "value1",
+ pref2: "value2"
+ },
+ expect: []
+ },
+ "changed 1/1": {
+ previous: {
+ pref1: "value1"
+ },
+ current: {
+ pref1: "changed value1"
+ },
+ expect: {
+ name: "pref1",
+ newValue: "changed value1"
+ }
+ },
+ "changed 2/2": {
+ previous: {
+ pref1: "value1",
+ pref2: "value2"
+ },
+ current: {
+ pref1: "changed value1",
+ pref2: "changed value2"
+ },
+ expect: [{
+ name: "pref1",
+ newValue: "changed value1"
+ }, {
+ name: "pref2",
+ newValue: "changed value2"
+ }]
+ },
+ "changed 2/3": {
+ previous: {
+ pref1: "value1",
+ pref2: "value2",
+ pref3: "value3"
+ },
+ current: {
+ pref1: "changed value1",
+ pref2: "changed value2",
+ pref3: "value3"
+ },
+ expect: [{
+ name: "pref1",
+ newValue: "changed value1"
+ }, {
+ name: "pref2",
+ newValue: "changed value2"
+ }]
+ },
+ "add 1+1": {
+ previous: {
+ pref1: "value1"
+ },
+ current: {
+ pref1: "value1",
+ pref2: "new value2"
+ },
+ expect: [{
+ name: "pref2",
+ newValue: "new value2"
+ }]
+ },
+ "add+change": {
+ previous: {
+ pref1: "value1"
+ },
+ current: {
+ pref1: "changed value1",
+ pref2: "new value2"
+ },
+ expect: [{
+ name: "pref1",
+ newValue: "changed value1"
+ }, {
+ name: "pref2",
+ newValue: "new value2"
+ }]
+ },
+ "remove 1-1": {
+ previous: {
+ pref1: "value1"
+ },
+ current: {
+ },
+ expect: []
+ },
+ "remove 2-1": {
+ previous: {
+ pref1: "value1",
+ pref2: "value2"
+ },
+ current: {
+ pref2: "value2"
+ },
+ expect: []
+ },
+ "remove+change+static+add": {
+ previous: {
+ pref1: "value1",
+ pref2: "value2",
+ pref3: "value3"
+ },
+ current: {
+ pref2: "changed value2",
+ pref3: "value3",
+ pref4: "new value4"
+ },
+ expect: [{
+ name: "pref2",
+ newValue: "changed value2"
+ }, {
+ name: "pref4",
+ newValue: "new value4"
+ }]
+ }
+});
+
+fluid.defaults("gpii.tests.metricsWrapper", {
+ gradeNames: ["fluid.component", "gpii.metrics", "gpii.eventLog", "gpii.lifecycleManager"],
+ listeners: {
+ "onStartMetrics.application": null,
+ "onStartMetrics.input": null,
+ "onStopMetrics.application": null,
+ "onStopMetrics.input": null
+ }
+});
+
+
+jqUnit.test("preferenceChanged", function () {
+
+ var tests = gpii.tests.metrics.preferenceChangedTestData;
+
+ var logValues;
+
+ var metrics = gpii.tests.metricsWrapper({
+ invokers: {
+ "logMetric": function (metric, data) {
+ logValues.push(data);
+ }
+ }
+ });
+
+ fluid.each(tests, function (test) {
+ logValues = [];
+
+ gpii.metrics.preferenceChanged(metrics, test.current, test.previous);
+
+ jqUnit.assertDeepEq("preferenceChanged should have logged the expected value.",
+ fluid.makeArray(test.expect), logValues);
+ });
+
+});
diff --git a/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5 b/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5
new file mode 100644
index 000000000..753a12200
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5
@@ -0,0 +1,26 @@
+/**
+ * This configuration runs the flat MatchMaker in development mode
+ */
+{
+ "type": "gpii.flatMatchMaker.config.base",
+ "options": {
+ "gradeNames": ["fluid.component"],
+ "distributeOptions": {
+ "flatMatchMaker.ontologyHandler": {
+ "record": "{ontologyHandler}",
+ "target": "{that gpii.flatMatchMaker}.options.components.ontologyHandler"
+ }
+ },
+ "components": {
+ "server": {
+ "options": {
+ "components": {
+ "matchMaker": {
+ "type": "gpii.flatMatchMaker"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/gpii/node_modules/flatMatchMaker/index.js b/gpii/node_modules/flatMatchMaker/index.js
new file mode 100644
index 000000000..adf92560a
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/index.js
@@ -0,0 +1,7 @@
+"use strict";
+
+var fluid = require("infusion");
+
+fluid.module.register("flatMatchMaker", __dirname, require);
+
+require("./src/FlatMatchMaker.js");
diff --git a/gpii/node_modules/flatMatchMaker/package.json b/gpii/node_modules/flatMatchMaker/package.json
new file mode 100644
index 000000000..3d750950a
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "flatMatchMaker",
+ "description": "The Flat MatchMaker is a simple lightweight Matchmaker",
+ "version": "0.3.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js b/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js
new file mode 100644
index 000000000..0645d5120
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js
@@ -0,0 +1,136 @@
+/*
+* Flat MatchMaker
+*
+* Copyright 2014 Raising the Floor - International
+* Copyright 2018 OCAD University
+*
+* Licensed under the New BSD license. You may not use this file except in
+* compliance with this License.
+*
+* The research leading to these results has received funding from the European Union's
+* Seventh Framework Programme (FP7/2007-2013)
+* under grant agreement no. 289016.
+*
+* You may obtain a copy of the License at
+* https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+/* eslint-env browser */
+/* eslint strict: ["error", "function"] */
+
+(function () {
+ "use strict";
+
+ var fluid = fluid || require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+ require("kettle");
+
+ fluid.defaults("gpii.flatMatchMaker", {
+ gradeNames: ["fluid.component"],
+ components: {
+ ontologyHandler: {
+ "type": "gpii.ontologyHandler"
+ }
+ },
+ invokers: {
+ match: {
+ funcName: "gpii.flatMatchMaker.match",
+ args: [ "{ontologyHandler}", "{arguments}.0", "{flatMatchMaker}.disposeStrategy"]
+ },
+ disposeStrategy: {
+ funcName: "gpii.flatMatchMaker.disposeStrategy",
+ args: [ "{arguments}.0", "{arguments}.1", "{arguments}.2"]
+ // leaves, solrecs, solutionTypeMapping
+ }
+ }
+ });
+
+ /*
+ * Main function of the flat matchmaker. Ensures that the inferred common terms are taken
+ * into account and that the preferences are stored in a hierarchical format, needed by
+ * the flat MM, and then runs the framework's 'disposeSolutions' function with the
+ * flatMatchMaker.disposeStrategy strategy. The correct output format is ensured by the
+ * buildReturnPayload function of the MM framework.
+ */
+ gpii.flatMatchMaker.match = function (ontologyHandler, payload, disposeStrategy) {
+ // augment payload with information about the solution types
+ var appTransformSpec = gpii.ontologyHandler.getTransformSpec(ontologyHandler.ontologyTransformSpecs, "flat", "apptology");
+ gpii.matchMakerFramework.utils.addSolutionTypeInformation(payload, appTransformSpec);
+
+ payload.hierarchicalPrefs = ontologyHandler.prefsToOntology(payload.preferences, "flat", "ISO24751");
+
+ var transformSpec = gpii.ontologyHandler.getTransformSpec(ontologyHandler.ontologyTransformSpecs, "flat", "ISO24751");
+ var disposed = gpii.matchMakerFramework.utils.disposeSolutions(payload, disposeStrategy, transformSpec);
+ return gpii.matchMakerFramework.utils.buildReturnPayload(payload, disposed);
+ };
+
+ /*
+ * Very simple strategy that accepts any solution for which any preference el-path matches
+ * a one of the solutions capabilities (inferred from the capabilities transformations and
+ * capabilities block of that solution).
+ *
+ * We consider that any accepted solution will be activated because of the simple-minded
+ * policy adopted by the flat matchmaker
+ */
+ gpii.flatMatchMaker.disposeStrategy = function (leaves, solrecs, solutionTypeMapping) {
+ fluid.each(solrecs, function (solrec) {
+ var accepted = fluid.find(leaves, function (leaf) {
+ return fluid.get(solrec.skeleton, leaf, fluid.model.escapedGetConfig);
+ });
+ if (solrec.priority !== undefined) {
+ accepted = true;
+ }
+ fluid.extend(solrec, {
+ disposition: accepted ? "accept" : "reject",
+ active: accepted ? true : false // always true if accepted
+ });
+ });
+
+ // Apply Apptology
+ fluid.each(solutionTypeMapping, function (solutions) {
+ var numOfSolutions = fluid.keys(solutions).length;
+ // If there's only one solution for a solution type, the filtering by apptology is not needed.
+ if (numOfSolutions > 1) {
+ gpii.flatMatchMaker.applyApptology(solrecs, solutions);
+ }
+ });
+
+ return solrecs;
+ };
+
+ // Apply "Apptology" to only select one solution from all that perform the same functionality. For example, only one
+ // screen reader will be selected when there are multiple matched screen readers.
+ gpii.flatMatchMaker.applyApptology = function (solrecs, solutions) {
+ var bestSolutionId, highestPriority;
+
+ // Find the best solution:
+ // 1. the one with the highest priority value;
+ // 2. if all solutions have the same priority value, select the first one.
+ fluid.each(solutions, function (value, solutionId) {
+ var thisPriority = solrecs[solutionId].priority ? solrecs[solutionId].priority : 0;
+
+ if (solrecs[solutionId].active) {
+ // Select the first active solution to be the best match in case all solutions have the same priority.
+ if (!bestSolutionId) {
+ bestSolutionId = solutionId;
+ highestPriority = thisPriority;
+ }
+ // find the solution with the highest priority
+ if (thisPriority > highestPriority) {
+ bestSolutionId = solutionId;
+ highestPriority = thisPriority;
+ }
+ }
+ });
+
+ // Reject all non-best solutions
+ fluid.each(solutions, function (value, solutionId) {
+ // Reject solutions from 2nd place onwards
+ if (solrecs[solutionId].active && solutionId !== bestSolutionId) {
+ fluid.set(solrecs, [solutionId, "disposition"], "reject");
+ fluid.set(solrecs, [solutionId, "active"], false);
+ }
+ });
+ };
+})();
diff --git a/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js b/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js
new file mode 100644
index 000000000..5f6bb4fd4
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js
@@ -0,0 +1,412 @@
+/*
+* Flat MatchMaker Tests
+*
+* Copyright 2014 Raising the Floor - International
+*
+* Licensed under the New BSD license. You may not use this file except in
+* compliance with this License.
+*
+* The research leading to these results has received funding from the European Union's
+* Seventh Framework Programme (FP7/2007-2013)
+* under grant agreement no. 289016.
+*
+* You may obtain a copy of the License at
+* https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+/* eslint strict: ["error", "function"] */
+
+// TODO: This should be converted to a set of browser tests
+
+(function () {
+ "use strict";
+
+ var fluid = require("infusion"),
+ jqUnit = fluid.registerNamespace("jqUnit"),
+ kettle = require("kettle"),
+ gpii = fluid.registerNamespace("gpii");
+
+ kettle.loadTestingSupport();
+
+ require("flatMatchMaker");
+ require("ontologyHandler"); // TODO: We don't require to depend on the entire ontologyHandler
+ require("matchMakerFramework");
+
+ fluid.defaults("gpii.flatMatchMaker.tests", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ components: {
+ tester: {
+ type: "gpii.flatMatchMaker.tests.testCaseHolder"
+ }
+ }
+ });
+
+ var disposeStrategyFixtures = [
+ {
+ description: "Basic test for disposeStrategy function",
+ leavesArg: [
+ "display.screenEnhancement.fontSize",
+ "display.screenEnhancement.magnification",
+ "display.screenEnhancement.-provisional-magnification/enabled",
+ "display.screenReader.-provisional-screenReaderTTS/enabled"
+ ],
+ solRecsArg: {
+ "test.dummy.matched1": {
+ "solution": {
+ "name": "Test dummy - matched1",
+ "contexts": {},
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler.set",
+ "options": {
+ "path": "/some/path/to/file.json"
+ },
+ "capabilitiesTransformations": {
+ "setting1": "http://registry\\.gpii\\.net/common/fontSize",
+ "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled",
+ "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification"
+ }
+ }
+ }
+ },
+ "skeleton": {
+ "applications": {
+ "test.dummy.matched1": {
+ "id": {}
+ }
+ },
+ "display": {
+ "screenEnhancement": {
+ "fontSize": {},
+ "magnification": {},
+ "-provisional-magnification/enabled": {}
+ },
+ "screenReader": {
+ "-provisional-screenReaderTTS/enabled": {}
+ }
+ }
+ },
+ "index": "test.dummy.matched1"
+ },
+ "test.dummy.unmatched": {
+ "solution": {
+ "name": "Test dummy - unmatched",
+ "contexts": {},
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.noSettings"
+ }
+ }
+ },
+ "skeleton": {
+ "applications": {
+ "test.dummy.unmatched": {
+ "id": {}
+ }
+ }
+ },
+ "index": "test.dummy.unmatched"
+ }
+ },
+ solutionTypeMapping: {},
+ expect: {
+ "test.dummy.matched1": {
+ disposition: "accept",
+ active: true
+ },
+ "test.dummy.unmatched": {
+ disposition: "reject",
+ active: false
+ }
+ }
+ },
+ {
+ description: "Test for disposeStrategy function with the solution type mapping",
+ leavesArg: [
+ "display.screenEnhancement.fontSize",
+ "display.screenEnhancement.magnification",
+ "display.screenEnhancement.-provisional-magnification/enabled",
+ "display.screenReader.-provisional-screenReaderTTS/enabled"
+ ],
+ solRecsArg: {
+ "test.dummy.matched1": {
+ "solution": {
+ "name": "Test dummy - matched1",
+ "contexts": {},
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler.set",
+ "options": {
+ "path": "/some/path/to/file.json"
+ },
+ "capabilitiesTransformations": {
+ "setting1": "http://registry\\.gpii\\.net/common/fontSize",
+ "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled",
+ "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification"
+ }
+ }
+ }
+ },
+ "skeleton": {
+ "applications": {
+ "test.dummy.matched1": {
+ "id": {}
+ }
+ },
+ "display": {
+ "screenEnhancement": {
+ "fontSize": {},
+ "magnification": {},
+ "-provisional-magnification/enabled": {}
+ },
+ "screenReader": {
+ "-provisional-screenReaderTTS/enabled": {}
+ }
+ }
+ },
+ "index": "test.dummy.matched1"
+ },
+ "test.dummy.matched2": {
+ "solution": {
+ "name": "Test dummy - matched2",
+ "contexts": {},
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler.set",
+ "options": {
+ "path": "/some/path/to/file.json"
+ },
+ "capabilitiesTransformations": {
+ "setting1": "http://registry\\.gpii\\.net/common/fontSize",
+ "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled",
+ "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification"
+ }
+ }
+ }
+ },
+ "skeleton": {
+ "applications": {
+ "test.dummy.matched1": {
+ "id": {}
+ }
+ },
+ "display": {
+ "screenEnhancement": {
+ "fontSize": {},
+ "magnification": {},
+ "-provisional-magnification/enabled": {}
+ },
+ "screenReader": {
+ "-provisional-screenReaderTTS/enabled": {}
+ }
+ }
+ },
+ "index": "test.dummy.matched1"
+ },
+ "test.dummy.unmatched": {
+ "solution": {
+ "name": "Test dummy - unmatched",
+ "contexts": {},
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.noSettings"
+ }
+ }
+ },
+ "skeleton": {
+ "applications": {
+ "test.dummy.unmatched": {
+ "id": {}
+ }
+ }
+ },
+ "index": "test.dummy.unmatched"
+ }
+ },
+ solutionTypeMapping: {
+ "http://registry\\.gpii\\.net/common/fontSize": {
+ "test.dummy.matched1": true,
+ "test.dummy.matched2": true
+ },
+ "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled": {
+ "test.dummy.matched1": true,
+ "test.dummy.matched2": true
+ },
+ "http://registry\\.gpii\\.net/common/magnification/enabled": {
+ "test.dummy.matched1": true,
+ "test.dummy.matched2": true
+ },
+ "http://registry\\.gpii\\.net/common/magnification": {
+ "test.dummy.matched1": true,
+ "test.dummy.matched2": true
+ }
+ },
+ expect: {
+ "test.dummy.matched1": {
+ disposition: "accept",
+ active: true
+ },
+ "test.dummy.matched2": {
+ disposition: "reject",
+ active: false
+ },
+ "test.dummy.unmatched": {
+ disposition: "reject",
+ active: false
+ }
+ }
+ }
+ ];
+
+ gpii.flatMatchMaker.tests.disposeStrategy = function () {
+ fluid.each(disposeStrategyFixtures, function (fixture) {
+ var result = gpii.flatMatchMaker.disposeStrategy(fixture.leavesArg, fixture.solRecsArg, fixture.solutionTypeMapping);
+ var expected = fluid.extend(true, {}, fixture.solRecsArg, fixture.expect);
+
+ jqUnit.assertDeepEq(fixture.description, expected, result);
+ });
+ };
+
+ var matchFixtures = [
+ {
+ description: "Full match call test",
+ input: require("./data/pre_mm_payload1.json"),
+ expect: {
+ "inferredConfiguration": {
+ "gpii-default": {
+ "applications": {
+ "mac.dummy": {
+ "active": true,
+ "settings": {
+ "http://registry.gpii.net/common/fontSize": 16,
+ "http://registry.gpii.net/common/screenReaderTTS/enabled": false,
+ "http://registry.gpii.net/common/magnification/enabled": false,
+ "http://registry.gpii.net/common/magnification": 1
+ }
+ }
+ }
+ }
+ }
+ }
+ }, {
+ description: "GPII-3369: the top level high contrast theme is dispatched to both windows and UIO+",
+ input: require("./data/pre_mm_payload2.json"),
+ expect: {
+ "inferredConfiguration": {
+ "gpii-default": {
+ "applications": {
+ "com.microsoft.windows.highContrast": {
+ "active": true,
+ "settings": {
+ "http://registry.gpii.net/common/highContrast/enabled": true,
+ "http://registry.gpii.net/common/highContrastTheme": "white-black"
+ }
+ },
+ "net.gpii.uioPlus": {
+ "active": true,
+ "settings": {
+ "http://registry.gpii.net/common/highContrast/enabled": true,
+ "http://registry.gpii.net/common/highContrastTheme": "white-black"
+ }
+ }
+ }
+ }
+ }
+ }
+ }, {
+ description: "GPII-3388: Apptology: when there are multiple matched solutions for one solution type, only one is selected",
+ input: require("./data/pre_mm_payload3.json"),
+ expect: {
+ "inferredConfiguration": {
+ "gpii-default": {
+ "applications": {
+ "com.freedomscientific.jaws": {
+ "active": true,
+ "settings": {
+ "http://registry.gpii.net/common/keyEcho": false,
+ "http://registry.gpii.net/common/wordEcho": true,
+ "http://registry.gpii.net/common/announceCapitals": true,
+ "http://registry.gpii.net/common/readingUnit": "word",
+ "http://registry.gpii.net/common/punctuationVerbosity": "all",
+ "http://registry.gpii.net/common/screenReaderTTS/enabled": true,
+ "http://registry.gpii.net/common/speechRate": 400,
+ "http://registry.gpii.net/common/auditoryOutLanguage": "en-BS",
+ "http://registry.gpii.net/common/pitch": 0.1
+ }
+ }
+ }
+ }
+ }
+ }
+ }, {
+ description: "GPII-3388: Apptology: the specified application is selected for an application specific preference",
+ input: require("./data/pre_mm_payload4.json"),
+ expect: {
+ "inferredConfiguration": {
+ "gpii-default": {
+ "applications": {
+ "org.nvda-project": {
+ "active": true,
+ "settings": {
+ "http://registry.gpii.net/applications/org.nvda-project": {
+ "speech.synth": "espeak",
+ "speech.outputDevice": "Microsoft Sound Mapper",
+ "speech.symbolLevel": 300,
+ "speech.espeak.rate": 40,
+ "speech.espeak.voice": "en-us",
+ "speech.espeak.pitch": 60,
+ "speech.espeak.volume": 80,
+ "reviewCursor.followFocus": "False",
+ "reviewCursor.followCaret": "True",
+ "reviewCursor.followMouse": "True",
+ "keyboard.speakTypedWords": "True",
+ "speech.espeak.rateBoost": "True",
+ "keyboard.speakTypedCharacters": "False",
+ "presentation.reportHelpBalloons": "False",
+ "speech.espeak.sayCapForCapitals": "True",
+ "virtualBuffers.autoSayAllOnPageLoad": "False"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ];
+
+ gpii.flatMatchMaker.tests.match = function (ontologyHandler) {
+ fluid.each(matchFixtures, function (fixture) {
+ fixture.input = gpii.matchMakerFramework.utils.preProcess(fixture.input);
+ var result = gpii.flatMatchMaker.match(ontologyHandler, fixture.input, gpii.flatMatchMaker.disposeStrategy);
+ jqUnit.assertDeepEq(fixture.description, fixture.expect, result);
+ });
+ };
+
+ fluid.defaults("gpii.flatMatchMaker.tests.testCaseHolder", {
+ gradeNames: ["fluid.test.testCaseHolder"],
+ components: {
+ "ontologyHandler": {
+ type: "gpii.ontologyHandler"
+ }
+ },
+ modules: [{
+ name: "flatMatchMakerTests",
+ tests: [{
+ expect: 2,
+ name: "gpii.flatMatchMaker.disposeStrategy tests",
+ func: "gpii.flatMatchMaker.tests.disposeStrategy"
+ }, {
+ expect: 4,
+ name: "gpii.flatMatchMaker.match tests",
+ func: "gpii.flatMatchMaker.tests.match",
+ args: "{ontologyHandler}"
+ }]
+ }]
+ });
+
+ module.exports = kettle.test.bootstrap("gpii.flatMatchMaker.tests");
+})();
diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json
new file mode 100644
index 000000000..3d66f6369
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json
@@ -0,0 +1,79 @@
+{
+ "gpiiKey": "mac",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/fontSize": 16,
+ "http://registry.gpii.net/common/screenReaderTTS/enabled": false,
+ "http://registry.gpii.net/common/magnification/enabled": false,
+ "http://registry.gpii.net/common/magnification": 1
+ }
+ }
+ }
+ },
+ "deviceContext": {
+ "solutions": [
+ {
+ "id": "mac.dummy"
+ },
+ {
+ "id": "mac.dummy_empty"
+ }
+ ],
+ "OS": {
+ "id": "darwin",
+ "version": "12.5.0"
+ }
+ },
+ "solutionsRegistryEntries": {
+ "mac.dummy": {
+ "name": "Mac dummy",
+ "contexts": {
+ "OS": [
+ {
+ "id": "darwin"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler.set",
+ "options": {
+ "path": "/path/to/some/file.json"
+ },
+ "capabilitiesTransformations": {
+ "setting1": "http://registry\\.gpii\\.net/common/fontSize",
+ "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled",
+ "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification"
+ }
+ }
+ },
+ "configure": [
+ "settings.myconf"
+ ],
+ "restore": [
+ "settings.myconf"
+ ]
+ },
+ "mac.dummy_empty": {
+ "name": "Mac dummy empty",
+ "contexts": {
+ "OS": [
+ {
+ "id": "darwin"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "otherconf": {
+ "type": "gpii.settingsHandlers.noSettings"
+ }
+ },
+ "start": [],
+ "stop": []
+ }
+ }
+}
diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json
new file mode 100644
index 000000000..e6553194d
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json
@@ -0,0 +1,281 @@
+{
+ "gpiiKey": "highContrast",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/highContrast/enabled": true,
+ "http://registry.gpii.net/common/highContrastTheme": "white-black"
+ }
+ }
+ }
+ },
+ "deviceContext": {
+ "solutions": [
+ {
+ "id": "com.microsoft.windows.highContrast"
+ }, {
+ "id": "net.gpii.uioPlus"
+ }
+ ],
+ "OS": {
+ "id": "win32",
+ "version": "6.1.7601"
+ }
+ },
+ "solutionsRegistryEntries": {
+ "com.microsoft.windows.highContrast": {
+ "name": "Windows High Contrast",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "configure-spi": {
+ "type": "gpii.windows.spiSettingsHandler",
+ "liveness": "live",
+ "options": {
+ "getAction": "SPI_GETHIGHCONTRAST",
+ "setAction": "SPI_SETHIGHCONTRAST",
+ "uiParam": "struct_size",
+ "pvParam": {
+ "type": "struct",
+ "name": "HIGHCONTRAST"
+ },
+ "verifySettings": true
+ },
+ "supportedSettings": {
+ "HighContrastOn": {
+ "schema": {
+ "title": "High Contrast",
+ "description": "Whether to enable/disable High Contrast.",
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "capabilitiesTransformations": {
+ "HighContrastOn": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled",
+ "left": false,
+ "operator": "||",
+ "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "right": false
+ }
+ },
+ "operator": "&&",
+ "right": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": "",
+ "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "operator": "!==",
+ "right": "regular-contrast"
+ }
+ },
+ "outputPath": "value"
+ },
+ "path": {
+ "transform": {
+ "type": "fluid.transforms.literalValue",
+ "input": "pvParam.dwFlags.HCF_HIGHCONTRASTON"
+ }
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/highContrast/enabled": "HighContrastOn.value"
+ }
+ },
+ "configure-registry": {
+ "type": "gpii.windows.registrySettingsHandler",
+ "liveness": "live",
+ "options": {
+ "hKey": "HKEY_CURRENT_USER",
+ "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes",
+ "dataTypes": {
+ "LastHighContrastTheme": "REG_SZ"
+ }
+ },
+ "supportedSettings": {
+ "LastHighContrastTheme": {
+ "schema": {
+ "title": "High Contrast theme",
+ "description": "High Contrast Theme.",
+ "type": "string",
+ "default": "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme",
+ "enum": [
+ "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme",
+ "%SystemRoot%\\resources\\Ease of Access Themes\\hcblack.theme",
+ "%SystemRoot%\\resources\\Ease of Access Themes\\hc1.theme"]
+ }
+ }
+ },
+ "capabilitiesTransformations": {
+ "LastHighContrastTheme": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "match": {
+ "black-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcwhite.theme",
+ "white-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcblack.theme",
+ "yellow-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc1.theme",
+ "black-yellow": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\yellowOnBlack.theme",
+ "lime-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc2.theme",
+ "black-brown": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\blackOnBrown.theme",
+ "gray-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnDark.theme",
+ "grey-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnDark.theme",
+ "gray-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnWhite.theme",
+ "grey-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnWhite.theme"
+ }
+ }
+ }
+ }
+ }
+ },
+ "configure": [
+ "settings.configure-registry",
+ {
+ "type": "gpii.windows.spiSettingsHandler.setHighContrastTheme",
+ "filename": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\LastHighContrastTheme}"
+ },
+ "settings.configure-spi"
+ ],
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ },
+
+ "net.gpii.uioPlus": {
+ "name": "UIO+",
+ "contexts": {
+ "OS": [{
+ "id": "win32",
+ "version": ">=5.0"
+ }]
+ },
+ "settingsHandlers": {
+ "configuration": {
+ "type": "gpii.settingsHandlers.webSockets",
+ "liveness": "live",
+ "options": {
+ "path": "net.gpii.uioPlus"
+ },
+ "supportedSettings": {
+ "lineSpace": {},
+ "fontSize": {},
+ "characterSpace": {},
+ "inputsLargerEnabled": {},
+ "contrastTheme": {},
+ "selfVoicingEnabled": {},
+ "selectionTheme": {},
+ "tableOfContentsEnabled": {},
+ "simplifiedUiEnabled": {},
+ "syllabificationEnabled": {}
+ },
+ "capabilitiesTransformations": {
+ "lineSpace": "http://registry\\.gpii\\.net/common/lineSpace",
+ "fontSize": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "scale": 1,
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/fontSize",
+ "right": 12,
+ "operator": "/"
+ }
+ }
+ }
+ },
+ "characterSpace": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "scale": 1,
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "offset": 1,
+ "inputPath": "http://registry\\.gpii\\.net/common/characterSpace"
+ }
+ }
+ }
+ },
+ "inputsLargerEnabled": "http://registry\\.gpii\\.net/common/inputsLarger/enabled",
+ "contrastTheme": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled",
+ "left": false,
+ "operator": "||",
+ "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "right": false
+ }
+ },
+ "operator": "&&",
+ "right": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": "",
+ "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "operator": "!==",
+ "right": "regular-contrast"
+ }
+ }
+ }
+ },
+ "true": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme",
+ "defaultOutputValue": "default",
+ "match": {
+ "black-white": "bw",
+ "white-black": "wb",
+ "black-yellow": "by",
+ "yellow-black": "yb",
+ "gray-dark": "gd",
+ "gray-white": "gw",
+ "black-brown": "bbr"
+ }
+ }
+ },
+ "false": "default"
+ }
+ },
+ "selfVoicingEnabled": "http://registry\\.gpii\\.net/common/selfVoicing/enabled",
+ "selectionTheme": "http://registry\\.gpii\\.net/common/highlightColor",
+ "tableOfContentsEnabled": "http://registry\\.gpii\\.net/common/tableOfContents",
+ "simplifiedUiEnabled": "http://registry\\.gpii\\.net/common/simplifiedUi/enabled",
+ "syllabificationEnabled": "http://registry\\.gpii\\.net/common/syllabification/enabled"
+ },
+ "inverseCapabilitiesTransformations": {}
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ }
+ }
+}
diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json
new file mode 100644
index 000000000..ec7b46efe
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json
@@ -0,0 +1,3686 @@
+{
+ "gpiiKey": "screenreader",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/speakTutorialMessages": false ,
+ "http://registry.gpii.net/common/keyEcho": false ,
+ "http://registry.gpii.net/common/wordEcho": true ,
+ "http://registry.gpii.net/common/announceCapitals": true ,
+ "http://registry.gpii.net/common/readingUnit": "word" ,
+ "http://registry.gpii.net/common/punctuationVerbosity": "all" ,
+ "http://registry.gpii.net/common/screenReaderTTS/enabled": true ,
+ "http://registry.gpii.net/common/trackingTTS": ["mouse", "caret"],
+ "http://registry.gpii.net/common/speechRate": 400 ,
+ "http://registry.gpii.net/common/auditoryOutLanguage": "en-BS",
+ "http://registry.gpii.net/common/pitch": 0.1 ,
+ "http://registry.gpii.net/common/volumeTTS": 0.75
+ }
+ }
+ }
+ },
+ "deviceContext": {
+ "solutions": [
+ {
+ "id": "com.freedomscientific.jaws"
+ }, {
+ "id": "org.nvda-project"
+ }
+ ],
+ "OS": {
+ "id": "win32",
+ "version": "6.1.7601"
+ }
+ },
+ "solutionsRegistryEntries": {
+ "com.freedomscientific.jaws": {
+ "name": "JAWS",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32"
+ }
+ ]
+ },
+ "capabilities": [
+ "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled"
+ ],
+ "settingsHandlers": {
+ "configuration1": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "liveness": "manualRestart",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF"
+ },
+ "supportedSettings": {
+ "Braille.AllDotsBrailleCursor": {
+ "title": "All Dots Braille Cursor",
+ "description": "Whether to display the braille cursor as all dots raised instead of showing it using only dots seven and eight.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "Braille.AttributeRotationDelay": {
+ "schema": {
+ "title": "Attribute Rotation Delay",
+ "description": "When in Attribute Mode and characters have multiple attributes, determines how long each attribute should be shown for. This value is in Milliseconds.",
+ "type": "integer",
+ "default": 1000
+ }
+ },
+ "Braille.AutoPanMode": {
+ "schema": {
+ "title": "Auto Pan Mode",
+ "description": "The \"Auto Pan\" algorithm to use.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 255
+ ],
+ "enumLabels": [
+ "off",
+ "move display minimally to include whole word containing cursor",
+ "match user pan",
+ "move display to show whole word at cursor nearest the middle of the display area",
+ "maximize text after cursor",
+ "maximize text before cursor",
+ "autopan to default structured segment",
+ "Smart Autopan, autopan using the best algorithm for the current situation"
+ ],
+ "default": 255
+ }
+ },
+ "Braille.BrailleAutoRouteToCursor": {
+ "title": "Braille Auto Route To Cursor",
+ "description": "Whether to automatically route the Braille display to the active cursor whenever the active cursor moves or a key on the keyboard is pressed.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "Braille.BrailleCursorBlinkRate": {
+ "title": "Braille Cursor Blink Rate",
+ "description": "Speed, in milliseconds, at which the cursor on the braille display should blink.",
+ "type": "integer",
+ "default": 500
+ },
+ "Braille.BrailleKeyInterruptSpeech": {
+ "title": "Braille Key Interrupt Speech",
+ "description": "Whether to Interrupt speech if a keystroke in the Braille display is pressed.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "Braille.BrailleMessages": {
+ "schema": {
+ "title": "Enable Flash Messages",
+ "description": "Whether or not to show Flash Messages on a braille display.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Braille.BrailleMode": {
+ "schema": {
+ "title": "Braille Mode",
+ "description": "Use this option to control the format of the information sent to the braille display. When Line mode is selected, JAWS sends the line of text at the current cursor position to the braille display. When Structured mode is selected, JAWS sends information to the display that is relevant to the current cursor position. The information sent includes things such as control type, dialog name, or number of items in a list. When Speech Output mode is selected, JAWS sends the same text to the display that it sends to the synthesizer. The default setting for this option is Structured.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "line",
+ "structured",
+ "speech output",
+ "attribute indicators"
+ ]
+ }
+ },
+ "Braille.BrailleMoveActiveCursor": {
+ "title": "Braille Move Active Cursor",
+ "description": "Whether to move the active cursor when the Braille cursor moves.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "Braille.BrailleSleepMode": {
+ "schema": {
+ "title": "Enable Braille Sleep Mode",
+ "description": "Whether to turn off the braille driver, typically used to disable braille for a specific application.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Braille.BrailleVerbosity": {
+ "schema": {
+ "title": "Braille Verbosity",
+ "description": "Sets braille verbosity for Flash messages.",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "beginner",
+ "intermediate",
+ "advanced"
+ ]
+ }
+ },
+ "Braille.ContractedBrailleInput": {
+ "title": "Contracted Braille Input",
+ "description": "Enable contracted braille to be entered on the braille display's keyboard.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "Braille.EightDotBraille": {
+ "schema": {
+ "title": "Braille Dots",
+ "description": "The number of braille dots (six or eight) to use.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "6 dot braille",
+ "8 dot braille"
+ ],
+ "default": 1
+ }
+ },
+ "Braille.FilterControlCharacters": {
+ "title": "Filter Control Characters",
+ "description": "Whether or not to filter certain control characters (currently newlines, linefeeds and tabs)",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "Braille.GeneralizeBullets": {
+ "title": "Generalize Bullets",
+ "description": "Whether to treat all bullets the same.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "Braille.Grade2SuppressCapitalSigns": {
+ "title": "Grade2 Suppress Capital Signs",
+ "description": "When Grade 2 is on, whether or not to show capital indicators",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "Braille.MessagePrefixes": {
+ "schema": {
+ "title": "Enable Flash Message Prefixes",
+ "description": "If a display has no Status Cells, whether or not to prepend the Message Prefix to the Flash Message",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Braille.MessageStatusText": {
+ "title": "Message Status Text",
+ "description": "The text to show in the status area of the display when a Flash message is being shown if the display has no status cells, the text will be prepended to the Flash message.",
+ "type": "string",
+ "default": "msg"
+ },
+ "Braille.MessageTime": {
+ "schema": {
+ "title": "Flash Message Timeout",
+ "description": "When a Flash message is sent to the display via a script, how long in milliseconds should it be shown if not specified by the script.",
+ "type": "integer",
+ "default": 5000
+ }
+ },
+ "Braille.ReversePanningButtons": {
+ "schema": {
+ "title": "Reverse Panning Buttons",
+ "description": "Whether or not panning buttons are reversed.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Braille.StructuredModeReverseOrder": {
+ "schema": {
+ "title": "Reverse Order of Structured Data",
+ "description": "Whether to reverse the order of the structured data to show the focused control's info first, followed by its grouping information, and then the dialog box information. By default, items are read in the opposite order.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Braille.UseHowManyStatusCells": {
+ "title": "Use How Many Status Cells",
+ "description": "How many cells to use for status information if the display defines none of its own.",
+ "type": "integer",
+ "default": 4
+ },
+ "Braille.UseScreenModelForBrailleInRichEdits": {
+ "title": "Use Screen Model for Braille in Rich Edits",
+ "description": "Whether to use the screen model when providing field prompts and text in Braille with rich edits.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "Braille.WordWrap": {
+ "schema": {
+ "title": "Enable Word Wrap",
+ "description": "Whether or not to allow words to be split in order to maximize the use of the braille display. Ignored if panning is set to fixed increment",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "FSCasts.EnableNotifications": {
+ "schema": {
+ "title": "FSCast Notifications",
+ "description": "Whether to have JAWS display a dialog when new podcasts from Freedom Scientific are available.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "HTML.Abbreviations": {
+ "schema": {
+ "title": "Abbreviations",
+ "description": "Whether to expand abbreviations. If this is enabled, abbreviations with a title attribute will have their title read instead of the on screen text.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "HTML.AccessKeys": {
+ "schema": {
+ "title": "Speak Access Keys Within Web Page",
+ "description": "Whether to announce HTML element access keys when reading a web page.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "HTML.Acronyms": {
+ "schema": {
+ "title": "Acronyms",
+ "description": "If this is enabled, acronyms with a title attribute will have their title read instead of the on screen text.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "HTML.BlockQuoteIndication": {
+ "title": "Block Quote Indication",
+ "description": "Whether to indicate HTML block quotations.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.EmbeddedActiveXSupport": {
+ "title": "Embedded ActiveX Support",
+ "description": "Whether or not to support embedded ActiveX controls such as Macromedia Flash Movies. If enabled, only objects defined in JActiveX.ini are affected by this setting.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.ExpandAbbreviations": {
+ "schema": {
+ "title": "Expand Abbreviations",
+ "description": "Whether or not to expand abbreviations (using the abbr tag).",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "HTML.ExpandAcronyms": {
+ "schema": {
+ "title": "Expand Acronyms",
+ "description": "Whether or not to expand acronyms (using the acronym tag).",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "HTML.FilterConsecutiveDuplicateLinks": {
+ "schema": {
+ "title": "Filter Consecutive Duplicate Links",
+ "description": "Whether to skip (not announce) consecutive duplicate links.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "HTML.FormFieldPromptOptions": {
+ "schema": {
+ "title": "Form Field Prompt Options",
+ "description": "Which attributes to prefer when describing form fields.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "enumLabels": [
+ "favor label tag",
+ "favor title attribute",
+ "favor alt attribute",
+ "favor longest",
+ "use both label and title if different",
+ "use both label and alt if different"
+ ],
+ "default": 0
+ }
+ },
+ "HTML.FormsModeAutoOff": {
+ "schema": {
+ "title": "Forms Mode Auto Off",
+ "description": "Whether or not Forms Mode should be automatically turned off when the current page is updated.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "Leave Forms Mode on",
+ "Turn Forms Mode off automatically"
+ ],
+ "default": 1
+ }
+ },
+ "HTML.GraphicalLinkLastResort": {
+ "title": "Graphical Link Last Resort",
+ "description": "What to announce when a graphical link contains no title or alt text for its enclosing image and the enclosing anchor has no title.",
+ "enum": [0,1],
+ "enumLabels": "Read the image's src, Read the image's enclosing anchor's href.",
+ "default": 0
+ },
+ "HTML.IdentifyLinkType": {
+ "title": "Identify Link Type",
+ "description": "Whether to distinguish the various types of links e.g. \"FTP link\", \"mailto link\" versus simply announcing \"link\" for all types of links.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.IdentifySamePageLinks": {
+ "title": "Identify Same Page Links",
+ "description": "Whether to identify links that point to other places on the current page by saying \"same page link\".",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.IgnoreInlineFrames": {
+ "schema": {
+ "title": "Ignore Inline Frames",
+ "description": "Whether to ignore inline frames, such as those used for advertising.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "HTML.IncludeGraphics": {
+ "schema": {
+ "title": "Include Graphics",
+ "description": "Whether/when to include graphics in web pages.",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "never include graphics",
+ "include graphics that have labels, i.e. alt=attributes",
+ "include all graphics"
+ ],
+ "default": 1
+ }
+ },
+ "HTML.IndicateColSpan": {
+ "schema": {
+ "title": "Indicate ColSpan for Braille",
+ "description": "Whether to announce column spans when reading table data in web pages.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+
+ "HTML.IndicateElementAttributes": {
+ "title": "Indicate Element Attributes",
+ "description": "Whether to announce any HTML attributes defined in the \"HTML Attributes Behavior\" map of the current scheme.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.ListIndication": {
+ "title": "List Indication",
+ "description": "Whether to announce lists with the item count and nesting level.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.MaxLineLength": {
+ "title": "Max Line Length",
+ "description": "The maximum number of characters which can appear on a line of a paragraph.",
+ "type": "integer",
+ "default": 150
+ },
+ "HTML.PageRefreshFilter": {
+ "title": "Page Refresh Filter",
+ "description": "How often, in milliseconds, to allow all refreshes. Set to 0 to allow all refreshes without any delay.",
+ "type": "integer",
+ "default": 0
+ },
+ "HTML.SayAllOnDocumentLoad": {
+ "schema": {
+ "title": "Read Web Pages Automatically When Loaded",
+ "description": "Whether to automatically read web pages when they're first loaded.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "HTML.ScreenFollowsVCursor": {
+ "title": "Screen Follows Virtual Cursor",
+ "description": "Whether the screen should automatically scroll to display the line on which the Virtual Cursor is positioned.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.SkipPastRepeatedText": {
+ "title": "Skip Past Repeated Text",
+ "description": "Whether to position the Virtual Cursor on the first line which is different than the previous page whenever a new page is displayed.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.SmartNavigation": {
+ "schema": {
+ "title": "Smart Navigation",
+ "description": "Whether to enable \"smart navigation\" when reading web pages.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "HTML.TableDetection": {
+ "title": "Table Detection",
+ "description": "Whether to announce all tables, or only data tables.",
+ "enum": ["0","1"],
+ "enumLabels": "Indicate all tables, Only indicate data tables",
+ "default": "1"
+ },
+ "HTML.TblMaxCellTextLength": {
+ "title": "Table Maximum Cell Text Length",
+ "description": "A cell is considered a valid data cell if it contains upto this number of characters",
+ "type": "integer",
+ "default": 250
+ },
+ "HTML.TblMinCellTextLength": {
+ "title": "Table Minimum Cell Text Length",
+ "description": "A cell is considered a valid data cell if it contains at least this number of characters",
+ "type": "integer",
+ "default": 1
+ },
+ "HTML.TblMinTextColumns": {
+ "title": "Table Minimum Text Columns",
+ "description": "This option specifies the minimum number of columns which must contain text in order for the table to be considered a data table.",
+ "type": "integer",
+ "default": 2
+ },
+ "HTML.TblMinTextRows": {
+ "title": "Table Minimum Text Rows",
+ "description": "This option specifies the minimum number of rows which must contain text in order for the table to be considered a data table.",
+ "type": "integer",
+ "default": 2
+ },
+ "HTML.TblMinValidDataRows": {
+ "title": "Table Minimum Valid Data Rows",
+ "description": "Next options used for configuring how data tables are detected This option specifies the minimum number of valid data rows that a table must contain in order for it to be considered a data table.",
+ "type": "integer",
+ "default": 2
+ },
+ "HTML.TblValidRowThreshold": {
+ "title": "Table Valid Row Threshold",
+ "description": "This option specifies the minimum number of cells that a row must contain in order for it to be considered a valid data row.",
+ "type": "integer",
+ "default": 2
+ },
+ "HTML.TextBlockLength": {
+ "schema": {
+ "title": "Text Block Length",
+ "description": "The number of consecutive characters that must appear in a web page for JAWS to treat this as a block of text.",
+ "type": "integer",
+ "default": 25
+ }
+ },
+ "HTML.UseLegacyIESupport": {
+ "title": "Use Legacy IE Support",
+ "description": "Whether or not to use legacy IE support instead of the default FS Dom Server support.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "HTML.WrapNavigation": {
+ "title": "Wrap Navigation",
+ "description": "Whether or not to allow navigation keystrokes such as tab and shift tab to wrap to the top or bottom of the document.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "Options.AllCapsIndicator": {
+ "schema": {
+ "title": "AllCapsIndicator",
+ "description": "The text to read before text that appears in all capital letters.",
+ "type": "string",
+ "default": "all cap"
+ }
+ },
+ "Options.AllowMouseEchoWhenMuted": {
+ "schema": {
+ "title": "Allow Mouse Echo",
+ "description": "Whether to echo mouse movement when otherwise muted.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.AllowSpeechOnDemandWhenMuted": {
+ "schema": {
+ "title": "Allow Reading Commands",
+ "description": "Whether to allow speech on demand when otherwise muted.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.AllowTypingEchoWhenMuted": {
+ "schema": {
+ "title": "Allow Typing Echo",
+ "description": "Whether to echo typed keys when otherwise muted.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.CapIndicator": {
+ "schema": {
+ "title": "Cap Indicator",
+ "description": "The text to read before a capital letter.",
+ "type": "string",
+ "default": "cap"
+ }
+ },
+ "Options.CaretBlinkRate": {
+ "schema": {
+ "title": "Caret Blink Rate",
+ "description": "How fast the caret blinks, in milliseconds.",
+ "type": "integer",
+ "default": 53,
+ "minimum": 1
+ }
+ },
+ "Options.CaretDetect": {
+ "schema": {
+ "title": "Caret Detect",
+ "description": "How many cursor blinks are used to locate the caret.",
+ "type": "integer",
+ "default": 1,
+ "minimum": 1
+ }
+ },
+ "Options.CaretDetectTimeOut": {
+ "schema": {
+ "title": "Caret Detect Timeout",
+ "description": "The number of milliseconds before JAWS for Windows will give up looking for the caret",
+ "type": "integer",
+ "default": 250,
+ "minimum": 0
+ }
+ },
+ "OSM.CaretTimeOut": {
+ "title": "Caret Time Out",
+ "description": "The time in milliseconds to wait before redrawing the caret.",
+ "type": "integer",
+ "default": 1000
+ },
+ "Options.Case": {
+ "schema": {
+ "title": "Case",
+ "description": "Whether to indicate the case of spoken text with a change of inflection.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.CustomPageSummary": {
+ "schema": {
+ "title": "Custom Page Summary",
+ "description": "What to do when a virtual document loads for which a custom page summary has been defined.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "do nothing (\"Say All\" will be started as normal)",
+ "The summary will be spoken and focus left on the page at the place defined by the page",
+ "the summary will be presented in the virtual viewer for the user to immediately read"
+ ]
+ }
+ },
+ "Options.DetectKeyboardInputLanguage": {
+ "schema": {
+ "title": "Detect Keyboard Languages",
+ "description": "Whether to detect the language used by the keyboard.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.Dictionary": {
+ "schema": {
+ "title": "Dictionary",
+ "description": "Whether words, phrases, abbreviations, or symbols should be processed through a global or application specific dictionary to determine proper pronunciation. This is enabled by default.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.Filter": {
+ "schema": {
+ "title": "Filter",
+ "description": "Determines how repeated characters are handled.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "enumLabels": [
+ "Say First 3 Repeated Characters",
+ "Say First 4 Repeated Characters",
+ "Say First 5 Repeated Characters",
+ "Say First 6 Repeated Characters",
+ "Say All Repeated Characters",
+ "Count Repeated Characters"
+ ]
+ }
+ },
+ "Options.GeneralizeDialect": {
+ "schema": {
+ "title": "Generalize Dialect",
+ "description": "Whether to switch languages when encountering content with the same underlying base language.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.Indentation": {
+ "schema": {
+ "title": "Indentation",
+ "description": "Whether or not to announce indentation.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.IndicateAttributesInDialogsAndMenus": {
+ "schema": {
+ "title": "Indicate Attributes on Dialogs and Menus",
+ "description": "Whether to announce attributes in dialogs and menus if the scheme allows for it.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.IndicateCaps": {
+ "schema": {
+ "title": "Indicate Caps",
+ "description": "When to indicate the presence of capital letters.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "Off",
+ "indicate caps when spelling or when navigating by character",
+ "also indicate caps when reading by words",
+ "also indicate caps when reading by lines"
+ ]
+ }
+ },
+ "Options.IndicateNewlinesAndParagraphs": {
+ "schema": {
+ "title": "Indicate Newlines and Paragraphs",
+ "description": "Whether/how to indicate the presence of new lines and paragraphs.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "enumLabels": [
+ "off",
+ "indicate when typing and the editor or wordprocessor wraps to a new line",
+ "indicate when arrowing left/right or using left/right with modifiers",
+ "indicate when typing and also when arrowing. the method of indication is controlled by the Speech and Sounds scheme in use",
+ "indicate when reading text containing newline characters bitwise or the values together to indicate in multiple contexts eg a value of"
+ ]
+ }
+ },
+ "Options.IndicateSelected": {
+ "schema": {
+ "title": "List Item",
+ "description": "Use this list to determine how JAWS describes list box items. Select \"Say None\" to silence reading of list box descriptions. When you select \"Say Selected\", JAWS only tells you when list box items are selected. When you select the \"Say Not Selected\" item, JAWS only tells you when list box items are not selected. This is the default setting. If you select \"Say Both\", JAWS tells you when list box items are selected, and when they are not selected.",
+ "default": 2,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "None",
+ "Selected",
+ "Not Selected",
+ "Both"
+ ]
+ }
+ },
+ "Options.InitialNumlockState": {
+ "schema": {
+ "title": "Initial State of NumLock Key",
+ "description": "The initial state of the NumLock key.",
+ "type": "number",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "off at startup",
+ "on at startup",
+ "leave unmodified"
+ ],
+ "default": 0
+ }
+ },
+ "Options.InsertKeyMode": {
+ "schema": {
+ "title": "Insert Key Mode",
+ "description": "The operating mode of the insert key.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "JAWS",
+ "Sticky"
+ ]
+ }
+ },
+ "Options.JAWSInsertKey": {
+ "schema": {
+ "title": "JAWS Insert Key",
+ "description": "Whether/which insert keys can be used as a JAWS Insert.",
+ "type": "number",
+ "default": 3,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "none",
+ "Numpad Insert",
+ "Extended Insert",
+ "both Numpad and Extended"
+ ]
+ }
+ },
+ "Options.JAWSPunctuationEnabled": {
+ "schema": {
+ "title": "JAWS Punctuation Enabled",
+ "description": "Which punctuation marks to speak.",
+ "type": "number",
+ "default": 2,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "None",
+ "Some",
+ "Most",
+ "All"
+ ]
+ }
+ },
+ "Options.KeyRepeat": {
+ "schema": {
+ "title": "Key Repeat",
+ "description": "Whether or not to allow repeated keys. Repeated keys are allowed by default.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.LanguageDetection": {
+ "schema": {
+ "title": "Language Detection",
+ "description": "Whether to enable automatic Language detection.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.LeftShiftSkipBack": {
+ "schema": {
+ "title": "Left Shift Skip Back",
+ "description": "How the shift keys should behave in \"Say All\" mode.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "during SayAll, right shift will skip back and left shift will skip forward",
+ "during SayAll, left shift will skip back and right shift will skip forward"
+ ]
+ }
+ },
+ "Options.LessSpeechMode": {
+ "schema": {
+ "title": "Less Speech Options",
+ "description": "How to behave when in \"less speech mode\".",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "Speech On Demand",
+ "Mute Speech"
+ ]
+ }
+ },
+ "Options.LinePauses": {
+ "schema": {
+ "title": "Line Pauses",
+ "description": "Whether to pause at end of a line.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.LowerOtherAppsVolumeWhileJAWSIsRunning": {
+ "schema": {
+ "title": "Lower Audio Volume of Programs while JAWS Speaks",
+ "description": "Whether to lower the volume of programs other than JAWS when speaking.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.MigrationWizardDisplayed": {
+ "schema": {
+ "title": "Migration Wizard Displayed",
+ "description": "Whether or not to display the migration wizard on startup.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.MixedCase": {
+ "schema": {
+ "title": "Mixed Case",
+ "description": "Whether to announce words with embedded capital letters (such as \"MixedCase\") as separate words.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.MouseEchoSpeaksControlTypeAndState": {
+ "schema": {
+ "title": "Speak Control Type and State of Item",
+ "description": "Whether to speak the type of control and state of item when mousing over a control.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.MouseEchoSpeaksHelpAndDescription": {
+ "schema": {
+ "title": "Speak Description of Item",
+ "description": "Whether to speak the description of an item on mouseover.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.MouseMovementStopsSpeech": {
+ "schema": {
+ "title": "Mouse Echo Interrupt",
+ "description": "Whether to stop speaking when the mouse is moved.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.MouseSpeechDelay": {
+ "schema": {
+ "title": "Mouse Echo Delay",
+ "description": "How long to delay announcing content under the mouse, in milliseconds. Defaults to 0 (no delay).",
+ "type": "integer",
+ "default": 0
+ }
+ },
+ "Options.MouseSpeechEchoUnit": {
+ "schema": {
+ "title": "Mouse Echo Unit",
+ "description": "The unit of text to announce when the mouse pauses over text in a control. Defaults to reading the whole line.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "character",
+ "word",
+ "line",
+ "paragraph"
+ ],
+ "default": 2
+ }
+ },
+ "Options.MouseSpeechEnabled": {
+ "schema": {
+ "title": "Enable Mouse Echo",
+ "description": "Whether or not to enable the mouse echo, i.e. reading content under the mouse pointer.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.Numbers": {
+ "schema": {
+ "title": "Numbers",
+ "description": "How to announce numbers.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "off (handled by synth)",
+ "digits",
+ "pairs",
+ "full numbers"
+ ]
+ }
+ },
+ "Options.OnScreenKeyboard": {
+ "schema": {
+ "title": "Allow On Screen Keyboards",
+ "description": "Whether to allow on screen keyboards.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.ProcessToolTipEvent": {
+ "schema": {
+ "title": "Process Tooltip Event",
+ "description": "Whether or not to process tooltip events.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "on",
+ "off"
+ ]
+ }
+ },
+ "Options.ProgressBarUpdateInterval": {
+ "schema": {
+ "title": "Progress Bar Update Interval",
+ "description": "How often to announce progress bar updates, in milliseconds. Set to 0 to disable update announcements.",
+ "type": "integer",
+ "default": 5000,
+ "minimum": 0
+ }
+ },
+ "Options.QuickKeyNavigationMode": {
+ "schema": {
+ "title": "Quick Key Navigation Mode",
+ "description": "Navigation Quick Keys let you move through Web pages with easy to remember commands, such as T for table, F for form field, N for non link text, and V for visited link. These commands are only available when the Virtual Cursor is active. Use these radio buttons to set Navigation Quick Keys off, on, or on only during Say All reading. The default is on.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "off",
+ "on",
+ "only on during \"Say All\""
+ ]
+ }
+ },
+ "Options.ReadingInterrupt": {
+ "schema": {
+ "title": "Reading Interrupt",
+ "description": "Whether to interrupt speech when a key that is bound to a macro is pressed.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.Repetitions": {
+ "schema": {
+ "title": "Repetitions",
+ "description": "Whether or not to indicate repeated characters.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayAllIgnoreShiftKeys": {
+ "schema": {
+ "title": "\"Say All\" Ignores Shift Keys",
+ "description": "Allows sticky keys to be used, i.e.: ignores shift keys during SayAll. User can use left/right arrow to do same functions.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "on",
+ "off"
+ ]
+ }
+ },
+ "Options.SayAllIndicateCaps": {
+ "schema": {
+ "title": "\"Say All\" Announces Capitals",
+ "description": "In \"Say All\" mode, announce an initial capital letter or capitalized word.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "on",
+ "off"
+ ]
+ }
+ },
+ "Options.SayAllMode": {
+ "schema": {
+ "title": "\"Say All\" Mode",
+ "description": "Sets the amount of text which \"Say All\" sends to the synthesizer as a single unit.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "line",
+ "sentence",
+ "paragraph"
+ ]
+ }
+ },
+ "Options.SayBlankLineCount": {
+ "schema": {
+ "title": "Say Blank Line Count",
+ "description": "In \"Say All\" mode, whether to announce the number of blank lines.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayCursorShapeChange": {
+ "schema": {
+ "title": "Say Cursor Shape Change",
+ "description": "Whether to announce a change in the shape of the cursor.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayDollars": {
+ "schema": {
+ "title": "Say Dollars",
+ "description": "Whether to announce currency symbols.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayNumericDates": {
+ "schema": {
+ "title": "Say Numeric Dates",
+ "description": "How to announce numeric dates. With no translation, dates are read as numbers. With some translation, dd-mm-yy values are read as text. With extended translation, both dd-mm-yy and dd-mm values are read as text.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "no translation",
+ "some translation",
+ "extended translation"
+ ]
+ }
+ },
+ "Options.SaySelectedFirst": {
+ "schema": {
+ "title": "Say Selected First",
+ "description": "Whether to announce the selected text first.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayStateFirst": {
+ "schema": {
+ "title": "Say State First",
+ "description": "Whether to announce the window state before title/text.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SayWindowTypeFirst": {
+ "schema": {
+ "title": "Say Window Type First",
+ "description": "Whether to announce the type of window before announcing the window title/text.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.Scheme": {
+ "schema": {
+ "title": "Scheme",
+ "description": "Whether or not to intercept CreateDIBSection Note that this value is only read once per JAWS session at startup thus changing this value while JAWS is running has no effect.",
+ "type": "string",
+ "default": "Classic"
+ }
+ },
+ "Options.ScreenEcho": {
+ "schema": {
+ "title": "Screen Echo",
+ "description": "Use the radio buttons in this group to determine how much text is read when information on the screen changes. This includes highlighted text, all text as it appears, or no speech echo as text appears or changes on screen. The default setting is Echo Highlighted Text. Changing the Screen Echo setting may affect the reading of menus and other items. For this reason, it is recommended that you only make Screen Echo changes in application-specific settings files.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "off",
+ "highlight",
+ "all"
+ ]
+ }
+ },
+ "Options.SimultaneousSynthAndWave": {
+ "schema": {
+ "title": "Simultaneous Synth and Wave",
+ "description": "Whether to enable smoother playing of WAV files when using the DEC Talk Access 32 software synthesizer. This option is only available in Default.jcf. Changes to this setting take effect the next time you start JAWS. Disabled by default.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SingleDigitThreshold": {
+ "schema": {
+ "title": "Single Digit Threshold",
+ "description": "The number of digits a number must contain before it is read as single digits (like a phone number). Defaults to five.",
+ "type": "integer",
+ "default": 5
+ }
+ },
+ "Options.SkimReadingIndication": {
+ "schema": {
+ "title": "Skim Reading Indication",
+ "description": "Whether or not to indicate (via a beep) when skimming over text units for which the regular expression returns FALSE.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "beep every 20 units skimmed over"
+ ]
+ }
+ },
+ "Options.SkipILM": {
+ "schema": {
+ "title": "Skip ILM",
+ "description": "Whether to show the licensing manager on startup when running in 40-minute evaluation mode.",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "display the license manager on startup",
+ "do not display the license manager on startup"
+ ]
+ }
+ },
+ "Options.SmartWordReading": {
+ "schema": {
+ "title": "Smart Word Reading",
+ "description": "Whether or not to enable \"smart reading\" when using the \"Say Word\" command.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.SpeakANSIChars": {
+ "schema": {
+ "title": "Speak ANSI Chars",
+ "description": "Whether or not to announce ANSI characters.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.SpeakCharacterValueAsMultibyteSequence": {
+ "schema": {
+ "title": "Speak Character Value As Sequence of Multibyte Values",
+ "description": "Whether to read unicode characters as a series of multi-byte values.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.SpeakCharacterValueInHex": {
+ "schema": {
+ "title": "Speak Character Value In Hex",
+ "description": "Whether to announce the hex value of a character when numpad 5 is pressed three times quickly.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.SpeakNumbersSepByDashesAsDigits": {
+ "schema": {
+ "title": "Numbers Containing Dashes as Digits",
+ "description": "Whether to read numbers that contain dashes (such as phone numbers) as a series of digits.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.SpeechHistory": {
+ "schema": {
+ "title": "Enable Speech History",
+ "description": "Whether to read through the 50-item speech history when \"insert+spacebar\" and then \"h\" are pressed.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.SpeechMode": {
+ "schema": {
+ "title": "Speech Mode",
+ "description": "The \"speech mode\" to use, either \"full\" or \"less\".",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "full speech",
+ "less speech"
+ ],
+ "default": 0
+ }
+ },
+ "Options.SpellAlphanumericData": {
+ "schema": {
+ "title": "Spell Alphanumeric Data",
+ "description": "How to read alphanumeric data.",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "read as appears",
+ "spell",
+ "spell phonetically"
+ ],
+ "default": 0
+ }
+ },
+ "Options.SpellPhonetic": {
+ "schema": {
+ "title": "Spell Phonetic",
+ "description": "How to announce word spellings.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "spell normal",
+ "spell phonetic"
+ ]
+ }
+ },
+ "Options.Synthesizer": {
+ "schema": {
+ "title": "Synthesizer",
+ "description": "The synthesizer to use/ Available synthesizers are listed in JfW.INI",
+ "type": "string",
+ "default": "Synth1"
+ }
+ },
+ "Options.SynthesizerResetFrequency": {
+ "schema": {
+ "title": "Synthesizer Reset Frequency",
+ "description": "How often to reset communication with the synthesizer, in milliseconds.",
+ "type": "integer",
+ "default": 2000,
+ "minimum": 0
+ }
+ },
+ "Options.TetherJawsToPC": {
+ "schema": {
+ "title": "Tether Jaws to PC",
+ "description": "Whether or not to tether the JAWS cursor to the PC cursor. If this is set to 'on', whenever the Pc cursor moves, the Jaws cursor will follow it. If this is set to 'off', the JAWS cursor is indifferent to PC cursor movements (this is the default).",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.TextAnalyser": {
+ "schema": {
+ "title": "Text Analyzer",
+ "description": "The type of alert to use when announcing formatting errors.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "turn off",
+ "indicate with sound",
+ "speak count",
+ "describe inconsistencies"
+ ],
+ "default": 0
+ }
+ },
+ "Options.TouchKeyboardChildPanelNotification": {
+ "schema": {
+ "title": "Touch Alternative Character Panel Popup Notification",
+ "description": "How to announce the appearance of a panel of alternate keys in the touch keyboard.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "by both message and sound",
+ "by sound only"
+ ],
+ "default": 0
+ }
+ },
+ "Options.TouchKeyboardNotification": {
+ "schema": {
+ "title": "Touch Keyboard Notification",
+ "description": "Whether/how to notify when the touch keyboard appears/disappears.",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "by message",
+ "by sound",
+ "by both message and sound"
+ ],
+ "default": 2
+ }
+ },
+ "Options.TouchTypingEcho": {
+ "schema": {
+ "title": "Touch Typing Echo",
+ "description": "Whether/how to echo text entered using the touch keyboard.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "none",
+ "characters",
+ "words",
+ "both characters and words"
+ ],
+ "default": 3
+ }
+ },
+ "Options.TouchTypingMode": {
+ "schema": {
+ "title": "Touch Typing Mode",
+ "description": "Which keyboard mode to use for the touch keyboard.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "standard",
+ "touch typing"
+ ],
+ "default": 1
+ }
+ },
+ "Options.TypingEcho": {
+ "schema": {
+ "title": "Typing echo",
+ "description": "How to announce typed words and/or characters.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "enumLabels": [
+ "don't announce characters or words",
+ "announce characters",
+ "announce words",
+ "announce characters and words"
+ ]
+ }
+ },
+ "Options.TypingInterrupt": {
+ "schema": {
+ "title": "Typing Interrupt",
+ "description": "Whether to stop speaking when the user types.",
+ "type": "number",
+ "default": 1,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.UseExtendedKeys": {
+ "schema": {
+ "title": "Use Extended Keys",
+ "description": "Whether to distinguish between extended and number pad keys.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "Treat Extended and Numpad keys the same",
+ "Differentiate between Extended and Numpad keys"
+ ]
+ }
+ },
+ "Options.UseVirtualInfoInFormsMode": {
+ "schema": {
+ "title": "Use Virtual Info in Forms Mode",
+ "description": "Whether to use information from the virtual HTML area for prompts in Forms mode. Warning: this will not always result in accurate information, particularly on pages where there are errors in the HTML coding which result in forms mode and virtual mode being out of synchronization.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ]
+ }
+ },
+ "Options.Verbosity": {
+ "schema": {
+ "title": "Verbosity",
+ "description": "How verbose announcements should be.",
+ "type": "number",
+ "default": 0,
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "most speech",
+ "intermediate",
+ "least speech"
+ ]
+ }
+ },
+ "Options.VIRTUALMSAAREFRESH": {
+ "schema": {
+ "title": "Virtual MSAA Refresh",
+ "description": "How often (in milliseconds) should an embedded ActiveX control cause the Virtual HTML area to be updated when receiving rapid ValueChange events from MSAA. Set to -1 to disable refreshes.",
+ "type": "integer",
+ "default": 0,
+ "minimum": -1
+ }
+ },
+ "Options.VTcolor": {
+ "schema": {
+ "title": "Visual Tracking Color",
+ "description": "The colour to use when highlighting items selected using the touch cursor.",
+ "type": "integer"
+ }
+ },
+ "Options.VTspacing": {
+ "schema": {
+ "title": "Visual Tracking Spacing",
+ "description": "The amount of spacing between the highlight box and the highlighted item.",
+ "type": "integer"
+ }
+ },
+ "Options.VTstyle": {
+ "schema": {
+ "title": "Visual Tracking Style",
+ "description": "The style of highlighting to use.",
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "enumLabels": [
+ "box",
+ "block",
+ "underline"
+ ],
+ "default": 0
+ }
+ },
+ "Options.VTthickness": {
+ "schema": {
+ "title": "Visual Tracking Thickness",
+ "description": "How thick to make the \"box\" and \"underline\" highlighting styles.",
+ "type": "integer"
+ }
+ },
+ "Options.VTtransparency": {
+ "schema": {
+ "title": "Visual Tracking Transparency",
+ "description": "How transparent to make the visual highlighting of selected items.",
+ "type": "integer"
+ }
+ },
+ "Options.VTUseTouchCursor": {
+ "schema": {
+ "title": "Highlight Touch Cursor",
+ "description": "Whether or not to highlight selected items when using the touch cursor or touch gestures.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "Options.VTUseVirtualPCCursor": {
+ "schema": {
+ "title": "Use Virtual PC Cursor",
+ "description": "Whether to use the virtual PC cursor when reading HTML documents.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "Options.VTUseVirtualRibbon": {
+ "schema": {
+ "title": "Use Virtual Ribbon Menu",
+ "description": "Whether to turn on the virtual ribbon menu.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ }
+ },
+ "OSM.GraphicsMode": {
+ "title": "Graphics Mode",
+ "description": "How to handle graphic CRC values.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "Store graphic CRC values when the graphics are being drawn to the screen.",
+ "Compute them from the screen image at the time they are requested to be spoken."
+ ],
+ "default": 0
+ },
+ "OSM.IgnoreSetSysModal": {
+ "title": "Ignore Set Sys Modal",
+ "description": "Whether to disallow Windows calling the SetSysModalWindow function. In most cases this is desirable, because System Modal Windows cannot be spoken.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "OSM.IncludeGraphics": {
+ "title": "Include Graphics",
+ "description": "Which graphics to announce.",
+ "enum": [ 0, 1, 2],
+ "enumLabels": ["Ignore all graphics", "Announce labeled graphics only", "Announce all graphics"],
+ "default": 1
+ },
+ "OSM.IncludeLines": {
+ "title": "Include Lines",
+ "description": "Whether to include line segments in the Off Screen Model. This is required for Excel and other programs which draw a focus rectangle.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "OSM.MouseClickFudgeX": {
+ "title": "Mouse Click Fudge X",
+ "description": "number of pixels to the right of the current JAWS cursor at which a mouse click should be simulated",
+ "type": "integer",
+ "default": 1
+ },
+ "OSM.MouseClickFudgeY": {
+ "title": "Mouse Click Fudge Y",
+ "description": "number of pixels below the current JAWS cursor at which a mouse click should be simulated",
+ "type": "integer",
+ "default": -2
+ },
+ "OSM.PixelsPerBlankLine": {
+ "title": "Pixels Per Blank Line",
+ "description": "how many pixels (high) per blank line",
+ "type": "integer",
+ "default": 13
+ },
+ "OSM.PixelsPerSpace": {
+ "schema": {
+ "title": "Pixels Per Space",
+ "description": "When simulating spaces in blank portions of a line, the number of pixels used to represent each space.",
+ "type": "integer",
+ "default": 8
+ }
+ },
+ "OSM.PixelsPerTab": {
+ "schema": {
+ "title": "Pixels Per Tab",
+ "description": "The number of pixels per tab, used to estimate the number of tab spaces contained within the blank portion of a line.",
+ "type": "integer",
+ "default": 10
+ }
+ },
+ "OSM.SpeakSysModalMessageBox": {
+ "title": "Speak Sys Modal Message Box",
+ "description": "Whether to announce System Modal Message Boxes.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "OSM.TableDetection": {
+ "schema": {
+ "title": "Table Presentation Information",
+ "description": "Which tables to read as tables (data tables, or all tables).",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "data tables only",
+ "all tables"
+ ],
+ "default": 0
+ }
+ },
+ "OSM.TextOutDelay": {
+ "title": "Text Out Delay",
+ "description": "The number of milliseconds to wait before announcing newly written text. Setting this to a non-zero value may cause terminal programs to respond in a less choppy fashion.",
+ "type": "integer",
+ "default": 0
+ },
+ "OSM.TrackCommandBars": {
+ "title": "Track Command Bars",
+ "description": "Whether to track the menu bar in Office applications.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ },
+ "OSM.TrackFocusRect": {
+ "schema": {
+ "title": "Track Focus Rectangle",
+ "description": "Set to one causes MAGic to track the focus rectangle in standard controls that utilize focus rectangles.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 1
+ }
+ },
+ "OSM.TrimBrailleGraphicLabels": {
+ "title": "Trim Braille Graphic Labels",
+ "description": "Whether to trim the size of a braille graphic. Useful to preserve the virtical positioning of braille text.",
+ "enum": [
+ 0,
+ 1
+ ],
+ "enumLabels": [
+ "off",
+ "on"
+ ],
+ "default": 0
+ },
+ "OSM.UnderlineProximity": {
+ "schema": {
+ "title": "Underline Proximity",
+ "description": "How far away in pixels can an underline be before being considered an actual underline.",
+ "type": "integer",
+ "default": 0
+ }
+ },
+ "Touch.FlickVelocityMin": {
+ "schema": {
+ "title": "Flick Velocity",
+ "description": "The minimum finger velocity to treat as a \"flick\".",
+ "type": "integer"
+ }
+ },
+ "Touch.TapEventDurationMax": {
+ "schema": {
+ "title": "Explore Delay",
+ "description": "The delay between touching a touch screen and displaying the explore menu, in milliseconds.",
+ "type": "integer"
+ }
+ },
+ "Touch.TapInterEventDurationMax": {
+ "schema": {
+ "title": "Double Tap Speed",
+ "description": "The threshold for treating two taps as a double-tap, expressed in milliseconds between taps.",
+ "type": "integer"
+ }
+ },
+ "Touch.TapTranslationMax": {
+ "schema": {
+ "title": "Tap Radius",
+ "description": "The size of the finger contact spot on the touch screen.",
+ "type": "integer"
+ }
+ },
+ "Voice Profiles.ActiveVoiceProfileName": {
+ "schema": {
+ "title": "Active Voice Profile Name",
+ "description": "The active voice profile.",
+ "type": "string",
+ "default": "GPII"
+ }
+ }
+ },
+ "capabilitiesTransformations": {
+ "Voice Profiles\\.ActiveVoiceProfileName": {
+ "literalValue": "GPII"
+ },
+ "Options\\.SayAllIndicateCaps": {
+ "transform": {
+ "type": "valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/announceCapitals",
+ "match": [
+ {
+ "inputValue": true,
+ "outputValue": 1
+ },
+ {
+ "inputValue": false,
+ "outputValue": 0
+ }
+ ],
+ "noMatch": {
+ "outputValue": 0
+ }
+ }
+ },
+ "Options\\.TypingEcho": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/keyEcho",
+ "rightPath": "http://registry\\.gpii\\.net/common/wordEcho",
+ "operator": "&&"
+ }
+ },
+ "true": 3,
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho",
+ "true": 1,
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho",
+ "true": 2
+ }
+ }
+ }
+ }
+ }
+ },
+ "Options\\.SayAllMode": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/readingUnit",
+ "match": {
+ "line": 0,
+ "sentence": 1,
+ "paragraph": 2
+ }
+ }
+ },
+ "Braille\\.BrailleMode": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/brailleMode",
+ "match": {
+ "line": 0,
+ "structured": 1,
+ "speechHistory": 2
+ }
+ }
+ },
+ "Options\\.SayAllIgnoreShiftKeys": {
+ "transform": {
+ "type": "valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/stickyKeys",
+ "match": [
+ {
+ "inputValue": true,
+ "outputValue": 1
+ },
+ {
+ "inputValue": false,
+ "outputValue": 0
+ }
+ ],
+ "noMatch": {
+ "outputValue": 0
+ }
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/announceCapitals": {
+ "transform": {
+ "type": "valueMapper",
+ "defaultInputPath": "Options\\.SayAllIndicateCaps",
+ "match": [
+ {
+ "inputValue": 0,
+ "outputValue": false
+ },
+ {
+ "inputValue": 1,
+ "outputValue": true
+ }
+ ],
+ "noMatch": {
+ "outputValue": false
+ }
+ }
+ },
+ "http://registry\\.gpii\\.net/common/keyEcho": {
+ "transform": {
+ "type": "valueMapper",
+ "defaultInputPath": "Options\\.TypingEcho",
+ "match": [
+ {
+ "inputValue": 1,
+ "outputValue": true
+ },
+ {
+ "inputValue": 3,
+ "outputValue": true
+ }
+ ],
+ "noMatch": {
+ "outputValue": false
+ }
+ }
+ },
+ "http://registry\\.gpii\\.net/common/wordEcho": {
+ "transform": {
+ "type": "valueMapper",
+ "defaultInputPath": "Options\\.TypingEcho",
+ "match": [
+ {
+ "inputValue": 2,
+ "outputValue": true
+ },
+ {
+ "inputValue": 3,
+ "outputValue": true
+ }
+ ],
+ "noMatch": {
+ "outputValue": false
+ }
+ }
+ },
+ "http://registry\\.gpii\\.net/common/readingUnit": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "Options\\.SayAllMode",
+ "match": [
+ {
+ "inputValue": 0,
+ "outputValue": "line"
+ },
+ {
+ "inputValue": 1,
+ "outputValue": "sentence"
+ },
+ {
+ "inputValue": 2,
+ "outputValue": "paragraph"
+ }
+ ]
+ }
+ },
+ "http://registry\\.gpii\\.net/common/brailleMode": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "Braille\\.BrailleMode",
+ "match": [
+ {
+ "inputValue": 0,
+ "outputValue": "line"
+ },
+ {
+ "inputValue": 1,
+ "outputValue": "structured"
+ },
+ {
+ "inputValue": 2,
+ "outputValue": "speechHistory"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "configuration2": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "liveness": "manualRestart",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF"
+ },
+ "supportedSettings": {
+ "Options.PrimarySynthesizer": {},
+ "ENU-Global.Pitch": {
+ "schema": {
+ "title": "Pitch",
+ "description": "The pitch to use for all types of announcements.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-Global.Punctuation": {
+ "schema": {
+ "title": "Punctuation",
+ "description": "How much punctuation to announce for all types of announcements.",
+ "enum": [0, 1, 2, 3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-Global.Rate": {
+ "schema": {
+ "title": "Rate",
+ "description": "Speech rate for all announcements (scale varies by voice).",
+ "type": "integer"
+ }
+ },
+ "ENU-Global.SynthLangString": {
+ "schema": {
+ "title": "Synth Language",
+ "description": "The language to use for all announcements.",
+ "type": "string"
+ }
+ },
+ "ENU-Global.Volume": {
+ "schema": {
+ "title": "Global Volume",
+ "description": "The volume for all announcements.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ },
+ "ENU-JAWSCursor.Pitch": {
+ "schema": {
+ "title": "JAWS Cursor Pitch",
+ "description": "The pitch to use for announcements related to the JAWS cursor.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-JAWSCursor.Punctuation": {
+ "schema": {
+ "title": "JAWS Cursor Punctuation",
+ "description": "How much punctuation to announce for the JAWS cursor.",
+ "enum": [0, 1, 2, 3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-JAWSCursor.Rate": {
+ "schema": {
+ "title": "JAWS Cursor Rate",
+ "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).",
+ "type": "integer"
+ }
+ },
+ "ENU-JAWSCursor.SynthLangString": {
+ "schema": {
+ "title": "JAWS Cursor Language",
+ "description": "The language to use for announcements related to the JAWS cursor.",
+ "type": "string"
+ }
+ },
+ "ENU-JAWSCursor.Volume": {
+ "schema": {
+ "title": "JAWS Cursor Volume",
+ "description": "The volume for announcements related to the JAWS cursor.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ },
+ "ENU-Keyboard.Pitch": {
+ "schema": {
+ "title": "Keyboard Pitch",
+ "description": "The pitch to use for announcements related to the keyboard.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-Keyboard.Punctuation": {
+ "schema": {
+ "title": "Keyboard Punctuation",
+ "description": "How much punctuation to announce for announcements related to the keyboard.",
+ "enum": [0,1,2,3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-Keyboard.Rate": {
+ "schema": {
+ "title": "Keyboard Rate",
+ "description": "Speech rate for announcements related to the keyboard (scale varies by voice).",
+ "type": "integer"
+ }
+ },
+ "ENU-Keyboard.SynthLangString": {
+ "schema": {
+ "title": "Keyboard Language",
+ "description": "The language to use for announcements related to the keyboard.",
+ "type": "string"
+ }
+ },
+ "ENU-Keyboard.Volume": {
+ "schema": {
+ "title": "Keyboard Volume",
+ "description": "The volume for announcements related to the keyboard.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ },
+ "ENU-MenuAndDialog.Pitch": {
+ "schema": {
+ "title": "Menu and Dialog Pitch",
+ "description": "The pitch to use for announcements related to menus and dialogs.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-MenuAndDialog.Punctuation": {
+ "schema": {
+ "title": "Menu and Dialog Punctuation",
+ "description": "How much punctuation to announce for menus and dialogs.",
+ "enum": [0,1,2,3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-MenuAndDialog.Rate": {
+ "schema": {
+ "title": "Menu and Dialog Rate",
+ "description": "Speech rate for announcements related to menus and dialogs (scale varies by voice).",
+ "type": "integer"
+ }
+ },
+ "ENU-MenuAndDialog.SynthLangString": {
+ "schema": {
+ "title": "Menu and Dialog Language",
+ "description": "The language to use for announcements related to menus and dialogs.",
+ "type": "string"
+ }
+ },
+ "ENU-MenuAndDialog.Volume": {
+ "schema": {
+ "title": "Menu and Dialog Volume",
+ "description": "The volume for announcements related to menus and dialogs.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ },
+ "ENU-Message.Pitch": {
+ "schema": {
+ "title": "Message Pitch",
+ "description": "The pitch to use for message announcements.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-Message.Punctuation": {
+ "schema": {
+ "title": "Message Punctuation",
+ "description": "How much punctuation to announce for messages.",
+ "enum": [0,1,2,3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-Message.Rate": {
+ "schema": {
+ "title": "Message Rate",
+ "description": "Speech rate for message announcements.",
+ "type": "integer"
+ }
+ },
+ "ENU-Message.SynthLangString": {
+ "schema": {
+ "title": "Message Language",
+ "description": "The language to use for message announcements.",
+ "type": "string"
+ }
+ },
+ "ENU-Message.Volume": {
+ "schema": {
+ "title": "Message Volume",
+ "description": "The volume for message announcements.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ },
+ "ENU-PCCursor.Pitch": {
+ "schema": {
+ "title": "PC Cursor Pitch",
+ "description": "The pitch to use for announcements related to the PC cursor.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 65
+ }
+ },
+ "ENU-PCCursor.Punctuation": {
+ "schema": {
+ "title": "PC Cursor Punctuation",
+ "description": "How much punctuation to announce for the PC cursor.",
+ "enum": [0,1,2,3],
+ "enumLabels": ["None", "Some", "Most", "All"],
+ "default": 2
+ }
+ },
+ "ENU-PCCursor.Rate": {
+ "schema": {
+ "title": "PC Cursor Rate",
+ "description": "Speech rate for announcements related to the PC cursor (scale varies by voice).",
+ "type": "integer"
+ }
+ },
+ "ENU-PCCursor.SynthLangString": {
+ "schema": {
+ "title": "PC Cursor Language",
+ "description": "The language to use for announcements related to the PC cursor.",
+ "type": "string"
+ }
+ },
+ "ENU-PCCursor.Volume": {
+ "schema": {
+ "title": "PC Cursor Volume",
+ "description": "The volume for announcements related to the PC cursor.",
+ "type": "integer",
+ "minimum": 10,
+ "maximum": 100,
+ "default": 100
+ }
+ }
+ },
+ "capabilitiesTransformations": {
+ "Options\\.PrimarySynthesizer": "http://registry\\.gpii\\.net/common/speechSynthesizer",
+ "ENU-Global\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-JAWSCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-Keyboard\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-MenuAndDialog\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-Message\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-PCCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate",
+ "ENU-Global\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-JAWSCursor\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-Keyboard\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-MenuAndDialog\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-Message\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-PCCursor\\.Pitch": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 99,
+ "offset": 1
+ }
+ }
+ }
+ },
+ "ENU-Global\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-JAWSCursor\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-Keyboard\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-MenuAndDialog\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-Message\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-PCCursor\\.Volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volume",
+ "factor": 100
+ }
+ },
+ "ENU-Global\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-JAWSCursor\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-Keyboard\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-MenuAndDialog\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-Message\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-PCCursor\\.Punctuation": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 1,
+ "most": 2,
+ "all": 3
+ }
+ }
+ },
+ "ENU-Global\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ },
+ "ENU-JAWSCursor\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ },
+ "ENU-Keyboard\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ },
+ "ENU-MenuAndDialog\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ },
+ "ENU-Message\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ },
+ "ENU-PCCursor\\.SynthLangString": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en-US": "American English",
+ "en-GB": "British English",
+ "fr": "French",
+ "fr-CA": "French",
+ "it": "Italian",
+ "pt-BR": "Brazilian Portuguese",
+ "de": "German",
+ "es-US": "Latin American Spanish",
+ "es-ES": "Castilian Spanish",
+ "fi": "Finnish"
+ }
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/speechSynthesizer": "Options\\.PrimarySynthesizer",
+ "http://registry\\.gpii\\.net/common/speechRate": "ENU-Global\\.Rate",
+ "http://registry\\.gpii\\.net/common/pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "ENU-Global\\.Pitch",
+ "factor": 0.010101,
+ "offset": -0.010101
+ }
+ },
+ "http://registry\\.gpii\\.net/common/volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "ENU-Global\\.Volume",
+ "factor": 0.01
+ }
+ },
+ "http://registry\\.gpii\\.net/common/punctuationVerbosity": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "ENU-Global\\.Punctuation",
+ "match": [
+ {
+ "inputValue": 0,
+ "outputValue": "none"
+ },
+ {
+ "inputValue": 1,
+ "outputValue": "some"
+ },
+ {
+ "inputValue": 2,
+ "outputValue": "most"
+ },
+ {
+ "inputValue": 3,
+ "outputValue": "all"
+ }
+ ]
+ }
+ },
+ "http://registry\\.gpii\\.net/common/auditoryOutLanguage": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "ENU-Global\\.SynthLangString",
+ "match": {
+ "American English": "en-US",
+ "British English": "en-GB",
+ "French": "fr",
+ "Italian": "it",
+ "Brazilian Portuguese": "pt-BR",
+ "German": "de",
+ "Latin American Spanish": "es-US",
+ "Castilian Spanish": "es-ES",
+ "Finnish": "fi"
+ }
+ }
+ }
+ }
+ }
+ },
+ "launchHandlers": {
+ "launcher": {
+ "type": "gpii.launchHandlers.flexibleHandler",
+ "options": {
+ "verifySettings": true,
+ "retryOptions": {
+ "rewriteEvery": 0,
+ "numRetries": 20
+ },
+ "getState": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "jfw.exe"
+ }
+ ],
+ "setTrue": [
+ {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2018.exe\\}\""
+ }
+ ],
+ "setFalse": [
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "jfw.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "fsSynth32.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "jhookldr.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "ScannerHandler.exe"
+ }
+ ]
+ }
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.registryKeyExists",
+ "hKey": "HKEY_LOCAL_MACHINE",
+ "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2018.exe",
+ "subPath": "",
+ "dataType": "REG_SZ"
+ }
+ ]
+ },
+
+ "org.nvda-project": {
+ "name": "NVDA Screen Reader",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "configs": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "liveness": "manualRestart",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini"
+ },
+ "supportedSettings": {
+ "speech.espeak.pitch": {},
+ "speech.espeak.volume": {},
+ "presentation.reportHelpBalloons": {},
+ "keyboard.speakTypedCharacters": {},
+ "keyboard.speakTypedWords": {},
+ "speech.espeak.sayCapForCapitals": {},
+ "reviewCursor.followFocus": {},
+ "reviewCursor.followCaret": {},
+ "reviewCursor.followMouse": {},
+ "speech.synth": {},
+ "speech.outputDevice": {},
+ "speech.espeak.rate": {},
+ "speech.espeak.rateBoost": {},
+ "speech.symbolLevel": {},
+ "speech.espeak.voice": {}
+ },
+ "capabilitiesTransformations": {
+ "speech\\.espeak\\.pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 100
+ }
+ },
+ "speech\\.espeak\\.volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS",
+ "factor": 100
+ }
+ },
+ "presentation\\.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages",
+ "keyboard\\.speakTypedCharacters": "http://registry\\.gpii\\.net/common/keyEcho",
+ "keyboard\\.speakTypedWords": "http://registry\\.gpii\\.net/common/wordEcho",
+ "speech\\.espeak\\.sayCapForCapitals": "http://registry\\.gpii\\.net/common/announceCapitals",
+ "transform": [
+ {
+ "type": "fluid.transforms.arrayToSetMembership",
+ "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "outputPath": "",
+ "presentValue": true,
+ "missingValue": false,
+ "options": {
+ "focus": "reviewCursor\\.followFocus",
+ "caret": "reviewCursor\\.followCaret",
+ "mouse": "reviewCursor\\.followMouse"
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "match": {
+ "false": {
+ "outputValue": {
+ "transform": [
+ {
+ "type": "fluid.transforms.literalValue",
+ "input": "silence",
+ "outputPath": "speech\\.synth"
+ },
+ {
+ "type": "fluid.transforms.literalValue",
+ "input": "Microsoft Sound Mapper",
+ "outputPath": "speech\\.outputDevice"
+ }
+ ]
+ }
+ }
+ }
+ }
+ ],
+ "speech\\.espeak\\.rate": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": 3.10,
+ "operator": "/",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": 80,
+ "operator": "-",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "truePath": "http://registry\\.gpii\\.net/common/speechRate",
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": "/",
+ "right": 3
+ }
+ },
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": "<=",
+ "right": 390
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "speech\\.espeak\\.rateBoost": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": ">",
+ "right": 390
+ }
+ },
+ "speech\\.symbolLevel": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 100,
+ "most": 200,
+ "all": 300
+ }
+ }
+ },
+ "speech\\.espeak\\.voice": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en": "en-us",
+ "en-GB": "en-gb",
+ "en-US": "en-us",
+ "en-scotland": "en-gb-scotland",
+ "af": "af",
+ "bg": "bg",
+ "bs": "bs",
+ "ca": "ca",
+ "cs": "cs",
+ "cy": "cy",
+ "da": "da",
+ "de": "de",
+ "el": "el",
+ "eo": "eo",
+ "es": "es",
+ "es-419": "es-la",
+ "et": "et",
+ "fi": "fi",
+ "fr": "fr",
+ "fr-BE": "fr-be",
+ "hi": "hi",
+ "hr": "hr",
+ "hu": "hu",
+ "hy": "hy",
+ "hy-arevmda": "hy-west",
+ "id": "id",
+ "is": "is",
+ "it": "it",
+ "ka": "ka",
+ "kn": "kn",
+ "ku": "ku",
+ "la": "la",
+ "lv": "lv",
+ "mk": "mk",
+ "ml": "ml",
+ "nl": "nl",
+ "no": "no",
+ "pl": "pl",
+ "pt-BR": "pt",
+ "pt-PT": "pt-pt",
+ "ro": "ro",
+ "ru": "ru",
+ "sk": "sk",
+ "sq": "sq",
+ "sr": "sr",
+ "sv": "sv",
+ "sw": "sw",
+ "ta": "ta",
+ "tr": "tr",
+ "vi": "vi",
+ "zh-cmn": "zh",
+ "cmn": "zh",
+ "zh-yue": "zh-yue"
+ },
+ "noMatch": {
+ "outputValue": "en-us"
+ }
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation\\.reportHelpBalloons",
+ "http://registry\\.gpii\\.net/common/keyEcho": "keyboard\\.speakTypedCharacters",
+ "http://registry\\.gpii\\.net/common/wordEcho": "keyboard\\.speakTypedWords",
+ "http://registry\\.gpii\\.net/common/announceCapitals": "speech\\.espeak\\.sayCapForCapitals",
+ "transform": [
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit",
+ "match": {
+ "true": "all",
+ "false": "sentence"
+ }
+ },
+ {
+ "type": "fluid.transforms.setMembershipToArray",
+ "inputPath": "",
+ "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "presentValue": true,
+ "missingValue": false,
+ "options": {
+ "reviewCursor\\.followFocus": "focus",
+ "reviewCursor\\.followCaret": "caret",
+ "reviewCursor\\.followMouse": "mouse"
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "defaultInputPath": "speech\\.symbolLevel",
+ "match": {
+ "0": "none",
+ "100": "some",
+ "200": "most",
+ "300": "all"
+ }
+ },
+ {
+ "type": "fluid.transforms.condition",
+ "outputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "false": true,
+ "true": false,
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "speech\\.synth",
+ "operator": "===",
+ "right": "silence"
+ }
+ },
+ "right": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "speech\\.outputDevice",
+ "operator": "===",
+ "right": "Microsoft Sound Mapper"
+ }
+ },
+ "operator": "&&"
+ }
+ }
+ },
+ {
+ "type": "fluid.transforms.condition",
+ "outputPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": true,
+ "operator": "===",
+ "leftPath": "speech\\.espeak\\.rateBoost"
+ }
+ },
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "speech\\.espeak\\.rate",
+ "factor": 3.1,
+ "offset": 80
+ }
+ },
+ "true": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "speech\\.espeak\\.rate",
+ "factor": 3.1,
+ "offset": 80
+ }
+ },
+ "operator": "*",
+ "right": 3
+ }
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "speech\\.espeak\\.voice",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en\\en": "en",
+ "en\\en-us": "en-US",
+ "en\\en-sc": "en-scotland",
+ "en\\en-wi": "en-BZ",
+ "af": "af",
+ "bg": "bg",
+ "bs": "bs",
+ "ca": "ca",
+ "cs": "cs",
+ "cy": "cy",
+ "da": "da",
+ "de": "de",
+ "el": "el",
+ "test\\grc": "grc",
+ "eo": "eo",
+ "es": "es",
+ "es-la": "es-419",
+ "et": "et",
+ "fi": "fi",
+ "fr": "fr",
+ "fr-be": "fr-BE",
+ "hi": "hi",
+ "hr": "hr",
+ "hu": "hu",
+ "hy": "hy",
+ "hy-west": "hy-arevmda",
+ "id": "id",
+ "is": "is",
+ "it": "it",
+ "test\\jbo": "jbo",
+ "ka": "ka",
+ "kn": "kn",
+ "ku": "ku",
+ "la": "la",
+ "lv": "lv",
+ "mk": "mk",
+ "ml": "ml",
+ "test\\nci": "nci",
+ "nl": "nl",
+ "no": "no",
+ "test\\pap": "pap",
+ "pl": "pl",
+ "pt": "pt-BR",
+ "pt-pt": "pt-PT",
+ "ro": "ro",
+ "ru": "ru",
+ "sk": "sk",
+ "sq": "sq",
+ "sr": "sr",
+ "sv": "sv",
+ "sw": "sw",
+ "ta": "ta",
+ "tr": "tr",
+ "vi": "vi",
+ "zh": "zh-cmn",
+ "zh-yue": "zh-yue"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "launchHandlers": {
+ "launcher": {
+ "type": "gpii.launchHandlers.flexibleHandler",
+ "options": {
+ "verifySettings": true,
+ "retryOptions": {
+ "rewriteEvery": 0,
+ "numRetries": 20
+ },
+ "getState": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "nvda.exe"
+ }
+ ],
+ "setTrue": [
+ {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\""
+ }
+ ],
+ "setFalse": [
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda_service.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda.exe"
+ }
+ ]
+ }
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.registryKeyExists",
+ "hKey": "HKEY_LOCAL_MACHINE",
+ "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe",
+ "subPath": "",
+ "dataType": "REG_SZ"
+ }
+ ]
+ }
+ }
+}
diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json
new file mode 100644
index 000000000..ffc139def
--- /dev/null
+++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json
@@ -0,0 +1,773 @@
+{
+ "gpiiKey": "screenreader",
+ "preferences": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/applications/org.nvda-project": {
+ "speech.synth": "espeak",
+ "speech.outputDevice": "Microsoft Sound Mapper",
+ "speech.symbolLevel": 300,
+ "speech.espeak.rate": 40,
+ "speech.espeak.voice": "en-us",
+ "speech.espeak.pitch": 60,
+ "speech.espeak.volume": 80,
+ "reviewCursor.followFocus": "False",
+ "reviewCursor.followCaret": "True",
+ "reviewCursor.followMouse": "True",
+ "keyboard.speakTypedWords": "True",
+ "speech.espeak.rateBoost": "True",
+ "keyboard.speakTypedCharacters": "False",
+ "presentation.reportHelpBalloons": "False",
+ "speech.espeak.sayCapForCapitals": "True",
+ "virtualBuffers.autoSayAllOnPageLoad": "False"
+ }
+ }
+ }
+ }
+ },
+ "deviceContext": {
+ "solutions": [
+ {
+ "id": "com.microsoft.windows.narrator"
+ }, {
+ "id": "org.nvda-project"
+ }, {
+ "id": "webinsight.webAnywhere.windows"
+ }
+ ],
+ "OS": {
+ "id": "win32",
+ "version": "6.1.7601"
+ }
+ },
+ "solutionsRegistryEntries": {
+ "com.microsoft.windows.narrator": {
+ "name": "Windows Built-in Narrator",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "capabilities": [
+ "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled"
+ ],
+ "settingsHandlers": {
+ "configure": {
+ "type": "gpii.windows.registrySettingsHandler",
+ "liveness": "liveRestart",
+ "options": {
+ "hKey": "HKEY_CURRENT_USER",
+ "path": "Software\\Microsoft\\Narrator",
+ "dataTypes": {
+ "SpeechSpeed": "REG_DWORD",
+ "SpeechPitch": "REG_DWORD",
+ "InteractionMouse": "REG_DWORD",
+ "CoupleNarratorCursorKeyboard": "REG_DWORD",
+ "FollowInsertion": "REG_DWORD",
+ "EchoChars": "REG_DWORD",
+ "EchoWords": "REG_DWORD"
+ }
+ },
+ "supportedSettings": {
+ "SpeechSpeed": {},
+ "SpeechPitch": {},
+ "CoupleNarratorCursorKeyboard": {},
+ "FollowInsertion": {},
+ "InteractionMouse": {},
+ "EchoChars": {},
+ "EchoWords": {}
+ },
+ "capabilitiesTransformations": {
+ "SpeechSpeed": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.quantize",
+ "inputPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "ranges": [
+ {
+ "upperBound": 50,
+ "output": 0
+ },
+ {
+ "upperBound": 150,
+ "output": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "factor": 0.1,
+ "offset": -5
+ }
+ }
+ },
+ {
+ "upperBound": 450,
+ "output": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "factor": 0.033333333333,
+ "offset": -5
+ }
+ }
+ },
+ {
+ "output": 20
+ }
+ ]
+ }
+ }
+ }
+ },
+ "SpeechPitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 20,
+ "offset": 0
+ }
+ },
+ "transform": [
+ {
+ "type": "fluid.transforms.arrayToSetMembership",
+ "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "outputPath": "",
+ "presentValue": 1,
+ "missingValue": 0,
+ "options": {
+ "focus": "CoupleNarratorCursorKeyboard",
+ "caret": "FollowInsertion",
+ "mouse": "InteractionMouse"
+ }
+ }
+ ],
+ "EchoChars": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "false": 0,
+ "true": 1,
+ "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho"
+ }
+ },
+ "EchoWords": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "false": 0,
+ "true": 1,
+ "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho"
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/speechRate": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "SpeechSpeed",
+ "right": 10,
+ "operator": "<"
+ }
+ },
+ "true": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "SpeechSpeed",
+ "factor": 10,
+ "offset": 50
+ }
+ },
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "SpeechSpeed",
+ "factor": 30,
+ "offset": -150
+ }
+ }
+ }
+ },
+ "http://registry\\.gpii\\.net/common/pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "SpeechPitch",
+ "factor": 0.05,
+ "offset": 0
+ }
+ },
+ "transform": [
+ {
+ "type": "fluid.transforms.setMembershipToArray",
+ "inputPath": "",
+ "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "presentValue": 1,
+ "missingValue": 0,
+ "options": {
+ "CoupleNarratorCursorKeyboard": "focus",
+ "FollowInsertion": "caret",
+ "InteractionMouse": "mouse"
+ }
+ }
+ ],
+ "http://registry\\.gpii\\.net/common/keyEcho": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "EchoChars",
+ "right": 0,
+ "operator": "!=="
+ }
+ },
+ "http://registry\\.gpii\\.net/common/wordEcho": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "EchoWords",
+ "right": 0,
+ "operator": "!=="
+ }
+ }
+ }
+ },
+ "configureNoRoam": {
+ "type": "gpii.windows.registrySettingsHandler",
+ "options": {
+ "hKey": "HKEY_CURRENT_USER",
+ "path": "Software\\Microsoft\\Narrator\\NoRoam",
+ "dataTypes": {
+ "SpeechVolume": "REG_DWORD",
+ "ShowKeyboardIntroduction": "REG_DWORD",
+ "ShowBrowserSelection": "REG_DWORD"
+ }
+ },
+ "supportedSettings": {
+ "SpeechVolume": {},
+ "ShowKeyboardIntroduction": {},
+ "ShowBrowserSelection": {}
+ },
+ "capabilitiesTransformations": {
+ "SpeechVolume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS",
+ "factor": 100,
+ "offset": 0
+ }
+ },
+ "ShowKeyboardIntroduction": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "false": 0,
+ "true": 1,
+ "conditionPath": "http://registry\\.gpii\\.net/common/speakTutorialMessages"
+ }
+ },
+ "ShowBrowserSelection": {
+ "transform": {
+ "type": "fluid.transforms.literalValue",
+ "input": 0
+ }
+ }
+ }
+ }
+ },
+ "launchHandlers": {
+ "launcher": {
+ "type": "gpii.windows.enableRegisteredAT",
+ "options": {
+ "registryName": "Narrator",
+ "getState": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "Narrator.exe"
+ }
+ ]
+ }
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ },
+
+ "webinsight.webAnywhere.windows": {
+ "name": "Web Anywhere",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "capabilities": [
+ "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled"
+ ],
+ "launchHandlers": {
+ "launcher": {
+ "type": "gpii.launchHandlers.flexibleHandler",
+ "options": {
+ "setTrue": {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe\\}\" \"http://webanywhere.cs.washington.edu/beta/?starting_url=http%3A%2F%2Fcloud4all.info\""
+ },
+ "setFalse": {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "firefox.exe"
+ },
+ "getState": [
+ {
+ "type": "gpii.processReporter.neverRunning"
+ }
+ ]
+ }
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ },
+
+ "org.nvda-project": {
+ "name": "NVDA Screen Reader",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32",
+ "version": ">=5.0"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "configs": {
+ "type": "gpii.settingsHandlers.INISettingsHandler",
+ "liveness": "manualRestart",
+ "options": {
+ "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini"
+ },
+ "supportedSettings": {
+ "speech.espeak.pitch": {},
+ "speech.espeak.volume": {},
+ "presentation.reportHelpBalloons": {},
+ "keyboard.speakTypedCharacters": {},
+ "keyboard.speakTypedWords": {},
+ "speech.espeak.sayCapForCapitals": {},
+ "reviewCursor.followFocus": {},
+ "reviewCursor.followCaret": {},
+ "reviewCursor.followMouse": {},
+ "speech.synth": {},
+ "speech.outputDevice": {},
+ "speech.espeak.rate": {},
+ "speech.espeak.rateBoost": {},
+ "speech.symbolLevel": {},
+ "speech.espeak.voice": {}
+ },
+ "capabilitiesTransformations": {
+ "speech\\.espeak\\.pitch": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/pitch",
+ "factor": 100
+ }
+ },
+ "speech\\.espeak\\.volume": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS",
+ "factor": 100
+ }
+ },
+ "presentation\\.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages",
+ "keyboard\\.speakTypedCharacters": "http://registry\\.gpii\\.net/common/keyEcho",
+ "keyboard\\.speakTypedWords": "http://registry\\.gpii\\.net/common/wordEcho",
+ "speech\\.espeak\\.sayCapForCapitals": "http://registry\\.gpii\\.net/common/announceCapitals",
+ "transform": [
+ {
+ "type": "fluid.transforms.arrayToSetMembership",
+ "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "outputPath": "",
+ "presentValue": true,
+ "missingValue": false,
+ "options": {
+ "focus": "reviewCursor\\.followFocus",
+ "caret": "reviewCursor\\.followCaret",
+ "mouse": "reviewCursor\\.followMouse"
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "match": {
+ "false": {
+ "outputValue": {
+ "transform": [
+ {
+ "type": "fluid.transforms.literalValue",
+ "input": "silence",
+ "outputPath": "speech\\.synth"
+ },
+ {
+ "type": "fluid.transforms.literalValue",
+ "input": "Microsoft Sound Mapper",
+ "outputPath": "speech\\.outputDevice"
+ }
+ ]
+ }
+ }
+ }
+ }
+ ],
+ "speech\\.espeak\\.rate": {
+ "transform": {
+ "type": "fluid.transforms.round",
+ "input": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": 3.10,
+ "operator": "/",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": 80,
+ "operator": "-",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.condition",
+ "truePath": "http://registry\\.gpii\\.net/common/speechRate",
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": "/",
+ "right": 3
+ }
+ },
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": "<=",
+ "right": 390
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "speech\\.espeak\\.rateBoost": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "operator": ">",
+ "right": 390
+ }
+ },
+ "speech\\.symbolLevel": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "match": {
+ "none": 0,
+ "some": 100,
+ "most": 200,
+ "all": 300
+ }
+ }
+ },
+ "speech\\.espeak\\.voice": {
+ "transform": {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en": "en\\en",
+ "en-GB": "en\\en",
+ "en-US": "en\\en-us",
+ "en-scotland": "en\\en-sc",
+ "en-BZ": "en\\en-wi",
+ "en-BS": "en\\en-wi",
+ "en-AG": "en\\en-wi",
+ "en-AI": "en\\en-wi",
+ "af": "af",
+ "bg": "bg",
+ "bs": "bs",
+ "ca": "ca",
+ "cs": "cs",
+ "cy": "cy",
+ "da": "da",
+ "de": "de",
+ "el": "el",
+ "grc": "test\\grc",
+ "eo": "eo",
+ "es": "es",
+ "es-419": "es-la",
+ "et": "et",
+ "fi": "fi",
+ "fr": "fr",
+ "fr-BE": "fr-be",
+ "hi": "hi",
+ "hr": "hr",
+ "hu": "hu",
+ "hy": "hy",
+ "hy-arevmda": "hy-west",
+ "id": "id",
+ "is": "is",
+ "it": "it",
+ "jbo": "test\\jbo",
+ "ka": "ka",
+ "kn": "kn",
+ "ku": "ku",
+ "la": "la",
+ "lv": "lv",
+ "mk": "mk",
+ "ml": "ml",
+ "nci": "test\\nci",
+ "nl": "nl",
+ "no": "no",
+ "pap": "test\\pap",
+ "pl": "pl",
+ "pt-BR": "pt",
+ "pt-PT": "pt-pt",
+ "ro": "ro",
+ "ru": "ru",
+ "sk": "sk",
+ "sq": "sq",
+ "sr": "sr",
+ "sv": "sv",
+ "sw": "sw",
+ "ta": "ta",
+ "tr": "tr",
+ "vi": "vi",
+ "zh-cmn": "zh",
+ "cmn": "zh",
+ "zh-yue": "zh-yue"
+ }
+ }
+ }
+ },
+ "inverseCapabilitiesTransformations": {
+ "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation\\.reportHelpBalloons",
+ "http://registry\\.gpii\\.net/common/keyEcho": "keyboard\\.speakTypedCharacters",
+ "http://registry\\.gpii\\.net/common/wordEcho": "keyboard\\.speakTypedWords",
+ "http://registry\\.gpii\\.net/common/announceCapitals": "speech\\.espeak\\.sayCapForCapitals",
+ "transform": [
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit",
+ "match": {
+ "true": "all",
+ "false": "sentence"
+ }
+ },
+ {
+ "type": "fluid.transforms.setMembershipToArray",
+ "inputPath": "",
+ "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS",
+ "presentValue": true,
+ "missingValue": false,
+ "options": {
+ "reviewCursor\\.followFocus": "focus",
+ "reviewCursor\\.followCaret": "caret",
+ "reviewCursor\\.followMouse": "mouse"
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity",
+ "defaultInputPath": "speech\\.symbolLevel",
+ "match": {
+ "0": "none",
+ "100": "some",
+ "200": "most",
+ "300": "all"
+ }
+ },
+ {
+ "type": "fluid.transforms.condition",
+ "outputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled",
+ "false": true,
+ "true": false,
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "speech\\.synth",
+ "operator": "===",
+ "right": "silence"
+ }
+ },
+ "right": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "leftPath": "speech\\.outputDevice",
+ "operator": "===",
+ "right": "Microsoft Sound Mapper"
+ }
+ },
+ "operator": "&&"
+ }
+ }
+ },
+ {
+ "type": "fluid.transforms.condition",
+ "outputPath": "http://registry\\.gpii\\.net/common/speechRate",
+ "condition": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "right": true,
+ "operator": "===",
+ "leftPath": "speech\\.espeak\\.rateBoost"
+ }
+ },
+ "false": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "speech\\.espeak\\.rate",
+ "factor": 3.1,
+ "offset": 80
+ }
+ },
+ "true": {
+ "transform": {
+ "type": "fluid.transforms.binaryOp",
+ "left": {
+ "transform": {
+ "type": "fluid.transforms.linearScale",
+ "inputPath": "speech\\.espeak\\.rate",
+ "factor": 3.1,
+ "offset": 80
+ }
+ },
+ "operator": "*",
+ "right": 3
+ }
+ }
+ },
+ {
+ "type": "fluid.transforms.valueMapper",
+ "defaultInputPath": "speech\\.espeak\\.voice",
+ "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage",
+ "match": {
+ "en\\en": "en",
+ "en\\en-us": "en-US",
+ "en\\en-sc": "en-scotland",
+ "en\\en-wi": "en-BZ",
+ "af": "af",
+ "bg": "bg",
+ "bs": "bs",
+ "ca": "ca",
+ "cs": "cs",
+ "cy": "cy",
+ "da": "da",
+ "de": "de",
+ "el": "el",
+ "test\\grc": "grc",
+ "eo": "eo",
+ "es": "es",
+ "es-la": "es-419",
+ "et": "et",
+ "fi": "fi",
+ "fr": "fr",
+ "fr-be": "fr-BE",
+ "hi": "hi",
+ "hr": "hr",
+ "hu": "hu",
+ "hy": "hy",
+ "hy-west": "hy-arevmda",
+ "id": "id",
+ "is": "is",
+ "it": "it",
+ "test\\jbo": "jbo",
+ "ka": "ka",
+ "kn": "kn",
+ "ku": "ku",
+ "la": "la",
+ "lv": "lv",
+ "mk": "mk",
+ "ml": "ml",
+ "test\\nci": "nci",
+ "nl": "nl",
+ "no": "no",
+ "test\\pap": "pap",
+ "pl": "pl",
+ "pt": "pt-BR",
+ "pt-pt": "pt-PT",
+ "ro": "ro",
+ "ru": "ru",
+ "sk": "sk",
+ "sq": "sq",
+ "sr": "sr",
+ "sv": "sv",
+ "sw": "sw",
+ "ta": "ta",
+ "tr": "tr",
+ "vi": "vi",
+ "zh": "zh-cmn",
+ "zh-yue": "zh-yue"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "launchHandlers": {
+ "launcher": {
+ "type": "gpii.launchHandlers.flexibleHandler",
+ "options": {
+ "verifySettings": true,
+ "retryOptions": {
+ "rewriteEvery": 0,
+ "numRetries": 20
+ },
+ "getState": [
+ {
+ "type": "gpii.processReporter.find",
+ "command": "nvda.exe"
+ }
+ ],
+ "setTrue": [
+ {
+ "type": "gpii.launch.exec",
+ "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\""
+ }
+ ],
+ "setFalse": [
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda_service.exe"
+ },
+ {
+ "type": "gpii.windows.closeProcessByName",
+ "filename": "nvda.exe"
+ }
+ ]
+ }
+ }
+ },
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.registryKeyExists",
+ "hKey": "HKEY_LOCAL_MACHINE",
+ "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe",
+ "subPath": "",
+ "dataType": "REG_SZ"
+ }
+ ]
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/README.md b/gpii/node_modules/flowManager/README.md
index 9d6a91a34..467546303 100644
--- a/gpii/node_modules/flowManager/README.md
+++ b/gpii/node_modules/flowManager/README.md
@@ -1,7 +1,7 @@
-Flow Manager
-===
+# Flow Manager
-The flow manager is responsible for the overall orchestration of the various subcomponents which comprise the GPII personalization architecture. It:
+The flow manager is responsible for the overall orchestration of the various subcomponents which comprise the GPII
+personalization architecture. It:
- is able to be notified by the user listener.
- queries the preferences server for user preferences.
@@ -9,59 +9,73 @@ The flow manager is responsible for the overall orchestration of the various sub
- communicates with the matchmaker.
- communicates with the launch manager.
-### Dependencies
+## Dependencies
- [infusion](https://github.com/fluid-project/infusion) framework.
-### Installation
+## Installation
-Run the following command in your newly checked out universal repository. This
-will pull all dependencies that are required by universal.
+Run the following command in your newly checked out universal repository. This will pull all dependencies that are
+required by universal.
npm install
-### Flow Manager API
+## Flow Manager API
The app currently supports the following urls:
// LOCAL DEPLOYMENT
- {urlToFlowManager}/user/{userName}/login // GET
- {urlToFlowManager}/user/{uesrName}/logout // GET
+ {urlToFlowManager}/user/{gpiiKey}/login // GET
+ {urlToFlowManager}/user/{gpiiKey}/logout // GET
// CLOUD BASED DEPLOYMENT
- {urlToFlowManager}/{userName}/settings/:device // GET
+ {urlToFlowManager}/health // GET
+ {urlToFlowManager}/ready // GET
+ {urlToFlowManager}/revision // GET
+ {urlToFlowManager}/access_token // POST
+ {urlToFlowManager}/{gpiiKey}/settings/:device // GET
// where :device is an url encoded {device: "..."} block.
+ {urlToFlowManager}/{gpiiKey}/settings/ // PUT
+
+Refer to [GPII OAuth2 Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide) for how to use endpoints provided by the cloud
+based deployment.
For example:
{urlToFlowManager}/user/andrei/login // GET
- {urlToFlowManager}/andrei/settings/%7BOS%3A%20%7B%7D%7D // GET with {OS: {}}
- {urlToFlowManager}/user/nisha/logout // GET
+ {urlToFlowManager}/user/andrei/logout // GET
-### Cloud based flow manager
+## Cloud based flow manager
-The cloud based configuration of the flow manager enables web apps to communicate to the GPII architecture components and
-to retrieve the relevant settings filtered/mapped based on the user profile, platform and application specification. This
-configuration of the flow manager is intended for the apps on platforms without a locally running core framework (like web apps).
+The cloud based configuration of the flow manager enables GPII installations on local computers to communicate to the
+GPII architecture components and to retrieve the relevant lifecycle instructions filtered/mapped based on the user
+preferences, platform and application specification. This configuration of the flow manager is intended for the GPII
+apps on platforms without a locally running core framework.
-The expected payload contains the settings for the particular solution keyed by the solution id. Those settings are
-produced by the match maker that matches the supplied device/platform information along with the solutions installed with the
-current preferences set. These settings are transformed to be consumed by the actual application. For example:
+The expected payload contains the lifecycle instructions for the particular solution keyed by the solution id. Those
+instructions are produced by the MatchMaker that matches the supplied device/platform information along with the
+solutions installed with the current preference set. These settings are transformed to be consumed by the actual
+application. For example:
{
- "org.gnome.desktop.a11y.magnifier": {
- "mag-factor":2,
- "mouse-tracking":"mouse"
+ "http://registry.gpii.net/applications/com.texthelp.readWriteGold": {
+ "ApplicationSettings.AppBar.Width.$t": 788,
+ "ApplicationSettings.AppBar.ShowText.$t": true,
+ "ApplicationSettings.AppBar.optToolbarShowText.$t": true,
+ "ApplicationSettings.AppBar.LargeIcons.$t": true,
+ "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true,
+ "ApplicationSettings.Speech.optSAPI5Speed.$t": 50,
+ "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false
},
...
}
-### Run
+## Run
To run flow manager app simply type:
[NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/flowManager/configs/folder
- [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/gpii/configs/folder // That folder contains configs for other configurations that inlcude Flow Manager.
+ [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/gpii/configs/folder // That folder contains configs for other configurations that include Flow Manager.
- Default environment is development.
- Path to configs folder can be absolute or relative to the current user directory.
@@ -69,5 +83,5 @@ To run flow manager app simply type:
For example:
node node_modules/kettle/lib/init.js gpii/node_modules/flowManager/configs/
- NODE_ENV=production node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/flowManager/configs/
- NODE_ENV=fm.ps.sr.dr.mm.development node node_modules/kettle/lib/init.js gpii/configs/
+ NODE_ENV=production node node_modules/kettle/lib/init.js /Users/{gpiiKey}/universal/gpii/node_modules/flowManager/configs/
+ NODE_ENV=gpii.config.cloudBased.flowManager.production node node_modules/kettle/lib/init.js gpii/configs/
diff --git a/gpii/node_modules/flowManager/configs/base.json b/gpii/node_modules/flowManager/configs/base.json
deleted file mode 100644
index 30bcc0494..000000000
--- a/gpii/node_modules/flowManager/configs/base.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "modules": [
- "flowManager"
- ]
-}
diff --git a/gpii/node_modules/flowManager/configs/cloudBased.json b/gpii/node_modules/flowManager/configs/cloudBased.json
deleted file mode 100644
index 165f62ba3..000000000
--- a/gpii/node_modules/flowManager/configs/cloudBased.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "typeName": "flowManager.cloudBased",
- "options": {
- "components": {
- "server": {
- "options": {
- "logging": true,
- "components": {
- "flowManager": {
- "type": "gpii.flowManager.cloudBased",
- "options": {
- "typeName": "flowManager.cloudBased",
- "urls": {
- "preferences": "http://preferences.gpii.net/user/%token",
- "lifecycleManagerServer": "",
- "deviceReporter": "",
- "matchMaker": "http://localhost:8080/match"
- }
- }
- }
- }
- }
- }
- }
- },
- "includes": [
- "./production.json"
- ]
-}
diff --git a/gpii/node_modules/flowManager/configs/development.json b/gpii/node_modules/flowManager/configs/development.json
deleted file mode 100644
index 3e61fe5d9..000000000
--- a/gpii/node_modules/flowManager/configs/development.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "typeName": "flowManager.development",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": true,
- "port": 8081,
- "components": {
- "flowManager": {
- "type": "gpii.flowManager",
- "options": {
- "gradeNames": ["gpii.flowManager.dev"],
- "typeName": "flowManager.development",
- "urls": {
- "preferences": "http://localhost:%port/user/%token",
- "lifecycleManagerServer": "http://localhost:%port/%operation/%token",
- "deviceReporter": "http://localhost:%port/device",
- "matchMaker": "http://localhost:%port/match"
- }
- }
- }
- }
- }
- }
- }
- },
- "includes": [
- "./base.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5
new file mode 100644
index 000000000..30f65ca07
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5
@@ -0,0 +1,40 @@
+// This config contains common flow manager configurations that are shared by both Local Flow Manager and
+// Cloud Based Flow Manager.
+{
+ "type": "gpii.flowManager.config.base",
+ "options": {
+ "mainServerPort": 8081,
+ "distributeOptions": {
+ "flowManager.solutions": {
+ "record": {
+ "type": "gpii.flowManager.solutionsRegistry.dataSource",
+ "options": {
+ "gradeNames": "gpii.flowManager.solutionsRegistry.dataSource.moduleTerms",
+ "path": "%gpii-universal/testData/solutions/"
+ }
+ },
+ "target": "{that flowManager solutionsRegistryDataSource}"
+ }
+ },
+ "components": {
+ "server": {
+ "type": "kettle.server",
+ "options": {
+ "gradeNames": ["kettle.server.ws"],
+ "port": "{kettle.config}.options.mainServerPort",
+ "components": {
+ "flowManager": {
+ "type": "gpii.flowManager"
+ },
+ "ontologyHandler": {
+ "type": "gpii.ontologyHandler"
+ }
+ }
+ }
+ }
+ }
+ },
+ "require": [
+ "flowManager"
+ ]
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5
new file mode 100644
index 000000000..41c4663ec
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5
@@ -0,0 +1,41 @@
+{
+ "type": "gpii.flowManager.config.cloud.base",
+ "options": {
+ "dbServerPort": 25984,
+ "dataSourceConfig": {
+ "baseUrl": "http://localhost",
+ "port": "{kettle.config}.options.dbServerPort",
+ "dbName": "gpii"
+ },
+ "distributeOptions": {
+ "flowManager.base.dataStore": {
+ "source": "{that}.options.dataSourceConfig",
+ "target": "{that flowManager oauth2DataStore}.options.dataSourceConfig"
+ },
+ "flowManager.cloudGrades": {
+ "record": ["gpii.flowManager.cloudBased"],
+ "target": "{that flowManager}.options.gradeNames"
+ },
+ "flowManager.gpiiRevisionPath": {
+ "record": "%gpii-universal/gpii-revision.json",
+ "target": "{that flowManager}.options.gpiiRevisionPath"
+ },
+ "flowManager.envPort": {
+ "record": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_LISTEN_PORT)",
+ "target": "{that kettle.server}.options.port"
+ },
+ "flowManager.prefsServerDataSource.default": {
+ "record": "http://localhost:8081",
+ "target": "{that flowManager prefsServerDataSource}.options.prefsServerURL"
+ },
+ "flowManager.prefsServerDataSource.envUrl": {
+ "record": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL)",
+ "target": "{that flowManager prefsServerDataSource}.options.prefsServerURL",
+ "priority": "after:flowManager.prefsServerDataSource.default"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "./gpii.flowManager.config.base.json5"
+ ]
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5
new file mode 100644
index 000000000..b7814ffb0
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5
@@ -0,0 +1,14 @@
+{
+ "type": "gpii.flowManager.config.local.base",
+ "options": {
+ "distributeOptions": {
+ "flowManager.local": {
+ "record": ["gpii.flowManager.local"],
+ "target": "{that flowManager}.options.gradeNames"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "./gpii.flowManager.config.base.json5"
+ ]
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5
new file mode 100644
index 000000000..2fb91267a
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5
@@ -0,0 +1,13 @@
+// This production config that fetches default settings for reset from a remote url
+// when GPII runs in production config
+{
+ "type": "gpii.flowManager.config.remoteDefaultSettings.base",
+ "options": {
+ "distributeOptions": {
+ "flowManager.remoteDefaultSettings": {
+ "record": "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5",
+ "target": "{that gpii.flowManager.local defaultSettingsLoader}.options.defaultSettingsUrl"
+ }
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5
new file mode 100644
index 000000000..3ea68bcb0
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5
@@ -0,0 +1,12 @@
+// This config turns on the local flowManager flag "resetAtStart" to reset the system to default settings on system startup.
+{
+ "type": "gpii.flowManager.config.resetAtStart.base",
+ "options": {
+ "distributeOptions": {
+ "flowManager.resetAtStart": {
+ "record": true,
+ "target": "{that gpii.flowManager.local}.options.resetAtStart"
+ }
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5
new file mode 100644
index 000000000..5e58cd071
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5
@@ -0,0 +1,13 @@
+// This production config that hides /login, /logout, /proximityTriggered http endpoints
+// when GPII runs in production config
+{
+ "type": "gpii.flowManager.config.suppressHttpEndpoints.base",
+ "options": {
+ "distributeOptions": {
+ "flowManager.suppressHttpEndpoints": {
+ "record": true,
+ "target": "{that gpii.flowManager.local}.options.suppressHttpEndpoints"
+ }
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5
new file mode 100644
index 000000000..5ae7cfa1b
--- /dev/null
+++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5
@@ -0,0 +1,28 @@
+{
+ "type": "gpii.flowManager.config.untrusted.base",
+ "options": {
+ "gradeNames": "fluid.component",
+ "distributeOptions": {
+ "flowManager.untrustedGrades": {
+ "record": "gpii.flowManager.untrusted",
+ "target": "{that flowManager}.options.gradeNames"
+ },
+ "flowManager.defaultCloudURL": {
+ "record": "http://localhost:8084",
+ "target": "{that flowManager settingsDataSource}.options.cloudURL"
+ },
+ "flowManager.envCloudURL": {
+ "record": "@expand:kettle.resolvers.env(GPII_CLOUD_URL)",
+ "target": "{that flowManager settingsDataSource}.options.cloudURL",
+ "priority": "after:flowManager.defaultCloudURL"
+ },
+ "flowManager.clientCredentialFilePath": {
+ "record": "%gpii-universal/testData/clientCredentials/pilot.json",
+ "target": "{that flowManager settingsDataSource}.options.clientCredentialFilePath"
+ }
+ }
+ },
+ "mergeConfigs": [
+ "./gpii.flowManager.config.local.base.json5"
+ ]
+}
diff --git a/gpii/node_modules/flowManager/configs/kettleModuleLoader.js b/gpii/node_modules/flowManager/configs/kettleModuleLoader.js
deleted file mode 100644
index 20c0170a7..000000000
--- a/gpii/node_modules/flowManager/configs/kettleModuleLoader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*!
-Kettle Module Loader.
-
-Copyright 2012-2013 OCAD University
-
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
-
-You may obtain a copy of the License at
-https://github.com/gpii/kettle/LICENSE.txt
-*/
-
-// The purpose of this file is to be copied to a location for which the
-// require function is needed. It allows to find node modules relative
-// to that location that are otherwise non-resolvable.
-module.exports = require;
\ No newline at end of file
diff --git a/gpii/node_modules/flowManager/configs/production.json b/gpii/node_modules/flowManager/configs/production.json
deleted file mode 100644
index b846cdc3d..000000000
--- a/gpii/node_modules/flowManager/configs/production.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "typeName": "flowManager.production",
- "options": {
- "gradeNames": ["autoInit", "fluid.littleComponent"],
- "components": {
- "server": {
- "type": "kettle.server",
- "options": {
- "logging": false,
- "port": 8081,
- "components": {
- "flowManager": {
- "type": "gpii.flowManager",
- "options": {
- "typeName": "flowManager.production",
- "urls": {
- "preferences": "http://preferences.gpii.net/user/%token",
- "lifecycleManagerServer": "http://localhost:%port/%operation/%token",
- "deviceReporter": "http://localhost:%port/device",
- "matchMaker": "http://localhost:%port/match"
- }
- }
- }
- }
- }
- }
- }
- },
- "includes": [
- "./base.json"
- ]
-}
\ No newline at end of file
diff --git a/gpii/node_modules/flowManager/index.js b/gpii/node_modules/flowManager/index.js
index 84eb96187..b5dc91184 100644
--- a/gpii/node_modules/flowManager/index.js
+++ b/gpii/node_modules/flowManager/index.js
@@ -1,6 +1,12 @@
+"use strict";
+
var fluid = require("infusion");
-var loader = fluid.getLoader(__dirname);
+fluid.module.register("flowManager", __dirname, require);
-loader.require("./src/FlowManager.js");
-loader.require("./src/FlowManagerUtilities.js");
+require("./src/SystemUtils.js");
+require("./src/MatchMaking.js");
+require("./src/SessionAware.js");
+require("./src/FlowManager.js");
+require("./src/CloudBasedFlowManager.js");
+require("./src/SolutionsRegistryDataSource.js");
diff --git a/gpii/node_modules/flowManager/package.json b/gpii/node_modules/flowManager/package.json
index 751c701d5..d889e622a 100644
--- a/gpii/node_modules/flowManager/package.json
+++ b/gpii/node_modules/flowManager/package.json
@@ -1,19 +1,13 @@
{
- "name": "flowManager",
- "description": "Flow Manager handles communication between various data sources: user preferences, device reporter, configuration manager, etc.",
- "version": "0.1",
- "author": "GPII",
- "bugs": "http://wiki.gpii.net/index.php/Main_Page",
- "homepage": "http://gpii.net/",
- "dependencies": {},
- "licenses": [
- {
- "type": "BSD-3-Clause",
- "url": "http://www.opensource.org/licenses/BSD-3-Clause"
- }
- ],
- "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"],
- "repository": "git://github.com/GPII/universal.git",
- "main": "./index.js",
- "engines": { "node" : ">=0.1.9" }
-}
\ No newline at end of file
+ "name": "flowManager",
+ "description": "The GPII Flow Manager handles communication between various data sources: user preferences, device reporter, configuration manager, etc.",
+ "version": "0.3.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/flowManager/src/BrowserChannel.js b/gpii/node_modules/flowManager/src/BrowserChannel.js
new file mode 100644
index 000000000..728739ed8
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/BrowserChannel.js
@@ -0,0 +1,115 @@
+/*!
+ GPII BrowserChannel Handler
+
+ Copyright 2014, 2015 Emergya
+ Copyright 2015-2018 Raising the Floor - International
+ Copyright 2020 OCAD University
+
+ Licensed under the New BSD license. You may not use this file except in
+ compliance with this License.
+
+ You may obtain a copy of the License at
+ https://github.com/gpii/universal/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion");
+var gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.flowManager");
+
+fluid.defaults("gpii.flowManager.browserChannel", {
+ gradeNames: ["fluid.modelComponent"]
+});
+
+fluid.defaults("gpii.flowManager.browserChannel.handler", {
+ gradeNames: ["kettle.request.ws", "gpii.flowManager.sessionAware"],
+ listeners: {
+ onReceiveMessage: {
+ funcName: "gpii.flowManager.browserChannel.receiveMessage",
+ args: [
+ "{that}",
+ "{arguments}.1",
+ "{flowManager}.solutionsRegistryDataSource",
+ "{deviceReporter}.platformReporter"
+ ]
+ }
+ }
+});
+
+/**
+ * Send an error response for the request.
+ * @param {Component} request - An instance of gpii.flowManager.browserChannel.handler.
+ * @param {String} message - Error message text to include in the response.
+ */
+gpii.flowManager.browserChannel.sendError = function (request, message) {
+ fluid.log("Sending browserChannel error ", message);
+ var error = {
+ isError: true,
+ message: message
+ };
+ request.sendMessage(error);
+ // These codes bizarrely are not HTTP codes, but listed here: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
+ request.ws.close(1008, "Solution id not authorized");
+};
+
+/**
+ * Handler for all message types:
+ * - an initial "connect" message type establishes the connection and
+ * initializes the channel and its relationship with the WebSockets settings
+ * handler. This includes dynamically attaching the
+ * gpii.flowManager.browserChannel.receiveChangeSettingsMsg() listener to
+ * handle "changeSettings" message types after the connection is established.
+ * - any subsequent "connect" message types are silently ignored,
+ * - all other message types cause an error response and close the connection
+ * The one exception is the "changeSettings" message type (see first point).
+ * @param {Component} that - An instance of gpii.flowManager.browserChannel.handler.
+ * @param {Object} message - Object containing the message type and its payload.
+ * @param {Component} solutionsRegistryDataSource - Used to match the solution
+ * given in the payload.
+ * @param {Component} platformReporter - Used to determine the platform this
+ * is running on.
+ */
+gpii.flowManager.browserChannel.receiveMessage = function (that, message, solutionsRegistryDataSource, platformReporter) {
+ if (message.type !== "connect") {
+ return;
+ }
+ if (that.established) {
+ gpii.flowManager.browserChannel.sendError(that, "Connection already established - cannot send a second connect message");
+ }
+ var solutionId = message.payload.solutionId;
+ solutionsRegistryDataSource.get({os: platformReporter.reportPlatform().id}, function onSuccess(entries) {
+ if (!(solutionId in entries)) {
+ gpii.flowManager.browserChannel.sendError(that, "Rejecting a connection request from '" + solutionId +
+ "'. The solution id was not found in the solutions registry");
+ } else {
+ gpii.settingsHandlers.webSockets.instance.addClient(solutionId, that);
+ that.established = true;
+ that.solutionId = solutionId;
+ }
+ }, function (error) {
+ gpii.flowManager.browserChannel.sendError(that, error.message);
+ });
+};
+
+/**
+ * Listener for the "changeSettings" message type. This is added as a listener
+ * after the connection has been established. That is, this will not function
+ * without a previous "connect" message type.
+ * @param {Component} that - An instance of gpii.flowManager.browserChannel.handler.
+ * @param {Object} message - Object containing the message type and its payload.
+ */
+gpii.flowManager.browserChannel.receiveChangeSettingsMsg = function (that, message) {
+ if (message.type === "changeSettings" && that.established) {
+ var wsPayload = {};
+ wsPayload[that.solutionId] = [{
+ options: {
+ path: that.solutionId,
+ source: that
+ },
+ settings: message.payload.settings
+ }];
+ gpii.settingsHandlers.webSockets.set(wsPayload);
+ }
+};
diff --git a/gpii/node_modules/flowManager/src/Capture.js b/gpii/node_modules/flowManager/src/Capture.js
new file mode 100755
index 000000000..f87f2068f
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/Capture.js
@@ -0,0 +1,203 @@
+/**
+ * GPII Capture Component
+ *
+ * Copyright 2020 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/gpii/universal/LICENSE.txt
+ */
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.defaults("gpii.flowManager.capture", {
+ gradeNames: ["fluid.component"],
+ events: {
+ // Pseudoevents for transforming promise chains to fetch the solutions on the current device
+ // and capture the settings for the device.
+ onSolutionsForCurrentDevice: null,
+ onCaptureSettingsForCurrentDevice: null
+ },
+ listeners: {
+ // Begin declaration of Promise Chain for onSolutionsForCurrentDevice
+ "onSolutionsForCurrentDevice.getDeviceContextPromise": {
+ funcName: "gpii.lifecycleManager.getDeviceContextPromise",
+ args: ["{flowManager}.deviceReporter"],
+ priority: "first"
+ },
+ "onSolutionsForCurrentDevice.getSolutions": {
+ funcName: "gpii.flowManager.getSolutions",
+ args: [ "{flowManager}.solutionsRegistryDataSource", "{arguments}.0"],
+ priority: "after:getDeviceContextPromise"
+ },
+ "onSolutionsForCurrentDevice.solutionsRegistryEntriesToPromise": {
+ funcName: "fluid.toPromise",
+ args: ["{arguments}.0.solutionsRegistryEntries"],
+ priority: "after:getSolutions"
+ },
+ // Begin declaration of Promise Chain for onCaptureSettingsForCurrentDevice
+ "onCaptureSettingsForCurrentDevice.getInstalledSolutionsForCurrentDevice": {
+ func: "{that}.getInstalledSolutionsForCurrentDevice",
+ priority: "first"
+ },
+ "onCaptureSettingsForCurrentDevice.captureSystemSettings": {
+ funcName: "gpii.flowManager.capture.captureSystemSettings",
+ args: ["{lifecycleManager}.read", "{lifecycleManager}.getSession", "{arguments}.0", "{arguments}.1"], // solutionsRegistryEntries, options
+ priority: "after:getInstalledSolutionsForCurrentDevice"
+ },
+ "onCaptureSettingsForCurrentDevice.formatRawCapturedSettings": {
+ func: "gpii.flowManager.capture.formatRawCapturedSettings",
+ args: ["{arguments}.0"],
+ priority: "after:captureSystemSettings"
+ }
+ },
+ invokers: {
+ getInstalledSolutionsForCurrentDevice: {
+ funcName: "fluid.promise.fireTransformEvent",
+ args: ["{that}.events.onSolutionsForCurrentDevice"]
+ },
+ getSystemSettingsCapture: {
+ funcName: "fluid.promise.fireTransformEvent",
+ args: ["{that}.events.onCaptureSettingsForCurrentDevice", null, "{arguments}.0"] // options
+ }
+ }
+});
+
+/**
+ * Invoker `{gpii.flowManager.capture}.getInstalledSolutionsForCurrentDevice`
+ *
+ * @method
+ * @name {gpii.flowManager.capture}.getInstalledSolutionsForCurrentDevice
+ *
+ * This invoker method will return the solution registry entries, in their usual json format,
+ * that are available on the current device.
+ *
+ * @return {Promise} A promise resolved with an object of solutions registry entries available on the
+ * current device. As with the solutions registry itself, these are keyed by the solution ID.
+ */
+
+/**
+ * Invoker `{gpii.flowManager.capture}.getSystemSettingsCapture`
+ *
+ * @method
+ * @name {gpii.flowManager.capture}.getSystemSettingsCapture
+ *
+ * This main API entry point for capturing settings from a system or computer. This captures
+ * the actual settings on the device, so it assumed to be running in a local untrusted flow
+ * manager.
+ *
+ * @param {Object} options - Options for this chain.
+ * @param {Array} options.solutionsList - An array of solution IDs to filter by when
+ * retreiving settings. If this option is not included, all available settings will be
+ * returned. ex: `["com.microsoft.windows.mouseSettings", "com.freedomscientific.jaws"]`.
+ * @return {Promise} A promise resolved with the payload of captured system settings.
+ */
+
+/**
+ * Runs through all the solutions currently available on the system, pulls the current
+ * setting for each supportedSetting and returns them in an object. Primary use case
+ * is for backing Capture tools that would allow a user to set up their GPII profile
+ * starting with the current settings for their applications on the local machine.
+ *
+ * @param {Function|gpii.lifecycleManager.read} readSettingsFunc - lifecycleManager.read (or suitable implementation),
+ * that takes solution registry entries, reads their current values on the device, and returns a promise resolved to
+ * them.
+ * @param {Function|gpii.lifecycleManager.getSession} getSession - Function to return the current GPII session which
+ * has a `localResolver` we can use for expanding material.
+ * @param {Object} solutions - Solutions registry entries for solutions available on the current machine.
+ * @param {Object} options - Extra options for processing.
+ * @param {Array} options.solutionsList - If provided, only solutions in this list of `solutionsID`s will
+ * be captured. Example:
+ *
+ * '''json
+ * ["com.microsoft.windows.cursors", "com.freedomscientific.jaws"]
+ * '''
+ * @return {fluid.promise} Returns a promise resolving with the entire system settings capture.
+ */
+gpii.flowManager.capture.captureSystemSettings = function (readSettingsFunc, getSession, solutions, options) {
+ var solutionsToFetch = fluid.copy(solutions);
+ if (options.solutionsList) {
+ fluid.remove_if(solutionsToFetch, function (solution, solutionID) {
+ return !options.solutionsList.includes(solutionID);
+ });
+ }
+
+ var expandedSolutions = getSession().localResolver(solutionsToFetch);
+ return readSettingsFunc(expandedSolutions);
+};
+
+/**
+ * The raw return payload from the capture promise sequence looks like:
+ * '''json
+ * [
+ * {
+ * "fakemag1": [
+ * {
+ * "settings": {
+ * "magnification": 2
+ * }
+ * }
+ * ]
+ * },
+ * {
+ * "fakemag1": [
+ * {
+ * "settings": {
+ * "invert": true
+ * }
+ * }
+ * ]
+ * },
+ * {
+ * "fakemag2": [
+ * {
+ * "settings": {
+ * "magnification": 2,
+ * "invert": true
+ * }
+ * }
+ * ]
+ * }
+ * ]
+ * '''
+ *
+ * and we want:
+ * '''json
+ * {
+ * "fakemag1": {
+ * "magnification": 2,
+ * "invert": true
+ * },
+ * "fakemag2": {
+ * "magnification": 2,
+ * "invert": true
+ * }
+ * }
+ * '''
+ *
+ * @param {Object} data - The raw captured data.
+ * @return {Object} Returns a new payload with collapsed data, and multiple settings handler
+ * results for the same solution merged together.
+ */
+gpii.flowManager.capture.formatRawCapturedSettings = function (data) {
+ var togo = {};
+ fluid.each(data, function (sequenceItem) {
+ if (sequenceItem.isError) {
+ fluid.log("Error capturing settings for: ", sequenceItem);
+ return;
+ }
+ fluid.each(sequenceItem, function (item, key) {
+ if (!togo[key]) {
+ togo[key] = {};
+ }
+ fluid.each(fluid.get(item, [0, "settings"]), function (value, settingId) {
+ togo[key][settingId] = value;
+ });
+ });
+ });
+ return togo;
+};
diff --git a/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js b/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js
new file mode 100644
index 000000000..de64b0517
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js
@@ -0,0 +1,136 @@
+/**
+ * GPII Cloud-Based FlowManager
+ *
+ * Copyright 2013 OCAD University
+ * Copyright 2017-2019 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+
+require("./PrefsServerDataSource.js");
+require("./HealthGetHandler.js");
+require("./ReadyGetHandler.js");
+require("./SettingsGetHandler.js");
+require("./SettingsPutHandler.js");
+require("./RevisionGetHandler.js");
+
+require("gpii-oauth2");
+
+/**
+ * OAUTH2 SECURED CLOUD-BASED FLOW MANAGER
+ */
+
+fluid.defaults("gpii.flowManager.cloudBased", {
+ gradeNames: ["fluid.contextAware"],
+ contextAwareness: {
+ cloudStatus: {
+ checks: {
+ addCloudStatusOption: {
+ contextValue: "{preferencesServer}",
+ gradeNames: null
+ }
+ },
+ defaultGradeNames: "gpii.flowManager.cloudBased.cloudStatus"
+ }
+ },
+ // JSON file containing the full sha256 of the revision of the repository for
+ // this CBFM, e.g., { "sha256": "86a83d2f93a6f8f954a4fef618ca6aea1399c980" }
+ // When CI is triggered to build a Docker image of gpii-universal, the
+ // scripts and Dockerfile determine the sha256 and write it to this file.
+ // CI names it "gpii-revision.json" and puts it in the root directory.
+ // Developers have the option of putting it elsewhere -- the
+ // "gpiiRevisionPath" is distributed down from a configuration such as
+ // gpii.flowManager.config.cloud.base
+ gpiiRevisionPath: null,
+ components: {
+ prefsServerDataSource: {
+ type: "gpii.flowManager.prefsServerDataSource"
+ },
+ oauth2DataStore: {
+ type: "gpii.dbOperation.dbDataStore"
+ },
+ authServer: {
+ // note that this subcomponent directly attaches express routes to our underlying express app via its direct API -
+ // e.g. at the paths /access_token
+ type: "gpii.oauth2.authServer",
+ options: {
+ components: {
+ dataStore: "{gpii.dbOperation.dataStore}"
+ },
+ members: {
+ expressApp: "{kettle.server}.expressApp"
+ },
+ events: {
+ onContributeMiddleware: "{kettle.server}.events.onContributeMiddleware",
+ onContributeRouteHandlers: "{kettle.server}.events.onContributeRouteHandlers"
+ }
+ }
+ },
+ authGrantFinder: {
+ type: "gpii.oauth2.authGrantFinder"
+ }
+ },
+ distributeOptions: {
+ setDataStoreOnAuthGrantFinder: {
+ record: "{gpii.dbOperation.dataStore}",
+ target: "{that gpii.oauth2.authorizationService}.options.components.dataStore"
+ }
+ },
+ requestHandlers: {
+ // The endpoint to request user settings
+ settingsGet: {
+ route: "/:gpiiKey/settings/:device",
+ method: "get",
+ type: "gpii.flowManager.cloudBased.settings.get.handler"
+ },
+ // The endpoint to update user settings
+ settingsPut: {
+ route: "/:gpiiKey/settings",
+ method: "put",
+ type: "gpii.flowManager.cloudBased.settings.put.handler"
+ },
+ // The endpoint to request the revision of the repository used for this
+ // CPFM
+ revisionGet: {
+ route: "/revision",
+ method: "get",
+ type: "gpii.flowManager.cloudBased.gpiiRevision.handler"
+ }
+ }
+});
+
+// The addon grade that adds the liveness (/ready) and the readiness (/health) endpoints to the cloud based flow manager only
+// when it runs as a separate server from the preferences server. /ready endpoint on CBFM will ping the /ready endpoint
+// on the preferences server to ensure the preferences server is ready to handle requests. When the cloud based flow manager
+// and preferences server are running as one server, this addon grade will NOT be added to CBFM, instead, the /ready and /health
+// endpoints from the preferenes server are used, where the /ready endpoint of the preferences server will check the db
+// connection.
+fluid.defaults("gpii.flowManager.cloudBased.cloudStatus", {
+ gradeNames: ["fluid.component"],
+ requestHandlers: {
+ // The endpoint to check the readiness of the cloud based flow manager: the server is ready but does not check the database connection.
+ healthGet: {
+ route: "/health",
+ method: "get",
+ type: "gpii.health.handler"
+ },
+ // The endpoint to check the liveness of the cloud based flow manager: both the server and the database connection is ok
+ readyGet: {
+ route: "/ready",
+ method: "get",
+ type: "gpii.flowManager.cloudBased.ready.handler"
+ }
+ }
+});
diff --git a/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js b/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js
new file mode 100644
index 000000000..8abee922d
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js
@@ -0,0 +1,157 @@
+/*!
+ * Default Settings Loader
+ *
+ * Copyright 2018 OCAD University
+ * Copyright 2020 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = fluid || require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ fs = require("fs"),
+ path = require("path"),
+ JSON5 = require("json5");
+
+// The default settings loader reads default settings thru these steps:
+// 1. Fetch default settings from the remote URL;
+// 2. If the remote fetch fails, read default settings from the local system settings directory. In the case that the
+// default settings file doesn't exists in the system settings directory, copy it from the code base to the settings
+// directory.
+// If the default settings file is not found at both places, return undefined.
+fluid.defaults("gpii.defaultSettingsLoader", {
+ gradeNames: ["fluid.component"],
+ // The path to the file that has all QSS supported settings.
+ defaultSettingsInCodeBase: "%gpii-universal/testData/defaultSettings/defaultSettings.json5",
+ // The url to the remote default settings file. Must be provided by integrators.
+ defaultSettingsUrl: null,
+ members: {
+ gpiiSettingsDir: "@expand:{settingsDir}.getGpiiSettingsDir()",
+ // Populated by "onCreate.prepareSettingsFile" listener. It points to the default settings file in
+ // the settings directory rather than defaultSettingsInCodeBase, the default settings file in the code base.
+ defaultSettingsFile: null
+ },
+ components: {
+ settingsDir: {
+ type: "gpii.settingsDir"
+ },
+ remoteDefaultSettingsDataSource: {
+ type: "kettle.dataSource.URL",
+ options: {
+ url: "{defaultSettingsLoader}.options.defaultSettingsUrl",
+ components: {
+ encoding: {
+ type: "kettle.dataSource.encoding.JSON5"
+ }
+ }
+ }
+ }
+ },
+ events: {
+ onGet: null
+ },
+ listeners: {
+ "onCreate.prepareSettingsFile": {
+ listener: "fluid.set",
+ args: ["{that}", ["defaultSettingsFile"], "@expand:gpii.defaultSettingsLoader.prepareSettingsFile({that}.options.defaultSettingsInCodeBase,{that}.gpiiSettingsDir)"]
+ },
+
+ // The promise chain to fetch default settings:
+ // 1. If defaultSettingsUrl is provided, the first attempt is to fetch from the remote url;
+ // 2. If the fetch remote fails, perform the first fallback to read from the local system settings directory;
+ // 3. If the read from the local fails, return undefined.
+ "onGet.getRemoteDefaultSettings": {
+ listener: "gpii.defaultSettingsLoader.getRemoteDefaultSettings",
+ args: ["{that}.options.defaultSettingsUrl", "{that}.remoteDefaultSettingsDataSource"],
+ priority: "first"
+ },
+ "onGet.getLocalDefaultSettings": {
+ listener: "gpii.defaultSettingsLoader.getLocalDefaultSettings",
+ args: ["{arguments}.0", "{that}.defaultSettingsFile"],
+ priority: "after:getRemoteDefaultSettings"
+ }
+ },
+ invokers: {
+ get: {
+ funcName: "fluid.promise.fireTransformEvent",
+ args: ["{that}.events.onGet"]
+ }
+ }
+});
+
+/**
+ * Return the path to the default settings file in the settings directory. If it doesn't yet exist,
+ * copy the default settings file from the code base to the setting dir then return the path.
+ *
+ * @param {String} defaultSettingsInCodeBase - The path to the default settings file in the universal code base.
+ * @param {String} gpiiSettingsDir - GPII settings directory.
+ * @return {String} Return the path to the default settings file in the settings dir.
+ */
+gpii.defaultSettingsLoader.prepareSettingsFile = function (defaultSettingsInCodeBase, gpiiSettingsDir) {
+ var defaultSettingsInCodeBaseFullPath = fluid.module.resolvePath(defaultSettingsInCodeBase);
+
+ // Return undefined if the default settings file does not exist in the code base
+ if (!fs.existsSync(defaultSettingsInCodeBaseFullPath)) {
+ return undefined;
+ }
+
+ var gpiiSettingsAbsolutePath = fluid.module.resolvePath(gpiiSettingsDir);
+
+ var defaultSettingsFileName = path.basename(defaultSettingsInCodeBaseFullPath);
+ var defaultSettingsFile = path.join(gpiiSettingsAbsolutePath, defaultSettingsFileName);
+
+ // Copy the default settings file to the settings dir if it doesn't already in there
+ if (!fs.existsSync(defaultSettingsFile)) {
+ fs.writeFileSync(defaultSettingsFile, fs.readFileSync(defaultSettingsInCodeBaseFullPath, "utf-8"));
+ }
+
+ return defaultSettingsFile;
+};
+
+/**
+ * Read default settings from the remote URL
+ *
+ * @param {String} url - The url pointing to the remote default settings file.
+ * @param {Object} remoteDefaultSettingsDataSource - The content fetched from the remote default settings url.
+ * @return {Object} The content of default settings fetched from the remote URL. If the fetch fails,
+ * return undefined.
+ */
+gpii.defaultSettingsLoader.getRemoteDefaultSettings = function (url, remoteDefaultSettingsDataSource) {
+ if (!url) {
+ return undefined;
+ } else {
+ var promiseTogo = fluid.promise();
+ var remoteContentPromise = remoteDefaultSettingsDataSource.get();
+
+ remoteContentPromise.then(function (remoteDefaultSettings) {
+ promiseTogo.resolve(remoteDefaultSettings);
+ }, function () {
+ // If the fetch fails, return undefined.
+ promiseTogo.resolve(undefined);
+ });
+ return promiseTogo;
+ }
+};
+
+/**
+ * The fall back: read default settings from the local file system
+ *
+ * @param {Object} remoteDefaultSettings - The content fetched from the remote default settings url.
+ * @param {String} defaultSettingsFile - The path to the local default settings file.
+ * @return {Object} Default settings.
+ */
+gpii.defaultSettingsLoader.getLocalDefaultSettings = function (remoteDefaultSettings, defaultSettingsFile) {
+ var promiseTogo = fluid.promise();
+ if (remoteDefaultSettings) {
+ promiseTogo.resolve(remoteDefaultSettings);
+ } else {
+ promiseTogo.resolve(defaultSettingsFile ? JSON5.parse(fs.readFileSync(defaultSettingsFile, "utf-8")) : undefined);
+ }
+ return promiseTogo;
+};
diff --git a/gpii/node_modules/flowManager/src/FlowManager.js b/gpii/node_modules/flowManager/src/FlowManager.js
index 6bd3d9158..61fc345d3 100644
--- a/gpii/node_modules/flowManager/src/FlowManager.js
+++ b/gpii/node_modules/flowManager/src/FlowManager.js
@@ -1,125 +1,390 @@
-/*!
-GPII Flow Manager
-
-Copyright 2012 OCAD University
-
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
-
-You may obtain a copy of the License at
-https://github.com/gpii/universal/LICENSE.txt
-*/
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- path = require("path"),
- gpii = fluid.registerNamespace("gpii");
-
- fluid.require("kettle", require);
- fluid.require("./UserLogin.js", require);
- fluid.require("./Settings.js", require);
- fluid.require("./UserLogout.js", require);
-
- fluid.require("lifecycleManager", require);
-
- fluid.defaults("gpii.flowManager", {
- gradeNames: ["kettle.app", "autoInit"],
- urls: {
- preferences: "",
- deviceReporter: "",
- matchMaker: "",
- solutionsReporter: "",
- lifecycleManagerServer: ""
- },
- root: path.join(__dirname, ".."),
- components: {
- preferencesDataSource: {
- type: "kettle.dataSource.URL",
- options: {
- gradeNames: ["kettle.dataSource.simpleCallbackWrapper"],
- url: "{gpii.flowManager}.options.urls.preferences",
- termMap: {
- token: "%token"
+/*
+ * GPII Flow Manager
+ *
+ * Copyright 2012 OCAD University
+ * Copyright 2017-2019 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+require("kettle");
+
+require("./UserLogonHandlers.js");
+require("./BrowserChannel.js");
+require("./DefaultSettingsLoader.js");
+require("./PSPChannel.js");
+require("./SettingsDataSource.js");
+require("./UntrustedFlowManager.js");
+require("./Capture.js");
+
+require("preferencesServer");
+require("lifecycleManager");
+require("deviceReporter");
+require("transformer");
+require("journal");
+require("singleInstance");
+require("eventLog");
+require("gpii-user-errors");
+
+fluid.defaults("gpii.flowManager", {
+ gradeNames: ["kettle.app"],
+ components: {
+ // TODO: Make this use the solutions registry data source by default?
+ solutionsRegistryDataSource: {
+ type: "kettle.dataSource",
+ options: {
+ termMap: {
+ "os": "%os",
+ "version": "%version"
+ },
+ components: {
+ encoding: {
+ type: "kettle.dataSource.encoding.none"
}
}
- },
- deviceReporterDataSource: {
- type: "kettle.dataSource.URL",
- options: {
- gradeNames: ["kettle.dataSource.simpleCallbackWrapper"],
- url: "{gpii.flowManager}.options.urls.deviceReporter"
+ }
+ },
+ matchMakerFramework: {
+ type: "gpii.matchMakerFramework"
+ },
+ browserChannel: {
+ type: "gpii.flowManager.browserChannel"
+ },
+ // persistent validation component for settings.
+ prefsValidator: {
+ type: "gpii.universal.solutionsRegistry.validators.preferences"
+ }
+ }
+});
+
+
+// Mixin grades for the FlowManager
+
+fluid.defaults("gpii.flowManager.local", {
+ gradeNames: ["fluid.contextAware"],
+ // The "resetAtStart" flag is to indicate whether to reset the system to the default settings on system startup.
+ // It is turned on in production configs.
+ resetAtStart: false,
+ // The "suppressHttpEndpoints" flag is to indicate whether to hide un-needed localhost http endpoints.
+ // It is turned on in production configs.
+ suppressHttpEndpoints: false,
+ members: {
+ /* The resolved value is an object containing data for performing reset actions:
+ {
+ defaultSettings: {Object}, // The content of the reset to default file
+ defaultLifecycleInstructions: {Object}, // For the reset action at key-in
+ defaultSnapshot: {Object} // For reset actions at system startup and explicit reset request
+ }
+ "defaultSnapshot" and "defaultLifecycleInstructions" structures are generated for performing
+ "reset to system default settings" functionality that is applied when:
+ 1. reset to default settings on system startup using lifecycleManager.restoreSnapshot();
+ 2. at receiving request "/user/reset/login" to logout the current key and reset to default settings.
+ This reset also uses lifecycleManager.restoreSnapshot();
+ 3. at any keyin. The settings for the keyin will be combined with default settings, then apply using
+ lifecycleManager.start(lifecycleInstructions).
+
+ "defaultSnapshot" is used at case 1 & 2 while "defaultLifecycleInstructions" is used at case 3.
+
+ The difference between "defaultLifecycleInstructions" and "defaultSnapshot" is the "settings" block for each
+ solution:
+ * "defaultSnapshot": the snapshot structure expected by lifecycleManager.restoreSnapshot():
+ {
+ "settings": {
+ "masterVolume": {
+ "type": "ADD",
+ "value": 75
+ }
+ }
+ }
+ * "defaultLifecycleInstructions": the lifecycle instruction structure expected by lifecycleManager.start():
+ {
+ "settings": {
+ "masterVolume": 75
+ }
+ }
+ */
+ defaultSettingsDataPromise: "@expand:fluid.promise()"
+ },
+ contextAwareness: {
+ suppressHttpEndpoints: {
+ checks: {
+ addHttpEndpointsOption: {
+ contextValue: "{gpii.flowManager.local}.options.suppressHttpEndpoints",
+ gradeNames: null
}
},
- matchMakerDataSource: {
- type: "kettle.dataSource.URL",
- options: {
- gradeNames: ["kettle.dataSource.simpleCallbackWrapper"],
- url: "{gpii.flowManager}.options.urls.matchMaker",
- writable: true
- }
+ defaultGradeNames: "gpii.flowManager.local.requestHandlers"
+ }
+ },
+ distributeOptions: {
+ // Distribute the grade to handle user logon requests.
+ // In all-in-local config, the handler is "gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler".
+ // In untrusted config, the handler should be "gpii.lifecycleManager.untrusted.stateChangeHandler".
+ "lifecycleManager.loginRequestHandler": {
+ record: "gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler",
+ target: "{that gpii.lifecycleManager.loginRequest}.options.gradeNames"
+ },
+ "userLogonHandling.defaultSettingsDataPromise": {
+ record: "{gpii.flowManager.local}.defaultSettingsDataPromise",
+ target: "{that gpii.lifecycleManager.userLogonHandling.stateChangeHandler}.options.members.defaultSettingsDataPromise"
+ },
+ "lifecycleManager.privateMatchMaker": {
+ record: {
+ privateMatchMaker: "{gpii.flowManager.local}.privateMatchMaker"
},
- lifecycleManagerServerDataSource: {
- type: "kettle.dataSource.URL",
- options: {
- gradeNames: ["kettle.dataSource.simpleCallbackWrapper"],
- url: "{gpii.flowManager}.options.urls.lifecycleManagerServer",
- writable: true,
- termMap: {
- operation: "%operation",
- token: "%token"
- }
+ target: "{that lifecycleManager}.options.components"
+ },
+ "userListeners.lifecycleManager": {
+ record: "{gpii.flowManager.local}.lifecycleManager",
+ target: "{that gpii.userListener}.options.components.lifecycleManager"
+ },
+ "lifecycleManager.pspChannel.sessionBinder": {
+ record: "gpii.pspChannel.sessionBinder",
+ target: "{that lifecycleManager gpii.lifecycleManager.userSession}.options.gradeNames"
+ }
+ },
+ components: {
+ lifecycleManager: {
+ type: "gpii.lifecycleManager",
+ options: {
+ components: {
+ userErrors: "{gpii.flowManager.local}.userErrors"
}
}
},
- handlers: {
- userLogin: {
- route: "/user/:token/login",
- type: "get"
- },
- userLogout: {
- route: "/user/:token/logout",
- type: "get"
+ privateMatchMaker: {
+ type: "gpii.lifecycleManager.privateMatchMaker"
+ },
+ deviceReporter: {
+ type: "gpii.deviceReporter"
+ },
+ journal: {
+ type: "gpii.journal",
+ options: {
+ gradeNames: ["gpii.journalLifecycleManager", "gpii.journalApp"]
}
+ },
+ eventLog: {
+ type: "gpii.eventLog",
+ options: {
+ gradeNames: ["gpii.metrics", "gpii.metrics.lifecycle"]
+ }
+ },
+ capture: {
+ type: "gpii.flowManager.capture"
+ },
+ userListeners: {
+ type: "gpii.userListeners"
+ },
+ webSocketsSettingsHandlerComponent: {
+ type: "gpii.settingsHandlers.webSockets.component"
+ },
+ pspChannel: {
+ type: "gpii.pspChannel"
+ },
+ defaultSettingsLoader: {
+ type: "gpii.defaultSettingsLoader"
+ },
+ userErrors: {
+ type: "gpii.userErrors"
}
- });
-
- fluid.defaults("gpii.flowManager.dev", {
- gradeNames: ["autoInit", "fluid.littleComponent"],
- distributeOptions: {
- source: "{that}.options.urlExpanderGradeNames",
- target: "{that urlExpander}.options.gradeNames"
+ },
+ requestHandlers: {
+ browserChannel: {
+ route: "/browserChannel",
+ type: "gpii.flowManager.browserChannel.handler"
+ },
+ pspChannel: {
+ route: "/pspChannel",
+ type: "gpii.pspChannel.handler"
+ }
+ },
+ invokers: {
+ savePreferences: {
+ funcName: "gpii.flowManager.local.savePreferences",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ }
+ },
+ events: {
+ preferencesSavedSuccess: null,
+ preferencesSavedError: null,
+ noUserLoggedIn: null,
+ getDefaultSettingsData: null,
+ defaultSettingsDataLoaded: null,
+ flowManagerReady: {
+ events: {
+ "defaultSettingsDataLoaded": "defaultSettingsDataLoaded",
+ "kettleReady": "{kettle.server}.events.onListen"
+ }
+ },
+ // Fired only when:
+ // 1. The flag for resetting to default settings on system start, that.options.resetAtStart, is turned on;
+ // 2. The default settings are defined. See documentation/ResetComputer.md.
+ resetAtStartSuccess: null,
+ resetAtStartError: null
+ },
+ listeners: {
+ "onCreate.registerInstance": "gpii.singleInstance.registerInstance",
+ "onCreate.mountWebSocketsSettingsHandler": {
+ funcName: "gpii.flowManager.local.mountWebSocketsSettingsHandler",
+ args: ["{webSocketsSettingsHandlerComponent}"]
},
- urlExpanderGradeNames: ["kettle.urlExpander.development"]
- });
- fluid.defaults("gpii.flowManager.cloudBased", {
- gradeNames: ["autoInit", "gpii.flowManager"],
- mergePolicy: {
- handleUnsupported: "noexpand"
+ // Fire "getDefaultSettingsData" event to trigger promise transform chain that does:
+ // 1. Read default settings from the reset to default file;
+ // 2. Calculate defaultLifecycleInstructions and defaultSnapshot based on the default settings;
+ // 3. All default settings data are held in flowManager.defaultSettingsDataPromise.
+ // Note: all of this machinery should be removed once we can update to the FLUID-6148 framework and we can
+ // put all of this I/O into the "resources" block of a fluid.resourceLoader.
+ "onCreate.calculateDefaultSettingsData": {
+ listener: "fluid.promise.fireTransformEvent",
+ args: ["{that}.events.getDefaultSettingsData"]
},
- components: {
- deviceReporterDataSource: {
- type: "fluid.emptySubcomponent"
- },
- lifecycleManagerServerDataSource: {
- type: "fluid.emptySubcomponent"
- }
+ "getDefaultSettingsData.loadDefaultSettngs": {
+ listener: "{defaultSettingsLoader}.get",
+ priority: "first"
},
- typeName: "flowManager.cloudBased",
- urls: {
- deviceReporter: "",
+ "getDefaultSettingsData.convertDefaultSettings": {
+ listener: "gpii.flowManager.convertDefaultSettingsToSnapshot",
+ args: ["{that}", "{arguments}.0", "{that}.events.defaultSettingsDataLoaded"],
+ priority: "after:loadDefaultSettngs"
},
- handlers: {
- settings: {
- route: "/:token/settings/:device",
- type: "get"
- },
- userLogin: null,
- userLogout: null
+
+ "flowManagerReady.loginWithNoUser": {
+ listener: "gpii.flowManager.local.noUserLoggedIn",
+ args: ["{lifecycleManager}", "{that}.events.noUserLoggedIn"]
+ },
+ "noUserLoggedIn.resetAtStart": {
+ listener: "gpii.flowManager.local.resetAtStart",
+ args: ["{lifecycleManager}", "{that}.options.resetAtStart", "{that}.defaultSettingsDataPromise", "{that}.events.resetAtStartSuccess", "{that}.events.resetAtStartError"]
+ },
+ "afterDestroy.deregisterInstance": "gpii.singleInstance.deregisterInstance",
+ "{lifecycleManager}.events.onAutoSaveRequired": {
+ namespace: "savePreferences",
+ listener: "{that}.savePreferences"
+ }
+ }
+});
+
+gpii.flowManager.local.mountWebSocketsSettingsHandler = function (webSocketsSettingsHandlerComponent) {
+ gpii.settingsHandlers.webSockets.instance = webSocketsSettingsHandlerComponent;
+};
+
+gpii.flowManager.local.noUserLoggedIn = function (lifecycleManager, noUserLoggedInEvent) {
+ var promise = lifecycleManager.addToUserLogonRequestQueue({
+ gpiiKey: "noUser",
+ logonState: "login"
+ });
+ promise.then(noUserLoggedInEvent.fire);
+};
+
+gpii.flowManager.convertDefaultSettingsToSnapshot = function (that, defaultSettings, defaultSettingsDataCreatedEvent) {
+ var promiseTogo = fluid.promise();
+ if (defaultSettings) {
+ var matchMakingPromise = that.privateMatchMaker.doMatch("noUser", defaultSettings);
+ matchMakingPromise.then(function (payload) {
+ var activePrefsSetName = payload.activePrefsSetName || "gpii-default";
+ // convert the matchmaker output to lifecycle instructions
+ var defaultLifecycleInstructions = gpii.transformer.configurationToSettings(payload.matchMakerOutput.inferredConfiguration[activePrefsSetName], payload.solutionsRegistryEntries);
+ // convert lifecycle instructions to snapshots
+ var defaultSnapshot = fluid.transform(defaultLifecycleInstructions, function (handlerResponse) {
+ return gpii.settingsHandlers.settingsPayloadToChanges(handlerResponse);
+ });
+ promiseTogo.resolve({
+ defaultSettings: defaultSettings,
+ defaultLifecycleInstructions: defaultLifecycleInstructions,
+ defaultSnapshot: defaultSnapshot
+ });
+ defaultSettingsDataCreatedEvent.fire();
+ });
+ } else {
+ promiseTogo.resolve({});
+ defaultSettingsDataCreatedEvent.fire();
+ }
+ fluid.promise.follow(promiseTogo, that.defaultSettingsDataPromise);
+};
+
+gpii.flowManager.local.resetAtStart = function (lifecycleManager, resetAtStart, defaultSettingsDataPromise, resetSuccessEvent, resetErrorEvent) {
+ defaultSettingsDataPromise.then(function (defaultSettingsData) {
+ var defaultSnapshot = defaultSettingsData.defaultSnapshot;
+ if (resetAtStart && defaultSnapshot) {
+ fluid.log("Resetting on system startup to default settings: ", defaultSnapshot);
+ var resetPromise = lifecycleManager.restoreSnapshot(defaultSnapshot);
+ resetPromise.then(function () {
+ fluid.log("Resetting on system startup completes successfully.");
+ resetSuccessEvent.fire();
+ }, function (error) {
+ fluid.log("Resetting on system startup fails with the error: ", error);
+ resetErrorEvent.fire(error);
+ });
+ } else {
+ resetSuccessEvent.fire();
}
});
+};
+
+/**
+ * Auto save updated preferences received from PSP or QSS.
+ * @param {Component} that - An instance of gpii.flowManager.local.
+ * @param {String} [gpiiKey] - [optional] The GPII key that the auto-saved preferences associates with.
+ * @param {Object} [preferences] - [optional] The preferences to be auto saved to the cloud.
+ * Note: when there isn't any preference to be auto saved, gpiiKey and preferences should not be provided.
+ * In this case, preferencesSavedSuccess event will still be fired, which will trigger the firing of its parent
+ * aggregate event preferencesApplied for the auto save process to proceed.
+ */
+gpii.flowManager.local.savePreferences = function (that, gpiiKey, preferences) {
+ // Don't support the save when the active user is "noUser"
+ if (gpiiKey && preferences && gpiiKey !== "noUser") {
+ if (that.setSettings) {
+ // With untrusted config where that.setSettings() is defined, auto save updated preferences that are allowed
+ // to be auto saved.
+ var savePromise = that.setSettings(gpiiKey, preferences);
+ savePromise.then(that.events.preferencesSavedSuccess.fire, that.events.preferencesSavedError.fire);
+ }
+ else {
+ // With trusted config where that.setSettings() is not defined, fire preferencesSavedSuccess() for the
+ // workflow to proceed.
+ that.events.preferencesSavedSuccess.fire();
+ }
+ } else {
+ // When the updated preferences are not allowed to be auto saved, fire success event automatically
+ that.events.preferencesSavedSuccess.fire();
+ }
+};
-})();
\ No newline at end of file
+/**
+ * The add-on grade containing http endpoints that will be attached to the local flow manager when the
+ * "suppressHttpEndpoints" flag is turned on.
+ */
+fluid.defaults("gpii.flowManager.local.requestHandlers", {
+ requestHandlers: {
+ userLogin: {
+ route: "/user/:gpiiKey/login",
+ method: "get",
+ type: "gpii.flowManager.userLogin.handler"
+ },
+ userLogout: {
+ route: "/user/:gpiiKey/logout",
+ method: "get",
+ type: "gpii.flowManager.userLogout.handler"
+ },
+ proximityTriggered: {
+ route: "/user/:gpiiKey/proximityTriggered",
+ method: "get",
+ type: "gpii.flowManager.proximityTrigger.handler"
+ }
+ }
+});
diff --git a/gpii/node_modules/flowManager/src/FlowManagerUtilities.js b/gpii/node_modules/flowManager/src/FlowManagerUtilities.js
deleted file mode 100644
index 6e15daa61..000000000
--- a/gpii/node_modules/flowManager/src/FlowManagerUtilities.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- GPII Flow Manager Utilities.
-
- Copyright 2012 OCAD University
-
- Licensed under the New BSD license. You may not use this file except in
- compliance with this License.
-
- You may obtain a copy of the License at
- https://github.com/gpii/universal/LICENSE.txt
-*/
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- request = fluid.registerNamespace("gpii.request.flowManager");
-
- request.logAndNotify = function (msg, event, callback) {
- return function (data) {
- fluid.log(msg, data);
- event.fire(callback ? callback(data) : data);
- };
- };
-
- request.getPreferences = function (preferencesDataSource, event, token) {
- preferencesDataSource.get({
- token: token
- }, request.logAndNotify("Fetched user preferences: ", event));
- };
-
- request.getDevice = function (deviceReporterDataSource, event) {
- deviceReporterDataSource.get(null, request.logAndNotify(
- "Fetched device reporter data: ", event));
- };
-
- request.getMatch = function (matchMakerDataSource, event, preferences, device) {
- matchMakerDataSource.set(null, {
- preferences: preferences,
- device: device
- }, request.logAndNotify(
- "Matched preferences and device reporter data: ", event));
- };
-
- fluid.defaults("gpii.request.flowManager.token", {
- gradeNames: ["fluid.eventedComponent", "autoInit"],
- invokers: {
- getPreferences: {
- funcName: "gpii.request.flowManager.getPreferences",
- args: ["{flowManager}.preferencesDataSource", "{that}.events.onPreferences", "{that}.userToken"]
- },
- getMatch: {
- funcName: "gpii.request.flowManager.getMatch",
- args: ["{flowManager}.matchMakerDataSource", "{that}.events.onMatch", "{arguments}.0", "{arguments}.1"]
- }
- },
- events: {
- onUserListener: null,
- onPreferences: null,
- onReadyToMatch: null,
- onMatch: null
- },
- listeners: {
- onUserListener: "{that}.getPreferences",
- onReadyToMatch: "{that}.getMatch"
- }
- });
-
-})();
diff --git a/gpii/node_modules/flowManager/src/HealthGetHandler.js b/gpii/node_modules/flowManager/src/HealthGetHandler.js
new file mode 100644
index 000000000..337b5739b
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/HealthGetHandler.js
@@ -0,0 +1,30 @@
+/*!
+GPII Health Check Handler shared by Cloud Based Flow Manager and Preferences Server
+
+Copyright 2018 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.defaults("gpii.health.handler", {
+ gradeNames: ["kettle.request.http"],
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.health.handleRequest",
+ args: ["{that}"]
+ }
+ }
+});
+
+gpii.health.handleRequest = function (that) {
+ that.events.onSuccess.fire({isHealthy: true});
+};
diff --git a/gpii/node_modules/flowManager/src/MatchMaking.js b/gpii/node_modules/flowManager/src/MatchMaking.js
new file mode 100644
index 000000000..8bad672e8
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/MatchMaking.js
@@ -0,0 +1,225 @@
+/**
+ * GPII Flow Manager Requests
+ *
+ * Copyright 2012 OCAD University
+ * Copyright 2015 Raising the Floor - International
+ * Copyright 2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+/* eslint-env browser */
+/* eslint strict: ["error", "function"] */
+
+(function () {
+
+ "use strict";
+
+ var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+ fluid.registerNamespace("gpii.flowManager");
+
+ gpii.logFully = function () {
+ if (!process.env.GITHUB_ACTIONS) {
+ var oldRenderChars = fluid.logObjectRenderChars;
+ fluid.logObjectRenderChars = 2 << 20;
+ fluid.log.apply(null, arguments);
+ fluid.logObjectRenderChars = oldRenderChars;
+ } else {
+ fluid.log.apply(null, arguments);
+ }
+ };
+
+ /** Render an instance of the "megapayload" which circulates amongst the FlowManager, MatchMaker, LifecycleManager etc. methods.
+ * This builds up incrementally starting with the GPII key during logon until we reach the "finalPayload" dispatched to the
+ * lifecycleManager. This method transforms the payload into a form appropriate for logging to the console as an aid
+ * to diagnosing and debugging issues encountered in the field. Currently it only performs the action of suppressing the
+ * body of any entries of `solutionsRegistryEntries` since these are bulky and can easily be recovered from the
+ * solutions registry itself.
+ * @param {Object} megapayload - A megapayload record.
+ * @return {Object} The megapayload transformed to a more appropriate form for logging.
+ */
+ gpii.renderMegapayload = function (megapayload) {
+ var solutionsRegistryEntries = fluid.transform(megapayload.solutionsRegistryEntries, function () {
+ return "";
+ });
+ return fluid.extend({}, megapayload, {
+ solutionsRegistryEntries: solutionsRegistryEntries
+ });
+ };
+
+ gpii.flowManager.getPreferences = function (that, prefsServerDataSource) {
+ var gpiiKey = that.gpiiKey;
+ if (gpiiKey !== "noUser") {
+ fluid.log("gpii.flowManager.getPreferences called - fetching preferences from URL " + prefsServerDataSource.options.prefsServerURL);
+ }
+
+ var noPrefs = {};
+ var promise = gpiiKey === "noUser" ? fluid.promise().resolve(noPrefs) : prefsServerDataSource.get(gpiiKey);
+ promise.then(function (data) {
+ that.events.onPreferences.fire(data.preferences || data);
+ }, function (err) {
+ // GPII-3721: Return an empty payload for nonexistent GPII keys
+ if (err.errorCode === "GPII_ERR_NO_GPIIKEY") {
+ that.events.onPreferences.fire(noPrefs);
+ } else {
+ that.events.onError.fire({
+ isError: true,
+ message: "Error when retrieving preferences: " + err.message,
+ statusCode: 404
+ });
+ }
+ });
+ };
+
+ /**
+ * Asynchronous/Promise returning function which makes a get call to the solutions registry (1st parameter) to
+ * retrieve the solutions registry matching what is passed in the `device` parameter.
+ * This is appended to the matchmaker payload (mmpayload) parameter, which in turn is passed
+ * as parameter in the event fired.
+ *
+ * This function can be used with either (or both) asyncronously with an `event` and `onError` handlers
+ * passed in, or with the returned `fluid.promise`.
+ *
+ * @param {Object} solutionsRegistryDataSource - a solutions registry data source
+ * @param {Object} deviceContext - output from a device reporter. Used to filter solutions registry entries
+ * @param {Object} onSuccessEvent - Optional: Event to be fired when the solutionsRegistry entry has been retrieved
+ * @param {Object} onErrorEvent - Optional: Event to be fired when an error occurs
+ * @return {fluid.promise} - Returns a promise resolving with the mmpayload. Optionally if provided, the events
+ * are also fired with the modified mmpayload.
+ */
+ gpii.flowManager.getSolutions = function (solutionsRegistryDataSource, deviceContext, onSuccessEvent, onErrorEvent) {
+ var promiseTogo = fluid.promise();
+
+ var os = fluid.get(deviceContext, "OS.id");
+ var promise = solutionsRegistryDataSource.get({});
+ promise.then(function (solutions) {
+ var solutionsRegistryEntries = gpii.matchMakerFramework.filterSolutions(solutions[os], deviceContext);
+ fluid.log("Fetched filtered solutions registry entries: ", gpii.renderMegapayload({solutionsRegistryEntries: solutionsRegistryEntries}));
+ promiseTogo.resolve({
+ solutionsRegistryEntries: solutionsRegistryEntries,
+ solutions: solutions
+ });
+ if (onSuccessEvent) {
+ onSuccessEvent.fire(solutionsRegistryEntries, solutions);
+ }
+ }, function (error) {
+ promiseTogo.reject(error);
+ if (onErrorEvent) {
+ onErrorEvent.fire(error);
+ }
+ });
+
+ return promiseTogo;
+ };
+
+ // initialPayload contains fields
+ // gpiiKey, preferences, deviceContext, solutionsRegistryEntry
+ // resulting from the initial fetch process
+ gpii.flowManager.processMatch = function (that, initialPayload) {
+ var promise = fluid.promise.fireTransformEvent(that.events.processMatch, initialPayload, {});
+ promise.then(function (finalPayload) {
+ that.events.onMatchDone.fire(finalPayload);
+ }, function (error) { // TODO: This rejection handler is untested
+ that.handlerPromise.reject(error);
+ });
+ };
+
+ // This is a table of priorities for handlers in the "processMatch" filter chain governing the MatchMaking process.
+ // This will be removed in favour of a relative constraints system once we have FLUID-5506 completed
+ // Higher priority numbers are handled earlier than lower ones
+ // This process is kicked off by the "onReadyToMatch" event defined in the "gpii.flowManager.matchMaking" grade
+ gpii.flowManager.processMatch.priorities = {
+ // onReadyToMatch event signals start of process
+ preProcess: 100,
+ matchMakerDispatcher: 90
+ // onMatchDone event fired to lifecycleManager or cloud-based settings endpoint
+ };
+
+ /* This component orchestrates the lifecycle for a component which assembles the raw materials required for the matchmaking
+ * process, invokes it, and distributes the results. These raw materials are the gpiiKey (of the user whose preferences are to
+ * be fetched), the device information for the current platform, and the registry of solutions which are available to be
+ * invoked. It is a request-scoped grade and intended as a mixin for a matchmaking request such as gpii.lifecycleManager.loginRequest
+ * and gpii.flowManager.cloudBased.settings.get.handler.
+ * This grade is used in the following places:
+ * UserLogonStateChange.js - where it coordinates the standard lifecycle for a user logging on to a local FlowManager
+ * CloudBasedFlowManager.js - where it coordinates the lifecycle for a user requesting settings from a cloud-based FlowManager
+ */
+ fluid.defaults("gpii.flowManager.matchMaking", {
+ invokers: {
+ getPreferences: {
+ funcName: "gpii.flowManager.getPreferences",
+ args: ["{that}", "{flowManager}.prefsServerDataSource"]
+ },
+ processMatch: {
+ funcName: "gpii.flowManager.processMatch",
+ args: [ "{that}", "{arguments}.0"]
+ // initial payload
+ },
+ getSolutions: {
+ funcName: "gpii.flowManager.getSolutions",
+ args: [ "{flowManager}.solutionsRegistryDataSource", "{arguments}.0", "{that}.events.onSolutions", "{that}.events.onError"]
+ }
+ },
+ events: {
+ // Four pre-requisites for the match process to begin
+ onGpiiKey: null,
+ onPreferences: null,
+ onDeviceContext: null,
+ onSolutions: null,
+ // The "pseudo-event" whose handlers govern the match processing chain
+ processMatch: null,
+ // Output of the matching process - listeners in derived grades
+ onMatchDone: null,
+ // Boiled event which initiates the match process
+ onReadyToMatch: {
+ events: {
+ preferences: "onPreferences",
+ deviceContext: "onDeviceContext",
+ solutions: "onSolutions"
+ },
+ args: [{
+ gpiiKey: "{that}.gpiiKey",
+ preferences: "{arguments}.preferences.0",
+ deviceContext: "{arguments}.deviceContext.0",
+ solutionsRegistryEntries: "{arguments}.solutions.0",
+ fullSolutionsRegistry: "{arguments}.solutions.1"
+ }]
+ }
+ },
+ listeners: {
+ "onGpiiKey.setGpiiKey": {
+ listener: "gpii.flowManager.setGpiiKey",
+ args: ["{that}", "{arguments}.0"]
+ },
+ "onGpiiKey.getPreferences": {
+ func: "{that}.getPreferences",
+ priority: "after:setGpiiKey"
+ },
+ "onDeviceContext.getSolutions": "{that}.getSolutions",
+ processMatch: [{ // Definition of the MatchMaking processing chain
+ priority: gpii.flowManager.processMatch.priorities.preProcess,
+ namespace: "preProcess",
+ listener: "gpii.matchMakerFramework.utils.preProcess"
+ }, {
+ priority: gpii.flowManager.processMatch.priorities.matchMakerDispatcher,
+ namespace: "matchMakerDispatcher",
+ listener: "{flowManager}.matchMakerFramework.matchMakerDispatcher"
+ }],
+ "onReadyToMatch.processMatch": "{that}.processMatch"
+ }
+ });
+
+ gpii.flowManager.setGpiiKey = function (that, gpiiKey) {
+ that.gpiiKey = gpiiKey;
+ };
+})();
diff --git a/gpii/node_modules/flowManager/src/PSPChannel.js b/gpii/node_modules/flowManager/src/PSPChannel.js
new file mode 100644
index 000000000..ba5c9a6e9
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/PSPChannel.js
@@ -0,0 +1,442 @@
+"use strict";
+
+
+var fluid = require("infusion");
+var gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.flowManager");
+
+// A mixin grade applied to the lifecycleManager's session by the pspChannel
+fluid.defaults("gpii.pspChannel.sessionBinder", {
+ modelRelay: {
+ pspChannel: {
+ source: "",
+ target: "{flowManager}.pspChannel.model",
+ singleTransform: {
+ type: "gpii.pspChannel.sessionToPSP",
+ pspChannel: "{pspChannel}"
+ },
+ // compensate for FLUID-6194
+ backward: "never",
+ forward: {
+ // avoid notifying the client for the init of LifecycleManagerSession, simplifies test and clients
+ excludeSource: "init"
+ }
+ }
+ },
+ modelListeners: {
+ // When PSP channel receives a setting change request from PSP clients, this model listener informs flowManager
+ // to apply this new setting.
+ updatePreferences: {
+ path: "{flowManager}.pspChannel.model.settingControls",
+ includeSource: "PSP",
+ funcName: "{flowManager}.lifecycleManager.applyPreferences",
+ args: ["{change}.value", "{pspChannel}.events.preferencesAppliedLocal"]
+ },
+ updatePrefsSetName: {
+ path: "{flowManager}.pspChannel.model.activePrefsSetName",
+ includeSource: "PSP",
+ funcName: "gpii.pspChannel.updatePrefsSetName",
+ args: ["{flowManager}.lifecycleManager", "{pspChannel}", "{change}.value"]
+ },
+ savePreferences: { // explicit save when the save button is clicked
+ path: "{flowManager}.pspChannel.model.saveButtonClickCount",
+ includeSource: "PSP",
+ listener: "gpii.pspChannel.savePreferences",
+ args: ["{flowManager}", "{lifecycleManager}", "{pspChannel}.events.preferencesAppliedLocal"]
+ }
+ }
+});
+
+/**
+ * The PSP channel maintains its own model of the PSP state, it contains the following data:
+ *
+ * "gpiiKey": "", // currently logged in GPII key - { type: "string" }
+ * "activePrefsSetName": "", // currently applied preferences set - { type: "string" }
+ * "settingControls": { // any number of preferences with values and schemas
+ * "": { // settings URI, note that '.'s are interpreted as paths unless escaped
+ * "value": , // the value that the given preference has
+ * "schema": { ... } // schema for the preference
+ * },
+ * "": { .. },
+ * "": { .. }
+ * },
+ * "preferences": { // Used for listing available preference sets and name of preference set
+ * "name": "My preference set",
+ * "contexts": {
+ * "": { // the name/id of the preferences set
+ * "name": "" // name to be displayed in the PSP
+ * }
+ * "": {...},
+ * "": {...}
+ * }
+ * },
+ * saveButtonClickCount // increments by 1 every time the save button is clicked
+ */
+fluid.defaults("gpii.pspChannel", {
+ gradeNames: ["fluid.modelComponent"],
+ settingsSchemaPath: "%gpii-universal/build/schemas/solution-schema-codex.json",
+ settingsSchema: "@expand:fluid.require({that}.options.settingsSchemaPath)",
+ members: {
+ outputBlocked: null,
+ // Holds schemas containing default preferences with values defined in the reset to default file.
+ // This static value is populated the first time handling a key in request.
+ defaultSettingControls: null
+ },
+ listeners: {
+ "{lifecycleManager}.events.onSessionStop": {
+ namespace: "pspChannel",
+ funcName: "gpii.pspChannel.sessionStop",
+ args: "{pspChannel}"
+ }
+ },
+ events: {
+ // Fired when preferences are applied to the local computer. Its main current purpose is to compose the aggregate event "preferencesApplied"
+ preferencesAppliedLocal: null,
+ // Fired when preferences are applied to the local computer and saved to the cloud.
+ preferencesApplied: {
+ events: {
+ preferencesAppliedLocal: "preferencesAppliedLocal",
+ preferencesSavedSuccess: "{gpii.flowManager}.events.preferencesSavedSuccess"
+ }
+ },
+ // Fired when the read of a preference completes successfully
+ preferenceReadSuccess: null,
+ // Fired when the read of a preference fails
+ preferenceReadFail: null
+ },
+ invokers: {
+ generateDefaultSettingControls: {
+ funcName: "gpii.pspChannel.generateDefaultSettingControls",
+ args: ["{flowManager}.defaultSettingsDataPromise", "{that}.options.settingsSchema"]
+ }
+ }
+});
+
+/**
+ * Calculate the schemas containing preferences with their default values from the reset to default file.
+ * @param {Object} defaultSettingsDataPromise - The value of flowManager.defaultSettingsDataPromise.
+ * @param {Object} schemas - The content of build/schemas/solution-schema-codex.json.
+ * @return {Object} - A collection of schemas for each preference defined in the reset to default file.
+ * Each default preference value defined in the reset to default file are populated into the corresponding "default"
+ * field in the output schema. An example:
+ * {
+ * "http://registry\\.gpii\\.net/common/cursorSize": {
+ * "schema": {
+ * "title": "Cursor Size",
+ * "description": "Cursor size",
+ * "type": "number",
+ * "default": 0.8, // This default value is from the reset to default file
+ * "minimum": 0,
+ * "maximum": 1,
+ * "multipleOf": 0.1
+ * },
+ * "liveness": "live"
+ * },
+ * ...
+ * }
+ */
+gpii.pspChannel.generateDefaultSettingControls = function (defaultSettingsDataPromise, schemas) {
+ var defaultSettingControls;
+
+ defaultSettingsDataPromise.then(function (defaultSettingsData) {
+ var defaultPreferences = fluid.get(defaultSettingsData.defaultSettings, ["contexts", "gpii-default", "preferences"]);
+ defaultSettingControls = {};
+
+ // Set the setting default values from reset to standard file to schema.default field for each setting in
+ // "settingControls" block.
+ fluid.each(defaultPreferences, function (defaultPrefsVal, defaultPrefsKey) {
+ var prefsInfo = gpii.pspChannel.getPreferenceInfo(schemas, defaultPrefsKey, defaultPrefsVal);
+ var schema = prefsInfo.schema;
+ if (schema) {
+ fluid.set(schema, ["default"], prefsInfo.prefsValue);
+ gpii.pspChannel.emitSettingControl(defaultSettingControls, schema, undefined, prefsInfo.prefsKeySegs, "live", prefsInfo.solutionName);
+ }
+ });
+ });
+
+ // Although defaultSettingControls is populated asynchronously in the promise callback function above, in practice,
+ // we know that if the architecture is organised properly, the interlock that prevents a PSP request before
+ // defaultSettingsDataPromise is resolved should ensure that this return is actually synchronous. The check here
+ // is to ensure that defaultSettingControls has been initialised before the function returns.
+ if (defaultSettingControls) {
+ return defaultSettingControls;
+ } else {
+ fluid.fail("PSPChannel: defaultSettingControls is not populated in time.");
+ }
+};
+
+/**
+ * Get the preference information required for generating the output for PSPChannel clients.
+ * @param {Object} solutionSchemas - The content of "build/schemas/solution-schema-codex.json"
+ * @param {String} prefsKey - A common term or an application term.
+ * @param {Primitive|Object} prefsValue - The value of the prefsKey. It could be the actual preference value
+ * or an object containing a nested preference path with the value.
+ * @return {Object} - The preference information.
+ *
+ * @typedef {Object} returnObject
+ * @property {Object} schema - The schema of the input preference.
+ * @property {String} solutionName - The solution name for the input preference.
+ * @property {String} presentedCommonTerm - The common term that is presented as the prefsKey or in the prefsValue.
+ * Returns undefined if no common term is presented.
+ * @property {Primitive|Object} prefsValue - The actual preference value.
+ * @property {Boolean} prefsKeySegs - The segments of preference keys. It can contain 1 or 2 elements depending on
+ * if there's a nested preference path.
+ */
+gpii.pspChannel.getPreferenceInfo = function (solutionSchemas, prefsKey, prefsValue) {
+ if (!solutionSchemas || !prefsKey || prefsValue === undefined) {
+ return undefined;
+ };
+
+ var thisSolutionSchema = fluid.get(solutionSchemas, [prefsKey]);
+
+ if (!thisSolutionSchema) {
+ return {};
+ }
+
+ // Actual handling when the schema for the input preference is found.
+ var togo = {};
+ if (gpii.matchMakerFramework.utils.isApplicationTerm(prefsKey)) {
+ var solutionName = fluid.get(thisSolutionSchema, ["title"]);
+ // Handle application terms
+ fluid.each(prefsValue, function (val, innerPrefsKey) {
+ var schema, presentedCommonTerm;
+ if (gpii.matchMakerFramework.utils.isCommonTerm(innerPrefsKey)) {
+ // When the inner pref key is a common term, use the schema of this common term.
+ // TODO: This if block should be removed at fixing https://issues.gpii.net/browse/GPII-4063
+ // when the support for "common terms scoped within applications" discontinues.
+ schema = fluid.get(solutionSchemas, [innerPrefsKey]);
+ delete schema.$schema;
+ presentedCommonTerm = innerPrefsKey;
+ } else {
+ // When the inner pref key is not a common term, find the schema of the corresponding inner key
+ // within th application schema.
+ schema = fluid.get(thisSolutionSchema, ["properties", innerPrefsKey]);
+ presentedCommonTerm = undefined;
+ }
+ togo = {
+ schema: schema,
+ solutionName: solutionName,
+ presentedCommonTerm: presentedCommonTerm,
+ prefsValue: val,
+ prefsKeySegs: [prefsKey, innerPrefsKey]
+ };
+ });
+ } else {
+ // Handle common terms
+ var schema = fluid.get(solutionSchemas, [prefsKey]);
+ delete schema.$schema;
+ togo = {
+ schema: schema,
+ solutionName: fluid.get(solutionSchemas, [prefsKey, "title"]),
+ presentedCommonTerm: prefsKey,
+ prefsValue: prefsValue,
+ prefsKeySegs: [prefsKey]
+ };
+ }
+ return togo;
+};
+
+gpii.pspChannel.updatePrefsSetName = function (lifecycleManager, pspChannel, newPrefsSetName) {
+ fluid.log("Received prefsSet update from PSP UI of ", newPrefsSetName);
+ // Abominable hack to avoid confusing client with numerous update messages. The ChangeApplier should really support
+ // "manifest transactions" / "vertical transactions"
+ pspChannel.outputBlocked = fluid.promise();
+ var clearBlock = function () {
+ delete pspChannel.outputBlocked;
+ };
+ pspChannel.outputBlocked.then(clearBlock, clearBlock);
+ pspChannel.applier.change([], null, "DELETE");
+ var promise = lifecycleManager.prefsSetNameChanged(newPrefsSetName);
+ if (promise) {
+ fluid.promise.follow(promise, pspChannel.outputBlocked);
+ } else {
+ pspChannel.outputBlocked.resolve();
+ }
+};
+
+gpii.pspChannel.filterSolution = function (solution) {
+ return {
+ name: solution.name,
+ settingsHandlers: fluid.transform(solution.settingsHandlers, function (oneHandler) {
+ return fluid.filterKeys(oneHandler, ["supportedSettings"]);
+ })
+ };
+};
+
+// Explicit save when the save button is clicked
+gpii.pspChannel.savePreferences = function (flowManager, lifecycleManager, preferencesAppliedLocalEvent) {
+ // Grab current active lifecycle manager session
+ var userSession = lifecycleManager.getSession();
+ var gpiiKey = userSession.model.gpiiKey;
+ var preferences = userSession.model.preferences;
+ fluid.log("PSPChannel: explicit save for gpiiKey (", gpiiKey, "), with preferences: ", preferences);
+ flowManager.savePreferences(gpiiKey, preferences);
+
+ // At explicit save, preferencesAppliedLocal event will not be triggered. This event is only triggered when
+ // there is setting change that needs to be applied to the local computer. Directly firing this event is to
+ // trigger its parent aggregate event "preferencesApplied" to be fired.
+ preferencesAppliedLocalEvent.fire();
+};
+
+/** Emit an entry in the `settingControls` block for a single setting.
+ *
+ * @param {Object} settingControls - *This object will be modified by the function's action* One top-level member
+ * will be added to this object, with a key given by composing the argument `keySegs`. The value of the
+ * member will be a structure {SettingControl} consisting of:
+ * @member {Any} value - The actual value of the corresponding setting.
+ * @member {JSONSchema} schema - A JSON schema structure describing the value space of the setting.
+ * @member {String} [solutionName] - [optional] The solution name to which this setting is allocated in the preferences
+ * document, if there is one. If it is defined as a top-level common term, this member will be omitted.
+ * @member {String} liveness - The liveness value of the setting.
+ */
+
+gpii.pspChannel.emitSettingControl = function (settingControls, schema, prefVal, keySegs, liveness, solutionName) {
+ var fullKey = fluid.pathUtil.composeSegments.apply(null, keySegs);
+ if (schema) {
+ settingControls[fullKey] = {
+ schema: schema,
+ liveness: liveness
+ };
+ }
+ if (solutionName) {
+ fluid.set(settingControls, [fullKey, "solutionName"], solutionName);
+ }
+ if (prefVal !== null && prefVal !== undefined) {
+ fluid.set(settingControls, [fullKey, "value"], prefVal);
+ }
+};
+
+/** Transduces the session model held for the currently logged-on user in the LifecycleManager's session into the model
+ * structure which is suitable for shipping to the PSP over its bus. Each setting control output in the section
+ * `settingControls` will be dumped using the utility `gpii.pspChannel.emitSettingControl`. This is a model relay
+ * function which is run continuously as the session's model is updated.
+ *
+ * @param {Object} model - The LifecycleManager's session model.
+ * @param {ModelTransformSpec} transformSpec - The model transformation spec.
+ * @return {Object} The output model suitable for shipping to the PSP, including top-level members:
+ * - {Object} settingControls - A hash keyed by preference path, whose values are {SettingControl} objects.
+ * - {Object} preferences - A filtered skeleton of the user's preferences document, just containing the names of preferences sets (`contexts`).
+ */
+gpii.pspChannel.sessionToPSP = function (model, transformSpec) {
+ var that = transformSpec.pspChannel;
+ var schemas = that.options.settingsSchema;
+
+ // that.defaultSettingControls is generated at the system startup when "noUser" logs in. At then, all default
+ // settings data are ready.
+ if (!that.defaultSettingControls) {
+ that.defaultSettingControls = that.generateDefaultSettingControls();
+ }
+
+ var outModel = fluid.filterKeys(model, ["gpiiKey", "activePrefsSetName"]);
+ var settingControls = {};
+ var activePreferences = fluid.get(model, ["currentPreferences"]);
+ var activeSolutionIds, activeSolutions;
+ var applications = fluid.get(model, "activeConfiguration.inferredConfiguration.applications");
+ if (applications) {
+ activeSolutionIds = Object.keys(applications);
+ activeSolutions = fluid.filterKeys(model.solutionsRegistryEntries, activeSolutionIds);
+ }
+
+ fluid.each(activePreferences, function (prefsVal, prefsKey) {
+ var prefsInfo = gpii.pspChannel.getPreferenceInfo(schemas, prefsKey, prefsVal);
+ var liveness = gpii.matchMakerFramework.utils.getLeastLiveness(activeSolutions, prefsInfo.presentedCommonTerm);
+ gpii.pspChannel.emitSettingControl(settingControls, prefsInfo.schema, prefsInfo.prefsValue, prefsInfo.prefsKeySegs, liveness, prefsInfo.solutionName);
+ });
+ outModel.settingControls = fluid.extend(true, {}, settingControls, that.defaultSettingControls || {});
+ outModel.preferences = {
+ name: fluid.get(model, "preferences.name"),
+ contexts: fluid.transform(fluid.get(model, "preferences.contexts"), function (contextVal) {
+ return fluid.filterKeys(contextVal, ["name"]);
+ })
+ };
+ return outModel;
+};
+
+
+gpii.pspChannel.modelChangeListener = function (handler, pspChannel, value, oldValue, path, transaction) {
+ fluid.log("PSPChannel's PSP-facing modelChangeListener, sources are ", fluid.keys(transaction.sources));
+ if (!transaction.sources.PSP && !transaction.sources.SessionCleanup) {
+ fluid.log("Model change source is not PSP - candidate for update message");
+ if (pspChannel.outputBlocked) {
+ // Ensure that we queue just a single outgoing message for when the channel unblocks
+ if (!pspChannel.outputBlocked.queued) {
+ pspChannel.outputBlocked.queued = true;
+ pspChannel.outputBlocked.then(function () {
+ fluid.log("PSPChannel sending unblocked full update message", JSON.stringify(pspChannel.model, null, 2));
+ handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: pspChannel.model});
+ });
+ }
+ } else {
+ var changes = fluid.modelPairToChanges(value, oldValue);
+ var hasDeletion = fluid.find(changes, function (change) {
+ return change.type === "DELETE";
+ });
+ if (hasDeletion) {
+ changes.forEach(function (change) {
+ handler.sendTypedMessage("modelChanged", change);
+ });
+ } else {
+ handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: value});
+ }
+ }
+ }
+};
+
+gpii.pspChannel.sessionStop = function (pspChannel) {
+ pspChannel.applier.change("", null, "DELETE");
+};
+
+fluid.defaults("gpii.pspChannel.handler", {
+ gradeNames: ["kettle.request.ws"],
+ invokers: {
+ modelChangeListener: {
+ funcName: "gpii.pspChannel.modelChangeListener",
+ args: ["{that}", "{pspChannel}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.4"]
+ // value, oldValue, pathSegs, transaction: http://docs.fluidproject.org/infusion/development/ChangeApplierAPI.html#programmatic-style-for-listening-to-changes
+ }
+ },
+ listeners: {
+ onBindWs: {
+ funcName: "gpii.pspChannel.bindWs",
+ args: ["{that}", "{pspChannel}"]
+ },
+ "{pspChannel}.events.preferencesApplied": {
+ funcName: "{that}.sendTypedMessage",
+ args: ["preferencesApplied"]
+ },
+ "{pspChannel}.events.preferenceReadSuccess": {
+ funcName: "{that}.sendTypedMessage",
+ args: ["preferenceReadSuccess"]
+ },
+ "{pspChannel}.events.preferenceReadFail": {
+ funcName: "{that}.sendTypedMessage",
+ args: ["preferenceReadFail"]
+ },
+ onReceiveMessage: {
+ funcName: "gpii.pspChannel.receiveMessage",
+ args: ["{arguments}.1", "{pspChannel}", "{lifecycleManager}"]
+ },
+ "onDestroy.unbindModel": {
+ func: "{pspChannel}.applier.modelChanged.removeListener",
+ args: ["{that}.id"]
+ }
+ }
+});
+
+gpii.pspChannel.bindWs = function (handler, pspChannel) {
+ pspChannel.applier.modelChanged.addListener("", handler.modelChangeListener, handler.id);
+ // Note that this is inconsistent with the Nexus' protocol, but is more correct - for example if the model consists
+ // purely of a primitive or is undefined, the initial Nexus message will break
+ handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: pspChannel.model});
+};
+
+gpii.pspChannel.receiveMessage = function (message, pspChannel, lifecycleManager) {
+ fluid.log("pspChannel received a message: ", message);
+ if (message.type === "modelChanged") {
+ pspChannel.applier.change("", message.value, "ADD", "PSP");
+ }
+ if (message.type === "pullModel") {
+ lifecycleManager.readPreferences(message.value.settingControls, pspChannel.events.preferenceReadSuccess, pspChannel.events.preferenceReadFail);
+ }
+};
diff --git a/gpii/node_modules/flowManager/src/PrefsServerDataSource.js b/gpii/node_modules/flowManager/src/PrefsServerDataSource.js
new file mode 100644
index 000000000..bfe06573b
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/PrefsServerDataSource.js
@@ -0,0 +1,180 @@
+/**
+ * GPII Preferences Server Data Source
+ *
+ * Copyright 2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.flowManager.prefsServerDataSource");
+
+// "gpii.flowManager.prefsServerDataSource" is used by the cloud based flow manager to communicate with the
+// preferences server.
+fluid.defaults("gpii.flowManager.prefsServerDataSource", {
+ gradeNames: ["fluid.component"],
+
+ // This option should be distributed down from, e.g., gpii.flowManager.config.cloud.base
+ prefsServerURL: null,
+
+ preferencesUrlTemplate: "%prefsServerURL/preferences/%gpiiKey?merge=%merge",
+ prefsServerReadyUrlTemplate: "%prefsServerURL/ready",
+
+ // The URLs below are calculated from the flow manager base URL and their templates.
+ preferencesUrl: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["{that}.options.preferencesUrlTemplate", {
+ prefsServerURL: "{that}.options.prefsServerURL"
+ }]
+ }
+ },
+ prefsServerHealthUrl: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["{that}.options.prefsServerReadyUrlTemplate", {
+ prefsServerURL: "{that}.options.prefsServerURL"
+ }]
+ }
+ },
+
+ distributeOptions: {
+ preferencesUrl: {
+ source: "{that}.options.preferencesUrl",
+ target: "{that > preferencesDataSourceImpl}.options.url"
+ },
+ prefsServerHealthUrl: {
+ source: "{that}.options.prefsServerHealthUrl",
+ target: "{that > prefsServerHealthImpl}.options.url"
+ }
+ },
+
+ components: {
+ preferencesDataSourceImpl: {
+ type: "kettle.dataSource.URL",
+ options: {
+ termMap: {
+ gpiiKey: "%gpiiKey"
+ },
+ writable: true,
+ writeMethod: "PUT"
+ }
+ },
+ prefsServerHealthImpl: {
+ type: "kettle.dataSource.URL"
+ }
+ },
+ invokers: {
+ // Retrieve preferences
+ get: {
+ funcName: "gpii.flowManager.prefsServerDataSource.get",
+ args: ["{that}", "{arguments}.0"]
+ // gpiiKey
+ },
+ // Update preferences
+ update: {
+ funcName: "gpii.flowManager.prefsServerDataSource.update",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
+ // gpiiKey, merge, preferences
+ },
+ // Create GPII key and associated preferences
+ create: {
+ funcName: "gpii.flowManager.prefsServerDataSource.create",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ // gpiiKey, preferences
+ },
+ // Check the liveness of the preferences server: running with proper database connection
+ isLive: {
+ funcName: "gpii.flowManager.prefsServerDataSource.isLive",
+ args: ["{prefsServerHealthImpl}"]
+ }
+ }
+});
+
+/**
+ * Retrieve user preferences from the preferences server for the given GPII key.
+ * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource.
+ * @param {String} gpiiKey - A GPII key.
+ * @return {Promise} A promise whose resolved value is the user preferences.
+ */
+gpii.flowManager.prefsServerDataSource.get = function (that, gpiiKey) {
+ return that.preferencesDataSourceImpl.get({
+ gpiiKey: gpiiKey
+ });
+};
+
+/**
+ * Update user preferences on the preferences server for the given GPII key.
+ * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource.
+ * @param {String} gpiiKey A GPII key.
+ * @param {Boolean} merge The flag of whether to merge the given preferences with the existing preferences saved
+ * in the preferences server.
+ * @param {Object} preferences The to-be-updated preferences.
+ * @return {Promise} A promise whose resolved value is the status of the update.
+ */
+gpii.flowManager.prefsServerDataSource.update = function (that, gpiiKey, merge, preferences) {
+ fluid.log("prefsServerDataSource, Merging preferences for GPII key (", gpiiKey , ") with preferences: ", preferences);
+ merge = merge || true;
+ var mergeStr = merge.toString();
+
+ var directModel = {
+ gpiiKey: gpiiKey,
+ merge: mergeStr
+ };
+
+ return that.preferencesDataSourceImpl.set(directModel, preferences, {
+ writeMethod: "PUT",
+ termMap: {
+ gpiiKey: "%gpiiKey",
+ merge: "%merge"
+ }
+ });
+};
+
+/**
+ * Create the given GPII key and associated preferences on the preferences server.
+ * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource.
+ * @param {String} gpiiKey A GPII key.
+ * @param {Object} preferences The preferences associated with the GPII key.
+ * @return {Promise} A promise whose resolved value is the status of the create.
+ */
+gpii.flowManager.prefsServerDataSource.create = function (that, gpiiKey, preferences) {
+ fluid.log("prefsServerDataSource, Creating GPII key (", gpiiKey , ") and its preferences: ", preferences);
+ var directModel = {
+ gpiiKey: gpiiKey
+ };
+
+ return that.preferencesDataSourceImpl.set(directModel, preferences, {
+ writeMethod: "PUT",
+ termMap: {
+ gpiiKey: "%gpiiKey"
+ }
+ });
+};
+
+/**
+ * Check the liveness of the preferences server: running with proper database connection.
+ * @param {Component} prefsServerHealthImpl - An instance of {gpii.flowManager.prefsServerDataSource}.prefsServerHealthImpl.
+ * @return {Promise} A promise whose resolved value is the liveness status of the preferences server. The resolved value is
+ * true if the server is alive. Otherwise, false.
+ */
+gpii.flowManager.prefsServerDataSource.isLive = function (prefsServerHealthImpl) {
+ var promiseTogo = fluid.promise();
+ var isLivePromise = prefsServerHealthImpl.get();
+
+ isLivePromise.then(function (value) { // onResolve
+ promiseTogo.resolve(value);
+ }, function (value) { // onReject
+ promiseTogo.resolve(value);
+ });
+
+ return promiseTogo;
+};
diff --git a/gpii/node_modules/flowManager/src/ReadyGetHandler.js b/gpii/node_modules/flowManager/src/ReadyGetHandler.js
new file mode 100644
index 000000000..59171532f
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/ReadyGetHandler.js
@@ -0,0 +1,70 @@
+/*!
+GPII Cloud Based Flow Manager Ready Check Handler
+
+Copyright 2018 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+// TODO: Discuss how/whether to test this.
+fluid.defaults("gpii.flowManager.cloudBased.ready.handler", {
+ gradeNames: ["kettle.request.http"],
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.flowManager.cloudBased.ready.handler.isLive",
+ args: ["{gpii.flowManager.cloudBased}.prefsServerDataSource", "{that}"]
+ }
+ }
+});
+
+gpii.flowManager.cloudBased.ready.handler.reportError = function (that, prefsServerURL) {
+ fluid.log ("Error connecting to Preferences Sever: '" + prefsServerURL + "'");
+ that.events.onError.fire({
+ isError: true,
+ message: "Error connecting to Preferences Server",
+ statusCode: 503
+ });
+};
+
+gpii.flowManager.cloudBased.ready.handler.isSameHost = function (flowManagerHost, prefsServerHost) {
+ // Convert "http://localhost:8081" to "127.0.0.1:8081"
+ prefsServerHost = prefsServerHost.replace(/http:\/\/|https:\/\//, "");
+ prefsServerHost = prefsServerHost.replace("localhost", "127.0.0.1");
+
+ // Remove the trailing slash if any
+ prefsServerHost = prefsServerHost.replace(/\/$/, "");
+ flowManagerHost = flowManagerHost.replace(/\/$/, "");
+ return flowManagerHost === prefsServerHost;
+};
+
+gpii.flowManager.cloudBased.ready.handler.isLive = function (prefsServerDataSource, that) {
+ var isSameHost = gpii.flowManager.cloudBased.ready.handler.isSameHost(that.req.headers.host, prefsServerDataSource.options.prefsServerURL);
+
+ // Reaching code here means the cloud based flow manager and the preferences server are running as separate kettle servers.
+ // If the given preferences server host is same as the host of the cloud based flow manager, sending the liveness check to
+ // the preferences server will result in the infinite loop. This case occurs when the preferences server host defined in the
+ // config file is wrong, so report "preferences server is not alive" error.
+ if (isSameHost) {
+ gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL);
+ } else {
+ var isLivePromise = prefsServerDataSource.isLive();
+ isLivePromise.then(function (result) {
+ if (result.isReady) {
+ that.events.onSuccess.fire(result);
+ } else {
+ gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL);
+ }
+ }, function () {
+ gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL);
+ });
+ }
+};
diff --git a/gpii/node_modules/flowManager/src/RevisionGetHandler.js b/gpii/node_modules/flowManager/src/RevisionGetHandler.js
new file mode 100644
index 000000000..0cfbd57d7
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/RevisionGetHandler.js
@@ -0,0 +1,55 @@
+/*!
+GPII GET /revision handler used by the Cloud Based Flowmanager
+
+Copyright 2020 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fs = require("fs"),
+ fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.defaults("gpii.flowManager.cloudBased.gpiiRevision.handler", {
+ gradeNames: ["kettle.request.http"],
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.flowManager.cloudBased.gpiiRevision.handler.handleRequest",
+ args: ["{gpii.flowManager.cloudBased}.options.gpiiRevisionPath", "{that}"]
+ }
+ }
+});
+
+gpii.flowManager.cloudBased.gpiiRevision.handler.reportError = function (that, filePath, errMsg) {
+ fluid.log ("Error retrieving full git revision: '" + filePath + "'");
+ that.events.onError.fire({
+ isError: true,
+ message: "Error retrieving full git revision: " + errMsg,
+ statusCode: 404
+ });
+};
+
+gpii.flowManager.cloudBased.gpiiRevision.handler.handleRequest = function (gpiiRevisionPath, that) {
+ var errMsg;
+ var fullPath = fluid.module.resolvePath(gpiiRevisionPath);
+ var result = {sha256: ""};
+ try {
+ result = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
+ if (result.sha256.length === 0) {
+ errMsg = "Missing revision value";
+ }
+ } catch (e) {
+ errMsg = e.message;
+ };
+ if (!errMsg) {
+ that.events.onSuccess.fire(result);
+ } else {
+ gpii.flowManager.cloudBased.gpiiRevision.handler.reportError(that, fullPath, errMsg);
+ }
+};
diff --git a/gpii/node_modules/flowManager/src/SessionAware.js b/gpii/node_modules/flowManager/src/SessionAware.js
new file mode 100644
index 000000000..9b0b2f10c
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SessionAware.js
@@ -0,0 +1,58 @@
+/**
+ * GPII Session Aware
+ *
+ * Copyright 2012 OCAD University
+ * Copyright 2015 Raising the Floor - International
+ * Copyright 2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+/* eslint-env browser */
+/* eslint strict: ["error", "function"] */
+
+(function () {
+
+ "use strict";
+
+ var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+ fluid.registerNamespace("gpii.lifecycleManager");
+
+ /** A mixin request grade for requests which require an active user session to do their work. Exposes a method
+ * withSession which will acquire the session tokens and supply them as an argument or else fail if none is available.
+ */
+ fluid.defaults("gpii.flowManager.sessionAware", {
+ invokers: {
+ withSession: {
+ funcName: "gpii.flowManager.sessionAware.withSession",
+ args: ["{that}", "{lifecycleManager}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
+ }
+ }
+ });
+
+ gpii.flowManager.sessionAware.withSession = function (that, lifecycleManager, onSuccess, failMessage, onError) {
+ onError = onError || that.events.onError.fire;
+ var userSession = lifecycleManager.getSession();
+ if (!userSession.model.gpiiKey) {
+ failMessage = failMessage || "Error handling request which required active session, but none was active";
+ onError({
+ isError: true,
+ statusCode: 401,
+ message: failMessage
+ });
+ } else {
+ onSuccess(userSession, userSession.model.gpiiKey);
+ }
+ };
+
+})();
diff --git a/gpii/node_modules/flowManager/src/Settings.js b/gpii/node_modules/flowManager/src/Settings.js
deleted file mode 100644
index c07f7cb22..000000000
--- a/gpii/node_modules/flowManager/src/Settings.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- GPII Settings Component
-
- Copyright 2013 OCAD University
-
- Licensed under the New BSD license. You may not use this file except in
- compliance with this License.
-
- You may obtain a copy of the License at
- https://github.com/gpii/universal/LICENSE.txt
-*/
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- $ = fluid.registerNamespace("jQuery"),
- request = fluid.registerNamespace("gpii.request.flowManager");
-
- request.onSettings = function (token, handler, device) {
- try {
- device = JSON.parse(device);
- } catch (ex) {
- handler.requestProxy.events.onError.fire({
- isError: true,
- message: "Cloud based flow manager requires device information."
- });
- return;
- } finally {
- handler.device = device;
- handler.userToken = token;
- handler.events.onUserListener.fire(token);
- }
- };
-
- request.processMatch = function (match) {
- var settings = {};
- fluid.each(match, function processSolution(solution) {
- if (!solution.settingsHandlers) {
- return;
- }
- fluid.each(solution.settingsHandlers,
- function findSettings(settingsHandler) {
- if (!settingsHandler.settings) {
- return;
- }
- settings[solution.id] = settingsHandler.settings;
- }
- );
- });
- return settings;
- };
-
- request.getSettings = function (event, match) {
- request.logAndNotify(
- "Flow manager matched the following settings/solutions: ", event,
- request.processMatch)(match);
- };
-
- fluid.defaults("kettle.requests.request.handler.settings", {
- gradeNames: ["fluid.littleComponent", "gpii.request.flowManager.token", "autoInit"],
- invokers: {
- onMatchHandler: {
- funcName: "gpii.request.flowManager.getSettings",
- args: ["{requestProxy}.events.onSuccess", "{arguments}.0"]
- },
- handle: {
- funcName: "gpii.request.flowManager.onSettings",
- args: ["{request}.req.params.token", "{that}", "{request}.req.params.device"],
- dynamic: true
- }
- },
- listeners: {
- onPreferences: {
- listener: "{that}.events.onReadyToMatch.fire",
- args: [
- "{arguments}.0",
- "{that}.device"
- ]
- },
- onMatch: "{that}.onMatchHandler"
- }
- });
-
-})();
diff --git a/gpii/node_modules/flowManager/src/SettingsDataSource.js b/gpii/node_modules/flowManager/src/SettingsDataSource.js
new file mode 100644
index 000000000..496b4a8ef
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SettingsDataSource.js
@@ -0,0 +1,243 @@
+/**
+ * GPII Untrusted Settings Data Source
+ *
+ * Copyright 2017 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+require("accessRequester");
+
+// To reuse gpii.oauth2.getExpiresIn() and gpii.oauth2.getTimestampExpires()
+fluid.require("%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js");
+
+// gpii.flowManager.settingsDataSource provides a get() API that returns a promise
+// whose resolved value is user settings. The internal steps performed by this API:
+// 1. check the access token requested last time. If it's still valid, use it to request then return user settings;
+// 2. If the access token has not been requested or it has expired, request an access token via accessRequester subcomponent;
+// 3. Save the received access token as member options;
+// 4. Use the access token to request then return user settings.
+fluid.defaults("gpii.flowManager.settingsDataSource", {
+ gradeNames: ["fluid.component"],
+
+ // This option should be distributed down from, e.g., gpii.flowManager.config.untrusted.base
+ cloudURL: null,
+
+ settingsGetUrlTemplate: "%cloudURL/%gpiiKey/settings/%device",
+ settingsPutUrlTemplate: "%cloudURL/%gpiiKey/settings",
+ accessTokenUrlTemplate: "%cloudURL/access_token",
+
+ // The URLs below are calculated from the flow manager base URL and their templates.
+ settingsGetUrl: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["{that}.options.settingsGetUrlTemplate", {
+ cloudURL: "{that}.options.cloudURL"
+ }]
+ }
+ },
+ settingsPutUrl: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["{that}.options.settingsPutUrlTemplate", {
+ cloudURL: "{that}.options.cloudURL"
+ }]
+ }
+ },
+ accessTokenUrl: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["{that}.options.accessTokenUrlTemplate", {
+ cloudURL: "{that}.options.cloudURL"
+ }]
+ }
+ },
+
+ // TODO: Reading the client credential from the file system is a temporary solution. This option
+ // should be removed once a proper access requester is in place (https://issues.gpii.net/browse/GPII-2436).
+ clientCredentialFilePath: null,
+
+ // The minimum number of seconds of the lfe time of an access token for it to continuing to be used.
+ minAccessTokenLifeTimeInSecond: 10,
+ distributeOptions: {
+ settingsGetUrl: {
+ source: "{that}.options.settingsGetUrl",
+ target: "{that > settingsDataSourceGetImpl}.options.url"
+ },
+ settingsPutUrl: {
+ source: "{that}.options.settingsPutUrl",
+ target: "{that > settingsDataSourcePutImpl}.options.url"
+ },
+ accessTokenUrl: {
+ source: "{that}.options.accessTokenUrl",
+ target: "{that > accessRequester}.options.url"
+ },
+ clientCredentialFilePath: {
+ source: "{that}.options.clientCredentialFilePath",
+ target: "{that clientCredentialDataSource}.options.path"
+ }
+ },
+
+ members: {
+ accessTokens: {
+ // To keep track of the most recent access token and its expiresIn timestamp for each GPII key. The structure looks like:
+ // "gpiiKey1": {
+ // accessToken: "a-accessToken-value",
+ // timestampExpires: "an-timestamp-the-accessToken-expires"
+ // }
+ // ...
+ }
+ },
+ components: {
+ settingsDataSourceGetImpl: {
+ type: "kettle.dataSource.URL",
+ options: {
+ // url: distributed down from the parent component gpii.flowManager.settingsDataSource
+ termMap: {
+ "gpiiKey": "%gpiiKey",
+ "device": "%device"
+ }
+ }
+ },
+ settingsDataSourcePutImpl: {
+ type: "kettle.dataSource.URL",
+ options: {
+ // url: distributed down from the parent component gpii.flowManager.settingsDataSource
+ termMap: {
+ "gpiiKey": "%gpiiKey"
+ },
+ writable: true,
+ writeMethod: "PUT"
+ }
+ },
+ accessRequester: {
+ type: "gpii.accessRequester",
+ options: {
+ clientCredentialDataSourceGrade: "gpii.accessRequester.clientCredentialDataSource.file"
+ }
+ }
+ },
+ invokers: {
+ get: {
+ funcName: "gpii.flowManager.settingsDataSource.get",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ // gpiiKey, device
+ },
+ set: {
+ funcName: "gpii.flowManager.settingsDataSource.set",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ // gpiiKey, preferences
+ },
+ save: {
+ funcName: "gpii.flowManager.settingsDataSource.save",
+ args: ["{that}.accessTokens", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
+ // gpiiKey, accessToken, timestampExpires
+ }
+ }
+});
+
+/**
+ * Retrieve user settings from the cloud using the access token requested for the keyed in GPII key.
+ * @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
+ * @param {String} gpiiKey - A GPII key.
+ * @param {Object} device - The device information provided by the device reporter.
+ * @return {Promise} A promise whose resolved value is the user settings.
+ */
+gpii.flowManager.settingsDataSource.get = function (that, gpiiKey, device) {
+ var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey);
+ var promiseTogo = fluid.promise();
+
+ accessTokenPromise.then(function (accessToken) {
+ var settingsPromise = that.settingsDataSourceGetImpl.get({
+ gpiiKey: gpiiKey,
+ device: JSON.stringify(device)
+ }, {
+ headers: {
+ "Authorization": "Bearer " + accessToken
+ }
+ });
+ fluid.promise.follow(settingsPromise, promiseTogo);
+ }, function (err) {
+ promiseTogo.reject(err);
+ });
+
+ return promiseTogo;
+};
+
+/**
+ * Update user preferences to the cloud using the access token requested for the keyed in GPII key.
+ * @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
+ * @param {String} gpiiKey A GPII key.
+ * @param {Object} preferences The to-be-updated preferences.
+ * @return {Promise} A promise whose resolved value is the status of the update.
+ */
+gpii.flowManager.settingsDataSource.set = function (that, gpiiKey, preferences) {
+ var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey);
+ var promiseTogo = fluid.promise();
+
+ accessTokenPromise.then(function (accessToken) {
+ var updatePromise = that.settingsDataSourcePutImpl.set({
+ gpiiKey: gpiiKey
+ }, preferences, {
+ headers: {
+ "Authorization": "Bearer " + accessToken
+ }
+ });
+ fluid.promise.follow(updatePromise, promiseTogo);
+ }, function (err) {
+ promiseTogo.reject(err);
+ });
+
+ return promiseTogo;
+};
+
+/**
+ * Find a valid access token. It first checks the saved access token for the keyed in GPII key,
+ * If it has expired, request and return a new one from the cloud, otherwise, return the saved access token.
+ * @param {Component} that - An instance of gpii.flowManager.settingsDataSource.
+ * @param {String} gpiiKey - A GPII key.
+ * @return {Promise} A promise whose resolved value is a valid access token.
+ */
+gpii.flowManager.settingsDataSource.findValidAccessToken = function (that, gpiiKey) {
+ var allAccessTokens = that.accessTokens;
+
+ var accessToken = fluid.get(allAccessTokens, [gpiiKey, "accessToken"]);
+ var expiresIn = gpii.oauth2.getExpiresIn(new Date(), fluid.get(allAccessTokens, [gpiiKey, "timestampExpires"]));
+
+ // If the locally saved access token exists and is still valid, return it.
+ // Otherwise, request an new access token from the cloud and return.
+ // The new access token is saved locally for the continuing use.
+ if (!accessToken || !expiresIn || expiresIn < that.options.minAccessTokenLifeTimeInSecond) {
+ var accessTokenPromise = that.accessRequester.getAccessToken(gpiiKey);
+ var mapper = function (accessTokenObj) {
+ var accessTokenFromCloud = accessTokenObj.access_token;
+ var timestampExpiresFromCloud = gpii.oauth2.getTimestampExpires(new Date(), accessTokenObj.expiresIn);
+ that.save(gpiiKey, accessTokenFromCloud, timestampExpiresFromCloud);
+ return accessTokenFromCloud;
+ };
+ return fluid.promise.map(accessTokenPromise, mapper);
+ } else {
+ return fluid.promise().resolve(accessToken);
+ }
+};
+
+/**
+ * Save the access token and its timestampExpires in the index of the GPII key for the next use.
+ * @param {Component} allAccessTokens - All saved access tokens
+ * @param {String} gpiiKey - The GPII key that the access token associates with.
+ * @param {String} accessToken - The access token to be saved.
+ * @param {String} timestampExpires - A timestampExpires to be saved.
+ */
+gpii.flowManager.settingsDataSource.save = function (allAccessTokens, gpiiKey, accessToken, timestampExpires) {
+ fluid.set(allAccessTokens, [gpiiKey, "accessToken"], accessToken);
+ fluid.set(allAccessTokens, [gpiiKey, "timestampExpires"], timestampExpires);
+};
diff --git a/gpii/node_modules/flowManager/src/SettingsGetHandler.js b/gpii/node_modules/flowManager/src/SettingsGetHandler.js
new file mode 100644
index 000000000..c0c0139c7
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SettingsGetHandler.js
@@ -0,0 +1,102 @@
+/*
+ * GPII Untrusted Settings Get Handler
+ *
+ * Copyright 2017 OCAD University
+ * Copyright 2019 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ kettle = require("kettle"),
+ gpii = fluid.registerNamespace("gpii");
+
+// Get settings in the ontology of preferences from the cloud based flow manager.
+// These settings are untransformed lifecycle instructions.
+// See [an example of the return payload of this endpoint](https://github.com/GPII/gpii-payloads/blob/master/CloudBasedFlowManagerUntrustedSettings.md#user-content-return-payload).
+fluid.defaults("gpii.flowManager.cloudBased.settings.get.handler", {
+ gradeNames: ["kettle.request.http", "gpii.flowManager.matchMaking"],
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.flowManager.cloudBased.settings.get.handleRequest",
+ args: [
+ "{that}",
+ "{gpii.flowManager.cloudBased}.authGrantFinder"
+ ]
+ },
+ matchToSettings: {
+ funcName: "gpii.flowManager.cloudBased.matchToSettings",
+ args: ["{arguments}.0", "{that}.events.onSuccess", "CloudBased FlowManager"]
+ }
+ },
+ listeners: {
+ onMatchDone: "{that}.matchToSettings"
+ }
+});
+
+gpii.flowManager.cloudBased.settings.get.handleRequest = function (that, authGrantFinder) {
+ // Verify the access token
+ var accessToken = gpii.oauth2.parseAccessTokenFromRequest(that.req);
+ var authorizationPromise = gpii.oauth2.getAuthorization(accessToken, authGrantFinder);
+ var gpiiKey = that.req.params.gpiiKey;
+ var deviceString = that.req.params.device;
+
+ authorizationPromise.then(function (authorization) {
+ if (authorization && authorization.gpiiKey === gpiiKey && authorization.allowSettingsGet) {
+ var deviceContext;
+ try {
+ deviceContext = kettle.JSON.parse(deviceString);
+ } catch (ex) {
+ that.events.onError.fire({
+ isError: true,
+ message: "Cloud based flow manager requires device information - failed to parse " + ex.message
+ });
+ return;
+ }
+ that.events.onGpiiKey.fire(gpiiKey);
+ that.events.onDeviceContext.fire(deviceContext);
+ } else {
+ fluid.log("CloudBased flowManager: unauthorized GET request at /settings due to one of these reasons: 1. authorization record is missing; 2. gpiiKey associated with the authorization does not match the in-used GPII key " + gpiiKey + "; 3. the access token is unauthorized for using GET method at /settings endpoint.");
+ that.events.onError.fire(gpii.dbOperation.errors.unauthorized);
+ return;
+ }
+ }, function (error) {
+ fluid.log("CloudBased flowManager: GET request at /settings for the GPII key (" + gpiiKey + ") failed with error: ", error);
+ that.events.onError.fire(gpii.dbOperation.errors.unauthorized);
+ return;
+ });
+};
+
+/**
+ * This should be used as the last step of the matchmaking process in a cloudbased/Untrusted flowmanager setup.
+ * It modifies the final payload by filtering out the irrelevant data before passing it on to the
+ * given event.
+ *
+ * Note that we modify (filter) the users preference set before passing it on in the payload. This is done because
+ * this function is meant for a configuration of the GPII where the local flowmanager is untrusted - therefore we do not
+ * want to pass the full preference set back to it. On the other hand, the PSP need access to the users preferences to show and
+ * modify them. Therefore the preference set is filtered to only contain the preferences and settings that are
+ * relevant to the configuration of the device (i.e. in the inferredConfiguration), any preferences not present there
+ * are removed. This means a compromised local flowmanager would not gain any further information from looking at
+ * the preference set than it could have got by looking at the inferred configuration.
+ * @param {Object} finalPayload - The MatchMaker final payload result.
+ * @param {Event} event - Fired when with the filtered output payload.
+ * @param {String} sourceName - The name of the source component that calls this function. The source could be the
+ * cloud based flow manager or the private matchmaker.
+ */
+gpii.flowManager.cloudBased.matchToSettings = function (finalPayload, event, sourceName) {
+ // Note that we send the filtered preferences to assist a PSP - we may do more filtering here at some point
+ var settings = fluid.filterKeys(finalPayload, [
+ "gpiiKey", "activePrefsSetName", "preferences",
+ "activeConfiguration", "solutionsRegistryEntries", "matchMakerOutput"
+ ]);
+ settings.preferences = gpii.matchMakerFramework.utils.filterPreferencesFromInferredConfig(settings.preferences, settings.matchMakerOutput.inferredConfiguration);
+ fluid.log(sourceName, ": /settings endpoint responding settings ", settings);
+ event.fire(settings);
+};
diff --git a/gpii/node_modules/flowManager/src/SettingsPutHandler.js b/gpii/node_modules/flowManager/src/SettingsPutHandler.js
new file mode 100644
index 000000000..c55d86195
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SettingsPutHandler.js
@@ -0,0 +1,117 @@
+/*
+ * GPII Untrusted Settings Put Handler
+ *
+ * Copyright 2017 OCAD University
+ * Copyright 2019 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+require("../../solutionsRegistry/src/js/sr-validation-middleware");
+
+// Update preferences by first ensuring the client that requests the update action does have the privilege
+// to update, by verifying the access token embedded in the request "Authorization" header.
+fluid.defaults("gpii.flowManager.cloudBased.settings.put.handler", {
+ gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.preferences"],
+ members: {
+ prefsServerDataSource: "{flowManager}.prefsServerDataSource",
+ authGrantFinder: "{gpii.flowManager.cloudBased}.authGrantFinder",
+ dataStore: "{gpii.flowManager.cloudBased}.oauth2DataStore"
+ },
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.flowManager.cloudBased.settings.put.handleRequest",
+ args: [
+ "{that}",
+ "{that}.req.params.gpiiKey",
+ "{that}.req.body"
+ ]
+ },
+ reject: {
+ funcName: "gpii.flowManager.cloudBased.settings.put.reject",
+ args: ["{that}", "{arguments}.0"]
+ }
+ }
+});
+
+gpii.flowManager.cloudBased.settings.put.messages = {
+ success: "Successfully updated."
+};
+
+gpii.flowManager.cloudBased.settings.put.handleRequest = function (that, gpiiKey, preferences) {
+ var accessToken = gpii.oauth2.parseAccessTokenFromRequest(that.req);
+ var authorizationPromise = gpii.oauth2.getAuthorization(accessToken, that.authGrantFinder);
+
+ authorizationPromise.then(function (authorization) {
+ if (authorization && authorization.gpiiKey === gpiiKey && authorization.allowSettingsPut) {
+ var prefs = fluid.get(preferences, ["contexts", "gpii-default", "preferences"]);
+ // GPII-3717: Verify incoming preferences are in the allowed list when "allowedPrefsToWrite" array is
+ // provided. If allowedPrefsToWrite === null, skip this verification and save preferences.
+ if (authorization.allowedPrefsToWrite) {
+ if (Array.isArray(authorization.allowedPrefsToWrite) && gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite(prefs, authorization.allowedPrefsToWrite)) {
+ gpii.flowManager.cloudBased.settings.put.savePrefs(that, gpiiKey, preferences);
+ } else {
+ that.reject("unauthorized request due to one of these reasons: 1. allowedPrefsToWrite field holds a non-array value; 2. one or more preference keys in the preferences - ", preferences, " - are not in the allowed list: ", authorization.allowedPrefsToWrite);
+ return;
+ }
+ } else {
+ gpii.flowManager.cloudBased.settings.put.savePrefs(that, gpiiKey, preferences);
+ }
+ } else {
+ that.reject("unauthorized request due to one of these reasons: 1. authorization record is missing; 2. gpiiKey associated with the authorization does not match the in-used token " + gpiiKey + "; 3. the access token is unauthorized for using PUT method at /settings endpoint.");
+ return;
+ }
+ }, function (error) {
+ that.reject(error);
+ return;
+ });
+};
+
+gpii.flowManager.cloudBased.settings.put.savePrefs = function (that, gpiiKey, preferences) {
+ var handlerPromise;
+ var gpiiKeyPromise = that.dataStore.findGpiiKey(gpiiKey);
+ gpiiKeyPromise.then(function (data) {
+ // GPII-3721: If the GPII key already exists, update its prefs safe. Otherwise, create the GPII key and its prefs safe.
+ if (data) {
+ handlerPromise = that.prefsServerDataSource.update(gpiiKey, true, preferences);
+ } else {
+ handlerPromise = that.prefsServerDataSource.create(gpiiKey, preferences);
+ }
+
+ fluid.log("CloudBased flowManager: SettingsPutHandler - Save the GPII key (", gpiiKey ,") with a preference set: ", preferences);
+ handlerPromise.then(function (response) {
+ // The default success response from the preference server contains the updated preferences,
+ that.events.onSuccess.fire({
+ gpiiKey: response.gpiiKey,
+ message: gpii.flowManager.cloudBased.settings.put.messages.success
+ });
+ }, that.events.onError.fire);
+ }, that.events.onError.fire);
+};
+
+gpii.flowManager.cloudBased.settings.put.reject = function (that, errorMsg) {
+ fluid.log("CloudBased flowManager: PUT request at /settings failed with error: ", errorMsg);
+ that.events.onError.fire(gpii.dbOperation.errors.unauthorized);
+};
+
+/**
+ * This function verifies if all preference keys are in the allowed list.
+ *
+ * @param {Object} prefs - The object that is in the path of "contexts.gpii-defaults.preferences" of a prefs safe.
+ * @param {Array} allowedPrefs - An array of preference keys that are allowed.
+ * @return {Boolean} - If all preference keys are in the allowed list, return true. Otherwise, return false.
+ */
+gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite = function (prefs, allowedPrefs) {
+ var actualPrefKeys = fluid.keys(prefs);
+ gpii.arrayDifference(actualPrefKeys, allowedPrefs);
+ return actualPrefKeys.length === 0;
+};
diff --git a/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js
new file mode 100644
index 000000000..1f81a4c72
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js
@@ -0,0 +1,96 @@
+/*
+ * GPII Solutions Registry Datasource
+ *
+ * Copyright 2016 RtF-I
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ fs = require("fs");
+
+
+require("kettle");
+
+fluid.registerNamespace("gpii.flowManager.solutionsRegistry");
+
+fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource", {
+ gradeNames: ["kettle.dataSource"],
+ components: {
+ encoding: {
+ type: "kettle.dataSource.encoding.none"
+ }
+ },
+ members: {
+ fullSolutionsRegistry: null
+ },
+ readOnlyGrade: "gpii.flowManager.solutionsRegistry.dataSource",
+ invokers: {
+ getImpl: {
+ funcName: "gpii.flowManager.solutionsRegistry.dataSource.handle",
+ args: ["{that}", "{arguments}.1", "{arguments}.2"]
+ // options, directModel
+ }
+ },
+ listeners: {
+ onCreate: "gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry"
+ }
+});
+
+// TODO: Add an invoker to reload once we are "more live".
+gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry = function (that) {
+ if (!that.options.path) {
+ fluid.fail("The solutionsRegistry datasource ", that, " needs a \"path\" option pointing to the solution entries folder");
+ }
+ var url = fluid.module.resolvePath(that.options.path);
+ if (!fs.existsSync(url)) {
+ fluid.fail("The path provided to the solutionsRegistry datasource (", url, ") has not been found on the file system");
+ }
+
+ that.fullSolutionsRegistry = fluid.freezeRecursive(require(url));
+};
+
+/**
+ * Handler for get requests of solutions registry. It will return either a full solution registry,
+ * or if an 'os' is provided in the requestOptions, only the entries for that os will be returned
+ *
+ * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource.
+ * @param {Object} requestOptions - Currently the only request option supported is "os". If provided,
+ * the returned solutions registry will be filtered by OS version.
+ * @return {Promise} A promise that will be resolved with results (see above) or rejected on error.
+ */
+gpii.flowManager.solutionsRegistry.dataSource.handle = function (that, requestOptions) {
+ var promise = fluid.promise();
+ if (requestOptions.os) { // if "os" is defined, return only solution registry entries for that OS
+ if (requestOptions.os in that.fullSolutionsRegistry) {
+ promise.resolve(that.fullSolutionsRegistry[requestOptions.os]);
+ } else {
+ promise.reject({
+ isError: true,
+ message: "The requested OS (" + requestOptions.os + ") was not present in the solutions registry",
+ statusCode: 404
+ });
+ }
+ } else { // if no "os" is requested, return the full solutions registry
+ promise.resolve(that.fullSolutionsRegistry);
+ }
+ return promise;
+};
+
+/** A mixin grade which automatically expands any %terms corresponding to module names registered in Infusion's module database */
+// TODO: This is a duplicate of kettle.dataSource.file.moduleTerms - this should be rewritten after KETTLE-50 is resolved
+fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource.moduleTerms", {
+ gradeNames: "gpii.flowManager.solutionsRegistry.dataSource",
+ termMap: "@expand:fluid.module.terms()"
+});
diff --git a/gpii/node_modules/flowManager/src/SystemUtils.js b/gpii/node_modules/flowManager/src/SystemUtils.js
new file mode 100644
index 000000000..cc11b84c7
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/SystemUtils.js
@@ -0,0 +1,88 @@
+/*
+ * GPII Utilities
+ *
+ * Copyright 2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = fluid || require("infusion"),
+ util = util || require("util"),
+ gpii = fluid.registerNamespace("gpii");
+
+gpii.reservedGpiiKeys = ["noUser", "restore", "reset", "readSetting"];
+
+/**
+ * Check if the given GPII key is a system reserved key.
+ * @param {String} gpiiKey - The GPII key.
+ * @return {Boolean} Return true if the given GPII key is a system reserved key. Otherwise, return false.
+ */
+gpii.isReservedGpiiKey = function (gpiiKey) {
+ return gpii.reservedGpiiKeys.indexOf(gpiiKey) !== -1;
+};
+
+/**
+ * Modify an array not to include string(s) in the other given array (or a string).
+ *
+ * @param {Array} array - An array to have given values removed - MODIFIED IN PLACE.
+ * @param {Primitive|Array} valuesToRemove - A primitive value, or an array of primitive values, to be removed.
+ */
+
+gpii.arrayDifference = function (array, valuesToRemove) {
+ if (!array || !valuesToRemove) {
+ return;
+ }
+ var arrayToRemove = fluid.makeArray(valuesToRemove);
+ fluid.remove_if(array, function (i) {
+ return arrayToRemove.includes(i);
+ });
+};
+
+/**
+ * Get inverse capability tranformation rule. If the settings handler has "inverseCapabilitiesTransformations"
+ * block defined, return it. Otherwise, attempt to calculate the inverse rule based on "capabilitiesTransformations"
+ * definition.
+ *
+ * @param {Object} settingsHandler - A settings handler object from the solution registry.
+ * @return {Object} - The inverse capability transformation rule.
+ */
+gpii.getInverseRules = function (settingsHandler) {
+ return settingsHandler.inverseCapabilitiesTransformations ? settingsHandler.inverseCapabilitiesTransformations :
+ settingsHandler.capabilitiesTransformations ? fluid.model.transform.invertConfiguration(settingsHandler.capabilitiesTransformations) : {};
+};
+
+// Solve GPII-3310 by ensuring fluid.log output in the cloud lies on a single line
+gpii.renderCloudLoggingArgs = function (args) {
+ fluid.each(args, function (arg, i) {
+ var togo = fluid.isPrimitive(arg) ? arg : util.inspect(arg, {
+ breakLength: Infinity,
+ depth: 3,
+ compact: true
+ });
+ if (typeof(togo) === "string" && togo.length > fluid.logObjectRenderChars) {
+ togo = togo.substring(0, fluid.logObjectRenderChars) + " .... [output suppressed at " + fluid.logObjectRenderChars + " chars - for more output, increase fluid.logObjectRenderChars]";
+ }
+ args[i] = togo;
+ });
+};
+
+gpii.applyCloudLogging = function () {
+ fluid.loggingEvent.addListener(gpii.renderCloudLoggingArgs, "renderNodeLoggingArgs", "before:log");
+};
+
+gpii.removeCloudLogging = function () {
+ fluid.loggingEvent.removeListener("renderNodeLoggingArgs");
+};
+
+fluid.defaults("gpii.withCloudLogging", {
+ listeners: {
+ "onCreate.applyCloudLogging": "gpii.applyCloudLogging",
+ "onDestroy.removeCloudLogging": "gpii.removeCloudLogging"
+ }
+});
diff --git a/gpii/node_modules/flowManager/src/UntrustedFlowManager.js b/gpii/node_modules/flowManager/src/UntrustedFlowManager.js
new file mode 100644
index 000000000..9957994c8
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/UntrustedFlowManager.js
@@ -0,0 +1,71 @@
+/**
+ * GPII Untrusted Local FlowManager
+ *
+ * Copyright 2015-2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+require("accessRequester");
+fluid.require("%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js");
+
+// The Untrusted or Hybrid (Local) FlowManager is a Local FlowManager which defers to the cloud for the matchMaking
+// and Preferences fetch process. The architectural goal of the untrusted FlowManager is that the unfiltered
+// user preferences never reach the local device. Therefore the userLogon stages of the "gpii.flowManager.matchMaking"
+// are all abridged, and instead we simply receive a final settings payload from a cloudBased flowManager which are
+// then directly applied to the local device.
+
+// Described at https://issues.gpii.net/browse/GPII-1224
+
+// Overrides request handlers in "gpii.flowManager.local"
+
+fluid.defaults("gpii.flowManager.untrusted", {
+ // Distribute the grade to handle user logon requests.
+ distributeOptions: {
+ "lifecycleManager.loginRequestHandler": {
+ record: "gpii.lifecycleManager.untrusted.stateChangeHandler",
+ target: "{that gpii.lifecycleManager.loginRequest}.options.gradeNames"
+ }
+ },
+ components: {
+ settingsDataSource: {
+ type: "gpii.flowManager.settingsDataSource"
+ // cloudURL: distributed down from, e.g., gpii.flowManager.untrusted.config.development
+ }
+ },
+ invokers: {
+ setSettings: {
+ func: "{that}.settingsDataSource.set",
+ args: ["{arguments}.0", "{arguments}.1"]
+ }
+ },
+ listeners: {
+ "preferencesSavedSuccess.log": {
+ listener: "fluid.log",
+ args: ["FlowManager: updated preferences have been saved to the cloud"]
+ },
+ "preferencesSavedError.reportSaveError": {
+ listener: "gpii.flowManager.untrusted.reportSaveError",
+ args: ["{that}.userErrors.events.userError", "{arguments}.0"]
+ }
+ }
+});
+
+gpii.flowManager.untrusted.reportSaveError = function (userErrorEvent, error) {
+ fluid.log("The save of preferences to the cloud fails with the error: ", error);
+
+ userErrorEvent.fire({
+ isError: true,
+ messageKey: gpii.userErrors.isConnectionError(error.code) ? "NoConnection" : "SaveToCloudFail",
+ originalError: error
+ });
+};
diff --git a/gpii/node_modules/flowManager/src/UserLogin.js b/gpii/node_modules/flowManager/src/UserLogin.js
deleted file mode 100644
index d25a36349..000000000
--- a/gpii/node_modules/flowManager/src/UserLogin.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*!
-GPII User Login Component
-
-Copyright 2012 OCAD University
-
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
-
-You may obtain a copy of the License at
-https://github.com/gpii/universal/LICENSE.txt
-*/
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- request = fluid.registerNamespace("gpii.request.flowManager");
-
- request.onUserLogin = function (token, handler) {
- handler.userToken = token;
- handler.events.onUserListener.fire(token);
- };
-
- request.onMatchHandler = function (lifecycleManagerServerDataSource, event, token, match) {
- lifecycleManagerServerDataSource.set({
- operation: "start",
- token: token
- }, match, request.logAndNotify("Lifecycle manager returned: ", event,
- function () {
- return "User with token " + token +
- " was successfully logged in.";
- }
- ));
- };
-
- fluid.defaults("kettle.requests.request.handler.userLogin", {
- gradeNames: ["fluid.littleComponent", "gpii.request.flowManager.token", "autoInit"],
- invokers: {
- onMatchHandler: {
- funcName: "gpii.request.flowManager.onMatchHandler",
- args: ["{flowManager}.lifecycleManagerServerDataSource", "{requestProxy}.events.onSuccess", "{that}.userToken", "{arguments}.0"]
- },
- getDevice: {
- funcName: "gpii.request.flowManager.getDevice",
- args: ["{flowManager}.deviceReporterDataSource", "{that}.events.onDevice"]
- },
- handle: {
- funcName: "gpii.request.flowManager.onUserLogin",
- args: ["{request}.req.params.token", "{that}"],
- dynamic: true
- }
- },
- events: {
- onDevice: null,
- onReadyToMatch: {
- events: {
- preferences: "onPreferences",
- device: "onDevice"
- },
- args: ["{arguments}.preferences.0", "{arguments}.device.0"]
- }
- },
- listeners: {
- onUserListener: "{that}.getDevice",
- onMatch: "{that}.onMatchHandler"
- }
- });
-
-})();
diff --git a/gpii/node_modules/flowManager/src/UserLogonHandlers.js b/gpii/node_modules/flowManager/src/UserLogonHandlers.js
new file mode 100644
index 000000000..89ebd812b
--- /dev/null
+++ b/gpii/node_modules/flowManager/src/UserLogonHandlers.js
@@ -0,0 +1,72 @@
+/*
+ * GPII User Logon State Change
+ *
+ * Copyright 2012 OCAD University
+ * Copyright 2015, 2017 Raising the Floor - International
+ * Copyright 2018 OCAD University
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The research leading to these results has received funding from the European Union's
+ * Seventh Framework Programme (FP7/2007-2013)
+ * under grant agreement no. 289016.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion");
+var gpii = fluid.registerNamespace("gpii");
+
+// The base handler component containing common functions required by all http request handlers for these endpoints:
+// * /user/:gpiiKey/proximityTriggered
+// * /user/:gpiiKey/login
+// * /user/:gpiiKey/logout
+fluid.defaults("gpii.flowManager.baseHandler", {
+ gradeNames: ["kettle.request.http"],
+ logonState: "proximityTriggered",
+ invokers: {
+ handleRequest: {
+ funcName: "gpii.flowManager.baseHandler.handleRequest",
+ args: ["{flowManager}.lifecycleManager", "{that}", "{that}.options.logonState", "{that}.req.params.gpiiKey"]
+ // logonFunc
+ }
+ }
+});
+
+/**
+ * Capitalize the first letter of a string.
+ * @param {String} inputStr - The string to have the first letter capitalized.
+ * @return {String} - The input string having the first letter capitalized.
+ */
+
+gpii.flowManager.capitalizeFirstLetter = function (inputStr) {
+ return inputStr.charAt(0).toUpperCase() + inputStr.substr(1);
+};
+
+gpii.flowManager.baseHandler.handleRequest = function (lifecycleManager, request, logonState, gpiiKey) {
+ var logonFunc = "perform" + gpii.flowManager.capitalizeFirstLetter(logonState);
+ var logonPromise = lifecycleManager[logonFunc](gpiiKey);
+ logonPromise.then(request.events.onSuccess.fire, request.events.onError.fire);
+};
+
+// Request handler for /user/:gpiiKey/proximityTriggered
+fluid.defaults("gpii.flowManager.proximityTrigger.handler", {
+ gradeNames: ["gpii.flowManager.baseHandler"],
+ logonState: "proximityTriggered"
+});
+
+// Request handler for /user//login
+fluid.defaults("gpii.flowManager.userLogin.handler", {
+ gradeNames: ["gpii.flowManager.baseHandler"],
+ logonState: "login"
+});
+
+// Request handler for /user//logout
+fluid.defaults("gpii.flowManager.userLogout.handler", {
+ gradeNames: ["gpii.flowManager.baseHandler"],
+ logonState: "logout"
+});
diff --git a/gpii/node_modules/flowManager/src/UserLogout.js b/gpii/node_modules/flowManager/src/UserLogout.js
deleted file mode 100644
index 4333467f7..000000000
--- a/gpii/node_modules/flowManager/src/UserLogout.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/*!
-GPII User Logout Component
-
-Copyright 2012 OCAD University
-
-Licensed under the New BSD license. You may not use this file except in
-compliance with this License.
-
-You may obtain a copy of the License at
-https://github.com/gpii/universal/LICENSE.txt
-*/
-
-(function () {
-
- "use strict";
-
- var fluid = require("infusion"),
- request = fluid.registerNamespace("gpii.request.flowManager");
-
- fluid.defaults("kettle.requests.request.handler.userLogout", {
- gradeNames: ["fluid.littleComponent", "autoInit"],
- invokers: {
- handle: {
- funcName: "gpii.request.flowManager.onUserLogout",
- args: ["{requestProxy}", "{request}.req.params.token", "{flowManager}.lifecycleManagerServerDataSource"],
- dynamic: true
- }
- }
- });
-
- request.onUserLogout = function (requestProxy, token, lifecycleManagerServerDataSource) {
- // TODO: clarify semantics of multiple active start/stop cycles.
- lifecycleManagerServerDataSource.set({
- operation: "stop",
- token: token
- }, {}, function onSuccess(response) {
- fluid.log("Lifecycle manager returned: ", response);
- requestProxy.events.onSuccess.fire("User with token " + token +
- " was successfully logged out.");
- });
- };
-
-})();
diff --git a/gpii/node_modules/flowManager/test/BrowserChannelTests.js b/gpii/node_modules/flowManager/test/BrowserChannelTests.js
new file mode 100644
index 000000000..291842352
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/BrowserChannelTests.js
@@ -0,0 +1,25 @@
+/**
+ * GPII Flow Manager Browser Channel Tests
+ *
+ * Copyright 2013 OCAD University
+ * Copyright 2015 Emergya
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/kettle/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.require("%gpii-universal");
+
+gpii.loadTestingSupport();
+
+require("./shared/BrowserChannelTestDefs.js");
+
+gpii.test.runCouchTestDefs(gpii.tests.flowManager.browserChannel.testDefs);
diff --git a/gpii/node_modules/flowManager/test/CaptureTests.js b/gpii/node_modules/flowManager/test/CaptureTests.js
new file mode 100644
index 000000000..981fafa96
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/CaptureTests.js
@@ -0,0 +1,238 @@
+/**
+ * GPII Capture Tests
+ *
+ * Copyright 2019 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ kettle = require("kettle");
+
+var jqUnit = require("node-jqunit");
+
+fluid.require("%gpii-universal");
+gpii.loadTestingSupport();
+
+fluid.registerNamespace("gpii.tests.flowManager.capture");
+
+kettle.config.createDefaults({
+ configName: "gpii.flowManager.tests.capture.fakeData.config",
+ configPath: "%gpii-universal/gpii/node_modules/flowManager/test/configs"
+});
+
+gpii.tests.flowManager.capture.checkForFakeMags = function (payload) {
+ jqUnit.assertDeepEq("FakeMag1 with multiple settings handlers", {
+ "magnification": 2,
+ "invert": true
+ }, payload.fakemag1);
+
+ jqUnit.assertDeepEq("FakeMag2 with single settings handler", {
+ "invert": 1,
+ "magnification": 4.5,
+ "location": "topRight"
+ }, payload.fakemag2);
+};
+
+gpii.tests.flowManager.capture.platformReporter = function () {
+ return {
+ id: "darwin"
+ };
+};
+
+gpii.tests.flowManager.capture.checkForInstalledMags = function (payload) {
+ jqUnit.assertEquals("There should be 2 solutions installed", 2, Object.keys(payload).length);
+ jqUnit.assertEquals("Check for Fake Mag 1", "Fake Magnifier 1", payload.fakemag1.name);
+ jqUnit.assertEquals("Check for Fake Mag 2", "Fake Magnifier 2 - fully featured", payload.fakemag2.name);
+};
+
+/*
+ * A simple resolver config for testing resolver substituion inside of solutions for
+ * capture. This only needs to support a single varialbe in test `PWD` that will be
+ * used to lookup json settings files for the fake magnifier configurations.
+ */
+fluid.defaults("gpii.tests.flowManager.capture.standardResolverConfig", {
+ gradeNames: "fluid.component",
+ resolvers: {
+ test: "gpii.tests.flowManager.capture.testResolver"
+ }
+});
+
+gpii.tests.flowManager.capture.testResolver = function (name) {
+ if (name === "PWD") {
+ return __dirname;
+ }
+ else {
+ return null;
+ }
+};
+
+fluid.defaults("gpii.tests.flowManager.capture.tests", {
+ gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder"],
+ modules: [
+ {
+ name: "Simple system capture",
+ tests: [{
+ name: "Check for existing FakeMag Settings",
+ expect: 2,
+ sequence: [{
+ task: "{config}.server.flowManager.capture.getSystemSettingsCapture",
+ args: [],
+ resolve: "gpii.tests.flowManager.capture.checkForFakeMags",
+ resolveArgs: ["{arguments}.0"]
+ }]
+ }]
+ },
+ {
+ name: "Simple installed solutions fetch",
+ tests: [{
+ name: "Testing get installed solutions",
+ expect: 3,
+ sequence: [{
+ task: "{config}.server.flowManager.capture.getInstalledSolutionsForCurrentDevice",
+ args: [],
+ resolve: "gpii.tests.flowManager.capture.checkForInstalledMags",
+ resolveArgs: ["{arguments}.0"]
+ }]
+ }]
+ }
+ ],
+ components: {
+ "config": {
+ type: "gpii.flowManager.tests.capture.fakeData.config"
+ }
+ }
+});
+
+fluid.test.runTests([
+ "gpii.tests.flowManager.capture.tests"
+]);
+
+/*
+ * Tests for formatting raw captures.
+ */
+gpii.tests.flowManager.capture.formatRawCapturedSettingsTestCases = {
+ emptyInput: {
+ message: "Empy Input should produce an empty object",
+ input: [],
+ expected: {}
+ },
+ allCorrectSettings: {
+ message: "There should be 2 solutions each with 2 settings from the all correct payload",
+ input: [
+ {
+ "fakemag1": [
+ {
+ "settings": {
+ "magnification": 3
+ }
+ }
+ ]
+ },
+ {
+ "fakemag1": [
+ {
+ "settings": {
+ "invert": false
+ }
+ }
+ ]
+ },
+ {
+ "fakemag2": [
+ {
+ "settings": {
+ "magnification": 2,
+ "invert": true
+ }
+ }
+ ]
+ }
+ ],
+ expected: {
+ "fakemag1": {
+ "magnification": 3,
+ "invert": false
+ },
+ "fakemag2": {
+ "magnification": 2,
+ "invert": true
+ }
+ }
+ },
+ someErrorsSettings: {
+ message: "There should still be 2 solutions each with 2 settings from the all payload that contained an error in addition to correct entries.",
+ input: [
+ {
+ "fakemag1": [
+ {
+ "settings": {
+ "bouncedelay": 2
+ }
+ }
+ ]
+ },
+ {
+ "fakemag1": [
+ {
+ "settings": {
+ "invert": true
+ }
+ }
+ ]
+ },
+ {
+ isError: true,
+ msg: "This didn't work properly during capture"
+ },
+ {
+ "fakemag2": [
+ {
+ "settings": {
+ "magnification": 5,
+ "invert": false
+ }
+ }
+ ]
+ }
+ ],
+ expected: {
+ "fakemag1": {
+ "bouncedelay": 2,
+ "invert": true
+ },
+ "fakemag2": {
+ "magnification": 5,
+ "invert": false
+ }
+ }
+ },
+ allErrorsSettings: {
+ message: "Settings with just errors should yield an empty object",
+ input: [
+ {
+ isError: true,
+ msg: "This didn't work properly during capture"
+ },
+ {
+ isError: true,
+ msg: "This is another error apparently"
+ }
+ ],
+ expected: {}
+ }
+};
+
+jqUnit.test("Test Raw Settings Formatting", function () {
+ fluid.each(gpii.tests.flowManager.capture.formatRawCapturedSettingsTestCases, function (nextTest) {
+ var result = gpii.flowManager.capture.formatRawCapturedSettings(nextTest.input);
+ jqUnit.assertDeepEq(nextTest.message, nextTest.expected, result);
+ });
+});
diff --git a/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js b/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js
new file mode 100644
index 000000000..17fb790a2
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js
@@ -0,0 +1,231 @@
+/*!
+Default Settings Loader Tests
+
+Copyright 2018 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ kettle = require("kettle"),
+ jqUnit = fluid.registerNamespace("jqUnit"),
+ fs = require("fs"),
+ rimraf = require("rimraf");
+
+kettle.loadTestingSupport();
+
+require("journal");
+fluid.require("%flowManager/src/DefaultSettingsLoader.js");
+
+fluid.registerNamespace("gpii.tests.defaultSettingsLoader");
+
+gpii.tests.defaultSettingsLoader.testData = {
+ defaultSettingsUrl: "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5",
+ gpiiSettingsDir: "%flowManager/test/data/temp",
+ defaultSettingsInCodeBase: "%flowManager/test/data/defaultSettings.json5",
+ defaultSettings: {
+ "solution1": "mock default settings"
+ },
+ userOwnSettings: {
+ "userOwnSolution": "my settings"
+ }
+};
+
+fluid.defaults("gpii.tests.defaultSettingsLoader", {
+ gradeNames: ["gpii.defaultSettingsLoader"],
+ members: {
+ // Use a temp dir to not disturb the data in the real GPII settings dir
+ gpiiSettingsDir: gpii.tests.defaultSettingsLoader.testData.gpiiSettingsDir
+ },
+ defaultSettingsInCodeBase: gpii.tests.defaultSettingsLoader.testData.defaultSettingsInCodeBase,
+ defaultSettings: gpii.tests.defaultSettingsLoader.testData.defaultSettings,
+ listeners: {
+ "onDestroy.cleanup": {
+ listener: "gpii.tests.defaultSettingsLoader.cleanup",
+ args: ["{that}.gpiiSettingsDir"]
+ }
+ }
+});
+
+// Create the initial settings file in the code base
+gpii.tests.defaultSettingsLoader.prepareDefaultSettingsInCodeBase = function (gpiiSettingsDir, defaultSettingsInCodeBase, defaultSettings) {
+ // Create the settings dir for the test
+ var fullDir = fluid.module.resolvePath(gpiiSettingsDir);
+ if (!fs.existsSync(fullDir)) {
+ fs.mkdirSync(fullDir);
+ }
+
+ // Prepare the test by writing an initial settings file to the settings dir
+ var sourceSettingsFile = fluid.module.resolvePath(defaultSettingsInCodeBase);
+ gpii.tests.defaultSettingsLoader.createFile(sourceSettingsFile, defaultSettings);
+};
+
+gpii.tests.defaultSettingsLoader.cleanup = function (gpiiSettingsDir) {
+ // Remove the system settings directory
+ var fullDir = fluid.module.resolvePath(gpiiSettingsDir);
+ rimraf(fullDir, function () {
+ fluid.log("Cleanup: removed ", fullDir);
+ });
+};
+
+gpii.tests.defaultSettingsLoader.createFile = function (fileLocation, jsonContent) {
+ fs.writeFileSync(fileLocation, JSON.stringify(jsonContent));
+};
+
+gpii.tests.defaultSettingsLoader.verifyResultWithCopyAction = function (defaultSettingsLoader, result) {
+ jqUnit.assertTrue("The settings file has been copied to the settings dir", fs.existsSync(defaultSettingsLoader.defaultSettingsFile));
+ jqUnit.assertDeepEq("get() returns the expected settings", defaultSettingsLoader.options.defaultSettings, result);
+};
+
+// Test that the user's own settings file already exists in the settings dir
+gpii.tests.defaultSettingsLoader.userOwnSettings = function (defaultSettingsLoader) {
+ // Create the user's own settings in the settings file at the system settings directory
+ fs.writeFileSync(defaultSettingsLoader.defaultSettingsFile, JSON.stringify(gpii.tests.defaultSettingsLoader.testData.userOwnSettings));
+ return defaultSettingsLoader.get();
+};
+
+gpii.tests.defaultSettingsLoader.verifyUserOwnSettings = function (defaultSettingsLoader, result) {
+ jqUnit.assertDeepNeq("The loaded settings is as expected", JSON.stringify(gpii.tests.defaultSettingsLoader.testData.userOwnSettings), result);
+ jqUnit.assertDeepNeq("The expected settings is different from default settings in the code base", defaultSettingsLoader.options.defaultSettings, result);
+};
+
+// ==================== Test the fetch from the remote url ====================
+fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder.fetchRemoteUrl", {
+ gradeNames: ["fluid.test.testCaseHolder"],
+ components: {
+ defaultSettingsLoader: {
+ type: "gpii.tests.defaultSettingsLoader",
+ options: {
+ defaultSettingsUrl: gpii.tests.defaultSettingsLoader.testData.defaultSettingsUrl
+ }
+ }
+ },
+ modules: [{
+ name: "gpii.defaultSettingsLoader Tests - fetch default settings from the remote url",
+ tests: [{
+ expect: 1,
+ name: "Fetch default settings from the remote url",
+ task: "{defaultSettingsLoader}.get",
+ resolve: "jqUnit.assertNotUndefined",
+ resolveArgs: ["Default settings are fetched from the remote URL", "{arguments}.0"]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.defaultSettingsLoader.fetchRemoteUrl", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ components: {
+ tester: {
+ type: "gpii.tests.defaultSettingsLoader.testCaseHolder.fetchRemoteUrl"
+ }
+ }
+});
+
+kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.fetchRemoteUrl");
+
+// ==================== Test the first fallback of reading the local default settings file ====================
+fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder", {
+ gradeNames: ["fluid.test.testCaseHolder"],
+ components: {
+ defaultSettingsLoader: {
+ type: "gpii.tests.defaultSettingsLoader",
+ options: {
+ defaultSettingsUrl: undefined, // Cannot fetch from the remote url
+ listeners: {
+ "onCreate.prepareDefaultSettingsInCodeBase": {
+ listener: "gpii.tests.defaultSettingsLoader.prepareDefaultSettingsInCodeBase",
+ args: [
+ gpii.tests.defaultSettingsLoader.testData.gpiiSettingsDir,
+ gpii.tests.defaultSettingsLoader.testData.defaultSettingsInCodeBase,
+ gpii.tests.defaultSettingsLoader.testData.defaultSettings
+ ],
+ priority: "before:prepareSettingsFile"
+ },
+ "onDestroy.moreCleanup": {
+ listener: "gpii.tests.defaultSettingsLoader.moreCleanup",
+ args: ["{that}.options.defaultSettingsInCodeBase"]
+ }
+ }
+ }
+ }
+ },
+ modules: [{
+ name: "gpii.defaultSettingsLoader Tests",
+ tests: [{
+ expect: 2,
+ name: "Test the fallback of reading the local default settings file - when the settings file is not initially copied to the settings dir",
+ task: "{defaultSettingsLoader}.get",
+ resolve: "gpii.tests.defaultSettingsLoader.verifyResultWithCopyAction",
+ resolveArgs: ["{defaultSettingsLoader}", "{arguments}.0"]
+ }, {
+ expect: 2,
+ name: "Test the fallback of reading the local default settings file - when users own settings file is already in the settings directory",
+ task: "gpii.tests.defaultSettingsLoader.userOwnSettings",
+ args: ["{defaultSettingsLoader}"],
+ resolve: "gpii.tests.defaultSettingsLoader.verifyUserOwnSettings",
+ resolveArgs: ["{defaultSettingsLoader}", "{arguments}.0"]
+ }]
+ }]
+});
+
+gpii.tests.defaultSettingsLoader.moreCleanup = function (defaultSettingsInCodeBase) {
+ // Remove the default settings file in the code base
+ var sourceSettingsFile = fluid.module.resolvePath(defaultSettingsInCodeBase);
+ fs.unlinkSync(sourceSettingsFile);
+};
+
+fluid.defaults("gpii.tests.defaultSettingsLoader.existInCodeBase", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ components: {
+ tester: {
+ type: "gpii.tests.defaultSettingsLoader.testCaseHolder"
+ }
+ }
+});
+
+kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.existInCodeBase");
+
+// ==================== Test when the default settings file doesn't exist in the code base ====================
+fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder.nonExistentInCodeBase", {
+ gradeNames: ["fluid.test.testCaseHolder"],
+ components: {
+ defaultSettingsLoader: {
+ type: "gpii.tests.defaultSettingsLoader",
+ options: {
+ defaultSettingsUrl: undefined, // Cannot fetch from the remote url
+ listeners: {
+ // Do not create the settings file either in the code base or the settings directory
+ "onCreate.prepareSettingsFile": "fluid.identity"
+ }
+ }
+ }
+ },
+ modules: [{
+ name: "gpii.defaultSettingsLoader Tests",
+ tests: [{
+ expect: 1,
+ name: "Tests when none of the remote url, the settings file in the code base and the settings directory exist",
+ task: "{defaultSettingsLoader}.get",
+ resolve: "jqUnit.assertUndefined",
+ resolveArgs: ["Receive undefined when the settings file in the code base does not exist", "{arguments}.0"]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.defaultSettingsLoader.nonExistentInCodeBase", {
+ gradeNames: ["fluid.test.testEnvironment"],
+ components: {
+ tester: {
+ type: "gpii.tests.defaultSettingsLoader.testCaseHolder.nonExistentInCodeBase"
+ }
+ }
+});
+
+kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.nonExistentInCodeBase");
diff --git a/gpii/node_modules/flowManager/test/PSPChannelTests.js b/gpii/node_modules/flowManager/test/PSPChannelTests.js
new file mode 100644
index 000000000..b6c043682
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/PSPChannelTests.js
@@ -0,0 +1,565 @@
+/**
+ * GPII PSP Channel Tests
+ *
+ * Copyright 2017 Raising the Floor - International
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ jqUnit = fluid.require("node-jqunit", require, "jqUnit"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.require("%gpii-universal");
+
+gpii.loadTestingSupport();
+
+fluid.registerNamespace("gpii.tests.pspChannel");
+
+gpii.tests.pspChannel.payloads = {
+ "connectWithNoUsers": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "noUser",
+ "activePrefsSetName": "gpii-default",
+ "settingControls": {},
+ "preferences": {}
+ }
+ },
+ "logoutUser": {
+ "path": [],
+ "value": null,
+ "type": "DELETE"
+ },
+ "snapset_1a_loggedIn": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "snapset_1a",
+ "activePrefsSetName": "gpii-default",
+ // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once.
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/DPIScale": {
+ "solutionName": "DPI Scale",
+ "value": 1,
+ "schema": {
+ "title": "DPI Scale",
+ "description": "DPI scale factor on default monitor",
+ "type": "integer",
+ "default": 0,
+ "minimum": -2,
+ "maximum": 4
+ },
+ "liveness": "live"
+ },
+ "http://registry\\.gpii\\.net/common/cursorSize": {
+ "solutionName": "Cursor Size",
+ "value": 1,
+ "schema": {
+ "title": "Cursor Size",
+ "description": "Cursor size",
+ "type": "number",
+ "default": 0.5,
+ "minimum": 0,
+ "maximum": 1,
+ "multipleOf": 0.1
+ },
+ "liveness": "liveRestart"
+ }
+ },
+ "preferences": {
+ "name": "Larger 125%",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ }
+ }
+ }
+ }
+ },
+ "snapset_2c_loggedIn": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "snapset_2c",
+ "activePrefsSetName": "gpii-default",
+ // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once.
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/cursorSize": {
+ "solutionName": "Cursor Size",
+ "value": 1,
+ "schema": {
+ "title": "Cursor Size",
+ "description": "Cursor size",
+ "type": "number",
+ "default": 0.5,
+ "minimum": 0,
+ "maximum": 1,
+ "multipleOf": 0.1
+ },
+ "liveness": "liveRestart"
+ },
+ "http://registry\\.gpii\\.net/common/DPIScale": {
+ "solutionName": "DPI Scale",
+ "value": 3,
+ "schema": {
+ "title": "DPI Scale",
+ "description": "DPI scale factor on default monitor",
+ "type": "integer",
+ "default": 0,
+ "minimum": -2,
+ "maximum": 4
+ },
+ "liveness": "live"
+ },
+ "http://registry\\.gpii\\.net/common/highContrastTheme": {
+ "solutionName": "High Contrast theme",
+ "value": "white-black",
+ "schema": {
+ "title": "High Contrast theme",
+ "description": "High Contrast Theme",
+ "type": "string",
+ "default": "black-white",
+ "enum": [
+ "regular-contrast",
+ "black-white",
+ "white-black",
+ "yellow-black",
+ "black-yellow",
+ "lime-black",
+ "black-brown",
+ "grey-black",
+ "grey-white"
+ ],
+ "enumLabels": [
+ "Regular Contrast",
+ "Black on White",
+ "White on Black",
+ "Yellow on Black",
+ "Black on Yellow",
+ "Lime on Black",
+ "Black on Brown",
+ "Grey on Dark",
+ "Grey on White"
+ ]
+ },
+ "liveness": "live"
+ }
+ },
+ "preferences": {
+ "name": "Dark & Larger 175%",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ }
+ }
+ }
+ }
+ },
+ "context1_loggedIn": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "context1",
+ "activePrefsSetName": "gpii-default",
+ // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once.
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/magnification": {
+ "solutionName": "Magnification",
+ "value": 1.5,
+ "schema": {
+ "title": "Magnification",
+ "description": "Level of magnification",
+ "type": "number",
+ "default": 1,
+ "minimum": 1,
+ "multipleOf": 0.1
+ },
+ "liveness": "liveRestart"
+ },
+ "http://registry\\.gpii\\.net/common/volume": {
+ "solutionName": "Volume",
+ "value": 0.5,
+ "schema": {
+ "title": "Volume",
+ "description": "General volume of the operating system",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "liveness": "live"
+ }
+ },
+ "preferences": {
+ "name": "Multiple Contexts",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ },
+ "bright": {
+ "name": "bright"
+ },
+ "noise": {
+ "name": "noise"
+ },
+ "brightandnoise": {
+ "name": "bright and noise"
+ }
+ }
+ }
+ }
+ },
+ "context1_bright": {
+ "path": [],
+ "type": "ADD",
+ "value": {
+ "gpiiKey": "context1",
+ "activePrefsSetName": "bright",
+ // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once.
+ "settingControls": {
+ "http://registry\\.gpii\\.net/common/magnification": {
+ "solutionName": "Magnification",
+ "value": 2,
+ "schema": {
+ "title": "Magnification",
+ "description": "Level of magnification",
+ "type": "number",
+ "default": 1,
+ "minimum": 1,
+ "multipleOf": 0.1
+ },
+ "liveness": "liveRestart"
+ }
+ },
+ "preferences": {
+ "name": "Multiple Contexts",
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences"
+ },
+ "bright": {
+ "name": "bright"
+ },
+ "noise": {
+ "name": "noise"
+ },
+ "brightandnoise": {
+ "name": "bright and noise"
+ }
+ }
+ }
+ }
+ }
+};
+
+gpii.tests.pspChannel.connectionSucceeded = function (data) {
+ jqUnit.assertValue("Connection between client and server can be established", data);
+};
+
+gpii.tests.pspChannel.checkPayload = function (data, expected) {
+ jqUnit.assertDeepEq("Check PSP channel response is as expected", {
+ "type": "modelChanged",
+ "payload": gpii.tests.pspChannel.payloads[expected]
+ }, data);
+};
+
+fluid.defaults("gpii.tests.pspChannel.client", {
+ gradeNames: "kettle.test.request.ws",
+ path: "/pspChannel",
+ port: "{configuration}.options.mainServerPort",
+ events: {
+ connectionSucceeded: null
+ },
+ listeners: {
+ connectionSucceeded: {
+ funcName: "gpii.tests.pspChannel.connectionSucceeded",
+ args: ["{arguments}.0"]
+ }
+ }
+});
+
+fluid.defaults("gpii.tests.pspChannel.gpiiKeyRequest", {
+ gradeNames: "kettle.test.request.http",
+ termMap: {
+ gpiiKey: "{that}.options.gpiiKeyName"
+ }
+});
+
+fluid.defaults("gpii.tests.pspChannel.loginRequest", {
+ gradeNames: "gpii.tests.pspChannel.gpiiKeyRequest",
+ path: "/user/%gpiiKey/login"
+});
+
+fluid.defaults("gpii.tests.pspChannel.logoutRequest", {
+ gradeNames: "gpii.tests.pspChannel.gpiiKeyRequest",
+ path: "/user/%gpiiKey/logout"
+});
+
+gpii.tests.pspChannel.testDef = {
+ name: "PSP Channel - Basic user login/logout and client connect/disconnect tests",
+ expect: 10,
+ config: {
+ configName: "gpii.config.development.local.mock.windows",
+ configPath: "%gpii-universal/gpii/configs/mocks"
+ },
+ components: {
+ clientOne: {
+ type: "gpii.tests.pspChannel.client"
+ },
+ clientTwo: {
+ type: "gpii.tests.pspChannel.client"
+ },
+ loginUser1a: {
+ type: "gpii.tests.pspChannel.loginRequest",
+ options: {
+ gpiiKeyName: "snapset_1a"
+ }
+ },
+ loginUser2c: {
+ type: "gpii.tests.pspChannel.loginRequest",
+ options: {
+ gpiiKeyName: "snapset_2c"
+ }
+ },
+ logoutUser1a: {
+ type: "gpii.tests.pspChannel.logoutRequest",
+ options: {
+ gpiiKeyName: "snapset_1a"
+ }
+ }
+ },
+ sequence: [
+ {
+ func: "{clientOne}.connect"
+ },
+ {
+ event: "{clientOne}.events.onConnect",
+ listener: "gpii.tests.pspChannel.connectionSucceeded"
+ },
+ {
+ event: "{clientOne}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "connectWithNoUsers"]
+ },
+ {
+ func: "{loginUser1a}.send"
+ },
+ {
+ event: "{clientOne}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "logoutUser"]
+ },
+ {
+ event: "{clientOne}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "snapset_1a_loggedIn"]
+ },
+ {
+ func: "{clientOne}.disconnect"
+ },
+ {
+ func: "{clientTwo}.connect"
+ },
+ {
+ event: "{clientTwo}.events.onConnect",
+ listener: "gpii.tests.pspChannel.connectionSucceeded"
+ },
+ {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "snapset_1a_loggedIn"]
+ },
+ {
+ func: "{logoutUser1a}.send"
+ },
+ {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "logoutUser"]
+ },
+ {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "connectWithNoUsers"]
+ },
+ {
+ func: "{loginUser2c}.send"
+ },
+ {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "logoutUser"]
+ },
+ {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "snapset_2c_loggedIn"]
+ }
+ ]
+};
+
+gpii.tests.pspChannel.sendPrefsSetChange = function (client, newPrefsSet) {
+ client.send({
+ type: "modelChanged",
+ value: {
+ activePrefsSetName: newPrefsSet
+ }
+ });
+};
+
+gpii.tests.pspChannel.prefsSetTestDef = {
+ name: "PSP Channel - Test triggering preferences set changes and receiving updates",
+ expect: 5,
+ gradeNames: [
+ "gpii.test.integration.testCaseHolder.windows",
+ "gpii.test.integration.actionHandlersAware.windows"
+ ],
+ config: {
+ configName: "gpii.tests.acceptance.windows.builtIn.config",
+ configPath: "%gpii-universal/tests/platform/windows/configs"
+ },
+ components: {
+ pspClient: {
+ type: "gpii.tests.pspChannel.client"
+ },
+ loginPrefsSetUser: {
+ type: "gpii.tests.pspChannel.loginRequest",
+ options: {
+ gpiiKeyName: "context1"
+ }
+ }
+ },
+ sequence: [{
+ func: "{pspClient}.connect"
+ }, {
+ event: "{pspClient}.events.onConnect",
+ listener: "gpii.tests.pspChannel.connectionSucceeded"
+ }, {
+ event: "{pspClient}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "connectWithNoUsers"]
+ }, {
+ func: "{loginPrefsSetUser}.send"
+ }, {
+ event: "{pspClient}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "logoutUser"]
+ }, {
+ event: "{pspClient}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "context1_loggedIn"]
+ }, {
+ funcName: "gpii.tests.pspChannel.sendPrefsSetChange",
+ args: ["{pspClient}", "bright"]
+ }, {
+ event: "{pspClient}.events.onReceiveMessage",
+ listener: "gpii.tests.pspChannel.checkPayload",
+ args: ["{arguments}.0", "context1_bright"]
+ }]
+};
+
+gpii.test.runCouchTestDefs([
+ gpii.tests.pspChannel.testDef,
+ gpii.tests.pspChannel.prefsSetTestDef
+]);
+
+jqUnit.test("gpii.pspChannel.getPreferenceInfo() tests", function () {
+ var schemas = {
+ "http://registry.gpii.net/common/announceCapitals": {
+ "$schema": "gss-v7-full#",
+ "title": "Announce capitals",
+ "description": "Whether to announce capitals",
+ "type": "boolean",
+ "default": false
+ },
+ "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": {
+ "$schema": "gss-v7-full#",
+ "title": "Windows Built-in Screen Magnifier",
+ "type": "object",
+ "properties": {
+ "Invert": {
+ "title": "Invert Colours",
+ "description": "Enable colour inversion for Magnifier",
+ "enum": [0, 1],
+ "enumLabels": ["Off", "On"]
+ }
+ },
+ "additionalProperties": false
+ }
+ };
+ var testCases = {
+ "commonTermPref": {
+ name: "Process a common term",
+ prefsKey: "http://registry.gpii.net/common/announceCapitals",
+ prefsValue: 4,
+ expected: {
+ schema: {
+ "title": "Announce capitals",
+ "description": "Whether to announce capitals",
+ "type": "boolean",
+ "default": false
+ },
+ solutionName: "Announce capitals",
+ presentedCommonTerm: "http://registry.gpii.net/common/announceCapitals",
+ prefsValue: 4,
+ prefsKeySegs: ["http://registry.gpii.net/common/announceCapitals"]
+ }
+ },
+ "appTermWithoutSubCommonTerm": {
+ name: "Process an application specific term without having a common term being a sub-path",
+ prefsKey: "http://registry.gpii.net/applications/com.microsoft.windows.magnifier",
+ prefsValue: {
+ Invert: 0
+ },
+ expected: {
+ schema: {
+ "title": "Invert Colours",
+ "description": "Enable colour inversion for Magnifier",
+ "enum": [0, 1],
+ "enumLabels": ["Off", "On"]
+ },
+ solutionName: "Windows Built-in Screen Magnifier",
+ presentedCommonTerm: undefined,
+ prefsValue: 0,
+ prefsKeySegs: ["http://registry.gpii.net/applications/com.microsoft.windows.magnifier", "Invert"]
+ }
+ },
+ "appTermWithSubCommonTerm": {
+ name: "Process an application specific term with a common term being a sub-path",
+ prefsKey: "http://registry.gpii.net/applications/com.microsoft.windows.magnifier",
+ prefsValue: {
+ "http://registry.gpii.net/common/announceCapitals": true
+ },
+ expected: {
+ schema: {
+ "title": "Announce capitals",
+ "description": "Whether to announce capitals",
+ "type": "boolean",
+ "default": false
+ },
+ solutionName: "Windows Built-in Screen Magnifier",
+ presentedCommonTerm: "http://registry.gpii.net/common/announceCapitals",
+ prefsValue: true,
+ prefsKeySegs: ["http://registry.gpii.net/applications/com.microsoft.windows.magnifier", "http://registry.gpii.net/common/announceCapitals"]
+ }
+ }
+ };
+
+ fluid.each(testCases, function (oneTest) {
+ var result = gpii.pspChannel.getPreferenceInfo(schemas, oneTest.prefsKey, oneTest.prefsValue);
+ jqUnit.assertDeepEq("gpii.pspChannel.getPreferenceInfo() test - " + oneTest.name, oneTest.expected, result);
+ });
+});
diff --git a/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js b/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js
new file mode 100644
index 000000000..86befacab
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js
@@ -0,0 +1,171 @@
+/*!
+GPII Preferences Server Data Source Tests
+
+Copyright 2018 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ kettle = require("kettle"),
+ nock = require("nock");
+
+require("flowManager");
+
+fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js");
+
+kettle.loadTestingSupport();
+
+fluid.registerNamespace("gpii.tests.prefsServerDataSource");
+
+gpii.tests.prefsServerDataSource.hostname = "http://gpii.net";
+gpii.tests.prefsServerDataSource.preferencesUrl = "/preferences/%gpiiKey?merge=%merge";
+gpii.tests.prefsServerDataSource.readyUrl = "/ready";
+gpii.tests.prefsServerDataSource.gpiiKey = "gpii-key-test";
+gpii.tests.prefsServerDataSource.preferences = {
+ "http://registry.gpii.net/common/fontSize": 24
+};
+
+gpii.tests.prefsServerDataSource.nockConfig = {
+ preferencesGet: {
+ url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl, {
+ gpiiKey: gpii.tests.prefsServerDataSource.gpiiKey
+ }),
+ status: 200,
+ response: {
+ preferences: "user preferences received successfully"
+ }
+ },
+ preferencesPut: {
+ url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl, {
+ gpiiKey: gpii.tests.prefsServerDataSource.gpiiKey,
+ merge: "true"
+ }),
+ preferences: gpii.tests.prefsServerDataSource.preferences,
+ status: 200,
+ response: {
+ preferences: "saved successfully",
+ merge: true
+ }
+ },
+ preferencesPost: {
+ url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl),
+ preferences: gpii.tests.prefsServerDataSource.preferences,
+ status: 200,
+ response: {
+ preferences: "saved successfully"
+ }
+ },
+ ready: {
+ url: gpii.tests.prefsServerDataSource.readyUrl,
+ status: 200,
+ response: {
+ isReady: true
+ }
+ }
+};
+
+// Set up mock ajax responses
+gpii.tests.prefsServerDataSource.setUpNock = function (config) {
+ var cloudMock = nock(gpii.tests.prefsServerDataSource.hostname);
+
+ // log nock matches
+ cloudMock.log(console.log);
+
+ // mock GET requests to "/preferences"
+ cloudMock.get(config.preferencesGet.url)
+ .reply(config.preferencesGet.status, config.preferencesGet.response);
+
+ // mock PUT requests to "/preferences"
+ cloudMock.put(config.preferencesPut.url, gpii.tests.prefsServerDataSource.preferences)
+ .reply(config.preferencesPut.status, config.preferencesPut.response);
+
+ // mock POST requests to "/preferences"
+ cloudMock.post(config.preferencesPost.url, gpii.tests.prefsServerDataSource.preferences)
+ .reply(config.preferencesPost.status, config.preferencesPost.response);
+
+ // mock GET requests to "/ready"
+ cloudMock.get(config.ready.url)
+ .reply(config.ready.status, config.ready.response);
+};
+
+// The customized prefsServerDataSource component for tests
+fluid.defaults("gpii.tests.prefsServerDataSource", {
+ gradeNames: ["gpii.flowManager.prefsServerDataSource"],
+ prefsServerURL: gpii.tests.prefsServerDataSource.hostname
+});
+
+fluid.defaults("gpii.tests.prefsServerDataSourceTests", {
+ gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder", "gpii.test.testWithNock"],
+ invokers: {
+ setUpNock: {
+ funcName: "gpii.tests.prefsServerDataSource.setUpNock",
+ args: gpii.tests.prefsServerDataSource.nockConfig
+ }
+ },
+ components: {
+ prefsServerDataSource: {
+ type: "gpii.tests.prefsServerDataSource"
+ }
+ },
+ events: {
+ onResponse: null,
+ onError: null
+ }
+});
+
+fluid.defaults("gpii.tests.prefsServerDataSourceTests.tests", {
+ gradeNames: ["gpii.tests.prefsServerDataSourceTests"],
+ modules: [
+ {
+ name: "Get preferences",
+ tests: {
+ name: "Test get()",
+ expect: 1,
+ sequence: [{
+ task: "{prefsServerDataSource}.get",
+ args: [gpii.tests.prefsServerDataSource.gpiiKey],
+ resolve: "jqUnit.assertValue",
+ resolveArgs: ["The user preferences are received", "{arguments}.0.preferences"]
+ }]
+ }
+ },
+ {
+ name: "Update preferences",
+ tests: {
+ name: "Test update()",
+ expect: 1,
+ sequence: [{
+ task: "{prefsServerDataSource}.update",
+ args: [gpii.tests.prefsServerDataSource.gpiiKey, true, gpii.tests.prefsServerDataSource.preferences],
+ resolve: "jqUnit.assertTrue",
+ resolveArgs: ["The preferences are updated successfully", "{arguments}.0.merge"]
+ }]
+ }
+ },
+ {
+ name: "Check the liveness of the preferences server",
+ tests: {
+ name: "Test isLive()",
+ expect: 1,
+ sequence: [{
+ task: "{prefsServerDataSource}.isLive",
+ args: [],
+ resolve: "jqUnit.assertTrue",
+ resolveArgs: ["The expected liveness flag is received", "{arguments}.0"]
+ }]
+ }
+ }
+ ]
+});
+
+fluid.test.runTests([
+ "gpii.tests.prefsServerDataSourceTests.tests"
+]);
diff --git a/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js b/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js
new file mode 100644
index 000000000..58fede28c
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js
@@ -0,0 +1,437 @@
+/*!
+GPII Untrusted Settings Data Source Tests
+
+Copyright 2017 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ kettle = require("kettle"),
+ jqUnit = fluid.registerNamespace("jqUnit"),
+ nock = require("nock");
+
+require("flowManager");
+
+fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js");
+
+kettle.loadTestingSupport();
+
+fluid.registerNamespace("gpii.tests.settingsDataSource");
+
+gpii.tests.settingsDataSource.hostname = "http://gpii.net";
+gpii.tests.settingsDataSource.accessTokenUrl = "/access_token";
+gpii.tests.settingsDataSource.settingsGetUrl = "/%gpiiKey/settings/%device";
+gpii.tests.settingsDataSource.settingsPutUrl = "/%gpiiKey/settings";
+gpii.tests.settingsDataSource.gpiiKey = "gpii-key-test";
+gpii.tests.settingsDataSource.device = {
+ "OS": "windows 10"
+};
+gpii.tests.settingsDataSource.preferencesToUpdate = {
+ "http://registry.gpii.net/common/fontSize": 24
+};
+
+gpii.tests.settingsDataSource.nockConfig = {
+ accessToken: {
+ url: gpii.tests.settingsDataSource.accessTokenUrl,
+ data: {
+ "grant_type": "password",
+ "password": "dummy",
+ "client_id": "pilot-computer",
+ "client_secret": "pilot-computer-secret",
+ "username": gpii.tests.settingsDataSource.gpiiKey
+ },
+ status: 200,
+ response: {
+ access_token: "access-token-test-in-settingsDataSource",
+ expiresIn: 3600,
+ token_type: "Bearer"
+ }
+ },
+ settingsGet: {
+ url: fluid.stringTemplate(gpii.tests.settingsDataSource.settingsGetUrl, {
+ gpiiKey: gpii.tests.settingsDataSource.gpiiKey,
+ device: encodeURIComponent(JSON.stringify(gpii.tests.settingsDataSource.device))
+ }),
+ authHeader: {
+ header: "Authorization",
+ value: /Bearer .*/
+ },
+ status: 200,
+ response: {
+ settings: "user settings in success"
+ }
+ },
+ settingsPut: {
+ url: fluid.stringTemplate(gpii.tests.settingsDataSource.settingsPutUrl, {
+ gpiiKey: gpii.tests.settingsDataSource.gpiiKey
+ }),
+ authHeader: {
+ header: "Authorization",
+ value: /Bearer .*/
+ },
+ status: 200,
+ response: {
+ gpiiKey: gpii.tests.settingsDataSource.gpiiKey,
+ message: "Successfully updated."
+ }
+ }
+};
+
+// Set up mock ajax responses
+gpii.tests.settingsDataSource.setUpNock = function (config) {
+ var cloudMock = nock(gpii.tests.settingsDataSource.hostname);
+
+ // log nock matches
+ cloudMock.log(console.log);
+
+ // mock POST requests to "/access_token"
+ cloudMock.post(config.accessToken.url, config.accessToken.data)
+ .reply(config.accessToken.status, config.accessToken.response);
+
+ // mock GET requests to "/settings"
+ cloudMock.get(config.settingsGet.url)
+ .matchHeader(config.settingsGet.authHeader.header, config.settingsGet.authHeader.value)
+ .reply(config.settingsGet.status, config.settingsGet.response);
+
+ // mock PUT requests to "/settings"
+ cloudMock.put(config.settingsPut.url, gpii.tests.settingsDataSource.preferencesToUpdate)
+ .matchHeader(config.settingsPut.authHeader.header, config.settingsPut.authHeader.value)
+ .reply(config.settingsPut.status, config.settingsPut.response);
+};
+
+// The customized settingsDataSource component for tests
+fluid.defaults("gpii.tests.settingsDataSource", {
+ gradeNames: ["gpii.flowManager.settingsDataSource"],
+ cloudURL: gpii.tests.settingsDataSource.hostname,
+ clientCredentialFilePath: "%gpii-universal/gpii/node_modules/flowManager/test/data/clientCredential-correct.json"
+});
+
+// The base testEnvironment grade to be inherited by all tests
+fluid.defaults("gpii.tests.settingsDataSourceTests", {
+ gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder", "gpii.test.testWithNock"],
+ settingsDataSourceGrade: "gpii.tests.settingsDataSource", // supplied by individual tests
+ testCaseHolderGrade: null, // supplied by individual tests
+ invokers: {
+ setUpNock: {
+ funcName: "gpii.tests.settingsDataSource.setUpNock",
+ args: gpii.tests.settingsDataSource.nockConfig
+ }
+ },
+ distributeOptions: {
+ settingsDataSourceGrade: {
+ source: "{that}.options.settingsDataSourceGrade",
+ target: "{that > settingsDataSource}.type"
+ }
+ },
+ components: {
+ settingsDataSource: {
+ type: "gpii.tests.settingsDataSource"
+ }
+ },
+ events: {
+ onResponse: null,
+ onError: null
+ }
+});
+
+// Common test sequence elements
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.get", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ task: "{settingsDataSource}.get",
+ args: [gpii.tests.settingsDataSource.gpiiKey, gpii.tests.settingsDataSource.device],
+ resolve: "jqUnit.assertValue",
+ resolveArgs: ["The user settings are received", "{arguments}.0.settings"]
+ }]
+});
+
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.set", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ task: "{settingsDataSource}.set",
+ args: [gpii.tests.settingsDataSource.gpiiKey, gpii.tests.settingsDataSource.preferencesToUpdate],
+ resolve: "jqUnit.assertValue",
+ resolveArgs: ["The preferences are updated successfully", gpii.tests.settingsDataSource.gpiiKey, "{arguments}.0.gpiiKey"]
+ }]
+});
+
+// 1. No saved access token: retrieve and save an access token that then is used to retrieve user settings
+// 1.1 Define test sequence elements only used in this test
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.noInitialAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "jqUnit.assertDeepEq",
+ args: ["No saved access token", {}, "{settingsDataSource}.accessTokens"]
+ }]
+});
+
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyReceivedInitialAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "jqUnit.assertValue",
+ args: ["The access token is received and saved", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"]
+ }, {
+ func: "jqUnit.assertValue",
+ args: ["The expiresIn is received and saved", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"]
+ }]
+});
+
+// 1.2 define test sequences
+fluid.defaults("gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken", {
+ gradeNames: "fluid.test.sequence",
+ sequenceElements: {
+ noInitialAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.noInitialAccessToken",
+ priority: "first"
+ },
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get",
+ priority: "after:noInitialAccessToken"
+ },
+ verifyReceivedInitialAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyReceivedInitialAccessToken",
+ priority: "after:interactWithCloud"
+ }
+ }
+});
+
+fluid.defaults("gpii.tests.settingsDataSource.sequence.set.noSavedAccessToken", {
+ gradeNames: "gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken",
+ sequenceElements: {
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set"
+ }
+ }
+});
+
+// 1.3 define tests
+fluid.defaults("gpii.tests.settingsDataSourceTests.get.noSavedAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests"],
+ modules: [{
+ name: "A workflow with undefined initial access token: an access token is requested, saved and used to retrieve user settings",
+ tests: {
+ name: "Test get()",
+ expect: 4,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken"
+ }
+ }]
+});
+
+fluid.defaults("gpii.tests.settingsDataSourceTests.set.noSavedAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests", "fluid.test.testCaseHolder"],
+ modules: [{
+ name: "A workflow with undefined initial access token: an access token is requested, saved and used to retrieve user settings",
+ tests: {
+ name: "Test set()",
+ expect: 4,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.noSavedAccessToken"
+ }
+ }]
+});
+
+// 2. Has unexpired access token: use saved access token to retrieve user settings
+// 2.1 Custom "settingsDataSource" test component with an unexpired initial access token
+fluid.defaults("gpii.tests.settingsDataSource.validInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSource"],
+ members: {
+ accessTokens: {
+ "gpii-key-test": {
+ accessToken: "a-valid-initial-access-token",
+ timestampExpires: new Date(new Date().getTime() + 3600 * 1000).toISOString() // expires in 1 hour
+ }
+ }
+ }
+});
+
+// 2.2 Define test sequence elements only used in this test
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyValidInitialAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "gpii.tests.settingsDataSource.assertInitialAccessToken",
+ args: ["{settingsDataSourceTests}", "{settingsDataSource}", false]
+ }]
+});
+
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifySameAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "jqUnit.assertEquals",
+ args: ["The saved access token is same as the initial saved value", "{settingsDataSourceTests}.initialSavedAccessToken", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"]
+ }, {
+ func: "jqUnit.assertEquals",
+ args: ["The saved timestampExpires is same as the initial saved value", "{settingsDataSourceTests}.initialSavedTimestampExpires", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"]
+ }]
+});
+
+// 2.3 define test sequences
+fluid.defaults("gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken", {
+ gradeNames: "fluid.test.sequence",
+ sequenceElements: {
+ verifyValidInitialAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyValidInitialAccessToken",
+ priority: "first"
+ },
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get",
+ priority: "after:verifyValidInitialAccessToken"
+ },
+ verifySameAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifySameAccessToken",
+ priority: "after:interactWithCloud"
+ }
+ }
+});
+
+fluid.defaults("gpii.tests.settingsDataSource.sequence.set.validInitialAccessToken", {
+ gradeNames: "gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken",
+ sequenceElements: {
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set"
+ }
+ }
+});
+
+// 2.4 define tests
+fluid.defaults("gpii.tests.settingsDataSourceTests.get.validInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests"],
+ settingsDataSourceGrade: "gpii.tests.settingsDataSource.validInitialAccessToken",
+ modules: [{
+ name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings",
+ tests: {
+ name: "Test get()",
+ expect: 5,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken"
+ }
+ }]
+});
+
+fluid.defaults("gpii.tests.settingsDataSourceTests.set.validInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests"],
+ settingsDataSourceGrade: "gpii.tests.settingsDataSource.validInitialAccessToken",
+ modules: [{
+ name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings",
+ tests: {
+ name: "Test set()",
+ expect: 5,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.validInitialAccessToken"
+ }
+ }]
+});
+
+// 3. Has expired initial access token: retrieve a new access token before using /settings
+// 3.1 Custom "settingsDataSource" test component with an expired initial access token
+fluid.defaults("gpii.tests.settingsDataSource.expiredInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSource"],
+ members: {
+ accessTokens: {
+ "gpii-key-test": {
+ accessToken: "a-valid-initial-access-token",
+ timestampExpires: new Date(new Date().getTime() - 10 * 1000).toISOString() // already expired 10 secs ago
+ }
+ }
+ }
+});
+
+// 3.2 Define test sequence elements only used in this test
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyExpiredInitialAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "gpii.tests.settingsDataSource.assertInitialAccessToken",
+ args: ["{settingsDataSourceTests}", "{settingsDataSource}", true]
+ }]
+});
+
+fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyReceivedValidAccessToken", {
+ gradeNames: "fluid.test.sequenceElement",
+ sequence: [{
+ func: "jqUnit.assertNotEquals",
+ args: ["The saved access token is different from the initial saved value", "{settingsDataSourceTests}.initialSavedAccessToken", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"]
+ }, {
+ func: "jqUnit.assertNotEquals",
+ args: ["The saved timestampExpires is different from the initial saved value", "{settingsDataSourceTests}.initialSavedTimestampExpires", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"]
+ }]
+});
+
+// 3.3 define test sequences
+fluid.defaults("gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken", {
+ gradeNames: "fluid.test.sequence",
+ sequenceElements: {
+ verifyExpiredInitialAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyExpiredInitialAccessToken",
+ priority: "first"
+ },
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get",
+ priority: "after:verifyExpiredInitialAccessToken"
+ },
+ verifyReceivedValidAccessToken: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyReceivedValidAccessToken",
+ priority: "after:interactWithCloud"
+ }
+ }
+});
+
+fluid.defaults("gpii.tests.settingsDataSource.sequence.set.expiredInitialAccessToken", {
+ gradeNames: "gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken",
+ sequenceElements: {
+ interactWithCloud: {
+ gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set"
+ }
+ }
+});
+
+// 3.4 define tests
+fluid.defaults("gpii.tests.settingsDataSourceTests.get.expiredInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests"],
+ settingsDataSourceGrade: "gpii.tests.settingsDataSource.expiredInitialAccessToken",
+ modules: [{
+ name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings",
+ tests: {
+ name: "Test get()",
+ expect: 5,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken"
+ }
+ }]
+});
+
+fluid.defaults("gpii.tests.settingsDataSourceTests.set.expiredInitialAccessToken", {
+ gradeNames: ["gpii.tests.settingsDataSourceTests"],
+ settingsDataSourceGrade: "gpii.tests.settingsDataSource.expiredInitialAccessToken",
+ modules: [{
+ name: "A workflow with a valid initial access token: the saved access token is used to update preferences",
+ tests: {
+ name: "Test set()",
+ expect: 5,
+ sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.expiredInitialAccessToken"
+ }
+ }]
+});
+
+// Shared utility functions
+gpii.tests.settingsDataSource.assertInitialAccessToken = function (that, settingsDataSource, isExpired) {
+ var initialAccessToken = fluid.get(settingsDataSource, ["accessTokens", gpii.tests.settingsDataSource.gpiiKey, "accessToken"]);
+ var initialTimestampExpires = fluid.get(settingsDataSource, ["accessTokens", gpii.tests.settingsDataSource.gpiiKey, "timestampExpires"]);
+
+ jqUnit.assertValue("A saved access token is in place", initialAccessToken);
+ jqUnit[isExpired ? "assertTrue" : "assertFalse"]("A saved access token is not expired", gpii.oauth2.getExpiresIn(new Date(), initialTimestampExpires) === 0);
+ that.initialSavedAccessToken = initialAccessToken;
+ that.initialSavedTimestampExpires = initialTimestampExpires;
+};
+
+// Run all tests
+fluid.test.runTests([
+ "gpii.tests.settingsDataSourceTests.get.noSavedAccessToken",
+ "gpii.tests.settingsDataSourceTests.set.noSavedAccessToken",
+ "gpii.tests.settingsDataSourceTests.get.validInitialAccessToken",
+ "gpii.tests.settingsDataSourceTests.set.validInitialAccessToken",
+ "gpii.tests.settingsDataSourceTests.get.expiredInitialAccessToken",
+ "gpii.tests.settingsDataSourceTests.set.expiredInitialAccessToken"
+]);
diff --git a/gpii/node_modules/flowManager/test/SystemUtilsTests.js b/gpii/node_modules/flowManager/test/SystemUtilsTests.js
new file mode 100644
index 000000000..8d444f822
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/SystemUtilsTests.js
@@ -0,0 +1,123 @@
+/*!
+System Utils Tests
+
+Copyright 2019 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ jqUnit = fluid.require("node-jqunit", require, "jqUnit");
+
+fluid.require("%flowManager/src/SystemUtils.js");
+
+fluid.registerNamespace("gpii.tests.systemUtils");
+
+gpii.tests.systemUtils.arrayDifferenceTests = [
+ {
+ "name": "Remove a single element",
+ inputArray: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ],
+ elementsToRemove: "settings.configure2",
+ expected: [
+ "settings.configure1",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ]
+ }, {
+ "name": "Remove multiple elements",
+ inputArray: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ],
+ elementsToRemove: ["settings.configure2", "settings.configure3"],
+ expected: [
+ "settings.configure1",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ]
+ }, {
+ "name": "Remove unfounded elements",
+ inputArray: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ],
+ elementsToRemove: ["unfounded1", "unfounded2"],
+ expected: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ]
+ }, {
+ "name": "The input array stays unchanged when one argument is not provided",
+ inputArray: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ],
+ elementsToRemove: undefined,
+ expected: [
+ "settings.configure1",
+ "settings.configure2",
+ "settings.configure3",
+ {
+ "type": "gpii.windows.updateLanguage",
+ "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}"
+ }
+ ]
+ }, {
+ "name": "Remove element from array in which other items are substring of the element to be removed",
+ inputArray: [
+ "settings.configure",
+ "settings.configureSecond"
+ ],
+ elementsToRemove: "settings.configureSecond",
+ expected: [
+ "settings.configure"
+ ]
+ }
+];
+
+jqUnit.test("gpii.arrayDifference() tests", function () {
+ fluid.each(gpii.tests.systemUtils.arrayDifferenceTests, function (oneTest) {
+ gpii.arrayDifference(oneTest.inputArray, oneTest.elementsToRemove);
+ jqUnit.assertDeepEq("gpii.arrayDifference() test - " + oneTest.name, oneTest.expected, oneTest.inputArray);
+ });
+});
diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5
new file mode 100644
index 000000000..fee8f3f4c
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5
@@ -0,0 +1,25 @@
+{
+ "type": "gpii.flowManager.tests.browserChannel.config",
+ "options": {
+ "gradeNames": ["fluid.component"],
+ "distributeOptions": {
+ "browserChannel.tests.solutions": {
+ "record": "%flowManager/test/data/browserChannel_solutionsRegistry.json",
+ "target": "{that flowManager solutionsRegistryDataSource}.options.path",
+ "priority": "after:flowManager.solutions"
+ },
+ "browserChannel.tests.deviceReporter": {
+ "record": "%flowManager/test/data/browserChannel_deviceReporter.json",
+ "target": "{that deviceReporter installedSolutionsDataSource}.options.path",
+ "priority": "after:development.installedSolutionsPath"
+ },
+ "browserChannel.tests.platformReporting": {
+ "record": {
+ "funcName": "gpii.tests.flowManager.browserChannel.reportPlatform"
+ },
+ "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform"
+ }
+ }
+ },
+ "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5"
+}
diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5
new file mode 100644
index 000000000..d3a12630f
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5
@@ -0,0 +1,36 @@
+//
+// This configuration is used for testing the snappingshotting Capture API. Capturing setting will only
+// ever happen on a users local machine, so there is only an untrusted test configuration.
+//
+{
+ "type": "gpii.flowManager.tests.capture.fakeData.config",
+ "options": {
+ "distributeOptions": {
+ "capture.solutionsRegistry": {
+ "record": "%gpii-universal/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json",
+ "target": "{that flowManager solutionsRegistryDataSource}.options.path",
+ "priority": "after:flowManager.solutions"
+ },
+ "capture.deviceReporter": {
+ "record": "%gpii-universal/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json",
+ "target": "{that deviceReporter installedSolutionsDataSource}.options.path",
+ "priority": "after:development.installedSolutionsPath"
+ },
+ "capture.deviceReporterOS": {
+ "record": {
+ "funcName": "gpii.tests.flowManager.capture.platformReporter"
+ },
+ "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform",
+ "priority": "after:development.installedSolutionsPath"
+ },
+ "capture.resolverConfig": {
+ "record": {
+ "type": "gpii.tests.flowManager.capture.standardResolverConfig"
+ },
+ "target": "{that gpii.lifecycleManager.variableResolver}.options.components.resolverConfig",
+ "priority": "before:capture.solutionsRegistry"
+ }
+ }
+ },
+ "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5"
+}
diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5
new file mode 100644
index 000000000..2d7391a3b
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5
@@ -0,0 +1,6 @@
+{
+ "type": "gpii.flowManager.tests.config.base",
+ "mergeConfigs": [
+ "%gpii-universal/tests/configs/gpii.config.development.manualTesting.json5"
+ ]
+}
diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5
new file mode 100644
index 000000000..e6cbbef71
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5
@@ -0,0 +1,26 @@
+{
+ "type": "gpii.flowManager.tests.update.config",
+ "options": {
+ "gradeNames": ["fluid.component"],
+ "distributeOptions": {
+ "update.tests.solutions": {
+ "record": "%flowManager/test/data/update_solutionsRegistry.json",
+ "target": "{that flowManager solutionsRegistryDataSource}.options.path",
+ "priority": "after:flowManager.solutions"
+ },
+ "update.tests.deviceReporter": {
+ "record": "%flowManager/test/data/update_deviceReporter.json",
+ "target": "{that deviceReporter installedSolutionsDataSource}.options.path",
+ "priority": "after:development.installedSolutionsPath"
+ },
+ "update.tests.deviceReporterOS": {
+ "record": {
+ "funcName": "gpii.tests.flowManager.update.platformReporter"
+ },
+ "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform",
+ "priority": "after:development.installedSolutionsPath"
+ }
+ }
+ },
+ "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5"
+}
diff --git a/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json b/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json
new file mode 100644
index 000000000..70e3ee2e4
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json
@@ -0,0 +1,9 @@
+[
+ {
+ "id": "org.nvda-project"
+ },
+
+ {
+ "id": "org.gnome.orca"
+ }
+]
diff --git a/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json
new file mode 100644
index 000000000..4da58996c
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json
@@ -0,0 +1,52 @@
+{
+ "win32": {
+ "org.nvda-project": {
+ "name": "Dummy solution registry entry",
+ "contexts": {
+ "OS": [
+ {
+ "id": "win32"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "conf": {
+ "type": "gpii.settingsHandlers.webSockets",
+ "options": {
+ "path": "org.nvda-project"
+ }
+ }
+ },
+ "configure": [
+ "settings.conf"
+ ],
+ "restore": [
+ "settings.conf"
+ ]
+ },
+ "org.gnome.orca": {
+ "name": "Dummy solution registry entry",
+ "contexts": {
+ "OS": [
+ {
+ "id": "linux"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "conf": {
+ "type": "gpii.settingsHandlers.webSockets",
+ "options": {
+ "path": "org.gnome.orca"
+ }
+ }
+ },
+ "configure": [
+ "settings.conf"
+ ],
+ "restore": [
+ "settings.conf"
+ ]
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json b/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json
new file mode 100644
index 000000000..eb53914cb
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json
@@ -0,0 +1,8 @@
+[
+ {
+ "id": "fakemag1"
+ },
+ {
+ "id": "fakemag2"
+ }
+]
diff --git a/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json b/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json
new file mode 100644
index 000000000..a3c3735d9
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json
@@ -0,0 +1,5 @@
+{
+ "invert": 1,
+ "magnification": 4.5,
+ "location": "topRight"
+}
diff --git a/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json b/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json
new file mode 100644
index 000000000..5b2a4985b
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json
@@ -0,0 +1,4 @@
+{
+ "invert": true,
+ "magnification": 2.0
+}
diff --git a/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json
new file mode 100644
index 000000000..4be47fed0
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json
@@ -0,0 +1,138 @@
+{
+ "darwin": {
+ "fakemag1": {
+ "name": "Fake Magnifier 1",
+ "contexts": {
+ "OS": [
+ {
+ "id": "darwin"
+ }
+ ]
+ },
+ "capabilities": [
+ "http://registry\\.gpii\\.net/common/magnification/enabled"
+ ],
+ "settingsHandlers": {
+ "configuration": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler",
+ "liveness": "live",
+ "options": {
+ "filename": "${{test}.PWD}/data/capture_fakemag_settings.json"
+ },
+ "capabilitiesTransformations": {
+ "magnification": "http://registry\\.gpii\\.net/common/magnification"
+ },
+ "supportedSettings": {
+ "magnification": {
+ "schema": {
+ "title": "Magnification Level",
+ "description": "How many times to magnify content when magnification is enabled.",
+ "type": "number",
+ "default": 1.10
+ }
+ }
+ }
+ },
+ "configuration1": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler",
+ "liveness": "live",
+ "options": {
+ "filename": "${{test}.PWD}/data/capture_fakemag_settings.json"
+ },
+ "capabilitiesTransformations": {
+ "invert": "http://registry\\.gpii\\.net/common/invertColours"
+ },
+ "supportedSettings": {
+ "invert": {
+ "schema": {
+ "title": "Invert Colours",
+ "description": "Enable colour inversion for Magnifier",
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "configure": [
+ "settings.configuration",
+ "settings.configuration1"
+ ],
+ "restore": [
+ "settings.configuration",
+ "settings.configuration1"
+ ],
+ "start": [],
+ "stop": [],
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ },
+ "fakemag2": {
+ "name": "Fake Magnifier 2 - fully featured",
+ "contexts": {
+ "OS": [
+ {
+ "id": "darwin"
+ }
+ ]
+ },
+ "capabilities": [
+ "http://registry\\.gpii\\.net/common/magnification/enabled"
+ ],
+ "settingsHandlers": {
+ "configuration": {
+ "type": "gpii.settingsHandlers.JSONSettingsHandler",
+ "liveness": "live",
+ "options": {
+ "filename": "${{test}.PWD}/data/capture_fakemag2_settings.json"
+ },
+ "capabilitiesTransformations": {
+ "magnification": "http://registry\\.gpii\\.net/common/magnification",
+ "invert": "http://registry\\.gpii\\.net/common/invertColours",
+ "location": "http://registry\\.gpii\\.net/common/magnifierLocation"
+ },
+ "supportedSettings": {
+ "magnification": {
+ "schema": {
+ "title": "Magnification Level",
+ "description": "How many times to magnify content when magnification is enabled.",
+ "type": "number",
+ "default": 1.10
+ }
+ },
+ "invert": {
+ "schema": {
+ "title": "Invert Colours",
+ "description": "Enable colour inversion for Magnifier",
+ "type": "integer",
+ "default": 0
+ }
+ },
+ "location": {
+ "schema": {
+ "title": "Magnifier Location",
+ "description": "Magnifier location on screen",
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "configure": [
+ "settings.configuration"
+ ],
+ "restore": [
+ "settings.configuration"
+ ],
+ "start": [ ],
+ "stop": [ ],
+ "isInstalled": [
+ {
+ "type": "gpii.deviceReporter.alwaysInstalled"
+ }
+ ]
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/test/data/clientCredential-correct.json b/gpii/node_modules/flowManager/test/data/clientCredential-correct.json
new file mode 100644
index 000000000..c8a964a69
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/clientCredential-correct.json
@@ -0,0 +1,4 @@
+{
+ "client_id": "pilot-computer",
+ "client_secret": "pilot-computer-secret"
+}
diff --git a/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json b/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json
deleted file mode 100644
index cc04d8d2f..000000000
--- a/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json
+++ /dev/null
@@ -1,57 +0,0 @@
-[
- {
- "name": "GNOME Shell Magnifier",
- "id": "org.gnome.desktop.a11y.magnifier",
- "version": "3.2.1",
- "contexts": {
- "OS": {
- "id": "linux",
- "version": ">=2.6.26"
- }
- },
- "settingsHandlers": [
- {
- "type": "gpii.gsettings.set",
- "capabilities": [
- "display.screenEnhancement.magnification",
- "display.screenEnhancement.tracking"
- ],
- "capabilitiesTransformations": {
- "screenEnhancement": {
- "magnification": "mag-factor",
- "tracking": {
- "expander": {
- "type": "gpii.transform.valueMapper",
- "path": "mouse-tracking",
- "options": {
- "valueMap": {
- "mouse": "centered"
- }
- }
- }
- }
- }
- }
- }
- ],
- "launchHandlers": [
- {
- "type": "gpii.launch.exec",
- "options": {
- "start": {
- "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled",
- "args": [
- "true"
- ]
- },
- "stop": {
- "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled",
- "args": [
- "false"
- ]
- }
- }
- }
- ]
- }
-]
\ No newline at end of file
diff --git a/gpii/node_modules/flowManager/test/data/transformer.payload.example.json b/gpii/node_modules/flowManager/test/data/transformer.payload.example.json
deleted file mode 100644
index 9cb9c8f50..000000000
--- a/gpii/node_modules/flowManager/test/data/transformer.payload.example.json
+++ /dev/null
@@ -1,31 +0,0 @@
-[
- {
- "id": "org.gnome.desktop.a11y.magnifier",
- "settingsHandlers": [
- {
- "type": "gpii.gsettings.set,
- "settings": {
- "mag-factor": 200,
- "mouse-tracking": "centered",
- "show-cross-hairs": true
- },
- "options": {}
- }
- ],
- "launchHandlers": [
- {
- "type": "gpii.launch.gsettings",
- "start": {
- "schema": "org.gnome.desktop.a11y.applications",
- "key": "screen-magnifier-enabled",
- "value": true
- },
- "stop": {
- "schema": "org.gnome.desktop.a11y.applications",
- "key": "screen-magnifier-enabled",
- "value": false
- }
- }
- ]
- }
-]
\ No newline at end of file
diff --git a/gpii/node_modules/flowManager/test/data/update_deviceReporter.json b/gpii/node_modules/flowManager/test/data/update_deviceReporter.json
new file mode 100644
index 000000000..a3e8027d8
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/update_deviceReporter.json
@@ -0,0 +1,5 @@
+[
+ {
+ "id": "test.dummy"
+ }
+]
diff --git a/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json
new file mode 100644
index 000000000..634afcc92
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json
@@ -0,0 +1,25 @@
+{
+ "win32": {
+ "test.dummy": {
+ "name": "Dummy solution registry entry",
+ "contexts": {
+ "OS": [
+ {
+ "id": "darwin"
+ }
+ ]
+ },
+ "settingsHandlers": {
+ "myconf": {
+ "type": "gpii.settingsHandlers.noSettings",
+ "capabilitiesTransformations": {
+ "dummySetting1": "http://registry\\.gpii\\.net/common/fontSize",
+ "dummySetting2": "http://registry\\.gpii\\.net/common/setting2"
+ }
+ }
+ },
+ "configure": [],
+ "restore": []
+ }
+ }
+}
diff --git a/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js b/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js
new file mode 100644
index 000000000..9856241bd
--- /dev/null
+++ b/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js
@@ -0,0 +1,552 @@
+/**
+ * GPII Flow Manager Browser Channel Tests
+ *
+ * Copyright 2013-2020 OCAD University
+ * Copyright 2015 Emergya
+ * Copyright 2015 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/kettle/LICENSE.txt
+ */
+"use strict";
+
+var fluid = require("infusion"),
+ jqUnit = fluid.require("node-jqunit", require, "jqUnit"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.tests.flowManager.browserChannel");
+
+// TODO: These payloads are based on the "real" solutions 'org.nvda-project'
+// and 'org.gnome.orca'. The reason for the scare-quotes is because the
+// solutions are real and declared within the solutions registries used in
+// production. However, the solution registry used in these tests is a mock
+// (see ./data/browserChannel_solutionsRegistry.json).
+//
+// The solutions should be replaced with mock solutions, and associated mock
+// preferences, a mock solutions registry, and a mock device context. (Note:
+// these tests currently also use a mock device reporter payload, and could
+// continue to do so, but updated with new mock data. See
+// ./data/browserChannel_deviceReporter.json.
+//
+// As a warning: if mock solutions and mock preferences are used, and the
+// preferences file is located in %gpii-universal/tests/data/preferences, then
+// the preferences will not validate -- see:
+// %gpii-universal/gpii/node_modules/solutionsRegistry/test/SettingsPayloadTests.js
+//
+// GpiiKeys and prefsSafes are generated from those test preferences
+// (%gpii-universal/tests/data/preferences) and then used by the couchDB test
+// harness: It is a convenient place to put any test preferences. The
+// issue is that using mock solutions/preferences will create validation issues
+// elsewhere and that will have to be addressed somehow.
+gpii.tests.flowManager.browserChannel.payloads = {
+ "org.nvda-project": {
+ "speech.synth": "espeak",
+ "speech.outputDevice": "Microsoft Sound Mapper",
+ "speech.symbolLevel": 300,
+ "speech.espeak.rate": 40,
+ "speech.espeak.voice": "en-us",
+ "speech.espeak.pitch": 60,
+ "speech.espeak.volume": 80,
+ "reviewCursor.followFocus": "False",
+ "reviewCursor.followCaret": "True",
+ "reviewCursor.followMouse": "True",
+ "keyboard.speakTypedWords": "True",
+ "speech.espeak.rateBoost": "True",
+ "keyboard.speakTypedCharacters": "False",
+ "presentation.reportHelpBalloons": "False",
+ "speech.espeak.sayCapForCapitals": "True",
+ "virtualBuffers.autoSayAllOnPageLoad": "False"
+ },
+ "org.gnome.orca": {
+ "sayAllStyle": 1,
+ "enableEchoByWord": 1,
+ "enableEchoByCharacter": 0,
+ "voices.default.rate": 102.27272727272727,
+ "enableTutorialMessages": 0,
+ "voices.default.family": {
+ "locale": "en",
+ "name": "en-westindies"
+ },
+ "voices.default.average-pitch": 1,
+ "voices.default.gain": 7,
+ "verbalizePunctuationStyle": 0
+ }
+};
+
+// Modify two of the settings for 'org.nvda-project' to test set function via
+// BrowserChannel.
+gpii.tests.flowManager.browserChannel.nvdaSetSettings = fluid.extend(
+ true, {},
+ gpii.tests.flowManager.browserChannel.payloads["org.nvda-project"],
+ {
+ "speech.espeak.rate": 50,
+ "keyboard.speakTypedCharacters": "False"
+ }
+);
+
+gpii.tests.flowManager.browserChannel.reportPlatform = function () {
+ return {
+ id: "win32",
+ version: "x86-64"
+ };
+};
+
+gpii.tests.flowManager.browserChannel.checkConnectionRequest = function (data, request) {
+ request.events.onReceiveMessage.addListener(function (message, request) {
+ fluid.log("BrowserChannel checkConnectionRequest got onReceiveMessage ", message);
+ request.events[message.type].fire(message.payload);
+ });
+};
+
+gpii.tests.flowManager.browserChannel.checkClients = function (spec) {
+ spec = spec || {};
+ fluid.each(gpii.tests.flowManager.browserChannel.payloads, function (value, key) {
+ var count = spec[key] || 0;
+ jqUnit.assertEquals("Count of clients of type " + key, count, Object.keys(gpii.settingsHandlers.webSockets.instance.clients[key] || {}).length);
+ });
+};
+
+gpii.tests.flowManager.browserChannel.checkErrorResponse = function (gpiiKey, data, request) {
+ var expMsg = "Got logout request from user " + gpiiKey + ", but the user noUser is logged in. So ignoring the request.";
+ data = JSON.parse(data);
+ jqUnit.assertTrue("Received error as expected", data.isError);
+ jqUnit.assertEquals("Received message as expected", expMsg, data.message);
+ jqUnit.assertEquals("Received error code 409", 409, request.nativeResponse.statusCode);
+};
+
+gpii.tests.flowManager.browserChannel.checkPersistentSettings = function (clientId) {
+ var expectedSettings = gpii.tests.flowManager.browserChannel.payloads[clientId];
+ jqUnit.assertDeepEq("The settings for " + clientId + " in persistence are the expected following logon",
+ expectedSettings, gpii.settingsHandlers.webSockets.instance.getSettingsForId(clientId));
+};
+
+gpii.tests.flowManager.browserChannel.checkSettingsAfterSet = function (responses, expectedSettings) {
+ fluid.each(responses, function (settingsArray, response) {
+ jqUnit.assertDeepEq(
+ "The persistent settings for " + response + " are, after setting them,",
+ expectedSettings, settingsArray[0]
+ );
+ });
+};
+
+gpii.tests.flowManager.browserChannel.loginAndSettingsChanged = function (multiArg, spec) {
+ fluid.each(spec.clientIds, function (clientId) {
+ var expectedSettings = gpii.tests.flowManager.browserChannel.payloads[clientId];
+ gpii.tests.flowManager.browserChannel.checkPersistentSettings(clientId);
+ jqUnit.assertDeepEq("The settings sent via browserChannel for " + clientId + " are the expected following logon",
+ expectedSettings, multiArg[clientId][0]);
+ });
+ jqUnit.assertEquals("Login response is correct", "User with GPII key " + spec.gpiiKey + " was successfully logged in.", multiArg.login[0]);
+};
+
+gpii.tests.flowManager.browserChannel.connectionSucceeded = function (data, solutionId) {
+ // TODO: Slight risk of race condition here - this message might not arrive until settings have changed
+ jqUnit.assertDeepEq("Right after connecting, the initial settings transmitted for " + solutionId + " are as in persistence",
+ gpii.settingsHandlers.webSockets.instance.getSettingsForId(solutionId),
+ data
+ );
+};
+
+gpii.tests.flowManager.browserChannel.checkRejectedConnection = function (data, solutionId) {
+ jqUnit.assertDeepEq("An untrusted solutionId can't make use of the browserChannel", {
+ isError: true,
+ message: "Rejecting a connection request from '" + solutionId + "'. The solution id was not found in the solutions registry"
+ }, data);
+};
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.clientHolder", {
+ gradeNames: "kettle.test.request.ws",
+ path: "/browserChannel",
+ port: "{configuration}.options.mainServerPort",
+ solutionId: "",
+ settings: {},
+ events: {
+ onSettingsChanged: null,
+ changeSettingsReceived: null,
+ connectionSucceeded: null
+ },
+ listeners: {
+ connectionSucceeded: {
+ funcName: "gpii.tests.flowManager.browserChannel.connectionSucceeded",
+ args: ["{arguments}.0", "{that}.options.solutionId"]
+ }
+ },
+ invokers: {
+ sendId: {
+ func: "{that}.send",
+ args: {
+ type: "connect",
+ payload: {
+ solutionId: "{that}.options.solutionId"
+ }
+ }
+ },
+ sendChangeSettings: {
+ func: "{that}.send",
+ args: {
+ type: "changeSettings",
+ payload: {
+ settings: "{that}.options.settings"
+ }
+ }
+ }
+ }
+});
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.chromeClient", {
+ gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder",
+ solutionId: "org.nvda-project",
+ settings: gpii.tests.flowManager.browserChannel.nvdaSetSettings
+});
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.firefoxClient", {
+ gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder",
+ solutionId: "org.gnome.orca"
+});
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.unauthorizedClient", {
+ gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder",
+ solutionId: "com.unauthorized.app"
+});
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.gpiiKeyRequest", {
+ gradeNames: "kettle.test.request.http",
+ termMap: {
+ gpiiKey: "{that}.options.gpiiKey"
+ }
+});
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.loginRequest", {
+ gradeNames: "gpii.tests.flowManager.browserChannel.gpiiKeyRequest",
+ path: "/user/%gpiiKey/login"
+});
+
+gpii.tests.flowManager.browserChannel.testLogoutResponse = function (data, gpiiKey) {
+ jqUnit.assertEquals("Response is correct", "User with GPII key " + gpiiKey + " was successfully logged out.", data);
+};
+
+fluid.defaults("gpii.tests.flowManager.browserChannel.logoutRequest", {
+ gradeNames: "gpii.tests.flowManager.browserChannel.gpiiKeyRequest",
+ path: "/user/%gpiiKey/logout",
+ invokers: {
+ checkResponse: {
+ funcName: "gpii.tests.flowManager.browserChannel.testLogoutResponse",
+ args: ["{arguments}.0", "{that}.options.gpiiKey"]
+ }
+ }
+});
+
+gpii.tests.flowManager.browserChannel.testDefs = [{
+ name: "Flow Manager Simple BrowserChannel tests",
+ expect: 15,
+ config: {
+ configName: "gpii.flowManager.tests.browserChannel.config",
+ configPath: "%flowManager/test/configs"
+ },
+ events: {
+ loginAndSettingsChangedNvda: {
+ events: {
+ login: "{loginNvda}.events.onComplete",
+ "org.nvda-project": "{clientOne}.events.onSettingsChanged"
+ },
+ args: ["{arguments}", {
+ gpiiKey: "{loginNvda}.options.gpiiKey",
+ clientIds: ["org.nvda-project"]
+ }]
+ }
+ },
+ components: {
+ clientOne: {
+ type: "gpii.tests.flowManager.browserChannel.chromeClient"
+ },
+ loginNvda: {
+ type: "gpii.tests.flowManager.browserChannel.loginRequest",
+ options: {
+ gpiiKey: "screenreader_nvda"
+ }
+ },
+ logoutNvda1: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "screenreader_nvda"
+ }
+ },
+ logoutNvda2: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "screenreader_nvda"
+ }
+ }
+ },
+ sequence: [{
+ func: "gpii.tests.flowManager.browserChannel.checkClients"
+ }, {
+ func: "{clientOne}.connect" // chromeClient (org.nvda-project)
+ }, {
+ event: "{clientOne}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientOne}.sendId"
+ }, {
+ event: "{clientOne}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 1
+ }
+ }, {
+ func: "{loginNvda}.send"
+ }, {
+ event: "{testCaseHolder}.events.loginAndSettingsChangedNvda",
+ listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged"
+ }, {
+ func: "{logoutNvda1}.send"
+ }, {
+ event: "{logoutNvda1}.events.onComplete",
+ listener: "{logoutNvda1}.checkResponse"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 1
+ }
+ }, {
+ func: "{clientOne}.disconnect"
+ }, {
+ func: "{logoutNvda2}.send"
+ }, {
+ event: "{logoutNvda2}.events.onComplete",
+ listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse",
+ args: ["screenreader_nvda", "{arguments}.0", "{arguments}.1"]
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients"
+ }]
+}, {
+ name: "Flow Manager BrowserChannel tests",
+ expect: 32,
+ config: {
+ configName: "gpii.flowManager.tests.browserChannel.config",
+ configPath: "%flowManager/test/configs"
+ },
+ events: {
+ loginAndSettingsChangedChromeAndFirefox: {
+ events: {
+ login: "{loginChromeAndFirefox}.events.onComplete",
+ "org.nvda-project": "{clientTwo}.events.onSettingsChanged",
+ "org.gnome.orca": "{clientThree}.events.onSettingsChanged"
+ },
+ args: ["{arguments}", {
+ gpiiKey: "{loginChromeAndFirefox}.options.gpiiKey",
+ clientIds: ["org.nvda-project", "org.gnome.orca"]
+ }]
+ },
+ clientOneChangeSettings: {
+ events: {
+ "clientOneChangeReceipt": "{clientOne}.events.changeSettingsReceived",
+ "clientTwoSettingsChanged": "{clientTwo}.events.onSettingsChanged"
+ },
+ args: ["{arguments}", "{clientOne}.options.settings"]
+ }
+ },
+ components: {
+ clientOne: {
+ type: "gpii.tests.flowManager.browserChannel.chromeClient"
+ },
+ clientTwo: {
+ type: "gpii.tests.flowManager.browserChannel.chromeClient"
+ },
+ clientThree: {
+ type: "gpii.tests.flowManager.browserChannel.firefoxClient"
+ },
+ clientFour: {
+ type: "gpii.tests.flowManager.browserChannel.unauthorizedClient"
+ },
+ clientFive: {
+ type: "gpii.tests.flowManager.browserChannel.chromeClient"
+ },
+ loginNvda: {
+ type: "gpii.tests.flowManager.browserChannel.loginRequest",
+ options: {
+ gpiiKey: "screenreader_nvda"
+ }
+ },
+ logoutNvda1: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "Nvda1"
+ }
+ },
+ logoutNvda2: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "screenreader_nvda"
+ }
+ },
+ logoutOrca: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "screenreader_orca"
+ }
+ },
+ loginChromeAndFirefox: {
+ type: "gpii.tests.flowManager.browserChannel.loginRequest",
+ options: {
+ gpiiKey: "nvda_and_orca"
+ }
+ },
+ logoutChromeAndFirefox: {
+ type: "gpii.tests.flowManager.browserChannel.logoutRequest",
+ options: {
+ gpiiKey: "nvda_and_orca"
+ }
+ }
+ },
+ sequence: [{
+ func: "gpii.tests.flowManager.browserChannel.checkClients"
+ }, {
+ func: "{clientOne}.connect"
+ }, {
+ event: "{clientOne}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientOne}.sendId"
+ }, {
+ event: "{clientOne}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 1
+ }
+ }, {
+ func: "{clientTwo}.connect"
+ }, {
+ event: "{clientTwo}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientTwo}.sendId"
+ }, {
+ event: "{clientTwo}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 2
+ }
+ }, {
+ func: "{clientThree}.connect"
+ }, {
+ event: "{clientThree}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientThree}.sendId"
+ }, {
+ event: "{clientThree}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest"
+ }, {
+ func: "{clientFour}.connect"
+ }, {
+ event: "{clientFour}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientFour}.sendId"
+ }, {
+ event: "{clientFour}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkRejectedConnection",
+ args: ["{arguments}.0", "{clientFour}.options.solutionId"]
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 2,
+ "org.gnome.orca": 1
+ }
+ },
+ // All three clients now connected - clientOne sends a "changeSettings"
+ // request and gets back a receipt, whereas clientTwo receives the changes.
+ {
+ func: "{clientOne}.sendChangeSettings"
+ },
+ {
+ event: "{testCaseHolder}.events.clientOneChangeSettings",
+ listener: "gpii.tests.flowManager.browserChannel.checkSettingsAfterSet"
+ },
+ // Disconnect client 1 and try a spurious logout
+ {
+ func: "{clientOne}.disconnect"
+ }, {
+ func: "{logoutNvda1}.send"
+ }, {
+ event: "{logoutNvda1}.events.onComplete",
+ listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse",
+ args: ["Nvda1", "{arguments}.0", "{arguments}.1"]
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.nvda-project": 1,
+ "org.gnome.orca": 1
+ }
+ },
+ // Log in a user who has settings for two browsers
+ {
+ func: "{loginChromeAndFirefox}.send"
+ }, {
+ event: "{testCaseHolder}.events.loginAndSettingsChangedChromeAndFirefox",
+ listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged"
+ }, {
+ func: "{logoutChromeAndFirefox}.send"
+ }, {
+ event: "{logoutChromeAndFirefox}.events.onComplete",
+ listener: "{logoutChromeAndFirefox}.checkResponse"
+ }, {
+ func: "{clientTwo}.disconnect"
+ }, {
+ func: "{logoutOrca}.send"
+ }, {
+ event: "{logoutOrca}.events.onComplete",
+ listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse",
+ args: ["screenreader_orca", "{arguments}.0", "{arguments}.1"]
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients",
+ args: {
+ "org.gnome.orca": 1
+ }
+ }, {
+ func: "{clientThree}.disconnect"
+ }, // At this point, zero clients are connected
+ {
+ func: "{loginNvda}.send"
+ }, {
+ event: "{loginNvda}.events.onComplete",
+ listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged",
+ args: [{ // manually construct multiArg since there is only one event
+ login: ["{arguments}.0"]
+ }, {
+ gpiiKey: "{loginNvda}.options.gpiiKey"
+ }]
+ }, {
+ func: "{clientFive}.connect"
+ }, {
+ event: "{clientFive}.events.onConnect",
+ listener: "fluid.identity"
+ }, {
+ func: "{clientFive}.sendId"
+ }, {
+ event: "{clientFive}.events.onReceiveMessage",
+ listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkPersistentSettings",
+ args: "org.nvda-project"
+ }, {
+ func: "{clientFive}.disconnect"
+ }, {
+ func: "{logoutNvda2}.send"
+ }, {
+ event: "{logoutNvda2}.events.onComplete",
+ listener: "{logoutNvda2}.checkResponse"
+ }, {
+ func: "gpii.tests.flowManager.browserChannel.checkClients"
+ }]
+}];
diff --git a/gpii/node_modules/gpii-db-operation/README.md b/gpii/node_modules/gpii-db-operation/README.md
new file mode 100644
index 000000000..1f6386e2f
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/README.md
@@ -0,0 +1,4 @@
+# gpii-db-operation
+
+The gpii-db-operation module is responsible for handling CouchDB create/read/update/delete operations for all GPII
+components.
diff --git a/gpii/node_modules/gpii-db-operation/index.js b/gpii/node_modules/gpii-db-operation/index.js
new file mode 100644
index 000000000..869e83e51
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/index.js
@@ -0,0 +1,11 @@
+"use strict";
+
+var fluid = require("infusion");
+
+fluid.module.register("gpii-db-operation", __dirname, require);
+
+require("./src/DataStore.js");
+require("./src/DbConst.js");
+require("./src/DbDataStoreUtils.js");
+require("./src/DbDataStore.js");
+require("./src/DbUtils.js");
diff --git a/gpii/node_modules/gpii-db-operation/package.json b/gpii/node_modules/gpii-db-operation/package.json
new file mode 100644
index 000000000..649875a29
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "gpii-db-operation",
+ "description": "Responsible for handling the CouchDB create/read/update/delete operations for all GPII components",
+ "version": "0.1.0",
+ "author": "GPII",
+ "bugs": "http://issues.gpii.net/browse/GPII",
+ "homepage": "http://gpii.net/",
+ "dependencies": {},
+ "license" : "BSD-3-Clause",
+ "repository": "git://github.com/GPII/universal.git",
+ "main": "./index.js",
+ "engines": { "node" : ">=4.2.1" }
+}
diff --git a/gpii/node_modules/gpii-db-operation/src/DataStore.js b/gpii/node_modules/gpii-db-operation/src/DataStore.js
new file mode 100644
index 000000000..7a84bdc7d
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/src/DataStore.js
@@ -0,0 +1,21 @@
+/*!
+GPII Base Data Store
+
+Copyright 2016 OCAD university
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = fluid || require("infusion");
+
+fluid.registerNamespace("gpii.dbOperation");
+
+fluid.defaults("gpii.dbOperation.dataStore", {
+ gradeNames: "fluid.component"
+});
diff --git a/gpii/node_modules/gpii-db-operation/src/DbConst.js b/gpii/node_modules/gpii-db-operation/src/DbConst.js
new file mode 100644
index 000000000..b00ce58a5
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/src/DbConst.js
@@ -0,0 +1,53 @@
+/*!
+Copyright 2017 OCAD university
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+"use strict";
+
+var fluid = fluid || require("infusion");
+
+var gpii = fluid.registerNamespace("gpii");
+fluid.registerNamespace("gpii.dbOperation");
+
+// The current version of schemas that document structures correspond with.
+gpii.dbOperation.schemaVersion = "0.2";
+
+// All doc types used for saving different documents into CouchDB
+// See [GPII Data Model](https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences#Data_Model)
+// regarding accepted fields for each document type.
+gpii.dbOperation.docTypes = fluid.freezeRecursive({
+ gpiiKey: "gpiiKey",
+ prefsSafe: "prefsSafe",
+ clientCredential: "clientCredential",
+ gpiiAppInstallationClient: "gpiiAppInstallationClient",
+ gpiiAppInstallationAuthorization: "gpiiAppInstallationAuthorization"
+});
+
+// All error details that the dbOperation module reports.
+gpii.dbOperation.errors = fluid.freezeRecursive({
+ missingInput: {
+ message: "The input field \"%fieldName\" was undefined",
+ statusCode: 400,
+ isError: true
+ },
+ missingDoc: {
+ message: "A record of type \"%docType\" was not found",
+ statusCode: 400,
+ isError: true
+ },
+ mismatchedDocType: {
+ message: "The document type must be \"%docType\" instead of the selected document type \"%selectedDocType\"",
+ statusCode: 400,
+ isError: true
+ },
+ unauthorized: {
+ message: "Unauthorized",
+ statusCode: 401,
+ isError: true
+ }
+});
diff --git a/gpii/node_modules/gpii-db-operation/src/DbDataStore.js b/gpii/node_modules/gpii-db-operation/src/DbDataStore.js
new file mode 100644
index 000000000..5888e4700
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/src/DbDataStore.js
@@ -0,0 +1,265 @@
+/*!
+GPII CouchDB Data Store
+
+Copyright 2016-2019 OCAD university
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+/*
+ * gpii.dbOperation.dbDataStore provides APIs for the authorization server to communicate with the backend
+ * data storage using CouchDB.
+ *
+ * This DB data store is a re-writing of the initial synchronized in memory data store. It now uses
+ * async promise API to satisfy the async database operations. However, the in memory data store continues
+ * to serve as a good reference with an much simpler logic. It can be found at:
+ * https://github.com/GPII/universal/blob/820e4919907e56f6412b2e3bab18675d5388b00b/gpii/node_modules/gpii-oauth2/gpii-oauth2-datastore/src/InMemoryDataStore.js
+ */
+
+"use strict";
+
+var fluid = fluid || require("infusion");
+var gpii = fluid.registerNamespace("gpii");
+
+if (typeof require !== "undefined") {
+ fluid.require("kettle");
+}
+
+fluid.defaults("gpii.dbOperation.dbDataSource", {
+ gradeNames: ["kettle.dataSource.URL", "kettle.dataSource.CouchDB"],
+ baseUrl: null, // Supplied by integrators
+ port: null, // Supplied by integrators
+ dbName: null, // Supplied by integrators
+ requestUrl: null, // Supplied by integrators
+ termMap: {
+ },
+ directModel: {
+ },
+ notFoundIsEmpty: true,
+ rules: {
+ writePayload: {
+ "": ""
+ },
+ readPayload: {
+ "": ""
+ }
+ },
+ // requestUrl needs to be resolved upfront because it contains more string templates that need to be replaced at the
+ // next round when kettle.dataSource.URL kicks in to compose the actual URL.
+ // An example of requestUrl is "/%id", in which case the expected url should be "%baseUrl:%port/%dbName/%id" instead
+ // of having "%requestUrl" embedded. The expander below is to prepare the url that's sensible to kettle.dataSource.
+ url: {
+ expander: {
+ funcName: "fluid.stringTemplate",
+ args: ["%baseUrl:%port/%dbName%requestUrl", {
+ requestUrl: "{that}.options.requestUrl",
+ baseUrl: "{that}.options.baseUrl",
+ port: "{that}.options.port",
+ dbName: "{that}.options.dbName"
+ }]
+ }
+ }
+});
+
+fluid.defaults("gpii.dbOperation.dbDataSource.writable", {
+ gradeNames: ["gpii.dbOperation.dbDataSource", "kettle.dataSource.CouchDB.writable"],
+ writable: true,
+ writeMethod: "PUT"
+});
+
+fluid.defaults("gpii.dbOperation.dbDataStore", {
+ gradeNames: ["gpii.dbOperation.dataStore"],
+ // Supplied by GPII configuration to config all gpii.dbOperation.dbDataSource instances.
+ // It contains these elements:
+ // 1. gradeNames: The mixin grade
+ // 2. baseUrl: The base URL to where the database is located. For example, a default locally installed CouchDB uses http://127.0.1.1
+ // 3. port: The port where the database is located. For example, a default locally installed CouchDB uses port 5984
+ // 4. dbName: The database name
+ dataSourceConfig: {
+ },
+ distributeOptions: {
+ "dbDataStore.dataSourceConfig": {
+ source: "{that}.options.dataSourceConfig",
+ target: "{that > gpii.dbOperation.dbDataSource}.options"
+ }
+ },
+ components: {
+ findByIdDataSource: {
+ type: "gpii.dbOperation.dbDataSource",
+ options: {
+ requestUrl: "/%id",
+ termMap: {
+ id: "%id"
+ }
+ }
+ },
+ findPrefsSafeByGpiiKeyDataSource: {
+ type: "gpii.dbOperation.dbDataSource",
+ options: {
+ requestUrl: "/_design/views/_view/findPrefsSafeByGpiiKey?key=%22%gpiiKey%22&include_docs=true",
+ termMap: {
+ gpiiKey: "%gpiiKey"
+ },
+ rules: {
+ readPayload: {
+ "": "rows.0"
+ }
+ }
+ }
+ },
+ findClientByOauth2ClientIdDataSource: {
+ type: "gpii.dbOperation.dbDataSource",
+ options: {
+ requestUrl: "/_design/views/_view/findClientByOauth2ClientId?key=%22%oauth2ClientId%22&include_docs=true",
+ termMap: {
+ oauth2ClientId: "%oauth2ClientId"
+ },
+ rules: {
+ readPayload: {
+ "": "rows.0"
+ }
+ }
+ }
+ },
+ findAllViewsDataSource: {
+ type: "gpii.dbOperation.dbDataSource",
+ options: {
+ requestUrl: "/_design/views"
+ }
+ },
+ findInfoByAccessTokenDataSource: {
+ type: "gpii.dbOperation.dbDataSource",
+ options: {
+ requestUrl: "/_design/views/_view/findInfoByAccessToken?key=%22%accessToken%22&include_docs=true",
+ termMap: {
+ accessToken: "%accessToken"
+ },
+ rules: {
+ readPayload: {
+ "": "rows.0"
+ }
+ }
+ }
+ },
+ saveDataSource: {
+ type: "gpii.dbOperation.dbDataSource.writable",
+ options: {
+ requestUrl: "/%id",
+ termMap: {
+ id: "%id"
+ }
+ }
+ }
+ },
+ invokers: {
+ findById: {
+ funcName: "gpii.dbOperation.dbDataStore.findRecord",
+ args: [
+ "{that}.findByIdDataSource",
+ {
+ id: "{arguments}.0"
+ },
+ "id"
+ ]
+ // id
+ },
+ findGpiiKey: {
+ func: "{that}.findById"
+ // gpiiKey
+ },
+ findClientById: {
+ func: "{that}.findById"
+ // clientId
+ },
+ findClientCredentialById: {
+ func: "{that}.findById"
+ // clientCredentialId
+ },
+ findPrefsSafeByGpiiKey: {
+ funcName: "gpii.dbOperation.dbDataStore.findRecord",
+ args: [
+ "{that}.findPrefsSafeByGpiiKeyDataSource",
+ {
+ gpiiKey: "{arguments}.0"
+ },
+ "gpiiKey",
+ gpii.dbOperation.dbDataStore.findPrefsSafeByGpiiKeyPostProcess
+ ]
+ // gpiiKey
+ },
+ findClientByOauth2ClientId: {
+ funcName: "gpii.dbOperation.dbDataStore.findRecord",
+ args: [
+ "{that}.findClientByOauth2ClientIdDataSource",
+ {
+ oauth2ClientId: "{arguments}.0"
+ },
+ "oauth2ClientId",
+ gpii.dbOperation.dbDataStore.findClientByOauth2ClientIdPostProcess
+ ]
+ // oauth2ClientId
+ },
+ findInfoByAccessToken: {
+ funcName: "gpii.dbOperation.dbDataStore.findRecord",
+ args: [
+ "{that}.findInfoByAccessTokenDataSource",
+ {
+ accessToken: "{arguments}.0"
+ },
+ "accessToken",
+ gpii.dbOperation.dbDataStore.findInfoByAccessTokenPostProcess
+ ]
+ // accessToken
+ },
+ findAllViews: {
+ funcName: "gpii.dbOperation.dbDataStore.findRecord",
+ args: ["{that}.findAllViewsDataSource"]
+ },
+ addGpiiKey: {
+ funcName: "gpii.dbOperation.dbDataStore.addGpiiKey",
+ args: [
+ "{that}.saveDataSource",
+ "{arguments}.0"
+ ]
+ // gpiiKeyData
+ },
+ updateGpiiKey: {
+ funcName: "gpii.dbOperation.dbDataStore.updateGpiiKey",
+ args: [
+ "{that}.saveDataSource",
+ "{arguments}.0",
+ "{arguments}.1"
+ ]
+ // gpiiKey, gpiiKeyData
+ },
+ addPrefsSafe: {
+ funcName: "gpii.dbOperation.dbDataStore.addPrefsSafe",
+ args: [
+ "{that}.saveDataSource",
+ "{arguments}.0"
+ ]
+ // prefsSafeData
+ },
+ updatePrefsSafe: {
+ funcName: "gpii.dbOperation.dbDataStore.updatePrefsSafe",
+ args: [
+ "{that}.saveDataSource",
+ "{arguments}.0",
+ "{arguments}.1"
+ ]
+ // prefsSafeId, prefsSafeData
+ },
+ addAuthorization: {
+ funcName: "gpii.dbOperation.dbDataStore.addAuthorization",
+ args: [
+ "{that}.saveDataSource",
+ "{arguments}.0"
+ ]
+ // authorizationData
+ }
+ }
+});
diff --git a/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js b/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js
new file mode 100644
index 000000000..1fb7fd042
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js
@@ -0,0 +1,562 @@
+/*!
+GPII CouchDB Data Store Utilities
+
+Copyright 2016-2019 OCAD university
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = fluid || require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ $ = fluid.registerNamespace("jQuery"),
+ uuid = uuid || require("node-uuid");
+
+fluid.registerNamespace("gpii.dbOperation.dbDataStore");
+
+/**
+ * Use the kettle dataSource `get` method to retrieve one record. This function provides extra
+ * verification on input required fields. It returns an empty object if the record is not found.
+ * This requires further processing besides using the kettle dataSource `notFoundIsEmpty` option because
+ * when retrieving CouchDB using views , an empty `rows` array rather than 404 (not found) http
+ * response code will be received when the record is not found.
+ * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource.
+ * @param {Object} directModel - The direct model expressing the "coordinates" of the model to be fetched.
+ * @param {String|Array} valueNotEmpty - One or more required field(s).
+ * @param {Function} dataProcessFunc - The function to further process the retrieved record when the returned
+ * record is not empty.
+ * @return {Promise} A promise for the retrieved record.
+ */
+gpii.dbOperation.dbDataStore.findRecord = function (dataSource, directModel, valueNotEmpty, dataProcessFunc) {
+ // Remove or rename CouchDB specific fields such as _id, _rev, type
+ dataProcessFunc = dataProcessFunc || gpii.dbOperation.dbDataStore.cleanUpDoc;
+ var promiseTogo = fluid.promise();
+
+ // Verify required field values. Make sure they are not undefined.
+ var emptyFields = gpii.dbOperation.dbDataStore.filterEmptyFields(directModel, valueNotEmpty);
+
+ if (emptyFields.length > 0) {
+ var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: emptyFields.join(" & ")});
+ fluid.log("gpii-db-operation, findRecord(), error: ", error);
+ promiseTogo.reject(error); // reject #1 of 3.
+ } else {
+ var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel);
+ var promise = dataSource.get(finalDirectModel);
+ promise.then(function (data) {
+ // TODO: The line below that converts an empty object to undefined is to work around an issue with using the
+ // kettle notFoundIsEmpty option with fetching couchDB documents by views. The way that notFoundIsEmpty is
+ // implemented in kettle is that, it returns undefined when encountering a 404 response. However, when querying
+ // couchdb by views, the returned value would not be a 404 http status code even when the doc is not found.
+ // The response would still be an object but with an empty "rows" array. An example response is:
+ // { total_rows: 1, offset: 0, rows: [] }
+ // This response is then further transformed using kettle readPayload option:
+ // readPayload: { "": "rows.0.value" }
+ // Due to an issue with the infusion model transformation described at https://issues.fluidproject.org/browse/FLUID-5969,
+ // after the transformation, an empty object is eventually received here and then converted into undefined.
+ // Note that this issue only occurs when querying CouchDB by a view(map) function. when querying CouchDB directly
+ // by a document id, 404 status is returned and this conversion is not needed.
+ var result = $.isEmptyObject(data) ? undefined : dataProcessFunc(data);
+ if (result !== undefined && result.isError) {
+ promiseTogo.reject(result); // reject #2 of 3.
+ } else {
+ promiseTogo.resolve(result); // result can be undefined.
+ }
+ }, function (error) {
+ fluid.log("gpii-db-operation, findRecord(), error: ", error);
+ promiseTogo.reject(error); // reject #3 of 3.
+ });
+ }
+ return promiseTogo;
+};
+
+/**
+ * Filter the given array valueNotEmpty to return elements that satisfy:
+ * 1. the element isn't used as a path name in the object;
+ * 2. the element matches a path name in the object but the corresponding value is undefined.
+ * Note the given object can NOT be a nested object.
+ * @param {Object} obj - The object used for path name check.
+ * @param {String|Array} valueNotEmpty - One or a set of path name(s) to look up in the give obj.
+ * @return {Array} An subset array of valueNotEmpty.
+ * For example, gpii.dbOperation.dbDataStore.filterEmptyFields({"a": 1, "c": undefined}, ["a", "b", "c"]) returns ["b", "c"].
+ */
+gpii.dbOperation.dbDataStore.filterEmptyFields = function (obj, valueNotEmpty) {
+ var emptyFields = [];
+
+ valueNotEmpty = fluid.makeArray(valueNotEmpty);
+ fluid.each(valueNotEmpty, function (fieldName) {
+ if (obj[fieldName] === undefined) {
+ emptyFields.push(fieldName);
+ }
+ });
+ return emptyFields;
+};
+
+/**
+ * Remove the CouchDB internal fields: _id and _rev. Also save "_id" field value into "id" field.
+ * The use of "id" instead of "_id" field name is to maintain the API backward compatibility as data store
+ * API is expected to output the record identifier in "id" field instead of a couchdb specific name
+ * of "_id".
+ * @param {Object} data - An object to transform.
+ * @return {Object} An object with CouchDB-specific internal fields transformed.
+ */
+gpii.dbOperation.dbDataStore.cleanUpDoc = function (data) {
+ if (data) {
+ data.id = data._id;
+ delete data._id;
+ delete data._rev;
+ }
+ return data;
+};
+
+/** Use the kettle dataSource `set` method to create a new record. Before sending the input data to
+ * CouchDB, it is modified by adding an unique _id field and a proper document type.
+ * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record creation.
+ * @param {String} docType - The document type. See gpii.dbOperation.docTypes defined in
+ * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js.
+ * @param {String} idName - The name for the unique id field. Usually "id".
+ * @param {Object} data - The data to be saved in the new record.
+ * @param {String} [idValue] - [optional] The id value. If not provided, an UUID will be generated.
+ * @return {Promise} A promise for the save response.
+*/
+gpii.dbOperation.dbDataStore.addRecord = function (dataSource, docType, idName, data, idValue) {
+ var promise = fluid.promise();
+
+ if (data !== undefined) {
+ var directModel = {};
+ directModel[idName] = idValue || uuid.v4();
+ fluid.extend(data, {type: docType});
+ var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel);
+ promise = dataSource.set(finalDirectModel, data);
+ } else {
+ fluid.log("gpii-db-operation, addRecord(), error occurs: ", error);
+ var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType});
+ promise.reject(error);
+ }
+ return promise;
+};
+
+/** Use the kettle dataSource `set` method to udpate a record by its id.
+ * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record update.
+ * @param {String} docType - The expected document type in the updated record. See gpii.dbOperation.docTypes defined in
+ * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js.
+ * @param {String} docId - The document id.
+ * @param {Object} data - The data to be updated.
+ * @return {Promise} A promise for the update response.
+*/
+gpii.dbOperation.dbDataStore.updateRecord = function (dataSource, docType, docId, data) {
+ var promise = fluid.promise();
+ var error;
+
+ if (!data) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType});
+ promise.reject(error);
+ } else if (data.type !== docType) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.mismatchedDocType, {docType: docType, selectedDocType: data.type});
+ promise.reject(error);
+ } else {
+ var directModel = {
+ id: docId
+ };
+ var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel);
+ promise = dataSource.set(finalDirectModel, data);
+ }
+ return promise;
+};
+
+// General GPII keys Functions
+// ---------------------------
+
+/**
+ * Add a GPII key
+ * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore.
+ * @param {Object} gpiiKeyData - The GPII key data. An example of the data:
+ * {
+ * gpiiKey: {String}, // Optional
+ * prefsSafeId: {String},
+ * prefsSetId: {String}
+ * }
+ * If this parameter is not provided, a new GPII key is still generated but does not associate with a prefs safe.
+ * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the
+ * GPII key record. When `gpiiKeyData` is not provided, returns the object {prefsSafeId: null, prefsSetId: null}.
+ */
+gpii.dbOperation.dbDataStore.addGpiiKey = function (saveDataSource, gpiiKeyData) {
+ var promiseTogo = fluid.promise();
+ var data;
+
+ if (!gpiiKeyData) {
+ gpiiKeyData = {
+ prefsSafeId: null,
+ prefsSetId: null
+ };
+ }
+
+ data = {
+ schemaVersion: gpii.dbOperation.schemaVersion,
+ prefsSafeId: gpiiKeyData.prefsSafeId || null,
+ prefsSetId: gpiiKeyData.prefsSetId || null,
+ revoked: false,
+ revokedReason: null,
+ timestampCreated: gpii.dbOperation.getCurrentTimestamp(),
+ timestampUpdated: null
+ };
+
+ promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, "id", data, gpiiKeyData.gpiiKey);
+
+ return promiseTogo;
+};
+
+/**
+ * Update a GPII key record
+ * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore.
+ * @param {String} gpiiKey - The GPII key.
+ * @param {Object} gpiiKeyData - The GPII key data. An example of the data:
+ * {
+ * type: {String},
+ * schemaVersion: {String},
+ * prefsSafeId: {String},
+ * prefsSetId: {String},
+ * revoked: {String},
+ * revokedReason: {String},
+ * timestampCreated: {Date},
+ * timestampRevoked: {Date}
+ * }
+ *
+ * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the
+ * GPII key record, or an error if `gpiiKey` or `gpiiKeyData` is not provided or `gpiiKeyData.type` is not
+ * the doc type for gpiiKey.
+ */
+gpii.dbOperation.dbDataStore.updateGpiiKey = function (saveDataSource, gpiiKey, gpiiKeyData) {
+ var promiseTogo = fluid.promise();
+ var data, error;
+
+ if (!gpiiKey) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "gpiiKey"});
+ promiseTogo.reject(error);
+ } else if (!gpiiKeyData) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiKey});
+ promiseTogo.reject(error);
+ } else {
+ data = {
+ type: gpiiKeyData.type,
+ schemaVersion: gpiiKeyData.schemaVersion,
+ prefsSafeId: gpiiKeyData.prefsSafeId,
+ prefsSetId: gpiiKeyData.prefsSetId,
+ revoked: gpiiKeyData.revoked,
+ revokedReason: gpiiKeyData.revokedReason,
+ timestampCreated: gpiiKeyData.timestampCreated,
+ timestampUpdated: gpii.dbOperation.getCurrentTimestamp(),
+ timestampRevoked: gpiiKeyData.timestampRevoked
+ };
+
+ promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, gpiiKey, data);
+ }
+ return promiseTogo;
+};
+
+// General Preferences Safes Functions
+// -----------------------------------
+
+/**
+ * Transform the data in CouchDB form to a more understandable structure
+ * @param {Object} data - Contains GPII key and preferences safe information associated with a GPII key.
+ * An input example:
+ * {
+ * key: {String}, // GPII key
+ * id: {String}, // GPII key
+ * value: {
+ * _id: {String}, // prefs Safe id
+ * gpiiKey: {
+ * type: {String},
+ * schemaVersion: {String},
+ * prefsSafeId: {String},
+ * prefsSetId: {String},
+ * revoked: {Boolean},
+ * revokedReason: {String},
+ * timestampCreated: {Date},
+ * timestampUpdated: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * },
+ * doc: {
+ * type: {String},
+ * schemaVersion: {String},
+ * name: {String},
+ * password: {String},
+ * email: {String},
+ * preferences: {Object},
+ * timestampCreated: {Date},
+ * timestampUpdated: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * }
+ * @return {Object} An object in the structure:
+ * {
+ * gpiiKey: {String},
+ * gpiiKeyDetails: {Object},
+ * prefsSafe: {Object}
+ * }
+ */
+gpii.dbOperation.dbDataStore.findPrefsSafeByGpiiKeyPostProcess = function (data) {
+ var result;
+
+ if (data && data.doc && data.value) {
+ result = {
+ gpiiKey: data.key,
+ gpiiKeyDetails: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.gpiiKey),
+ prefsSafe: data.doc.type === gpii.dbOperation.docTypes.prefsSafe ? gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc) : null
+ };
+ }
+
+ return result;
+};
+
+/**
+ * Add a prefs safe
+ * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore.
+ * @param {Object} prefsSafeData - The prefs safe data. An example of the data:
+ * {
+ * prefsSafeType: {String},
+ * name: {String},
+ * password: {String},
+ * email: {String},
+ * preferences: {Object}
+ * }
+ *
+ * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the
+ * prefs safe record, or an error if `prefsSafeData` parameter is not provided.
+ */
+gpii.dbOperation.dbDataStore.addPrefsSafe = function (saveDataSource, prefsSafeData) {
+ var promiseTogo = fluid.promise();
+ var data;
+
+ if (!prefsSafeData) {
+ var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe});
+ promiseTogo.reject(error);
+ } else {
+ data = {
+ schemaVersion: gpii.dbOperation.schemaVersion,
+ prefsSafeType: prefsSafeData.prefsSafeType,
+ name: prefsSafeData.name,
+ password: prefsSafeData.password,
+ email: prefsSafeData.email,
+ preferences: prefsSafeData.preferences,
+ timestampCreated: gpii.dbOperation.getCurrentTimestamp(),
+ timestampUpdated: null
+ };
+
+ promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, "id", data);
+ }
+ return promiseTogo;
+};
+
+/**
+ * Update a prefs safe
+ * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore.
+ * @param {String} prefsSafeId - The prefs safe id.
+ * @param {Object} prefsSafeData - The prefs safe data. An example of the data:
+ * {
+ * type: {String},
+ * schemaVersion: {String},
+ * prefsSafeType: {String},
+ * name: {String},
+ * password: {String},
+ * email: {String},
+ * preferences: {Object},
+ * timestampCreated: {Date}
+ * }
+ * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the
+ * prefs safe record, or an error if `prefsSafeId` or `prefsSafeData` is not provided or `prefsSafeData.type` is not
+ * the doc type for prefsSafe.
+ */
+gpii.dbOperation.dbDataStore.updatePrefsSafe = function (saveDataSource, prefsSafeId, prefsSafeData) {
+ var promiseTogo = fluid.promise();
+ var data, error;
+
+ if (!prefsSafeId) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "prefsSafeId"});
+ promiseTogo.reject(error);
+ } else if (!prefsSafeData) {
+ error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe});
+ promiseTogo.reject(error);
+ } else {
+ data = {
+ type: prefsSafeData.type,
+ schemaVersion: prefsSafeData.schemaVersion,
+ prefsSafeType: prefsSafeData.prefsSafeType,
+ name: prefsSafeData.name || null,
+ password: prefsSafeData.password || null,
+ email: prefsSafeData.email || null,
+ preferences: prefsSafeData.preferences,
+ timestampCreated: prefsSafeData.timestampCreated,
+ timestampUpdated: gpii.dbOperation.getCurrentTimestamp()
+ };
+
+ promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, prefsSafeId, data);
+ }
+ return promiseTogo;
+};
+
+// General Client Functions
+// ------------------------
+
+/**
+* Transform the data in CouchDB form to a more understandable structure
+ * @param {Object} data - Contains both client and client credential information associated with an oauth2 client id.
+ * An input example:
+ * {
+ * key: {String}, // access token
+ * id: {String}, // authorization id
+ * value: {
+ * _id: {String}, // client id
+ * clientCredential: {
+ * type: {String},
+ * schemaVersion: {String},
+ * clientId: {String},
+ * oauth2ClientId: {String},
+ * oauth2ClientSecret: {String},
+ * revoked: {Boolean},
+ * revokedReason: {String},
+ * timestampCreated: {Date},
+ * timestampRevoked: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * },
+ * doc: {
+ * type: {String}, // client type
+ * schemaVersion: {String},
+ * name: {String},
+ * computerType: {String},
+ * timestampCreated: {Date},
+ * timestampUpdated: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * }
+ * @return {Object} An object in the structure:
+ * {
+ * oauth2ClientId: {String},
+ * client: {Object},
+ * clientCredential: {Object}
+ * }
+ */
+gpii.dbOperation.dbDataStore.findClientByOauth2ClientIdPostProcess = function (data) {
+ var result;
+
+ if (data && data.doc && data.value) {
+ result = {
+ oauth2ClientId: data.key,
+ client: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc),
+ clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.clientCredential)
+ };
+ }
+
+ return result;
+};
+
+// General Authorization Functions
+// ------------------------------------
+
+/**
+* Transform the data in CouchDB form to a more understandable structure
+ * @param {Object} data - Contains both client and authorization information associated with an access token.
+ * An input example:
+ * {
+ * key: {String}, // access token
+ * id: {String}, // authorization id
+ * value: {
+ * _id: {String}, // client id
+ * authorization: {
+ * type: {String},
+ * schemaVersion: {String},
+ * clientId: {String},
+ * gpiiKey: {String},
+ * accessToken: {String},
+ * revoked: {Boolean},
+ * revokedReason: {String},
+ * timestampCreated: {Date},
+ * timestampRevoked: {Date},
+ * timestampExpires: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * },
+ * doc: {
+ * type: {String}, // client type
+ * name: {String},
+ * computerType: {String},
+ * timestampCreated: {Date},
+ * timestampUpdated: {Date},
+ * _id: {String},
+ * _rev: {String}
+ * }
+ * }
+ * @return {Object} An object in the structure:
+ * {
+ * accessToken: {String},
+ * authorization: {Object}
+ * }
+ */
+gpii.dbOperation.dbDataStore.findInfoByAccessTokenPostProcess = function (data) {
+ var result;
+
+ if (data && data.doc && data.value) {
+ result = {
+ accessToken: data.key,
+ clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc),
+ authorization: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.authorization)
+ };
+ }
+
+ return result;
+};
+
+/**
+ * Add an authorization
+ * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore.
+ * @param {Object} authorizationData - The authorization data. An example of gpiiAppInstallationAuthorization data:
+ *
+ * gpiiAppInstallationAuthorization:
+ * {
+ * clientId: {String},
+ * gpiiKey: {String},
+ * clientCredentialId: {String},
+ * accessToken: {String},
+ * timestampExpires: {String}
+ * }
+ *
+ * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the
+ * authorization record, or an error if `authorizationData` parameter is not provided.
+ */
+gpii.dbOperation.dbDataStore.addAuthorization = function (saveDataSource, authorizationData) {
+ var promiseTogo = fluid.promise();
+ var data;
+
+ if (!authorizationData) {
+ var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization});
+ promiseTogo.reject(error);
+ } else {
+ data = {
+ schemaVersion: gpii.dbOperation.schemaVersion,
+ clientId: authorizationData.clientId,
+ gpiiKey: authorizationData.gpiiKey,
+ clientCredentialId: authorizationData.clientCredentialId,
+ accessToken: authorizationData.accessToken,
+ revoked: false,
+ revokedReason: null,
+ timestampCreated: gpii.dbOperation.getCurrentTimestamp(),
+ timestampRevoked: null,
+ timestampExpires: authorizationData.timestampExpires
+ };
+
+ promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization, "id", data);
+ }
+ return promiseTogo;
+};
diff --git a/gpii/node_modules/gpii-db-operation/src/DbUtils.js b/gpii/node_modules/gpii-db-operation/src/DbUtils.js
new file mode 100644
index 000000000..4113d1bc7
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/src/DbUtils.js
@@ -0,0 +1,38 @@
+/*!
+GPII Database Utilities
+
+Copyright 2017 OCAD university
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = fluid || require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+fluid.registerNamespace("gpii.dbOperation");
+
+/** Use `fluid.stringTemplate` method to replace terms within error.message with actual values.
+ * @param {Object} error - An object that contains an element keyed by "message".
+ * @param {Object} termMap - An object that contains the mapping between terms used within error.message and their actual values.
+ * @return {Object} The input object "error" with terms in error.message being replaced by actual values.
+*/
+gpii.dbOperation.composeError = function (error, termMap) {
+ var err = fluid.copy(error);
+ err.message = fluid.stringTemplate(err.message, termMap);
+ return err;
+};
+
+/**
+ * Returns the current time in a human readable string that also naturally sort in chronological order.
+ * See http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.5.43
+ * @return {String} The current time in ISO string format.
+ */
+gpii.dbOperation.getCurrentTimestamp = function () {
+ return new Date().toISOString();
+};
diff --git a/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js
new file mode 100644
index 000000000..466f6a9f7
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js
@@ -0,0 +1,526 @@
+/**
+ GPII DB Data Store Tests
+
+ Copyright 2016-2019 OCAD University
+
+ Licensed under the New BSD license. You may not use this file except in
+ compliance with this License.
+
+ You may obtain a copy of the License at
+ https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+require("gpii-couchdb-test-harness");
+gpii.test.couchdb.loadTestingSupport();
+
+require("gpii-db-operation");
+require("./DbDataStoreTestsUtils.js");
+
+// The test data is from %gpii-db-operation/test/data/*.json
+fluid.defaults("gpii.tests.dbDataStore.findGpiiKey", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findGpiiKey()",
+ tests: [
+ {
+ name: "Find a GPII key record by a GPII key",
+ sequence: [{
+ task: "{dbDataStore}.findGpiiKey",
+ args: ["chrome_high_contrast"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected GPII key data is received", gpii.tests.dbDataStore.testData.gpiiKeyChromehcDefault, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Finding a GPII key record by a non-existing GPII key returns undefined",
+ sequence: [
+ {
+ task: "{dbDataStore}.findGpiiKey",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertUndefined",
+ resolveArgs: ["Finding an user by a non-existing GPII key returns undefined", "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing a GPII key returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findGpiiKey",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"id\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findPrefsSafeByGpiiKey", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findPrefsSafeByGpiiKey()",
+ tests: [
+ {
+ name: "Find preferences by a proper GPII key that associates with a prefs safe",
+ sequence: [{
+ task: "{dbDataStore}.findPrefsSafeByGpiiKey",
+ args: ["chrome_high_contrast"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findPrefsSafeByGpiiKey_withPrefsSafe, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Find preferences by a proper GPII key that does not associate with any prefs safe",
+ sequence: [{
+ task: "{dbDataStore}.findPrefsSafeByGpiiKey",
+ args: ["chrome_high_contrast_dark"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findPrefsSafeByGpiiKey_noPrefsSafe, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Finding preferences by a non-existing GPII key returns undefined",
+ sequence: [{
+ task: "{dbDataStore}.findPrefsSafeByGpiiKey",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertUndefined",
+ resolvArgs: ["Finding preferences by a non-existing GPII key returns undefined", "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing an GPII key returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findPrefsSafeByGpiiKey",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"gpiiKey\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findClientById", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findClientById()",
+ tests: [
+ {
+ name: "Find a client record by a proper client id",
+ sequence: [{
+ task: "{dbDataStore}.findClientById",
+ args: ["gpiiAppInstallationClient-1"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.client1, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Finding a client record by a non-existing client id returns undefined",
+ sequence: [{
+ task: "{dbDataStore}.findClientById",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertUndefined",
+ resolveArgs: ["Finding a client by a non-existing client id returns undefined", "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing client id returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findClientById",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"id\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findClientCredentialById", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findClientCredentialById()",
+ tests: [
+ {
+ name: "Find a client credential record by a proper client credential id",
+ sequence: [{
+ task: "{dbDataStore}.findClientCredentialById",
+ args: ["clientCredential-1"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.clientCredential1, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Finding a client record by a non-existing client credential id returns undefined",
+ sequence: [{
+ task: "{dbDataStore}.findClientCredentialById",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertUndefined",
+ resolveArgs: ["Finding a client by a non-existing client credential id returns undefined", "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing client credential id returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findClientCredentialById",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"id\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findClientByOauth2ClientId", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findClientByOauth2ClientId()",
+ tests: [
+ {
+ name: "Find a client record by a proper oauth2 client id",
+ sequence: [{
+ task: "{dbDataStore}.findClientByOauth2ClientId",
+ args: ["net.gpii.ajc.bakersfield"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findClientByOauth2ClientId, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Finding a client record by a non-existing oauth2 client id returns undefined",
+ sequence: [{
+ task: "{dbDataStore}.findClientByOauth2ClientId",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertUndefined",
+ resolveArgs: ["Finding a client by a non-existing oauth2 client id returns undefined", "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing an oauth2 client id returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findClientByOauth2ClientId",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"oauth2ClientId\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findInfoByAccessToken", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findInfoByAccessToken()",
+ tests: [
+ {
+ name: "Find an authorization information by a GPII app installation access token",
+ sequence: [{
+ task: "{dbDataStore}.findInfoByAccessToken",
+ args: ["gpii-app-installation-accessToken-1"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected data is received", gpii.tests.dbDataStore.testData.findGpiiAppInstallationAuthorizationByAccessToken, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Returns undefined when the authorization is revoked",
+ sequence: [{
+ task: "{dbDataStore}.findInfoByAccessToken",
+ args: ["gpii-app-installation-accessToken-3"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected undefined is received", undefined, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Not providing an input argument returns 400 status code and error message",
+ sequence: [{
+ task: "{dbDataStore}.findInfoByAccessToken",
+ args: [],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"accessToken\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Find by a non-existing access token returns undefined",
+ sequence: [{
+ task: "{dbDataStore}.findInfoByAccessToken",
+ args: ["non-existing"],
+ resolve: "jqUnit.assertDeepEq",
+ resolveArgs: ["The expected error is received", undefined, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.findAllViews", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test findAllViews()",
+ tests: [{
+ name: "Find all views",
+ sequence: [{
+ task: "{dbDataStore}.findAllViews",
+ args: [],
+ resolve: "jqUnit.assertValue",
+ resolveArgs: ["The views received", "{arguments}.0.views"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.addGpiiKey", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test addGpiiKey()",
+ tests: [
+ {
+ name: "Add a GPII key - auto generates the GPII key if the key value is not provided at the input",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "addGpiiKey", [gpii.tests.dbDataStore.testData.gpiiKeyToCreate_noGpiiKey], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToCreate_noGpiiKey, "{that}"]
+ }]
+ },
+ {
+ name: "Add a GPII key - use the provided GPII key if it is provided at the input",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "addGpiiKey", [gpii.tests.dbDataStore.testData.gpiiKeyToCreate_withGpiiKey], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToCreate_withGpiiKey, "{that}"]
+ }]
+ },
+ {
+ name: "Not providing gpiiKeyData still creates a new GPII key but it does not associate with a prefs safe.",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "addGpiiKey", [undefined], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey",
+ resolveArgs: ["{arguments}.0", {
+ prefsSafeId: null,
+ prefsSetId: null
+ }, "{that}"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.updateGpiiKey", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test updateGpiiKey()",
+ tests: [
+ {
+ name: "Update a GPII key",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "updateGpiiKey", ["chrome_high_contrast", gpii.tests.dbDataStore.testData.gpiiKeyToUpdate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyUpdatedRecord",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToUpdate, "{that}"]
+ }]
+ },
+ {
+ name: "Not providing gpiiKey returns error",
+ sequence: [{
+ task: "{dbDataStore}.updateGpiiKey",
+ args: [undefined, gpii.tests.dbDataStore.testData.gpiiKeyToUpdate],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"gpiiKey\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Updating an empty object returns error",
+ sequence: [{
+ task: "{dbDataStore}.updateGpiiKey",
+ args: ["chrome_high_contrast", undefined],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "A record of type \"gpiiKey\" was not found",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Updating a document type other than gpiiKey returns an error",
+ sequence: [{
+ task: "{dbDataStore}.updateGpiiKey",
+ args: ["prefsSafe-1", {type: "non-gpiiKey"}],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The document type must be \"gpiiKey\" instead of the selected document type \"non-gpiiKey\"",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.addPrefsSafe", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test addPrefsSafe()",
+ tests: [
+ {
+ name: "Add a prefs safe",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "addPrefsSafe", [gpii.tests.dbDataStore.testData.prefsSafeToCreate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyCreatedPrefsSafe",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.prefsSafeToCreate, "{that}"]
+ }]
+ },
+ {
+ name: "Adding an empty object returns error",
+ sequence: [{
+ task: "{dbDataStore}.addPrefsSafe",
+ args: [undefined],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "A record of type \"prefsSafe\" was not found",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.updatePrefsSafe", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test updatePrefsSafe()",
+ tests: [
+ {
+ name: "Update a prefs safe",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "updatePrefsSafe", ["prefsSafe-1", gpii.tests.dbDataStore.testData.prefsSafeToUpdate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyUpdatedRecord",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.prefsSafeToUpdate, "{that}"]
+ }]
+ },
+ {
+ name: "Not providing prefsSafeId returns error",
+ sequence: [{
+ task: "{dbDataStore}.updatePrefsSafe",
+ args: [undefined, gpii.tests.dbDataStore.testData.prefsSafeToUpdate],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The input field \"prefsSafeId\" was undefined",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Updating an empty object returns error",
+ sequence: [{
+ task: "{dbDataStore}.updatePrefsSafe",
+ args: ["prefsSafe-1", undefined],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "A record of type \"prefsSafe\" was not found",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ },
+ {
+ name: "Updating a document type other that prefsSafe returns an error",
+ sequence: [{
+ task: "{dbDataStore}.updatePrefsSafe",
+ args: ["prefsSafe-1", {type: "non-prefsSafe"}],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "The document type must be \"prefsSafe\" instead of the selected document type \"non-prefsSafe\"",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }]
+ }]
+});
+
+fluid.defaults("gpii.tests.dbDataStore.addAuthorization", {
+ gradeNames: ["gpii.tests.dbDataStore.environment"],
+ rawModules: [{
+ name: "Test addAuthorization()",
+ tests: [
+ {
+ name: "Add a GPII app installation authorization",
+ sequence: [{
+ task: "gpii.tests.dbDataStore.executeFnAndVerifyResults",
+ args: ["{dbDataStore}", "addAuthorization", [gpii.tests.dbDataStore.testData.gpiiAppInstallationAuthorizationToCreate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder
+ resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiAppInstallationAuthorization",
+ resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiAppInstallationAuthorizationToCreate, "{that}"]
+ }]
+ },
+ {
+ name: "Adding an empty object returns error",
+ sequence: [{
+ task: "{dbDataStore}.addAuthorization",
+ args: [undefined],
+ reject: "jqUnit.assertDeepEq",
+ rejectArgs: ["The expected error is received", {
+ message: "A record of type \"gpiiAppInstallationAuthorization\" was not found",
+ statusCode: 400,
+ isError: true
+ }, "{arguments}.0"]
+ }]
+ }
+ ]
+ }]
+});
+
+fluid.test.runTests([
+ "gpii.tests.dbDataStore.findGpiiKey",
+ "gpii.tests.dbDataStore.findClientById",
+ "gpii.tests.dbDataStore.findClientCredentialById",
+ "gpii.tests.dbDataStore.findPrefsSafeByGpiiKey",
+ "gpii.tests.dbDataStore.findClientByOauth2ClientId",
+ "gpii.tests.dbDataStore.findInfoByAccessToken",
+ "gpii.tests.dbDataStore.findAllViews",
+ "gpii.tests.dbDataStore.addGpiiKey",
+ "gpii.tests.dbDataStore.updateGpiiKey",
+ "gpii.tests.dbDataStore.addPrefsSafe",
+ "gpii.tests.dbDataStore.updatePrefsSafe",
+ "gpii.tests.dbDataStore.addAuthorization"
+]);
diff --git a/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js
new file mode 100644
index 000000000..318770bf3
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js
@@ -0,0 +1,326 @@
+/**
+GPII DB Data Store Tests
+
+Copyright 2016-2019 OCAD University
+
+Licensed under the New BSD license. You may not use this file except in
+compliance with this License.
+
+You may obtain a copy of the License at
+https://github.com/GPII/universal/blob/master/LICENSE.txt
+*/
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii"),
+ jqUnit = fluid.require("node-jqunit", require, "jqUnit");
+
+require("gpii-couchdb-test-harness");
+gpii.test.couchdb.loadTestingSupport();
+
+fluid.defaults("gpii.tests.dbDataStore.environment", {
+ gradeNames: ["gpii.test.couchdb.environment.base"],
+ databases: {
+ gpii: {
+ data: [
+ "%gpii-db-operation/test/data/clientCredentials.json",
+ "%gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json",
+ "%gpii-db-operation/test/data/gpiiAppInstallationClients.json",
+ "%gpii-db-operation/test/data/gpiiKeys.json",
+ "%gpii-db-operation/test/data/prefsSafes.json",
+ "%gpii-universal/testData/dbData/views.json"
+ ]
+ }
+ },
+ components: {
+ testCaseHolder: {
+ type: "gpii.tests.dbDataStore.baseTestCaseHolder"
+ }
+ },
+ distributeOptions: {
+ source: "{that}.options.rawModules",
+ target: "{that > testCaseHolder}.options.rawModules"
+ },
+ mergePolicy: {
+ rawModules: "noexpand"
+ }
+});
+
+fluid.defaults("gpii.tests.dbDataStore.baseTestCaseHolder", {
+ gradeNames: ["gpii.test.couchdb.caseHolder"],
+ events: {
+ onResponse: null,
+ onError: null
+ },
+ components: {
+ dbDataStore: {
+ type: "gpii.dbOperation.dbDataStore",
+ options: {
+ dataSourceConfig: {
+ baseUrl: "http://localhost",
+ port: "{gpii.tests.dbDataStore.environment}.options.couch.port",
+ dbName: "gpii"
+ }
+ }
+ }
+ }
+});
+
+gpii.tests.dbDataStore.executeFnAndVerifyResults = function (dbDataStore, fnName, fnArgs, caseHolder) {
+ var outerPromise = fluid.promise();
+
+ dbDataStore[fnName].apply(null, fnArgs).then(
+ function (results) {
+ caseHolder.lastSavedId = results.id;
+ dbDataStore.findById(results.id).then(outerPromise.resolve, outerPromise.reject);
+ },
+ outerPromise.reject
+ );
+ return outerPromise;
+};
+
+gpii.tests.dbDataStore.verifyFetched = function (response, expected, that) {
+ jqUnit.assertEquals("The fetched document id matches the saved record", that.lastSavedId, response.id);
+ jqUnit.assertLeftHand("The data is saved successfully", expected, response);
+};
+
+gpii.tests.dbDataStore.verifyFetchedGpiiKey = function (response, expected, that) {
+ // verify the GPII key
+ var gpiiKey = expected.gpiiKey;
+ if (gpiiKey) {
+ jqUnit.assertEquals("The \"id\" value has been set to the provided key", gpiiKey, response.id);
+ } else {
+ jqUnit.assertNotUndefined("The \"id\" value has been set to an auto generated key", response.id);
+ }
+
+ // verify other fields
+ var nonIdFields = fluid.censorKeys(expected, ["gpiiKey"]);
+ gpii.tests.dbDataStore.verifyFetched(response, nonIdFields, that);
+ jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.gpiiKey, response.type);
+ jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion);
+ jqUnit.assertFalse("The \"revoked\" value has been set to false", response.revoked);
+ jqUnit.assertNull("The \"revokedReason\" value has been set to null", response.revokedReason);
+ jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated);
+ jqUnit.assertNull("The \"timestampUpdated\" value has been set to null", response.timestampUpdated);
+ jqUnit.assertNull("The \"timestampRevoked\" value has been set to null", response.timestampRevoked);
+};
+
+gpii.tests.dbDataStore.verifyCreatedPrefsSafe = function (response, expected, that) {
+ gpii.tests.dbDataStore.verifyFetched(response, expected, that);
+ jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.prefsSafe, response.type);
+ jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion);
+ jqUnit.assertEquals("The \"prefsSafeType\" value has been set to default", expected.prefsSafeType, response.prefsSafeType);
+ jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated);
+ jqUnit.assertNull("The \"timestampUpdated\" value has been set to null", response.timestampUpdated);
+};
+
+gpii.tests.dbDataStore.verifyUpdatedRecord = function (response, expected, that) {
+ gpii.tests.dbDataStore.verifyFetched(response, expected, that);
+ jqUnit.assertNotUndefined("The \"timestampUpdated\" value has been created", response.timestampUpdated);
+};
+
+gpii.tests.dbDataStore.verifyFetchedGpiiAppInstallationAuthorization = function (response, expected, that) {
+ gpii.tests.dbDataStore.verifyFetched(response, expected, that);
+ jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization, response.type);
+ jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion);
+ jqUnit.assertFalse("The \"revoked\" value has been set to false", response.revoked);
+ jqUnit.assertNull("The \"revokedReason\" value has been set to null", response.revokedReason);
+ jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated);
+ jqUnit.assertNull("The \"timestampRevoked\" value has been set to null", response.timestampRevoked);
+};
+
+gpii.tests.dbDataStore.testData = {
+ gpiiKeyChromehcDefault: {
+ "id": "chrome_high_contrast",
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": "prefsSafe-1",
+ "prefsSetId": "gpii-default",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ client1: {
+ "id": "gpiiAppInstallationClient-1",
+ "type": "gpiiAppInstallationClient",
+ "schemaVersion": "0.2",
+ "name": "AJC-Bakersfield",
+ "computerType": "public",
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ clientCredential1: {
+ "id": "clientCredential-1",
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "oauth2ClientSecret": "client_secret_ajc_bakersfield",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null
+ },
+ findPrefsSafeByGpiiKey_withPrefsSafe: {
+ "gpiiKey": "chrome_high_contrast",
+ "gpiiKeyDetails": {
+ "id": "chrome_high_contrast",
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": "prefsSafe-1",
+ "prefsSetId": "gpii-default",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ "prefsSafe": {
+ "id": "prefsSafe-1",
+ "type": "prefsSafe",
+ "schemaVersion": "0.2",
+ "prefsSafeType": "user",
+ "name": null,
+ "password": null,
+ "email": null,
+ "preferences": {
+ "flat": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/matchMakerType": "ruleBased",
+ "http://registry.gpii.net/common/fontSize": 24,
+ "http://registry.gpii.net/common/foregroundColor": "white",
+ "http://registry.gpii.net/common/backgroundColor": "black",
+ "http://registry.gpii.net/common/fontFaceFontName": [
+ "Comic Sans"
+ ],
+ "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif",
+ "http://registry.gpii.net/common/magnification": 2
+ }
+ }
+ }
+ }
+ },
+ "timestampCreated": "2017-12-01T18:43:32.889Z",
+ "timestampUpdated": null
+ }
+ },
+ findPrefsSafeByGpiiKey_noPrefsSafe: {
+ "gpiiKey": "chrome_high_contrast_dark",
+ "gpiiKeyDetails": {
+ "id": "chrome_high_contrast_dark",
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": null,
+ "prefsSetId": null,
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ "prefsSafe": null
+ },
+ findClientByOauth2ClientId: {
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "client": {
+ "type": "gpiiAppInstallationClient",
+ "schemaVersion": "0.2",
+ "name": "AJC-Bakersfield",
+ "computerType": "public",
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null,
+ "id": "gpiiAppInstallationClient-1"
+ },
+ "clientCredential": {
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "oauth2ClientSecret": "client_secret_ajc_bakersfield",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null,
+ "id": "clientCredential-1"
+ }
+ },
+ gpiiKeyToCreate_noGpiiKey: {
+ "prefsSafeId": "a-new-prefs-safe-id",
+ "prefsSetId": "gpii-default"
+ },
+ gpiiKeyToCreate_withGpiiKey: {
+ "gpiiKey": "a-new-gpii-key",
+ "prefsSafeId": "a-new-prefs-safe-id",
+ "prefsSetId": "gpii-default"
+ },
+ gpiiKeyToUpdate: {
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": "prefsSafe-1",
+ "prefsSetId": "updated_name",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-05-29T17:54:00.000Z",
+ "timestampRevoked": "2017-06-29T17:54:00.000Z"
+ },
+ prefsSafeToCreate: {
+ "prefsSafeType": "user",
+ "name": null,
+ "password": null,
+ "email": null,
+ "preferences": {
+ "test": "test"
+ }
+ },
+ prefsSafeToUpdate: {
+ "type": "prefsSafe",
+ "schemaVersion": "0.2",
+ "prefsSafeType": "snapset",
+ "name": "updated_name",
+ "password": "updated_password",
+ "email": "updated_email",
+ "preferences": {
+ "test": "test"
+ },
+ "timestampCreated": "2017-05-29T17:54:00.000Z"
+ },
+ gpiiAppInstallationAuthorizationToCreate: {
+ "clientId": "client-1",
+ "gpiiKey": "chrome_high_contrast",
+ "clientCredentialId": "clientCredential-1",
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "timestampExpires": "3020-05-29T17:54:00.000Z"
+ },
+ findGpiiAppInstallationAuthorizationByAccessToken: {
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "clientCredential": {
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "oauth2ClientSecret": "client_secret_ajc_bakersfield",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null,
+ "id": "clientCredential-1"
+ },
+ "authorization": {
+ "type": "gpiiAppInstallationAuthorization",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "gpiiKey": "chrome_high_contrast",
+ "clientCredentialId": "clientCredential-1",
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-05-29T17:54:00.000Z",
+ "timestampRevoked": null,
+ "timestampExpires": "3020-05-30T17:54:00.000Z",
+ "id": "gpiiAppInstallationAuthorization-1"
+ }
+ }
+};
diff --git a/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json b/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json
new file mode 100644
index 000000000..312503480
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json
@@ -0,0 +1,26 @@
+[
+ {
+ "_id": "clientCredential-1",
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "oauth2ClientId": "net.gpii.ajc.bakersfield",
+ "oauth2ClientSecret": "client_secret_ajc_bakersfield",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null
+ },
+ {
+ "_id": "clientCredential-2",
+ "type": "clientCredential",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-2",
+ "oauth2ClientId": "net.gpii.ajc.richmond",
+ "oauth2ClientSecret": "client_secret_ajc_richmond",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampRevoked": null
+ }
+]
diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json
new file mode 100644
index 000000000..2a2453459
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json
@@ -0,0 +1,44 @@
+[
+ {
+ "_id": "gpiiAppInstallationAuthorization-1",
+ "type": "gpiiAppInstallationAuthorization",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-1",
+ "gpiiKey": "chrome_high_contrast",
+ "clientCredentialId": "clientCredential-1",
+ "accessToken": "gpii-app-installation-accessToken-1",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-05-29T17:54:00.000Z",
+ "timestampRevoked": null,
+ "timestampExpires": "3020-05-30T17:54:00.000Z"
+ },
+ {
+ "_id": "gpiiAppInstallationAuthorization-2",
+ "type": "gpiiAppInstallationAuthorization",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-2",
+ "gpiiKey": "chrome_high_contrast",
+ "clientCredentialId": "clientCredential-2",
+ "accessToken": "gpii-app-installation-accessToken-2",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "3020-05-29T17:54:00.000Z",
+ "timestampRevoked": null,
+ "timestampExpires": "3020-05-30T17:54:00.000Z"
+ },
+ {
+ "_id": "gpiiAppInstallationAuthorization-3",
+ "type": "gpiiAppInstallationAuthorization",
+ "schemaVersion": "0.2",
+ "clientId": "gpiiAppInstallationClient-2",
+ "gpiiKey": "chrome_high_contrast",
+ "clientCredentialId": "clientCredential-2",
+ "accessToken": "gpii-app-installation-accessToken-3",
+ "revoked": true,
+ "revokedReason": null,
+ "timestampCreated": "2017-05-29T12:54:00.000Z",
+ "timestampRevoked": null,
+ "timestampExpires": "2017-05-30T13:54:00.000Z"
+ }
+]
diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json
new file mode 100644
index 000000000..adf2efc56
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json
@@ -0,0 +1,20 @@
+[
+ {
+ "_id": "gpiiAppInstallationClient-1",
+ "type": "gpiiAppInstallationClient",
+ "schemaVersion": "0.2",
+ "name": "AJC-Bakersfield",
+ "computerType": "public",
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ {
+ "_id": "gpiiAppInstallationClient-2",
+ "type": "gpiiAppInstallationClient",
+ "schemaVersion": "0.2",
+ "name": "AJC-Richmond",
+ "computerType": "public",
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ }
+]
diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json
new file mode 100644
index 000000000..3b318f739
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json
@@ -0,0 +1,24 @@
+[
+ {
+ "_id": "chrome_high_contrast",
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": "prefsSafe-1",
+ "prefsSetId": "gpii-default",
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ },
+ {
+ "_id": "chrome_high_contrast_dark",
+ "type": "gpiiKey",
+ "schemaVersion": "0.2",
+ "prefsSafeId": null,
+ "prefsSetId": null,
+ "revoked": false,
+ "revokedReason": null,
+ "timestampCreated": "2017-11-21T18:11:22.101Z",
+ "timestampUpdated": null
+ }
+]
diff --git a/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json b/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json
new file mode 100644
index 000000000..9fd2dfc47
--- /dev/null
+++ b/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json
@@ -0,0 +1,33 @@
+[
+ {
+ "_id": "prefsSafe-1",
+ "type": "prefsSafe",
+ "schemaVersion": "0.2",
+ "prefsSafeType": "user",
+ "name": null,
+ "password": null,
+ "email": null,
+ "preferences": {
+ "flat": {
+ "contexts": {
+ "gpii-default": {
+ "name": "Default preferences",
+ "preferences": {
+ "http://registry.gpii.net/common/matchMakerType": "ruleBased",
+ "http://registry.gpii.net/common/fontSize": 24,
+ "http://registry.gpii.net/common/foregroundColor": "white",
+ "http://registry.gpii.net/common/backgroundColor": "black",
+ "http://registry.gpii.net/common/fontFaceFontName": [
+ "Comic Sans"
+ ],
+ "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif",
+ "http://registry.gpii.net/common/magnification": 2
+ }
+ }
+ }
+ }
+ },
+ "timestampCreated": "2017-12-01T18:43:32.889Z",
+ "timestampUpdated": null
+ }
+]
diff --git a/gpii/node_modules/gpii-ini-file/README.md b/gpii/node_modules/gpii-ini-file/README.md
new file mode 100644
index 000000000..b052f5a59
--- /dev/null
+++ b/gpii/node_modules/gpii-ini-file/README.md
@@ -0,0 +1,175 @@
+# The INI file reader/writer.
+
+Reads and writes INI files.
+
+The main goal of this library is to only make small specific changes to an existing INI file, while preserving the
+formatting of the rest of the file.
+
+In order to implement this, the same thing could not be done with the original ini library,
+[GPII/ini](https://github.com/GPII/ini), where the required changes were too significant for its original charm to
+remain. So, while looking roughly the same from the outside, this is a re-write rather than a fork.
+
+## INI file format
+
+INI file can look something like this:
+
+```ini
+# comments
+; other comments
+
+# = or : delimiters.
+key=value1
+key2:value2
+key3 = value3
+
+# Sections (and sub-sections)
+[section]
+key="section-value"
+
+[[subsection]]
+key=subsection value
+
+[section2]
+# Multiple lines
+multi-line="""line1
+line2
+line3"""
+multi-line2=line1
+ line2
+ line3
+```
+
+Which will parse to the following:
+
+```json5
+{
+ key: "value1",
+ key2: "value2",
+ key3: "value3",
+ section: {
+ key: "section-value",
+ subsection: {key: "subsection value"}
+ },
+ section2: {
+ "multi-line": "line1\nline2\nline3",
+ "multi-line2": "line1\nline2\nline3"
+ }
+}
+```
+
+See [test/read-test.ini](test/read-test.ini) for an extreme example.
+
+When writing to an existing file, the content is modified rather than re-writing. Effort is made to respect the
+unchanged values and current format of the file. An updated version of the above example will look like this:
+
+```ini
+# comments
+; other comments
+
+# = or : delimiters.
+key=new value
+key2:another new value
+key3 = and again
+
+# Sections
+[section]
+key="quotes respected"
+# sub-sections
+[[subsection]]
+key=subsection value
+
+new1=new value
+[section2]
+# Multiple lines
+multi-line="""modified line1
+modified line2
+modified line3"""
+multi-line2=modified line1
+ modified line2
+ modified line3
+
+[newSection]
+newValue=value
+```
+
+Compare [test/write-test.ini](test/write-test.ini) with [test/write-test.expect.ini](test/write-test.expect.ini) for an
+example.
+
+## Usage
+
+### Reading
+
+```snippet
+/**
+ * Reads INI file content, returning the parsed data as an object.
+ *
+ * @param {String} content The ini file content.
+ * @param {Object} options Options:
+ * @param {Boolean} options.strings true to always return strings, otherwise try to return numbers and booleans for
+ * unquoted values, where appropriate.
+ * @param {Boolean} options.firstDuplicate true to use the first occurrence of a value with a duplicate name, otherwise
+ * the last is used. [default: false]
+ *
+ * @return {Object} The parsed ini file data.
+ */
+gpii.iniFile.read = function (content, options) {
+ // ...
+};
+```
+
+### Writing
+
+```snippet
+/**
+ * Writes an object to existing INI file content.
+ *
+ * @param {String} input The ini file content to update (can be empty).
+ * @param {Object} data The new settings data.
+ * @param {iniFile.WriteOptions} options INI file output options.
+ * @return {String} The new ini file content.
+ */
+gpii.iniFile.write = function (input, data, options) {
+ // ...
+};
+```
+
+```javascript
+/**
+ * Options for writing ini files with gpii.iniFile.write().
+ *
+ * @typedef {Object} iniFile.WriteOptions
+ *
+ * @property {String} keyValueDelimiter - Text which separates keys and values, for new values. [default: "="].
+ * @property {Boolean} keepUndefined - true to keep items and sections that are not in data. Use to define values
+ * without reading the file beforehand.
+ * @property {String} multilineStyle - How new multi-line values are written:
+ * "'''" or '"""': Surround the value with 3x single or double quotes (default).
+ * "indent": Indent the additional lines.
+ * "escape": Use an escaped n (\n).
+ * anything else: Wrap the value with the given value.
+ * @property {String} quote - For new values, quote them "always", "never", "strings" (for only strings), or
+ * "spaces" (if the value starts or ends with a space) [default: "spaces"].
+ * @property {String} quoteChar - The quote character for new values when quoting. [default: " (double quote)].
+ * @property {String} eol - The file's end of line character(s). [default: auto-detect]
+ */
+```
+
+## How it works
+
+Parsing is performed by a single regular expression ([ini.regex](ini.regex)), which calls a function when it matches
+either a section header (`[example]`), or a `key=value` pair.
+
+When reading data, it fills an object with the matched values.
+
+When writing, it will replace the matches with text from an existing object. Only the text in the value is replaced, and
+only if it is different to the stored value.
+
+## Limitations
+
+Sub-sections can't have the same name as a value in the same section, eg:
+
+```ini
+[section1]
+same-name=value
+[[same-name]]
+```
diff --git a/gpii/node_modules/gpii-ini-file/index.js b/gpii/node_modules/gpii-ini-file/index.js
new file mode 100644
index 000000000..79f42a2a7
--- /dev/null
+++ b/gpii/node_modules/gpii-ini-file/index.js
@@ -0,0 +1,572 @@
+/*
+ * INI File reader/writer
+ *
+ * Copyright 2018 Raising the Floor - International
+ *
+ * Licensed under the New BSD license. You may not use this file except in
+ * compliance with this License.
+ *
+ * The R&D leading to these results received funding from the
+ * Department of Education - Grant H421A150005 (GPII-APCP). However,
+ * these results do not necessarily represent the policy of the
+ * Department of Education, and you should not assume endorsement by the
+ * Federal Government.
+ *
+ * You may obtain a copy of the License at
+ * https://github.com/GPII/universal/blob/master/LICENSE.txt
+ */
+
+"use strict";
+
+var fluid = require("infusion"),
+ gpii = fluid.registerNamespace("gpii");
+
+var fs = require("fs"),
+ os = require("os"),
+ XRegExp = require("xregexp");
+
+gpii.iniFile = fluid.registerNamespace("gpii.iniFile");
+
+var pattern = fs.readFileSync(__dirname + "/ini.regex");
+gpii.iniFile.regex = new XRegExp(pattern, "gmsx");
+
+/**
+ * Options for writing ini files with gpii.iniFile.write().
+ *
+ * @typedef {Object} iniFile.WriteOptions
+ *
+ * @property {String} keyValueDelimiter - Text which separates keys and values, for new values. [default: "="].
+ * @property {Boolean} keepUndefined - true to keep items and sections that are not in data. Use to define values
+ * without reading the file beforehand.
+ * @property {String} multilineStyle - How new multi-line values are written:
+ * "'''" or '"""': Surround the value with 3x single or double quotes (default).
+ * "indent": Indent the additional lines.
+ * "escape": Use an escaped n (\n).
+ * anything else: Wrap the value with the given value.
+ * @property {String} quote - For new values, quote them "always", "never", "strings" (for only strings), or
+ * "spaces" (if the value starts or ends with a space) [default: "spaces"].
+ * @property {String} quoteChar - The quote character for new values when quoting. [default: " (double quote)].
+ * @property {String} eol - The file's end of line character(s). [default: auto-detect]
+ */
+
+/**
+ * Callback for iniFile.parse(), invoked at the start of a new section.
+ *
+ * @callback iniFile.sectionBegin
+ * @param {Object} state The state object that was passed to the parse function.
+ * @param {Array} sectionPath The path to the new section object.
+ * @return {String} The text to add before the new section, or fluid.NO_VALUE to remove the section. Ignored when
+ * reading
+ */
+
+/**
+ * Callback for iniFile.parse(), invoked at the end of a section, after all of its sub-sections have been parsed.
+ *
+ * @callback iniFile.sectionEnd
+ * @param {Object} state The state object that was passed to the parse function.
+ * @param {Array} sectionPath The path to the section object.
+ * @param {Boolean} eof true if the end of section is the end of the file (rather than just before another section).
+ * @return {String} The text to add at the end of the section. Ignored when reading.
+ */
+
+/**
+ * Callback for iniFile.parse(), called when a value has been parsed.
+ *
+ * @callback iniFile.gotValue
+ * @param {String} sectionPath The path of the section.
+ * @param {String} key The value name.
+ * @param {String} value The value.
+ * @param {Boolean} quoted true if the value was quoted.
+ * @return {String} The new value, fluid.NO_VALUE to remove the value, or undefined if it's unchanged. Ignored when
+ * reading.
+ */
+
+/**
+ * Parse an INI file, invoking call backs at interesting points: start/end of a section, and on a key=value.
+ *
+ * If options.write=true, the return values of the callbacks will be used to modify the content, the entirety of which
+ * is then returned by this function.
+ *
+ * It uses the regular expression in ./ini.regex to perform the actual parsing.
+ *
+ * @param {String} content The INI file content.
+ * @param {Object} handlers Object containing the callbacks.
+ * @param {iniFile.sectionBegin} handlers.sectionBegin - called at the start of a new section, returning a string to
+ * write before the section line, if options.write=true.
+ * @param {iniFile.sectionEnd} handlers.sectionEnd - called at the end of a section, after all of its sub-sections
+ * have been seen. Returning a string to write at the end of the section, if options.write=true.
+ * @param {iniFile.gotValue} handlers.gotValue - called when a value has been parsed. If options.write=true, return the
+ * text to replace the content of the value, undefined for no update, or fluid.NO_VALUE to remove the key=value pair.
+ * @param {Object} options Parser options.
+ * @param {Boolean} options.write True if writing.
+ * @param {Object} options.state An object to pass to the handler callbacks.
+ * @return {Object|String} The data from the INI file, or the new file content if options.write=true.
+ */
+gpii.iniFile.parse = function (content, handlers, options) {
+ options = Object.assign({}, options);
+
+ var sectionPath = [];
+
+ if (handlers.sectionBegin) {
+ handlers.sectionBegin(options.state, sectionPath);
+ }
+
+ var regexFunction = options.write ? XRegExp.replace : XRegExp.forEach;
+
+ var output = regexFunction(content, gpii.iniFile.regex, function (match) {
+ return gpii.iniFile.parse.processMatch(match, sectionPath, handlers, options);
+ });
+
+ var append = gpii.iniFile.parse.endSection(sectionPath, 0, handlers.sectionEnd, true, options);
+ if (options.write && append) {
+ // If the content doesn't end with a new line, then add one before the new addition.
+ var hasEol = output.endsWith(options.state.options.eol);
+ if (!hasEol) {
+ output += options.state.options.eol;
+ if (append.endsWith(options.state.options.eol)) {
+ append = append.trimRight();
+ }
+ }
+ output += append;
+ }
+
+ return options.write && output;
+};
+
+/**
+ * Called during parsing when a [section] has ended, either due to the start of another section, or at the end of file.
+ * Adjust the current path to match the new depth, calling the sectionEnd callback for each level.
+ *
+ * @param {Array} sectionPath Path of the section that's ending.
+ * @param {Number} depth The new section path depth.
+ * @param {iniFile.sectionEnd} sectionEnd The end of section callback
+ * @param {Boolean} eof true if the end of section is the end of the file (rather than just before another section).
+ * @param {Object} options Parser options.
+ * @param {Boolean} options.write True if writing.
+ * @param {Object} options.state An object to pass to the handler callbacks.
+ * @return {String} If writing, the text to append to the section.
+ */
+gpii.iniFile.parse.endSection = function (sectionPath, depth, sectionEnd, eof, options) {
+ var result = "";
+ // Go back up the stack (if required)
+ while (depth <= sectionPath.length) {
+ // At this point, the sub-sections for the current section are done.
+ if (sectionEnd) {
+ var endContent = sectionEnd(options.state, sectionPath, eof);
+ if (options.write && endContent) {
+ result += endContent;
+ }
+ }
+ if (sectionPath.length === 0) {
+ break;
+ }
+ sectionPath.pop();
+ }
+ return result;
+};
+
+/**
+ * Processes a match from the INI parser.
+ *
+ * @param {Object} match The match from the regular expression.
+ * @param {String} match.section The section name, if matching a section header, otherwise undefined.
+ * @param {String} match.sectionCount The depth of the section (the [ characters).
+ * @param {String} match.key Name of the value.
+ * @param {String} match.prefix Everything up to the value (key and surrounding whitespace).
+ * @param {String} match.indent The indentation whitespace.
+ * @param {String} match.value The value, if unquoted.
+ * @param {String} match.value_ml_indent Multi-line value, if indented.
+ * @param {String} match.value_ml_quote Multi-line value, if quoted.
+ * @param {String} match.value_quote The value, if quoted.
+ * @param {String} match.qqq The quotes used for value_ml_quote
+ * @param {String} match.q The quotes used for value_quote
+ * @param {String} match.suffix Everything after the value, up to and including the newline.
+ * @param {Array} sectionPath The path of the current section.
+ * @param {Object} handlers Parser callbacks.
+ * @param {Object} options INI file options.
+ * @return {String} The replacement string, if writing.
+ */
+gpii.iniFile.parse.processMatch = function (match, sectionPath, handlers, options) {
+ var result = options.write && match.toString();
+ if (match.section) {
+ // The number of ['s in the section line.
+ var depth = match.sectionCount.length;
+
+ if (depth > sectionPath.length + 1) {
+ fluid.log("INI file parse error: Current section is not a direct descendant of the previous section");
+ }
+
+ var endContent = gpii.iniFile.parse.endSection(sectionPath, depth, handlers.sectionEnd, false, options);
+ if (options.write) {
+ result = endContent + result;
+ }
+
+ sectionPath.push(match.section);
+
+ if (handlers.sectionBegin) {
+ var prepend = handlers.sectionBegin(options.state, sectionPath);
+ if (options.write && prepend) {
+ result = prepend + result;
+ }
+ }
+
+ } else {
+ // It's some type of key=value pair.
+ var value = fluid.find(
+ [match.value, match.value_ml_indent, match.value_ml_quote, match.value_quote], fluid.identity);
+
+ if (match.value_quote) {
+ value = value.replace("\\\"", "\"");
+ } else if (match.value_ml_indent || match.value_ml_quote) {
+ value = value.replace(/\n[^\S\n]+/g, "\n");
+ }
+
+ if (handlers.gotValue) {
+ var quoted = match.value_ml_quote !== undefined || match.value_quote !== undefined;
+ var newValue = handlers.gotValue(options.state, sectionPath, match.key, value, quoted);
+ if (options.write) {
+ if (newValue === fluid.NO_VALUE) {
+ result = "";
+ } else if (newValue !== undefined) {
+ var quote = (match.qqq || match.q || "");
+ if (match.value_ml_indent) {
+ newValue = newValue.replace(/\n/g, "\n" + match.indent + " ".repeat(4));
+ }
+ result = match.prefix + quote + newValue + quote + match.suffix;
+ }
+ }
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Stringify a value - converts it to a string as returned by JSON.stringify.
+ * @param {Mixed} value The value.
+ * @return {String} The stringified version of value.
+ */
+gpii.iniFile.stringify = function (value) {
+ if (value === null || value === undefined) {
+ return "";
+ } else {
+ return typeof(value) === "string" ? value : JSON.stringify(value);
+ }
+};
+
+/**
+ * Generates the text of a new key=value pair, or a new section and its values and sub-sections if value is an
+ * object.
+ *
+ * @param {String} key The name of it.
+ * @param {String|Number|Boolean} value The value.
+ * @param {Array} path The path of the containing section.
+ * @param {iniFile.WriteOptions} options INI file output options.
+ * @return {Array} Array of lines for the new value or sub-section.
+ */
+gpii.iniFile.writeValue = function (key, value, path, options) {
+ var output = [];
+ if (fluid.isPlainObject(value)) {
+ // The section header.
+ output.push("");
+ output.push("[".repeat(path.length + 1) + key + "]".repeat(path.length + 1));
+ // Section items.
+ fluid.each(Object.keys(value), function (subkey) {
+ output.push.apply(output, gpii.iniFile.writeValue(subkey, value[subkey], path.concat(key), options));
+ });
+ } else {
+ var newValue = gpii.iniFile.stringify(value);
+
+ if (newValue.indexOf("\n") >= 0) {
+ // A multi-line value.
+ switch (options.multilineStyle) {
+ case "indent":
+ // eol is added at the end to ensure the next line isn't included in the value if it happens to be
+ // at the same indentation as the value.
+ newValue = newValue.replace(/\n/g, options.eol + " ") + options.eol;
+ break;
+ case "escape":
+ newValue = newValue.replace(/\n/g, "\\n");
+ break;
+ case "\"\"\"":
+ case "'''":
+ default:
+ newValue = options.multilineStyle + newValue + options.multilineStyle;
+ break;
+ }
+ } else {
+ var quote = (options.quote === "always" || (options.quote === "string" && typeof(s) === "string"));
+ quote = quote || (options.quote === "spaces" && /^\s|\s$/.test(newValue));
+ if (quote) {
+ newValue = options.quoteChar + newValue + options.quoteChar;
+ }
+ }
+ output.push(key + options.keyValueDelimiter + newValue);
+ }
+
+ return output;
+};
+
+/**
+ * Get the values that have not been written to for the current section (that is, the new values).
+ *
+ * @param {iniFile.WriteState} state The parser state.
+ * @param {Array} sectionPath The path to the section object.
+ * @param {Boolean} subSections true to return only sub-sections (objects), otherwise return values.
+ * @return {Array