diff --git a/app/common/database.js b/app/common/database.js index 4e24a6a0c2..ac4cac6af9 100644 --- a/app/common/database.js +++ b/app/common/database.js @@ -10,6 +10,7 @@ import * as models from '../models/index'; import AlertModal from '../ui/components/modals/alert-modal'; import {showModal} from '../ui/components/modals/index'; import {trackEvent} from '../analytics/index'; +import {workspace} from '../models/index'; export const CHANGE_INSERT = 'insert'; export const CHANGE_UPDATE = 'update'; @@ -40,7 +41,7 @@ function getDBFilePath (modelType) { * @param forceReset * @returns {null} */ -export function init (types: Array, config: Object = {}, forceReset: boolean = false) { +export async function init (types: Array, config: Object = {}, forceReset: boolean = false) { if (forceReset) { changeListeners = []; db = {}; @@ -89,6 +90,12 @@ export function init (types: Array, config: Object = {}, forceReset: boo db[modelType] = collection; } + // Make sure CookieJars and environments exist for all workspaces + for (const workspace of await models.workspace.all()) { + await models.cookieJar.getOrCreateForParentId(workspace._id); + await models.environment.getOrCreateForWorkspace(workspace); + } + console.log(`[db] Initialized DB at ${getDBFilePath('$TYPE')}`); } diff --git a/app/models/cookie-jar.js b/app/models/cookie-jar.js index 59c7d32e39..80a85b00bb 100644 --- a/app/models/cookie-jar.js +++ b/app/models/cookie-jar.js @@ -1,18 +1,19 @@ // @flow import * as db from '../common/database'; import type {BaseModel} from './index'; - +import uuid from 'uuid'; export const name = 'Cookie Jar'; export const type = 'CookieJar'; export const prefix = 'jar'; export const canDuplicate = true; export type Cookie = { + id: string, domain: string, path: string, key: string, value: string, - expires: number, + expires: number | null, httpOnly: boolean, secure: boolean } @@ -32,6 +33,7 @@ export function init () { } export function migrate (doc: CookieJar): CookieJar { + doc = migrateCookieId(doc); return doc; } @@ -59,3 +61,13 @@ export function getById (id: string) { export function update (cookieJar: CookieJar, patch: Object = {}) { return db.docUpdate(cookieJar, patch); } + +/** Ensure every cookie has an ID property */ +function migrateCookieId (cookieJar: CookieJar) { + for (const cookie of cookieJar.cookies) { + if (!cookie.id) { + cookie.id = uuid.v4(); + } + } + return cookieJar; +} diff --git a/app/ui/components/cookie-list.js b/app/ui/components/cookie-list.js index 77e9f7d28b..e7a0ce7fc8 100644 --- a/app/ui/components/cookie-list.js +++ b/app/ui/components/cookie-list.js @@ -1,11 +1,12 @@ // @flow import React, {PureComponent} from 'react'; +import uuid from 'uuid'; +import * as toughCookie from 'tough-cookie'; import autobind from 'autobind-decorator'; -import {Cookie} from 'tough-cookie'; - import {cookieToString} from '../../common/cookies'; import PromptButton from './base/prompt-button'; import RenderedText from './rendered-text'; +import type {Cookie} from '../../models/cookie-jar'; @autobind class CookieList extends PureComponent { @@ -19,14 +20,16 @@ class CookieList extends PureComponent { }; _handleCookieAdd () { - const newCookie = new Cookie({ + const newCookie: Cookie = { + id: uuid.v4(), key: 'foo', value: 'bar', domain: this.props.newCookieDomainName, + expires: null, path: '/', secure: false, httpOnly: false - }); + }; this.props.onCookieAdd(newCookie); } @@ -60,7 +63,7 @@ class CookieList extends PureComponent { {cookies.map((cookie, i) => { - const cookieString = cookieToString(Cookie.fromJSON(JSON.stringify(cookie))); + const cookieString = cookieToString(toughCookie.Cookie.fromJSON(cookie)); return ( diff --git a/app/ui/components/modals/cookie-modify-modal.js b/app/ui/components/modals/cookie-modify-modal.js index 1076483468..8b138b3dc1 100644 --- a/app/ui/components/modals/cookie-modify-modal.js +++ b/app/ui/components/modals/cookie-modify-modal.js @@ -1,257 +1,270 @@ -// @flow -import React, {PureComponent} from 'react'; -import {Tabs, TabList, Tab, TabPanel} from 'react-tabs'; -import autobind from 'autobind-decorator'; -import deepEqual from 'deep-equal'; -import * as toughCookie from 'tough-cookie'; -import * as models from '../../../models'; -import clone from 'clone'; -import {DEBOUNCE_MILLIS} from '../../../common/constants'; -import {trackEvent} from '../../../analytics/index'; -import Modal from '../base/modal'; -import ModalBody from '../base/modal-body'; -import ModalHeader from '../base/modal-header'; -import ModalFooter from '../base/modal-footer'; -import OneLineEditor from '../codemirror/one-line-editor'; -import {cookieToString} from '../../../common/cookies'; -import type {Cookie, CookieJar} from '../../../models/cookie-jar'; -import type {Workspace} from '../../../models/workspace'; - -@autobind -class CookieModifyModal extends PureComponent { - props: { - handleRender: Function, - handleGetRenderContext: Function, - workspace: Workspace, - cookieJar: CookieJar - }; - - state: { - cookie: Cookie | null, - rawValue: string, - isValid: { - key: boolean, - value: boolean, - domain: boolean, - path: boolean, - expires: boolean - } - }; - - modal: Modal | null; - _rawTimeout: number | null; - _cookieUpdateTimeout: number | null; - - constructor (props: any) { - super(props); - - this.state = { - cookie: null, - rawValue: '', - isValid: { - key: true, - value: true, - domain: true, - path: true, - expires: true - } - }; - - this._rawTimeout = null; - this._cookieUpdateTimeout = null; - } - - _setModalRef (n: Modal | null) { - this.modal = n; - } - - async show (cookie: Cookie) { - // Dunno why this is sent as an array - cookie = cookie[0] || cookie; - this.setState({cookie}); - - this.modal && this.modal.show(); - - trackEvent('Cookie Modifier', 'Show'); - } - - hide () { - this.modal && this.modal.hide(); - } - - async _saveChanges (cookieJar: CookieJar) { - await models.cookieJar.update(cookieJar); - } - - _handleChangeRawValue (e: Event & {target: HTMLInputElement}) { - const value = e.target.value; - - clearTimeout(this._rawTimeout); - this._rawTimeout = setTimeout(async () => { - const cookie = toughCookie.Cookie.parse(value); - if (!this.state.cookie) { - return; - } - - await this._handleCookieUpdate(this.state.cookie, cookie); - }, DEBOUNCE_MILLIS); - } - - async _handleCookieUpdate (oldCookie: Cookie, cookie: Cookie) { - // Clone so we don't modify the original - const cookieJar = clone(this.props.cookieJar); - - const {cookies} = cookieJar; - const index = cookies.findIndex(c => deepEqual(c, oldCookie)); - - cookieJar.cookies = [ - ...cookies.slice(0, index), - cookie, - ...cookies.slice(index + 1) - ]; - - this.setState({cookie}); - - await this._saveChanges(cookieJar); - trackEvent('Cookie', 'Update'); - } - - _handleChange (field: string, eventOrValue: Event & {target: HTMLInputElement}) { - const {cookie} = this.state; - let valid = true; - - const value = typeof eventOrValue === 'object' - ? eventOrValue.target.checked - : eventOrValue; - - if (valid) { - const newCookie = Object.assign({}, cookie, {[field]: value}); - - clearTimeout(this._cookieUpdateTimeout); - this._cookieUpdateTimeout = setTimeout(async () => { - if (cookie) { - await this._handleCookieUpdate(cookie, newCookie); - this.setState({cookie: newCookie}); - } - }, DEBOUNCE_MILLIS * 2); - } - - this.setState({ - isValid: { - ...this.state.isValid, - [field]: valid - } - }); - } - - _capitalize (str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - _getRawCookieString () { - const {cookie} = this.state; - - if (!cookie) { - return ''; - } - - try { - return cookieToString(toughCookie.Cookie.fromJSON(JSON.stringify(cookie))); - } catch (err) { - console.warn('Failed to parse cookie string', err); - return ''; - } - } - - render () { - const { - cookieJar, - handleRender, - handleGetRenderContext - } = this.props; - - const { - isValid, - cookie - } = this.state; - - const textFields = ['key', 'value', 'domain', 'path', 'expires']; - const checkFields = ['secure', 'httpOnly']; - - return ( - - Edit Cookie - - {cookieJar && cookie && ( - - - - - - - - - - -
- {textFields.map((field, i) => { - const val = (cookie[field] || '').toString(); - - return ( -
- -
- ); - })} -
-
- {checkFields.map((field, i) => { - const checked = !!cookie[field]; - - return ( - - ); - })} -
-
- -
- -
-
-
- )} -
- - - -
- ); - } -} - -// export CookieModifyModal; -export default CookieModifyModal; +// @flow +import React, {PureComponent} from 'react'; +import uuid from 'uuid'; +import {Tabs, TabList, Tab, TabPanel} from 'react-tabs'; +import autobind from 'autobind-decorator'; +import deepEqual from 'deep-equal'; +import * as toughCookie from 'tough-cookie'; +import * as models from '../../../models'; +import clone from 'clone'; +import {DEBOUNCE_MILLIS} from '../../../common/constants'; +import {trackEvent} from '../../../analytics/index'; +import Modal from '../base/modal'; +import ModalBody from '../base/modal-body'; +import ModalHeader from '../base/modal-header'; +import ModalFooter from '../base/modal-footer'; +import OneLineEditor from '../codemirror/one-line-editor'; +import {cookieToString} from '../../../common/cookies'; +import type {Cookie, CookieJar} from '../../../models/cookie-jar'; +import type {Workspace} from '../../../models/workspace'; + +@autobind +class CookieModifyModal extends PureComponent { + props: { + handleRender: Function, + handleGetRenderContext: Function, + workspace: Workspace, + cookieJar: CookieJar + }; + + state: { + cookie: Cookie | null, + rawValue: string + }; + + modal: Modal | null; + _rawTimeout: number | null; + _cookieUpdateTimeout: number | null; + + constructor (props: any) { + super(props); + + this.state = { + cookie: null, + rawValue: '' + }; + + this._rawTimeout = null; + this._cookieUpdateTimeout = null; + } + + _setModalRef (n: Modal | null) { + this.modal = n; + } + + async show (cookie: Cookie) { + // Dunno why this is sent as an array + cookie = cookie[0] || cookie; + + const {cookieJar} = this.props; + const oldCookie = cookieJar.cookies.find(c => deepEqual(c, cookie)); + + if (!oldCookie) { + // Cookie not found in jar + return; + } + + this.setState({cookie}); + + this.modal && this.modal.show(); + + trackEvent('Cookie Modifier', 'Show'); + } + + hide () { + this.modal && this.modal.hide(); + } + + async _saveChanges (cookieJar: CookieJar) { + await models.cookieJar.update(cookieJar); + } + + _handleChangeRawValue (e: Event & {target: HTMLInputElement}) { + const value = e.target.value; + + clearTimeout(this._rawTimeout); + this._rawTimeout = setTimeout(async () => { + const cookie = toughCookie.Cookie.parse(value); + if (!this.state.cookie) { + return; + } + + await this._handleCookieUpdate(cookie); + }, DEBOUNCE_MILLIS * 2); + } + + async _handleCookieUpdate (newCookie: Cookie) { + const cookie = clone(newCookie); + + // Sanitize expires field + const expires = (new Date(cookie.expires || '')).getTime(); + if (isNaN(expires)) { + delete cookie.expires; + } else { + cookie.expires = expires; + } + + // Clone so we don't modify the original + const cookieJar = clone(this.props.cookieJar); + + const {cookies} = cookieJar; + + const index = cookies.findIndex(c => c.id === cookie.id); + + cookieJar.cookies = [ + ...cookies.slice(0, index), + cookie, + ...cookies.slice(index + 1) + ]; + + this.setState({cookie}); + + await this._saveChanges(cookieJar); + trackEvent('Cookie', 'Update'); + + return cookie; + } + + _handleChange (field: string, eventOrValue: string | Event & {target: HTMLInputElement}) { + const {cookie} = this.state; + + let value; + + if (typeof eventOrValue === 'string') { + value = eventOrValue.trim(); + } else { + if (eventOrValue.target.type === 'checkbox') { + value = eventOrValue.target.checked; + } else { + value = eventOrValue.target.value.trim(); + } + } + + const newCookie = Object.assign({}, cookie, {[field]: value}); + + clearTimeout(this._cookieUpdateTimeout); + this._cookieUpdateTimeout = setTimeout(async () => { + await this._handleCookieUpdate(newCookie); + this.setState({cookie: newCookie}); + }, DEBOUNCE_MILLIS * 2); + } + + _capitalize (str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + _getRawCookieString () { + const {cookie} = this.state; + + if (!cookie) { + return ''; + } + + try { + return cookieToString(toughCookie.Cookie.fromJSON(JSON.stringify(cookie))); + } catch (err) { + console.warn('Failed to parse cookie string', err); + return ''; + } + } + + _renderInputField (field: string, error: string | null = null) { + const {cookie} = this.state; + const {handleRender, handleGetRenderContext} = this.props; + + if (!cookie) { + return null; + } + + const val = (cookie[field] || '').toString(); + + return ( +
+ +
+ ); + } + + render () { + const {cookieJar} = this.props; + const {cookie} = this.state; + const checkFields = ['secure', 'httpOnly']; + + return ( + + Edit Cookie + + {cookieJar && cookie && ( + + + + + + + + + + +
+
+ {this._renderInputField('key')} + {this._renderInputField('value')} +
+
+ {this._renderInputField('domain')} + {this._renderInputField('path')} +
+ {this._renderInputField( + 'expires', + isNaN(new Date(cookie.expires || 0).getTime()) ? 'Invalid Date' : null + )} +
+
+ {checkFields.map((field, i) => { + const checked = !!cookie[field]; + + return ( + + ); + })} +
+
+ +
+ +
+
+
+ )} +
+ + + +
+ ); + } +} + +// export CookieModifyModal; +export default CookieModifyModal; diff --git a/app/ui/components/modals/cookies-modal.js b/app/ui/components/modals/cookies-modal.js index 488c1bec96..a577f8631a 100644 --- a/app/ui/components/modals/cookies-modal.js +++ b/app/ui/components/modals/cookies-modal.js @@ -39,13 +39,6 @@ class CookiesModal extends PureComponent { }; } - async _ensureCookieJarExists () { - const {cookieJar, workspace} = this.props; - if (!cookieJar) { - models.cookieJar.getOrCreateForParentId(workspace._id); - } - } - _setModalRef (n: React.Element<*> | null) { this.modal = n; } @@ -115,8 +108,6 @@ class CookiesModal extends PureComponent { visibleCookieIndexes = null; } - console.log('APPLIED FILTER', filter, visibleCookieIndexes); - this.setState({filter, visibleCookieIndexes}); } @@ -132,7 +123,8 @@ class CookiesModal extends PureComponent { } async show () { - await this._ensureCookieJarExists(); + // Opened without cookie jar. Bail out and create one + const {cookieJar, workspace} = this.props; setTimeout(() => { this.filterInput && this.filterInput.focus(); diff --git a/app/ui/components/wrapper.js b/app/ui/components/wrapper.js index c559d91c7a..696e1b4238 100644 --- a/app/ui/components/wrapper.js +++ b/app/ui/components/wrapper.js @@ -536,6 +536,7 @@ class Wrapper extends React.PureComponent { workspace={activeWorkspace} cookieJar={activeCookieJar} /> + +