/*============= * GLOBAL VARS * ============*/ var eventGraph; var dataHandler; var mispInteraction; var nodes = new vis.DataSet(); var edges = new vis.DataSet(); var typeaheadDataSearch; var event_last_change = $('#eventgraph_network').data('event-timestamp'); var scope_id = $('#eventgraph_network').data('event-id'); var user_email = $('#eventgraph_network').data('user-email'); var container = document.getElementById('eventgraph_network'); var user_manipulation = $('#eventgraph_network').data('user-manipulation'); var is_siteadmin = $('#eventgraph_network').data('is-site-admin'); var root_id_attr = "rootNode:attribute"; var root_id_object = "rootNode:object"; var root_id_tag = "rootNode:tag"; var root_id_keyType = "rootNode:keyType"; var mapping_root_id_to_type = {}; mapping_root_id_to_type[root_id_attr] = 'attribute'; mapping_root_id_to_type[root_id_object] = 'object'; mapping_root_id_to_type[root_id_tag] = 'tag'; mapping_root_id_to_type[root_id_keyType] = 'keyType'; var root_node_x_pos = 800; var cluster_expand_threshold = 100; var nodes_ask_threshold = 300; /*========= * CLASSES * ========*/ // network class (handle the event graph manipulation and events) class EventGraph { constructor(network_options, nodes, edges) { // FIXME: Do the mapping between meta-catory and fa-icons. // Should be replaced later on. this.mapping_meta_fa = new Map(); this.mapping_meta_fa.set('file', {"meta-category": "file","fa_text": "file","fa-hex": "f15b"}); this.mapping_meta_fa.set('financial', {"meta-category": "financial","fa_text": "money-bil-alt","fa-hex": "f09d"}); this.mapping_meta_fa.set('network', {"meta-category": "network","fa_text": "server","fa-hex": "f233"}); this.mapping_meta_fa.set('misc', {"meta-category": "misc","fa_text": "cube","fa-hex": "f1b2"}); // Also considered as default // FIXME this.network_options = network_options; this.scope_name; this.scope_keyType; this.globalCounter = 0; this.first_draw = true; this.can_be_fitted_again = true; this.root_node_shown = false; this.is_filtered = false; this.menu_scope = this.init_scope_menu(); this.menu_physic = this.init_physic_menu(); this.menu_display = this.init_display_menu(); this.menu_filter = this.init_filter_menu(); this.menu_canvas = this.init_canvas_menu(); this.menu_import = this.init_import_menu(); this.menu_history = this.init_history_menu(); this.new_edges_for_unreferenced_nodes = []; this.layout = 'default'; this.solver = 'barnesHut'; this.backup_connection_edges = {}; this.nodes = nodes; this.edges = edges; var data = { // empty nodes: this.nodes, edges: this.edges }; this.hiddenNode = new vis.DataSet(); this.object_templates = {}; this.canvasContext; this.cluster_index = 0; // use to get uniq cluster ID this.clusters = []; this.extended_event_color_mapping = {}; this.extended_event_points = {}; this.extended_event_uuid_mapping = {}; this.network = new vis.Network(container, data, this.network_options); this.add_unreferenced_root_node(); this.bind_listener(); } bind_listener() { var that = this; this.network.on("selectNode", function (params) { // that.network.moveTo({ // position: { // x: params.pointer.canvas.x, // y: params.pointer.canvas.y // }, // animation: true, // }); }); this.network.on("dragStart", function (params) { eventGraph.physics_state(false); eventGraph.physics_activate_physics_for_nodes(params.nodes); }); this.network.on("dragEnd", function (params) { eventGraph.physics_disable_physics_for_nodes(params.nodes); eventGraph.physics_state($('#checkbox_physics_enable').prop("checked")); }); // create Hull for extending events this.network.on("beforeDrawing", function (ctx) { if (that.scope_name != "Reference" || !that.canDrawHull) { return; } for (var event_id in that.extended_event_points) { if (that.extended_event_color_mapping[event_id] === undefined) { eventGraph.extended_event_color_mapping[event_id] = stringToRGB(that.extended_event_uuid_mapping[event_id]); } var chosen_color = eventGraph.extended_event_color_mapping[event_id]; var nodes = that.network.getPositions(that.extended_event_points[event_id]); nodes = $.map(nodes, function(value, index) { // object to array return [value]; }); drawExtendedEventHull(ctx, nodes, chosen_color, "Event "+event_id); } }); this.network.on("afterDrawing", function (ctx) { that.canvasContext = ctx; }); this.network.on("oncontext", function (event) { var node = that.network.getNodeAt({x: event.pointer.DOM.x, y: event.pointer.DOM.y}); if (node !== undefined) { that.network.selectNodes([node]); } }); } // Util get_node_color(uuid) { return this.nodes.get(uuid).icon.color; } get_FA_icon(metaCateg) { var dict = this.mapping_meta_fa.get(metaCateg); dict = dict === undefined ? this.mapping_meta_fa.get('misc') : dict; // if unknown meta-categ, take default return String.fromCharCode(parseInt(dict['fa-hex'], 16)) } getUniqId() { this.globalCounter++; return this.globalCounter-1; } update_scope(value) { if (value === undefined) { value = $("#select_graph_scope").val(); } else { $("#select_graph_scope").val(value); } if (value == "Pivot key") { $("#network-scope-badge").text(value + ": " + eventGraph.scope_keyType); } else { $("#network-scope-badge").text(value); } this.scope_name = value; dataHandler.scope_name = value; } init_scope_menu() { var menu_scope = new ContextualMenu({ trigger_container: document.getElementById("network-scope"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div"), }); menu_scope.add_select({ id: "select_graph_scope", label: "Scope", tooltip: "The scope represented by the network", event: function(value) { if (value == "Pivot key" && $('#input_graph_scope_jsonkey').val() == "") { // no key selected for Pivot key scope return; } else { eventGraph.update_scope(value); dataHandler.fetch_data_and_update(); } }, options: ["Reference", "Tag", "Pivot key"], default: "Reference" }); menu_scope.add_select({ id: "input_graph_scope_jsonkey", label: "Pivot key", tooltip: "The key around which the network will be constructed", event: function(value) { if (value == "Pivot key" && $('#input_graph_scope_jsonkey').val() == "") { // no key selected for Pivot key scope return; } else { eventGraph.scope_keyType = value; eventGraph.update_scope("Pivot key"); dataHandler.fetch_data_and_update(); } }, options: dataHandler.available_pivot_key ? dataHandler.available_pivot_key : [], default: "" }); return menu_scope; } init_physic_menu() { var menu_physic = new ContextualMenu({ trigger_container: document.getElementById("network-physic"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_physic.add_select({ id: "select_physic_solver", label: "Solver", tooltip: "Physics solver to use", event: function(value) { eventGraph.physics_change_solver(value); }, options: ["barnesHut", "repulsion"], default: "barnesHut" }); menu_physic.add_slider({ id: 'slider_physic_node_repulsion', label: "Node repulsion", min: 0, max: 1000, value: this.network_options.physics.barnesHut.springLength, step: 10, event: function(value) { eventGraph.physics_change_repulsion(parseInt(value)); }, tooltip: "Correspond to spring length for barnesHut and node spacing for hierarchical" }); menu_physic.add_checkbox({ label: "Enable physics", id: "checkbox_physics_enable", event: function(checked) { eventGraph.physics_state(checked); }, checked: true }); return menu_physic; } init_display_menu() { var menu_display = new ContextualMenu({ trigger_container: document.getElementById("network-display"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_display.add_select({ id: "select_display_layout", label: "Layout", event: function(value) { switch(value) { case "default": eventGraph.change_layout_type("default"); break; case "hierarchical.directed": eventGraph.change_layout_type("directed"); break; case "hierarchical.hubsize": eventGraph.change_layout_type("hubsize"); break; default: eventGraph.change_layout_type("default"); } }, options: [ {text: "Default layout", value: "default"}, {text: "Hierarchical directed", value: "hierarchical.directed"}, {text: "Hierarchical hubsize", value: "hierarchical.hubsize"} ], tooltip: "Choose layout", default: "default" }); menu_display.add_select({ id: "select_display_object_field", label: "Object-relation in label", event: function(value) { dataHandler.selected_type_to_display = value; dataHandler.fetch_data_and_update(); }, options: [], tooltip: "If no item is selected display the first requiredOneOf of the object" }); menu_display.add_button({ label: "Expand all nodes", type: "danger", event: function() { var objectIds = eventGraph.nodes.getIds({ filter: function(item) { return item.group == 'object'; } }) for(var nodeId of objectIds) { eventGraph.expand_node(nodeId); } }, title: "Expanding all nodes may takes some time" }); menu_display.add_button({ label: "Collapse all nodes", type: "danger", event: function() { var objectIds = eventGraph.nodes.getIds({ filter: function(item) { return item.group == 'object'; } }); for(var nodeId of objectIds) { eventGraph.collapse_node(nodeId); } }, title: "Collapsing all nodes may takes some time" }); menu_display.add_slider({ id: 'slider_display_max_char_num', label: "Characters to show", tooltip: "Maximum number of characters to display in the label", min: 8, max: 1024, value: max_displayed_char, step: 8, applyButton: true, event: function(value) { $("#slider_display_max_char_num").parent().find("span").text(value); }, eventApply: function(value) { dataHandler.fetch_data_and_update(); } }); menu_display.add_slider({ id: 'slider_display_picture_size', label: "Picture size", tooltip: "Picture size", min: 10, max: 500, value: 50, step: 10, applyButton: true, event: function(value) { $("#slider_display_picture_size").parent().find("span").text(value); }, eventApply: function(value) { dataHandler.fetch_data_and_update(); } }); menu_display.add_button({ label: "Remove leaves", type: "primary", event: function () { eventGraph.remove_leaves(); }, }); return menu_display; } init_filter_menu() { var menu_filter = new ContextualMenu({ trigger_container: document.getElementById("network-filter"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_filter.add_action_table({ id: "table_attr_presence", container: menu_filter.menu, title: "Filter on Attribute presence", header: ["Relation", "Attribute"], control_items: [ { DOMType: "select", item_options: { options: ["Contains", "Do not contain"], } }, { DOMType: "select", item_options: { id: "table_control_select_attr_presence", options: [] } }, ], data: [], }); menu_filter.create_divider(3); menu_filter.add_action_table({ id: "table_tag_presence", container: menu_filter.menu, title: "Filter on Tag presence", header: ["Relation", "Tag"], control_items: [ { DOMType: "select", item_options: { options: ["Contains", "Do not contain"] } }, { DOMType: "select", item_options: { id: "table_control_select_tag_presence", options: [] } }, ], data: [], }); menu_filter.create_divider(3); menu_filter.add_action_table({ id: "table_attr_value", container: menu_filter.menu, title: "Filter on Attribute value", header: ["Attribute", "Comparison", "Value"], control_items: [ { DOMType: "select", item_options: { id: "table_control_select_attr_value", options: [] } }, { DOMType: "select", item_options: { options: ["<", "<=", "==", ">=", ">"] } }, { DOMType: "input", item_options: {} } ], data: [], onAddition: function(data) { eventGraph.menu_filter.items["table_attr_presence"].add_row(["Contains", data[0]]); } }); menu_filter.items["table_attr_value"].table.style.minWidth = "550px"; menu_filter.add_button({ label: "Filter", type: "primary", event: function() { dataHandler.fetch_data_and_update(); } }); return menu_filter; } init_canvas_menu() { var menu_canvas = new ContextualMenu({ trigger_container: document.getElementById("eventgraph_network"), right_click: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_canvas.add_button({ label: "View/Edit", type: "primary", event: function() { var selected_id = eventGraph.network.getSelectedNodes()[0]; if (selected_id === undefined) { // A node is selected return; } var data = { id: selected_id }; mispInteraction.edit_item(data); } }); menu_canvas.add_button({ label: "Hide", type: "info", event: function() { var selected_id = eventGraph.network.getSelectedNodes()[0]; if (selected_id === undefined) { // A node is selected return; } eventGraph.hideNode([selected_id]); } }); menu_canvas.add_button({ label: "Expand", type: "primary", event: function() { var selected_id = eventGraph.network.getSelectedNodes()[0]; if (selected_id === undefined) { // A node is selected return; } eventGraph.expand_node(selected_id); } }); menu_canvas.add_button({ label: "Collapse", type: "primary", event: function() { var selected_id = eventGraph.network.getSelectedNodes()[0]; if (selected_id === undefined) { // A node is selected return; } eventGraph.collapse_node(selected_id); } }); return menu_canvas; } init_import_menu() { var menu_import = new ContextualMenu({ trigger_container: document.getElementById("network-import"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_import.add_select_button({ id: "select_button_graph_import_export", label: "Export", tooltip: "Export graph", textButton: "Export", event: function(selected_value) { if (selected_value == 'json') { var jsonData = eventGraph.toJSON(); download_file(jsonData, 'json'); } else if (selected_value == 'png' || selected_value == 'jpeg') { var dataURL = eventGraph.canvasContext.canvas.toDataURL('image/'+selected_value); download_file(dataURL, selected_value); } else if (selected_value == 'DOT Language') { var hiddenNodeIds = []; eventGraph.hiddenNode.forEach(function(node) { hiddenNodeIds.push(node.id); }); var nodePositions = eventGraph.network.getPositions(); var validNodes = eventGraph.nodes.get({ filter: function (nodeD) { var nodeP = nodePositions[nodeD.id]; if (nodeP !== undefined) { return true; } return false; }}); var dotData = convert_to_dot_lang(validNodes, eventGraph.edges, hiddenNodeIds); download_file(dotData, 'dot'); } }, options: ["json", "png", "jpeg", "DOT Language"], default: "json" }); return menu_import; } init_history_menu() { var menu_history= new ContextualMenu({ trigger_container: document.getElementById("network-history"), bootstrap_popover: true, style: "z-index: 1", container: document.getElementById("eventgraph_div") }); menu_history.add_action_table({ id: "table_graph_history_actiontable", container: menu_history.menu, title: "Network history", header: ["Id", "Name", "Owner", "Date"], control_items: [ { DOMType: "input", colspan: 4, item_options: { style: "width: 98%;", placeholder: "Network's name", id: "networkHistory_input_name_save", disabled: !user_manipulation } } ], header_action_button: { additionEnabled: false, style: { type: "success", icon: "fa-save", tooltip: "Save network" }, disabled: !user_manipulation }, row_action_button: { removalEnabled: false, style: { tooltip: "Delete saved network" }, others: [ { style: { type: "success", icon: "fa-share ", tooltip: "Load saved network" }, event: function(data) { var network_id = data[0]; dataHandler.fetch_and_import_graph(network_id); } } ] }, data: [], onAddition: function(network_name, selfTable) { var network_json = eventGraph.toJSON(); var preview = eventGraph.canvasContext.canvas.toDataURL('image/png', 0.1); mispInteraction.save_network(network_json, network_name, preview); $('#networkHistory_input_name_save').val(''); }, onRemove: function(data, selfTable) { mispInteraction.delete_saved_network(data); } }); menu_history.items["table_graph_history_actiontable"].table.style.minWidth = "450px"; // fill history table // has to do it manually here (not using reset_graph_history) because menu_history still not constructed yet dataHandler.fetch_graph_history(function(history_formatted, network_previews) { menu_history.items["table_graph_history_actiontable"].set_table_data(history_formatted); for(var i=0; i'; }, placement: 'right', trigger: 'hover', template: '', html: true, }); } } }); return menu_history; } get_filtering_rules() { var rules_presence = eventGraph.menu_filter.items["table_attr_presence"].get_data(); var rules_tag_presence = eventGraph.menu_filter.items["table_tag_presence"].get_data(); var rules_value = eventGraph.menu_filter.items["table_attr_value"].get_data(); var rules = { presence: rules_presence, tag_presence: rules_tag_presence, value: rules_value }; return rules; } // Graph interaction // Clusterize the specified node with its connected childs clusterize(rootID) { var that = eventGraph; var type = mapping_root_id_to_type[rootID]; var clusterOptionsByData = { processProperties: global_processProperties, clusterNodeProperties: {borderWidth: 3, shape: 'database', font: {size: 30}}, joinCondition: function(nodeOptions) { return nodeOptions.unreferenced == type || nodeOptions.id == rootID; } }; that.network.cluster(clusterOptionsByData); } init_clusterize() { for(var key of Object.keys(mapping_root_id_to_type)) { this.clusterize(key); } } reset_graphs(hard) { this.nodes.clear(); this.edges.clear(); if (hard) { this.backup_connection_edges = {}; this.extended_event_points = {}; this.extended_event_uuid_mapping = {}; this.extended_event_color_mapping = {}; } } update_graph(data) { var that = this; that.network_loading(true, loadingText_creating); this.extended_event_uuid_mapping = data.extended_event_uuid_mapping; dataHandler.mapping_node_to_from_edges = {}; dataHandler.mapping_node_to_to_edges = {}; // New nodes will be automatically added // removed references will be deleted var node_conf; var newNodes = []; var newNodeIDs = []; for(var node of data.items) { var group, label; if (node.event_id != scope_id) { // add node ids of extended event if (that.extended_event_points[node.event_id] === undefined) { that.extended_event_points[node.event_id] = []; } that.extended_event_points[node.event_id].push(node.id); } if ( node.node_type == 'object' ) { var group = 'object'; var label = dataHandler.generate_label(node); var labelHtml = label + '
' + escapeHtml(node.comment) + '' label += ' ' + escapeHtml(node.comment) var striped_value = that.strip_text_value(label); node_conf = { id: node.id, uuid: node.uuid, Attribute: node.Attribute, event_id: node.event_id, label: striped_value, title: labelHtml, group: group, mass: 5, icon: { color: node.color, face: '"Font Awesome 5 Free"', code: that.get_FA_icon(node['meta-category']), } }; dataHandler.mapping_value_to_nodeID.set(label, node.id); } else if (node.node_type == 'tag') { var tag_color = node.tagContent.colour; group = 'tag'; label = node.label; node_conf = { id: node.id, uuid: node.uuid, label: label, title: label, group: group, mass: 20, color: { background: tag_color, border: tag_color }, font: { color: getTextColour(tag_color), bold: true, size: 28 }, shapeProperties: { borderRadius: 6 } }; dataHandler.mapping_value_to_nodeID.set(label, node.id); } else if (node.node_type == 'keyType') { group = 'keyType'; label = that.scope_keyType + ": " + node.label; var striped_value = that.strip_text_value(label); node_conf = { id: node.id, label: striped_value, title: label, group: group }; dataHandler.mapping_value_to_nodeID.set(label, node.id); } else { group = 'attribute'; label = node.type + ': ' + node.label; label += ' ' + escapeHtml(node.comment) var labelHtml = label + '
' + escapeHtml(node.comment) + '' var striped_value = that.strip_text_value(label); node_conf = { id: node.id, uuid: node.uuid, event_id: node.event_id, label: striped_value, title: labelHtml, group: group, mass: 5, }; if (node.type == 'attachment' && isPicture(node.label)) { // fetch picture via attributes/viewPicture node_conf.group = 'attribute_image'; node_conf.size = $('#slider_display_picture_size').val(); node_conf.image = baseurl + '/attributes/viewPicture/' + node.id + '/1'; } dataHandler.mapping_value_to_nodeID.set(label, node.id); } newNodes.push(node_conf); newNodeIDs.push(node.id); } // check if nodes got deleted var old_node_ids = that.nodes.getIds(); for (var old_id of old_node_ids) { // Ignore root node if (old_id == "rootNode:attribute" || old_id == "rootNode:object" || old_id == "rootNode:tag" || old_id == "rootNode:keyType") { continue; } // This old node got removed if (newNodeIDs.indexOf(old_id) == -1) { that.nodes.remove(old_id); } } that.nodes.update(newNodes); // New relations will be automatically added // removed references will be deleted var newRelations = []; var newRelationIDs = []; for(var rel of data.relations) { var rel = { id: rel.id, from: rel.from, to: rel.to, label: rel.type, title: rel.comment, color: { opacity: 1.0, } }; newRelations.push(rel); newRelationIDs.push(rel.id); if (!dataHandler.mapping_node_to_from_edges[rel.from]) { dataHandler.mapping_node_to_from_edges[rel.from] = []; } if (!dataHandler.mapping_node_to_to_edges[rel.to]) { dataHandler.mapping_node_to_to_edges[rel.to] = []; } dataHandler.mapping_node_to_from_edges[rel.from].push(rel.to); dataHandler.mapping_node_to_to_edges[rel.to].push(rel.from); } // check if nodes got deleted var old_rel_ids = that.edges.getIds(); for (var old_id of old_rel_ids) { // This old node got removed if (newRelationIDs.indexOf(old_id) == -1) { that.edges.remove(old_id); } } that.edges.update(newRelations); that.remove_root_nodes(); // do not clusterize if the network is filtered if (!that.is_filtered) { if (that.scope_name == 'Reference') { that.add_unreferenced_root_node(); // links unreferenced attributes and object to root nodes if (that.first_draw) { that.link_not_referenced_nodes(); that.first_draw = !that.first_draw } } else if (that.scope_name == 'Tag') { that.add_tag_root_node(); // links untagged attributes and object to root nodes if (that.first_draw) { that.link_not_referenced_nodes(); that.first_draw = !that.first_draw } } else if (that.scope_name == 'Distribution') { } else if (that.scope_name == 'Correlation') { } else { that.add_keyType_root_node(); if (that.first_draw) { that.link_not_referenced_nodes(); that.first_draw = !that.first_draw } } } eventGraph.canDrawHull = true; that.network_loading(false, ""); } strip_text_value(text) { var max_num = $("#slider_display_max_char_num").val(); return text.substring(0, max_num) + (text.length < max_num ? "" : "[...]") } reset_view() { this.network.fit({animation: true }); } reset_view_on_stabilized() { // Avoid fitting more than once, (cause a bug if it occurs) var that = eventGraph; if (that.can_be_fitted_again) { that.can_be_fitted_again = false; this.network.once("stabilized", function(params) { that.network.fit({ animation: true }); that.can_be_fitted_again = true; }); } } focus_on_stabilized(nodeID) { this.network.once("stabilized", function(params) { eventGraph.network.focus(nodeID, {animation: true, scale: 1}); }); } physics_state(state) { var that = eventGraph; that.network_options.physics.enabled = state; if(that.layout == "default") { $("#select_physic_solver").prop('disabled', !state); } $("#slider_physic_node_repulsion").prop('disabled', !state); that.network.setOptions({physics: { enabled: state} }) } physics_change_repulsion(value) { var that = eventGraph; if(that.layout == 'default') { // repulsion on default is related to spring length if(that.solver == "barnesHut") { that.network.setOptions({physics: { barnesHut: {springLength: value} } }) } else { that.network.setOptions({physics: { repulsion: {nodeDistance: value} } }) } } else { that.network.setOptions({physics: { hierarchicalRepulsion: {nodeDistance: value} } }) } } physics_change_solver(solver) { var that = eventGraph; if(that.layout == 'default') { // only hierarchical repulsion for other layout that.network.setOptions({physics: { solver: solver } }) // update physics slider value if(solver == "barnesHut") { $("#slider_physic_node_repulsion").val(that.network_options.physics.barnesHut.springLength); $("#slider_physic_node_repulsion").parent().find("span").text(that.network_options.physics.barnesHut.springLength); } else { $("#slider_physic_node_repulsion").val(that.network_options.physics.repulsion.nodeDistance); $("#slider_physic_node_repulsion").parent().find("span").text(that.network_options.physics.repulsion.nodeDistance); } } that.solver = solver; } physics_disable_physics_for_nodes(nodes) { var update = []; nodes.forEach(function(nodeId) { if (!eventGraph.network.isCluster(nodeId)) { update.push({id: nodeId, fixed: {x: true, y: true}}); } }); eventGraph.nodes.update(update); } physics_activate_physics_for_nodes(nodes) { var update = []; nodes.forEach(function(nodeId) { if (!eventGraph.network.isCluster(nodeId)) { update.push({id: nodeId, fixed: {x: false, y: false}}); } }); eventGraph.nodes.update(update); } remove_leaves() { var nodeIds = [] eventGraph.nodes.forEach(function (node) { // Hide node that have no outgoing references and are not being targetted by others if ( dataHandler.mapping_node_to_from_edges[node.id] === undefined && ( dataHandler.mapping_node_to_to_edges[node.id] === undefined || dataHandler.mapping_node_to_to_edges[node.id].length < 2 ) ) { nodeIds.push(node.id) } }) eventGraph.hideNode(nodeIds) } // state true: loading // state false: finished network_loading(state, message) { if(state) { $('.loading-network-div').show(); $('.loadingText-network').text(message); } else { setTimeout(function() { $('.loading-network-div').hide(); }, 500) } } collapse_node(parent_id) { if(parent_id === undefined) { return; } if (!(parent_id == root_id_attr || parent_id == root_id_object || parent_id == root_id_tag || parent_id == root_id_keyType)) { // Is not a root node var parent_node = this.nodes.get(parent_id); var node_group = parent_node.group; if (parent_id === undefined || node_group != 'object') { // No node selected or collapse not permitted return } parent_node.expanded = false; var connected_nodes_ids = this.network.getConnectedNodes(parent_id); var connected_nodes = this.nodes.get(connected_nodes_ids); for (var node of connected_nodes) { if (node.group.slice(0, 12) == "obj_relation") { // remove edge var connected_edges = this.network.getConnectedEdges(node.id); for (var edgeID of connected_edges) { this.edges.remove(edgeID); } this.nodes.remove(node.id); } } this.nodes.update(parent_node); } else { // Is a root node this.clusterize(parent_id); } } expand_node(parent_id) { if (!this.network.isCluster(parent_id)) { var parent_node = this.nodes.get(parent_id); if (parent_id === undefined // Node node selected || parent_node.group != "object") { // Cannot expand attribute return; } parent_node.expanded = true; var objAttributes = parent_node.Attribute; var newNodes = []; var newRelations = []; var parent_pos = this.network.getPositions([parent_id])[parent_id]; for(var attr of objAttributes) { var parent_color = eventGraph.get_node_color(parent_id); // Ensure unicity of nodes if (this.nodes.get(attr.uuid) !== null) { continue; } var striped_value = this.strip_text_value(attr.value); var node = { id: attr.uuid, x: parent_pos.x, y: parent_pos.y, label: attr.object_relation + ': ' + striped_value, title: attr.object_relation + ': ' + attr.value, group: 'obj_relation', color: { background: parent_color }, font: { color: getTextColour(parent_color) } }; if (attr.type == 'attachment' && isPicture(attr.value)) { // fetch picture via attributes/viewPicture node.group = 'obj_relation_image'; node.size = $('#slider_display_picture_size').val(); node.image = baseurl + '/attributes/viewPicture/' + attr.id + '/1'; } newNodes.push(node); dataHandler.mapping_obj_relation_value_to_nodeID.set(attr.value, node.id); var rel = { from: parent_id, to: attr.uuid, arrows: '', color: { opacity: 0.5, color: parent_color }, length: 40 }; newRelations.push(rel); } this.nodes.add(newNodes); this.edges.add(newRelations); this.nodes.update(parent_node); } else { // is a cluster if(this.network.getNodesInCluster(parent_id).length > cluster_expand_threshold) { if(!confirm("The cluster contains lots of nodes. Are you sure you want to expand it?")) { return; } } // expand cluster this.network.openCluster(parent_id); } } expand_previous_expansion(nodes) { var that = this; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { var node = nodes[id]; if (node.expanded) { eventGraph.expand_node(node.id); } } } } hideNode(nodeIds) { nodeIds.forEach(function(nodeId) { var node = eventGraph.nodes.get(nodeId); eventGraph.hiddenNode.add(node); eventGraph.nodes.remove(nodeId); }); } link_not_referenced_nodes() { // unlink previously linked this.edges.remove(this.new_edges_for_unreferenced_nodes) this.new_edges_for_unreferenced_nodes = []; // link not referenced nodes var newEdges = []; var that = this; this.nodes.forEach(function(nodeData) { var cur_id = nodeData.id; var cur_group = nodeData.group; // Do not link already connected nodes if (that.network.getConnectedEdges(cur_id).length > 0) { if (nodeData['unreferenced'] !== undefined) { that.nodes.remove(nodeData.id); delete nodeData['unreferenced']; that.nodes.add(nodeData); } return; } var new_edge = { to: cur_id, id: "temp_edge_unreferenced_" + that.getUniqId(), arrows: '', color: { opacity: 0.7, color: '#d9d9d9' }, length: 150 } if (that.scope_name == 'Reference') { if (cur_group.slice(0, 9) == 'attribute' || cur_group == 'object') { new_edge.from = cur_group.slice(0, 9) == 'attribute' ? root_id_attr : root_id_object; that.nodes.update({id: nodeData.id, unreferenced: cur_group.slice(0, 9)}); } } else if (that.scope_name == 'Tag') { if (cur_group.slice(0, 9) == 'attribute' || cur_group == 'object') { new_edge.from = root_id_tag; that.nodes.update({id: nodeData.id, unreferenced: 'tag'}); } } else { // specified key if (cur_group.slice(0, 9) == 'attribute' || cur_group == 'object') { new_edge.from = root_id_keyType; that.nodes.update({id: nodeData.id, unreferenced: that.scope_name}); } } newEdges.push(new_edge); that.new_edges_for_unreferenced_nodes.push(new_edge.id); }); this.edges.add(newEdges); this.init_clusterize(); } remove_root_nodes() { this.remove_unreferenced_root_node(); this.remove_tag_root_node(); this.remove_keyType_root_node(); } add_unreferenced_root_node() { if (this.root_node_shown) { return; } var root_node_attr = { id: root_id_attr, x: -root_node_x_pos, y: 0, label: 'Unreferenced Attributes', title: 'All Attributes not being referenced', group: 'rootNodeAttribute' }; var root_node_obj = { id: root_id_object, x: root_node_x_pos, y: 0, label: 'Unreferenced Objects', title: 'All Objects not being referenced', group: 'rootNodeObject' }; this.nodes.add([root_node_attr, root_node_obj]); this.root_node_shown = true; } remove_unreferenced_root_node() { this.nodes.remove([root_id_attr, root_id_object]); this.root_node_shown = false; } add_tag_root_node() { if (this.root_node_shown) { return; } var root_node_tag = { id: root_id_tag, x: -root_node_x_pos, y: 0, label: 'Untagged Attribute', title: 'All Attributes not being tagged', group: 'rootNodeTag' }; this.nodes.add([root_node_tag]); this.root_node_shown = true; } remove_tag_root_node() { this.nodes.remove([root_id_tag]); this.root_node_shown = false; } add_keyType_root_node() { if (this.root_node_shown) { return; } var root_node_keyType = { id: root_id_keyType, x: -root_node_x_pos, y: 0, label: this.scope_keyType + ': No value', title: 'All Attributes not having a value for the specified field', group: 'rootNodeKeyType' }; this.nodes.add([root_node_keyType]); this.root_node_shown = true; } remove_keyType_root_node() { this.nodes.remove([root_id_keyType]); this.root_node_shown = false; } switch_unreferenced_nodes_connection() { var that = eventGraph; var to_update = []; var root_ids; switch(that.scope_name) { case "Reference": root_ids = [root_id_attr, root_id_object]; break; case "Tag": root_ids = [root_id_tag]; break; default: root_ids = [root_id_keyType]; break; } for(var root_id of root_ids) { if(that.layout == 'default') { var all_edgesID = that.backup_connection_edges[root_id] if (all_edgesID === undefined) { // edgesID was not saved (happen if we switch scope then layout) // redraw everything eventGraph.destroy_and_redraw(); return; } } else { that.network.storePositions(); var prev_node = root_id; var all_edgesID = that.network.getConnectedEdges(root_id) that.backup_connection_edges[root_id] = all_edgesID; } var all_edges = that.edges.get(all_edgesID); for(var i=0; i 0 || filtering_rules.value.length > 0); eventGraph.first_draw = true; // update object state var available_object_references = Object.keys(data.existing_object_relation); var available_tags = Object.keys(data.existing_tags); var available_tags = $.map(data.existing_tags, function(value, index) { // object to array return [[index, value]]; }); dataHandler.update_filtering_selectors(available_object_references, available_tags); dataHandler.available_pivot_key = data.available_pivot_key; eventGraph.menu_scope.add_options("input_graph_scope_jsonkey", dataHandler.available_pivot_key); if (data.items.length < nodes_ask_threshold) { eventGraph.update_graph(data); } else if (data.items.length > nodes_ask_threshold && confirm("The network contains a lot of nodes, displaying it may slow down your browser. Continue?")) { eventGraph.update_graph(data); } else { eventGraph.network_loading(false, ""); $("#eventgraph_toggle").click(); } if ( stabilize === undefined || stabilize) { eventGraph.reset_view_on_stabilized(); } if (callback !== undefined) { callback(); } }, error: function( jqXhr, textStatus, errorThrown ){ console.log( errorThrown ); } }); }); } fetch_reference_data(rel_uuid, callback) { $.getJSON(baseurl + "/events/getReferenceData/"+rel_uuid+"/reference.json", function( data ) { callback(data); }); } fetch_objects_template() { return $.getJSON(baseurl + "/events/getObjectTemplate/templates.json", function( data ) { for (var i in data) { var template = data[i].ObjectTemplate; var requiredFields; // add both requiredOneOf and required field if (template.requirements.requiredOneOf !== undefined) { requiredFields = template.requirements.requiredOneOf; } else { requiredFields = []; } if (template.requirements.required !== undefined) { requiredFields = requiredFields.concat(template.requirements.required); } dataHandler.mapping_uuid_to_template.set(template.uuid, requiredFields); } }); } // same event, same timestamp validateImportedFile(data) { if (scope_id != data.eventId) { showMessage('fail', 'Failed to import file: Event '+data.eventId+' not compatible with event '+scope_id); return false; } if (parseInt(event_last_change) < parseInt(data.eventLastChange)) { showMessage('fail', 'Fail: Imported graph is newer than current event'); return false; } if (parseInt(event_last_change) > parseInt(data.eventLastChange)) { showMessage('success', 'Warning: Imported graph is not the latest version'); } return true; } fetch_graph_history(callback) { $.getJSON(baseurl + "/eventGraph/view/"+scope_id, function( history ) { var history_formatted = []; var network_previews = []; history.forEach(function(item) { history_formatted.push([ item['EventGraph']['id'], item['EventGraph']['network_name'], item['User']['email'], new Date(parseInt(item['EventGraph']['timestamp'])*1000).toLocaleString() ]); dataHandler.networkHistoryJsonData.set(item['EventGraph']['id'], item['EventGraph']['network_json']); network_previews.push(item['EventGraph']['preview_img']); }); callback(history_formatted, network_previews); }); } fetch_and_import_graph(network_id) { var data = dataHandler.networkHistoryJsonData.get(network_id); var json = JSON.parse(data); import_graph_from_json(json); } get_typeaheadData_search() { var to_ret = [] for( var entry of this.mapping_value_to_nodeID) { var value = entry[0]; to_ret.push(value); } // object relation for( var entry of this.mapping_obj_relation_value_to_nodeID) { var value = entry[0]; to_ret.push(value); } return to_ret; } } // MISP interaction class (handle interaction with misp) class MispInteraction { constructor(nodes, edges) { this.nodes = nodes; this.edges = edges; // Dirty way to know what modif was successful as the callback gives no information // May be changed in the futur this.callback_to_be_called = null; } register_callback(callback) { this.callback_to_be_called = callback; } apply_callback() { var that = mispInteraction; if (that.callback_to_be_called !== null) { that.callback_to_be_called(that.callback_data); } that.callback_to_be_called = null; that.callback_data = null; } remove_reference(edgeData, callback) { var that = mispInteraction; var edge_id = edgeData.edges[0]; var relation_id = edge_id; deleteObject('object_references', 'delete', relation_id, scope_id); if (callback !== undefined) { callback(); } } add_reference(edgeData, callback) { var that = mispInteraction; var uuid = that.nodes.get(edgeData.to).uuid; if (!that.can_create_reference(edgeData.from) || !that.can_be_referenced(edgeData.to)) { return; } var edgeFromId = edgeData.from.startsWith('o-') ? edgeData.from.substr(2) : edgeData.from; genericPopup(baseurl+'/objectReferences/add/'+edgeFromId, '#popover_form', function() { $('#ObjectReferenceReferencedUuid').val(uuid); objectReferenceInput(); }); } edit_reference(edgeData, callback) { if (callback !== undefined) { callback(); } var that = mispInteraction; var rel_id = edgeData.id; var rel_uuid = edgeData.uuid; that.register_callback(function() { var relation_id = edgeData.id; submitDeletion(scope_id, 'delete', 'object_references', relation_id); }); dataHandler.fetch_reference_data(rel_uuid, function(data) { data = data[0].ObjectReference; var uuid = data.referenced_uuid; genericPopup(baseurl + '/objectReferences/add/'+data.object_id, '#popover_form', function() { $('#targetSelect').val(uuid); $('#ObjectReferenceComment').val(data.comment); $('#ObjectReferenceRelationshipTypeSelect').val(data.relationship_type); $('option[value='+uuid+']').click(); }); }); } can_create_reference(id) { var node = this.nodes.get(id) return node.group == "object"; } can_be_referenced(id) { var res; var node = this.nodes.get(id) if (node.event_id != scope_id) { showMessage('fail', 'Cannot reference a node not belonging in this event') return false; } if (node.group == "object") { res = true; } else if (node.group.slice(0, 9) == "attribute") { res = true; } else { showMessage('fail', 'This node cannot be referenced') res = false; } return res; } add_item(nodeData, callback) { var that = mispInteraction; choicePopup("Add an element", [ { text: "Add an Object", onclick: "getPopup('"+scope_id+"', 'objectTemplates', 'objectChoice');" }, { text: "Add an Attribute", onclick: "openGenericModal('"+baseurl+"/attributes/add/"+scope_id+"');" }, ]); } delete_item(nodeData, callback) { var selected_nodes = nodeData.nodes; for (var nodeID of selected_nodes) { var node = this.nodes.get(nodeID) nodeID = nodeID.startsWith('o-') ? nodeID.substr(2) : nodeID; if (node.group.slice(0, 9) == "attribute") { deleteObject('attributes', 'delete', nodeID, scope_id); } else if (node.group == "object") { deleteObject('objects', 'delete', nodeID, scope_id); } } } edit_item(nodeData, callback) { var that = mispInteraction; var id = nodeData.id var group = nodes.get(id).group; id = id.startsWith('o-') ? id.substr(2) : id; if (group.slice(0, 9) == 'attribute') { openGenericModal(baseurl + '/attributes/edit/' + id); } else if (group == 'object') { window.location = baseurl + '/objects/edit/' + id; } } save_network(network_json, network_name, network_preview) { var network_json = eventGraph.toJSON(); this.quickSaveNetworkHistory(scope_id, network_json, network_name, network_preview, reset_graph_history); } delete_saved_network(data) { var network_id = data[0]; var url = baseurl + "/" + "eventGraph" + "/" + "delete" + "/" + network_id; $.get(url, function(data) { openPopup("#confirmation_box"); $("#confirmation_box").html(data); }); } quickSaveNetworkHistory(event_id, network_json, network_name, network_preview, callback) { this.networkFetchForm('add', event_id, undefined, function(form) { var container = $('#eventgraph_network'); // append the form somewhere container.append(form); var url = form.attr('action'); // locate wanted field and set the value var field_network_json = form.find('#' + 'EventGraph' + 'NetworkJson'); field_network_json.val(network_json); var field_network_name = form.find('#' + 'EventGraph' + 'NetworkName'); field_network_name.val(network_name); var field_network_preview = form.find('#' + 'EventGraph' + 'PreviewImg'); field_network_preview.val(network_preview); // submit the form $.ajax({ data: form.serialize(), cache: false, beforeSend: function(XMLHttpRequest) { $('.loading').show(); }, success: function(data, textStatus) { showMessage('success', 'Network has been saved'); if (callback !== undefined) { callback(); } }, error: function( jqXhr, textStatus, errorThrown ){ showMessage('fail', 'Could not save network'); console.log( errorThrown ); }, complete: function() { $(".loading").hide(); form.remove(); }, type: 'post', url: url }); }); } networkFetchForm(type, event_id, network_id, callback) { var url = baseurl + '/' + 'EventGraph' + '/' + 'add' + '/' + event_id; $.ajax({ beforeSend: function(XMLHttpRequest) { $('.loading').show(); }, dataType: 'html', cache: false, success: function(data, textStatus) { var form = $(data); form.css('display', 'none'); if (callback !== undefined) { callback(form); } else { return form; } }, error: function( jqXhr, textStatus, errorThrown ){ console.log( errorThrown ); }, complete: function() { $(".loading").hide(); }, type: 'get', url: url }); } } /*========= * UTILS * ========*/ function drawExtendedEventHull(ctx, nodes, color, text) { ctx.fillStyle = color+'55'; ctx.strokeStyle = color+'aa'; var centroid = getCentroid(nodes); var hull = getHullFromPoints(nodes, centroid); var hullExtraPoints = [] hull.forEach(p => { hullExtraPoints.push(applyAngularOffsetForCentroid(p, centroid, false)) hullExtraPoints.push(p) hullExtraPoints.push(applyAngularOffsetForCentroid(p, centroid, true)) }) centroid = getCentroid(hullExtraPoints); var hullFinal = getHullFromPoints(hullExtraPoints, centroid); hullFinal.push(hullFinal[0]) ctx.beginPath(); ctx.moveTo(hullFinal[0].x, hullFinal[0].y); for(var i=1; i 0 ? 1 : 2; // clock or counterclock wise } // Implementation of Gift wrapping algorithm (jarvis march in 2D) // Inspired from https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/ function getHullFromPoints(points, centroid) { var n = points.length; var l = 0; var hull = []; var cur_point; // get leftmost point for (var i=0; i points[i].x ? l : i; } var p = l; var q; do { cur_point = points[p] cur_point = applyOffsetForCentroid(cur_point, centroid) hull.push(cur_point); q = (p+1) % n; for (var i=0; i centroid.x ? 1 : -1; var newPoint = { x: point.x + Math.cos(angle) * DEFAULT_OFFSET_X * sign, y: point.y + Math.sin(angle) * DEFAULT_OFFSET_Y * sign, } return newPoint; } function applyAngularOffsetForCentroid(point, centroid, left) { var DEFAULT_ANGULAR_OFFSET = 2; var DEFAULT_OFFSET = 40; var slope = (point.y - centroid.y) / (point.x - centroid.x); var angle = Math.atan(slope); angle = angle + ((left ? 1 : -1) * DEFAULT_ANGULAR_OFFSET); var sign = point.x > centroid.x ? 1 : -1 var newPoint = { x: point.x + Math.cos(angle) * DEFAULT_OFFSET * sign, y: point.y + Math.sin(angle) * DEFAULT_OFFSET * sign, } return newPoint; } function generate_background_shortcuts(shortcut_text) { var table = document.createElement('table'); for (var shortcut of shortcut_text.split("\n")) { var index = shortcut.indexOf(" "); var text1 = shortcut.substring(0, index); var text2 = shortcut.substring(index, shortcut.length); var tr = document.createElement('tr'); var td = document.createElement('td'); td.innerHTML = text1; tr.appendChild(td); var td = document.createElement('td'); td.innerHTML = text2; tr.appendChild(td); table.appendChild(tr); } document.getElementById("eventgraph_shortcuts_background").appendChild(table); } function getTextColour(hex) { hex = hex.slice(1); var r = parseInt(hex.substring(0,2), 16); var g = parseInt(hex.substring(2,4), 16); var b = parseInt(hex.substring(4,6), 16); var avg = ((2 * r) + b + (3 * g))/6; if (avg < 128) { return 'white'; } else { return 'black'; } } function genericPopupCallback(result) { // sucess and eventgraph is enabled if (result == "success" && dataHandler !== undefined) { mispInteraction.apply_callback(); dataHandler.fetch_data_and_update(false, true); } } function download_file(data, type) { var dataUri; var filename = 'graphExport_'+ parseInt(new Date().getTime()/1000); if (type == 'json') { dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(data); filename += '.json'; } else if (type == 'png' || type == 'jpeg') { dataUri = data; filename += type; } else if (type == 'dot') { dataUri = 'data:text/x-graphviz;charset=utf-8,' + encodeURIComponent(data); filename += '.dot'; } var a = document.createElement('a'); a.setAttribute('href', dataUri); a.setAttribute('download', filename); var aj = $(a); aj.appendTo('body'); aj[0].click(); aj.remove(); } function reset_graph_history() { var table = eventGraph.menu_history.items["table_graph_history_actiontable"]; dataHandler.fetch_graph_history(function(history_formatted, network_previews) { table.set_table_data(history_formatted); for(var i=0; i'; }, placement: 'right', trigger: 'hover', template: '', html: true, }); } } }); } function import_graph_from_json(data) { if (dataHandler.validateImportedFile(data)) { // set options eventGraph.scope_name = data.scope; eventGraph.scope_keyType = data.scope.keyType; eventGraph.update_scope(data.scope.scope) var layoutVal; switch(data.display.layout) { case "default": layoutVal = 'default'; break; case "directed": layoutVal = 'hierarchical.directed'; break; case "hubsize": layoutVal = 'hierarchical.hubsize'; break; default: layoutVal = 'default'; } $('#select_display_layout').val(layoutVal); eventGraph.change_layout_type(data.display.layout); dataHandler.selected_type_to_display = data.display.label; $('#select_display_object_field').val(data.display.label); $("#slider_display_max_char_num").val(data.display.charLength); $('#slider_display_max_char_num').trigger('reflectOnSpan'); eventGraph.solver = data.physics.solver; eventGraph.physics_change_solver(data.physics.solver) $('#select_physic_solver').val(data.physics.solver); $('#slider_physic_node_repulsion').val(data.physics.repulsion); $('#slider_physic_node_repulsion').trigger('reflectOnSpan'); eventGraph.physics_change_repulsion(data.physics.repulsion) eventGraph.physics_state(data.physics.enabled) $('#checkbox_physics_enable').prop('checked', data.physics.enabled); // update data dataHandler.fetch_data_and_update(false, false, function() { eventGraph.nodes.update(data.nodes); eventGraph.expand_previous_expansion(data.nodes); eventGraph.hiddenNode.clear(); eventGraph.hideNode(data.hiddenNodes); }); } } function escapeQuote(str) { return str.replace(/"/g, '\\\"'); } function convert_to_dot_lang(nodes, edges, hiddenNodeIds) { var mappingStringDic = new Map(); // in case the id is not an int, map it to a letter var dotNodes = []; var validNodeId = {}; nodes.forEach(function(node) { if (hiddenNodeIds.indexOf(node.id) != -1) return; var nodeId = node.id; if (node.id != parseInt(node.id, 10)) { nodeId = 'autgenerated_id_'+mappingStringDic.size; mappingStringDic.set(node.id, nodeId); } var dnode = { id: nodeId, shape: node.group == 'object' ? 'box' : 'ellipse', label: escapeQuote(node.label), style: 'filled', }; switch(node.group) { case 'object': dnode.fillcolor = node.icon.color; break; case 'tag': dnode.fillcolor = node.color.background; break; case 'keyType': dnode.fillcolor = node.color.background; break; default: dnode.fillcolor = '#f3a500'; break; } validNodeId[nodeId] = true; dotNodes.push(dnode); }); var dotNodesStr = ""; dotNodes.forEach(function(node) { var nodeAttr = ""; for (var attr in node) { if (!node.hasOwnProperty(attr)) continue; if (attr=='id') continue; nodeAttr += attr + "=\"" + node[attr] + "\" "; } dotNodesStr += node.id + " ["+nodeAttr+"];\n"; }); var dotEdges = []; edges.forEach(function(edge) { if (edge.to.includes("rootNode:")) return; // drop root nodes if (edge.from.includes("rootNode:")) return; // drop root nodes var from = edge.from; if (edge.from != parseInt(edge.from, 10)) { from = mappingStringDic.get(edge.from); } var to = edge.to; if (edge.to != parseInt(edge.to, 10)) { to = mappingStringDic.get(edge.to); } var dedge = { from: from, to: to, label: edge.label !== undefined ? escapeQuote(edge.label) : "", color: edge.color.color !== undefined ? edge.color.color : "#597ce9", dirType: edge.label !== undefined ? "forward" : "none", }; dotEdges.push(dedge); }); var dotEdgesStr = ""; dotEdges.forEach(function(edge) { if (hiddenNodeIds.indexOf(edge.from) != -1 || hiddenNodeIds.indexOf(edge.to) != -1) return; var edgeAttr = ""; for (var attr in edge) { if (!edge.hasOwnProperty(attr)) continue; if (attr=='id' || attr=='from' || attr=='to') continue; edgeAttr += attr + "=\"" + edge[attr] + "\" "; } dotEdgesStr += edge.from + " -> " + edge.to + " ["+edgeAttr+"];\n"; }); var dotLang = "digraph network_event_"+scope_id+" {\n"; dotLang += dotNodesStr; dotLang += "\n"; dotLang += dotEdgesStr; dotLang += "}"; return dotLang; } // Called when the user click on the 'Event graph' toggle function enable_interactive_graph() { // unregister onclick $('#eventgraph_toggle').removeAttr('onclick'); // Defer the loading of the network to let some time for the DIV to appear setTimeout(function() { $('.shortcut-help').popover({ container: 'body', title: 'Shortcuts', content: shortcut_text, placement: 'left', trigger: 'hover', template: '', html: true, }); generate_background_shortcuts(shortcut_text); $('#fullscreen-btn-eventgraph').click(function() { var network_div = $('#eventgraph_div'); var fullscreen_enabled = !network_div.data('fullscreen'); network_div.data('fullscreen', fullscreen_enabled); var height_val = fullscreen_enabled == true ? "calc(100vh - 42px - 42px - 10px)" : "500px"; network_div.css("height", height_val); network_div[0].scrollIntoView({ behavior: "smooth", }); }); dataHandler = new DataHandler(); eventGraph = new EventGraph(network_options, nodes, edges); $(document).on("keydown", function(evt) { if (evt.target !== undefined && ($(evt.target).is('input') || $(evt.target).is('textarea'))) { return; } switch(evt.keyCode) { case 88: // x var selected_id = eventGraph.network.getSelectedNodes()[0]; eventGraph.expand_node(selected_id); break; case 67: // c var selected_id = eventGraph.network.getSelectedNodes()[0]; eventGraph.collapse_node(selected_id); break; case 86: // v eventGraph.reset_view(); break; case 69: // e if (evt.shiftKey) { var selected_id = eventGraph.network.getSelectedNodes()[0]; if (selected_id !== undefined) { // A node is selected var data = { id: selected_id }; mispInteraction.edit_item(data); break; } selected_id = eventGraph.network.getSelectedEdges()[0]; if (selected_id !== undefined) { // A edge is selected var data = { id: selected_id }; mispInteraction.edit_reference(data); break; } } break; case 70: // f if (evt.shiftKey) { // set focus to search input eventGraph.network.disableEditMode(); // un-toggle edit mode $('#network-typeahead').focus(); $('#network-typeahead').text(''); evt.preventDefault(); // avoid writting a 'F' in the input field } break; case 16: // if (!user_manipulation) { // user can't modify references break; } eventGraph.network.addEdgeMode(); // toggle edit mode break; case 46: // if (!user_manipulation) { // user can't modify references break; } // References var selected_ids = eventGraph.network.getSelectedEdges(); for (var selected_id of selected_ids) { var edge = { edges: [selected_id] }; // trick to use the same function mispInteraction.remove_reference(edge); } // Objects or Attributes selected_ids = eventGraph.network.getSelectedNodes(); data = { nodes: selected_ids }; mispInteraction.delete_item(data); break; default: break; } }); $(document).on("keyup", function(evt) { switch(evt.keyCode) { case 16: // if (!user_manipulation) { // user can't modify references break; } eventGraph.network.disableEditMode(); // un-toggle edit mode break; default: break; } }); eventGraph.update_scope(); dataHandler.fetch_data_and_update(true, false, function() { var $select = $('#network-typeahead'); dataHandler.get_typeaheadData_search().forEach(function(element) { var $option = $(''); $option.text(element); $option.attr('value', $option.text()); $select.append($option); }); $('#network-typeahead').chosen(chosen_options).on('change', function(evt, params) { var value = params.selected; var nodeID = dataHandler.mapping_value_to_nodeID.get(value); // in case we searched for an object relation nodeID = nodeID === undefined ? dataHandler.mapping_obj_relation_value_to_nodeID.get(value) : nodeID; // check if node in cluster nested_length = eventGraph.network.findNode(nodeID).length; if (nested_length > 1) { // Node is in cluster // As vis.js cannot supply a way to uncluster a single node, we remove it and add it again searched_node = eventGraph.nodes.get(nodeID); // Remove old node and edges eventGraph.nodes.remove(nodeID); eventGraph.nodes.add(searched_node); /* don't need to re-add the edge as it is the same */ eventGraph.focus_on_stabilized(nodeID); } else { // set focus to the network eventGraph.network.focus(nodeID, {animation: true, scale: 1}); } // select node and focus on it eventGraph.network.selectNodes([nodeID]); $("#network-typeahead").blur(); }); }); }, 1); } /*========= * OPTIONS * ========*/ mispInteraction = new MispInteraction(nodes, edges); var network_options = { interaction: { hover: true }, layout: { improvedLayout: false, hierarchical: { enabled: false, levelSeparation: 150, nodeSpacing: 5, treeSpacing: 200, blockShifting: true, edgeMinimization: true, parentCentralization: true, direction: 'UD', // UD, DU, LR, RL sortMethod: 'directed' // hubsize, directed } }, manipulation: { enabled: user_manipulation, initiallyActive: false, addEdge: mispInteraction.add_reference, editEdge: { editWithoutDrag: mispInteraction.edit_reference }, addNode: mispInteraction.add_item, editNode: mispInteraction.edit_item, deleteNode: mispInteraction.delete_item, deleteEdge: mispInteraction.remove_reference, }, physics: { enabled: true, barnesHut: { gravitationalConstant: -10000, centralGravity: 5, springLength: 150, springConstant: 0.24, damping: 1.0, }, repulsion: { centralGravity: 5, springLength: 150, springConstant: 0.04, nodeDistance: 240, damping: 0.3 }, hierarchicalRepulsion: { centralGravity: 0, springLength: 150, springConstant: 0.24, nodeDistance: 120, damping: 1 }, minVelocity: 3.0, }, edges: { width: 3, arrows: 'to' }, nodes: { chosen: { node: function(values, id, selected, hovering) { values.shadow = true; values.shadowSize = 5; values.shadowX = 2; values.shadowY = 2; values.shadowColor = "rgba(0,0,0,0.1)"; } } }, groups: { object: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', size: 50 }, font: { size: 18, // px background: 'rgba(255, 255, 255, 0.7)' }, }, obj_relation: { mass: 3, size: 10, color: { border:'black' } }, attribute: { shape: 'box', color: { background:'orange', border:'black' }, size: 15 }, attribute_image: { shape: 'image', borderWidth: 4, mass: 15 }, obj_relation_image: { shape: 'image', borderWidth: 4, mass: 15 }, tag: { shape: 'box', size: 15, shadow: { enabled: true, size: 3, x: 3, y: 3 }, mass: 20 }, keyType: { shape: 'box', color: { border: '#303030', background: '#808080', }, font: { size: 18, //px color: 'white' }, mass: 25 }, rootNodeObject: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', code: '\uf00a', }, font: { size: 18, // px background: 'rgba(255, 255, 255, 0.7)' }, mass: 5, physics: false }, rootNodeAttribute: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', code: '\uf1c0', }, font: { size: 18, // px background: 'rgba(255, 255, 255, 0.7)' }, mass: 5, physics: false }, rootNodeKeyType: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', code: '\uf111', }, font: { size: 22, // px background: 'rgba(255, 255, 255, 0.7)' }, mass: 5, physics: false }, rootNodeTag: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', code: '\uf02b', }, font: { size: 22, // px background: 'rgba(255, 255, 255, 0.7)' }, mass: 5, physics: false }, clustered_object: { shape: 'icon', icon: { face: '"Font Awesome 5 Free"', code: '\uf009', }, font: { size: 18, // px background: 'rgba(255, 255, 255, 0.7)' }, mass: 5, physics: false } }, locales: { en: { edit: 'Edit', del: 'Delete selected', back: 'Back', addNode: 'Add Object or Attribute', editNode: 'Edit selected item', addDescription: 'Click in an empty space to place a new node.', addEdge: 'Add Reference', editEdge: 'Edit Reference', edgeDescription: 'Click on an Object and drag the edge to another Object (or Attribute) to connect them.' } } }; var default_layout_option = $.extend(true, {}, network_options); var chosen_options = { max_shown_results: 20, inherit_select_classes: true } var max_displayed_char = 32; var progressbar_length = 3; // divided by 100 var loadingText_fetching = 'Fetching data'; var loadingText_creating = 'Constructing network'; var loadingText_redrawing = 'Redrawing network'; var shortcut_text = "V Center camera" + "\nX Expand node" + "\nC Collapse node" + "\nSHIFT+E Edit node" + "\nSHIFT+F Search for value" + "\nSHIFT Hold to add a reference" + "\nDEL Delete selected item" + "\nRIGHT-CLICK Open contextual menu"; function global_processProperties(clusterOptions, childNodes) { var concerned_root_node; var that = eventGraph; that.cluster_index = that.cluster_index + 1; var childrenCount = 0; for (var i = 0; i < childNodes.length; i++) { var childNodeID = childNodes[i].id if ( childNodeID.includes("rootNode:")) { concerned_root_node = childNodeID; } childrenCount += childNodes[i].childrenCount || 1; } childrenCount--; // -1 because 2 nodes merged into 1 clusterOptions.childrenCount = childrenCount; clusterOptions.font = {size: Math.sqrt(childrenCount)*0.5+30} clusterOptions.id = 'cluster:' + that.cluster_index; if (concerned_root_node !== undefined) { clusterOptions.icon = { size: Math.sqrt(childrenCount)*5+100 }; if (concerned_root_node == "rootNode:object") { clusterOptions.label = "Unreferenced Objects (" + childrenCount + ")"; clusterOptions.x = root_node_x_pos; clusterOptions.group = 'rootNodeObject'; } else if (concerned_root_node == "rootNode:attribute") { clusterOptions.label = "Unreferenced Attributes (" + childrenCount + ")"; clusterOptions.x = -root_node_x_pos; clusterOptions.group = 'rootNodeAttribute'; } else if (concerned_root_node == "rootNode:tag") { clusterOptions.label = "Untagged elements (" + childrenCount + ")"; clusterOptions.x = -root_node_x_pos; clusterOptions.group = 'rootNodeTag'; } else if (concerned_root_node == "rootNode:keyType") { clusterOptions.label = "Empty value elements (" + childrenCount + ")"; clusterOptions.x = -root_node_x_pos; clusterOptions.group = 'rootNodeKeyType'; } } clusterOptions.y = 0 that.clusters.push({id:'cluster:' + that.cluster_index, scale: that.cur_scale, group: clusterOptions.group}); return clusterOptions; } function isPicture(filename) { var extension = filename.split('.').pop() var validExtensions = ['jpg', 'jpeg', 'png', 'gif'] return validExtensions.includes(extension) }