diff --git a/website/web/static/tree.js b/website/web/static/tree.js index 8522b80..8b9c791 100644 --- a/website/web/static/tree.js +++ b/website/web/static/tree.js @@ -2,13 +2,14 @@ // Set the dimensions and margins of the diagram var margin = {top: 20, right: 200, bottom: 30, left: 90}, - width = 9600 - margin.left - margin.right, - height = 10000 - margin.top - margin.bottom; + width = 960 - margin.left - margin.right, + height = 1000 - margin.top - margin.bottom; var node_width = 0; var max_overlay_width = 1500; var default_max_overlay_height = 500; -var node_height = 45; +var node_height = 55; +var t = d3.transition().duration(750); var main_svg = d3.select("body").append("svg") .attr("width", width + margin.right + margin.left) @@ -40,36 +41,23 @@ pattern.append('rect') .attr('height', height) .attr("fill", "#EEEEEE"); -var background = main_svg.append('rect') - .attr('y', 0) - .attr('width', width) - .attr('height', height) - .style('fill', "url(#backstripes)"); - // append the svg object to the body of the page // appends a 'group' element to 'svg' // moves the 'group' element to the top left margin var node_container = main_svg - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var i = 0, - duration = 750, - root; - -// declares a tree layout and assigns the size -var treemap = d3.tree().size([height, width]); + duration = 750; // Assigns parent, children, height, depth -root = d3.hierarchy(treeData, function(d) { return d.children; }); -root.x0 = height / 2; +var root = d3.hierarchy(treeData); +root.x0 = height / 2; // middle of the page root.y0 = 0; - -// Fancy expand all the nodes one after the other -// Collapse after the second level -//root.children.forEach(collapse); - +// declares a tree layout +var tree = d3.tree(); update(root); // Collapse the node and all it's children @@ -81,14 +69,6 @@ function collapse(d) { } }; -// Gets the size of the box and increase it -function getBB(selection) { - selection.each(function(d) { - d.data.total_width = d.data.total_width ? d.data.total_width : 0; - d.data.total_width += this.getBBox().width; - }) -}; - function urlnode_click(d) { var url = "/tree/url/" + d.data.uuid; d3.blob(url, {credentials: 'same-origin'}).then(function(data) { @@ -102,6 +82,15 @@ d3.selection.prototype.moveToFront = function() { }); }; +d3.selection.prototype.moveToBack = function() { + return this.each(function() { + var firstChild = this.parentNode.firstChild; + if (firstChild) { + this.parentNode.insertBefore(this, firstChild); + } + }); +}; + // What happen when clicking on a domain (load a modal display) function hostnode_click(d) { // Move the node to the front (end of the list) @@ -248,27 +237,29 @@ function hostnode_click(d) { }); }; -function icon(icons, key, icon_path){ - var content = icons.append("g"); +function icon(key, icon_path, d){ + var iconContent = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox + .attr('class', 'icon'); var icon_size = 16; + var has_icon = false; - content.filter(function(d){ + iconContent.datum(d); + iconContent.filter(d => { if (typeof d.data[key] === 'boolean') { - return d.data[key]; + has_icon = d.data[key]; } else if (typeof d.data[key] === 'number') { - return d.data[key] > 0; + has_icon = d.data[key] > 0; } else if (d.data[key] instanceof Array) { - return d.data[key].length > 0; + has_icon = d.data[key].length > 0; } - return false; + return has_icon; }).append('image') .attr("width", icon_size) .attr("height", icon_size) - .attr('x', function(d) { return d.data.total_width ? d.data.total_width + 1 : 0 }) - .attr("xlink:href", icon_path).call(getBB); + .attr("xlink:href", icon_path); - content.filter(function(d){ + iconContent.filter(d => { if (typeof d.data[key] === 'boolean') { return false; // return d.data[key]; @@ -283,34 +274,50 @@ function icon(icons, key, icon_path){ }).append('text') .attr("dy", 8) .style("font-size", "10px") - .attr('x', function(d) { return d.data.total_width ? d.data.total_width + 1 : 0 }) - .attr('width', function(d) { return d.to_print.toString().length + 'em'; }) - .text(function(d) { return d.to_print; }).call(getBB); - + .attr('x', icon_size + 1) + .attr('width', d => d.to_print.toString().length + 'em') + .text(d => d.to_print); + if (has_icon) { + return iconContent.node(); + } + return false; }; -function icon_list(parent_svg, relative_x_pos, relative_y_pos) { +function icon_list(relative_x_pos, relative_y_pos, d) { // Put all the icone in one sub svg document - var icons = parent_svg - .append('svg') + var icons = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox .attr('x', relative_x_pos) - .attr('y', relative_y_pos); + .attr('y', relative_y_pos) + .datum(d); + icon_options = [ + ['js', "/static/javascript.png"], + ['exe', "/static/exe.png"], + ['css', "/static/css.png"], + ['font', "/static/font.png"], + ['html', "/static/html.png"], + ['json', "/static/json.png"], + ['iframe', "/static/ifr.png"], + ['image', "/static/img.png"], + ['unknown_mimetype', "/static/wtf.png"], + ['video', "/static/video.png"], + ['request_cookie', "/static/cookie_read.png"], + ['response_cookie', "/static/cookie_received.png"], + ['redirect', "/static/redirect.png"], + ['redirect_to_nothing', "/static/cookie_in_url.png"] + ]; - icon(icons, 'js', "/static/javascript.png"); - icon(icons, 'exe', "/static/exe.png"); - icon(icons, 'css', "/static/css.png"); - icon(icons, 'font', "/static/font.png"); - icon(icons, 'html', "/static/html.png"); - icon(icons, 'json', "/static/json.png"); - icon(icons, 'iframe', "/static/ifr.png"); - icon(icons, 'image', "/static/img.png"); - icon(icons, 'unknown_mimetype', "/static/wtf.png"); - icon(icons, 'video', "/static/video.png"); - icon(icons, 'request_cookie', "/static/cookie_read.png"); - icon(icons, 'response_cookie', "/static/cookie_received.png"); - icon(icons, 'redirect', "/static/redirect.png"); - icon(icons, 'redirect_to_nothing', "/static/cookie_in_url.png"); + nb_icons = 0 + icon_options.forEach(entry => { + bloc = icon(entry[0], entry[1], d); + if (bloc){ + icons + .append(() => bloc) + .attr('x', 25 * nb_icons); // FIXME: make that distance a variable + nb_icons += 1; + }; + }) + // FIXME: that need to move somewhere else, doesn't make sense here. icons.filter(function(d){ if (d.data.sane_js_details) { d.libinfo = d.data.sane_js_details[0]; @@ -318,19 +325,21 @@ function icon_list(parent_svg, relative_x_pos, relative_y_pos) { } return false; }).append('text') - .attr('x', function(d) { return d.data.total_width ? d.data.total_width + 5 : 0 }) + .attr('x', 5) .attr('y', 15) .style("font-size", "15px") - .text(function(d) { return 'Library inforamtion: ' + d.libinfo }).call(getBB); + .text(function(d) { return 'Library information: ' + d.libinfo }); + + return icons.node(); } -function text_entry(parent_svg, relative_x_pos, relative_y_pos, onclick_callback) { +function text_entry(relative_x_pos, relative_y_pos, onclick_callback, d) { // Avoid hiding the content after the circle - var nodeContent = parent_svg - .append('svg') + var nodeContent = d3.create("svg") // WARNING: svg is required there, "g" doesn't have getBBox .attr('height', node_height) .attr('x', relative_x_pos) - .attr('y', relative_y_pos); + .attr('y', relative_y_pos) + .datum(d); // Add labels for the nodes var text_nodes = nodeContent.append("text") @@ -341,174 +350,165 @@ function text_entry(parent_svg, relative_x_pos, relative_y_pos, onclick_callback .style("opacity", .9) .attr('cursor', 'pointer') .attr("clip-path", "url(#textOverlay)") - .text(function(d) { - d.data.total_width = 0; // reset total_width - to_display = d.data.name - if (d.data.urls_count) { - // Only on Hostname node. - to_display += ' (' + d.data.urls_count + ')'; - }; - return to_display; - }) - .on('click', onclick_callback); - - // This value has to be set once for all for the whole tree and cannot be updated - // on click as clicking only updates a part of the tree - if (node_width === 0) { - text_nodes.each(function(d) { - node_width = node_width > this.getBBox().width ? node_width : this.getBBox().width; - }) - node_width += 20; - }; - return text_nodes; + .text(d.data.name + ' (' + d.data.urls_count + ')') + .on('click',onclick_callback); + return nodeContent.node(); } -// Recursiveluy generate the tree -function update(source) { +// Recursively generate the tree +function update(root, computed_node_width=0) { - // reinitialize max_depth + // Current height of the tree (cannot use height because it isn't recomputed when we rename children -> _children) var max_depth = 1 + root.each(d => { + if (d.children){ + max_depth = d.depth > max_depth ? d.depth : max_depth; + } + }); - // Update height - // 50 is the height of a node, 500 is the minimum so the root node isn't behind the icon - var newHeight = Math.max(treemap(root).descendants().reverse().length * node_height, 10 * node_height); - treemap = d3.tree().size([newHeight, width]); + if (computed_node_width != 0) { + // Re-compute SVG size depending on the generated tree + var newWidth = Math.max((max_depth + 1) * computed_node_width, node_width); + // Update height + // node_height is the height of a node, node_height * 10 is the minimum so the root node isn't behind the lookyloo icon + var newHeight = Math.max(root.descendants().reverse().length * node_height, 10 * node_height); + tree.size([newHeight, newWidth]) + + // Set background based on the computed width and height + var background = main_svg.insert('rect', ':first-child') + .attr('y', 0) + // FIXME: + 200 doesn't make much sense... + .attr('width', newWidth + margin.right + margin.left + 200) + .attr('height', newHeight + margin.top + margin.bottom) + .style('fill', "url(#backstripes)"); + + // Update size + d3.select("body svg") + // FIXME: + 200 doesn't make much sense... + .attr("width", newWidth + margin.right + margin.left + 200) + .attr("height", newHeight + margin.top + margin.bottom) + + // Update pattern + main_svg.selectAll('pattern') + .attr('width', computed_node_width * 2) + pattern.selectAll('rect') + .attr('width', computed_node_width) + + } // Assigns the x and y position for the nodes - var treeData = treemap(root); - - // Compute the new tree layout. - var nodes = treeData.descendants(), - links = treeData.descendants().slice(1); + var treemap = tree(root); + // Compute the new tree layout. => Note: Need d.x & d.y + var nodes = treemap.descendants(), + links = treemap.descendants().slice(1); // ****************** Nodes section *************************** // Update the nodes... - // TODO: set that ID to the ete3 node ID - var node = node_container.selectAll('g.node') - .data(nodes, function(d) {return d.id || (d.id = ++i); }); + const tree_nodes = node_container.selectAll('g.node') + .data(nodes, node => node.data.uuid); - // Enter any new modes at the parent's previous position. - var nodeEnter = node.enter().append('g') - .attr('class', 'node') - .attr("id", function(d) { - return 'node_' + d.data.uuid; - }) - .attr("transform", function(d) { - return "translate(" + source.y0 + "," + source.x0 + ")"; + tree_nodes.join( + // Enter any new modes at the parent's previous position. + enter => { + var node_group = enter.append('g'); + node_group + .attr('class', 'node') + .attr("id", d => 'node_' + d.data.uuid) + .attr("transform", "translate(" + root.y0 + "," + root.x0 + ")") + // Add Circle for the nodes + .append('circle') + .attr('class', 'node') + .attr('r', 1e-6) + .style("fill", d => d._children ? "lightsteelblue" : "#fff") + .on('click', click); + // Rectangle around the domain name & icons + //.append('rect') + //.attr("rx", 6) + //.attr("ry", 6) + //.attr('x', 13) + //.attr('y', -23) + //.style("opacity", "0.5") + //.attr("stroke", "black") + //.attr('stroke-opacity', "0.8") + //.attr("stroke-width", "1.5") + //.attr("stroke-linecap", "round") + //.attr("fill", "white") + // Set Hostname text + node_group + .append(d => text_entry(15, -20, hostnode_click, d)); + // Set list of icons + node_group + .append(d => icon_list(17, 10, d)); + + return node_group; + }, + update => update, + exit => exit + // Remove any exiting nodes + .call(exit => exit + .transition(t) + .attr("transform", "translate(" + root.y0 + "," + root.x0 + ")") + .remove() + ) + // On exit reduce the node circles size to 0 + .attr('r', 1e-6) + // On exit reduce the opacity of text labels + .style('fill-opacity', 1e-6) + ).call(node => { + // Transition to the proper position for the node + node.attr("transform", node => "translate(" + node.y + "," + node.x + ")"); + // Update the node attributes and style + node.select('circle.node') + .attr('r', 10) + .style("fill", node => node._children ? "lightsteelblue" : "#fff") + .attr('cursor', 'pointer'); + node.selectAll('text').nodes().forEach(n => { + // Set the width for all the nodes + node_width = node_width > n.getBBox().width ? node_width : n.getBBox().width; + }); + // FIXME: should get the bbox of the whole node group + node_width += 30; }); - // Add Circle for the nodes - nodeEnter.append('circle') - .attr('class', 'node') - .attr('r', 1e-6) - .style("fill", function(d) { - return d._children ? "lightsteelblue" : "#fff"; - }) - .on('click', click); - - // Set Hostname text - text_entry(nodeEnter, 10, -20, hostnode_click); - // Set list of icons - icon_list(nodeEnter, 12, 10); - - // Normalize for fixed-depth. - nodes.forEach(function(d){ d.y = d.depth * node_width}); - // Update pattern - main_svg.selectAll('pattern') - .attr('width', node_width * 2) - pattern.selectAll('rect') - .attr('width', node_width) - - // Update svg width - nodes.forEach(function(d){ - if (d.children){ - max_depth = d.depth > max_depth ? d.depth : max_depth; - } + nodes.forEach(d => { + // Store the old positions for transition. + d.x0 = d.x; + d.y0 = d.y; }); - // Re-compute SVG size depending on the generated tree - var newWidth = Math.max((max_depth + 2) * node_width, node_width); - background.attr('height', newHeight + margin.top + margin.bottom) - background.attr('width', newWidth + margin.right + margin.left) - treemap.size([newHeight, newWidth]) - d3.select("body svg") - .attr("width", newWidth + margin.right + margin.left) - .attr("height", newHeight + margin.top + margin.bottom) - // UPDATE - var nodeUpdate = nodeEnter.merge(node); - - // Transition to the proper position for the node - nodeUpdate.transition() - .duration(duration) - .attr("transform", function(d) { - return "translate(" + d.y + "," + d.x + ")"; - }); - - // Update the node attributes and style - nodeUpdate.select('circle.node') - .attr('r', 10) - .style("fill", function(d) { - return d._children ? "lightsteelblue" : "#fff"; - }) - .attr('cursor', 'pointer'); - - - // Remove any exiting nodes - var nodeExit = node.exit().transition() - .duration(duration) - .attr("transform", function(d) { - return "translate(" + source.y + "," + source.x + ")"; - }) - .remove(); - - // On exit reduce the node circles size to 0 - nodeExit.select('circle') - .attr('r', 1e-6); - - // On exit reduce the opacity of text labels - nodeExit.select('text') - .style('fill-opacity', 1e-6); - // ****************** links section *************************** // Update the links... - var link = node_container.selectAll('path.link') - .data(links, function(d) { return d.id; }); + const link = node_container.selectAll('path.link') + .data(links, d => d.id); - // Enter any new links at the parent's previous position. - var linkEnter = link.enter().insert('path', "g") - .attr("class", "link") - .attr('d', function(d){ - var o = {x: source.x0, y: source.y0} - return diagonal(o, o) - }); - - // UPDATE - var linkUpdate = linkEnter.merge(link); - - // Transition back to the parent element position - linkUpdate.transition() - .duration(duration) - .attr('d', function(d){ return diagonal(d, d.parent) }); - - // Remove any exiting links - var linkExit = link.exit().transition() - .duration(duration) - .attr('d', function(d) { - var o = {x: source.x, y: source.y} - return diagonal(o, o) - }) - .remove(); - - // Store the old positions for transition. - nodes.forEach(function(d){ - d.x0 = d.x; - d.y0 = d.y; - }); + link.join( + enter => enter + // Enter any new links at the parent's previous position. + .insert('path', "g") + .attr("class", "link") + .attr('d', d => { + var o = {x: root.x0, y: root.y0} + return diagonal(o, o) + }), + update => update, + exit => exit + .call(exit => exit + .transition(t) + .attr('d', d => { + var o = {x: root.x0, y: root.y0} + return diagonal(o, o) + }) + .remove() + ) + ).call(link => link + //.transition(t) + .attr('d', d => diagonal(d, d.parent)) + ); // Creates a curved (diagonal) path from parent to the child nodes function diagonal(s, d) { @@ -526,10 +526,16 @@ function update(source) { if (d.children) { d._children = d.children; d.children = null; - } else { + } + else { d.children = d._children; d._children = null; - } - update(d); + } + // Call update on the whole Tree + update(d.ancestors().reverse()[0]); + } + + if (computed_node_width === 0) { + update(root, node_width) } }