Even more robust cookie editing

This commit is contained in:
Gregory Schier
2017-08-22 20:33:07 -07:00
parent c69c8d2dd0
commit 33b126ee87
7 changed files with 312 additions and 276 deletions

View File

@@ -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<string>, config: Object = {}, forceReset: boolean = false) {
export async function init (types: Array<string>, config: Object = {}, forceReset: boolean = false) {
if (forceReset) {
changeListeners = [];
db = {};
@@ -89,6 +90,12 @@ export function init (types: Array<string>, 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')}`);
}

View File

@@ -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;
}

View File

@@ -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 {
</thead>
<tbody key={cookies.length}>
{cookies.map((cookie, i) => {
const cookieString = cookieToString(Cookie.fromJSON(JSON.stringify(cookie)));
const cookieString = cookieToString(toughCookie.Cookie.fromJSON(cookie));
return (
<tr className="selectable" key={i}>

View File

@@ -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 (
<Modal ref={this._setModalRef} {...this.props}>
<ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify">
{cookieJar && cookie && (
<Tabs>
<TabList>
<Tab>
<button>Friendly</button>
</Tab>
<Tab>
<button>Raw</button>
</Tab>
</TabList>
<TabPanel>
<div className="pad">
{textFields.map((field, i) => {
const val = (cookie[field] || '').toString();
return (
<div className="form-control form-control--outlined" key={i}>
<label>{this._capitalize(field)}
<OneLineEditor
className={isValid[field] ? '' : 'input--error'}
forceEditor
type="text"
render={handleRender}
getRenderContext={handleGetRenderContext}
defaultValue={val || ''}
onChange={value => this._handleChange(field, value)}/>
</label>
</div>
);
})}
</div>
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
{checkFields.map((field, i) => {
const checked = !!cookie[field];
return (
<label key={i}>{this._capitalize(field)}
<input
className="space-left"
type="checkbox"
name={field}
defaultChecked={checked || false}
onChange={e => this._handleChange(field, e)}
/>
</label>
);
})}
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad">
<div className="form-control form-control--outlined">
<label>Raw Cookie String
<input type="text"
onChange={this._handleChangeRawValue}
defaultValue={this._getRawCookieString()}/>
</label>
</div>
</TabPanel>
</Tabs>
)}
</ModalBody>
<ModalFooter>
<button className="btn" onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
// 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 (
<div className="form-control form-control--outlined">
<label>
{this._capitalize(field)} <span className="danger">{error}</span>
<OneLineEditor
render={handleRender}
getRenderContext={handleGetRenderContext}
defaultValue={val || ''}
onChange={value => this._handleChange(field, value)}/>
</label>
</div>
);
}
render () {
const {cookieJar} = this.props;
const {cookie} = this.state;
const checkFields = ['secure', 'httpOnly'];
return (
<Modal ref={this._setModalRef} {...this.props}>
<ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify">
{cookieJar && cookie && (
<Tabs>
<TabList>
<Tab>
<button>Friendly</button>
</Tab>
<Tab>
<button>Raw</button>
</Tab>
</TabList>
<TabPanel>
<div className="pad">
<div className="form-row">
{this._renderInputField('key')}
{this._renderInputField('value')}
</div>
<div className="form-row">
{this._renderInputField('domain')}
{this._renderInputField('path')}
</div>
{this._renderInputField(
'expires',
isNaN(new Date(cookie.expires || 0).getTime()) ? 'Invalid Date' : null
)}
</div>
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
{checkFields.map((field, i) => {
const checked = !!cookie[field];
return (
<label key={i}>{this._capitalize(field)}
<input
className="space-left"
type="checkbox"
name={field}
defaultChecked={checked || false}
onChange={e => this._handleChange(field, e)}
/>
</label>
);
})}
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad">
<div className="form-control form-control--outlined">
<label>Raw Cookie String
<input type="text"
onChange={this._handleChangeRawValue}
defaultValue={this._getRawCookieString()}/>
</label>
</div>
</TabPanel>
</Tabs>
)}
</ModalBody>
<ModalFooter>
<button className="btn" onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
// export CookieModifyModal;
export default CookieModifyModal;

View File

@@ -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();

View File

@@ -536,6 +536,7 @@ class Wrapper extends React.PureComponent {
workspace={activeWorkspace}
cookieJar={activeCookieJar}
/>
<CookieModifyModal
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
@@ -543,6 +544,7 @@ class Wrapper extends React.PureComponent {
cookieJar={activeCookieJar}
workspace={activeWorkspace}
/>
<NunjucksModal
uniqueKey={`key::${this.state.forceRefreshKey}`}
ref={registerModal}

View File

@@ -4,15 +4,17 @@ import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import {DragDropContext} from 'react-dnd';
import App from './containers/app';
import * as models from '../models';
import {types as modelTypes} from '../models';
import {init as initStore} from './redux/modules';
import {init as initDB} from '../common/database';
import {init as initSync} from '../sync';
import {init as initAnalytics} from '../analytics';
import {init as initPlugins} from '../plugins';
import {types as modelTypes} from '../models';
import {getAccountId} from '../sync/session';
import DNDBackend from './dnd-backend';
import './css/index.less';
import {isDevelopment} from '../common/constants';
(async function () {
await initDB(modelTypes());
@@ -47,3 +49,8 @@ import './css/index.less';
// Do things that can wait
process.nextTick(initSync);
})();
// Export some useful things for dev
if (isDevelopment()) {
window.models = models;
}