From 968c7021dfef00c459899520921faf7367e923d9 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 23 Mar 2015 00:24:56 +0100 Subject: web: add basic edit capability for first line --- web/src/css/flowdetail.less | 24 +++ web/src/js/actions.js | 21 ++- web/src/js/components/common.js | 8 + web/src/js/components/flowtable-columns.js | 4 +- web/src/js/components/flowview/contentview.js | 20 ++- web/src/js/components/flowview/messages.js | 231 +++++++++++++++++++++++--- web/src/js/components/mainview.js | 9 + web/src/js/flow/utils.js | 70 ++++++-- web/src/js/utils.js | 34 ++-- 9 files changed, 361 insertions(+), 60 deletions(-) (limited to 'web') diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index cc67eeb2..edf97566 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -27,6 +27,13 @@ max-height: 100px; overflow-y: auto; } + .request-line { + margin-bottom: 2px; + } + /*.request .response-line, + .response .request-line { + opacity: 0.7; + }*/ hr { margin: 0 0 5px; @@ -34,6 +41,23 @@ } +.inline-input { + margin: 0 -5px; + padding: 0 5px; + + &[contenteditable] { + + background-color: rgba(255, 255, 255, 0.2); + + &.has-warning { + color: rgb(255, 184, 184); + } + &.has-success { + //color: green; + } + } +} + .view-options { margin-top: 10px; } diff --git a/web/src/js/actions.js b/web/src/js/actions.js index 78fd4bf7..87c99dd2 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -1,4 +1,5 @@ var $ = require("jquery"); +var _ = require("lodash"); var AppDispatcher = require("./dispatcher.js").AppDispatcher; var ActionTypes = { @@ -44,7 +45,8 @@ var SettingsActions = { $.ajax({ type: "PUT", url: "/settings", - data: settings + contentType: 'application/json', + data: JSON.stringify(settings) }); /* @@ -95,15 +97,26 @@ var FlowActions = { revert: function(flow){ $.post("/flows/" + flow.id + "/revert"); }, - update: function (flow) { + update: function (flow, nextProps) { + /* + //Facebook Flux: We do an optimistic update on the client already. + var nextFlow = _.cloneDeep(flow); + _.merge(nextFlow, nextProps); AppDispatcher.dispatchViewAction({ type: ActionTypes.FLOW_STORE, cmd: StoreCmds.UPDATE, - data: flow + data: nextFlow + }); + */ + $.ajax({ + type: "PUT", + url: "/flows/" + flow.id, + contentType: 'application/json', + data: JSON.stringify(nextProps) }); }, clear: function(){ - $.post("/clear"); + $.post("/flows/" + flow.id); } }; diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js index 3ed035ee..956a46d2 100644 --- a/web/src/js/components/common.js +++ b/web/src/js/components/common.js @@ -30,6 +30,13 @@ var StickyHeadMixin = { }; +var ChildFocus = { + contextTypes: { + returnFocus: React.PropTypes.func + } +}; + + var Navigation = _.extend({}, ReactRouter.Navigation, { setQuery: function (dict) { var q = this.context.router.getCurrentQuery(); @@ -176,6 +183,7 @@ var Splitter = React.createClass({ }); module.exports = { + ChildFocus: ChildFocus, State: State, Navigation: Navigation, StickyHeadMixin: StickyHeadMixin, diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js index a82c607a..b3b47910 100644 --- a/web/src/js/components/flowtable-columns.js +++ b/web/src/js/components/flowtable-columns.js @@ -16,7 +16,7 @@ var TLSColumn = React.createClass({ }, render: function () { var flow = this.props.flow; - var ssl = (flow.request.scheme == "https"); + var ssl = (flow.request.scheme === "https"); var classes; if (ssl) { classes = "col-tls col-tls-https"; @@ -44,7 +44,7 @@ var IconColumn = React.createClass({ var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - if (flow.response.code == 304) { + if (flow.response.code === 304) { icon = "resource-icon-not-modified"; } else if (300 <= flow.response.code && flow.response.code < 400) { icon = "resource-icon-redirect"; diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js index 828c6d08..63d22c1c 100644 --- a/web/src/js/components/flowview/contentview.js +++ b/web/src/js/components/flowview/contentview.js @@ -27,7 +27,7 @@ var RawMixin = { } }, requestContent: function (nextProps) { - if(this.state.request){ + if (this.state.request) { this.state.request.abort(); } var request = MessageUtils.getContent(nextProps.flow, nextProps.message); @@ -38,11 +38,11 @@ var RawMixin = { request.done(function (data) { this.setState({content: data}); }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { - if(textStatus === "abort"){ + if (textStatus === "abort") { return; } this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown}); - }.bind(this)).always(function(){ + }.bind(this)).always(function () { this.setState({request: undefined}); }.bind(this)); @@ -55,8 +55,8 @@ var RawMixin = { this.requestContent(nextProps); } }, - componentWillUnmount: function(){ - if(this.state.request){ + componentWillUnmount: function () { + if (this.state.request) { this.state.request.abort(); } }, @@ -94,7 +94,7 @@ var ViewJSON = React.createClass({ var json = this.state.content; try { json = JSON.stringify(JSON.parse(json), null, 2); - } catch(e) { + } catch (e) { } return
{json}
; } @@ -139,7 +139,7 @@ var ContentMissing = React.createClass({ var TooLarge = React.createClass({ statics: { - isTooLarge: function(message){ + isTooLarge: function (message) { var max_mb = ViewImage.matches(message) ? 10 : 0.2; return message.contentLength > 1024 * 1024 * max_mb; } @@ -225,8 +225,10 @@ var ContentView = React.createClass({
-   - +   + + +
; } diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js index fe8fa200..cb8516fc 100644 --- a/web/src/js/components/flowview/messages.js +++ b/web/src/js/components/flowview/messages.js @@ -1,5 +1,8 @@ var React = require("react"); +var _ = require("lodash"); +var common = require("../common.js"); +var actions = require("../../actions.js"); var flowutils = require("../../flow/utils.js"); var utils = require("../../utils.js"); var ContentView = require("./contentview.js"); @@ -24,20 +27,217 @@ var Headers = React.createClass({ } }); -var Request = React.createClass({ +var InlineInput = React.createClass({ + mixins: [common.ChildFocus], + getInitialState: function () { + return { + editable: false + }; + }, + render: function () { + var Tag = this.props.tag || "span"; + var className = "inline-input " + (this.props.className || ""); + var html = {__html: _.escape(this.props.content)}; + return ; + }, + onKeyDown: function (e) { + e.stopPropagation(); + switch (e.keyCode) { + case utils.Key.ESC: + this.blur(); + break; + case utils.Key.ENTER: + e.preventDefault(); + if (!e.ctrlKey) { + this.blur(); + } else { + this.props.onDone && this.props.onDone(); + } + break; + default: + break; + } + }, + blur: function(){ + this.getDOMNode().blur(); + this.context.returnFocus && this.context.returnFocus(); + }, + selectContents: function () { + var range = document.createRange(); + range.selectNodeContents(this.getDOMNode()); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }, + onFocus: function () { + this.setState({editable: true}, this.selectContents); + }, + onBlur: function (e) { + this.setState({editable: false}); + this.handleChange(); + this.props.onDone && this.props.onDone(); + }, + onInput: function () { + this.handleChange(); + }, + handleChange: function () { + var content = this.getDOMNode().textContent; + if (content !== this.props.content) { + this.props.onChange(content); + } + } +}); + +var ValidateInlineInput = React.createClass({ + getInitialState: function () { + return { + content: ""+this.props.content, + originalContent: ""+this.props.content + }; + }, + onChange: function (val) { + this.setState({ + content: val + }); + }, + onDone: function () { + if (this.state.content === this.state.originalContent) { + return true; + } + if (this.props.isValid(this.state.content)) { + this.props.onChange(this.state.content); + } else { + this.setState({ + content: this.state.originalContent + }); + } + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.content !== this.state.content) { + this.setState({ + content: ""+nextProps.content, + originalContent: ""+nextProps.content + }) + } + }, + render: function () { + var className = this.props.className || ""; + if (this.props.isValid(this.state.content)) { + className += " has-success"; + } else { + className += " has-warning" + } + return ; + } +}); + +var RequestLine = React.createClass({ render: function () { var flow = this.props.flow; - var first_line = [ - flow.request.method, - flowutils.RequestUtils.pretty_url(flow.request), - "HTTP/" + flow.request.httpversion.join(".") - ].join(" "); + var url = flowutils.RequestUtils.pretty_url(flow.request); + var httpver = "HTTP/" + flow.request.httpversion.join("."); - //TODO: Styling + return
+ +   + +   + +
+ }, + isValidMethod: function (method) { + return true; + }, + isValidUrl: function (url) { + var u = flowutils.parseUrl(url); + return !!u.host; + }, + onMethodChange: function (nextMethod) { + actions.FlowActions.update( + this.props.flow, + {request: {method: nextMethod}} + ); + }, + onUrlChange: function (nextUrl) { + var props = flowutils.parseUrl(nextUrl); + props.path = props.path || ""; + actions.FlowActions.update( + this.props.flow, + {request: props} + ); + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {request: {httpversion: ver}} + ); + } +}); +var ResponseLine = React.createClass({ + render: function () { + var flow = this.props.flow; + var httpver = "HTTP/" + flow.response.httpversion.join("."); + return
+ +   + +   + + +
; + }, + isValidCode: function (code) { + return /^\d+$/.test(code); + }, + isValidMsg: function () { + return true; + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {response: {httpversion: ver}} + ); + }, + onMsgChange: function(nextMsg){ + actions.FlowActions.update( + this.props.flow, + {response: {msg: nextMsg}} + ); + }, + onCodeChange: function(nextCode){ + nextCode = parseInt(nextCode); + actions.FlowActions.update( + this.props.flow, + {response: {code: nextCode}} + ); + } +}); + +var Request = React.createClass({ + render: function () { + var flow = this.props.flow; return ( -
-
{ first_line }
+
+ + {/**/}
@@ -49,17 +249,10 @@ var Request = React.createClass({ var Response = React.createClass({ render: function () { var flow = this.props.flow; - var first_line = [ - "HTTP/" + flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); - - //TODO: Styling - return ( -
-
{ first_line }
+
+ {/**/} +
diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js index 81bf3b03..9790a69e 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -11,6 +11,15 @@ var FlowView = require("./flowview/index.js"); var MainView = React.createClass({ mixins: [common.Navigation, common.State], + childContextTypes: { + returnFocus: React.PropTypes.func.isRequired + }, + getChildContext: function() { + return { returnFocus: this.returnFocus }; + }, + returnFocus: function(){ + this.getDOMNode().focus(); + }, getInitialState: function () { return { flows: [], diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js index 29462a78..aa91e3b6 100644 --- a/web/src/js/flow/utils.js +++ b/web/src/js/flow/utils.js @@ -1,9 +1,17 @@ var _ = require("lodash"); var $ = require("jquery"); +var defaultPorts = { + "http": 80, + "https": 443 +}; + var MessageUtils = { getContentType: function (message) { - return this.get_first_header(message, /^Content-Type$/i).split(";")[0].trim(); + var ct = this.get_first_header(message, /^Content-Type$/i); + if(ct){ + return ct.split(";")[0].trim(); + } }, get_first_header: function (message, regex) { //FIXME: Cache Invalidation. @@ -36,25 +44,20 @@ var MessageUtils = { } return false; }, - getContentURL: function(flow, message){ - if(message === flow.request){ + getContentURL: function (flow, message) { + if (message === flow.request) { message = "request"; - } else if (message === flow.response){ + } else if (message === flow.response) { message = "response"; } return "/flows/" + flow.id + "/" + message + "/content"; }, - getContent: function(flow, message){ + getContent: function (flow, message) { var url = MessageUtils.getContentURL(flow, message); return $.get(url); } }; -var defaultPorts = { - "http": 80, - "https": 443 -}; - var RequestUtils = _.extend(MessageUtils, { pretty_host: function (request) { //FIXME: Add hostheader @@ -72,8 +75,53 @@ var RequestUtils = _.extend(MessageUtils, { var ResponseUtils = _.extend(MessageUtils, {}); +var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i; +var parseUrl = function (url) { + //there are many correct ways to parse a URL, + //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) + var parts = parseUrl_regex.exec(url); + + var scheme = parts[1], + host = parts[2], + port = parseInt(parts[3]), + path = parts[4]; + if (scheme) { + port = port || defaultPorts[scheme]; + } + var ret = {}; + if (scheme) { + ret.scheme = scheme; + } + if (host) { + ret.host = host; + } + if (port) { + ret.port = port; + } + if (path) { + ret.path = path; + } + return ret; +}; + + +var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i; +var isValidHttpVersion = function (httpVersion) { + return isValidHttpVersion_regex.test(httpVersion); +}; + +var parseHttpVersion = function (httpVersion) { + httpVersion = httpVersion.replace("HTTP/", "").split("."); + return _.map(httpVersion, function (x) { + return parseInt(x); + }); +}; + module.exports = { ResponseUtils: ResponseUtils, RequestUtils: RequestUtils, - MessageUtils: MessageUtils + MessageUtils: MessageUtils, + parseUrl: parseUrl, + parseHttpVersion: parseHttpVersion, + isValidHttpVersion: isValidHttpVersion }; \ No newline at end of file diff --git a/web/src/js/utils.js b/web/src/js/utils.js index be59db96..6fc11f9e 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -2,6 +2,10 @@ var $ = require("jquery"); var _ = require("lodash"); var actions = require("./actions.js"); +//debug +window.$ = $; +window._ = _; + var Key = { UP: 38, DOWN: 40, @@ -28,17 +32,17 @@ var formatSize = function (bytes) { if (bytes === 0) return "0"; var prefix = ["b", "kb", "mb", "gb", "tb"]; - for (var i = 0; i < prefix.length; i++){ - if (Math.pow(1024, i + 1) > bytes){ + for (var i = 0; i < prefix.length; i++) { + if (Math.pow(1024, i + 1) > bytes) { break; } } var precision; - if (bytes%Math.pow(1024, i) === 0) + if (bytes % Math.pow(1024, i) === 0) precision = 0; else precision = 1; - return (bytes/Math.pow(1024, i)).toFixed(precision) + prefix[i]; + return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i]; }; @@ -60,17 +64,16 @@ var formatTimeStamp = function (seconds) { return ts.replace("T", " ").replace("Z", ""); }; - // At some places, we need to sort strings alphabetically descending, // but we can only provide a key function. // This beauty "reverses" a JS string. var end = String.fromCharCode(0xffff); -function reverseString(s){ +function reverseString(s) { return String.fromCharCode.apply(String, - _.map(s.split(""), function (c) { - return 0xffff - c.charCodeAt(0); - }) - ) + end; + _.map(s.split(""), function (c) { + return 0xffff - c.charCodeAt(0); + }) + ) + end; } function getCookie(name) { @@ -82,21 +85,22 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")}); //Tornado XSRF Protection. $.ajaxPrefilter(function (options) { if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { - if (options.data) { - options.data += ("&" + xsrf); + if(options.url.indexOf("?") === -1){ + options.url += "?" + xsrf; } else { - options.data = xsrf; + options.url += "&" + xsrf; } } }); // Log AJAX Errors $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) { - if(thrownError === "abort"){ + if (thrownError === "abort") { return; } var message = jqXHR.responseText; console.error(thrownError, message, arguments); actions.EventLogActions.add_event(thrownError + ": " + message); + alert(message); }); module.exports = { @@ -104,5 +108,5 @@ module.exports = { formatTimeDelta: formatTimeDelta, formatTimeStamp: formatTimeStamp, reverseString: reverseString, - Key: Key + Key: Key, }; \ No newline at end of file -- cgit v1.2.3