Skip to content

Commit

Permalink
Add support for setting browser :config with JSON
Browse files Browse the repository at this point in the history
Two changes in this commit:

- Add ability to accept setting browser configs with a JSON string, like: `:config {"maxFrames":50, "theme": "outline"}`
- No longer accept non quoted keys when setting single value configs. This is not supported anymore: `:config x: 1`. Instead use: `:config "x": 1`.

Helpful hints are displayed below the editor to enlighten users of this change.
  • Loading branch information
oskarhane committed Jun 20, 2017
1 parent 98780ab commit df11599
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 12 deletions.
29 changes: 27 additions & 2 deletions src/shared/modules/commands/commandsDuck.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { send } from 'shared/modules/requests/requestsDuck'
import * as frames from 'shared/modules/stream/streamDuck'
import { disconnectAction } from 'shared/modules/connections/connectionsDuck'
import { merge, set } from 'shared/modules/params/paramsDuck'
import { update as updateSettings } from 'shared/modules/settings/settingsDuck'
import { update as updateSettings, replace as replaceSettings } from 'shared/modules/settings/settingsDuck'
import { cleanCommand, getInterpreter } from 'services/commandUtils'

const bus = createBus()
Expand Down Expand Up @@ -184,7 +184,7 @@ describe('commandsDuck', () => {
test('does the right thing for :config x: 2', (done) => {
// Given
const cmd = store.getState().settings.cmdchar + 'config'
const cmdString = cmd + ' x: 2'
const cmdString = cmd + ' "x": 2'
const id = 1
const action = commands.executeCommand(cmdString, id)
bus.take('NOOP', (currentAction) => {
Expand All @@ -206,6 +206,31 @@ describe('commandsDuck', () => {
// Then
// See above
})
test('does the right thing for :config {"x": 2, "y":3}', (done) => {
// Given
const cmd = store.getState().settings.cmdchar + 'config'
const cmdString = cmd + ' {"x": 2, "y":3}'
const id = 1
const action = commands.executeCommand(cmdString, id)
bus.take('NOOP', (currentAction) => {
// Then
expect(store.getActions()).toEqual([
action,
addHistory(cmdString, maxHistory),
{ type: commands.KNOWN_COMMAND },
replaceSettings({x: 2, y: 3}),
frames.add({...action, type: 'pre', result: JSON.stringify({cmdchar: ':', maxHistory: 20}, null, 2)}),
{ type: 'NOOP' }
])
done()
})

// When
store.dispatch(action)

// Then
// See above
})

test('does the right thing for :config', (done) => {
// Given
Expand Down
34 changes: 24 additions & 10 deletions src/shared/modules/commands/helpers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*/

import { getSettings, update, replace } from 'shared/modules/settings/settingsDuck'
import { extractCommandParameters, splitStringOnFirst } from 'services/commandUtils'
import { splitStringOnFirst } from 'services/commandUtils'
import { getRemoteContentHostnameWhitelist } from 'shared/modules/dbMeta/dbMetaDuck'
import { hostIsAllowed } from 'services/utils'
import { getJSON } from 'services/remote'
Expand All @@ -36,25 +36,39 @@ export function handleUpdateConfigCommand (action, cmdchar, put, store) {
const parts = splitStringOnFirst(strippedCmd, ' ')
const p = new Promise((resolve, reject) => {
if (parts[1] === undefined || parts[1] === '') return resolve(true) // Nothing to do
if (!isValidURL(parts[1].trim())) { // Not an URL. Parse as command line params
const params = extractCommandParameters(`config`, strippedCmd)
if (!params) return reject(new Error('Could not parse input. Usage: `:config x: 2`.'))
put(update(params))
return resolve(params)
const param = parts[1].trim()
if (!isValidURL(param)) { // Not an URL. Parse as command line params
if (/^"?\{[^}]*\}"?$/.test(param)) { // JSON object string {"x": 2, "y":"string"}
try {
const res = JSON.parse(param.replace(/^"/, '').replace(/"$/, '')) // Remove any surrounding quotes
put(replace(res))
return resolve(res)
} catch (e) {
return reject(new Error('Could not parse input. Usage: `:config {"x":1,"y":"string"}`. ' + e))
}
} else { // Single param
try {
const json = '{' + param + '}'
const res = JSON.parse(json)
put(update(res))
return resolve(res)
} catch (e) {
return reject(new Error('Could not parse input. Usage: `:config "x": 2`. ' + e))
}
}
}
// It's an URL
const url = parts[1].trim()
const whitelist = getRemoteContentHostnameWhitelist(store.getState())
if (!hostIsAllowed(url, whitelist)) { // Make sure we're allowed to request entities on this host
if (!hostIsAllowed(param, whitelist)) { // Make sure we're allowed to request entities on this host
return reject(new Error('Hostname is not allowed according to server whitelist'))
}
getJSON(url)
getJSON(param)
.then((res) => {
put(replace(res))
resolve(res)
})
.catch((e) => {
reject(new Error(e))
reject(e)
})
})
return p
Expand Down
169 changes: 169 additions & 0 deletions src/shared/modules/commands/helpers/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/* global jest, describe, test, expect, afterEach */

import nock from 'nock'
import * as config from './config'
import { update, replace } from 'shared/modules/settings/settingsDuck'

function FetchError (message) {
this.name = 'FetchError'
this.message = message
}
FetchError.prototype = Object.create(Error.prototype)
FetchError.prototype.constructor = FetchError

describe('commandsDuck config helper', () => {
const store = {
getState: () => {
return {
meta: {
settings: {
'browser.remote_content_hostname_whitelist': 'okurl.com'
}
}
}
}
}
afterEach(() => {
nock.cleanAll()
})
test('handles :config "x": 2 and calls the update action creator', () => {
// Given
const action = { cmd: ':config "x": 2' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return p.then((res) => {
expect(res).toEqual({x: 2})
expect(put).toHaveBeenCalledWith(update({x: 2}))
})
})
test('handles :config "x y": 2 and calls the update action creator', () => {
// Given
const action = { cmd: ':config "x y": 2' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return p.then((res) => {
expect(res).toEqual({'x y': 2})
expect(put).toHaveBeenCalledWith(update({'x y': 2}))
})
})
test('hints that :config x: 2 is the wrong format', () => {
// Given
const action = { cmd: ':config x: 2' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return expect(p)
.rejects.toEqual(new Error('Could not parse input. Usage: `:config "x": 2`. ' + 'SyntaxError: Unexpected token x in JSON at position 1'))
.then(() => expect(put).not.toHaveBeenCalled())
})
test('handles :config {"hej": "ho", "let\'s": "go"} and calls the replace action creator', () => {
// Given
const action = { cmd: ':config {"hej": "ho", "let\'s": "go"}' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return p.then((res) => {
expect(res).toEqual({hej: 'ho', "let's": 'go'})
expect(put).toHaveBeenCalledWith(replace({hej: 'ho', "let's": 'go'}))
})
})
test('hints that :config {x: 1, y: 2} is the wrong format', () => {
// Given
const action = { cmd: ':config {x: 1, y: 2}' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return expect(p)
.rejects.toEqual(new Error('Could not parse input. Usage: `:config {"x":1,"y":"string"}`. ' + 'SyntaxError: Unexpected token x in JSON at position 1'))
.then(() => expect(put).not.toHaveBeenCalled())
})
test('rejects hostnames not in whitelist', () => {
// Given
const action = { cmd: ':config https://bad.com/cnf.json' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return expect(p)
.rejects.toEqual(new Error('Hostname is not allowed according to server whitelist'))
.then(() => expect(put).not.toHaveBeenCalled())
})
test('handles :config https://okurl.com/cnf.json and calls the replace action creator', () => {
// Given
const json = JSON.stringify({x: 1, y: 'hello'})
nock('https://okurl.com').get('/cnf.json').reply(200, json)
const action = { cmd: ':config https://okurl.com/cnf.json' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return p.then((res) => {
expect(res).toEqual(JSON.parse(json))
expect(put).toHaveBeenCalledWith(replace(JSON.parse(json)))
})
})
test('indicates error parsing remote content', () => {
// Given
const json = 'no json'
nock('https://okurl.com').get('/cnf.json').reply(200, json)
const action = { cmd: ':config https://okurl.com/cnf.json' }
const cmdchar = ':'
const put = jest.fn()

// When
const p = config.handleUpdateConfigCommand(action, cmdchar, put, store)

// Then
return expect(p)
.rejects.toEqual(new Error(new FetchError('invalid json response body at https://okurl.com/cnf.json reason: Unexpected token o in JSON at position 1')))
.then(() => expect(put).not.toHaveBeenCalled())
})
})

0 comments on commit df11599

Please sign in to comment.