Better cookie handling/editing

This commit is contained in:
Gregory Schier 2017-08-22 18:16:21 -07:00
parent 2887282032
commit c69c8d2dd0
4 changed files with 167 additions and 116 deletions

View File

@ -3,4 +3,5 @@ charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
end_of_line = lf

View File

@ -2,8 +2,10 @@
import React, {PureComponent} from 'react'; import React, {PureComponent} from 'react';
import {Tabs, TabList, Tab, TabPanel} from 'react-tabs'; import {Tabs, TabList, Tab, TabPanel} from 'react-tabs';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import deepEqual from 'deep-equal';
import * as toughCookie from 'tough-cookie'; import * as toughCookie from 'tough-cookie';
import * as models from '../../../models'; import * as models from '../../../models';
import clone from 'clone';
import {DEBOUNCE_MILLIS} from '../../../common/constants'; import {DEBOUNCE_MILLIS} from '../../../common/constants';
import {trackEvent} from '../../../analytics/index'; import {trackEvent} from '../../../analytics/index';
import Modal from '../base/modal'; import Modal from '../base/modal';
@ -96,9 +98,11 @@ class CookieModifyModal extends PureComponent {
} }
async _handleCookieUpdate (oldCookie: Cookie, cookie: Cookie) { async _handleCookieUpdate (oldCookie: Cookie, cookie: Cookie) {
const {cookieJar} = this.props; // Clone so we don't modify the original
const cookieJar = clone(this.props.cookieJar);
const {cookies} = cookieJar; const {cookies} = cookieJar;
const index = cookies.findIndex(c => c.domain === oldCookie.domain && c.key === oldCookie.key); const index = cookies.findIndex(c => deepEqual(c, oldCookie));
cookieJar.cookies = [ cookieJar.cookies = [
...cookies.slice(0, index), ...cookies.slice(0, index),
@ -144,6 +148,21 @@ class CookieModifyModal extends PureComponent {
return str.charAt(0).toUpperCase() + str.slice(1); 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 () { render () {
const { const {
cookieJar, cookieJar,
@ -151,91 +170,78 @@ class CookieModifyModal extends PureComponent {
handleGetRenderContext handleGetRenderContext
} = this.props; } = this.props;
if (!cookieJar) {
return null;
}
const { const {
isValid, isValid,
cookie cookie
} = this.state; } = this.state;
if (!cookie || !cookieJar) {
return null;
}
const textFields = ['key', 'value', 'domain', 'path', 'expires']; const textFields = ['key', 'value', 'domain', 'path', 'expires'];
const checkFields = ['secure', 'httpOnly']; const checkFields = ['secure', 'httpOnly'];
let rawCookieString = '';
try {
rawCookieString = cookieToString(toughCookie.Cookie.fromJSON(JSON.stringify(cookie)));
} catch (err) {
console.warn('Failed to parse cookie', err);
}
return ( return (
<Modal ref={this._setModalRef} {...this.props}> <Modal ref={this._setModalRef} {...this.props}>
<ModalHeader>Edit Cookie</ModalHeader> <ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify"> <ModalBody className="cookie-modify">
<Tabs> {cookieJar && cookie && (
<TabList> <Tabs>
<Tab> <TabList>
<button>Friendly</button> <Tab>
</Tab> <button>Friendly</button>
<Tab> </Tab>
<button>Raw</button> <Tab>
</Tab> <button>Raw</button>
</TabList> </Tab>
<TabPanel> </TabList>
<div className="pad"> <TabPanel>
{textFields.map((field, i) => { <div className="pad">
const val = (cookie[field] || '').toString(); {textFields.map((field, i) => {
const val = (cookie[field] || '').toString();
return ( return (
<div className="form-control form-control--outlined" key={i}> <div className="form-control form-control--outlined" key={i}>
<label>{this._capitalize(field)} <label>{this._capitalize(field)}
<OneLineEditor <OneLineEditor
className={isValid[field] ? '' : 'input--error'} className={isValid[field] ? '' : 'input--error'}
forceEditor forceEditor
type="text" type="text"
render={handleRender} render={handleRender}
getRenderContext={handleGetRenderContext} getRenderContext={handleGetRenderContext}
defaultValue={val || ''} defaultValue={val || ''}
onChange={value => this._handleChange(field, value)}/> 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> </label>
</div> );
); })}
})} </div>
</div> </TabPanel>
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg"> <TabPanel className="react-tabs__tab-panel pad">
{checkFields.map((field, i) => { <div className="form-control form-control--outlined">
const checked = !!cookie[field]; <label>Raw Cookie String
<input type="text"
return ( onChange={this._handleChangeRawValue}
<label key={i}>{this._capitalize(field)} defaultValue={this._getRawCookieString()}/>
<input </label>
className="space-left" </div>
type="checkbox" </TabPanel>
name={field} </Tabs>
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={rawCookieString}/>
</label>
</div>
</TabPanel>
</Tabs>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<button className="btn" onClick={this.hide}> <button className="btn" onClick={this.hide}>

View File

@ -1,5 +1,6 @@
// @flow // @flow
import React, {PureComponent} from 'react'; import React, {PureComponent} from 'react';
import deepEqual from 'deep-equal';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Modal from '../base/modal'; import Modal from '../base/modal';
import ModalBody from '../base/modal-body'; import ModalBody from '../base/modal-body';
@ -10,6 +11,7 @@ import * as models from '../../../models';
import {trackEvent} from '../../../analytics/index'; import {trackEvent} from '../../../analytics/index';
import type {Cookie, CookieJar} from '../../../models/cookie-jar'; import type {Cookie, CookieJar} from '../../../models/cookie-jar';
import type {Workspace} from '../../../models/workspace'; import type {Workspace} from '../../../models/workspace';
import {fuzzyMatch} from '../../../common/misc';
@autobind @autobind
class CookiesModal extends PureComponent { class CookiesModal extends PureComponent {
@ -21,7 +23,8 @@ class CookiesModal extends PureComponent {
}; };
state: { state: {
filter: string filter: string,
visibleCookieIndexes: Array<number> | null
}; };
modal: Modal | null; modal: Modal | null;
@ -29,8 +32,10 @@ class CookiesModal extends PureComponent {
constructor (props: any) { constructor (props: any) {
super(props); super(props);
this.state = { this.state = {
filter: '' filter: '',
visibleCookieIndexes: null
}; };
} }
@ -74,21 +79,56 @@ class CookiesModal extends PureComponent {
trackEvent('Cookie', 'Delete'); trackEvent('Cookie', 'Delete');
} }
_handleFilterChange (e: Event & {target: HTMLInputElement}) { async _handleFilterChange (e: Event & {target: HTMLInputElement}) {
const filter = e.target.value; const filter = e.target.value;
this.setState({filter}); this._applyFilter(filter, this.props.cookieJar.cookies);
trackEvent('Cookie Editor', 'Filter Change');
} }
_getFilteredSortedCookies () { componentWillReceiveProps (nextProps: any) {
const {cookieJar} = this.props; // Re-filter if we received new cookies
const {filter} = this.state; // Compare cookies with Dates cast to strings
const sameCookies = deepEqual(
this.props.cookieJar.cookies,
nextProps.cookieJar.cookies
);
const {cookies} = cookieJar; if (!sameCookies) {
return cookies.filter(c => { this._applyFilter(this.state.filter, nextProps.cookieJar.cookies);
const toSearch = JSON.stringify(c).toLowerCase(); }
return toSearch.indexOf(filter.toLowerCase()) !== -1; }
});
async _applyFilter (filter: string, cookies: Array<Cookie>) {
const renderedCookies = await this.props.handleRender(cookies);
let visibleCookieIndexes;
if (filter) {
visibleCookieIndexes = [];
for (let i = 0; i < renderedCookies.length; i++) {
const toSearch = JSON.stringify(renderedCookies[i]);
const matched = fuzzyMatch(filter, toSearch);
if (matched) {
visibleCookieIndexes.push(i);
}
}
} else {
visibleCookieIndexes = null;
}
console.log('APPLIED FILTER', filter, visibleCookieIndexes);
this.setState({filter, visibleCookieIndexes});
}
_getVisibleCookies () {
const {cookieJar} = this.props;
const {visibleCookieIndexes} = this.state;
if (visibleCookieIndexes === null) {
return cookieJar.cookies;
}
return cookieJar.cookies.filter((c, i) => visibleCookieIndexes.includes(i));
} }
async show () { async show () {
@ -98,6 +138,9 @@ class CookiesModal extends PureComponent {
this.filterInput && this.filterInput.focus(); this.filterInput && this.filterInput.focus();
}, 100); }, 100);
// make sure the filter is up to date
await this._applyFilter(this.state.filter, this.props.cookieJar.cookies);
this.modal && this.modal.show(); this.modal && this.modal.show();
trackEvent('Cookie Manager', 'Show'); trackEvent('Cookie Manager', 'Show');
} }
@ -113,44 +156,42 @@ class CookiesModal extends PureComponent {
cookieJar cookieJar
} = this.props; } = this.props;
if (!cookieJar) {
return null;
}
const { const {
filter filter
} = this.state; } = this.state;
const filteredCookies = this._getFilteredSortedCookies();
return ( return (
<Modal ref={this._setModalRef} wide tall {...this.props}> <Modal ref={this._setModalRef} wide tall {...this.props}>
<ModalHeader>Manage Cookies</ModalHeader> <ModalHeader>Manage Cookies</ModalHeader>
<ModalBody className="cookie-list" noScroll> <ModalBody className="cookie-list" noScroll>
<div className="pad"> {cookieJar && (
<div className="form-control form-control--outlined"> <div>
<label>Filter Cookies <div className="pad">
<input ref={this._setFilterInputRef} <div className="form-control form-control--outlined">
onChange={this._handleFilterChange} <label>Filter Cookies
type="text" <input ref={this._setFilterInputRef}
placeholder="twitter.com" onChange={this._handleFilterChange}
defaultValue=""/> type="text"
</label> placeholder="twitter.com"
defaultValue=""/>
</label>
</div>
</div>
<div className="cookie-list__list border-top">
<div className="pad-top">
<CookieList
handleShowModifyCookieModal={handleShowModifyCookieModal}
handleRender={handleRender}
cookies={this._getVisibleCookies()}
onCookieAdd={this._handleCookieAdd}
onCookieDelete={this._handleCookieDelete}
// Set the domain to the filter so that it shows up if we're filtering
newCookieDomainName={filter || 'domain.com'}
/>
</div>
</div>
</div> </div>
</div> )}
<div className="cookie-list__list border-top">
<div className="pad-top">
<CookieList
handleShowModifyCookieModal={handleShowModifyCookieModal}
handleRender={handleRender}
cookies={filteredCookies}
onCookieAdd={this._handleCookieAdd}
onCookieDelete={this._handleCookieDelete}
// Set the domain to the filter so that it shows up if we're filtering
newCookieDomainName={filter || 'domain.com'}
/>
</div>
</div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<div className="margin-left faint italic txt-sm tall"> <div className="margin-left faint italic txt-sm tall">

3
flow-typed/deep-equal.js vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'deep-equal' {
declare module.exports: Function
}